Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
usually rejects them. The new `SolveWithoutVariables` and
`NoVariablesFeasibilityTolerance` attributes control this behavior.

### Changed

- Setting a `MOI.ScalarNonlinearFunction` objective that is not a valid cubic
polynomial, or whose parametric form the inner optimizer does not support,
now throws `MOI.UnsupportedAttribute` instead of an `ErrorException`.

## Version 0.15.2 (March 15, 2026)

### Fixed
Expand Down
28 changes: 22 additions & 6 deletions src/cubic_objective.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@

function MOI.set(
model::Optimizer{T},
::MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction},
attr::MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction},
f::MOI.ScalarNonlinearFunction,
) where {T}
# 1. Attempt to parse as cubic
# 1. Attempt to parse as cubic. A function that does not parse is an
# objective this optimizer does not support, so it is reported with the
# MOI exception for that: callers such as bridges or DiffOpt's choice of
# differentiation model can then catch it and try something else
parsed = _parse_cubic_expression(f, T)
if parsed === nothing
error(
"ScalarNonlinearFunction must be a valid cubic polynomial with " *
"parameters multiplying at most quadratic variable terms. " *
"Non-polynomial operations or degree > 3 are not supported.",
throw(
MOI.UnsupportedAttribute(
attr,
"ScalarNonlinearFunction must be a valid cubic polynomial " *
"with parameters multiplying at most quadratic variable " *
"terms. Non-polynomial operations or degree > 3 are not " *
"supported.",
),
)
end

Expand All @@ -32,6 +39,15 @@ function MOI.set(
current,
)
catch e
msg =
"Failed to set cubic objective function, f = $f, on inner " *
"optimizer. Original error: $(sprint(showerror, e))"
# the inner optimizer not supporting the parametric form means this
# optimizer does not support the objective either; any other error is
# a genuine failure and keeps its plain report
if e isa MOI.UnsupportedError
throw(MOI.UnsupportedAttribute(attr, msg))
end
# rethrow the original error with the additional info of the objective function that caused it
error(
"Failed to set cubic objective function, f = $f, on inner " *
Expand Down
38 changes: 32 additions & 6 deletions test/test_cubic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,17 @@ end
# ============================================================================

"""
Mock optimizer that rejects ScalarQuadraticFunction objectives.
Mock optimizer that rejects ScalarQuadraticFunction objectives, with a plain
error or, if `unsupported`, with `MOI.UnsupportedAttribute`.
"""
struct NoQuadObjModel <: MOI.ModelLike
inner::MOI.Utilities.Model{Float64}
unsupported::Bool
end

NoQuadObjModel() = NoQuadObjModel(MOI.Utilities.Model{Float64}())
function NoQuadObjModel(; unsupported::Bool = false)
return NoQuadObjModel(MOI.Utilities.Model{Float64}(), unsupported)
end

MOI.add_variable(m::NoQuadObjModel) = MOI.add_variable(m.inner)
MOI.is_empty(m::NoQuadObjModel) = MOI.is_empty(m.inner)
Expand All @@ -46,10 +50,11 @@ function MOI.is_valid(m::NoQuadObjModel, vi::MOI.VariableIndex)
end

function MOI.set(
::NoQuadObjModel,
::MOI.ObjectiveFunction{<:MOI.ScalarQuadraticFunction},
m::NoQuadObjModel,
attr::MOI.ObjectiveFunction{<:MOI.ScalarQuadraticFunction},
::MOI.ScalarQuadraticFunction,
)
m.unsupported && throw(MOI.UnsupportedAttribute(attr))
return error("Quadratic objectives not supported")
end

Expand Down Expand Up @@ -865,9 +870,9 @@ function test_cubic_objective_set_invalid_expression()
y = MOI.add_variable(model)
z = MOI.add_variable(model)

# x*y*z (3 variables, no parameter) should error
# x*y*z (3 variables, no parameter) is not a supported objective
f = MOI.ScalarNonlinearFunction(:*, Any[x, y, z])
@test_throws ErrorException MOI.set(
@test_throws MOI.UnsupportedAttribute MOI.set(
model,
MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}(),
f,
Expand Down Expand Up @@ -897,6 +902,27 @@ function test_cubic_objective_set_error_on_inner_optimizer()
return
end

function test_cubic_objective_set_unsupported_by_inner_optimizer()
model = POI.Optimizer(NoQuadObjModel(; unsupported = true))
x = MOI.add_variable(model)
p, _ = MOI.add_constrained_variable(model, MOI.Parameter(2.0))
p_v = POI.v_idx(POI.p_idx(p))
# p * x^2 parses, but its parametric form is a quadratic objective, which
# the inner optimizer does not support, so neither does POI
f = MOI.ScalarNonlinearFunction(:*, Any[1.0, p_v, x, x])
attr = MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}()
err = try
MOI.set(model, attr, f)
nothing
catch e
e
end
@test err isa MOI.UnsupportedAttribute
@test err.attr == attr
@test occursin("Failed to set cubic objective function", err.message)
return
end

function test_cubic_objective_get_no_cache()
model = POI.Optimizer(HiGHS.Optimizer())

Expand Down
Loading