From d24d14bf4f40b4eaeb4e05d35ce922516ec309eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 13 Aug 2026 08:24:09 +0000 Subject: [PATCH 01/12] Use MOI.Nonlinear.QPBlockData Delete the local QPBlockData (MOI_utils.jl) in favor of the copy that moved to MathOptInterface, which includes the Jacobian and Hessian product callbacks this copy had added. Two adaptations: - MOI's QPBlockData treats a variable as a parameter if and only if its index is a key of the parameters dictionary, instead of the index-offset convention, so parameters are registered when the constrained variable is added (the value is still re-synced before every solve). - eval_constraint_jacobian and eval_hessian_lagrangian now return the number of entries written instead of a one-indexed cursor, so the callers drop the offset adjustment. --- ext/MadNLPMOI/MOI_utils.jl | 691 ----------------------------------- ext/MadNLPMOI/MOI_wrapper.jl | 13 +- ext/MadNLPMOI/MadNLPMOI.jl | 1 - 3 files changed, 10 insertions(+), 695 deletions(-) delete mode 100644 ext/MadNLPMOI/MOI_utils.jl diff --git a/ext/MadNLPMOI/MOI_utils.jl b/ext/MadNLPMOI/MOI_utils.jl deleted file mode 100644 index e3ef9d07f..000000000 --- a/ext/MadNLPMOI/MOI_utils.jl +++ /dev/null @@ -1,691 +0,0 @@ -# Copyright (c) 2013: Iain Dunning, Miles Lubin, and contributors -# 2025: Modified for MadNLP.jl and UnoSolver.jl by Alexis Montoison -# -# 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. - -# !!! warning -# -# The contents of this file are experimental. -# -# Until this message is removed, breaking changes to the functions and -# types, including their deletion, may be introduced in any minor or patch -# release of MadNLP. - -@enum( - _FunctionType, - _kFunctionTypeVariableIndex, - _kFunctionTypeScalarAffine, - _kFunctionTypeScalarQuadratic, -) - -function _function_type_to_set(::Type{T}, k::_FunctionType) where {T} - if k == _kFunctionTypeVariableIndex - return MOI.VariableIndex - elseif k == _kFunctionTypeScalarAffine - return MOI.ScalarAffineFunction{T} - else - @assert k == _kFunctionTypeScalarQuadratic - return MOI.ScalarQuadraticFunction{T} - end -end - -_function_info(::MOI.VariableIndex) = _kFunctionTypeVariableIndex -_function_info(::MOI.ScalarAffineFunction) = _kFunctionTypeScalarAffine -_function_info(::MOI.ScalarQuadraticFunction) = _kFunctionTypeScalarQuadratic - -@enum( - _BoundType, - _kBoundTypeLessThan, - _kBoundTypeGreaterThan, - _kBoundTypeEqualTo, - _kBoundTypeInterval, -) - -_set_info(s::MOI.LessThan) = _kBoundTypeLessThan, -Inf, s.upper -_set_info(s::MOI.GreaterThan) = _kBoundTypeGreaterThan, s.lower, Inf -_set_info(s::MOI.EqualTo) = _kBoundTypeEqualTo, s.value, s.value -_set_info(s::MOI.Interval) = _kBoundTypeInterval, s.lower, s.upper - -function _bound_type_to_set(::Type{T}, k::_BoundType) where {T} - if k == _kBoundTypeEqualTo - return MOI.EqualTo{T} - elseif k == _kBoundTypeLessThan - return MOI.LessThan{T} - elseif k == _kBoundTypeGreaterThan - return MOI.GreaterThan{T} - else - @assert k == _kBoundTypeInterval - return MOI.Interval{T} - end -end - -mutable struct QPBlockData{T} - objective::Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}} - objective_function_type::_FunctionType - constraints::Vector{ - Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}, - } - g_L::Vector{T} - g_U::Vector{T} - mult_g::Vector{Union{Nothing,T}} - function_type::Vector{_FunctionType} - bound_type::Vector{_BoundType} - parameters::Dict{Int64,T} - - function QPBlockData{T}() where {T} - return new( - zero(MOI.ScalarQuadraticFunction{T}), - _kFunctionTypeScalarAffine, - Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}[], - T[], - T[], - Union{Nothing,T}[], - _FunctionType[], - _BoundType[], - Dict{Int64,T}(), - ) - end -end - -function _value(variable::MOI.VariableIndex, x::AbstractVector, p::Dict) - if _is_parameter(variable) - return p[variable.value] - else - return x[variable.value] - end -end - -function eval_function( - f::MOI.ScalarQuadraticFunction{T}, - x::AbstractVector{T}, - p::Dict{Int64,T}, -)::T where {T} - y = f.constant - for term in f.affine_terms - y += term.coefficient * _value(term.variable, x, p) - end - for term in f.quadratic_terms - v1 = _value(term.variable_1, x, p) - v2 = _value(term.variable_2, x, p) - if term.variable_1 == term.variable_2 - y += term.coefficient * v1 * v2 / 2 - else - y += term.coefficient * v1 * v2 - end - end - return y -end - -function eval_function( - f::MOI.ScalarAffineFunction{T}, - x::AbstractVector{T}, - p::Dict{Int64,T}, -)::T where {T} - y = f.constant - for term in f.terms - y += term.coefficient * _value(term.variable, x, p) - end - return y -end - -function eval_dense_gradient( - ∇f::AbstractVector{T}, - f::MOI.ScalarQuadraticFunction{T}, - x::AbstractVector{T}, - p::Dict{Int64,T}, -)::Nothing where {T} - for term in f.affine_terms - if !_is_parameter(term.variable) - ∇f[term.variable.value] += term.coefficient - end - end - for term in f.quadratic_terms - if !_is_parameter(term.variable_1) - v = _value(term.variable_2, x, p) - ∇f[term.variable_1.value] += term.coefficient * v - end - if term.variable_1 != term.variable_2 && !_is_parameter(term.variable_2) - v = _value(term.variable_1, x, p) - ∇f[term.variable_2.value] += term.coefficient * v - end - end - return -end - -function eval_dense_gradient( - ∇f::AbstractVector{T}, - f::MOI.ScalarAffineFunction{T}, - x::AbstractVector{T}, - p::Dict{Int64,T}, -)::Nothing where {T} - for term in f.terms - if !_is_parameter(term.variable) - ∇f[term.variable.value] += term.coefficient - end - end - return -end - -function append_sparse_gradient_structure!( - f::MOI.ScalarQuadraticFunction, - J, - row, -) - for term in f.affine_terms - if !_is_parameter(term.variable) - push!(J, (row, term.variable.value)) - end - end - for term in f.quadratic_terms - if !_is_parameter(term.variable_1) - push!(J, (row, term.variable_1.value)) - end - if term.variable_1 != term.variable_2 && !_is_parameter(term.variable_2) - push!(J, (row, term.variable_2.value)) - end - end - return -end - -function append_sparse_gradient_structure!(f::MOI.ScalarAffineFunction, J, row) - for term in f.terms - if !_is_parameter(term.variable) - push!(J, (row, term.variable.value)) - end - end - return -end - -function eval_sparse_gradient( - ∇f::AbstractVector{T}, - f::MOI.ScalarQuadraticFunction{T}, - x::AbstractVector{T}, - p::Dict{Int64,T}, -)::Int where {T} - i = 0 - for term in f.affine_terms - if !_is_parameter(term.variable) - i += 1 - ∇f[i] = term.coefficient - end - end - for term in f.quadratic_terms - if !_is_parameter(term.variable_1) - v = _value(term.variable_2, x, p) - i += 1 - ∇f[i] = term.coefficient * v - end - if term.variable_1 != term.variable_2 && !_is_parameter(term.variable_2) - v = _value(term.variable_1, x, p) - i += 1 - ∇f[i] = term.coefficient * v - end - end - return i -end - -function eval_sparse_gradient( - ∇f::AbstractVector{T}, - f::MOI.ScalarAffineFunction{T}, - x::AbstractVector{T}, - p::Dict{Int64,T}, -)::Int where {T} - i = 0 - for term in f.terms - if !_is_parameter(term.variable) - i += 1 - ∇f[i] = term.coefficient - end - end - return i -end - -function append_sparse_hessian_structure!(f::MOI.ScalarQuadraticFunction, H) - for term in f.quadratic_terms - if !_is_parameter(term.variable_1) && !_is_parameter(term.variable_2) - push!(H, (term.variable_1.value, term.variable_2.value)) - end - end - return -end - -append_sparse_hessian_structure!(::MOI.ScalarAffineFunction, H) = nothing - -function eval_sparse_hessian( - ∇²f::AbstractVector{T}, - f::MOI.ScalarQuadraticFunction{T}, - σ::T, -)::Int where {T} - i = 0 - for term in f.quadratic_terms - if !_is_parameter(term.variable_1) && !_is_parameter(term.variable_2) - i += 1 - ∇²f[i] = term.coefficient * σ - end - end - return i -end - -function eval_sparse_hessian( - ∇²f::AbstractVector{T}, - f::MOI.ScalarAffineFunction{T}, - σ::T, -)::Int where {T} - return 0 -end - -function eval_Jv_product( - f::MOI.ScalarAffineFunction{T}, - y::AbstractVector{T}, - x::AbstractVector{T}, - w::AbstractVector{T}, - p::Dict{Int64,T}, - i::Int, -)::Nothing where {T} - for term in f.terms - if !_is_parameter(term.variable) - y[i] += term.coefficient * w[term.variable.value] - end - end - return -end - -function eval_Jv_product( - f::MOI.ScalarQuadraticFunction{T}, - y::AbstractVector{T}, - x::AbstractVector{T}, - w::AbstractVector{T}, - p::Dict{Int64,T}, - i::Int, -)::Nothing where {T} - for term in f.quadratic_terms - if !_is_parameter(term.variable_1) - val = _value(term.variable_2, x, p) - y[i] += term.coefficient * val * w[term.variable_1.value] - end - if term.variable_1 != term.variable_2 && !_is_parameter(term.variable_2) - val = _value(term.variable_1, x, p) - y[i] += term.coefficient * val * w[term.variable_2.value] - end - end - for term in f.affine_terms - if !_is_parameter(term.variable) - y[i] += term.coefficient * w[term.variable.value] - end - end - return -end - -function eval_Jtv_product( - f::MOI.ScalarAffineFunction{T}, - y::AbstractVector{T}, - x::AbstractVector{T}, - w::AbstractVector{T}, - p::Dict{Int64,T}, - i::Int, -)::Nothing where {T} - wi = w[i] - for term in f.terms - if !_is_parameter(term.variable) - y[term.variable.value] += wi * term.coefficient - end - end - return -end - -function eval_Jtv_product( - f::MOI.ScalarQuadraticFunction{T}, - y::AbstractVector{T}, - x::AbstractVector{T}, - w::AbstractVector{T}, - p::Dict{Int64,T}, - i::Int, -)::Nothing where {T} - wi = w[i] - for term in f.quadratic_terms - if !_is_parameter(term.variable_1) - y[term.variable_1.value] += wi * term.coefficient * _value(term.variable_2, x, p) - end - if term.variable_1 != term.variable_2 && !_is_parameter(term.variable_2) - y[term.variable_2.value] += wi * term.coefficient * _value(term.variable_1, x, p) - end - end - for term in f.affine_terms - if !_is_parameter(term.variable) - y[term.variable.value] += wi * term.coefficient - end - end - return -end - -function eval_Hv_product( - f::MOI.ScalarAffineFunction{T}, - H::AbstractVector{T}, - x::AbstractVector{T}, - v::AbstractVector{T}, - α::T, -)::Nothing where {T} - return -end - -function eval_Hv_product( - f::MOI.ScalarQuadraticFunction{T}, - H::AbstractVector{T}, - x::AbstractVector{T}, - v::AbstractVector{T}, - α::T, -)::Nothing where {T} - for term in f.quadratic_terms - if !_is_parameter(term.variable_1) && !_is_parameter(term.variable_2) - H[term.variable_1.value] += α * term.coefficient * v[term.variable_2.value] - if term.variable_1 != term.variable_2 - H[term.variable_2.value] += α * term.coefficient * v[term.variable_1.value] - end - end - end - return -end - -Base.length(block::QPBlockData) = length(block.bound_type) - -function MOI.set( - block::QPBlockData{T}, - ::MOI.ObjectiveFunction{F}, - f::F, -) where {T,F<:Union{MOI.VariableIndex,MOI.ScalarAffineFunction{T}}} - block.objective = convert(MOI.ScalarAffineFunction{T}, f) - block.objective_function_type = _function_info(f) - return -end - -function MOI.set( - block::QPBlockData{T}, - ::MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}, - f::MOI.ScalarQuadraticFunction{T}, -) where {T} - block.objective = f - block.objective_function_type = _function_info(f) - return -end - -function MOI.get(block::QPBlockData{T}, ::MOI.ObjectiveFunctionType) where {T} - return _function_type_to_set(T, block.objective_function_type) -end - -function MOI.get(block::QPBlockData{T}, ::MOI.ObjectiveFunction{F}) where {T,F} - return convert(F, block.objective) -end - -function MOI.get( - block::QPBlockData{T}, - ::MOI.ListOfConstraintTypesPresent, -) where {T} - constraints = Set{Tuple{Type,Type}}() - for i in 1:length(block) - F = _function_type_to_set(T, block.function_type[i]) - S = _bound_type_to_set(T, block.bound_type[i]) - push!(constraints, (F, S)) - end - return collect(constraints) -end - -function MOI.is_valid( - block::QPBlockData{T}, - ci::MOI.ConstraintIndex{F,S}, -) where { - T, - F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}, - S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T},MOI.Interval{T}}, -} - return 1 <= ci.value <= length(block) -end - -function MOI.get( - block::QPBlockData{T}, - ::MOI.ListOfConstraintIndices{F,S}, -) where { - T, - F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}, - S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T},MOI.Interval{T}}, -} - ret = MOI.ConstraintIndex{F,S}[] - for i in 1:length(block) - if _bound_type_to_set(T, block.bound_type[i]) != S - continue - elseif _function_type_to_set(T, block.function_type[i]) != F - continue - end - push!(ret, MOI.ConstraintIndex{F,S}(i)) - end - return ret -end - -function MOI.get( - block::QPBlockData{T}, - ::MOI.NumberOfConstraints{F,S}, -) where { - T, - F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}, - S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T},MOI.Interval{T}}, -} - return length(MOI.get(block, MOI.ListOfConstraintIndices{F,S}())) -end - -function MOI.add_constraint( - block::QPBlockData{T}, - f::Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}, - set::Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T},MOI.Interval{T}}, -) where {T} - push!(block.constraints, f) - bound_type, l, u = _set_info(set) - push!(block.g_L, l) - push!(block.g_U, u) - push!(block.mult_g, nothing) - push!(block.bound_type, bound_type) - push!(block.function_type, _function_info(f)) - return MOI.ConstraintIndex{typeof(f),typeof(set)}(length(block.bound_type)) -end - -function MOI.get( - block::QPBlockData{T}, - ::MOI.ConstraintFunction, - c::MOI.ConstraintIndex{F,S}, -) where {T,F,S} - return convert(F, block.constraints[c.value]) -end - -function MOI.get( - block::QPBlockData{T}, - ::MOI.ConstraintSet, - c::MOI.ConstraintIndex{F,S}, -) where {T,F,S} - row = c.value - if block.bound_type[row] == _kBoundTypeEqualTo - return MOI.EqualTo(block.g_L[row]) - elseif block.bound_type[row] == _kBoundTypeLessThan - return MOI.LessThan(block.g_U[row]) - elseif block.bound_type[row] == _kBoundTypeGreaterThan - return MOI.GreaterThan(block.g_L[row]) - else - @assert block.bound_type[row] == _kBoundTypeInterval - return MOI.Interval(block.g_L[row], block.g_U[row]) - end -end - -function MOI.set( - block::QPBlockData{T}, - ::MOI.ConstraintSet, - c::MOI.ConstraintIndex{F,MOI.LessThan{T}}, - set::MOI.LessThan{T}, -) where {T,F} - row = c.value - block.g_U[row] = set.upper - return -end - -function MOI.set( - block::QPBlockData{T}, - ::MOI.ConstraintSet, - c::MOI.ConstraintIndex{F,MOI.GreaterThan{T}}, - set::MOI.GreaterThan{T}, -) where {T,F} - row = c.value - block.g_L[row] = set.lower - return -end - -function MOI.set( - block::QPBlockData{T}, - ::MOI.ConstraintSet, - c::MOI.ConstraintIndex{F,MOI.EqualTo{T}}, - set::MOI.EqualTo{T}, -) where {T,F} - row = c.value - block.g_L[row] = set.value - block.g_U[row] = set.value - return -end - -function MOI.set( - block::QPBlockData{T}, - ::MOI.ConstraintSet, - c::MOI.ConstraintIndex{F,MOI.Interval{T}}, - set::MOI.Interval{T}, -) where {T,F} - row = c.value - block.g_L[row] = set.lower - block.g_U[row] = set.upper - return -end - -function MOI.get( - block::QPBlockData{T}, - ::MOI.ConstraintDualStart, - c::MOI.ConstraintIndex{F,S}, -) where {T,F,S} - return block.mult_g[c.value] -end - -function MOI.set( - block::QPBlockData{T}, - ::MOI.ConstraintDualStart, - c::MOI.ConstraintIndex{F,S}, - value, -) where {T,F,S} - block.mult_g[c.value] = value - return -end - -function MOI.eval_objective( - block::QPBlockData{T}, - x::AbstractVector{T}, -) where {T} - return eval_function(block.objective, x, block.parameters) -end - -function MOI.eval_objective_gradient( - block::QPBlockData{T}, - ∇f::AbstractVector{T}, - x::AbstractVector{T}, -) where {T} - ∇f .= zero(T) - eval_dense_gradient(∇f, block.objective, x, block.parameters) - return -end - -function MOI.eval_constraint( - block::QPBlockData{T}, - g::AbstractVector{T}, - x::AbstractVector{T}, -) where {T} - for (i, constraint) in enumerate(block.constraints) - g[i] = eval_function(constraint, x, block.parameters) - end - return -end - -function MOI.jacobian_structure(block::QPBlockData) - J = Tuple{Int,Int}[] - for (row, constraint) in enumerate(block.constraints) - append_sparse_gradient_structure!(constraint, J, row) - end - return J -end - -function MOI.eval_constraint_jacobian( - block::QPBlockData{T}, - J::AbstractVector{T}, - x::AbstractVector{T}, -) where {T} - i = 1 - for constraint in block.constraints - ∇f = view(J, i:length(J)) - i += eval_sparse_gradient(∇f, constraint, x, block.parameters) - end - return i -end - -function MOI.hessian_lagrangian_structure(block::QPBlockData) - H = Tuple{Int,Int}[] - append_sparse_hessian_structure!(block.objective, H) - for constraint in block.constraints - append_sparse_hessian_structure!(constraint, H) - end - return H -end - -function MOI.eval_hessian_lagrangian( - block::QPBlockData{T}, - H::AbstractVector{T}, - x::AbstractVector{T}, - σ::T, - μ::AbstractVector{T}, -) where {T} - i = 1 - i += eval_sparse_hessian(H, block.objective, σ) - for (row, constraint) in enumerate(block.constraints) - ∇²f = view(H, i:length(H)) - i += eval_sparse_hessian(∇²f, constraint, μ[row]) - end - return i -end - -function MOI.eval_constraint_jacobian_product( - block::QPBlockData{T}, - y::AbstractVector{T}, - x::AbstractVector{T}, - w::AbstractVector{T}, -) where {T} - for (i, constraint) in enumerate(block.constraints) - eval_Jv_product(constraint, y, x, w, block.parameters, i) - end - return -end - -function MOI.eval_constraint_jacobian_transpose_product( - block::QPBlockData{T}, - y::AbstractVector{T}, - x::AbstractVector{T}, - w::AbstractVector{T}, -) where {T} - for (i, constraint) in enumerate(block.constraints) - eval_Jtv_product(constraint, y, x, w, block.parameters, i) - end - return -end - -function MOI.eval_hessian_lagrangian_product( - block::QPBlockData{T}, - H::AbstractVector{T}, - x::AbstractVector{T}, - v::AbstractVector{T}, - σ::T, - μ::AbstractVector{T}, -) where {T} - eval_Hv_product(block.objective, H, x, v, σ) - for (i, constraint) in enumerate(block.constraints) - eval_Hv_product(constraint, H, x, v, μ[i]) - end - return -end diff --git a/ext/MadNLPMOI/MOI_wrapper.jl b/ext/MadNLPMOI/MOI_wrapper.jl index 79c8db50b..1eac197ba 100644 --- a/ext/MadNLPMOI/MOI_wrapper.jl +++ b/ext/MadNLPMOI/MOI_wrapper.jl @@ -4,6 +4,8 @@ _is_parameter(x::MOI.VariableIndex) = x.value >= _PARAMETER_OFFSET _is_parameter(term::MOI.ScalarAffineTerm) = _is_parameter(term.variable) _is_parameter(term::MOI.ScalarQuadraticTerm) = _is_parameter(term.variable_1) || _is_parameter(term.variable_2) +const QPBlockData = MOI.Nonlinear.QPBlockData + mutable struct _VectorNonlinearOracleCache set::MOI.VectorNonlinearOracle{Float64} x::Vector{Float64} @@ -219,6 +221,10 @@ function MOI.add_constrained_variable( 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 before every solve. + model.qp_data.parameters[p.value] = set.value ci = MOI.ConstraintIndex{MOI.VariableIndex,typeof(set)}(p.value) return p, ci end @@ -1224,7 +1230,6 @@ end function MOI.eval_constraint_jacobian(model::Optimizer, values, x) offset = MOI.eval_constraint_jacobian(model.qp_data, values, x) - offset -= 1 # .qp_data returns one-indexed offset for (f, s) in model.vector_nonlinear_oracle_constraints offset = _eval_constraint_jacobian(values, offset, x, f, s) end @@ -1330,7 +1335,6 @@ end function MOI.eval_hessian_lagrangian(model::Optimizer, H, x, σ, μ) offset = MOI.eval_hessian_lagrangian(model.qp_data, H, x, σ, μ) - offset -= 1 # model.qp_data returns one-indexed offset μ_offset = length(model.qp_data) for (f, s) in model.vector_nonlinear_oracle_constraints offset, μ_offset = @@ -1440,7 +1444,10 @@ function _setup_model(model::Optimizer) # Check model's structure. has_oracle = !isempty(model.vector_nonlinear_oracle_constraints) has_quadratic_constraints = - any(isequal(_kFunctionTypeScalarQuadratic), model.qp_data.function_type) + any( + isequal(MOI.Nonlinear._kFunctionTypeScalarQuadratic), + model.qp_data.function_type, + ) has_nlp_constraints = !isempty(model.nlp_data.constraint_bounds) || has_oracle has_nlp_objective = model.nlp_data.has_objective has_hessian = :Hess in MOI.features_available(model.nlp_data.evaluator) diff --git a/ext/MadNLPMOI/MadNLPMOI.jl b/ext/MadNLPMOI/MadNLPMOI.jl index 96eca65aa..4d7e85c14 100644 --- a/ext/MadNLPMOI/MadNLPMOI.jl +++ b/ext/MadNLPMOI/MadNLPMOI.jl @@ -10,7 +10,6 @@ function __init__() return end -include("MOI_utils.jl") include("MOI_wrapper.jl") end # module MadNLPMOI From 4e75c1ed0f2c5751de014bb1f2b587fe5ce6344e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 13 Aug 2026 10:45:14 +0000 Subject: [PATCH 02/12] Track the QP block entry counts in the Optimizer The evaluator methods of MOI.Nonlinear.QPBlockData now return nothing, following the documented contract of MOI.eval_constraint_jacobian and MOI.eval_hessian_lagrangian, so the offsets of the oracle and NLP blocks are computed once in _setup_model instead. --- ext/MadNLPMOI/MOI_wrapper.jl | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ext/MadNLPMOI/MOI_wrapper.jl b/ext/MadNLPMOI/MOI_wrapper.jl index 1eac197ba..5911558b0 100644 --- a/ext/MadNLPMOI/MOI_wrapper.jl +++ b/ext/MadNLPMOI/MOI_wrapper.jl @@ -53,6 +53,10 @@ mutable struct Optimizer <: MOI.AbstractOptimizer 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 nlp_model::Union{Nothing,MOI.Nonlinear.Model} ad_backend::MOI.Nonlinear.AbstractAutomaticDifferentiation vector_nonlinear_oracle_constraints::Vector{Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}} @@ -97,6 +101,8 @@ function Optimizer(; kwargs...) nothing, Dict{MOI.Nonlinear.ConstraintIndex,Float64}(), QPBlockData{Float64}(), + 0, + 0, nothing, MOI.Nonlinear.SparseReverseMode(), Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}[], @@ -163,6 +169,8 @@ function MOI.empty!(model::Optimizer) model.nlp_dual_start = nothing empty!(model.mult_g_nlp) model.qp_data = QPBlockData{Float64}() + model.qp_nnzj = 0 + model.qp_nnzh = 0 model.nlp_model = nothing # SKIP: model.ad_backend empty!(model.vector_nonlinear_oracle_constraints) @@ -1229,7 +1237,8 @@ function _eval_constraint_jacobian( end function MOI.eval_constraint_jacobian(model::Optimizer, values, x) - offset = MOI.eval_constraint_jacobian(model.qp_data, values, x) + MOI.eval_constraint_jacobian(model.qp_data, values, x) + offset = model.qp_nnzj for (f, s) in model.vector_nonlinear_oracle_constraints offset = _eval_constraint_jacobian(values, offset, x, f, s) end @@ -1334,7 +1343,8 @@ function _eval_hessian_lagrangian( end function MOI.eval_hessian_lagrangian(model::Optimizer, H, x, σ, μ) - offset = MOI.eval_hessian_lagrangian(model.qp_data, 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 offset, μ_offset = @@ -1443,6 +1453,8 @@ function _setup_model(model::Optimizer) end # Check model's structure. has_oracle = !isempty(model.vector_nonlinear_oracle_constraints) + 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), From fd1d51a94c038f939c6b13ce30912e4f1af2ddb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 13 Aug 2026 12:34:48 +0000 Subject: [PATCH 03/12] Use the add_... product functions of QPBlockData Follow-up to the rename in MathOptInterface: the accumulating product functions of QPBlockData are no longer methods of the MOI.eval_... generic functions. --- ext/MadNLPMOI/MOI_wrapper.jl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ext/MadNLPMOI/MOI_wrapper.jl b/ext/MadNLPMOI/MOI_wrapper.jl index 5911558b0..fc58545ce 100644 --- a/ext/MadNLPMOI/MOI_wrapper.jl +++ b/ext/MadNLPMOI/MOI_wrapper.jl @@ -1277,7 +1277,12 @@ function MOI.eval_constraint_jacobian_transpose_product(model::Optimizer, Jtv, x offset = length(model.qp_data) v_qp = view(v, 1:offset) # Evaluate jtprod for linear-quadratic part of the model. - MOI.eval_constraint_jacobian_transpose_product(model.qp_data, Jtv, x, v_qp) + MOI.Nonlinear.add_constraint_jacobian_transpose_product( + model.qp_data, + Jtv, + x, + v_qp, + ) # Evaluate jtprod for all VectorNonlinearOracle. for (f, s) in model.vector_nonlinear_oracle_constraints _eval_constraint_transpose_jacobian_product(Jtv, x, offset, f, s, v) @@ -1296,7 +1301,7 @@ function MOI.eval_constraint_jacobian_product(model::Optimizer, Jv, x, v) Jv_qp = view(Jv, 1:qp_offset) Jv_nlp = view(Jv, (qp_offset+1):length(Jv)) MOI.eval_constraint_jacobian_product(model.nlp_data.evaluator, Jv_nlp, x, v) - MOI.eval_constraint_jacobian_product(model.qp_data, Jv_qp, x, v) + MOI.Nonlinear.add_constraint_jacobian_product(model.qp_data, Jv_qp, x, v) return end @@ -1362,7 +1367,7 @@ function MOI.eval_hessian_lagrangian_product(model::Optimizer, Hv, x, v, σ, μ) qp_offset = length(model.qp_data) μ_nlp = view(μ, (qp_offset+1):length(μ)) MOI.eval_hessian_lagrangian_product(model.nlp_data.evaluator, Hv, x, v, σ, μ_nlp) - MOI.eval_hessian_lagrangian_product(model.qp_data, Hv, x, v, σ, μ) + MOI.Nonlinear.add_hessian_lagrangian_product(model.qp_data, Hv, x, v, σ, μ) return end From 68d5228d327e8c8a22adf42db3c53634a84d5132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 13 Aug 2026 12:35:12 +0000 Subject: [PATCH 04/12] Fix clobbered Jacobian-transpose product with mixed constraint blocks The fused MOI.eval_constraint_jacobian_transpose_product called the nonlinear-expressions evaluator LAST with the full output vector. Implementations of that function store the result (ReverseAD zeroes the output before writing), so the contributions of the QP block and of the vector-nonlinear-oracle constraints accumulated beforehand were erased whenever the model also had ScalarNonlinearFunction constraints. Call the evaluator first and accumulate the other blocks afterwards, like the fused Jacobian and Hessian products already did. --- ext/MadNLPMOI/MOI_wrapper.jl | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/ext/MadNLPMOI/MOI_wrapper.jl b/ext/MadNLPMOI/MOI_wrapper.jl index fc58545ce..25c29a462 100644 --- a/ext/MadNLPMOI/MOI_wrapper.jl +++ b/ext/MadNLPMOI/MOI_wrapper.jl @@ -1275,8 +1275,17 @@ end function MOI.eval_constraint_jacobian_transpose_product(model::Optimizer, Jtv, x, v) fill!(Jtv, 0.0) offset = length(model.qp_data) - v_qp = view(v, 1:offset) + for (_, s) in model.vector_nonlinear_oracle_constraints + offset += s.set.output_dimension + end + # Evaluate jtprod for the nonlinear expressions FIRST: implementations of + # MOI.eval_constraint_jacobian_transpose_product are allowed to overwrite + # their output (ReverseAD's does), so it must be called before the + # accumulating contributions of the other blocks. + v_nlp = view(v, (offset+1):length(v)) + MOI.eval_constraint_jacobian_transpose_product(model.nlp_data.evaluator, Jtv, x, v_nlp) # Evaluate jtprod for linear-quadratic part of the model. + v_qp = view(v, 1:length(model.qp_data)) MOI.Nonlinear.add_constraint_jacobian_transpose_product( model.qp_data, Jtv, @@ -1284,13 +1293,11 @@ function MOI.eval_constraint_jacobian_transpose_product(model::Optimizer, Jtv, x v_qp, ) # Evaluate jtprod for all VectorNonlinearOracle. + offset = length(model.qp_data) for (f, s) in model.vector_nonlinear_oracle_constraints _eval_constraint_transpose_jacobian_product(Jtv, x, offset, f, s, v) offset += s.set.output_dimension end - # Evaluate jtprod for remaining nonlinear expressions. - v_nlp = view(v, (offset+1):length(v)) - MOI.eval_constraint_jacobian_transpose_product(model.nlp_data.evaluator, Jtv, x, v_nlp) return end From 6110cbf0d401e7c28fad34ef3bc67bab7722aeab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 15 Aug 2026 14:09:32 +0000 Subject: [PATCH 05/12] 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, and it owns the transposed-Jacobian product fallback of the oracles. The jprod/hprod/hess availability flags now read directly off MOI.features_available of the evaluator, which performs the same oracle filtering. --- ext/MadNLPMOI/MOI_wrapper.jl | 312 ++++++++++++++++++++--------------- 1 file changed, 183 insertions(+), 129 deletions(-) diff --git a/ext/MadNLPMOI/MOI_wrapper.jl b/ext/MadNLPMOI/MOI_wrapper.jl index 25c29a462..5bea7c764 100644 --- a/ext/MadNLPMOI/MOI_wrapper.jl +++ b/ext/MadNLPMOI/MOI_wrapper.jl @@ -4,8 +4,6 @@ _is_parameter(x::MOI.VariableIndex) = x.value >= _PARAMETER_OFFSET _is_parameter(term::MOI.ScalarAffineTerm) = _is_parameter(term.variable) _is_parameter(term::MOI.ScalarQuadraticTerm) = _is_parameter(term.variable_1) || _is_parameter(term.variable_2) -const QPBlockData = MOI.Nonlinear.QPBlockData - mutable struct _VectorNonlinearOracleCache set::MOI.VectorNonlinearOracle{Float64} x::Vector{Float64} @@ -52,12 +50,12 @@ mutable struct Optimizer <: MOI.AbstractOptimizer 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}} ad_backend::MOI.Nonlinear.AbstractAutomaticDifferentiation vector_nonlinear_oracle_constraints::Vector{Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}} @@ -100,9 +98,8 @@ function Optimizer(; kwargs...) MOI.NLPBlockData([], _EmptyNLPEvaluator(), false), nothing, Dict{MOI.Nonlinear.ConstraintIndex,Float64}(), - QPBlockData{Float64}(), - 0, - 0, + MOI.Nonlinear.ModelWithQuad(MOI.Nonlinear.Model()), + nothing, nothing, MOI.Nonlinear.SparseReverseMode(), Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}[], @@ -168,10 +165,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 # SKIP: model.ad_backend empty!(model.vector_nonlinear_oracle_constraints) empty!(model.jrows) @@ -208,7 +204,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 @@ -232,7 +228,7 @@ function MOI.add_constrained_variable( # `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 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 @@ -329,7 +325,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})) @@ -492,7 +488,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( @@ -503,7 +499,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.solver = nothing return index end @@ -518,7 +514,7 @@ function MOI.get( }, S<:_SETS, } - return MOI.get(model.qp_data, attr) + return MOI.get(model.quad_data, attr) end function MOI.get( @@ -531,7 +527,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( @@ -546,7 +542,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_nlp = true return end @@ -574,7 +570,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( @@ -589,7 +585,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) model.needs_new_nlp = true return end @@ -663,7 +659,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.solver = nothing return end @@ -778,7 +776,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 @@ -1089,7 +1087,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( @@ -1115,7 +1113,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( @@ -1129,10 +1127,8 @@ 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.solver = nothing return end @@ -1145,7 +1141,7 @@ function MOI.eval_objective(model::Optimizer, x) elseif model.nlp_data.has_objective return MOI.eval_objective(model.nlp_data.evaluator, x) end - return MOI.eval_objective(model.qp_data, x) + return MOI.eval_objective(model.evaluator, x) end ### Eval_Grad_F_CB @@ -1156,11 +1152,73 @@ 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 +### _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 + +_ncon_oracle(d::_OracleNLPEvaluator) = sum(s.set.output_dimension for (_, s) in d.oracles; init = 0) + +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_G_CB function _eval_constraint( @@ -1178,17 +1236,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( @@ -1203,16 +1263,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 @@ -1220,6 +1278,8 @@ function MOI.jacobian_structure(model::Optimizer) return J end +MOI.jacobian_structure(model::Optimizer) = MOI.jacobian_structure(model.evaluator) + function _eval_constraint_jacobian( values::AbstractVector, offset::Int, @@ -1236,17 +1296,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 + # N.B.: VectorNonlinearOracle does not support transposed-Jacobian vector-product by default. # This function uses the original Jacobian when we have to compute the product in MadNLP. # This can be slow on large-scale instances. @@ -1272,44 +1335,39 @@ function _eval_constraint_transpose_jacobian_product( return end -function MOI.eval_constraint_jacobian_transpose_product(model::Optimizer, Jtv, x, v) - fill!(Jtv, 0.0) - offset = length(model.qp_data) - for (_, s) in model.vector_nonlinear_oracle_constraints - offset += s.set.output_dimension - end +function MOI.eval_constraint_jacobian_transpose_product( + d::_OracleNLPEvaluator, + Jtv, + x, + v, +) # Evaluate jtprod for the nonlinear expressions FIRST: implementations of # MOI.eval_constraint_jacobian_transpose_product are allowed to overwrite # their output (ReverseAD's does), so it must be called before the - # accumulating contributions of the other blocks. + # accumulating contributions of the oracles. + offset = _ncon_oracle(d) v_nlp = view(v, (offset+1):length(v)) - MOI.eval_constraint_jacobian_transpose_product(model.nlp_data.evaluator, Jtv, x, v_nlp) - # Evaluate jtprod for linear-quadratic part of the model. - v_qp = view(v, 1:length(model.qp_data)) - MOI.Nonlinear.add_constraint_jacobian_transpose_product( - model.qp_data, - Jtv, - x, - v_qp, - ) + MOI.eval_constraint_jacobian_transpose_product(d.nlp, Jtv, x, v_nlp) # Evaluate jtprod for all VectorNonlinearOracle. - offset = length(model.qp_data) - for (f, s) in model.vector_nonlinear_oracle_constraints + offset = 0 + for (f, s) in d.oracles _eval_constraint_transpose_jacobian_product(Jtv, x, offset, f, s, v) offset += s.set.output_dimension end return end +function MOI.eval_constraint_jacobian_transpose_product(model::Optimizer, Jtv, x, v) + return MOI.eval_constraint_jacobian_transpose_product(model.evaluator, Jtv, x, v) +end + +function MOI.eval_constraint_jacobian_product(d::_OracleNLPEvaluator, Jv, x, v) + @assert isempty(d.oracles) + return MOI.eval_constraint_jacobian_product(d.nlp, Jv, x, v) +end + function MOI.eval_constraint_jacobian_product(model::Optimizer, Jv, x, v) - @assert isempty(model.vector_nonlinear_oracle_constraints) - fill!(Jv, 0.0) - qp_offset = length(model.qp_data) - Jv_qp = view(Jv, 1:qp_offset) - Jv_nlp = view(Jv, (qp_offset+1):length(Jv)) - MOI.eval_constraint_jacobian_product(model.nlp_data.evaluator, Jv_nlp, x, v) - MOI.Nonlinear.add_constraint_jacobian_product(model.qp_data, Jv_qp, x, v) - return + return MOI.eval_constraint_jacobian_product(model.evaluator, Jv, x, v) end ### Eval_H_CB @@ -1325,15 +1383,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, @@ -1354,28 +1416,29 @@ 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 + +function MOI.eval_hessian_lagrangian_product(d::_OracleNLPEvaluator, Hv, x, v, σ, μ) + @assert isempty(d.oracles) + return MOI.eval_hessian_lagrangian_product(d.nlp, Hv, x, v, σ, μ) +end + function MOI.eval_hessian_lagrangian_product(model::Optimizer, Hv, x, v, σ, μ) - @assert isempty(model.vector_nonlinear_oracle_constraints) - fill!(Hv, 0.0) - qp_offset = length(model.qp_data) - μ_nlp = view(μ, (qp_offset+1):length(μ)) - MOI.eval_hessian_lagrangian_product(model.nlp_data.evaluator, Hv, x, v, σ, μ_nlp) - MOI.Nonlinear.add_hessian_lagrangian_product(model.qp_data, Hv, x, v, σ, μ) - return + return MOI.eval_hessian_lagrangian_product(model.evaluator, Hv, x, v, σ, μ) end ### MOI.AutomaticDifferentiationBackend @@ -1465,29 +1528,26 @@ function _setup_model(model::Optimizer) end # Check model's structure. has_oracle = !isempty(model.vector_nonlinear_oracle_constraints) - 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) || has_oracle has_nlp_objective = model.nlp_data.has_objective - has_hessian = :Hess in MOI.features_available(model.nlp_data.evaluator) - has_jacobian_operator = :JacVec in MOI.features_available(model.nlp_data.evaluator) - has_hessian_operator = :HessVec 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 + features = MOI.features_available(model.evaluator) + has_hessian = :Hess in features + has_jacobian_operator = :JacVec in features + has_hessian_operator = :HessVec in features model.has_only_linear_constraints = !has_quadratic_constraints && !has_nlp_constraints model.islp = model.has_only_linear_constraints && !has_nlp_objective - model.jprod_available = has_jacobian_operator && !has_oracle - model.hprod_available = has_hessian_operator && !has_oracle + model.jprod_available = has_jacobian_operator + model.hprod_available = has_hessian_operator model.hess_available = has_hessian # Initialize evaluator using model's structure. @@ -1504,7 +1564,7 @@ function _setup_model(model::Optimizer) if has_jacobian_operator push!(init_feat, :JacVec) end - MOI.initialize(model.nlp_data.evaluator, init_feat) + MOI.initialize(model.evaluator, init_feat) # Sparsity jacobian_sparsity = MOI.jacobian_structure(model) @@ -1552,23 +1612,17 @@ function _setup_nlp(model::Optimizer; array_type = nothing) end # Constraints bounds - 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] ncon = length(g_L) # Dual multipliers y0 = zeros(Float64, ncon) - for (i, start) in enumerate(model.qp_data.mult_g) + for (i, start) in enumerate(model.quad_data.qp.mult_g) y0[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 # First there is VectorNonlinearOracle... for (_, cache) in model.vector_nonlinear_oracle_constraints @@ -1633,9 +1687,9 @@ function MOI.optimize!(model::Optimizer) end 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 @@ -1829,7 +1883,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 @@ -1916,6 +1970,6 @@ end function MOI.get(model::Optimizer, attr::MOI.NLPBlockDual) MOI.check_result_index_bounds(model, attr) s = -_dual_multiplier(model) - offset = length(model.qp_data) + offset = length(model.quad_data) return s .* model.result.multipliers[(offset+1):end] end From 1e2d4c2d60c4f2188d130afbac0873a9ee39c303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 15 Aug 2026 14:15:59 +0000 Subject: [PATCH 06/12] MOI --- .github/workflows/test.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b9028e59..ca7150261 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,13 @@ jobs: with: channel: ${{ matrix.julia-version }} - uses: julia-actions/cache@v1 + - name: MOI + shell: julia --project=. --color=yes {0} + run: | + using Pkg + Pkg.add([ + PackageSpec(name="MathOptInterface", rev="bl/model_with_quad"), + ]) - name: test MadNLP shell: julia --project=. --color=yes {0} run: | From be7195cc068cda24e95a07ba7e917feedc18e65b 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 07/12] 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/MadNLPMOI/MOI_wrapper.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/MadNLPMOI/MOI_wrapper.jl b/ext/MadNLPMOI/MOI_wrapper.jl index 5bea7c764..32106e2a0 100644 --- a/ext/MadNLPMOI/MOI_wrapper.jl +++ b/ext/MadNLPMOI/MOI_wrapper.jl @@ -1128,7 +1128,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.solver = nothing return end From 1e09212b23f2528d236380d20cffff1ac66704cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 15 Aug 2026 16:37:14 +0000 Subject: [PATCH 08/12] 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/MadNLPMOI/MOI_wrapper.jl | 211 +++++++++++++++-------------------- 1 file changed, 88 insertions(+), 123 deletions(-) diff --git a/ext/MadNLPMOI/MOI_wrapper.jl b/ext/MadNLPMOI/MOI_wrapper.jl index 32106e2a0..ca1a1d5e2 100644 --- a/ext/MadNLPMOI/MOI_wrapper.jl +++ b/ext/MadNLPMOI/MOI_wrapper.jl @@ -1,6 +1,6 @@ -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) _is_parameter(term::MOI.ScalarQuadraticTerm) = _is_parameter(term.variable_1) || _is_parameter(term.variable_2) @@ -37,8 +37,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer solve_iterations::Int 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}} variable_names::Dict{MOI.VariableIndex, String} @@ -47,14 +46,13 @@ mutable struct Optimizer <: MOI.AbstractOptimizer name_to_constraint_index::Union{Nothing, Dict{String, Union{Nothing, MOI.ConstraintIndex}}} 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}} ad_backend::MOI.Nonlinear.AbstractAutomaticDifferentiation vector_nonlinear_oracle_constraints::Vector{Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}} @@ -87,8 +85,7 @@ function Optimizer(; kwargs...) NaN, 0, MOI.FEASIBILITY_SENSE, - Dict{MOI.VariableIndex,Float64}(), - MOI.Utilities.VariablesContainer{Float64}(), + MOI.Nonlinear.ModelWithQuad(MOI.Nonlinear.Model()), MOI.VariableIndex[], Union{Nothing,Float64}[], Dict{MOI.VariableIndex, String}(), @@ -96,10 +93,9 @@ function Optimizer(; kwargs...) nothing, nothing, MOI.NLPBlockData([], _EmptyNLPEvaluator(), false), + false, nothing, Dict{MOI.Nonlinear.ConstraintIndex,Float64}(), - MOI.Nonlinear.ModelWithQuad(MOI.Nonlinear.Model()), - nothing, nothing, MOI.Nonlinear.SparseReverseMode(), Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}[], @@ -154,8 +150,7 @@ function MOI.empty!(model::Optimizer) model.solve_time = NaN model.solve_iterations = 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.variable_names) @@ -163,10 +158,9 @@ function MOI.empty!(model::Optimizer) model.name_to_variable = nothing model.name_to_constraint_index = nothing 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 # SKIP: model.ad_backend empty!(model.vector_nonlinear_oracle_constraints) @@ -184,7 +178,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) && model.nlp_data.evaluator isa _EmptyNLPEvaluator && model.sense == MOI.FEASIBILITY_SENSE && @@ -199,12 +193,11 @@ end MOI.get(::Optimizer, ::MOI.SolverName) = "MadNLP" -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 @@ -220,16 +213,9 @@ function MOI.add_constrained_variable( model::Optimizer, set::MOI.Parameter{Float64}, ) - _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 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 @@ -238,33 +224,30 @@ function MOI.is_valid( ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.Parameter{Float64}} ) p = MOI.VariableIndex(ci.value) - 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::MOI.ListOfConstraintIndices{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 + return MOI.get(model.model, attr) end function MOI.get( model::Optimizer, - ::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.Parameter{Float64}}, + attr::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 @@ -272,7 +255,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 @@ -311,8 +294,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) @@ -324,13 +305,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 @@ -406,16 +387,13 @@ column(x::MOI.VariableIndex) = x.value function MOI.add_variable(model::Optimizer) push!(model.variable_primal_start, nothing) model.solver = nothing - x = MOI.add_variable(model.variables) + x = MOI.add_variable(model.model.variables) 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) @@ -430,7 +408,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( @@ -440,7 +418,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( @@ -448,11 +426,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.solver = nothing return index end @@ -463,7 +441,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_nlp = true return end @@ -472,7 +450,7 @@ function MOI.delete( model::Optimizer, ci::MOI.ConstraintIndex{MOI.VariableIndex,<:_SETS}, ) - MOI.delete(model.variables, ci) + MOI.delete(model.model.variables, ci) model.solver = nothing return end @@ -488,7 +466,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( @@ -499,7 +477,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.solver = nothing return index end @@ -514,7 +492,7 @@ function MOI.get( }, S<:_SETS, } - return MOI.get(model.quad_data, attr) + return MOI.get(model.model, attr) end function MOI.get( @@ -527,7 +505,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( @@ -542,7 +520,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_nlp = true return end @@ -570,7 +548,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( @@ -585,7 +563,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) model.needs_new_nlp = true return end @@ -596,11 +574,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.add_constraint( @@ -608,11 +583,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.solver = nothing return MOI.ConstraintIndex{typeof(f),typeof(s)}(index.value) end @@ -622,10 +597,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 @@ -637,10 +609,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.supports( @@ -655,13 +624,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.solver = nothing return end @@ -673,7 +642,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( @@ -684,8 +653,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_nlp = true return end @@ -776,7 +745,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 @@ -858,9 +827,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..., @@ -871,8 +840,8 @@ end ### ListOfSupportedNonlinearOperators function MOI.get(model::Optimizer, attr::MOI.ListOfSupportedNonlinearOperators) - _init_nlp_model(model) - return MOI.get(model.nlp_model, attr) + _check_no_nlp_block(model) + return MOI.get(model.model.inner, attr) end ### MOI.VariablePrimalStart @@ -1057,10 +1026,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.solver = nothing return end @@ -1084,10 +1054,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( @@ -1113,7 +1083,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( @@ -1128,7 +1098,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.solver = nothing return end @@ -1516,26 +1486,28 @@ end ### MOI.optimize! function _setup_model(model::Optimizer) - vars = MOI.get(model.variables, MOI.ListOfVariableIndices()) + vars = MOI.get(model.model.variables, MOI.ListOfVariableIndices()) if isempty(vars) model.invalid_model = true return end # Create NLP backend. - if model.nlp_model !== nothing - evaluator = MOI.Nonlinear.Evaluator(model.nlp_model, model.ad_backend, vars) + # 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 + evaluator = MOI.Nonlinear.Evaluator(model.model.inner, model.ad_backend, vars) model.nlp_data = MOI.NLPBlockData(evaluator) end # Check model's structure. has_oracle = !isempty(model.vector_nonlinear_oracle_constraints) model.evaluator = MOI.Nonlinear.EvaluatorWithQuad( - model.quad_data, + model.model, _OracleNLPEvaluator(model), - vars, ) 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) || has_oracle has_nlp_objective = model.nlp_data.has_objective @@ -1601,13 +1573,13 @@ function _setup_nlp(model::Optimizer; array_type = nothing) nnzh = length(model.hrows) # Initial variable - nvar = length(model.variables.lower) + nvar = length(model.model.variables.lower) x0 = zeros(Float64, nvar) for i in 1:length(model.variable_primal_start) x0[i] = if model.variable_primal_start[i] !== nothing model.variable_primal_start[i] else - 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 end @@ -1619,10 +1591,10 @@ function _setup_nlp(model::Optimizer; array_type = nothing) # Dual multipliers y0 = zeros(Float64, ncon) - for (i, start) in enumerate(model.quad_data.qp.mult_g) + for (i, start) in enumerate(model.model.qp.mult_g) y0[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 # First there is VectorNonlinearOracle... for (_, cache) in model.vector_nonlinear_oracle_constraints @@ -1647,8 +1619,8 @@ function _setup_nlp(model::Optimizer; array_type = nothing) NLPModels.NLPModelMeta( nvar, x0 = x0, - lvar = model.variables.lower, - uvar = model.variables.upper, + lvar = model.model.variables.lower, + uvar = model.model.variables.upper, ncon = ncon, y0 = y0, lcon = g_L, @@ -1686,13 +1658,6 @@ function MOI.optimize!(model::Optimizer) return end - 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 - array_type = pop!(model.options, :array_type, nothing) _setup_nlp(model; array_type = array_type) @@ -1859,8 +1824,8 @@ 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] + p = MOI.Nonlinear.ParameterIndex(vi.value - _PARAMETER_OFFSET) + return model.model.inner[p] end return model.result.solution[vi.value] end @@ -1883,7 +1848,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 @@ -1970,6 +1935,6 @@ end function MOI.get(model::Optimizer, attr::MOI.NLPBlockDual) MOI.check_result_index_bounds(model, attr) s = -_dual_multiplier(model) - offset = length(model.quad_data) + offset = length(model.model) return s .* model.result.multipliers[(offset+1):end] end From 7e0d67c62a19f21c5414627086345d9218e6597b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 15 Aug 2026 16:53:31 +0000 Subject: [PATCH 09/12] 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/MadNLPMOI/MOI_wrapper.jl | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/ext/MadNLPMOI/MOI_wrapper.jl b/ext/MadNLPMOI/MOI_wrapper.jl index ca1a1d5e2..80967dbdf 100644 --- a/ext/MadNLPMOI/MOI_wrapper.jl +++ b/ext/MadNLPMOI/MOI_wrapper.jl @@ -1,9 +1,3 @@ -const _PARAMETER_OFFSET = MOI.Nonlinear._PARAMETER_OFFSET - -const _is_parameter = MOI.Nonlinear._is_parameter -_is_parameter(term::MOI.ScalarAffineTerm) = _is_parameter(term.variable) -_is_parameter(term::MOI.ScalarQuadraticTerm) = _is_parameter(term.variable_1) || _is_parameter(term.variable_2) - mutable struct _VectorNonlinearOracleCache set::MOI.VectorNonlinearOracle{Float64} x::Vector{Float64} @@ -254,14 +248,14 @@ 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 @@ -269,8 +263,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 @@ -859,7 +853,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) @@ -872,7 +866,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) @@ -1823,8 +1817,8 @@ function MOI.get( ) MOI.check_result_index_bounds(model, attr) MOI.throw_if_not_valid(model, vi) - if _is_parameter(vi) - p = MOI.Nonlinear.ParameterIndex(vi.value - _PARAMETER_OFFSET) + if MOI.Nonlinear._is_parameter(vi) + p = MOI.Nonlinear.ParameterIndex(vi.value - MOI.Nonlinear._PARAMETER_OFFSET) return model.model.inner[p] end return model.result.solution[vi.value] From 01bfe297f56f929a99697e70d2c47d1b7d7a28bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 15 Aug 2026 17:32:44 +0000 Subject: [PATCH 10/12] Let ModelWithQuad substitute the parameters The generic add_constraint and set_objective of the layer replace the offset parameter indices by ParameterIndex, so forward nonlinear constraints to the layer instead of its inner model and drop _replace_parameters. --- ext/MadNLPMOI/MOI_wrapper.jl | 43 +----------------------------------- 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/ext/MadNLPMOI/MOI_wrapper.jl b/ext/MadNLPMOI/MOI_wrapper.jl index 80967dbdf..5a0649e03 100644 --- a/ext/MadNLPMOI/MOI_wrapper.jl +++ b/ext/MadNLPMOI/MOI_wrapper.jl @@ -245,39 +245,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{<:Union{MOI.VariableIndex,_FUNCTIONS}}, @@ -578,10 +545,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.solver = nothing return MOI.ConstraintIndex{typeof(f),typeof(s)}(index.value) end @@ -619,11 +583,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.solver = nothing return From dbfbd52995fc3a6811014d210775da9249107e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 17 Aug 2026 09:43:15 +0200 Subject: [PATCH 11/12] Get rid of list_of_variable_indices --- ext/MadNLPMOI/MOI_wrapper.jl | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/ext/MadNLPMOI/MOI_wrapper.jl b/ext/MadNLPMOI/MOI_wrapper.jl index 5a0649e03..6d2928c4a 100644 --- a/ext/MadNLPMOI/MOI_wrapper.jl +++ b/ext/MadNLPMOI/MOI_wrapper.jl @@ -32,7 +32,6 @@ mutable struct Optimizer <: MOI.AbstractOptimizer 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}} variable_names::Dict{MOI.VariableIndex, String} constraint_names::Dict{MOI.ConstraintIndex, String} @@ -80,7 +79,6 @@ function Optimizer(; kwargs...) 0, MOI.FEASIBILITY_SENSE, MOI.Nonlinear.ModelWithQuad(MOI.Nonlinear.Model()), - MOI.VariableIndex[], Union{Nothing,Float64}[], Dict{MOI.VariableIndex, String}(), Dict{MOI.ConstraintIndex, String}(), @@ -145,7 +143,6 @@ function MOI.empty!(model::Optimizer) model.solve_iterations = 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.variable_names) empty!(model.constraint_names) @@ -209,7 +206,6 @@ function MOI.add_constrained_variable( ) _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 @@ -349,7 +345,6 @@ function MOI.add_variable(model::Optimizer) push!(model.variable_primal_start, nothing) model.solver = nothing x = MOI.add_variable(model.model.variables) - push!(model.list_of_variable_indices, x) return x end @@ -357,12 +352,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( From d8fde493115f0eb1d09b6287a3cf8d78599ae086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Fri, 21 Aug 2026 07:30:58 +0200 Subject: [PATCH 12/12] MOI --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca7150261..86565c857 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: run: | using Pkg Pkg.add([ - PackageSpec(name="MathOptInterface", rev="bl/model_with_quad"), + PackageSpec(name="MathOptInterface", rev="bl/qp_block_data"), ]) - name: test MadNLP shell: julia --project=. --color=yes {0}