From a880cb8f82569f282fd788484d17df28a9a70666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 11 Mar 2026 15:25:50 +0100 Subject: [PATCH 01/41] Add support for starting values --- src/MOI_wrapper.jl | 287 +++++++++++++++++++++++++++++---- src/structures.jl | 23 +-- test/Project.toml | 4 + test/Tests/test_MOI_wrapper.jl | 34 ++-- test/Tests/test_attributes.jl | 145 +++++++++++++++++ test/Tests/test_dual_names.jl | 14 ++ test/runtests.jl | 1 + 7 files changed, 454 insertions(+), 54 deletions(-) create mode 100644 test/Tests/test_attributes.jl diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index b90cbe75..01b0b7cf 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -240,9 +240,11 @@ function MOI.copy_to(dest::DualOptimizer, src::MOI.ModelLike) assume_min_if_feasibility = dest.assume_min_if_feasibility, ) idx_map = MOI.Utilities.IndexMap() - for vi in MOI.get(src, MOI.ListOfVariableIndices()) + vis_src = MOI.get(src, MOI.ListOfVariableIndices()) + for vi in vis_src setindex!(idx_map, vi, vi) end + MOI.Utilities.pass_attributes(dest, src, idx_map, vis_src) for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) for con in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) setindex!(idx_map, con, con) @@ -271,9 +273,119 @@ function MOI.get(optimizer::DualOptimizer, ::MOI.SolverName) return "Dual model with $name attached" end +_minus(::Nothing) = nothing +_minus(x) = -x + +function constraint_attribute(attr::MOI.VariablePrimal) + return MOI.ConstraintPrimal(attr.result_index) +end +function constraint_attribute(attr::MOI.VariablePrimalStart) + return MOI.ConstraintPrimalStart() +end + +struct DualModelAttributeNotDefined <: MOI.AbstractModelAttribute end +struct DualVariableAttributeNotDefined <: MOI.AbstractVariableAttribute end +struct DualConstraintAttributeNotDefined <: MOI.AbstractConstraintAttribute end + +dual_attribute(::MOI.AbstractModelAttribute) = DualModelAttributeNotDefined() +function dual_attribute(::MOI.AbstractVariableAttribute) + return DualConstraintAttributeNotDefined() +end +function dual_attribute(::MOI.AbstractConstraintAttribute) + return DualVariableAttributeNotDefined() +end + +dual_attribute(attr::MOI.ResultCount) = attr + +function dual_attribute(attr::Union{MOI.VariablePrimal,MOI.ConstraintPrimal}) + return MOI.ConstraintDual(attr.result_index) +end + +function dual_attribute( + ::Union{MOI.VariablePrimalStart,MOI.ConstraintPrimalStart}, +) + return MOI.ConstraintDualStart() +end + +function dual_attribute_value( + ::Union{MOI.VariablePrimal,MOI.VariablePrimalStart}, + value, +) + return _minus(value) +end + +function dual_attribute_value( + ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + value, +) + return value +end + +function dual_attribute(attr::MOI.ConstraintDual) + return MOI.ConstraintPrimal(attr.result_index) +end + +function dual_attribute(::MOI.ConstraintDualStart) + return MOI.ConstraintPrimalStart() +end + +function _variable_dual_attribute(attr::MOI.ConstraintDual) + return MOI.VariablePrimal(attr.result_index) +end + +function _variable_dual_attribute(::MOI.ConstraintDualStart) + return MOI.VariablePrimalStart() +end + +# The inner optimizer may not support equality constraints (e.g. MOI.FileFormats.SDPA.Model) +# In this case, if all variables are created using constrained variables then dualization won't +# have to create any equality constraints so it will work. +# In that case, we have two choices: +# 1) say we don't support `MOI.VariablePrimalStart` and ignore them, rely on the value set to +# `MOI.ConstraintPrimalStart` to the constraint associated to the constrained variables +# 2) use the value in `MOI.VariablePrimalStart` as fallback in case `MOI.ConstraintPrimalStart` +# is not set +# The issue with option 2) is that it is difficult to know what type of constraints we should use +# in `MOI.supports` here so we should basically just return `true`. +# But if we `return true` and the solver don't support starting values then it will error, and we +# don't benefit from the silent ignoring of starting values relying on +# https://github.com/jump-dev/MathOptInterface.jl/blob/9884cfacb044724427a7d6c7a21f4bd6ff5a8c15/src/Utilities/copy.jl#L73-L74 +# So let's go for option 1) for now +function MOI.supports( + optimizer::DualOptimizer{T}, + attr::MOI.AbstractVariableAttribute, + ::Type{MOI.VariableIndex}, +) where {T} + return MOI.supports( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, + ) +end + +function MOI.set( + optimizer::DualOptimizer, + attr::MOI.AbstractVariableAttribute, + vi::MOI.VariableIndex, + value, +) + primal_dual_map = optimizer.dual_problem.primal_dual_map + if vi in keys(primal_dual_map.constrained_var_idx) + msg = "Setting starting value for variables constrained at creation is not supported yet" + throw(MOI.SetAttributeNotAllowed(attr, msg)) + end + MOI.set( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + get_ci_dual_problem(optimizer, vi), + dual_attribute_value(attr, value), + ) + return +end + function MOI.get( optimizer::DualOptimizer{T}, - ::MOI.VariablePrimal, + attr::MOI.AbstractVariableAttribute, vi::MOI.VariableIndex, )::T where {T} primal_dual_map = optimizer.dual_problem.primal_dual_map @@ -283,30 +395,67 @@ function MOI.get( elseif data.dual_constraint === nothing return zero(T) elseif data.primal_constrained_variable_constraint === nothing - return -MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), - data.dual_constraint, + return dual_attribute_value( + attr, + MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + data.dual_constraint, + ), ) - elseif data.dual_constraint isa - MOI.ConstraintIndex{<:MOI.AbstractVectorFunction} - return MOI.get( + end + con_attr = constraint_attribute(attr) + value = dual_attribute_value( + con_attr, + MOI.get( optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), + dual_attribute(con_attr), data.dual_constraint, - )[data.primal_constrained_variable_index] + ), + ) + if data.dual_constraint isa + MOI.ConstraintIndex{<:MOI.AbstractVectorFunction} + return value[data.primal_constrained_variable_index] else - return MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), - data.dual_constraint, - ) + return value end end +function MOI.supports( + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + ::Type{<:MOI.ConstraintIndex}, +) + return MOI.supports( + optimizer.dual_problem.dual_model, + _variable_dual_attribute(attr), + MOI.VariableIndex, + ) +end + +function MOI.set( + optimizer::DualOptimizer, + attr::MOI.ConstraintDualStart, + ci::MOI.ConstraintIndex, + value, +) + primal_dual_map = optimizer.dual_problem.primal_dual_map + if ci in keys(primal_dual_map.primal_constrained_variables) + msg = "Setting starting value for variables constrained at creation is not supported yet" + throw(MOI.SetAttributeNotAllowed(attr, msg)) + end + MOI.set( + optimizer.dual_problem.dual_model, + _variable_dual_attribute(attr), + primal_dual_map.primal_constraint_data[ci].dual_variables[], + value, + ) + return +end + function MOI.get( optimizer::DualOptimizer, - ::MOI.ConstraintDual, + attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, ci::MOI.ConstraintIndex{F,S}, ) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} primal_dual_map = optimizer.dual_problem.primal_dual_map @@ -319,7 +468,7 @@ function MOI.get( ) do inner_vi return MOI.get( optimizer.dual_problem.dual_model, - MOI.VariablePrimal(), + _variable_dual_attribute(attr), inner_vi, ) end @@ -331,21 +480,31 @@ function MOI.get( ) return MOI.get( optimizer.dual_problem.dual_model, - MOI.ConstraintPrimal(), + dual_attribute(attr), ci_dual, ) - MOI.constant(set) else + data = primal_dual_map.primal_constraint_data[ci] + ci_dual = data.dual_constrained_variable_constraint + if ci_dual === nothing + # TODO do something else not relying on `_variable_dual_attribute` + return MOI.get( + optimizer.dual_problem.dual_model, + _variable_dual_attribute(attr), + primal_dual_map.primal_constraint_data[ci].dual_variables[], + ) + end return MOI.get( optimizer.dual_problem.dual_model, - MOI.VariablePrimal(), - primal_dual_map.primal_constraint_data[ci].dual_variables[], + dual_attribute(attr), + ci_dual, ) end end function MOI.get( optimizer::DualOptimizer, - ::MOI.ConstraintDual, + attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, ci::MOI.ConstraintIndex{F,S}, ) where {F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} primal_dual_map = optimizer.dual_problem.primal_dual_map @@ -359,7 +518,7 @@ function MOI.get( ) do inner_vi return MOI.get( optimizer.dual_problem.dual_model, - MOI.VariablePrimal(), + _variable_dual_attribute(attr), inner_vi, ) end for vi in vis @@ -367,21 +526,70 @@ function MOI.get( end return MOI.get( optimizer.dual_problem.dual_model, - MOI.ConstraintPrimal(), + dual_attribute(attr), ci_dual, ) else - return MOI.get.( + data = primal_dual_map.primal_constraint_data[ci] + ci_dual = data.dual_constrained_variable_constraint + if ci_dual === nothing + # TODO do something else not relying on `_variable_dual_attribute` + return MOI.get.( + optimizer.dual_problem.dual_model, + _variable_dual_attribute(attr), + primal_dual_map.primal_constraint_data[ci].dual_variables, + ) + end + return MOI.get( optimizer.dual_problem.dual_model, - MOI.VariablePrimal(), - primal_dual_map.primal_constraint_data[ci].dual_variables, + dual_attribute(attr), + ci_dual, ) end end +function MOI.supports( + ::DualOptimizer, + attr::MOI.ConstraintPrimalStart, + C::Type{<:MOI.ConstraintIndex}, +) + return MOI.supports( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + C, + ) +end + +function MOI.set( + optimizer::DualOptimizer, + attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, + value, +) + primal_dual_map = optimizer.dual_problem.primal_dual_map + if ci in keys(primal_dual_map.constrained_var_dual) + error( + "Setting starting value for variables constrained at creation is not supported yet", + ) + elseif haskey(primal_dual_map.primal_con_dual_con, ci) + # If it has no key then there is no dual constraint + ci_dual_problem = get_ci_dual_problem(optimizer, ci) + if !isnothing(value) && (F <: MOI.AbstractScalarFunction) + value -= get_primal_ci_constant(optimizer, ci) + end + MOI.set( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + ci_dual_problem, + value, + ) + end + return +end + function MOI.get( optimizer::DualOptimizer{T}, - ::MOI.ConstraintPrimal, + attr::MOI.ConstraintPrimal, ci::MOI.ConstraintIndex{F,S}, ) where {T,F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} primal_dual_map = optimizer.dual_problem.primal_dual_map @@ -394,12 +602,12 @@ function MOI.get( else return MOI.get( optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), + dual_attribute(attr), ci_dual, ) end else - primal_ci_constant = data.primal_set_constants[1] + primal_ci_constant = data.primal_set_constants[] # If it has no key then there is no dual constraint ci_dual = data.dual_constrained_variable_constraint if ci_dual === nothing @@ -407,7 +615,7 @@ function MOI.get( end return MOI.get( optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), + dual_attribute(attr), ci_dual, ) - primal_ci_constant end @@ -415,7 +623,7 @@ end function MOI.get( optimizer::DualOptimizer{T}, - ::MOI.ConstraintPrimal, + attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, ci::MOI.ConstraintIndex{F,S}, ) where {T,F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} primal_dual_map = optimizer.dual_problem.primal_dual_map @@ -428,7 +636,7 @@ function MOI.get( else return MOI.get( optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), + dual_attribute(attr), ci_dual, ) end @@ -442,7 +650,7 @@ function MOI.get( end return MOI.get( optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), + dual_attribute(attr), ci_dual, ) end @@ -486,10 +694,19 @@ function MOI.get(optimizer::DualOptimizer, attr::MOI.AbstractOptimizerAttribute) return MOI.get(optimizer.dual_problem.dual_model, attr) end +dual_attribute(attr::MOI.PrimalStatus) = MOI.DualStatus(attr.result_index) +dual_attribute(attr::MOI.DualStatus) = MOI.PrimalStatus(attr.result_index) +function dual_attribute(attr::MOI.ObjectiveValue) + return MOI.DualObjectiveValue(attr.result_index) +end +function dual_attribute(attr::MOI.DualObjectiveValue) + return MOI.ObjectiveValue(attr.result_index) +end + # For now we don't support setting arbitrary AbstractModelAttribute because # we don't know if they need to be modified via the dualization. One example # would be `MOI.set(model, MOI.ObjectiveFunction{F}(), f)`. We currently # don't support the incremental interface. function MOI.get(optimizer::DualOptimizer, attr::MOI.AbstractModelAttribute) - return MOI.get(optimizer.dual_problem.dual_model, attr) + return MOI.get(optimizer.dual_problem.dual_model, dual_attribute(attr)) end diff --git a/src/structures.jl b/src/structures.jl index c9a99438..db923d6b 100644 --- a/src/structures.jl +++ b/src/structures.jl @@ -19,18 +19,23 @@ variables and their dual counterparts. this is the position in that vector. * `dual_constraint::Union{Nothing,MOI.ConstraintIndex}`: dual constraint - associated with the variable. If the variable is not constrained then the - set is EqualTo{T}(zero(T)). If the variable is a constrained variable then - the set is the dual set of the constrained variable set. If the dual set is - `Reals` then the field is kept as `nothing` as teh constraint is not added. - - * `dual_function::Union{Nothing,MOI.ScalarAffineFunction{T}}`: if the + associated with the variable. If this is a variable constrained in + `MOI.Zeros` or `MOI.EqualTo`, then this is `nothing`. Otherwise, this is + a constraint with set corresponding to the dual set this variable is + constrained to. If the variable is not constrained then this an equality + constrained, so the set is `MOI.EqualTo` even if strictly speaking, the dual + set of `MOI.Reals` is `MOI.Zeros`. If the variable is a constrained variable + in a given set `S`, this is a constraint on the dual set `MOI.dual_set(S)`. + + * `dual_function::Union{Nothing,MOI.ScalarAffineFunction{T}}`: if it is a constrained variable is `VectorOfVariables`-in-`Zeros` or `VariableIndex`-in-`EqualTo(zero(T))` then the dual is `func`-in-`Reals`, - which is "irrelevant" to the model. But this information is cached for - completeness of the `DualOptimizer` for `get`ting `ConstraintDuals`. + which is "irrelevant" to the model. So the no constrained is added (hence + `dual_constraint` is `nothing` but the function is cached in this field for + completeness of the `DualOptimizer` for `get`ting `ConstraintDual`s. + Otherwise, `dual_function` is `nothing`. -To got from the constrained variable constraint to the primal variable, use the +To go from the constrained variable constraint to the primal variable, use the `primal_constrained_variables` field of `PrimalDualMap`. See also `PrimalDualMap` and `PrimalConstraintData`. diff --git a/test/Project.toml b/test/Project.toml index 419f0f64..cd540b50 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,5 +1,6 @@ [deps] CSDP = "0a46da34-8e4b-519e-b418-48813639ff34" +Dualization = "191a621a-6537-11e9-281d-650236a99e60" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" Hypatia = "b99e6be6-89ff-11e8-14f8-45c827f4f8f2" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" @@ -12,3 +13,6 @@ CSDP = "1.0.0" HiGHS = "1.1.0" Hypatia = "0.8.1" SCS = "1.0.1" + +[sources] +Dualization = {path = ".."} diff --git a/test/Tests/test_MOI_wrapper.jl b/test/Tests/test_MOI_wrapper.jl index 5b926d86..561b9195 100644 --- a/test/Tests/test_MOI_wrapper.jl +++ b/test/Tests/test_MOI_wrapper.jl @@ -19,16 +19,16 @@ linear_config, include = ["test_linear_"], exclude = [ - "test_linear_FEASIBILITY_SENSE", - "test_linear_INFEASIBLE_2", - "test_linear_Interval_inactive", - "test_linear_add_constraints", - "test_linear_inactive_bounds", - "test_linear_integration_2", - "test_linear_integration_Interval", - "test_linear_integration_delete_variables", - "test_linear_complex_Zeros", - "test_linear_complex_Zeros_duplicate", + r"^test_linear_FEASIBILITY_SENSE$", + r"^test_linear_INFEASIBLE_2$", + r"^test_linear_Interval_inactive$", + r"^test_linear_add_constraints$", + r"^test_linear_inactive_bounds$", + r"^test_linear_integration_2$", + r"^test_linear_integration_Interval$", + r"^test_linear_integration_delete_variables$", + r"^test_linear_complex_Zeros$", + r"^test_linear_complex_Zeros_duplicate$", ], ) end @@ -144,4 +144,18 @@ ) @test model.assume_min_if_feasibility end + + @testset "Start" begin + model = MOI.Utilities.UniversalFallback(TestModel{Float64}()) + x = MOI.add_variable(model) + c = MOI.add_constraint(model, 2.0 * x, MOI.GreaterThan(0.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.VariablePrimalStart(), x, 1.0) + MOI.set(model, MOI.ConstraintPrimalStart(), c, 3.0) + MOI.set(model, MOI.ConstraintDualStart(), c, 4.0) + dual_problem = Dualization.DualProblem{Float64}(TestModel{Float64}()) + OptimizerType = typeof(dual_problem.dual_model) + dual = DualOptimizer{Float64,OptimizerType}(dual_problem) + index_map = MOI.copy_to(dual, model) + end end diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl new file mode 100644 index 00000000..9825657f --- /dev/null +++ b/test/Tests/test_attributes.jl @@ -0,0 +1,145 @@ +# Copyright (c) 2017: Guilherme Bodin, and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +module TestAttributes + +using Test + +import MathOptInterface as MOI +import MathOptInterface.Utilities as MOIU +import Dualization + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +### +### Helper structs +### + +struct DummyModelAttribute <: MOI.AbstractModelAttribute end + +struct DummyVariableAttribute <: MOI.AbstractVariableAttribute end + +struct DummyConstraintAttribute <: MOI.AbstractConstraintAttribute end + +### +### The tests +### + +function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) + T = Float64 + function rand_value() + value = rand(T) + if vector + return [value] + end + return value + end + mock = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()); + eval_variable_constraint_dual = false, + ) + dual = MOI.instantiate(() -> Dualization.DualOptimizer(mock), with_cache_type = T) + set_constant = T(-4) + if vector + set = MOI.Nonnegatives(1) + else + set = MOI.GreaterThan(set_constant) + end + if constrained_variable + if vector + X, ci = MOI.add_constrained_variables(dual, set) + x = X[] + else + x, ci = MOI.add_constrained_variable(dual, set) + end + else + x = MOI.add_variable(dual) + func = T(1) * x + if vector + func = MOI.Utilities.operate(vcat, T, func - set_constant) + end + ci = MOI.add_constraint(dual, func, set) + end + MOI.set(dual, MOI.ObjectiveSense(), MOI.MIN_SENSE) + obj = T(2) * x + MOI.set(dual, MOI.ObjectiveFunction{typeof(obj)}(), obj) + MOI.optimize!(dual) + for attr in [MOI.ConstraintDualStart(), MOI.ConstraintPrimalStart()] + attr = MOI.ConstraintDualStart() + @test MOI.supports(dual, attr, typeof(ci)) + value = rand_value() + MOI.set(dual, attr, ci, value) + @test MOI.get(dual, attr, ci) == value + end + + if vector && constrained_variable + value = zeros(T, 1) + else + value = rand(T) + mock_vi = MOI.get(mock, MOI.ListOfVariableIndices())[] + MOI.set(mock, MOI.VariablePrimal(), mock_vi, value) + if vector + value = [value] + end + end + #MOI.set(mock, MOI.ConstraintPrimal(), mock_ci, value) + @test MOI.get(dual.optimizer, MOI.ConstraintDual(), ci) ≈ value + + value = rand_value() + if vector && constrained_variable + F = MOI.VectorAffineFunction{T} + mock_ci = MOI.get(mock, MOI.ListOfConstraintIndices{F,typeof(set)}())[] + else + F = vector ? MOI.VectorOfVariables : MOI.VariableIndex + mock_ci = MOI.get(mock, MOI.ListOfConstraintIndices{F,typeof(set)}())[] + end + MOI.set(mock, MOI.ConstraintDual(), mock_ci, value) + if !vector + value += set_constant + end + @test MOI.get(dual.optimizer, MOI.ConstraintPrimal(), ci) ≈ value + return +end + +function test_constraint_attribute_VariableIndex() + return _test_constraint_attribute(; + constrained_variable = true, + vector = false, + ) +end + +function test_constraint_attribute_ScalarAffineFunction() + return _test_constraint_attribute(; + constrained_variable = false, + vector = false, + ) +end + +function test_constraint_attribute_VectorOfVariables() + return _test_constraint_attribute(; + constrained_variable = true, + vector = true, + ) +end + +function test_constraint_attribute_VectorAffineFunction() + return _test_constraint_attribute(; + constrained_variable = false, + vector = true, + ) +end + +end # module + +TestAttributes.runtests() diff --git a/test/Tests/test_dual_names.jl b/test/Tests/test_dual_names.jl index 0b994eb0..410f7bc1 100644 --- a/test/Tests/test_dual_names.jl +++ b/test/Tests/test_dual_names.jl @@ -47,10 +47,17 @@ @test MOI.get(dual_model, MOI.VariableName(), vi_1) == "" @test MOI.get(dual_model, MOI.VariableName(), vi_2) == "" # Query constraint names +<<<<<<< HEAD ci_1 = primal_dual_map.primal_variable_data[MOI.VariableIndex(1)].dual_constraint ci_2 = primal_dual_map.primal_variable_data[MOI.VariableIndex(2)].dual_constraint +======= + vi = MOI.VariableIndex(1) + ci_1 = primal_dual_map.primal_variable_data[vi].dual_constraint + vi = MOI.VariableIndex(2) + ci_2 = primal_dual_map.primal_variable_data[vi].dual_constraint +>>>>>>> 7e40a8a (Add support for starting values) @test MOI.get(dual_model, MOI.ConstraintName(), ci_1) == "" @test MOI.get(dual_model, MOI.ConstraintName(), ci_2) == "" @@ -75,10 +82,17 @@ )].dual_variables[1] @test MOI.get(dual_model, MOI.VariableName(), vi_2) == "dualvar_lessthan" # Query constraint names +<<<<<<< HEAD ci_1 = primal_dual_map.primal_variable_data[MOI.VariableIndex(1)].dual_constraint ci_2 = primal_dual_map.primal_variable_data[MOI.VariableIndex(2)].dual_constraint +======= + vi = MOI.VariableIndex(1) + ci_1 = primal_dual_map.primal_variable_data[vi].dual_constraint + vi = MOI.VariableIndex(2) + ci_2 = primal_dual_map.primal_variable_data[vi].dual_constraint +>>>>>>> 7e40a8a (Add support for starting values) @test MOI.get(dual_model, MOI.ConstraintName(), ci_1) == "dualcon_x1" @test MOI.get(dual_model, MOI.ConstraintName(), ci_2) == "dualcon_x2" end diff --git a/test/runtests.jl b/test/runtests.jl index e8e8b089..efb6f59a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -57,6 +57,7 @@ include("Problems/Power/power_cone_problems.jl") include("Problems/Feasibility/feasibility_problems.jl") # Run tests to travis ci +include("Tests/test_attributes.jl") include("Tests/test_structures.jl") include("Tests/test_supported.jl") include("Tests/test_objective_coefficients.jl") From 462f03dbe9fb1b85ad8d45a85358d6c0cd59f9e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 18 Mar 2026 17:07:39 +0100 Subject: [PATCH 02/41] Add manual mode --- src/Dualization.jl | 1 + src/MOI_wrapper.jl | 440 +--------------------------------- src/attributes.jl | 437 +++++++++++++++++++++++++++++++++ test/Tests/test_attributes.jl | 7 +- 4 files changed, 445 insertions(+), 440 deletions(-) create mode 100644 src/attributes.jl diff --git a/src/Dualization.jl b/src/Dualization.jl index de273649..b0a17802 100644 --- a/src/Dualization.jl +++ b/src/Dualization.jl @@ -20,6 +20,7 @@ include("dual_model_variables.jl") include("dual_equality_constraints.jl") include("dualize.jl") include("MOI_wrapper.jl") +include("attributes.jl") export dualize export dual_optimizer diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 01b0b7cf..ebb10f62 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -207,7 +207,7 @@ end function MOI.supports_add_constrained_variables( optimizer::DualOptimizer{T}, - S::Type{MOI.Reals}, + ::Type{MOI.Reals}, ) where {T} return MOI.supports_constraint( optimizer.dual_problem.dual_model, @@ -272,441 +272,3 @@ function MOI.get(optimizer::DualOptimizer, ::MOI.SolverName) name = MOI.get(optimizer.dual_problem.dual_model, MOI.SolverName()) return "Dual model with $name attached" end - -_minus(::Nothing) = nothing -_minus(x) = -x - -function constraint_attribute(attr::MOI.VariablePrimal) - return MOI.ConstraintPrimal(attr.result_index) -end -function constraint_attribute(attr::MOI.VariablePrimalStart) - return MOI.ConstraintPrimalStart() -end - -struct DualModelAttributeNotDefined <: MOI.AbstractModelAttribute end -struct DualVariableAttributeNotDefined <: MOI.AbstractVariableAttribute end -struct DualConstraintAttributeNotDefined <: MOI.AbstractConstraintAttribute end - -dual_attribute(::MOI.AbstractModelAttribute) = DualModelAttributeNotDefined() -function dual_attribute(::MOI.AbstractVariableAttribute) - return DualConstraintAttributeNotDefined() -end -function dual_attribute(::MOI.AbstractConstraintAttribute) - return DualVariableAttributeNotDefined() -end - -dual_attribute(attr::MOI.ResultCount) = attr - -function dual_attribute(attr::Union{MOI.VariablePrimal,MOI.ConstraintPrimal}) - return MOI.ConstraintDual(attr.result_index) -end - -function dual_attribute( - ::Union{MOI.VariablePrimalStart,MOI.ConstraintPrimalStart}, -) - return MOI.ConstraintDualStart() -end - -function dual_attribute_value( - ::Union{MOI.VariablePrimal,MOI.VariablePrimalStart}, - value, -) - return _minus(value) -end - -function dual_attribute_value( - ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, - value, -) - return value -end - -function dual_attribute(attr::MOI.ConstraintDual) - return MOI.ConstraintPrimal(attr.result_index) -end - -function dual_attribute(::MOI.ConstraintDualStart) - return MOI.ConstraintPrimalStart() -end - -function _variable_dual_attribute(attr::MOI.ConstraintDual) - return MOI.VariablePrimal(attr.result_index) -end - -function _variable_dual_attribute(::MOI.ConstraintDualStart) - return MOI.VariablePrimalStart() -end - -# The inner optimizer may not support equality constraints (e.g. MOI.FileFormats.SDPA.Model) -# In this case, if all variables are created using constrained variables then dualization won't -# have to create any equality constraints so it will work. -# In that case, we have two choices: -# 1) say we don't support `MOI.VariablePrimalStart` and ignore them, rely on the value set to -# `MOI.ConstraintPrimalStart` to the constraint associated to the constrained variables -# 2) use the value in `MOI.VariablePrimalStart` as fallback in case `MOI.ConstraintPrimalStart` -# is not set -# The issue with option 2) is that it is difficult to know what type of constraints we should use -# in `MOI.supports` here so we should basically just return `true`. -# But if we `return true` and the solver don't support starting values then it will error, and we -# don't benefit from the silent ignoring of starting values relying on -# https://github.com/jump-dev/MathOptInterface.jl/blob/9884cfacb044724427a7d6c7a21f4bd6ff5a8c15/src/Utilities/copy.jl#L73-L74 -# So let's go for option 1) for now -function MOI.supports( - optimizer::DualOptimizer{T}, - attr::MOI.AbstractVariableAttribute, - ::Type{MOI.VariableIndex}, -) where {T} - return MOI.supports( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, - ) -end - -function MOI.set( - optimizer::DualOptimizer, - attr::MOI.AbstractVariableAttribute, - vi::MOI.VariableIndex, - value, -) - primal_dual_map = optimizer.dual_problem.primal_dual_map - if vi in keys(primal_dual_map.constrained_var_idx) - msg = "Setting starting value for variables constrained at creation is not supported yet" - throw(MOI.SetAttributeNotAllowed(attr, msg)) - end - MOI.set( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - get_ci_dual_problem(optimizer, vi), - dual_attribute_value(attr, value), - ) - return -end - -function MOI.get( - optimizer::DualOptimizer{T}, - attr::MOI.AbstractVariableAttribute, - vi::MOI.VariableIndex, -)::T where {T} - primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_variable_data, vi, nothing) - if data === nothing - # error - elseif data.dual_constraint === nothing - return zero(T) - elseif data.primal_constrained_variable_constraint === nothing - return dual_attribute_value( - attr, - MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - data.dual_constraint, - ), - ) - end - con_attr = constraint_attribute(attr) - value = dual_attribute_value( - con_attr, - MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(con_attr), - data.dual_constraint, - ), - ) - if data.dual_constraint isa - MOI.ConstraintIndex{<:MOI.AbstractVectorFunction} - return value[data.primal_constrained_variable_index] - else - return value - end -end - -function MOI.supports( - optimizer::DualOptimizer, - attr::MOI.AbstractConstraintAttribute, - ::Type{<:MOI.ConstraintIndex}, -) - return MOI.supports( - optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), - MOI.VariableIndex, - ) -end - -function MOI.set( - optimizer::DualOptimizer, - attr::MOI.ConstraintDualStart, - ci::MOI.ConstraintIndex, - value, -) - primal_dual_map = optimizer.dual_problem.primal_dual_map - if ci in keys(primal_dual_map.primal_constrained_variables) - msg = "Setting starting value for variables constrained at creation is not supported yet" - throw(MOI.SetAttributeNotAllowed(attr, msg)) - end - MOI.set( - optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), - primal_dual_map.primal_constraint_data[ci].dual_variables[], - value, - ) - return -end - -function MOI.get( - optimizer::DualOptimizer, - attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, - ci::MOI.ConstraintIndex{F,S}, -) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} - primal_dual_map = optimizer.dual_problem.primal_dual_map - if haskey(primal_dual_map.primal_constrained_variables, ci) - vi = primal_dual_map.primal_constrained_variables[ci][] - ci_dual = primal_dual_map.primal_variable_data[vi].dual_constraint - if ci_dual === nothing - return MOI.Utilities.eval_variables( - primal_dual_map.primal_variable_data[vi].dual_function, - ) do inner_vi - return MOI.get( - optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), - inner_vi, - ) - end - end - set = MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintSet(), - ci_dual, - ) - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - MOI.constant(set) - else - data = primal_dual_map.primal_constraint_data[ci] - ci_dual = data.dual_constrained_variable_constraint - if ci_dual === nothing - # TODO do something else not relying on `_variable_dual_attribute` - return MOI.get( - optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), - primal_dual_map.primal_constraint_data[ci].dual_variables[], - ) - end - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - end -end - -function MOI.get( - optimizer::DualOptimizer, - attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, - ci::MOI.ConstraintIndex{F,S}, -) where {F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} - primal_dual_map = optimizer.dual_problem.primal_dual_map - if !haskey(primal_dual_map.primal_constraint_data, ci) - vis = primal_dual_map.primal_constrained_variables[ci] - ci_dual = primal_dual_map.primal_variable_data[vis[1]].dual_constraint - if ci_dual === nothing - return [ - MOI.Utilities.eval_variables( - primal_dual_map.primal_variable_data[vi].dual_function, - ) do inner_vi - return MOI.get( - optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), - inner_vi, - ) - end for vi in vis - ] - end - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - else - data = primal_dual_map.primal_constraint_data[ci] - ci_dual = data.dual_constrained_variable_constraint - if ci_dual === nothing - # TODO do something else not relying on `_variable_dual_attribute` - return MOI.get.( - optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), - primal_dual_map.primal_constraint_data[ci].dual_variables, - ) - end - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - end -end - -function MOI.supports( - ::DualOptimizer, - attr::MOI.ConstraintPrimalStart, - C::Type{<:MOI.ConstraintIndex}, -) - return MOI.supports( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - C, - ) -end - -function MOI.set( - optimizer::DualOptimizer, - attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, - ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, - value, -) - primal_dual_map = optimizer.dual_problem.primal_dual_map - if ci in keys(primal_dual_map.constrained_var_dual) - error( - "Setting starting value for variables constrained at creation is not supported yet", - ) - elseif haskey(primal_dual_map.primal_con_dual_con, ci) - # If it has no key then there is no dual constraint - ci_dual_problem = get_ci_dual_problem(optimizer, ci) - if !isnothing(value) && (F <: MOI.AbstractScalarFunction) - value -= get_primal_ci_constant(optimizer, ci) - end - MOI.set( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual_problem, - value, - ) - end - return -end - -function MOI.get( - optimizer::DualOptimizer{T}, - attr::MOI.ConstraintPrimal, - ci::MOI.ConstraintIndex{F,S}, -) where {T,F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} - primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_constraint_data, ci, nothing) - if data === nothing - first_vi = primal_dual_map.primal_constrained_variables[ci][1] - ci_dual = primal_dual_map.primal_variable_data[first_vi].dual_constraint - if ci_dual === nothing - return zero(T) - else - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - end - else - primal_ci_constant = data.primal_set_constants[] - # If it has no key then there is no dual constraint - ci_dual = data.dual_constrained_variable_constraint - if ci_dual === nothing - return -primal_ci_constant - end - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - primal_ci_constant - end -end - -function MOI.get( - optimizer::DualOptimizer{T}, - attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, - ci::MOI.ConstraintIndex{F,S}, -) where {T,F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} - primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_constraint_data, ci, nothing) - if data === nothing - vis = primal_dual_map.primal_constrained_variables[ci] - ci_dual = primal_dual_map.primal_variable_data[vis[1]].dual_constraint - if ci_dual === nothing - return zeros(T, length(vis)) - else - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - end - else - ci_dual = data.dual_constrained_variable_constraint - # If it has no key then there is no dual constraint - if ci_dual === nothing - # The number of dual variable associated with the primal constraint is the ci dimension - ci_dimension = length(data.dual_variables) - return zeros(T, ci_dimension) - end - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - end -end - -function MOI.get(optimizer::DualOptimizer, ::MOI.TerminationStatus) - return _dual_status( - MOI.get(optimizer.dual_problem.dual_model, MOI.TerminationStatus()), - ) -end - -function _dual_status(term::MOI.TerminationStatusCode) - if term == MOI.INFEASIBLE - return MOI.DUAL_INFEASIBLE - elseif term == MOI.DUAL_INFEASIBLE - return MOI.INFEASIBLE - elseif term == MOI.ALMOST_INFEASIBLE - return MOI.ALMOST_DUAL_INFEASIBLE - elseif term == MOI.ALMOST_DUAL_INFEASIBLE - return MOI.ALMOST_INFEASIBLE - end - return term -end - -function MOI.supports( - optimizer::DualOptimizer, - attr::MOI.AbstractOptimizerAttribute, -) - return MOI.supports(optimizer.dual_problem.dual_model, attr) -end - -function MOI.set( - optimizer::DualOptimizer, - attr::MOI.AbstractOptimizerAttribute, - value, -) - return MOI.set(optimizer.dual_problem.dual_model, attr, value) -end - -function MOI.get(optimizer::DualOptimizer, attr::MOI.AbstractOptimizerAttribute) - return MOI.get(optimizer.dual_problem.dual_model, attr) -end - -dual_attribute(attr::MOI.PrimalStatus) = MOI.DualStatus(attr.result_index) -dual_attribute(attr::MOI.DualStatus) = MOI.PrimalStatus(attr.result_index) -function dual_attribute(attr::MOI.ObjectiveValue) - return MOI.DualObjectiveValue(attr.result_index) -end -function dual_attribute(attr::MOI.DualObjectiveValue) - return MOI.ObjectiveValue(attr.result_index) -end - -# For now we don't support setting arbitrary AbstractModelAttribute because -# we don't know if they need to be modified via the dualization. One example -# would be `MOI.set(model, MOI.ObjectiveFunction{F}(), f)`. We currently -# don't support the incremental interface. -function MOI.get(optimizer::DualOptimizer, attr::MOI.AbstractModelAttribute) - return MOI.get(optimizer.dual_problem.dual_model, dual_attribute(attr)) -end diff --git a/src/attributes.jl b/src/attributes.jl new file mode 100644 index 00000000..6d0c4f8f --- /dev/null +++ b/src/attributes.jl @@ -0,0 +1,437 @@ +_minus(::Nothing) = nothing +_minus(x) = -x + +function constraint_attribute(attr::MOI.VariablePrimal) + return MOI.ConstraintPrimal(attr.result_index) +end +function constraint_attribute(attr::MOI.VariablePrimalStart) + return MOI.ConstraintPrimalStart() +end + +struct DualModelAttributeNotDefined <: MOI.AbstractModelAttribute end +struct DualVariableAttributeNotDefined <: MOI.AbstractVariableAttribute end +struct DualConstraintAttributeNotDefined <: MOI.AbstractConstraintAttribute end + +dual_attribute(::MOI.AbstractModelAttribute) = DualModelAttributeNotDefined() +function dual_attribute(::MOI.AbstractVariableAttribute) + return DualConstraintAttributeNotDefined() +end +function dual_attribute(::MOI.AbstractConstraintAttribute) + return DualVariableAttributeNotDefined() +end + +dual_attribute(attr::MOI.ResultCount) = attr + +function dual_attribute(attr::Union{MOI.VariablePrimal,MOI.ConstraintPrimal}) + return MOI.ConstraintDual(attr.result_index) +end + +function dual_attribute( + ::Union{MOI.VariablePrimalStart,MOI.ConstraintPrimalStart}, +) + return MOI.ConstraintDualStart() +end + +function dual_attribute_value( + ::Union{MOI.VariablePrimal,MOI.VariablePrimalStart}, + value, +) + return _minus(value) +end + +function dual_attribute_value( + ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + value, +) + return value +end + +function dual_attribute(attr::MOI.ConstraintDual) + return MOI.ConstraintPrimal(attr.result_index) +end + +function dual_attribute(::MOI.ConstraintDualStart) + return MOI.ConstraintPrimalStart() +end + +function _variable_dual_attribute(attr::MOI.ConstraintDual) + return MOI.VariablePrimal(attr.result_index) +end + +function _variable_dual_attribute(::MOI.ConstraintDualStart) + return MOI.VariablePrimalStart() +end + +# The inner optimizer may not support equality constraints (e.g. MOI.FileFormats.SDPA.Model) +# In this case, if all variables are created using constrained variables then dualization won't +# have to create any equality constraints so it will work. +# In that case, we have two choices: +# 1) say we don't support `MOI.VariablePrimalStart` and ignore them, rely on the value set to +# `MOI.ConstraintPrimalStart` to the constraint associated to the constrained variables +# 2) use the value in `MOI.VariablePrimalStart` as fallback in case `MOI.ConstraintPrimalStart` +# is not set +# The issue with option 2) is that it is difficult to know what type of constraints we should use +# in `MOI.supports` here so we should basically just return `true`. +# But if we `return true` and the solver don't support starting values then it will error, and we +# don't benefit from the silent ignoring of starting values relying on +# https://github.com/jump-dev/MathOptInterface.jl/blob/9884cfacb044724427a7d6c7a21f4bd6ff5a8c15/src/Utilities/copy.jl#L73-L74 +# So let's go for option 1) for now +function MOI.supports( + optimizer::DualOptimizer{T}, + attr::MOI.AbstractVariableAttribute, + ::Type{MOI.VariableIndex}, +) where {T} + return MOI.supports( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, + ) +end + +function MOI.set( + optimizer::DualOptimizer, + attr::MOI.AbstractVariableAttribute, + vi::MOI.VariableIndex, + value, +) + primal_dual_map = optimizer.dual_problem.primal_dual_map + if vi in keys(primal_dual_map.constrained_var_idx) + msg = "Setting starting value for variables constrained at creation is not supported yet" + throw(MOI.SetAttributeNotAllowed(attr, msg)) + end + MOI.set( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + get_ci_dual_problem(optimizer, vi), + dual_attribute_value(attr, value), + ) + return +end + +function MOI.get( + optimizer::DualOptimizer{T}, + attr::MOI.AbstractVariableAttribute, + vi::MOI.VariableIndex, +)::T where {T} + primal_dual_map = optimizer.dual_problem.primal_dual_map + data = get(primal_dual_map.primal_variable_data, vi, nothing) + if data === nothing + # error + elseif data.dual_constraint === nothing + return zero(T) + elseif data.primal_constrained_variable_constraint === nothing + return dual_attribute_value( + attr, + MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + data.dual_constraint, + ), + ) + end + con_attr = constraint_attribute(attr) + value = dual_attribute_value( + con_attr, + MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(con_attr), + data.dual_constraint, + ), + ) + if data.dual_constraint isa + MOI.ConstraintIndex{<:MOI.AbstractVectorFunction} + return value[data.primal_constrained_variable_index] + else + return value + end +end + +function MOI.supports( + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + ::Type{<:MOI.ConstraintIndex}, +) + return MOI.supports( + optimizer.dual_problem.dual_model, + _variable_dual_attribute(attr), + MOI.VariableIndex, + ) +end + +function MOI.set( + optimizer::DualOptimizer, + attr::MOI.ConstraintDualStart, + ci::MOI.ConstraintIndex, + value, +) + primal_dual_map = optimizer.dual_problem.primal_dual_map + if ci in keys(primal_dual_map.primal_constrained_variables) + msg = "Setting starting value for variables constrained at creation is not supported yet" + throw(MOI.SetAttributeNotAllowed(attr, msg)) + end + MOI.set( + optimizer.dual_problem.dual_model, + _variable_dual_attribute(attr), + primal_dual_map.primal_constraint_data[ci].dual_variables[], + value, + ) + return +end + +function MOI.get( + optimizer::DualOptimizer, + attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, + ci::MOI.ConstraintIndex{F,S}, +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} + primal_dual_map = optimizer.dual_problem.primal_dual_map + if haskey(primal_dual_map.primal_constrained_variables, ci) + vi = primal_dual_map.primal_constrained_variables[ci][] + ci_dual = primal_dual_map.primal_variable_data[vi].dual_constraint + if ci_dual === nothing + return MOI.Utilities.eval_variables( + primal_dual_map.primal_variable_data[vi].dual_function, + ) do inner_vi + return MOI.get( + optimizer.dual_problem.dual_model, + _variable_dual_attribute(attr), + inner_vi, + ) + end + end + set = MOI.get( + optimizer.dual_problem.dual_model, + MOI.ConstraintSet(), + ci_dual, + ) + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + ci_dual, + ) - MOI.constant(set) + else + data = primal_dual_map.primal_constraint_data[ci] + ci_dual = data.dual_constrained_variable_constraint + if ci_dual === nothing + # TODO do something else not relying on `_variable_dual_attribute` + return MOI.get( + optimizer.dual_problem.dual_model, + _variable_dual_attribute(attr), + primal_dual_map.primal_constraint_data[ci].dual_variables[], + ) + end + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + ci_dual, + ) + end +end + +function MOI.get( + optimizer::DualOptimizer, + attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, + ci::MOI.ConstraintIndex{F,S}, +) where {F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} + primal_dual_map = optimizer.dual_problem.primal_dual_map + if !haskey(primal_dual_map.primal_constraint_data, ci) + vis = primal_dual_map.primal_constrained_variables[ci] + ci_dual = primal_dual_map.primal_variable_data[vis[1]].dual_constraint + if ci_dual === nothing + return [ + MOI.Utilities.eval_variables( + primal_dual_map.primal_variable_data[vi].dual_function, + ) do inner_vi + return MOI.get( + optimizer.dual_problem.dual_model, + _variable_dual_attribute(attr), + inner_vi, + ) + end for vi in vis + ] + end + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + ci_dual, + ) + else + data = primal_dual_map.primal_constraint_data[ci] + ci_dual = data.dual_constrained_variable_constraint + if ci_dual === nothing + # TODO do something else not relying on `_variable_dual_attribute` + return MOI.get.( + optimizer.dual_problem.dual_model, + _variable_dual_attribute(attr), + primal_dual_map.primal_constraint_data[ci].dual_variables, + ) + end + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + ci_dual, + ) + end +end + +function MOI.supports( + ::DualOptimizer, + attr::MOI.ConstraintPrimalStart, + C::Type{<:MOI.ConstraintIndex}, +) + return MOI.supports( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + C, + ) +end + +function MOI.set( + optimizer::DualOptimizer, + attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, + value, +) + primal_dual_map = optimizer.dual_problem.primal_dual_map + if ci in keys(primal_dual_map.constrained_var_dual) + error( + "Setting starting value for variables constrained at creation is not supported yet", + ) + elseif haskey(primal_dual_map.primal_con_dual_con, ci) + # If it has no key then there is no dual constraint + ci_dual_problem = get_ci_dual_problem(optimizer, ci) + if !isnothing(value) && (F <: MOI.AbstractScalarFunction) + value -= get_primal_ci_constant(optimizer, ci) + end + MOI.set( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + ci_dual_problem, + value, + ) + end + return +end + +function MOI.get( + optimizer::DualOptimizer{T}, + attr::MOI.ConstraintPrimal, + ci::MOI.ConstraintIndex{F,S}, +) where {T,F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} + primal_dual_map = optimizer.dual_problem.primal_dual_map + data = get(primal_dual_map.primal_constraint_data, ci, nothing) + if data === nothing + first_vi = primal_dual_map.primal_constrained_variables[ci][1] + ci_dual = primal_dual_map.primal_variable_data[first_vi].dual_constraint + if ci_dual === nothing + return zero(T) + else + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + ci_dual, + ) + end + else + primal_ci_constant = data.primal_set_constants[] + # If it has no key then there is no dual constraint + ci_dual = data.dual_constrained_variable_constraint + if ci_dual === nothing + return -primal_ci_constant + end + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + ci_dual, + ) - primal_ci_constant + end +end + +function MOI.get( + optimizer::DualOptimizer{T}, + attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + ci::MOI.ConstraintIndex{F,S}, +) where {T,F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} + primal_dual_map = optimizer.dual_problem.primal_dual_map + data = get(primal_dual_map.primal_constraint_data, ci, nothing) + if data === nothing + vis = primal_dual_map.primal_constrained_variables[ci] + ci_dual = primal_dual_map.primal_variable_data[vis[1]].dual_constraint + if ci_dual === nothing + return zeros(T, length(vis)) + else + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + ci_dual, + ) + end + else + ci_dual = data.dual_constrained_variable_constraint + # If it has no key then there is no dual constraint + if ci_dual === nothing + # The number of dual variable associated with the primal constraint is the ci dimension + ci_dimension = length(data.dual_variables) + return zeros(T, ci_dimension) + end + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + ci_dual, + ) + end +end + +function MOI.get(optimizer::DualOptimizer, ::MOI.TerminationStatus) + return _dual_status( + MOI.get(optimizer.dual_problem.dual_model, MOI.TerminationStatus()), + ) +end + +function _dual_status(term::MOI.TerminationStatusCode) + if term == MOI.INFEASIBLE + return MOI.DUAL_INFEASIBLE + elseif term == MOI.DUAL_INFEASIBLE + return MOI.INFEASIBLE + elseif term == MOI.ALMOST_INFEASIBLE + return MOI.ALMOST_DUAL_INFEASIBLE + elseif term == MOI.ALMOST_DUAL_INFEASIBLE + return MOI.ALMOST_INFEASIBLE + end + return term +end + +function MOI.supports( + optimizer::DualOptimizer, + attr::MOI.AbstractOptimizerAttribute, +) + return MOI.supports(optimizer.dual_problem.dual_model, attr) +end + +function MOI.set( + optimizer::DualOptimizer, + attr::MOI.AbstractOptimizerAttribute, + value, +) + return MOI.set(optimizer.dual_problem.dual_model, attr, value) +end + +function MOI.get(optimizer::DualOptimizer, attr::MOI.AbstractOptimizerAttribute) + return MOI.get(optimizer.dual_problem.dual_model, attr) +end + +dual_attribute(attr::MOI.PrimalStatus) = MOI.DualStatus(attr.result_index) +dual_attribute(attr::MOI.DualStatus) = MOI.PrimalStatus(attr.result_index) +function dual_attribute(attr::MOI.ObjectiveValue) + return MOI.DualObjectiveValue(attr.result_index) +end +function dual_attribute(attr::MOI.DualObjectiveValue) + return MOI.ObjectiveValue(attr.result_index) +end + +# For now we don't support setting arbitrary AbstractModelAttribute because +# we don't know if they need to be modified via the dualization. One example +# would be `MOI.set(model, MOI.ObjectiveFunction{F}(), f)`. We currently +# don't support the incremental interface. +function MOI.get(optimizer::DualOptimizer, attr::MOI.AbstractModelAttribute) + return MOI.get(optimizer.dual_problem.dual_model, dual_attribute(attr)) +end diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index 9825657f..674d3d51 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -49,7 +49,11 @@ function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()); eval_variable_constraint_dual = false, ) - dual = MOI.instantiate(() -> Dualization.DualOptimizer(mock), with_cache_type = T) + dual = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()), + MOI.Utilities.MANUAL, # easier to debug with less try-catch hidding stuff + ) + MOI.Utilities.reset_optimizer(dual, Dualization.DualOptimizer(mock)) set_constant = T(-4) if vector set = MOI.Nonnegatives(1) @@ -74,6 +78,7 @@ function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) MOI.set(dual, MOI.ObjectiveSense(), MOI.MIN_SENSE) obj = T(2) * x MOI.set(dual, MOI.ObjectiveFunction{typeof(obj)}(), obj) + MOI.Utilities.attach_optimizer(dual) MOI.optimize!(dual) for attr in [MOI.ConstraintDualStart(), MOI.ConstraintPrimalStart()] attr = MOI.ConstraintDualStart() From 9422059c0812d87722251d7c6f995a32e84d789b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 18 Mar 2026 18:00:10 +0100 Subject: [PATCH 03/41] Fixes --- test/Tests/test_attributes.jl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index 674d3d51..bd638f14 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -84,12 +84,17 @@ function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) attr = MOI.ConstraintDualStart() @test MOI.supports(dual, attr, typeof(ci)) value = rand_value() - MOI.set(dual, attr, ci, value) - @test MOI.get(dual, attr, ci) == value + if constrained_variable && vector + # FIXME not supported yet + @test_throws MOI.SetAttributeNotAllowed{MOI.ConstraintDualStart} MOI.set(dual, attr, ci, value) + else + MOI.set(dual, attr, ci, value) + @test MOI.get(dual, attr, ci) == value + end end if vector && constrained_variable - value = zeros(T, 1) + value = 2ones(T, 1) else value = rand(T) mock_vi = MOI.get(mock, MOI.ListOfVariableIndices())[] From cc4ede7f4144b60dc4e7214e9e0a88a00aaee8b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 18 Mar 2026 18:00:47 +0100 Subject: [PATCH 04/41] Fix format --- test/Tests/test_attributes.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index bd638f14..a91c5efd 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -86,7 +86,12 @@ function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) value = rand_value() if constrained_variable && vector # FIXME not supported yet - @test_throws MOI.SetAttributeNotAllowed{MOI.ConstraintDualStart} MOI.set(dual, attr, ci, value) + @test_throws MOI.SetAttributeNotAllowed{MOI.ConstraintDualStart} MOI.set( + dual, + attr, + ci, + value, + ) else MOI.set(dual, attr, ci, value) @test MOI.get(dual, attr, ci) == value From 16f18663e938f8b87f6b3a4c1ed7c140ce7aecf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 18 Mar 2026 18:01:05 +0100 Subject: [PATCH 05/41] fix --- test/Tests/test_dual_names.jl | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/Tests/test_dual_names.jl b/test/Tests/test_dual_names.jl index 410f7bc1..0b994eb0 100644 --- a/test/Tests/test_dual_names.jl +++ b/test/Tests/test_dual_names.jl @@ -47,17 +47,10 @@ @test MOI.get(dual_model, MOI.VariableName(), vi_1) == "" @test MOI.get(dual_model, MOI.VariableName(), vi_2) == "" # Query constraint names -<<<<<<< HEAD ci_1 = primal_dual_map.primal_variable_data[MOI.VariableIndex(1)].dual_constraint ci_2 = primal_dual_map.primal_variable_data[MOI.VariableIndex(2)].dual_constraint -======= - vi = MOI.VariableIndex(1) - ci_1 = primal_dual_map.primal_variable_data[vi].dual_constraint - vi = MOI.VariableIndex(2) - ci_2 = primal_dual_map.primal_variable_data[vi].dual_constraint ->>>>>>> 7e40a8a (Add support for starting values) @test MOI.get(dual_model, MOI.ConstraintName(), ci_1) == "" @test MOI.get(dual_model, MOI.ConstraintName(), ci_2) == "" @@ -82,17 +75,10 @@ )].dual_variables[1] @test MOI.get(dual_model, MOI.VariableName(), vi_2) == "dualvar_lessthan" # Query constraint names -<<<<<<< HEAD ci_1 = primal_dual_map.primal_variable_data[MOI.VariableIndex(1)].dual_constraint ci_2 = primal_dual_map.primal_variable_data[MOI.VariableIndex(2)].dual_constraint -======= - vi = MOI.VariableIndex(1) - ci_1 = primal_dual_map.primal_variable_data[vi].dual_constraint - vi = MOI.VariableIndex(2) - ci_2 = primal_dual_map.primal_variable_data[vi].dual_constraint ->>>>>>> 7e40a8a (Add support for starting values) @test MOI.get(dual_model, MOI.ConstraintName(), ci_1) == "dualcon_x1" @test MOI.get(dual_model, MOI.ConstraintName(), ci_2) == "dualcon_x2" end From 3de770ad27cb016c92a59bde07321dbb87895ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 7 Apr 2026 16:40:11 +0200 Subject: [PATCH 06/41] Continue test --- src/attributes.jl | 2 +- test/Tests/test_MOI_wrapper.jl | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 6d0c4f8f..9262af11 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -95,7 +95,7 @@ function MOI.set( value, ) primal_dual_map = optimizer.dual_problem.primal_dual_map - if vi in keys(primal_dual_map.constrained_var_idx) + if vi in keys(primal_dual_map.primal_variable_data) msg = "Setting starting value for variables constrained at creation is not supported yet" throw(MOI.SetAttributeNotAllowed(attr, msg)) end diff --git a/test/Tests/test_MOI_wrapper.jl b/test/Tests/test_MOI_wrapper.jl index 561b9195..84b88dea 100644 --- a/test/Tests/test_MOI_wrapper.jl +++ b/test/Tests/test_MOI_wrapper.jl @@ -153,9 +153,12 @@ MOI.set(model, MOI.VariablePrimalStart(), x, 1.0) MOI.set(model, MOI.ConstraintPrimalStart(), c, 3.0) MOI.set(model, MOI.ConstraintDualStart(), c, 4.0) - dual_problem = Dualization.DualProblem{Float64}(TestModel{Float64}()) + dual_model = MOI.Utilities.UniversalFallback(TestModel{Float64}()) + dual_problem = Dualization.DualProblem{Float64}(dual_model) OptimizerType = typeof(dual_problem.dual_model) dual = DualOptimizer{Float64,OptimizerType}(dual_problem) index_map = MOI.copy_to(dual, model) + vars = MOI.get(dual_model, MOI.ListOfVariableIndices()) + MOI.get(dual_model, MOI.VariablePrimalStart(), vars[]) end end From 50fd1b5d38536cef87a80ce65c8328533e430b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 08:12:17 +0200 Subject: [PATCH 07/41] Simplify --- src/attributes.jl | 19 +++++++++------- src/structures.jl | 2 +- test/Project.toml | 4 ++-- test/Tests/test_MOI_wrapper.jl | 41 ++++++++++------------------------ test/Tests/test_attributes.jl | 25 +++++++++++++++++++++ 5 files changed, 51 insertions(+), 40 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 9262af11..6ce3a7a3 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -62,6 +62,8 @@ function _variable_dual_attribute(::MOI.ConstraintDualStart) return MOI.VariablePrimalStart() end +fixed_variable_value(::MOI.VariablePrimal, ::Type{T}) where {T} = zero(T) + # The inner optimizer may not support equality constraints (e.g. MOI.FileFormats.SDPA.Model) # In this case, if all variables are created using constrained variables then dualization won't # have to create any equality constraints so it will work. @@ -95,14 +97,15 @@ function MOI.set( value, ) primal_dual_map = optimizer.dual_problem.primal_dual_map - if vi in keys(primal_dual_map.primal_variable_data) + data = primal_dual_map.primal_variable_data[vi] + if !isnothing(data.primal_constrained_variable_constraint) msg = "Setting starting value for variables constrained at creation is not supported yet" throw(MOI.SetAttributeNotAllowed(attr, msg)) end MOI.set( optimizer.dual_problem.dual_model, dual_attribute(attr), - get_ci_dual_problem(optimizer, vi), + data.dual_constraint, dual_attribute_value(attr, value), ) return @@ -114,12 +117,8 @@ function MOI.get( vi::MOI.VariableIndex, )::T where {T} primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_variable_data, vi, nothing) - if data === nothing - # error - elseif data.dual_constraint === nothing - return zero(T) - elseif data.primal_constrained_variable_constraint === nothing + data = primal_dual_map.primal_variable_data[vi] + if isnothing(data.primal_constrained_variable_constraint) return dual_attribute_value( attr, MOI.get( @@ -129,6 +128,10 @@ function MOI.get( ), ) end + if isnothing(data.dual_constraint) + # Fixed variable: variable constrained to `MOI.EqualTo` or `MOI.Zeros` + return fixed_variable_value(attr, T) + end con_attr = constraint_attribute(attr) value = dual_attribute_value( con_attr, diff --git a/src/structures.jl b/src/structures.jl index db923d6b..adcbd50c 100644 --- a/src/structures.jl +++ b/src/structures.jl @@ -30,7 +30,7 @@ variables and their dual counterparts. * `dual_function::Union{Nothing,MOI.ScalarAffineFunction{T}}`: if it is a constrained variable is `VectorOfVariables`-in-`Zeros` or `VariableIndex`-in-`EqualTo(zero(T))` then the dual is `func`-in-`Reals`, - which is "irrelevant" to the model. So the no constrained is added (hence + which is "irrelevant" to the model. So then no constrained is added (hence `dual_constraint` is `nothing` but the function is cached in this field for completeness of the `DualOptimizer` for `get`ting `ConstraintDual`s. Otherwise, `dual_function` is `nothing`. diff --git a/test/Project.toml b/test/Project.toml index cd540b50..24f77f2b 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -11,8 +11,8 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] CSDP = "1.0.0" HiGHS = "1.1.0" -Hypatia = "0.8.1" -SCS = "1.0.1" +Hypatia = "0.9" +SCS = "2" [sources] Dualization = {path = ".."} diff --git a/test/Tests/test_MOI_wrapper.jl b/test/Tests/test_MOI_wrapper.jl index 84b88dea..3bfa7735 100644 --- a/test/Tests/test_MOI_wrapper.jl +++ b/test/Tests/test_MOI_wrapper.jl @@ -48,18 +48,18 @@ include = ["test_conic_"], exclude = [ # uses FEASIBILITY_SENSE - "test_conic_NormInfinityCone_INFEASIBLE", - "test_conic_NormOneCone_INFEASIBLE", - "test_conic_PositiveSemidefiniteConeSquare_3", - "test_conic_PositiveSemidefiniteConeTriangle_3", - "test_conic_SecondOrderCone_INFEASIBLE", - "test_conic_SecondOrderCone_negative_post_bound_2", - "test_conic_SecondOrderCone_negative_post_bound_3", - "test_conic_SecondOrderCone_no_initial_bound", - "test_conic_RotatedSecondOrderCone_out_of_order", - "test_conic_linear_INFEASIBLE", - "test_conic_empty_matrix", - "test_conic_HermitianPositiveSemidefiniteConeTriangle_2", + r"test_conic_NormInfinityCone_INFEASIBLE$", + r"test_conic_NormOneCone_INFEASIBLE$", + r"test_conic_PositiveSemidefiniteConeSquare_3$", + r"test_conic_PositiveSemidefiniteConeTriangle_3$", + r"test_conic_SecondOrderCone_INFEASIBLE$", + r"test_conic_SecondOrderCone_negative_post_bound_2$", + r"test_conic_SecondOrderCone_negative_post_bound_3$", + r"test_conic_SecondOrderCone_no_initial_bound$", + r"test_conic_RotatedSecondOrderCone_out_of_order$", + r"test_conic_linear_INFEASIBLE", + r"test_conic_empty_matrix$", + r"test_conic_HermitianPositiveSemidefiniteConeTriangle_2$", ], ) end @@ -144,21 +144,4 @@ ) @test model.assume_min_if_feasibility end - - @testset "Start" begin - model = MOI.Utilities.UniversalFallback(TestModel{Float64}()) - x = MOI.add_variable(model) - c = MOI.add_constraint(model, 2.0 * x, MOI.GreaterThan(0.0)) - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) - MOI.set(model, MOI.VariablePrimalStart(), x, 1.0) - MOI.set(model, MOI.ConstraintPrimalStart(), c, 3.0) - MOI.set(model, MOI.ConstraintDualStart(), c, 4.0) - dual_model = MOI.Utilities.UniversalFallback(TestModel{Float64}()) - dual_problem = Dualization.DualProblem{Float64}(dual_model) - OptimizerType = typeof(dual_problem.dual_model) - dual = DualOptimizer{Float64,OptimizerType}(dual_problem) - index_map = MOI.copy_to(dual, model) - vars = MOI.get(dual_model, MOI.ListOfVariableIndices()) - MOI.get(dual_model, MOI.VariablePrimalStart(), vars[]) - end end diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index a91c5efd..d0e61fc9 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -158,3 +158,28 @@ end end # module TestAttributes.runtests() + +model = MOI.Utilities.UniversalFallback(TestModel{Float64}()) +x = MOI.add_variable(model) +c = MOI.add_constraint(model, 2.0 * x, MOI.GreaterThan(0.0)) +MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) +MOI.set(model, MOI.VariablePrimalStart(), x, 1.0) +MOI.set(model, MOI.ConstraintPrimalStart(), c, 3.0) +MOI.set(model, MOI.ConstraintDualStart(), c, 4.0) +dual_model = MOI.Utilities.UniversalFallback(TestModel{Float64}()) +dual_problem = Dualization.DualProblem{Float64}(dual_model) +OptimizerType = typeof(dual_problem.dual_model) +dual = DualOptimizer{Float64,OptimizerType}(dual_problem) +index_map = MOI.copy_to(dual, model) +vars = MOI.get(dual_model, MOI.ListOfVariableIndices()) +println(dual_model) +MOI.get(dual_model, MOI.VariablePrimalStart(), vars[]) +T = Float64 +@show dual_model === dual.dual_problem.dual_model +dual_eq = MOI.get(dual_model, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}())[] +@test MOI.get(dual, MOI.VariablePrimalStart(), x) == 1 +@test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_eq) == -1 +MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_eq) +dual_bound = MOI.get(dual_model, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}())[] +MOI.get(dual_model, MOI.ConstraintDualStart(), dual_bound) +MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_bound) From 895184ab3fc52d591b8d1d5b4786e6ca3b2cbeb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 08:14:14 +0200 Subject: [PATCH 08/41] Add comments --- src/attributes.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/attributes.jl b/src/attributes.jl index 6ce3a7a3..593305ba 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -119,6 +119,7 @@ function MOI.get( primal_dual_map = optimizer.dual_problem.primal_dual_map data = primal_dual_map.primal_variable_data[vi] if isnothing(data.primal_constrained_variable_constraint) + # Classical free variable return dual_attribute_value( attr, MOI.get( @@ -132,6 +133,7 @@ function MOI.get( # Fixed variable: variable constrained to `MOI.EqualTo` or `MOI.Zeros` return fixed_variable_value(attr, T) end + # Added as constrained variable con_attr = constraint_attribute(attr) value = dual_attribute_value( con_attr, @@ -143,8 +145,10 @@ function MOI.get( ) if data.dual_constraint isa MOI.ConstraintIndex{<:MOI.AbstractVectorFunction} + # Added as part of a vector of constrained variable return value[data.primal_constrained_variable_index] else + # Added as a scalar constrained variable return value end end From 77422de5f3896621194e04eed416a6d44424a70d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 10:47:30 +0200 Subject: [PATCH 09/41] Fixes --- src/MOI_wrapper.jl | 17 ++++----- src/attributes.jl | 14 +++---- test/Tests/test_MOI_wrapper.jl | 18 +++++++++ test/Tests/test_attributes.jl | 67 +++++++++++++++++++++------------- 4 files changed, 74 insertions(+), 42 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index ebb10f62..76ed81a0 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -234,23 +234,20 @@ function MOI.supports_add_constrained_variables( end function MOI.copy_to(dest::DualOptimizer, src::MOI.ModelLike) + MOI.empty!(dest) dualize( src, dest.dual_problem, assume_min_if_feasibility = dest.assume_min_if_feasibility, ) - idx_map = MOI.Utilities.IndexMap() - vis_src = MOI.get(src, MOI.ListOfVariableIndices()) - for vi in vis_src - setindex!(idx_map, vi, vi) - end - MOI.Utilities.pass_attributes(dest, src, idx_map, vis_src) + index_map = MOI.Utilities.identity_index_map(src) + vis = MOI.get(src, MOI.ListOfVariableIndices()) + MOI.Utilities.pass_attributes(dest, src, index_map, vis) for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) - for con in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) - setindex!(idx_map, con, con) - end + cis = MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) + MOI.Utilities.pass_attributes(dest, src, index_map, cis) end - return idx_map + return index_map end function MOI.optimize!(optimizer::DualOptimizer) diff --git a/src/attributes.jl b/src/attributes.jl index 593305ba..9a63df5c 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -281,7 +281,7 @@ function MOI.get( end function MOI.supports( - ::DualOptimizer, + optimizer::DualOptimizer, attr::MOI.ConstraintPrimalStart, C::Type{<:MOI.ConstraintIndex}, ) @@ -299,15 +299,15 @@ function MOI.set( value, ) primal_dual_map = optimizer.dual_problem.primal_dual_map - if ci in keys(primal_dual_map.constrained_var_dual) + data = get(primal_dual_map.primal_constraint_data, ci, nothing) + if isnothing(data) error( "Setting starting value for variables constrained at creation is not supported yet", ) - elseif haskey(primal_dual_map.primal_con_dual_con, ci) - # If it has no key then there is no dual constraint - ci_dual_problem = get_ci_dual_problem(optimizer, ci) - if !isnothing(value) && (F <: MOI.AbstractScalarFunction) - value -= get_primal_ci_constant(optimizer, ci) + else + ci_dual_problem = data.dual_constrained_variable_constraint + if !isnothing(value) + value -= data.primal_set_constants[] end MOI.set( optimizer.dual_problem.dual_model, diff --git a/test/Tests/test_MOI_wrapper.jl b/test/Tests/test_MOI_wrapper.jl index 3bfa7735..c13ae7e6 100644 --- a/test/Tests/test_MOI_wrapper.jl +++ b/test/Tests/test_MOI_wrapper.jl @@ -144,4 +144,22 @@ ) @test model.assume_min_if_feasibility end + + @testset "Copy twice" begin + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + x = MOI.add_variable(model) + c = MOI.add_constraint(model, T(2) * x, MOI.GreaterThan(T(0))) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.VariablePrimalStart(), x, T(1)) + MOI.set(model, MOI.ConstraintPrimalStart(), c, T(3)) + MOI.set(model, MOI.ConstraintDualStart(), c, T(4)) + dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + dual_problem = Dualization.DualProblem{T}(dual_model) + OptimizerType = typeof(dual_problem.dual_model) + dual = DualOptimizer{T,OptimizerType}(dual_problem) + MOI.copy_to(dual, model) + # Test that it is emptied + MOI.copy_to(dual, model) + @test MOI.get(dual_model, MOI.NumberOfVariables()) == 1 + end end diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index d0e61fc9..c91f4a4c 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -155,31 +155,48 @@ function test_constraint_attribute_VectorAffineFunction() ) end +function _test_simple(T) + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + x = MOI.add_variable(model) + c = MOI.add_constraint(model, T(2) * x, MOI.GreaterThan(T(0))) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.VariablePrimalStart(), x, T(1)) + MOI.set(model, MOI.ConstraintPrimalStart(), c, T(3)) + MOI.set(model, MOI.ConstraintDualStart(), c, T(4)) + dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + dual_problem = Dualization.DualProblem{T}(dual_model) + OptimizerType = typeof(dual_problem.dual_model) + dual = Dualization.DualOptimizer{T,OptimizerType}(dual_problem) + @test MOI.supports(dual, MOI.VariablePrimalStart(), typeof(x)) + @test MOI.supports(dual, MOI.ConstraintDualStart(), typeof(c)) + @test MOI.supports(dual, MOI.ConstraintPrimalStart(), typeof(c)) + + index_map = MOI.copy_to(dual, model) + @test dual_model === dual.dual_problem.dual_model + + vars = MOI.get(dual_model, MOI.ListOfVariableIndices()) + @test MOI.get(dual_model, MOI.VariablePrimalStart(), vars[]) == 4 + + dual_eq = MOI.get(dual_model, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}())[] + @test MOI.get(dual, MOI.VariablePrimalStart(), x) == 1 + @test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_eq) == -1 + # We could set it to zero, but `nothing` should be fine for the solver, + # let's only revisit if we have a convincing use case + @test isnothing(MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_eq)) + + dual_bound = MOI.get(dual_model, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}())[] + @test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_bound) == 3 + # We could set it to the value of the variable, but `nothing` should be fine for the solver. + # Let's revisit only if we have a solver needing `ConstraintPrimalStart` for `VariableIndex`-in-`S` + # constraints. + @test isnothing(MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_bound)) +end + +function test_simple() + _test_simple(Float64) + _test_simple(Int) +end + end # module TestAttributes.runtests() - -model = MOI.Utilities.UniversalFallback(TestModel{Float64}()) -x = MOI.add_variable(model) -c = MOI.add_constraint(model, 2.0 * x, MOI.GreaterThan(0.0)) -MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) -MOI.set(model, MOI.VariablePrimalStart(), x, 1.0) -MOI.set(model, MOI.ConstraintPrimalStart(), c, 3.0) -MOI.set(model, MOI.ConstraintDualStart(), c, 4.0) -dual_model = MOI.Utilities.UniversalFallback(TestModel{Float64}()) -dual_problem = Dualization.DualProblem{Float64}(dual_model) -OptimizerType = typeof(dual_problem.dual_model) -dual = DualOptimizer{Float64,OptimizerType}(dual_problem) -index_map = MOI.copy_to(dual, model) -vars = MOI.get(dual_model, MOI.ListOfVariableIndices()) -println(dual_model) -MOI.get(dual_model, MOI.VariablePrimalStart(), vars[]) -T = Float64 -@show dual_model === dual.dual_problem.dual_model -dual_eq = MOI.get(dual_model, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}())[] -@test MOI.get(dual, MOI.VariablePrimalStart(), x) == 1 -@test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_eq) == -1 -MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_eq) -dual_bound = MOI.get(dual_model, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}())[] -MOI.get(dual_model, MOI.ConstraintDualStart(), dual_bound) -MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_bound) From 6e5a5724ba9283ad6406516808a4b9e2f2d73d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 10:57:14 +0200 Subject: [PATCH 10/41] More uniform --- src/attributes.jl | 100 ++++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 9a63df5c..9462392a 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -165,6 +165,18 @@ function MOI.supports( ) end +function MOI.supports( + optimizer::DualOptimizer, + attr::MOI.ConstraintPrimalStart, + C::Type{<:MOI.ConstraintIndex}, +) + return MOI.supports( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + C, + ) +end + function MOI.set( optimizer::DualOptimizer, attr::MOI.ConstraintDualStart, @@ -172,16 +184,45 @@ function MOI.set( value, ) primal_dual_map = optimizer.dual_problem.primal_dual_map - if ci in keys(primal_dual_map.primal_constrained_variables) + data = get(primal_dual_map.primal_constraint_data, ci, nothing) + if isnothing(data) msg = "Setting starting value for variables constrained at creation is not supported yet" throw(MOI.SetAttributeNotAllowed(attr, msg)) + else + MOI.set( + optimizer.dual_problem.dual_model, + _variable_dual_attribute(attr), + primal_dual_map.primal_constraint_data[ci].dual_variables[], + value, + ) + end + return +end + +function MOI.set( + optimizer::DualOptimizer, + attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, + value, +) + primal_dual_map = optimizer.dual_problem.primal_dual_map + data = get(primal_dual_map.primal_constraint_data, ci, nothing) + if isnothing(data) + error( + "Setting starting value for variables constrained at creation is not supported yet", + ) + else + ci_dual_problem = data.dual_constrained_variable_constraint + if !isnothing(value) + value -= data.primal_set_constants[] + end + MOI.set( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + ci_dual_problem, + value, + ) end - MOI.set( - optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), - primal_dual_map.primal_constraint_data[ci].dual_variables[], - value, - ) return end @@ -191,7 +232,8 @@ function MOI.get( ci::MOI.ConstraintIndex{F,S}, ) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} primal_dual_map = optimizer.dual_problem.primal_dual_map - if haskey(primal_dual_map.primal_constrained_variables, ci) + data = get(primal_dual_map.primal_constraint_data, ci, nothing) + if isnothing(data) vi = primal_dual_map.primal_constrained_variables[ci][] ci_dual = primal_dual_map.primal_variable_data[vi].dual_constraint if ci_dual === nothing @@ -240,7 +282,8 @@ function MOI.get( ci::MOI.ConstraintIndex{F,S}, ) where {F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} primal_dual_map = optimizer.dual_problem.primal_dual_map - if !haskey(primal_dual_map.primal_constraint_data, ci) + data = get(primal_dual_map.primal_constraint_data, ci, nothing) + if isnothing(data) vis = primal_dual_map.primal_constrained_variables[ci] ci_dual = primal_dual_map.primal_variable_data[vis[1]].dual_constraint if ci_dual === nothing @@ -280,45 +323,6 @@ function MOI.get( end end -function MOI.supports( - optimizer::DualOptimizer, - attr::MOI.ConstraintPrimalStart, - C::Type{<:MOI.ConstraintIndex}, -) - return MOI.supports( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - C, - ) -end - -function MOI.set( - optimizer::DualOptimizer, - attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, - ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, - value, -) - primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_constraint_data, ci, nothing) - if isnothing(data) - error( - "Setting starting value for variables constrained at creation is not supported yet", - ) - else - ci_dual_problem = data.dual_constrained_variable_constraint - if !isnothing(value) - value -= data.primal_set_constants[] - end - MOI.set( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual_problem, - value, - ) - end - return -end - function MOI.get( optimizer::DualOptimizer{T}, attr::MOI.ConstraintPrimal, From 1a97c61e95108fd061250b1783dc88c610c0533d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 12:06:15 +0200 Subject: [PATCH 11/41] Merge getters --- src/attributes.jl | 243 +++++++++++++++++++--------------------------- 1 file changed, 102 insertions(+), 141 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 9462392a..ec3fc4a6 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -226,169 +226,130 @@ function MOI.set( return end -function MOI.get( - optimizer::DualOptimizer, +function get_for_fixed_constrained_variables( + optimizer, attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, - ci::MOI.ConstraintIndex{F,S}, -) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} - primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_constraint_data, ci, nothing) - if isnothing(data) - vi = primal_dual_map.primal_constrained_variables[ci][] - ci_dual = primal_dual_map.primal_variable_data[vi].dual_constraint - if ci_dual === nothing - return MOI.Utilities.eval_variables( - primal_dual_map.primal_variable_data[vi].dual_function, - ) do inner_vi - return MOI.get( - optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), - inner_vi, - ) - end - end - set = MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintSet(), - ci_dual, - ) - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - MOI.constant(set) - else - data = primal_dual_map.primal_constraint_data[ci] - ci_dual = data.dual_constrained_variable_constraint - if ci_dual === nothing - # TODO do something else not relying on `_variable_dual_attribute` - return MOI.get( - optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), - primal_dual_map.primal_constraint_data[ci].dual_variables[], - ) - end + dual_function::MOI.ScalarAffineFunction, +) + function eval(inner_vi) return MOI.get( optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, + _variable_dual_attribute(attr), + inner_vi, ) end + return MOI.Utilities.eval_variables(eval, dual_function) end -function MOI.get( - optimizer::DualOptimizer, +function get_for_fixed_constrained_variables( + ::DualOptimizer{T}, + ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + ::MOI.ScalarAffineFunction, +) where {T} + # TODO evaluate functions + return zero(T) +end + +function get_for_constrained_variables( + optimizer, attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, - ci::MOI.ConstraintIndex{F,S}, -) where {F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} - primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_constraint_data, ci, nothing) - if isnothing(data) - vis = primal_dual_map.primal_constrained_variables[ci] - ci_dual = primal_dual_map.primal_variable_data[vis[1]].dual_constraint - if ci_dual === nothing - return [ - MOI.Utilities.eval_variables( - primal_dual_map.primal_variable_data[vi].dual_function, - ) do inner_vi - return MOI.get( - optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), - inner_vi, - ) - end for vi in vis - ] - end - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - else - data = primal_dual_map.primal_constraint_data[ci] - ci_dual = data.dual_constrained_variable_constraint - if ci_dual === nothing - # TODO do something else not relying on `_variable_dual_attribute` - return MOI.get.( - optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), - primal_dual_map.primal_constraint_data[ci].dual_variables, - ) - end - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - end + dual_ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, +) + set = MOI.get( + optimizer.dual_problem.dual_model, + MOI.ConstraintSet(), + dual_ci, + ) + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + dual_ci, + ) - MOI.constant(set) end -function MOI.get( - optimizer::DualOptimizer{T}, - attr::MOI.ConstraintPrimal, - ci::MOI.ConstraintIndex{F,S}, -) where {T,F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} - primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_constraint_data, ci, nothing) - if data === nothing - first_vi = primal_dual_map.primal_constrained_variables[ci][1] - ci_dual = primal_dual_map.primal_variable_data[first_vi].dual_constraint - if ci_dual === nothing - return zero(T) - else - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - end - else - primal_ci_constant = data.primal_set_constants[] - # If it has no key then there is no dual constraint - ci_dual = data.dual_constrained_variable_constraint - if ci_dual === nothing - return -primal_ci_constant - end - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) - primal_ci_constant - end +function get_for_constrained_variables(optimizer, attr, dual_ci) + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + dual_ci, + ) +end + +function get_for_equality_constraint(optimizer, attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, dual_variable::MOI.VariableIndex) + # TODO do something else not relying on `_variable_dual_attribute` + return MOI.get( + optimizer.dual_problem.dual_model, + _variable_dual_attribute(attr), + dual_variable, + ) +end + +function get_for_equality_constraint(::DualOptimizer{T}, ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, ::MOI.VariableIndex) where {T} + return zero(T) +end + +function shift_constant_for_get(::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, value, _) + return value +end + +shift_constant_for_get(::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, value::Vector, constant::Vector) = value + +shift_constant_for_get(::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, value::Real, constant::Real) = value - constant + +function _scalarize( + ::MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, + v, +) + return v +end + +function _scalarize( + ::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, + v, +) + return only(v) end function MOI.get( optimizer::DualOptimizer{T}, - attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, - ci::MOI.ConstraintIndex{F,S}, -) where {T,F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} + attr::MOI.AbstractConstraintAttribute, + ci::MOI.ConstraintIndex, +) where {T} primal_dual_map = optimizer.dual_problem.primal_dual_map data = get(primal_dual_map.primal_constraint_data, ci, nothing) - if data === nothing + if isnothing(data) + # Constraint associated to variables constrained at creation vis = primal_dual_map.primal_constrained_variables[ci] - ci_dual = primal_dual_map.primal_variable_data[vis[1]].dual_constraint - if ci_dual === nothing - return zeros(T, length(vis)) + data = primal_dual_map.primal_variable_data[first(vis)] + if isnothing(data.dual_constraint) + # Fixed variables (constrained in `MOI.Zeros` or `MOI.EqualTo`) + dual_functions = MOI.ScalarAffineFunction{T}[ + primal_dual_map.primal_variable_data[vi].dual_function + for vi in vis + ] + return get_for_fixed_constrained_variables.( + optimizer, + attr, + _scalarize(ci, dual_functions), + ) else - return MOI.get( + return get_for_constrained_variables(optimizer, attr, data.dual_constraint) + end + else + @assert !haskey(primal_dual_map.primal_constrained_variables, ci) + dual_ci = data.dual_constrained_variable_constraint + value = if isnothing(dual_ci) + # Primal equality constraint, so no dual constraint + # TODO do something else not relying on `_variable_dual_attribute` + get_for_equality_constraint.(optimizer, attr, _scalarize(ci, data.dual_variables)) + else + MOI.get( optimizer.dual_problem.dual_model, dual_attribute(attr), - ci_dual, + dual_ci, ) end - else - ci_dual = data.dual_constrained_variable_constraint - # If it has no key then there is no dual constraint - if ci_dual === nothing - # The number of dual variable associated with the primal constraint is the ci dimension - ci_dimension = length(data.dual_variables) - return zeros(T, ci_dimension) - end - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual, - ) + return shift_constant_for_get(attr, value, _scalarize(ci, data.primal_set_constants)) end end From b2cf44ed00e09c6f391fffd5747498a7849d5223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 12:06:22 +0200 Subject: [PATCH 12/41] Fix format --- src/attributes.jl | 73 ++++++++++++++++++++++++----------- test/Tests/test_attributes.jl | 19 +++++++-- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index ec3fc4a6..1a9218fb 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -255,11 +255,8 @@ function get_for_constrained_variables( attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, dual_ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, ) - set = MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintSet(), - dual_ci, - ) + set = + MOI.get(optimizer.dual_problem.dual_model, MOI.ConstraintSet(), dual_ci) return MOI.get( optimizer.dual_problem.dual_model, dual_attribute(attr), @@ -275,7 +272,11 @@ function get_for_constrained_variables(optimizer, attr, dual_ci) ) end -function get_for_equality_constraint(optimizer, attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, dual_variable::MOI.VariableIndex) +function get_for_equality_constraint( + optimizer, + attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, + dual_variable::MOI.VariableIndex, +) # TODO do something else not relying on `_variable_dual_attribute` return MOI.get( optimizer.dual_problem.dual_model, @@ -284,29 +285,43 @@ function get_for_equality_constraint(optimizer, attr::Union{MOI.ConstraintDual,M ) end -function get_for_equality_constraint(::DualOptimizer{T}, ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, ::MOI.VariableIndex) where {T} +function get_for_equality_constraint( + ::DualOptimizer{T}, + ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + ::MOI.VariableIndex, +) where {T} return zero(T) end -function shift_constant_for_get(::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, value, _) +function shift_constant_for_get( + ::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, + value, + _, +) return value end -shift_constant_for_get(::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, value::Vector, constant::Vector) = value - -shift_constant_for_get(::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, value::Real, constant::Real) = value - constant +function shift_constant_for_get( + ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + value::Vector, + constant::Vector, +) + return value +end -function _scalarize( - ::MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, - v, +function shift_constant_for_get( + ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + value::Real, + constant::Real, ) + return value - constant +end + +function _scalarize(::MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, v) return v end -function _scalarize( - ::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, - v, -) +function _scalarize(::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, v) return only(v) end @@ -324,8 +339,8 @@ function MOI.get( if isnothing(data.dual_constraint) # Fixed variables (constrained in `MOI.Zeros` or `MOI.EqualTo`) dual_functions = MOI.ScalarAffineFunction{T}[ - primal_dual_map.primal_variable_data[vi].dual_function - for vi in vis + primal_dual_map.primal_variable_data[vi].dual_function for + vi in vis ] return get_for_fixed_constrained_variables.( optimizer, @@ -333,7 +348,11 @@ function MOI.get( _scalarize(ci, dual_functions), ) else - return get_for_constrained_variables(optimizer, attr, data.dual_constraint) + return get_for_constrained_variables( + optimizer, + attr, + data.dual_constraint, + ) end else @assert !haskey(primal_dual_map.primal_constrained_variables, ci) @@ -341,7 +360,11 @@ function MOI.get( value = if isnothing(dual_ci) # Primal equality constraint, so no dual constraint # TODO do something else not relying on `_variable_dual_attribute` - get_for_equality_constraint.(optimizer, attr, _scalarize(ci, data.dual_variables)) + get_for_equality_constraint.( + optimizer, + attr, + _scalarize(ci, data.dual_variables), + ) else MOI.get( optimizer.dual_problem.dual_model, @@ -349,7 +372,11 @@ function MOI.get( dual_ci, ) end - return shift_constant_for_get(attr, value, _scalarize(ci, data.primal_set_constants)) + return shift_constant_for_get( + attr, + value, + _scalarize(ci, data.primal_set_constants), + ) end end diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index c91f4a4c..e0b71f88 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -177,24 +177,35 @@ function _test_simple(T) vars = MOI.get(dual_model, MOI.ListOfVariableIndices()) @test MOI.get(dual_model, MOI.VariablePrimalStart(), vars[]) == 4 - dual_eq = MOI.get(dual_model, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}())[] + dual_eq = MOI.get( + dual_model, + MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{T}, + MOI.EqualTo{T}, + }(), + )[] @test MOI.get(dual, MOI.VariablePrimalStart(), x) == 1 @test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_eq) == -1 # We could set it to zero, but `nothing` should be fine for the solver, # let's only revisit if we have a convincing use case @test isnothing(MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_eq)) - dual_bound = MOI.get(dual_model, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}())[] + dual_bound = MOI.get( + dual_model, + MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}(), + )[] @test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_bound) == 3 # We could set it to the value of the variable, but `nothing` should be fine for the solver. # Let's revisit only if we have a solver needing `ConstraintPrimalStart` for `VariableIndex`-in-`S` # constraints. - @test isnothing(MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_bound)) + @test isnothing( + MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_bound), + ) end function test_simple() _test_simple(Float64) - _test_simple(Int) + return _test_simple(Int) end end # module From 1b77f3e2cdb50056702c47f26dcb74adae11267a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 13:46:57 +0200 Subject: [PATCH 13/41] tmp --- src/attributes.jl | 62 ++++++++++++++++++++++++++++++++++++++++++++--- src/structures.jl | 6 ++--- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 1a9218fb..b4d20b67 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -293,6 +293,11 @@ function get_for_equality_constraint( return zero(T) end +""" + shift_constant_for_get(attr::MOI.AbstractConstraintAttribute, value) +""" +function shift_constant_for_get end + function shift_constant_for_get( ::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, value, @@ -317,6 +322,30 @@ function shift_constant_for_get( return value - constant end +function _get_through_constraint_vectorize( + ::MOI.ConstraintIndex, + _, + value, + _, +) + return value +end + +function _get_through_constraint_vectorize( + ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction,<:MOI.Utilities.ScalarLinearSet}, + attr, + value, + constants, +) + # Dualization handles scalar constraints like `f(x) >= lb` in a way that's equivalent + # to applying a `MOI.Bridges.Constraint.VectorizeBridge`. That is, it is equivalent to + # transforming it into `[f(x) - lb] in MOI.Nonnegatives(1)`. + # For packages that define custom attributes, to avoid having them to deal with both + # defining how it should go through the vectorize bridge and for a scalar constraint + # in a dualization layer, we just use the vectorize bridge implementation here: + return MOI.get(model, attr, ) +end + function _scalarize(::MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, v) return v end @@ -325,8 +354,34 @@ function _scalarize(::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, v) return only(v) end +struct _AfterVectorize{T,OT,F,S} <: MOI.ModelLike + inner::DualOptimizer{T,OT} + inner_ci::MOI.ConstraintIndex{F,S} +end + +# Dualization handles scalar constraints like `f(x) >= lb` in a way that's equivalent +# to applying a `MOI.Bridges.Constraint.VectorizeBridge`. That is, it is equivalent to +# transforming it into `[f(x) - lb] in MOI.Nonnegatives(1)`. +# For packages that define custom attributes, to avoid having them to deal with both +# defining how it should go through the vectorize bridge and for a scalar constraint +# in a dualization layer, we just use the vectorize bridge implementation here: + function MOI.get( - optimizer::DualOptimizer{T}, + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + ::MOI.ConstraintIndex, +) +end + +function MOI.get( + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + ::MOI.ConstraintIndex, +) +end + +function MOI.get( + optimizer::_AfterVectorize{T}, attr::MOI.AbstractConstraintAttribute, ci::MOI.ConstraintIndex, ) where {T} @@ -372,10 +427,11 @@ function MOI.get( dual_ci, ) end - return shift_constant_for_get( + return _get_through_constraint_vectorize( + ci, attr, value, - _scalarize(ci, data.primal_set_constants), + data.primal_set_constants, ) end end diff --git a/src/structures.jl b/src/structures.jl index adcbd50c..15dbea1f 100644 --- a/src/structures.jl +++ b/src/structures.jl @@ -57,9 +57,9 @@ constraints and their dual counterparts. Constraint indices for constrained variables are not in this structure. They are added in the `primal_constrained_variables` field of `PrimalDualMap`. - * `primal_set_constants::Vector{T}`: a vector of primal set constants that are - used in MOI getters. This is used to get the primal constants of the primal - constraints. + * `primal_set_constants::Vector{T}`: for vector constraints, it is `T[]`. + For scalar constraints, it is equal to the constant of the set minus + the constant of the function. * `dual_variables::Vector{MOI.VariableIndex}`: vector of dual variables. If primal constraint is scalar then, the vector has length = 1. From da86856b8ade89ad6dd59181bca92b90f8acb125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 14:02:30 +0200 Subject: [PATCH 14/41] Rely on explicit vectorization layer --- src/attributes.jl | 54 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index b4d20b67..91240d76 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -365,26 +365,58 @@ end # For packages that define custom attributes, to avoid having them to deal with both # defining how it should go through the vectorize bridge and for a scalar constraint # in a dualization layer, we just use the vectorize bridge implementation here: +# This also help us get the mechanism that detect if it is a ray or not. +# It's getting quite hacky, maybe we should just drop support for scalar constraint +# in Dualization and rely on a bridge layer. function MOI.get( optimizer::DualOptimizer, attr::MOI.AbstractConstraintAttribute, - ::MOI.ConstraintIndex, + ci::MOI.ConstraintIndex, ) + return MOI.get(_AfterVectorize(optimizer, ci), attr, ci) +end + +function _vectorize_bridge(::Type{MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}}, constant) where {T,F,S,G} + dummy_ci = MOI.ConstraintIndex{F,S}(1) + return MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}(dummy_ci, constant) end function MOI.get( - optimizer::DualOptimizer, + optimizer::DualOptimizer{T}, attr::MOI.AbstractConstraintAttribute, - ::MOI.ConstraintIndex, + ci::MOI.ConstraintIndex{F,S}, +) where {T,F<:MOI.AbstractScalarFunction,S<:MOI.Utilities.ScalarLinearSet} + model = _AfterVectorize(optimizer, ci) + primal_dual_map = optimizer.dual_problem.primal_dual_map + data = get(primal_dual_map.primal_constraint_data, ci, nothing) + if !isnothing(data) + constant = data.primal_set_constants[] + BT = MOI.Bridges.Constraint.concrete_bridge_type( + MOI.Bridges.Constraint.VectorizeBridge{T}, + F, + S, + ) + ci = _vectorize_bridge(BT, -constant) + end + return MOI.get(model, attr, ci) +end + +# Vectorize bridge uses this to check if it is a ray or not +function MOI.get( + av::_AfterVectorize, + attr::MOI.AbstractModelAttribute, ) + return MOI.get(av.inner, attr) end function MOI.get( - optimizer::_AfterVectorize{T}, + av::_AfterVectorize{T}, attr::MOI.AbstractConstraintAttribute, - ci::MOI.ConstraintIndex, + ::MOI.ConstraintIndex, ) where {T} + optimizer = av.inner + ci = av.inner_ci primal_dual_map = optimizer.dual_problem.primal_dual_map data = get(primal_dual_map.primal_constraint_data, ci, nothing) if isnothing(data) @@ -412,27 +444,21 @@ function MOI.get( else @assert !haskey(primal_dual_map.primal_constrained_variables, ci) dual_ci = data.dual_constrained_variable_constraint - value = if isnothing(dual_ci) + if isnothing(dual_ci) # Primal equality constraint, so no dual constraint # TODO do something else not relying on `_variable_dual_attribute` - get_for_equality_constraint.( + return get_for_equality_constraint.( optimizer, attr, _scalarize(ci, data.dual_variables), ) else - MOI.get( + return MOI.get( optimizer.dual_problem.dual_model, dual_attribute(attr), dual_ci, ) end - return _get_through_constraint_vectorize( - ci, - attr, - value, - data.primal_set_constants, - ) end end From d34dea8c061b0d6f5c4241d2e6759c5bbbe040cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 14:02:39 +0200 Subject: [PATCH 15/41] Fix format --- src/attributes.jl | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 91240d76..eb47e414 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -322,17 +322,15 @@ function shift_constant_for_get( return value - constant end -function _get_through_constraint_vectorize( - ::MOI.ConstraintIndex, - _, - value, - _, -) +function _get_through_constraint_vectorize(::MOI.ConstraintIndex, _, value, _) return value end function _get_through_constraint_vectorize( - ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction,<:MOI.Utilities.ScalarLinearSet}, + ci::MOI.ConstraintIndex{ + <:MOI.AbstractScalarFunction, + <:MOI.Utilities.ScalarLinearSet, + }, attr, value, constants, @@ -343,7 +341,7 @@ function _get_through_constraint_vectorize( # For packages that define custom attributes, to avoid having them to deal with both # defining how it should go through the vectorize bridge and for a scalar constraint # in a dualization layer, we just use the vectorize bridge implementation here: - return MOI.get(model, attr, ) + return MOI.get(model, attr) end function _scalarize(::MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, v) @@ -377,7 +375,10 @@ function MOI.get( return MOI.get(_AfterVectorize(optimizer, ci), attr, ci) end -function _vectorize_bridge(::Type{MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}}, constant) where {T,F,S,G} +function _vectorize_bridge( + ::Type{MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}}, + constant, +) where {T,F,S,G} dummy_ci = MOI.ConstraintIndex{F,S}(1) return MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}(dummy_ci, constant) end @@ -403,10 +404,7 @@ function MOI.get( end # Vectorize bridge uses this to check if it is a ray or not -function MOI.get( - av::_AfterVectorize, - attr::MOI.AbstractModelAttribute, -) +function MOI.get(av::_AfterVectorize, attr::MOI.AbstractModelAttribute) return MOI.get(av.inner, attr) end From ad4c37c187bff44f82022f8e83064c7eecf05940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 14:08:40 +0200 Subject: [PATCH 16/41] Fix --- test/Tests/test_MOI_wrapper.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Tests/test_MOI_wrapper.jl b/test/Tests/test_MOI_wrapper.jl index c13ae7e6..e63b96ed 100644 --- a/test/Tests/test_MOI_wrapper.jl +++ b/test/Tests/test_MOI_wrapper.jl @@ -146,6 +146,7 @@ end @testset "Copy twice" begin + T = Float64 model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) x = MOI.add_variable(model) c = MOI.add_constraint(model, T(2) * x, MOI.GreaterThan(T(0))) From 9622d0af89eec411a5041af8b3de57ef8c2a9e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 14:10:01 +0200 Subject: [PATCH 17/41] Cleanup --- src/attributes.jl | 51 ----------------------------------------------- 1 file changed, 51 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index eb47e414..954f05e4 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -293,57 +293,6 @@ function get_for_equality_constraint( return zero(T) end -""" - shift_constant_for_get(attr::MOI.AbstractConstraintAttribute, value) -""" -function shift_constant_for_get end - -function shift_constant_for_get( - ::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, - value, - _, -) - return value -end - -function shift_constant_for_get( - ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, - value::Vector, - constant::Vector, -) - return value -end - -function shift_constant_for_get( - ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, - value::Real, - constant::Real, -) - return value - constant -end - -function _get_through_constraint_vectorize(::MOI.ConstraintIndex, _, value, _) - return value -end - -function _get_through_constraint_vectorize( - ci::MOI.ConstraintIndex{ - <:MOI.AbstractScalarFunction, - <:MOI.Utilities.ScalarLinearSet, - }, - attr, - value, - constants, -) - # Dualization handles scalar constraints like `f(x) >= lb` in a way that's equivalent - # to applying a `MOI.Bridges.Constraint.VectorizeBridge`. That is, it is equivalent to - # transforming it into `[f(x) - lb] in MOI.Nonnegatives(1)`. - # For packages that define custom attributes, to avoid having them to deal with both - # defining how it should go through the vectorize bridge and for a scalar constraint - # in a dualization layer, we just use the vectorize bridge implementation here: - return MOI.get(model, attr) -end - function _scalarize(::MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, v) return v end From f368fe09ba2a347b859fb15423b7aaebb2d3c4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 14:26:14 +0200 Subject: [PATCH 18/41] Add docstrings --- src/attributes.jl | 75 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 954f05e4..7881c332 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -226,6 +226,23 @@ function MOI.set( return end +""" + get_for_fixed_constrained_variables( + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + dual_function::MOI.ScalarAffineFunction, + ) + +Given a fixed variable, so part of a `MOI.VariableIndex`-in-`MOI.EqualTo` +constraint or `MOI.VectorOfVariables`-in-`MOI.Zeros` constraint, +return the value of the attribute `attr` at the entry corresponding to +this variable. +The terms of `dual_function` are the product of the coefficient of the variable +in each constraint multiplied by the corresponding dual variable. +The constant is the coefficient of the variable in the objective function. +""" +function get_for_fixed_constrained_variables end + function get_for_fixed_constrained_variables( optimizer, attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, @@ -250,28 +267,51 @@ function get_for_fixed_constrained_variables( return zero(T) end -function get_for_constrained_variables( +# Not sure how to rely on a bridge for this one. +# What we did is equivalent to applying `MOI.Bridges.Variable.VectorizeBridge` +# so we substituted the variable, hence it's a bit trickier. +# Anyway I think we should just drop support for scalar constraints. +# We can revisit if we have a custom attribute that needs to extend this. +function _maybe_shift_for_vectorize( optimizer, - attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, - dual_ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, + attr, + dual_ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction,<:MOI.Utilities.ScalarLinearSet}, + value, +) + return _shift_for_vectorize(optimizer, attr, dual_ci, value) +end + +function _maybe_shift_for_vectorize( + optimizer, + attr, + dual_ci::MOI.ConstraintIndex, + value, ) + return value +end + +function _shift_for_vectorize(optimizer, ::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, dual_ci, value) set = MOI.get(optimizer.dual_problem.dual_model, MOI.ConstraintSet(), dual_ci) - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - dual_ci, - ) - MOI.constant(set) + return value - MOI.constant(set) end -function get_for_constrained_variables(optimizer, attr, dual_ci) - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - dual_ci, - ) +function _shift_for_vectorize(optimizer, ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, dual_ci, value) + return value end +""" + get_for_equality_constraint( + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + dual_variable::MOI.ScalarAffineFunction, + ) + +Return the value of `attr` for an equality constraint whose dual variable +is `dual_variable`. +""" +function get_for_equality_constraint end + function get_for_equality_constraint( optimizer, attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, @@ -382,10 +422,15 @@ function MOI.get( _scalarize(ci, dual_functions), ) else - return get_for_constrained_variables( + return _maybe_shift_for_vectorize( optimizer, attr, data.dual_constraint, + MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + data.dual_constraint, + ), ) end else From 7f0cd0497ac5a42e6845676f3bf1428516ff1181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 14:39:35 +0200 Subject: [PATCH 19/41] Fix correctness of ConstraintPrimal instead of hardcoding zero --- src/attributes.jl | 26 ++++++++++++++++---------- test/Tests/test_MOI_wrapper.jl | 1 + 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 7881c332..55331009 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -230,22 +230,24 @@ end get_for_fixed_constrained_variables( optimizer::DualOptimizer, attr::MOI.AbstractConstraintAttribute, + primal_vi::MOI.VariableIndex, dual_function::MOI.ScalarAffineFunction, ) -Given a fixed variable, so part of a `MOI.VariableIndex`-in-`MOI.EqualTo` -constraint or `MOI.VectorOfVariables`-in-`MOI.Zeros` constraint, -return the value of the attribute `attr` at the entry corresponding to -this variable. -The terms of `dual_function` are the product of the coefficient of the variable +Given a fixed variable `primal_vi`, so part of a +`MOI.VariableIndex`-in-`MOI.EqualTo` constraint or a +`MOI.VectorOfVariables`-in-`MOI.Zeros` constraint, return the value of the +attribute `attr` at the entry corresponding to `primal_vi`. +The terms of `dual_function` are the product of the coefficient of `primal_vi` in each constraint multiplied by the corresponding dual variable. -The constant is the coefficient of the variable in the objective function. +The constant is the coefficient of `primal_vi` in the objective function. """ function get_for_fixed_constrained_variables end function get_for_fixed_constrained_variables( optimizer, attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, + ::MOI.VariableIndex, dual_function::MOI.ScalarAffineFunction, ) function eval(inner_vi) @@ -258,13 +260,16 @@ function get_for_fixed_constrained_variables( return MOI.Utilities.eval_variables(eval, dual_function) end +_variable_attr(attr::MOI.ConstraintPrimal) = MOI.VariablePrimal(attr.result_index) +_variable_attr(::MOI.ConstraintPrimalStart) = MOI.VariablePrimalStart() + function get_for_fixed_constrained_variables( - ::DualOptimizer{T}, - ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + optimizer::DualOptimizer{T}, + attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + primal_vi::MOI.VariableIndex, ::MOI.ScalarAffineFunction, ) where {T} - # TODO evaluate functions - return zero(T) + return MOI.get(optimizer, _variable_attr(attr), primal_vi) end # Not sure how to rely on a bridge for this one. @@ -419,6 +424,7 @@ function MOI.get( return get_for_fixed_constrained_variables.( optimizer, attr, + _scalarize(ci, vis), _scalarize(ci, dual_functions), ) else diff --git a/test/Tests/test_MOI_wrapper.jl b/test/Tests/test_MOI_wrapper.jl index e63b96ed..cdcad6d6 100644 --- a/test/Tests/test_MOI_wrapper.jl +++ b/test/Tests/test_MOI_wrapper.jl @@ -38,6 +38,7 @@ conic_config = MOI.Test.Config(atol = 1e-4, rtol = 1e-4) conic_cache = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + MOI.empty!(opt) conic_cached = MOI.Utilities.CachingOptimizer(conic_cache, opt) conic_bridged = MOI.Bridges.full_bridge_optimizer(conic_cached, Float64) From 1c3fe15764fa705aa49c724535c4a84e785e0187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 14:59:13 +0200 Subject: [PATCH 20/41] Add test --- test/Tests/test_attributes.jl | 40 ++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index e0b71f88..5428de88 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -155,6 +155,41 @@ function test_constraint_attribute_VectorAffineFunction() ) end +function _test_fixed(T) + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + x, cx = MOI.add_constrained_variable(model, MOI.EqualTo(T(1))) + c = MOI.add_constraint(model, T(2) * x, MOI.LessThan(T(3))) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + + MOI.set(model, MOI.VariablePrimalStart(), x, T(4)) + MOI.set(model, MOI.ConstraintPrimalStart(), c, T(5)) + MOI.set(model, MOI.ConstraintDualStart(), c, T(6)) + + dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + dual_problem = Dualization.DualProblem{T}(dual_model) + OptimizerType = typeof(dual_problem.dual_model) + dual = Dualization.DualOptimizer{T,OptimizerType}(dual_problem) + @test MOI.supports(dual, MOI.VariablePrimalStart(), typeof(x)) + @test MOI.supports(dual, MOI.ConstraintDualStart(), typeof(c)) + @test MOI.supports(dual, MOI.ConstraintPrimalStart(), typeof(c)) + + index_map = MOI.copy_to(dual, model) + @test dual_model === dual.dual_problem.dual_model + + @test MOI.get(dual, MOI.VariablePrimalStart(), x) == 4 + @test MOI.get(dual, MOI.ConstraintPrimalStart(), cx) == 1 + @test isnothing(MOI.get(dual, MOI.ConstraintDualStart(), cx)) + @test_broken MOI.get(dual, MOI.ConstraintPrimalStart(), c) == 5 + @test_broken MOI.get(dual, MOI.ConstraintDualStart(), c) == 6 + return +end + +function test_fixed() + _test_fixed(Float64) + _test_fixed(Int) + return +end + function _test_simple(T) model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) x = MOI.add_variable(model) @@ -163,6 +198,7 @@ function _test_simple(T) MOI.set(model, MOI.VariablePrimalStart(), x, T(1)) MOI.set(model, MOI.ConstraintPrimalStart(), c, T(3)) MOI.set(model, MOI.ConstraintDualStart(), c, T(4)) + dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) dual_problem = Dualization.DualProblem{T}(dual_model) OptimizerType = typeof(dual_problem.dual_model) @@ -201,11 +237,13 @@ function _test_simple(T) @test isnothing( MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_bound), ) + return end function test_simple() _test_simple(Float64) - return _test_simple(Int) + _test_simple(Int) + return end end # module From 1d194de6d115da039a52c58b2d5b14a3d39d4279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 15:25:48 +0200 Subject: [PATCH 21/41] Simplify MOI.set --- src/Dualization.jl | 1 + src/attributes.jl | 107 ++++++---------------------------- src/vectorize_emulator.jl | 66 +++++++++++++++++++++ test/Tests/test_attributes.jl | 2 +- 4 files changed, 87 insertions(+), 89 deletions(-) create mode 100644 src/vectorize_emulator.jl diff --git a/src/Dualization.jl b/src/Dualization.jl index b0a17802..5f3e0e0d 100644 --- a/src/Dualization.jl +++ b/src/Dualization.jl @@ -20,6 +20,7 @@ include("dual_model_variables.jl") include("dual_equality_constraints.jl") include("dualize.jl") include("MOI_wrapper.jl") +include("vectorize_emulator.jl") include("attributes.jl") export dualize diff --git a/src/attributes.jl b/src/attributes.jl index 55331009..ab135cdd 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -62,6 +62,10 @@ function _variable_dual_attribute(::MOI.ConstraintDualStart) return MOI.VariablePrimalStart() end +function _variable_dual_attribute(attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}) + return dual_attribute(attr) +end + fixed_variable_value(::MOI.VariablePrimal, ::Type{T}) where {T} = zero(T) # The inner optimizer may not support equality constraints (e.g. MOI.FileFormats.SDPA.Model) @@ -178,48 +182,31 @@ function MOI.supports( end function MOI.set( - optimizer::DualOptimizer, - attr::MOI.ConstraintDualStart, - ci::MOI.ConstraintIndex, + av::_AfterVectorize, + attr::MOI.AbstractConstraintAttribute, + ::MOI.ConstraintIndex, value, ) + optimizer = av.inner + ci = av.inner_ci + value = _scalarize(ci, value) # Needed because the Vectorize bridge has vectorized it primal_dual_map = optimizer.dual_problem.primal_dual_map data = get(primal_dual_map.primal_constraint_data, ci, nothing) if isnothing(data) - msg = "Setting starting value for variables constrained at creation is not supported yet" + msg = "Setting $attr for variables constrained at creation is not supported yet" throw(MOI.SetAttributeNotAllowed(attr, msg)) else - MOI.set( - optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), - primal_dual_map.primal_constraint_data[ci].dual_variables[], - value, - ) - end - return -end - -function MOI.set( - optimizer::DualOptimizer, - attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, - ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, - value, -) - primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_constraint_data, ci, nothing) - if isnothing(data) - error( - "Setting starting value for variables constrained at creation is not supported yet", - ) - else - ci_dual_problem = data.dual_constrained_variable_constraint - if !isnothing(value) - value -= data.primal_set_constants[] + dual_attr = _variable_dual_attribute(attr) + if dual_attr isa MOI.AbstractVariableAttribute + index = _scalarize(ci, primal_dual_map.primal_constraint_data[ci].dual_variables) + else + @assert dual_attr isa MOI.AbstractConstraintAttribute + index = data.dual_constrained_variable_constraint end MOI.set( optimizer.dual_problem.dual_model, - dual_attribute(attr), - ci_dual_problem, + dual_attr, + index, value, ) end @@ -346,62 +333,6 @@ function _scalarize(::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, v) return only(v) end -struct _AfterVectorize{T,OT,F,S} <: MOI.ModelLike - inner::DualOptimizer{T,OT} - inner_ci::MOI.ConstraintIndex{F,S} -end - -# Dualization handles scalar constraints like `f(x) >= lb` in a way that's equivalent -# to applying a `MOI.Bridges.Constraint.VectorizeBridge`. That is, it is equivalent to -# transforming it into `[f(x) - lb] in MOI.Nonnegatives(1)`. -# For packages that define custom attributes, to avoid having them to deal with both -# defining how it should go through the vectorize bridge and for a scalar constraint -# in a dualization layer, we just use the vectorize bridge implementation here: -# This also help us get the mechanism that detect if it is a ray or not. -# It's getting quite hacky, maybe we should just drop support for scalar constraint -# in Dualization and rely on a bridge layer. - -function MOI.get( - optimizer::DualOptimizer, - attr::MOI.AbstractConstraintAttribute, - ci::MOI.ConstraintIndex, -) - return MOI.get(_AfterVectorize(optimizer, ci), attr, ci) -end - -function _vectorize_bridge( - ::Type{MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}}, - constant, -) where {T,F,S,G} - dummy_ci = MOI.ConstraintIndex{F,S}(1) - return MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}(dummy_ci, constant) -end - -function MOI.get( - optimizer::DualOptimizer{T}, - attr::MOI.AbstractConstraintAttribute, - ci::MOI.ConstraintIndex{F,S}, -) where {T,F<:MOI.AbstractScalarFunction,S<:MOI.Utilities.ScalarLinearSet} - model = _AfterVectorize(optimizer, ci) - primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_constraint_data, ci, nothing) - if !isnothing(data) - constant = data.primal_set_constants[] - BT = MOI.Bridges.Constraint.concrete_bridge_type( - MOI.Bridges.Constraint.VectorizeBridge{T}, - F, - S, - ) - ci = _vectorize_bridge(BT, -constant) - end - return MOI.get(model, attr, ci) -end - -# Vectorize bridge uses this to check if it is a ray or not -function MOI.get(av::_AfterVectorize, attr::MOI.AbstractModelAttribute) - return MOI.get(av.inner, attr) -end - function MOI.get( av::_AfterVectorize{T}, attr::MOI.AbstractConstraintAttribute, diff --git a/src/vectorize_emulator.jl b/src/vectorize_emulator.jl new file mode 100644 index 00000000..bfc62897 --- /dev/null +++ b/src/vectorize_emulator.jl @@ -0,0 +1,66 @@ +# Dualization handles scalar constraints like `f(x) >= lb` in a way that's equivalent +# to applying a `MOI.Bridges.Constraint.VectorizeBridge`. That is, it is equivalent to +# transforming it into `[f(x) - lb] in MOI.Nonnegatives(1)`. +# For packages that define custom attributes, to avoid having them to deal with both +# defining how it should go through the vectorize bridge and for a scalar constraint +# in a dualization layer, we just use the vectorize bridge implementation here: +# This also help us get the mechanism that detect if it is a ray or not. +# It's getting quite hacky, maybe we should just drop support for scalar constraint +# in Dualization and rely on a bridge layer. + +struct _AfterVectorize{T,OT,F,S} <: MOI.ModelLike + inner::DualOptimizer{T,OT} + inner_ci::MOI.ConstraintIndex{F,S} +end + +# Vectorize bridge uses this to check if it is a ray or not +function MOI.get(av::_AfterVectorize, attr::MOI.AbstractModelAttribute) + return MOI.get(av.inner, attr) +end + +function _vectorize_bridge( + ::Type{MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}}, + constant, +) where {T,F,S,G} + dummy_ci = MOI.ConstraintIndex{F,S}(1) + return MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}(dummy_ci, constant) +end + +function _wrap(optimizer::DualOptimizer, ci::MOI.ConstraintIndex) + return _AfterVectorize(optimizer, ci), ci +end + +function _wrap(optimizer::DualOptimizer{T}, ci::MOI.ConstraintIndex{F,S}) where {T,F<:MOI.AbstractScalarFunction,S<:MOI.Utilities.ScalarLinearSet} + model = _AfterVectorize(optimizer, ci) + primal_dual_map = optimizer.dual_problem.primal_dual_map + data = get(primal_dual_map.primal_constraint_data, ci, nothing) + if !isnothing(data) + constant = data.primal_set_constants[] + BT = MOI.Bridges.Constraint.concrete_bridge_type( + MOI.Bridges.Constraint.VectorizeBridge{T}, + F, + S, + ) + ci = _vectorize_bridge(BT, -constant) + end + return model, ci +end + +function MOI.set( + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + ci::MOI.ConstraintIndex, + value, +) + model, new_ci = _wrap(optimizer, ci) + return MOI.set(model, attr, new_ci, value) +end + +function MOI.get( + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + ci::MOI.ConstraintIndex, +) + model, new_ci = _wrap(optimizer, ci) + return MOI.get(model, attr, new_ci) +end diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index 5428de88..9422450b 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -179,7 +179,7 @@ function _test_fixed(T) @test MOI.get(dual, MOI.VariablePrimalStart(), x) == 4 @test MOI.get(dual, MOI.ConstraintPrimalStart(), cx) == 1 @test isnothing(MOI.get(dual, MOI.ConstraintDualStart(), cx)) - @test_broken MOI.get(dual, MOI.ConstraintPrimalStart(), c) == 5 + @test MOI.get(dual, MOI.ConstraintPrimalStart(), c) == 5 @test_broken MOI.get(dual, MOI.ConstraintDualStart(), c) == 6 return end From 800d35581cf3cfefef2b7264e7e14849b81a1b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 21:41:49 +0200 Subject: [PATCH 22/41] Fixes --- src/attributes.jl | 15 ++++++++++----- src/vectorize_emulator.jl | 3 ++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index ab135cdd..ef78ccfd 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -184,12 +184,15 @@ end function MOI.set( av::_AfterVectorize, attr::MOI.AbstractConstraintAttribute, - ::MOI.ConstraintIndex, + ci::MOI.ConstraintIndex, value, ) optimizer = av.inner - ci = av.inner_ci - value = _scalarize(ci, value) # Needed because the Vectorize bridge has vectorized it + if !isnothing(av.ci) + ci = av.ci + # Needed because the Vectorize bridge has vectorized it + value = _scalarize(ci, value) + end primal_dual_map = optimizer.dual_problem.primal_dual_map data = get(primal_dual_map.primal_constraint_data, ci, nothing) if isnothing(data) @@ -336,10 +339,12 @@ end function MOI.get( av::_AfterVectorize{T}, attr::MOI.AbstractConstraintAttribute, - ::MOI.ConstraintIndex, + ci::MOI.ConstraintIndex, ) where {T} optimizer = av.inner - ci = av.inner_ci + if !isnothing(av.ci) + ci = av.ci + end primal_dual_map = optimizer.dual_problem.primal_dual_map data = get(primal_dual_map.primal_constraint_data, ci, nothing) if isnothing(data) diff --git a/src/vectorize_emulator.jl b/src/vectorize_emulator.jl index bfc62897..da532c48 100644 --- a/src/vectorize_emulator.jl +++ b/src/vectorize_emulator.jl @@ -10,7 +10,8 @@ struct _AfterVectorize{T,OT,F,S} <: MOI.ModelLike inner::DualOptimizer{T,OT} - inner_ci::MOI.ConstraintIndex{F,S} + # If `ci` is `nothing`, the bridge isn't used + ci::Union{Nothing,MOI.ConstraintIndex{F,S}} end # Vectorize bridge uses this to check if it is a ray or not From c68132656fc3cdee6d7654c9c63cb336d5912c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 21:52:03 +0200 Subject: [PATCH 23/41] Fixes --- src/vectorize_emulator.jl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/vectorize_emulator.jl b/src/vectorize_emulator.jl index da532c48..838af565 100644 --- a/src/vectorize_emulator.jl +++ b/src/vectorize_emulator.jl @@ -27,15 +27,17 @@ function _vectorize_bridge( return MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}(dummy_ci, constant) end -function _wrap(optimizer::DualOptimizer, ci::MOI.ConstraintIndex) - return _AfterVectorize(optimizer, ci), ci +function _wrap(optimizer::DualOptimizer{T,OT}, ci::MOI.ConstraintIndex{F,S}) where {T,OT,F,S} + return _AfterVectorize{T,OT,F,S}(optimizer, nothing), ci end -function _wrap(optimizer::DualOptimizer{T}, ci::MOI.ConstraintIndex{F,S}) where {T,F<:MOI.AbstractScalarFunction,S<:MOI.Utilities.ScalarLinearSet} - model = _AfterVectorize(optimizer, ci) +function _wrap(optimizer::DualOptimizer{T,OT}, ci::MOI.ConstraintIndex{F,S}) where {T,OT,F<:MOI.AbstractScalarFunction,S<:MOI.Utilities.ScalarLinearSet} primal_dual_map = optimizer.dual_problem.primal_dual_map data = get(primal_dual_map.primal_constraint_data, ci, nothing) - if !isnothing(data) + if isnothing(data) + model = _AfterVectorize{T,OT,F,S}(optimizer, nothing) + else + model = _AfterVectorize{T,OT,F,S}(optimizer, ci) constant = data.primal_set_constants[] BT = MOI.Bridges.Constraint.concrete_bridge_type( MOI.Bridges.Constraint.VectorizeBridge{T}, From 4f956d8d8f4ab2d4b26a3f24fb9856a70e039673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 22:09:51 +0200 Subject: [PATCH 24/41] Fix format --- src/attributes.jl | 39 +++++++++++++++++++++++++++------------ src/vectorize_emulator.jl | 10 ++++++++-- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index ef78ccfd..e8d4c236 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -62,7 +62,9 @@ function _variable_dual_attribute(::MOI.ConstraintDualStart) return MOI.VariablePrimalStart() end -function _variable_dual_attribute(attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}) +function _variable_dual_attribute( + attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, +) return dual_attribute(attr) end @@ -201,17 +203,15 @@ function MOI.set( else dual_attr = _variable_dual_attribute(attr) if dual_attr isa MOI.AbstractVariableAttribute - index = _scalarize(ci, primal_dual_map.primal_constraint_data[ci].dual_variables) + index = _scalarize( + ci, + primal_dual_map.primal_constraint_data[ci].dual_variables, + ) else @assert dual_attr isa MOI.AbstractConstraintAttribute index = data.dual_constrained_variable_constraint end - MOI.set( - optimizer.dual_problem.dual_model, - dual_attr, - index, - value, - ) + MOI.set(optimizer.dual_problem.dual_model, dual_attr, index, value) end return end @@ -250,7 +250,9 @@ function get_for_fixed_constrained_variables( return MOI.Utilities.eval_variables(eval, dual_function) end -_variable_attr(attr::MOI.ConstraintPrimal) = MOI.VariablePrimal(attr.result_index) +function _variable_attr(attr::MOI.ConstraintPrimal) + return MOI.VariablePrimal(attr.result_index) +end _variable_attr(::MOI.ConstraintPrimalStart) = MOI.VariablePrimalStart() function get_for_fixed_constrained_variables( @@ -270,7 +272,10 @@ end function _maybe_shift_for_vectorize( optimizer, attr, - dual_ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction,<:MOI.Utilities.ScalarLinearSet}, + dual_ci::MOI.ConstraintIndex{ + <:MOI.AbstractScalarFunction, + <:MOI.Utilities.ScalarLinearSet, + }, value, ) return _shift_for_vectorize(optimizer, attr, dual_ci, value) @@ -285,13 +290,23 @@ function _maybe_shift_for_vectorize( return value end -function _shift_for_vectorize(optimizer, ::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, dual_ci, value) +function _shift_for_vectorize( + optimizer, + ::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, + dual_ci, + value, +) set = MOI.get(optimizer.dual_problem.dual_model, MOI.ConstraintSet(), dual_ci) return value - MOI.constant(set) end -function _shift_for_vectorize(optimizer, ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, dual_ci, value) +function _shift_for_vectorize( + optimizer, + ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + dual_ci, + value, +) return value end diff --git a/src/vectorize_emulator.jl b/src/vectorize_emulator.jl index 838af565..12156a8d 100644 --- a/src/vectorize_emulator.jl +++ b/src/vectorize_emulator.jl @@ -27,11 +27,17 @@ function _vectorize_bridge( return MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}(dummy_ci, constant) end -function _wrap(optimizer::DualOptimizer{T,OT}, ci::MOI.ConstraintIndex{F,S}) where {T,OT,F,S} +function _wrap( + optimizer::DualOptimizer{T,OT}, + ci::MOI.ConstraintIndex{F,S}, +) where {T,OT,F,S} return _AfterVectorize{T,OT,F,S}(optimizer, nothing), ci end -function _wrap(optimizer::DualOptimizer{T,OT}, ci::MOI.ConstraintIndex{F,S}) where {T,OT,F<:MOI.AbstractScalarFunction,S<:MOI.Utilities.ScalarLinearSet} +function _wrap( + optimizer::DualOptimizer{T,OT}, + ci::MOI.ConstraintIndex{F,S}, +) where {T,OT,F<:MOI.AbstractScalarFunction,S<:MOI.Utilities.ScalarLinearSet} primal_dual_map = optimizer.dual_problem.primal_dual_map data = get(primal_dual_map.primal_constraint_data, ci, nothing) if isnothing(data) From 74a8cc0f9a03ddf53df37dc94e63abc8467a6497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sun, 12 Apr 2026 09:40:06 +0200 Subject: [PATCH 25/41] Fixes --- src/attributes.jl | 68 ++++++++++++++++++++--------------- test/Tests/test_attributes.jl | 4 +-- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index e8d4c236..04b25554 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -46,24 +46,24 @@ function dual_attribute_value( return value end -function dual_attribute(attr::MOI.ConstraintDual) +function constrained_variable_dual_attribute(attr::MOI.ConstraintDual) return MOI.ConstraintPrimal(attr.result_index) end -function dual_attribute(::MOI.ConstraintDualStart) +function constrained_variable_dual_attribute(::MOI.ConstraintDualStart) return MOI.ConstraintPrimalStart() end -function _variable_dual_attribute(attr::MOI.ConstraintDual) +function dual_attribute(attr::MOI.ConstraintDual) return MOI.VariablePrimal(attr.result_index) end -function _variable_dual_attribute(::MOI.ConstraintDualStart) +function dual_attribute(::MOI.ConstraintDualStart) return MOI.VariablePrimalStart() end -function _variable_dual_attribute( - attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, +function constrained_variable_dual_attribute( + attr::MOI.AbstractConstraintAttribute, ) return dual_attribute(attr) end @@ -166,7 +166,7 @@ function MOI.supports( ) return MOI.supports( optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), + dual_attribute(attr), MOI.VariableIndex, ) end @@ -201,7 +201,7 @@ function MOI.set( msg = "Setting $attr for variables constrained at creation is not supported yet" throw(MOI.SetAttributeNotAllowed(attr, msg)) else - dual_attr = _variable_dual_attribute(attr) + dual_attr = dual_attribute(attr) if dual_attr isa MOI.AbstractVariableAttribute index = _scalarize( ci, @@ -217,7 +217,7 @@ function MOI.set( end """ - get_for_fixed_constrained_variables( + fixed_constrained_variables_get( optimizer::DualOptimizer, attr::MOI.AbstractConstraintAttribute, primal_vi::MOI.VariableIndex, @@ -232,9 +232,9 @@ The terms of `dual_function` are the product of the coefficient of `primal_vi` in each constraint multiplied by the corresponding dual variable. The constant is the coefficient of `primal_vi` in the objective function. """ -function get_for_fixed_constrained_variables end +function fixed_constrained_variables_get end -function get_for_fixed_constrained_variables( +function fixed_constrained_variables_get( optimizer, attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, ::MOI.VariableIndex, @@ -243,7 +243,7 @@ function get_for_fixed_constrained_variables( function eval(inner_vi) return MOI.get( optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), + dual_attribute(attr), inner_vi, ) end @@ -255,7 +255,7 @@ function _variable_attr(attr::MOI.ConstraintPrimal) end _variable_attr(::MOI.ConstraintPrimalStart) = MOI.VariablePrimalStart() -function get_for_fixed_constrained_variables( +function fixed_constrained_variables_get( optimizer::DualOptimizer{T}, attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, primal_vi::MOI.VariableIndex, @@ -311,7 +311,7 @@ function _shift_for_vectorize( end """ - get_for_equality_constraint( + equality_constraint_get( optimizer::DualOptimizer, attr::MOI.AbstractConstraintAttribute, dual_variable::MOI.ScalarAffineFunction, @@ -320,22 +320,21 @@ end Return the value of `attr` for an equality constraint whose dual variable is `dual_variable`. """ -function get_for_equality_constraint end +function equality_constraint_get end -function get_for_equality_constraint( +function equality_constraint_get( optimizer, attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, dual_variable::MOI.VariableIndex, ) - # TODO do something else not relying on `_variable_dual_attribute` return MOI.get( optimizer.dual_problem.dual_model, - _variable_dual_attribute(attr), + dual_attribute(attr), dual_variable, ) end -function get_for_equality_constraint( +function equality_constraint_get( ::DualOptimizer{T}, ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, ::MOI.VariableIndex, @@ -372,7 +371,7 @@ function MOI.get( primal_dual_map.primal_variable_data[vi].dual_function for vi in vis ] - return get_for_fixed_constrained_variables.( + return fixed_constrained_variables_get.( optimizer, attr, _scalarize(ci, vis), @@ -385,7 +384,7 @@ function MOI.get( data.dual_constraint, MOI.get( optimizer.dual_problem.dual_model, - dual_attribute(attr), + constrained_variable_dual_attribute(attr), data.dual_constraint, ), ) @@ -395,18 +394,31 @@ function MOI.get( dual_ci = data.dual_constrained_variable_constraint if isnothing(dual_ci) # Primal equality constraint, so no dual constraint - # TODO do something else not relying on `_variable_dual_attribute` - return get_for_equality_constraint.( + return equality_constraint_get.( optimizer, attr, _scalarize(ci, data.dual_variables), ) else - return MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - dual_ci, - ) + dual_attr = dual_attribute(attr) + if dual_attr isa MOI.AbstractVariableAttribute + index = _scalarize( + ci, + primal_dual_map.primal_constraint_data[ci].dual_variables, + ) + return _scalarize(ci, MOI.get.( + optimizer.dual_problem.dual_model, + dual_attr, + data.dual_variables, + )) + else + @assert dual_attr isa MOI.AbstractConstraintAttribute + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attr, + dual_ci, + ) + end end end end diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index 9422450b..c2e4bbfb 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -173,14 +173,14 @@ function _test_fixed(T) @test MOI.supports(dual, MOI.ConstraintDualStart(), typeof(c)) @test MOI.supports(dual, MOI.ConstraintPrimalStart(), typeof(c)) - index_map = MOI.copy_to(dual, model) + MOI.copy_to(dual, model) @test dual_model === dual.dual_problem.dual_model @test MOI.get(dual, MOI.VariablePrimalStart(), x) == 4 @test MOI.get(dual, MOI.ConstraintPrimalStart(), cx) == 1 @test isnothing(MOI.get(dual, MOI.ConstraintDualStart(), cx)) @test MOI.get(dual, MOI.ConstraintPrimalStart(), c) == 5 - @test_broken MOI.get(dual, MOI.ConstraintDualStart(), c) == 6 + @test MOI.get(dual, MOI.ConstraintDualStart(), c) == 6 return end From 54cbbf546c7efaf7855b7e78dfe60c779289b5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 13 Apr 2026 07:56:43 +0200 Subject: [PATCH 26/41] Fix format --- src/attributes.jl | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 04b25554..27c66b73 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -406,11 +406,14 @@ function MOI.get( ci, primal_dual_map.primal_constraint_data[ci].dual_variables, ) - return _scalarize(ci, MOI.get.( - optimizer.dual_problem.dual_model, - dual_attr, - data.dual_variables, - )) + return _scalarize( + ci, + MOI.get.( + optimizer.dual_problem.dual_model, + dual_attr, + data.dual_variables, + ), + ) else @assert dual_attr isa MOI.AbstractConstraintAttribute return MOI.get( From 1e0696ad766c5f979e64cf914b804cc72c1cdc88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 13 Apr 2026 08:38:06 +0200 Subject: [PATCH 27/41] Add docstrings --- docs/src/manual.md | 19 ++++++++++ docs/src/reference.md | 12 ++++++ src/attributes.jl | 88 ++++++++++++++++++++++++++++++++----------- 3 files changed, 97 insertions(+), 22 deletions(-) diff --git a/docs/src/manual.md b/docs/src/manual.md index 38f942e7..71ac8d5b 100644 --- a/docs/src/manual.md +++ b/docs/src/manual.md @@ -202,3 +202,22 @@ Dualize the model dual_model = dualize(model) print(dual_model) ``` + +## Advanced: add support for new attributes + +You might feel that the Dualization layer is getting in your way to get information from an inner layer. +If you ever see yourself typing `model.dual_problem.dual_model`, it is a sign that you need to define new attributes, +especially if these information are related to specific variables and constraints. +The mapping between variables and constraints before and after dualization is quite complex. +It depends on whether constraints corresponded to variables constrained at creation or not and +whether the corresponding set is an equality (i.e., `MOI.EqualTo` or `MOI.Zeros`). + +When define MOI attributes to communicate information in MOI, layers like Dualization can transfer +these attributes to the right variables or constraints before or after their transformation. +So the best practice is to define such variable or constraint attributes and attempt to `MOI.get` +or `MOI.set` them. They is an API that you need to define for these attributes that contains the +functions are `constraint_attribute`, `dual_attribute`, `dual_attribute_value`, +`constrained_variable_dual_attribute`, `fixed_variable_value`, `fixed_constrained_variables_get`, +`equality_constraint_get`. That's a lot of functions, some of which may never be useful for +your specific attributes (e.g., if it is never used on an equality constraint) so no need +to implement the whole API for all attributes. diff --git a/docs/src/reference.md b/docs/src/reference.md index 9938cb8c..178593de 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -70,3 +70,15 @@ Dualization.PrimalConstraintData ```@docs Dualization.PrimalDualMap ``` + +## Attributes + +```@docs +constraint_attribute +dual_attribute +dual_attribute_value +constrained_variable_dual_attribute +fixed_variable_value +fixed_constrained_variables_get +equality_constraint_get +``` diff --git a/src/attributes.jl b/src/attributes.jl index 27c66b73..5ccca4c5 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -1,10 +1,19 @@ _minus(::Nothing) = nothing _minus(x) = -x +""" + constraint_attribute(attr::MOI.AbstractVariableAttribute) + +When a variable is added as a constrained variable, this function is used to +get the value of the variable from the corresponding constraint. +""" +function constraint_attribute end + function constraint_attribute(attr::MOI.VariablePrimal) return MOI.ConstraintPrimal(attr.result_index) end -function constraint_attribute(attr::MOI.VariablePrimalStart) + +function constraint_attribute(::MOI.VariablePrimalStart) return MOI.ConstraintPrimalStart() end @@ -12,6 +21,16 @@ struct DualModelAttributeNotDefined <: MOI.AbstractModelAttribute end struct DualVariableAttributeNotDefined <: MOI.AbstractVariableAttribute end struct DualConstraintAttributeNotDefined <: MOI.AbstractConstraintAttribute end +""" + dual_attribute(attr::MOI.AbstractModelAttribute) + dual_attribute(attr::MOI.AbstractVariableAttribute) + dual_attribute(attr::MOI.AbstractConstraintAttribute) + +Corresponding attribute to get `MOI.set` or `MOI.get` `attr` from the primal +model with the dual model. +""" +function dual_attribute end + dual_attribute(::MOI.AbstractModelAttribute) = DualModelAttributeNotDefined() function dual_attribute(::MOI.AbstractVariableAttribute) return DualConstraintAttributeNotDefined() @@ -32,34 +51,52 @@ function dual_attribute( return MOI.ConstraintDualStart() end -function dual_attribute_value( +function dual_attribute(attr::MOI.ConstraintDual) + return MOI.VariablePrimal(attr.result_index) +end + +function dual_attribute(::MOI.ConstraintDualStart) + return MOI.VariablePrimalStart() +end + +""" + dual_attribute_value_set(attr::MOI.AbstractVariableAttribute, value) + +Used as pre-processing for `MOI.set`ting `value` for a variable. +""" +function dual_attribute_value_set end + +""" + dual_attribute_value_get(attr::MOI.AbstractVariableAttribute, value) + +Used as pre-processing for `MOI.get`ting `value` for a variable. +""" +function dual_attribute_value_get end + +function dual_attribute_value_set( ::Union{MOI.VariablePrimal,MOI.VariablePrimalStart}, value, ) return _minus(value) end -function dual_attribute_value( - ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, +function dual_attribute_value_get( + ::Union{MOI.VariablePrimal,MOI.VariablePrimalStart}, value, ) - return value -end - -function constrained_variable_dual_attribute(attr::MOI.ConstraintDual) - return MOI.ConstraintPrimal(attr.result_index) + return _minus(value) end -function constrained_variable_dual_attribute(::MOI.ConstraintDualStart) - return MOI.ConstraintPrimalStart() -end +""" + constrained_variable_dual_attribute(attr::MOI.AbstractConstraintAttribute) -function dual_attribute(attr::MOI.ConstraintDual) - return MOI.VariablePrimal(attr.result_index) -end +Same as [`dual_attribute`](@ref) but used in case a constraint was added as +part of constrained variables. +""" +function constrained_variable_dual_attribute end -function dual_attribute(::MOI.ConstraintDualStart) - return MOI.VariablePrimalStart() +function constrained_variable_dual_attribute(attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}) + return constraint_attribute(dual_attribute(attr)) end function constrained_variable_dual_attribute( @@ -68,6 +105,14 @@ function constrained_variable_dual_attribute( return dual_attribute(attr) end +""" + fixed_variable_value(attr::MOI.AbstractVariableAttribute, ::Type{T}) where {T} + +Value of `attr` for a value constrained to be equal to zero. This should be the +same as how `MOI.get` is implemented for the `MOI.Bridges.Variable.ZerosBridge`. +""" +function fixed_variable_value end + fixed_variable_value(::MOI.VariablePrimal, ::Type{T}) where {T} = zero(T) # The inner optimizer may not support equality constraints (e.g. MOI.FileFormats.SDPA.Model) @@ -112,7 +157,7 @@ function MOI.set( optimizer.dual_problem.dual_model, dual_attribute(attr), data.dual_constraint, - dual_attribute_value(attr, value), + dual_attribute_value_set(attr, value), ) return end @@ -126,7 +171,7 @@ function MOI.get( data = primal_dual_map.primal_variable_data[vi] if isnothing(data.primal_constrained_variable_constraint) # Classical free variable - return dual_attribute_value( + return dual_attribute_value_get( attr, MOI.get( optimizer.dual_problem.dual_model, @@ -140,9 +185,8 @@ function MOI.get( return fixed_variable_value(attr, T) end # Added as constrained variable - con_attr = constraint_attribute(attr) - value = dual_attribute_value( - con_attr, + value = dual_attribute_value_get( + attr, MOI.get( optimizer.dual_problem.dual_model, dual_attribute(con_attr), From eb7e03f1976ac30ab8f49544fa09c97934cc8d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 13 Apr 2026 08:38:10 +0200 Subject: [PATCH 28/41] Fix format --- src/attributes.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/attributes.jl b/src/attributes.jl index 5ccca4c5..96f82d13 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -95,7 +95,9 @@ part of constrained variables. """ function constrained_variable_dual_attribute end -function constrained_variable_dual_attribute(attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}) +function constrained_variable_dual_attribute( + attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, +) return constraint_attribute(dual_attribute(attr)) end From 1d39dd7f3c16982dbde4953dc600af83c7b5ba06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 13 Apr 2026 12:04:30 +0200 Subject: [PATCH 29/41] Improve code coverage --- src/attributes.jl | 29 ++++------ test/Tests/test_attributes.jl | 102 +++++++++++++++++++++++++++++++--- 2 files changed, 105 insertions(+), 26 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 96f82d13..8f4d7dbc 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -17,10 +17,6 @@ function constraint_attribute(::MOI.VariablePrimalStart) return MOI.ConstraintPrimalStart() end -struct DualModelAttributeNotDefined <: MOI.AbstractModelAttribute end -struct DualVariableAttributeNotDefined <: MOI.AbstractVariableAttribute end -struct DualConstraintAttributeNotDefined <: MOI.AbstractConstraintAttribute end - """ dual_attribute(attr::MOI.AbstractModelAttribute) dual_attribute(attr::MOI.AbstractVariableAttribute) @@ -31,14 +27,6 @@ model with the dual model. """ function dual_attribute end -dual_attribute(::MOI.AbstractModelAttribute) = DualModelAttributeNotDefined() -function dual_attribute(::MOI.AbstractVariableAttribute) - return DualConstraintAttributeNotDefined() -end -function dual_attribute(::MOI.AbstractConstraintAttribute) - return DualVariableAttributeNotDefined() -end - dual_attribute(attr::MOI.ResultCount) = attr function dual_attribute(attr::Union{MOI.VariablePrimal,MOI.ConstraintPrimal}) @@ -152,7 +140,7 @@ function MOI.set( primal_dual_map = optimizer.dual_problem.primal_dual_map data = primal_dual_map.primal_variable_data[vi] if !isnothing(data.primal_constrained_variable_constraint) - msg = "Setting starting value for variables constrained at creation is not supported yet" + msg = "Setting $attr for variables constrained at creation is not supported yet" throw(MOI.SetAttributeNotAllowed(attr, msg)) end MOI.set( @@ -168,7 +156,7 @@ function MOI.get( optimizer::DualOptimizer{T}, attr::MOI.AbstractVariableAttribute, vi::MOI.VariableIndex, -)::T where {T} +) where {T} primal_dual_map = optimizer.dual_problem.primal_dual_map data = primal_dual_map.primal_variable_data[vi] if isnothing(data.primal_constrained_variable_constraint) @@ -191,11 +179,11 @@ function MOI.get( attr, MOI.get( optimizer.dual_problem.dual_model, - dual_attribute(con_attr), + dual_attribute(attr), data.dual_constraint, ), ) - if data.dual_constraint isa + if !isnothing(value) && data.dual_constraint isa MOI.ConstraintIndex{<:MOI.AbstractVectorFunction} # Added as part of a vector of constrained variable return value[data.primal_constrained_variable_index] @@ -257,7 +245,10 @@ function MOI.set( @assert dual_attr isa MOI.AbstractConstraintAttribute index = data.dual_constrained_variable_constraint end - MOI.set(optimizer.dual_problem.dual_model, dual_attr, index, value) + # `index` is `nothing` for affine equality constraints + if !isnothing(index) + MOI.set(optimizer.dual_problem.dual_model, dual_attr, index, value) + end end return end @@ -299,7 +290,9 @@ end function _variable_attr(attr::MOI.ConstraintPrimal) return MOI.VariablePrimal(attr.result_index) end -_variable_attr(::MOI.ConstraintPrimalStart) = MOI.VariablePrimalStart() +function _variable_attr(::MOI.ConstraintPrimalStart) + MOI.VariablePrimalStart() +end function fixed_constrained_variables_get( optimizer::DualOptimizer{T}, diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index c2e4bbfb..0333faa9 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -10,6 +10,7 @@ using Test import MathOptInterface as MOI import MathOptInterface.Utilities as MOIU import Dualization +import SCS function runtests() for name in names(@__MODULE__; all = true) @@ -124,6 +125,9 @@ function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) value += set_constant end @test MOI.get(dual.optimizer, MOI.ConstraintPrimal(), ci) ≈ value + + #@show MOI.get(dual, DummyModelAttribute()) + #@test_throws Dualization.DualModelAttributeNotDefined MOI.get(dual, DummyModelAttribute()) return end @@ -155,7 +159,7 @@ function test_constraint_attribute_VectorAffineFunction() ) end -function _test_fixed(T) +function _test_fixed(T, dual_model) model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) x, cx = MOI.add_constrained_variable(model, MOI.EqualTo(T(1))) c = MOI.add_constraint(model, T(2) * x, MOI.LessThan(T(3))) @@ -165,7 +169,6 @@ function _test_fixed(T) MOI.set(model, MOI.ConstraintPrimalStart(), c, T(5)) MOI.set(model, MOI.ConstraintDualStart(), c, T(6)) - dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) dual_problem = Dualization.DualProblem{T}(dual_model) OptimizerType = typeof(dual_problem.dual_model) dual = Dualization.DualOptimizer{T,OptimizerType}(dual_problem) @@ -185,12 +188,16 @@ function _test_fixed(T) end function test_fixed() - _test_fixed(Float64) - _test_fixed(Int) + for T in [Int, Float64] + dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + _test_fixed(T, dual_model) + end + dual_model = MOI.instantiate(SCS.Optimizer, with_bridge_type=nothing, with_cache_type=Float64) + _test_fixed(Float64, dual_model) return end -function _test_simple(T) +function _test_simple(T, dual_model) model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) x = MOI.add_variable(model) c = MOI.add_constraint(model, T(2) * x, MOI.GreaterThan(T(0))) @@ -199,7 +206,6 @@ function _test_simple(T) MOI.set(model, MOI.ConstraintPrimalStart(), c, T(3)) MOI.set(model, MOI.ConstraintDualStart(), c, T(4)) - dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) dual_problem = Dualization.DualProblem{T}(dual_model) OptimizerType = typeof(dual_problem.dual_model) dual = Dualization.DualOptimizer{T,OptimizerType}(dual_problem) @@ -237,12 +243,92 @@ function _test_simple(T) @test isnothing( MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_bound), ) + + MOI.set(dual_model, MOI.VariablePrimalStart(), vars[], nothing) + @test isnothing(MOI.get(dual_model, MOI.VariablePrimalStart(), vars[])) return end function test_simple() - _test_simple(Float64) - _test_simple(Int) + for T in [Int, Float64] + dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + _test_simple(T, dual_model) + end + dual_model = MOI.instantiate(SCS.Optimizer, with_bridge_type=nothing, with_cache_type=Float64) + _test_simple(Float64, dual_model) + return +end + +function _test_conic(T, dual_model, cone1, cone2) + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + x, cx = MOI.add_constrained_variables(model, cone1) + c = MOI.add_constraint(model, MOI.Utilities.vectorize(x .+ T(1)), cone2) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set.(model, MOI.VariablePrimalStart(), x, T.(1:2)) + MOI.set(model, MOI.ConstraintPrimalStart(), cx, T.(3:4)) + MOI.set(model, MOI.ConstraintDualStart(), cx, T.(4:5)) + MOI.set(model, MOI.ConstraintPrimalStart(), c, T.(6:7)) + MOI.set(model, MOI.ConstraintDualStart(), c, T.(8:9)) + + dual_problem = Dualization.DualProblem{T}(dual_model) + OptimizerType = typeof(dual_problem.dual_model) + dual = Dualization.DualOptimizer{T,OptimizerType}(dual_problem) + @test MOI.supports(dual, MOI.VariablePrimalStart(), eltype(x)) + @test MOI.supports(dual, MOI.ConstraintDualStart(), typeof(cx)) + @test MOI.supports(dual, MOI.ConstraintPrimalStart(), typeof(cx)) + @test MOI.supports(dual, MOI.ConstraintDualStart(), typeof(c)) + @test MOI.supports(dual, MOI.ConstraintPrimalStart(), typeof(c)) + + attr = MOI.VariablePrimalStart() + msg = "Setting $attr for variables constrained at creation is not supported yet" + err = MOI.SetAttributeNotAllowed(attr, msg) + @test_throws err MOI.copy_to(dual, model) + + MOI.set.(model, MOI.VariablePrimalStart(), x, [nothing, nothing]) + attr = MOI.ConstraintPrimalStart() + msg = "Setting $attr for variables constrained at creation is not supported yet" + err = MOI.SetAttributeNotAllowed(attr, msg) + @test_throws err MOI.copy_to(dual, model) + + MOI.set(model, MOI.ConstraintPrimalStart(), cx, nothing) + MOI.set(model, MOI.ConstraintDualStart(), cx, nothing) + MOI.copy_to(dual, model) + + @test dual_model === dual.dual_problem.dual_model + + vars = MOI.get(dual_model, MOI.ListOfVariableIndices()) + @test MOI.get(dual_model, MOI.VariablePrimalStart(), vars) == T[8, 9] + + if !(cone2 isa MOI.Zeros) + dual_c = MOI.get( + dual_model, + MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, + typeof(cone2), + }(), + )[] + @test isnothing(MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_c)) + @test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_c) == T[6, 7] + end + + @test MOI.get(model, MOI.ConstraintPrimalStart(), c) == T.(6:7) + @test MOI.get(model, MOI.ConstraintDualStart(), c) == T.(8:9) + return +end + + +function test_conic() + cones = [MOI.SecondOrderCone(2), MOI.Zeros(2)] + for cone1 in cones + for cone2 in cones + for T in [Float32, Float64] + dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + _test_conic(T, dual_model, cone1, cone2) + end + dual_model = MOI.instantiate(SCS.Optimizer, with_bridge_type=nothing, with_cache_type=Float64) + _test_conic(Float64, dual_model, cone1, cone2) + end + end return end From 9e82d6112a29142b0c26b9477dde0d8b6f594508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 13 Apr 2026 17:16:57 +0200 Subject: [PATCH 30/41] Fix format --- src/attributes.jl | 5 +++-- test/Tests/test_attributes.jl | 31 +++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 8f4d7dbc..c198a18a 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -183,7 +183,8 @@ function MOI.get( data.dual_constraint, ), ) - if !isnothing(value) && data.dual_constraint isa + if !isnothing(value) && + data.dual_constraint isa MOI.ConstraintIndex{<:MOI.AbstractVectorFunction} # Added as part of a vector of constrained variable return value[data.primal_constrained_variable_index] @@ -291,7 +292,7 @@ function _variable_attr(attr::MOI.ConstraintPrimal) return MOI.VariablePrimal(attr.result_index) end function _variable_attr(::MOI.ConstraintPrimalStart) - MOI.VariablePrimalStart() + return MOI.VariablePrimalStart() end function fixed_constrained_variables_get( diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index 0333faa9..f43d577c 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -192,7 +192,11 @@ function test_fixed() dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) _test_fixed(T, dual_model) end - dual_model = MOI.instantiate(SCS.Optimizer, with_bridge_type=nothing, with_cache_type=Float64) + dual_model = MOI.instantiate( + SCS.Optimizer, + with_bridge_type = nothing, + with_cache_type = Float64, + ) _test_fixed(Float64, dual_model) return end @@ -254,7 +258,11 @@ function test_simple() dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) _test_simple(T, dual_model) end - dual_model = MOI.instantiate(SCS.Optimizer, with_bridge_type=nothing, with_cache_type=Float64) + dual_model = MOI.instantiate( + SCS.Optimizer, + with_bridge_type = nothing, + with_cache_type = Float64, + ) _test_simple(Float64, dual_model) return end @@ -302,12 +310,11 @@ function _test_conic(T, dual_model, cone1, cone2) if !(cone2 isa MOI.Zeros) dual_c = MOI.get( dual_model, - MOI.ListOfConstraintIndices{ - MOI.VectorOfVariables, - typeof(cone2), - }(), + MOI.ListOfConstraintIndices{MOI.VectorOfVariables,typeof(cone2)}(), )[] - @test isnothing(MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_c)) + @test isnothing( + MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_c), + ) @test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_c) == T[6, 7] end @@ -316,16 +323,20 @@ function _test_conic(T, dual_model, cone1, cone2) return end - function test_conic() cones = [MOI.SecondOrderCone(2), MOI.Zeros(2)] for cone1 in cones for cone2 in cones for T in [Float32, Float64] - dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + dual_model = + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) _test_conic(T, dual_model, cone1, cone2) end - dual_model = MOI.instantiate(SCS.Optimizer, with_bridge_type=nothing, with_cache_type=Float64) + dual_model = MOI.instantiate( + SCS.Optimizer, + with_bridge_type = nothing, + with_cache_type = Float64, + ) _test_conic(Float64, dual_model, cone1, cone2) end end From 7e85cf533c39a0e4d48548f4efa12e3cf6ccbdce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 13 Apr 2026 17:31:08 +0200 Subject: [PATCH 31/41] Fix --- test/Tests/test_attributes.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index f43d577c..8247404d 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -293,10 +293,13 @@ function _test_conic(T, dual_model, cone1, cone2) @test_throws err MOI.copy_to(dual, model) MOI.set.(model, MOI.VariablePrimalStart(), x, [nothing, nothing]) - attr = MOI.ConstraintPrimalStart() - msg = "Setting $attr for variables constrained at creation is not supported yet" - err = MOI.SetAttributeNotAllowed(attr, msg) - @test_throws err MOI.copy_to(dual, model) + attr1 = MOI.ConstraintPrimalStart() + attr2 = MOI.ConstraintDualStart() + msg1 = "Setting $attr1 for variables constrained at creation is not supported yet" + msg2 = "Setting $attr2 for variables constrained at creation is not supported yet" + err1 = MOI.SetAttributeNotAllowed(attr1, msg1) + err2 = MOI.SetAttributeNotAllowed(attr2, msg2) + @test_throws Union{typeof(err1),typeof(err2)} MOI.copy_to(dual, model) MOI.set(model, MOI.ConstraintPrimalStart(), cx, nothing) MOI.set(model, MOI.ConstraintDualStart(), cx, nothing) From 76a7b44dc4b5f54e55bdeeeaef16da58a1aac093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 13 Apr 2026 17:43:21 +0200 Subject: [PATCH 32/41] fixes --- docs/src/manual.md | 2 +- docs/src/reference.md | 1 - src/attributes.jl | 27 ++++++++++----------------- test/Tests/test_attributes.jl | 4 ++-- 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/docs/src/manual.md b/docs/src/manual.md index 71ac8d5b..127059d0 100644 --- a/docs/src/manual.md +++ b/docs/src/manual.md @@ -216,7 +216,7 @@ When define MOI attributes to communicate information in MOI, layers like Dualiz these attributes to the right variables or constraints before or after their transformation. So the best practice is to define such variable or constraint attributes and attempt to `MOI.get` or `MOI.set` them. They is an API that you need to define for these attributes that contains the -functions are `constraint_attribute`, `dual_attribute`, `dual_attribute_value`, +functions are `dual_attribute`, `dual_attribute_value`, `constrained_variable_dual_attribute`, `fixed_variable_value`, `fixed_constrained_variables_get`, `equality_constraint_get`. That's a lot of functions, some of which may never be useful for your specific attributes (e.g., if it is never used on an equality constraint) so no need diff --git a/docs/src/reference.md b/docs/src/reference.md index 178593de..e30a754a 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -74,7 +74,6 @@ Dualization.PrimalDualMap ## Attributes ```@docs -constraint_attribute dual_attribute dual_attribute_value constrained_variable_dual_attribute diff --git a/src/attributes.jl b/src/attributes.jl index c198a18a..0e22a4ae 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -1,19 +1,19 @@ _minus(::Nothing) = nothing _minus(x) = -x -""" - constraint_attribute(attr::MOI.AbstractVariableAttribute) +function _variable_attribute(attr::MOI.ConstraintPrimal) + return MOI.VariablePrimal(attr.result_index) +end -When a variable is added as a constrained variable, this function is used to -get the value of the variable from the corresponding constraint. -""" -function constraint_attribute end +function _variable_attribute(::MOI.ConstraintPrimalStart) + return MOI.VariablePrimalStart() +end -function constraint_attribute(attr::MOI.VariablePrimal) +function _constraint_attribute(attr::MOI.VariablePrimal) return MOI.ConstraintPrimal(attr.result_index) end -function constraint_attribute(::MOI.VariablePrimalStart) +function _constraint_attribute(::MOI.VariablePrimalStart) return MOI.ConstraintPrimalStart() end @@ -86,7 +86,7 @@ function constrained_variable_dual_attribute end function constrained_variable_dual_attribute( attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, ) - return constraint_attribute(dual_attribute(attr)) + return _constraint_attribute(dual_attribute(attr)) end function constrained_variable_dual_attribute( @@ -288,20 +288,13 @@ function fixed_constrained_variables_get( return MOI.Utilities.eval_variables(eval, dual_function) end -function _variable_attr(attr::MOI.ConstraintPrimal) - return MOI.VariablePrimal(attr.result_index) -end -function _variable_attr(::MOI.ConstraintPrimalStart) - return MOI.VariablePrimalStart() -end - function fixed_constrained_variables_get( optimizer::DualOptimizer{T}, attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, primal_vi::MOI.VariableIndex, ::MOI.ScalarAffineFunction, ) where {T} - return MOI.get(optimizer, _variable_attr(attr), primal_vi) + return MOI.get(optimizer, _variable_attribute(attr), primal_vi) end # Not sure how to rely on a bridge for this one. diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index 8247404d..0bd24f63 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -248,8 +248,8 @@ function _test_simple(T, dual_model) MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_bound), ) - MOI.set(dual_model, MOI.VariablePrimalStart(), vars[], nothing) - @test isnothing(MOI.get(dual_model, MOI.VariablePrimalStart(), vars[])) + MOI.set(dual, MOI.VariablePrimalStart(), vars[], nothing) + @test isnothing(MOI.get(dual, MOI.VariablePrimalStart(), vars[])) return end From ad72a47da787fa3418793bddf17c0272aa7c62e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 14 Apr 2026 11:36:08 +0200 Subject: [PATCH 33/41] Fixes --- src/attributes.jl | 4 ++++ test/Tests/test_attributes.jl | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 0e22a4ae..9679ead5 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -29,6 +29,10 @@ function dual_attribute end dual_attribute(attr::MOI.ResultCount) = attr +dual_attribute(::MOI.VariableName) = MOI.ConstraintName() + +dual_attribute(::MOI.ConstraintName) = MOI.VariableName() + function dual_attribute(attr::Union{MOI.VariablePrimal,MOI.ConstraintPrimal}) return MOI.ConstraintDual(attr.result_index) end diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index 0bd24f63..d38fcc7d 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -125,9 +125,6 @@ function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) value += set_constant end @test MOI.get(dual.optimizer, MOI.ConstraintPrimal(), ci) ≈ value - - #@show MOI.get(dual, DummyModelAttribute()) - #@test_throws Dualization.DualModelAttributeNotDefined MOI.get(dual, DummyModelAttribute()) return end @@ -213,9 +210,12 @@ function _test_simple(T, dual_model) dual_problem = Dualization.DualProblem{T}(dual_model) OptimizerType = typeof(dual_problem.dual_model) dual = Dualization.DualOptimizer{T,OptimizerType}(dual_problem) + @test MOI.supports(dual, MOI.VariablePrimalStart(), typeof(x)) @test MOI.supports(dual, MOI.ConstraintDualStart(), typeof(c)) @test MOI.supports(dual, MOI.ConstraintPrimalStart(), typeof(c)) + @test MOI.supports(dual, MOI.VariableName(), typeof(x)) + @test MOI.supports(dual, MOI.ConstraintName(), typeof(c)) index_map = MOI.copy_to(dual, model) @test dual_model === dual.dual_problem.dual_model From bcbeff26d1f9f020ded80c9373a8161d86ffe60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 14 Apr 2026 11:50:14 +0200 Subject: [PATCH 34/41] Improve code coverage --- test/Tests/test_attributes.jl | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index d38fcc7d..bca7e6c0 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -50,11 +50,12 @@ function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()); eval_variable_constraint_dual = false, ) - dual = MOI.Utilities.CachingOptimizer( + cached = MOI.Utilities.CachingOptimizer( MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()), MOI.Utilities.MANUAL, # easier to debug with less try-catch hidding stuff ) - MOI.Utilities.reset_optimizer(dual, Dualization.DualOptimizer(mock)) + dual = Dualization.DualOptimizer(mock) + MOI.Utilities.reset_optimizer(cached, dual) set_constant = T(-4) if vector set = MOI.Nonnegatives(1) @@ -63,24 +64,24 @@ function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) end if constrained_variable if vector - X, ci = MOI.add_constrained_variables(dual, set) + X, ci = MOI.add_constrained_variables(cached, set) x = X[] else - x, ci = MOI.add_constrained_variable(dual, set) + x, ci = MOI.add_constrained_variable(cached, set) end else - x = MOI.add_variable(dual) + x = MOI.add_variable(cached) func = T(1) * x if vector func = MOI.Utilities.operate(vcat, T, func - set_constant) end - ci = MOI.add_constraint(dual, func, set) + ci = MOI.add_constraint(cached, func, set) end - MOI.set(dual, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(cached, MOI.ObjectiveSense(), MOI.MIN_SENSE) obj = T(2) * x - MOI.set(dual, MOI.ObjectiveFunction{typeof(obj)}(), obj) - MOI.Utilities.attach_optimizer(dual) - MOI.optimize!(dual) + MOI.set(cached, MOI.ObjectiveFunction{typeof(obj)}(), obj) + MOI.Utilities.attach_optimizer(cached) + MOI.optimize!(cached) for attr in [MOI.ConstraintDualStart(), MOI.ConstraintPrimalStart()] attr = MOI.ConstraintDualStart() @test MOI.supports(dual, attr, typeof(ci)) @@ -93,6 +94,7 @@ function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) ci, value, ) + @test isnothing(MOI.get(dual, attr, ci)) else MOI.set(dual, attr, ci, value) @test MOI.get(dual, attr, ci) == value @@ -110,7 +112,7 @@ function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) end end #MOI.set(mock, MOI.ConstraintPrimal(), mock_ci, value) - @test MOI.get(dual.optimizer, MOI.ConstraintDual(), ci) ≈ value + @test MOI.get(dual, MOI.ConstraintDual(), ci) ≈ value value = rand_value() if vector && constrained_variable @@ -124,7 +126,7 @@ function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) if !vector value += set_constant end - @test MOI.get(dual.optimizer, MOI.ConstraintPrimal(), ci) ≈ value + @test MOI.get(dual, MOI.ConstraintPrimal(), ci) ≈ value return end From 1390e9cde16f2afde69acc62015083f7baba8345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 14 Apr 2026 12:44:05 +0200 Subject: [PATCH 35/41] fix --- src/MOI_wrapper.jl | 9 +-------- src/dualize.jl | 13 +++++++++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 76ed81a0..66f810f0 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -240,14 +240,7 @@ function MOI.copy_to(dest::DualOptimizer, src::MOI.ModelLike) dest.dual_problem, assume_min_if_feasibility = dest.assume_min_if_feasibility, ) - index_map = MOI.Utilities.identity_index_map(src) - vis = MOI.get(src, MOI.ListOfVariableIndices()) - MOI.Utilities.pass_attributes(dest, src, index_map, vis) - for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) - cis = MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) - MOI.Utilities.pass_attributes(dest, src, index_map, cis) - end - return index_map + return MOI.Utilities.identity_index_map(src) end function MOI.optimize!(optimizer::DualOptimizer) diff --git a/src/dualize.jl b/src/dualize.jl index d3dcdad3..92f38a1a 100644 --- a/src/dualize.jl +++ b/src/dualize.jl @@ -216,5 +216,18 @@ function dualize( # Add dual objective to the model _set_dual_objective(dual_problem.dual_model, dual_objective) end + + # Copy attributes except names which have already been passed + primal_without_names = MOI.Utilities.ModelFilter(primal_model) do attr + return !(attr isa Union{MOI.VariableName,MOI.ConstraintName}) + end + index_map = MOI.Utilities.identity_index_map(primal_model) + vis = MOI.get(primal_model, MOI.ListOfVariableIndices()) + MOI.Utilities.pass_attributes(dual_problem.dual_model, primal_without_names, index_map, vis) + for (F, S) in MOI.get(primal_model, MOI.ListOfConstraintTypesPresent()) + cis = MOI.get(primal_model, MOI.ListOfConstraintIndices{F,S}()) + MOI.Utilities.pass_attributes(dual_problem.dual_model, primal_without_names, index_map, cis) + end + return dual_problem end From 1ca341e00b513354ec841d6c39389fd464040b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 14 Apr 2026 12:44:09 +0200 Subject: [PATCH 36/41] fix --- src/dualize.jl | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/dualize.jl b/src/dualize.jl index 92f38a1a..bb80ac0b 100644 --- a/src/dualize.jl +++ b/src/dualize.jl @@ -223,10 +223,20 @@ function dualize( end index_map = MOI.Utilities.identity_index_map(primal_model) vis = MOI.get(primal_model, MOI.ListOfVariableIndices()) - MOI.Utilities.pass_attributes(dual_problem.dual_model, primal_without_names, index_map, vis) + MOI.Utilities.pass_attributes( + dual_problem.dual_model, + primal_without_names, + index_map, + vis, + ) for (F, S) in MOI.get(primal_model, MOI.ListOfConstraintTypesPresent()) cis = MOI.get(primal_model, MOI.ListOfConstraintIndices{F,S}()) - MOI.Utilities.pass_attributes(dual_problem.dual_model, primal_without_names, index_map, cis) + MOI.Utilities.pass_attributes( + dual_problem.dual_model, + primal_without_names, + index_map, + cis, + ) end return dual_problem From b92c47ce715d02fcb0d9a519c8d3feba86f5dc75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 14 Apr 2026 13:44:13 +0200 Subject: [PATCH 37/41] Fix --- src/MOI_wrapper.jl | 23 ++++++++++++++++++++++- src/dualize.jl | 22 ---------------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 66f810f0..5fe8b11e 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -240,7 +240,28 @@ function MOI.copy_to(dest::DualOptimizer, src::MOI.ModelLike) dest.dual_problem, assume_min_if_feasibility = dest.assume_min_if_feasibility, ) - return MOI.Utilities.identity_index_map(src) + # Copy attributes except names which have already been passed in `dualize` + primal_without_names = MOI.Utilities.ModelFilter(src) do attr + return !(attr isa Union{MOI.VariableName,MOI.ConstraintName}) + end + index_map = MOI.Utilities.identity_index_map(src) + vis = MOI.get(src, MOI.ListOfVariableIndices()) + MOI.Utilities.pass_attributes( + dest, + primal_without_names, + index_map, + vis, + ) + for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) + cis = MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) + MOI.Utilities.pass_attributes( + dest, + primal_without_names, + index_map, + cis, + ) + end + return index_map end function MOI.optimize!(optimizer::DualOptimizer) diff --git a/src/dualize.jl b/src/dualize.jl index bb80ac0b..f57f6c6d 100644 --- a/src/dualize.jl +++ b/src/dualize.jl @@ -217,27 +217,5 @@ function dualize( _set_dual_objective(dual_problem.dual_model, dual_objective) end - # Copy attributes except names which have already been passed - primal_without_names = MOI.Utilities.ModelFilter(primal_model) do attr - return !(attr isa Union{MOI.VariableName,MOI.ConstraintName}) - end - index_map = MOI.Utilities.identity_index_map(primal_model) - vis = MOI.get(primal_model, MOI.ListOfVariableIndices()) - MOI.Utilities.pass_attributes( - dual_problem.dual_model, - primal_without_names, - index_map, - vis, - ) - for (F, S) in MOI.get(primal_model, MOI.ListOfConstraintTypesPresent()) - cis = MOI.get(primal_model, MOI.ListOfConstraintIndices{F,S}()) - MOI.Utilities.pass_attributes( - dual_problem.dual_model, - primal_without_names, - index_map, - cis, - ) - end - return dual_problem end From 6c1fffbc9ca5af67d0e9b0a65d2f0928c5fbae2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 14 Apr 2026 13:44:18 +0200 Subject: [PATCH 38/41] Fix format --- src/MOI_wrapper.jl | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 5fe8b11e..b1e0bdb4 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -246,12 +246,7 @@ function MOI.copy_to(dest::DualOptimizer, src::MOI.ModelLike) end index_map = MOI.Utilities.identity_index_map(src) vis = MOI.get(src, MOI.ListOfVariableIndices()) - MOI.Utilities.pass_attributes( - dest, - primal_without_names, - index_map, - vis, - ) + MOI.Utilities.pass_attributes(dest, primal_without_names, index_map, vis) for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) cis = MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) MOI.Utilities.pass_attributes( From 2ca2322a308797c06338d8b1fd0126df1ab8397f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 14 Apr 2026 13:46:56 +0200 Subject: [PATCH 39/41] Fix --- docs/src/reference.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/reference.md b/docs/src/reference.md index e30a754a..3114c4d3 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -74,10 +74,10 @@ Dualization.PrimalDualMap ## Attributes ```@docs -dual_attribute -dual_attribute_value -constrained_variable_dual_attribute -fixed_variable_value -fixed_constrained_variables_get -equality_constraint_get +Dualization.dual_attribute +Dualization.dual_attribute_value +Dualization.constrained_variable_dual_attribute +Dualization.fixed_variable_value +Dualization.fixed_constrained_variables_get +Dualization.equality_constraint_get ``` From abf943e6be330de19b26d09f9ddcba81b956ffee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 14 Apr 2026 13:58:15 +0200 Subject: [PATCH 40/41] Fix --- src/attributes.jl | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 9679ead5..3d63b74b 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -179,13 +179,10 @@ function MOI.get( return fixed_variable_value(attr, T) end # Added as constrained variable - value = dual_attribute_value_get( - attr, - MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - data.dual_constraint, - ), + value = MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + data.dual_constraint, ) if !isnothing(value) && data.dual_constraint isa From 7788a18a194fbb4289c5b150c51db9ff277bc619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 14 Apr 2026 14:11:34 +0200 Subject: [PATCH 41/41] Fix --- docs/src/reference.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/reference.md b/docs/src/reference.md index 3114c4d3..30bdf97a 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -75,7 +75,8 @@ Dualization.PrimalDualMap ```@docs Dualization.dual_attribute -Dualization.dual_attribute_value +Dualization.dual_attribute_value_get +Dualization.dual_attribute_value_set Dualization.constrained_variable_dual_attribute Dualization.fixed_variable_value Dualization.fixed_constrained_variables_get