diff --git a/docs/src/manual.md b/docs/src/manual.md index f7303482..696aff6d 100644 --- a/docs/src/manual.md +++ b/docs/src/manual.md @@ -76,7 +76,7 @@ MOI.set(optimizer, POI.ParameterValue(), y, POI.Parameter(2.0)) ### Retrieving the dual of a parameter -Given an optimized model, one can calculate the dual associated to a parameter, **as long as it is an additive term in the constraints or objective**. +Given an optimized model, one can compute the dual associated to a parameter, **as long as it is an additive term in the constraints or objective**. One can do so by getting the `MOI.ConstraintDual` attribute of the parameter's `MOI.ConstraintIndex`: ```julia diff --git a/src/ParametricOptInterface.jl b/src/ParametricOptInterface.jl index 7382636e..158852cb 100644 --- a/src/ParametricOptInterface.jl +++ b/src/ParametricOptInterface.jl @@ -9,10 +9,13 @@ using MathOptInterface const MOI = MathOptInterface -const PARAMETER_INDEX_THRESHOLD = 1_000_000_000_000_000_000 +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 +const SIMPLE_SCALAR_SETS{T} = + Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}} + """ Parameter(val::Float64) @@ -43,9 +46,83 @@ end function MOI.Utilities.CleverDicts.key_to_index(key::ParameterIndex) return key.index end -function p_idx(vi::MOI.VariableIndex) +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 + +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, + typeof(MOI.Utilities.CleverDicts.key_to_index), + typeof(MOI.Utilities.CleverDicts.index_to_key), +} + +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 + +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 """ Optimizer{T, OT <: MOI.ModelLike} <: MOI.AbstractOptimizer @@ -74,102 +151,59 @@ ParametricOptInterface.Optimizer{Float64,GLPK.Optimizer} """ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer optimizer::OT - parameters::MOI.Utilities.CleverDicts.CleverDict{ - ParameterIndex, - T, - typeof(MOI.Utilities.CleverDicts.key_to_index), - typeof(MOI.Utilities.CleverDicts.index_to_key), - } + parameters::ParamTo{T} parameters_name::Dict{MOI.VariableIndex,String} # The updated_parameters dictionary has the same dimension of the # parameters dictionary and if the value stored is a NaN is means # that the parameter has not been updated. - updated_parameters::MOI.Utilities.CleverDicts.CleverDict{ - ParameterIndex, - T, - typeof(MOI.Utilities.CleverDicts.key_to_index), - typeof(MOI.Utilities.CleverDicts.index_to_key), - } - variables::MOI.Utilities.CleverDicts.CleverDict{ - MOI.VariableIndex, - MOI.VariableIndex, - typeof(MOI.Utilities.CleverDicts.key_to_index), - typeof(MOI.Utilities.CleverDicts.index_to_key), - } + updated_parameters::ParamTo{T} + variables::VariableMap last_variable_index_added::Int64 last_parameter_index_added::Int64 - # Store the constraint function and set passed to POI by MOI - original_constraint_function_and_set_cache::Dict{ - MOI.ConstraintIndex, - Tuple{MOI.AbstractFunction,MOI.AbstractSet}, - } - # Store the map for SAFs that might be transformed into VI - affine_added_cache::MOI.Utilities.DoubleDicts.DoubleDict{ - MOI.ConstraintIndex, - } + + # affine constraint data last_affine_added::Int64 - # Store reference to parameters of affine constraints with parameters: v + p - affine_constraint_cache::MOI.Utilities.DoubleDicts.DoubleDict{ - Vector{MOI.ScalarAffineTerm{Float64}}, - } - # Store constraint set - affine_constraint_cache_set::MOI.Utilities.DoubleDicts.DoubleDict{ - MOI.AbstractScalarSet, - } - # Store reference quadratic constraints with parameter * variable constraints: p * v - quadratic_constraint_cache_pv::MOI.Utilities.DoubleDicts.DoubleDict{ - Vector{MOI.ScalarQuadraticTerm{Float64}}, - } - # Store reference quadratic constraints with parameter * variable constraints: p * v - quadratic_constraint_cache_pp::MOI.Utilities.DoubleDicts.DoubleDict{ - Vector{MOI.ScalarQuadraticTerm{Float64}}, - } - # Store constraint set - quadratic_constraint_cache_pp_set::MOI.Utilities.DoubleDicts.DoubleDict{ - MOI.AbstractScalarSet, - } - # Store reference to constraints with quad_variable_term + affine_with_parameters: v * v + p - quadratic_constraint_cache_pc::MOI.Utilities.DoubleDicts.DoubleDict{ - Vector{MOI.ScalarAffineTerm{Float64}}, - } - # Store constraint set - quadratic_constraint_cache_pc_set::MOI.Utilities.DoubleDicts.DoubleDict{ - MOI.AbstractScalarSet, - } - # Store the reference to variables in the scalar affine part that are - # multiplied by parameters in the scalar quadratic terms. - # i.e. - # If we have a constraint function with both scalar quadratic terms and - # scalar affine terms such as p_1 * v_1 + 2.0 * v_1 - # When we need to update the constraint coefficient after updating the parameter - # we must do (new_p_1 + 2.0) * v_1 - # This cache is storing the 2.0 * v_1 part. - quadratic_constraint_variables_associated_to_parameters_cache::MOI.Utilities.DoubleDicts.DoubleDict{ - Vector{MOI.ScalarAffineTerm{T}}, - } - # Store the map for SQFs that might be transformed into SAF + # Store the map for SAFs (some might be transformed into VI) + affine_outer_to_inner::DoubleDict{MOI.ConstraintIndex} + # Clever cache of data (inner key) + affine_constraint_cache::DoubleDict{ParametricAffineFunction{T}} + # Store original constraint set (inner key) + affine_constraint_cache_set::DoubleDict{MOI.AbstractScalarSet} + + # quadratic constraitn data + last_quad_add_added::Int64 + # Store the map for SQFs (some might be transformed into SAF) # for instance p*p + var -> ScalarAffine(var) - quadratic_added_cache::Dict{MOI.ConstraintIndex,MOI.ConstraintIndex} + quadratic_outer_to_inner::DoubleDict{MOI.ConstraintIndex} + # Clever cache of data (inner key) + quadratic_constraint_cache::DoubleDict{ParametricQuadraticFunction{T}} + # Store original constraint set (inner key) + quadratic_constraint_cache_set::DoubleDict{MOI.AbstractScalarSet} + + # objective function data + # Clever cache of data (at most one can be !== nothing) + affine_objective_cache::Union{Nothing,ParametricAffineFunction{T}} + quadratic_objective_cache::Union{Nothing,ParametricQuadraticFunction{T}} + original_objective_cache::MOI.Utilities.ObjectiveContainer{T} # Store parametric expressions for product of variables quadratic_objective_cache_product::Dict{ Tuple{MOI.VariableIndex,MOI.VariableIndex}, MOI.AbstractFunction, } - last_quad_add_added::Int64 - vector_constraint_cache::MOI.Utilities.DoubleDicts.DoubleDict{ - Vector{MOI.VectorAffineTerm{Float64}}, - } - # Ditto from the constraint caches but for the objective function - original_objective_function::MOI.AbstractFunction - affine_objective_cache::Vector{MOI.ScalarAffineTerm{T}} - quadratic_objective_cache_pv::Vector{MOI.ScalarQuadraticTerm{T}} - quadratic_objective_cache_pp::Vector{MOI.ScalarQuadraticTerm{T}} - quadratic_objective_cache_pc::Vector{MOI.ScalarAffineTerm{T}} - quadratic_objective_variables_associated_to_parameters_cache::Vector{ - MOI.ScalarAffineTerm{T}, + quadratic_objective_cache_product_changed::Bool + + # vector affine function data + # vector_constraint_cache::DoubleDict{Vector{MOI.VectorAffineTerm{T}}} + # Clever cache of data (inner key) + vector_affine_constraint_cache::DoubleDict{ + ParametricVectorAffineFunction{T}, } + + # multiplicative_parameters::Set{Int64} - dual_value_of_parameters::Vector{Float64} + dual_value_of_parameters::Vector{T} + + # params evaluate_duals::Bool number_of_parameters_in_model::Int64 constraints_interpretation::ConstraintsInterpretationCode @@ -179,14 +213,15 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer evaluate_duals::Bool = true, save_original_objective_and_constraints::Bool = true, ) where {OT} - return new{Float64,OT}( + T = Float64 + return new{T,OT}( optimizer, - MOI.Utilities.CleverDicts.CleverDict{ParameterIndex,Float64}( + MOI.Utilities.CleverDicts.CleverDict{ParameterIndex,T}( MOI.Utilities.CleverDicts.key_to_index, MOI.Utilities.CleverDicts.index_to_key, ), Dict{MOI.VariableIndex,String}(), - MOI.Utilities.CleverDicts.CleverDict{ParameterIndex,Float64}( + MOI.Utilities.CleverDicts.CleverDict{ParameterIndex,T}( MOI.Utilities.CleverDicts.key_to_index, MOI.Utilities.CleverDicts.index_to_key, ), @@ -199,47 +234,32 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer ), 0, PARAMETER_INDEX_THRESHOLD, - Dict{ - MOI.ConstraintIndex, - Tuple{MOI.AbstractFunction,MOI.AbstractSet}, - }(), - MOI.Utilities.DoubleDicts.DoubleDict{MOI.ConstraintIndex}(), + # affine constraint 0, - MOI.Utilities.DoubleDicts.DoubleDict{ - Vector{MOI.ScalarAffineTerm{Float64}}, - }(), - MOI.Utilities.DoubleDicts.DoubleDict{MOI.AbstractScalarSet}(), - MOI.Utilities.DoubleDicts.DoubleDict{ - Vector{MOI.ScalarQuadraticTerm{Float64}}, - }(), - MOI.Utilities.DoubleDicts.DoubleDict{ - Vector{MOI.ScalarQuadraticTerm{Float64}}, - }(), - MOI.Utilities.DoubleDicts.DoubleDict{MOI.AbstractScalarSet}(), - MOI.Utilities.DoubleDicts.DoubleDict{ - Vector{MOI.ScalarAffineTerm{Float64}}, - }(), - MOI.Utilities.DoubleDicts.DoubleDict{MOI.AbstractScalarSet}(), - MOI.Utilities.DoubleDicts.DoubleDict{ - Vector{MOI.ScalarAffineTerm{Float64}}, - }(), - Dict{MOI.ConstraintIndex,MOI.ConstraintIndex}(), + DoubleDict{MOI.ConstraintIndex}(), + DoubleDict{ParametricAffineFunction{T}}(), + DoubleDict{MOI.AbstractScalarSet}(), + # quadratic constraint + 0, + DoubleDict{MOI.ConstraintIndex}(), + DoubleDict{ParametricQuadraticFunction{T}}(), + DoubleDict{MOI.AbstractScalarSet}(), + # objective + nothing, + nothing, + # nothing, + MOI.Utilities.ObjectiveContainer{T}(), Dict{ Tuple{MOI.VariableIndex,MOI.VariableIndex}, MOI.AbstractFunction, }(), - 0, - MOI.Utilities.DoubleDicts.DoubleDict{ - Vector{MOI.VectorAffineTerm{Float64}}, - }(), - MOI.VariableIndex(-1), - Vector{MOI.ScalarAffineTerm{Float64}}(), - Vector{MOI.ScalarQuadraticTerm{Float64}}(), - Vector{MOI.ScalarQuadraticTerm{Float64}}(), - Vector{MOI.ScalarAffineTerm{Float64}}(), - Vector{MOI.ScalarAffineTerm{Float64}}(), + false, + # vec affine + # DoubleDict{Vector{MOI.VectorAffineTerm{T}}}(), + DoubleDict{ParametricVectorAffineFunction{T}}(), + # other Set{Int64}(), - Vector{Float64}(), + Vector{T}(), evaluate_duals, 0, ONLY_CONSTRAINTS, @@ -256,39 +276,66 @@ function MOI.is_empty(model::Optimizer) return MOI.is_empty(model.optimizer) && isempty(model.parameters) && isempty(model.parameters_name) && - isempty(model.variables) && isempty(model.updated_parameters) && isempty(model.variables) && model.last_variable_index_added == 0 && model.last_parameter_index_added == PARAMETER_INDEX_THRESHOLD && - isempty(model.original_constraint_function_and_set_cache) && - isempty(model.affine_added_cache) && + # affine ctr model.last_affine_added == 0 && + isempty(model.affine_outer_to_inner) && isempty(model.affine_constraint_cache) && isempty(model.affine_constraint_cache_set) && - isempty(model.quadratic_constraint_cache_pv) && - isempty(model.quadratic_constraint_cache_pp) && - isempty(model.quadratic_constraint_cache_pp_set) && - isempty(model.quadratic_constraint_cache_pc) && - isempty(model.quadratic_constraint_cache_pc_set) && - isempty( - model.quadratic_constraint_variables_associated_to_parameters_cache, - ) && - isempty(model.quadratic_added_cache) && - isempty(model.quadratic_objective_cache_product) && + # quad ctr model.last_quad_add_added == 0 && - model.original_objective_function == MOI.VariableIndex(-1) && - isempty(model.affine_objective_cache) && - isempty(model.quadratic_objective_cache_pv) && - isempty(model.quadratic_objective_cache_pp) && - isempty(model.quadratic_objective_cache_pc) && - isempty( - model.quadratic_objective_variables_associated_to_parameters_cache, - ) && + 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 + # 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{ @@ -337,14 +384,6 @@ function MOI.supports( return MOI.supports(model.optimizer, attr) 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( model::Optimizer, ::MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}, @@ -355,6 +394,14 @@ function MOI.supports( ) 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 @@ -376,21 +423,16 @@ struct ListOfPureVariableIndices <: MOI.AbstractModelAttribute end struct ListOfParameterIndices <: MOI.AbstractModelAttribute end function MOI.get(model::Optimizer, ::ListOfPureVariableIndices) - return _all_variables(model) -end -function MOI.get(model::Optimizer, ::ListOfParameterIndices) - return _all_parameters(model) -end - -function _all_variables(model::Optimizer) return collect(keys(model.variables)) end -function _all_parameters(model::Optimizer) +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}, @@ -402,6 +444,7 @@ function MOI.get( end return MOI.get(model.optimizer, MOI.ListOfConstraintAttributesSet{F,S}()) end + function MOI.set( model::Optimizer, ::MOI.ConstraintFunction, @@ -411,6 +454,7 @@ function MOI.set( MOI.set(model.optimizer, MOI.ConstraintFunction(), c, f) return end + function MOI.set( model::Optimizer, ::MOI.ConstraintSet, @@ -420,62 +464,32 @@ function MOI.set( MOI.set(model.optimizer, MOI.ConstraintSet(), c, s) return end + function MOI.modify( model::Optimizer, c::MOI.ConstraintIndex{F,S}, - chg::MOI.ScalarCoefficientChange{Float64}, -) where {F,S} - MOI.modify(model.optimizer, c, chg) - return -end -function MOI.modify( - model::Optimizer, - c::MOI.ObjectiveFunction{F}, - chg::MOI.ScalarCoefficientChange{Float64}, -) where {F<:MathOptInterface.AbstractScalarFunction} + 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::MOI.ScalarConstantChange{Float64}, -) where {F<:MathOptInterface.AbstractScalarFunction} + 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) - return -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.original_constraint_function_and_set_cache) - empty!(model.affine_added_cache) - model.last_affine_added = 0 - empty!(model.affine_constraint_cache) - empty!(model.affine_constraint_cache_set) - empty!(model.quadratic_constraint_cache_pv) - empty!(model.quadratic_constraint_cache_pp) - empty!(model.quadratic_constraint_cache_pp_set) - empty!(model.quadratic_constraint_cache_pc) - empty!(model.quadratic_constraint_cache_pc_set) - empty!(model.quadratic_constraint_variables_associated_to_parameters_cache) - empty!(model.quadratic_added_cache) - empty!(model.quadratic_objective_cache_product) - model.last_quad_add_added = 0 - model.original_objective_function = MOI.VariableIndex(-1) - empty!(model.vector_constraint_cache) - empty!(model.affine_objective_cache) - empty!(model.quadratic_objective_cache_pv) - empty!(model.quadratic_objective_cache_pp) - empty!(model.quadratic_objective_cache_pc) - empty!(model.quadratic_objective_variables_associated_to_parameters_cache) - empty!(model.dual_value_of_parameters) - model.number_of_parameters_in_model = 0 + MOI.modify(model.original_objective_cache, c, chg) return end @@ -523,8 +537,8 @@ function MOI.set( c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},S}, name::String, ) where {T,S<:MOI.AbstractSet} - if haskey(model.quadratic_added_cache, c) - MOI.set(model.optimizer, attr, model.quadratic_added_cache[c], name) + 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 @@ -537,8 +551,8 @@ function MOI.set( c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}, name::String, ) where {T,S<:MOI.AbstractSet} - if haskey(model.affine_added_cache, c) - MOI.set(model.optimizer, attr, model.affine_added_cache[c], name) + 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 @@ -560,20 +574,8 @@ function MOI.get( attr::MOI.ConstraintName, c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},S}, ) where {T,S<:MOI.AbstractSet} - if haskey(model.quadratic_added_cache, c) - return MOI.get(model.optimizer, attr, model.quadratic_added_cache[c]) - else - return MOI.get(model.optimizer, attr, c) - end -end - -function MOI.get( - model::Optimizer, - attr::MOI.ConstraintName, - c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}, -) where {T,S<:MOI.AbstractSet} - if haskey(model.affine_added_cache, c) - return MOI.get(model.optimizer, attr, model.affine_added_cache[c]) + 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 @@ -592,14 +594,17 @@ function MOI.get( attr::MOI.ConstraintName, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}, ) where {T,S} - moi_ci = get(model.affine_added_cache, c, c) - # This SAF constraint was transformed into variable bound - if typeof(moi_ci) === MOI.ConstraintIndex{MOI.VariableIndex,S} - v = MOI.get(model.optimizer, MOI.ConstraintFunction(), moi_ci) - variable_name = MOI.get(model.optimizer, MOI.VariableName(), v) - return "ParametricBound_$(S)_$(variable_name)" + 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, moi_ci) + return MOI.get(model.optimizer, attr, c) end end @@ -620,9 +625,14 @@ function MOI.get( attr::MOI.ConstraintFunction, ci::MOI.ConstraintIndex{F,S}, ) where {F,S} - if haskey(model.original_constraint_function_and_set_cache, ci) - return model.original_constraint_function_and_set_cache[ci][1] + 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 @@ -688,8 +698,12 @@ function MOI.get( attr::MOI.ConstraintSet, ci::MOI.ConstraintIndex{F,S}, ) where {F,S} - if haskey(model.original_constraint_function_and_set_cache, ci) - return model.original_constraint_function_and_set_cache[ci][2] + 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) @@ -700,47 +714,31 @@ function MOI.get(model::Optimizer, attr::MOI.ObjectiveSense) return MOI.get(model.optimizer, attr) end -function MOI.get(model::Optimizer, attr::MOI.ObjectiveFunctionType) - return typeof(model.original_objective_function) +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{F}, -) where { - F<:Union{ - MOI.VariableIndex, - MOI.ScalarAffineFunction{T}, - MOI.ScalarQuadraticFunction{T}, - }, -} where {T} - if !function_has_parameters(model, model.original_objective_function) - return MOI.get(model.optimizer, attr) - else - if F === typeof(model.original_objective_function) - return model.original_objective_function - else - throw(InexactError) - end - 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 +# TODO: cleanup # In the AbstractBridgeOptimizer, we collect all the possible constraint types and them filter with NumberOfConstraints. # If NumberOfConstraints is zero then we remove it from the list. -# Here, you can look over keys(quadratic_added_cache) and add the F-S types of all the keys in constraints. +# Here, you can look over keys(quadratic_outer_to_inner) and add the F-S types of all the keys in constraints. # To implement NumberOfConstraints, you call NumberOfConstraints to the inner optimizer. -# Then you remove the number of constraints of that that in values(quadratic_added_cache) +# Then you remove the number of constraints of that that in values(quadratic_outer_to_inner) function MOI.get(model::Optimizer, ::MOI.ListOfConstraintTypesPresent) inner_ctrs = MOI.get(model.optimizer, MOI.ListOfConstraintTypesPresent()) if !has_quadratic_constraint_caches(model) return inner_ctrs end - cache_keys = collect(keys(model.quadratic_added_cache)) + cache_keys = collect(keys(model.quadratic_outer_to_inner)) constraints = Set{Tuple{DataType,DataType}}() for (F, S) in inner_ctrs @@ -751,7 +749,8 @@ function MOI.get(model::Optimizer, ::MOI.ListOfConstraintTypesPresent) for type in typeof.(cache_keys[cache_map_check]) push!(constraints, (type.parameters[1], type.parameters[2])) end - # If not all the constraints are chached then also push the original type + # If not all the constraints are cached then also push the original type + # since there was a function with no parameters of that type if !all(cache_map_check) push!(constraints, (F, S)) end @@ -791,23 +790,15 @@ function MOI.get( if MOI.supports_constraint(model.optimizer, F, S) inner_index = MOI.get(model.optimizer, MOI.ListOfConstraintIndices{F,S}()) - if !has_quadratic_constraint_caches(model) - return inner_index - end + end + if !has_quadratic_constraint_caches(model) + return inner_index end - quadratic_caches = [ - :quadratic_constraint_cache_pc, - :quadratic_constraint_cache_pp, - :quadratic_constraint_cache_pv, - # JD: Check if this applies here - # :quadratic_constraint_variables_associated_to_parameters_cache - ] - - for field in quadratic_caches - cache = getfield(model, field) - push!(inner_index, keys(cache)...) + for key in keys(model.quadratic_outer_to_inner) + push!(inner_index, key) end + return inner_index end @@ -847,7 +838,7 @@ function MOI.add_constraint( f::MOI.VariableIndex, set::MOI.AbstractScalarSet, ) - if is_parameter_in_model(model, f) + if !is_variable(f) error("Cannot constrain a parameter") elseif !is_variable_in_model(model, f) error("Variable not in the model") @@ -860,82 +851,70 @@ function add_constraint_with_parameters_on_function( f::MOI.ScalarAffineFunction{T}, set::S, ) where {T,S} - vars, params, param_constant = - separate_possible_terms_and_calculate_parameter_constant(model, f.terms) - model.last_affine_added += 1 + pf = ParametricAffineFunction(f) + cache_set_constant!(pf, set) if model.constraints_interpretation == ONLY_BOUNDS - if (length(vars) == 1) && isone(MOI.coefficient(vars[1])) - poi_ci = - add_vi_constraint(model, vars, params, param_constant, f, set) + 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, vars, params, param_constant, f, set) + poi_ci = add_saf_constraint(model, pf, set) elseif model.constraints_interpretation == BOUNDS_AND_CONSTRAINTS - if (length(vars) == 1) && isone(MOI.coefficient(vars[1])) - poi_ci = - add_vi_constraint(model, vars, params, param_constant, f, set) + 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, vars, params, param_constant, f, set) + poi_ci = add_saf_constraint(model, pf, set) end end - if model.save_original_objective_and_constraints - model.original_constraint_function_and_set_cache[poi_ci] = (f, set) - end return poi_ci end function add_saf_constraint( model::Optimizer, - vars::Vector{MOI.ScalarAffineTerm{T}}, - params::Vector{MOI.ScalarAffineTerm{T}}, - param_constant::T, - f::MOI.ScalarAffineFunction{T}, + pf::ParametricAffineFunction{T}, set::S, ) where {T,S} - moi_ci = MOI.Utilities.normalize_and_add_constraint( + update_cache!(pf, model) + inner_ci = MOI.Utilities.normalize_and_add_constraint( model.optimizer, - MOI.ScalarAffineFunction(vars, f.constant + param_constant), - set, + MOI.ScalarAffineFunction{T}(pf.v, 0.0), + set_with_new_constant(set, pf.current_constant), ) - poi_ci = create_new_poi_ci_and_save_affine_caches(model, params, moi_ci) - return poi_ci + 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.outer_to_inner_map[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, - vars::Vector{MOI.ScalarAffineTerm{T}}, - params::Vector{MOI.ScalarAffineTerm{T}}, - param_constant::T, - f::MOI.ScalarAffineFunction{T}, + pf::ParametricAffineFunction{T}, set::S, ) where {T,S} - moi_ci = MOI.Utilities.normalize_and_add_constraint( + update_cache!(pf, model) + inner_ci = MOI.Utilities.normalize_and_add_constraint( model.optimizer, - MOI.VariableIndex(vars[1].variable.value), - update_constant!(set, f.constant + param_constant), + pf.v[].variable, + set_with_new_constant(set, pf.current_constant), ) - poi_ci = create_new_poi_ci_and_save_affine_caches(model, params, moi_ci) - return poi_ci -end - -function create_new_poi_ci_and_save_affine_caches( - model::Optimizer, - params::Vector{MOI.ScalarAffineTerm{T}}, - moi_ci::MOI.ConstraintIndex{F,S}, -) where {T,F,S} - poi_ci = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}( + model.last_affine_added += 1 + outer_ci = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}( model.last_affine_added, ) - model.affine_constraint_cache[poi_ci] = params - model.affine_constraint_cache_set[poi_ci] = - MOI.get(model.optimizer, MOI.ConstraintSet(), moi_ci) - model.affine_added_cache[poi_ci] = moi_ci - return poi_ci + model.affine_outer_to_inner[outer_ci] = inner_ci + # model.outer_to_inner_map[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( @@ -943,7 +922,7 @@ function MOI.add_constraint( f::MOI.ScalarAffineFunction{T}, set::MOI.AbstractScalarSet, ) where {T} - if !function_has_parameters(model, f) + if !function_has_parameters(f) return MOI.add_constraint(model.optimizer, f, set) else return add_constraint_with_parameters_on_function(model, f, set) @@ -976,7 +955,7 @@ function MOI.set( model::Optimizer, attr::MOI.AbstractVariableAttribute, v::MOI.VariableIndex, - val::Float64, + val, ) if is_variable_in_model(model, v) MOI.set(model.optimizer, attr, v, val) @@ -1117,11 +1096,10 @@ function MOI.set( return model.constraints_interpretation = value end -function empty_objective_function_caches!(model::Optimizer) - empty!(model.affine_objective_cache) - empty!(model.quadratic_objective_cache_pv) - empty!(model.quadratic_objective_cache_pp) - empty!(model.quadratic_objective_cache_pc) +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 @@ -1132,24 +1110,48 @@ function MOI.set( ) where {T} # clear previously defined objetive function cache empty_objective_function_caches!(model) - if !function_has_parameters(model, f) + if !function_has_parameters(f) MOI.set(model.optimizer, attr, f) else - vars, params, param_constant = - separate_possible_terms_and_calculate_parameter_constant( - model, - f.terms, - ) + 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, - attr, - MOI.ScalarAffineFunction(vars, f.constant + param_constant), + MOI.ObjectiveFunction{( + is_affine(func) ? MOI.ScalarAffineFunction{T} : + MOI.ScalarQuadraticFunction{T} + )}(), + # func, + ( + is_affine(func) ? + MOI.ScalarAffineFunction(func.affine_terms, func.constant) : + func + ), ) - model.affine_objective_cache = params - end - if model.save_original_objective_and_constraints - model.original_objective_function = f + model.quadratic_objective_cache = pf end + MOI.set(model.original_objective_cache, attr, f) return end @@ -1158,15 +1160,14 @@ function MOI.set( attr::MOI.ObjectiveFunction, v::MOI.VariableIndex, ) - if is_parameter_in_model(model, v) + 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 - if model.save_original_objective_and_constraints - model.original_objective_function = v - end - return MOI.set(model.optimizer, attr, model.variables[v]) + MOI.set(model.optimizer, attr, model.variables[v]) + MOI.set(model.original_objective_cache, attr, v) + return end function MOI.set( @@ -1216,7 +1217,7 @@ function MOI.get( T, S<:MOI.AbstractScalarSet, } - moi_ci = get(model.affine_added_cache, c, c) + moi_ci = get(model.affine_outer_to_inner, c, c) return MOI.get(model.optimizer, attr, moi_ci) end @@ -1238,8 +1239,8 @@ function MOI.add_constraint( model::Optimizer, f::MOI.VectorOfVariables, set::MOI.AbstractVectorSet, -) where {T} - if function_has_parameters(model, f) +) + if function_has_parameters(f) error("VectorOfVariables does not allow parameters") end return MOI.add_constraint(model.optimizer, f, set) @@ -1250,7 +1251,7 @@ function MOI.add_constraint( f::MOI.VectorAffineFunction{T}, set::MOI.AbstractVectorSet, ) where {T} - if !function_has_parameters(model, f) + if !function_has_parameters(f) return MOI.add_constraint(model.optimizer, f, set) else return add_constraint_with_parameters_on_function(model, f, set) @@ -1262,78 +1263,57 @@ function add_constraint_with_parameters_on_function( f::MOI.VectorAffineFunction{T}, set::MOI.AbstractVectorSet, ) where {T} - vars, params, param_constants = - separate_possible_terms_and_calculate_parameter_constant(model, f, set) - ci = MOI.add_constraint( - model.optimizer, - MOI.VectorAffineFunction(vars, f.constants + param_constants), - set, - ) - model.vector_constraint_cache[ci] = params - if model.save_original_objective_and_constraints - model.original_constraint_function_and_set_cache[ci] = (f, set) - end - return ci + 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 + return inner_ci end function add_constraint_with_parameters_on_function( model::Optimizer, f::MOI.ScalarQuadraticFunction{T}, - set::S, + s::S, ) where {T,S<:MOI.AbstractScalarSet} - ( - quad_vars, - quad_aff_vars, - quad_params, - aff_terms, - variables_associated_to_parameters, - quad_param_constant, - ) = separate_possible_terms_and_calculate_parameter_constant( - model, - f.quadratic_terms, - ) - - ( - aff_vars, - aff_params, - terms_with_variables_associated_to_parameters, - aff_param_constant, - ) = separate_possible_terms_and_calculate_parameter_constant( - model, - f.affine_terms, - variables_associated_to_parameters, - ) - - aff_terms = vcat(aff_terms, aff_vars) - const_term = f.constant + aff_param_constant + quad_param_constant - quad_terms = quad_vars - f_quad = if !isempty(quad_vars) - MOI.ScalarQuadraticFunction(quad_terms, aff_terms, const_term) + 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.outer_to_inner_map[outer_ci] = inner_ci else - MOI.ScalarAffineFunction(aff_terms, const_term) - end - model.last_quad_add_added += 1 - ci = - MOI.Utilities.normalize_and_add_constraint(model.optimizer, f_quad, set) - # This part is used to remember that ci came from a quadratic function - # It is particularly useful because sometimes the constraint mutates - new_ci = MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},S}( - model.last_quad_add_added, - ) - model.quadratic_added_cache[new_ci] = ci - if model.save_original_objective_and_constraints - model.original_constraint_function_and_set_cache[new_ci] = (f, set) + 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.outer_to_inner_map[outer_ci] = inner_ci end - fill_quadratic_constraint_caches!( - model, - new_ci, - quad_aff_vars, - quad_params, - aff_params, - terms_with_variables_associated_to_parameters, - ci, - ) - return new_ci + model.quadratic_constraint_cache[inner_ci] = pf + model.quadratic_constraint_cache_set[inner_ci] = s + return outer_ci end function MOI.add_constraint( @@ -1341,7 +1321,7 @@ function MOI.add_constraint( f::MOI.ScalarQuadraticFunction{T}, set::MOI.AbstractScalarSet, ) where {T} - if !function_has_parameters(model, f) + if !function_has_parameters(f) return MOI.add_constraint(model.optimizer, f, set) else return add_constraint_with_parameters_on_function(model, f, set) @@ -1351,6 +1331,7 @@ 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) return end @@ -1368,14 +1349,20 @@ end function MOI.delete( model::Optimizer, c::MOI.ConstraintIndex{F,S}, -) where { - F<:Union{MOI.VariableIndex,MOI.VectorOfVariables,MOI.VectorAffineFunction}, - S<:MOI.AbstractSet, -} +) where {F<:Union{MOI.VariableIndex,MOI.VectorOfVariables},S<:MOI.AbstractSet} MOI.delete(model.optimizer, 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) + deleteat!(model.vector_affine_constraint_cache, c) + return +end + function MOI.is_valid( model::Optimizer, c::MOI.ConstraintIndex{F,S}, @@ -1415,86 +1402,24 @@ function _evaluate_parametric_expression( return evaluated_parameter_expression end -function set_quadratic_product_in_obj!(model::Optimizer) - f = model.original_objective_function - F = typeof(f) +function set_quadratic_product_in_obj!(model::Optimizer{T}) where {T} + n = length(model.quadratic_objective_cache_product) - if F <: MOI.VariableIndex - if f == MOI.VariableIndex(-1) - aff_vars = MOI.ScalarAffineTerm{Float64}[] - else - aff_vars = - MOI.ScalarAffineTerm{Float64}[MOI.ScalarAffineTerm{Float64}( - one(Float64), - f, - )] - end - aff_params = MOI.ScalarAffineTerm{Float64}[] - terms_with_variables_associated_to_parameters = - MOI.ScalarAffineTerm{Float64}[] - aff_param_constant = zero(Float64) - quad_vars = MOI.ScalarQuadraticTerm{Float64}[] - quad_aff_vars = MOI.ScalarQuadraticTerm{Float64}[] - quad_params = MOI.ScalarQuadraticTerm{Float64}[] - aff_terms = MOI.ScalarAffineTerm{Float64}[] - variables_associated_to_parameters = MOI.VariableIndex[] - quad_param_constant = zero(Float64) - constant = zero(Float64) - elseif F <: MOI.ScalarAffineFunction - num_vars, num_params = count_scalar_affine_terms_types(model, f.terms) - if num_vars == 0 && num_params == 0 - aff_vars = MOI.ScalarAffineTerm{Float64}[] - aff_params = MOI.ScalarAffineTerm{Float64}[] - terms_with_variables_associated_to_parameters = - MOI.ScalarAffineTerm{Float64}[] - aff_param_constant = zero(Float64) - else - (aff_vars, aff_params, aff_param_constant) = - separate_possible_terms_and_calculate_parameter_constant( - model, - f.terms, - ) - end - quad_vars = MOI.ScalarQuadraticTerm{Float64}[] - quad_aff_vars = MOI.ScalarQuadraticTerm{Float64}[] - quad_params = MOI.ScalarQuadraticTerm{Float64}[] - aff_terms = MOI.ScalarAffineTerm{Float64}[] - variables_associated_to_parameters = MOI.VariableIndex[] - quad_param_constant = zero(Float64) - constant = f.constant - elseif F <: MOI.ScalarQuadraticFunction - ( - quad_vars, - quad_aff_vars, - quad_params, - aff_terms, - variables_associated_to_parameters, - quad_param_constant, - ) = separate_possible_terms_and_calculate_parameter_constant( - model, - f.quadratic_terms, - ) - - ( - aff_vars, - aff_params, - terms_with_variables_associated_to_parameters, - aff_param_constant, - ) = separate_possible_terms_and_calculate_parameter_constant( - model, - f.affine_terms, - variables_associated_to_parameters, - ) - constant = f.constant + 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) - aff_terms = vcat(aff_terms, aff_vars) - - quadratic_prods_vector = MOI.ScalarQuadraticTerm{Float64}[] + quadratic_prods_vector = MOI.ScalarQuadraticTerm{T}[] + sizehint!(quadratic_prods_vector, n) - dict_vars_quad_prod = model.quadratic_objective_cache_product - for (prod_var, fparam) in dict_vars_quad_prod - x, y = prod_var + 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, @@ -1502,18 +1427,44 @@ function set_quadratic_product_in_obj!(model::Optimizer) ) end - quad_vars = vcat(quad_vars, quadratic_prods_vector) - const_term = constant + aff_param_constant + quad_param_constant + 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{Float64}}(), - MOI.ScalarQuadraticFunction(quad_vars, aff_terms, const_term), + 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, @@ -1526,6 +1477,7 @@ function MOI.set( x2 = aux end model.quadratic_objective_cache_product[(x1, x2)] = f_param + model.quadratic_objective_cache_product_changed = true return end @@ -1533,7 +1485,7 @@ function MOI.get( model::Optimizer, ::QuadraticObjectiveCoef, (x1, x2)::Tuple{MOI.VariableIndex,MOI.VariableIndex}, -) where {T} +) if x1.value > x2.value aux = x1 x1 = x2 @@ -1550,69 +1502,6 @@ function MOI.get( end end -function MOI.set( - model::Optimizer, - attr::MOI.ObjectiveFunction, - f::MOI.ScalarQuadraticFunction{T}, -) where {T} - # clear previously defined objetive function cache - empty_objective_function_caches!(model) - if model.save_original_objective_and_constraints - model.original_objective_function = f - end - if !function_has_parameters(model, f) - MOI.set(model.optimizer, attr, f) - return - end - ( - quad_vars, - quad_aff_vars, - quad_params, - aff_terms, - variables_associated_to_parameters, - quad_param_constant, - ) = separate_possible_terms_and_calculate_parameter_constant( - model, - f.quadratic_terms, - ) - - ( - aff_vars, - aff_params, - terms_with_variables_associated_to_parameters, - aff_param_constant, - ) = separate_possible_terms_and_calculate_parameter_constant( - model, - f.affine_terms, - variables_associated_to_parameters, - ) - - aff_terms = vcat(aff_terms, aff_vars) - const_term = f.constant + aff_param_constant + quad_param_constant - - if !isempty(quad_vars) - MOI.set( - model.optimizer, - attr, - MOI.ScalarQuadraticFunction(quad_vars, aff_terms, const_term), - ) - else - MOI.set( - model.optimizer, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}(), - MOI.ScalarAffineFunction(aff_terms, const_term), - ) - end - - model.quadratic_objective_cache_pv = quad_aff_vars - model.quadratic_objective_cache_pp = quad_params - model.quadratic_objective_cache_pc = aff_params - model.quadratic_objective_variables_associated_to_parameters_cache = - terms_with_variables_associated_to_parameters - - return -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.") @@ -1662,16 +1551,16 @@ function _poi_default_copy_to(dest::T, src::MOI.ModelLike) where {T} end function MOI.Utilities.default_copy_to( - dest::MOI.Bridges.LazyBridgeOptimizer{Optimizer{Float64,T}}, + dest::MOI.Bridges.LazyBridgeOptimizer{Optimizer{T,OT}}, src::MOI.ModelLike, -) where {T} +) where {T,OT} return _poi_default_copy_to(dest, src) end function MOI.Utilities.default_copy_to( - dest::Optimizer{Float64,T}, + dest::Optimizer{T,OT}, src::MOI.ModelLike, -) where {T} +) where {T,OT} return _poi_default_copy_to(dest, src) end @@ -1679,7 +1568,11 @@ function MOI.optimize!(model::Optimizer) if !isempty(model.updated_parameters) update_parameters!(model) end - if !isempty(model.quadratic_objective_cache_product) + 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) @@ -1687,7 +1580,7 @@ function MOI.optimize!(model::Optimizer) model.evaluate_duals @warn "Dual solution not available, ignoring `evaluate_duals`" elseif model.evaluate_duals - calculate_dual_of_parameters(model) + compute_dual_of_parameters!(model) end return end diff --git a/src/duals.jl b/src/duals.jl index 464839cc..5b368d04 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -3,154 +3,95 @@ # 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 create_param_dual_cum_sum(model::Optimizer) - return zeros(model.number_of_parameters_in_model) -end - -function calculate_dual_of_parameters(model::Optimizer) - param_dual_cum_sum = create_param_dual_cum_sum(model) - update_duals_with_affine_constraint_cache!(param_dual_cum_sum, model) - update_duals_with_quadratic_constraint_cache!(param_dual_cum_sum, model) - update_duals_in_affine_objective!( - param_dual_cum_sum, - model.affine_objective_cache, - ) - update_duals_in_quadratic_objective!( - param_dual_cum_sum, - model.quadratic_objective_cache_pc, - ) - empty_and_feed_duals_to_model(model, param_dual_cum_sum) - return model +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_quadratic_constraints!(model) + if model.affine_objective_cache !== nothing + 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) + end + return end -function update_duals_with_affine_constraint_cache!( - param_dual_cum_sum::Vector{Float64}, - 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] - affine_added_cache_inner = model.affine_added_cache[F, S] - if !isempty(affine_constraint_cache_inner) - update_duals_with_affine_constraint_cache!( - param_dual_cum_sum, - model.optimizer, - affine_constraint_cache_inner, - affine_added_cache_inner, - ) - end + # barrier for type instability + compute_parameters_in_ci!(model, affine_constraint_cache_inner) end return end -function update_duals_with_affine_constraint_cache!( - param_dual_cum_sum::Vector{Float64}, - optimizer::OT, - affine_constraint_cache_inner::MOI.Utilities.DoubleDicts.DoubleDictInner{ - F, - S, - V1, - }, - affine_added_cache_inner::MOI.Utilities.DoubleDicts.DoubleDictInner{F,S,V2}, -) where {OT,F,S,V1,V2} - for (ci, param_array) in affine_constraint_cache_inner - calculate_parameters_in_ci!( - param_dual_cum_sum, - optimizer, - param_array, - affine_added_cache_inner[ci], - ) +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) end return end -function update_duals_with_quadratic_constraint_cache!( - param_dual_cum_sum::Vector{Float64}, - model::Optimizer, -) - for (F, S) in keys(model.quadratic_constraint_cache_pc.dict) - quadratic_constraint_cache_pc_inner = - model.quadratic_constraint_cache_pc[F, S] - if !isempty(quadratic_constraint_cache_pc_inner) - update_duals_with_quadratic_constraint_cache!( - param_dual_cum_sum, - model, - quadratic_constraint_cache_pc_inner, - ) - end +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] + # barrier for type instability + compute_parameters_in_ci!(model, quadratic_constraint_cache_inner) end return end -function update_duals_with_quadratic_constraint_cache!( - param_dual_cum_sum::Vector{Float64}, - model::Optimizer, - quadratic_constraint_cache_pc_inner::MOI.Utilities.DoubleDicts.DoubleDictInner{ - F, - S, - V, - }, -) where {F,S,V} - for (poi_ci, param_array) in quadratic_constraint_cache_pc_inner - moi_ci = model.quadratic_added_cache[poi_ci] - calculate_parameters_in_ci!( - param_dual_cum_sum, - model.optimizer, - param_array, - moi_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) end return end -function calculate_parameters_in_ci!( - param_dual_cum_sum::Vector{Float64}, - optimizer::OT, - param_array::Vector{MOI.ScalarAffineTerm{T}}, - ci::CI, -) where {OT,CI,T} - cons_dual = MOI.get(optimizer, MOI.ConstraintDual(), ci) - - for param in param_array - param_dual_cum_sum[param.variable.value-PARAMETER_INDEX_THRESHOLD] -= - cons_dual * param.coefficient +function compute_parameters_in_ci!( + model::Optimizer{T}, + pf, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} where {T} + cons_dual = MOI.get(model.optimizer, MOI.ConstraintDual(), ci) + for term in pf.p + model.dual_value_of_parameters[p_val(term.variable)] -= + cons_dual * term.coefficient end return end -function update_duals_in_affine_objective!( - param_dual_cum_sum::Vector{Float64}, - affine_objective_cache::Vector{MOI.ScalarAffineTerm{T}}, -) where {T} - for param in affine_objective_cache - param_dual_cum_sum[param.variable.value-PARAMETER_INDEX_THRESHOLD] -= - param.coefficient +function compute_parameters_in_ci!( + model::Optimizer{T}, + pf::ParametricVectorAffineFunction{T}, + ci::MOI.ConstraintIndex{F,S}, +) where {F<:MOI.VectorAffineFunction{T},S} where {T} + cons_dual = MOI.get(model.optimizer, MOI.ConstraintDual(), ci) + for term in pf.p + model.dual_value_of_parameters[p_val(term.scalar_term.variable)] -= + cons_dual[term.output_index] * term.scalar_term.coefficient end return end -function update_duals_in_quadratic_objective!( - param_dual_cum_sum::Vector{Float64}, - quadratic_objective_cache_pc::Vector{MOI.ScalarAffineTerm{T}}, -) where {T} - for param in quadratic_objective_cache_pc - param_dual_cum_sum[param.variable.value-PARAMETER_INDEX_THRESHOLD] -= +# this one seem to be the same as the next +function update_duals_from_objective!(model::Optimizer{T}, pf) where {T} + for param in pf.p + model.dual_value_of_parameters[p_val(param.variable)] -= param.coefficient end return end -function empty_and_feed_duals_to_model( - model::Optimizer, - param_dual_cum_sum::Vector{Float64}, -) - empty!(model.dual_value_of_parameters) - model.dual_value_of_parameters = zeros(model.number_of_parameters_in_model) - for (vi_val_minus_threshold, param_dual) in enumerate(param_dual_cum_sum) - model.dual_value_of_parameters[vi_val_minus_threshold] = param_dual - end - return -end - """ ParameterDual <: MOI.AbstractVariableAttribute @@ -171,9 +112,9 @@ function MOI.get(model::Optimizer, ::ParameterDual, v::MOI.VariableIndex) model, MOI.ConstraintIndex{MOI.VariableIndex,Parameter}(v.value), ) - error("Cannot calculate the dual of a multiplicative parameter") + error("Cannot compute the dual of a multiplicative parameter") end - return model.dual_value_of_parameters[v.value-PARAMETER_INDEX_THRESHOLD] + return model.dual_value_of_parameters[p_val(v)] end function MOI.get( @@ -182,9 +123,9 @@ function MOI.get( cp::MOI.ConstraintIndex{MOI.VariableIndex,Parameter}, ) if !is_additive(model, cp) - error("Cannot calculate the dual of a multiplicative parameter") + error("Cannot compute the dual of a multiplicative parameter") end - return model.dual_value_of_parameters[cp.value-PARAMETER_INDEX_THRESHOLD] + return model.dual_value_of_parameters[p_val(cp)] end function is_additive(model::Optimizer, cp::MOI.ConstraintIndex) diff --git a/src/update_parameters.jl b/src/update_parameters.jl index a82ecc1c..604c4e70 100644 --- a/src/update_parameters.jl +++ b/src/update_parameters.jl @@ -3,489 +3,278 @@ # 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 update_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 update_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 update_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 update_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 -function update_parameter_in_affine_constraints!(model::Optimizer) +# 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) 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] - affine_added_cache_inner = model.affine_added_cache[F, S] if !isempty(affine_constraint_cache_inner) - update_parameter_in_affine_constraints!( - model.optimizer, - model.parameters, - model.updated_parameters, + # barrier to avoid type instability of inner dicts + update_parametric_affine_constraints!( + model, affine_constraint_cache_inner, affine_constraint_cache_set_inner, - affine_added_cache_inner, ) end end - return model + return end -function update_parameter_in_affine_constraints!( - optimizer::OT, - parameters::MOI.Utilities.CleverDicts.CleverDict{ - ParameterIndex, - T, - typeof(MOI.Utilities.CleverDicts.key_to_index), - typeof(MOI.Utilities.CleverDicts.index_to_key), - }, - updated_parameters::MOI.Utilities.CleverDicts.CleverDict{ - ParameterIndex, - T, - typeof(MOI.Utilities.CleverDicts.key_to_index), - typeof(MOI.Utilities.CleverDicts.index_to_key), - }, - affine_constraint_cache_inner::MOI.Utilities.DoubleDicts.DoubleDictInner{ - F, - S, - V1, - }, - affine_constraint_cache_set_inner::MOI.Utilities.DoubleDicts.DoubleDictInner{ +# TODO: cache changes and then batch them instead + +function update_parametric_affine_constraints!( + model::Optimizer, + affine_constraint_cache_inner::DoubleDictInner{F,S,V}, + affine_constraint_cache_set_inner::DoubleDictInner{ F, S, MOI.AbstractScalarSet, }, - affine_added_cache_inner::MOI.Utilities.DoubleDicts.DoubleDictInner{F,S,V2}, -) where {OT,T,F,S,V1,V2} - for (ci, param_array) in affine_constraint_cache_inner - new_set = update_parameter_in_affine_constraints!( - optimizer, - affine_added_cache_inner[ci], - param_array, - parameters, - updated_parameters, - affine_constraint_cache_set_inner[ci], - ) - affine_constraint_cache_set_inner[ci] = new_set - end - return optimizer -end - -function update_parameter_in_affine_constraints!( - optimizer::OT, - ci::CI, - param_array::Vector{MOI.ScalarAffineTerm{T}}, - parameters::MOI.Utilities.CleverDicts.CleverDict{ - ParameterIndex, - T, - typeof(MOI.Utilities.CleverDicts.key_to_index), - typeof(MOI.Utilities.CleverDicts.index_to_key), - }, - updated_parameters::MOI.Utilities.CleverDicts.CleverDict{ - ParameterIndex, - T, - typeof(MOI.Utilities.CleverDicts.key_to_index), - typeof(MOI.Utilities.CleverDicts.index_to_key), - }, - set::S, -) where {OT,T,CI,S} - param_constant = zero(T) - for term in param_array - if !isnan(updated_parameters[p_idx(term.variable)]) - param_constant += - term.coefficient * ( - updated_parameters[p_idx(term.variable)] - - parameters[p_idx(term.variable)] - ) +) where {F,S<:SIMPLE_SCALAR_SETS{T},V} where {T} + # cis = MOI.ConstraintIndex{F,S}[] + # sets = S[] + # 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) + 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) + MOI.set(model.optimizer, MOI.ConstraintSet(), inner_ci, new_set) + # push!(cis, inner_ci) + # push!(sets, new_set) end end - if param_constant != zero(Float64) - new_set = update_constant!(set, param_constant) - MOI.set(optimizer, MOI.ConstraintSet(), ci, new_set) - return new_set - end - return set + # if !isempty(cis) + # MOI.set(model.optimizer, MOI.ConstraintSet(), cis, sets) + # end + return end -function update_parameter_in_affine_objective!(model::Optimizer) - if !isempty(model.affine_objective_cache) - objective_constant = zero(Float64) - for term in model.affine_objective_cache - if !isnan(model.updated_parameters[p_idx(term.variable)]) - param_old = model.parameters[p_idx(term.variable)] - param_new = model.updated_parameters[p_idx(term.variable)] - aux = param_new - param_old - objective_constant += term.coefficient * aux - end - end - if !iszero(objective_constant) - F = MOI.get(model.optimizer, MOI.ObjectiveFunctionType()) - f = MOI.get(model.optimizer, MOI.ObjectiveFunction{F}()) - MOI.modify( - model.optimizer, - MOI.ObjectiveFunction{F}(), - MOI.ScalarConstantChange(f.constant + objective_constant), - ) +function update_parametric_affine_constraints!( + model::Optimizer, + affine_constraint_cache_inner::DoubleDictInner{F,S,V}, + affine_constraint_cache_set_inner::DoubleDictInner{ + F, + S, + MOI.AbstractScalarSet, + }, +) 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) + 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 + MOI.set(model.optimizer, MOI.ConstraintSet(), inner_ci, new_set) end end - return model + return end -function update_parameter_in_quadratic_constraints_pc!(model::Optimizer) - for (ci, fparam) in model.quadratic_constraint_cache_pc - param_constant = zero(Float64) - for term in fparam - if !isnan(model.updated_parameters[p_idx(term.variable)]) - param_constant += - term.coefficient * ( - model.updated_parameters[p_idx(term.variable)] - - model.parameters[p_idx(term.variable)] - ) - end - end - if param_constant != zero(Float64) - old_set = model.quadratic_constraint_cache_pc_set[ci] - new_set = update_constant!(old_set, param_constant) - MOI.set( - model.optimizer, - MOI.ConstraintSet(), - model.quadratic_added_cache[ci], - new_set, +function update_parametric_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!( + model, + vector_affine_constraint_cache_inner, ) - model.quadratic_constraint_cache_pc_set[ci] = new_set end end + return end -function update_parameter_in_quadratic_objective_pc!(model::Optimizer) - if !isempty(model.quadratic_objective_cache_pc) - objective_constant = zero(Float64) - for term in model.quadratic_objective_cache_pc - if !isnan(model.updated_parameters[p_idx(term.variable)]) - param_old = model.parameters[p_idx(term.variable)] - param_new = model.updated_parameters[p_idx(term.variable)] - aux = param_new - param_old - objective_constant += term.coefficient * aux - end - end - if !iszero(objective_constant) - F = MOI.get(model.optimizer, MOI.ObjectiveFunctionType()) - f = MOI.get(model.optimizer, MOI.ObjectiveFunction{F}()) +function update_parametric_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) + if !iszero(delta_constant) + pf.current_constant .+= delta_constant MOI.modify( model.optimizer, - MOI.ObjectiveFunction{F}(), - MOI.ScalarConstantChange(f.constant + objective_constant), + inner_ci, + MOI.VectorConstantChange(pf.current_constant), ) end end - return model + return end -function update_parameter_in_quadratic_constraints_pp!(model::Optimizer) - for (ci, fparam) in model.quadratic_constraint_cache_pp - param_constant = zero(Float64) - for term in fparam - if !isnan(model.updated_parameters[p_idx(term.variable_1)]) && - !isnan(model.updated_parameters[p_idx(term.variable_2)]) - param_constant += - term.coefficient * ( - ( - model.updated_parameters[p_idx(term.variable_1)] * - model.updated_parameters[p_idx(term.variable_2)] - ) - ( - model.parameters[p_idx(term.variable_1)] * - model.parameters[p_idx(term.variable_2)] - ) - ) - elseif !isnan(model.updated_parameters[p_idx(term.variable_1)]) - param_constant += - term.coefficient * - model.parameters[p_idx(term.variable_2)] * - ( - model.updated_parameters[p_idx(term.variable_1)] - - model.parameters[p_idx(term.variable_1)] - ) - elseif !isnan(model.updated_parameters[p_idx(term.variable_2)]) - param_constant += - term.coefficient * - model.parameters[p_idx(term.variable_1)] * - ( - model.updated_parameters[p_idx(term.variable_2)] - - model.parameters[p_idx(term.variable_2)] - ) - end - end - if param_constant != zero(Float64) - old_set = model.quadratic_constraint_cache_pp_set[ci] - new_set = update_constant!(old_set, param_constant) - MOI.set( - model.optimizer, - MOI.ConstraintSet(), - model.quadratic_added_cache[ci], - new_set, +function update_parametric_quadratic_constraints!(model::Optimizer) + for (F, S) in keys(model.quadratic_constraint_cache.dict) + quadratic_constraint_cache_inner = + model.quadratic_constraint_cache[F, S] + quadratic_constraint_cache_set_inner = + 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!( + model, + quadratic_constraint_cache_inner, + quadratic_constraint_cache_set_inner, ) - model.quadratic_constraint_cache_pp_set[ci] = new_set end end - return model + return end -function update_parameter_in_quadratic_objective_pp!(model::Optimizer) - if !isempty(model.quadratic_objective_cache_pp) - objective_constant = zero(Float64) - for term in model.quadratic_objective_cache_pp - if !isnan(model.updated_parameters[p_idx(term.variable_1)]) && - !isnan(model.updated_parameters[p_idx(term.variable_2)]) - objective_constant += - term.coefficient * ( - ( - model.updated_parameters[p_idx(term.variable_1)] * - model.updated_parameters[p_idx(term.variable_2)] - ) - ( - model.parameters[p_idx(term.variable_1)] * - model.parameters[p_idx(term.variable_2)] - ) - ) - elseif !isnan(model.updated_parameters[p_idx(term.variable_1)]) - objective_constant += - term.coefficient * - model.parameters[p_idx(term.variable_2)] * - ( - model.updated_parameters[p_idx(term.variable_1)] - - model.parameters[p_idx(term.variable_1)] - ) - elseif !isnan(model.updated_parameters[p_idx(term.variable_2)]) - objective_constant += - term.coefficient * - model.parameters[p_idx(term.variable_1)] * - ( - model.updated_parameters[p_idx(term.variable_2)] - - model.parameters[p_idx(term.variable_2)] - ) - end - end - if objective_constant != zero(Float64) - F = MOI.get(model.optimizer, MOI.ObjectiveFunctionType()) - f = MOI.get(model.optimizer, MOI.ObjectiveFunction{F}()) - MOI.modify( - model.optimizer, - MOI.ObjectiveFunction{F}(), - MOI.ScalarConstantChange(f.constant + objective_constant), - ) - end +function affine_build_change_and_up_param_func( + pf::ParametricQuadraticFunction{T}, + delta_terms, +) where {T} + changes = Vector{MOI.ScalarCoefficientChange}(undef, length(delta_terms)) + i = 1 + for (var, coef) in delta_terms + base_coef = pf.current_terms_with_p[var] + new_coef = base_coef + coef + pf.current_terms_with_p[var] = new_coef + changes[i] = MOI.ScalarCoefficientChange(var, new_coef) + i += 1 end - return model + return changes end -function update_parameter_in_quadratic_constraints_pv!(model::Optimizer) - for (ci, quad_aff_vars) in model.quadratic_constraint_cache_pv - # We need this dictionary because we could have terms like - # p_1 * v_1 + p_2 * v_1 and we should add p_1 and p_2 on the update - new_coeff_per_variable = Dict{MOI.VariableIndex,Float64}() - for term in quad_aff_vars - # Here we use the convention that the parameter always comes as the first variables - # in the caches - if !isnan(model.updated_parameters[p_idx(term.variable_1)]) - coef = term.coefficient - param_new = model.updated_parameters[p_idx(term.variable_1)] - if haskey(new_coeff_per_variable, term.variable_2) - new_coeff_per_variable[term.variable_2] += param_new * coef - else - new_coeff_per_variable[term.variable_2] = param_new * coef - end - end - end - if haskey( - model.quadratic_constraint_variables_associated_to_parameters_cache, - ci, - ) - for aff_term in - model.quadratic_constraint_variables_associated_to_parameters_cache[ci] - coef = aff_term.coefficient - if haskey(new_coeff_per_variable, aff_term.variable) - new_coeff_per_variable[aff_term.variable] += coef - else - new_coeff_per_variable[aff_term.variable] = coef - end - end +function update_parametric_quadratic_constraints!( + model::Optimizer, + quadratic_constraint_cache_inner::DoubleDictInner{F,S,V}, + quadratic_constraint_cache_set_inner::DoubleDictInner{ + F, + S, + MOI.AbstractScalarSet, + }, +) where {F,S<:SIMPLE_SCALAR_SETS{T},V} where {T} + # cis = MOI.ConstraintIndex{F,S}[] + # sets = S[] + # 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) + 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) + MOI.set(model.optimizer, MOI.ConstraintSet(), inner_ci, new_set) + # push!(cis, inner_ci) + # push!(sets, new_set) end - old_ci = model.quadratic_added_cache[ci] - changes = Vector{MOI.ScalarCoefficientChange}( - undef, - length(new_coeff_per_variable), - ) - i = 1 - for (vi, value) in new_coeff_per_variable - changes[i] = MOI.ScalarCoefficientChange(vi, value) - i += 1 + delta_terms = delta_parametric_affine_terms(model, pf) + if !isempty(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 - # Make multiple changes at once. - MOI.modify( - model.optimizer, - fill(old_ci, length(new_coeff_per_variable)), - changes, - ) end - return model + # if !isempty(cis) + # MOI.set(model.optimizer, MOI.ConstraintSet(), cis, sets) + # end + return end -function update_parameter_in_quadratic_objective_pv!(model::Optimizer) - if !isempty(model.quadratic_objective_cache_pv) - # We need this dictionary because we could have terms like - # p_1 * v_1 + p_2 * v_1 and we should add p_1 and p_2 on the update - new_coeff_per_variable = Dict{MOI.VariableIndex,Float64}() - for term in model.quadratic_objective_cache_pv - # Here we use the convention that the parameter always comes as the first variables - # in the caches - if !isnan(model.updated_parameters[p_idx(term.variable_1)]) - coef = term.coefficient - param_new = model.updated_parameters[p_idx(term.variable_1)] - if haskey(new_coeff_per_variable, term.variable_2) - new_coeff_per_variable[term.variable_2] += param_new * coef - else - new_coeff_per_variable[term.variable_2] = param_new * coef - end - end - end - - for aff_term in - model.quadratic_objective_variables_associated_to_parameters_cache - coef = aff_term.coefficient - if haskey(new_coeff_per_variable, aff_term.variable) - new_coeff_per_variable[aff_term.variable] += coef - else - new_coeff_per_variable[aff_term.variable] = coef - end +function update_parametric_quadratic_constraints!( + model::Optimizer, + quadratic_constraint_cache_inner::DoubleDictInner{F,S,V}, + quadratic_constraint_cache_set_inner::DoubleDictInner{ + F, + S, + MOI.AbstractScalarSet, + }, +) 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) + 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 + MOI.set(model.optimizer, MOI.ConstraintSet(), inner_ci, new_set) end - - F_pv = MOI.get(model.optimizer, MOI.ObjectiveFunctionType()) - changes = Vector{MOI.ScalarCoefficientChange}( - undef, - length(new_coeff_per_variable), - ) - i = 1 - for (vi, value) in new_coeff_per_variable - changes[i] = MOI.ScalarCoefficientChange(vi, value) - i += 1 + delta_terms = delta_parametric_affine_terms(model, pf) + if !isempty(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 - MOI.modify(model.optimizer, MOI.ObjectiveFunction{F_pv}(), changes) end - return model + return end -# Vector Affine -function update_parameter_in_vector_affine_constraints!(model::Optimizer) - for (F, S) in keys(model.vector_constraint_cache.dict) - vector_constraint_cache_inner = model.vector_constraint_cache[F, S] - if !isempty(vector_constraint_cache_inner) - update_parameter_in_vector_affine_constraints!( - model.optimizer, - model.parameters, - model.updated_parameters, - vector_constraint_cache_inner, - ) - end +function update_parametric_affine_objective!(model::Optimizer{T}) where {T} + if model.affine_objective_cache === nothing + return end - return model -end - -function update_parameter_in_vector_affine_constraints!( - optimizer::OT, - parameters::MOI.Utilities.CleverDicts.CleverDict{ - ParameterIndex, - T, - typeof(MOI.Utilities.CleverDicts.key_to_index), - typeof(MOI.Utilities.CleverDicts.index_to_key), - }, - updated_parameters::MOI.Utilities.CleverDicts.CleverDict{ - ParameterIndex, - T, - typeof(MOI.Utilities.CleverDicts.key_to_index), - typeof(MOI.Utilities.CleverDicts.index_to_key), - }, - vector_constraint_cache_inner::MOI.Utilities.DoubleDicts.DoubleDictInner{ - F, - S, - V, - }, -) where {OT,T,F,S,V} - for (ci, param_array) in vector_constraint_cache_inner - update_parameter_in_vector_affine_constraints!( - optimizer, - ci, - param_array, - parameters, - updated_parameters, + pf = model.affine_objective_cache + delta_constant = delta_parametric_constant(model, pf) + if !iszero(delta_constant) + pf.current_constant += delta_constant + # F = MOI.get(model.optimizer, MOI.ObjectiveFunctionType()) + MOI.modify( + model.optimizer, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}(), + MOI.ScalarConstantChange(pf.current_constant), ) end - - return optimizer + return end -function update_parameter_in_vector_affine_constraints!( - optimizer::OT, - ci::CI, - param_array::Vector{MOI.VectorAffineTerm{T}}, - parameters::MOI.Utilities.CleverDicts.CleverDict{ - ParameterIndex, - T, - typeof(MOI.Utilities.CleverDicts.key_to_index), - typeof(MOI.Utilities.CleverDicts.index_to_key), - }, - updated_parameters::MOI.Utilities.CleverDicts.CleverDict{ - ParameterIndex, - T, - typeof(MOI.Utilities.CleverDicts.key_to_index), - typeof(MOI.Utilities.CleverDicts.index_to_key), - }, -) where {OT,T,CI} - cf = MOI.get(optimizer, MOI.ConstraintFunction(), ci) - - n_dims = length(cf.constants) - param_constants = zeros(T, n_dims) - - for term in param_array - vi = term.scalar_term.variable - - if !isnan(updated_parameters[p_idx(vi)]) - param_constants[term.output_index] = - term.scalar_term.coefficient * - (updated_parameters[p_idx(vi)] - parameters[p_idx(vi)]) - end +function update_parametric_quadratic_objective!(model::Optimizer{T}) where {T} + if model.quadratic_objective_cache === nothing + return end - - if param_constants != zeros(T, n_dims) + pf = model.quadratic_objective_cache + delta_constant = delta_parametric_constant(model, pf) + if !iszero(delta_constant) + pf.current_constant += delta_constant + F = MOI.get(model.optimizer, MOI.ObjectiveFunctionType()) MOI.modify( - optimizer, - ci, - MOI.VectorConstantChange(cf.constants + param_constants), + model.optimizer, + MOI.ObjectiveFunction{F}(), + MOI.ScalarConstantChange(pf.current_constant), ) end - - return ci + 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) + MOI.modify(model.optimizer, MOI.ObjectiveFunction{F}(), changes) + end + return end function update_parameters!(model::Optimizer) - update_parameter_in_affine_constraints!(model) - update_parameter_in_affine_objective!(model) - update_parameter_in_quadratic_constraints_pc!(model) - update_parameter_in_quadratic_objective_pc!(model) - update_parameter_in_quadratic_constraints_pv!(model) - update_parameter_in_quadratic_objective_pv!(model) - update_parameter_in_quadratic_constraints_pp!(model) - update_parameter_in_quadratic_objective_pp!(model) - update_parameter_in_vector_affine_constraints!(model) + 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 parameters and put NaN to indicate that the parameter has been updated + # Update parameters and put NaN to indicate that the parameter has been + # updated for (parameter_index, val) in model.updated_parameters if !isnan(val) model.parameters[parameter_index] = val @@ -493,5 +282,5 @@ function update_parameters!(model::Optimizer) end end - return model + return end diff --git a/src/utils.jl b/src/utils.jl index a174bbab..c6034b04 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -26,56 +26,46 @@ function is_variable_in_model(model::Optimizer, v::MOI.VariableIndex) end function has_quadratic_constraint_caches(model::Optimizer) - return !isempty(model.quadratic_added_cache) + return !isempty(model.quadratic_outer_to_inner) end -function function_has_parameters( - model::Optimizer, - f::MOI.ScalarAffineFunction{T}, -) where {T} +function function_has_parameters(f::MOI.ScalarAffineFunction{T}) where {T} for term in f.terms - if is_parameter_in_model(model, term.variable) + if is_parameter(term.variable) return true end end return false end -function function_has_parameters(model::Optimizer, f::MOI.VectorOfVariables) +function function_has_parameters(f::MOI.VectorOfVariables) for variable in f.variables - if is_parameter_in_model(model, variable) + if is_parameter(variable) return true end end return false end -function function_has_parameters( - model::Optimizer, - f::MOI.VectorAffineFunction{T}, -) where {T} +function function_has_parameters(f::MOI.VectorAffineFunction{T}) where {T} for term in f.terms - if is_parameter_in_model(model, term.scalar_term.variable) + if is_parameter(term.scalar_term.variable) return true end end return false end -function function_has_parameters( - model::Optimizer, - f::MOI.ScalarQuadraticFunction{T}, -) where {T} - return function_affine_terms_has_parameters(model, f.affine_terms) || - function_quadratic_terms_has_parameters(model, f.quadratic_terms) +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( - model::Optimizer, affine_terms::Vector{MOI.ScalarAffineTerm{T}}, ) where {T} for term in affine_terms - if is_parameter_in_model(model, term.variable) + if is_parameter(term.variable) return true end end @@ -83,300 +73,484 @@ function function_affine_terms_has_parameters( end function function_quadratic_terms_has_parameters( - model::Optimizer, quadratic_terms::Vector{MOI.ScalarQuadraticTerm{T}}, ) where {T} for term in quadratic_terms - if is_parameter_in_model(model, term.variable_1) || - is_parameter_in_model(model, term.variable_2) + if is_parameter(term.variable_1) || is_parameter(term.variable_2) return true end end return false end -function count_scalar_affine_terms_types( - model::Optimizer, - terms::Vector{MOI.ScalarAffineTerm{T}}, -) where {T} - num_vars = count(x -> is_variable_in_model(model, x.variable), terms) - num_params = length(terms) - num_vars - return num_vars, num_params +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( - model::Optimizer, terms::Vector{MOI.ScalarAffineTerm{T}}, - variables_associated_to_parameters::Vector{MOI.VariableIndex}, ) where {T} num_vars = 0 num_params = 0 - num_vars_associated_to_params = 0 for term in terms - if is_variable_in_model(model, term.variable) + if is_variable(term.variable) num_vars += 1 - if term.variable in variables_associated_to_parameters - num_vars_associated_to_params += 1 - end else num_params += 1 end end - return num_vars, num_params, num_vars_associated_to_params + return num_vars, num_params end -function separate_possible_terms_and_calculate_parameter_constant( +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, - terms::Vector{MOI.ScalarAffineTerm{T}}, ) where {T} - num_vars, num_params = count_scalar_affine_terms_types(model, terms) - vars = Vector{MOI.ScalarAffineTerm{T}}(undef, num_vars) - params = Vector{MOI.ScalarAffineTerm{T}}(undef, num_params) - param_constant = zero(T) - i_vars = 1 - i_params = 1 + f.current_constant = parametric_constant(model, f) + return nothing +end + +function parametric_constant( + model::Optimizer, + 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::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_in_model(model, term.variable) - vars[i_vars] = term - i_vars += 1 - elseif is_parameter_in_model(model, term.variable) - params[i_params] = term - param_constant += - term.coefficient * model.parameters[p_idx(term.variable)] - i_params += 1 + if is_variable(term.scalar_term.variable) + num_vars += 1 else - error("Constraint uses a variable that is not in the model") + num_params += 1 end end - return vars, params, param_constant + 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 + else + p[i_p] = term + i_p += 1 + end + end + return v, p end -# This version is used on SQFs -function separate_possible_terms_and_calculate_parameter_constant( +function ParametricVectorAffineFunction( + f::MOI.VectorAffineFunction{T}, +) where {T} + v, p = split_affine_terms(f.terms) + return ParametricVectorAffineFunction{T}( + p, + v, + copy(f.constants), + zeros(T, length(f.constants)), + zeros(T, length(f.constants)), + ) +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, - terms::Vector{MOI.ScalarAffineTerm{T}}, - variables_associated_to_parameters::Vector{MOI.VariableIndex}, ) where {T} - num_vars, num_params, num_vars_associated_to_params = - count_scalar_affine_terms_types( - model, - terms, - variables_associated_to_parameters, - ) - vars = Vector{MOI.ScalarAffineTerm{T}}(undef, num_vars) - params = Vector{MOI.ScalarAffineTerm{T}}(undef, num_params) - terms_with_variables_associated_to_parameters = - Vector{MOI.ScalarAffineTerm{T}}(undef, num_vars_associated_to_params) - param_constant = zero(T) - i_vars = 1 - i_params = 1 - i_vars_associated_to_params = 1 + 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_in_model(model, term.variable) - vars[i_vars] = term - if term.variable in variables_associated_to_parameters - terms_with_variables_associated_to_parameters[i_vars_associated_to_params] = - term - i_vars_associated_to_params += 1 + if is_variable(term.variable_1) + if is_variable(term.variable_2) + num_vv += 1 + else + num_pv += 1 end - i_vars += 1 - elseif is_parameter_in_model(model, term.variable) - params[i_params] = term - param_constant += - term.coefficient * model.parameters[p_idx(term.variable)] - i_params += 1 else - error("Constraint uses a variable that is not in the model") + if is_variable(term.variable_2) + num_pv += 1 + else + num_pp += 1 + end end end - return vars, - params, - terms_with_variables_associated_to_parameters, - param_constant + return num_vv, num_pp, num_pv end -function count_scalar_quadratic_terms_types( - model::Optimizer, +function split_quadratic_terms( terms::Vector{MOI.ScalarQuadraticTerm{T}}, ) where {T} - num_quad_vars = 0 - num_quad_params = 0 - num_quad_aff_vars = 0 + 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 + i_vv = 1 + i_pp = 1 + i_pv = 1 for term in terms - if is_variable_in_model(model, term.variable_1) && - is_variable_in_model(model, term.variable_2) - num_quad_vars += 1 - elseif is_variable_in_model(model, term.variable_1) && - is_parameter_in_model(model, term.variable_2) - num_quad_aff_vars += 1 - elseif is_parameter_in_model(model, term.variable_1) && - is_variable_in_model(model, term.variable_2) - num_quad_aff_vars += 1 + if is_variable(term.variable_1) + if is_variable(term.variable_2) + vv[i_vv] = term + i_vv += 1 + else + pv[i_pv] = MOI.ScalarQuadraticTerm( + term.coefficient, + term.variable_2, + term.variable_1, + ) + i_pv += 1 + end else - num_quad_params += 1 + if is_variable(term.variable_2) + pv[i_pv] = term + i_pv += 1 + else + pp[i_pp] = term + i_pp += 1 + end end end - return num_quad_vars, num_quad_params, num_quad_aff_vars + return pv, pp, vv end -function separate_possible_terms_and_calculate_parameter_constant( +function ParametricQuadraticFunction( + f::MOI.ScalarQuadraticFunction{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 + else + base = get(affine_data_np, term.variable, zero(T)) + affine_data_np[term.variable] = term.coefficient + base + 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), + ) +end + +function original_function(f::ParametricQuadraticFunction{T}) where {T} + return MOI.ScalarQuadraticFunction{T}( + vcat(f.pv, f.pp, f.vv), + vcat(f.p, f.v), + f.c, + ) +end + +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 + push!(affine, MOI.ScalarAffineTerm{T}(c, v)) + end + for (v, c) in f.affine_data_np + push!(affine, MOI.ScalarAffineTerm{T}(c, v)) + end + return MOI.ScalarQuadraticFunction{T}(f.vv, affine, f.current_constant) +end + +function update_cache!( + f::ParametricQuadraticFunction{T}, model::Optimizer, - terms::Vector{MOI.ScalarQuadraticTerm{T}}, ) where {T} - num_quad_vars, num_quad_params, num_quad_aff_vars = - count_scalar_quadratic_terms_types(model, terms) - - quad_params = Vector{MOI.ScalarQuadraticTerm{T}}(undef, num_quad_params) # parameter x parameter - quad_aff_vars = Vector{MOI.ScalarQuadraticTerm{T}}(undef, num_quad_aff_vars) # parameter (as a variable) x variable - quad_vars = Vector{MOI.ScalarQuadraticTerm{T}}(undef, num_quad_vars) # variable x variable - aff_terms = Vector{MOI.ScalarAffineTerm{T}}(undef, num_quad_aff_vars) # parameter (as a number) x variable - variables_associated_to_parameters = - Vector{MOI.VariableIndex}(undef, num_quad_aff_vars) - quad_param_constant = zero(T) - - i_quad_vars = 1 - i_quad_params = 1 - i_quad_aff_vars = 1 - - # When we have a parameter x variable or a variable x parameter the convention is to rewrite - # the SQT with parameter as variable_index_1 and variable as variable_index_2 - for term in terms - if ( - is_variable_in_model(model, term.variable_1) && - is_variable_in_model(model, term.variable_2) - ) - quad_vars[i_quad_vars] = term # if there are only variables, it remains a quadratic term - i_quad_vars += 1 - elseif ( - is_parameter_in_model(model, term.variable_1) && - is_variable_in_model(model, term.variable_2) - ) - quad_aff_vars[i_quad_aff_vars] = term - variables_associated_to_parameters[i_quad_aff_vars] = - term.variable_2 - aff_terms[i_quad_aff_vars] = MOI.ScalarAffineTerm( - term.coefficient * model.parameters[p_idx(term.variable_1)], - term.variable_2, - ) - model.evaluate_duals && - push!(model.multiplicative_parameters, term.variable_1.value) - i_quad_aff_vars += 1 - elseif ( - is_variable_in_model(model, term.variable_1) && - is_parameter_in_model(model, term.variable_2) - ) - # Check convention defined above. We use the convention to know decide who is a variable and who is - # a parameter withou having to recheck which is which. - quad_aff_vars[i_quad_aff_vars] = MOI.ScalarQuadraticTerm( - term.coefficient, - term.variable_2, - term.variable_1, + f.current_constant = parametric_constant(model, f) + f.current_terms_with_p = parametric_affine_terms(model, f) + return nothing +end + +function parametric_constant( + model::Optimizer, + f::ParametricQuadraticFunction{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 + for term in f.pp + param_constant += + term.coefficient * + model.parameters[p_idx(term.variable_1)] * + model.parameters[p_idx(term.variable_2)] + end + return param_constant +end + +function delta_parametric_constant( + model::Optimizer, + f::ParametricQuadraticFunction{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 + for term in f.pp + p1 = p_idx(term.variable_1) + p2 = p_idx(term.variable_2) + isnan_1 = isnan(model.updated_parameters[p1]) + isnan_2 = isnan(model.updated_parameters[p2]) + if !isnan_1 || !isnan_2 + new_1 = ifelse( + isnan_1, + model.parameters[p1], + model.updated_parameters[p1], ) - variables_associated_to_parameters[i_quad_aff_vars] = - term.variable_1 - aff_terms[i_quad_aff_vars] = MOI.ScalarAffineTerm( - term.coefficient * model.parameters[p_idx(term.variable_2)], - term.variable_1, + new_2 = ifelse( + isnan_2, + model.parameters[p2], + model.updated_parameters[p2], ) - model.evaluate_duals && - push!(model.multiplicative_parameters, term.variable_2.value) - i_quad_aff_vars += 1 - elseif ( - is_parameter_in_model(model, term.variable_1) && - is_parameter_in_model(model, term.variable_2) - ) - quad_params[i_quad_params] = term - model.evaluate_duals && - push!(model.multiplicative_parameters, term.variable_1.value) - model.evaluate_duals && - push!(model.multiplicative_parameters, term.variable_2.value) - quad_param_constant += + delta_constant += term.coefficient * - model.parameters[p_idx(term.variable_1)] * - model.parameters[p_idx(term.variable_2)] - i_quad_params += 1 - else - throw( - ErrorException( - "Constraint uses a variable or parameter that is not in the model", - ), - ) + (new_1 * new_2 - model.parameters[p1] * model.parameters[p2]) end end - return quad_vars, - quad_aff_vars, - quad_params, - aff_terms, - variables_associated_to_parameters, - quad_param_constant + return delta_constant +end + +function parametric_affine_terms( + model::Optimizer, + f::ParametricQuadraticFunction{T}, +) where {T} + param_terms_dict = Dict{MOI.VariableIndex,T}() + sizehint!(param_terms_dict, length(f.pv)) + # remember a variable may appear more than once in pv + for term in f.pv + base = get(param_terms_dict, term.variable_2, zero(T)) + param_terms_dict[term.variable_2] = + base + term.coefficient * model.parameters[p_idx(term.variable_1)] + end + # by definition affine data only contains variables that appear in pv + for (var, coef) in f.affine_data + param_terms_dict[var] += coef + end + return param_terms_dict end -function fill_quadratic_constraint_caches!( +function delta_parametric_affine_terms( model::Optimizer, - new_ci::MOI.ConstraintIndex, - quad_aff_vars::Vector{MOI.ScalarQuadraticTerm{T}}, - quad_params::Vector{MOI.ScalarQuadraticTerm{T}}, - aff_params::Vector{MOI.ScalarAffineTerm{T}}, - terms_with_variables_associated_to_parameters::Vector{ - MOI.ScalarAffineTerm{T}, - }, - ci::MOI.ConstraintIndex, -) where {T,S} - if !isempty(quad_aff_vars) - model.quadratic_constraint_cache_pv[new_ci] = quad_aff_vars + f::ParametricQuadraticFunction{T}, +) where {T} + delta_terms_dict = Dict{MOI.VariableIndex,T}() + sizehint!(delta_terms_dict, length(f.pv)) + # remember a variable may appear more than once in pv + for term in f.pv + p = p_idx(term.variable_1) + if !isnan(model.updated_parameters[p]) + base = get(delta_terms_dict, term.variable_2, zero(T)) + delta_terms_dict[term.variable_2] = + base + + term.coefficient * + (model.updated_parameters[p] - model.parameters[p]) + end end - if !isempty(quad_params) - model.quadratic_constraint_cache_pp[new_ci] = quad_params - model.quadratic_constraint_cache_pp_set[new_ci] = - MOI.get(model.optimizer, MOI.ConstraintSet(), ci) + return delta_terms_dict +end + +function cache_set_constant!( + f::ParametricAffineFunction{T}, + s::Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, +) where {T} + f.set_constant = MOI.constant(s) + return +end + +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}}, +) where {T} + f.set_constant = MOI.constant(s) + return +end + +function cache_set_constant!( + f::ParametricQuadraticFunction{T}, + s::MOI.AbstractScalarSet, +) where {T} + return +end + +function is_affine(f::MOI.ScalarQuadraticFunction) + if isempty(f.quadratic_terms) + return true end - if !isempty(aff_params) - model.quadratic_constraint_cache_pc[new_ci] = aff_params - model.quadratic_constraint_cache_pc_set[new_ci] = - MOI.get(model.optimizer, MOI.ConstraintSet(), ci) + 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_2.value) end - if !isempty(terms_with_variables_associated_to_parameters) - model.quadratic_constraint_variables_associated_to_parameters_cache[new_ci] = - terms_with_variables_associated_to_parameters + # 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 nothing + return 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_added_cache) - # Using this becuase some custom brodcast method throws errors if + 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 end - -# Vector Affine -function separate_possible_terms_and_calculate_parameter_constant( - model::Optimizer, - f::MOI.VectorAffineFunction{T}, - set::S, -) where {T,S<:MOI.AbstractVectorSet} - vars = MOI.VectorAffineTerm{T}[] - params = MOI.VectorAffineTerm{T}[] - n_dims = length(f.constants) - param_constants = zeros(T, n_dims) - for term in f.terms - oi = term.output_index - - if is_variable_in_model(model, term.scalar_term.variable) - push!(vars, term) - elseif is_parameter_in_model(model, term.scalar_term.variable) - push!(params, term) - param_constants[oi] += - term.scalar_term.coefficient * - model.parameters[p_idx(term.scalar_term.variable)] - else - error("Constraint uses a variable that is not in the model") - end - end - return vars, params, param_constants -end diff --git a/test/jump_tests.jl b/test/jump_tests.jl index 7265fd4d..a732df77 100644 --- a/test/jump_tests.jl +++ b/test/jump_tests.jl @@ -108,89 +108,98 @@ function test_jump_constraintfunction_getter() c1 = @constraint(model, con, sum(x) + sum(p) >= 1) c2 = @constraint(model, conq, sum(x .* p) >= 1) c3 = @constraint(model, conqa, sum(x .* p) + x[1]^2 + x[1] + p[1] >= 1) - @test MOI.get(model, MOI.ConstraintFunction(), c1) ≈ - MOI.ScalarAffineFunction{Float64}( - [ - MOI.ScalarAffineTerm{Float64}(1.0, MOI.VariableIndex(1)), - MOI.ScalarAffineTerm{Float64}(1.0, MOI.VariableIndex(2)), - MOI.ScalarAffineTerm{Float64}( - 1.0, - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), - ), - MOI.ScalarAffineTerm{Float64}( - 1.0, - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 2), - ), - ], - 0.0, + @test MOI.Utilities.canonical( + MOI.get(model, MOI.ConstraintFunction(), c1), + ) ≈ MOI.Utilities.canonical( + MOI.ScalarAffineFunction{Float64}( + [ + MOI.ScalarAffineTerm{Float64}(1.0, MOI.VariableIndex(1)), + MOI.ScalarAffineTerm{Float64}(1.0, MOI.VariableIndex(2)), + MOI.ScalarAffineTerm{Float64}( + 1.0, + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), + ), + MOI.ScalarAffineTerm{Float64}( + 1.0, + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 2), + ), + ], + 0.0, + ), ) - @test MOI.get(model, MOI.ConstraintFunction(), c2) ≈ - MOI.ScalarQuadraticFunction{Float64}( - [ - MOI.ScalarQuadraticTerm{Float64}( - 1.0, - MOI.VariableIndex(1), - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), - ), - MOI.ScalarQuadraticTerm{Float64}( - 1.0, - MOI.VariableIndex(2), - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 2), - ), - ], - [], - 0.0, + @test canonical_compare( + MOI.get(model, MOI.ConstraintFunction(), c2), + MOI.ScalarQuadraticFunction{Float64}( + [ + MOI.ScalarQuadraticTerm{Float64}( + 1.0, + MOI.VariableIndex(1), + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), + ), + MOI.ScalarQuadraticTerm{Float64}( + 1.0, + MOI.VariableIndex(2), + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 2), + ), + ], + [], + 0.0, + ), ) - @test MOI.get(model, MOI.ConstraintFunction(), c3) ≈ - MOI.ScalarQuadraticFunction{Float64}( - [ - MOI.ScalarQuadraticTerm{Float64}( - 1.0, - MOI.VariableIndex(1), - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), - ), - MOI.ScalarQuadraticTerm{Float64}( - 1.0, - MOI.VariableIndex(2), - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 2), - ), - MOI.ScalarQuadraticTerm{Float64}( - 2.0, - MOI.VariableIndex(1), - MOI.VariableIndex(1), - ), - ], - [ - MOI.ScalarAffineTerm{Float64}(1.0, MOI.VariableIndex(1)), - MOI.ScalarAffineTerm{Float64}( - 1.0, - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), - ), - ], - 0.0, + @test canonical_compare( + MOI.get(model, MOI.ConstraintFunction(), c3), + MOI.ScalarQuadraticFunction{Float64}( + [ + MOI.ScalarQuadraticTerm{Float64}( + 1.0, + MOI.VariableIndex(1), + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), + ), + MOI.ScalarQuadraticTerm{Float64}( + 1.0, + MOI.VariableIndex(2), + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 2), + ), + MOI.ScalarQuadraticTerm{Float64}( + 2.0, + MOI.VariableIndex(1), + MOI.VariableIndex(1), + ), + ], + [ + MOI.ScalarAffineTerm{Float64}(1.0, MOI.VariableIndex(1)), + MOI.ScalarAffineTerm{Float64}( + 1.0, + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), + ), + ], + 0.0, + ), ) o1 = @objective(model, Min, sum(x) + sum(p)) F = MOI.get(model, MOI.ObjectiveFunctionType()) - @test MOI.get(model, MOI.ObjectiveFunction{F}()) ≈ - MOI.ScalarAffineFunction{Float64}( - [ - MOI.ScalarAffineTerm{Float64}(1.0, MOI.VariableIndex(1)), - MOI.ScalarAffineTerm{Float64}(1.0, MOI.VariableIndex(2)), - MOI.ScalarAffineTerm{Float64}( - 1.0, - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), - ), - MOI.ScalarAffineTerm{Float64}( - 1.0, - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 2), - ), - ], - 0.0, + @test canonical_compare( + MOI.get(model, MOI.ObjectiveFunction{F}()), + MOI.ScalarAffineFunction{Float64}( + [ + MOI.ScalarAffineTerm{Float64}(1.0, MOI.VariableIndex(1)), + MOI.ScalarAffineTerm{Float64}(1.0, MOI.VariableIndex(2)), + MOI.ScalarAffineTerm{Float64}( + 1.0, + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), + ), + MOI.ScalarAffineTerm{Float64}( + 1.0, + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 2), + ), + ], + 0.0, + ), ) o2 = @objective(model, Min, sum(x .* p) + 2) F = MOI.get(model, MOI.ObjectiveFunctionType()) - @test MOI.get(model, MOI.ObjectiveFunction{F}()) ≈ - MOI.ScalarQuadraticFunction{Float64}( + f = MOI.get(model, MOI.ObjectiveFunction{F}()) + f_ref = MOI.ScalarQuadraticFunction{Float64}( [ MOI.ScalarQuadraticTerm{Float64}( 1.0, @@ -206,35 +215,38 @@ function test_jump_constraintfunction_getter() [], 2.0, ) + @test canonical_compare(f, f_ref) o3 = @objective(model, Min, sum(x .* p) + x[1]^2 + x[1] + p[1]) F = MOI.get(model, MOI.ObjectiveFunctionType()) - @test MOI.get(model, MOI.ObjectiveFunction{F}()) ≈ - MOI.ScalarQuadraticFunction{Float64}( - [ - MOI.ScalarQuadraticTerm{Float64}( - 1.0, - MOI.VariableIndex(1), - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), - ), - MOI.ScalarQuadraticTerm{Float64}( - 1.0, - MOI.VariableIndex(2), - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 2), - ), - MOI.ScalarQuadraticTerm{Float64}( - 2.0, - MOI.VariableIndex(1), - MOI.VariableIndex(1), - ), - ], - [ - MOI.ScalarAffineTerm{Float64}(1.0, MOI.VariableIndex(1)), - MOI.ScalarAffineTerm{Float64}( - 1.0, - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), - ), - ], - 0.0, + @test canonical_compare( + MOI.get(model, MOI.ObjectiveFunction{F}()), + MOI.ScalarQuadraticFunction{Float64}( + [ + MOI.ScalarQuadraticTerm{Float64}( + 1.0, + MOI.VariableIndex(1), + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), + ), + MOI.ScalarQuadraticTerm{Float64}( + 1.0, + MOI.VariableIndex(2), + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 2), + ), + MOI.ScalarQuadraticTerm{Float64}( + 2.0, + MOI.VariableIndex(1), + MOI.VariableIndex(1), + ), + ], + [ + MOI.ScalarAffineTerm{Float64}(1.0, MOI.VariableIndex(1)), + MOI.ScalarAffineTerm{Float64}( + 1.0, + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), + ), + ], + 0.0, + ), ) return end @@ -753,6 +765,7 @@ function test_jump_direct_vector_parameter_affine_nonnegatives() ) optimizer = POI.Optimizer(cached) model = direct_model(optimizer) + set_silent(model) @variable(model, x) @variable(model, y) @variable(model, t in POI.Parameter(5)) @@ -788,6 +801,7 @@ function test_jump_direct_vector_parameter_affine_nonpositives() ) optimizer = POI.Optimizer(cached) model = direct_model(optimizer) + set_silent(model) @variable(model, x) @variable(model, y) @variable(model, t in POI.Parameter(5)) @@ -828,6 +842,7 @@ function test_jump_direct_soc_parameters() ) optimizer = POI.Optimizer(cached) model = direct_model(optimizer) + set_silent(model) @variable(model, x) @variable(model, y) @variable(model, t) @@ -858,6 +873,10 @@ function test_jump_direct_qp_objective() @constraint(model, 2x + y <= 4) @constraint(model, x + 2y <= 4) @objective(model, Max, (x^2 + y^2) / 2) + optimize!(model) + @test objective_value(model) ≈ 16 / 9 atol = ATOL + @test value(x) ≈ 4 / 3 atol = ATOL + @test value(y) ≈ 4 / 3 atol = ATOL MOI.set( backend(model), POI.QuadraticObjectiveCoef(), @@ -865,25 +884,59 @@ function test_jump_direct_qp_objective() 2index(p) + 3, ) optimize!(model) - @test MOI.get( + @test canonical_compare( + MOI.get( + backend(model), + POI.QuadraticObjectiveCoef(), + (index(x), index(y)), + ), + MOI.ScalarAffineFunction{Int64}( + MOI.ScalarAffineTerm{Int64}[MOI.ScalarAffineTerm{Int64}( + 2, + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), + )], + 3, + ), + ) + @test objective_value(model) ≈ 32 / 3 atol = ATOL + @test value(x) ≈ 4 / 3 atol = ATOL + @test value(y) ≈ 4 / 3 atol = ATOL + MOI.set(model, POI.ParameterValue(), p, 2.0) + optimize!(model) + @test objective_value(model) ≈ 128 / 9 atol = ATOL + @test value(x) ≈ 4 / 3 atol = ATOL + @test value(y) ≈ 4 / 3 atol = ATOL + MOI.set( backend(model), POI.QuadraticObjectiveCoef(), (index(x), index(y)), - ) ≈ MOI.ScalarAffineFunction{Int64}( - MOI.ScalarAffineTerm{Int64}[MOI.ScalarAffineTerm{Int64}( - 2, - MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), - )], - 3, + nothing, ) - @test objective_value(model) ≈ 32 / 3 atol = ATOL + optimize!(model) + @test objective_value(model) ≈ 16 / 9 atol = ATOL @test value(x) ≈ 4 / 3 atol = ATOL @test value(y) ≈ 4 / 3 atol = ATOL - MOI.set(model, POI.ParameterValue(), p, 2.0) + # now in reverse order + MOI.set( + backend(model), + POI.QuadraticObjectiveCoef(), + (index(y), index(x)), + 2index(p) + 3, + ) optimize!(model) @test objective_value(model) ≈ 128 / 9 atol = ATOL @test value(x) ≈ 4 / 3 atol = ATOL @test value(y) ≈ 4 / 3 atol = ATOL + MOI.set( + backend(model), + POI.QuadraticObjectiveCoef(), + (index(y), index(x)), + nothing, + ) + optimize!(model) + @test objective_value(model) ≈ 16 / 9 atol = ATOL + @test value(x) ≈ 4 / 3 atol = ATOL + @test value(y) ≈ 4 / 3 atol = ATOL return end @@ -932,3 +985,73 @@ function test_jump_direct_rsoc_constraints() @test value(y) ≈ 2 atol = ATOL return end + +function test_jump_quadratic_interval() + optimizer = POI.Optimizer(GLPK.Optimizer()) + # model = direct_model(optimizer) + model = Model(() -> optimizer) + MOI.set(model, MOI.Silent(), true) + @variable(model, x >= 0) + @variable(model, y >= 0) + @variable(model, p in POI.Parameter(10.0)) + @variable(model, q in POI.Parameter(4.0)) + @constraint(model, 0 <= x - p * y + q <= 0) + @objective(model, Min, x + y) + optimize!(model) + @test value(x) ≈ 0 atol = ATOL + @test value(y) ≈ 0.4 atol = ATOL + MOI.set(model, POI.ParameterValue(), p, 20.0) + optimize!(model) + @test value(x) ≈ 0 atol = ATOL + @test value(y) ≈ 0.2 atol = ATOL + MOI.set(model, POI.ParameterValue(), q, 6.0) + optimize!(model) + @test value(x) ≈ 0 atol = ATOL + @test value(y) ≈ 0.3 atol = ATOL + return +end + +function test_jump_quadratic_interval_cached() + cached = MOI.Bridges.full_bridge_optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + GLPK.Optimizer(), + ), + Float64, + ) + optimizer = POI.Optimizer(cached) + model = direct_model(optimizer) + # optimizer = POI.Optimizer(GLPK.Optimizer()) + # model = direct_model(optimizer) + # model = Model(() -> optimizer) + # MOI.set(model, MOI.Silent(), true) + @variable(model, x >= 0) + @variable(model, y >= 0) + @variable(model, p in POI.Parameter(10.0)) + @variable(model, q in POI.Parameter(4.0)) + @constraint(model, 0 <= x - p * y + q <= 0) + @objective(model, Min, x + y) + optimize!(model) + @test value(x) ≈ 0 atol = ATOL + @test value(y) ≈ 0.4 atol = ATOL + MOI.set(model, POI.ParameterValue(), p, 20.0) + optimize!(model) + @test value(x) ≈ 0 atol = ATOL + @test value(y) ≈ 0.2 atol = ATOL + MOI.set(model, POI.ParameterValue(), q, 6.0) + optimize!(model) + @test value(x) ≈ 0 atol = ATOL + @test value(y) ≈ 0.3 atol = ATOL + return +end + +function test_affine_parametric_objective() + model = Model(() -> POI.Optimizer(GLPK.Optimizer())) + @variable(model, p in POI.Parameter(1.0)) + @variable(model, 0 <= x <= 1) + @objective(model, Max, (p + 0.5) * x) + optimize!(model) + @test value(x) ≈ 1.0 + @test objective_value(model) ≈ 1.5 + @test value(objective_function(model)) ≈ 1.5 +end diff --git a/test/moi_tests.jl b/test/moi_tests.jl index b674058e..ed79fa9d 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -94,6 +94,8 @@ function test_basic_tests() @test MOI.get(optimizer, MOI.ObjectiveSense()) == MOI.MIN_SENSE @test MOI.get(optimizer, MOI.VariableName(), x[1]) == "" @test MOI.get(optimizer, MOI.ConstraintName(), c1) == "" + MOI.set(optimizer, MOI.ConstraintName(), c1, "ctr123") + @test MOI.get(optimizer, MOI.ConstraintName(), c1) == "ctr123" return end @@ -129,7 +131,7 @@ function test_basic_special_cases_of_getters() ) MOI.set( optimizer, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), obj_func, ) MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE) @@ -266,6 +268,9 @@ function test_moi_ipopt() # - CachingOptimizer does not throw if optimizer not attached "test_model_copy_to_UnsupportedAttribute", "test_model_copy_to_UnsupportedConstraint", + # - POI throws a ErrorException if user tries to modify parametric + # functions + "test_objective_get_ObjectiveFunction_ScalarAffineFunction", ], ) return @@ -275,6 +280,7 @@ function test_moi_ListOfConstraintTypesPresent() N = 10 ipopt = Ipopt.Optimizer() model = POI.Optimizer(ipopt) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variables(model, N / 2) y = first.( @@ -572,6 +578,7 @@ function test_vector_parameter_affine_nonnegatives() SCS.Optimizer(), ) model = POI.Optimizer(cached) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) y = MOI.add_variable(model) t, ct = MOI.add_constrained_variable(model, POI.Parameter(5)) @@ -630,6 +637,7 @@ function test_vector_parameter_affine_nonpositives() Float64, ) model = POI.Optimizer(cached) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) y = MOI.add_variable(model) t, ct = MOI.add_constrained_variable(model, POI.Parameter(5)) @@ -693,6 +701,7 @@ function test_vector_soc_parameters() SCS.Optimizer(), ) model = POI.Optimizer(cached) + MOI.set(model, MOI.Silent(), true) x, y, t = MOI.add_variables(model, 3) p, cp = MOI.add_constrained_variable(model, POI.Parameter(0)) MOI.set( @@ -778,6 +787,7 @@ function test_vector_soc_no_parameters() Float64, ) model = POI.Optimizer(cached) + MOI.set(model, MOI.Silent(), true) x, y, t = MOI.add_variables(model, 3) MOI.set( model, @@ -1315,7 +1325,6 @@ function test_qp_objective_parameter_times_parameter() opt_in = MOI.Utilities.CachingOptimizer(MOI.Utilities.Model{Float64}(), ipopt) optimizer = POI.Optimizer(opt_in) - A = [0.0 1.0; 1.0 0.0] a = [1.0, 1.0] x = MOI.add_variables(optimizer, 2) for x_i in x @@ -1324,7 +1333,7 @@ function test_qp_objective_parameter_times_parameter() y, cy = MOI.add_constrained_variable(optimizer, POI.Parameter(1)) z, cz = MOI.add_constrained_variable(optimizer, POI.Parameter(1)) quad_terms = MOI.ScalarQuadraticTerm{Float64}[] - push!(quad_terms, MOI.ScalarQuadraticTerm(A[1, 2], y, z)) + push!(quad_terms, MOI.ScalarQuadraticTerm(1.0, y, z)) objective_function = MOI.ScalarQuadraticFunction( quad_terms, MOI.ScalarAffineTerm.(a, x), @@ -1343,9 +1352,8 @@ function test_qp_objective_parameter_times_parameter() 0.0, atol = ATOL, ) - err = ErrorException( - "Cannot calculate the dual of a multiplicative parameter", - ) + err = + ErrorException("Cannot compute the dual of a multiplicative parameter") @test_throws err MOI.get(optimizer, MOI.ConstraintDual(), cy) @test_throws err MOI.get(optimizer, MOI.ConstraintDual(), cz) MOI.set(optimizer, MOI.ConstraintSet(), cy, POI.Parameter(2.0)) diff --git a/test/runtests.jl b/test/runtests.jl index 4b7d393b..cbdff78f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -17,6 +17,10 @@ const POI = ParametricOptInterface const ATOL = 1e-4 +function canonical_compare(f1, f2) + return MOI.Utilities.canonical(f1) ≈ MOI.Utilities.canonical(f2) +end + include("moi_tests.jl") include("jump_tests.jl")