From a10e52158dd5d9bc3315c26cce7354fc307328b8 Mon Sep 17 00:00:00 2001 From: jalving Date: Sat, 20 Jan 2024 21:55:12 -0800 Subject: [PATCH] add support for fixed variables --- Project.toml | 2 +- src/Plasmo.jl | 2 +- src/{moi_graph_backend.jl => moi_backend.jl} | 149 +++++++++++++++++-- src/optinode.jl | 137 +++++++++++++---- 4 files changed, 247 insertions(+), 43 deletions(-) rename src/{moi_graph_backend.jl => moi_backend.jl} (77%) diff --git a/Project.toml b/Project.toml index 51d5f65..409dd86 100644 --- a/Project.toml +++ b/Project.toml @@ -2,7 +2,7 @@ name = "Plasmo" uuid = "d3f7391f-f14a-50cc-bbe4-76a32d1bad3c" authors = ["Jordan Jalving "] repo = "https://github.com/plasmo-dev/Plasmo.jl.git" -version = "0.5.3" +version = "0.6.0" [deps] DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" diff --git a/src/Plasmo.jl b/src/Plasmo.jl index 1008aec..5ed2773 100644 --- a/src/Plasmo.jl +++ b/src/Plasmo.jl @@ -22,7 +22,7 @@ include("optiedge.jl") include("optigraph.jl") -include("moi_graph_backend.jl") +include("moi_backend.jl") include("moi_aggregate.jl") diff --git a/src/moi_graph_backend.jl b/src/moi_backend.jl similarity index 77% rename from src/moi_graph_backend.jl rename to src/moi_backend.jl index b69c400..6889a7e 100644 --- a/src/moi_graph_backend.jl +++ b/src/moi_backend.jl @@ -76,7 +76,6 @@ mutable struct GraphMOIBackend <: MOI.AbstractOptimizer # map of variables and constraints on nodes and edges to graph backend indices node_variables::OrderedDict{OptiNode,Vector{MOI.VariableIndex}} element_constraints::OrderedDict{OptiElement,Vector{MOI.ConstraintIndex}} - #edge_constraints::OrderedDict{OptiEdge,Vector{MOI.ConstraintIndex}} # TODO (maybe): legacy JuMP nonlinear support # nlp_model::MOI.Nonlinear.Model @@ -97,8 +96,7 @@ function GraphMOIBackend(optigraph::AbstractOptiGraph) ElementToGraphMap(), GraphToElementMap(), OrderedDict{OptiNode,Vector{MOI.VariableIndex}}(), - OrderedDict{OptiElement,Vector{MOI.ConstraintIndex}}(), - #OrderedDict{OptiEdge,Vector{MOI.ConstraintIndex}}() + OrderedDict{OptiElement,Vector{MOI.ConstraintIndex}}() ) end @@ -133,16 +131,45 @@ function MOI.get(gb::GraphMOIBackend, attr::MOI.AnyAttribute, ref::NodeVariableR return MOI.get(gb.moi_backend, attr, graph_index) end -function MOI.set(graph_backend::GraphMOIBackend, attr::MOI.AnyAttribute, args...) - MOI.set(graph_backend.moi_backend, attr, args...) +function MOI.set(gb::GraphMOIBackend, attr::MOI.AnyAttribute, args...) + MOI.set(gb.moi_backend, attr, args...) + return +end + +function MOI.delete(gb::GraphMOIBackend, nvref::NodeVariableRef) + MOI.delete(gb.moi_backend, gb.element_to_graph_map[nvref]) + delete!(gb.graph_to_element_map.var_map, gb.element_to_graph_map[nvref]) + delete!(gb.element_to_graph_map.var_map, nvref) + return +end + +function MOI.delete(gb::GraphMOIBackend, cref::ConstraintRef) + MOI.delete(gb.moi_backend, gb.element_to_graph_map[cref]) + delete!(gb.graph_to_element_map.con_map, gb.element_to_graph_map[cref]) + delete!(gb.element_to_graph_map.con_map, cref) + return +end + +function MOI.is_valid(gb::GraphMOIBackend, vi::MOI.VariableIndex) + return MOI.is_valid(gb.moi_backend, vi) +end + +function MOI.is_valid(gb::GraphMOIBackend, ci::MOI.ConstraintIndex) + return MOI.is_valid(gb.moi_backend, ci) end ### Variables function next_variable_index(node::OptiNode) - return MOI.VariableIndex(num_variables(node) + 1) + return MOI.VariableIndex(JuMP.num_variables(node) + 1) end +function graph_index(gb::GraphMOIBackend, nvref::NodeVariableRef) + return gb.element_to_graph_map[nvref] +end + +# add variable + function _moi_add_node_variable( node::OptiNode, v::JuMP.AbstractVariable @@ -154,7 +181,6 @@ function _moi_add_node_variable( # add variable to all containing optigraphs for graph in containing_optigraphs(node) graph_var_index = _add_variable_to_backend(graph_backend(graph), vref) - #push!(graph_backend(graph).node_variables[node], graph_var_index) # TODO: move _moi_constrain_node_variable( graph_backend(graph), vref, @@ -222,6 +248,97 @@ function _add_variable_to_backend( return graph_var_index end +# fix/unfix variable + +function _moi_fix_node_variable( + nvref::NodeVariableRef, + value::Number, + force::Bool, + ::Type{T} +) where {T} + new_set = MOI.EqualTo(convert(T, value)) + if _moi_nv_is_fixed(nvref) + cref = _nv_fix_ref(nvref) + # updates each backend graph + MOI.set(nvref.node, MOI.ConstraintSet(), cref, new_set) + else + # add a new fixing constraint + if _moi_nv_has_upper_bound(nvref) || + _moi_nv_has_lower_bound(nvref) + if !force + error( + "Unable to fix $(nvref) to $(value) because it has " * + "existing variable bounds. Consider calling " * + "`JuMP.fix(variable, value; force=true)` which will " * + "delete existing bounds before fixing the variable.", + ) + end + if _moi_nv_has_upper_bound(nvref) + MOI.delete(nvref.node, _nv_upper_bound_ref(nvref)) + end + if _moi_nv_has_lower_bound(nvref) + MOI.delete(nvref.node, _nv_lower_bound_ref(nvref)) + end + end + con = JuMP.ScalarConstraint(nvref, MOI.EqualTo{T}(value)) + _moi_add_node_constraint(nvref.node, con) + end + return +end + +function _moi_nv_is_fixed(nvref::NodeVariableRef) + gb = graph_backend(nvref.node) + ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{Float64}}( + gb.element_to_graph_map[nvref].value + ) + return MOI.is_valid(graph_backend(nvref.node), ci) +end + +function _nv_fix_ref(nvref::NodeVariableRef) + gb = graph_backend(nvref.node) + ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{Float64}}( + gb.element_to_graph_map[nvref].value + ) + cref = gb.graph_to_element_map[ci] + return cref +end + +# get/set variable bounds + +function _moi_nv_has_upper_bound(nvref::NodeVariableRef) + gb = graph_backend(nvref.node) + ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}( + gb.element_to_graph_map[nvref].value + ) + return MOI.is_valid(graph_backend(nvref.node), ci) +end + +function _nv_upper_bound_ref(nvref::NodeVariableRef) + gb = graph_backend(nvref.node) + ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}( + gb.element_to_graph_map[nvref].value + ) + cref = gb.graph_to_element_map[ci] + return cref +end + +function _moi_nv_has_lower_bound(nvref::NodeVariableRef) + gb = graph_backend(nvref.node) + ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}( + gb.element_to_graph_map[nvref].value + ) + return MOI.is_valid(graph_backend(nvref.node), ci) +end + +function _nv_lower_bound_ref(nvref::NodeVariableRef) + gb = graph_backend(nvref.node) + ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}( + gb.element_to_graph_map[nvref].value + ) + cref = gb.graph_to_element_map[ci] + return cref +end + ### Constraints function next_constraint_index( @@ -253,9 +370,13 @@ function _moi_add_node_constraint( # update func variable indices moi_func_graph = _create_graph_moi_func(graph_backend(graph), moi_func, jump_func) - # add to optinode backend - # _add_node_constraint_to_backend(graph_backend(graph), cref, moi_func_graph, moi_set) - _add_element_constraint_to_backend(graph_backend(graph), cref, moi_func_graph, moi_set) + # add contraint to backend + _add_element_constraint_to_backend( + graph_backend(graph), + cref, + moi_func_graph, + moi_set + ) end return cref end @@ -295,9 +416,13 @@ function _moi_add_edge_constraint( moi_func_graph = _create_graph_moi_func(graph_backend(graph), moi_func, jump_func) # add the constraint to the backend - _add_element_constraint_to_backend(graph_backend(graph), cref, moi_func_graph, moi_set) + _add_element_constraint_to_backend( + graph_backend(graph), + cref, + moi_func_graph, + moi_set + ) end - return cref end diff --git a/src/optinode.jl b/src/optinode.jl index f341e1d..db7bbce 100644 --- a/src/optinode.jl +++ b/src/optinode.jl @@ -47,11 +47,11 @@ end """ optimizer_graph(node::OptiNode) -Return the `OptiGraph` that contains the node model attributes. In most cases, this is the +Return the `OptiGraph` that contains the node backend attributes. In most cases, this is the same as `source_graph(node)`. For improved performance when modeling with subgraphs, it is -possible to define all node and edge attributes on the parent-level graph. In this case, -`backend_graph(node)` would return a parent graph, whereas `source_graph(node)` would return -the subgraph that contains the node. +possible to define all node and edge attributes on a parent graph as opposed to +the source graph. In this case, `backend_graph(node)` would return said parent graph, +whereas `source_graph(node)` would return the subgraph. """ function optimizer_graph(node::OptiNode) return source_graph(node).optimizer_graph @@ -59,15 +59,26 @@ end function containing_optigraphs(node::OptiNode) source = source_graph(node) - backend = optimizer_graph(node) - graphs = [backend] + backend_graph = optimizer_graph(node) + graphs = [backend_graph] if haskey(source.node_to_graphs, node) graphs = [graphs; source_graph.node_to_graphs[node]] end return graphs end -### OptiNode Extension +function containing_backends(node::OptiNode) + return graph_backend.(containing_optigraphs(node)) +end + +function _set_dirty(node::OptiNode) + for graph in containing_optigraphs(node) + graph.is_model_dirty = true + end + return +end + +### OptiNode MOI Extension function MOI.get(node::OptiNode, attr::MOI.AnyAttribute) return MOI.get(graph_backend(node), attr) @@ -77,7 +88,7 @@ function MOI.get(node::OptiNode, attr::MOI.AbstractConstraintAttribute, ref::Con return MOI.get(graph_backend(node), attr, ref) end -# TODO: look into caching constraint types in graph backend versus using unique and filters +# TODO: consider caching constraint types in graph backend versus using unique to filter function MOI.get(node::OptiNode, attr::MOI.ListOfConstraintTypesPresent) cons = graph_backend(node).element_constraints[node] con_types = unique(typeof.(cons)) @@ -98,34 +109,28 @@ function MOI.get( return con_inds end -function JuMP.object_dictionary(node::OptiNode) - return node.source_graph.node_obj_dict +function MOI.delete(node::OptiNode, vref::NodeVariableRef) + for graph in containing_optigraphs(node) + MOI.delete(graph_backend(graph), vref) + end + return end -function JuMP.backend(node::OptiNode) - return JuMP.backend(graph_backend(node)) +function MOI.delete(node::OptiNode, cref::ConstraintRef) + for graph in containing_optigraphs(node) + MOI.delete(graph_backend(graph), cref) + end + return end -function JuMP.all_variables(node::OptiNode) - gb = graph_backend(node) - graph_indices = gb.node_variables[node] - return getindex.(Ref(gb.graph_to_element_map), graph_indices) -end +### JuMP Extension -function JuMP.num_variables(node::OptiNode) - return length(graph_backend(node).node_variables[node]) +function JuMP.object_dictionary(node::OptiNode) + return node.source_graph.node_obj_dict end -# TODO: update -function JuMP.num_constraints( - node::OptiNode, - ::Type{F}, - ::Type{S} -)::Int64 where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} - g2n = graph_backend(node).graph_to_element_map - cons = MOI.get(JuMP.backend(node), MOI.ListOfConstraintIndices{F,S}()) - refs = [g2n[con] for con in cons] - return length(filter((cref) -> cref.model == node, refs)) +function JuMP.backend(node::OptiNode) + return JuMP.backend(graph_backend(node)) end ### Node Variables @@ -160,6 +165,53 @@ function JuMP.add_variable(node::OptiNode, v::JuMP.AbstractVariable, name::Strin return vref end +function JuMP.num_variables(node::OptiNode) + return length(graph_backend(node).node_variables[node]) +end + +function JuMP.all_variables(node::OptiNode) + gb = graph_backend(node) + graph_indices = gb.node_variables[node] + return getindex.(Ref(gb.graph_to_element_map), graph_indices) +end + +function JuMP.delete(node::OptiNode, nvref::NodeVariableRef) + if node !== JuMP.owner_model(nvref) + error( + "The variable reference you are trying to delete does not " * + "belong to the model.", + ) + end + _set_dirty(node) + for graph in containing_optigraphs(node) + MOI.delete(graph_backend(node), nvref) + end + return +end + +function JuMP.is_valid(node::OptiNode, nvref::NodeVariableRef) + return node === JuMP.owner_model(nvref) && + MOI.is_valid(graph_backend(node), nvref) +end + +function JuMP.fix(nvref::NodeVariableRef, value::Number; force::Bool=false) + if !JuMP.isfinite(value) + error("Unable to fix variable to $(value)") + end + node = nvref.node + _set_dirty(node) + _moi_fix_node_variable(nvref, value, force, Float64) + return +end + +function JuMP.is_fixed(nvref::NodeVariableRef) + return _moi_is_nv_fixed(graph_backend(nvref), nvref) +end + +function JuMP.has_upper_bound(nvref::NodeVariableRef) + return _moi_has_upper_bound(graph_backend(nvref), nvref) +end + function JuMP.index(vref::NodeVariableRef) return vref.index end @@ -200,4 +252,31 @@ function JuMP.add_constraint( con = JuMP.model_convert(node, con) cref = _moi_add_node_constraint(node, con) return cref +end + +# TODO: update to use backend lookup +function JuMP.num_constraints( + node::OptiNode, + ::Type{F}, + ::Type{S} +)::Int64 where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} + g2n = graph_backend(node).graph_to_element_map + cons = MOI.get(JuMP.backend(node), MOI.ListOfConstraintIndices{F,S}()) + refs = [g2n[con] for con in cons] + return length(filter((cref) -> cref.model == node, refs)) +end + +function JuMP.delete(node::OptiNode, cref::ConstraintRef) + if node !== JuMP.owner_model(cref) + error( + "The constraint reference you are trying to delete does not " * + "belong to the model.", + ) + end + _set_dirty(node) + MOI.delete(node, cref) + # for graph in containing_optigraphs(node) + # MOI.delete(graph_backend(node), cref) + # end + return end \ No newline at end of file