From c8588c9a6835798010d988c638df43ad177d9dbd Mon Sep 17 00:00:00 2001 From: Joaquim Garcia Date: Sun, 3 Dec 2023 04:02:45 -0300 Subject: [PATCH 1/2] re-organize code --- src/MOI_wrapper.jl | 1466 ++++++++++++++++++++ src/ParametricOptInterface.jl | 1477 +-------------------- src/duals.jl | 30 +- src/{utils.jl => parametric_functions.jl} | 651 ++++----- src/update_parameters.jl | 84 +- test/moi_tests.jl | 6 +- 6 files changed, 1855 insertions(+), 1859 deletions(-) create mode 100644 src/MOI_wrapper.jl rename src/{utils.jl => parametric_functions.jl} (60%) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl new file mode 100644 index 00000000..1b93e2c3 --- /dev/null +++ b/src/MOI_wrapper.jl @@ -0,0 +1,1466 @@ +# +# Helpers +# + +function _is_variable(v::MOI.VariableIndex) + return v.value < PARAMETER_INDEX_THRESHOLD +end + +function _is_parameter(v::MOI.VariableIndex) + return v.value > PARAMETER_INDEX_THRESHOLD +end + +function _has_parameters(f::MOI.ScalarAffineFunction{T}) where {T} + for term in f.terms + if _is_parameter(term.variable) + return true + end + end + return false +end + +function _has_parameters(f::MOI.VectorOfVariables) + for variable in f.variables + if _is_parameter(variable) + return true + end + end + return false +end + +function _has_parameters(f::MOI.VectorAffineFunction{T}) where {T} + for term in f.terms + if _is_parameter(term.scalar_term.variable) + return true + end + end + return false +end + +function _has_parameters(f::MOI.ScalarQuadraticFunction{T}) where {T} + for term_l in f.affine_terms + if _is_parameter(term_l.variable) + return true + end + end + for term in f.quadratic_terms + if _is_parameter(term.variable_1) || _is_parameter(term.variable_2) + return true + end + end + return false +end + +function _cache_multiplicative_params!( + model::Optimizer{T}, + f::ParametricQuadraticFunction{T}, +) where {T} + for term in f.pv + push!(model.multiplicative_parameters, term.variable_1.value) + end + # TODO compute these duals might be feasible + for term in f.pp + push!(model.multiplicative_parameters, term.variable_1.value) + push!(model.multiplicative_parameters, term.variable_2.value) + end + return +end + +# +# Empty +# + +function MOI.is_empty(model::Optimizer) + return MOI.is_empty(model.optimizer) && + isempty(model.parameters) && + isempty(model.parameters_name) && + isempty(model.updated_parameters) && + isempty(model.variables) && + model.last_variable_index_added == 0 && + model.last_parameter_index_added == PARAMETER_INDEX_THRESHOLD && + isempty(model.constraint_outer_to_inner) && + # affine ctr + model.last_affine_added == 0 && + isempty(model.affine_outer_to_inner) && + isempty(model.affine_constraint_cache) && + isempty(model.affine_constraint_cache_set) && + # quad ctr + model.last_quad_add_added == 0 && + isempty(model.quadratic_outer_to_inner) && + isempty(model.quadratic_constraint_cache) && + isempty(model.quadratic_constraint_cache_set) && + # obj + model.affine_objective_cache === nothing && + model.quadratic_objective_cache === nothing && + MOI.is_empty(model.original_objective_cache) && + isempty(model.quadratic_objective_cache_product) && + # + isempty(model.vector_affine_constraint_cache) && + # + isempty(model.multiplicative_parameters) && + isempty(model.dual_value_of_parameters) && + model.number_of_parameters_in_model == 0 +end + +function MOI.empty!(model::Optimizer{T}) where {T} + MOI.empty!(model.optimizer) + empty!(model.parameters) + empty!(model.parameters_name) + empty!(model.updated_parameters) + empty!(model.variables) + model.last_variable_index_added = 0 + model.last_parameter_index_added = PARAMETER_INDEX_THRESHOLD + empty!(model.constraint_outer_to_inner) + # affine ctr + model.last_affine_added = 0 + empty!(model.affine_outer_to_inner) + empty!(model.affine_constraint_cache) + empty!(model.affine_constraint_cache_set) + # quad ctr + model.last_quad_add_added = 0 + empty!(model.quadratic_outer_to_inner) + empty!(model.quadratic_constraint_cache) + empty!(model.quadratic_constraint_cache_set) + # obj + model.affine_objective_cache = nothing + model.quadratic_objective_cache = nothing + MOI.empty!(model.original_objective_cache) + empty!(model.quadratic_objective_cache_product) + # + empty!(model.vector_affine_constraint_cache) + # + empty!(model.multiplicative_parameters) + empty!(model.dual_value_of_parameters) + # + model.number_of_parameters_in_model = 0 + return +end + +# +# Variables +# + +# TODO: This is not correct +function MOI.is_valid(model::Optimizer, vi::MOI.VariableIndex) + return MOI.is_valid(model.optimizer, vi) +end + +function MOI.supports( + model::Optimizer, + attr::MOI.VariableName, + tp::Type{MOI.VariableIndex}, +) + return MOI.supports(model.optimizer, attr, tp) +end + +function MOI.set( + model::Optimizer, + attr::MOI.VariableName, + v::MOI.VariableIndex, + name::String, +) + if _parameter_in_model(model, v) + model.parameters_name[v] = name + else + MOI.set(model.optimizer, attr, v, name) + end + return +end + +function MOI.get(model::Optimizer, attr::MOI.VariableName, v::MOI.VariableIndex) + if _parameter_in_model(model, v) + return get(model.parameters_name, v, "") + else + return MOI.get(model.optimizer, attr, v) + end +end + +function MOI.get(model::Optimizer, tp::Type{MOI.VariableIndex}, attr::String) + return MOI.get(model.optimizer, tp, attr) +end + +function MOI.add_variable(model::Optimizer) + _next_variable_index!(model) + return MOI.Utilities.CleverDicts.add_item( + model.variables, + MOI.add_variable(model.optimizer), + ) +end + +function MOI.supports_add_constrained_variable( + ::Optimizer{T}, + ::Type{MOI.Parameter{T}}, +) where {T} + return true +end + +function MOI.supports_add_constrained_variables( + model::Optimizer, + ::Type{MOI.Reals}, +) + return MOI.supports_add_constrained_variables(model.optimizer, MOI.Reals) +end + +function MOI.add_constrained_variable( + model::Optimizer{T}, + set::MOI.Parameter{T}, +) where {T} + _next_parameter_index!(model) + p = MOI.VariableIndex(model.last_parameter_index_added) + MOI.Utilities.CleverDicts.add_item(model.parameters, set.value) + cp = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}( + model.last_parameter_index_added, + ) + _add_to_constraint_map!(model, cp) + MOI.Utilities.CleverDicts.add_item(model.updated_parameters, NaN) + _update_number_of_parameters!(model) + return p, cp +end + +function _add_to_constraint_map!(model::Optimizer, ci) + model.constraint_outer_to_inner[ci] = ci + return +end +function _add_to_constraint_map!(model::Optimizer, ci_in, ci_out) + model.constraint_outer_to_inner[ci_out] = ci_in + return +end + +function MOI.supports( + model::Optimizer, + attr::MOI.AbstractVariableAttribute, + tp::Type{MOI.VariableIndex}, +) + return MOI.supports(model.optimizer, attr, tp) +end + +function MOI.set( + model::Optimizer, + attr::MOI.AbstractVariableAttribute, + v::MOI.VariableIndex, + val, +) + if _variable_in_model(model, v) + MOI.set(model.optimizer, attr, v, val) + else + error("$attr is not supported for parameters") + end +end + +function MOI.get( + model::Optimizer, + attr::MOI.AbstractVariableAttribute, + v::MOI.VariableIndex, +) + if _variable_in_model(model, v) + return MOI.get(model.optimizer, attr, model.variables[v]) + else + error("$attr is not supported for parameters") + end +end + +function MOI.delete(model::Optimizer, v::MOI.VariableIndex) + delete!(model.variables, v) + MOI.delete(model.optimizer, v) + MOI.delete(model.original_objective_cache, v) + # TODO - what happens if the variable was in a SAF that was converted to bounds? + # solution: do not allow if that is the case (requires going trhought the scalar affine cache) + # TODO - deleting a variable also deletes constraints + for (F, S) in MOI.Utilities.DoubleDicts.nonempty_outer_keys( + model.constraint_outer_to_inner, + ) + _delete_variable_index_constraint( + model.constraint_outer_to_inner, + F, + S, + v.value, + ) + end + return +end + +function _delete_variable_index_constraint(d, F, S, v) + return +end + +function _delete_variable_index_constraint( + d, + F::Type{MOI.VariableIndex}, + S, + value, +) + inner = d[F, S] + for k in keys(inner) + if k.value == value + delete!(inner, k) + end + end + return +end + +# +# Constraints +# + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{F,S}, +) where { + F<:Union{MOI.VariableIndex,MOI.VectorOfVariables,MOI.VectorAffineFunction}, + S<:MOI.AbstractSet, +} + return MOI.is_valid(model.optimizer, c) +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{F,S}, +) where {F<:MOI.ScalarAffineFunction,S<:MOI.AbstractSet} + return MOI.is_valid(model.optimizer, c) +end + +function MOI.supports_constraint( + model::Optimizer, + F::Union{ + Type{MOI.VariableIndex}, + Type{MOI.ScalarAffineFunction{T}}, + Type{MOI.VectorOfVariables}, + Type{MOI.VectorAffineFunction{T}}, + }, + S::Type{<:MOI.AbstractSet}, +) where {T} + return MOI.supports_constraint(model.optimizer, F, S) +end + +function MOI.supports_constraint( + model::Optimizer, + ::Type{MOI.ScalarQuadraticFunction{T}}, + S::Type{<:MOI.AbstractSet}, +) where {T} + return MOI.supports_constraint( + model.optimizer, + MOI.ScalarAffineFunction{T}, + S, + ) +end + +function MOI.supports_constraint( + model::Optimizer, + ::Type{MOI.VectorQuadraticFunction{T}}, + S::Type{<:MOI.AbstractSet}, +) where {T} + return MOI.supports_constraint( + model.optimizer, + MOI.VectorAffineFunction{T}, + S, + ) +end + +function MOI.supports( + model::Optimizer, + attr::MOI.ConstraintName, + tp::Type{<:MOI.ConstraintIndex}, +) + return MOI.supports(model.optimizer, attr, tp) +end + +function MOI.set( + model::Optimizer, + attr::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},S}, + name::String, +) where {T,S<:MOI.AbstractSet} + if haskey(model.quadratic_outer_to_inner, c) + MOI.set(model.optimizer, attr, model.quadratic_outer_to_inner[c], name) + else + MOI.set(model.optimizer, attr, c, name) + end + return +end + +function MOI.set( + model::Optimizer, + attr::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}, + name::String, +) where {T,S<:MOI.AbstractSet} + if haskey(model.affine_outer_to_inner, c) + MOI.set(model.optimizer, attr, model.affine_outer_to_inner[c], name) + else + MOI.set(model.optimizer, attr, c, name) + end + return +end + +function MOI.set( + model::Optimizer, + attr::MOI.ConstraintName, + c::MOI.ConstraintIndex, + name::String, +) + MOI.set(model.optimizer, attr, c, name) + return +end + +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},S}, +) where {T,S<:MOI.AbstractSet} + if haskey(model.quadratic_outer_to_inner, c) + return MOI.get(model.optimizer, attr, model.quadratic_outer_to_inner[c]) + else + return MOI.get(model.optimizer, attr, c) + end +end + +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintName, + c::MOI.ConstraintIndex, +) + return MOI.get(model.optimizer, attr, c) +end + +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}, +) where {T,S} + if haskey(model.affine_outer_to_inner, c) + inner_ci = model.affine_outer_to_inner[c] + # This SAF constraint was transformed into variable bound + if typeof(inner_ci) === MOI.ConstraintIndex{MOI.VariableIndex,S} + v = MOI.get(model.optimizer, MOI.ConstraintFunction(), inner_ci) + variable_name = MOI.get(model.optimizer, MOI.VariableName(), v) + return "ParametricBound_$(S)_$(variable_name)" + end + return MOI.get(model.optimizer, attr, inner_ci) + else + return MOI.get(model.optimizer, attr, c) + end +end + +function MOI.get( + model::Optimizer, + tp::Type{MOI.ConstraintIndex{F,S}}, + name::String, +) where {F,S} + return MOI.get(model.optimizer, tp, name) +end + +function MOI.get(model::Optimizer, tp::Type{MOI.ConstraintIndex}, name::String) + return MOI.get(model.optimizer, tp, name) +end + +function MOI.set( + model::Optimizer, + ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{F,S}, + f::F, +) where {F,S} + MOI.set(model.optimizer, MOI.ConstraintFunction(), c, f) + return +end + +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintFunction, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + if haskey(model.quadratic_outer_to_inner, ci) + inner_ci = model.quadratic_outer_to_inner[ci] + return _original_function(model.quadratic_constraint_cache[inner_ci]) + elseif haskey(model.affine_outer_to_inner, ci) + inner_ci = model.affine_outer_to_inner[ci] + return _original_function(model.affine_constraint_cache[inner_ci]) + else + MOI.throw_if_not_valid(model, ci) + return MOI.get(model.optimizer, attr, ci) + end +end + +function MOI.get( + model::Optimizer{T}, + ::MOI.ConstraintFunction, + cp::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}, +) where {T} + p = MOI.VariableIndex(cp.value) + if !_parameter_in_model(model, p) + error("Parameter not in the model") + end + return p +end + +function MOI.set( + model::Optimizer, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{F,S}, + s::S, +) where {F,S} + MOI.set(model.optimizer, MOI.ConstraintSet(), c, s) + return +end + +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintSet, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + if haskey(model.quadratic_outer_to_inner, ci) + inner_ci = model.quadratic_outer_to_inner[ci] + return model.quadratic_constraint_cache_set[inner_ci] + elseif haskey(model.affine_outer_to_inner, ci) + inner_ci = model.affine_outer_to_inner[ci] + return model.affine_constraint_cache_set[inner_ci] + else + MOI.throw_if_not_valid(model, ci) + return MOI.get(model.optimizer, attr, ci) + end +end + +function MOI.set( + model::Optimizer{T}, + ::MOI.ConstraintSet, + cp::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}, + set::MOI.Parameter{T}, +) where {T} + p = MOI.VariableIndex(cp.value) + if !_parameter_in_model(model, p) + error("Parameter not in the model") + end + return model.updated_parameters[p_idx(p)] = set.value +end + +function MOI.get( + model::Optimizer{T}, + ::MOI.ConstraintSet, + cp::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}, +) where {T} + p = MOI.VariableIndex(cp.value) + if !_parameter_in_model(model, p) + error("Parameter not in the model") + end + val = model.updated_parameters[p_idx(p)] + if isnan(val) + return MOI.Parameter{T}(model.parameters[p_idx(p)]) + end + return MOI.Parameter{T}(val) +end + +function MOI.modify( + model::Optimizer, + c::MOI.ConstraintIndex{F,S}, + chg::MOI.ScalarCoefficientChange{T}, +) where {F,S,T} + if haskey(model.quadratic_constraint_cache, c) || + haskey(model.affine_constraint_cache, c) + error("Parametric constraint cannot be modified") + end + MOI.modify(model.optimizer, c, chg) + return +end + +function _add_constraint_direct_and_cache_map!(model::Optimizer, f, set) + ci = MOI.add_constraint(model.optimizer, f, set) + _add_to_constraint_map!(model, ci) + return ci +end + +function MOI.add_constraint( + model::Optimizer, + f::MOI.VariableIndex, + set::MOI.AbstractScalarSet, +) + if !_is_variable(f) + error("Cannot constrain a parameter") + elseif !_variable_in_model(model, f) + error("Variable not in the model") + end + return _add_constraint_direct_and_cache_map!(model, f, set) +end + +function _add_constraint_with_parameters_on_function( + model::Optimizer, + f::MOI.ScalarAffineFunction{T}, + set::S, +) where {T,S} + pf = ParametricAffineFunction(f) + _cache_set_constant!(pf, set) + if model.constraints_interpretation == ONLY_BOUNDS + if length(pf.v) == 1 && isone(MOI.coefficient(pf.v[])) + poi_ci = _add_vi_constraint(model, pf, set) + else + error( + "It was not possible to interpret this constraint as a variable bound.", + ) + end + elseif model.constraints_interpretation == ONLY_CONSTRAINTS + poi_ci = _add_saf_constraint(model, pf, set) + elseif model.constraints_interpretation == BOUNDS_AND_CONSTRAINTS + if length(pf.v) == 1 && isone(MOI.coefficient(pf.v[])) + poi_ci = _add_vi_constraint(model, pf, set) + else + poi_ci = _add_saf_constraint(model, pf, set) + end + end + return poi_ci +end + +function _add_saf_constraint( + model::Optimizer, + pf::ParametricAffineFunction{T}, + set::S, +) where {T,S} + _update_cache!(pf, model) + inner_ci = MOI.Utilities.normalize_and_add_constraint( + model.optimizer, + MOI.ScalarAffineFunction{T}(pf.v, 0.0), + _set_with_new_constant(set, pf.current_constant), + ) + model.last_affine_added += 1 + outer_ci = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}( + model.last_affine_added, + ) + model.affine_outer_to_inner[outer_ci] = inner_ci + model.constraint_outer_to_inner[outer_ci] = inner_ci + model.affine_constraint_cache[inner_ci] = pf + model.affine_constraint_cache_set[inner_ci] = set + return outer_ci +end + +function _add_vi_constraint( + model::Optimizer, + pf::ParametricAffineFunction{T}, + set::S, +) where {T,S} + _update_cache!(pf, model) + inner_ci = MOI.Utilities.normalize_and_add_constraint( + model.optimizer, + pf.v[].variable, + _set_with_new_constant(set, pf.current_constant), + ) + model.last_affine_added += 1 + outer_ci = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}( + model.last_affine_added, + ) + model.affine_outer_to_inner[outer_ci] = inner_ci + model.constraint_outer_to_inner[outer_ci] = inner_ci + model.affine_constraint_cache[inner_ci] = pf + model.affine_constraint_cache_set[inner_ci] = set + return outer_ci +end + +function MOI.add_constraint( + model::Optimizer, + f::MOI.ScalarAffineFunction{T}, + set::MOI.AbstractScalarSet, +) where {T} + if !_has_parameters(f) + return _add_constraint_direct_and_cache_map!(model, f, set) + else + return _add_constraint_with_parameters_on_function(model, f, set) + end +end + +function MOI.add_constraint( + model::Optimizer, + f::MOI.VectorOfVariables, + set::MOI.AbstractVectorSet, +) + if _has_parameters(f) + error("VectorOfVariables does not allow parameters") + end + return _add_constraint_direct_and_cache_map!(model, f, set) +end + +function MOI.add_constraint( + model::Optimizer, + f::MOI.VectorAffineFunction{T}, + set::MOI.AbstractVectorSet, +) where {T} + if !_has_parameters(f) + return _add_constraint_direct_and_cache_map!(model, f, set) + else + return _add_constraint_with_parameters_on_function(model, f, set) + end +end + +function _add_constraint_with_parameters_on_function( + model::Optimizer, + f::MOI.VectorAffineFunction{T}, + set::MOI.AbstractVectorSet, +) where {T} + pf = ParametricVectorAffineFunction(f) + # _cache_set_constant!(pf, set) # there is no constant is vector sets + _update_cache!(pf, model) + inner_ci = MOI.add_constraint(model.optimizer, _current_function(pf), set) + model.vector_affine_constraint_cache[inner_ci] = pf + _add_to_constraint_map!(model, inner_ci) + return inner_ci +end + +function _add_constraint_with_parameters_on_function( + model::Optimizer, + f::MOI.ScalarQuadraticFunction{T}, + s::S, +) where {T,S<:MOI.AbstractScalarSet} + pf = ParametricQuadraticFunction(f) + _cache_multiplicative_params!(model, pf) + _cache_set_constant!(pf, s) + _update_cache!(pf, model) + + func = _current_function(pf) + f_quad = if !_is_affine(func) + fq = func + inner_ci = MOI.Utilities.normalize_and_add_constraint( + model.optimizer, + fq, + s, + ) + model.last_quad_add_added += 1 + outer_ci = MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},S}( + model.last_quad_add_added, + ) + model.quadratic_outer_to_inner[outer_ci] = inner_ci + model.constraint_outer_to_inner[outer_ci] = inner_ci + else + fa = MOI.ScalarAffineFunction(func.affine_terms, func.constant) + inner_ci = MOI.Utilities.normalize_and_add_constraint( + model.optimizer, + fa, + s, + ) + model.last_quad_add_added += 1 + outer_ci = MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},S}( + model.last_quad_add_added, + ) + # This part is used to remember that ci came from a quadratic function + # It is particularly useful because sometimes the constraint mutates + model.quadratic_outer_to_inner[outer_ci] = inner_ci + model.constraint_outer_to_inner[outer_ci] = inner_ci + end + model.quadratic_constraint_cache[inner_ci] = pf + model.quadratic_constraint_cache_set[inner_ci] = s + return outer_ci +end + +function _is_affine(f::MOI.ScalarQuadraticFunction) + if isempty(f.quadratic_terms) + return true + end + return false +end + +function MOI.add_constraint( + model::Optimizer, + f::MOI.ScalarQuadraticFunction{T}, + set::MOI.AbstractScalarSet, +) where {T} + if !_has_parameters(f) + return _add_constraint_direct_and_cache_map!(model, f, set) + else + return _add_constraint_with_parameters_on_function(model, f, set) + end +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{F,S}, +) where {F<:MOI.ScalarQuadraticFunction,S<:MOI.AbstractSet} + if haskey(model.quadratic_outer_to_inner, c) + ci_inner = model.quadratic_outer_to_inner[c] + deleteat!(model.quadratic_outer_to_inner, c) + deleteat!(model.quadratic_constraint_cache, c) + deleteat!(model.quadratic_constraint_cache_set, c) + MOI.delete(model.optimizer, ci_inner) + else + MOI.delete(model.optimizer, c) + end + deleteat!(model.constraint_outer_to_inner, c) + return +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{F,S}, +) where {F<:MOI.ScalarAffineFunction,S<:MOI.AbstractSet} + if haskey(model.affine_outer_to_inner, c) + ci_inner = model.affine_outer_to_inner[c] + delete!(model.affine_outer_to_inner, c) + delete!(model.affine_constraint_cache, c) + delete!(model.affine_constraint_cache_set, c) + MOI.delete(model.optimizer, ci_inner) + else + MOI.delete(model.optimizer, c) + end + delete!(model.constraint_outer_to_inner, c) + return +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{F,S}, +) where {F<:Union{MOI.VariableIndex,MOI.VectorOfVariables},S<:MOI.AbstractSet} + MOI.delete(model.optimizer, c) + delete!(model.constraint_outer_to_inner, c) + return +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{F,S}, +) where {F<:MOI.VectorAffineFunction,S<:MOI.AbstractSet} + MOI.delete(model.optimizer, c) + delete!(model.constraint_outer_to_inner, c) + deleteat!(model.vector_affine_constraint_cache, c) + return +end + +# +# Objective +# + +function MOI.supports( + model::Optimizer, + attr::Union{ + MOI.ObjectiveSense, + MOI.ObjectiveFunction{MOI.VariableIndex}, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}, + }, +) where {T} + return MOI.supports(model.optimizer, attr) +end + +function MOI.supports( + model::Optimizer, + ::MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}, +) where {T} + return MOI.supports( + model.optimizer, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}(), + ) +end + +function MOI.modify( + model::Optimizer, + c::MOI.ObjectiveFunction{F}, + chg::Union{MOI.ScalarConstantChange{T},MOI.ScalarCoefficientChange{T}}, +) where {F<:MathOptInterface.AbstractScalarFunction,T} + if model.quadratic_objective_cache !== nothing || + model.affine_objective_cache !== nothing || + !isempty(model.quadratic_objective_cache_product) + error("Parametric objective cannot be modified") + end + MOI.modify(model.optimizer, c, chg) + MOI.modify(model.original_objective_cache, c, chg) + return +end + +function MOI.get(model::Optimizer, attr::MOI.ObjectiveSense) + return MOI.get(model.optimizer, attr) +end + +function MOI.get(model::Optimizer, attr::MOI.ObjectiveFunctionType) + return MOI.get(model.original_objective_cache, attr) +end + +function MOI.get(model::Optimizer, attr::MOI.ObjectiveFunction) + return MOI.get(model.original_objective_cache, attr) +end + +function _empty_objective_function_caches!(model::Optimizer{T}) where {T} + model.affine_objective_cache = nothing + model.quadratic_objective_cache = nothing + model.original_objective_cache = MOI.Utilities.ObjectiveContainer{T}() + return +end + +function MOI.set( + model::Optimizer, + attr::MOI.ObjectiveFunction, + f::MOI.ScalarAffineFunction{T}, +) where {T} + # clear previously defined objetive function cache + _empty_objective_function_caches!(model) + if !_has_parameters(f) + MOI.set(model.optimizer, attr, f) + else + pf = ParametricAffineFunction(f) + _update_cache!(pf, model) + MOI.set(model.optimizer, attr, _current_function(pf)) + model.affine_objective_cache = pf + end + MOI.set(model.original_objective_cache, attr, f) + return +end + +function MOI.set( + model::Optimizer, + attr::MOI.ObjectiveFunction{F}, + f::F, +) where {F<:MOI.ScalarQuadraticFunction{T}} where {T} + # clear previously defined objetive function cache + _empty_objective_function_caches!(model) + if !_has_parameters(f) + MOI.set(model.optimizer, attr, f) + else + pf = ParametricQuadraticFunction(f) + _cache_multiplicative_params!(model, pf) + _update_cache!(pf, model) + func = _current_function(pf) + MOI.set( + model.optimizer, + MOI.ObjectiveFunction{( + _is_affine(func) ? MOI.ScalarAffineFunction{T} : + MOI.ScalarQuadraticFunction{T} + )}(), + # func, + ( + _is_affine(func) ? + MOI.ScalarAffineFunction(func.affine_terms, func.constant) : + func + ), + ) + model.quadratic_objective_cache = pf + end + MOI.set(model.original_objective_cache, attr, f) + return +end + +function MOI.set( + model::Optimizer, + attr::MOI.ObjectiveFunction, + v::MOI.VariableIndex, +) + if _is_parameter(v) + error("Cannot use a parameter as objective function alone") + elseif !_variable_in_model(model, v) + error("Variable not in the model") + end + MOI.set(model.optimizer, attr, model.variables[v]) + MOI.set(model.original_objective_cache, attr, v) + return +end + +function MOI.set( + model::Optimizer, + attr::MOI.ObjectiveSense, + sense::MOI.OptimizationSense, +) + MOI.set(model.optimizer, attr, sense) + return +end + +# +# NLP +# + +function MOI.supports(model::Optimizer, ::MOI.NLPBlock) + return MOI.supports(model.optimizer, MOI.NLPBlock()) +end + +function MOI.set(model::Optimizer, ::MOI.NLPBlock, nlp_data::MOI.NLPBlockData) + return MOI.set(model.optimizer, MOI.NLPBlock(), nlp_data) +end + +# +# Other +# + +function MOI.supports_incremental_interface(model::Optimizer) + return MOI.supports_incremental_interface(model.optimizer) +end + +# +# Attributes +# + +function MOI.supports(model::Optimizer, ::MOI.Name) + return MOI.supports(model.optimizer, MOI.Name()) +end + +MOI.get(model::Optimizer, ::MOI.Name) = MOI.get(model.optimizer, MOI.Name()) + +function MOI.set(model::Optimizer, ::MOI.Name, name::String) + return MOI.set(model.optimizer, MOI.Name(), name) +end + +function MOI.get(model::Optimizer, ::MOI.ListOfModelAttributesSet) + return MOI.get(model.optimizer, MOI.ListOfModelAttributesSet()) +end + +function MOI.get(model::Optimizer, ::MOI.ListOfVariableAttributesSet) + return MOI.get(model.optimizer, MOI.ListOfVariableAttributesSet()) +end + +function MOI.get( + model::Optimizer, + ::MOI.ListOfConstraintAttributesSet{F,S}, +) where {F,S} + if F === MOI.ScalarQuadraticFunction + error( + "MOI.ListOfConstraintAttributesSet is not implemented for ScalarQuadraticFunction.", + ) + end + return MOI.get(model.optimizer, MOI.ListOfConstraintAttributesSet{F,S}()) +end + +function MOI.get(model::Optimizer, ::MOI.NumberOfVariables) + return length(model.parameters) + length(model.variables) +end + +function MOI.get(model::Optimizer, ::MOI.NumberOfConstraints{F,S}) where {S,F} + return length(model.constraint_outer_to_inner[F, S]) +end + +function MOI.get(model::Optimizer, ::MOI.ListOfVariableIndices) + return MOI.get(model.optimizer, MOI.ListOfVariableIndices()) +end + +function MOI.get(model::Optimizer, ::MOI.ListOfConstraintTypesPresent) + constraint_types = MOI.Utilities.DoubleDicts.nonempty_outer_keys( + model.constraint_outer_to_inner, + ) + return collect(constraint_types) +end + +function MOI.get( + model::Optimizer, + ::MOI.ListOfConstraintIndices{F,S}, +) where {S,F} + list = collect(values(model.constraint_outer_to_inner[F, S])) + sort!(list, lt = (x, y) -> (x.value < y.value)) + return list +end + +function MOI.supports(model::Optimizer, attr::MOI.AbstractOptimizerAttribute) + return MOI.supports(model.optimizer, attr) +end + +function MOI.get(model::Optimizer, attr::MOI.AbstractOptimizerAttribute) + return MOI.get(model.optimizer, attr) +end + +function MOI.set(model::Optimizer, attr::MOI.AbstractOptimizerAttribute, value) + MOI.set(model.optimizer, attr, value) + return +end + +function MOI.set(model::Optimizer, attr::MOI.RawOptimizerAttribute, val::Any) + MOI.set(model.optimizer, attr, val) + return +end + +function MOI.get(model::Optimizer, ::MOI.SolverName) + name = MOI.get(model.optimizer, MOI.SolverName()) + return "Parametric Optimizer with $(name) attached" +end + +function MOI.get(model::Optimizer, ::MOI.SolverVersion) + return MOI.get(model.optimizer, MOI.SolverVersion()) +end + +# +# Solutions Attributes +# + +function MOI.get(model::Optimizer, attr::MOI.AbstractModelAttribute) + return MOI.get(model.optimizer, attr) +end + +function MOI.get( + model::Optimizer, + attr::MOI.VariablePrimal, + v::MOI.VariableIndex, +) + if _parameter_in_model(model, v) + return model.parameters[p_idx(v)] + elseif _variable_in_model(model, v) + return MOI.get(model.optimizer, attr, model.variables[v]) + else + error("Variable not in the model") + end +end + +function MOI.get( + model::Optimizer, + attr::T, +) where { + T<:Union{ + MOI.TerminationStatus, + MOI.ObjectiveValue, + MOI.DualObjectiveValue, + MOI.PrimalStatus, + MOI.DualStatus, + }, +} + return MOI.get(model.optimizer, attr) +end + +function MOI.get( + model::Optimizer, + attr::T, + c::MOI.ConstraintIndex, +) where { + T<:Union{MOI.ConstraintPrimal,MOI.ConstraintDual,MOI.ConstraintBasisStatus}, +} + return MOI.get(model.optimizer, attr, c) +end + +function MOI.get( + model::Optimizer, + attr::AT, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}, +) where { + AT<:Union{ + MOI.ConstraintPrimal, + MOI.ConstraintDual, + MOI.ConstraintBasisStatus, + }, + T, + S<:MOI.AbstractScalarSet, +} + moi_ci = get(model.affine_outer_to_inner, c, c) + return MOI.get(model.optimizer, attr, moi_ci) +end + +# +# Special Attributes +# + +struct ListOfPureVariableIndices <: MOI.AbstractModelAttribute end + +function MOI.get(model::Optimizer, ::ListOfPureVariableIndices) + return collect(keys(model.variables)) +end + +struct ListOfParameterIndices <: MOI.AbstractModelAttribute end + +function MOI.get(model::Optimizer, ::ListOfParameterIndices) + return collect(keys(model.parameters)) +end + +""" + ParameterValue <: MOI.AbstractVariableAttribute + +Attribute defined to set and get parameter values + +# Example + +```julia +MOI.set(model, POI.ParameterValue(), p, 2.0) +MOI.get(model, POI.ParameterValue(), p) +``` +""" +struct ParameterValue <: MOI.AbstractVariableAttribute end + +# We need a CachingOptimizer fallback to +# get ParameterValue working correctly on JuMP +# TODO: Think of a better solution for this + +function MOI.set( + opt::MOI.Utilities.CachingOptimizer, + ::ParameterValue, + var::MOI.VariableIndex, + val::Float64, +) + ci = + MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{Float64}}(var.value) + set = MOI.set(opt, MOI.ConstraintSet(), ci, MOI.Parameter(val)) + return nothing +end + +function MOI.set( + model::Optimizer, + ::ParameterValue, + var::MOI.VariableIndex, + val::Float64, +) + ci = + MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{Float64}}(var.value) + set = MOI.set(model, MOI.ConstraintSet(), ci, MOI.Parameter(val)) + return nothing +end + +function MOI.set( + opt::MOI.Utilities.CachingOptimizer, + ::ParameterValue, + vi::MOI.VariableIndex, + val::Real, +) + return MOI.set(opt, ParameterValue(), vi, convert(Float64, val)) +end + +function MOI.set( + model::Optimizer, + ::ParameterValue, + vi::MOI.VariableIndex, + val::Real, +) + return MOI.set(model, ParameterValue(), vi, convert(Float64, val)) +end + +function MOI.get( + opt::MOI.Utilities.CachingOptimizer, + ::ParameterValue, + var::MOI.VariableIndex, +) + ci = + MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{Float64}}(var.value) + set = MOI.get(opt, MOI.ConstraintSet(), ci) + return set.value +end + +function MOI.get(model::Optimizer, ::ParameterValue, var::MOI.VariableIndex) + return model.parameters[p_idx(var)] +end + +""" + ConstraintsInterpretation <: MOI.AbstractOptimizerAttribute + +Attribute to define how [`POI.Optimizer`](@ref) should interpret constraints. + +- `POI.ONLY_CONSTRAINTS`: Only interpret `ScalarAffineFunction` constraints as linear constraints + If an expression such as `x >= p1 + p2` appears it will be trated like a new constraint. + **This is the default behaviour of [`POI.Optimizer`](@ref)** + +- `POI.ONLY_BOUNDS`: Only interpret `ScalarAffineFunction` constraints as a variable bound. + This is valid for constraints such as `x >= p` or `x >= p1 + p2`. If a constraint `x1 + x2 >= p` appears, + which is not a valid variable bound it will throw an error. + +- `POI.BOUNDS_AND_CONSTRAINTS`: Interpret `ScalarAffineFunction` constraints as a variable bound if they + are a valid variable bound, i.e., `x >= p` or `x >= p1 + p2` and interpret them as linear constraints + otherwise. + +# Example + +```julia +MOI.set(model, POI.InterpretConstraintsAsBounds(), POI.ONLY_BOUNDS) +MOI.set(model, POI.InterpretConstraintsAsBounds(), POI.ONLY_CONSTRAINTS) +MOI.set(model, POI.InterpretConstraintsAsBounds(), POI.BOUNDS_AND_CONSTRAINTS) +``` +""" +struct ConstraintsInterpretation <: MOI.AbstractOptimizerAttribute end + +function MOI.set( + model::Optimizer, + ::ConstraintsInterpretation, + value::ConstraintsInterpretationCode, +) + return model.constraints_interpretation = value +end + +struct QuadraticObjectiveCoef <: MOI.AbstractModelAttribute end + +function _set_quadratic_product_in_obj!(model::Optimizer{T}) where {T} + n = length(model.quadratic_objective_cache_product) + + f = if model.affine_objective_cache !== nothing + _current_function(model.affine_objective_cache) + elseif model.quadratic_objective_cache !== nothing + _current_function(model.quadratic_objective_cache) + else + F = MOI.get(model.original_objective_cache, MOI.ObjectiveFunctionType()) + MOI.get(model.original_objective_cache, MOI.ObjectiveFunction{F}()) + end + F = typeof(f) + + quadratic_prods_vector = MOI.ScalarQuadraticTerm{T}[] + sizehint!(quadratic_prods_vector, n) + + for ((x, y), fparam) in model.quadratic_objective_cache_product + # x, y = prod_var + evaluated_fparam = _evaluate_parametric_expression(model, fparam) + push!( + quadratic_prods_vector, + MOI.ScalarQuadraticTerm(evaluated_fparam, x, y), + ) + end + + f_new = if F <: MOI.VariableIndex + MOI.ScalarQuadraticFunction( + quadratic_prods_vector, + MOI.ScalarAffineTerm{T}[MOI.ScalarAffineTerm{T}(1.0, f)], + 0.0, + ) + elseif F <: MOI.ScalarAffineFunction{T} + MOI.ScalarQuadraticFunction(quadratic_prods_vector, f.terms, f.constant) + elseif F <: MOI.ScalarQuadraticFunction{T} + quadratic_terms = vcat(f.quadratic_terms, quadratic_prods_vector) + MOI.ScalarQuadraticFunction(quadratic_terms, f.affine_terms, f.constant) + end + + MOI.set( + model.optimizer, + MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}(), + f_new, + ) + + return +end + +function _evaluate_parametric_expression(model::Optimizer, p::MOI.VariableIndex) + return model.parameters[p_idx(p)] +end + +function _evaluate_parametric_expression( + model::Optimizer, + fparam::MOI.ScalarAffineFunction{T}, +) where {T} + constant = fparam.constant + terms = fparam.terms + evaluated_parameter_expression = zero(T) + for term in terms + coef = term.coefficient + p = term.variable + evaluated_parameter_expression += coef * model.parameters[p_idx(p)] + evaluated_parameter_expression += constant + end + return evaluated_parameter_expression +end + +function MOI.set( + model::Optimizer, + ::QuadraticObjectiveCoef, + (x1, x2)::Tuple{MOI.VariableIndex,MOI.VariableIndex}, + ::Nothing, +) + if x1.value > x2.value + aux = x1 + x1 = x2 + x2 = aux + end + delete!(model.quadratic_objective_cache_product, (x1, x2)) + model.quadratic_objective_cache_product_changed = true + return +end + +function MOI.set( + model::Optimizer, + ::QuadraticObjectiveCoef, + (x1, x2)::Tuple{MOI.VariableIndex,MOI.VariableIndex}, + f_param::Union{MOI.VariableIndex,MOI.ScalarAffineFunction{T}}, +) where {T} + if x1.value > x2.value + aux = x1 + x1 = x2 + x2 = aux + end + model.quadratic_objective_cache_product[(x1, x2)] = f_param + model.quadratic_objective_cache_product_changed = true + return +end + +function MOI.get( + model::Optimizer, + ::QuadraticObjectiveCoef, + (x1, x2)::Tuple{MOI.VariableIndex,MOI.VariableIndex}, +) + if x1.value > x2.value + aux = x1 + x1 = x2 + x2 = aux + end + if haskey(model.quadratic_objective_cache_product, (x1, x2)) + return model.quadratic_objective_cache_product[(x1, x2)] + else + throw( + ErrorException( + "Parameter not set in product of variables ($x1,$x2)", + ), + ) + end +end + +# +# Copy +# + +function MOI.Utilities.default_copy_to( + dest::MOI.Bridges.LazyBridgeOptimizer{Optimizer{T,OT}}, + src::MOI.ModelLike, +) where {T,OT} + return _poi_default_copy_to(dest, src) +end + +function MOI.Utilities.default_copy_to( + dest::Optimizer{T,OT}, + src::MOI.ModelLike, +) where {T,OT} + return _poi_default_copy_to(dest, src) +end + +function _poi_default_copy_to(dest::T, src::MOI.ModelLike) where {T} + if !MOI.supports_incremental_interface(dest) + error("Model $(typeof(dest)) does not support copy_to.") + end + MOI.empty!(dest) + vis_src = MOI.get(src, MOI.ListOfVariableIndices()) + index_map = MOI.IndexMap() + # The `NLPBlock` assumes that the order of variables does not change (#849) + # Therefore, all VariableIndex and VectorOfVariable constraints are added + # seprately, and no variables constrained-on-creation are added. + + # This is not valid for NLPs with Parameters, they should enter + has_nlp = MOI.NLPBlock() in MOI.get(src, MOI.ListOfModelAttributesSet()) + constraints_not_added = if has_nlp + vcat( + Any[ + MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) for + (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) if + MOI.Utilities._is_variable_function(F) && + S != MOI.Parameter{Float64} + ], + Any[MOI.Utilities._try_constrain_variables_on_creation( + dest, + src, + index_map, + MOI.Parameter{Float64}, + )], + ) + else + Any[ + MOI.Utilities._try_constrain_variables_on_creation( + dest, + src, + index_map, + S, + ) for S in MOI.Utilities.sorted_variable_sets_by_cost(dest, src) + ] + end + MOI.Utilities._copy_free_variables(dest, index_map, vis_src) + # Copy variable attributes + MOI.Utilities.pass_attributes(dest, src, index_map, vis_src) + # Copy model attributes + MOI.Utilities.pass_attributes(dest, src, index_map) + # Copy constraints + MOI.Utilities._pass_constraints(dest, src, index_map, constraints_not_added) + MOI.Utilities.final_touch(dest, index_map) + return index_map +end + +# +# Optimize +# + +function MOI.optimize!(model::Optimizer) + if !isempty(model.updated_parameters) + update_parameters!(model) + end + if ( + !isempty(model.quadratic_objective_cache_product) || + model.quadratic_objective_cache_product_changed + ) + model.quadratic_objective_cache_product_changed = false + _set_quadratic_product_in_obj!(model) + end + MOI.optimize!(model.optimizer) + if MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION && + model.evaluate_duals + @warn "Dual solution not available, ignoring `evaluate_duals`" + elseif model.evaluate_duals + _compute_dual_of_parameters!(model) + end + return +end diff --git a/src/ParametricOptInterface.jl b/src/ParametricOptInterface.jl index cf0aacf5..951ece7e 100644 --- a/src/ParametricOptInterface.jl +++ b/src/ParametricOptInterface.jl @@ -9,42 +9,56 @@ using MathOptInterface const MOI = MathOptInterface -const PARAMETER_INDEX_THRESHOLD = Int64(4_611_686_018_427_387_904) # div(typemax(Int64),2)+1 - @enum ConstraintsInterpretationCode ONLY_CONSTRAINTS ONLY_BOUNDS BOUNDS_AND_CONSTRAINTS +# +# Parameter Index +# + const SIMPLE_SCALAR_SETS{T} = Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}} -# Utilities for using a CleverDict in Parameters +const PARAMETER_INDEX_THRESHOLD = Int64(4_611_686_018_427_387_904) # div(typemax(Int64),2)+1 + struct ParameterIndex index::Int64 end -function MOI.Utilities.CleverDicts.index_to_key( - ::Type{ParameterIndex}, - index::Int64, -) - return ParameterIndex(index) -end -function MOI.Utilities.CleverDicts.key_to_index(key::ParameterIndex) - return key.index -end + function p_idx(vi::MOI.VariableIndex)::ParameterIndex return ParameterIndex(vi.value - PARAMETER_INDEX_THRESHOLD) end + function p_val(vi::MOI.VariableIndex)::Int64 return vi.value - PARAMETER_INDEX_THRESHOLD end + function p_val(ci::MOI.ConstraintIndex)::Int64 return ci.value - PARAMETER_INDEX_THRESHOLD end +# +# MOI Special structure helpers +# + +# Utilities for using a CleverDict in Parameters +function MOI.Utilities.CleverDicts.index_to_key( + ::Type{ParameterIndex}, + index::Int64, +) + return ParameterIndex(index) +end + +function MOI.Utilities.CleverDicts.key_to_index(key::ParameterIndex) + return key.index +end + const ParamTo{T} = MOI.Utilities.CleverDicts.CleverDict{ ParameterIndex, T, typeof(MOI.Utilities.CleverDicts.key_to_index), typeof(MOI.Utilities.CleverDicts.index_to_key), } + const VariableMap = MOI.Utilities.CleverDicts.CleverDict{ MOI.VariableIndex, MOI.VariableIndex, @@ -55,57 +69,11 @@ const VariableMap = MOI.Utilities.CleverDicts.CleverDict{ const DoubleDict{T} = MOI.Utilities.DoubleDicts.DoubleDict{T} const DoubleDictInner{F,S,T} = MOI.Utilities.DoubleDicts.DoubleDictInner{F,S,T} -mutable struct ParametricQuadraticFunction{T} - # helper to efficiently update affine terms - affine_data::Dict{MOI.VariableIndex,T} - affine_data_np::Dict{MOI.VariableIndex,T} - # constant * parameter * variable (in this order) - pv::Vector{MOI.ScalarQuadraticTerm{T}} - # constant * parameter * parameter - pp::Vector{MOI.ScalarQuadraticTerm{T}} - # constant * variable * variable - vv::Vector{MOI.ScalarQuadraticTerm{T}} - # constant * parameter - p::Vector{MOI.ScalarAffineTerm{T}} - # constant * variable - v::Vector{MOI.ScalarAffineTerm{T}} - # constant (does not include the set constant) - c::T - # to avoid unnecessary lookups in updates - set_constant::T - # cache data that is inside the solver to avoid slow getters - current_terms_with_p::Dict{MOI.VariableIndex,T} - current_constant::T - # computed on runtime - # updated_terms_with_p::Dict{MOI.VariableIndex,T} - # updated_constant::T -end - -mutable struct ParametricAffineFunction{T} - # constant * parameter - p::Vector{MOI.ScalarAffineTerm{T}} - # constant * variable - v::Vector{MOI.ScalarAffineTerm{T}} - # constant - c::T - # to avoid unnecessary lookups in updates - set_constant::T - # cache to avoid slow getters - current_constant::T -end +# +# parametric functions +# -mutable struct ParametricVectorAffineFunction{T} - # constant * parameter - p::Vector{MOI.VectorAffineTerm{T}} - # constant * variable - v::Vector{MOI.VectorAffineTerm{T}} - # constant - c::Vector{T} - # to avoid unnecessary lookups in updates - set_constant::Vector{T} - # cache to avoid slow getters - current_constant::Vector{T} -end +include("parametric_functions.jl") """ Optimizer{T, OT <: MOI.ModelLike} <: MOI.AbstractOptimizer @@ -255,1385 +223,30 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer end end -include("utils.jl") -include("duals.jl") -include("update_parameters.jl") - -function MOI.is_empty(model::Optimizer) - return MOI.is_empty(model.optimizer) && - isempty(model.parameters) && - isempty(model.parameters_name) && - isempty(model.updated_parameters) && - isempty(model.variables) && - model.last_variable_index_added == 0 && - model.last_parameter_index_added == PARAMETER_INDEX_THRESHOLD && - isempty(model.constraint_outer_to_inner) && - # affine ctr - model.last_affine_added == 0 && - isempty(model.affine_outer_to_inner) && - isempty(model.affine_constraint_cache) && - isempty(model.affine_constraint_cache_set) && - # quad ctr - model.last_quad_add_added == 0 && - isempty(model.quadratic_outer_to_inner) && - isempty(model.quadratic_constraint_cache) && - isempty(model.quadratic_constraint_cache_set) && - # obj - model.affine_objective_cache === nothing && - model.quadratic_objective_cache === nothing && - MOI.is_empty(model.original_objective_cache) && - isempty(model.quadratic_objective_cache_product) && - # - isempty(model.vector_affine_constraint_cache) && - # - isempty(model.multiplicative_parameters) && - isempty(model.dual_value_of_parameters) && - model.number_of_parameters_in_model == 0 -end - -function MOI.empty!(model::Optimizer{T}) where {T} - MOI.empty!(model.optimizer) - empty!(model.parameters) - empty!(model.parameters_name) - empty!(model.updated_parameters) - empty!(model.variables) - model.last_variable_index_added = 0 - model.last_parameter_index_added = PARAMETER_INDEX_THRESHOLD - empty!(model.constraint_outer_to_inner) - # affine ctr - model.last_affine_added = 0 - empty!(model.affine_outer_to_inner) - empty!(model.affine_constraint_cache) - empty!(model.affine_constraint_cache_set) - # quad ctr - model.last_quad_add_added = 0 - empty!(model.quadratic_outer_to_inner) - empty!(model.quadratic_constraint_cache) - empty!(model.quadratic_constraint_cache_set) - # obj - model.affine_objective_cache = nothing - model.quadratic_objective_cache = nothing - MOI.empty!(model.original_objective_cache) - empty!(model.quadratic_objective_cache_product) - # - empty!(model.vector_affine_constraint_cache) - # - empty!(model.multiplicative_parameters) - empty!(model.dual_value_of_parameters) - # - model.number_of_parameters_in_model = 0 - return -end - -function MOI.supports_constraint( - model::Optimizer, - F::Union{ - Type{MOI.VariableIndex}, - Type{MOI.ScalarAffineFunction{T}}, - Type{MOI.VectorOfVariables}, - Type{MOI.VectorAffineFunction{T}}, - }, - S::Type{<:MOI.AbstractSet}, -) where {T} - return MOI.supports_constraint(model.optimizer, F, S) -end - -function MOI.supports_constraint( - model::Optimizer, - ::Type{MOI.ScalarQuadraticFunction{T}}, - S::Type{<:MOI.AbstractSet}, -) where {T} - return MOI.supports_constraint( - model.optimizer, - MOI.ScalarAffineFunction{T}, - S, - ) -end - -function MOI.supports_constraint( - model::Optimizer, - ::Type{MOI.VectorQuadraticFunction{T}}, - S::Type{<:MOI.AbstractSet}, -) where {T} - return MOI.supports_constraint( - model.optimizer, - MOI.VectorAffineFunction{T}, - S, - ) -end - -function MOI.supports( - model::Optimizer, - attr::Union{ - MOI.ObjectiveSense, - MOI.ObjectiveFunction{MOI.VariableIndex}, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}, - }, -) where {T} - return MOI.supports(model.optimizer, attr) -end - -function MOI.supports( - model::Optimizer, - ::MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}, -) where {T} - return MOI.supports( - model.optimizer, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}(), - ) -end - -function MOI.supports(model::Optimizer, ::MOI.NLPBlock) - return MOI.supports(model.optimizer, MOI.NLPBlock()) -end - -function MOI.set(model::Optimizer, ::MOI.NLPBlock, nlp_data::MOI.NLPBlockData) - return MOI.set(model.optimizer, MOI.NLPBlock(), nlp_data) -end - -function MOI.supports_incremental_interface(model::Optimizer) - return MOI.supports_incremental_interface(model.optimizer) -end -function MOI.supports(model::Optimizer, ::MOI.Name) - return MOI.supports(model.optimizer, MOI.Name()) -end -function MOI.get(model::Optimizer, ::MOI.ListOfModelAttributesSet) - return MOI.get(model.optimizer, MOI.ListOfModelAttributesSet()) -end -MOI.get(model::Optimizer, ::MOI.Name) = MOI.get(model.optimizer, MOI.Name()) -function MOI.set(model::Optimizer, ::MOI.Name, name::String) - return MOI.set(model.optimizer, MOI.Name(), name) -end -function MOI.get(model::Optimizer, ::MOI.ListOfVariableIndices) - return MOI.get(model.optimizer, MOI.ListOfVariableIndices()) -end - -struct ListOfPureVariableIndices <: MOI.AbstractModelAttribute end -struct ListOfParameterIndices <: MOI.AbstractModelAttribute end - -function MOI.get(model::Optimizer, ::ListOfPureVariableIndices) - return collect(keys(model.variables)) -end -function MOI.get(model::Optimizer, ::ListOfParameterIndices) - return collect(keys(model.parameters)) -end - -function MOI.get(model::Optimizer, ::MOI.ListOfVariableAttributesSet) - return MOI.get(model.optimizer, MOI.ListOfVariableAttributesSet()) -end - -function MOI.get( - model::Optimizer, - ::MOI.ListOfConstraintAttributesSet{F,S}, -) where {F,S} - if F === MOI.ScalarQuadraticFunction - error( - "MOI.ListOfConstraintAttributesSet is not implemented for ScalarQuadraticFunction.", - ) - end - return MOI.get(model.optimizer, MOI.ListOfConstraintAttributesSet{F,S}()) -end - -function MOI.set( - model::Optimizer, - ::MOI.ConstraintFunction, - c::MOI.ConstraintIndex{F,S}, - f::F, -) where {F,S} - MOI.set(model.optimizer, MOI.ConstraintFunction(), c, f) - return -end - -function MOI.set( - model::Optimizer, - ::MOI.ConstraintSet, - c::MOI.ConstraintIndex{F,S}, - s::S, -) where {F,S} - MOI.set(model.optimizer, MOI.ConstraintSet(), c, s) - return -end - -function MOI.modify( - model::Optimizer, - c::MOI.ConstraintIndex{F,S}, - chg::MOI.ScalarCoefficientChange{T}, -) where {F,S,T} - if haskey(model.quadratic_constraint_cache, c) || - haskey(model.affine_constraint_cache, c) - error("Parametric constraint cannot be modified") - end - MOI.modify(model.optimizer, c, chg) - return -end - -function MOI.modify( - model::Optimizer, - c::MOI.ObjectiveFunction{F}, - chg::Union{MOI.ScalarConstantChange{T},MOI.ScalarCoefficientChange{T}}, -) where {F<:MathOptInterface.AbstractScalarFunction,T} - if model.quadratic_objective_cache !== nothing || - model.affine_objective_cache !== nothing || - !isempty(model.quadratic_objective_cache_product) - error("Parametric objective cannot be modified") - end - MOI.modify(model.optimizer, c, chg) - MOI.modify(model.original_objective_cache, c, chg) - return -end - -function MOI.set( - model::Optimizer, - attr::MOI.VariableName, - v::MOI.VariableIndex, - name::String, -) - if is_parameter_in_model(model, v) - model.parameters_name[v] = name - else - MOI.set(model.optimizer, attr, v, name) - end - return -end - -function MOI.get(model::Optimizer, attr::MOI.VariableName, v::MOI.VariableIndex) - if is_parameter_in_model(model, v) - return get(model.parameters_name, v, "") - else - return MOI.get(model.optimizer, attr, v) - end -end - -function MOI.get(model::Optimizer, tp::Type{MOI.VariableIndex}, attr::String) - return MOI.get(model.optimizer, tp, attr) -end - -function MOI.get(model::Optimizer, attr::MOI.ObjectiveBound) - return MOI.get(model.optimizer, attr) -end - -function MOI.supports( - model::Optimizer, - attr::MOI.VariableName, - tp::Type{MOI.VariableIndex}, -) - return MOI.supports(model.optimizer, attr, tp) -end - -function MOI.set( - model::Optimizer, - attr::MOI.ConstraintName, - c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},S}, - name::String, -) where {T,S<:MOI.AbstractSet} - if haskey(model.quadratic_outer_to_inner, c) - MOI.set(model.optimizer, attr, model.quadratic_outer_to_inner[c], name) - else - MOI.set(model.optimizer, attr, c, name) - end - return -end - -function MOI.set( - model::Optimizer, - attr::MOI.ConstraintName, - c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}, - name::String, -) where {T,S<:MOI.AbstractSet} - if haskey(model.affine_outer_to_inner, c) - MOI.set(model.optimizer, attr, model.affine_outer_to_inner[c], name) - else - MOI.set(model.optimizer, attr, c, name) - end - return -end - -function MOI.set( - model::Optimizer, - attr::MOI.ConstraintName, - c::MOI.ConstraintIndex, - name::String, -) - MOI.set(model.optimizer, attr, c, name) - return -end - -function MOI.get( - model::Optimizer, - attr::MOI.ConstraintName, - c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},S}, -) where {T,S<:MOI.AbstractSet} - if haskey(model.quadratic_outer_to_inner, c) - return MOI.get(model.optimizer, attr, model.quadratic_outer_to_inner[c]) - else - return MOI.get(model.optimizer, attr, c) - end -end - -function MOI.get( - model::Optimizer, - attr::MOI.ConstraintName, - c::MOI.ConstraintIndex, -) - return MOI.get(model.optimizer, attr, c) -end - -function MOI.get( - model::Optimizer, - attr::MOI.ConstraintName, - c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}, -) where {T,S} - if haskey(model.affine_outer_to_inner, c) - inner_ci = model.affine_outer_to_inner[c] - # This SAF constraint was transformed into variable bound - if typeof(inner_ci) === MOI.ConstraintIndex{MOI.VariableIndex,S} - v = MOI.get(model.optimizer, MOI.ConstraintFunction(), inner_ci) - variable_name = MOI.get(model.optimizer, MOI.VariableName(), v) - return "ParametricBound_$(S)_$(variable_name)" - end - return MOI.get(model.optimizer, attr, inner_ci) - else - return MOI.get(model.optimizer, attr, c) - end -end - -function MOI.get(model::Optimizer, ::MOI.NumberOfVariables) - return length(model.parameters) + length(model.variables) -end - -function MOI.supports( - model::Optimizer, - attr::MOI.ConstraintName, - tp::Type{<:MOI.ConstraintIndex}, -) - return MOI.supports(model.optimizer, attr, tp) -end - -function MOI.get( - model::Optimizer, - attr::MOI.ConstraintFunction, - ci::MOI.ConstraintIndex{F,S}, -) where {F,S} - if haskey(model.quadratic_outer_to_inner, ci) - inner_ci = model.quadratic_outer_to_inner[ci] - return original_function(model.quadratic_constraint_cache[inner_ci]) - elseif haskey(model.affine_outer_to_inner, ci) - inner_ci = model.affine_outer_to_inner[ci] - return original_function(model.affine_constraint_cache[inner_ci]) - else - MOI.throw_if_not_valid(model, ci) - return MOI.get(model.optimizer, attr, ci) - end -end -function MOI.get( - model::Optimizer{T}, - ::MOI.ConstraintFunction, - cp::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}, -) where {T} - p = MOI.VariableIndex(cp.value) - if !is_parameter_in_model(model, p) - error("Parameter not in the model") - end - return p -end - -function MOI.get( - model::Optimizer, - tp::Type{MOI.ConstraintIndex{F,S}}, - attr::String, -) where {F,S} - return MOI.get(model.optimizer, tp, attr) -end - -function MOI.get(model::Optimizer, tp::Type{MOI.ConstraintIndex}, attr::String) - return MOI.get(model.optimizer, tp, attr) -end - -function MOI.is_valid(model::Optimizer, vi::MOI.VariableIndex) - return MOI.is_valid(model.optimizer, vi) -end - -function MOI.supports(model::Optimizer, ::MOI.NumberOfThreads) - return MOI.supports(model.optimizer, MOI.NumberOfThreads()) -end - -function MOI.supports(model::Optimizer, ::MOI.TimeLimitSec) - return MOI.supports(model.optimizer, MOI.TimeLimitSec()) -end - -function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, value) - MOI.set(model.optimizer, MOI.TimeLimitSec(), value) - return +function _next_variable_index!(model::Optimizer) + return model.last_variable_index_added += 1 end -function MOI.get(model::Optimizer, ::MOI.TimeLimitSec) - return MOI.get(model.optimizer, MOI.TimeLimitSec()) +function _next_parameter_index!(model::Optimizer) + return model.last_parameter_index_added += 1 end -function MOI.get(model::Optimizer, ::MOI.SolveTimeSec) - return MOI.get(model.optimizer, MOI.SolveTimeSec()) +function _update_number_of_parameters!(model::Optimizer) + return model.number_of_parameters_in_model += 1 end -function MOI.supports(model::Optimizer, ::MOI.Silent) - return MOI.supports(model.optimizer, MOI.Silent()) -end - -function MOI.set(model::Optimizer, ::MOI.Silent, value::Bool) - MOI.set(model.optimizer, MOI.Silent(), value) - return -end - -MOI.get(model::Optimizer, ::MOI.Silent) = MOI.get(model.optimizer, MOI.Silent()) - -function MOI.get(model::Optimizer, ::MOI.RawStatusString) - return MOI.get(model.optimizer, MOI.RawStatusString()) -end - -function MOI.supports(model::Optimizer, attr::MOI.AbstractOptimizerAttribute) - return MOI.supports(model.optimizer, attr) -end - -function MOI.get(model::Optimizer, attr::MOI.AbstractOptimizerAttribute) - return MOI.get(model.optimizer, attr) -end - -function MOI.set(model::Optimizer, attr::MOI.AbstractOptimizerAttribute, value) - MOI.set(model.optimizer, attr, value) - return -end - -function MOI.get( - model::Optimizer, - attr::MOI.ConstraintSet, - ci::MOI.ConstraintIndex{F,S}, -) where {F,S} - if haskey(model.quadratic_outer_to_inner, ci) - inner_ci = model.quadratic_outer_to_inner[ci] - return model.quadratic_constraint_cache_set[inner_ci] - elseif haskey(model.affine_outer_to_inner, ci) - inner_ci = model.affine_outer_to_inner[ci] - return model.affine_constraint_cache_set[inner_ci] - else - MOI.throw_if_not_valid(model, ci) - return MOI.get(model.optimizer, attr, ci) - end -end - -function MOI.get(model::Optimizer, attr::MOI.ObjectiveSense) - return MOI.get(model.optimizer, attr) -end - -function MOI.get(model::Optimizer{T}, attr::MOI.ObjectiveFunctionType) where {T} - return MOI.get(model.original_objective_cache, attr) -end - -function MOI.get(model::Optimizer, attr::MOI.ObjectiveFunction) - return MOI.get(model.original_objective_cache, attr) -end - -function MOI.get(model::Optimizer, attr::MOI.ResultCount) - return MOI.get(model.optimizer, attr) -end - -function MOI.get(model::Optimizer, ::MOI.ListOfConstraintTypesPresent) - constraint_types = MOI.Utilities.DoubleDicts.nonempty_outer_keys( - model.constraint_outer_to_inner, - ) - return collect(constraint_types) -end - -function MOI.get( - model::Optimizer, - ::MOI.ListOfConstraintIndices{F,S}, -) where {S,F} - list = collect(values(model.constraint_outer_to_inner[F, S])) - sort!(list, lt = (x, y) -> (x.value < y.value)) - return list -end - -function MOI.get(model::Optimizer, ::MOI.NumberOfConstraints{F,S}) where {S,F} - return length(model.constraint_outer_to_inner[F, S]) -end - -function MOI.supports_add_constrained_variable( - ::Optimizer{T}, - ::Type{MOI.Parameter{T}}, -) where {T} - return true -end - -function MOI.supports_add_constrained_variables( - model::Optimizer, - ::Type{MOI.Reals}, -) - return MOI.supports_add_constrained_variables(model.optimizer, MOI.Reals) -end - -function MOI.add_variable(model::Optimizer) - next_variable_index!(model) - return MOI.Utilities.CleverDicts.add_item( - model.variables, - MOI.add_variable(model.optimizer), - ) -end - -function MOI.add_constrained_variable( - model::Optimizer{T}, - set::MOI.Parameter{T}, -) where {T} - next_parameter_index!(model) - p = MOI.VariableIndex(model.last_parameter_index_added) - MOI.Utilities.CleverDicts.add_item(model.parameters, set.value) - cp = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}( - model.last_parameter_index_added, - ) - _add_to_constraint_map!(model, cp) - MOI.Utilities.CleverDicts.add_item(model.updated_parameters, NaN) - update_number_of_parameters!(model) - return p, cp -end - -function _add_to_constraint_map!(model::Optimizer, ci) - model.constraint_outer_to_inner[ci] = ci - return -end -function _add_to_constraint_map!(model::Optimizer, ci_in, ci_out) - model.constraint_outer_to_inner[ci_out] = ci_in - return -end -function _add_constraint_direct_and_cache_map!(model::Optimizer, f, set) - ci = MOI.add_constraint(model.optimizer, f, set) - _add_to_constraint_map!(model, ci) - return ci -end - -function MOI.add_constraint( - model::Optimizer, - f::MOI.VariableIndex, - set::MOI.AbstractScalarSet, -) - if !is_variable(f) - error("Cannot constrain a parameter") - elseif !is_variable_in_model(model, f) - error("Variable not in the model") - end - return _add_constraint_direct_and_cache_map!(model, f, set) -end - -function add_constraint_with_parameters_on_function( - model::Optimizer, - f::MOI.ScalarAffineFunction{T}, - set::S, -) where {T,S} - pf = ParametricAffineFunction(f) - cache_set_constant!(pf, set) - if model.constraints_interpretation == ONLY_BOUNDS - if length(pf.v) == 1 && isone(MOI.coefficient(pf.v[])) - poi_ci = add_vi_constraint(model, pf, set) - else - error( - "It was not possible to interpret this constraint as a variable bound.", - ) - end - elseif model.constraints_interpretation == ONLY_CONSTRAINTS - poi_ci = add_saf_constraint(model, pf, set) - elseif model.constraints_interpretation == BOUNDS_AND_CONSTRAINTS - if length(pf.v) == 1 && isone(MOI.coefficient(pf.v[])) - poi_ci = add_vi_constraint(model, pf, set) - else - poi_ci = add_saf_constraint(model, pf, set) - end - end - return poi_ci +function _parameter_in_model(model::Optimizer, v::MOI.VariableIndex) + return PARAMETER_INDEX_THRESHOLD < + v.value <= + model.last_parameter_index_added end -function add_saf_constraint( - model::Optimizer, - pf::ParametricAffineFunction{T}, - set::S, -) where {T,S} - update_cache!(pf, model) - inner_ci = MOI.Utilities.normalize_and_add_constraint( - model.optimizer, - MOI.ScalarAffineFunction{T}(pf.v, 0.0), - set_with_new_constant(set, pf.current_constant), - ) - model.last_affine_added += 1 - outer_ci = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}( - model.last_affine_added, - ) - model.affine_outer_to_inner[outer_ci] = inner_ci - model.constraint_outer_to_inner[outer_ci] = inner_ci - model.affine_constraint_cache[inner_ci] = pf - model.affine_constraint_cache_set[inner_ci] = set - return outer_ci +function _variable_in_model(model::Optimizer, v::MOI.VariableIndex) + return 0 < v.value <= model.last_variable_index_added end -function add_vi_constraint( - model::Optimizer, - pf::ParametricAffineFunction{T}, - set::S, -) where {T,S} - update_cache!(pf, model) - inner_ci = MOI.Utilities.normalize_and_add_constraint( - model.optimizer, - pf.v[].variable, - set_with_new_constant(set, pf.current_constant), - ) - model.last_affine_added += 1 - outer_ci = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}( - model.last_affine_added, - ) - model.affine_outer_to_inner[outer_ci] = inner_ci - model.constraint_outer_to_inner[outer_ci] = inner_ci - model.affine_constraint_cache[inner_ci] = pf - model.affine_constraint_cache_set[inner_ci] = set - return outer_ci -end - -function MOI.add_constraint( - model::Optimizer, - f::MOI.ScalarAffineFunction{T}, - set::MOI.AbstractScalarSet, -) where {T} - if !function_has_parameters(f) - return _add_constraint_direct_and_cache_map!(model, f, set) - else - return add_constraint_with_parameters_on_function(model, f, set) - end -end - -function MOI.get( - model::Optimizer, - attr::MOI.VariablePrimal, - v::MOI.VariableIndex, -) - if is_parameter_in_model(model, v) - return model.parameters[p_idx(v)] - elseif is_variable_in_model(model, v) - return MOI.get(model.optimizer, attr, model.variables[v]) - else - error("Variable not in the model") - end -end - -function MOI.supports( - model::Optimizer, - attr::MOI.AbstractVariableAttribute, - tp::Type{MOI.VariableIndex}, -) - return MOI.supports(model.optimizer, attr, tp) -end - -function MOI.set( - model::Optimizer, - attr::MOI.AbstractVariableAttribute, - v::MOI.VariableIndex, - val, -) - if is_variable_in_model(model, v) - MOI.set(model.optimizer, attr, v, val) - else - error("$attr is not supported for parameters") - end -end - -function MOI.get( - model::Optimizer, - attr::MOI.AbstractVariableAttribute, - v::MOI.VariableIndex, -) - if is_variable_in_model(model, v) - return MOI.get(model.optimizer, attr, model.variables[v]) - else - error("$attr is not supported for parameters") - end -end - -function MOI.set( - model::Optimizer{T}, - ::MOI.ConstraintSet, - cp::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}, - set::MOI.Parameter{T}, -) where {T} - p = MOI.VariableIndex(cp.value) - if !is_parameter_in_model(model, p) - error("Parameter not in the model") - end - return model.updated_parameters[p_idx(p)] = set.value -end - -function MOI.get( - model::Optimizer{T}, - ::MOI.ConstraintSet, - cp::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}, -) where {T} - p = MOI.VariableIndex(cp.value) - if !is_parameter_in_model(model, p) - error("Parameter not in the model") - end - val = model.updated_parameters[p_idx(p)] - if isnan(val) - return MOI.Parameter{T}(model.parameters[p_idx(p)]) - end - return MOI.Parameter{T}(val) -end - -""" - ParameterValue <: MOI.AbstractVariableAttribute - -Attribute defined to set and get parameter values - -# Example - -```julia -MOI.set(model, POI.ParameterValue(), p, 2.0) -MOI.get(model, POI.ParameterValue(), p) -``` -""" -struct ParameterValue <: MOI.AbstractVariableAttribute end - -# We need a CachingOptimizer fallback to -# get ParameterValue working correctly on JuMP -# TODO: Think of a better solution for this - -function MOI.set( - opt::MOI.Utilities.CachingOptimizer, - ::ParameterValue, - var::MOI.VariableIndex, - val::Float64, -) - ci = - MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{Float64}}(var.value) - set = MOI.set(opt, MOI.ConstraintSet(), ci, MOI.Parameter(val)) - return nothing -end - -function MOI.set( - model::Optimizer, - ::ParameterValue, - var::MOI.VariableIndex, - val::Float64, -) - ci = - MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{Float64}}(var.value) - set = MOI.set(model, MOI.ConstraintSet(), ci, MOI.Parameter(val)) - return nothing -end - -function MOI.set( - opt::MOI.Utilities.CachingOptimizer, - ::ParameterValue, - vi::MOI.VariableIndex, - val::Real, -) - return MOI.set(opt, ParameterValue(), vi, convert(Float64, val)) -end - -function MOI.set( - model::Optimizer, - ::ParameterValue, - vi::MOI.VariableIndex, - val::Real, -) - return MOI.set(model, ParameterValue(), vi, convert(Float64, val)) -end - -function MOI.get( - opt::MOI.Utilities.CachingOptimizer, - ::ParameterValue, - var::MOI.VariableIndex, -) - ci = - MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{Float64}}(var.value) - set = MOI.get(opt, MOI.ConstraintSet(), ci) - return set.value -end - -function MOI.get(model::Optimizer, ::ParameterValue, var::MOI.VariableIndex) - return model.parameters[p_idx(var)] -end - -""" - ConstraintsInterpretation <: MOI.AbstractOptimizerAttribute - -Attribute to define how [`POI.Optimizer`](@ref) should interpret constraints. - -- `POI.ONLY_CONSTRAINTS`: Only interpret `ScalarAffineFunction` constraints as linear constraints - If an expression such as `x >= p1 + p2` appears it will be trated like a new constraint. - **This is the default behaviour of [`POI.Optimizer`](@ref)** - -- `POI.ONLY_BOUNDS`: Only interpret `ScalarAffineFunction` constraints as a variable bound. - This is valid for constraints such as `x >= p` or `x >= p1 + p2`. If a constraint `x1 + x2 >= p` appears, - which is not a valid variable bound it will throw an error. - -- `POI.BOUNDS_AND_CONSTRAINTS`: Interpret `ScalarAffineFunction` constraints as a variable bound if they - are a valid variable bound, i.e., `x >= p` or `x >= p1 + p2` and interpret them as linear constraints - otherwise. - -# Example - -```julia -MOI.set(model, POI.InterpretConstraintsAsBounds(), POI.ONLY_BOUNDS) -MOI.set(model, POI.InterpretConstraintsAsBounds(), POI.ONLY_CONSTRAINTS) -MOI.set(model, POI.InterpretConstraintsAsBounds(), POI.BOUNDS_AND_CONSTRAINTS) -``` -""" -struct ConstraintsInterpretation <: MOI.AbstractOptimizerAttribute end - -function MOI.set( - model::Optimizer, - ::ConstraintsInterpretation, - value::ConstraintsInterpretationCode, -) - return model.constraints_interpretation = value -end - -function empty_objective_function_caches!(model::Optimizer{T}) where {T} - model.affine_objective_cache = nothing - model.quadratic_objective_cache = nothing - model.original_objective_cache = MOI.Utilities.ObjectiveContainer{T}() - return -end - -function MOI.set( - model::Optimizer, - attr::MOI.ObjectiveFunction, - f::MOI.ScalarAffineFunction{T}, -) where {T} - # clear previously defined objetive function cache - empty_objective_function_caches!(model) - if !function_has_parameters(f) - MOI.set(model.optimizer, attr, f) - else - pf = ParametricAffineFunction(f) - update_cache!(pf, model) - MOI.set(model.optimizer, attr, current_function(pf)) - model.affine_objective_cache = pf - end - MOI.set(model.original_objective_cache, attr, f) - return -end - -function MOI.set( - model::Optimizer, - attr::MOI.ObjectiveFunction{F}, - f::F, -) where {F<:MOI.ScalarQuadraticFunction{T}} where {T} - # clear previously defined objetive function cache - empty_objective_function_caches!(model) - if !function_has_parameters(f) - MOI.set(model.optimizer, attr, f) - else - pf = ParametricQuadraticFunction(f) - cache_multiplicative_params!(model, pf) - update_cache!(pf, model) - func = current_function(pf) - MOI.set( - model.optimizer, - MOI.ObjectiveFunction{( - is_affine(func) ? MOI.ScalarAffineFunction{T} : - MOI.ScalarQuadraticFunction{T} - )}(), - # func, - ( - is_affine(func) ? - MOI.ScalarAffineFunction(func.affine_terms, func.constant) : - func - ), - ) - model.quadratic_objective_cache = pf - end - MOI.set(model.original_objective_cache, attr, f) - return -end - -function MOI.set( - model::Optimizer, - attr::MOI.ObjectiveFunction, - v::MOI.VariableIndex, -) - if is_parameter(v) - error("Cannot use a parameter as objective function alone") - elseif !is_variable_in_model(model, v) - error("Variable not in the model") - end - MOI.set(model.optimizer, attr, model.variables[v]) - MOI.set(model.original_objective_cache, attr, v) - return -end - -function MOI.set( - model::Optimizer, - attr::MOI.ObjectiveSense, - sense::MOI.OptimizationSense, -) - MOI.set(model.optimizer, attr, sense) - return -end - -function MOI.get( - model::Optimizer, - attr::T, -) where { - T<:Union{ - MOI.TerminationStatus, - MOI.ObjectiveValue, - MOI.DualObjectiveValue, - MOI.PrimalStatus, - MOI.DualStatus, - }, -} - return MOI.get(model.optimizer, attr) -end - -function MOI.get( - model::Optimizer, - attr::T, - c::MOI.ConstraintIndex, -) where { - T<:Union{MOI.ConstraintPrimal,MOI.ConstraintDual,MOI.ConstraintBasisStatus}, -} - return MOI.get(model.optimizer, attr, c) -end - -function MOI.get( - model::Optimizer, - attr::AT, - c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}, -) where { - AT<:Union{ - MOI.ConstraintPrimal, - MOI.ConstraintDual, - MOI.ConstraintBasisStatus, - }, - T, - S<:MOI.AbstractScalarSet, -} - moi_ci = get(model.affine_outer_to_inner, c, c) - return MOI.get(model.optimizer, attr, moi_ci) -end - -function MOI.set(model::Optimizer, attr::MOI.RawOptimizerAttribute, val::Any) - MOI.set(model.optimizer, attr, val) - return -end - -function MOI.get(model::Optimizer, ::MOI.SolverName) - name = MOI.get(model.optimizer, MOI.SolverName()) - return "Parametric Optimizer with $(name) attached" -end - -function MOI.get(model::Optimizer, ::MOI.SolverVersion) - return MOI.get(model.optimizer, MOI.SolverVersion()) -end - -function MOI.add_constraint( - model::Optimizer, - f::MOI.VectorOfVariables, - set::MOI.AbstractVectorSet, -) - if function_has_parameters(f) - error("VectorOfVariables does not allow parameters") - end - return _add_constraint_direct_and_cache_map!(model, f, set) -end - -function MOI.add_constraint( - model::Optimizer, - f::MOI.VectorAffineFunction{T}, - set::MOI.AbstractVectorSet, -) where {T} - if !function_has_parameters(f) - return _add_constraint_direct_and_cache_map!(model, f, set) - else - return add_constraint_with_parameters_on_function(model, f, set) - end -end - -function add_constraint_with_parameters_on_function( - model::Optimizer, - f::MOI.VectorAffineFunction{T}, - set::MOI.AbstractVectorSet, -) where {T} - pf = ParametricVectorAffineFunction(f) - # cache_set_constant!(pf, set) # there is no constant is vector sets - update_cache!(pf, model) - inner_ci = MOI.add_constraint(model.optimizer, current_function(pf), set) - model.vector_affine_constraint_cache[inner_ci] = pf - _add_to_constraint_map!(model, inner_ci) - return inner_ci -end - -function add_constraint_with_parameters_on_function( - model::Optimizer, - f::MOI.ScalarQuadraticFunction{T}, - s::S, -) where {T,S<:MOI.AbstractScalarSet} - pf = ParametricQuadraticFunction(f) - cache_multiplicative_params!(model, pf) - cache_set_constant!(pf, s) - update_cache!(pf, model) - - func = current_function(pf) - f_quad = if !is_affine(func) - fq = func - inner_ci = MOI.Utilities.normalize_and_add_constraint( - model.optimizer, - fq, - s, - ) - model.last_quad_add_added += 1 - outer_ci = MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},S}( - model.last_quad_add_added, - ) - model.quadratic_outer_to_inner[outer_ci] = inner_ci - model.constraint_outer_to_inner[outer_ci] = inner_ci - else - fa = MOI.ScalarAffineFunction(func.affine_terms, func.constant) - inner_ci = MOI.Utilities.normalize_and_add_constraint( - model.optimizer, - fa, - s, - ) - model.last_quad_add_added += 1 - outer_ci = MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},S}( - model.last_quad_add_added, - ) - # This part is used to remember that ci came from a quadratic function - # It is particularly useful because sometimes the constraint mutates - model.quadratic_outer_to_inner[outer_ci] = inner_ci - model.constraint_outer_to_inner[outer_ci] = inner_ci - end - model.quadratic_constraint_cache[inner_ci] = pf - model.quadratic_constraint_cache_set[inner_ci] = s - return outer_ci -end - -function MOI.add_constraint( - model::Optimizer, - f::MOI.ScalarQuadraticFunction{T}, - set::MOI.AbstractScalarSet, -) where {T} - if !function_has_parameters(f) - return _add_constraint_direct_and_cache_map!(model, f, set) - else - return add_constraint_with_parameters_on_function(model, f, set) - end -end - -function MOI.delete(model::Optimizer, v::MOI.VariableIndex) - delete!(model.variables, v) - MOI.delete(model.optimizer, v) - MOI.delete(model.original_objective_cache, v) - # TODO - what happens if the variable was in a SAF that was converted to bounds? - # solution: do not allow if that is the case (requires going trhought the scalar affine cache) - # TODO - deleting a variable also deletes constraints - for (F, S) in MOI.Utilities.DoubleDicts.nonempty_outer_keys( - model.constraint_outer_to_inner, - ) - _delete_variable_index_constraint( - model.constraint_outer_to_inner, - F, - S, - v.value, - ) - end - return -end - -function _delete_variable_index_constraint(d, F, S, v) - return -end -function _delete_variable_index_constraint( - d, - F::Type{MOI.VariableIndex}, - S, - value, -) - inner = d[F, S] - for k in keys(inner) - if k.value == value - delete!(inner, k) - end - end - return -end - -function MOI.delete( - model::Optimizer, - c::MOI.ConstraintIndex{F,S}, -) where {F<:MOI.ScalarQuadraticFunction,S<:MOI.AbstractSet} - if haskey(model.quadratic_outer_to_inner, c) - ci_inner = model.quadratic_outer_to_inner[c] - deleteat!(model.quadratic_outer_to_inner, c) - deleteat!(model.quadratic_constraint_cache, c) - deleteat!(model.quadratic_constraint_cache_set, c) - MOI.delete(model.optimizer, ci_inner) - else - MOI.delete(model.optimizer, c) - end - deleteat!(model.constraint_outer_to_inner, c) - return -end - -function MOI.delete( - model::Optimizer, - c::MOI.ConstraintIndex{F,S}, -) where {F<:MOI.ScalarAffineFunction,S<:MOI.AbstractSet} - if haskey(model.affine_outer_to_inner, c) - ci_inner = model.affine_outer_to_inner[c] - delete!(model.affine_outer_to_inner, c) - delete!(model.affine_constraint_cache, c) - delete!(model.affine_constraint_cache_set, c) - MOI.delete(model.optimizer, ci_inner) - else - MOI.delete(model.optimizer, c) - end - delete!(model.constraint_outer_to_inner, c) - return -end - -function MOI.delete( - model::Optimizer, - c::MOI.ConstraintIndex{F,S}, -) where {F<:Union{MOI.VariableIndex,MOI.VectorOfVariables},S<:MOI.AbstractSet} - MOI.delete(model.optimizer, c) - delete!(model.constraint_outer_to_inner, c) - return -end - -function MOI.delete( - model::Optimizer, - c::MOI.ConstraintIndex{F,S}, -) where {F<:MOI.VectorAffineFunction,S<:MOI.AbstractSet} - MOI.delete(model.optimizer, c) - delete!(model.constraint_outer_to_inner, c) - deleteat!(model.vector_affine_constraint_cache, c) - return -end - -function MOI.is_valid( - model::Optimizer, - c::MOI.ConstraintIndex{F,S}, -) where { - F<:Union{MOI.VariableIndex,MOI.VectorOfVariables,MOI.VectorAffineFunction}, - S<:MOI.AbstractSet, -} - return MOI.is_valid(model.optimizer, c) -end - -function MOI.is_valid( - model::Optimizer, - c::MOI.ConstraintIndex{F,S}, -) where {F<:MOI.ScalarAffineFunction,S<:MOI.AbstractSet} - return MOI.is_valid(model.optimizer, c) -end - -struct QuadraticObjectiveCoef <: MOI.AbstractModelAttribute end - -function _evaluate_parametric_expression(model::Optimizer, p::MOI.VariableIndex) - return model.parameters[p_idx(p)] -end - -function _evaluate_parametric_expression( - model::Optimizer, - fparam::MOI.ScalarAffineFunction{T}, -) where {T} - constant = fparam.constant - terms = fparam.terms - evaluated_parameter_expression = zero(T) - for term in terms - coef = term.coefficient - p = term.variable - evaluated_parameter_expression += coef * model.parameters[p_idx(p)] - evaluated_parameter_expression += constant - end - return evaluated_parameter_expression -end - -function set_quadratic_product_in_obj!(model::Optimizer{T}) where {T} - n = length(model.quadratic_objective_cache_product) - - f = if model.affine_objective_cache !== nothing - current_function(model.affine_objective_cache) - elseif model.quadratic_objective_cache !== nothing - current_function(model.quadratic_objective_cache) - else - F = MOI.get(model.original_objective_cache, MOI.ObjectiveFunctionType()) - MOI.get(model.original_objective_cache, MOI.ObjectiveFunction{F}()) - end - F = typeof(f) - - quadratic_prods_vector = MOI.ScalarQuadraticTerm{T}[] - sizehint!(quadratic_prods_vector, n) - - for ((x, y), fparam) in model.quadratic_objective_cache_product - # x, y = prod_var - evaluated_fparam = _evaluate_parametric_expression(model, fparam) - push!( - quadratic_prods_vector, - MOI.ScalarQuadraticTerm(evaluated_fparam, x, y), - ) - end - - f_new = if F <: MOI.VariableIndex - MOI.ScalarQuadraticFunction( - quadratic_prods_vector, - MOI.ScalarAffineTerm{T}[MOI.ScalarAffineTerm{T}(1.0, f)], - 0.0, - ) - elseif F <: MOI.ScalarAffineFunction{T} - MOI.ScalarQuadraticFunction(quadratic_prods_vector, f.terms, f.constant) - elseif F <: MOI.ScalarQuadraticFunction{T} - quadratic_terms = vcat(f.quadratic_terms, quadratic_prods_vector) - MOI.ScalarQuadraticFunction(quadratic_terms, f.affine_terms, f.constant) - end - - MOI.set( - model.optimizer, - MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}(), - f_new, - ) - - return -end - -function MOI.set( - model::Optimizer, - ::QuadraticObjectiveCoef, - (x1, x2)::Tuple{MOI.VariableIndex,MOI.VariableIndex}, - ::Nothing, -) - if x1.value > x2.value - aux = x1 - x1 = x2 - x2 = aux - end - delete!(model.quadratic_objective_cache_product, (x1, x2)) - model.quadratic_objective_cache_product_changed = true - return -end - -function MOI.set( - model::Optimizer, - ::QuadraticObjectiveCoef, - (x1, x2)::Tuple{MOI.VariableIndex,MOI.VariableIndex}, - f_param::Union{MOI.VariableIndex,MOI.ScalarAffineFunction{T}}, -) where {T} - if x1.value > x2.value - aux = x1 - x1 = x2 - x2 = aux - end - model.quadratic_objective_cache_product[(x1, x2)] = f_param - model.quadratic_objective_cache_product_changed = true - return -end - -function MOI.get( - model::Optimizer, - ::QuadraticObjectiveCoef, - (x1, x2)::Tuple{MOI.VariableIndex,MOI.VariableIndex}, -) - if x1.value > x2.value - aux = x1 - x1 = x2 - x2 = aux - end - if haskey(model.quadratic_objective_cache_product, (x1, x2)) - return model.quadratic_objective_cache_product[(x1, x2)] - else - throw( - ErrorException( - "Parameter not set in product of variables ($x1,$x2)", - ), - ) - end -end - -function _poi_default_copy_to(dest::T, src::MOI.ModelLike) where {T} - if !MOI.supports_incremental_interface(dest) - error("Model $(typeof(dest)) does not support copy_to.") - end - MOI.empty!(dest) - vis_src = MOI.get(src, MOI.ListOfVariableIndices()) - index_map = MOI.IndexMap() - # The `NLPBlock` assumes that the order of variables does not change (#849) - # Therefore, all VariableIndex and VectorOfVariable constraints are added - # seprately, and no variables constrained-on-creation are added. - - # This is not valid for NLPs with Parameters, they should enter - has_nlp = MOI.NLPBlock() in MOI.get(src, MOI.ListOfModelAttributesSet()) - constraints_not_added = if has_nlp - vcat( - Any[ - MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) for - (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) if - MOI.Utilities._is_variable_function(F) && - S != MOI.Parameter{Float64} - ], - Any[MOI.Utilities._try_constrain_variables_on_creation( - dest, - src, - index_map, - MOI.Parameter{Float64}, - )], - ) - else - Any[ - MOI.Utilities._try_constrain_variables_on_creation( - dest, - src, - index_map, - S, - ) for S in MOI.Utilities.sorted_variable_sets_by_cost(dest, src) - ] - end - MOI.Utilities._copy_free_variables(dest, index_map, vis_src) - # Copy variable attributes - MOI.Utilities.pass_attributes(dest, src, index_map, vis_src) - # Copy model attributes - MOI.Utilities.pass_attributes(dest, src, index_map) - # Copy constraints - MOI.Utilities._pass_constraints(dest, src, index_map, constraints_not_added) - MOI.Utilities.final_touch(dest, index_map) - return index_map -end - -function MOI.Utilities.default_copy_to( - dest::MOI.Bridges.LazyBridgeOptimizer{Optimizer{T,OT}}, - src::MOI.ModelLike, -) where {T,OT} - return _poi_default_copy_to(dest, src) -end - -function MOI.Utilities.default_copy_to( - dest::Optimizer{T,OT}, - src::MOI.ModelLike, -) where {T,OT} - return _poi_default_copy_to(dest, src) -end - -function MOI.optimize!(model::Optimizer) - if !isempty(model.updated_parameters) - update_parameters!(model) - end - if ( - !isempty(model.quadratic_objective_cache_product) || - model.quadratic_objective_cache_product_changed - ) - model.quadratic_objective_cache_product_changed = false - set_quadratic_product_in_obj!(model) - end - MOI.optimize!(model.optimizer) - if MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION && - model.evaluate_duals - @warn "Dual solution not available, ignoring `evaluate_duals`" - elseif model.evaluate_duals - compute_dual_of_parameters!(model) - end - return -end +include("duals.jl") +include("update_parameters.jl") +include("MOI_wrapper.jl") end # module diff --git a/src/duals.jl b/src/duals.jl index 0a74646f..ef632849 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -3,11 +3,11 @@ # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. -function compute_dual_of_parameters!(model::Optimizer{T}) where {T} +function _compute_dual_of_parameters!(model::Optimizer{T}) where {T} model.dual_value_of_parameters = zeros(T, model.number_of_parameters_in_model) - update_duals_from_affine_constraints!(model) - update_duals_from_vector_affine_constraints!(model) + _update_duals_from_affine_constraints!(model) + _update_duals_from_vector_affine_constraints!(model) update_duals_from_quadratic_constraints!(model) if model.affine_objective_cache !== nothing update_duals_from_objective!(model, model.affine_objective_cache) @@ -18,21 +18,21 @@ function compute_dual_of_parameters!(model::Optimizer{T}) where {T} return end -function update_duals_from_affine_constraints!(model::Optimizer) +function _update_duals_from_affine_constraints!(model::Optimizer) for (F, S) in keys(model.affine_constraint_cache.dict) affine_constraint_cache_inner = model.affine_constraint_cache[F, S] # barrier for type instability - compute_parameters_in_ci!(model, affine_constraint_cache_inner) + _compute_parameters_in_ci!(model, affine_constraint_cache_inner) end return end -function update_duals_from_vector_affine_constraints!(model::Optimizer) +function _update_duals_from_vector_affine_constraints!(model::Optimizer) for (F, S) in keys(model.vector_affine_constraint_cache.dict) vector_affine_constraint_cache_inner = model.vector_affine_constraint_cache[F, S] # barrier for type instability - compute_parameters_in_ci!(model, vector_affine_constraint_cache_inner) + _compute_parameters_in_ci!(model, vector_affine_constraint_cache_inner) end return end @@ -42,22 +42,22 @@ function update_duals_from_quadratic_constraints!(model::Optimizer) quadratic_constraint_cache_inner = model.quadratic_constraint_cache[F, S] # barrier for type instability - compute_parameters_in_ci!(model, quadratic_constraint_cache_inner) + _compute_parameters_in_ci!(model, quadratic_constraint_cache_inner) end return end -function compute_parameters_in_ci!( +function _compute_parameters_in_ci!( model::OT, constraint_cache_inner::DoubleDictInner{F,S,V}, ) where {OT,F,S,V} for (inner_ci, pf) in constraint_cache_inner - compute_parameters_in_ci!(model, pf, inner_ci) + _compute_parameters_in_ci!(model, pf, inner_ci) end return end -function compute_parameters_in_ci!( +function _compute_parameters_in_ci!( model::Optimizer{T}, pf, ci::MOI.ConstraintIndex{F,S}, @@ -70,7 +70,7 @@ function compute_parameters_in_ci!( return end -function compute_parameters_in_ci!( +function _compute_parameters_in_ci!( model::Optimizer{T}, pf::ParametricVectorAffineFunction{T}, ci::MOI.ConstraintIndex{F,S}, @@ -112,7 +112,7 @@ function MOI.get( ::ParameterDual, v::MOI.VariableIndex, ) where {T} - if !is_additive( + if !_is_additive( model, MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}(v.value), ) @@ -126,13 +126,13 @@ function MOI.get( ::MOI.ConstraintDual, cp::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}, ) where {T} - if !is_additive(model, cp) + if !_is_additive(model, cp) error("Cannot compute the dual of a multiplicative parameter") end return model.dual_value_of_parameters[p_val(cp)] end -function is_additive(model::Optimizer, cp::MOI.ConstraintIndex) +function _is_additive(model::Optimizer, cp::MOI.ConstraintIndex) if cp.value in model.multiplicative_parameters return false end diff --git a/src/utils.jl b/src/parametric_functions.jl similarity index 60% rename from src/utils.jl rename to src/parametric_functions.jl index c381d5bf..bc29a4df 100644 --- a/src/utils.jl +++ b/src/parametric_functions.jl @@ -1,299 +1,76 @@ -# Copyright (c) 2020: Tomás Gutierrez and contributors -# -# Use of this source code is governed by an MIT-style license that can be found -# in the LICENSE.md file or at https://opensource.org/licenses/MIT. -function next_variable_index!(model::Optimizer) - return model.last_variable_index_added += 1 +mutable struct ParametricQuadraticFunction{T} + # helper to efficiently update affine terms + affine_data::Dict{MOI.VariableIndex,T} + affine_data_np::Dict{MOI.VariableIndex,T} + # constant * parameter * variable (in this order) + pv::Vector{MOI.ScalarQuadraticTerm{T}} + # constant * parameter * parameter + pp::Vector{MOI.ScalarQuadraticTerm{T}} + # constant * variable * variable + vv::Vector{MOI.ScalarQuadraticTerm{T}} + # constant * parameter + p::Vector{MOI.ScalarAffineTerm{T}} + # constant * variable + v::Vector{MOI.ScalarAffineTerm{T}} + # constant (does not include the set constant) + c::T + # to avoid unnecessary lookups in updates + set_constant::T + # cache data that is inside the solver to avoid slow getters + current_terms_with_p::Dict{MOI.VariableIndex,T} + current_constant::T + # computed on runtime + # updated_terms_with_p::Dict{MOI.VariableIndex,T} + # updated_constant::T end -function next_parameter_index!(model::Optimizer) - return model.last_parameter_index_added += 1 -end - -function update_number_of_parameters!(model::Optimizer) - return model.number_of_parameters_in_model += 1 -end - -function is_parameter_in_model(model::Optimizer, v::MOI.VariableIndex) - return PARAMETER_INDEX_THRESHOLD < - v.value <= - model.last_parameter_index_added -end - -function is_variable_in_model(model::Optimizer, v::MOI.VariableIndex) - return 0 < v.value <= model.last_variable_index_added -end - -function has_quadratic_constraint_caches(model::Optimizer) - return !isempty(model.quadratic_outer_to_inner) -end - -function function_has_parameters(f::MOI.ScalarAffineFunction{T}) where {T} - for term in f.terms - if is_parameter(term.variable) - return true - end - end - return false -end - -function function_has_parameters(f::MOI.VectorOfVariables) - for variable in f.variables - if is_parameter(variable) - return true - end - end - return false -end - -function function_has_parameters(f::MOI.VectorAffineFunction{T}) where {T} - for term in f.terms - if is_parameter(term.scalar_term.variable) - return true - end - end - return false -end - -function function_has_parameters(f::MOI.ScalarQuadraticFunction{T}) where {T} - return function_affine_terms_has_parameters(f.affine_terms) || - function_quadratic_terms_has_parameters(f.quadratic_terms) -end - -function function_affine_terms_has_parameters( - affine_terms::Vector{MOI.ScalarAffineTerm{T}}, -) where {T} - for term in affine_terms - if is_parameter(term.variable) - return true - end - end - return false -end - -function function_quadratic_terms_has_parameters( - quadratic_terms::Vector{MOI.ScalarQuadraticTerm{T}}, -) where {T} - for term in quadratic_terms - if is_parameter(term.variable_1) || is_parameter(term.variable_2) - return true - end - end - return false -end - -function is_variable(v::MOI.VariableIndex) - return v.value < PARAMETER_INDEX_THRESHOLD -end - -function is_parameter(v::MOI.VariableIndex) - return v.value > PARAMETER_INDEX_THRESHOLD -end - -function count_scalar_affine_terms_types( - terms::Vector{MOI.ScalarAffineTerm{T}}, -) where {T} - num_vars = 0 - num_params = 0 - for term in terms - if is_variable(term.variable) - num_vars += 1 - else - num_params += 1 - end - end - return num_vars, num_params -end - -function split_affine_terms(terms::Vector{MOI.ScalarAffineTerm{T}}) where {T} - num_v, num_p = count_scalar_affine_terms_types(terms) - v = Vector{MOI.ScalarAffineTerm{T}}(undef, num_v) - p = Vector{MOI.ScalarAffineTerm{T}}(undef, num_p) - i_v = 1 - i_p = 1 - for term in terms - if is_variable(term.variable) - v[i_v] = term - i_v += 1 - else - p[i_p] = term - i_p += 1 - end - end - return v, p -end - -function ParametricAffineFunction(f::MOI.ScalarAffineFunction{T}) where {T} - v, p = split_affine_terms(f.terms) - return ParametricAffineFunction{T}(p, v, f.constant, zero(T), zero(T)) -end - -function original_function(f::ParametricAffineFunction{T}) where {T} - return MOI.ScalarAffineFunction{T}(vcat(f.p, f.v), f.c) -end - -function current_function(f::ParametricAffineFunction{T}) where {T} - return MOI.ScalarAffineFunction{T}(f.v, f.current_constant) -end - -function update_cache!( - f::ParametricAffineFunction{T}, - model::Optimizer, -) where {T} - f.current_constant = parametric_constant(model, f) - return nothing -end - -function parametric_constant( - model::Optimizer, - f::ParametricAffineFunction{T}, +function ParametricQuadraticFunction( + f::MOI.ScalarQuadraticFunction{T}, ) where {T} - # do not add set_function here - param_constant = f.c - for term in f.p - param_constant += - term.coefficient * model.parameters[p_idx(term.variable)] - end - return param_constant -end + v, p = _split_affine_terms(f.affine_terms) + pv, pp, vv = _split_quadratic_terms(f.quadratic_terms) -function delta_parametric_constant( - model::Optimizer, - f::ParametricAffineFunction{T}, -) where {T} - delta_constant = zero(T) - for term in f.p - p = p_idx(term.variable) - if !isnan(model.updated_parameters[p]) - delta_constant += - term.coefficient * - (model.updated_parameters[p] - model.parameters[p]) - end - end - return delta_constant -end - -function count_vector_affine_terms_types( - terms::Vector{MOI.VectorAffineTerm{T}}, -) where {T} - num_vars = 0 - num_params = 0 - for term in terms - if is_variable(term.scalar_term.variable) - num_vars += 1 - else - num_params += 1 - end + # find variables related to parameters + # so that we only cache the important part of the v (affine part) + v_in_pv = Set{MOI.VariableIndex}() + sizehint!(v_in_pv, length(pv)) + for term in pv + push!(v_in_pv, term.variable_2) end - return num_vars, num_params -end - -function split_affine_terms(terms::Vector{MOI.VectorAffineTerm{T}}) where {T} - num_v, num_p = count_vector_affine_terms_types(terms) - v = Vector{MOI.VectorAffineTerm{T}}(undef, num_v) - p = Vector{MOI.VectorAffineTerm{T}}(undef, num_p) - i_v = 1 - i_p = 1 - for term in terms - if is_variable(term.scalar_term.variable) - v[i_v] = term - i_v += 1 + affine_data = Dict{MOI.VariableIndex,T}() + sizehint!(affine_data, length(v_in_pv)) + affine_data_np = Dict{MOI.VariableIndex,T}() + sizehint!(affine_data, length(v)) + for term in v + if term.variable in v_in_pv + base = get(affine_data, term.variable, zero(T)) + affine_data[term.variable] = term.coefficient + base else - p[i_p] = term - i_p += 1 + base = get(affine_data_np, term.variable, zero(T)) + affine_data_np[term.variable] = term.coefficient + base end end - return v, p -end -function ParametricVectorAffineFunction( - f::MOI.VectorAffineFunction{T}, -) where {T} - v, p = split_affine_terms(f.terms) - return ParametricVectorAffineFunction{T}( + return ParametricQuadraticFunction{T}( + affine_data, + affine_data_np, + pv, + pp, + vv, p, v, - copy(f.constants), - zeros(T, length(f.constants)), - zeros(T, length(f.constants)), + f.constant, + zero(T), + Dict{MOI.VariableIndex,T}(), + zero(T), ) end -function original_function(f::ParametricVectorAffineFunction{T}) where {T} - return MOI.VectorAffineFunction{T}(vcat(f.p, f.v), f.c) -end - -function current_function(f::ParametricVectorAffineFunction{T}) where {T} - return MOI.VectorAffineFunction{T}(f.v, f.current_constant) -end - -function update_cache!( - f::ParametricVectorAffineFunction{T}, - model::Optimizer, -) where {T} - f.current_constant = parametric_constant(model, f) - return nothing -end - -function parametric_constant( - model::Optimizer, - f::ParametricVectorAffineFunction{T}, -) where {T} - # do not add set_function here - param_constant = copy(f.c) - for term in f.p - param_constant[term.output_index] += - term.scalar_term.coefficient * - model.parameters[p_idx(term.scalar_term.variable)] - end - return param_constant -end - -function delta_parametric_constant( - model::Optimizer, - f::ParametricVectorAffineFunction{T}, -) where {T} - delta_constant = zeros(T, length(f.c)) - for term in f.p - p = p_idx(term.scalar_term.variable) - if !isnan(model.updated_parameters[p]) - delta_constant[term.output_index] += - term.scalar_term.coefficient * - (model.updated_parameters[p] - model.parameters[p]) - end - end - return delta_constant -end - -function count_scalar_quadratic_terms_types( - terms::Vector{MOI.ScalarQuadraticTerm{T}}, -) where {T} - num_vv = 0 - num_pp = 0 - num_pv = 0 - for term in terms - if is_variable(term.variable_1) - if is_variable(term.variable_2) - num_vv += 1 - else - num_pv += 1 - end - else - if is_variable(term.variable_2) - num_pv += 1 - else - num_pp += 1 - end - end - end - return num_vv, num_pp, num_pv -end - -function split_quadratic_terms( +function _split_quadratic_terms( terms::Vector{MOI.ScalarQuadraticTerm{T}}, ) where {T} - num_vv, num_pp, num_pv = count_scalar_quadratic_terms_types(terms) + num_vv, num_pp, num_pv = _count_scalar_quadratic_terms_types(terms) pp = Vector{MOI.ScalarQuadraticTerm{T}}(undef, num_pp) # parameter x parameter pv = Vector{MOI.ScalarQuadraticTerm{T}}(undef, num_pv) # parameter (as a variable) x variable vv = Vector{MOI.ScalarQuadraticTerm{T}}(undef, num_vv) # variable x variable @@ -301,8 +78,8 @@ function split_quadratic_terms( i_pp = 1 i_pv = 1 for term in terms - if is_variable(term.variable_1) - if is_variable(term.variable_2) + if _is_variable(term.variable_1) + if _is_variable(term.variable_2) vv[i_vv] = term i_vv += 1 else @@ -314,7 +91,7 @@ function split_quadratic_terms( i_pv += 1 end else - if is_variable(term.variable_2) + if _is_variable(term.variable_2) pv[i_pv] = term i_pv += 1 else @@ -326,49 +103,31 @@ function split_quadratic_terms( return pv, pp, vv end -function ParametricQuadraticFunction( - f::MOI.ScalarQuadraticFunction{T}, +function _count_scalar_quadratic_terms_types( + terms::Vector{MOI.ScalarQuadraticTerm{T}}, ) where {T} - v, p = split_affine_terms(f.affine_terms) - pv, pp, vv = split_quadratic_terms(f.quadratic_terms) - - # find variables related to parameters - # so that we only cache the important part of the v (affine part) - v_in_pv = Set{MOI.VariableIndex}() - sizehint!(v_in_pv, length(pv)) - for term in pv - push!(v_in_pv, term.variable_2) - end - affine_data = Dict{MOI.VariableIndex,T}() - sizehint!(affine_data, length(v_in_pv)) - affine_data_np = Dict{MOI.VariableIndex,T}() - sizehint!(affine_data, length(v)) - for term in v - if term.variable in v_in_pv - base = get(affine_data, term.variable, zero(T)) - affine_data[term.variable] = term.coefficient + base + num_vv = 0 + num_pp = 0 + num_pv = 0 + for term in terms + if _is_variable(term.variable_1) + if _is_variable(term.variable_2) + num_vv += 1 + else + num_pv += 1 + end else - base = get(affine_data_np, term.variable, zero(T)) - affine_data_np[term.variable] = term.coefficient + base + if _is_variable(term.variable_2) + num_pv += 1 + else + num_pp += 1 + end end end - - return ParametricQuadraticFunction{T}( - affine_data, - affine_data_np, - pv, - pp, - vv, - p, - v, - f.constant, - zero(T), - Dict{MOI.VariableIndex,T}(), - zero(T), - ) + return num_vv, num_pp, num_pv end -function original_function(f::ParametricQuadraticFunction{T}) where {T} +function _original_function(f::ParametricQuadraticFunction{T}) where {T} return MOI.ScalarQuadraticFunction{T}( vcat(f.pv, f.pp, f.vv), vcat(f.p, f.v), @@ -376,7 +135,7 @@ function original_function(f::ParametricQuadraticFunction{T}) where {T} ) end -function current_function(f::ParametricQuadraticFunction{T}) where {T} +function _current_function(f::ParametricQuadraticFunction{T}) where {T} affine = MOI.ScalarAffineTerm{T}[] sizehint!(affine, length(f.current_terms_with_p) + length(f.affine_data_np)) for (v, c) in f.current_terms_with_p @@ -388,17 +147,23 @@ function current_function(f::ParametricQuadraticFunction{T}) where {T} return MOI.ScalarQuadraticFunction{T}(f.vv, affine, f.current_constant) end -function update_cache!( +function _cache_set_constant!( f::ParametricQuadraticFunction{T}, - model::Optimizer, + s::Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, ) where {T} - f.current_constant = parametric_constant(model, f) - f.current_terms_with_p = parametric_affine_terms(model, f) - return nothing + f.set_constant = MOI.constant(s) + return +end + +function _cache_set_constant!( + ::ParametricQuadraticFunction{T}, + ::MOI.AbstractScalarSet, +) where {T} + return end -function parametric_constant( - model::Optimizer, +function _parametric_constant( + model, f::ParametricQuadraticFunction{T}, ) where {T} # do not add set_function here @@ -416,8 +181,8 @@ function parametric_constant( return param_constant end -function delta_parametric_constant( - model::Optimizer, +function _delta_parametric_constant( + model, f::ParametricQuadraticFunction{T}, ) where {T} delta_constant = zero(T) @@ -453,8 +218,8 @@ function delta_parametric_constant( return delta_constant end -function parametric_affine_terms( - model::Optimizer, +function _parametric_affine_terms( + model, f::ParametricQuadraticFunction{T}, ) where {T} param_terms_dict = Dict{MOI.VariableIndex,T}() @@ -472,8 +237,8 @@ function parametric_affine_terms( return param_terms_dict end -function delta_parametric_affine_terms( - model::Optimizer, +function _delta_parametric_affine_terms( + model, f::ParametricQuadraticFunction{T}, ) where {T} delta_terms_dict = Dict{MOI.VariableIndex,T}() @@ -492,7 +257,72 @@ function delta_parametric_affine_terms( return delta_terms_dict end -function cache_set_constant!( +function _update_cache!(f::ParametricQuadraticFunction{T}, model) where {T} + f.current_constant = _parametric_constant(model, f) + f.current_terms_with_p = _parametric_affine_terms(model, f) + return nothing +end + +mutable struct ParametricAffineFunction{T} + # constant * parameter + p::Vector{MOI.ScalarAffineTerm{T}} + # constant * variable + v::Vector{MOI.ScalarAffineTerm{T}} + # constant + c::T + # to avoid unnecessary lookups in updates + set_constant::T + # cache to avoid slow getters + current_constant::T +end + +function ParametricAffineFunction(f::MOI.ScalarAffineFunction{T}) where {T} + v, p = _split_affine_terms(f.terms) + return ParametricAffineFunction{T}(p, v, f.constant, zero(T), zero(T)) +end + +function _split_affine_terms(terms::Vector{MOI.ScalarAffineTerm{T}}) where {T} + num_v, num_p = _count_scalar_affine_terms_types(terms) + v = Vector{MOI.ScalarAffineTerm{T}}(undef, num_v) + p = Vector{MOI.ScalarAffineTerm{T}}(undef, num_p) + i_v = 1 + i_p = 1 + for term in terms + if _is_variable(term.variable) + v[i_v] = term + i_v += 1 + else + p[i_p] = term + i_p += 1 + end + end + return v, p +end + +function _count_scalar_affine_terms_types( + terms::Vector{MOI.ScalarAffineTerm{T}}, +) where {T} + num_vars = 0 + num_params = 0 + for term in terms + if _is_variable(term.variable) + num_vars += 1 + else + num_params += 1 + end + end + return num_vars, num_params +end + +function _original_function(f::ParametricAffineFunction{T}) where {T} + return MOI.ScalarAffineFunction{T}(vcat(f.p, f.v), f.c) +end + +function _current_function(f::ParametricAffineFunction{T}) where {T} + return MOI.ScalarAffineFunction{T}(f.v, f.current_constant) +end + +function _cache_set_constant!( f::ParametricAffineFunction{T}, s::Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, ) where {T} @@ -500,57 +330,144 @@ function cache_set_constant!( return end -function cache_set_constant!( +function _cache_set_constant!( f::ParametricAffineFunction{T}, s::MOI.AbstractScalarSet, ) where {T} return end -function cache_set_constant!( - f::ParametricQuadraticFunction{T}, - s::Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, +function _parametric_constant(model, f::ParametricAffineFunction{T}) where {T} + # do not add set_function here + param_constant = f.c + for term in f.p + param_constant += + term.coefficient * model.parameters[p_idx(term.variable)] + end + return param_constant +end + +function _delta_parametric_constant( + model, + f::ParametricAffineFunction{T}, ) where {T} - f.set_constant = MOI.constant(s) - return + delta_constant = zero(T) + for term in f.p + p = p_idx(term.variable) + if !isnan(model.updated_parameters[p]) + delta_constant += + term.coefficient * + (model.updated_parameters[p] - model.parameters[p]) + end + end + return delta_constant end -function cache_set_constant!( - f::ParametricQuadraticFunction{T}, - s::MOI.AbstractScalarSet, +function _update_cache!(f::ParametricAffineFunction{T}, model) where {T} + f.current_constant = _parametric_constant(model, f) + return nothing +end + +mutable struct ParametricVectorAffineFunction{T} + # constant * parameter + p::Vector{MOI.VectorAffineTerm{T}} + # constant * variable + v::Vector{MOI.VectorAffineTerm{T}} + # constant + c::Vector{T} + # to avoid unnecessary lookups in updates + set_constant::Vector{T} + # cache to avoid slow getters + current_constant::Vector{T} +end + +function ParametricVectorAffineFunction( + f::MOI.VectorAffineFunction{T}, ) where {T} - return + v, p = _split_vector_affine_terms(f.terms) + return ParametricVectorAffineFunction{T}( + p, + v, + copy(f.constants), + zeros(T, length(f.constants)), + zeros(T, length(f.constants)), + ) +end + +function _split_vector_affine_terms( + terms::Vector{MOI.VectorAffineTerm{T}}, +) where {T} + num_v, num_p = _count_vector_affine_terms_types(terms) + v = Vector{MOI.VectorAffineTerm{T}}(undef, num_v) + p = Vector{MOI.VectorAffineTerm{T}}(undef, num_p) + i_v = 1 + i_p = 1 + for term in terms + if _is_variable(term.scalar_term.variable) + v[i_v] = term + i_v += 1 + else + p[i_p] = term + i_p += 1 + end + end + return v, p end -function is_affine(f::MOI.ScalarQuadraticFunction) - if isempty(f.quadratic_terms) - return true +function _count_vector_affine_terms_types( + terms::Vector{MOI.VectorAffineTerm{T}}, +) where {T} + num_vars = 0 + num_params = 0 + for term in terms + if _is_variable(term.scalar_term.variable) + num_vars += 1 + else + num_params += 1 + end end - return false + return num_vars, num_params end -function cache_multiplicative_params!( - model::Optimizer{T}, - f::ParametricQuadraticFunction{T}, +function _original_function(f::ParametricVectorAffineFunction{T}) where {T} + return MOI.VectorAffineFunction{T}(vcat(f.p, f.v), f.c) +end + +function _current_function(f::ParametricVectorAffineFunction{T}) where {T} + return MOI.VectorAffineFunction{T}(f.v, f.current_constant) +end + +function _parametric_constant( + model, + f::ParametricVectorAffineFunction{T}, ) where {T} - for term in f.pv - push!(model.multiplicative_parameters, term.variable_1.value) + # do not add set_function here + param_constant = copy(f.c) + for term in f.p + param_constant[term.output_index] += + term.scalar_term.coefficient * + model.parameters[p_idx(term.scalar_term.variable)] end - # TODO compute these duals might be feasible - for term in f.pp - push!(model.multiplicative_parameters, term.variable_1.value) - push!(model.multiplicative_parameters, term.variable_2.value) + return param_constant +end + +function _delta_parametric_constant( + model, + f::ParametricVectorAffineFunction{T}, +) where {T} + delta_constant = zeros(T, length(f.c)) + for term in f.p + p = p_idx(term.scalar_term.variable) + if !isnan(model.updated_parameters[p]) + delta_constant[term.output_index] += + term.scalar_term.coefficient * + (model.updated_parameters[p] - model.parameters[p]) + end end - return + return delta_constant end -# TODO: review comment -function quadratic_constraint_cache_map_check( - model::Optimizer, - idx::MOI.ConstraintIndex{F,S}, -) where {F,S} - cached_constraints = values(model.quadratic_outer_to_inner) - # Using this because some custom brodcast method throws errors if - # inner_idex .∈ cached_constraints is used - return idx ∈ cached_constraints +function _update_cache!(f::ParametricVectorAffineFunction{T}, model) where {T} + f.current_constant = _parametric_constant(model, f) + return nothing end diff --git a/src/update_parameters.jl b/src/update_parameters.jl index 604c4e70..69756f80 100644 --- a/src/update_parameters.jl +++ b/src/update_parameters.jl @@ -3,33 +3,33 @@ # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. -function set_with_new_constant(s::MOI.LessThan{T}, val::T) where {T} +function _set_with_new_constant(s::MOI.LessThan{T}, val::T) where {T} return MOI.LessThan{T}(s.upper - val) end -function set_with_new_constant(s::MOI.GreaterThan{T}, val::T) where {T} +function _set_with_new_constant(s::MOI.GreaterThan{T}, val::T) where {T} return MOI.GreaterThan{T}(s.lower - val) end -function set_with_new_constant(s::MOI.EqualTo{T}, val::T) where {T} +function _set_with_new_constant(s::MOI.EqualTo{T}, val::T) where {T} return MOI.EqualTo{T}(s.value - val) end -function set_with_new_constant(s::MOI.Interval{T}, val::T) where {T} +function _set_with_new_constant(s::MOI.Interval{T}, val::T) where {T} return MOI.Interval{T}(s.lower - val, s.upper - val) end # Affine # change to use only inner_ci all around so tha tupdates are faster # modifications should not be used any ways, afterall we have param all around -function update_parametric_affine_constraints!(model::Optimizer) +function _update_affine_constraints!(model::Optimizer) for (F, S) in keys(model.affine_constraint_cache.dict) affine_constraint_cache_inner = model.affine_constraint_cache[F, S] affine_constraint_cache_set_inner = model.affine_constraint_cache_set[F, S] if !isempty(affine_constraint_cache_inner) # barrier to avoid type instability of inner dicts - update_parametric_affine_constraints!( + _update_affine_constraints!( model, affine_constraint_cache_inner, affine_constraint_cache_set_inner, @@ -41,7 +41,7 @@ end # TODO: cache changes and then batch them instead -function update_parametric_affine_constraints!( +function _update_affine_constraints!( model::Optimizer, affine_constraint_cache_inner::DoubleDictInner{F,S,V}, affine_constraint_cache_set_inner::DoubleDictInner{ @@ -55,11 +55,11 @@ function update_parametric_affine_constraints!( # sizehint!(cis, length(affine_constraint_cache_inner)) # sizehint!(sets, length(affine_constraint_cache_inner)) for (inner_ci, pf) in affine_constraint_cache_inner - delta_constant = delta_parametric_constant(model, pf) + delta_constant = _delta_parametric_constant(model, pf) if !iszero(delta_constant) pf.current_constant += delta_constant new_set = S(pf.set_constant - pf.current_constant) - # new_set = set_with_new_constant(set, param_constant) + # new_set = _set_with_new_constant(set, param_constant) MOI.set(model.optimizer, MOI.ConstraintSet(), inner_ci, new_set) # push!(cis, inner_ci) # push!(sets, new_set) @@ -71,7 +71,7 @@ function update_parametric_affine_constraints!( return end -function update_parametric_affine_constraints!( +function _update_affine_constraints!( model::Optimizer, affine_constraint_cache_inner::DoubleDictInner{F,S,V}, affine_constraint_cache_set_inner::DoubleDictInner{ @@ -81,25 +81,25 @@ function update_parametric_affine_constraints!( }, ) where {F,S<:MOI.Interval{T},V} where {T} for (inner_ci, pf) in affine_constraint_cache_inner - set = affine_constraint_cache_set_inner[inner_ci]::S - delta_constant = delta_parametric_constant(model, pf) + delta_constant = _delta_parametric_constant(model, pf) if !iszero(delta_constant) pf.current_constant += delta_constant # new_set = S(pf.set_constant - pf.current_constant) - new_set = set_with_new_constant(set, pf.current_constant)::S + set = affine_constraint_cache_set_inner[inner_ci]::S + new_set = _set_with_new_constant(set, pf.current_constant)::S MOI.set(model.optimizer, MOI.ConstraintSet(), inner_ci, new_set) end end return end -function update_parametric_vector_affine_constraints!(model::Optimizer) +function _update_vector_affine_constraints!(model::Optimizer) for (F, S) in keys(model.vector_affine_constraint_cache.dict) vector_affine_constraint_cache_inner = model.vector_affine_constraint_cache[F, S] if !isempty(vector_affine_constraint_cache_inner) # barrier to avoid type instability of inner dicts - update_parametric_vector_affine_constraints!( + _update_vector_affine_constraints!( model, vector_affine_constraint_cache_inner, ) @@ -108,12 +108,12 @@ function update_parametric_vector_affine_constraints!(model::Optimizer) return end -function update_parametric_vector_affine_constraints!( +function _update_vector_affine_constraints!( model::Optimizer, vector_affine_constraint_cache_inner::DoubleDictInner{F,S,V}, ) where {F<:MOI.VectorAffineFunction{T},S,V} where {T} for (inner_ci, pf) in vector_affine_constraint_cache_inner - delta_constant = delta_parametric_constant(model, pf) + delta_constant = _delta_parametric_constant(model, pf) if !iszero(delta_constant) pf.current_constant .+= delta_constant MOI.modify( @@ -126,7 +126,7 @@ function update_parametric_vector_affine_constraints!( return end -function update_parametric_quadratic_constraints!(model::Optimizer) +function _update_quadratic_constraints!(model::Optimizer) for (F, S) in keys(model.quadratic_constraint_cache.dict) quadratic_constraint_cache_inner = model.quadratic_constraint_cache[F, S] @@ -134,7 +134,7 @@ function update_parametric_quadratic_constraints!(model::Optimizer) model.quadratic_constraint_cache_set[F, S] if !isempty(quadratic_constraint_cache_inner) # barrier to avoid type instability of inner dicts - update_parametric_quadratic_constraints!( + _update_quadratic_constraints!( model, quadratic_constraint_cache_inner, quadratic_constraint_cache_set_inner, @@ -144,7 +144,7 @@ function update_parametric_quadratic_constraints!(model::Optimizer) return end -function affine_build_change_and_up_param_func( +function _affine_build_change_and_up_param_func( pf::ParametricQuadraticFunction{T}, delta_terms, ) where {T} @@ -160,7 +160,7 @@ function affine_build_change_and_up_param_func( return changes end -function update_parametric_quadratic_constraints!( +function _update_quadratic_constraints!( model::Optimizer, quadratic_constraint_cache_inner::DoubleDictInner{F,S,V}, quadratic_constraint_cache_set_inner::DoubleDictInner{ @@ -174,18 +174,18 @@ function update_parametric_quadratic_constraints!( # sizehint!(cis, length(quadratic_constraint_cache_inner)) # sizehint!(sets, length(quadratic_constraint_cache_inner)) for (inner_ci, pf) in quadratic_constraint_cache_inner - delta_constant = delta_parametric_constant(model, pf) + delta_constant = _delta_parametric_constant(model, pf) if !iszero(delta_constant) pf.current_constant += delta_constant new_set = S(pf.set_constant - pf.current_constant) - # new_set = set_with_new_constant(set, param_constant) + # new_set = _set_with_new_constant(set, param_constant) MOI.set(model.optimizer, MOI.ConstraintSet(), inner_ci, new_set) # push!(cis, inner_ci) # push!(sets, new_set) end - delta_terms = delta_parametric_affine_terms(model, pf) + delta_terms = _delta_parametric_affine_terms(model, pf) if !isempty(delta_terms) - changes = affine_build_change_and_up_param_func(pf, delta_terms) + changes = _affine_build_change_and_up_param_func(pf, delta_terms) cis = fill(inner_ci, length(changes)) MOI.modify(model.optimizer, cis, changes) end @@ -196,7 +196,7 @@ function update_parametric_quadratic_constraints!( return end -function update_parametric_quadratic_constraints!( +function _update_quadratic_constraints!( model::Optimizer, quadratic_constraint_cache_inner::DoubleDictInner{F,S,V}, quadratic_constraint_cache_set_inner::DoubleDictInner{ @@ -206,17 +206,17 @@ function update_parametric_quadratic_constraints!( }, ) where {F,S<:MOI.Interval{T},V} where {T} for (inner_ci, pf) in quadratic_constraint_cache_inner - set = quadratic_constraint_cache_set_inner[inner_ci]::S - delta_constant = delta_parametric_constant(model, pf) + delta_constant = _delta_parametric_constant(model, pf) if !iszero(delta_constant) pf.current_constant += delta_constant # new_set = S(pf.set_constant - pf.current_constant) - new_set = set_with_new_constant(set, pf.current_constant)::S + set = quadratic_constraint_cache_set_inner[inner_ci]::S + new_set = _set_with_new_constant(set, pf.current_constant)::S MOI.set(model.optimizer, MOI.ConstraintSet(), inner_ci, new_set) end - delta_terms = delta_parametric_affine_terms(model, pf) + delta_terms = _delta_parametric_affine_terms(model, pf) if !isempty(delta_terms) - changes = affine_build_change_and_up_param_func(pf, delta_terms) + changes = _affine_build_change_and_up_param_func(pf, delta_terms) cis = fill(inner_ci, length(changes)) MOI.modify(model.optimizer, cis, changes) end @@ -224,12 +224,12 @@ function update_parametric_quadratic_constraints!( return end -function update_parametric_affine_objective!(model::Optimizer{T}) where {T} +function _update_affine_objective!(model::Optimizer{T}) where {T} if model.affine_objective_cache === nothing return end pf = model.affine_objective_cache - delta_constant = delta_parametric_constant(model, pf) + delta_constant = _delta_parametric_constant(model, pf) if !iszero(delta_constant) pf.current_constant += delta_constant # F = MOI.get(model.optimizer, MOI.ObjectiveFunctionType()) @@ -242,12 +242,12 @@ function update_parametric_affine_objective!(model::Optimizer{T}) where {T} return end -function update_parametric_quadratic_objective!(model::Optimizer{T}) where {T} +function _update_quadratic_objective!(model::Optimizer{T}) where {T} if model.quadratic_objective_cache === nothing return end pf = model.quadratic_objective_cache - delta_constant = delta_parametric_constant(model, pf) + delta_constant = _delta_parametric_constant(model, pf) if !iszero(delta_constant) pf.current_constant += delta_constant F = MOI.get(model.optimizer, MOI.ObjectiveFunctionType()) @@ -257,21 +257,21 @@ function update_parametric_quadratic_objective!(model::Optimizer{T}) where {T} MOI.ScalarConstantChange(pf.current_constant), ) end - delta_terms = delta_parametric_affine_terms(model, pf) + delta_terms = _delta_parametric_affine_terms(model, pf) if !isempty(delta_terms) F = MOI.get(model.optimizer, MOI.ObjectiveFunctionType()) - changes = affine_build_change_and_up_param_func(pf, delta_terms) + changes = _affine_build_change_and_up_param_func(pf, delta_terms) MOI.modify(model.optimizer, MOI.ObjectiveFunction{F}(), changes) end return end function update_parameters!(model::Optimizer) - update_parametric_affine_constraints!(model) - update_parametric_vector_affine_constraints!(model) - update_parametric_quadratic_constraints!(model) - update_parametric_affine_objective!(model) - update_parametric_quadratic_objective!(model) + _update_affine_constraints!(model) + _update_vector_affine_constraints!(model) + _update_quadratic_constraints!(model) + _update_affine_objective!(model) + _update_quadratic_objective!(model) # Update parameters and put NaN to indicate that the parameter has been # updated diff --git a/test/moi_tests.jl b/test/moi_tests.jl index ef2cba31..da02d381 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -336,7 +336,7 @@ function test_production_problem_example() ) MOI.add_constraint(optimizer, cons2, MOI.LessThan(b2)) @test cons1.terms[1].coefficient == 2 - @test POI.is_parameter_in_model(optimizer, cons2.terms[3].variable) + @test POI._parameter_in_model(optimizer, cons2.terms[3].variable) obj_func = MOI.ScalarAffineFunction( MOI.ScalarAffineTerm.([c[1], c[2], 3.0], [x[1], x[2], w]), 0.0, @@ -414,7 +414,7 @@ function test_production_problem_example_duals() ) ci2 = MOI.add_constraint(optimizer, cons2, MOI.LessThan(b2)) @test cons1.terms[1].coefficient == 2 - @test POI.is_parameter_in_model(optimizer, cons2.terms[3].variable) + @test POI._parameter_in_model(optimizer, cons2.terms[3].variable) obj_func = MOI.ScalarAffineFunction( MOI.ScalarAffineTerm.([c[1], c[2], 2.0], [x[1], x[2], w]), 0.0, @@ -507,7 +507,7 @@ function test_production_problem_example_parameters_for_duals_and_intervals() ) ci2 = MOI.add_constraint(optimizer, cons2, MOI.LessThan(b2)) @test cons1.terms[1].coefficient == 2 - @test POI.is_parameter_in_model(optimizer, cons2.terms[3].variable) + @test POI._parameter_in_model(optimizer, cons2.terms[3].variable) obj_func = MOI.ScalarAffineFunction( MOI.ScalarAffineTerm.([c[1], c[2], 2.0], [x[1], x[2], w]), 0.0, From de02f979edf4c0a803e70b3617309fddebae8fae Mon Sep 17 00:00:00 2001 From: Joaquim Garcia Date: Sun, 3 Dec 2023 04:39:38 -0300 Subject: [PATCH 2/2] fix names --- src/MOI_wrapper.jl | 4 ---- src/duals.jl | 10 +++++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 1b93e2c3..70f3aff5 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -221,10 +221,6 @@ function _add_to_constraint_map!(model::Optimizer, ci) model.constraint_outer_to_inner[ci] = ci return end -function _add_to_constraint_map!(model::Optimizer, ci_in, ci_out) - model.constraint_outer_to_inner[ci_out] = ci_in - return -end function MOI.supports( model::Optimizer, diff --git a/src/duals.jl b/src/duals.jl index ef632849..e4d27276 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -8,12 +8,12 @@ function _compute_dual_of_parameters!(model::Optimizer{T}) where {T} zeros(T, model.number_of_parameters_in_model) _update_duals_from_affine_constraints!(model) _update_duals_from_vector_affine_constraints!(model) - update_duals_from_quadratic_constraints!(model) + _update_duals_from_quadratic_constraints!(model) if model.affine_objective_cache !== nothing - update_duals_from_objective!(model, model.affine_objective_cache) + _update_duals_from_objective!(model, model.affine_objective_cache) end if model.quadratic_objective_cache !== nothing - update_duals_from_objective!(model, model.quadratic_objective_cache) + _update_duals_from_objective!(model, model.quadratic_objective_cache) end return end @@ -37,7 +37,7 @@ function _update_duals_from_vector_affine_constraints!(model::Optimizer) return end -function update_duals_from_quadratic_constraints!(model::Optimizer) +function _update_duals_from_quadratic_constraints!(model::Optimizer) for (F, S) in keys(model.quadratic_constraint_cache.dict) quadratic_constraint_cache_inner = model.quadratic_constraint_cache[F, S] @@ -83,7 +83,7 @@ function _compute_parameters_in_ci!( return end -function update_duals_from_objective!(model::Optimizer{T}, pf) where {T} +function _update_duals_from_objective!(model::Optimizer{T}, pf) where {T} is_min = MOI.get(model.optimizer, MOI.ObjectiveSense()) == MOI.MIN_SENSE for param in pf.p model.dual_value_of_parameters[p_val(param.variable)] +=