Skip to content

Add QPBlockData - #3048

Open
blegat wants to merge 6 commits into
masterfrom
bl/qp_block_data
Open

Add QPBlockData#3048
blegat wants to merge 6 commits into
masterfrom
bl/qp_block_data

Conversation

@blegat

@blegat blegat commented Aug 13, 2026

Copy link
Copy Markdown
Member

This is essentially a copy-paste from @odow 's implementation in Ipopt : https://github.com/jump-dev/Ipopt.jl/blob/b5b0e62b11aff4ae4bf0c5f0bc852c55b8953b4f/ext/IpoptMathOptInterfaceExt/utils.jl#L62 with these 3 functions added by @frapac in MadNLP https://github.com/madsuite-org/MadNLP.jl/blob/9947ee116559e9a2ab684a2006d37951f0bb9a6f/ext/MadNLPMOI/MOI_utils.jl#L654-L691
The changes I made on top of it were: adding _ in front of a few functions like eval_function-> _eval_function.
The returned value of eval_constraint_jacobian/eval_hessian_lagrangian was returning nnz + 1, not it returns nnz.
_is_parameter(x) = x.value >= 0x00f0000000000000 is replaced with haskey(block.parameters, x.value) which means the parameters need to be registered before being used.

Downstream PRs:

Comment thread src/Nonlinear/qp_block_data.jl Outdated
Comment thread src/Nonlinear/qp_block_data.jl
Comment thread src/Nonlinear/qp_block_data.jl Outdated
Comment thread src/Nonlinear/qp_block_data.jl Outdated
Comment thread src/Nonlinear/qp_block_data.jl Outdated

# 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be something other than a comment.

Comment thread src/Nonlinear/qp_block_data.jl Outdated
w::AbstractVector{T},
) where {T}
for (i, constraint) in enumerate(block.constraints)
_eval_Jv_product(constraint, y, x, w, block.parameters, i)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, we don't zero y before this first call.

x::AbstractVector{T},
w::AbstractVector{T},
) where {T}
for (i, constraint) in enumerate(block.constraints)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the source of bugs like madsuite-org/MadNLP.jl#641

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll just rename them to add_.... These eval_... need to not zero out so that we can combine the NL part with the quadratic part so I'll just create new add functions here. We could make them internal functions if we just used this QPBlockData internally and only expose the EvaluatorWithQuad. But if we want to first release an QPBlockData used by wrappers, we have to expose these new add_... functions. That's probably an argument toward EvaluatorWithQuad 😇

Comment thread src/Nonlinear/qp_block_data.jl Outdated
σ::T,
μ::AbstractVector{T},
) where {T}
_eval_Hv_product(block.objective, H, x, v, σ, block.parameters)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here

blegat added 5 commits August 21, 2026 07:15
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.
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.
…3053)

* Add ModelWithQuad and privatize the product functions of QPBlockData

Making a first release with public add_constraint_jacobian_product,
add_constraint_jacobian_transpose_product, and
add_hessian_lagrangian_product commits MOI to three new function names.
Instead, rename them _add_... and add ModelWithQuad/EvaluatorWithQuad:
the evaluator implements the standard MOI.AbstractNLPEvaluator
interface (including the product callbacks, composed inner-first so
that store-semantics inner implementations such as ReverseAD do not
clobber the QP contribution), so consumers only rely on existing
generic functions and the QPBlockData internals stay private.

ModelWithQuad routes affine and quadratic objectives and constraints
to a QPBlockData and everything else to an inner model, forwards the
MOI attribute queries of the QP block, and tracks where the objective
lives. EvaluatorWithQuad remaps the variables of the QP block to their
consecutive index in ordered_variables during MOI.initialize (variables
absent from ordered_variables are parameters and keep their index,
with the parameters dictionary aliased so per-solve value syncs are
visible), and MOI.NLPBlockData(evaluator) assembles the combined
constraint bounds.

* Remove the unused nothing-inner support of ModelWithQuad

No solver passes `inner = nothing`: the consumers all wrap an eager inner
`Nonlinear.Model`, so drop the `set_objective(::Nothing, ::Nothing)` hook
and the docstring sentence advertising it. A solver that manages its own
nonlinear storage can still define `set_objective` for its inner type.

