From a0c6f8f8a05a50a146f29fc4b4e80f0a61698f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 13 Aug 2026 10:05:00 +0200 Subject: [PATCH 1/5] Add QPBlockData --- docs/src/submodules/Nonlinear/reference.md | 2 + src/Nonlinear/Nonlinear.jl | 2 + src/Nonlinear/qp_block_data.jl | 739 +++++++++++++++++++++ 3 files changed, 743 insertions(+) create mode 100644 src/Nonlinear/qp_block_data.jl diff --git a/docs/src/submodules/Nonlinear/reference.md b/docs/src/submodules/Nonlinear/reference.md index 842db49713..ab48feb1dd 100644 --- a/docs/src/submodules/Nonlinear/reference.md +++ b/docs/src/submodules/Nonlinear/reference.md @@ -72,6 +72,8 @@ Nonlinear.AbstractAutomaticDifferentiation Nonlinear.ExprGraphOnly Nonlinear.SparseReverseMode Nonlinear.SymbolicMode +Nonlinear.QPBlockData + ``` ## Data-structure diff --git a/src/Nonlinear/Nonlinear.jl b/src/Nonlinear/Nonlinear.jl index 9a5e4bd8d9..e057052029 100644 --- a/src/Nonlinear/Nonlinear.jl +++ b/src/Nonlinear/Nonlinear.jl @@ -42,4 +42,6 @@ include("evaluator.jl") include("ReverseAD/ReverseAD.jl") include("SymbolicAD/SymbolicAD.jl") +include("qp_block_data.jl") + end # module diff --git a/src/Nonlinear/qp_block_data.jl b/src/Nonlinear/qp_block_data.jl new file mode 100644 index 0000000000..1b16647d47 --- /dev/null +++ b/src/Nonlinear/qp_block_data.jl @@ -0,0 +1,739 @@ +# Copyright (c) 2013: Iain Dunning, Miles Lubin, and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +# This file is adapted from `Ipopt.jl/ext/IpoptMathOptInterfaceExt/utils.jl`. +# +# Unlike the Ipopt version, a variable is treated as a parameter if and only +# if its index is a key of the `parameters` dictionary, instead of an +# index-offset convention. Parameters must therefore be registered in +# `parameters` before any structure query, but their values may be updated +# freely between function evaluations. + +@enum( + _FunctionType, + _kFunctionTypeVariableIndex, + _kFunctionTypeScalarAffine, + _kFunctionTypeScalarQuadratic, +) + +function _function_type_to_func(::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 + +""" + QPBlockData{T}() + +A data structure holding an affine or quadratic objective and a block of +affine and quadratic constraints, together with methods to evaluate them +following the [`MOI.AbstractNLPEvaluator`](@ref) callback conventions. + +This is the storage behind [`ModelWithQuad`](@ref); it is not typically used +directly. + +## Parameters + +A variable is treated as a parameter if and only if its index is a key of the +`parameters` dictionary, which maps the raw `MOI.VariableIndex` value of the +parameter to its current value. Register every parameter in `parameters` +before querying any structure; the values may be updated freely between +function evaluations. +""" +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 + +_is_parameter(v::MOI.VariableIndex, p::Dict) = haskey(p, v.value) + +function _value(v::MOI.VariableIndex, x, p::Dict) + return _is_parameter(v, p) ? p[v.value] : x[v.value] +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, p) + ∇f[term.variable.value] += term.coefficient + end + end + for term in f.quadratic_terms + if !_is_parameter(term.variable_1, p) + 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, p) + 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, p) + ∇f[term.variable.value] += term.coefficient + end + end + return +end + +function _append_sparse_gradient_structure!( + f::MOI.ScalarQuadraticFunction, + J, + row, + p::Dict, +) + for term in f.affine_terms + if !_is_parameter(term.variable, p) + push!(J, (row, term.variable.value)) + end + end + for term in f.quadratic_terms + if !_is_parameter(term.variable_1, p) + push!(J, (row, term.variable_1.value)) + end + if term.variable_1 != term.variable_2 && + !_is_parameter(term.variable_2, p) + push!(J, (row, term.variable_2.value)) + end + end + return +end + +function _append_sparse_gradient_structure!( + f::MOI.ScalarAffineFunction, + J, + row, + p::Dict, +) + for term in f.terms + if !_is_parameter(term.variable, p) + 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, p) + i += 1 + ∇f[i] = term.coefficient + end + end + for term in f.quadratic_terms + if !_is_parameter(term.variable_1, p) + 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, p) + 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, p) + i += 1 + ∇f[i] = term.coefficient + end + end + return i +end + +function _append_sparse_hessian_structure!( + f::MOI.ScalarQuadraticFunction, + H, + p::Dict, +) + for term in f.quadratic_terms + if _is_parameter(term.variable_1, p) || + _is_parameter(term.variable_2, p) + continue + end + push!(H, (term.variable_1.value, term.variable_2.value)) + end + return +end + +function _append_sparse_hessian_structure!( + ::MOI.ScalarAffineFunction, + H, + ::Dict, +) + return nothing +end + +function _eval_sparse_hessian( + ∇²f::AbstractVector{T}, + f::MOI.ScalarQuadraticFunction{T}, + σ::T, + p::Dict{Int64,T}, +)::Int where {T} + i = 0 + for term in f.quadratic_terms + if _is_parameter(term.variable_1, p) || + _is_parameter(term.variable_2, p) + continue + end + i += 1 + ∇²f[i] = term.coefficient * σ + end + return i +end + +function _eval_sparse_hessian( + ∇²f::AbstractVector{T}, + f::MOI.ScalarAffineFunction{T}, + σ::T, + p::Dict{Int64,T}, +)::Int where {T} + return 0 +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_func(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_func(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_func(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}}, + s::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(s) + 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(s)}(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} + block.g_U[c.value] = 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} + block.g_L[c.value] = 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} + block.g_L[c.value] = set.value + block.g_U[c.value] = 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} + block.g_L[c.value] = set.lower + block.g_U[c.value] = 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, block.parameters) + end + return J +end + +# Returns the number of entries written to `J`. +function MOI.eval_constraint_jacobian( + block::QPBlockData{T}, + J::AbstractVector{T}, + x::AbstractVector{T}, +) where {T} + i = 0 + for constraint in block.constraints + ∇f = view(J, (i+1):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, block.parameters) + for constraint in block.constraints + _append_sparse_hessian_structure!(constraint, H, block.parameters) + end + return H +end + +# Returns the number of entries written to `H`. +function MOI.eval_hessian_lagrangian( + block::QPBlockData{T}, + H::AbstractVector{T}, + x::AbstractVector{T}, + σ::T, + μ::AbstractVector{T}, +) where {T} + i = _eval_sparse_hessian(H, block.objective, σ, block.parameters) + for (row, constraint) in enumerate(block.constraints) + ∇²f = view(H, (i+1):length(H)) + i += _eval_sparse_hessian(∇²f, constraint, μ[row], block.parameters) + end + return i +end + +# The product evaluators below ACCUMULATE into their output vector, so that +# they compose with the products of the other layers. Zero the output before +# the first call. + +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, p) + 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.affine_terms + if !_is_parameter(term.variable, p) + y[i] += term.coefficient * w[term.variable.value] + end + end + for term in f.quadratic_terms + if !_is_parameter(term.variable_1, p) + v = _value(term.variable_2, x, p) + y[i] += term.coefficient * v * w[term.variable_1.value] + end + if term.variable_1 != term.variable_2 && + !_is_parameter(term.variable_2, p) + v = _value(term.variable_1, x, p) + y[i] += term.coefficient * v * w[term.variable_2.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} + for term in f.terms + if !_is_parameter(term.variable, p) + y[term.variable.value] += term.coefficient * w[i] + 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} + for term in f.affine_terms + if !_is_parameter(term.variable, p) + y[term.variable.value] += term.coefficient * w[i] + end + end + for term in f.quadratic_terms + if !_is_parameter(term.variable_1, p) + v = _value(term.variable_2, x, p) + y[term.variable_1.value] += term.coefficient * v * w[i] + end + if term.variable_1 != term.variable_2 && + !_is_parameter(term.variable_2, p) + v = _value(term.variable_1, x, p) + y[term.variable_2.value] += term.coefficient * v * w[i] + end + end + return +end + +function _eval_Hv_product( + f::MOI.ScalarQuadraticFunction{T}, + H::AbstractVector{T}, + x::AbstractVector{T}, + v::AbstractVector{T}, + λ::T, + p::Dict{Int64,T}, +)::Nothing where {T} + for term in f.quadratic_terms + if _is_parameter(term.variable_1, p) || + _is_parameter(term.variable_2, p) + continue + end + i, j = term.variable_1.value, term.variable_2.value + H[i] += λ * term.coefficient * v[j] + if i != j + H[j] += λ * term.coefficient * v[i] + end + end + return +end + +function _eval_Hv_product( + ::MOI.ScalarAffineFunction{T}, + H::AbstractVector{T}, + x::AbstractVector{T}, + v::AbstractVector{T}, + λ::T, + p::Dict{Int64,T}, +) where {T} + return nothing +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, σ, block.parameters) + for (i, constraint) in enumerate(block.constraints) + _eval_Hv_product(constraint, H, x, v, μ[i], block.parameters) + end + return +end From 0e1b00fa7b84063dce29312c254edd1248092fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 13 Aug 2026 08:16:53 +0000 Subject: [PATCH 2/5] Fix reference to a type of another PR in the QPBlockData docstring --- src/Nonlinear/qp_block_data.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Nonlinear/qp_block_data.jl b/src/Nonlinear/qp_block_data.jl index 1b16647d47..2384e53532 100644 --- a/src/Nonlinear/qp_block_data.jl +++ b/src/Nonlinear/qp_block_data.jl @@ -66,8 +66,9 @@ A data structure holding an affine or quadratic objective and a block of affine and quadratic constraints, together with methods to evaluate them following the [`MOI.AbstractNLPEvaluator`](@ref) callback conventions. -This is the storage behind [`ModelWithQuad`](@ref); it is not typically used -directly. +This is a helper for solvers that pass affine and quadratic constraints to +the solver through the same callbacks as an [`MOI.AbstractNLPEvaluator`](@ref) +(for example, Ipopt and MadNLP). ## Parameters From 242b753b04508bcb36de5b780e20285817921c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 13 Aug 2026 10:57:02 +0200 Subject: [PATCH 3/5] Add type assert --- src/Nonlinear/qp_block_data.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Nonlinear/qp_block_data.jl b/src/Nonlinear/qp_block_data.jl index 2384e53532..0623049095 100644 --- a/src/Nonlinear/qp_block_data.jl +++ b/src/Nonlinear/qp_block_data.jl @@ -431,14 +431,14 @@ function MOI.get( ) where {T,F,S} row = c.value if block.bound_type[row] == _kBoundTypeEqualTo - return MOI.EqualTo(block.g_L[row]) + return MOI.EqualTo(block.g_L[row])::S elseif block.bound_type[row] == _kBoundTypeLessThan - return MOI.LessThan(block.g_U[row]) + return MOI.LessThan(block.g_U[row])::S elseif block.bound_type[row] == _kBoundTypeGreaterThan - return MOI.GreaterThan(block.g_L[row]) + return MOI.GreaterThan(block.g_L[row])::S else @assert block.bound_type[row] == _kBoundTypeInterval - return MOI.Interval(block.g_L[row], block.g_U[row]) + return MOI.Interval(block.g_L[row], block.g_U[row])::S end end From 86feab818c2ac8ad8d9ee191f6899768065b1814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 13 Aug 2026 10:37:04 +0000 Subject: [PATCH 4/5] Return nothing from the evaluator methods of QPBlockData The generic functions MOI.eval_constraint_jacobian and MOI.eval_hessian_lagrangian are documented to return nothing: the caller constructs the sparsity pattern, so it knows how many entries are written. Returning the count invited callers to rely on a return value that no other evaluator provides. --- src/Nonlinear/qp_block_data.jl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Nonlinear/qp_block_data.jl b/src/Nonlinear/qp_block_data.jl index 0623049095..bc528b43ca 100644 --- a/src/Nonlinear/qp_block_data.jl +++ b/src/Nonlinear/qp_block_data.jl @@ -538,7 +538,6 @@ function MOI.jacobian_structure(block::QPBlockData) return J end -# Returns the number of entries written to `J`. function MOI.eval_constraint_jacobian( block::QPBlockData{T}, J::AbstractVector{T}, @@ -549,7 +548,7 @@ function MOI.eval_constraint_jacobian( ∇f = view(J, (i+1):length(J)) i += _eval_sparse_gradient(∇f, constraint, x, block.parameters) end - return i + return end function MOI.hessian_lagrangian_structure(block::QPBlockData) @@ -561,7 +560,6 @@ function MOI.hessian_lagrangian_structure(block::QPBlockData) return H end -# Returns the number of entries written to `H`. function MOI.eval_hessian_lagrangian( block::QPBlockData{T}, H::AbstractVector{T}, @@ -574,7 +572,7 @@ function MOI.eval_hessian_lagrangian( ∇²f = view(H, (i+1):length(H)) i += _eval_sparse_hessian(∇²f, constraint, μ[row], block.parameters) end - return i + return end # The product evaluators below ACCUMULATE into their output vector, so that From 60f50f0628b23e09fd1926d4c102709b9c24ca30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 13 Aug 2026 12:34:28 +0000 Subject: [PATCH 5/5] Rename the product functions of QPBlockData to add_... The products accumulate into their output vector, so that the contributions of several blocks (the QP block, oracle constraints, and an MOI.AbstractNLPEvaluator) can be composed into the same output. This is incompatible with the contract of MOI.eval_constraint_jacobian_product and friends, which store the result, so the functions are renamed add_constraint_jacobian_product, add_constraint_jacobian_transpose_product, and add_hessian_lagrangian_product instead of overloading the MOI generic functions: QPBlockData is not an MOI.AbstractNLPEvaluator, so it does not have to define the same interface as evaluators. The private helpers are renamed _eval_... to _add_... accordingly. --- docs/src/submodules/Nonlinear/reference.md | 3 + src/Nonlinear/qp_block_data.jl | 90 ++++++++++++++++++---- 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/docs/src/submodules/Nonlinear/reference.md b/docs/src/submodules/Nonlinear/reference.md index ab48feb1dd..468f44cdfe 100644 --- a/docs/src/submodules/Nonlinear/reference.md +++ b/docs/src/submodules/Nonlinear/reference.md @@ -73,6 +73,9 @@ Nonlinear.ExprGraphOnly Nonlinear.SparseReverseMode Nonlinear.SymbolicMode Nonlinear.QPBlockData +Nonlinear.add_constraint_jacobian_product +Nonlinear.add_constraint_jacobian_transpose_product +Nonlinear.add_hessian_lagrangian_product ``` diff --git a/src/Nonlinear/qp_block_data.jl b/src/Nonlinear/qp_block_data.jl index bc528b43ca..85e6ad8ce4 100644 --- a/src/Nonlinear/qp_block_data.jl +++ b/src/Nonlinear/qp_block_data.jl @@ -575,11 +575,15 @@ function MOI.eval_hessian_lagrangian( return end -# The product evaluators below ACCUMULATE into their output vector, so that -# they compose with the products of the other layers. Zero the output before -# the first call. - -function _eval_Jv_product( +# The product functions below ACCUMULATE into their output vector, so that +# the contributions of several blocks (for example, the QP block, the +# vector-nonlinear-oracle constraints, and an `MOI.AbstractNLPEvaluator`) can +# be composed into the same output. This is why they are not methods of the +# corresponding `MOI.eval_...` functions, whose contract is to store the +# result: `QPBlockData` is not an `MOI.AbstractNLPEvaluator`, so it does not +# have to define the same interface as evaluators. + +function _add_Jv_product( f::MOI.ScalarAffineFunction{T}, y::AbstractVector{T}, x::AbstractVector{T}, @@ -595,7 +599,7 @@ function _eval_Jv_product( return end -function _eval_Jv_product( +function _add_Jv_product( f::MOI.ScalarQuadraticFunction{T}, y::AbstractVector{T}, x::AbstractVector{T}, @@ -622,7 +626,7 @@ function _eval_Jv_product( return end -function _eval_Jtv_product( +function _add_Jtv_product( f::MOI.ScalarAffineFunction{T}, y::AbstractVector{T}, x::AbstractVector{T}, @@ -638,7 +642,7 @@ function _eval_Jtv_product( return end -function _eval_Jtv_product( +function _add_Jtv_product( f::MOI.ScalarQuadraticFunction{T}, y::AbstractVector{T}, x::AbstractVector{T}, @@ -665,7 +669,7 @@ function _eval_Jtv_product( return end -function _eval_Hv_product( +function _add_Hv_product( f::MOI.ScalarQuadraticFunction{T}, H::AbstractVector{T}, x::AbstractVector{T}, @@ -687,7 +691,7 @@ function _eval_Hv_product( return end -function _eval_Hv_product( +function _add_Hv_product( ::MOI.ScalarAffineFunction{T}, H::AbstractVector{T}, x::AbstractVector{T}, @@ -698,31 +702,83 @@ function _eval_Hv_product( return nothing end -function MOI.eval_constraint_jacobian_product( +# These are used to add the QP contribution on top of the NL contribution. + +""" + add_constraint_jacobian_product( + block::QPBlockData{T}, + y::AbstractVector{T}, + x::AbstractVector{T}, + w::AbstractVector{T}, + )::Nothing where {T} + +Add to `y` the product of the Jacobian of the constraints of `block` at `x` +with `w`. + +Unlike [`MOI.eval_constraint_jacobian_product`](@ref), this function +accumulates into `y` instead of storing the result, so that the contributions +of several blocks can be composed: the caller is responsible for zeroing `y` +before the first contribution. +""" +function add_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) + _add_Jv_product(constraint, y, x, w, block.parameters, i) end return end -function MOI.eval_constraint_jacobian_transpose_product( +""" + add_constraint_jacobian_transpose_product( + block::QPBlockData{T}, + y::AbstractVector{T}, + x::AbstractVector{T}, + w::AbstractVector{T}, + )::Nothing where {T} + +Add to `y` the product of the transpose of the Jacobian of the constraints of +`block` at `x` with `w`. + +Unlike [`MOI.eval_constraint_jacobian_transpose_product`](@ref), this +function accumulates into `y` instead of storing the result, so that the +contributions of several blocks can be composed: the caller is responsible +for zeroing `y` before the first contribution. +""" +function add_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) + _add_Jtv_product(constraint, y, x, w, block.parameters, i) end return end -function MOI.eval_hessian_lagrangian_product( +""" + add_hessian_lagrangian_product( + block::QPBlockData{T}, + H::AbstractVector{T}, + x::AbstractVector{T}, + v::AbstractVector{T}, + σ::T, + μ::AbstractVector{T}, + )::Nothing where {T} + +Add to `H` the product of the Hessian of the Lagrangian of `block` at `x`, +with objective weight `σ` and constraint weights `μ`, with `v`. + +Unlike [`MOI.eval_hessian_lagrangian_product`](@ref), this function +accumulates into `H` instead of storing the result, so that the contributions +of several blocks can be composed: the caller is responsible for zeroing `H` +before the first contribution. +""" +function add_hessian_lagrangian_product( block::QPBlockData{T}, H::AbstractVector{T}, x::AbstractVector{T}, @@ -730,9 +786,9 @@ function MOI.eval_hessian_lagrangian_product( σ::T, μ::AbstractVector{T}, ) where {T} - _eval_Hv_product(block.objective, H, x, v, σ, block.parameters) + _add_Hv_product(block.objective, H, x, v, σ, block.parameters) for (i, constraint) in enumerate(block.constraints) - _eval_Hv_product(constraint, H, x, v, μ[i], block.parameters) + _add_Hv_product(constraint, H, x, v, μ[i], block.parameters) end return end