From a1959feba472fc1668a2f35a17da2e04e6b5ecbb Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Tue, 25 Nov 2025 18:49:48 -0500 Subject: [PATCH 01/12] track variable bounds for ExaModels.Optimizer --- ext/ExaModelsMOI.jl | 40 ++++++++++++++++++++++++++++++++++----- test/JuMPTest/JuMPTest.jl | 5 +++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/ext/ExaModelsMOI.jl b/ext/ExaModelsMOI.jl index 5a649404..bedc6512 100644 --- a/ext/ExaModelsMOI.jl +++ b/ext/ExaModelsMOI.jl @@ -195,7 +195,19 @@ function copy_constraints!(c, moim, var_to_idx, T) con_types = MOI.get(moim, MOI.ListOfConstraintTypesPresent()) for (F, S) in con_types - F <: MOI.VariableIndex && continue + if F <: MOI.VariableIndex + cis = MOI.get(moim, MOI.ListOfConstraintIndices{F,S}()) + for ci in cis + vi = MOI.get(moim, MOI.ConstraintFunction(), ci) + vartype, var_idx = var_to_idx[vi] + if vartype === :variable + con_to_idx[ci] = var_idx + else + # error("Bound constraints on parameters are not supported") + end + end + continue + end cis = MOI.get(moim, MOI.ListOfConstraintIndices{F,S}()) bin, offset = exafy_con(moim, cis, bin, offset, lcon, ucon, y0, var_to_idx, con_to_idx) @@ -682,6 +694,8 @@ mutable struct Optimizer{B,S} <: MOI.AbstractOptimizer result::Any solve_time::Float64 options::Dict{Symbol,Any} + var_to_idx::Dict{MOI.VariableIndex,NamedTuple{(:type, :idx),Tuple{Symbol,Int}}} + con_to_idx::Dict{MOI.ConstraintIndex,Int} end MOI.is_empty(model::Optimizer) = isnothing(model.model) @@ -711,16 +725,29 @@ function MOI.supports(::Optimizer, ::MOI.VariablePrimalStart, ::Type{MOI.Variabl end function ExaModels.Optimizer(solver, backend = nothing; kwargs...) - return Optimizer(solver, backend, nothing, nothing, 0.0, Dict{Symbol,Any}(kwargs...)) + return Optimizer( + solver, + backend, + nothing, + nothing, + 0.0, + Dict{Symbol,Any}(kwargs...), + Dict{MOI.VariableIndex,NamedTuple{(:type, :idx),Tuple{Symbol,Int}}}(), + Dict{MOI.ConstraintIndex,Int}(), + ) end function MOI.empty!(model::ExaModelsMOI.Optimizer) model.model = nothing + empty!(model.var_to_idx) + empty!(model.con_to_idx) end function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) core, maps = to_exacore(src; backend = dest.backend) dest.model = ExaModels.ExaModel(core; prod = true) + dest.var_to_idx = maps[1] + dest.con_to_idx = maps[2] return _make_index_map(src, maps) end @@ -772,7 +799,8 @@ function MOI.get( ) MOI.check_result_index_bounds(model, attr) # MOI.throw_if_not_valid(model, ci) - rc = model.result.multipliers_L[ci.value] - model.result.multipliers_U[ci.value] + var_idx = model.con_to_idx[ci] + rc = model.result.multipliers_L[var_idx] - model.result.multipliers_U[var_idx] return min(0.0, rc) end @@ -783,7 +811,8 @@ function MOI.get( ) MOI.check_result_index_bounds(model, attr) # MOI.throw_if_not_valid(model, ci) - rc = model.result.multipliers_L[ci.value] - model.result.multipliers_U[ci.value] + var_idx = model.con_to_idx[ci] + rc = model.result.multipliers_L[var_idx] - model.result.multipliers_U[var_idx] return max(0.0, rc) end @@ -794,7 +823,8 @@ function MOI.get( ) MOI.check_result_index_bounds(model, attr) # MOI.throw_if_not_valid(model, ci) - rc = model.result.multipliers_L[ci.value] - model.result.multipliers_U[ci.value] + var_idx = model.con_to_idx[ci] + rc = model.result.multipliers_L[var_idx] - model.result.multipliers_U[var_idx] return rc end diff --git a/test/JuMPTest/JuMPTest.jl b/test/JuMPTest/JuMPTest.jl index 3dcd7845..03e2f3f3 100644 --- a/test/JuMPTest/JuMPTest.jl +++ b/test/JuMPTest/JuMPTest.jl @@ -293,6 +293,11 @@ function runtests() optimize!(jm) sol = value.(all_variables(jm)) + set_optimizer(jm, () -> ExaModels.Optimizer(ipopt)) + optimize!(jm) + sol2 = value.(all_variables(jm)) + @test sol ≈ sol2 atol = 1e-6 + for backend in BACKENDS @testset "$backend" begin m = WrapperNLPModel(ExaModel(jm; backend = backend)) From cb138fa588ff14e5bc698f26d5dc003a1283bea0 Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Tue, 25 Nov 2025 19:09:24 -0500 Subject: [PATCH 02/12] format --- ext/ExaModelsMOI.jl | 14 +++++++------- test/JuMPTest/JuMPTest.jl | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ext/ExaModelsMOI.jl b/ext/ExaModelsMOI.jl index bedc6512..9c7deb44 100644 --- a/ext/ExaModelsMOI.jl +++ b/ext/ExaModelsMOI.jl @@ -196,7 +196,7 @@ function copy_constraints!(c, moim, var_to_idx, T) con_types = MOI.get(moim, MOI.ListOfConstraintTypesPresent()) for (F, S) in con_types if F <: MOI.VariableIndex - cis = MOI.get(moim, MOI.ListOfConstraintIndices{F,S}()) + cis = MOI.get(moim, MOI.ListOfConstraintIndices{F, S}()) for ci in cis vi = MOI.get(moim, MOI.ConstraintFunction(), ci) vartype, var_idx = var_to_idx[vi] @@ -694,8 +694,8 @@ mutable struct Optimizer{B,S} <: MOI.AbstractOptimizer result::Any solve_time::Float64 options::Dict{Symbol,Any} - var_to_idx::Dict{MOI.VariableIndex,NamedTuple{(:type, :idx),Tuple{Symbol,Int}}} - con_to_idx::Dict{MOI.ConstraintIndex,Int} + var_to_idx::Dict{MOI.VariableIndex, NamedTuple{(:type, :idx), Tuple{Symbol, Int}}} + con_to_idx::Dict{MOI.ConstraintIndex, Int} end MOI.is_empty(model::Optimizer) = isnothing(model.model) @@ -731,16 +731,16 @@ function ExaModels.Optimizer(solver, backend = nothing; kwargs...) nothing, nothing, 0.0, - Dict{Symbol,Any}(kwargs...), - Dict{MOI.VariableIndex,NamedTuple{(:type, :idx),Tuple{Symbol,Int}}}(), - Dict{MOI.ConstraintIndex,Int}(), + Dict{Symbol, Any}(kwargs...), + Dict{MOI.VariableIndex, NamedTuple{(:type, :idx), Tuple{Symbol, Int}}}(), + Dict{MOI.ConstraintIndex, Int}(), ) end function MOI.empty!(model::ExaModelsMOI.Optimizer) model.model = nothing empty!(model.var_to_idx) - empty!(model.con_to_idx) + return empty!(model.con_to_idx) end function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) diff --git a/test/JuMPTest/JuMPTest.jl b/test/JuMPTest/JuMPTest.jl index 03e2f3f3..50ef9d94 100644 --- a/test/JuMPTest/JuMPTest.jl +++ b/test/JuMPTest/JuMPTest.jl @@ -296,7 +296,7 @@ function runtests() set_optimizer(jm, () -> ExaModels.Optimizer(ipopt)) optimize!(jm) sol2 = value.(all_variables(jm)) - @test sol ≈ sol2 atol = 1e-6 + @test sol ≈ sol2 atol = 1.0e-6 for backend in BACKENDS @testset "$backend" begin From eb88ff2bac55c62dd79fd6f58c085ac889a6e7fa Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Tue, 25 Nov 2025 19:21:10 -0500 Subject: [PATCH 03/12] return nothing --- ext/ExaModelsMOI.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/ExaModelsMOI.jl b/ext/ExaModelsMOI.jl index 9c7deb44..8f83d2e2 100644 --- a/ext/ExaModelsMOI.jl +++ b/ext/ExaModelsMOI.jl @@ -740,7 +740,8 @@ end function MOI.empty!(model::ExaModelsMOI.Optimizer) model.model = nothing empty!(model.var_to_idx) - return empty!(model.con_to_idx) + empty!(model.con_to_idx) + return end function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) From 9fb79bc570c3089eb4e98c47fed85784bcbe502e Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Tue, 25 Nov 2025 19:29:50 -0500 Subject: [PATCH 04/12] revert getter change --- ext/ExaModelsMOI.jl | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ext/ExaModelsMOI.jl b/ext/ExaModelsMOI.jl index 8f83d2e2..0f4b220d 100644 --- a/ext/ExaModelsMOI.jl +++ b/ext/ExaModelsMOI.jl @@ -800,8 +800,7 @@ function MOI.get( ) MOI.check_result_index_bounds(model, attr) # MOI.throw_if_not_valid(model, ci) - var_idx = model.con_to_idx[ci] - rc = model.result.multipliers_L[var_idx] - model.result.multipliers_U[var_idx] + rc = model.result.multipliers_L[ci.value] - model.result.multipliers_U[ci.value] return min(0.0, rc) end @@ -812,8 +811,7 @@ function MOI.get( ) MOI.check_result_index_bounds(model, attr) # MOI.throw_if_not_valid(model, ci) - var_idx = model.con_to_idx[ci] - rc = model.result.multipliers_L[var_idx] - model.result.multipliers_U[var_idx] + rc = model.result.multipliers_L[ci.value] - model.result.multipliers_U[ci.value] return max(0.0, rc) end @@ -824,8 +822,7 @@ function MOI.get( ) MOI.check_result_index_bounds(model, attr) # MOI.throw_if_not_valid(model, ci) - var_idx = model.con_to_idx[ci] - rc = model.result.multipliers_L[var_idx] - model.result.multipliers_U[var_idx] + rc = model.result.multipliers_L[ci.value] - model.result.multipliers_U[ci.value] return rc end From 0ceb99555e2392be57933aa72fc87075f42ae65c Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Tue, 25 Nov 2025 19:30:00 -0500 Subject: [PATCH 05/12] test duals --- test/JuMPTest/JuMPTest.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/JuMPTest/JuMPTest.jl b/test/JuMPTest/JuMPTest.jl index 50ef9d94..8a3d17b8 100644 --- a/test/JuMPTest/JuMPTest.jl +++ b/test/JuMPTest/JuMPTest.jl @@ -292,17 +292,21 @@ function runtests() set_optimizer_attribute(jm, "print_level", 0) optimize!(jm) sol = value.(all_variables(jm)) + dsol = dual.(all_constraints(jm, include_variable_in_set_constraints=true)) set_optimizer(jm, () -> ExaModels.Optimizer(ipopt)) optimize!(jm) sol2 = value.(all_variables(jm)) + dsol2 = dual.(all_constraints(jm, include_variable_in_set_constraints=true)) @test sol ≈ sol2 atol = 1.0e-6 + @test dsol ≈ dsol2 atol = 1.0e-6 for backend in BACKENDS @testset "$backend" begin m = WrapperNLPModel(ExaModel(jm; backend = backend)) result = ipopt(m; print_level = 0) - + + # NOTE: assumes IndexMap is identity! @test sol ≈ result.solution atol = 1e-6 end end From ef42ffc5f3d75fe4b3efd50c5ed8ea2930458eb7 Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Tue, 25 Nov 2025 19:34:20 -0500 Subject: [PATCH 06/12] fmt --- test/JuMPTest/JuMPTest.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/JuMPTest/JuMPTest.jl b/test/JuMPTest/JuMPTest.jl index 8a3d17b8..44fcb0f2 100644 --- a/test/JuMPTest/JuMPTest.jl +++ b/test/JuMPTest/JuMPTest.jl @@ -292,12 +292,12 @@ function runtests() set_optimizer_attribute(jm, "print_level", 0) optimize!(jm) sol = value.(all_variables(jm)) - dsol = dual.(all_constraints(jm, include_variable_in_set_constraints=true)) + dsol = dual.(all_constraints(jm, include_variable_in_set_constraints = true)) set_optimizer(jm, () -> ExaModels.Optimizer(ipopt)) optimize!(jm) sol2 = value.(all_variables(jm)) - dsol2 = dual.(all_constraints(jm, include_variable_in_set_constraints=true)) + dsol2 = dual.(all_constraints(jm, include_variable_in_set_constraints = true)) @test sol ≈ sol2 atol = 1.0e-6 @test dsol ≈ dsol2 atol = 1.0e-6 @@ -305,7 +305,7 @@ function runtests() @testset "$backend" begin m = WrapperNLPModel(ExaModel(jm; backend = backend)) result = ipopt(m; print_level = 0) - + # NOTE: assumes IndexMap is identity! @test sol ≈ result.solution atol = 1e-6 end From e0418e793ca48acd2d1ddc810cb381807ca19083 Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Tue, 25 Nov 2025 20:25:22 -0500 Subject: [PATCH 07/12] no need to store maps in Optimizer --- ext/ExaModelsMOI.jl | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ext/ExaModelsMOI.jl b/ext/ExaModelsMOI.jl index 0f4b220d..96c559fb 100644 --- a/ext/ExaModelsMOI.jl +++ b/ext/ExaModelsMOI.jl @@ -694,8 +694,6 @@ mutable struct Optimizer{B,S} <: MOI.AbstractOptimizer result::Any solve_time::Float64 options::Dict{Symbol,Any} - var_to_idx::Dict{MOI.VariableIndex, NamedTuple{(:type, :idx), Tuple{Symbol, Int}}} - con_to_idx::Dict{MOI.ConstraintIndex, Int} end MOI.is_empty(model::Optimizer) = isnothing(model.model) @@ -732,23 +730,17 @@ function ExaModels.Optimizer(solver, backend = nothing; kwargs...) nothing, 0.0, Dict{Symbol, Any}(kwargs...), - Dict{MOI.VariableIndex, NamedTuple{(:type, :idx), Tuple{Symbol, Int}}}(), - Dict{MOI.ConstraintIndex, Int}(), ) end function MOI.empty!(model::ExaModelsMOI.Optimizer) model.model = nothing - empty!(model.var_to_idx) - empty!(model.con_to_idx) return end function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) core, maps = to_exacore(src; backend = dest.backend) dest.model = ExaModels.ExaModel(core; prod = true) - dest.var_to_idx = maps[1] - dest.con_to_idx = maps[2] return _make_index_map(src, maps) end From c5a1a39584ee2cf249e287a88400d4218ac4d430 Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Tue, 25 Nov 2025 20:26:31 -0500 Subject: [PATCH 08/12] silence --- test/JuMPTest/JuMPTest.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/JuMPTest/JuMPTest.jl b/test/JuMPTest/JuMPTest.jl index 44fcb0f2..c89139f2 100644 --- a/test/JuMPTest/JuMPTest.jl +++ b/test/JuMPTest/JuMPTest.jl @@ -295,6 +295,7 @@ function runtests() dsol = dual.(all_constraints(jm, include_variable_in_set_constraints = true)) set_optimizer(jm, () -> ExaModels.Optimizer(ipopt)) + set_optimizer_attribute(jm, "print_level", 0) optimize!(jm) sol2 = value.(all_variables(jm)) dsol2 = dual.(all_constraints(jm, include_variable_in_set_constraints = true)) From 4619efb0b78da71e94b7a7074d88d01a71133db8 Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Tue, 25 Nov 2025 20:40:04 -0500 Subject: [PATCH 09/12] [skip ci] minimize diff --- ext/ExaModelsMOI.jl | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/ext/ExaModelsMOI.jl b/ext/ExaModelsMOI.jl index 96c559fb..7c3d802c 100644 --- a/ext/ExaModelsMOI.jl +++ b/ext/ExaModelsMOI.jl @@ -723,14 +723,7 @@ function MOI.supports(::Optimizer, ::MOI.VariablePrimalStart, ::Type{MOI.Variabl end function ExaModels.Optimizer(solver, backend = nothing; kwargs...) - return Optimizer( - solver, - backend, - nothing, - nothing, - 0.0, - Dict{Symbol, Any}(kwargs...), - ) + return Optimizer(solver, backend, nothing, nothing, 0.0, Dict{Symbol, Any}(kwargs...)) end function MOI.empty!(model::ExaModelsMOI.Optimizer) From dbcf7ac07190b91c508748d74ed39ff76f79376e Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Tue, 25 Nov 2025 20:41:17 -0500 Subject: [PATCH 10/12] [skip ci] further minimize diff --- ext/ExaModelsMOI.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ext/ExaModelsMOI.jl b/ext/ExaModelsMOI.jl index 7c3d802c..51b51e7f 100644 --- a/ext/ExaModelsMOI.jl +++ b/ext/ExaModelsMOI.jl @@ -723,12 +723,11 @@ function MOI.supports(::Optimizer, ::MOI.VariablePrimalStart, ::Type{MOI.Variabl end function ExaModels.Optimizer(solver, backend = nothing; kwargs...) - return Optimizer(solver, backend, nothing, nothing, 0.0, Dict{Symbol, Any}(kwargs...)) + return Optimizer(solver, backend, nothing, nothing, 0.0, Dict{Symbol,Any}(kwargs...)) end function MOI.empty!(model::ExaModelsMOI.Optimizer) model.model = nothing - return end function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) From ea0afa32ff66c6bf03251f68d3bf38b05d859301 Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Thu, 4 Dec 2025 18:43:54 -0500 Subject: [PATCH 11/12] simplify --- ext/ExaModelsMOI.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ext/ExaModelsMOI.jl b/ext/ExaModelsMOI.jl index 51b51e7f..a0498964 100644 --- a/ext/ExaModelsMOI.jl +++ b/ext/ExaModelsMOI.jl @@ -195,20 +195,17 @@ function copy_constraints!(c, moim, var_to_idx, T) con_types = MOI.get(moim, MOI.ListOfConstraintTypesPresent()) for (F, S) in con_types + cis = MOI.get(moim, MOI.ListOfConstraintIndices{F,S}()) if F <: MOI.VariableIndex - cis = MOI.get(moim, MOI.ListOfConstraintIndices{F, S}()) for ci in cis vi = MOI.get(moim, MOI.ConstraintFunction(), ci) vartype, var_idx = var_to_idx[vi] if vartype === :variable con_to_idx[ci] = var_idx - else - # error("Bound constraints on parameters are not supported") end end continue end - cis = MOI.get(moim, MOI.ListOfConstraintIndices{F,S}()) bin, offset = exafy_con(moim, cis, bin, offset, lcon, ucon, y0, var_to_idx, con_to_idx) end From b65f0ab5179230ef9a6829b1e93edef4a34f8fac Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Thu, 4 Dec 2025 19:33:54 -0500 Subject: [PATCH 12/12] [skip ci] rm comment --- test/JuMPTest/JuMPTest.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/test/JuMPTest/JuMPTest.jl b/test/JuMPTest/JuMPTest.jl index c89139f2..5ff455a7 100644 --- a/test/JuMPTest/JuMPTest.jl +++ b/test/JuMPTest/JuMPTest.jl @@ -307,7 +307,6 @@ function runtests() m = WrapperNLPModel(ExaModel(jm; backend = backend)) result = ipopt(m; print_level = 0) - # NOTE: assumes IndexMap is identity! @test sol ≈ result.solution atol = 1e-6 end end