From 1b17e8c2fa8d8666a2773cda2c550c2d4e086ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 18:49:27 +0200 Subject: [PATCH 1/4] Add support for DifferentiationBackend --- ext/NLoptMathOptInterfaceExt.jl | 2 +- test/MOI_wrapper.jl | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/ext/NLoptMathOptInterfaceExt.jl b/ext/NLoptMathOptInterfaceExt.jl index 050b86c..8234b16 100644 --- a/ext/NLoptMathOptInterfaceExt.jl +++ b/ext/NLoptMathOptInterfaceExt.jl @@ -31,7 +31,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer variables::MOI.Utilities.VariablesContainer{Float64} starting_values::Vector{Union{Nothing,Float64}} nlp_data::MOI.NLPBlockData - nlp_model::Union{Nothing,MOI.Nonlinear.Model} + nlp_model::Any # Union{Nothing, MOI.Nonlinear.Model, ...} ad_backend::MOI.Nonlinear.AbstractAutomaticDifferentiation sense::Union{Nothing,MOI.OptimizationSense} objective::Union{ diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 63e38f6..a0d3a5c 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -361,6 +361,30 @@ function test_ListOfSupportedNonlinearOperators() return end +function test_ScalarNonlinearFunction_SymbolicMode() + # Test that the AutomaticDifferentiationBackend is used when building the + # evaluator. SymbolicMode is a built-in alternative to SparseReverseMode. + model = NLopt.Optimizer() + MOI.set(model, MOI.RawOptimizerAttribute("algorithm"), :LD_LBFGS) + MOI.set( + model, + MOI.AutomaticDifferentiationBackend(), + MOI.Nonlinear.SymbolicMode(), + ) + x = MOI.add_variable(model) + MOI.set(model, MOI.VariablePrimalStart(), x, 2.0) + # min (x - 1)^2 => x* = 1 + f = MOI.ScalarNonlinearFunction( + :^, + Any[MOI.ScalarNonlinearFunction(:-, Any[x, 1.0]), 2.0], + ) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}(), f) + MOI.optimize!(model) + @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), 1.0; atol = 1e-4) + return +end + end # module TestMOIWrapper.runtests() From c978411023c39d416a16bf7a9e133d9b3012d8a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Apr 2026 22:11:04 +0200 Subject: [PATCH 2/4] Custom AD backend --- ext/NLoptMathOptInterfaceExt.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ext/NLoptMathOptInterfaceExt.jl b/ext/NLoptMathOptInterfaceExt.jl index 8234b16..9b6bc4c 100644 --- a/ext/NLoptMathOptInterfaceExt.jl +++ b/ext/NLoptMathOptInterfaceExt.jl @@ -31,7 +31,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer variables::MOI.Utilities.VariablesContainer{Float64} starting_values::Vector{Union{Nothing,Float64}} nlp_data::MOI.NLPBlockData - nlp_model::Any # Union{Nothing, MOI.Nonlinear.Model, ...} + nlp_model::Any # created by MOI.Nonlinear.nonlinear_model(ad_backend) ad_backend::MOI.Nonlinear.AbstractAutomaticDifferentiation sense::Union{Nothing,MOI.OptimizationSense} objective::Union{ @@ -562,8 +562,11 @@ function MOI.supports( end function MOI.get(model::Optimizer, ::MOI.ObjectiveFunctionType) - if model.nlp_model !== nothing && model.nlp_model.objective !== nothing - return MOI.ScalarNonlinearFunction + if model.nlp_model !== nothing + obj = model.nlp_model.objective + if obj !== nothing + return MOI.ScalarNonlinearFunction + end end return typeof(model.objective) end @@ -598,7 +601,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 = MOI.Nonlinear.nonlinear_model(model.ad_backend) end return end From 9a82f23b6f5bb806b1fb6200e8dc9bc1c1da7733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 21 Apr 2026 10:35:50 +0200 Subject: [PATCH 3/4] Checkout MOI bl/nonlinear_model --- .github/workflows/ci.yml | 7 +++++++ ext/NLoptMathOptInterfaceExt.jl | 11 ++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8517f10..ed76da2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,13 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v1 + - name: dev + shell: julia --project=@. {0} + run: | + using Pkg + Pkg.add([ + PackageSpec(name="MathOptInterface", rev="bl/nonlinear_model"), + ]) - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 with: diff --git a/ext/NLoptMathOptInterfaceExt.jl b/ext/NLoptMathOptInterfaceExt.jl index 9b6bc4c..50dd55b 100644 --- a/ext/NLoptMathOptInterfaceExt.jl +++ b/ext/NLoptMathOptInterfaceExt.jl @@ -31,7 +31,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer variables::MOI.Utilities.VariablesContainer{Float64} starting_values::Vector{Union{Nothing,Float64}} nlp_data::MOI.NLPBlockData - nlp_model::Any # created by MOI.Nonlinear.nonlinear_model(ad_backend) + nlp_model::Any # created by MOI.Nonlinear.model(ad_backend) ad_backend::MOI.Nonlinear.AbstractAutomaticDifferentiation sense::Union{Nothing,MOI.OptimizationSense} objective::Union{ @@ -562,11 +562,8 @@ function MOI.supports( end function MOI.get(model::Optimizer, ::MOI.ObjectiveFunctionType) - if model.nlp_model !== nothing - obj = model.nlp_model.objective - if obj !== nothing - return MOI.ScalarNonlinearFunction - end + if model.nlp_model !== nothing && model.nlp_model.objective !== nothing + return MOI.ScalarNonlinearFunction end return typeof(model.objective) end @@ -601,7 +598,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.nonlinear_model(model.ad_backend) + model.nlp_model = MOI.Nonlinear.model(model.ad_backend) end return end From 25729217a293385a7d4f89c93ae59002dee018f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 5 May 2026 10:06:16 +0200 Subject: [PATCH 4/4] Fix --- Project.toml | 1 + ext/NLoptMathOptInterfaceExt.jl | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index b3919b8..c2b5857 100644 --- a/Project.toml +++ b/Project.toml @@ -3,6 +3,7 @@ uuid = "76087f3c-5699-56af-9a33-bf431cd00edd" version = "1.2.1" [deps] +ArrayDiff = "c45fa1ca-6901-44ac-ae5b-5513a4852d50" CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" NLopt_jll = "079eb43e-fd8e-5478-9966-2cf3e3edb778" diff --git a/ext/NLoptMathOptInterfaceExt.jl b/ext/NLoptMathOptInterfaceExt.jl index 50dd55b..2552647 100644 --- a/ext/NLoptMathOptInterfaceExt.jl +++ b/ext/NLoptMathOptInterfaceExt.jl @@ -5,6 +5,7 @@ module NLoptMathOptInterfaceExt +import ArrayDiff import MathOptInterface as MOI import NLopt @@ -31,7 +32,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer variables::MOI.Utilities.VariablesContainer{Float64} starting_values::Vector{Union{Nothing,Float64}} nlp_data::MOI.NLPBlockData - nlp_model::Any # created by MOI.Nonlinear.model(ad_backend) + nlp_model::Union{Nothing,MOI.Nonlinear.Model,ArrayDiff.Model} ad_backend::MOI.Nonlinear.AbstractAutomaticDifferentiation sense::Union{Nothing,MOI.OptimizationSense} objective::Union{ @@ -598,7 +599,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.ad_backend) + model.nlp_model = ArrayDiff.model(model.ad_backend) end return end