From 92f73ae2464e4969eae10e7c4b209ada361b5086 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 14 Aug 2023 12:06:48 +1200 Subject: [PATCH] Add VectorNonlinearFunction (#2201) --- docs/src/manual/standard_form.md | 1 + docs/src/reference/standard_form.md | 1 + src/Bridges/Constraint/bridges/square.jl | 84 +++++++---- src/Bridges/Constraint/bridges/vectorize.jl | 8 -- src/Test/test_basic_constraint.jl | 19 ++- src/Test/test_multiobjective.jl | 79 ++++++++++ src/Test/test_nonlinear.jl | 90 ++++++++++++ src/Utilities/functions.jl | 91 +++++++++++- src/Utilities/model.jl | 2 +- src/Utilities/objective_container.jl | 42 ++++++ src/Utilities/operate.jl | 152 +++++++++++++++++++- src/Utilities/parser.jl | 9 ++ src/Utilities/promote_operation.jl | 65 ++++++++- src/functions.jl | 76 ++++++++++ test/Bridges/Constraint/flip_sign.jl | 11 ++ test/Bridges/Constraint/scalarize.jl | 47 ++++++ test/Bridges/Constraint/square.jl | 74 ++++++++++ test/Bridges/Constraint/vectorize.jl | 25 ++++ test/Utilities/functions.jl | 61 ++++++++ test/Utilities/test_operate!.jl | 79 ++++++++-- test/Utilities/test_promote_operation.jl | 40 +++--- 21 files changed, 979 insertions(+), 77 deletions(-) diff --git a/docs/src/manual/standard_form.md b/docs/src/manual/standard_form.md index 2d8855e21a..431c1fc03b 100644 --- a/docs/src/manual/standard_form.md +++ b/docs/src/manual/standard_form.md @@ -41,6 +41,7 @@ The function types implemented in MathOptInterface.jl are: | [`VectorAffineFunction`](@ref) | ``A x + b``, where ``A`` is a matrix and ``b`` is a vector. | | [`ScalarQuadraticFunction`](@ref) | ``\frac{1}{2} x^T Q x + a^T x + b``, where ``Q`` is a symmetric matrix, ``a`` is a vector, and ``b`` is a constant. | | [`VectorQuadraticFunction`](@ref) | A vector of scalar-valued quadratic functions. | +| [`VectorNonlinearFunction`](@ref) | ``f(x)``, where ``f`` is a vector-valued nonlinear function. | Extensions for nonlinear programming are present but not yet well documented. diff --git a/docs/src/reference/standard_form.md b/docs/src/reference/standard_form.md index e4f77fe341..9b4e43ab50 100644 --- a/docs/src/reference/standard_form.md +++ b/docs/src/reference/standard_form.md @@ -37,6 +37,7 @@ VectorAffineTerm VectorAffineFunction VectorQuadraticTerm VectorQuadraticFunction +VectorNonlinearFunction ``` ## Sets diff --git a/src/Bridges/Constraint/bridges/square.jl b/src/Bridges/Constraint/bridges/square.jl index 09787e3db0..450499d942 100644 --- a/src/Bridges/Constraint/bridges/square.jl +++ b/src/Bridges/Constraint/bridges/square.jl @@ -76,6 +76,58 @@ _square_offset(::MOI.AbstractSymmetricMatrixSetSquare) = Int[] _square_offset(::MOI.RootDetConeSquare) = Int[1] _square_offset(::MOI.LogDetConeSquare) = Int[1, 2] +function _constrain_off_diagonals( + model::MOI.ModelLike, + ::Type{T}, + ::Tuple{Int,Int}, + f_ij::F, + f_ji::F, +) where {T,F<:MOI.ScalarNonlinearFunction} + if isapprox(f_ij, f_ji) + return nothing + end + return MOI.Utilities.normalize_and_add_constraint( + model, + MOI.ScalarNonlinearFunction(:-, Any[f_ij, f_ji]), + MOI.EqualTo(zero(T)); + allow_modify_function = true, + ) +end + +function _constrain_off_diagonals( + model::MOI.ModelLike, + ::Type{T}, + ij::Tuple{Int,Int}, + f_ij::F, + f_ji::F, +) where {T,F} + diff = MOI.Utilities.operate!(-, T, f_ij, f_ji) + MOI.Utilities.canonicalize!(diff) + # The value 1e-10 was decided in https://github.com/jump-dev/JuMP.jl/pull/976 + # This avoid generating symmetrization constraints when the + # functions at entries (i, j) and (j, i) are almost identical + if MOI.Utilities.isapprox_zero(diff, 1e-10) + return nothing + end + if MOI.Utilities.isapprox_zero(diff, 1e-8) + i, j = ij + @warn( + "The entries ($i, $j) and ($j, $i) of the matrix are " * + "almost identical, but a constraint has been added " * + "to ensure their equality because the largest " * + "difference between the coefficients is smaller than " * + "1e-8 but larger than 1e-10. This usually means that " * + "there is a modeling error in your formulation.", + ) + end + return MOI.Utilities.normalize_and_add_constraint( + model, + diff, + MOI.EqualTo(zero(T)); + allow_modify_function = true, + ) +end + function bridge_constraint( ::Type{SquareBridge{T,F,G,TT,ST}}, model::MOI.ModelLike, @@ -93,32 +145,14 @@ function bridge_constraint( for i in 1:j k += 1 push!(upper_triangle_indices, k) - # We constrain the entries (i, j) and (j, i) to be equal - f_ij = f_scalars[offset+i+(j-1)*dim] - f_ji = f_scalars[offset+j+(i-1)*dim] - diff = MOI.Utilities.operate!(-, T, f_ij, f_ji) - MOI.Utilities.canonicalize!(diff) - # The value 1e-10 was decided in https://github.com/jump-dev/JuMP.jl/pull/976 - # This avoid generating symmetrization constraints when the - # functions at entries (i, j) and (j, i) are almost identical - if !MOI.Utilities.isapprox_zero(diff, 1e-10) - if MOI.Utilities.isapprox_zero(diff, 1e-8) - @warn( - "The entries ($i, $j) and ($j, $i) of the matrix are " * - "almost identical, but a constraint has been added " * - "to ensure their equality because the largest " * - "difference between the coefficients is smaller than " * - "1e-8 but larger than 1e-10. This usually means that " * - "there is a modeling error in your formulation.", - ) + if i !== j + # We constrain the entries (i, j) and (j, i) to be equal + f_ij = f_scalars[offset+i+(j-1)*dim] + f_ji = f_scalars[offset+j+(i-1)*dim] + ci = _constrain_off_diagonals(model, T, (i, j), f_ij, f_ji) + if ci !== nothing + push!(sym, (i, j) => ci) end - ci = MOI.Utilities.normalize_and_add_constraint( - model, - diff, - MOI.EqualTo(zero(T)); - allow_modify_function = true, - ) - push!(sym, (i, j) => ci) end end k += dim - j diff --git a/src/Bridges/Constraint/bridges/vectorize.jl b/src/Bridges/Constraint/bridges/vectorize.jl index 2a26007c5f..142badcc49 100644 --- a/src/Bridges/Constraint/bridges/vectorize.jl +++ b/src/Bridges/Constraint/bridges/vectorize.jl @@ -67,14 +67,6 @@ function MOI.supports_constraint( return true end -function MOI.supports_constraint( - ::Type{VectorizeBridge{T}}, - ::Type{MOI.ScalarNonlinearFunction}, - ::Type{<:MOI.Utilities.ScalarLinearSet{T}}, -) where {T} - return false -end - function MOI.Bridges.added_constrained_variable_types(::Type{<:VectorizeBridge}) return Tuple{Type}[] end diff --git a/src/Test/test_basic_constraint.jl b/src/Test/test_basic_constraint.jl index 061f0821f4..6f7dae5a41 100644 --- a/src/Test/test_basic_constraint.jl +++ b/src/Test/test_basic_constraint.jl @@ -77,6 +77,18 @@ function _function( ) end +function _function( + ::Type{T}, + ::Type{MOI.VectorNonlinearFunction}, + x::Vector{MOI.VariableIndex}, +) where {T} + f = MOI.ScalarNonlinearFunction( + :+, + Any[MOI.ScalarNonlinearFunction(:^, Any[xi, 2]) for xi in x], + ) + return MOI.VectorNonlinearFunction([f; x]) +end + # Default fallback. _set(::Any, ::Type{S}) where {S} = _set(S) @@ -334,7 +346,12 @@ for s in [ :ScalarNonlinearFunction, ) else - (:VectorOfVariables, :VectorAffineFunction, :VectorQuadraticFunction) + ( + :VectorOfVariables, + :VectorAffineFunction, + :VectorQuadraticFunction, + :VectorNonlinearFunction, + ) end for f in functions func = Symbol("test_basic_$(f)_$(s)") diff --git a/src/Test/test_multiobjective.jl b/src/Test/test_multiobjective.jl index eeeb29499e..c33b4d14b5 100644 --- a/src/Test/test_multiobjective.jl +++ b/src/Test/test_multiobjective.jl @@ -228,3 +228,82 @@ function test_multiobjective_vector_quadratic_function_delete_vector( @test MOI.get(model, MOI.ObjectiveFunction{F}()) ≈ new_f return end + +function test_multiobjective_vector_nonlinear( + model::MOI.ModelLike, + ::Config{T}, +) where {T} + F = MOI.VectorNonlinearFunction + @requires MOI.supports(model, MOI.ObjectiveFunction{F}()) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.GreaterThan(T(0))) + f = MOI.VectorNonlinearFunction( + Any[MOI.ScalarNonlinearFunction(:^, Any[x[1], 2]), x[2]], + ) # [x[1]^2, x[2]] + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{F}(), f) + @test MOI.get(model, MOI.ObjectiveFunctionType()) == F + @test MOI.get(model, MOI.ObjectiveFunction{F}()) ≈ f + return +end + +function test_multiobjective_vector_nonlinear_delete( + model::MOI.ModelLike, + ::Config{T}, +) where {T} + F = MOI.VectorNonlinearFunction + @requires MOI.supports(model, MOI.ObjectiveFunction{F}()) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.GreaterThan(T(0))) + f = MOI.VectorNonlinearFunction( + Any[MOI.ScalarNonlinearFunction(:^, Any[x[1], 2]), x[2]], + ) # [x[1]^2, x[2]] + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{F}(), f) + @test MOI.get(model, MOI.ObjectiveFunctionType()) == F + @test MOI.get(model, MOI.ObjectiveFunction{F}()) ≈ f + @test_throws MOI.DeleteNotAllowed MOI.delete(model, x[1]) + return +end + +function test_multiobjective_vector_nonlinear_delete_vector( + model::MOI.ModelLike, + ::Config{T}, +) where {T} + F = MOI.VectorNonlinearFunction + @requires MOI.supports(model, MOI.ObjectiveFunction{F}()) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.GreaterThan(T(0))) + f = MOI.VectorNonlinearFunction( + Any[MOI.ScalarNonlinearFunction(:^, Any[x[1], 2]), x[2]], + ) # [x[1]^2, x[2]] + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{F}(), f) + @test MOI.get(model, MOI.ObjectiveFunctionType()) == F + @test MOI.get(model, MOI.ObjectiveFunction{F}()) ≈ f + @test_throws MOI.DeleteNotAllowed MOI.delete(model, x) + return +end + +function test_multiobjective_vector_nonlinear_modify( + model::MOI.ModelLike, + ::Config{T}, +) where {T} + F = MOI.VectorNonlinearFunction + attr = MOI.ObjectiveFunction{F}() + @requires MOI.supports(model, attr) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.GreaterThan(T(0))) + f = MOI.VectorNonlinearFunction( + Any[MOI.ScalarNonlinearFunction(:^, Any[x[1], 2]), x[2]], + ) # [x[1]^2, x[2]] + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, attr, f) + @test MOI.get(model, MOI.ObjectiveFunctionType()) == F + @test MOI.get(model, attr) ≈ f + @test_throws( + MOI.ModifyObjectiveNotAllowed, + MOI.modify(model, attr, MOI.VectorConstantChange(T[1, 2])), + ) + return +end diff --git a/src/Test/test_nonlinear.jl b/src/Test/test_nonlinear.jl index 45ecf497a6..57d2509d47 100644 --- a/src/Test/test_nonlinear.jl +++ b/src/Test/test_nonlinear.jl @@ -1673,3 +1673,93 @@ function setup_test( model.eval_objective_value = obj_flag end end + +function test_nonlinear_vector_complements( + model::MOI.ModelLike, + config::MOI.Test.Config{T}, +) where {T} + @requires T == Float64 + @requires _supports(config, MOI.optimize!) + F = MOI.ScalarNonlinearFunction + @requires MOI.supports_constraint(model, F, MOI.Complements) + x = MOI.add_variables(model, 4) + MOI.add_constraint.(model, x, MOI.Interval(T(0), T(10))) + MOI.set.(model, MOI.VariablePrimalStart(), x, T(1)) + # f = [ + # -1 * x3^2 + -1 * x4 + 2.0 + # x3^3 + -1.0 * 2x4^2 + 2.0 + # x1^5 + -1.0 * x2 + 2.0 * x3 + -2.0 * x4 + -2.0 + # x1 + 2.0 * x2^3 + -2.0 * x3 + 4.0 * x4 + -6.0 + # x... + # ] + f = MOI.VectorNonlinearFunction([ + MOI.ScalarNonlinearFunction( + :+, + Any[ + MOI.ScalarNonlinearFunction(:*, Any[-T(1), x[3], x[3]]), + MOI.ScalarNonlinearFunction(:*, Any[-T(1), x[4]]), + T(2), + ], + ), + MOI.ScalarNonlinearFunction( + :+, + Any[ + MOI.ScalarNonlinearFunction(:^, Any[x[3], 3]), + MOI.ScalarNonlinearFunction(:*, Any[-T(2), x[4], x[4]]), + T(2), + ], + ), + MOI.ScalarNonlinearFunction( + :+, + Any[ + MOI.ScalarNonlinearFunction(:^, Any[x[1], 5]), + MOI.ScalarAffineFunction{T}( + MOI.ScalarAffineTerm.(T[-1, 2, -2], x[2:4]), + -T(2), + ), + ], + ), + MOI.ScalarNonlinearFunction( + :+, + Any[ + MOI.ScalarNonlinearFunction(:*, Any[T(2), x[2], x[2], x[2]]), + MOI.ScalarAffineFunction{T}( + MOI.ScalarAffineTerm.(T[1, -2, 4], [x[1], x[3], x[4]]), + -T(6), + ), + ], + ), + x[1], + x[2], + x[3], + x[4], + ]) + MOI.add_constraint(model, f, MOI.Complements(8)) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + @test ≈( + MOI.get.(model, MOI.VariablePrimal(), x), + T[1.2847523, 0.9729165, 0.9093762, 1.1730350], + config, + ) + return +end + +function setup_test( + ::typeof(test_nonlinear_vector_complements), + model::MOIU.MockOptimizer, + config::Config{T}, +) where {T} + if T != Float64 + return # Skip for non-Float64 solvers + end + MOI.Utilities.set_mock_optimize!( + model, + mock -> MOI.Utilities.mock_optimize!( + mock, + config.optimal_status, + T[1.2847523, 0.9729165, 0.9093762, 1.1730350], + ), + ) + return +end diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index a561829ca1..017b0a7421 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -189,7 +189,7 @@ for a similar function where `value_fn` returns an function eval_variables( value_fn::F, model::MOI.ModelLike, - f::MOI.AbstractFunction, + f::Union{MOI.AbstractFunction,Real,AbstractVector{<:Real}}, ) where {F} return eval_variables(value_fn, f) end @@ -197,6 +197,16 @@ end # The `eval_variables(::F, ::MOI.ModelLike, ::MOI.ScalarNonlinearFunction)` # method is defined in the MOI.Nonlinear submodule. +function eval_variables( + value_fn::F, + model::MOI.ModelLike, + f::MOI.VectorNonlinearFunction, +) where {F} + return map(f.rows) do row + return eval_variables(value_fn, model, row) + end +end + """ map_indices(index_map::Function, attr::MOI.AnyAttribute, x::X)::X where {X} @@ -334,6 +344,13 @@ function map_indices( ) end +function map_indices( + index_map::F, + f::MOI.VectorNonlinearFunction, +) where {F<:Function} + return MOI.VectorNonlinearFunction(map_indices(index_map, f.rows)) +end + map_indices(::F, change::MOI.ScalarConstantChange) where {F<:Function} = change map_indices(::F, change::MOI.VectorConstantChange) where {F<:Function} = change @@ -473,11 +490,15 @@ function substitute_variables( variable_map::F, f::MOI.ScalarNonlinearFunction, ) where {F<:Function} - # TODO(odow): this uses recursion. We should remove at some point. - return MOI.ScalarNonlinearFunction( - f.head, - Any[substitute_variables(variable_map, a) for a in f.args], - ) + new_args = Any[] + for arg in f.args + if arg isa MOI.VariableIndex + push!(new_args, variable_map(arg)) + else + push!(new_args, substitute_variables(variable_map, arg)) + end + end + return MOI.ScalarNonlinearFunction(f.head, new_args) end function substitute_variables( @@ -515,6 +536,14 @@ function substitute_variables( return g end +function substitute_variables( + variable_map::F, + f::MOI.VectorNonlinearFunction, +) where {F<:Function} + rows = [substitute_variables(variable_map, row) for row in f.rows] + return MOI.VectorNonlinearFunction(rows) +end + """ scalar_type(F::Type{<:MOI.AbstractVectorFunction}) @@ -535,6 +564,8 @@ function scalar_type(::Type{MOI.VectorQuadraticFunction{T}}) where {T} return MOI.ScalarQuadraticFunction{T} end +scalar_type(::Type{MOI.VectorNonlinearFunction}) = MOI.ScalarNonlinearFunction + """ vector_type(::Type{<:MOI.AbstractScalarFunction}) @@ -555,6 +586,10 @@ function vector_type(::Type{MOI.ScalarQuadraticFunction{T}}) where {T} return MOI.VectorQuadraticFunction{T} end +function vector_type(::Type{MOI.ScalarNonlinearFunction}) + return MOI.VectorNonlinearFunction +end + """ ScalarFunctionIterator{F<:MOI.AbstractVectorFunction} @@ -680,6 +715,12 @@ function Base.eltype( return MOI.ScalarQuadraticFunction{T} end +function Base.eltype( + ::ScalarFunctionIterator{F}, +) where {F<:MOI.AbstractVectorFunction} + return scalar_type(F) +end + Base.lastindex(it::ScalarFunctionIterator) = length(it) function Base.getindex( @@ -762,6 +803,20 @@ function Base.getindex( return MOI.VectorQuadraticFunction(vqt, vat, it.f.constants[output_indices]) end +function Base.getindex( + it::ScalarFunctionIterator{MOI.VectorNonlinearFunction}, + output_index::Integer, +) + return it.f.rows[output_index] +end + +function Base.getindex( + it::ScalarFunctionIterator{MOI.VectorNonlinearFunction}, + output_index::AbstractVector{<:Integer}, +) + return MOI.VectorNonlinearFunction(it.f.rows[output_index]) +end + """ zero_with_output_dimension(::Type{T}, output_dimension::Integer) where {T} @@ -888,6 +943,8 @@ function is_canonical( _is_strictly_sorted(f.quadratic_terms) end +is_canonical(f::MOI.VectorNonlinearFunction) = all(is_canonical, f.rows) + function _is_strictly_sorted(x::Vector) if isempty(x) return true @@ -964,6 +1021,13 @@ function canonicalize!(f::MOI.ScalarNonlinearFunction) return f end +function canonicalize!(f::MOI.VectorNonlinearFunction) + for (i, fi) in enumerate(f.rows) + f.rows[i] = canonicalize!(fi) + end + return f +end + """ canonicalize!(f::Union{ScalarQuadraticFunction, VectorQuadraticFunction}) @@ -2079,6 +2143,12 @@ function vectorize( return MOI.VectorQuadraticFunction(quadratic_terms, affine_terms, constant) end +function vectorize(x::AbstractVector{MOI.ScalarNonlinearFunction}) + return MOI.VectorNonlinearFunction(x) +end + +scalarize(f::AbstractVector, ::Bool = false) = f + """ scalarize(func::MOI.VectorOfVariables, ignore_constants::Bool = false) @@ -2154,6 +2224,13 @@ function scalarize( return functions end +function scalarize( + f::MOI.VectorNonlinearFunction, + ignore_constants::Bool = false, +) + return f.rows +end + function count_terms(counting::Vector{<:Integer}, terms::Vector{T}) where {T} for term in terms counting[term.output_index] += 1 @@ -2252,6 +2329,8 @@ is_coefficient_type(::Type{<:TypedLike}, ::Type) = false is_coefficient_type(::Type{<:MOI.ScalarNonlinearFunction}, ::Type) = true +is_coefficient_type(::Type{MOI.VectorNonlinearFunction}, ::Type) = true + similar_type(::Type{F}, ::Type{T}) where {F,T} = F function similar_type(::Type{<:MOI.ScalarAffineFunction}, ::Type{T}) where {T} diff --git a/src/Utilities/model.jl b/src/Utilities/model.jl index 128c9b20d0..8881039983 100644 --- a/src/Utilities/model.jl +++ b/src/Utilities/model.jl @@ -819,7 +819,7 @@ const LessThanIndicatorZero{T} = ), (MOI.ScalarNonlinearFunction,), (MOI.ScalarAffineFunction, MOI.ScalarQuadraticFunction), - (MOI.VectorOfVariables,), + (MOI.VectorOfVariables, MOI.VectorNonlinearFunction), (MOI.VectorAffineFunction, MOI.VectorQuadraticFunction) ) diff --git a/src/Utilities/objective_container.jl b/src/Utilities/objective_container.jl index 412cf4a7e8..2dafc71974 100644 --- a/src/Utilities/objective_container.jl +++ b/src/Utilities/objective_container.jl @@ -21,6 +21,7 @@ mutable struct ObjectiveContainer{T} <: MOI.ModelLike vector_variables::Union{Nothing,MOI.VectorOfVariables} vector_affine::Union{Nothing,MOI.VectorAffineFunction{T}} vector_quadratic::Union{Nothing,MOI.VectorQuadraticFunction{T}} + vector_nonlinear::Union{Nothing,MOI.VectorNonlinearFunction} function ObjectiveContainer{T}() where {T} o = new{T}() MOI.empty!(o) @@ -39,6 +40,7 @@ function MOI.empty!(o::ObjectiveContainer{T}) where {T} o.vector_variables = nothing o.vector_affine = nothing o.vector_quadratic = nothing + o.vector_nonlinear = nothing return end @@ -85,6 +87,8 @@ function MOI.get( return MOI.VectorAffineFunction{T} elseif o.vector_quadratic !== nothing return MOI.VectorQuadraticFunction{T} + elseif o.vector_nonlinear !== nothing + return MOI.VectorNonlinearFunction end # The default if no objective is set. return MOI.ScalarAffineFunction{T} @@ -105,6 +109,7 @@ function MOI.supports( MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, }, }, ) where {T} @@ -129,6 +134,8 @@ function MOI.get( return convert(F, o.vector_affine) elseif o.vector_quadratic !== nothing return convert(F, o.vector_quadratic) + elseif o.vector_nonlinear !== nothing + return convert(F, o.vector_nonlinear) end # The default if no objective is set. return convert(F, zero(MOI.ScalarAffineFunction{T})) @@ -218,6 +225,17 @@ function MOI.set( return end +function MOI.set( + o::ObjectiveContainer, + ::MOI.ObjectiveFunction{MOI.VectorNonlinearFunction}, + f::MOI.VectorNonlinearFunction, +) + _empty_keeping_sense(o) + o.is_function_set = true + o.vector_nonlinear = copy(f) + return +end + ### ### MOI.ListOfModelAttributesSet ### @@ -263,6 +281,14 @@ function MOI.modify( o.vector_quadratic = modify_function!(o.vector_quadratic, change) elseif o.vector_affine !== nothing o.vector_affine = modify_function!(o.vector_affine, change) + elseif o.vector_nonlinear !== nothing + throw( + MOI.ModifyObjectiveNotAllowed( + change, + "Cannot modify objective when there is a " * + "`VectorNonlinearFunction` objective", + ), + ) else # If no objective is set, modify a ScalarAffineFunction by default. f = zero(MOI.ScalarAffineFunction{T}) @@ -302,6 +328,14 @@ function MOI.delete(o::ObjectiveContainer, x::MOI.VariableIndex) o.vector_affine = remove_variable(o.vector_affine, x) elseif o.vector_quadratic !== nothing o.vector_quadratic = remove_variable(o.vector_quadratic, x) + elseif o.vector_nonlinear !== nothing + throw( + MOI.DeleteNotAllowed( + x, + "Cannot delete variable when there is a " * + "`VectorNonlinearFunction` objective", + ), + ) end return end @@ -333,6 +367,14 @@ function MOI.delete(o::ObjectiveContainer, x::Vector{MOI.VariableIndex}) o.vector_affine = filter_variables(keep, o.vector_affine) elseif o.vector_quadratic !== nothing o.vector_quadratic = filter_variables(keep, o.vector_quadratic) + elseif o.vector_nonlinear !== nothing + throw( + MOI.DeleteNotAllowed( + first(x), + "Cannot delete variable when there is a " * + "`VectorNonlinearFunction` objective", + ), + ) end return end diff --git a/src/Utilities/operate.jl b/src/Utilities/operate.jl index 51905cecc6..8b9e30984c 100644 --- a/src/Utilities/operate.jl +++ b/src/Utilities/operate.jl @@ -42,10 +42,10 @@ No argument can be modified. One assumption is that the element type `T` is invariant under each operation. That is, `op(::T, ::T)::T` where `op` is a `+`, `-`, `*`, and `/`. -In each case, `F` (or `F1` and `F2`) is one of the nine supported types, with +In each case, `F` (or `F1` and `F2`) is one of the ten supported types, with a restriction that the mathematical operation makes sense, for example, we don't define `promote_operation(-, T, F1, F2)` where `F1` is a scalar-valued function -and `F2` is a vector-valued function. The nine supported types are: +and `F2` is a vector-valued function. The ten supported types are: 1. ::T 2. ::VariableIndex @@ -56,6 +56,7 @@ and `F2` is a vector-valued function. The nine supported types are: 7. ::VectorOfVariables 8. ::VectorAffineFunction{T} 9. ::VectorQuadraticFunction{T} +10. ::VectorNonlinearFunction """ function operate end @@ -284,6 +285,41 @@ function operate( return operate!(+, T, copy(f), g) end +function operate( + ::typeof(+), + ::Type{T}, + f::MOI.VectorNonlinearFunction, + g::Union{ + AbstractVector{T}, + MOI.VectorOfVariables, + MOI.VectorAffineFunction{T}, + MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, + }, +) where {T<:Number} + args = Any[ + operate(+, T, fi, gi) for (fi, gi) in zip(scalarize(f), scalarize(g)) + ] + return MOI.VectorNonlinearFunction(args) +end + +function operate( + ::typeof(+), + ::Type{T}, + f::Union{ + AbstractVector{T}, + MOI.VectorOfVariables, + MOI.VectorAffineFunction{T}, + MOI.VectorQuadraticFunction{T}, + }, + g::MOI.VectorNonlinearFunction, +) where {T<:Number} + args = Any[ + operate(+, T, fi, gi) for (fi, gi) in zip(scalarize(f), scalarize(g)) + ] + return MOI.VectorNonlinearFunction(args) +end + ### 1c: operate(+, T, args...) function operate(::typeof(+), ::Type{T}, f, g, h, args...) where {T<:Number} @@ -338,6 +374,14 @@ function operate( return MOI.ScalarNonlinearFunction(:-, Any[f]) end +function operate( + ::typeof(-), + ::Type{T}, + f::MOI.VectorNonlinearFunction, +) where {T<:Number} + return MOI.VectorNonlinearFunction(Any[operate(-, T, fi) for fi in f.rows]) +end + ### 2b: operate(::typeof(-), ::Type{T}, ::F1, ::F2) function operate( @@ -546,6 +590,41 @@ function operate( return operate!(op, T, copy(f), g) end +function operate( + ::typeof(-), + ::Type{T}, + f::MOI.VectorNonlinearFunction, + g::Union{ + AbstractVector{T}, + MOI.VectorOfVariables, + MOI.VectorAffineFunction{T}, + MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, + }, +) where {T<:Number} + args = Any[ + operate(-, T, fi, gi) for (fi, gi) in zip(scalarize(f), scalarize(g)) + ] + return MOI.VectorNonlinearFunction(args) +end + +function operate( + ::typeof(-), + ::Type{T}, + f::Union{ + AbstractVector{T}, + MOI.VectorOfVariables, + MOI.VectorAffineFunction{T}, + MOI.VectorQuadraticFunction{T}, + }, + g::MOI.VectorNonlinearFunction, +) where {T<:Number} + args = Any[ + operate(-, T, fi, gi) for (fi, gi) in zip(scalarize(f), scalarize(g)) + ] + return MOI.VectorNonlinearFunction(args) +end + ### 3a: operate(::typeof(*), ::Type{T}, ::T, ::F) function operate( @@ -591,6 +670,15 @@ function operate( return MOI.ScalarNonlinearFunction(:*, Any[f, g]) end +function operate( + ::typeof(*), + ::Type{T}, + f::T, + g::MOI.VectorNonlinearFunction, +) where {T<:Number} + return MOI.VectorNonlinearFunction(Any[operate(*, T, f, h) for h in g.rows]) +end + ### 3b: operate(::typeof(*), ::Type{T}, ::F, ::T) function operate( @@ -618,6 +706,15 @@ function operate( return MOI.ScalarNonlinearFunction(:*, Any[f, g]) end +function operate( + ::typeof(*), + ::Type{T}, + f::MOI.VectorNonlinearFunction, + g::T, +) where {T<:Number} + return MOI.VectorNonlinearFunction(Any[operate(*, T, h, g) for h in f.rows]) +end + ### 3c: operate(::typeof(*), ::Type{T}, ::F1, ::F2) function operate( @@ -790,6 +887,15 @@ function operate( return MOI.ScalarNonlinearFunction(:/, Any[f, g]) end +function operate( + ::typeof(/), + ::Type{T}, + f::MOI.VectorNonlinearFunction, + g::T, +) where {T<:Number} + return MOI.VectorNonlinearFunction(Any[operate(/, T, h, g) for h in f.rows]) +end + ### 5a: operate(::typeof(vcat), ::Type{T}, ::F...) operate(::typeof(vcat), ::Type{T}) where {T<:Number} = T[] @@ -857,6 +963,37 @@ function operate( return MOI.VectorQuadraticFunction(qterms, aterms, constants) end +function operate( + ::typeof(vcat), + ::Type{T}, + args::Union{ + T, + MOI.VariableIndex, + MOI.ScalarAffineFunction{T}, + MOI.ScalarQuadraticFunction{T}, + MOI.ScalarNonlinearFunction, + AbstractVector{T}, + MOI.VectorOfVariables, + MOI.VectorAffineFunction{T}, + MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, + }..., +) where {T<:Number} + out = Any[] + for a in args + if a isa T + push!(out, a) + elseif a isa AbstractVector{T} + append!(out, a) + elseif a isa MOI.AbstractScalarFunction + push!(out, a) + else + append!(out, scalarize(a)) + end + end + return MOI.VectorNonlinearFunction(out) +end + ### 6a: operate(::typeof(imag), ::Type{T}, ::F) function operate( @@ -1600,6 +1737,17 @@ function operate_output_index!( return f end +function operate_output_index!( + op::Union{typeof(+),typeof(-)}, + ::Type{T}, + output_index::Integer, + f::MOI.VectorNonlinearFunction, + g::Union{T,MOI.AbstractScalarFunction}, +) where {T<:Number} + f.rows[output_index] = operate!(op, T, f.rows[output_index], g) + return f +end + """ operate_coefficient( op::Function, diff --git a/src/Utilities/parser.jl b/src/Utilities/parser.jl index 63f4645246..8b1884a840 100644 --- a/src/Utilities/parser.jl +++ b/src/Utilities/parser.jl @@ -115,6 +115,8 @@ function _parse_function(ex, ::Type{T} = Float64) where {T} else if isexpr(ex, :call, 2) && ex.args[1] == :ScalarNonlinearFunction return ex + elseif isexpr(ex, :call, 2) && ex.args[1] == :VectorNonlinearFunction + return ex end # For simplicity, only accept Expr(:call, :+, ...); no recursive # expressions @@ -241,6 +243,8 @@ _parsed_to_moi(model, s::Number) = s function _parsed_to_moi(model, s::Expr) if isexpr(s, :call, 2) && s.args[1] == :ScalarNonlinearFunction return _parsed_scalar_to_moi(model, s.args[2]) + elseif isexpr(s, :call, 2) && s.args[1] == :VectorNonlinearFunction + return _parsed_vector_to_moi(model, s.args[2]) end args = Any[_parsed_to_moi(model, arg) for arg in s.args[2:end]] return MOI.ScalarNonlinearFunction(s.args[1], args) @@ -251,6 +255,11 @@ function _parsed_scalar_to_moi(model, s::Expr) return MOI.ScalarNonlinearFunction(s.args[1], args) end +function _parsed_vector_to_moi(model, s::Expr) + args = Any[_parsed_to_moi(model, arg) for arg in s.args] + return MOI.VectorNonlinearFunction(args) +end + for typename in [ :_ParsedScalarAffineTerm, :_ParsedScalarAffineFunction, diff --git a/src/Utilities/promote_operation.jl b/src/Utilities/promote_operation.jl index b47832c104..2d7fbe2107 100644 --- a/src/Utilities/promote_operation.jl +++ b/src/Utilities/promote_operation.jl @@ -17,7 +17,7 @@ of the arguments `args` are `ArgsTypes`. One assumption is that the element type `T` is invariant under each operation. That is, `op(::T, ::T)::T` where `op` is a `+`, `-`, `*`, and `/`. -There are five methods for which we implement `Utilities.promote_operation`: +There are six methods for which we implement `Utilities.promote_operation`: 1. `+` a. `promote_operation(::typeof(+), ::Type{T}, ::Type{F1}, ::Type{F2})` @@ -38,10 +38,10 @@ There are five methods for which we implement `Utilities.promote_operation`: a. `promote_operation(::typeof(imag), ::Type{T}, ::Type{F})` where `F` is `VariableIndex` or `VectorOfVariables` -In each case, `F` (or `F1` and `F2`) is one of the nine supported types, with +In each case, `F` (or `F1` and `F2`) is one of the ten supported types, with a restriction that the mathematical operation makes sense, for example, we don't define `promote_operation(-, T, F1, F2)` where `F1` is a scalar-valued function -and `F2` is a vector-valued function. The nine supported types are: +and `F2` is a vector-valued function. The ten supported types are: 1. ::T 2. ::VariableIndex @@ -52,6 +52,7 @@ and `F2` is a vector-valued function. The nine supported types are: 7. ::VectorOfVariables 8. ::VectorAffineFunction{T} 9. ::VectorQuadraticFunction{T} + 10. ::VectorNonlinearFunction """ function promote_operation end @@ -122,12 +123,14 @@ function promote_operation( MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, }, F2<:Union{ AbstractVector{T}, MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, }, } S = promote_operation(op, T, scalar_type(F1), scalar_type(F2)) @@ -171,6 +174,14 @@ function promote_operation( return MOI.VectorAffineFunction{T} end +function promote_operation( + ::typeof(-), + ::Type{T}, + ::Type{MOI.VectorNonlinearFunction}, +) where {T<:Number} + return vector_type(promote_operation(-, T, MOI.ScalarNonlinearFunction)) +end + ### Method 3a function promote_operation( @@ -220,6 +231,15 @@ function promote_operation( return MOI.VectorAffineFunction{T} end +function promote_operation( + ::typeof(*), + ::Type{T}, + ::Type{T}, + ::Type{MOI.VectorNonlinearFunction}, +) where {T<:Number} + return vector_type(promote_operation(*, T, T, MOI.ScalarNonlinearFunction)) +end + ### Method 3b function promote_operation( @@ -260,6 +280,15 @@ function promote_operation( return MOI.VectorAffineFunction{T} end +function promote_operation( + ::typeof(*), + ::Type{T}, + ::Type{MOI.VectorNonlinearFunction}, + ::Type{T}, +) where {T<:Number} + return vector_type(promote_operation(*, T, MOI.ScalarNonlinearFunction, T)) +end + ### Method 3c function promote_operation( @@ -331,6 +360,15 @@ function promote_operation( return MOI.VectorAffineFunction{T} end +function promote_operation( + ::typeof(/), + ::Type{T}, + ::Type{MOI.VectorNonlinearFunction}, + ::Type{T}, +) where {T} + return vector_type(promote_operation(/, T, MOI.ScalarNonlinearFunction, T)) +end + ### Method 5a function promote_operation( @@ -387,6 +425,27 @@ function promote_operation( return MOI.VectorQuadraticFunction{T} end +function promote_operation( + ::typeof(vcat), + ::Type{T}, + ::Type{ + <:Union{ + T, + MOI.VariableIndex, + MOI.ScalarAffineFunction{T}, + MOI.ScalarQuadraticFunction{T}, + MOI.ScalarNonlinearFunction, + AbstractVector{T}, + MOI.VectorOfVariables, + MOI.VectorAffineFunction{T}, + MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, + }, + }..., +) where {T<:Number} + return MOI.VectorNonlinearFunction +end + ### Method 6a function promote_operation( diff --git a/src/functions.jl b/src/functions.jl index c2c6a984bf..0b27cfd3f1 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -665,6 +665,74 @@ function Base.copy(f::VectorQuadraticFunction) ) end +""" + VectorNonlinearFunction(args::Vector{ScalarNonlinearFunction}) + +The vector-valued nonlinear function composed of a vector of +[`ScalarNonlinearFunction`](@ref). + +## `args` + +The vector `args` contains the scalar elements of the nonlinear function. Each +element must be a [`ScalarNonlinearFunction`](@ref), but if you pass a +`Vector{Any}`, the elements can be automatically converted from one of the +following: + + * A constant value of type `T<:Real` + * A [`VariableIndex`](@ref) + * A [`ScalarAffineFunction`](@ref) + * A [`ScalarQuadraticFunction`](@ref) + * A [`ScalarNonlinearFunction`](@ref) + +## Example + +To represent the function ``f(x) = [sin(x)^2, x]``, do: + +```jldoctest +julia> import MathOptInterface as MOI + +julia> x = MOI.VariableIndex(1) +MOI.VariableIndex(1) + +julia> g = MOI.ScalarNonlinearFunction( + :^, + Any[MOI.ScalarNonlinearFunction(:sin, Any[x]), 2.0], + ) +^(sin(MOI.VariableIndex(1)), 2.0) + +julia> MOI.VectorNonlinearFunction([g, x]) +┌ ┐ +│^(sin(MOI.VariableIndex(1)), 2.0)│ +│+(MOI.VariableIndex(1)) │ +└ ┘ +``` + +Note the automatic conversion from `x` to `+(x)`. +""" +struct VectorNonlinearFunction <: AbstractVectorFunction + rows::Vector{ScalarNonlinearFunction} +end + +output_dimension(f::VectorNonlinearFunction) = length(f.rows) + +function constant(f::VectorNonlinearFunction, ::Type{T}) where {T} + return zeros(T, output_dimension(f)) +end + +Base.copy(f::VectorNonlinearFunction) = VectorNonlinearFunction(copy(f.rows)) + +function Base.:(==)(f::VectorNonlinearFunction, g::VectorNonlinearFunction) + return f.rows == g.rows +end + +function Base.isapprox( + x::VectorNonlinearFunction, + y::VectorNonlinearFunction; + kwargs..., +) + return all(isapprox(xi, yi; kwargs...) for (xi, yi) in zip(x.rows, y.rows)) +end + # Function modifications """ @@ -1096,6 +1164,14 @@ end # ScalarNonlinearFunction +function Base.convert(::Type{ScalarNonlinearFunction}, x::Real) + return ScalarNonlinearFunction(:+, Any[x]) +end + +function Base.convert(::Type{ScalarNonlinearFunction}, x::VariableIndex) + return ScalarNonlinearFunction(:+, Any[x]) +end + function Base.convert(::Type{ScalarNonlinearFunction}, term::ScalarAffineTerm) return ScalarNonlinearFunction(:*, Any[term.coefficient, term.variable]) end diff --git a/test/Bridges/Constraint/flip_sign.jl b/test/Bridges/Constraint/flip_sign.jl index 9aaaa18cf2..036aaf503f 100644 --- a/test/Bridges/Constraint/flip_sign.jl +++ b/test/Bridges/Constraint/flip_sign.jl @@ -425,6 +425,17 @@ function test_runtests() [-2.1 * x + 1.0] in Nonnegatives(1) """, ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.NonposToNonnegBridge, + """ + variables: x + VectorNonlinearFunction([2.1 * x - 1.0]) in Nonpositives(1) + """, + """ + variables: x + VectorNonlinearFunction([-(2.1 * x - 1.0)]) in Nonnegatives(1) + """, + ) return end diff --git a/test/Bridges/Constraint/scalarize.jl b/test/Bridges/Constraint/scalarize.jl index bf154ff98f..159ac3a877 100644 --- a/test/Bridges/Constraint/scalarize.jl +++ b/test/Bridges/Constraint/scalarize.jl @@ -223,6 +223,53 @@ function test_runtests() 4.0 * x == 5.0 """, ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ScalarizeBridge, + """ + variables: x + VectorNonlinearFunction([2.0 * x - 1.0]) in Nonnegatives(1) + VectorNonlinearFunction([3.0 * x + 1.0]) in Nonpositives(1) + VectorNonlinearFunction([4.0 * x - 5.0]) in Zeros(1) + """, + """ + variables: x + ScalarNonlinearFunction(2.0 * x - 1.0) >= 0.0 + ScalarNonlinearFunction(3.0 * x + 1.0) <= 0.0 + ScalarNonlinearFunction(4.0 * x - 5.0) == 0.0 + """, + ) + return +end + +function test_VectorNonlinearFunction_mixed_type() + # We can't use the standard runtests because ScalarNonlinearFunction does + # not preserve f(x) ≈ (f(x) - g(x)) + g(x) + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Constraint.Scalarize{Float64}(inner) + x = MOI.add_variable(model) + f = MOI.ScalarNonlinearFunction(:log, Any[x]) + g = MOI.VectorNonlinearFunction(Any[1.0, x, 2.0*x-1.0, f]) + c = MOI.add_constraint(model, g, MOI.Nonnegatives(4)) + F, S = MOI.ScalarNonlinearFunction, MOI.GreaterThan{Float64} + indices = MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()) + @test length(indices) == 4 + inner_variables = MOI.get(inner, MOI.ListOfVariableIndices()) + @test length(inner_variables) == 1 + y = inner_variables[1] + out = convert.(MOI.ScalarNonlinearFunction, Any[1.0, y, 2.0*y-1.0]) + push!(out, MOI.ScalarNonlinearFunction(:log, Any[y])) + for (input, output) in zip(indices, out) + @test ≈(MOI.get(inner, MOI.ConstraintFunction(), input), output) + end + new_g = MOI.VectorNonlinearFunction(Any[f, 2.0*x-1.0, 1.0, x]) + MOI.set(model, MOI.ConstraintFunction(), c, new_g) + out = vcat( + MOI.ScalarNonlinearFunction(:log, Any[y]), + convert.(MOI.ScalarNonlinearFunction, Any[2.0*y-1.0, 1.0, y]), + ) + for (input, output) in zip(indices, out) + @test ≈(MOI.get(inner, MOI.ConstraintFunction(), input), output) + end return end diff --git a/test/Bridges/Constraint/square.jl b/test/Bridges/Constraint/square.jl index ec1ee3fa49..e9e1c0036a 100644 --- a/test/Bridges/Constraint/square.jl +++ b/test/Bridges/Constraint/square.jl @@ -240,6 +240,80 @@ function test_square_warning() return end +function test_VectorNonlinearFunction_symmetric() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Constraint.Square{Float64}(inner) + x = MOI.add_variables(model, 3) + fis = Any[MOI.ScalarNonlinearFunction(:log, Any[x[i]]) for i in 1:3] + f = MOI.VectorNonlinearFunction(Any[fis[1], fis[2], fis[2], fis[3]]) + c = MOI.add_constraint(model, f, MOI.PositiveSemidefiniteConeSquare(2)) + F, S = MOI.VectorNonlinearFunction, MOI.PositiveSemidefiniteConeTriangle + indices = MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()) + @test length(indices) == 1 + g = MOI.get(inner, MOI.ConstraintFunction(), indices[1]) + y = MOI.get(inner, MOI.ListOfVariableIndices()) + gis = Any[MOI.ScalarNonlinearFunction(:log, Any[y[i]]) for i in 1:3] + @test g ≈ MOI.VectorNonlinearFunction(gis) + return +end + +function test_VectorNonlinearFunction_nonsymmetric() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Constraint.Square{Float64}(inner) + x = MOI.add_variables(model, 4) + fis = Any[MOI.ScalarNonlinearFunction(:log, Any[x[i]]) for i in 1:4] + f = MOI.VectorNonlinearFunction(fis) + c = MOI.add_constraint(model, f, MOI.PositiveSemidefiniteConeSquare(2)) + F, S = MOI.VectorNonlinearFunction, MOI.PositiveSemidefiniteConeTriangle + indices = MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()) + @test length(indices) == 1 + g = MOI.get(inner, MOI.ConstraintFunction(), indices[1]) + y = MOI.get(inner, MOI.ListOfVariableIndices()) + gis = Any[MOI.ScalarNonlinearFunction(:log, Any[y[i]]) for i in 1:4] + @test g ≈ MOI.VectorNonlinearFunction(gis[[1, 3, 4]]) + F, S = MOI.ScalarNonlinearFunction, MOI.EqualTo{Float64} + indices = MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()) + @test length(indices) == 1 + g = MOI.get(inner, MOI.ConstraintFunction(), indices[1]) + @test g ≈ MOI.ScalarNonlinearFunction(:-, Any[gis[3], gis[2]]) + return +end + +function test_VectorNonlinearFunction_mixed_type() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Constraint.Square{Float64}(inner) + x = MOI.add_variables(model, 4) + fis = vcat( + Any[MOI.ScalarNonlinearFunction(:log, Any[x[i]]) for i in 1:2], + 1.0 * x[3] + 2.0, + x[4], + ) + f = MOI.VectorNonlinearFunction(fis) + c = MOI.add_constraint(model, f, MOI.PositiveSemidefiniteConeSquare(2)) + F, S = MOI.VectorNonlinearFunction, MOI.PositiveSemidefiniteConeTriangle + indices = MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()) + @test length(indices) == 1 + g = MOI.get(inner, MOI.ConstraintFunction(), indices[1]) + y = MOI.get(inner, MOI.ListOfVariableIndices()) + gis = vcat( + Any[MOI.ScalarNonlinearFunction(:log, Any[y[i]]) for i in 1:2], + 1.0 * y[3] + 2.0, + y[4], + ) + @test g ≈ MOI.VectorNonlinearFunction(gis[[1, 3, 4]]) + F, S = MOI.ScalarNonlinearFunction, MOI.EqualTo{Float64} + indices = MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()) + @test length(indices) == 1 + @test ≈( + MOI.get(inner, MOI.ConstraintFunction(), indices[1]), + MOI.ScalarNonlinearFunction( + :-, + Any[convert(MOI.ScalarNonlinearFunction, gis[3]), gis[2]], + ), + ) + return +end + end # module TestConstraintSquare.runtests() diff --git a/test/Bridges/Constraint/vectorize.jl b/test/Bridges/Constraint/vectorize.jl index e51e6cb398..fefbe0fc4a 100644 --- a/test/Bridges/Constraint/vectorize.jl +++ b/test/Bridges/Constraint/vectorize.jl @@ -260,6 +260,31 @@ function test_unsupported_ScalarNonlinearFunction() return end +function test_VectorNonlinearFunction() + # We can't use the standard runtests because ScalarNonlinearFunction does + # not preserve f(x) ≈ (f(x) - g(x)) + g(x) + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Constraint.Vectorize{Float64}(inner) + x = MOI.add_variable(model) + f = MOI.ScalarNonlinearFunction(:log, Any[x]) + c = MOI.add_constraint(model, f, MOI.EqualTo(1.0)) + F, S = MOI.VectorNonlinearFunction, MOI.Zeros + indices = MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()) + @test length(indices) == 1 + inner_variables = MOI.get(inner, MOI.ListOfVariableIndices()) + @test length(inner_variables) == 1 + y = inner_variables[1] + g = MOI.ScalarNonlinearFunction( + :-, + Any[MOI.ScalarNonlinearFunction(:log, Any[x]), 1.0], + ) + @test ≈( + MOI.get(inner, MOI.ConstraintFunction(), indices[1]), + MOI.VectorNonlinearFunction(Any[g]), + ) + return +end + end # module TestConstraintVectorize.runtests() diff --git a/test/Utilities/functions.jl b/test/Utilities/functions.jl index f417073c0c..b98c090783 100644 --- a/test/Utilities/functions.jl +++ b/test/Utilities/functions.jl @@ -261,6 +261,18 @@ function test_eval_variables_scalar_nonlinear_function() return end +function test_eval_variables_vector_nonlinear_function() + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + x = MOI.add_variable(model) + f = MOI.ScalarNonlinearFunction(:log, Any[x]) + g = MOI.VectorNonlinearFunction(Any[1.0, x, 2.0*x, f]) + @test ≈( + MOI.Utilities.eval_variables(xi -> 0.5, model, g), + [1.0, 0.5, 1.0, log(0.5)], + ) + return +end + function test_substitute_variables() # We do tests twice to make sure the function is not modified subs = Dict(w => 1.0y + 1.0z, x => 2.0y + 1.0, y => 1.0y, z => -1.0w) @@ -321,6 +333,55 @@ function test_substitute_variables() return end +function test_substitute_variables_vector_nonlinear_function() + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + x = MOI.add_variable(model) + f = MOI.ScalarNonlinearFunction(:log, Any[x]) + g = MOI.VectorNonlinearFunction(Any[1.0, x, 2.0*x, f]) + @test ≈( + MOI.Utilities.substitute_variables(x -> 1.5 * x, g), + MOI.VectorNonlinearFunction([ + MOI.ScalarNonlinearFunction(:+, Any[1.0]), + MOI.ScalarNonlinearFunction(:+, Any[1.5*x]), + MOI.ScalarNonlinearFunction( + :+, + Any[MOI.ScalarNonlinearFunction(:*, Any[2.0, 1.5*x])], + ), + MOI.ScalarNonlinearFunction(:log, Any[1.5*x]), + ]), + ) + return +end + +function test_canonicalize_vector_nonlinear_function() + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + x = MOI.add_variable(model) + f = MOI.ScalarNonlinearFunction(:log, Any[1.0*x+2.0*x]) + fi = 1.0 * x + 1.0 * x + @test length(fi.terms) == 2 + g = MOI.VectorNonlinearFunction(Any[1.0, x, fi, f]) + @test g.rows[4] === f + MOI.Utilities.canonicalize!(g) + @test g.rows[4] === f + @test length(f.args[1].terms) == 1 + return +end + +function test_eachscalar_vector_nonlinear_function() + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + x = MOI.add_variable(model) + f = MOI.ScalarNonlinearFunction(:log, Any[1.0*x+2.0*x]) + g = MOI.VectorNonlinearFunction(Any[1.0, x, 2.0*x, f]) + scalars = MOI.Utilities.eachscalar(g) + @test eltype(scalars) == MOI.ScalarNonlinearFunction + @test ≈(scalars[1], MOI.ScalarNonlinearFunction(:+, Any[1.0])) + @test ≈(scalars[2], MOI.ScalarNonlinearFunction(:+, Any[x])) + @test ≈(scalars[3], convert(MOI.ScalarNonlinearFunction, 2.0 * x)) + @test ≈(scalars[4], f) + @test ≈(scalars[2:3], MOI.VectorNonlinearFunction(Any[x, 2.0*x])) + return +end + function test_map_indices() fsq = MOI.ScalarQuadraticFunction( MOI.ScalarQuadraticTerm.(1.0, [x, w, w], [z, z, y]), diff --git a/test/Utilities/test_operate!.jl b/test/Utilities/test_operate!.jl index 3a9a48abcc..4a82333729 100644 --- a/test/Utilities/test_operate!.jl +++ b/test/Utilities/test_operate!.jl @@ -56,6 +56,10 @@ function _test_function(pair::Pair{Symbol,<:Any}) end end +function _test_function(pairs::Vector{<:Any}) + return MOI.VectorNonlinearFunction(Any[_test_function(f) for f in pairs]) +end + function test_operate_1a() for coef in ( (0, 0, 0), @@ -69,6 +73,7 @@ function test_operate_1a() [(0, 1, 0)], [(0, 0, 1)], [(1, 1, 1)], + [:log => (0, 0, 0)], ) f = _test_function(coef) @test MOI.Utilities.operate(+, Int, f) == f @@ -90,6 +95,7 @@ function test_operate_1b() [(0, 1, 0)], [(0, 0, 1)], [(1, 1, 1)], + [:log => (0, 0, 0)], ) special_cases = Dict((0, 0, 0) => (0, 1, 0)) for i in 1:6, j in 1:6 @@ -104,13 +110,30 @@ function test_operate_1b() @test MOI.Utilities.operate(+, Int, fi, fj) ≈ fk @test MOI.Utilities.operate!(+, Int, fi, fj) ≈ fk end - for i in 7:11, j in 7:11 + for i in 7:12, j in 7:12 fi, fj = _test_function(F[i]), _test_function(F[j]) - k = map(zip(F[i], F[j])) do (x, y) - return get(special_cases, x, x) .+ get(special_cases, y, y) + if i == 12 || j == 12 + args = Any[] + for (fi_, fj_) in zip(F[i], F[j]) + push!( + args, + MOI.Utilities.operate( + +, + Int, + _test_function(fi_), + _test_function(fj_), + ), + ) + end + fk = MOI.VectorNonlinearFunction(args) + else + k = map(zip(F[i], F[j])) do (x, y) + return get(special_cases, x, x) .+ get(special_cases, y, y) + end + fk = _test_function(k) end - @test MOI.Utilities.operate(+, Int, fi, fj) ≈ _test_function(k) - @test MOI.Utilities.operate!(+, Int, fi, fj) ≈ _test_function(k) + @test MOI.Utilities.operate(+, Int, fi, fj) ≈ fk + @test MOI.Utilities.operate!(+, Int, fi, fj) ≈ fk end return end @@ -161,6 +184,7 @@ function test_operate_2a() [(0, 1, 0)] => [(0, -1, 0)], [(0, 0, 1)] => [(0, 0, -1)], [(1, 1, 1)] => [(-1, -1, -1)], + [(:log => (0, 0, 0))] => [(:- => (:log => (0, 0, 0)))], ) @test MOI.Utilities.operate(-, T, _test_function(f)) ≈ _test_function(g) @test MOI.Utilities.operate!(-, T, _test_function(f)) ≈ @@ -192,6 +216,7 @@ function test_operate_2b() [(0, 1, 0)], [(0, 0, 1)], [(1, 1, 1)], + [:log => (0, 0, 0)], ) special_cases = Dict((0, 0, 0) => (0, 1, 0)) for i in 1:6, j in 1:6 @@ -213,15 +238,32 @@ function test_operate_2b() @test MOI.Utilities.operate(-, Int, fi, fj) ≈ fk @test MOI.Utilities.operate!(-, Int, fi, fj) ≈ fk end - for i in 7:11, j in 7:11 - F2 = [2 .* fi for fi in F[i]] - fi, fj = _test_function(F2), _test_function(F[j]) - k = map(zip(F2, F[j])) do (x, y) - return get(special_cases, x, x) .- get(special_cases, y, y) - end - fk = _test_function(k) - if (i, j) in ((7, 7), (7, 9)) - fk = MOI.VectorAffineFunction(MOI.VectorAffineTerm{Int}[], [0]) + for i in 7:12, j in 7:12 + fi, fj = _test_function(F[i]), _test_function(F[j]) + if i == 12 || j == 12 + args = Any[] + for (fi_, fj_) in zip(F[i], F[j]) + push!( + args, + MOI.Utilities.operate( + -, + Int, + _test_function(fi_), + _test_function(fj_), + ), + ) + end + fk = MOI.VectorNonlinearFunction(args) + else + F2 = [2 .* fi for fi in F[i]] + fi, fj = _test_function(F2), _test_function(F[j]) + k = map(zip(F2, F[j])) do (x, y) + return get(special_cases, x, x) .- get(special_cases, y, y) + end + fk = _test_function(k) + if (i, j) in ((7, 7), (7, 9)) + fk = MOI.VectorAffineFunction(MOI.VectorAffineTerm{Int}[], [0]) + end end @test MOI.Utilities.operate(-, Int, fi, fj) ≈ fk @test MOI.Utilities.operate!(-, Int, fi, fj) ≈ fk @@ -243,6 +285,7 @@ function test_operate_3a() [(0, 1, 0)] => [(0, 3, 0)], [(0, 0, 1)] => [(0, 0, 3)], [(1, 1, 1)] => [(3, 3, 3)], + [(:log => (0, 0, 0))] => [(:* => [(3, 0, 0), (:log => (0, 0, 0))])], ) f = _test_function(f) @test MOI.Utilities.operate(*, T, 3, f) ≈ _test_function(g) @@ -265,6 +308,7 @@ function test_operate_3b() [(0, 1, 0)] => [(0, 3, 0)], [(0, 0, 1)] => [(0, 0, 3)], [(1, 1, 1)] => [(3, 3, 3)], + [(:log => (0, 0, 0))] => [(:* => [(:log => (0, 0, 0)), (3, 0, 0)])], ) f = _test_function(f) @test MOI.Utilities.operate(*, T, f, 3) ≈ _test_function(g) @@ -296,6 +340,8 @@ function test_operate_4a() [(0.0, 1.0, 0.0)] => [(0.0, 0.5, 0.0)], [(0.0, 0.0, 1.0)] => [(0.0, 0.0, 0.5)], [(1.0, 1.0, 1.0)] => [(0.5, 0.5, 0.5)], + [(:log => (0, 0, 0))] => + [(:/ => [(:log => (0, 0, 0)), (2.0, 0.0, 0.0)])], ) f = _test_function(f) @test MOI.Utilities.operate(/, T, f, 2.0) ≈ _test_function(g) @@ -312,11 +358,13 @@ function test_operate_5a() (0.0, 1.0, 0.0), (0.0, 0.0, 1.0), (1.0, 1.0, 1.0), + (:log => (0, 0, 0)), [(0.0, 0.0, 0.0)], [(1.0, 0.0, 0.0)], [(0.0, 1.0, 0.0)], [(0.0, 0.0, 1.0)], [(1.0, 1.0, 1.0)], + [:log => (0, 0, 0)], ) for f in F, g in F h = vcat(f, g) @@ -549,8 +597,9 @@ function test_operate_output_index_1a() (0.0, 1.0, 0.0), (0.0, 0.0, 1.0), (1.0, 1.0, 1.0), + :log => (0, 0, 0), ) - for i in 2:5 + for i in 2:6 for j in 1:i if (i, j) == (2, 1) continue diff --git a/test/Utilities/test_promote_operation.jl b/test/Utilities/test_promote_operation.jl index b295923b18..ce9f2c1d7b 100644 --- a/test/Utilities/test_promote_operation.jl +++ b/test/Utilities/test_promote_operation.jl @@ -33,6 +33,7 @@ function test_promote_operation_1a() MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, ) special_cases = Dict( (1, 2) => 3, @@ -46,7 +47,7 @@ function test_promote_operation_1a() k = get(special_cases, (i, j), max(i, j)) @test MOI.Utilities.promote_operation(+, T, F[i], F[j]) == F[k] end - for i in 6:9, j in 6:9 + for i in 6:10, j in 6:10 k = get(special_cases, (i, j), max(i, j)) @test MOI.Utilities.promote_operation(+, T, F[i], F[j]) == F[k] end @@ -65,9 +66,10 @@ function test_promote_operation_2a() MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, ) special_cases = Dict(2 => 3, 7 => 8) - for i in 1:8 + for i in 1:10 j = get(special_cases, i, i) @test MOI.Utilities.promote_operation(-, T, F[i]) == F[j] end @@ -86,6 +88,7 @@ function test_promote_operation_2b() MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, ) special_cases = Dict( (1, 2) => 3, @@ -99,7 +102,7 @@ function test_promote_operation_2b() k = get(special_cases, (i, j), max(i, j)) @test MOI.Utilities.promote_operation(-, T, F[i], F[j]) == F[k] end - for i in 6:9, j in 6:9 + for i in 6:10, j in 6:10 k = get(special_cases, (i, j), max(i, j)) @test MOI.Utilities.promote_operation(-, T, F[i], F[j]) == F[k] end @@ -118,9 +121,10 @@ function test_promote_operation_3a() MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, ) special_cases = Dict(2 => 3, 7 => 8) - for i in 1:9 + for i in 1:10 j = get(special_cases, i, i) @test MOI.Utilities.promote_operation(*, T, T, F[i]) == F[j] end @@ -139,9 +143,10 @@ function test_promote_operation_3b() MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, ) special_cases = Dict(2 => 3, 7 => 8) - for i in 1:9 + for i in 1:10 j = get(special_cases, i, i) @test MOI.Utilities.promote_operation(*, T, F[i], T) == F[j] end @@ -160,9 +165,10 @@ function test_promote_operation_4a() MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, ) special_cases = Dict(2 => 3, 7 => 8) - for i in 1:9 + for i in 1:10 j = get(special_cases, i, i) @test MOI.Utilities.promote_operation(/, T, F[i], T) == F[j] end @@ -176,23 +182,25 @@ function test_promote_operation_5a() MOI.VariableIndex, MOI.ScalarAffineFunction{T}, MOI.ScalarQuadraticFunction{T}, + MOI.ScalarNonlinearFunction, Vector{T}, MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, + MOI.VectorNonlinearFunction, ) special_cases = Dict( - (1, 2) => 7, - (2, 1) => 7, - (1, 6) => 7, - (6, 1) => 7, - (2, 5) => 7, - (5, 2) => 7, - (5, 6) => 7, - (6, 5) => 7, + (1, 2) => 8, + (2, 1) => 8, + (1, 7) => 8, + (7, 1) => 8, + (2, 6) => 8, + (6, 2) => 8, + (6, 7) => 8, + (7, 6) => 8, ) - for i in 1:8, j in 1:8 - k = max(i <= 4 ? i + 4 : i, j <= 4 ? j + 4 : j) + for i in 1:10, j in 1:10 + k = max(i <= 5 ? i + 5 : i, j <= 5 ? j + 5 : j) k = get(special_cases, (i, j), k) @test MOI.Utilities.promote_operation(vcat, T, F[i], F[j]) == F[k] end