Quickstart
This quickstart gives a brief overview of the functions needed to effectively use Plasmo.jl to build optimization models. If you have used JuMP.jl, much of the functionality here will look familiar. In fact, the primary modeling objects in Plasmo.jl extend the JuMP.AbstractModel
and support most JuMP methods.
The below example demonstrates the construction of a simple linear optimization problem that contains two optinodes coupled by a simple linking contraint (which induces an OptiEdge
) that is solved with the HiGHS linear optimization solver.
Once Plasmo.jl has been installed, you can use it from a Julia session as following:
julia> using Plasmo
For this example we also need to import the HiGHS optimization solver and the PlasmoPlots package which we use to visualize the graph structure.
julia> using HiGHS
Create an OptiGraph
The following command will create the optigraph (referred to as graph
). We also see the printed output which denotes the number of optinodes, optiedges, subgraphs, variables, and constraints in the graph.
julia> graph = OptiGraph(;name=:quickstart_graph)
An OptiGraph
quickstart_graph #local elements #total elements
--------------------------------------------------
Nodes: 0 0
Edges: 0 0
Subgraphs: 0 0
Variables: 0 0
Constraints: 0 0
An OptiGraph
distinguishes between its local elements (optinodes and optiedges contained directly within the graph) and its total elements (local elements plus elements contained within subgraphs). This distinction helps to describe nested graph structures as described in Modeling with OptiGraphs.
Add Variables and Constraints (using OptiNodes)
An optigraph consists of OptiNode
objects which represent stand-alone optimization models. An optinode supports JuMP macros used to create variables, constraints, expressions, and objective functions (i.e. it supports JuMP macros such as @variable
, @constraint
, @expression
and @objective
). The simplest way to add optinodes to an optigraph is to use the @optinode
macro as shown in the following code snippet. Here we create the optinode n1
and add two variables x
and y
. We also add a single constraint and an objective function to the node. By default, the name of a node is pre-pended with the name of the graph it was created in.
julia> @optinode(graph, n1)
n1
julia> @variable(n1, y >= 2)
n1[:y]
julia> @variable(n1, x >= 1)
n1[:x]
julia> @constraint(n1, x + y >= 3)
n1[:y] + n1[:x] ≥ 3
julia> @objective(n1, Min, y)
n1[:y]
julia> graph
An OptiGraph
quickstart_graph #local elements #total elements
--------------------------------------------------
Nodes: 1 1
Edges: 0 0
Subgraphs: 0 0
Variables: 2 2
Constraints: 3 3
We can create more optinodes and add variables, constraints, and objective functions to each of them.
@optinode(graph, n2);
@variable(n2, y >= 0);
@variable(n2, x >= 2);
@constraint(n2, x + y >= 3);
@objective(n2, Min, y);
@optinode(graph, n3);
@variable(n3, y >= 0);
@variable(n3, x >= 0);
@constraint(n3, x + y >= 3);
@objective(n3, Min, y);
println(graph)
# output
An OptiGraph
quickstart_graph #local elements #total elements
--------------------------------------------------
Nodes: 3 3
Edges: 0 0
Subgraphs: 0 0
Variables: 6 6
Constraints: 9 9
Add Linking Constraints (using Edges)
A linking constraint can be created by adding a constraint to an OptiEdge
. Linking constraints couple variables across nodes (or graphs!) and can take any valid JuMP expression composed of node variables. We add a simple linear equality constraint here between variables that exist on nodes n1
, n2
, and n3
.
julia> edge = add_edge(graph, n1, n2, n3);
julia> @constraint(edge, n1[:x] + n2[:x] + n3[:x] == 3)
n1[:x] + n2[:x] + n3[:x] = 3
julia> println(graph)
An OptiGraph
quickstart_graph #local elements #total elements
--------------------------------------------------
Nodes: 3 3
Edges: 1 1
Subgraphs: 0 0
Variables: 6 6
Constraints: 10 10
You can also create edges implicitly using the @linkconstraint
macro which takes the exact same input as the JuMP.@constraint
macro above. The above snippet would correspond to doing:
@linkconstraint(graph, n1[:x] + n2[:x] + n3[:x] == 3)
This would create a new edge between nodes n1
, n2
, and n3
(if one does not exist) and add the constraint to it. You can also use the JuMP.@constraint
macro directly on an optigraph to generate linking constraints, but the syntax displayed here is preferred to help code readability.
Plasmo.jl no longer supports the legacy nonlinear interface from JuMP.jl. Consequently, @NLconstraint
, @NLobjective
, and @NLexpression
will no longer work. If you have code that uses these macros, you will need to update to the current interface using @constraint
, @objective
, and @expression
.
Solve the OptiGraph and Query the Solution
We can set the objective function of an optigraph using either the node objectives or by defining an objective directly using the JuMP.@objective
macro on the graph. Since we already defined an objective for each node we can use the set_to_node_objectives
function to denote the graph objective.
julia> set_to_node_objectives(graph)
julia> objective_function(graph)
n1[:y] + n2[:y] + n3[:y]
The graph objective approach would look like:
@objective(graph, Min, n1[:y] + n2[:y] + n3[:y])
Plasmo.jl uses MathOptInterface.jl internally to interface with optimization solvers. We can optimize an optigraph using the set_optimizer
and optimize!
functions just like in JuMP.jl. Here we also set the HiGHS attribute log_to_console
to false to suppress the output.
julia> set_optimizer(graph, HiGHS.Optimizer);
julia> set_optimizer_attribute(graph, MOI.RawOptimizerAttribute("log_to_console"), false)
julia> optimize!(graph)
After returning from the optimizer we can query the termination status using termination_status
. We can also query the solution of variables using value
and the objective value of the graph using objective_value
julia> termination_status(graph)
OPTIMAL::TerminationStatusCode = 1
julia> value(graph, n1[:x])
1.0
julia> value(graph, n2[:x])
2.0
julia> value(graph, n3[:x])
0.0
julia> objective_value(graph)
6.0
It is possible to optimize individual optinodes or different optigraphs that contain the same optinode (nodes can be used in multiple optigraphs). Graph-specific solutions can be accessed using value(node, variable)
(if optimizing a single node) or value(graph, variable)
(if optimizing an optigraph). \
Note that optimizing a node creates a new graph internally; the optimizer interface always goes through a graph. It is also possible to use value(variable)
without specifying a graph, but this will always return the value corresponding to the graph that created the node (this graph can be queried using source_graph(node)
). Using value(node
) is likely fine for most use-cases, but be aware that you should use the value(graph, variable)
method when dealing with multiple graphs to avoid grabbing a wrong solution.
Visualize the Structure
Lastly, it is often useful to visualize the structure of an optigraph. The visualization can lead to insights about an optimization problem and understand its connectivity. Plasmo.jl uses PlasmoPlots.jl (which builds on Plots.jl and NetworkLayout.jl) to visualize the layout of an optigraph. The code here shows how to obtain the graph topology using PlasmoPlots.layout_plot
and we plot the corresponding incidence matrix structure using PlasmoPlots.matrix_plot
. Both of these functions can accept keyword arguments to customize their layout or appearance. The matrix visualization also encodes information on the number of variables and constraints in each optinode and optiedge. The left figure shows a standard graph visualization where we draw an edge between each pair of nodes if they share an edge, and the right figure shows the matrix representation where labeled blocks correspond to nodes and blue marks represent linking constraints that connect their variables. The node layout helps visualize the overall connectivity of the graph while the matrix layout helps visualize the size of nodes and edges.
plt_graph = PlasmoPlots.layout_plot(
graph,
node_labels=true,
markersize=30,
labelsize=15,
linewidth=4,
layout_options=Dict(
:tol=>0.01,
:iterations=>2
),
plt_options=Dict(
:legend=>false,
:framestyle=>:box,
:grid=>false,
:size=>(400,400),
:axis=>nothing)
)
plt_matrix = PlasmoPlots.matrix_plot(graph, node_labels=true, markersize=15)