diff --git a/Project.toml b/Project.toml index 9714355faa..4cb406d105 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "MathOptInterface" uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" -version = "1.45.0" +version = "1.46.0" [deps] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" diff --git a/docs/Project.toml b/docs/Project.toml index 0c18617ad0..26713e4f16 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -6,5 +6,5 @@ MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" [compat] Documenter = "1" -JSON = "0.21" +JSON = "1" JSONSchema = "1" diff --git a/docs/src/changelog.md b/docs/src/changelog.md index 60afad0831..c81c695cc1 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -7,6 +7,26 @@ CurrentModule = MathOptInterface The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v1.46.0 (October 21, 2025) + +### Added + + - Added [`VectorNonlinearOracle`](@ref) (#2860) + +### Fixed + + - Fixed [`Bridges.Constraint.IndicatorToMILPBridge`](@ref) when the `z` variable + was not binary (#2857), (#2868) + - Fixed an `error` to [`GetAttributeNotAllowed`](@ref) in + [`Bridges.Variable.ZerosBridge`](@ref) (#2863) + - Fixed various bridges to throw MOI error subtypes instead of `Base.error()` + (#2866) + +### Other + + - Documentation updates (#2855), (#2858), (#2864) + - Added `@nospecialize` to some methods (#2830) + ## v1.45.0 (September 20, 2025) ### Added diff --git a/docs/src/manual/standard_form.md b/docs/src/manual/standard_form.md index 57b8a8e2d2..1700236547 100644 --- a/docs/src/manual/standard_form.md +++ b/docs/src/manual/standard_form.md @@ -82,6 +82,7 @@ The vector-valued set types implemented in MathOptInterface.jl are: | [`RelativeEntropyCone(d)`](@ref MathOptInterface.RelativeEntropyCone) | ``\{ (u, v, w) \in \mathbb{R}^{d} : u \ge \sum_i w_i \log (\frac{w_i}{v_i}), v_i \ge 0, w_i \ge 0 \}`` | | [`HyperRectangle(l, u)`](@ref MathOptInterface.HyperRectangle) | ``\{x \in \bar{\mathbb{R}}^d: x_i \in [l_i, u_i] \forall i=1,\ldots,d\}`` | | [`NormCone(p, d)`](@ref MathOptInterface.NormCone) | ``\{ (t,x) \in \mathbb{R}^{d} : t \ge \left(\sum\limits_i \lvert x_i \rvert^p\right)^{\frac{1}{p}} \}`` | +| [`VectorNonlinearOracle`](@ref MathOptInterface.VectorNonlinearOracle)| ``\{x \in \mathbb{R}^{dimension}: l \le f(x) \le u \}`` | ## Matrix cones diff --git a/docs/src/reference/standard_form.md b/docs/src/reference/standard_form.md index ffe855b531..d78681869e 100644 --- a/docs/src/reference/standard_form.md +++ b/docs/src/reference/standard_form.md @@ -104,6 +104,7 @@ ACTIVATE_ON_ONE Complements HyperRectangle Scaled +VectorNonlinearOracle ``` ## Constraint programming sets diff --git a/docs/src/submodules/FileFormats/LP.md b/docs/src/submodules/FileFormats/LP.md index 61676133f7..e1c204333d 100644 --- a/docs/src/submodules/FileFormats/LP.md +++ b/docs/src/submodules/FileFormats/LP.md @@ -37,7 +37,7 @@ In addition to the grammar, there are the following rules: * Newlines are ignored, except where explicitly described ``` - :== + :== \n [\n] \n @@ -52,7 +52,7 @@ In addition to the grammar, there are the following rules: i"min" | i"minimum" | i"minimize" | i"minimise" | i"max" | i"maximum" | i"maximize" | i"maximise" - :== + :== (i"subject to" | i"st" | i"st." | i"s.t." | i"such that")[":"] :== i"bound" | i"bounds" @@ -71,7 +71,7 @@ In addition to the grammar, there are the following rules: :== ( | | ".")* - :== + :== "+" | "-" | +[.()*][("e" | "E")("+" | "-")()+] @@ -106,11 +106,11 @@ In addition to the grammar, there are the following rules: := - := + := :== "=" (0 | 1) "->" - :== + :== "S1::" (":")+\n | "S2::" (":")+\n @@ -138,7 +138,7 @@ integers x ``` Gurobi will interpret this as `x in MOI.Integer()`. FICO Xpress will interpret -this as `x in MOI.ZeroOne()`. +this as `x in MOI.ZeroOne()`. FICO document this behavior, but they're an outlier. @@ -168,7 +168,7 @@ x >= 0 end ``` -**We choose to allow variables to be named as keywords, and we use context to +**We choose to allow variables to be named as keywords, and we use context to disambiguate.** ### Whitespace @@ -186,7 +186,7 @@ Xpress will interpret this as the expression `2 * x`. Gurobi document this behavior, saying that they require whitespace around all tokens, but they're an outlier. -**We choose to allow juxtaposted tokens without whitespace.** +**We choose to allow juxtaposed tokens without whitespace.** ### Identifiers diff --git a/docs/src/submodules/FileFormats/overview.md b/docs/src/submodules/FileFormats/overview.md index 1d75c96c95..29b1e41862 100644 --- a/docs/src/submodules/FileFormats/overview.md +++ b/docs/src/submodules/FileFormats/overview.md @@ -282,7 +282,7 @@ Use `JSONSchema.validate` to obtain more insight into why the validation failed: julia> JSONSchema.validate(schema, bad_model) Validation failed: path: [variables][1] -instance: Dict{String, Any}("NaMe" => "x") +instance: JSON.Object{String, Any}("NaMe" => "x") schema key: required schema value: Any["name"] ``` diff --git a/docs/src/tutorials/latency.md b/docs/src/tutorials/latency.md index a54e23dbde..bf0de572a1 100644 --- a/docs/src/tutorials/latency.md +++ b/docs/src/tutorials/latency.md @@ -24,7 +24,7 @@ with the reasons for latency in Julia and how to fix them. * Read the blogposts on julialang.org on [precompilation](https://julialang.org/blog/2021/01/precompile_tutorial/) and [SnoopCompile](https://julialang.org/blog/2021/01/snoopi_deep/) - * Read the [SnoopCompile](https://timholy.github.io/SnoopCompile.jl/stable/) + * Read the [SnoopCompile](https://juliadebug.github.io/SnoopCompile.jl/stable/) documentation. * Watch Tim Holy's [talk at JuliaCon 2021](https://www.youtube.com/watch?v=rVBgrWYKLHY) * Watch the [package development workshop at JuliaCon 2021](https://www.youtube.com/watch?v=wXRMwJdEjX4) diff --git a/docs/styles/config/vocabularies/JuMP/accept.txt b/docs/styles/config/vocabularies/JuMP/accept.txt index f526736192..1be6d39214 100644 --- a/docs/styles/config/vocabularies/JuMP/accept.txt +++ b/docs/styles/config/vocabularies/JuMP/accept.txt @@ -15,7 +15,6 @@ errored flamegraph getters [Jj]ulia -juxtaposted linkcheck nonlinearly nonzeros @@ -59,4 +58,4 @@ QSopt preprint Lubin Nemirovski -Xpress \ No newline at end of file +Xpress diff --git a/src/Bridges/Constraint/bridge.jl b/src/Bridges/Constraint/bridge.jl index 0320db5e1f..4337336b03 100644 --- a/src/Bridges/Constraint/bridge.jl +++ b/src/Bridges/Constraint/bridge.jl @@ -36,9 +36,9 @@ Return a `Bool` indicating whether the bridges of type `BT` support bridging constraint types that the bridge implements. """ function MOI.supports_constraint( - ::Type{<:AbstractBridge}, - ::Type{<:MOI.AbstractFunction}, - ::Type{<:MOI.AbstractSet}, + @nospecialize(BT::Type{<:AbstractBridge}), + @nospecialize(F::Type{<:MOI.AbstractFunction}), + @nospecialize(S::Type{<:MOI.AbstractSet}), ) return false end diff --git a/src/Bridges/Constraint/bridges/ComplexNormInfinityToSecondOrderConeBridge.jl b/src/Bridges/Constraint/bridges/ComplexNormInfinityToSecondOrderConeBridge.jl index 8a989883a2..86ec17f1ea 100644 --- a/src/Bridges/Constraint/bridges/ComplexNormInfinityToSecondOrderConeBridge.jl +++ b/src/Bridges/Constraint/bridges/ComplexNormInfinityToSecondOrderConeBridge.jl @@ -42,10 +42,8 @@ function bridge_constraint( ) where {T} fi_s = MOI.Utilities.scalarize(f) if !iszero(imag(fi_s[1])) - error( - "The epigraph variable `t` in `[t; x] in NormInfinityCone()` " * - "must be real. It is: $(fi_s[1])", - ) + msg = "The epigraph variable `t` in `[t; x] in NormInfinityCone()` must be real. It is: $(fi_s[1])" + throw(MOI.AddConstraintNotAllowed{typeof(f),typeof(s)}(msg)) end t = real(fi_s[1]) cis = MOI.ConstraintIndex{MOI.VectorAffineFunction{T},MOI.SecondOrderCone}[] diff --git a/src/Bridges/Constraint/bridges/IndicatorToMILPBridge.jl b/src/Bridges/Constraint/bridges/IndicatorToMILPBridge.jl index c109256e37..18ce4616a2 100644 --- a/src/Bridges/Constraint/bridges/IndicatorToMILPBridge.jl +++ b/src/Bridges/Constraint/bridges/IndicatorToMILPBridge.jl @@ -81,7 +81,7 @@ end function MOI.Bridges.added_constrained_variable_types( ::Type{<:IndicatorToMILPBridge}, ) - return Tuple{Type}[(MOI.Reals,)] + return Tuple{Type}[(MOI.Reals,), (MOI.ZeroOne,)] end function MOI.Bridges.added_constraint_types( @@ -188,16 +188,31 @@ end MOI.Bridges.needs_final_touch(::IndicatorToMILPBridge) = true +function _is_binary(model::MOI.ModelLike, f::MOI.AbstractScalarFunction) + x = convert(MOI.VariableIndex, f) + return _is_binary(model, x) +end + +function _is_binary(model::MOI.ModelLike, x::MOI.VariableIndex) + return MOI.is_valid( + model, + MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(x.value), + ) +end + function MOI.Bridges.final_touch( - bridge::IndicatorToMILPBridge{T,F}, + bridge::IndicatorToMILPBridge{T,F,A,S}, model::MOI.ModelLike, -) where {T,F} +) where {T,F,A,S} bounds = Dict{MOI.VariableIndex,NTuple{2,T}}() scalars = collect(MOI.Utilities.eachscalar(bridge.f)) fi = scalars[2] ret = MOI.Utilities.get_bounds(model, bounds, fi) if ret === nothing throw(MOI.Bridges.BridgeRequiresFiniteDomainError(bridge, fi)) + elseif !_is_binary(model, scalars[1]) + msg = "Unable to reformulate indicator constraint to a MILP. The indicator variable must be binary." + throw(MOI.AddConstraintNotAllowed{F,MOI.Indicator{A,S}}(msg)) end if bridge.slack === nothing # This is the first time calling final_touch diff --git a/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl b/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl index 4f31530e13..136d64905e 100644 --- a/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl +++ b/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl @@ -156,10 +156,7 @@ function MOI.Bridges.final_touch( if ret === bridge.last_bounds return nothing # final_touch already called elseif ret[1] == typemin(T) || ret[2] == typemax(T) - error( - "Unable to use IntegerToZeroOneBridge because the variable " * - "$(bridge.x) has a non-finite domain", - ) + throw(MOI.Bridges.BridgeRequiresFiniteDomainError(bridge, bridge.x)) end f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(T(1), bridge.x)], T(0)) lb, ub = ceil(Int, ret[1]), floor(Int, ret[2]) diff --git a/src/Bridges/Constraint/bridges/QuadtoSOCBridge.jl b/src/Bridges/Constraint/bridges/QuadtoSOCBridge.jl index 98364a5dcb..1031949636 100644 --- a/src/Bridges/Constraint/bridges/QuadtoSOCBridge.jl +++ b/src/Bridges/Constraint/bridges/QuadtoSOCBridge.jl @@ -256,12 +256,12 @@ function _primal_start_or_error(model, attr, v) var_attr = MOI.VariablePrimalStart() value = MOI.get(model, MOI.VariablePrimalStart(), v) if isnothing(value) - error( - "In order to set the `$attr`, the", - "`MOI.Bridges.Constraint.QuadtoSOCBridge` needs to get the ", - "`$var_attr` but it is not set. Set the `$var_attr` first before ", - "setting the `$attr` in order to fix this.", - ) + msg = + "In order to set the `$attr`, the " * + "`MOI.Bridges.Constraint.QuadtoSOCBridge` needs to get the " * + "`$var_attr` but it is not set. Set the `$var_attr` first before " * + "setting the `$attr` in order to fix this." + throw(MOI.SetAttributeNotAllowed(attr, msg)) end return value end diff --git a/src/Bridges/Constraint/bridges/SplitIntervalBridge.jl b/src/Bridges/Constraint/bridges/SplitIntervalBridge.jl index 0c9e501671..ab720e9f46 100644 --- a/src/Bridges/Constraint/bridges/SplitIntervalBridge.jl +++ b/src/Bridges/Constraint/bridges/SplitIntervalBridge.jl @@ -230,9 +230,8 @@ function MOI.get( if bridge.upper !== nothing return MOI.get(model, attr, bridge.upper) end - return error( - "Cannot get `$attr` for a constraint in the interval `[-Inf, Inf]`.", - ) + msg = "Cannot get `$attr` for a constraint in the interval `[-Inf, Inf]`." + return throw(MOI.GetAttributeNotAllowed(attr, msg)) end function MOI.set( @@ -333,10 +332,8 @@ function MOI.get( # The only case where the interval `[-∞, ∞]` is allowed is for # `VariableIndex` constraints but `ConstraintBasisStatus` is not # defined for `VariableIndex` constraints. - error( - "Cannot get `$attr` for a constraint in the interval " * - "`[-Inf, Inf]`.", - ) + msg = "Cannot get `$attr` for a constraint in the interval `[-Inf, Inf]`." + throw(MOI.GetAttributeNotAllowed(attr, msg)) end return upper_stat end diff --git a/src/Bridges/Constraint/bridges/SquareBridge.jl b/src/Bridges/Constraint/bridges/SquareBridge.jl index 0d3d40d0d8..ec8ad70d70 100644 --- a/src/Bridges/Constraint/bridges/SquareBridge.jl +++ b/src/Bridges/Constraint/bridges/SquareBridge.jl @@ -48,7 +48,8 @@ because the expressions are the same. `SquareBridge` creates: - * `G` in `TT` + * `F` in `TT` + * `G` in [`MOI.EqualTo{T}`](@ref) """ struct SquareBridge{ T, diff --git a/src/Bridges/Constraint/single_bridge_optimizer.jl b/src/Bridges/Constraint/single_bridge_optimizer.jl index 5839ac7c1a..496447f25e 100644 --- a/src/Bridges/Constraint/single_bridge_optimizer.jl +++ b/src/Bridges/Constraint/single_bridge_optimizer.jl @@ -105,9 +105,9 @@ function MOI.Bridges.supports_bridging_constrained_variable( end function MOI.Bridges.supports_bridging_constraint( - ::SingleBridgeOptimizer{BT}, - F::Type{<:MOI.AbstractFunction}, - S::Type{<:MOI.AbstractSet}, + @nospecialize(b::SingleBridgeOptimizer{BT}), + @nospecialize(F::Type{<:MOI.AbstractFunction}), + @nospecialize(S::Type{<:MOI.AbstractSet}), ) where {BT} return MOI.supports_constraint(BT, F, S) end diff --git a/src/Bridges/Objective/bridges/SlackBridge.jl b/src/Bridges/Objective/bridges/SlackBridge.jl index 9413f65c02..60b963697d 100644 --- a/src/Bridges/Objective/bridges/SlackBridge.jl +++ b/src/Bridges/Objective/bridges/SlackBridge.jl @@ -58,15 +58,16 @@ function bridge_objective( ) where {T,F,G<:MOI.AbstractScalarFunction} slack = MOI.add_variable(model) f = MOI.Utilities.operate(-, T, func, slack) - if MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE - set = MOI.LessThan(zero(T)) - elseif MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE - set = MOI.GreaterThan(zero(T)) + sense = MOI.get(model, MOI.ObjectiveSense()) + if sense == MOI.FEASIBILITY_SENSE + msg = "Set `MOI.ObjectiveSense` before `MOI.ObjectiveFunction` when using `MOI.Bridges.Objective.SlackBridge`." + throw(MOI.SetAttributeNotAllowed(MOI.ObjectiveFunction{G}(), msg)) + end + set = if sense == MOI.MIN_SENSE + MOI.LessThan(zero(T)) else - error( - "Set `MOI.ObjectiveSense` before `MOI.ObjectiveFunction` when", - " using `MOI.Bridges.Objective.SlackBridge`.", - ) + @assert sense == MOI.MAX_SENSE + MOI.GreaterThan(zero(T)) end constraint = MOI.Utilities.normalize_and_add_constraint(model, f, set) MOI.set(model, MOI.ObjectiveFunction{MOI.VariableIndex}(), slack) diff --git a/src/Bridges/Objective/bridges/VectorSlackBridge.jl b/src/Bridges/Objective/bridges/VectorSlackBridge.jl index 9a8d979e39..5c3227cef8 100644 --- a/src/Bridges/Objective/bridges/VectorSlackBridge.jl +++ b/src/Bridges/Objective/bridges/VectorSlackBridge.jl @@ -69,15 +69,14 @@ function bridge_objective( ) sense = MOI.get(model, MOI.ObjectiveSense()) if sense == MOI.FEASIBILITY_SENSE - error( - "Set `MOI.ObjectiveSense` before `MOI.ObjectiveFunction` when", - " using `MOI.Bridges.Objective.VectorSlackBridge`.", - ) + msg = "Set `MOI.ObjectiveSense` before `MOI.ObjectiveFunction` when using `MOI.Bridges.Objective.VectorSlackBridge`." + throw(MOI.SetAttributeNotAllowed(MOI.ObjectiveFunction{G}(), msg)) end slacks = MOI.VectorOfVariables(variables[slacked_objectives]) f = if sense == MOI.MIN_SENSE MOI.Utilities.operate(-, T, slacks, funcs[slacked_objectives]) - elseif sense == MOI.MAX_SENSE + else + @assert sense == MOI.MAX_SENSE MOI.Utilities.operate(-, T, funcs[slacked_objectives], slacks) end set = MOI.Nonnegatives(length(slacked_objectives)) diff --git a/src/Bridges/Variable/bridges/ZerosBridge.jl b/src/Bridges/Variable/bridges/ZerosBridge.jl index d2f005bcbd..7352055e4b 100644 --- a/src/Bridges/Variable/bridges/ZerosBridge.jl +++ b/src/Bridges/Variable/bridges/ZerosBridge.jl @@ -90,16 +90,16 @@ end function MOI.get( ::MOI.ModelLike, - ::MOI.ConstraintDual, + attr::MOI.ConstraintDual, ::ZerosBridge{T}, ) where {T} - return error( + msg = "Unable to query the dual of a variable bound that was reformulated " * "using `ZerosBridge`. This usually arises in conic solvers when a " * "variable is fixed to a value. As a work-around, instead of creating " * "a fixed variable using variable bounds like `p == 1`, add an affine " * - "equality constraint like `1 * p == 1` (or `[1 * p - 1,] in Zeros(1)`).", - ) + "equality constraint like `1 * p == 1` (or `[1 * p - 1,] in Zeros(1)`)." + return throw(MOI.GetAttributeNotAllowed(attr, msg)) end function MOI.get( diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 76fbae02d1..a45fbdc185 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -2032,36 +2032,32 @@ _check_double_single_variable(::AbstractBridgeOptimizer, ::Any, ::Any) = nothing function MOI.add_constraint( b::AbstractBridgeOptimizer, - f::MOI.AbstractFunction, - s::MOI.AbstractSet, -) + f::F, + s::S, +) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} if Variable.has_bridges(Variable.bridges(b)) - if f isa MOI.VariableIndex + if F <: MOI.VariableIndex if is_bridged(b, f) - if MOI.is_valid( - b, - MOI.ConstraintIndex{MOI.VariableIndex,typeof(s)}(f.value), - ) + ci = MOI.ConstraintIndex{MOI.VariableIndex,S}(f.value) + if MOI.is_valid(b, ci) # The other constraint could have been through a variable bridge. - error( - "Cannot add two `VariableIndex`-in-`$(typeof(s))`", - " on the same variable $(f).", - ) + msg = "Cannot add two `VariableIndex`-in-`$S` on the same variable $f." + throw(MOI.AddConstraintNotAllowed{F,S}(msg)) end BridgeType = Constraint.concrete_bridge_type( constraint_scalar_functionize_bridge(b), - typeof(f), - typeof(s), + F, + S, ) MOI.add_constraint(Variable.bridges(b), f, s) return add_bridged_constraint(b, BridgeType, f, s) end - elseif f isa MOI.VectorOfVariables + elseif F <: MOI.VectorOfVariables if any(vi -> is_bridged(b, vi), f.variables) BridgeType = Constraint.concrete_bridge_type( constraint_vector_functionize_bridge(b), - typeof(f), - typeof(s), + F, + S, ) return add_bridged_constraint(b, BridgeType, f, s) end @@ -2069,12 +2065,12 @@ function MOI.add_constraint( f, s = bridged_constraint_function(b, f, s) end end - if is_bridged(b, typeof(f), typeof(s)) + if is_bridged(b, F, S) _check_double_single_variable(b, f, s) # We compute `BridgeType` first as `concrete_bridge_type` calls # `bridge_type` which might throw an `UnsupportedConstraint` error in # which case, we do not want any modification to have been done - BridgeType = Constraint.concrete_bridge_type(b, typeof(f), typeof(s)) + BridgeType = Constraint.concrete_bridge_type(b, F, S) # `add_constraint` might throw an `UnsupportedConstraint` but no # modification has been done in the previous line return add_bridged_constraint(b, BridgeType, f, s) diff --git a/src/Bridges/lazy_bridge_optimizer.jl b/src/Bridges/lazy_bridge_optimizer.jl index 83870fc32c..37255c56b9 100644 --- a/src/Bridges/lazy_bridge_optimizer.jl +++ b/src/Bridges/lazy_bridge_optimizer.jl @@ -155,9 +155,9 @@ end Return the list of `VariableNode` that would be added if `BT` is used in `b`. """ function _variable_nodes( - b::LazyBridgeOptimizer, - ::Type{BT}, -) where {BT<:AbstractBridge} + @nospecialize(b::LazyBridgeOptimizer), + @nospecialize(BT), +) return map(added_constrained_variable_types(BT)) do (S,) return node(b, S)::VariableNode end @@ -169,9 +169,9 @@ end Return the list of `ConstraintNode` that would be added if `BT` is used in `b`. """ function _constraint_nodes( - b::LazyBridgeOptimizer, - ::Type{BT}, -) where {BT<:AbstractBridge} + @nospecialize(b::LazyBridgeOptimizer), + @nospecialize(BT), +) return ConstraintNode[ node(b, F, S) for (F, S) in added_constraint_types(BT) ] @@ -183,7 +183,11 @@ end Return the `Edge` or `ObjectiveEdge` in the hyper-graph associated with the bridge `BT`, where `index` is the index of `BT` in the list of bridges. """ -function _edge(b::LazyBridgeOptimizer, index::Int, BT::Type{<:AbstractBridge}) +function _edge( + @nospecialize(b::LazyBridgeOptimizer), + @nospecialize(index::Int), + @nospecialize(BT::Type{<:AbstractBridge}), +) return Edge( index, _variable_nodes(b, BT), @@ -247,7 +251,10 @@ end Return the `VariableNode` associated with set `S` in `b`. """ -function node(b::LazyBridgeOptimizer, S::Type{<:MOI.AbstractSet}) +function node( + @nospecialize(b::LazyBridgeOptimizer), + @nospecialize(S::Type{<:MOI.AbstractSet}), +) # If we support the set, the node is 0. if ( S <: MOI.AbstractScalarSet && @@ -304,9 +311,9 @@ end Return the `ConstraintNode` associated with constraint `F`-in-`S` in `b`. """ function node( - b::LazyBridgeOptimizer, - F::Type{<:MOI.AbstractFunction}, - S::Type{<:MOI.AbstractSet}, + @nospecialize(b::LazyBridgeOptimizer), + @nospecialize(F::Type{<:MOI.AbstractFunction}), + @nospecialize(S::Type{<:MOI.AbstractSet}), ) # If we support the constraint type, the node is 0. if MOI.supports_constraint(b.model, F, S) @@ -377,8 +384,8 @@ function _bridge_types( end function _bridge_types( - b::LazyBridgeOptimizer, - ::Type{<:Constraint.AbstractBridge}, + @nospecialize(b::LazyBridgeOptimizer), + @nospecialize(BT::Type{<:Constraint.AbstractBridge}), ) return b.constraint_bridge_types end @@ -395,7 +402,10 @@ end Enable the use of the bridges of type `BT` by `b`. """ -function add_bridge(b::LazyBridgeOptimizer, BT::Type{<:AbstractBridge}) +function add_bridge( + @nospecialize(b::LazyBridgeOptimizer), + @nospecialize(BT::Type{<:AbstractBridge}), +) if !has_bridge(b, BT) push!(_bridge_types(b, BT), BT) _reset_bridge_graph(b) @@ -427,7 +437,10 @@ end Return a `Bool` indicating whether the bridges of type `BT` are used by `b`. """ -function has_bridge(b::LazyBridgeOptimizer, BT::Type{<:AbstractBridge}) +function has_bridge( + @nospecialize(b::LazyBridgeOptimizer), + @nospecialize(BT::Type{<:AbstractBridge}), +)::Bool return findfirst(isequal(BT), _bridge_types(b, BT)) !== nothing end diff --git a/src/Test/Test.jl b/src/Test/Test.jl index 3332e3cc3c..18e8d5d0a2 100644 --- a/src/Test/Test.jl +++ b/src/Test/Test.jl @@ -500,9 +500,9 @@ function _test_attribute_value_type( end function _test_attribute_value_type( - model::MOI.ModelLike, - attribute::MOI.AbstractConstraintAttribute, - ci::MOI.ConstraintIndex, + @nospecialize(model::MOI.ModelLike), + @nospecialize(attribute::MOI.AbstractConstraintAttribute), + @nospecialize(ci::MOI.ConstraintIndex), ) T = MOI.attribute_value_type(attribute) @test @inferred(T, MOI.get(model, attribute, ci)) isa T diff --git a/src/Test/test_basic_constraint.jl b/src/Test/test_basic_constraint.jl index bc73cf2632..6e36bf7867 100644 --- a/src/Test/test_basic_constraint.jl +++ b/src/Test/test_basic_constraint.jl @@ -175,6 +175,31 @@ function _set(::Type{T}, ::Type{MOI.HyperRectangle}) where {T} return MOI.HyperRectangle(zeros(T, 3), ones(T, 3)) end +function _set(::Type{T}, ::Type{MOI.VectorNonlinearOracle}) where {T} + set = MOI.VectorNonlinearOracle(; + dimension = 3, + l = T[0, 0], + u = T[1, 0], + eval_f = (ret, x) -> begin + ret[1] = x[2]^2 + ret[2] = x[3]^2 + x[4]^3 - x[1] + return + end, + jacobian_structure = [(1, 2), (2, 1), (2, 3), (2, 4)], + eval_jacobian = (ret, x) -> begin + ret[1] = T(2) * x[2] + ret[2] = -T(1) + ret[3] = T(2) * x[3] + ret[4] = T(3) * x[4]^2 + return + end, + ) + x, ret_f, ret_J = T[1, 2, 3, 4, 5], T[0, 0], T[0, 0, 0, 0] + set.eval_f(ret_f, x) + set.eval_jacobian(ret_J, x) + return set +end + function _test_function_modification( model::MOI.ModelLike, config::Config{T}, @@ -392,6 +417,7 @@ for s in [ :Table, :Path, :HyperRectangle, + :VectorNonlinearOracle, ] S = getfield(MOI, s) functions = if S <: MOI.AbstractScalarSet diff --git a/src/Test/test_nonlinear.jl b/src/Test/test_nonlinear.jl index 79829fbaf7..609dccb5bc 100644 --- a/src/Test/test_nonlinear.jl +++ b/src/Test/test_nonlinear.jl @@ -2223,3 +2223,162 @@ function setup_test( end version_added(::typeof(test_nonlinear_quadratic_4)) = v"1.35.0" + +function test_vector_nonlinear_oracle( + model::MOI.ModelLike, + config::Config{T}, +) where {T} + @requires _supports(config, MOI.optimize!) + @requires MOI.supports_constraint( + model, + MOI.VectorOfVariables, + MOI.VectorNonlinearOracle{T}, + ) + set = MOI.VectorNonlinearOracle(; + dimension = 5, + l = T[0, 0], + u = T[0, 0], + eval_f = (ret, x) -> begin + @test length(ret) == 2 + @test length(x) == 5 + ret[1] = x[1]^2 - x[4] + ret[2] = x[2]^2 + x[3]^3 - x[5] + return + end, + jacobian_structure = [(1, 1), (2, 2), (2, 3), (1, 4), (2, 5)], + eval_jacobian = (ret, x) -> begin + @test length(ret) == 5 + @test length(x) == 5 + ret[1] = T(2) * x[1] + ret[2] = T(2) * x[2] + ret[3] = T(3) * x[3]^2 + ret[4] = -T(1) + ret[5] = -T(1) + return + end, + hessian_lagrangian_structure = [(1, 1), (2, 2), (3, 3)], + eval_hessian_lagrangian = (ret, x, u) -> begin + @test length(ret) == 3 + @test length(x) == 5 + @test length(u) == 2 + ret[1] = T(2) * u[1] + ret[2] = T(2) * u[2] + ret[3] = T(6) * x[3] * u[2] + return + end, + ) + @test MOI.dimension(set) == 5 + x = T[1, 2, 3, 4, 5] + ret = T[0, 0] + set.eval_f(ret, x) + @test ret == T[-3, 26] + ret = T[0, 0, 0, 0, 0] + set.eval_jacobian(ret, x) + @test ret == T[2, 4, 27, -1, -1] + ret = T[0, 0, 0] + set.eval_hessian_lagrangian(ret, x, T[2, 3]) + @test ret == T[4, 6, 54] + x, y = MOI.add_variables(model, 3), MOI.add_variables(model, 2) + MOI.add_constraints.(model, x, MOI.EqualTo.(T(1):T(3))) + c = MOI.add_constraint(model, MOI.VectorOfVariables([x; y]), set) + MOI.optimize!(model) + x_v = MOI.get.(model, MOI.VariablePrimal(), x) + y_v = MOI.get.(model, MOI.VariablePrimal(), y) + @test ≈(y_v, [x_v[1]^2, x_v[2]^2 + x_v[3]^3], config) + @test ≈(MOI.get(model, MOI.ConstraintPrimal(), c), [x_v; y_v], config) + @test ≈(MOI.get(model, MOI.ConstraintDual(), c), zeros(T, 5), config) + return +end + +function setup_test( + ::typeof(test_vector_nonlinear_oracle), + model::MOIU.MockOptimizer, + config::Config{T}, +) where {T} + MOI.Utilities.set_mock_optimize!( + model, + mock -> MOI.Utilities.mock_optimize!( + mock, + config.optimal_status, + T[1, 2, 3, 1, 31], + (MOI.VectorOfVariables, MOI.VectorNonlinearOracle{T}) => + [zeros(T, 5)], + ), + ) + model.eval_variable_constraint_dual = false + return () -> model.eval_variable_constraint_dual = true +end + +version_added(::typeof(test_vector_nonlinear_oracle)) = v"1.46.0" + +function test_vector_nonlinear_oracle_no_hessian( + model::MOI.ModelLike, + config::Config{T}, +) where {T} + @requires _supports(config, MOI.optimize!) + @requires MOI.supports_constraint( + model, + MOI.VectorOfVariables, + MOI.VectorNonlinearOracle{T}, + ) + set = MOI.VectorNonlinearOracle(; + dimension = 5, + l = T[0, 0], + u = T[0, 0], + eval_f = (ret, x) -> begin + ret[1] = x[1]^2 - x[4] + ret[2] = x[2]^2 + x[3]^3 - x[5] + return + end, + jacobian_structure = [(1, 1), (2, 2), (2, 3), (1, 4), (2, 5)], + eval_jacobian = (ret, x) -> begin + ret[1] = T(2) * x[1] + ret[2] = T(2) * x[2] + ret[3] = T(3) * x[3]^2 + ret[4] = -T(1) + ret[5] = -T(1) + return + end, + ) + @test MOI.dimension(set) == 5 + x = T[1, 2, 3, 4, 5] + ret = T[0, 0] + set.eval_f(ret, x) + @test ret == T[-3, 26] + ret = T[0, 0, 0, 0, 0] + set.eval_jacobian(ret, x) + @test ret == T[2, 4, 27, -1, -1] + @test isempty(set.hessian_lagrangian_structure) + @test set.eval_hessian_lagrangian === nothing + x, y = MOI.add_variables(model, 3), MOI.add_variables(model, 2) + MOI.add_constraints.(model, x, MOI.EqualTo.(T(1):T(3))) + c = MOI.add_constraint(model, MOI.VectorOfVariables([x; y]), set) + MOI.optimize!(model) + x_v = MOI.get.(model, MOI.VariablePrimal(), x) + y_v = MOI.get.(model, MOI.VariablePrimal(), y) + @test ≈(y_v, [x_v[1]^2, x_v[2]^2 + x_v[3]^3], config) + @test ≈(MOI.get(model, MOI.ConstraintPrimal(), c), [x_v; y_v], config) + @test ≈(MOI.get(model, MOI.ConstraintDual(), c), zeros(T, 5), config) + return +end + +function setup_test( + ::typeof(test_vector_nonlinear_oracle_no_hessian), + model::MOIU.MockOptimizer, + config::Config{T}, +) where {T} + MOI.Utilities.set_mock_optimize!( + model, + mock -> MOI.Utilities.mock_optimize!( + mock, + config.optimal_status, + T[1, 2, 3, 1, 31], + (MOI.VectorOfVariables, MOI.VectorNonlinearOracle{T}) => + [zeros(T, 5)], + ), + ) + model.eval_variable_constraint_dual = false + return () -> model.eval_variable_constraint_dual = true +end + +version_added(::typeof(test_vector_nonlinear_oracle_no_hessian)) = v"1.46.0" diff --git a/src/Utilities/model.jl b/src/Utilities/model.jl index 184488c3b8..600e97158a 100644 --- a/src/Utilities/model.jl +++ b/src/Utilities/model.jl @@ -843,6 +843,7 @@ const EqualToIndicatorZero{T} = MOI.Table, MOI.BinPacking, MOI.HyperRectangle, + MOI.VectorNonlinearOracle, ), (MOI.ScalarNonlinearFunction,), (MOI.ScalarAffineFunction, MOI.ScalarQuadraticFunction), diff --git a/src/constraints.jl b/src/constraints.jl index 081b4235d2..65f6fd27dc 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -19,8 +19,8 @@ combined with another type of constraint, it should still return `true`. """ function supports_constraint( ::ModelLike, - ::Type{<:AbstractFunction}, - ::Type{<:AbstractSet}, + F::Type{<:AbstractFunction}, + S::Type{<:AbstractSet}, ) return false end diff --git a/src/sets.jl b/src/sets.jl index b564a338b9..857b85774c 100644 --- a/src/sets.jl +++ b/src/sets.jl @@ -2666,6 +2666,192 @@ function Base.:(==)(x::Reified{S}, y::Reified{S}) where {S} return x.set == y.set end +""" + VectorNonlinearOracle(; + dimension::Int, + l::Vector{Float64}, + u::Vector{Float64}, + eval_f::Function, + jacobian_structure::Vector{Tuple{Int,Int}}, + eval_jacobian::Function, + hessian_lagrangian_structure::Vector{Tuple{Int,Int}} = Tuple{Int,Int}[], + eval_hessian_lagrangian::Union{Nothing,Function} = nothing, + ) <: AbstractVectorSet + +The set: +```math +S = \\{x \\in \\mathbb{R}^{dimension}: l \\le f(x) \\le u\\} +``` +where ``f`` is defined by the vectors `l` and `u`, and the callback oracles +`eval_f`, `eval_jacobian`, and `eval_hessian_lagrangian`. + +## f + +The `eval_f` function must have the signature +```julia +eval_f(ret::AbstractVector, x::AbstractVector)::Nothing +``` +which fills ``f(x)`` into the dense vector `ret`. + +## Jacobian + +The `eval_jacobian` function must have the signature +```julia +eval_jacobian(ret::AbstractVector, x::AbstractVector)::Nothing +``` +which fills the sparse Jacobian ``\\nabla f(x)`` into `ret`. + +The one-indexed sparsity structure must be provided in the `jacobian_structure` +argument. + +## Hessian + +The `eval_hessian_lagrangian` function is optional. + +If `eval_hessian_lagrangian === nothing`, Ipopt will use a Hessian approximation +instead of the exact Hessian. + +If `eval_hessian_lagrangian` is a function, it must have the signature +```julia +eval_hessian_lagrangian( + ret::AbstractVector, + x::AbstractVector, + μ::AbstractVector, +)::Nothing +``` +which fills the sparse Hessian of the Lagrangian ``\\sum \\mu_i \\nabla^2 f_i(x)`` +into `ret`. + +The one-indexed sparsity structure must be provided in the +`hessian_lagrangian_structure` argument. + +## Example + +To model the set: +```math +\\begin{align} +0 \\le & x^2 \\le 1 +0 \\le & y^2 + z^3 - w \\le 0 +\\end{align} +``` +do +```jldoctest +julia> import MathOptInterface as MOI + +julia> set = MOI.VectorNonlinearOracle(; + dimension = 3, + l = [0.0, 0.0], + u = [1.0, 0.0], + eval_f = (ret, x) -> begin + ret[1] = x[2]^2 + ret[2] = x[3]^2 + x[4]^3 - x[1] + return + end, + jacobian_structure = [(1, 2), (2, 1), (2, 3), (2, 4)], + eval_jacobian = (ret, x) -> begin + ret[1] = 2.0 * x[2] + ret[2] = -1.0 + ret[3] = 2.0 * x[3] + ret[4] = 3.0 * x[4]^2 + return + end, + hessian_lagrangian_structure = [(2, 2), (3, 3), (4, 4)], + eval_hessian_lagrangian = (ret, x, u) -> begin + ret[1] = 2.0 * u[1] + ret[2] = 2.0 * u[2] + ret[3] = 6.0 * x[4] * u[2] + return + end, + ); + +julia> set +VectorNonlinearOracle{Float64}(; + dimension = 3, + l = [0.0, 0.0], + u = [1.0, 0.0], + ..., +) +``` +""" +struct VectorNonlinearOracle{T} <: AbstractVectorSet + input_dimension::Int + output_dimension::Int + l::Vector{T} + u::Vector{T} + eval_f::Function + jacobian_structure::Vector{Tuple{Int,Int}} + eval_jacobian::Function + hessian_lagrangian_structure::Vector{Tuple{Int,Int}} + eval_hessian_lagrangian::Union{Nothing,Function} + + function VectorNonlinearOracle(; + dimension::Int, + l::Vector{T}, + u::Vector{T}, + eval_f::Function, + jacobian_structure::Vector{Tuple{Int,Int}}, + eval_jacobian::Function, + # The hessian_lagrangian is optional. + hessian_lagrangian_structure::Vector{Tuple{Int,Int}} = Tuple{Int,Int}[], + eval_hessian_lagrangian::Union{Nothing,Function} = nothing, + ) where {T} + if length(l) != length(u) + throw(DimensionMismatch()) + end + return new{T}( + dimension, + length(l), + l, + u, + eval_f, + jacobian_structure, + eval_jacobian, + hessian_lagrangian_structure, + eval_hessian_lagrangian, + ) + end +end + +dimension(s::VectorNonlinearOracle) = s.input_dimension + +function Base.copy(s::VectorNonlinearOracle) + return VectorNonlinearOracle(; + dimension = s.input_dimension, + l = copy(s.l), + u = copy(s.u), + eval_f = s.eval_f, + jacobian_structure = copy(s.jacobian_structure), + eval_jacobian = s.eval_jacobian, + hessian_lagrangian_structure = copy(s.hessian_lagrangian_structure), + eval_hessian_lagrangian = s.eval_hessian_lagrangian, + ) +end + +function Base.:(==)( + x::VectorNonlinearOracle{T}, + y::VectorNonlinearOracle{T}, +) where {T} + return x.input_dimension == y.input_dimension && + x.output_dimension == y.output_dimension && + x.l == y.l && + x.u == y.u && + x.eval_f == y.eval_f && + x.jacobian_structure == y.jacobian_structure && + x.eval_jacobian == y.eval_jacobian && + x.hessian_lagrangian_structure == y.hessian_lagrangian_structure && + x.eval_hessian_lagrangian == y.eval_hessian_lagrangian +end + +function Base.show(io::IO, s::VectorNonlinearOracle{T}) where {T} + println(io, "VectorNonlinearOracle{$T}(;") + println(io, " dimension = ", s.input_dimension, ",") + println(io, " l = ", s.l, ",") + println(io, " u = ", s.u, ",") + println(io, " ...,") + print(io, ")") + return +end + # TODO(odow): these are not necessarily isbits. They may not be safe to return # without copying if the number is BigFloat, for example. function Base.copy( diff --git a/test/Bridges/Constraint/ComplexNormInfinityToSecondOrderConeBridge.jl b/test/Bridges/Constraint/ComplexNormInfinityToSecondOrderConeBridge.jl index 4c4c48dbaa..dd7dd92bdf 100644 --- a/test/Bridges/Constraint/ComplexNormInfinityToSecondOrderConeBridge.jl +++ b/test/Bridges/Constraint/ComplexNormInfinityToSecondOrderConeBridge.jl @@ -58,7 +58,7 @@ function test_imag_t() f_x = (1.0 + 2.0im) * x[2] + (3.0 + 4.0im) f = MOI.Utilities.operate(vcat, Complex{Float64}, f_t, f_x) @test_throws( - ErrorException( + MOI.AddConstraintNotAllowed{typeof(f),MOI.NormInfinityCone}( "The epigraph variable `t` in `[t; x] in NormInfinityCone()` " * "must be real. It is: $f_t", ), diff --git a/test/Bridges/Constraint/IndicatorToMILPBridge.jl b/test/Bridges/Constraint/IndicatorToMILPBridge.jl index 95e7a6bfa5..2f4a34668f 100644 --- a/test/Bridges/Constraint/IndicatorToMILPBridge.jl +++ b/test/Bridges/Constraint/IndicatorToMILPBridge.jl @@ -301,6 +301,59 @@ function test_delete_before_final_touch() return end +function test_runtests_error_not_binary() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.IndicatorToMILP{Int}(inner) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, x[2], MOI.Interval(0, 4)) + set = MOI.Indicator{MOI.ACTIVATE_ON_ZERO}(MOI.GreaterThan(2)) + c = MOI.add_constraint(model, MOI.VectorOfVariables(x), set) + @test_throws( + MOI.AddConstraintNotAllowed{MOI.VectorOfVariables,typeof(set)}( + "Unable to reformulate indicator constraint to a MILP. The indicator variable must be binary.", + ), + MOI.Bridges.final_touch(model), + ) + return +end + +MOI.Utilities.@model( + Model2867, + (), + (MOI.EqualTo, MOI.LessThan), + (), + (), + (), + (MOI.ScalarAffineFunction,), + (), + () +) + +function MOI.supports_constraint( + model::Model2867, + ::Type{MOI.VariableIndex}, + ::Type{<:Union{MOI.Integer,MOI.ZeroOne}}, +) + return get(model.ext, :supports, false) +end + +function test_issue_2867() + model = MOI.instantiate(Model2867{Float64}; with_bridge_type = Float64) + @test !MOI.supports_constraint( + model, + MOI.VectorOfVariables, + MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.EqualTo{Float64}}, + ) + model = MOI.instantiate(Model2867{Float64}; with_bridge_type = Float64) + model.model.ext[:supports] = true + @test MOI.supports_constraint( + model, + MOI.VectorOfVariables, + MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.EqualTo{Float64}}, + ) + return +end + end # module TestConstraintIndicatorToMILP.runtests() diff --git a/test/Bridges/Constraint/IntegerToZeroOneBridge.jl b/test/Bridges/Constraint/IntegerToZeroOneBridge.jl index cc2f4bfa7a..936cec85ae 100644 --- a/test/Bridges/Constraint/IntegerToZeroOneBridge.jl +++ b/test/Bridges/Constraint/IntegerToZeroOneBridge.jl @@ -78,10 +78,7 @@ function test_finite_domain_error() model = MOI.Bridges.Constraint.IntegerToZeroOne{Int}(inner) x, _ = MOI.add_constrained_variable(model, MOI.Integer()) @test_throws( - ErrorException( - "Unable to use IntegerToZeroOneBridge because the variable " * - "$(x) has a non-finite domain", - ), + MOI.Bridges.BridgeRequiresFiniteDomainError, MOI.Bridges.final_touch(model), ) return diff --git a/test/Bridges/Constraint/QuadtoSOCBridge.jl b/test/Bridges/Constraint/QuadtoSOCBridge.jl index 0c460cf9cf..20db1a09df 100644 --- a/test/Bridges/Constraint/QuadtoSOCBridge.jl +++ b/test/Bridges/Constraint/QuadtoSOCBridge.jl @@ -164,9 +164,8 @@ function test_qcp() S = MOI.GreaterThan{Float64} ci = first(MOI.get(bridged_mock, MOI.ListOfConstraintIndices{F,S}())) for attr in [MOI.ConstraintPrimalStart(), MOI.ConstraintDualStart()] - err = ErrorException( - "In order to set the `$attr`, the`MOI.Bridges.Constraint.QuadtoSOCBridge` needs to get the `MathOptInterface.VariablePrimalStart()` but it is not set. Set the `MathOptInterface.VariablePrimalStart()` first before setting the `$attr` in order to fix this.", - ) + msg = "In order to set the `$attr`, the `MOI.Bridges.Constraint.QuadtoSOCBridge` needs to get the `MathOptInterface.VariablePrimalStart()` but it is not set. Set the `MathOptInterface.VariablePrimalStart()` first before setting the `$attr` in order to fix this." + err = MOI.SetAttributeNotAllowed(attr, msg) @test_throws err MOI.set(bridged_mock, attr, ci, 0.0) end return diff --git a/test/Bridges/Constraint/SplitIntervalBridge.jl b/test/Bridges/Constraint/SplitIntervalBridge.jl index 21395cad26..488a8a490a 100644 --- a/test/Bridges/Constraint/SplitIntervalBridge.jl +++ b/test/Bridges/Constraint/SplitIntervalBridge.jl @@ -320,10 +320,7 @@ function _test_interval( MOI.ConstraintPrimalStart(), MOI.ConstraintBasisStatus(), ] - err = ErrorException( - "Cannot get `$attr` for a constraint " * - "in the interval `[-Inf, Inf]`.", - ) + err = MOI.GetAttributeNotAllowed{typeof(attr)} @test_throws err MOI.get(bridged_mock, attr, ci) end @test zero(T) == MOI.get(bridged_mock, MOI.ConstraintDualStart(), ci) diff --git a/test/Bridges/Objective/SlackBridge.jl b/test/Bridges/Objective/SlackBridge.jl index 9757aae392..2295c5b036 100644 --- a/test/Bridges/Objective/SlackBridge.jl +++ b/test/Bridges/Objective/SlackBridge.jl @@ -28,12 +28,14 @@ function test_SlackBridge_ObjectiveSense_error() model = MOI.Bridges.Objective.Slack{Float64}(inner) x = MOI.add_variable(model) f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.1, x)], 1.2) + attr = MOI.ObjectiveFunction{typeof(f)}() @test_throws( - ErrorException( - "Set `MOI.ObjectiveSense` before `MOI.ObjectiveFunction` when" * - " using `MOI.Bridges.Objective.SlackBridge`.", + MOI.SetAttributeNotAllowed( + attr, + "Set `MOI.ObjectiveSense` before `MOI.ObjectiveFunction` when " * + "using `MOI.Bridges.Objective.SlackBridge`.", ), - MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f), + MOI.set(model, attr, f), ) return end diff --git a/test/Bridges/Objective/VectorSlackBridge.jl b/test/Bridges/Objective/VectorSlackBridge.jl index 142007c4fa..c7342bc4e2 100644 --- a/test/Bridges/Objective/VectorSlackBridge.jl +++ b/test/Bridges/Objective/VectorSlackBridge.jl @@ -115,11 +115,15 @@ function test_objective_sense_before_function() model = MOI.Bridges.Objective.VectorSlack{Float64}(inner) x = MOI.add_variable(model) f = MOI.Utilities.operate(vcat, Float64, 1.0 * x, 1.0 * x) - err = ErrorException( - "Set `MOI.ObjectiveSense` before `MOI.ObjectiveFunction` when using " * - "`MOI.Bridges.Objective.VectorSlackBridge`.", + attr = MOI.ObjectiveFunction{typeof(f)}() + @test_throws( + MOI.SetAttributeNotAllowed( + attr, + "Set `MOI.ObjectiveSense` before `MOI.ObjectiveFunction` when " * + "using `MOI.Bridges.Objective.VectorSlackBridge`.", + ), + MOI.set(model, attr, f), ) - @test_throws err MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) return end diff --git a/test/Bridges/Variable/VectorizeBridge.jl b/test/Bridges/Variable/VectorizeBridge.jl index 0f732cbc8e..e24570ddc1 100644 --- a/test/Bridges/Variable/VectorizeBridge.jl +++ b/test/Bridges/Variable/VectorizeBridge.jl @@ -128,7 +128,7 @@ function test_exp3_with_add_constrained_variable_y() @test MOI.get(bridged_mock, MOI.ConstraintDual(), ec) ≈ [-1.0, log(5) - 1, 1 / 5] - err = ErrorException( + err = MOI.AddConstraintNotAllowed{MOI.VariableIndex,MOI.LessThan{Float64}}( "Cannot add two `VariableIndex`-in-`MathOptInterface.LessThan{Float64}`" * " on the same variable MOI.VariableIndex(-1).", ) diff --git a/test/Bridges/Variable/zeros.jl b/test/Bridges/Variable/zeros.jl index a531b199ef..5b73159ab3 100644 --- a/test/Bridges/Variable/zeros.jl +++ b/test/Bridges/Variable/zeros.jl @@ -175,7 +175,8 @@ function test_zeros() @test MOI.get(bridged_mock, MOI.ConstraintDual(), c1) == 0.0 @test MOI.get(bridged_mock, MOI.ConstraintDual(), c2) == 1.0 attr = MOI.ConstraintDual() - err = ErrorException( + err = MOI.GetAttributeNotAllowed( + attr, "Unable to query the dual of a variable bound that was reformulated " * "using `ZerosBridge`. This usually arises in conic solvers when a " * "variable is fixed to a value. As a work-around, instead of creating " * diff --git a/test/General/sets.jl b/test/General/sets.jl index fdc32648a4..cffd2d985d 100644 --- a/test/General/sets.jl +++ b/test/General/sets.jl @@ -419,6 +419,54 @@ function test_update_dimension() return end +function test_VectorNonlinearOracle() + @test_throws( + DimensionMismatch, + MOI.VectorNonlinearOracle(; + dimension = 3, + l = [0.0, 0.0, 1.0], + u = [1.0, 0.0], + eval_f = (ret, x) -> nothing, + jacobian_structure = Tuple{Int,Int}[], + eval_jacobian = (ret, x) -> nothing, + ), + ) + set = MOI.VectorNonlinearOracle(; + dimension = 3, + l = [0.0, 0.0], + u = [1.0, 0.0], + eval_f = (ret, x) -> begin + ret[1] = x[2]^2 + ret[2] = x[3]^2 + x[4]^3 - x[1] + return + end, + jacobian_structure = [(1, 2), (2, 1), (2, 3), (2, 4)], + eval_jacobian = (ret, x) -> begin + ret[1] = 2.0 * x[2] + ret[2] = -1.0 + ret[3] = 2.0 * x[3] + ret[4] = 3.0 * x[4]^2 + return + end, + hessian_lagrangian_structure = [(2, 2), (3, 3), (4, 4)], + eval_hessian_lagrangian = (ret, x, u) -> begin + ret[1] = 2.0 * u[1] + ret[2] = 2.0 * u[2] + ret[3] = 6.0 * x[4] * u[2] + return + end, + ) + contents = """ + VectorNonlinearOracle{Float64}(; + dimension = 3, + l = [0.0, 0.0], + u = [1.0, 0.0], + ..., + )""" + @test sprint(show, set) == contents + return +end + end # module TestSets.runtests()