* Remove MOI.set for the objective of ModelWithQuad

Nonlinear.set_objective is the single way to set the objective: it does
everything the MOI.set method did, and it also accepts nothing to clear the
objective, which the MOI attribute interface cannot express. The MOI getters
stay since the function API has no counterpart for them.

* Move the variables and the parameter convention into ModelWithQuad

ModelWithQuad now owns a MOI.Utilities.VariablesContainer and implements
MOI.add_variable, guaranteeing variable indices 1:n like
MOI.Utilities.MatrixOfConstraints, so the EvaluatorWithQuad no longer remaps
the QP block. Parameters are added with MOI.add_constrained_variable: their
indices are offset by _PARAMETER_OFFSET (moved here from Ipopt), QPBlockData
is back to the simple offset-based _is_parameter instead of the parameters
dictionary, and its parameters vector aliases the parameter storage of the
inner Nonlinear.Model, so solvers no longer sync parameter values before a
solve.

* Always alias the parameter storage of the inner model

Sharing only when the inner model is a Nonlinear.Model silently left
qp.parameters empty for any other inner model type. Assume instead that the
inner model exposes its parameter values as parameters::Vector{T}, like
Nonlinear.Model does, and always alias it; an inner model without that field
fails loudly at construction.

* Define _is_parameter on the affine and quadratic terms

They belong next to the convention; Ipopt and MadNLP defined them on their
local alias of the function, which pirates it.

* Move the parameter substitution into ModelWithQuad

The substitution of the offset parameter indices by ParameterIndex before
the inner model parses a function is index arithmetic tied to the parameter
convention of the layer, so it belongs here: the generic add_constraint and
set_objective now perform it, and the solvers just forward.

* Use an enum for the objective sink of ModelWithQuad

* Implement NumberOfVariables and ListOfVariableIndices

* Return ListOfVariableIndices in the order of creation

Its docstring requires the order in which the variables were added, with the
parameters interleaved, so ModelWithQuad records that order instead of
appending the parameters after the variables.

* Fail if the inner evaluator does not implement the structure queries

An inner evaluator with constraint rows must support :Jac (and :Hess when
the Hessian is queried), so do not silently skip it in jacobian_structure
and hessian_lagrangian_structure.

* Rely on the store contract of the product callbacks

MOI.eval_constraint_jacobian_transpose_product and
MOI.eval_hessian_lagrangian_product store their result, zeroing the output,
so calling the inner evaluator first makes pre-filling the output redundant.
The Jacobian product only zeroes the rows of the QP block, into which the
block accumulates; the inner evaluator stores its own rows.

* Fix unresolvable references in the documentation

_PARAMETER_OFFSET and _is_parameter are private and not part of any docs
block, so the docstrings of QPBlockData and ModelWithQuad cannot link to
them with @ref.

* Cover all lines of model_with_quad.jl and qp_block_data.jl

Add tests for the gradients of affine and quadratic objectives (affine and
off-diagonal terms), the nonlinear objective gradient, the VariableIndex
objective function type, the ConstraintSet getters and constraint-index
filtering of every bound set, the Hessian and product evaluations of a
parameterized QP block, nonlinear functions mentioning the parameter and the
variable directly, and a parameter-free affine subfunction that the
substitution leaves as is.

* Cover the quadratic parameter substitution

Add nonlinear constraints with quadratic subfunctions, with and without a
parameter, and query ListOfSupportedNonlinearOperators through the layer.
These methods were never compiled by the tests, which local coverage reports
as non-executable lines but codecov reports as uncovered.
@blegat

blegat commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

I merged this PR into #3053 and did the same for downstream PRs. Replying to #3053 (comment):

I'm not overly enthused by having another 1000 lines of code in MOI...

I agree, I try to think of it as avoiding to duplicate 1000 lines in 5 downstream packages. We've waiting a bit since the new MOI.Nonlinear so that its downstream packages stabilizes so we should be ready now to commit to merging the handling of quadratic constraints in MOI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants