diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index a595277..5c82418 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -1072,6 +1072,7 @@ function _add_constraint_with_parameters_on_function( # Create parametric vector quadratic function pf = ParametricVectorQuadraticFunction(f) _cache_multiplicative_params!(model, pf) + # _cache_set_constant!(pf, set) # there is no constant in vector sets _update_cache!(pf, model) # Get the current function after parameter substitution @@ -1768,6 +1769,10 @@ function MOI.get( DoubleDicts.nonempty_outer_keys(model.quadratic_constraint_cache) push!(output, (F, S, ParametricQuadraticFunction{T})) end + for (F, S) in + DoubleDicts.nonempty_outer_keys(model.vector_quadratic_constraint_cache) + push!(output, (F, S, ParametricVectorQuadraticFunction{T})) + end return collect(output) end @@ -1990,6 +1995,41 @@ function MOI.compute_conflict!(model::Optimizer) end end end + for (F, S) in keys(model.vector_quadratic_constraint_cache.dict) + vector_quadratic_constraint_cache_inner = + model.vector_quadratic_constraint_cache[F, S] + for (inner_ci, pf) in vector_quadratic_constraint_cache_inner + if MOI.get( + model.optimizer, + MOI.ConstraintConflictStatus(), + inner_ci, + ) == MOI.NOT_IN_CONFLICT + continue + end + for term in vector_affine_parameter_terms(pf) + push!( + model.parameters_in_conflict, + term.scalar_term.variable, + ) + end + for term in vector_quadratic_parameter_parameter_terms(pf) + push!( + model.parameters_in_conflict, + term.scalar_term.variable_1, + ) + push!( + model.parameters_in_conflict, + term.scalar_term.variable_2, + ) + end + for term in vector_quadratic_parameter_variable_terms(pf) + push!( + model.parameters_in_conflict, + term.scalar_term.variable_1, + ) + end + end + end end return end diff --git a/src/update_parameters.jl b/src/update_parameters.jl index 23ab7a9..2188add 100644 --- a/src/update_parameters.jl +++ b/src/update_parameters.jl @@ -152,19 +152,17 @@ end function _affine_build_change_and_up_param_func( pf::ParametricVectorQuadraticFunction{T}, - delta_terms, + delta_terms::Dict, ) where {T} + new_terms = Dict{MOI.VariableIndex,Vector{Tuple{Int64,T}}}() for ((var, output_idx), coef) in delta_terms base_coef = pf.current_terms_with_p[(var, output_idx)] new_coef = base_coef + coef pf.current_terms_with_p[(var, output_idx)] = new_coef - end - new_terms = Dict{MOI.VariableIndex,Vector{Tuple{Int64,T}}}() - for ((var, output_idx), coef) in pf.current_terms_with_p - if !iszero(coef) - base = get!(new_terms, var, Tuple{Int64,T}[]) - push!(base, (output_idx, coef)) - end + base = get!(new_terms, var, Tuple{Int64,T}[]) + # we can rely on push because delta_terms if a Dict so coef are + # unique per (var, output_idx) + push!(base, (output_idx, new_coef)) end changes = Vector{MOI.MultirowChange}(undef, length(new_terms)) for (i, (var, tuples)) in enumerate(new_terms) diff --git a/test/test_MathOptInterface.jl b/test/test_MathOptInterface.jl index 46651bc..2e9dccd 100644 --- a/test/test_MathOptInterface.jl +++ b/test/test_MathOptInterface.jl @@ -2380,6 +2380,131 @@ function test_constraint_primal_start_get_for_parameter() return end +function test_vector_quadratic_parameter_update_round_trip() + # Maximize x s.t. x >= 0, p*x <= 1. + # Initial p=2 → x=0.5; update p=4 → x=0.25. + model = POI.Optimizer(SCS.Optimizer) + MOI.set(model, MOI.Silent(), true) + x = MOI.add_variable(model) + p, cp = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) + MOI.add_constraint(model, x, MOI.GreaterThan(0.0)) + # VectorQuadratic: [1.0 - p*x] ∈ Nonnegatives(1) → p*x ≤ 1 + f = MOI.VectorQuadraticFunction( + [MOI.VectorQuadraticTerm(1, MOI.ScalarQuadraticTerm(-1.0, p, x))], + MOI.VectorAffineTerm{Float64}[], + [1.0], + ) + MOI.add_constraint(model, f, MOI.Nonnegatives(1)) + # Minimize -x (maximize x) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(-1.0, x)], 0.0), + ) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 0.5 atol = ATOL + # Update p = 4 → now x ≤ 0.25 + MOI.set(model, MOI.ConstraintSet(), cp, MOI.Parameter(4.0)) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 0.25 atol = ATOL + return +end + +function test_list_of_parametric_constraint_types_with_vector_quadratic() + T = Float64 + model = POI.Optimizer(MOI.Utilities.Model{T}()) + x = MOI.add_variable(model) + p, _cp = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) + # VectorQuadratic constraint with a pv term + f = MOI.VectorQuadraticFunction( + [MOI.VectorQuadraticTerm(1, MOI.ScalarQuadraticTerm(-1.0, p, x))], + MOI.VectorAffineTerm{T}[], + [1.0], + ) + MOI.add_constraint(model, f, MOI.Nonnegatives(1)) + types = MOI.get(model, POI.ListOfParametricConstraintTypesPresent()) + @test any(types) do (_, _, P) + return P == POI.ParametricVectorQuadraticFunction{T} + end + return +end + +function test_compute_conflict_vector_quadratic() + T = Float64 + mock = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{T}()) + MOI.set(mock, MOI.ConflictStatus(), MOI.COMPUTE_CONFLICT_NOT_CALLED) + model = POI.Optimizer( + MOI.Utilities.CachingOptimizer(MOI.Utilities.Model{T}(), mock), + ) + x = MOI.add_variable(model) + p, p_ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) + p2, p2_ci = MOI.add_constrained_variable(model, MOI.Parameter(3.0)) + p3, p3_ci = MOI.add_constrained_variable(model, MOI.Parameter(4.0)) + p4, p4_ci = MOI.add_constrained_variable(model, MOI.Parameter(5.0)) + # f1: pv term (IN_CONFLICT) — covers the pv push branch + f1 = MOI.VectorQuadraticFunction( + [MOI.VectorQuadraticTerm(1, MOI.ScalarQuadraticTerm(-1.0, p, x))], + MOI.VectorAffineTerm{T}[], + [1.0], + ) + # f2: affine parameter term only (IN_CONFLICT) — covers the affine-p push branch + f2 = MOI.VectorQuadraticFunction( + MOI.VectorQuadraticTerm{T}[], + [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, p2))], + [0.0], + ) + # f3: pp term (IN_CONFLICT) — covers the pp push branch + f3 = MOI.VectorQuadraticFunction( + [MOI.VectorQuadraticTerm(1, MOI.ScalarQuadraticTerm(1.0, p3, p3))], + MOI.VectorAffineTerm{T}[], + [0.0], + ) + # f4: pv term (NOT_IN_CONFLICT) — covers the continue branch + f4 = MOI.VectorQuadraticFunction( + [MOI.VectorQuadraticTerm(1, MOI.ScalarQuadraticTerm(-1.0, p4, x))], + MOI.VectorAffineTerm{T}[], + [1.0], + ) + MOI.add_constraint(model, f1, MOI.Nonnegatives(1)) + MOI.add_constraint(model, f2, MOI.Nonnegatives(1)) + MOI.add_constraint(model, f3, MOI.Nonnegatives(1)) + MOI.add_constraint(model, f4, MOI.Nonnegatives(1)) + MOI.Utilities.set_mock_optimize!( + mock, + mock::MOI.Utilities.MockOptimizer -> begin + MOI.Utilities.mock_optimize!( + mock, + MOI.INFEASIBLE, + MOI.NO_SOLUTION, + MOI.NO_SOLUTION; + constraint_conflict_status = [ + (MOI.VectorAffineFunction{T}, MOI.Nonnegatives) => [ + MOI.IN_CONFLICT, # f1 (pv) + MOI.IN_CONFLICT, # f2 (affine-p) + MOI.IN_CONFLICT, # f3 (pp) + MOI.NOT_IN_CONFLICT, # f4 (pv, not conflicting) + ], + ], + ) + MOI.set(mock, MOI.ConflictStatus(), MOI.CONFLICT_FOUND) + end, + ) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE + MOI.compute_conflict!(model) + @test MOI.get(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND + @test MOI.get(model, MOI.ConstraintConflictStatus(), p_ci) == + MOI.MAYBE_IN_CONFLICT # p in f1 (IN_CONFLICT, pv) + @test MOI.get(model, MOI.ConstraintConflictStatus(), p2_ci) == + MOI.MAYBE_IN_CONFLICT # p2 in f2 (IN_CONFLICT, affine-p) + @test MOI.get(model, MOI.ConstraintConflictStatus(), p3_ci) == + MOI.MAYBE_IN_CONFLICT # p3 in f3 (IN_CONFLICT, pp) + @test MOI.get(model, MOI.ConstraintConflictStatus(), p4_ci) == + MOI.NOT_IN_CONFLICT # p4 in f4 (NOT_IN_CONFLICT) + return +end + function test_vector_quadratic_no_parameters_affine_get_constraint_function() # A VectorQuadraticFunction with no parameters and no quadratic terms # (empty quadratic_terms) should use the affine fast path. The outer