From 7caf3eb2d0c4414fa2ca89518153316aabfaff8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 15 Aug 2026 14:06:20 +0000 Subject: [PATCH 1/7] Use MOI.Nonlinear.ModelWithQuad instead of QPBlockData Store the affine and quadratic objective and constraints in a MOI.Nonlinear.ModelWithQuad whose inner model is the nonlinear model (nlp_model now aliases quad_data.inner), and evaluate everything through a MOI.Nonlinear.EvaluatorWithQuad built in _setup_model. The evaluator owns the QP block entry counts, so the qp_nnzj/qp_nnzh fields are removed. The inner evaluator is a new _OracleNLPEvaluator that stacks the VectorNonlinearOracle rows before the NLPBlock rows; it extends the private MOI.Nonlinear._constraint_bounds so that the constraint bounds of the whole stack are assembled by the evaluator. --- ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl | 258 ++++++++++++-------- 1 file changed, 155 insertions(+), 103 deletions(-) diff --git a/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl b/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl index 58d9712..fdca217 100644 --- a/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl +++ b/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl @@ -3,9 +3,6 @@ # 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. -const QPBlockData = MOI.Nonlinear.QPBlockData - - const _PARAMETER_OFFSET = 0x00f0000000000000 _is_parameter(x::MOI.VariableIndex) = x.value >= _PARAMETER_OFFSET @@ -53,12 +50,12 @@ mutable struct Optimizer <: MOI.AbstractOptimizer nlp_data::MOI.NLPBlockData nlp_dual_start::Union{Nothing,Vector{Float64}} mult_g_nlp::Dict{MOI.Nonlinear.ConstraintIndex,Float64} - qp_data::QPBlockData{Float64} - # The number of entries of the Jacobian and of the Hessian of the - # Lagrangian of `qp_data`, computed in `_setup_model`. - qp_nnzj::Int - qp_nnzh::Int + quad_data::MOI.Nonlinear.ModelWithQuad{Float64,MOI.Nonlinear.Model} + # `=== quad_data.inner` once the new nonlinear API is in use, `nothing` + # otherwise. See `_init_nlp_model`. nlp_model::Union{Nothing,MOI.Nonlinear.Model} + # The evaluator of `quad_data`, rebuilt in `_setup_model`. + evaluator::Union{Nothing,MOI.Nonlinear.EvaluatorWithQuad{Float64}} callback::Union{Nothing,Function} barrier_iterations::Int ad_backend::MOI.Nonlinear.AbstractAutomaticDifferentiation @@ -88,9 +85,8 @@ mutable struct Optimizer <: MOI.AbstractOptimizer MOI.NLPBlockData([], _EmptyNLPEvaluator(), false), nothing, Dict{MOI.Nonlinear.ConstraintIndex,Float64}(), - QPBlockData{Float64}(), - 0, - 0, + MOI.Nonlinear.ModelWithQuad(MOI.Nonlinear.Model()), + nothing, nothing, nothing, 0, @@ -142,10 +138,9 @@ function MOI.empty!(model::Optimizer) model.nlp_data = MOI.NLPBlockData([], _EmptyNLPEvaluator(), false) model.nlp_dual_start = nothing empty!(model.mult_g_nlp) - model.qp_data = QPBlockData{Float64}() - model.qp_nnzj = 0 - model.qp_nnzh = 0 + model.quad_data = MOI.Nonlinear.ModelWithQuad(MOI.Nonlinear.Model()) model.nlp_model = nothing + model.evaluator = nothing model.callback = nothing model.barrier_iterations = 0 # SKIP: model.ad_backend @@ -187,7 +182,7 @@ function _init_nlp_model(model) if !(model.nlp_data.evaluator isa _EmptyNLPEvaluator) error("Cannot mix the new and legacy nonlinear APIs") end - model.nlp_model = MOI.Nonlinear.Model() + model.nlp_model = model.quad_data.inner end return end @@ -206,7 +201,7 @@ function MOI.add_constrained_variable( # is a key of `parameters`, so the parameter must be registered before # any structure query. The value is re-synced in `copy_parameters` before # every solve. - model.qp_data.parameters[p.value] = set.value + model.quad_data.qp.parameters[p.value] = set.value ci = MOI.ConstraintIndex{MOI.VariableIndex,typeof(set)}(p.value) return p, ci end @@ -309,7 +304,7 @@ end function MOI.get(model::Optimizer, attr::MOI.ListOfConstraintTypesPresent) ret = MOI.get(model.variables, attr) - append!(ret, MOI.get(model.qp_data, attr)) + append!(ret, MOI.get(model.quad_data, attr)) _add_scalar_nonlinear_constraints(ret, model.nlp_model) if !isempty(model.vector_nonlinear_oracle_constraints) push!(ret, (MOI.VectorOfVariables, MOI.VectorNonlinearOracle{Float64})) @@ -474,7 +469,7 @@ function MOI.is_valid( MOI.ScalarQuadraticFunction{Float64}, }, } - return MOI.is_valid(model.qp_data, ci) + return MOI.is_valid(model.quad_data, ci) end function MOI.add_constraint( @@ -485,7 +480,7 @@ function MOI.add_constraint( }, set::_SETS, ) - index = MOI.add_constraint(model.qp_data, func, set) + index = MOI.add_constraint(model.quad_data, func, set) model.inner = nothing return index end @@ -500,7 +495,7 @@ function MOI.get( }, S<:_SETS, } - return MOI.get(model.qp_data, attr) + return MOI.get(model.quad_data, attr) end function MOI.get( @@ -513,7 +508,7 @@ function MOI.get( MOI.ScalarQuadraticFunction{Float64}, }, } - return MOI.get(model.qp_data, attr, c) + return MOI.get(model.quad_data, attr, c) end function MOI.set( @@ -528,7 +523,7 @@ function MOI.set( }, S<:_SETS, } - MOI.set(model.qp_data, MOI.ConstraintSet(), ci, set) + MOI.set(model.quad_data, MOI.ConstraintSet(), ci, set) model.needs_new_inner = true return end @@ -556,7 +551,7 @@ function MOI.get( MOI.ScalarQuadraticFunction{Float64}, }, } - return MOI.get(model.qp_data, attr, c) + return MOI.get(model.quad_data, attr, c) end function MOI.set( @@ -571,7 +566,7 @@ function MOI.set( }, } MOI.throw_if_not_valid(model, ci) - MOI.set(model.qp_data, attr, ci, value) + MOI.set(model.quad_data, attr, ci, value) # No need to reset model.inner, because this gets handled in optimize!. return end @@ -645,7 +640,9 @@ function MOI.set( if !isempty(model.parameters) _replace_parameters(model, func) end - MOI.Nonlinear.set_objective(model.nlp_model, func) + # Set through `quad_data` so that the objective sink is updated and any + # affine or quadratic objective is cleared. + MOI.Nonlinear.set_objective(model.quad_data, func) model.inner = nothing return end @@ -760,7 +757,7 @@ function row( model::Optimizer, ci::MOI.ConstraintIndex{F,S}, ) where {F<:MOI.VectorOfVariables,S<:MOI.VectorNonlinearOracle{Float64}} - offset = length(model.qp_data) + offset = length(model.quad_data) for i in 1:(ci.value-1) _, s = model.vector_nonlinear_oracle_constraints[i] offset += s.set.output_dimension @@ -1045,7 +1042,7 @@ function MOI.get(model::Optimizer, attr::MOI.ObjectiveFunctionType) if model.nlp_model !== nothing && model.nlp_model.objective !== nothing return MOI.ScalarNonlinearFunction end - return MOI.get(model.qp_data, attr) + return MOI.get(model.quad_data, attr) end function MOI.supports( @@ -1071,7 +1068,7 @@ function MOI.get( MOI.ScalarQuadraticFunction{Float64}, }, } - return convert(F, MOI.get(model.qp_data, attr)) + return convert(F, MOI.get(model.quad_data, attr)) end function MOI.set( @@ -1085,14 +1082,72 @@ function MOI.set( MOI.ScalarQuadraticFunction{Float64}, }, } - MOI.set(model.qp_data, attr, func) - if model.nlp_model !== nothing - MOI.Nonlinear.set_objective(model.nlp_model, nothing) - end + # This also clears any objective of the inner nonlinear model. + MOI.set(model.quad_data, attr, func) model.inner = nothing return end +### _OracleNLPEvaluator + +""" + _OracleNLPEvaluator(model::Optimizer) + +The inner evaluator of the `MOI.Nonlinear.EvaluatorWithQuad` of `model`: the +rows of the `MOI.VectorNonlinearOracle` constraints followed by the rows of +the `MOI.NLPBlock` evaluator. +""" +struct _OracleNLPEvaluator{E<:MOI.AbstractNLPEvaluator} <: + MOI.AbstractNLPEvaluator + oracles::Vector{Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}} + nlp::E + nlp_bounds::Vector{MOI.NLPBoundsPair} + has_objective::Bool +end + +function _OracleNLPEvaluator(model::Optimizer) + return _OracleNLPEvaluator( + model.vector_nonlinear_oracle_constraints, + model.nlp_data.evaluator, + model.nlp_data.constraint_bounds, + model.nlp_data.has_objective, + ) +end + +function MOI.features_available(d::_OracleNLPEvaluator) + features = MOI.features_available(d.nlp) + if any(s.set.eval_hessian_lagrangian === nothing for (_, s) in d.oracles) + features = setdiff(features, [:Hess, :HessVec]) + end + if !isempty(d.oracles) + # The oracles do not implement the Jacobian and Hessian products. + features = setdiff(features, [:JacVec, :HessVec]) + end + return features +end + +function MOI.initialize(d::_OracleNLPEvaluator, features::Vector{Symbol}) + return MOI.initialize(d.nlp, features) +end + +MOI.eval_objective(d::_OracleNLPEvaluator, x) = MOI.eval_objective(d.nlp, x) + +function MOI.eval_objective_gradient(d::_OracleNLPEvaluator, grad, x) + return MOI.eval_objective_gradient(d.nlp, grad, x) +end + +function MOI.Nonlinear._constraint_bounds(d::_OracleNLPEvaluator) + bounds = MOI.NLPBoundsPair[] + for (_, s) in d.oracles + for i in 1:s.set.output_dimension + push!(bounds, MOI.NLPBoundsPair(s.set.l[i], s.set.u[i])) + end + end + return append!(bounds, d.nlp_bounds) +end + +MOI.Nonlinear._has_objective(d::_OracleNLPEvaluator) = d.has_objective + ### Eval_F_CB function MOI.eval_objective(model::Optimizer, x) @@ -1103,7 +1158,7 @@ function MOI.eval_objective(model::Optimizer, x) elseif model.nlp_data.has_objective return MOI.eval_objective(model.nlp_data.evaluator, x)::Float64 end - return MOI.eval_objective(model.qp_data, x) + return MOI.eval_objective(model.evaluator, x) end ### Eval_Grad_F_CB @@ -1114,7 +1169,7 @@ function MOI.eval_objective_gradient(model::Optimizer, grad, x) elseif model.nlp_data.has_objective MOI.eval_objective_gradient(model.nlp_data.evaluator, grad, x) else - MOI.eval_objective_gradient(model.qp_data, grad, x) + MOI.eval_objective_gradient(model.evaluator, grad, x) end return end @@ -1136,17 +1191,19 @@ function _eval_constraint( return offset + s.set.output_dimension end -function MOI.eval_constraint(model::Optimizer, g, x) - MOI.eval_constraint(model.qp_data, g, x) - offset = length(model.qp_data) - for (f, s) in model.vector_nonlinear_oracle_constraints +function MOI.eval_constraint(d::_OracleNLPEvaluator, g, x) + offset = 0 + for (f, s) in d.oracles offset = _eval_constraint(g, offset, x, f, s) end - g_nlp = view(g, (offset+1):length(g)) - MOI.eval_constraint(model.nlp_data.evaluator, g_nlp, x) + MOI.eval_constraint(d.nlp, view(g, (offset+1):length(g)), x) return end +function MOI.eval_constraint(model::Optimizer, g, x) + return MOI.eval_constraint(model.evaluator, g, x) +end + ### Eval_Jac_G_CB function _jacobian_structure( @@ -1161,16 +1218,14 @@ function _jacobian_structure( return row_offset + s.set.output_dimension end -function MOI.jacobian_structure(model::Optimizer) - J = MOI.jacobian_structure(model.qp_data) - offset = length(model.qp_data) - for (f, s) in model.vector_nonlinear_oracle_constraints +function MOI.jacobian_structure(d::_OracleNLPEvaluator) + J = Tuple{Int,Int}[] + offset = 0 + for (f, s) in d.oracles offset = _jacobian_structure(J, offset, f, s) end - if length(model.nlp_data.constraint_bounds) > 0 - J_nlp = MOI.jacobian_structure( - model.nlp_data.evaluator, - )::Vector{Tuple{Int64,Int64}} + if length(d.nlp_bounds) > 0 + J_nlp = MOI.jacobian_structure(d.nlp)::Vector{Tuple{Int64,Int64}} for (row, col) in J_nlp push!(J, (row + offset, col)) end @@ -1178,6 +1233,10 @@ function MOI.jacobian_structure(model::Optimizer) return J end +function MOI.jacobian_structure(model::Optimizer) + return MOI.jacobian_structure(model.evaluator) +end + function _eval_constraint_jacobian( values::AbstractVector, offset::Int, @@ -1194,17 +1253,20 @@ function _eval_constraint_jacobian( return offset + nnz end -function MOI.eval_constraint_jacobian(model::Optimizer, values, x) - MOI.eval_constraint_jacobian(model.qp_data, values, x) - offset = model.qp_nnzj - for (f, s) in model.vector_nonlinear_oracle_constraints +function MOI.eval_constraint_jacobian(d::_OracleNLPEvaluator, values, x) + offset = 0 + for (f, s) in d.oracles offset = _eval_constraint_jacobian(values, offset, x, f, s) end nlp_values = view(values, (offset+1):length(values)) - MOI.eval_constraint_jacobian(model.nlp_data.evaluator, nlp_values, x) + MOI.eval_constraint_jacobian(d.nlp, nlp_values, x) return end +function MOI.eval_constraint_jacobian(model::Optimizer, values, x) + return MOI.eval_constraint_jacobian(model.evaluator, values, x) +end + ### Eval_H_CB function _hessian_lagrangian_structure( @@ -1218,15 +1280,19 @@ function _hessian_lagrangian_structure( return end -function MOI.hessian_lagrangian_structure(model::Optimizer) - H = MOI.hessian_lagrangian_structure(model.qp_data) - for (f, s) in model.vector_nonlinear_oracle_constraints +function MOI.hessian_lagrangian_structure(d::_OracleNLPEvaluator) + H = Tuple{Int,Int}[] + for (f, s) in d.oracles _hessian_lagrangian_structure(H, f, s) end - append!(H, MOI.hessian_lagrangian_structure(model.nlp_data.evaluator)) + append!(H, MOI.hessian_lagrangian_structure(d.nlp)) return H end +function MOI.hessian_lagrangian_structure(model::Optimizer) + return MOI.hessian_lagrangian_structure(model.evaluator) +end + function _eval_hessian_lagrangian( H::AbstractVector, H_offset::Int, @@ -1247,20 +1313,22 @@ function _eval_hessian_lagrangian( return H_offset + H_nnz, μ_offset + s.set.output_dimension end -function MOI.eval_hessian_lagrangian(model::Optimizer, H, x, σ, μ) - MOI.eval_hessian_lagrangian(model.qp_data, H, x, σ, μ) - offset = model.qp_nnzh - μ_offset = length(model.qp_data) - for (f, s) in model.vector_nonlinear_oracle_constraints +function MOI.eval_hessian_lagrangian(d::_OracleNLPEvaluator, H, x, σ, μ) + offset, μ_offset = 0, 0 + for (f, s) in d.oracles offset, μ_offset = _eval_hessian_lagrangian(H, offset, x, μ, μ_offset, f, s) end H_nlp = view(H, (offset+1):length(H)) μ_nlp = view(μ, (μ_offset+1):length(μ)) - MOI.eval_hessian_lagrangian(model.nlp_data.evaluator, H_nlp, x, σ, μ_nlp) + MOI.eval_hessian_lagrangian(d.nlp, H_nlp, x, σ, μ_nlp) return end +function MOI.eval_hessian_lagrangian(model::Optimizer, H, x, σ, μ) + return MOI.eval_hessian_lagrangian(model.evaluator, H, x, σ, μ) +end + ### MOI.AutomaticDifferentiationBackend MOI.supports(::Optimizer, ::MOI.AutomaticDifferentiationBackend) = true @@ -1310,15 +1378,9 @@ function _setup_inner(model::Optimizer)::Ipopt.IpoptProblem if !model.needs_new_inner return model.inner end - g_L, g_U = copy(model.qp_data.g_L), copy(model.qp_data.g_U) - for (_, s) in model.vector_nonlinear_oracle_constraints - append!(g_L, s.set.l) - append!(g_U, s.set.u) - end - for bound in model.nlp_data.constraint_bounds - push!(g_L, bound.lower) - push!(g_U, bound.upper) - end + bounds = MOI.Nonlinear._constraint_bounds(model.evaluator) + g_L = Float64[b.lower for b in bounds] + g_U = Float64[b.upper for b in bounds] function eval_h_cb(x, rows, cols, obj_factor, lambda, values) return _eval_h_cb(model, x, rows, cols, obj_factor, lambda, values) end @@ -1384,29 +1446,25 @@ function _setup_model(model::Optimizer) model.invalid_model = true return end + vars = MOI.get(model.variables, MOI.ListOfVariableIndices()) if model.nlp_model !== nothing - vars = MOI.get(model.variables, MOI.ListOfVariableIndices()) model.nlp_data = MOI.NLPBlockData( MOI.Nonlinear.Evaluator(model.nlp_model, model.ad_backend, vars), ) end - model.qp_nnzj = length(MOI.jacobian_structure(model.qp_data)) - model.qp_nnzh = length(MOI.hessian_lagrangian_structure(model.qp_data)) - has_quadratic_constraints = - any( - isequal(MOI.Nonlinear._kFunctionTypeScalarQuadratic), - model.qp_data.function_type, - ) + model.evaluator = MOI.Nonlinear.EvaluatorWithQuad( + model.quad_data, + _OracleNLPEvaluator(model), + vars, + ) + has_quadratic_constraints = any( + isequal(MOI.Nonlinear._kFunctionTypeScalarQuadratic), + model.quad_data.qp.function_type, + ) has_nlp_constraints = !isempty(model.nlp_data.constraint_bounds) || !isempty(model.vector_nonlinear_oracle_constraints) - has_hessian = :Hess in MOI.features_available(model.nlp_data.evaluator) - for (_, s) in model.vector_nonlinear_oracle_constraints - if s.set.eval_hessian_lagrangian === nothing - has_hessian = false - break - end - end + has_hessian = :Hess in MOI.features_available(model.evaluator) init_feat = [:Grad] if has_hessian push!(init_feat, :Hess) @@ -1414,7 +1472,7 @@ function _setup_model(model::Optimizer) if has_nlp_constraints push!(init_feat, :Jac) end - MOI.initialize(model.nlp_data.evaluator, init_feat) + MOI.initialize(model.evaluator, init_feat) model.jacobian_sparsity = MOI.jacobian_structure(model) model.hessian_sparsity = nothing if has_hessian @@ -1436,9 +1494,9 @@ function MOI.optimize!(model::Optimizer) end inner = _setup_inner(model) if model.nlp_model !== nothing - empty!(model.qp_data.parameters) + empty!(model.quad_data.qp.parameters) for (p, index) in model.parameters - model.qp_data.parameters[p.value] = model.nlp_model[index] + model.quad_data.qp.parameters[p.value] = model.nlp_model[index] end end # The default print level is `5` @@ -1467,10 +1525,10 @@ function MOI.optimize!(model::Optimizer) clamp(0.0, model.variables.lower[i], model.variables.upper[i]), ) end - for (i, start) in enumerate(model.qp_data.mult_g) + for (i, start) in enumerate(model.quad_data.qp.mult_g) inner.mult_g[i] = _dual_start(model, start, -1) end - offset = length(model.qp_data.mult_g) + offset = length(model.quad_data.qp.mult_g) if model.nlp_dual_start === nothing inner.mult_g[(offset+1):end] .= 0.0 # First there is VectorNonlinearOracle... @@ -1570,15 +1628,9 @@ end function _manually_evaluated_primal_status(model::Optimizer) x, g = model.inner.x, model.inner.g x_L, x_U = model.variables.lower, model.variables.upper - g_L, g_U = copy(model.qp_data.g_L), copy(model.qp_data.g_U) - for (_, cache) in model.vector_nonlinear_oracle_constraints - append!(g_L, cache.set.l) - append!(g_U, cache.set.u) - end - for bound in model.nlp_data.constraint_bounds - push!(g_L, bound.lower) - push!(g_U, bound.upper) - end + bounds = MOI.Nonlinear._constraint_bounds(model.evaluator) + g_L = Float64[b.lower for b in bounds] + g_U = Float64[b.upper for b in bounds] m, n = length(g_L), length(x) # 1e-8 is the default tolerance tol = get(model.options, "tol", 1e-8) @@ -1667,7 +1719,7 @@ function row( model::Optimizer, ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction}, ) - offset = length(model.qp_data) + offset = length(model.quad_data) for (_, s) in model.vector_nonlinear_oracle_constraints offset += s.set.output_dimension end @@ -1770,7 +1822,7 @@ end function MOI.get(model::Optimizer, attr::MOI.NLPBlockDual) MOI.check_result_index_bounds(model, attr) s = -_dual_multiplier(model) - return s .* model.inner.mult_g[(length(model.qp_data)+1):end] + return s .* model.inner.mult_g[(length(model.quad_data)+1):end] end ### Ipopt.CallbackFunction From 44f290e2626bea9407300fd8fcb1fa41facf812a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 15 Aug 2026 14:21:15 +0000 Subject: [PATCH 2/7] MOI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 659c538..e31b020 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: run: | using Pkg Pkg.add([ - PackageSpec(name="MathOptInterface", rev="bl/qp_block_data"), + PackageSpec(name="MathOptInterface", rev="bl/model_with_quad"), ]) - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 From 632b4f9bb3c31a18589ab2e3c81dfae7f35ef133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 15 Aug 2026 15:26:24 +0000 Subject: [PATCH 3/7] Set the quadratic objective with Nonlinear.set_objective MOI.Nonlinear.ModelWithQuad no longer implements MOI.set for the objective; Nonlinear.set_objective is the single way to set it. --- ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl b/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl index fdca217..5b0d1e5 100644 --- a/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl +++ b/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl @@ -1083,7 +1083,7 @@ function MOI.set( }, } # This also clears any objective of the inner nonlinear model. - MOI.set(model.quad_data, attr, func) + MOI.Nonlinear.set_objective(model.quad_data, func) model.inner = nothing return end From fc5a8a6a289a6b9652ad6b31e724609dfb233da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 15 Aug 2026 16:34:41 +0000 Subject: [PATCH 4/7] Merge the variables, parameters and nonlinear model into one field MOI.Nonlinear.ModelWithQuad now owns the variables (with indices guaranteed to be 1:n) and the parameters of the model, so the variables, parameters, quad_data and nlp_model fields collapse into a single model field that most of the MOI API forwards to. The parameter convention is back to the simple _PARAMETER_OFFSET test, now defined in MOI.Nonlinear, and the parameter values are stored once, in the inner nonlinear model, aliased by the QP block: the per-solve parameter sync is gone. A uses_nlp_block flag replaces the nlp_model !== nothing test to tell the legacy MOI.NLPBlock API apart, because optimize! overwrites nlp_data; the NLPBlock is rebuilt on every setup unless that flag is set, so that a stale objective cannot survive an objective switch. --- ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl | 225 ++++++++------------ 1 file changed, 93 insertions(+), 132 deletions(-) diff --git a/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl b/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl index 5b0d1e5..ac64332 100644 --- a/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl +++ b/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl @@ -3,9 +3,9 @@ # 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. -const _PARAMETER_OFFSET = 0x00f0000000000000 +const _PARAMETER_OFFSET = MOI.Nonlinear._PARAMETER_OFFSET -_is_parameter(x::MOI.VariableIndex) = x.value >= _PARAMETER_OFFSET +const _is_parameter = MOI.Nonlinear._is_parameter _is_parameter(term::MOI.ScalarAffineTerm) = _is_parameter(term.variable) @@ -41,20 +41,18 @@ mutable struct Optimizer <: MOI.AbstractOptimizer options::Dict{String,Any} solve_time::Float64 sense::MOI.OptimizationSense - parameters::Dict{MOI.VariableIndex,MOI.Nonlinear.ParameterIndex} - variables::MOI.Utilities.VariablesContainer{Float64} + model::MOI.Nonlinear.ModelWithQuad{Float64,MOI.Nonlinear.Model} list_of_variable_indices::Vector{MOI.VariableIndex} variable_primal_start::Vector{Union{Nothing,Float64}} mult_x_L::Vector{Union{Nothing,Float64}} mult_x_U::Vector{Union{Nothing,Float64}} nlp_data::MOI.NLPBlockData + # Whether `nlp_data` was set through the legacy `MOI.NLPBlock` API, in + # which case it must not be rebuilt from the inner nonlinear model. + uses_nlp_block::Bool nlp_dual_start::Union{Nothing,Vector{Float64}} mult_g_nlp::Dict{MOI.Nonlinear.ConstraintIndex,Float64} - quad_data::MOI.Nonlinear.ModelWithQuad{Float64,MOI.Nonlinear.Model} - # `=== quad_data.inner` once the new nonlinear API is in use, `nothing` - # otherwise. See `_init_nlp_model`. - nlp_model::Union{Nothing,MOI.Nonlinear.Model} - # The evaluator of `quad_data`, rebuilt in `_setup_model`. + # The evaluator of `model`, rebuilt in `_setup_model`. evaluator::Union{Nothing,MOI.Nonlinear.EvaluatorWithQuad{Float64}} callback::Union{Nothing,Function} barrier_iterations::Int @@ -76,17 +74,15 @@ mutable struct Optimizer <: MOI.AbstractOptimizer Dict{String,Any}(), NaN, MOI.FEASIBILITY_SENSE, - Dict{MOI.VariableIndex,Float64}(), - MOI.Utilities.VariablesContainer{Float64}(), + MOI.Nonlinear.ModelWithQuad(MOI.Nonlinear.Model()), MOI.VariableIndex[], Union{Nothing,Float64}[], Union{Nothing,Float64}[], Union{Nothing,Float64}[], MOI.NLPBlockData([], _EmptyNLPEvaluator(), false), + false, nothing, Dict{MOI.Nonlinear.ConstraintIndex,Float64}(), - MOI.Nonlinear.ModelWithQuad(MOI.Nonlinear.Model()), - nothing, nothing, nothing, 0, @@ -129,17 +125,15 @@ function MOI.empty!(model::Optimizer) # SKIP: model.options model.solve_time = 0.0 model.sense = MOI.FEASIBILITY_SENSE - empty!(model.parameters) - MOI.empty!(model.variables) + model.model = MOI.Nonlinear.ModelWithQuad(MOI.Nonlinear.Model()) empty!(model.list_of_variable_indices) empty!(model.variable_primal_start) empty!(model.mult_x_L) empty!(model.mult_x_U) model.nlp_data = MOI.NLPBlockData([], _EmptyNLPEvaluator(), false) + model.uses_nlp_block = false model.nlp_dual_start = nothing empty!(model.mult_g_nlp) - model.quad_data = MOI.Nonlinear.ModelWithQuad(MOI.Nonlinear.Model()) - model.nlp_model = nothing model.evaluator = nothing model.callback = nothing model.barrier_iterations = 0 @@ -153,7 +147,7 @@ function MOI.empty!(model::Optimizer) end function MOI.is_empty(model::Optimizer) - return MOI.is_empty(model.variables) && + return MOI.is_empty(model.model.variables) && isempty(model.variable_primal_start) && isempty(model.mult_x_L) && isempty(model.mult_x_U) && @@ -177,12 +171,11 @@ function MOI.supports_add_constrained_variable( return true end -function _init_nlp_model(model) - if model.nlp_model === nothing - if !(model.nlp_data.evaluator isa _EmptyNLPEvaluator) - error("Cannot mix the new and legacy nonlinear APIs") - end - model.nlp_model = model.quad_data.inner +# The nonlinear model is `model.model.inner` and always exists; this guard +# only rejects mixing it with the legacy `MOI.NLPBlock` API. +function _check_no_nlp_block(model) + if model.uses_nlp_block + error("Cannot mix the new and legacy nonlinear APIs") end return end @@ -192,17 +185,9 @@ function MOI.add_constrained_variable( set::MOI.Parameter{Float64}, ) model.inner = nothing - _init_nlp_model(model) - p = MOI.VariableIndex(_PARAMETER_OFFSET + length(model.parameters)) + _check_no_nlp_block(model) + p, ci = MOI.add_constrained_variable(model.model, set) push!(model.list_of_variable_indices, p) - model.parameters[p] = - MOI.Nonlinear.add_parameter(model.nlp_model, set.value) - # `QPBlockData` treats a variable as a parameter if and only if its index - # is a key of `parameters`, so the parameter must be registered before - # any structure query. The value is re-synced in `copy_parameters` before - # every solve. - model.quad_data.qp.parameters[p.value] = set.value - ci = MOI.ConstraintIndex{MOI.VariableIndex,typeof(set)}(p.value) return p, ci end @@ -210,33 +195,23 @@ function MOI.is_valid( model::Optimizer, ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{Float64}}, ) - return haskey(model.parameters, MOI.VariableIndex(ci.value)) + return MOI.is_valid(model.model, ci) end function MOI.get( model::Optimizer, - ::MOI.ListOfConstraintIndices{F,S}, + attr::Union{MOI.ListOfConstraintIndices{F,S},MOI.NumberOfConstraints{F,S}}, ) where {F<:MOI.VariableIndex,S<:MOI.Parameter{Float64}} - ret = [MOI.ConstraintIndex{F,S}(p.value) for p in keys(model.parameters)] - sort!(ret; by = x -> x.value) - return ret -end - -function MOI.get( - model::Optimizer, - ::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.Parameter{Float64}}, -) - return length(model.parameters) + return MOI.get(model.model, attr) end function MOI.set( model::Optimizer, - ::MOI.ConstraintSet, + attr::MOI.ConstraintSet, ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{Float64}}, set::MOI.Parameter{Float64}, ) - p = model.parameters[MOI.VariableIndex(ci.value)] - model.nlp_model[p] = set.value + MOI.set(model.model, attr, ci, set) return end @@ -244,7 +219,7 @@ _replace_parameters(model::Optimizer, f) = f function _replace_parameters(model::Optimizer, f::MOI.VariableIndex) if _is_parameter(f) - return model.parameters[f] + return MOI.Nonlinear.ParameterIndex(f.value - _PARAMETER_OFFSET) end return f end @@ -290,8 +265,6 @@ end ### MOI.ListOfConstraintTypesPresent -_add_scalar_nonlinear_constraints(ret, ::Nothing) = nothing - function _add_scalar_nonlinear_constraints(ret, nlp_model::MOI.Nonlinear.Model) for v in values(nlp_model.constraints) F, S = MOI.ScalarNonlinearFunction, typeof(v.set) @@ -303,13 +276,13 @@ function _add_scalar_nonlinear_constraints(ret, nlp_model::MOI.Nonlinear.Model) end function MOI.get(model::Optimizer, attr::MOI.ListOfConstraintTypesPresent) - ret = MOI.get(model.variables, attr) - append!(ret, MOI.get(model.quad_data, attr)) - _add_scalar_nonlinear_constraints(ret, model.nlp_model) + ret = MOI.get(model.model.variables, attr) + append!(ret, MOI.get(model.model, attr)) + _add_scalar_nonlinear_constraints(ret, model.model.inner) if !isempty(model.vector_nonlinear_oracle_constraints) push!(ret, (MOI.VectorOfVariables, MOI.VectorNonlinearOracle{Float64})) end - if !isempty(model.parameters) + if !isempty(model.model.qp.parameters) push!(ret, (MOI.VariableIndex, MOI.Parameter{Float64})) end return ret @@ -387,16 +360,13 @@ function MOI.add_variable(model::Optimizer) push!(model.mult_x_L, nothing) push!(model.mult_x_U, nothing) model.inner = nothing - x = MOI.add_variable(model.variables) + x = MOI.add_variable(model.model) push!(model.list_of_variable_indices, x) return x end function MOI.is_valid(model::Optimizer, x::MOI.VariableIndex) - if _is_parameter(x) - return haskey(model.parameters, x) - end - return MOI.is_valid(model.variables, x) + return MOI.is_valid(model.model, x) end function MOI.get(model::Optimizer, ::MOI.ListOfVariableIndices) @@ -411,7 +381,7 @@ function MOI.is_valid( model::Optimizer, ci::MOI.ConstraintIndex{MOI.VariableIndex,<:_SETS}, ) - return MOI.is_valid(model.variables, ci) + return MOI.is_valid(model.model.variables, ci) end function MOI.get( @@ -421,7 +391,7 @@ function MOI.get( MOI.ListOfConstraintIndices{MOI.VariableIndex,<:_SETS}, }, ) - return MOI.get(model.variables, attr) + return MOI.get(model.model.variables, attr) end function MOI.get( @@ -429,11 +399,11 @@ function MOI.get( attr::Union{MOI.ConstraintFunction,MOI.ConstraintSet}, c::MOI.ConstraintIndex{MOI.VariableIndex,<:_SETS}, ) - return MOI.get(model.variables, attr, c) + return MOI.get(model.model.variables, attr, c) end function MOI.add_constraint(model::Optimizer, x::MOI.VariableIndex, set::_SETS) - index = MOI.add_constraint(model.variables, x, set) + index = MOI.add_constraint(model.model.variables, x, set) model.inner = nothing return index end @@ -444,7 +414,7 @@ function MOI.set( ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, set::S, ) where {S<:_SETS} - MOI.set(model.variables, MOI.ConstraintSet(), ci, set) + MOI.set(model.model.variables, MOI.ConstraintSet(), ci, set) model.needs_new_inner = true return end @@ -453,7 +423,7 @@ function MOI.delete( model::Optimizer, ci::MOI.ConstraintIndex{MOI.VariableIndex,<:_SETS}, ) - MOI.delete(model.variables, ci) + MOI.delete(model.model.variables, ci) model.inner = nothing return end @@ -469,7 +439,7 @@ function MOI.is_valid( MOI.ScalarQuadraticFunction{Float64}, }, } - return MOI.is_valid(model.quad_data, ci) + return MOI.is_valid(model.model, ci) end function MOI.add_constraint( @@ -480,7 +450,7 @@ function MOI.add_constraint( }, set::_SETS, ) - index = MOI.add_constraint(model.quad_data, func, set) + index = MOI.add_constraint(model.model, func, set) model.inner = nothing return index end @@ -495,7 +465,7 @@ function MOI.get( }, S<:_SETS, } - return MOI.get(model.quad_data, attr) + return MOI.get(model.model, attr) end function MOI.get( @@ -508,7 +478,7 @@ function MOI.get( MOI.ScalarQuadraticFunction{Float64}, }, } - return MOI.get(model.quad_data, attr, c) + return MOI.get(model.model, attr, c) end function MOI.set( @@ -523,7 +493,7 @@ function MOI.set( }, S<:_SETS, } - MOI.set(model.quad_data, MOI.ConstraintSet(), ci, set) + MOI.set(model.model, MOI.ConstraintSet(), ci, set) model.needs_new_inner = true return end @@ -551,7 +521,7 @@ function MOI.get( MOI.ScalarQuadraticFunction{Float64}, }, } - return MOI.get(model.quad_data, attr, c) + return MOI.get(model.model, attr, c) end function MOI.set( @@ -566,7 +536,7 @@ function MOI.set( }, } MOI.throw_if_not_valid(model, ci) - MOI.set(model.quad_data, attr, ci, value) + MOI.set(model.model, attr, ci, value) # No need to reset model.inner, because this gets handled in optimize!. return end @@ -577,11 +547,8 @@ function MOI.is_valid( model::Optimizer, ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,<:_SETS}, ) - if model.nlp_model === nothing - return false - end index = MOI.Nonlinear.ConstraintIndex(ci.value) - return MOI.is_valid(model.nlp_model, index) + return MOI.is_valid(model.model.inner, index) end function MOI.get( @@ -589,10 +556,7 @@ function MOI.get( attr::MOI.ListOfConstraintIndices{F,S}, ) where {F<:MOI.ScalarNonlinearFunction,S<:_SETS} ret = MOI.ConstraintIndex{F,S}[] - if model.nlp_model === nothing - return ret - end - for (k, v) in model.nlp_model.constraints + for (k, v) in model.model.inner.constraints if v.set isa S push!(ret, MOI.ConstraintIndex{F,S}(k.value)) end @@ -604,10 +568,7 @@ function MOI.get( model::Optimizer, attr::MOI.NumberOfConstraints{F,S}, ) where {F<:MOI.ScalarNonlinearFunction,S<:_SETS} - if model.nlp_model === nothing - return 0 - end - return count(v.set isa S for v in values(model.nlp_model.constraints)) + return count(v.set isa S for v in values(model.model.inner.constraints)) end function MOI.add_constraint( @@ -615,11 +576,11 @@ function MOI.add_constraint( f::MOI.ScalarNonlinearFunction, s::_SETS, ) - _init_nlp_model(model) - if !isempty(model.parameters) + _check_no_nlp_block(model) + if !isempty(model.model.qp.parameters) _replace_parameters(model, f) end - index = MOI.Nonlinear.add_constraint(model.nlp_model, f, s) + index = MOI.Nonlinear.add_constraint(model.model.inner, f, s) model.inner = nothing return MOI.ConstraintIndex{typeof(f),typeof(s)}(index.value) end @@ -636,13 +597,13 @@ function MOI.set( attr::MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}, func::MOI.ScalarNonlinearFunction, ) - _init_nlp_model(model) - if !isempty(model.parameters) + _check_no_nlp_block(model) + if !isempty(model.model.qp.parameters) _replace_parameters(model, func) end - # Set through `quad_data` so that the objective sink is updated and any + # Set through the layer so that the objective sink is updated and any # affine or quadratic objective is cleared. - MOI.Nonlinear.set_objective(model.quad_data, func) + MOI.Nonlinear.set_objective(model.model, func) model.inner = nothing return end @@ -654,7 +615,7 @@ function MOI.get( ) MOI.throw_if_not_valid(model, ci) index = MOI.Nonlinear.ConstraintIndex(ci.value) - return model.nlp_model[index].set + return model.model.inner[index].set end function MOI.set( @@ -665,8 +626,8 @@ function MOI.set( ) where {S<:_SETS} MOI.throw_if_not_valid(model, ci) index = MOI.Nonlinear.ConstraintIndex(ci.value) - func = model.nlp_model[index].expression - model.nlp_model.constraints[index] = MOI.Nonlinear.Constraint(func, set) + func = model.model.inner[index].expression + model.model.inner.constraints[index] = MOI.Nonlinear.Constraint(func, set) model.needs_new_inner = true return end @@ -757,7 +718,7 @@ function row( model::Optimizer, ci::MOI.ConstraintIndex{F,S}, ) where {F<:MOI.VectorOfVariables,S<:MOI.VectorNonlinearOracle{Float64}} - offset = length(model.quad_data) + offset = length(model.model) for i in 1:(ci.value-1) _, s = model.vector_nonlinear_oracle_constraints[i] offset += s.set.output_dimension @@ -843,9 +804,9 @@ end MOI.supports(model::Optimizer, ::MOI.UserDefinedFunction) = true function MOI.set(model::Optimizer, attr::MOI.UserDefinedFunction, args) - _init_nlp_model(model) + _check_no_nlp_block(model) MOI.Nonlinear.register_operator( - model.nlp_model, + model.model.inner, attr.name, attr.arity, args..., @@ -856,8 +817,7 @@ end ### ListOfSupportedNonlinearOperators function MOI.get(model::Optimizer, attr::MOI.ListOfSupportedNonlinearOperators) - _init_nlp_model(model) - return MOI.get(model.nlp_model, attr) + return MOI.get(model.model, attr) end ### MOI.VariablePrimalStart @@ -1012,10 +972,11 @@ MOI.supports(::Optimizer, ::MOI.NLPBlock) = true MOI.get(model::Optimizer, ::MOI.NLPBlock) = model.nlp_data function MOI.set(model::Optimizer, ::MOI.NLPBlock, nlp_data::MOI.NLPBlockData) - if model.nlp_model !== nothing + if !MOI.is_empty(model.model.inner) error("Cannot mix the new and legacy nonlinear APIs") end model.nlp_data = nlp_data + model.uses_nlp_block = !(nlp_data.evaluator isa _EmptyNLPEvaluator) model.inner = nothing return end @@ -1039,10 +1000,10 @@ MOI.get(model::Optimizer, ::MOI.ObjectiveSense) = model.sense ### ObjectiveFunction function MOI.get(model::Optimizer, attr::MOI.ObjectiveFunctionType) - if model.nlp_model !== nothing && model.nlp_model.objective !== nothing + if model.model.inner.objective !== nothing return MOI.ScalarNonlinearFunction end - return MOI.get(model.quad_data, attr) + return MOI.get(model.model, attr) end function MOI.supports( @@ -1068,7 +1029,7 @@ function MOI.get( MOI.ScalarQuadraticFunction{Float64}, }, } - return convert(F, MOI.get(model.quad_data, attr)) + return convert(F, MOI.get(model.model, attr)) end function MOI.set( @@ -1083,7 +1044,7 @@ function MOI.set( }, } # This also clears any objective of the inner nonlinear model. - MOI.Nonlinear.set_objective(model.quad_data, func) + MOI.Nonlinear.set_objective(model.model, func) model.inner = nothing return end @@ -1386,9 +1347,9 @@ function _setup_inner(model::Optimizer)::Ipopt.IpoptProblem end has_hessian = model.hessian_sparsity !== nothing model.inner = Ipopt.CreateIpoptProblem( - length(model.variables.lower), - model.variables.lower, - model.variables.upper, + length(model.model.variables.lower), + model.model.variables.lower, + model.model.variables.upper, length(g_L), g_L, g_U, @@ -1446,20 +1407,20 @@ function _setup_model(model::Optimizer) model.invalid_model = true return end - vars = MOI.get(model.variables, MOI.ListOfVariableIndices()) - if model.nlp_model !== nothing + # Rebuild even when the inner model is empty: a previous solve may have + # left a stale `nlp_data` (for example, a nonlinear objective replaced by + # a quadratic one). + if !model.uses_nlp_block + vars = MOI.get(model.model.variables, MOI.ListOfVariableIndices()) model.nlp_data = MOI.NLPBlockData( - MOI.Nonlinear.Evaluator(model.nlp_model, model.ad_backend, vars), + MOI.Nonlinear.Evaluator(model.model.inner, model.ad_backend, vars), ) end - model.evaluator = MOI.Nonlinear.EvaluatorWithQuad( - model.quad_data, - _OracleNLPEvaluator(model), - vars, - ) + model.evaluator = + MOI.Nonlinear.EvaluatorWithQuad(model.model, _OracleNLPEvaluator(model)) has_quadratic_constraints = any( isequal(MOI.Nonlinear._kFunctionTypeScalarQuadratic), - model.quad_data.qp.function_type, + model.model.qp.function_type, ) has_nlp_constraints = !isempty(model.nlp_data.constraint_bounds) || @@ -1493,12 +1454,6 @@ function MOI.optimize!(model::Optimizer) return end inner = _setup_inner(model) - if model.nlp_model !== nothing - empty!(model.quad_data.qp.parameters) - for (p, index) in model.parameters - model.quad_data.qp.parameters[p.value] = model.nlp_model[index] - end - end # The default print level is `5` Ipopt.AddIpoptIntOption(inner, "print_level", model.silent ? 0 : 5) # Other misc options that over-ride the ones set above. @@ -1522,13 +1477,17 @@ function MOI.optimize!(model::Optimizer) for i in 1:length(model.variable_primal_start) inner.x[i] = something( model.variable_primal_start[i], - clamp(0.0, model.variables.lower[i], model.variables.upper[i]), + clamp( + 0.0, + model.model.variables.lower[i], + model.model.variables.upper[i], + ), ) end - for (i, start) in enumerate(model.quad_data.qp.mult_g) + for (i, start) in enumerate(model.model.qp.mult_g) inner.mult_g[i] = _dual_start(model, start, -1) end - offset = length(model.quad_data.qp.mult_g) + offset = length(model.model.qp.mult_g) if model.nlp_dual_start === nothing inner.mult_g[(offset+1):end] .= 0.0 # First there is VectorNonlinearOracle... @@ -1627,7 +1586,7 @@ end function _manually_evaluated_primal_status(model::Optimizer) x, g = model.inner.x, model.inner.g - x_L, x_U = model.variables.lower, model.variables.upper + x_L, x_U = model.model.variables.lower, model.model.variables.upper bounds = MOI.Nonlinear._constraint_bounds(model.evaluator) g_L = Float64[b.lower for b in bounds] g_U = Float64[b.upper for b in bounds] @@ -1695,8 +1654,10 @@ function MOI.get( MOI.check_result_index_bounds(model, attr) MOI.throw_if_not_valid(model, vi) if _is_parameter(vi) - p = model.parameters[vi] - return model.nlp_model[p] + ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{Float64}}( + vi.value, + ) + return MOI.get(model.model, MOI.ConstraintSet(), ci).value end return model.inner.x[Ipopt.column(vi)] end @@ -1719,7 +1680,7 @@ function row( model::Optimizer, ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction}, ) - offset = length(model.quad_data) + offset = length(model.model) for (_, s) in model.vector_nonlinear_oracle_constraints offset += s.set.output_dimension end @@ -1822,7 +1783,7 @@ end function MOI.get(model::Optimizer, attr::MOI.NLPBlockDual) MOI.check_result_index_bounds(model, attr) s = -_dual_multiplier(model) - return s .* model.inner.mult_g[(length(model.quad_data)+1):end] + return s .* model.inner.mult_g[(length(model.model)+1):end] end ### Ipopt.CallbackFunction From d2e6f76f02061321e1b5f9c206e4c6b3dc18eaab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 15 Aug 2026 16:53:41 +0000 Subject: [PATCH 5/7] Use the _is_parameter of MOI.Nonlinear directly Defining the term methods on a local alias of the function pirated it; the term methods are in MOI.Nonlinear now. --- ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl | 28 ++++++++------------- 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl b/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl index ac64332..484f609 100644 --- a/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl +++ b/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl @@ -3,16 +3,6 @@ # 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. -const _PARAMETER_OFFSET = MOI.Nonlinear._PARAMETER_OFFSET - -const _is_parameter = MOI.Nonlinear._is_parameter - -_is_parameter(term::MOI.ScalarAffineTerm) = _is_parameter(term.variable) - -function _is_parameter(term::MOI.ScalarQuadraticTerm) - return _is_parameter(term.variable_1) || _is_parameter(term.variable_2) -end - mutable struct _VectorNonlinearOracleCache set::MOI.VectorNonlinearOracle{Float64} x::Vector{Float64} @@ -218,14 +208,16 @@ end _replace_parameters(model::Optimizer, f) = f function _replace_parameters(model::Optimizer, f::MOI.VariableIndex) - if _is_parameter(f) - return MOI.Nonlinear.ParameterIndex(f.value - _PARAMETER_OFFSET) + if MOI.Nonlinear._is_parameter(f) + return MOI.Nonlinear.ParameterIndex( + f.value - MOI.Nonlinear._PARAMETER_OFFSET, + ) end return f end function _replace_parameters(model::Optimizer, f::MOI.ScalarAffineFunction) - if any(_is_parameter, f.terms) + if any(MOI.Nonlinear._is_parameter, f.terms) g = convert(MOI.ScalarNonlinearFunction, f) return _replace_parameters(model, g) end @@ -233,8 +225,8 @@ function _replace_parameters(model::Optimizer, f::MOI.ScalarAffineFunction) end function _replace_parameters(model::Optimizer, f::MOI.ScalarQuadraticFunction) - if any(_is_parameter, f.affine_terms) || - any(_is_parameter, f.quadratic_terms) + if any(MOI.Nonlinear._is_parameter, f.affine_terms) || + any(MOI.Nonlinear._is_parameter, f.quadratic_terms) g = convert(MOI.ScalarNonlinearFunction, f) return _replace_parameters(model, g) end @@ -835,7 +827,7 @@ function MOI.get( attr::MOI.VariablePrimalStart, vi::MOI.VariableIndex, ) - if _is_parameter(vi) + if MOI.Nonlinear._is_parameter(vi) throw(MOI.GetAttributeNotAllowed(attr, "Variable is a Parameter")) end MOI.throw_if_not_valid(model, vi) @@ -848,7 +840,7 @@ function MOI.set( vi::MOI.VariableIndex, value::Union{Real,Nothing}, ) - if _is_parameter(vi) + if MOI.Nonlinear._is_parameter(vi) throw(MOI.SetAttributeNotAllowed(attr, "Variable is a Parameter")) end MOI.throw_if_not_valid(model, vi) @@ -1653,7 +1645,7 @@ function MOI.get( ) MOI.check_result_index_bounds(model, attr) MOI.throw_if_not_valid(model, vi) - if _is_parameter(vi) + if MOI.Nonlinear._is_parameter(vi) ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{Float64}}( vi.value, ) From 72c26621e3aa7a84e5d621282aeb8e798e21b573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 15 Aug 2026 19:30:33 +0200 Subject: [PATCH 6/7] Fix --- ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl | 45 +-------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl b/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl index 484f609..c30d8d4 100644 --- a/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl +++ b/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl @@ -205,41 +205,6 @@ function MOI.set( return end -_replace_parameters(model::Optimizer, f) = f - -function _replace_parameters(model::Optimizer, f::MOI.VariableIndex) - if MOI.Nonlinear._is_parameter(f) - return MOI.Nonlinear.ParameterIndex( - f.value - MOI.Nonlinear._PARAMETER_OFFSET, - ) - end - return f -end - -function _replace_parameters(model::Optimizer, f::MOI.ScalarAffineFunction) - if any(MOI.Nonlinear._is_parameter, f.terms) - g = convert(MOI.ScalarNonlinearFunction, f) - return _replace_parameters(model, g) - end - return f -end - -function _replace_parameters(model::Optimizer, f::MOI.ScalarQuadraticFunction) - if any(MOI.Nonlinear._is_parameter, f.affine_terms) || - any(MOI.Nonlinear._is_parameter, f.quadratic_terms) - g = convert(MOI.ScalarNonlinearFunction, f) - return _replace_parameters(model, g) - end - return f -end - -function _replace_parameters(model::Optimizer, f::MOI.ScalarNonlinearFunction) - for (i, arg) in enumerate(f.args) - f.args[i] = _replace_parameters(model, arg) - end - return f -end - function MOI.supports_constraint( ::Optimizer, ::Type{ @@ -569,10 +534,7 @@ function MOI.add_constraint( s::_SETS, ) _check_no_nlp_block(model) - if !isempty(model.model.qp.parameters) - _replace_parameters(model, f) - end - index = MOI.Nonlinear.add_constraint(model.model.inner, f, s) + index = MOI.Nonlinear.add_constraint(model.model, f, s) model.inner = nothing return MOI.ConstraintIndex{typeof(f),typeof(s)}(index.value) end @@ -590,11 +552,6 @@ function MOI.set( func::MOI.ScalarNonlinearFunction, ) _check_no_nlp_block(model) - if !isempty(model.model.qp.parameters) - _replace_parameters(model, func) - end - # Set through the layer so that the objective sink is updated and any - # affine or quadratic objective is cleared. MOI.Nonlinear.set_objective(model.model, func) model.inner = nothing return From c093b0050bade03f4ceb52e1e8c5c4fab4df0634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 17 Aug 2026 09:42:37 +0200 Subject: [PATCH 7/7] Get rid of list_of_variable_indices --- ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl b/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl index c30d8d4..1f9c152 100644 --- a/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl +++ b/ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl @@ -32,7 +32,6 @@ mutable struct Optimizer <: MOI.AbstractOptimizer solve_time::Float64 sense::MOI.OptimizationSense model::MOI.Nonlinear.ModelWithQuad{Float64,MOI.Nonlinear.Model} - list_of_variable_indices::Vector{MOI.VariableIndex} variable_primal_start::Vector{Union{Nothing,Float64}} mult_x_L::Vector{Union{Nothing,Float64}} mult_x_U::Vector{Union{Nothing,Float64}} @@ -65,7 +64,6 @@ mutable struct Optimizer <: MOI.AbstractOptimizer NaN, MOI.FEASIBILITY_SENSE, MOI.Nonlinear.ModelWithQuad(MOI.Nonlinear.Model()), - MOI.VariableIndex[], Union{Nothing,Float64}[], Union{Nothing,Float64}[], Union{Nothing,Float64}[], @@ -116,7 +114,6 @@ function MOI.empty!(model::Optimizer) model.solve_time = 0.0 model.sense = MOI.FEASIBILITY_SENSE model.model = MOI.Nonlinear.ModelWithQuad(MOI.Nonlinear.Model()) - empty!(model.list_of_variable_indices) empty!(model.variable_primal_start) empty!(model.mult_x_L) empty!(model.mult_x_U) @@ -177,7 +174,6 @@ function MOI.add_constrained_variable( model.inner = nothing _check_no_nlp_block(model) p, ci = MOI.add_constrained_variable(model.model, set) - push!(model.list_of_variable_indices, p) return p, ci end @@ -318,7 +314,6 @@ function MOI.add_variable(model::Optimizer) push!(model.mult_x_U, nothing) model.inner = nothing x = MOI.add_variable(model.model) - push!(model.list_of_variable_indices, x) return x end @@ -326,12 +321,12 @@ function MOI.is_valid(model::Optimizer, x::MOI.VariableIndex) return MOI.is_valid(model.model, x) end -function MOI.get(model::Optimizer, ::MOI.ListOfVariableIndices) - return model.list_of_variable_indices +function MOI.get(model::Optimizer, attr::MOI.ListOfVariableIndices) + return MOI.get(model.model, attr) end -function MOI.get(model::Optimizer, ::MOI.NumberOfVariables) - return length(model.list_of_variable_indices) +function MOI.get(model::Optimizer, attr::MOI.NumberOfVariables) + return MOI.get(model.model, attr) end function MOI.is_valid(