Partitioning and Graph Operations

The Modeling section describes how to construct optigraphs using a bottom-up approach. Specifically, we showed how to to use Hierarchical Modeling with subgraphs to create multi-level optigraphs. This part of the documentation deals with creating optigraphs using a top-down approach. Specifically, we show how to construct subgraphs using graph partitions and show how Plasmo.jl interfaces with standard graph partitioning tools such as Metis and KaHyPar.

Example Problem: Dynamic Optimization

To help demonstrate graph partitioning capabilities in Plasmo.jl, we instantiate a simple optimal control problem described by the following equations. In this problem, $x$ is a vector of states and $u$ is a vector of control actions which are both indexed over the set of time indices $t \in \{1,...,T\}$. The objective function minimizes the state trajectory with minimal control effort (energy), the second equation describes the state dynamics, and the third equation defines the initial condition. The last two equations define limits on the state and control actions.

\[\begin{aligned} \min_{\{ x,u \}} & \sum_{t = 1}^T x_t^2 + u_t^2 & \\ \textrm{s.t.} \quad & x_{t+1} = x_t + u_t + d_t, \quad t \in \{1,...,T-1\} & \\ & x_{1} = 0 &\\ & x_t \ge 0, \quad t \in \{1,...,T\}\\ & u_t \ge -1000, \quad t \in \{1,...,T-1\} \end{aligned}\]

This snippet shows how to construct the optimal control problem in Plasmo.jl as described in Modeling. We create an optigraph, add optinodes which represent states and controls at each time period, we set objective functions for each optinode, and we use linking constraints to describe the dynamics.

using Plasmo

T = 100          #number of time points
d = sin.(1:T)    #disturbance vector

graph = OptiGraph()
@optinode(graph,state[1:T])
@optinode(graph,control[1:T-1])

for node in state
    @variable(node,x)
    @constraint(node, x >= 0)
    @objective(node,Min,x^2)
end
for node in control
    @variable(node,u)
    @constraint(node, u >= -1000)
    @objective(node,Min,u^2)
end

@linkconstraint(graph,[i = 1:T-1],state[i+1][:x] == state[i][:x] + control[i][:u] + d[i])
n1 = state[1]
@constraint(n1,n1[:x] == 0)

When we print the newly created optigraph for our optimal control problem, we see it contains about 200 optinodes (one for each state and control) and contains almost 100 linking constraints (which couple the time periods).

julia> println(graph)
OptiGraph:
local nodes: 199, total nodes: 199
local link constraints: 99, total link constraints 99
local subgraphs: 0, total subgraphs 0

We can also plot the resulting optigraph (see Plotting) which produces a simple chain, but otherwise there is no real structure in the problem as we have modeled it.

julia> using Plots

julia> plt_chain = plt_graph4 = plot(graph,layout_options = Dict(:tol => 0.1,:iterations => 500), linealpha = 0.2,markersize = 6)
Plot{Plots.GRBackend() n=298}

julia> Plots.savefig(plt_chain,"chain_layout.svg");

julia> plt_chain_matrix = spy(graph);

julia> Plots.savefig(plt_chain_matrix,"chain_layout_matrix.svg");
chainchain_matrix

Partitioning OptiGraphs

At its core, the OptiGraph is a hypergraph and can thus naturally exploit hypergraph partitioning tools. For our example here we demonstrate how to use hypergraph partitioning (using KaHyPar), but Plasmo.jl also facilitates using standard graph partitioning algorithms (a hypergraph can be projected to various standard graph representations) or partitioning by manually defining partition vectors. The below snippet uses the gethypergraph function which returns a HyperGraph object and a hyper_map (a Julia dictionary) which maps hypernodes and hyperedges back to the original optigraph.

julia> hypergraph,hyper_map = gethypergraph(graph);

julia> println(hypergraph)
Hypergraph: (199 , 99)

With our hypergraph we can now use KaHyPar to perform hypergraph partitioning in the next snippet which returns a partition_vector . Each index in the partition_vector corresponds to a hypernode index in hypergraph, and each value denotes which partition the hypernode belongs to. So in our example, partition_vector contains 199 elements which can take on integer values between 0 and 7 (for 8 total partitions). Once we have a partition_vector, we can create a Partition object which describes sets (partitions) of optinodes and optiedges, as well as the shared optinodes and optiedges that cross partitions. We can lastly use the produced partition (a Partition object) to formulate subgraphs in our original optigraph (graph) using make_subgraphs!. After doing so, we see that our graph now contains 8 subgraphs with 7 linking constraints that correspond to the optiedges that cross partitions (i.e. connect subgraphs).

julia> using KaHyPar

julia> partition_vector = KaHyPar.partition(hypergraph,8,configuration = :connectivity,imbalance = 0.01);

julia> partition = Partition(graph,partition_vector,hyper_map);

julia> make_subgraphs!(graph,partition);
julia> println(length(partition_vector))
199

julia> println(partition)
    OptiGraph Partition w/ 8 subpartitions

julia> println(length(getsubgraphs(graph)))
8

