From a2ef5cf50667b43267877f5feb37fa6e05792d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 12 Aug 2026 20:29:05 +0000 Subject: [PATCH 1/6] Override MOI.Nonlinear.model for ArrayDiff.Mode Solvers that build their nonlinear model with MOI.Nonlinear.model(backend) (NLopt, NLPModelsJuMP) now receive an ArrayDiff.Model when the backend is an ArrayDiff.Mode, without hardcoding ArrayDiff. --- src/ArrayDiff.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ArrayDiff.jl b/src/ArrayDiff.jl index e840859..248e911 100644 --- a/src/ArrayDiff.jl +++ b/src/ArrayDiff.jl @@ -104,6 +104,12 @@ function from_onnx end model(::Mode{S}) where {S} = Model{eltype(S)}() +# Hook so that solvers using `MOI.Nonlinear.model(backend)` (for example, +# NLopt and NLPModelsJuMP) receive an ArrayDiff model for an ArrayDiff mode. +# ArrayDiff's model natively parses scalar and array nonlinear functions, but +# not the `MOI.VectorNonlinearOracle` set, hence no oracle layer either. +Nonlinear.model(mode::Mode) = model(mode) + # Extend MOI.Nonlinear.set_objective so that solvers calling # MOI.Nonlinear.set_objective(arraydiff_model, snf) dispatch here. function Nonlinear.set_objective(model::Model, obj::MOI.ScalarNonlinearFunction) From 26904c49c3ce2ac4c326602a3ba57c8e9794cc40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 12 Aug 2026 23:21:03 +0200 Subject: [PATCH 2/6] MOI --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79a4c27..a260def 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,13 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v3 + - name: MOI + shell: julia --project=@. {0} + run: | + using Pkg + Pkg.add([ + PackageSpec(name="MathOptInterface", rev="bl/linearity"), + ]) - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 with: From 3c2595666c84e2f6775e7f9178cf2adf7e53da5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sun, 6 Sep 2026 15:50:00 +0200 Subject: [PATCH 3/6] Implement model --- src/ArrayDiff.jl | 112 +++++++++++++++++++++++++++++++++++++- src/evaluator.jl | 17 +++++- src/model.jl | 3 + src/types.jl | 4 +- test/test_IpoptBackend.jl | 60 ++++++++++++++++++++ 5 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 test/test_IpoptBackend.jl diff --git a/src/ArrayDiff.jl b/src/ArrayDiff.jl index 248e911..d7121be 100644 --- a/src/ArrayDiff.jl +++ b/src/ArrayDiff.jl @@ -106,14 +106,21 @@ model(::Mode{S}) where {S} = Model{eltype(S)}() # Hook so that solvers using `MOI.Nonlinear.model(backend)` (for example, # NLopt and NLPModelsJuMP) receive an ArrayDiff model for an ArrayDiff mode. -# ArrayDiff's model natively parses scalar and array nonlinear functions, but -# not the `MOI.VectorNonlinearOracle` set, hence no oracle layer either. -Nonlinear.model(mode::Mode) = model(mode) +# ArrayDiff handles scalar nonlinear functions. The outer layers own quadratic +# functions, variables, bounds, parameters, and vector nonlinear oracles. +function Nonlinear.model(mode::Mode) + return Nonlinear.ModelWithQuad( + Nonlinear.ModelWithOracles(model(mode)), + ) +end # Extend MOI.Nonlinear.set_objective so that solvers calling # MOI.Nonlinear.set_objective(arraydiff_model, snf) dispatch here. function Nonlinear.set_objective(model::Model, obj::MOI.ScalarNonlinearFunction) model.objective = parse_expression(model, obj) + if model.objective_sense == MOI.FEASIBILITY_SENSE + model.objective_sense = MOI.MIN_SENSE + end return end @@ -122,6 +129,99 @@ function Nonlinear.set_objective(model::Model, ::Nothing) return end +Nonlinear._parameter_values(model::Model) = model.parameters +Nonlinear._has_nonlinear_data(model::Model) = + model.objective !== nothing || !isempty(model.constraints) +Nonlinear._is_nonlinear_input( + ::Model{T}, + ::MOI.ScalarNonlinearFunction, + ::Union{ + MOI.LessThan{T}, + MOI.GreaterThan{T}, + MOI.EqualTo{T}, + MOI.Interval{T}, + }, +) where {T} = true +Nonlinear._is_nonlinear_objective( + ::Model, + ::MOI.ScalarNonlinearFunction, +) = true + +MOI.supports_incremental_interface(::Model) = true +MOI.supports(::Model, ::MOI.ObjectiveSense) = true +MOI.get(model::Model, ::MOI.ObjectiveSense) = model.objective_sense +function MOI.set(model::Model, ::MOI.ObjectiveSense, sense) + model.objective_sense = sense + return +end +MOI.supports( + ::Model, + ::MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}, +) = true +function MOI.set( + model::Model, + ::MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}, + f::MOI.ScalarNonlinearFunction, +) + return Nonlinear.set_objective(model, f) +end + +function MOI.supports_constraint( + ::Model{T}, + ::Type{MOI.ScalarNonlinearFunction}, + ::Type{S}, +) where { + T, + S<:Union{ + MOI.LessThan{T}, + MOI.GreaterThan{T}, + MOI.EqualTo{T}, + MOI.Interval{T}, + }, +} + return true +end +function MOI.add_constraint( + model::Model{T}, + f::MOI.ScalarNonlinearFunction, + s::S, +) where { + T, + S<:Union{ + MOI.LessThan{T}, + MOI.GreaterThan{T}, + MOI.EqualTo{T}, + MOI.Interval{T}, + }, +} + ci = add_constraint(model, f, s) + return MOI.ConstraintIndex{typeof(f),S}(ci.value) +end +function MOI.is_valid( + model::Model{T}, + ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,S}, +) where { + T, + S<:Union{ + MOI.LessThan{T}, + MOI.GreaterThan{T}, + MOI.EqualTo{T}, + MOI.Interval{T}, + }, +} + return haskey(model.constraints, ConstraintIndex(ci.value)) +end + +function Nonlinear.constraint_rows( + model::Model, + ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,<:MOI.AbstractScalarSet}, +) + row = findfirst(==(ConstraintIndex(ci.value)), keys(model.constraints)) + return [something(row)] +end +Nonlinear.constraint_dual_starts(model::Model{T}) where {T} = + fill(nothing, length(model.constraints)) + # Create an ArrayDiff Evaluator from an ArrayDiff Model. function Evaluator( model::ArrayDiff.Model, @@ -149,6 +249,12 @@ function Nonlinear.Evaluator( return Evaluator(model, mode, ordered_variables) end +function Nonlinear._constraint_bounds(evaluator::Evaluator) + return [_bound(c.set) for (_, c) in evaluator.model.constraints] +end +Nonlinear._has_objective(evaluator::Evaluator) = + evaluator.model.objective !== nothing + include("JuMP/JuMP.jl") end # module diff --git a/src/evaluator.jl b/src/evaluator.jl index 6b8d0d7..2ca1a13 100644 --- a/src/evaluator.jl +++ b/src/evaluator.jl @@ -30,7 +30,8 @@ end function MOI.eval_objective(evaluator::Evaluator, x) start = time() - obj = MOI.eval_objective(evaluator.backend, x) + obj = Nonlinear._objective_sign(evaluator.model.objective_sense) * + MOI.eval_objective(evaluator.backend, x) evaluator.eval_objective_timer += time() - start return obj end @@ -38,6 +39,7 @@ end function MOI.eval_objective_gradient(evaluator::Evaluator, g, x) start = time() MOI.eval_objective_gradient(evaluator.backend, g, x) + g .*= Nonlinear._objective_sign(evaluator.model.objective_sense) evaluator.eval_objective_gradient_timer += time() - start return end @@ -113,7 +115,8 @@ end function MOI.eval_hessian_lagrangian(evaluator::Evaluator, H, x, σ, μ) start = time() - MOI.eval_hessian_lagrangian(evaluator.backend, H, x, σ, μ) + sign = Nonlinear._objective_sign(evaluator.model.objective_sense) + MOI.eval_hessian_lagrangian(evaluator.backend, H, x, sign * σ, μ) evaluator.eval_hessian_lagrangian_timer += time() - start return end @@ -146,7 +149,15 @@ function MOI.eval_hessian_lagrangian_product( μ, ) start = time() - MOI.eval_hessian_lagrangian_product(evaluator.backend, H, x, v, σ, μ) + sign = Nonlinear._objective_sign(evaluator.model.objective_sense) + MOI.eval_hessian_lagrangian_product( + evaluator.backend, + H, + x, + v, + sign * σ, + μ, + ) evaluator.eval_hessian_lagrangian_timer += time() - start return end diff --git a/src/model.jl b/src/model.jl index 1af4b90..4f896ee 100644 --- a/src/model.jl +++ b/src/model.jl @@ -3,6 +3,9 @@ function set_objective(model::Model, obj) model.objective = parse_expression(model, obj) + if model.objective_sense == MOI.FEASIBILITY_SENSE + model.objective_sense = MOI.MIN_SENSE + end return end diff --git a/src/types.jl b/src/types.jl index 7cba31d..f539b86 100644 --- a/src/types.jl +++ b/src/types.jl @@ -213,7 +213,7 @@ It has the following fields: * `parameters::Vector{Float64}` : holds the current values of the parameters. * `operators::OperatorRegistry` : stores the operators used in the model. """ -mutable struct Model{T} +mutable struct Model{T} <: MOI.ModelLike objective::Union{Nothing,Expression{T}} # Vector residual for nonlinear least-squares objectives. When set, callers # can query its value, `J*v`, `J'*v`, etc. via the evaluator. @@ -222,6 +222,7 @@ mutable struct Model{T} constraints::OrderedCollections.OrderedDict{ConstraintIndex,Constraint{T}} parameters::Vector{T} operators::OperatorRegistry + objective_sense::MOI.OptimizationSense # This is a private field, used only to increment the ConstraintIndex. last_constraint_index::Int64 function Model{T}() where {T} @@ -232,6 +233,7 @@ mutable struct Model{T} OrderedCollections.OrderedDict{ConstraintIndex,Constraint{T}}(), T[], OperatorRegistry(), + MOI.FEASIBILITY_SENSE, 0, ) end diff --git a/test/test_IpoptBackend.jl b/test/test_IpoptBackend.jl new file mode 100644 index 0000000..921c81a --- /dev/null +++ b/test/test_IpoptBackend.jl @@ -0,0 +1,60 @@ +module TestIpoptBackend + +using Test +import ArrayDiff +import Ipopt +import MathOptInterface as MOI + +function _square_minus(x, value) + difference = MOI.ScalarNonlinearFunction(:-, Any[x, value]) + return MOI.ScalarNonlinearFunction(:^, Any[difference, 2.0]) +end + +function test_ipopt_with_arraydiff_mode() + model = Ipopt.Optimizer() + MOI.set(model, MOI.Silent(), true) + mode = ArrayDiff.Mode() + MOI.set(model, MOI.AutomaticDifferentiationBackend(), mode) + @test MOI.get(model, MOI.AutomaticDifferentiationBackend()) === mode + + x, y = MOI.add_variables(model, 2) + objective = MOI.ScalarNonlinearFunction( + :+, + Any[_square_minus(x, 1.0), _square_minus(y, 2.0)], + ) + MOI.set(model, MOI.ObjectiveFunction{typeof(objective)}(), objective) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + + quadratic = MOI.ScalarQuadraticFunction( + [ + MOI.ScalarQuadraticTerm(2.0, x, x), + MOI.ScalarQuadraticTerm(2.0, y, y), + ], + MOI.ScalarAffineTerm{Float64}[], + 0.0, + ) + MOI.add_constraint(model, quadratic, MOI.LessThan(10.0)) + + oracle = MOI.VectorNonlinearOracle(; + dimension = 2, + l = [3.0], + u = [Inf], + eval_f = (output, input) -> (output[1] = input[1] + input[2]), + jacobian_structure = [(1, 1), (1, 2)], + eval_jacobian = (values, input) -> (values .= 1.0), + hessian_lagrangian_structure = Tuple{Int,Int}[], + eval_hessian_lagrangian = (values, input, multipliers) -> nothing, + ) + MOI.add_constraint(model, MOI.VectorOfVariables([x, y]), oracle) + + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.LOCALLY_SOLVED + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1.0 atol = 1e-4 + @test MOI.get(model, MOI.VariablePrimal(), y) ≈ 2.0 atol = 1e-4 + @test MOI.get(model, MOI.ObjectiveValue()) ≈ 0.0 atol = 1e-8 + return +end + +test_ipopt_with_arraydiff_mode() + +end # module From 234e362b528b4242456f1a24a747966a99b3658b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 22 Sep 2026 10:46:12 +0200 Subject: [PATCH 4/6] Use ModelWithQuad --- src/ArrayDiff.jl | 37 ++++++++++++------------------------- src/evaluator.jl | 11 +++++++---- test/test_NLPModelsJuMP.jl | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/ArrayDiff.jl b/src/ArrayDiff.jl index d7121be..55ba6ba 100644 --- a/src/ArrayDiff.jl +++ b/src/ArrayDiff.jl @@ -130,23 +130,6 @@ function Nonlinear.set_objective(model::Model, ::Nothing) end Nonlinear._parameter_values(model::Model) = model.parameters -Nonlinear._has_nonlinear_data(model::Model) = - model.objective !== nothing || !isempty(model.constraints) -Nonlinear._is_nonlinear_input( - ::Model{T}, - ::MOI.ScalarNonlinearFunction, - ::Union{ - MOI.LessThan{T}, - MOI.GreaterThan{T}, - MOI.EqualTo{T}, - MOI.Interval{T}, - }, -) where {T} = true -Nonlinear._is_nonlinear_objective( - ::Model, - ::MOI.ScalarNonlinearFunction, -) = true - MOI.supports_incremental_interface(::Model) = true MOI.supports(::Model, ::MOI.ObjectiveSense) = true MOI.get(model::Model, ::MOI.ObjectiveSense) = model.objective_sense @@ -212,12 +195,22 @@ function MOI.is_valid( return haskey(model.constraints, ConstraintIndex(ci.value)) end -function Nonlinear.constraint_rows( +function MOI.Utilities.rows( model::Model, ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,<:MOI.AbstractScalarSet}, ) row = findfirst(==(ConstraintIndex(ci.value)), keys(model.constraints)) - return [something(row)] + return something(row) +end +function MOI.Utilities.constraint_bounds(model::Model{T}) where {T} + lower = T[] + upper = T[] + for constraint in values(model.constraints) + bound = _bound(constraint.set) + push!(lower, bound.lower) + push!(upper, bound.upper) + end + return MOI.Utilities.Hyperrectangle(lower, upper) end Nonlinear.constraint_dual_starts(model::Model{T}) where {T} = fill(nothing, length(model.constraints)) @@ -249,12 +242,6 @@ function Nonlinear.Evaluator( return Evaluator(model, mode, ordered_variables) end -function Nonlinear._constraint_bounds(evaluator::Evaluator) - return [_bound(c.set) for (_, c) in evaluator.model.constraints] -end -Nonlinear._has_objective(evaluator::Evaluator) = - evaluator.model.objective !== nothing - include("JuMP/JuMP.jl") end # module diff --git a/src/evaluator.jl b/src/evaluator.jl index 2ca1a13..5389129 100644 --- a/src/evaluator.jl +++ b/src/evaluator.jl @@ -1,6 +1,9 @@ # Largely inspired by MathOptInterface/src/Nonlinear/parse.jl # Most functions have been copy-pasted and slightly modified to adapt to small changes in OperatorRegistry and Model. +_objective_sign(sense::MOI.OptimizationSense) = + sense == MOI.MAX_SENSE ? -1.0 : sense == MOI.MIN_SENSE ? 1.0 : 0.0 + function MOI.initialize(evaluator::Evaluator, features::Vector{Symbol}) start_time = time() empty!(evaluator.ordered_constraints) @@ -30,7 +33,7 @@ end function MOI.eval_objective(evaluator::Evaluator, x) start = time() - obj = Nonlinear._objective_sign(evaluator.model.objective_sense) * + obj = _objective_sign(evaluator.model.objective_sense) * MOI.eval_objective(evaluator.backend, x) evaluator.eval_objective_timer += time() - start return obj @@ -39,7 +42,7 @@ end function MOI.eval_objective_gradient(evaluator::Evaluator, g, x) start = time() MOI.eval_objective_gradient(evaluator.backend, g, x) - g .*= Nonlinear._objective_sign(evaluator.model.objective_sense) + g .*= _objective_sign(evaluator.model.objective_sense) evaluator.eval_objective_gradient_timer += time() - start return end @@ -115,7 +118,7 @@ end function MOI.eval_hessian_lagrangian(evaluator::Evaluator, H, x, σ, μ) start = time() - sign = Nonlinear._objective_sign(evaluator.model.objective_sense) + sign = _objective_sign(evaluator.model.objective_sense) MOI.eval_hessian_lagrangian(evaluator.backend, H, x, sign * σ, μ) evaluator.eval_hessian_lagrangian_timer += time() - start return @@ -149,7 +152,7 @@ function MOI.eval_hessian_lagrangian_product( μ, ) start = time() - sign = Nonlinear._objective_sign(evaluator.model.objective_sense) + sign = _objective_sign(evaluator.model.objective_sense) MOI.eval_hessian_lagrangian_product( evaluator.backend, H, diff --git a/test/test_NLPModelsJuMP.jl b/test/test_NLPModelsJuMP.jl index 8e8dc65..9ccba0e 100644 --- a/test/test_NLPModelsJuMP.jl +++ b/test/test_NLPModelsJuMP.jl @@ -60,6 +60,20 @@ function test_neural_tronls() return _test_neural_nlpmodels_jump(JSOSolvers.TronSolverNLS) end +function test_arraydiff_backend() + model = Model(NLPModelsJuMP.Optimizer) + set_attribute(model, "solver", JSOSolvers.LBFGSSolver) + mode = ArrayDiff.Mode() + set_attribute(model, MOI.AutomaticDifferentiationBackend(), mode) + @variable(model, x, start = 0.5) + @objective(model, Min, (sin(x) - sin(1.0))^2) + optimize!(model) + @test termination_status(model) == MOI.LOCALLY_SOLVED + @test value(x) ≈ 1.0 atol = 1e-4 + @test objective_value(model) ≈ 0.0 atol = 1e-8 + return +end + end TestWithNLPModelsJuMP.runtests() From 25e19141a64f052acf8bc834826a528798f70143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 26 Oct 2026 17:16:46 +0100 Subject: [PATCH 5/6] Fixes --- .github/workflows/ci.yml | 2 +- src/ArrayDiff.jl | 44 ++++++++++++++++---------------------- src/evaluator.jl | 25 +++++++++------------- test/Project.toml | 2 +- test/test_NLPModelsJuMP.jl | 3 +++ 5 files changed, 33 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a260def..983caaf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: run: | using Pkg Pkg.add([ - PackageSpec(name="MathOptInterface", rev="bl/linearity"), + PackageSpec(name="MathOptInterface", rev="bl/qp_block_data"), ]) - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 diff --git a/src/ArrayDiff.jl b/src/ArrayDiff.jl index 55ba6ba..840bdf6 100644 --- a/src/ArrayDiff.jl +++ b/src/ArrayDiff.jl @@ -109,9 +109,7 @@ model(::Mode{S}) where {S} = Model{eltype(S)}() # ArrayDiff handles scalar nonlinear functions. The outer layers own quadratic # functions, variables, bounds, parameters, and vector nonlinear oracles. function Nonlinear.model(mode::Mode) - return Nonlinear.ModelWithQuad( - Nonlinear.ModelWithOracles(model(mode)), - ) + return Nonlinear.ModelWithQuad(Nonlinear.ModelWithOracles(model(mode))) end # Extend MOI.Nonlinear.set_objective so that solvers calling @@ -130,6 +128,9 @@ function Nonlinear.set_objective(model::Model, ::Nothing) end Nonlinear._parameter_values(model::Model) = model.parameters +function Nonlinear._has_objective(evaluator::Evaluator) + return evaluator.model.objective !== nothing +end MOI.supports_incremental_interface(::Model) = true MOI.supports(::Model, ::MOI.ObjectiveSense) = true MOI.get(model::Model, ::MOI.ObjectiveSense) = model.objective_sense @@ -137,10 +138,12 @@ function MOI.set(model::Model, ::MOI.ObjectiveSense, sense) model.objective_sense = sense return end -MOI.supports( +function MOI.supports( ::Model, ::MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}, -) = true +) + return true +end function MOI.set( model::Model, ::MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}, @@ -155,12 +158,7 @@ function MOI.supports_constraint( ::Type{S}, ) where { T, - S<:Union{ - MOI.LessThan{T}, - MOI.GreaterThan{T}, - MOI.EqualTo{T}, - MOI.Interval{T}, - }, + S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T},MOI.Interval{T}}, } return true end @@ -170,12 +168,7 @@ function MOI.add_constraint( s::S, ) where { T, - S<:Union{ - MOI.LessThan{T}, - MOI.GreaterThan{T}, - MOI.EqualTo{T}, - MOI.Interval{T}, - }, + S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T},MOI.Interval{T}}, } ci = add_constraint(model, f, s) return MOI.ConstraintIndex{typeof(f),S}(ci.value) @@ -185,19 +178,17 @@ function MOI.is_valid( ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,S}, ) where { T, - S<:Union{ - MOI.LessThan{T}, - MOI.GreaterThan{T}, - MOI.EqualTo{T}, - MOI.Interval{T}, - }, + S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T},MOI.Interval{T}}, } return haskey(model.constraints, ConstraintIndex(ci.value)) end function MOI.Utilities.rows( model::Model, - ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,<:MOI.AbstractScalarSet}, + ci::MOI.ConstraintIndex{ + MOI.ScalarNonlinearFunction, + <:MOI.AbstractScalarSet, + }, ) row = findfirst(==(ConstraintIndex(ci.value)), keys(model.constraints)) return something(row) @@ -212,8 +203,9 @@ function MOI.Utilities.constraint_bounds(model::Model{T}) where {T} end return MOI.Utilities.Hyperrectangle(lower, upper) end -Nonlinear.constraint_dual_starts(model::Model{T}) where {T} = - fill(nothing, length(model.constraints)) +function Nonlinear.constraint_dual_starts(model::Model{T}) where {T} + return fill(nothing, length(model.constraints)) +end # Create an ArrayDiff Evaluator from an ArrayDiff Model. function Evaluator( diff --git a/src/evaluator.jl b/src/evaluator.jl index 5389129..a58e854 100644 --- a/src/evaluator.jl +++ b/src/evaluator.jl @@ -1,8 +1,11 @@ # Largely inspired by MathOptInterface/src/Nonlinear/parse.jl # Most functions have been copy-pasted and slightly modified to adapt to small changes in OperatorRegistry and Model. -_objective_sign(sense::MOI.OptimizationSense) = - sense == MOI.MAX_SENSE ? -1.0 : sense == MOI.MIN_SENSE ? 1.0 : 0.0 +function _objective_sign(evaluator::Evaluator{T}) where {T} + sense = evaluator.model.objective_sense + return sense == MOI.MAX_SENSE ? -one(T) : + sense == MOI.MIN_SENSE ? one(T) : zero(T) +end function MOI.initialize(evaluator::Evaluator, features::Vector{Symbol}) start_time = time() @@ -33,8 +36,7 @@ end function MOI.eval_objective(evaluator::Evaluator, x) start = time() - obj = _objective_sign(evaluator.model.objective_sense) * - MOI.eval_objective(evaluator.backend, x) + obj = _objective_sign(evaluator) * MOI.eval_objective(evaluator.backend, x) evaluator.eval_objective_timer += time() - start return obj end @@ -42,7 +44,7 @@ end function MOI.eval_objective_gradient(evaluator::Evaluator, g, x) start = time() MOI.eval_objective_gradient(evaluator.backend, g, x) - g .*= _objective_sign(evaluator.model.objective_sense) + g .*= _objective_sign(evaluator) evaluator.eval_objective_gradient_timer += time() - start return end @@ -118,7 +120,7 @@ end function MOI.eval_hessian_lagrangian(evaluator::Evaluator, H, x, σ, μ) start = time() - sign = _objective_sign(evaluator.model.objective_sense) + sign = _objective_sign(evaluator) MOI.eval_hessian_lagrangian(evaluator.backend, H, x, sign * σ, μ) evaluator.eval_hessian_lagrangian_timer += time() - start return @@ -152,15 +154,8 @@ function MOI.eval_hessian_lagrangian_product( μ, ) start = time() - sign = _objective_sign(evaluator.model.objective_sense) - MOI.eval_hessian_lagrangian_product( - evaluator.backend, - H, - x, - v, - sign * σ, - μ, - ) + sign = _objective_sign(evaluator) + MOI.eval_hessian_lagrangian_product(evaluator.backend, H, x, v, sign * σ, μ) evaluator.eval_hessian_lagrangian_timer += time() - start return end diff --git a/test/Project.toml b/test/Project.toml index 69b7d6b..45aface 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -27,7 +27,7 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [sources] ArrayDiff = {path = ".."} -NLPModelsJuMP = {rev = "bl/arraydiff", url = "https://github.com/JuliaSmoothOptimizers/NLPModelsJuMP.jl"} +NLPModelsJuMP = {rev = "bl/arraydiff_qp", url = "https://github.com/JuliaSmoothOptimizers/NLPModelsJuMP.jl"} NLopt = {rev = "bl/diff_backend", url = "https://github.com/jump-dev/NLopt.jl/"} [compat] diff --git a/test/test_NLPModelsJuMP.jl b/test/test_NLPModelsJuMP.jl index 9ccba0e..054b901 100644 --- a/test/test_NLPModelsJuMP.jl +++ b/test/test_NLPModelsJuMP.jl @@ -63,6 +63,9 @@ end function test_arraydiff_backend() model = Model(NLPModelsJuMP.Optimizer) set_attribute(model, "solver", JSOSolvers.LBFGSSolver) + # The first solve compiles ArrayDiff, which can exceed the default + # `max_time` of 30 seconds. + set_attribute(model, "max_time", 600.0) mode = ArrayDiff.Mode() set_attribute(model, MOI.AutomaticDifferentiationBackend(), mode) @variable(model, x, start = 0.5) From 0f7f77ea716712e4145dfb606cacf0c4172ade79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 28 Oct 2026 09:39:02 +0100 Subject: [PATCH 6/6] Fix --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 983caaf..d3b21e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: using Pkg Pkg.add([ PackageSpec(name="MathOptInterface", rev="bl/qp_block_data"), + PackageSpec(name="Ipopt", rev="bl/qp_block_data"), ]) - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1