diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index a3c8aa63..eee208b0 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -125,7 +125,9 @@ function MOI.is_empty(model::Optimizer) isempty(model.affine_constraint_cache_set) && # quad ctr model.last_quad_add_added == 0 && + model.last_vec_quad_add_added == 0 && isempty(model.quadratic_outer_to_inner) && + isempty(model.vector_quadratic_outer_to_inner) && isempty(model.quadratic_constraint_cache) && isempty(model.quadratic_constraint_cache_set) && isempty(model.vector_quadratic_constraint_cache) && @@ -162,7 +164,9 @@ function MOI.empty!(model::Optimizer{T}) where {T} empty!(model.affine_constraint_cache_set) # quad ctr model.last_quad_add_added = 0 + model.last_vec_quad_add_added = 0 empty!(model.quadratic_outer_to_inner) + empty!(model.vector_quadratic_outer_to_inner) empty!(model.quadratic_constraint_cache) empty!(model.quadratic_constraint_cache_set) empty!(model.vector_quadratic_constraint_cache) @@ -513,11 +517,16 @@ end function MOI.set( model::Optimizer, attr::MOI.ConstraintName, - c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}, + c::MOI.ConstraintIndex{MOI.VectorQuadraticFunction{T},S}, name::String, ) where {T,S<:MOI.AbstractSet} - if haskey(model.affine_outer_to_inner, c) - MOI.set(model.optimizer, attr, model.affine_outer_to_inner[c], name) + if haskey(model.vector_quadratic_outer_to_inner, c) + MOI.set( + model.optimizer, + attr, + model.vector_quadratic_outer_to_inner[c], + name, + ) else MOI.set(model.optimizer, attr, c, name) end @@ -527,10 +536,14 @@ end function MOI.set( model::Optimizer, attr::MOI.ConstraintName, - c::MOI.ConstraintIndex, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}, name::String, -) - MOI.set(model.optimizer, attr, c, name) +) where {T,S<:MOI.AbstractSet} + if haskey(model.affine_outer_to_inner, c) + MOI.set(model.optimizer, attr, model.affine_outer_to_inner[c], name) + else + MOI.set(model.optimizer, attr, c, name) + end return end @@ -549,9 +562,17 @@ end function MOI.get( model::Optimizer, attr::MOI.ConstraintName, - c::MOI.ConstraintIndex, -) - return MOI.get(model.optimizer, attr, c) + c::MOI.ConstraintIndex{MOI.VectorQuadraticFunction{T},S}, +) where {T,S<:MOI.AbstractSet} + if haskey(model.vector_quadratic_outer_to_inner, c) + return MOI.get( + model.optimizer, + attr, + model.vector_quadratic_outer_to_inner[c], + ) + else + return MOI.get(model.optimizer, attr, c) + end end function MOI.get( @@ -606,13 +627,21 @@ function MOI.get( return _original_function( model.quadratic_constraint_cache[inner_ci], ) - elseif haskey(model.vector_quadratic_constraint_cache, inner_ci) + else + return convert( + MOI.ScalarQuadraticFunction{T}, + MOI.get(model.optimizer, attr, inner_ci), + ) + end + elseif haskey(model.vector_quadratic_outer_to_inner, ci) + inner_ci = model.vector_quadratic_outer_to_inner[ci] + if haskey(model.vector_quadratic_constraint_cache, inner_ci) return _original_function( model.vector_quadratic_constraint_cache[inner_ci], ) else return convert( - MOI.ScalarQuadraticFunction{T}, + MOI.VectorQuadraticFunction{T}, MOI.get(model.optimizer, attr, inner_ci), ) end @@ -655,8 +684,8 @@ function MOI.get( 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.vector_quadratic_constraint_cache, ci) - inner_ci = model.vector_quadratic_constraint_cache[ci] + elseif haskey(model.vector_quadratic_outer_to_inner, ci) + inner_ci = model.vector_quadratic_outer_to_inner[ci] return model.vector_quadratic_constraint_cache_set[inner_ci] elseif haskey(model.affine_outer_to_inner, ci) inner_ci = model.affine_outer_to_inner[ci] @@ -956,22 +985,22 @@ function _add_constraint_with_parameters_on_function( if !_is_vector_affine(func) fq = func inner_ci = MOI.add_constraint(model.optimizer, fq, set) - model.last_quad_add_added += 1 + model.last_vec_quad_add_added += 1 outer_ci = MOI.ConstraintIndex{MOI.VectorQuadraticFunction{T},S}( - model.last_quad_add_added, + model.last_vec_quad_add_added, ) - model.quadratic_outer_to_inner[outer_ci] = inner_ci + model.vector_quadratic_outer_to_inner[outer_ci] = inner_ci model.constraint_outer_to_inner[outer_ci] = inner_ci else fa = MOI.VectorAffineFunction(func.affine_terms, func.constants) inner_ci = MOI.add_constraint(model.optimizer, fa, set) - model.last_quad_add_added += 1 + model.last_vec_quad_add_added += 1 outer_ci = MOI.ConstraintIndex{MOI.VectorQuadraticFunction{T},S}( - model.last_quad_add_added, + model.last_vec_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.vector_quadratic_outer_to_inner[outer_ci] = inner_ci model.constraint_outer_to_inner[outer_ci] = inner_ci end model.vector_quadratic_constraint_cache[inner_ci] = pf @@ -995,10 +1024,11 @@ function MOI.delete( model::Optimizer, c::MOI.ConstraintIndex{F,S}, ) where {F<:MOI.VectorQuadraticFunction,S<:MOI.AbstractSet} - ci_inner = model.constraint_outer_to_inner[c] - if haskey(model.quadratic_constraint_cache, ci_inner) - delete!(model.quadratic_constraint_cache, ci_inner) - delete!(model.quadratic_constraint_cache_set, ci_inner) + if haskey(model.vector_quadratic_outer_to_inner, c) + ci_inner = model.vector_quadratic_outer_to_inner[c] + delete!(model.vector_quadratic_outer_to_inner, c) + delete!(model.vector_quadratic_constraint_cache, ci_inner) + delete!(model.vector_quadratic_constraint_cache_set, ci_inner) MOI.delete(model.optimizer, ci_inner) else MOI.delete(model.optimizer, c) diff --git a/src/ParametricOptInterface.jl b/src/ParametricOptInterface.jl index d9b386ed..c1ba6942 100644 --- a/src/ParametricOptInterface.jl +++ b/src/ParametricOptInterface.jl @@ -130,9 +130,11 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer # quadratic constraitn data last_quad_add_added::Int64 + last_vec_quad_add_added::Int64 # Store the map for SQFs (some might be transformed into SAF) # for instance p*p + var -> ScalarAffine(var) quadratic_outer_to_inner::DoubleDict{MOI.ConstraintIndex} + vector_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) @@ -212,6 +214,8 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer DoubleDict{MOI.AbstractScalarSet}(), # quadratic constraint 0, + 0, + DoubleDict{MOI.ConstraintIndex}(), DoubleDict{MOI.ConstraintIndex}(), DoubleDict{ParametricQuadraticFunction{T}}(), DoubleDict{MOI.AbstractScalarSet}(), diff --git a/src/parametric_functions.jl b/src/parametric_functions.jl index 47d57cb6..d5932358 100644 --- a/src/parametric_functions.jl +++ b/src/parametric_functions.jl @@ -705,57 +705,60 @@ function _parametric_affine_terms( base + term.scalar_term.coefficient * model.parameters[p_idx_val] end - for (term, coef) in f.affine_data - output_idx = term.output_index - var = term.scalar_term.variable - param_terms_dict[(var, output_idx)] = coef + # TODO: check if affine data should only contains variables that appear in pv + for (var, coef) in f.affine_data + if !haskey(param_terms_dict, var) + param_terms_dict[var] = zero(T) + end + param_terms_dict[var] += coef end return param_terms_dict end -function _delta_parametric_affine_terms( - model, - f::ParametricVectorQuadraticFunction{T}, -) where {T} - delta_terms = Dict{Tuple{Int,MOI.VariableIndex},T}() - - # Handle parameter-variable quadratic terms (px) that become affine (x) when p is updated - for term in f.pv - p_idx_val = p_idx(term.scalar_term.variable_1) - var = term.scalar_term.variable_2 - output_idx = term.output_index - - if haskey(model.updated_parameters, p_idx_val) && - !isnan(model.updated_parameters[p_idx_val]) - old_param_val = model.parameters[p_idx_val] - new_param_val = model.updated_parameters[p_idx_val] - delta_coef = - term.scalar_term.coefficient * (new_param_val - old_param_val) - - key = (output_idx, var) - current_delta = get(delta_terms, key, zero(T)) - delta_terms[key] = current_delta + delta_coef - end - end - - # Handle parameter-only affine terms - for term in f.p - p_idx_val = p_idx(term.scalar_term.variable) - output_idx = term.output_index - - if haskey(model.updated_parameters, p_idx_val) && - !isnan(model.updated_parameters[p_idx_val]) - old_param_val = model.parameters[p_idx_val] - new_param_val = model.updated_parameters[p_idx_val] - - # This becomes a constant change, not an affine term change - # We'll handle this in the constant update function - end - end - - return delta_terms -end +# TODO: USED once we update _update_vector_quadratic_constraints! +# function _delta_parametric_affine_terms( +# model, +# f::ParametricVectorQuadraticFunction{T}, +# ) where {T} +# delta_terms = Dict{Tuple{Int,MOI.VariableIndex},T}() + +# # Handle parameter-variable quadratic terms (px) that become affine (x) when p is updated +# for term in f.pv +# p_idx_val = p_idx(term.scalar_term.variable_1) +# var = term.scalar_term.variable_2 +# output_idx = term.output_index + +# if haskey(model.updated_parameters, p_idx_val) && +# !isnan(model.updated_parameters[p_idx_val]) +# old_param_val = model.parameters[p_idx_val] +# new_param_val = model.updated_parameters[p_idx_val] +# delta_coef = +# term.scalar_term.coefficient * (new_param_val - old_param_val) + +# key = (output_idx, var) +# current_delta = get(delta_terms, key, zero(T)) +# delta_terms[key] = current_delta + delta_coef +# end +# end + +# # Handle parameter-only affine terms +# for term in f.p +# p_idx_val = p_idx(term.scalar_term.variable) +# output_idx = term.output_index + +# if haskey(model.updated_parameters, p_idx_val) && +# !isnan(model.updated_parameters[p_idx_val]) +# old_param_val = model.parameters[p_idx_val] +# new_param_val = model.updated_parameters[p_idx_val] + +# # This becomes a constant change, not an affine term change +# # We'll handle this in the constant update function +# end +# end + +# return delta_terms +# end function _update_cache!( f::ParametricVectorQuadraticFunction{T}, diff --git a/src/update_parameters.jl b/src/update_parameters.jl index 9fc00e62..c14db5bd 100644 --- a/src/update_parameters.jl +++ b/src/update_parameters.jl @@ -330,58 +330,61 @@ function _update_vector_quadratic_constraints!(model::Optimizer) return end -function _delta_parametric_constant( - model, - f::ParametricVectorQuadraticFunction{T}, -) where {T} - delta_constants = zeros(T, length(f.current_constant)) - - # Handle parameter-only affine terms - for term in f.p - p_idx_val = p_idx(term.scalar_term.variable) - output_idx = term.output_index - - if !isnan(model.updated_parameters[p_idx_val]) - old_param_val = model.parameters[p_idx_val] - new_param_val = model.updated_parameters[p_idx_val] - delta_constants[output_idx] += - term.scalar_term.coefficient * (new_param_val - old_param_val) - end - end +# TODO: USED once we update _update_vector_quadratic_constraints! +# function _delta_parametric_constant( +# model, +# f::ParametricVectorQuadraticFunction{T}, +# ) where {T} +# delta_constants = zeros(T, length(f.current_constant)) - # Handle parameter-parameter quadratic terms - for term in f.pp - idx = term.output_index - var1 = term.scalar_term.variable_1 - var2 = term.scalar_term.variable_2 - p1 = p_idx(var1) - p2 = p_idx(var2) - - if !isnan(model.updated_parameters[p1]) || - !isnan(model.updated_parameters[p2]) - old_val1 = model.parameters[p1] - old_val2 = model.parameters[p2] - new_val1 = - !isnan(model.updated_parameters[p1]) ? - model.updated_parameters[p1] : old_val1 - new_val2 = - !isnan(model.updated_parameters[p2]) ? - model.updated_parameters[p2] : old_val2 - - coef = term.scalar_term.coefficient / (var1 == var2 ? 2 : 1) - delta_constants[idx] += - coef * (new_val1 * new_val2 - old_val1 * old_val2) - end - end +# # Handle parameter-only affine terms +# for term in f.p +# p_idx_val = p_idx(term.scalar_term.variable) +# output_idx = term.output_index - return delta_constants -end +# if !isnan(model.updated_parameters[p_idx_val]) +# old_param_val = model.parameters[p_idx_val] +# new_param_val = model.updated_parameters[p_idx_val] +# delta_constants[output_idx] += +# term.scalar_term.coefficient * (new_param_val - old_param_val) +# end +# end +# # Handle parameter-parameter quadratic terms +# for term in f.pp +# idx = term.output_index +# var1 = term.scalar_term.variable_1 +# var2 = term.scalar_term.variable_2 +# p1 = p_idx(var1) +# p2 = p_idx(var2) + +# if !isnan(model.updated_parameters[p1]) || +# !isnan(model.updated_parameters[p2]) +# old_val1 = model.parameters[p1] +# old_val2 = model.parameters[p2] +# new_val1 = +# !isnan(model.updated_parameters[p1]) ? +# model.updated_parameters[p1] : old_val1 +# new_val2 = +# !isnan(model.updated_parameters[p2]) ? +# model.updated_parameters[p2] : old_val2 + +# coef = term.scalar_term.coefficient / (var1 == var2 ? 2 : 1) +# delta_constants[idx] += +# coef * (new_val1 * new_val2 - old_val1 * old_val2) +# end +# end + +# return delta_constants +# end + +# TODO: Update once MOI.VectorConstantChange is implemented function _update_vector_quadratic_constraints!( model::Optimizer, vector_quadratic_constraint_cache_inner::DoubleDictInner{F,S,V}, ) where {F,S,V} for (inner_ci, pf) in vector_quadratic_constraint_cache_inner + # delta_constants = _delta_parametric_constant(model, pf) # if !iszero(delta_constants) # pf.current_constant .+= delta_constants diff --git a/test/jump_tests.jl b/test/jump_tests.jl index f9d36410..f3e3f6f3 100644 --- a/test/jump_tests.jl +++ b/test/jump_tests.jl @@ -1305,7 +1305,7 @@ function test_jump_psd_cone_with_parameter_pv() set_parameter_value(p, 3.0) optimize!(model) @test value(x) ≈ 1 / 3 atol = 1e-5 - # delete(model, con) + return delete(model, con) end function test_jump_psd_cone_with_parameter_pp() @@ -1336,7 +1336,7 @@ function test_jump_psd_cone_with_parameter_pp() set_parameter_value(p, 3.0) optimize!(model) @test value(x) ≈ 9.0 atol = 1e-5 - # delete(model, con) + return delete(model, con) end function test_jump_psd_cone_with_parameter_p() @@ -1363,5 +1363,214 @@ function test_jump_psd_cone_with_parameter_p() set_parameter_value(p, 3.0) optimize!(model) @test value(x) ≈ 3.0 atol = 1e-5 - # delete(model, con) + return delete(model, con) +end + +function test_jump_psd_cone_with_parameter_pv_v_pv() + cached = MOI.Bridges.full_bridge_optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + SCS.Optimizer(), + ), + Float64, + ) + optimizer = POI.Optimizer(cached) + model = direct_model(optimizer) + @variable(model, x) + @variable(model, p in MOI.Parameter(1.0)) + @constraint( + model, + con, + [p * x, (2 * x - 3), p * 3 * x] in + MOI.PositiveSemidefiniteConeTriangle(2) + ) + @objective(model, Min, x) + @test is_valid(model, con) + optimize!(model) + @test value(x) ≈ 0.803845 atol = 1e-5 + set_parameter_value(p, 3.0) + optimize!(model) + @test value(x) ≈ 0.416888 atol = 1e-5 + return delete(model, con) +end + +function test_jump_psd_cone_with_parameter_pp_v_pv() + cached = MOI.Bridges.full_bridge_optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + SCS.Optimizer(), + ), + Float64, + ) + optimizer = POI.Optimizer(cached) + model = direct_model(optimizer) + @variable(model, x) + @variable(model, p in MOI.Parameter(1.0)) + @constraint( + model, + con, + [p * p, (2 * x - 3), p * 3 * x] in + MOI.PositiveSemidefiniteConeTriangle(2) + ) + @objective(model, Min, x) + @test is_valid(model, con) + optimize!(model) + @test value(x) ≈ 0.7499854 atol = 1e-5 + set_parameter_value(p, 3.0) + optimize!(model) + @test value(x) ≈ 0.0971795 atol = 1e-5 + return delete(model, con) +end + +function test_jump_psd_cone_with_parameter_p_v_pv() + cached = MOI.Bridges.full_bridge_optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + SCS.Optimizer(), + ), + Float64, + ) + optimizer = POI.Optimizer(cached) + model = direct_model(optimizer) + @variable(model, x) + @variable(model, p in MOI.Parameter(1.0)) + @constraint( + model, + con, + [p, (2 * x - 3), p * 3 * x] in MOI.PositiveSemidefiniteConeTriangle(2) + ) + @objective(model, Min, x) + @test is_valid(model, con) + optimize!(model) + @test value(x) ≈ 0.7499854 atol = 1e-5 + set_parameter_value(p, 3.0) + optimize!(model) + @test value(x) ≈ 0.236506 atol = 1e-5 + return delete(model, con) +end + +function test_jump_psd_cone_with_parameter_p_v_pp() + cached = MOI.Bridges.full_bridge_optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + SCS.Optimizer(), + ), + Float64, + ) + optimizer = POI.Optimizer(cached) + model = direct_model(optimizer) + @variable(model, x) + @variable(model, p in MOI.Parameter(1.0)) + @constraint( + model, + con, + [p, (2 * x - 3), p * 3 * p] in MOI.PositiveSemidefiniteConeTriangle(2) + ) + @objective(model, Min, x) + @test is_valid(model, con) + optimize!(model) + @test value(x) ≈ 0.633969 atol = 1e-5 + set_parameter_value(p, 3.0) + optimize!(model) + @test value(x) ≈ -2.9999734 atol = 1e-5 + return delete(model, con) +end + +function test_jump_psd_cone_without_parameter_v_and_vv() + cached = MOI.Bridges.full_bridge_optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + SCS.Optimizer(), + ), + Float64, + ) + optimizer = POI.Optimizer(cached) + model = direct_model(optimizer) + @variable(model, x) + @variable(model, p in MOI.Parameter(1.0)) + @constraint( + model, + con, + [x, (x - 1), x] in MOI.PositiveSemidefiniteConeTriangle(2) + ) + @test is_valid(model, con) + optimize!(model) + @test value(x) ≈ 0.50000 atol = 1e-5 + delete(model, con) + unregister(model, :con) + @test_throws MOI.UnsupportedConstraint @constraint( + model, + con, + [p * p, x * (x - 1), p] in MOI.PositiveSemidefiniteConeTriangle(2) + ) + @test_throws MOI.UnsupportedConstraint @constraint( + model, + con, + [x * x, (x - 1), p] in MOI.PositiveSemidefiniteConeTriangle(2) + ) + @test_throws MOI.UnsupportedConstraint @constraint( + model, + con, + [p, (x - 1), x * x] in MOI.PositiveSemidefiniteConeTriangle(2) + ) + @test_throws MOI.UnsupportedConstraint @constraint( + model, + con, + [x, p * (x - 1), x * x] in MOI.PositiveSemidefiniteConeTriangle(2) + ) +end + +function test_variable_and_constraint_not_registered() + cached1 = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + SCS.Optimizer(), + ) + optimizer1 = POI.Optimizer(cached1) + cached2 = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + SCS.Optimizer(), + ) + optimizer2 = POI.Optimizer(cached2) + model = direct_model(optimizer1) + model2 = direct_model(optimizer2) + set_silent(model) + set_silent(model2) + @variable(model, x) + @variable(model, p in MOI.Parameter(1.0)) + @variable(model, p1 in MOI.Parameter(1.0)) + @variable(model2, p2 in MOI.Parameter(1.0)) + @constraint(model, con, [x - p] in MOI.Nonnegatives(1)) + @test_throws ErrorException("Variable not in the model") MOI.set( + backend(model2), + MOI.VariablePrimalStart(), + index(x), + 1.0, + ) + @test_throws ErrorException("Variable not in the model") MOI.get( + backend(model2), + MOI.VariablePrimalStart(), + index(x), + ) + @test_throws ErrorException("Parameter not in the model") MOI.get( + backend(model2), + MOI.ConstraintFunction(), + index(ParameterRef(p1)), + ) + @test_throws ErrorException("Parameter not in the model") MOI.get( + backend(model2), + MOI.ConstraintSet(), + index(ParameterRef(p1)), + ) + @test_throws ErrorException("Variable not in the model") MOI.set( + backend(model2), + MOI.ObjectiveFunction{MOI.VariableIndex}(), + index(x), + ) + @test_throws ErrorException( + "Cannot use a parameter as objective function alone", + ) MOI.set( + backend(model2), + MOI.ObjectiveFunction{MOI.VariableIndex}(), + index(p), + ) end diff --git a/test/moi_tests.jl b/test/moi_tests.jl index 31590440..81af74f8 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -2044,4 +2044,9 @@ function test_psd_cone_with_parameter() MOI.optimize!(model) @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1 / 3 atol = 1e-5 + + # test constraint name + @test MOI.get(model, MOI.ConstraintName(), c_index) == "" + MOI.set(model, MOI.ConstraintName(), c_index, "psd_cone") + @test MOI.get(model, MOI.ConstraintName(), c_index) == "psd_cone" end