julia> num_linkconstraints(graph)
7

If we plot the partitioned optigraph, it reveals eight distinct partitions and the coupling between them. The plots show that the partitions are well-balanced and the matrix visualization shows the problem is reordered into a banded structure that is typical of dynamic optimization problems.

julia> plt_chain_partition = plot(graph,layout_options = Dict(:tol => 0.01, :iterations => 500),linealpha = 0.2,markersize = 6,subgraph_colors = true);

julia> Plots.savefig(plt_chain_partition,"chain_layout_partition.svg");

julia> plt_chain_matrix_partition = spy(graph,subgraph_colors = true);

julia> Plots.savefig(plt_chain_matrix_partition,"chain_layout_matrix_partition.svg");
chain_partitionchain_matrix_partition

Aggregating OptiGraphs

Subgraphs can be converted into stand-alone optinodes using the using the aggregate function. This is important from a solver stand-point because optinodes represent solvable subproblems which can be communicated to decomposition algorithms. This aggregation step also takes place when using standard optimization solvers with Plasmo.jl, such as Ipopt wherein the optigraph is aggregated into a single optinode and solved using the underlying JuMP interface. In the snippet below, we aggregate our optigraph that contains 8 subgraphs. We include the argument 0 which specifies how many subgraph levels to retain. In this case, 0 means we aggregate subgraphs at the highest level so graph contains only new aggregated optinodes. For hierarchical graphs with many levels, we can define how many subgraph levels we wish to retain. The function returns a new aggregated graph (aggregate_graph), as well as reference_map which maps variables in aggregate_graph to the original optigraph graph.

julia> aggregate_graph,reference_map = aggregate(graph,0);
Creating Combined OptiGraph with a maximum subgraph depth of 0

julia> println(aggregate_graph)
OptiGraph:
local nodes: 8, total nodes: 8
local link constraints: 7, total link constraints 7
local subgraphs: 0, total subgraphs 0

We can lastly plot the aggregated graph structure which simply shows 8 optinodes with 7 linking constraints.

julia> plt_chain_aggregate = plot(aggregate_graph,layout_options = Dict(:tol => 0.01,:iterations => 10),node_labels = true,markersize = 30,labelsize = 20,node_colors = true);

julia> Plots.savefig(plt_chain_aggregate,"chain_layout_aggregate.svg");

julia> plt_chain_matrix_aggregate = spy(aggregate_graph,node_labels = true,node_colors = true);

julia> Plots.savefig(plt_chain_matrix_aggregate,"chain_layout_matrix_aggregate.svg");
chain_aggregatechain_matrix_aggregate

Methods

Plasmo.PartitionType
Partition(hypergraph::HyperGraph,node_membership_vector::Vector{Int64},ref_map::Dict)

Create a partition of optinodes using hypergraph, node_membership_vector, and 'refmap'. The 'refmap' is a dictionary that maps hypernode indices (integers) and hyperedge indices (tuples) back to optinodes and optiedges.

Partition(optigraph::OptiGraph,node_membership_vector::Vector{Int64},ref_map::Dict)

Create a partition using optigraph, node_membership_vector, and 'refmap'. The `refmap` is a mapping of node_indices to the original optinodes.

Partition(optigraph::OptiGraph,optinode_vectors::Vector{Vector{OptiNode}})

Manually create a partition using optigraph and a vector of vectors containing sets of optinodes that represent each partition.

Plasmo.make_subgraphs!Function
make_subgraphs!(optigraph::OptiGraph,partition::Partition)

Create subgraphs in optigraph using a produced 'partition'.

Plasmo.aggregateFunction
aggregate(graph::OptiGraph)

Aggregate the optigraph graph into a new optinode. Return an optinode and a dictionary which maps optinode variable and constraint references to the original optigraph.

aggregate(graph::OptiGraph,max_depth::Int64)

Aggregate the optigraph 'graph' into a new aggregated optigraph. Return a newly aggregated optigraph and a dictionary which maps new variables and constraints to the original optigraph. max_depth determines how many levels of subgraphs remain in the new aggregated optigraph. For example, a max_depth of 0 signifies there should be no subgraphs in the aggregated optigraph.

Plasmo.expandFunction
expand(graph::OptiGraph,subgraph::OptiGraph,distance::Int64)

Return a new expanded subgraph given the optigraph graph and an existing subgraph subgraph. The returned subgraph contains the expanded neighborhood within distance of the given subgraph.

Plasmo.neighborhoodFunction
neighborhood(graph::OptiGraph,nodes::Vector{OptiNode},distance::Int64)::Vector{OptiNode}

Return the optinodes within distance of the given nodes in the optigraph graph.

Plasmo.HyperGraphType
Hypergraph

A simple hypergraph type. Contains attributes for vertices and hyperedges.

Plasmo.gethypergraphFunction
gethypergraph(graph::OptiGraph)

Retrieve a hypergraph representation of the optigraph graph. Returns a HyperGraph object, as well as a dictionary that maps hypernodes and hyperedges to the original optinodes and optiedges.