From 78650b74d0d33385d92b30621b60b07b58dadaeb Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 1 Jul 2026 13:47:34 +1200 Subject: [PATCH 1/4] Restore uncached version of moi_function --- src/nlp_expr.jl | 28 ++++++++++++++++++++++++++++ test/test_nlp_expr.jl | 21 +++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/nlp_expr.jl b/src/nlp_expr.jl index 67354a1cfb7..7be568c27d3 100644 --- a/src/nlp_expr.jl +++ b/src/nlp_expr.jl @@ -634,6 +634,34 @@ function moi_function(model::GenericModel, f::GenericNonlinearExpr{V}) where {V} return ret end + +# A backwards-compatible function to preserve behavior prior to #4032. This was +# used by Plasmo.jl +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])) + else + ret.args[i] = moi_function(f.args[i]) + end + end + while !isempty(stack) + parent, i, arg = pop!(stack) + child = MOI.ScalarNonlinearFunction(arg.head, similar(arg.args)) + parent.args[i] = child + for j in length(arg.args):-1:1 + if arg.args[j] isa GenericNonlinearExpr{V} + push!(stack, (child, j, arg.args[j])) + else + child.args[j] = moi_function(arg.args[j]) + end + end + end + return ret +end + jump_function(::GenericModel{T}, x::Number) where {T} = convert(T, x) function jump_function(model::GenericModel, f::MOI.ScalarNonlinearFunction) diff --git a/test/test_nlp_expr.jl b/test/test_nlp_expr.jl index bbfe010ebf2..85fbe639cce 100644 --- a/test/test_nlp_expr.jl +++ b/test/test_nlp_expr.jl @@ -1381,4 +1381,25 @@ function test_custom_array() return end +function test_scalar_nonlinear_moi_function() + model = Model() + @variable(model, x) + y = sin(x) + y_moi = MOI.ScalarNonlinearFunction(:sin, Any[index(x)]) + z = cos(y) + z_moi = MOI.ScalarNonlinearFunction(:cos, Any[y_moi]) + @test isapprox(moi_function(y), y_moi) + @test isempty(model.subexpressions) + @test isapprox(moi_function(model, y), y_moi) + @test length(model.subexpressions) == 1 + @test isapprox(model.subexpressions[objectid(y)], y_moi) + @test isapprox(moi_function(z), z_moi) + @test length(model.subexpressions) == 1 + @test isapprox(moi_function(model, z), z_moi) + @test length(model.subexpressions) == 2 + @test isapprox(model.subexpressions[objectid(y)], y_moi) + @test isapprox(model.subexpressions[objectid(z)], z_moi) + return +end + end # module From 90014dbb5afc25cbc7054f72728009b484338388 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 1 Jul 2026 13:51:38 +1200 Subject: [PATCH 2/4] Fix formatting --- src/nlp_expr.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nlp_expr.jl b/src/nlp_expr.jl index 7be568c27d3..61e018588d0 100644 --- a/src/nlp_expr.jl +++ b/src/nlp_expr.jl @@ -634,7 +634,6 @@ function moi_function(model::GenericModel, f::GenericNonlinearExpr{V}) where {V} return ret end - # A backwards-compatible function to preserve behavior prior to #4032. This was # used by Plasmo.jl function moi_function(f::GenericNonlinearExpr{V}) where {V} From 82438a1872b1dfe91d7bedd25fc3442e41b72c66 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 1 Jul 2026 14:50:19 +1200 Subject: [PATCH 3/4] Add more test coverage --- test/test_nlp_expr.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_nlp_expr.jl b/test/test_nlp_expr.jl index 85fbe639cce..8ba79335ea9 100644 --- a/test/test_nlp_expr.jl +++ b/test/test_nlp_expr.jl @@ -1384,8 +1384,8 @@ end function test_scalar_nonlinear_moi_function() model = Model() @variable(model, x) - y = sin(x) - y_moi = MOI.ScalarNonlinearFunction(:sin, Any[index(x)]) + y = atan(x, 2) + y_moi = MOI.ScalarNonlinearFunction(:atan, Any[index(x), 2]) z = cos(y) z_moi = MOI.ScalarNonlinearFunction(:cos, Any[y_moi]) @test isapprox(moi_function(y), y_moi) From 9e33fcd6c73e1d36fe82fb5212f3eab6cd352a71 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 1 Jul 2026 16:53:40 +1200 Subject: [PATCH 4/4] Fix coverage --- test/test_nlp_expr.jl | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/test/test_nlp_expr.jl b/test/test_nlp_expr.jl index 8ba79335ea9..84859bea94d 100644 --- a/test/test_nlp_expr.jl +++ b/test/test_nlp_expr.jl @@ -1384,21 +1384,33 @@ end function test_scalar_nonlinear_moi_function() model = Model() @variable(model, x) - y = atan(x, 2) - y_moi = MOI.ScalarNonlinearFunction(:atan, Any[index(x), 2]) - z = cos(y) - z_moi = MOI.ScalarNonlinearFunction(:cos, Any[y_moi]) - @test isapprox(moi_function(y), y_moi) + y1 = atan(x, 2) + y1_moi = MOI.ScalarNonlinearFunction(:atan, Any[index(x), 2]) + y2 = y1 + y1 + y2_moi = MOI.ScalarNonlinearFunction(:+, Any[y1_moi, y1_moi]) + y3 = exp(y2) + y3_moi = MOI.ScalarNonlinearFunction(:exp, Any[y2_moi]) + # Test y1 + @test isapprox(moi_function(y1), y1_moi) @test isempty(model.subexpressions) - @test isapprox(moi_function(model, y), y_moi) + @test isapprox(moi_function(model, y1), y1_moi) @test length(model.subexpressions) == 1 - @test isapprox(model.subexpressions[objectid(y)], y_moi) - @test isapprox(moi_function(z), z_moi) + @test isapprox(model.subexpressions[objectid(y1)], y1_moi) + # Test y2 + @test isapprox(moi_function(y2), y2_moi) @test length(model.subexpressions) == 1 - @test isapprox(moi_function(model, z), z_moi) + @test isapprox(moi_function(model, y2), y2_moi) @test length(model.subexpressions) == 2 - @test isapprox(model.subexpressions[objectid(y)], y_moi) - @test isapprox(model.subexpressions[objectid(z)], z_moi) + @test isapprox(model.subexpressions[objectid(y1)], y1_moi) + @test isapprox(model.subexpressions[objectid(y2)], y2_moi) + # Test y3 + @test isapprox(moi_function(y3), y3_moi) + @test length(model.subexpressions) == 2 + @test isapprox(moi_function(model, y3), y3_moi) + @test length(model.subexpressions) == 3 + @test isapprox(model.subexpressions[objectid(y1)], y1_moi) + @test isapprox(model.subexpressions[objectid(y2)], y2_moi) + @test isapprox(model.subexpressions[objectid(y3)], y3_moi) return end