diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index ea9b4d6..bcd2a4d 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -51,6 +51,22 @@ function _has_parameters(f::MOI.ScalarQuadraticFunction{T}) where {T} return false end +function _has_parameters(f::MOI.VectorQuadraticFunction) + # quadratic part + for qt in f.quadratic_terms + if _is_parameter(qt.scalar_term.variable_1) || _is_parameter(qt.scalar_term.variable_2) + return true + end + end + # affine part + for at in f.affine_terms + if _is_parameter(at.scalar_term.variable) + return true + end + end + return false +end + function _cache_multiplicative_params!( model::Optimizer{T}, f::ParametricQuadraticFunction{T}, @@ -65,6 +81,21 @@ function _cache_multiplicative_params!( return end +function _cache_multiplicative_params!( + model::Optimizer{T}, + f::ParametricVectorQuadraticFunction{T}, +) where {T} + for term in f.pv + push!(model.multiplicative_parameters_pv, + term.scalar_term.variable_1.value) + end + for term in f.pp + push!(model.multiplicative_parameters_pp, term.scalar_term.variable_1.value) + push!(model.multiplicative_parameters_pp, term.scalar_term.variable_2.value) + end + return +end + # # Empty # @@ -88,6 +119,8 @@ function MOI.is_empty(model::Optimizer) isempty(model.quadratic_outer_to_inner) && isempty(model.quadratic_constraint_cache) && isempty(model.quadratic_constraint_cache_set) && + isempty(model.vector_quadratic_constraint_cache) && + isempty(model.vector_quadratic_constraint_cache_set) && # obj model.affine_objective_cache === nothing && model.quadratic_objective_cache === nothing && @@ -123,6 +156,8 @@ function MOI.empty!(model::Optimizer{T}) where {T} empty!(model.quadratic_outer_to_inner) empty!(model.quadratic_constraint_cache) empty!(model.quadratic_constraint_cache_set) + empty!(model.vector_quadratic_constraint_cache) + empty!(model.vector_quadratic_constraint_cache_set) # obj model.affine_objective_cache = nothing model.quadratic_objective_cache = nothing @@ -538,6 +573,10 @@ function MOI.get( return _original_function( model.quadratic_constraint_cache[inner_ci], ) + elseif haskey(model.vector_quadratic_constraint_cache, inner_ci) + return _original_function( + model.vector_quadratic_constraint_cache[inner_ci], + ) else return convert( MOI.ScalarQuadraticFunction{T}, @@ -583,6 +622,9 @@ 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] + 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] return model.affine_constraint_cache_set[inner_ci] @@ -858,6 +900,80 @@ function MOI.add_constraint( end end +function _is_vector_affine(f::MOI.VectorQuadraticFunction{T}) where {T} + return isempty(f.quadratic_terms) +end + +function _is_vector_affine(::MOI.VectorAffineFunction{T}) where {T} + return true # VectorAffineFunction is always affine +end + +function _add_constraint_with_parameters_on_function( + model::Optimizer, + f::MOI.VectorQuadraticFunction{T}, + set::S, +) where {T,S} + # Create parametric vector quadratic function + pf = ParametricVectorQuadraticFunction(f) + _cache_multiplicative_params!(model, pf) + _update_cache!(pf, model) + + # Get the current function after parameter substitution + func = _current_function(pf) + if !_is_vector_affine(func) + fq = func + inner_ci = MOI.add_constraint(model.optimizer, fq, set) + model.last_quad_add_added += 1 + outer_ci = MOI.ConstraintIndex{MOI.VectorQuadraticFunction{T},S}( + model.last_quad_add_added, + ) + model.quadratic_outer_to_inner[outer_ci] = inner_ci + model.constraint_outer_to_inner[outer_ci] = inner_ci + else + fa = MOI.VectorAffineFunction(func.affine_terms, func.constants) + inner_ci = MOI.add_constraint(model.optimizer, fa, set) + model.last_quad_add_added += 1 + outer_ci = MOI.ConstraintIndex{MOI.VectorQuadraticFunction{T},S}( + model.last_quad_add_added, + ) + # This part is used to remember that ci came from a quadratic function + # It is particularly useful because sometimes the constraint mutates + model.quadratic_outer_to_inner[outer_ci] = inner_ci + model.constraint_outer_to_inner[outer_ci] = inner_ci + end + model.vector_quadratic_constraint_cache[inner_ci] = pf + model.vector_quadratic_constraint_cache_set[inner_ci] = set + return outer_ci +end + +function MOI.add_constraint( + model::Optimizer, + f::MOI.VectorQuadraticFunction{T}, + set::MOI.AbstractVectorSet, +) where {T} + if !_has_parameters(f) + return _add_constraint_direct_and_cache_map!(model, f, set) + else + return _add_constraint_with_parameters_on_function(model, f, set) + end +end + +function 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) + MOI.delete(model.optimizer, ci_inner) + else + MOI.delete(model.optimizer, c) + end + delete!(model.constraint_outer_to_inner, c) + return +end + function MOI.delete( model::Optimizer, c::MOI.ConstraintIndex{F,S}, @@ -1411,6 +1527,13 @@ function MOI.get( return model.quadratic_constraint_cache[F, S] end +function MOI.get( + model::Optimizer, + ::DictOfParametricConstraintIndicesAndFunctions{F,S,P}, +) where {F,S,P<:ParametricVectorQuadraticFunction} + return model.vector_quadratic_constraint_cache[F, S] +end + """ NumberOfPureVariables diff --git a/src/ParametricOptInterface.jl b/src/ParametricOptInterface.jl index 22cee83..6ac64af 100644 --- a/src/ParametricOptInterface.jl +++ b/src/ParametricOptInterface.jl @@ -137,6 +137,10 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer quadratic_constraint_cache::DoubleDict{ParametricQuadraticFunction{T}} # Store original constraint set (inner key) quadratic_constraint_cache_set::DoubleDict{MOI.AbstractScalarSet} + # Vector quadratic function data + vector_quadratic_constraint_cache::DoubleDict{ParametricVectorQuadraticFunction{T}} + # Store original constraint set (inner key) + vector_quadratic_constraint_cache_set::DoubleDict{MOI.AbstractVectorSet} # objective function data # Clever cache of data (at most one can be !== nothing) @@ -209,6 +213,8 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer DoubleDict{MOI.ConstraintIndex}(), DoubleDict{ParametricQuadraticFunction{T}}(), DoubleDict{MOI.AbstractScalarSet}(), + DoubleDict{ParametricVectorQuadraticFunction{T}}(), + DoubleDict{MOI.AbstractVectorSet}(), # objective nothing, nothing, diff --git a/src/duals.jl b/src/duals.jl index 1ab2099..aac9388 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -9,6 +9,7 @@ function _compute_dual_of_parameters!(model::Optimizer{T}) where {T} _update_duals_from_affine_constraints!(model) _update_duals_from_vector_affine_constraints!(model) _update_duals_from_quadratic_constraints!(model) + _update_duals_from_vector_quadratic_constraints!(model) if model.affine_objective_cache !== nothing _update_duals_from_objective!(model, model.affine_objective_cache) end @@ -174,3 +175,33 @@ function _is_additive(model::Optimizer, cp::MOI.ConstraintIndex) end return true end + +function _update_duals_from_vector_quadratic_constraints!(model::Optimizer) + for (F, S) in keys(model.vector_quadratic_constraint_cache.dict) + vector_quadratic_constraint_cache_inner = model.vector_quadratic_constraint_cache[F, S] + _compute_parameters_in_ci!(model, vector_quadratic_constraint_cache_inner) + end + return +end + +function _compute_parameters_in_ci!( + model::Optimizer{T}, + pf::ParametricVectorQuadraticFunction{T}, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S,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 + for term in pf.pp + coef = ifelse(term.scalar_term.variable_1 == term.scalar_term.variable_2, T(1 // 2), T(1)) + model.dual_value_of_parameters[p_val(term.scalar_term.variable_1)] -= + coef * cons_dual[term.output_index] * term.scalar_term.coefficient * + MOI.get(model, ParameterValue(), term.scalar_term.variable_2) + model.dual_value_of_parameters[p_val(term.scalar_term.variable_2)] -= + coef * cons_dual[term.output_index] * term.scalar_term.coefficient * + MOI.get(model, ParameterValue(), term.scalar_term.variable_1) + end + return +end \ No newline at end of file diff --git a/src/parametric_functions.jl b/src/parametric_functions.jl index ce4e4ce..ff70e38 100644 --- a/src/parametric_functions.jl +++ b/src/parametric_functions.jl @@ -529,3 +529,268 @@ function _update_cache!(f::ParametricVectorAffineFunction{T}, model) where {T} f.current_constant = _parametric_constant(model, f) return nothing end + +mutable struct ParametricVectorQuadraticFunction{T} + # helper to efficiently update affine terms + affine_data::Dict{Tuple{MOI.VariableIndex,Int},T} + affine_data_np::Dict{Tuple{MOI.VariableIndex,Int},T} + # constant * parameter * variable (in this order) + pv::Vector{MOI.VectorQuadraticTerm{T}} + # constant * parameter * parameter + pp::Vector{MOI.VectorQuadraticTerm{T}} + # constant * variable * variable + vv::Vector{MOI.VectorQuadraticTerm{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 data that is inside the solver to avoid slow getters + current_terms_with_p::Dict{Tuple{MOI.VariableIndex,Int},T} + current_constant::Vector{T} +end + +function ParametricVectorQuadraticFunction( + f::MOI.VectorQuadraticFunction{T}, +) where {T} + v, p = _split_vector_affine_terms(f.affine_terms) + pv, pp, vv = _split_vector_quadratic_terms(f.quadratic_terms) + + # Find variables related to parameters in parameter-variable quadratic terms + v_in_pv = Set{MOI.VariableIndex}() + sizehint!(v_in_pv, length(pv)) + for term in pv + push!(v_in_pv, term.scalar_term.variable_2) + end + affine_data = Dict{Tuple{MOI.VariableIndex,Int},T}() + sizehint!(affine_data, length(v_in_pv)) + affine_data_np = Dict{Tuple{MOI.VariableIndex,Int},T}() + sizehint!(affine_data_np, length(v)) + for term in v + if term.scalar_term.variable in v_in_pv + base = get(affine_data, (term.scalar_term.variable, term.output_index), zero(T)) + affine_data[(term.scalar_term.variable, term.output_index)] = term.scalar_term.coefficient + base + else + base = get(affine_data_np, (term.scalar_term.variable, term.output_index), zero(T)) + affine_data_np[(term.scalar_term.variable, term.output_index)] = term.scalar_term.coefficient + base + end + end + + return ParametricVectorQuadraticFunction{T}( + affine_data, + affine_data_np, + pv, + pp, + vv, + p, + v, + copy(f.constants), + zeros(T, length(f.constants)), + Dict{Tuple{MOI.VariableIndex,Int},T}(), + zeros(T, length(f.constants)), + ) +end + +function vector_quadratic_parameter_variable_terms(f::ParametricVectorQuadraticFunction) + return f.pv +end + +function vector_quadratic_parameter_parameter_terms(f::ParametricVectorQuadraticFunction) + return f.pp +end + +function vector_quadratic_variable_variable_terms(f::ParametricVectorQuadraticFunction) + return f.vv +end + +function vector_affine_parameter_terms(f::ParametricVectorQuadraticFunction) + return f.p +end + +function vector_affine_variable_terms(f::ParametricVectorQuadraticFunction) + return f.v +end + +function _split_vector_quadratic_terms( + terms::Vector{MOI.VectorQuadraticTerm{T}}, +) where {T} + num_vv = 0 + num_pp = 0 + num_pv = 0 + for term in terms + if _is_variable(term.scalar_term.variable_1) + if _is_variable(term.scalar_term.variable_2) + num_vv += 1 + else + num_pv += 1 + end + else + if _is_variable(term.scalar_term.variable_2) + num_pv += 1 + else + num_pp += 1 + end + end + end + vv = Vector{MOI.VectorQuadraticTerm{T}}(undef, num_vv) + pp = Vector{MOI.VectorQuadraticTerm{T}}(undef, num_pp) + pv = Vector{MOI.VectorQuadraticTerm{T}}(undef, num_pv) + i_vv = 1 + i_pp = 1 + i_pv = 1 + for term in terms + if _is_variable(term.scalar_term.variable_1) + if _is_variable(term.scalar_term.variable_2) + vv[i_vv] = term + i_vv += 1 + else + pv[i_pv] = MOI.VectorQuadraticTerm( + term.output_index, + MOI.ScalarQuadraticTerm( + term.scalar_term.coefficient, + term.scalar_term.variable_2, + term.scalar_term.variable_1, + ), + ) + i_pv += 1 + end + else + if _is_variable(term.scalar_term.variable_2) + pv[i_pv] = term + i_pv += 1 + else + pp[i_pp] = term + i_pp += 1 + end + end + end + return pv, pp, vv +end + +function _parametric_affine_terms( + model, + f::ParametricVectorQuadraticFunction{T}, +) where {T} + param_terms_dict = Dict{Tuple{MOI.VariableIndex,Int},T}() + sizehint!(param_terms_dict, length(vector_quadratic_parameter_variable_terms(f))) + + for term in vector_quadratic_parameter_variable_terms(f) + p_idx_val = p_idx(term.scalar_term.variable_1) + var = term.scalar_term.variable_2 + output_idx = term.output_index + base = get(param_terms_dict, (var, output_idx), zero(T)) + param_terms_dict[(var, output_idx)] = + 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 + 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 + +function _update_cache!(f::ParametricVectorQuadraticFunction{T}, model) where {T} + f.current_constant = _parametric_constant(model, f) + f.current_terms_with_p = _parametric_affine_terms(model, f) + return nothing +end + +function _original_function(f::ParametricVectorQuadraticFunction{T}) where {T} + return MOI.VectorQuadraticFunction{T}( + vcat( + vector_quadratic_parameter_variable_terms(f), + vector_quadratic_parameter_parameter_terms(f), + vector_quadratic_variable_variable_terms(f), + ), + vcat(vector_affine_parameter_terms(f), vector_affine_variable_terms(f)), + f.c, + ) +end + +function _parametric_constant( + model, + f::ParametricVectorQuadraticFunction{T}, +) where {T} + param_constant = f.c + + # Add contributions from parameter terms in affine part + for term in vector_affine_parameter_terms(f) + param_constant[term.output_index] += + term.scalar_term.coefficient * + model.parameters[p_idx(term.scalar_term.variable)] + end + + # Add contributions from parameter-parameter quadratic terms + for term in vector_quadratic_parameter_parameter_terms(f) + idx = term.output_index + coef = term.scalar_term.coefficient / + (term.scalar_term.variable_1 == term.scalar_term.variable_2 ? 2 : 1) + param_constant[idx] += coef * + model.parameters[p_idx(term.scalar_term.variable_1)] * + model.parameters[p_idx(term.scalar_term.variable_2)] + end + + return param_constant +end + +function _current_function(f::ParametricVectorQuadraticFunction{T}) where {T} + affine_terms = MOI.VectorAffineTerm{T}[] + sizehint!(affine_terms, length(f.current_constant) + length(f.v)) + for ((var, idx), coef) in f.current_terms_with_p + push!(affine_terms, MOI.VectorAffineTerm{T}(idx, MOI.ScalarAffineTerm{T}(coef, var))) + end + for ((var, idx), coef) in f.affine_data_np + push!(affine_terms, MOI.VectorAffineTerm{T}(idx, MOI.ScalarAffineTerm{T}(coef, var))) + end + return MOI.VectorQuadraticFunction{T}( + f.vv, + affine_terms, + f.current_constant, + ) +end diff --git a/src/update_parameters.jl b/src/update_parameters.jl index 69756f8..4c345b1 100644 --- a/src/update_parameters.jl +++ b/src/update_parameters.jl @@ -160,6 +160,22 @@ function _affine_build_change_and_up_param_func( return changes end +# function _affine_build_change_and_up_param_func( +# pf::ParametricVectorQuadraticFunction{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 changes +# end + function _update_quadratic_constraints!( model::Optimizer, quadratic_constraint_cache_inner::DoubleDictInner{F,S,V}, @@ -272,6 +288,7 @@ function update_parameters!(model::Optimizer) _update_quadratic_constraints!(model) _update_affine_objective!(model) _update_quadratic_objective!(model) + _update_vector_quadratic_constraints!(model) # Update parameters and put NaN to indicate that the parameter has been # updated @@ -284,3 +301,115 @@ function update_parameters!(model::Optimizer) return end + +function _update_vector_quadratic_constraints!(model::Optimizer) + for (F, S) in keys(model.vector_quadratic_constraint_cache.dict) + vector_quadratic_constraint_cache_inner = + model.vector_quadratic_constraint_cache[F, S] + if !isempty(vector_quadratic_constraint_cache_inner) + _update_vector_quadratic_constraints!( + model, + vector_quadratic_constraint_cache_inner, + ) + end + end + 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 + + # 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 + +function _quadratic_build_change_and_up_param_func!( + pf::ParametricVectorQuadraticFunction{T}, + delta_quad_terms::Dict{Int, Vector{MOI.ScalarQuadraticTerm{T}}} +) where {T} + for (output_idx, terms) in delta_quad_terms + if haskey(pf.quadratic_terms_with_p, output_idx) + for term in terms + # Update the current coefficient in the parametric function + for (vars, coeff_info) in pf.quadratic_terms_with_p[output_idx] + param_coeff, current_coeff = coeff_info + pf.quadratic_terms_with_p[output_idx][vars] = (param_coeff, current_coeff + term.coefficient) + end + end + end + end +end + +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 + # MOI.modify( + # model.optimizer, + # inner_ci, + # MOI.VectorConstantChange(pf.current_constant), + # ) + # end + # delta_quad_terms = _delta_parametric_affine_terms(model, pf) + # if !isempty(delta_quad_terms) + # _quadratic_build_change_and_up_param_func!(pf, delta_quad_terms) + # changes = Vector{MOI.ScalarQuadraticTermChange{T}}() + # for (output_idx, terms) in delta_quad_terms + # for term in terms + # push!(changes, MOI.ScalarQuadraticTermChange(output_idx, term)) + # end + # end + # MOI.modify(model.optimizer, inner_ci, changes) + # end + _update_cache!(pf, model) + new_function = _current_function(pf) + if _is_vector_affine(new_function) + # Build new function if affine + new_function = MOI.VectorAffineFunction(new_function.affine_terms, new_function.constants) + end + MOI.set(model.optimizer, MOI.ConstraintFunction(), inner_ci, new_function) + end + + return +end diff --git a/test/jump_tests.jl b/test/jump_tests.jl index cdbc75f..6b52e0c 100644 --- a/test/jump_tests.jl +++ b/test/jump_tests.jl @@ -1274,3 +1274,15 @@ function test_parameter_Cannot_be_inf_2() @test_throws AssertionError MOI.set(model, POI.ParameterValue(), p, Inf) return end + +@testset "JuMP PVQF" begin + model = Model(SCS.Optimizer) + @variable(model, x) + @variable(model, p in MOI.Parameter(1.0)) + @constraint(model, [0, px + -1, 0] in JuMP.PSDCone(3)) + optimize!(model) + @test value(x) ≈ 1.0 atol = 1e-5 + set_value(p, 3.0) + optimize!(model) + @test value(x) ≈ 1/3 atol = 1e-5 +end diff --git a/test/moi_tests.jl b/test/moi_tests.jl index dd967af..cb20a06 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -1982,3 +1982,90 @@ function test_no_quadratic_terms() @test MOI.get(optimizer, MOI.ConstraintDual(), c) ≈ -1 atol = ATOL return end + +#= +# Initialize model with SCS solver and necessary bridges +model = MOI.instantiate(SCS.Optimizer; with_bridge_type = Float64) +MOI.set(model, MOI.Silent(), true) # Disable solver output + +# Add variable +x = MOI.add_variable(model) + +# Set objective: minimize x +MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0) +) +MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + +# Build constraint [0, x-1, 0] ∈ PositiveSemidefiniteConeTriangle(2) +terms = [MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x))] +constants = [0.0, -1.0, 0.0] +vec_func = MOI.VectorAffineFunction(terms, constants) +psd_cone = MOI.PositiveSemidefiniteConeTriangle(2) +c_index = MOI.add_constraint(model, vec_func, psd_cone) + +# Optimize and retrieve results +MOI.optimize!(model) +if MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL + x_val = MOI.get(model, MOI.VariablePrimal(), x) + println("Optimal x: ", x_val) # Expected: x ≈ 1.0 +else + println("Optimization failed.") +end +=# +@testset "Vector Quadratic – parameter update" begin + #= + variables: x + parameters: p + minobjective: 1x + c1: [0, px + -1, 0] in PositiveSemidefiniteConeTriangle(2) + =# + cached = MOI.Bridges.full_bridge_optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + SCS.Optimizer(), + ), + Float64, + ) + + model = POI.Optimizer(cached) + MOI.set(model, MOI.Silent(), true) + x = MOI.add_variable(model) + p = + first.( + MOI.add_constrained_variable.( + model, + MOI.Parameter(1.0), + ), + ) + + # Set objective: minimize x + obj_func = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0) + MOI.set(model, MOI.ObjectiveFunction{typeof(obj_func)}(), obj_func) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + + # Build constraint: [0, px - 1, 0] ∈ PositiveSemidefiniteConeTriangle(2) + quadratic_terms = [ + MOI.VectorQuadraticTerm( + 2, # Index in the output vector (position 2: off-diagonal element) + MOI.ScalarQuadraticTerm(1.0, p, x) # 1.0 * p * x + ) + ] + affine_terms = MOI.VectorAffineTerm{Float64}[] # No affine terms + constants = [0.0, -1.0, 0.0] # Constants for [diag1, off-diag, diag2] + + vec_func = MOI.VectorQuadraticFunction(quadratic_terms, affine_terms, constants) + psd_cone = MOI.PositiveSemidefiniteConeTriangle(2) + c_index = MOI.add_constraint(model, vec_func, psd_cone) + + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1.0 atol=1e-5 + + MOI.set(model, POI.ParameterValue(), p, 3.0) + + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1/3 atol=1e-5 +end