From e5d92b242ce9623bed15e9e7c8561635342cb1d1 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Jul 2025 14:45:19 -0400 Subject: [PATCH 01/19] add ParametricVectorQuadraticFunction --- src/MOI_wrapper.jl | 40 +++++++ src/ParametricOptInterface.jl | 3 + src/duals.jl | 31 ++++++ src/parametric_functions.jl | 204 ++++++++++++++++++++++++++++++++++ src/update_parameters.jl | 34 ++++++ test/jump_tests.jl | 15 +++ test/moi_tests.jl | 35 ++++++ 7 files changed, 362 insertions(+) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index ea9b4d6..2673956 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 _has_parameters(qt.scalar_term) + return true + end + end + # affine part + for at in f.affine_terms + if _has_parameters(at.scalar_term) + return true + end + end + return false +end + function _cache_multiplicative_params!( model::Optimizer{T}, f::ParametricQuadraticFunction{T}, @@ -858,6 +874,23 @@ function MOI.add_constraint( end end +function add_constraint(model::Optimizer, + f::MOI.VectorQuadraticFunction{T}, + S::MOI.AbstractVectorSet) where {T} + if _has_parameters(f) + pvqf = ParametricVectorQuadraticFunction(f) # strip parameters + ci = MOI.add_constraint(model.optimizer, + pvqf.current_function, + S) # plain MOI call + pvqf.ci = ci # remember link + # cache is a DoubleDict keyed by (F,S) like the other caches + model.vector_quadratic_constraint_cache[pvqf, S] = pvqf + return ci + else + return MOI.add_constraint(model.optimizer, f, S) # non-parametric + end +end + function MOI.delete( model::Optimizer, c::MOI.ConstraintIndex{F,S}, @@ -1411,6 +1444,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..d61335e 100644 --- a/src/ParametricOptInterface.jl +++ b/src/ParametricOptInterface.jl @@ -137,6 +137,8 @@ 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}} # objective function data # Clever cache of data (at most one can be !== nothing) @@ -209,6 +211,7 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer DoubleDict{MOI.ConstraintIndex}(), DoubleDict{ParametricQuadraticFunction{T}}(), DoubleDict{MOI.AbstractScalarSet}(), + DoubleDict{ParametricVectorQuadraticFunction{T}}(), # 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..c4fbe52 100644 --- a/src/parametric_functions.jl +++ b/src/parametric_functions.jl @@ -529,3 +529,207 @@ function _update_cache!(f::ParametricVectorAffineFunction{T}, model) where {T} f.current_constant = _parametric_constant(model, f) return nothing end + +mutable struct ParametricVectorQuadraticFunction{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 to avoid slow getters + 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 + + # Only cache affine variable terms that are involved in parameter-variable quadratic terms + v_filtered = Vector{MOI.VectorAffineTerm{T}}() + for term in v + if term.scalar_term.variable in v_in_pv + push!(v_filtered, term) + end + end + + return ParametricVectorQuadraticFunction{T}( + pv, + pp, + vv, + p, + v_filtered, + copy(f.constants), + zeros(T, length(f.constants)), + 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_constant( + model, + f::ParametricVectorQuadraticFunction{T}, +) where {T} + param_constant = copy(f.c) + 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 + 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 _delta_parametric_constant( + model, + f::ParametricVectorQuadraticFunction{T}, +) where {T} + delta_constant = zeros(T, length(f.c)) + for term in vector_affine_parameter_terms(f) + 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 + for term in vector_quadratic_parameter_parameter_terms(f) + idx = term.output_index + p1 = p_idx(term.scalar_term.variable_1) + p2 = p_idx(term.scalar_term.variable_2) + isnan_1 = isnan(model.updated_parameters[p1]) + isnan_2 = isnan(model.updated_parameters[p2]) + if !isnan_1 || !isnan_2 + new_1 = isnan_1 ? model.parameters[p1] : model.updated_parameters[p1] + new_2 = isnan_2 ? model.parameters[p2] : model.updated_parameters[p2] + coef = term.scalar_term.coefficient / + (term.scalar_term.variable_1 == term.scalar_term.variable_2 ? 2 : 1) + delta_constant[idx] += coef * (new_1 * new_2 - model.parameters[p1] * model.parameters[p2]) + end + end + return delta_constant +end + +function _update_cache!(f::ParametricVectorQuadraticFunction{T}, model) where {T} + f.current_constant = _parametric_constant(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 _current_function(f::ParametricVectorQuadraticFunction{T}) where {T} + return MOI.VectorQuadraticFunction{T}( + vector_quadratic_variable_variable_terms(f), + vector_affine_variable_terms(f), + f.current_constant, + ) +end \ No newline at end of file diff --git a/src/update_parameters.jl b/src/update_parameters.jl index 69756f8..cca6705 100644 --- a/src/update_parameters.jl +++ b/src/update_parameters.jl @@ -272,6 +272,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 +285,36 @@ 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 _update_vector_quadratic_constraints!( + model::Optimizer, + vector_quadratic_constraint_cache_inner::DoubleDictInner{F,S,V}, +) where {F<:MOI.VectorQuadraticFunction{T},S,V} where {T} + for (inner_ci, pf) in vector_quadratic_constraint_cache_inner + delta_constant = _delta_parametric_constant(model, pf) + if !iszero(sum(abs, delta_constant)) + pf.current_constant .+= delta_constant + MOI.modify( + model.optimizer, + inner_ci, + MOI.VectorConstantChange(pf.current_constant), + ) + end + # TODO: handle variable coefficients if needed + end + return +end \ No newline at end of file diff --git a/test/jump_tests.jl b/test/jump_tests.jl index cdbc75f..0cc6261 100644 --- a/test/jump_tests.jl +++ b/test/jump_tests.jl @@ -1274,3 +1274,18 @@ 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(Optimizer) + @variable(model, x >= 0) + p = add_parameter(model, 0.5) + + @constraint(model, [ p * x + x^2 ; 1 ] .== [ 0 ; 0 ]) + optimize!(model) + @test termination_status(model) == MOI.LOCALLY_SOLVED + + set_value(p, 4.0) + update_parameters!(backend(model)) + optimize!(model) + @test primal_status(model) == MOI.FEASIBLE_POINT +end diff --git a/test/moi_tests.jl b/test/moi_tests.jl index dd967af..bdbb0e0 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -1982,3 +1982,38 @@ function test_no_quadratic_terms() @test MOI.get(optimizer, MOI.ConstraintDual(), c) ≈ -1 atol = ATOL return end + +@testset "Vector Quadratic – parameter update" begin + model = Optimizer() + @variable(model, x) + p = MOI.add_parameter(model, 1.0) # initial value 1.0 + + # f₁ = p * x + x² (output index 1) + # f₂ = 2p² + 3x² + 4 (output index 2) + f = MOI.VectorQuadraticFunction( + [ + MOI.VectorQuadraticTerm(1, + MOI.ScalarQuadraticTerm(1.0, v_idx(p), x)) # p·x + ], # pv + [ + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(4.0, x)) # 4·x (plain v) + ], # v + [0.0, 4.0], # c + ) + ci = @constraint(model, f in MOI.Zeros(2)) + + MOI.optimize!(model.optimizer) + @test value(x) ≈ 0.0 atol=1e-8 + + # --- update parameter --- + MOI.set(model, MOI.ParameterValue(), p, 3.0) + update_parameters!(model) + + # After the update the constant term of output‑2 should be + # 2*p^2 + 4 = 22 + f_cur = MOI.get(model.optimizer, MOI.ConstraintFunction(), ci) + @test f_cur.constant[2] ≈ 22.0 atol=1e-8 + # and the coefficient of x in output‑1 should be 3 (new p) + coeff = first(f_cur.affine_terms).scalar_term.coefficient + @test coeff ≈ 3.0 atol=1e-8 +end From a29973d38b2bb3ea6ce262f4d003b90fed94e6cf Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Jul 2025 08:47:39 -0400 Subject: [PATCH 02/19] use at_variable and remove unecessary calls --- test/jump_tests.jl | 5 ++--- test/moi_tests.jl | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/test/jump_tests.jl b/test/jump_tests.jl index 0cc6261..186ee54 100644 --- a/test/jump_tests.jl +++ b/test/jump_tests.jl @@ -1278,14 +1278,13 @@ end @testset "JuMP PVQF" begin model = Model(Optimizer) @variable(model, x >= 0) - p = add_parameter(model, 0.5) - + @variable(model, p in MOI.Parameter(0.5)) @constraint(model, [ p * x + x^2 ; 1 ] .== [ 0 ; 0 ]) optimize!(model) @test termination_status(model) == MOI.LOCALLY_SOLVED set_value(p, 4.0) - update_parameters!(backend(model)) + optimize!(model) @test primal_status(model) == MOI.FEASIBLE_POINT end diff --git a/test/moi_tests.jl b/test/moi_tests.jl index bdbb0e0..588ee87 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -1986,7 +1986,7 @@ end @testset "Vector Quadratic – parameter update" begin model = Optimizer() @variable(model, x) - p = MOI.add_parameter(model, 1.0) # initial value 1.0 + @variable(model, p in MOI.Parameter(1.0)) # f₁ = p * x + x² (output index 1) # f₂ = 2p² + 3x² + 4 (output index 2) From e7363da34fac1389779f451439ff6066f94ecbae Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Jul 2025 08:51:03 -0400 Subject: [PATCH 03/19] start addressing variable coefficients --- src/update_parameters.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/update_parameters.jl b/src/update_parameters.jl index cca6705..41c3b74 100644 --- a/src/update_parameters.jl +++ b/src/update_parameters.jl @@ -314,7 +314,12 @@ function _update_vector_quadratic_constraints!( MOI.VectorConstantChange(pf.current_constant), ) end - # TODO: handle variable coefficients if needed + 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 end return end \ No newline at end of file From e4f68c4a45abe6ea2d42aa23def88df086343b1f Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Jul 2025 10:02:37 -0400 Subject: [PATCH 04/19] update tests --- test/moi_tests.jl | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/test/moi_tests.jl b/test/moi_tests.jl index 588ee87..085e24a 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -1984,23 +1984,33 @@ function test_no_quadratic_terms() end @testset "Vector Quadratic – parameter update" begin - model = Optimizer() - @variable(model, x) - @variable(model, p in MOI.Parameter(1.0)) + ipopt = Ipopt.Optimizer() + model = POI.Optimizer(ipopt) + MOI.set(model, MOI.Silent(), true) + x = MOI.add_variable(model) + p = + first.( + MOI.add_constrained_variable.( + model, + MOI.Parameter(1.0), + ), + ) # f₁ = p * x + x² (output index 1) # f₂ = 2p² + 3x² + 4 (output index 2) f = MOI.VectorQuadraticFunction( [ MOI.VectorQuadraticTerm(1, - MOI.ScalarQuadraticTerm(1.0, v_idx(p), x)) # p·x + MOI.ScalarQuadraticTerm(1.0, p, x)) # p·x ], # pv [ MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(4.0, x)) # 4·x (plain v) ], # v [0.0, 4.0], # c ) - ci = @constraint(model, f in MOI.Zeros(2)) + + # f .>= 0.0 + MOI.add_constraint(model, f, MOI.Nonnegatives(2)) MOI.optimize!(model.optimizer) @test value(x) ≈ 0.0 atol=1e-8 From 58b4270c583d5cd7e1af5ae384662ee6a8eceeb8 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Jul 2025 10:32:13 -0400 Subject: [PATCH 05/19] fix bugs --- src/MOI_wrapper.jl | 4 ++-- test/moi_tests.jl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 2673956..82888b7 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -54,13 +54,13 @@ end function _has_parameters(f::MOI.VectorQuadraticFunction) # quadratic part for qt in f.quadratic_terms - if _has_parameters(qt.scalar_term) + 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 _has_parameters(at.scalar_term) + if _is_parameter(at.scalar_term.variable) return true end end diff --git a/test/moi_tests.jl b/test/moi_tests.jl index 085e24a..21e08cd 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -2010,7 +2010,7 @@ end ) # f .>= 0.0 - MOI.add_constraint(model, f, MOI.Nonnegatives(2)) + POI.add_constraint(model, f, MOI.Nonnegatives(2)) MOI.optimize!(model.optimizer) @test value(x) ≈ 0.0 atol=1e-8 From b247e969e38e76b06f053ce8f3de61b69dba6b60 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Jul 2025 11:04:04 -0400 Subject: [PATCH 06/19] fix bug --- src/MOI_wrapper.jl | 25 +++++++++++++------------ test/moi_tests.jl | 10 +++++++--- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 82888b7..debca1e 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -874,20 +874,21 @@ function MOI.add_constraint( end end -function add_constraint(model::Optimizer, - f::MOI.VectorQuadraticFunction{T}, - S::MOI.AbstractVectorSet) where {T} +function MOI.add_constraint( + model::Optimizer, + f::MOI.VectorQuadraticFunction{T}, + set::MOI.AbstractVectorSet, +) where {T} if _has_parameters(f) - pvqf = ParametricVectorQuadraticFunction(f) # strip parameters - ci = MOI.add_constraint(model.optimizer, - pvqf.current_function, - S) # plain MOI call - pvqf.ci = ci # remember link - # cache is a DoubleDict keyed by (F,S) like the other caches - model.vector_quadratic_constraint_cache[pvqf, S] = pvqf - return ci + # wrap into our parametric form + pvqf = ParametricVectorQuadraticFunction(f) + # initialize constant cache + _update_cache!(pvqf, model) + # add the stripped (no‐parameter) constraint, caching for duals/updates + return _add_constraint_direct_and_cache_map!(model, _current_function(pvqf), set) else - return MOI.add_constraint(model.optimizer, f, S) # non-parametric + # no parameters → delegate to direct helper + return _add_constraint_direct_and_cache_map!(model, f, set) end end diff --git a/test/moi_tests.jl b/test/moi_tests.jl index 21e08cd..5616086 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -1984,8 +1984,12 @@ function test_no_quadratic_terms() end @testset "Vector Quadratic – parameter update" begin - ipopt = Ipopt.Optimizer() - model = POI.Optimizer(ipopt) + cached = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + SCS.Optimizer(), + ) + + model = POI.Optimizer(cached) MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) p = @@ -2010,7 +2014,7 @@ end ) # f .>= 0.0 - POI.add_constraint(model, f, MOI.Nonnegatives(2)) + MOI.add_constraint(model, f, MOI.PositiveSemidefiniteConeSquare(2)) MOI.optimize!(model.optimizer) @test value(x) ≈ 0.0 atol=1e-8 From 5490b49fce5bee51556ff346435be8964a26c51d Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Jul 2025 14:25:20 -0400 Subject: [PATCH 07/19] update add constraint --- src/MOI_wrapper.jl | 50 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index debca1e..02d6c3c 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -81,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 # @@ -874,21 +889,38 @@ function MOI.add_constraint( end end +function _add_constraint_with_parameters_on_function( + model::Optimizer, + f::MOI.VectorQuadraticFunction{T}, + set::S, +) where {T,S} + # wrap into our parametric type + pf = ParametricVectorQuadraticFunction(f) + _cache_multiplicative_params!(model, pf) + _update_cache!(pf, model) + + # strip parameters and add to underlying optimizer + fq = _current_function(pf) + inner_ci = MOI.add_constraint(model.optimizer, fq, set) + + # cache for future duals/updates + model.vector_quadratic_constraint_cache[inner_ci] = pf + + # register in our outer→inner map + _add_to_constraint_map!(model, inner_ci) + + return inner_ci +end + function MOI.add_constraint( model::Optimizer, f::MOI.VectorQuadraticFunction{T}, set::MOI.AbstractVectorSet, ) where {T} - if _has_parameters(f) - # wrap into our parametric form - pvqf = ParametricVectorQuadraticFunction(f) - # initialize constant cache - _update_cache!(pvqf, model) - # add the stripped (no‐parameter) constraint, caching for duals/updates - return _add_constraint_direct_and_cache_map!(model, _current_function(pvqf), set) - else - # no parameters → delegate to direct helper + 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 From 8f6ba8c08feef9b93b309cbd7001156cadffc79a Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Jul 2025 15:20:17 -0400 Subject: [PATCH 08/19] update test --- test/moi_tests.jl | 80 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/test/moi_tests.jl b/test/moi_tests.jl index 5616086..921d6b8 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -1983,7 +1983,45 @@ function test_no_quadratic_terms() 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.Utilities.CachingOptimizer( MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), SCS.Optimizer(), @@ -2000,34 +2038,32 @@ end ), ) - # f₁ = p * x + x² (output index 1) - # f₂ = 2p² + 3x² + 4 (output index 2) - f = MOI.VectorQuadraticFunction( - [ - MOI.VectorQuadraticTerm(1, - MOI.ScalarQuadraticTerm(1.0, p, x)) # p·x - ], # pv - [ - MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(4.0, x)) # 4·x (plain v) - ], # v - [0.0, 4.0], # c - ) + # 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] - # f .>= 0.0 - MOI.add_constraint(model, f, MOI.PositiveSemidefiniteConeSquare(2)) + 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.optimizer) - @test value(x) ≈ 0.0 atol=1e-8 + @test value(x) ≈ 1.0 atol=1e-8 # --- update parameter --- MOI.set(model, MOI.ParameterValue(), p, 3.0) update_parameters!(model) - # After the update the constant term of output‑2 should be - # 2*p^2 + 4 = 22 - f_cur = MOI.get(model.optimizer, MOI.ConstraintFunction(), ci) - @test f_cur.constant[2] ≈ 22.0 atol=1e-8 - # and the coefficient of x in output‑1 should be 3 (new p) - coeff = first(f_cur.affine_terms).scalar_term.coefficient - @test coeff ≈ 3.0 atol=1e-8 + MOI.optimize!(model.optimizer) + @test value(x) ≈ 1/3 atol=1e-8 end From ecdeeec08a62ac1a69d08db4b982429ef8e259d6 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Jul 2025 15:22:30 -0400 Subject: [PATCH 09/19] update jump test --- test/jump_tests.jl | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/jump_tests.jl b/test/jump_tests.jl index 186ee54..6b52e0c 100644 --- a/test/jump_tests.jl +++ b/test/jump_tests.jl @@ -1276,15 +1276,13 @@ function test_parameter_Cannot_be_inf_2() end @testset "JuMP PVQF" begin - model = Model(Optimizer) - @variable(model, x >= 0) - @variable(model, p in MOI.Parameter(0.5)) - @constraint(model, [ p * x + x^2 ; 1 ] .== [ 0 ; 0 ]) + 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 termination_status(model) == MOI.LOCALLY_SOLVED - - set_value(p, 4.0) - + @test value(x) ≈ 1.0 atol = 1e-5 + set_value(p, 3.0) optimize!(model) - @test primal_status(model) == MOI.FEASIBLE_POINT + @test value(x) ≈ 1/3 atol = 1e-5 end From 0c32ad9d0e254cefd4468ac46ab6eb6e98d53bc5 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Jul 2025 15:33:54 -0400 Subject: [PATCH 10/19] fix test --- src/MOI_wrapper.jl | 15 +++++++++++++-- test/moi_tests.jl | 11 +++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 02d6c3c..4828ed9 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -889,6 +889,10 @@ function MOI.add_constraint( end end +function _is_vector_affine(f::MOI.VectorQuadraticFunction{T}) where {T} + return isempty(f.quadratic_terms) +end + function _add_constraint_with_parameters_on_function( model::Optimizer, f::MOI.VectorQuadraticFunction{T}, @@ -901,8 +905,15 @@ function _add_constraint_with_parameters_on_function( # strip parameters and add to underlying optimizer fq = _current_function(pf) - inner_ci = MOI.add_constraint(model.optimizer, fq, set) - + + # Check if the function is actually affine after parameter evaluation + if _is_vector_affine(fq) # Need to implement this function + fa = MOI.VectorAffineFunction(fq.affine_terms, fq.constants) + inner_ci = MOI.add_constraint(model.optimizer, fa, set) + else + inner_ci = MOI.add_constraint(model.optimizer, fq, set) + end + # cache for future duals/updates model.vector_quadratic_constraint_cache[inner_ci] = pf diff --git a/test/moi_tests.jl b/test/moi_tests.jl index 921d6b8..ef34cb9 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -2022,9 +2022,12 @@ end minobjective: 1x c1: [0, px + -1, 0] in PositiveSemidefiniteConeTriangle(2) =# - cached = MOI.Utilities.CachingOptimizer( - MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), - SCS.Optimizer(), + cached = MOI.Bridges.full_bridge_optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + SCS.Optimizer(), + ), + Float64, ) model = POI.Optimizer(cached) @@ -2061,7 +2064,7 @@ end @test value(x) ≈ 1.0 atol=1e-8 # --- update parameter --- - MOI.set(model, MOI.ParameterValue(), p, 3.0) + MOI.set(model, POI.ParameterValue(), p, 3.0) update_parameters!(model) MOI.optimize!(model.optimizer) From 7460ae65013bf41305aa8a739f6021081d564c98 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Jul 2025 15:34:27 -0400 Subject: [PATCH 11/19] rm update --- test/moi_tests.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/moi_tests.jl b/test/moi_tests.jl index ef34cb9..0783fad 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -2063,9 +2063,7 @@ end MOI.optimize!(model.optimizer) @test value(x) ≈ 1.0 atol=1e-8 - # --- update parameter --- MOI.set(model, POI.ParameterValue(), p, 3.0) - update_parameters!(model) MOI.optimize!(model.optimizer) @test value(x) ≈ 1/3 atol=1e-8 From 819cc2a4aa7af167dde39c853b52e34de054be54 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Jul 2025 15:44:28 -0400 Subject: [PATCH 12/19] update add_constraint --- src/MOI_wrapper.jl | 16 ++++++++++++---- test/moi_tests.jl | 9 +++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 4828ed9..178b2b0 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -907,15 +907,23 @@ function _add_constraint_with_parameters_on_function( fq = _current_function(pf) # Check if the function is actually affine after parameter evaluation - if _is_vector_affine(fq) # Need to implement this function + if _is_vector_affine(fq) fa = MOI.VectorAffineFunction(fq.affine_terms, fq.constants) inner_ci = MOI.add_constraint(model.optimizer, fa, set) + # Convert to ParametricVectorAffineFunction and store in the correct cache + pf_affine = ParametricVectorAffineFunction( + pf.p, # parameter terms + pf.v, # variable terms + pf.c, # constants + pf.set_constant, + pf.current_constant + ) + model.vector_affine_constraint_cache[inner_ci] = pf_affine else inner_ci = MOI.add_constraint(model.optimizer, fq, set) + # cache for future duals/updates + model.vector_quadratic_constraint_cache[inner_ci] = pf end - - # cache for future duals/updates - model.vector_quadratic_constraint_cache[inner_ci] = pf # register in our outer→inner map _add_to_constraint_map!(model, inner_ci) diff --git a/test/moi_tests.jl b/test/moi_tests.jl index 0783fad..8e103e7 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -2060,11 +2060,12 @@ end psd_cone = MOI.PositiveSemidefiniteConeTriangle(2) c_index = MOI.add_constraint(model, vec_func, psd_cone) - MOI.optimize!(model.optimizer) - @test value(x) ≈ 1.0 atol=1e-8 + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1.0 atol=1e-8 MOI.set(model, POI.ParameterValue(), p, 3.0) - MOI.optimize!(model.optimizer) - @test value(x) ≈ 1/3 atol=1e-8 + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1/3 atol=1e-8 end From 1a2bb27a13bbefcfb7632027dac53c2608ee3177 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 18 Jul 2025 09:44:17 -0400 Subject: [PATCH 13/19] attemp replace --- src/MOI_wrapper.jl | 38 +++------ src/parametric_functions.jl | 140 ++++++++++++++++++++----------- src/update_parameters.jl | 161 ++++++++++++++++++++++++++++++++---- 3 files changed, 252 insertions(+), 87 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 178b2b0..2b72ab8 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -893,41 +893,29 @@ 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} - # wrap into our parametric type + # Create parametric vector quadratic function pf = ParametricVectorQuadraticFunction(f) _cache_multiplicative_params!(model, pf) _update_cache!(pf, model) - - # strip parameters and add to underlying optimizer - fq = _current_function(pf) - # Check if the function is actually affine after parameter evaluation - if _is_vector_affine(fq) - fa = MOI.VectorAffineFunction(fq.affine_terms, fq.constants) - inner_ci = MOI.add_constraint(model.optimizer, fa, set) - # Convert to ParametricVectorAffineFunction and store in the correct cache - pf_affine = ParametricVectorAffineFunction( - pf.p, # parameter terms - pf.v, # variable terms - pf.c, # constants - pf.set_constant, - pf.current_constant - ) - model.vector_affine_constraint_cache[inner_ci] = pf_affine - else - inner_ci = MOI.add_constraint(model.optimizer, fq, set) - # cache for future duals/updates - model.vector_quadratic_constraint_cache[inner_ci] = pf - end - - # register in our outer→inner map + # Get the current function after parameter substitution + current_func = _current_function(pf) + + # Add the constraint with whatever function type we got + inner_ci = MOI.add_constraint(model.optimizer, current_func, set) + + # Store in the vector quadratic cache for future updates + model.vector_quadratic_constraint_cache[inner_ci] = pf _add_to_constraint_map!(model, inner_ci) - return inner_ci end diff --git a/src/parametric_functions.jl b/src/parametric_functions.jl index c4fbe52..258bf1b 100644 --- a/src/parametric_functions.jl +++ b/src/parametric_functions.jl @@ -658,59 +658,74 @@ function _split_vector_quadratic_terms( return pv, pp, vv end -function _parametric_constant( +function _delta_parametric_affine_terms( model, f::ParametricVectorQuadraticFunction{T}, ) where {T} - param_constant = copy(f.c) - 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)] + 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 - 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)] + + # 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 param_constant + + return delta_terms end -function _delta_parametric_constant( - model, - f::ParametricVectorQuadraticFunction{T}, -) where {T} - delta_constant = zeros(T, length(f.c)) - for term in vector_affine_parameter_terms(f) - 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 +function _update_cache!(f::ParametricVectorQuadraticFunction{T}, model) where {T} + # Update the cached constant values + param_constant = copy(f.c) + + # Add parameter contributions from affine terms + for term in f.p + param_idx = p_idx(term.scalar_term.variable) + param_val = model.parameters[param_idx] + param_constant[term.output_index] += term.scalar_term.coefficient * param_val end - for term in vector_quadratic_parameter_parameter_terms(f) + + # Add parameter-parameter quadratic terms to constants + for term in f.pp idx = term.output_index p1 = p_idx(term.scalar_term.variable_1) p2 = p_idx(term.scalar_term.variable_2) - isnan_1 = isnan(model.updated_parameters[p1]) - isnan_2 = isnan(model.updated_parameters[p2]) - if !isnan_1 || !isnan_2 - new_1 = isnan_1 ? model.parameters[p1] : model.updated_parameters[p1] - new_2 = isnan_2 ? model.parameters[p2] : model.updated_parameters[p2] - coef = term.scalar_term.coefficient / - (term.scalar_term.variable_1 == term.scalar_term.variable_2 ? 2 : 1) - delta_constant[idx] += coef * (new_1 * new_2 - model.parameters[p1] * model.parameters[p2]) + param_val1 = model.parameters[p1] + param_val2 = model.parameters[p2] + + coef = term.scalar_term.coefficient + if term.scalar_term.variable_1 == term.scalar_term.variable_2 + coef = coef / 2 # Handle diagonal terms end + + param_constant[idx] += coef * param_val1 * param_val2 end - return delta_constant -end - -function _update_cache!(f::ParametricVectorQuadraticFunction{T}, model) where {T} - f.current_constant = _parametric_constant(model, f) + + f.current_constant = param_constant return nothing end @@ -726,10 +741,43 @@ function _original_function(f::ParametricVectorQuadraticFunction{T}) where {T} ) end +function _parametric_constant( + model, + f::ParametricVectorQuadraticFunction{T}, +) where {T} + param_constant = copy(f.c) + + # Add contributions from parameter terms in affine part + for term in f.p + 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 f.pp + 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} - return MOI.VectorQuadraticFunction{T}( - vector_quadratic_variable_variable_terms(f), - vector_affine_variable_terms(f), - f.current_constant, - ) -end \ No newline at end of file + # Only include variable-variable quadratic terms + quad_terms = vector_quadratic_variable_variable_terms(f) + + # Only include variable affine terms + affine_terms = vector_affine_variable_terms(f) + + # Return either a VectorQuadraticFunction or VectorAffineFunction based on whether we have quadratic terms + if isempty(quad_terms) + return MOI.VectorAffineFunction(affine_terms, f.current_constant) + else + return MOI.VectorQuadraticFunction(quad_terms, affine_terms, f.current_constant) + end +end diff --git a/src/update_parameters.jl b/src/update_parameters.jl index 41c3b74..876649b 100644 --- a/src/update_parameters.jl +++ b/src/update_parameters.jl @@ -300,26 +300,155 @@ 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 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_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 (haskey(model.updated_parameters, p1) && !isnan(model.updated_parameters[p1])) || + (haskey(model.updated_parameters, p2) && !isnan(model.updated_parameters[p2])) + + old_val1 = model.parameters[p1] + old_val2 = model.parameters[p2] + new_val1 = haskey(model.updated_parameters, p1) && !isnan(model.updated_parameters[p1]) ? + model.updated_parameters[p1] : old_val1 + new_val2 = haskey(model.updated_parameters, p2) && !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 _delta_parametric_quadratic_terms( + model::Optimizer, + f::ParametricVectorQuadraticFunction{T} +) where {T} + delta_quad_terms = Dict{Int, Vector{MOI.ScalarQuadraticTerm{T}}}() + + for (output_idx, quad_terms) in f.quadratic_terms_with_p + new_terms = MOI.ScalarQuadraticTerm{T}[] + + for (vars, coeff_info) in quad_terms + var1, var2 = vars + param_coeff, current_coeff = coeff_info + + # Calculate new coefficient based on current parameter values + new_coeff = param_coeff + if haskey(model.updated_parameters, var1) && !isnan(model.updated_parameters[var1]) + new_coeff *= model.updated_parameters[var1] + elseif haskey(model.parameters, var1) + new_coeff *= model.parameters[var1] + end + + if haskey(model.updated_parameters, var2) && !isnan(model.updated_parameters[var2]) + new_coeff *= model.updated_parameters[var2] + elseif haskey(model.parameters, var2) + new_coeff *= model.parameters[var2] + end + + # Only add if coefficient changed + if !isapprox(new_coeff, current_coeff) + # Find the actual variable (non-parameter) + actual_var = _is_parameter(model, var1) ? var2 : var1 + push!(new_terms, MOI.ScalarQuadraticTerm(new_coeff - current_coeff, actual_var, actual_var)) + end + end + + if !isempty(new_terms) + delta_quad_terms[output_idx] = new_terms + end + end + + return delta_quad_terms +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<:MOI.VectorQuadraticFunction{T},S,V} where {T} +) where {F,S,V} for (inner_ci, pf) in vector_quadratic_constraint_cache_inner - delta_constant = _delta_parametric_constant(model, pf) - if !iszero(sum(abs, delta_constant)) - pf.current_constant .+= delta_constant - MOI.modify( - model.optimizer, - inner_ci, - MOI.VectorConstantChange(pf.current_constant), - ) - end - 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) + # First, save the old state + old_constant = copy(pf.current_constant) + + # Update the parametric function cache + _update_cache!(pf, model) + + # Determine if constants changed + constant_changed = !isapprox(old_constant, pf.current_constant) + + # Get the current function after parameter updates + current_func = _current_function(pf) + + # Always replace the function to ensure all parameter updates are applied + try + # Try to update the function directly + MOI.set(model.optimizer, MOI.ConstraintFunction(), inner_ci, current_func) + catch e + # If that fails, recreate the constraint + constraint_set = MOI.get(model.optimizer, MOI.ConstraintSet(), inner_ci) + MOI.delete(model.optimizer, inner_ci) + + # Add with the new function + new_ci = MOI.add_constraint(model.optimizer, current_func, constraint_set) + + # Update mappings + for (outer_ci, old_inner_ci) in model.constraint_outer_to_inner + if old_inner_ci == inner_ci + model.constraint_outer_to_inner[outer_ci] = new_ci + break + end + end + + # Update the cache + vector_quadratic_constraint_cache_inner[new_ci] = pf + delete!(vector_quadratic_constraint_cache_inner, inner_ci) + + # Exit this iteration since we've deleted the original constraint + continue end end + return -end \ No newline at end of file +end From 4f963c3f833ffe44cd8cee680cd9ead8d1507543 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 18 Jul 2025 12:29:45 -0400 Subject: [PATCH 14/19] update functions --- src/parametric_functions.jl | 107 +++++++++++++++------------ src/update_parameters.jl | 141 +++++++++++++++--------------------- 2 files changed, 119 insertions(+), 129 deletions(-) diff --git a/src/parametric_functions.jl b/src/parametric_functions.jl index 258bf1b..e36d5d9 100644 --- a/src/parametric_functions.jl +++ b/src/parametric_functions.jl @@ -268,6 +268,31 @@ function _parametric_affine_terms( return param_terms_dict 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::ParametricQuadraticFunction{T}, @@ -531,6 +556,9 @@ function _update_cache!(f::ParametricVectorAffineFunction{T}, model) where {T} 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 @@ -545,7 +573,8 @@ mutable struct ParametricVectorQuadraticFunction{T} c::Vector{T} # to avoid unnecessary lookups in updates set_constant::Vector{T} - # cache to avoid slow getters + # 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 @@ -561,23 +590,31 @@ function ParametricVectorQuadraticFunction( for term in pv push!(v_in_pv, term.scalar_term.variable_2) end - - # Only cache affine variable terms that are involved in parameter-variable quadratic terms - v_filtered = Vector{MOI.VectorAffineTerm{T}}() + 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 - push!(v_filtered, term) + 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_filtered, + v, copy(f.constants), zeros(T, length(f.constants)), + Dict{Tuple{MOI.VariableIndex,Int},T}(), zeros(T, length(f.constants)), ) end @@ -699,33 +736,8 @@ function _delta_parametric_affine_terms( end function _update_cache!(f::ParametricVectorQuadraticFunction{T}, model) where {T} - # Update the cached constant values - param_constant = copy(f.c) - - # Add parameter contributions from affine terms - for term in f.p - param_idx = p_idx(term.scalar_term.variable) - param_val = model.parameters[param_idx] - param_constant[term.output_index] += term.scalar_term.coefficient * param_val - end - - # Add parameter-parameter quadratic terms to constants - for term in f.pp - idx = term.output_index - p1 = p_idx(term.scalar_term.variable_1) - p2 = p_idx(term.scalar_term.variable_2) - param_val1 = model.parameters[p1] - param_val2 = model.parameters[p2] - - coef = term.scalar_term.coefficient - if term.scalar_term.variable_1 == term.scalar_term.variable_2 - coef = coef / 2 # Handle diagonal terms - end - - param_constant[idx] += coef * param_val1 * param_val2 - end - - f.current_constant = param_constant + f.current_constant = _parametric_constant(model, f) + f.current_terms_with_p = _parametric_affine_terms(model, f) return nothing end @@ -745,17 +757,17 @@ function _parametric_constant( model, f::ParametricVectorQuadraticFunction{T}, ) where {T} - param_constant = copy(f.c) + param_constant = f.c # Add contributions from parameter terms in affine part - for term in f.p + 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 f.pp + 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) @@ -768,16 +780,17 @@ function _parametric_constant( end function _current_function(f::ParametricVectorQuadraticFunction{T}) where {T} - # Only include variable-variable quadratic terms - quad_terms = vector_quadratic_variable_variable_terms(f) - - # Only include variable affine terms - affine_terms = vector_affine_variable_terms(f) - - # Return either a VectorQuadraticFunction or VectorAffineFunction based on whether we have quadratic terms - if isempty(quad_terms) - return MOI.VectorAffineFunction(affine_terms, f.current_constant) - else - return MOI.VectorQuadraticFunction(quad_terms, affine_terms, f.current_constant) + affine_terms = MOI.VectorAffineFunction{T}[] + sizehint!(affine_terms, length(f.current_constant) + length(f.v)) + for (var, coef) in f.current_terms_with_p + push!(affine_terms, MOI.VectorAffineTerm{T}(coef, var)) + end + for (var, coef) in f.affine_data_np + push!(affine_terms, MOI.VectorAffineTerm{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 876649b..faa41f4 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}, @@ -311,7 +327,7 @@ function _delta_parametric_constant( 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]) + 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) @@ -326,14 +342,14 @@ function _delta_parametric_constant( p1 = p_idx(var1) p2 = p_idx(var2) - if (haskey(model.updated_parameters, p1) && !isnan(model.updated_parameters[p1])) || - (haskey(model.updated_parameters, p2) && !isnan(model.updated_parameters[p2])) + if !isnan(model.updated_parameters[p1]) || + !isnan(model.updated_parameters[p2]) old_val1 = model.parameters[p1] old_val2 = model.parameters[p2] - new_val1 = haskey(model.updated_parameters, p1) && !isnan(model.updated_parameters[p1]) ? + new_val1 = !isnan(model.updated_parameters[p1]) ? model.updated_parameters[p1] : old_val1 - new_val2 = haskey(model.updated_parameters, p2) && !isnan(model.updated_parameters[p2]) ? + new_val2 = !isnan(model.updated_parameters[p2]) ? model.updated_parameters[p2] : old_val2 coef = term.scalar_term.coefficient / (var1 == var2 ? 2 : 1) @@ -344,47 +360,24 @@ function _delta_parametric_constant( return delta_constants end -function _delta_parametric_quadratic_terms( - model::Optimizer, - f::ParametricVectorQuadraticFunction{T} +function _delta_parametric_affine_terms( + model, + f::ParametricVectorQuadraticFunction{T}, ) where {T} - delta_quad_terms = Dict{Int, Vector{MOI.ScalarQuadraticTerm{T}}}() - - for (output_idx, quad_terms) in f.quadratic_terms_with_p - new_terms = MOI.ScalarQuadraticTerm{T}[] - - for (vars, coeff_info) in quad_terms - var1, var2 = vars - param_coeff, current_coeff = coeff_info - - # Calculate new coefficient based on current parameter values - new_coeff = param_coeff - if haskey(model.updated_parameters, var1) && !isnan(model.updated_parameters[var1]) - new_coeff *= model.updated_parameters[var1] - elseif haskey(model.parameters, var1) - new_coeff *= model.parameters[var1] - end - - if haskey(model.updated_parameters, var2) && !isnan(model.updated_parameters[var2]) - new_coeff *= model.updated_parameters[var2] - elseif haskey(model.parameters, var2) - new_coeff *= model.parameters[var2] - end - - # Only add if coefficient changed - if !isapprox(new_coeff, current_coeff) - # Find the actual variable (non-parameter) - actual_var = _is_parameter(model, var1) ? var2 : var1 - push!(new_terms, MOI.ScalarQuadraticTerm(new_coeff - current_coeff, actual_var, actual_var)) - end - end - - if !isempty(new_terms) - delta_quad_terms[output_idx] = new_terms + delta_terms_dict = Dict{Tuple{MOI.VariableIndex, Int}, T}() + sizehint!(delta_terms_dict, length(quadratic_parameter_variable_terms(f))) + # remember a variable may appear more than once in pv + for term in f.pp + p = p_idx(term.scalar_term.variable_1) + if !isnan(model.updated_parameters[p]) + base = get(delta_terms_dict, (term.scalar_term.variable_2, term.output_index), zero(T)) + delta_terms_dict[(term.scalar_term.variable_2, term.output_index)] = + base + + term.coefficient * + (model.updated_parameters[p] - model.parameters[p]) end end - - return delta_quad_terms + return delta_terms_dict end function _quadratic_build_change_and_up_param_func!( @@ -409,46 +402,30 @@ function _update_vector_quadratic_constraints!( vector_quadratic_constraint_cache_inner::DoubleDictInner{F,S,V}, ) where {F,S,V} for (inner_ci, pf) in vector_quadratic_constraint_cache_inner - # First, save the old state - old_constant = copy(pf.current_constant) - - # Update the parametric function cache + # 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) - - # Determine if constants changed - constant_changed = !isapprox(old_constant, pf.current_constant) - - # Get the current function after parameter updates - current_func = _current_function(pf) - - # Always replace the function to ensure all parameter updates are applied - try - # Try to update the function directly - MOI.set(model.optimizer, MOI.ConstraintFunction(), inner_ci, current_func) - catch e - # If that fails, recreate the constraint - constraint_set = MOI.get(model.optimizer, MOI.ConstraintSet(), inner_ci) - MOI.delete(model.optimizer, inner_ci) - - # Add with the new function - new_ci = MOI.add_constraint(model.optimizer, current_func, constraint_set) - - # Update mappings - for (outer_ci, old_inner_ci) in model.constraint_outer_to_inner - if old_inner_ci == inner_ci - model.constraint_outer_to_inner[outer_ci] = new_ci - break - end - end - - # Update the cache - vector_quadratic_constraint_cache_inner[new_ci] = pf - delete!(vector_quadratic_constraint_cache_inner, inner_ci) - - # Exit this iteration since we've deleted the original constraint - continue - end + new_function = _current_function(pf) + MOI.set(model.optimizer, MOI.ConstraintFunction(), inner_ci, new_function) end - + return end From 9be1937ec0cadc12ecaf5b59e1c917ef11a49c57 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 18 Jul 2025 15:28:23 -0400 Subject: [PATCH 15/19] update --- src/MOI_wrapper.jl | 9 +----- src/parametric_functions.jl | 60 ++++++++++++++++++------------------- src/update_parameters.jl | 20 ------------- 3 files changed, 31 insertions(+), 58 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 2b72ab8..de84a72 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -909,14 +909,7 @@ function _add_constraint_with_parameters_on_function( # Get the current function after parameter substitution current_func = _current_function(pf) - - # Add the constraint with whatever function type we got - inner_ci = MOI.add_constraint(model.optimizer, current_func, set) - - # Store in the vector quadratic cache for future updates - model.vector_quadratic_constraint_cache[inner_ci] = pf - _add_to_constraint_map!(model, inner_ci) - return inner_ci + # TODO: XXXXX STOPED HERE end function MOI.add_constraint( diff --git a/src/parametric_functions.jl b/src/parametric_functions.jl index e36d5d9..ff70e38 100644 --- a/src/parametric_functions.jl +++ b/src/parametric_functions.jl @@ -268,31 +268,6 @@ function _parametric_affine_terms( return param_terms_dict 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::ParametricQuadraticFunction{T}, @@ -695,6 +670,31 @@ function _split_vector_quadratic_terms( 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}, @@ -780,13 +780,13 @@ function _parametric_constant( end function _current_function(f::ParametricVectorQuadraticFunction{T}) where {T} - affine_terms = MOI.VectorAffineFunction{T}[] + affine_terms = MOI.VectorAffineTerm{T}[] sizehint!(affine_terms, length(f.current_constant) + length(f.v)) - for (var, coef) in f.current_terms_with_p - push!(affine_terms, MOI.VectorAffineTerm{T}(coef, var)) + 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, coef) in f.affine_data_np - push!(affine_terms, MOI.VectorAffineTerm{T}(coef, var)) + 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, diff --git a/src/update_parameters.jl b/src/update_parameters.jl index faa41f4..7aa713f 100644 --- a/src/update_parameters.jl +++ b/src/update_parameters.jl @@ -360,26 +360,6 @@ function _delta_parametric_constant( return delta_constants end -function _delta_parametric_affine_terms( - model, - f::ParametricVectorQuadraticFunction{T}, -) where {T} - delta_terms_dict = Dict{Tuple{MOI.VariableIndex, Int}, T}() - sizehint!(delta_terms_dict, length(quadratic_parameter_variable_terms(f))) - # remember a variable may appear more than once in pv - for term in f.pp - p = p_idx(term.scalar_term.variable_1) - if !isnan(model.updated_parameters[p]) - base = get(delta_terms_dict, (term.scalar_term.variable_2, term.output_index), zero(T)) - delta_terms_dict[(term.scalar_term.variable_2, term.output_index)] = - base + - term.coefficient * - (model.updated_parameters[p] - model.parameters[p]) - end - end - return delta_terms_dict -end - function _quadratic_build_change_and_up_param_func!( pf::ParametricVectorQuadraticFunction{T}, delta_quad_terms::Dict{Int, Vector{MOI.ScalarQuadraticTerm{T}}} From e90065756002b1ca4f94ec0afbb7a3d40845712a Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 21 Jul 2025 11:44:53 -0400 Subject: [PATCH 16/19] update --- src/MOI_wrapper.jl | 54 +++++++++++++++++++++++++++++++++-- src/ParametricOptInterface.jl | 3 ++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index de84a72..bcd2a4d 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -119,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 && @@ -154,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 @@ -569,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}, @@ -614,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] @@ -908,8 +919,31 @@ function _add_constraint_with_parameters_on_function( _update_cache!(pf, model) # Get the current function after parameter substitution - current_func = _current_function(pf) - # TODO: XXXXX STOPED HERE + 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( @@ -924,6 +958,22 @@ function MOI.add_constraint( 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}, diff --git a/src/ParametricOptInterface.jl b/src/ParametricOptInterface.jl index d61335e..6ac64af 100644 --- a/src/ParametricOptInterface.jl +++ b/src/ParametricOptInterface.jl @@ -139,6 +139,8 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer 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) @@ -212,6 +214,7 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer DoubleDict{ParametricQuadraticFunction{T}}(), DoubleDict{MOI.AbstractScalarSet}(), DoubleDict{ParametricVectorQuadraticFunction{T}}(), + DoubleDict{MOI.AbstractVectorSet}(), # objective nothing, nothing, From 551737c31bbb8eb8838cdb43b7c8b81f04be5aef Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 21 Jul 2025 13:48:18 -0400 Subject: [PATCH 17/19] working first set --- src/update_parameters.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/update_parameters.jl b/src/update_parameters.jl index 7aa713f..4c345b1 100644 --- a/src/update_parameters.jl +++ b/src/update_parameters.jl @@ -404,6 +404,10 @@ function _update_vector_quadratic_constraints!( # 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 From b8980d3f61401490cf5cf8d60ef5725f6d7517af Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 21 Jul 2025 13:49:54 -0400 Subject: [PATCH 18/19] update tol --- test/moi_tests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/moi_tests.jl b/test/moi_tests.jl index 8e103e7..400bed6 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -2062,7 +2062,7 @@ end MOI.optimize!(model) @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1.0 atol=1e-8 + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1.0 atol=1e-5 MOI.set(model, POI.ParameterValue(), p, 3.0) From 2d06df63f63a25858ed2487983adc1facc89854d Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 21 Jul 2025 13:55:11 -0400 Subject: [PATCH 19/19] update tol --- test/moi_tests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/moi_tests.jl b/test/moi_tests.jl index 400bed6..cb20a06 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -2067,5 +2067,5 @@ end MOI.set(model, POI.ParameterValue(), p, 3.0) MOI.optimize!(model) - @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1/3 atol=1e-8 + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1/3 atol=1e-5 end