EAGO.jl icon indicating copy to clipboard operation
EAGO.jl copied to clipboard

Legacy and standard constraint/objective macros represent `^2` differently in nonlinear expressions

Open DimitriAlston opened this issue 5 months ago • 6 comments

@objective and @constraint seem to be reformulating x^2 to x*x in nonlinear expressions, which leads to poorer McCormick relaxations. This isn't an issue when using the legacy macros of @NLobjective and @NLconstraint. I suspect this happens somewhere in https://github.com/PSORLab/EAGO.jl/blob/15bb6cb47a856abbd82cbd5dd54fafc836af8abf/src/eago_optimizer/moi_wrapper.jl#L74-L100 and https://github.com/PSORLab/EAGO.jl/blob/15bb6cb47a856abbd82cbd5dd54fafc836af8abf/src/eago_optimizer/moi_wrapper.jl#L277-L294

Example:

model = JuMP.Model(EAGO.Optimizer)
@variable(model, x[1:2])
@objective(model, Min, x[1]^2 + x[2]^3)
JuMP.optimize!(model)

returns

MathOptInterface.Nonlinear.Expression(
    MathOptInterface.Nonlinear.Node[
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_CALL_MULTIVARIATE, 2, -1),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_CALL_MULTIVARIATE, 1, 1),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_CALL_MULTIVARIATE, 3, 2),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_MOI_VARIABLE, 1, 3),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_MOI_VARIABLE, 1, 3),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_CALL_MULTIVARIATE, 4, 2),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_MOI_VARIABLE, 2, 6),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_VALUE, 1, 6),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_MOI_VARIABLE, 3, 1)
    ],
    [3.0]
)
model = JuMP.Model(EAGO.Optimizer)
@variable(model, x[1:2])
@NLobjective(model, Min, x[1]^2 + x[2]^3)
JuMP.optimize!(model)

returns

MathOptInterface.Nonlinear.Expression(
    MathOptInterface.Nonlinear.Node[
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_CALL_MULTIVARIATE, 2, -1),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_CALL_MULTIVARIATE, 1, 1),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_CALL_MULTIVARIATE, 4, 2),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_MOI_VARIABLE, 1, 3),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_VALUE, 1, 3),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_CALL_MULTIVARIATE, 4, 2),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_MOI_VARIABLE, 2, 6),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_VALUE, 2, 6),
        MathOptInterface.Nonlinear.Node(MathOptInterface.Nonlinear.NODE_MOI_VARIABLE, 3, 1)
    ],
    [2.0, 3.0]
)

(i.e., x^2 + x^3, as expected).

DimitriAlston avatar Aug 01 '25 15:08 DimitriAlston

@odow can you tell if this is an EAGO or a JuMP issue?

DimitriAlston avatar Aug 01 '25 15:08 DimitriAlston

This is coming from MOI:

https://github.com/jump-dev/MathOptInterface.jl/blob/406f7cd9b0e681f1934121055b7619ab3d666202/src/Nonlinear/parse.jl#L342-L361

The reason is that x[1]^2 is treated as a QuadExpr, not ^(x[1], 2).

julia> using JuMP

julia> model = Model();

julia> @variable(model, x[1:2]);

julia> @objective(model, Min, x[1]^2 + x[2]^3);

julia> f = objective_function(model)
(x[1]²) + (x[2] ^ 3.0)

julia> f.head
:+

julia> f.args
2-element Vector{Any}:
 x[1]²
 x[2] ^ 3.0

julia> f.args[1]
x[1]²

julia> typeof(f.args[1])
QuadExpr (alias for GenericQuadExpr{Float64, GenericVariableRef{Float64}})

Isn't there scope in EAGO to recognize this expression type? If someone writes x * x you don't currently exploit that as x^2?

odow avatar Aug 01 '25 22:08 odow

It's not really a fix, but a work-around is

julia> using JuMP

julia> model = Model();

julia> @variable(model, x[1:2]);

julia> @objective(model, Min, x[1]^2 + x[2]^3)
(x[1]²) + (x[2] ^ 3)

julia> @force_nonlinear @objective(model, Min, x[1]^2 + x[2]^3)
(x[1] ^ 2) + (x[2] ^ 3)

odow avatar Aug 01 '25 22:08 odow

Isn't there scope in EAGO to recognize this expression type? If someone writes x * x you don't currently exploit that as x^2?

I believe EAGO used to provide a functionality like this before v0.7 for user-defined functions, but it hasn't been updated after a lot of other internal changes. After the MOI changes for handling nonlinear functions, I'd written a recursive function to convert ScalarNonlinearFunctions to a DAG in EAGO, which would have checked for things like this, but after some discussion at the last JuMP-dev I switched to using a workaround in MOI to convert back to the old DAG format. So currently we rely on MOI's expression representation, though this might change if we get more time to develop out this functionality in EAGO.

DimitriAlston avatar Aug 04 '25 14:08 DimitriAlston

I've opened a PR here: https://github.com/jump-dev/MathOptInterface.jl/pull/2799

It's not guaranteed that I'll merge it, but I'll see if there are any other downstream impacts.

odow avatar Aug 04 '25 20:08 odow

This should be fixed in the next release of MOI

odow avatar Aug 13 '25 20:08 odow