diff --git a/src/Plasmo.jl b/src/Plasmo.jl index cd875d1..1e192e2 100644 --- a/src/Plasmo.jl +++ b/src/Plasmo.jl @@ -29,6 +29,7 @@ export OptiGraph, OptiNode, OptiEdge, NodeVariableRef, + EdgeConstraintRef, direct_moi_graph, graph_backend, graph_index, @@ -108,7 +109,8 @@ export OptiGraph, # other functions - set_jump_model + set_jump_model, + extract_variables include("core_types.jl") @@ -138,6 +140,8 @@ include("graph_functions/topology.jl") include("graph_functions/partition.jl") +include("utilities.jl") + # extensions function __init__() @require KaHyPar = "2a6221f6-aa48-11e9-3542-2d9e0ef01880" include( diff --git a/src/backends/moi_backend.jl b/src/backends/moi_backend.jl index f4ca62c..2dd76cb 100644 --- a/src/backends/moi_backend.jl +++ b/src/backends/moi_backend.jl @@ -528,22 +528,54 @@ end # MOI variables and constraints # -function MOI.add_variable(graph_backend::GraphMOIBackend, vref::NodeVariableRef) +function MOI.add_variable(backend::GraphMOIBackend, vref::NodeVariableRef) # return if variable already exists in backend - vref in keys(graph_backend.element_to_graph_map.var_map) && return nothing + vref in keys(backend.element_to_graph_map.var_map) && return nothing # add the variable - graph_var_index = MOI.add_variable(graph_backend.moi_backend) + graph_var_index = MOI.add_variable(backend.moi_backend) # map reference to index - graph_backend.element_to_graph_map[vref] = graph_var_index - graph_backend.graph_to_element_map[graph_var_index] = vref + backend.element_to_graph_map[vref] = graph_var_index + backend.graph_to_element_map[graph_var_index] = vref # create key for node if necessary - if !haskey(graph_backend.node_variables, vref.node) - graph_backend.node_variables[vref.node] = MOI.VariableIndex[] + if !haskey(backend.node_variables, vref.node) + backend.node_variables[vref.node] = MOI.VariableIndex[] end - push!(graph_backend.node_variables[vref.node], graph_var_index) + push!(backend.node_variables[vref.node], graph_var_index) + return graph_var_index +end + +function MOI.add_constrained_variable( + backend::GraphMOIBackend, + vref::NodeVariableRef, + cref::NodeConstraintRef, + set::MOI.AbstractScalarSet, +) + # return if variable already exists in backend + vref in keys(backend.element_to_graph_map.var_map) && return nothing + + # add the variable and parameter constraint + graph_var_index, graph_con_index = MOI.add_constrained_variable( + backend.moi_backend, set + ) + + # map reference to index + backend.element_to_graph_map[vref] = graph_var_index + backend.graph_to_element_map[graph_var_index] = vref + backend.element_to_graph_map[cref] = graph_con_index + backend.graph_to_element_map[graph_con_index] = cref + + # create key for node if necessary + if !haskey(backend.node_variables, vref.node) + backend.node_variables[vref.node] = MOI.VariableIndex[] + end + if !haskey(backend.element_constraints, vref.node) + graph_backend.element_constraints[vref.node] = MOI.ConstraintIndex[] + end + push!(backend.node_variables[vref.node], graph_var_index) + push!(backend.element_constraints[vref.node], graph_con_index) return graph_var_index end @@ -879,6 +911,7 @@ function _copy_node_variables( # map existing variables in the index_map # existing variables may come from linking constraints added between graphs + # TODO: could be slow... existing_vars = intersect(node_variables, keys(dest.element_to_graph_map.var_map)) for var in existing_vars src_graph_index = graph_index(var) diff --git a/src/core_types.jl b/src/core_types.jl index 19bd4cf..5398583 100644 --- a/src/core_types.jl +++ b/src/core_types.jl @@ -69,8 +69,6 @@ struct ElementData{GT<:AbstractOptiGraph} # track constraint indices last_constraint_index::OrderedDict{OptiElement,Int} end - -# default is OptiGraph function ElementData(GT::Type{<:AbstractOptiGraph}) return ElementData{GT}( OrderedDict{OptiNode{GT},Vector{GT}}(), diff --git a/src/node_variables.jl b/src/node_variables.jl index 4ff88a7..3088522 100644 --- a/src/node_variables.jl +++ b/src/node_variables.jl @@ -3,7 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -# TODO: parameterize on precision +# TODO: parameterize variables on precision struct NodeVariableRef <: JuMP.AbstractVariableRef node::OptiNode @@ -70,23 +70,20 @@ function MOI.delete(node::OptiNode, vref::NodeVariableRef) return nothing end +# add variable + """ JuMP.add_variable(node::OptiNode, v::JuMP.AbstractVariable, name::String="") Add variable `v` to optinode `node`. This function supports use of the `@variable` JuMP macro. Optionally add a `base_name` to the variable for printing. """ -function JuMP.add_variable(node::OptiNode, v::JuMP.AbstractVariable, name::String="") - vref = _moi_add_node_variable(node, v) - if !isempty(name) && MOI.supports( - JuMP.backend(graph_backend(node)), MOI.VariableName(), MOI.VariableIndex - ) - JuMP.set_name(vref, "$(JuMP.name(node))[:$(name)]") - end - return vref +function JuMP.add_variable(node::OptiNode, v::JuMP.ScalarVariable, name::String="") + nvref = _moi_add_node_variable(node, v, name) + return nvref end -function _moi_add_node_variable(node::OptiNode, v::JuMP.AbstractVariable) +function _moi_add_node_variable(node::OptiNode, v::JuMP.ScalarVariable, name::String) # get a new variable index and create a reference variable_index = next_variable_index(node) nvref = NodeVariableRef(node, variable_index) @@ -98,6 +95,11 @@ function _moi_add_node_variable(node::OptiNode, v::JuMP.AbstractVariable) # constrain node variable (hits all graph backends) _moi_constrain_node_variable(nvref, v.info, Float64) + + if !isempty(name) && + MOI.supports(JuMP.backend(node), MOI.VariableName(), MOI.VariableIndex) + JuMP.set_name(nvref, "$(JuMP.name(node))[:$(name)]") + end return nvref end @@ -128,6 +130,64 @@ function _moi_constrain_node_variable(nvref::NodeVariableRef, info, ::Type{T}) w end end +# add variable constrained on creation + +function JuMP.add_variable( + node::OptiNode, variable::VariableConstrainedOnCreation, name::String +) + nvref = _moi_add_constrained_node_variable( + node, variable.scalar_variable, variable.set, name, Float64 + ) + return nvref +end + +function JuMP.add_variable( + node::OptiNode, + variables::AbstractArray{<:VariableConstrainedOnCreation}, + names::AbstractArray{<:String}, +) + return JuMP.add_variable.(node, variables, names) +end + +function JuMP.add_variable( + node::OptiNode, variables::AbstractArray{<:VariableConstrainedOnCreation}, name::String +) + return JuMP.add_variable.(node, variables, Ref(name)) +end + +function _moi_add_constrained_node_variable( + node::OptiNode, + scalar_variable::ScalarVariable, + set::MOI.AbstractScalarSet, + name::String, + ::Type{T}, +) where {T} + # get a new variable index and create a reference + variable_index = next_variable_index(node) + nvref = NodeVariableRef(node, variable_index) + + # get a new constraint index and create a reference + constraint_index = next_constraint_index( + node, MOI.VariableIndex, typeof(set) + )::MOI.ConstraintIndex{MOI.VariableIndex,typeof(set)} + cref = ConstraintRef(node, constraint_index, JuMP.ScalarShape()) + + # add variable to all containing optigraphs + for graph in containing_optigraphs(node) + MOI.add_constrained_variable(JuMP.backend(graph), nvref, cref, set) + end + + _moi_constrain_node_variable(nvref, scalar_variable.info, T) + + if !isempty(name) && + MOI.supports(JuMP.backend(node), MOI.VariableName(), MOI.VariableIndex) + JuMP.set_name(nvref, "$(JuMP.name(node))[:$(name)]") + end + return nvref +end + +# variable methods + function JuMP.delete(node::OptiNode, nvref::NodeVariableRef) if node !== JuMP.owner_model(nvref) error( @@ -167,7 +227,7 @@ function JuMP.index(vref::NodeVariableRef) return vref.index end -### variable values +# variable primal values function JuMP.value(nvref::NodeVariableRef; result::Int=1) return MOI.get(graph_backend(nvref.node), MOI.VariablePrimal(result), nvref) @@ -177,7 +237,43 @@ function JuMP.value(var_value::Function, vref::NodeVariableRef) return var_value(vref) end -### variable start values +# parameters + +function JuMP.ParameterRef(nvref::NodeVariableRef) + if !JuMP.is_parameter(nvref) + error("Variable $x is not a parameter.") + end + return ConstraintRef(JuMP.owner_model(nvref), _parameter_index(nvref), ScalarShape()) +end + +function JuMP.is_parameter(nvref::NodeVariableRef) + return MOI.is_valid( + JuMP.backend(JuMP.owner_model(nvref)), _parameter_index(nvref) + )::Bool +end + +function JuMP.parameter_value(nvref::NodeVariableRef) + set = MOI.get( + JuMP.owner_model(nvref), MOI.ConstraintSet(), ParameterRef(nvref) + )::MOI.Parameter{JuMP.value_type(typeof(nvref))} + return set.value +end + +function JuMP.set_parameter_value(nvref::NodeVariableRef, value) + node = JuMP.owner_model(nvref) + T = JuMP.value_type(typeof(nvref)) + _set_dirty(node) + set = MOI.Parameter{T}(convert(T, value)) + MOI.set(node, MOI.ConstraintSet(), ParameterRef(nvref), set) + return nothing +end + +function _parameter_index(nvref::NodeVariableRef) + F, S = MOI.VariableIndex, MOI.Parameter{JuMP.value_type(typeof(nvref))} + return MOI.ConstraintIndex{F,S}(JuMP.index(nvref).value) +end + +# variable start values function JuMP.start_value(nvref::NodeVariableRef) return MOI.get(graph_backend(nvref.node), MOI.VariablePrimalStart(), nvref) @@ -192,7 +288,14 @@ function JuMP.set_start_value(nvref::NodeVariableRef, value::Union{Nothing,Real} ) end -### node variable bounds +# variable bounds - lower bound + +function JuMP.LowerBoundRef(nvref::NodeVariableRef) + if !JuMP.has_lower_bound(nvref) + error("Variable $(nvref) does not have a lower bound.") + end + return _nv_lower_bound_ref(nvref) +end function JuMP.has_lower_bound(nvref::NodeVariableRef) return _moi_nv_has_lower_bound(nvref) @@ -220,13 +323,6 @@ function JuMP.delete_lower_bound(nvref::NodeVariableRef) return nothing end -function JuMP.LowerBoundRef(nvref::NodeVariableRef) - if !JuMP.has_lower_bound(nvref) - error("Variable $(nvref) does not have a lower bound.") - end - return _nv_lower_bound_ref(nvref) -end - function _moi_nv_has_lower_bound(nvref::NodeVariableRef) backend = graph_backend(nvref.node) ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}( @@ -258,6 +354,15 @@ function _moi_nv_set_lower_bound(nvref::NodeVariableRef, lower::Number) return nothing end +# variable bounds - upper bound + +function JuMP.UpperBoundRef(nvref::NodeVariableRef) + if !JuMP.has_upper_bound(nvref) + error("Variable $(nvref) does not have an upper bound.") + end + return _nv_upper_bound_ref(nvref) +end + function JuMP.has_upper_bound(nvref::NodeVariableRef) return _moi_nv_has_upper_bound(nvref) end @@ -284,13 +389,6 @@ function JuMP.delete_upper_bound(nvref::NodeVariableRef) return nothing end -function JuMP.UpperBoundRef(nvref::NodeVariableRef) - if !JuMP.has_upper_bound(nvref) - error("Variable $(nvref) does not have an upper bound.") - end - return _nv_upper_bound_ref(nvref) -end - function _moi_nv_has_upper_bound(nvref::NodeVariableRef) backend = graph_backend(nvref.node) ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}( @@ -322,7 +420,7 @@ function _moi_nv_set_upper_bound(nvref::NodeVariableRef, upper::Number) return nothing end -### fix/unfix variable +# fix/unfix variable function JuMP.FixRef(nvref::NodeVariableRef) if !JuMP.is_fixed(nvref) @@ -404,7 +502,7 @@ function JuMP.unfix(nvref::NodeVariableRef) return nothing end -### node variable integer +# variable integer function JuMP.IntegerRef(nvref::NodeVariableRef) if !JuMP.is_integer(nvref) @@ -460,7 +558,7 @@ function JuMP.unset_integer(nvref::NodeVariableRef) return nothing end -### node variable binary +# variable binary function JuMP.BinaryRef(nvref::NodeVariableRef) if !JuMP.is_binary(nvref) @@ -516,7 +614,9 @@ function JuMP.unset_binary(nvref::NodeVariableRef) return nothing end -# Extended from https://github.com/jump-dev/JuMP.jl/blob/301d46e81cb66c74c6e22cd89fb89ced740f157b/src/variables.jl#L2721 +# normalized coefficient + +## Extended from https://github.com/jump-dev/JuMP.jl/blob/301d46e81cb66c74c6e22cd89fb89ced740f157b/src/variables.jl#L2721 function JuMP.set_normalized_coefficient( con_ref::S, variable::NodeVariableRef, value::Number ) where {S<:Union{NodeConstraintRef,EdgeConstraintRef}} @@ -647,39 +747,3 @@ function JuMP.set_normalized_coefficient( graph.is_model_dirty = true return nothing end - -### Utilities for querying variables used in constraints - -function _extract_variables(func::NodeVariableRef) - return [func] -end - -function _extract_variables(ref::ConstraintRef) - func = JuMP.jump_function(JuMP.constraint_object(ref)) - return _extract_variables(func) -end - -function _extract_variables(func::JuMP.GenericAffExpr) - return collect(keys(func.terms)) -end - -function _extract_variables(func::JuMP.GenericQuadExpr) - quad_vars = vcat([[term[2]; term[3]] for term in JuMP.quad_terms(func)]...) - aff_vars = _extract_variables(func.aff) - return union(quad_vars, aff_vars) -end - -function _extract_variables(func::JuMP.GenericNonlinearExpr) - vars = NodeVariableRef[] - for i in 1:length(func.args) - func_arg = func.args[i] - if func_arg isa Number - continue - elseif typeof(func_arg) == NodeVariableRef - push!(vars, func_arg) - else - append!(vars, _extract_variables(func_arg)) - end - end - return vars -end diff --git a/src/optiedge.jl b/src/optiedge.jl index 2bed1d9..518875b 100644 --- a/src/optiedge.jl +++ b/src/optiedge.jl @@ -11,13 +11,13 @@ Base.show(io::IO, edge::OptiEdge) = Base.print(io, edge) function Base.setindex!(edge::OptiEdge, value::Any, name::Symbol) t = (edge, name) - source_graph(edge).edge_obj_dict[t] = value + source_graph(edge).element_data.edge_obj_dict[t] = value return nothing end function Base.getindex(edge::OptiEdge, name::Symbol) t = (edge, name) - return edge.source_graph.edge_obj_dict[t] + return source_graph(edge).element_data.edge_obj_dict[t] end """ @@ -72,7 +72,7 @@ end function JuMP.all_variables(edge::OptiEdge) con_refs = JuMP.all_constraints(edge) - vars = vcat(_extract_variables.(con_refs)...) + vars = vcat(extract_variables.(con_refs)...) return unique(vars) end @@ -134,6 +134,10 @@ function JuMP.is_valid(edge::OptiEdge, cref::ConstraintRef) return edge === JuMP.owner_model(cref) && MOI.is_valid(graph_backend(edge), cref) end +function get_edge(cref::EdgeConstraintRef) + return JuMP.owner_model(cref) +end + """ JuMP.dual(cref::EdgeConstraintRef; result::Int=1) diff --git a/src/optigraph.jl b/src/optigraph.jl index 742f928..4ee979b 100644 --- a/src/optigraph.jl +++ b/src/optigraph.jl @@ -199,7 +199,7 @@ Add an existing optinode (created in another optigraph) to `graph`. This copies from the other graph to the new graph. """ function add_node(graph::OptiGraph, node::OptiNode) - node in all_nodes(graph) && error("Node already exists within graph") + # node in all_nodes(graph) && error("Node already exists within graph") push!(graph.optinodes, node) add_node(graph_backend(graph), node) _track_node_in_graph(graph, node) @@ -232,7 +232,7 @@ end Retrieve the optinodes contained in a JuMP expression. """ function collect_nodes(jump_func::T where {T<:JuMP.AbstractJuMPScalar}) - vars = _extract_variables(jump_func) + vars = extract_variables(jump_func) nodes = JuMP.owner_model.(vars) return collect(nodes) end @@ -319,7 +319,7 @@ Add an existing optiedge (created in another optigraph) to `graph`. This copies from the other graph to the new graph. """ function add_edge(graph::OptiGraph, edge::OptiEdge) - edge in all_edges(graph) && error("Cannot add the same edge to a graph multiple times") + # edge in all_edges(graph) && error("Cannot add the same edge to a graph multiple times") push!(graph.optiedges, edge) add_edge(graph_backend(graph), edge) _track_edge_in_graph(graph, edge) diff --git a/src/optimizer_interface.jl b/src/optimizer_interface.jl index 346cc2f..ee8d493 100644 --- a/src/optimizer_interface.jl +++ b/src/optimizer_interface.jl @@ -117,7 +117,7 @@ function JuMP.set_optimizer( ) JuMP.error_if_direct_mode(JuMP.backend(graph), :set_optimizer) if add_bridges - optimizer = MOI.instantiate(optimizer_constructor)#; with_bridge_type = T) + optimizer = MOI.instantiate(optimizer_constructor; with_bridge_type=Float64) for BT in graph.bridge_types _moi_call_bridge_function(MOI.Bridges.add_bridge, optimizer, BT) end diff --git a/src/optinode.jl b/src/optinode.jl index dde6fa5..11a0e11 100644 --- a/src/optinode.jl +++ b/src/optinode.jl @@ -202,7 +202,7 @@ function _check_node_variables( NodeVariableRef,JuMP.GenericAffExpr,JuMP.GenericQuadExpr,JuMP.GenericNonlinearExpr }, ) - extract_vars = _extract_variables(jump_func) + extract_vars = extract_variables(jump_func) for var in extract_vars if var.node != node error("Variable $var does not belong to node $node") diff --git a/src/utilities.jl b/src/utilities.jl new file mode 100644 index 0000000..5b0cb8a --- /dev/null +++ b/src/utilities.jl @@ -0,0 +1,39 @@ +### Utilities for querying variables used in constraints + +function extract_variables(func) + return _extract_variables(func) +end + +function _extract_variables(func::NodeVariableRef) + return [func] +end + +function _extract_variables(ref::EdgeConstraintRef) + func = JuMP.jump_function(JuMP.constraint_object(ref)) + return _extract_variables(func) +end + +function _extract_variables(func::JuMP.GenericAffExpr) + return collect(keys(func.terms)) +end + +function _extract_variables(func::JuMP.GenericQuadExpr) + quad_vars = vcat([[term[2]; term[3]] for term in JuMP.quad_terms(func)]...) + aff_vars = _extract_variables(func.aff) + return union(quad_vars, aff_vars) +end + +function _extract_variables(func::JuMP.GenericNonlinearExpr) + vars = NodeVariableRef[] + for i in 1:length(func.args) + func_arg = func.args[i] + if func_arg isa Number + continue + elseif typeof(func_arg) == NodeVariableRef + push!(vars, func_arg) + else + append!(vars, _extract_variables(func_arg)) + end + end + return vars +end diff --git a/test/test_optigraph.jl b/test/test_optigraph.jl index 7eb4195..2d499fd 100644 --- a/test/test_optigraph.jl +++ b/test/test_optigraph.jl @@ -330,6 +330,9 @@ function test_variable_constraints() @variable(n1, x >= 1) @variable(n2, 0 <= x <= 2) + # parameter + @variable(n1, p in Parameter(1.0)) + # start value set_start_value(n2[:x], 3.0) @test start_value(n2[:x]) == 3.0