From 0c788e49e05a251a9cb0f37b0cc2023b1a34a575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 15 Aug 2023 14:37:43 +0200 Subject: [PATCH 1/6] [POC] Allow array in nonlinear expressions --- src/nlp_expr.jl | 6 ++++++ test/test_nlp_expr.jl | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/nlp_expr.jl b/src/nlp_expr.jl index 45f3ac3496d..d19db6e898b 100644 --- a/src/nlp_expr.jl +++ b/src/nlp_expr.jl @@ -575,6 +575,8 @@ function moi_function(f::GenericNonlinearExpr{V}) where {V} for i in length(f.args):-1:1 if f.args[i] isa GenericNonlinearExpr{V} push!(stack, (ret, i, f.args[i])) + elseif f.args[i] isa AbstractArray + ret.args[i] = moi_function.(f.args[i]) else ret.args[i] = moi_function(f.args[i]) end @@ -586,6 +588,8 @@ function moi_function(f::GenericNonlinearExpr{V}) where {V} for j in length(arg.args):-1:1 if arg.args[j] isa GenericNonlinearExpr{V} push!(stack, (child, j, arg.args[j])) + elseif arg.args[j] isa AbstractArray + child.args[j] = moi_function.(arg.args[j]) else child.args[j] = moi_function(arg.args[j]) end @@ -611,6 +615,8 @@ function jump_function(model::GenericModel, f::MOI.ScalarNonlinearFunction) for child in reverse(arg.args) push!(stack, (new_ret, child)) end + elseif arg isa AbstractArray + push!(parent.args, jump_function.(model, arg)) else push!(parent.args, jump_function(model, arg)) end diff --git a/test/test_nlp_expr.jl b/test/test_nlp_expr.jl index d854b9348a1..1ae4440087c 100644 --- a/test/test_nlp_expr.jl +++ b/test/test_nlp_expr.jl @@ -6,6 +6,7 @@ module TestNLPExpr using JuMP +using LinearAlgebra using Test import LinearAlgebra @@ -1232,4 +1233,14 @@ function test_extension_euler_to_exp( return end +function test_array() + model = Model() + @variable(model, x) + op_norm = NonlinearOperator(:det, det) + @objective(model, Min, op_norm([x])) + f = MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}()) + @test f.head == :norm + @test f.args == [[index(x)]] +end + end # module From cd3c0ec382b2a63ac15dd07a3ae709b2f6406fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sun, 8 Mar 2026 07:25:36 +0100 Subject: [PATCH 2/6] is_real based on type --- src/nlp_expr.jl | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/nlp_expr.jl b/src/nlp_expr.jl index d19db6e898b..e6fe705d7a9 100644 --- a/src/nlp_expr.jl +++ b/src/nlp_expr.jl @@ -330,14 +330,16 @@ Base.isreal(::GenericNonlinearExpr) = true # Univariate operators -_is_real(::Any) = false -_is_real(::Real) = true -_is_real(::AbstractVariableRef) = true -_is_real(::GenericAffExpr{<:Real}) = true -_is_real(::GenericQuadExpr{<:Real}) = true -_is_real(::GenericNonlinearExpr) = true -_is_real(::NonlinearExpression) = true -_is_real(::NonlinearParameter) = true +_is_real(::Type) = false +_is_real(::Type{<:Real}) = true +_is_real(::Type{<:AbstractVariableRef}) = true +_is_real(::Type{<:GenericAffExpr{<:Real}}) = true +_is_real(::Type{<:GenericQuadExpr{<:Real}}) = true +_is_real(::Type{<:GenericNonlinearExpr}) = true +_is_real(::Type{<:NonlinearExpression}) = true +_is_real(::Type{<:NonlinearParameter}) = true +_is_real(::Type{<:AbstractArray{T}}) where {T} = _is_real(T) +_is_real(x) = _is_real(typeof(x)) function _throw_if_not_real(x) if !_is_real(x) From c205649143d5b4cace3f46e692d03a2157c77199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 26 May 2026 10:49:50 +0200 Subject: [PATCH 3/6] Define custom moi_function instead --- src/nlp_expr.jl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/nlp_expr.jl b/src/nlp_expr.jl index e6fe705d7a9..81b07b3b055 100644 --- a/src/nlp_expr.jl +++ b/src/nlp_expr.jl @@ -571,14 +571,19 @@ end moi_function(x::Number) = x +# MOI.Nonlinear.ReverseAD does not support arrays but ArrayDiff.jl and +# Convex.jl do. +# We use `Array` and not `AbstractArray` for now because ArrayDiff +# and Convex.jl currently only support `Array` anyway and we can expand +# the signature in a non-breaking way later anyway. +moi_function(x::Array{<:Number}) = x + function moi_function(f::GenericNonlinearExpr{V}) where {V} ret = MOI.ScalarNonlinearFunction(f.head, similar(f.args)) stack = Tuple{MOI.ScalarNonlinearFunction,Int,GenericNonlinearExpr{V}}[] for i in length(f.args):-1:1 if f.args[i] isa GenericNonlinearExpr{V} push!(stack, (ret, i, f.args[i])) - elseif f.args[i] isa AbstractArray - ret.args[i] = moi_function.(f.args[i]) else ret.args[i] = moi_function(f.args[i]) end @@ -590,8 +595,6 @@ function moi_function(f::GenericNonlinearExpr{V}) where {V} for j in length(arg.args):-1:1 if arg.args[j] isa GenericNonlinearExpr{V} push!(stack, (child, j, arg.args[j])) - elseif arg.args[j] isa AbstractArray - child.args[j] = moi_function.(arg.args[j]) else child.args[j] = moi_function(arg.args[j]) end @@ -617,8 +620,6 @@ function jump_function(model::GenericModel, f::MOI.ScalarNonlinearFunction) for child in reverse(arg.args) push!(stack, (new_ret, child)) end - elseif arg isa AbstractArray - push!(parent.args, jump_function.(model, arg)) else push!(parent.args, jump_function(model, arg)) end From a892f95705d3b9590644ab9ab58caa077493a2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 26 May 2026 10:56:52 +0200 Subject: [PATCH 4/6] better --- src/nlp_expr.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nlp_expr.jl b/src/nlp_expr.jl index 81b07b3b055..adbcd4194be 100644 --- a/src/nlp_expr.jl +++ b/src/nlp_expr.jl @@ -576,7 +576,7 @@ moi_function(x::Number) = x # We use `Array` and not `AbstractArray` for now because ArrayDiff # and Convex.jl currently only support `Array` anyway and we can expand # the signature in a non-breaking way later anyway. -moi_function(x::Array{<:Number}) = x +moi_function(x::Array) = moi_function.(x) function moi_function(f::GenericNonlinearExpr{V}) where {V} ret = MOI.ScalarNonlinearFunction(f.head, similar(f.args)) From cfac27ca3747d472d834652fd2b9f8618e1a6f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 26 May 2026 18:06:23 +0200 Subject: [PATCH 5/6] Fixes --- src/nlp_expr.jl | 16 ++++++++++------ test/test_nlp_expr.jl | 8 ++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/nlp_expr.jl b/src/nlp_expr.jl index adbcd4194be..efad8a89576 100644 --- a/src/nlp_expr.jl +++ b/src/nlp_expr.jl @@ -112,6 +112,13 @@ function variable_ref_type(::Type{GenericNonlinearExpr}, x::AbstractJuMPScalar) return variable_ref_type(x) end +function variable_ref_type( + ::Type{GenericNonlinearExpr}, + ::AbstractArray{T}, +) where {T<:AbstractJuMPScalar} + return variable_ref_type(T) +end + value_type(::Type{GenericNonlinearExpr{V}}) where {V} = value_type(V) function _has_variable_ref_type(a) @@ -571,12 +578,9 @@ end moi_function(x::Number) = x -# MOI.Nonlinear.ReverseAD does not support arrays but ArrayDiff.jl and -# Convex.jl do. -# We use `Array` and not `AbstractArray` for now because ArrayDiff -# and Convex.jl currently only support `Array` anyway and we can expand -# the signature in a non-breaking way later anyway. -moi_function(x::Array) = moi_function.(x) +# `moi_function(::Array)` would be ambiguous with +# `moi_function(AbstractArray{<:AbstractVariableRef})` +moi_function(x::AbstractArray) = moi_function.(x) function moi_function(f::GenericNonlinearExpr{V}) where {V} ret = MOI.ScalarNonlinearFunction(f.head, similar(f.args)) diff --git a/test/test_nlp_expr.jl b/test/test_nlp_expr.jl index 1ae4440087c..75953252079 100644 --- a/test/test_nlp_expr.jl +++ b/test/test_nlp_expr.jl @@ -1236,11 +1236,11 @@ end function test_array() model = Model() @variable(model, x) - op_norm = NonlinearOperator(:det, det) - @objective(model, Min, op_norm([x])) + op_det = NonlinearOperator(det, :det) + @objective(model, Min, op_det([x])) f = MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}()) - @test f.head == :norm - @test f.args == [[index(x)]] + @test f.head == :det + @test f.args == [MOI.VectorOfVariables([index(x)])] end end # module From 420980885d94f63bd4e1afd4b09978183104fc90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 26 May 2026 20:21:02 +0200 Subject: [PATCH 6/6] Add tests --- test/test_nlp_expr.jl | 51 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/test/test_nlp_expr.jl b/test/test_nlp_expr.jl index 75953252079..abf2a20d264 100644 --- a/test/test_nlp_expr.jl +++ b/test/test_nlp_expr.jl @@ -1236,11 +1236,60 @@ end function test_array() model = Model() @variable(model, x) + vov = MOI.VectorOfVariables([index(x)]) op_det = NonlinearOperator(det, :det) @objective(model, Min, op_det([x])) f = MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}()) @test f.head == :det - @test f.args == [MOI.VectorOfVariables([index(x)])] + @test f.args == [vov] + + op_dot = NonlinearOperator(dot, :dot) + a = [2.0] + @objective(model, Min, op_dot([x], a)) + f = MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}()) + @test f.head == :dot + @test length(f.args) == 2 + @test f.args[1] == vov + @test f.args[2] == a +end + +# Inspired from contiguous arrays in ArrayDiff and GenOpt +struct ContiguousVectorOfVariableRefs <: AbstractVector{JuMP.VariableRef} + offset::Int + length::Int +end + +struct Contiguous end + +function JuMP.Containers.container( + _, + axe::JuMP.Containers.VectorizedProductIterator{Tuple{Base.OneTo{Int}}}, + ::Contiguous, +) + # Correctness don't matter, it's not for the sake of this test + return ContiguousVectorOfVariableRefs(0, length(axe)) +end + +struct ContiguousVectorOfVariableIndices <: MOI.AbstractVectorFunction + offset::Int + length::Int +end + +Base.copy(x::ContiguousVectorOfVariableIndices) = x + +function JuMP.moi_function(x::ContiguousVectorOfVariableRefs) + return ContiguousVectorOfVariableIndices(x.offset, x.length) +end + +function test_custom_array() + model = Model() + @variable(model, x[1:2], container = Contiguous()) + @test x === ContiguousVectorOfVariableRefs(0, 2) + op_norm = NonlinearOperator(LinearAlgebra.norm, :norm) + @objective(model, Min, op_norm(x)) + f = MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}()) + @test f.head == :norm + @test f.args[] == ContiguousVectorOfVariableIndices(0, 2) end end # module