From 20cdcffc54511ed8ba1327e3a7d9a2752f175571 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 9 Jun 2023 14:37:34 +1200 Subject: [PATCH 01/29] Add VectorNonlinearFunction --- docs/src/manual/standard_form.md | 1 + docs/src/reference/standard_form.md | 1 + src/Test/test_basic_constraint.jl | 20 +++++- src/Test/test_multiobjective.jl | 79 +++++++++++++++++++++ src/Test/test_nonlinear.jl | 90 ++++++++++++++++++++++++ src/Utilities/functions.jl | 52 ++++++++++++++ src/Utilities/model.jl | 2 +- src/Utilities/objective_container.jl | 42 +++++++++++ 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 | 15 ++++ test/Utilities/test_promote_operation.jl | 40 ++++++----- 14 files changed, 482 insertions(+), 21 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/Test/test_basic_constraint.jl b/src/Test/test_basic_constraint.jl index 061f0821f4..7b1fb61627 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(Any[f; x]) +end + # Default fallback. _set(::Any, ::Type{S}) where {S} = _set(S) @@ -334,7 +346,13 @@ for s in [ :ScalarNonlinearFunction, ) else - (:VectorOfVariables, :VectorAffineFunction, :VectorQuadraticFunction) + ( + :VectorOfVariables, + :VectorAffineFunction, + :VectorQuadraticFunction, + # TODO(odow): re-add this at a later date + # :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..4a88f6eea2 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..cf22e09aa1 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -334,6 +334,15 @@ function map_indices( ) end +function map_indices( + index_map::F, + f::MOI.GenericVectorFunction{T}, +) where {F<:Function,T} + return MOI.GenericVectorFunction{T}( + convert(Vector{T}, 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 @@ -535,6 +544,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 +566,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 +695,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 +783,20 @@ function Base.getindex( return MOI.VectorQuadraticFunction(vqt, vat, it.f.constants[output_indices]) end +function Base.getindex( + it::ScalarFunctionIterator{<:MOI.GenericVectorFunction}, + output_index::Integer, +) + return it.f.rows[output_index] +end + +function Base.getindex( + it::ScalarFunctionIterator{MOI.GenericVectorFunction{F}}, + output_index::AbstractVector{<:Integer}, +) where {F} + return MOI.GenericVectorFunction{F}(it.f.rows[output_index]) +end + """ zero_with_output_dimension(::Type{T}, output_dimension::Integer) where {T} @@ -964,6 +999,13 @@ function canonicalize!(f::MOI.ScalarNonlinearFunction) return f end +function canonicalize!(f::MOI.GenericVectorFunction) + for (i, fi) in enumerate(f.rows) + f.rows[i] = canonicalize!(fi) + end + return f +end + """ canonicalize!(f::Union{ScalarQuadraticFunction, VectorQuadraticFunction}) @@ -2079,6 +2121,12 @@ function vectorize( return MOI.VectorQuadraticFunction(quadratic_terms, affine_terms, constant) end +function vectorize(x::AbstractVector) + return MOI.VectorNonlinearFunction(collect(x)) +end + +scalarize(f::AbstractVector, ::Bool = false) = f + """ scalarize(func::MOI.VectorOfVariables, ignore_constants::Bool = false) @@ -2154,6 +2202,8 @@ function scalarize( return functions end +scalarize(f::MOI.GenericVectorFunction, ignore_constants::Bool = false) = f.rows + function count_terms(counting::Vector{<:Integer}, terms::Vector{T}) where {T} for term in terms counting[term.output_index] += 1 @@ -2252,6 +2302,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/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..70f8222f53 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.GenericVectorFunction, }, F2<:Union{ AbstractVector{T}, MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, + MOI.GenericVectorFunction, }, } 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{F}, +) where {T,F<:MOI.GenericVectorFunction} + return vector_type(promote_operation(-, T, scalar_type(F))) +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{F}, +) where {T,F<:MOI.GenericVectorFunction} + return vector_type(promote_operation(*, T, T, scalar_type(F))) +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{F}, + ::Type{T}, +) where {T,F<:MOI.GenericVectorFunction} + return vector_type(promote_operation(*, T, scalar_type(F), 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{F}, + ::Type{T}, +) where {T,F<:MOI.GenericVectorFunction} + return vector_type(promote_operation(/, T, scalar_type(F), 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} + return MOI.VectorNonlinearFunction +end + ### Method 6a function promote_operation( diff --git a/src/functions.jl b/src/functions.jl index adc9b0747d..3e324867e3 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -13,6 +13,7 @@ components if `f` is an [`AbstractVectorFunction`](@ref). function output_dimension end output_dimension(::AbstractScalarFunction) = 1 +output_dimension(x::AbstractVector) = length(x) """ constant(f::AbstractFunction[, ::Type{T}]) where {T} @@ -406,6 +407,40 @@ All subtypes of `AbstractVectorFunction` must implement: """ abstract type AbstractVectorFunction <: AbstractFunction end +struct GenericVectorFunction{T} <: AbstractVectorFunction + rows::Vector{T} +end + +output_dimension(f::GenericVectorFunction) = length(f.rows) + +function constant(f::GenericVectorFunction, ::Type{T}) where {T} + return zeros(T, output_dimension(f)) +end + +Base.copy(f::GenericVectorFunction) = GenericVectorFunction(copy(f.rows)) + +function Base.:(==)( + f::GenericVectorFunction{T}, + g::GenericVectorFunction{T}, +) where {T} + return f.rows == g.rows +end + +function Base.isapprox( + x::GenericVectorFunction{T}, + y::GenericVectorFunction{T}; + kwargs..., +) where {T} + return all(isapprox(xi, yi; kwargs...) for (xi, yi) in zip(x.rows, y.rows)) +end + +function Base.convert( + ::Type{GenericVectorFunction{T}}, + rows::Vector{T}, +) where {T} + return GenericVectorFunction(rows) +end + """ VectorOfVariables(variables::Vector{VariableIndex}) <: AbstractVectorFunction @@ -665,6 +700,47 @@ function Base.copy(f::VectorQuadraticFunction) ) end +""" + VectorNonlinearFunction(args::Vector{Any}) + +The vector-valued nonlinear function composed of a vector of scalar functions. + +## `args` + +The vector `args` contains the scalar elements of the nonlinear function. Each +element must be 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(Any[g, x]) +┌ ┐ +│^(sin(MOI.VariableIndex(1)), 2.0)│ +│MOI.VariableIndex(1) │ +└ ┘ +``` +""" +const VectorNonlinearFunction = GenericVectorFunction{Any} + # Function modifications """ 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..6460388baf 100644 --- a/test/Bridges/Constraint/scalarize.jl +++ b/test/Bridges/Constraint/scalarize.jl @@ -223,6 +223,21 @@ 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 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 From 2b657c6b616a2d75753db1a7dfd779f3b9dd9a11 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 22 Jun 2023 14:03:33 +1200 Subject: [PATCH 02/29] Implement operate for VectorNonlinearFunction --- src/Utilities/operate.jl | 141 +++++++++++++++++++++++++++++++- test/Utilities/test_operate!.jl | 76 +++++++++++++---- 2 files changed, 201 insertions(+), 16 deletions(-) diff --git a/src/Utilities/operate.jl b/src/Utilities/operate.jl index 51905cecc6..4d490c15a4 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} + 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} + 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} + 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} + 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} + 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} + 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} + 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} + 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} + 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( diff --git a/test/Utilities/test_operate!.jl b/test/Utilities/test_operate!.jl index 3a9a48abcc..afec6f7db1 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) From 1c6bdeaf3db7adf22105ac8986298f95db548687 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 22 Jun 2023 15:07:05 +1200 Subject: [PATCH 03/29] Update --- src/Test/test_basic_constraint.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Test/test_basic_constraint.jl b/src/Test/test_basic_constraint.jl index 7b1fb61627..9472da95c6 100644 --- a/src/Test/test_basic_constraint.jl +++ b/src/Test/test_basic_constraint.jl @@ -350,8 +350,7 @@ for s in [ :VectorOfVariables, :VectorAffineFunction, :VectorQuadraticFunction, - # TODO(odow): re-add this at a later date - # :VectorNonlinearFunction, + :VectorNonlinearFunction, ) end for f in functions From 426bd976e06e5bc249ea16f429ced5d3102e64b3 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 23 Jun 2023 15:10:33 +1200 Subject: [PATCH 04/29] Updates --- src/Utilities/functions.jl | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index cf22e09aa1..709b9e2476 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -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.GenericVectorFunction, +) 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} @@ -339,7 +349,7 @@ function map_indices( f::MOI.GenericVectorFunction{T}, ) where {F<:Function,T} return MOI.GenericVectorFunction{T}( - convert(Vector{T}, map_indices(index_map, f.rows)) + convert(Vector{T}, map_indices(index_map, f.rows)), ) end @@ -524,6 +534,14 @@ function substitute_variables( return g end +function substitute_variables( + variable_map::F, + f::MOI.GenericVectorFunction{T}, +) where {T,F<:Function} + new_rows = map(Base.Fix1(substitute_variables, variable_map), f.rows) + return MOI.GenericVectorFunction{T}(convert(Vector{T}, new_rows)) +end + """ scalar_type(F::Type{<:MOI.AbstractVectorFunction}) @@ -923,6 +941,8 @@ function is_canonical( _is_strictly_sorted(f.quadratic_terms) end +is_canonical(f::MOI.GenericVectorFunction) = all(is_canonical, f.rows) + function _is_strictly_sorted(x::Vector) if isempty(x) return true From 98450ef9560bb86b1aafeeeeae6be1f4fe1f49bb Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 27 Jun 2023 11:01:26 +1200 Subject: [PATCH 05/29] Fix square bridge --- src/Bridges/Constraint/bridges/square.jl | 84 +++++++++++++++++------- src/Utilities/functions.jl | 6 +- src/functions.jl | 4 ++ test/Bridges/Constraint/square.jl | 74 +++++++++++++++++++++ 4 files changed, 140 insertions(+), 28 deletions(-) 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/Utilities/functions.jl b/src/Utilities/functions.jl index 709b9e2476..4d169feb9c 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -802,10 +802,10 @@ function Base.getindex( end function Base.getindex( - it::ScalarFunctionIterator{<:MOI.GenericVectorFunction}, + it::ScalarFunctionIterator{F}, output_index::Integer, -) - return it.f.rows[output_index] +) where {F<:MOI.GenericVectorFunction} + return convert(scalar_type(F), it.f.rows[output_index]) end function Base.getindex( diff --git a/src/functions.jl b/src/functions.jl index 3e324867e3..6fd4dd0ef5 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -1172,6 +1172,10 @@ end # ScalarNonlinearFunction +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/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() From f32a459192ce973fc69560c0cce595f50b1a69b7 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 29 Jun 2023 10:29:31 +1200 Subject: [PATCH 06/29] Improve test coverage --- src/Utilities/functions.jl | 27 ++++++++++++++++--- src/functions.jl | 4 +++ test/Utilities/functions.jl | 54 +++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index 4d169feb9c..091d3fe73f 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -488,15 +488,30 @@ function substitute_variables( return g end +function _unstable_substitute_variables( + variable_map::F, + x::MOI.VariableIndex, +) where {F<:Function} + return variable_map(x) +end + +function _unstable_substitute_variables( + variable_map::F, + x::Any, +) where {F<:Function} + return substitute_variables(variable_map, x) +end + 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 = map( + Base.Fix1(_unstable_substitute_variables, variable_map), + f.args, ) + return MOI.ScalarNonlinearFunction(f.head, convert(Vector{Any}, new_args)) end function substitute_variables( @@ -538,7 +553,10 @@ function substitute_variables( variable_map::F, f::MOI.GenericVectorFunction{T}, ) where {T,F<:Function} - new_rows = map(Base.Fix1(substitute_variables, variable_map), f.rows) + new_rows = map( + Base.Fix1(_unstable_substitute_variables, variable_map), + f.rows, + ) return MOI.GenericVectorFunction{T}(convert(Vector{T}, new_rows)) end @@ -996,6 +1014,7 @@ function canonical(f::MOI.AbstractFunction) end canonicalize!(f::Union{MOI.VectorOfVariables,MOI.VariableIndex}) = f +canonicalize!(f::Union{Real,AbstractVector{<:Real}}) = f """ canonicalize!(f::Union{ScalarAffineFunction, VectorAffineFunction}) diff --git a/src/functions.jl b/src/functions.jl index 6fd4dd0ef5..45e3fe4280 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -1172,6 +1172,10 @@ 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 diff --git a/test/Utilities/functions.jl b/test/Utilities/functions.jl index f417073c0c..f7feb88076 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,48 @@ 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]) + h = MOI.ScalarNonlinearFunction(:log, Any[1.5 * x]) + @test ≈( + MOI.Utilities.substitute_variables(x -> 1.5 * x, g), + MOI.VectorNonlinearFunction(Any[1.0, 1.5 * x, 3.0 * x, h]), + ) + 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[3] === fi + MOI.Utilities.canonicalize!(g) + @test g.rows[3] === fi + @test length(fi.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]), From 5b1d9511b02ab0b7197761aae0a69a34c515bd66 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 29 Jun 2023 10:51:33 +1200 Subject: [PATCH 07/29] Fix formatting --- src/Utilities/functions.jl | 12 ++++-------- test/Utilities/functions.jl | 16 ++++++++-------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index 091d3fe73f..c4c2673d76 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -507,10 +507,8 @@ function substitute_variables( f::MOI.ScalarNonlinearFunction, ) where {F<:Function} # TODO(odow): this uses recursion. We should remove at some point. - new_args = map( - Base.Fix1(_unstable_substitute_variables, variable_map), - f.args, - ) + new_args = + map(Base.Fix1(_unstable_substitute_variables, variable_map), f.args) return MOI.ScalarNonlinearFunction(f.head, convert(Vector{Any}, new_args)) end @@ -553,10 +551,8 @@ function substitute_variables( variable_map::F, f::MOI.GenericVectorFunction{T}, ) where {T,F<:Function} - new_rows = map( - Base.Fix1(_unstable_substitute_variables, variable_map), - f.rows, - ) + new_rows = + map(Base.Fix1(_unstable_substitute_variables, variable_map), f.rows) return MOI.GenericVectorFunction{T}(convert(Vector{T}, new_rows)) end diff --git a/test/Utilities/functions.jl b/test/Utilities/functions.jl index f7feb88076..cd991d341b 100644 --- a/test/Utilities/functions.jl +++ b/test/Utilities/functions.jl @@ -265,7 +265,7 @@ 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]) + 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)], @@ -337,11 +337,11 @@ 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]) - h = MOI.ScalarNonlinearFunction(:log, Any[1.5 * x]) + g = MOI.VectorNonlinearFunction(Any[1.0, x, 2.0*x, f]) + h = MOI.ScalarNonlinearFunction(:log, Any[1.5*x]) @test ≈( MOI.Utilities.substitute_variables(x -> 1.5 * x, g), - MOI.VectorNonlinearFunction(Any[1.0, 1.5 * x, 3.0 * x, h]), + MOI.VectorNonlinearFunction(Any[1.0, 1.5*x, 3.0*x, h]), ) return end @@ -349,7 +349,7 @@ 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]) + 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]) @@ -363,15 +363,15 @@ 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]) + 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])) + @test ≈(scalars[2:3], MOI.VectorNonlinearFunction(Any[x, 2.0*x])) return end From fcaa9d75ae4b6d16b607329cb845e189e4376a5f Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 29 Jun 2023 11:32:34 +1200 Subject: [PATCH 08/29] Update --- src/Utilities/functions.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index c4c2673d76..20c2fc9edc 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -119,6 +119,8 @@ for a similar function where `value_fn` returns an """ function eval_variables end +eval_variables(::Function, x::Union{Real,AbstractVector{<:Real}}) = x + function eval_variables(value_fn::Function, t::MOI.ScalarAffineTerm) return t.coefficient * value_fn(t.variable) end From 9592ed48120beec2443e6102db35b752d15a5422 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 29 Jun 2023 12:23:17 +1200 Subject: [PATCH 09/29] Update functions.jl --- src/Utilities/functions.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index 20c2fc9edc..e1578369ab 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -191,7 +191,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 From cd718895f732c6330ad1640d2a91801375138f8b Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 29 Jun 2023 14:35:56 +1200 Subject: [PATCH 10/29] Update --- src/functions.jl | 8 -------- test/Bridges/Constraint/scalarize.jl | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/functions.jl b/src/functions.jl index 45e3fe4280..fa4065057c 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -13,7 +13,6 @@ components if `f` is an [`AbstractVectorFunction`](@ref). function output_dimension end output_dimension(::AbstractScalarFunction) = 1 -output_dimension(x::AbstractVector) = length(x) """ constant(f::AbstractFunction[, ::Type{T}]) where {T} @@ -434,13 +433,6 @@ function Base.isapprox( return all(isapprox(xi, yi; kwargs...) for (xi, yi) in zip(x.rows, y.rows)) end -function Base.convert( - ::Type{GenericVectorFunction{T}}, - rows::Vector{T}, -) where {T} - return GenericVectorFunction(rows) -end - """ VectorOfVariables(variables::Vector{VariableIndex}) <: AbstractVectorFunction diff --git a/test/Bridges/Constraint/scalarize.jl b/test/Bridges/Constraint/scalarize.jl index 6460388baf..fdc02d909e 100644 --- a/test/Bridges/Constraint/scalarize.jl +++ b/test/Bridges/Constraint/scalarize.jl @@ -241,6 +241,29 @@ function test_runtests() 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 + return +end + end # module TestConstraintScalarize.runtests() From 8ec0c0a17d58b269375aff399bbbfa07d5d6eb3e Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 29 Jun 2023 15:09:39 +1200 Subject: [PATCH 11/29] Support more bridges --- src/Bridges/Constraint/bridges/scalarize.jl | 2 +- src/Bridges/Constraint/bridges/vectorize.jl | 8 ----- src/Utilities/operate.jl | 11 ++++++ test/Bridges/Constraint/scalarize.jl | 13 +++++-- test/Bridges/Constraint/vectorize.jl | 39 +++++++++++---------- 5 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/Bridges/Constraint/bridges/scalarize.jl b/src/Bridges/Constraint/bridges/scalarize.jl index 0038cb4553..ce8feb449f 100644 --- a/src/Bridges/Constraint/bridges/scalarize.jl +++ b/src/Bridges/Constraint/bridges/scalarize.jl @@ -263,7 +263,7 @@ end function MOI.set( model::MOI.ModelLike, - ::MOI.ConstraintFunction, + attr::MOI.ConstraintFunction, bridge::ScalarizeBridge{T,F,S}, func, ) where {T,F,S} 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/Utilities/operate.jl b/src/Utilities/operate.jl index 4d490c15a4..6229223184 100644 --- a/src/Utilities/operate.jl +++ b/src/Utilities/operate.jl @@ -1737,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.GenericVectorFunction, + g::Union{T,MOI.AbstractScalarFunction}, +) where {T} + f.rows[output_index] = operate!(op, T, f.rows[output_index], g) + return f +end + """ operate_coefficient( op::Function, diff --git a/test/Bridges/Constraint/scalarize.jl b/test/Bridges/Constraint/scalarize.jl index fdc02d909e..159ac3a877 100644 --- a/test/Bridges/Constraint/scalarize.jl +++ b/test/Bridges/Constraint/scalarize.jl @@ -248,7 +248,7 @@ function test_VectorNonlinearFunction_mixed_type() 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]) + 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}()) @@ -256,11 +256,20 @@ function test_VectorNonlinearFunction_mixed_type() 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]) + 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/vectorize.jl b/test/Bridges/Constraint/vectorize.jl index e51e6cb398..b08b9ce5b3 100644 --- a/test/Bridges/Constraint/vectorize.jl +++ b/test/Bridges/Constraint/vectorize.jl @@ -238,24 +238,27 @@ function test_runtests() return end -MOI.Utilities.@model( - Model2179, - (), - (MOI.GreaterThan, MOI.LessThan), - (), - (), - (), - (MOI.ScalarAffineFunction,), - (), - () -) - -function test_unsupported_ScalarNonlinearFunction() - model = MOI.instantiate(Model2179{Float64}; with_bridge_type = Float64) - MOI.supports_constraint( - model, - MOI.ScalarNonlinearFunction, - MOI.GreaterThan{Float64}, +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 From bca15db04058072f1e3d56a64c4adc4864599983 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 29 Jun 2023 15:16:37 +1200 Subject: [PATCH 12/29] Add test for operate_output_index --- test/Utilities/test_operate!.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Utilities/test_operate!.jl b/test/Utilities/test_operate!.jl index afec6f7db1..704dd3cf01 100644 --- a/test/Utilities/test_operate!.jl +++ b/test/Utilities/test_operate!.jl @@ -597,6 +597,7 @@ 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 j in 1:i From a1b5ddd9892af2a3c2c6a48e3ef5c2cafbcea744 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 4 Aug 2023 10:52:34 +1200 Subject: [PATCH 13/29] Fix ambiguity --- src/Utilities/operate.jl | 20 ++++++++++---------- src/Utilities/promote_operation.jl | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Utilities/operate.jl b/src/Utilities/operate.jl index 6229223184..5a6a546b82 100644 --- a/src/Utilities/operate.jl +++ b/src/Utilities/operate.jl @@ -296,7 +296,7 @@ function operate( MOI.VectorQuadraticFunction{T}, MOI.VectorNonlinearFunction, }, -) where {T} +) where {T<:Number} args = Any[ operate(+, T, fi, gi) for (fi, gi) in zip(scalarize(f), scalarize(g)) ] @@ -313,7 +313,7 @@ function operate( MOI.VectorQuadraticFunction{T}, }, g::MOI.VectorNonlinearFunction, -) where {T} +) where {T<:Number} args = Any[ operate(+, T, fi, gi) for (fi, gi) in zip(scalarize(f), scalarize(g)) ] @@ -378,7 +378,7 @@ function operate( ::typeof(-), ::Type{T}, f::MOI.VectorNonlinearFunction, -) where {T} +) where {T<:Number} return MOI.VectorNonlinearFunction(Any[operate(-, T, fi) for fi in f.rows]) end @@ -601,7 +601,7 @@ function operate( MOI.VectorQuadraticFunction{T}, MOI.VectorNonlinearFunction, }, -) where {T} +) where {T<:Number} args = Any[ operate(-, T, fi, gi) for (fi, gi) in zip(scalarize(f), scalarize(g)) ] @@ -618,7 +618,7 @@ function operate( MOI.VectorQuadraticFunction{T}, }, g::MOI.VectorNonlinearFunction, -) where {T} +) where {T<:Number} args = Any[ operate(-, T, fi, gi) for (fi, gi) in zip(scalarize(f), scalarize(g)) ] @@ -675,7 +675,7 @@ function operate( ::Type{T}, f::T, g::MOI.VectorNonlinearFunction, -) where {T} +) where {T<:Number} return MOI.VectorNonlinearFunction(Any[operate(*, T, f, h) for h in g.rows]) end @@ -711,7 +711,7 @@ function operate( ::Type{T}, f::MOI.VectorNonlinearFunction, g::T, -) where {T} +) where {T<:Number} return MOI.VectorNonlinearFunction(Any[operate(*, T, h, g) for h in f.rows]) end @@ -892,7 +892,7 @@ function operate( ::Type{T}, f::MOI.VectorNonlinearFunction, g::T, -) where {T} +) where {T<:Number} return MOI.VectorNonlinearFunction(Any[operate(/, T, h, g) for h in f.rows]) end @@ -978,7 +978,7 @@ function operate( MOI.VectorQuadraticFunction{T}, MOI.VectorNonlinearFunction, }..., -) where {T} +) where {T<:Number} out = Any[] for a in args if a isa T @@ -1743,7 +1743,7 @@ function operate_output_index!( output_index::Integer, f::MOI.GenericVectorFunction, g::Union{T,MOI.AbstractScalarFunction}, -) where {T} +) where {T<:Number} f.rows[output_index] = operate!(op, T, f.rows[output_index], g) return f end diff --git a/src/Utilities/promote_operation.jl b/src/Utilities/promote_operation.jl index 70f8222f53..94f4789dd7 100644 --- a/src/Utilities/promote_operation.jl +++ b/src/Utilities/promote_operation.jl @@ -442,7 +442,7 @@ function promote_operation( MOI.VectorNonlinearFunction, }, }..., -) where {T} +) where {T<:Number} return MOI.VectorNonlinearFunction end From 056d9a52064460efd489899a3bb4ab002373948b Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 4 Aug 2023 11:05:52 +1200 Subject: [PATCH 14/29] Update --- src/Utilities/promote_operation.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utilities/promote_operation.jl b/src/Utilities/promote_operation.jl index 94f4789dd7..af8b9af09d 100644 --- a/src/Utilities/promote_operation.jl +++ b/src/Utilities/promote_operation.jl @@ -236,7 +236,7 @@ function promote_operation( ::Type{T}, ::Type{T}, ::Type{F}, -) where {T,F<:MOI.GenericVectorFunction} +) where {T<:Number,F<:MOI.GenericVectorFunction} return vector_type(promote_operation(*, T, T, scalar_type(F))) end From e105f56b4b5481326df623123a09124c1e925415 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 4 Aug 2023 11:59:20 +1200 Subject: [PATCH 15/29] Remove GenericVectorFunction --- src/Utilities/functions.jl | 39 +++++++++++---------- src/Utilities/operate.jl | 2 +- src/Utilities/promote_operation.jl | 28 ++++++++-------- src/functions.jl | 54 ++++++++++++++---------------- 4 files changed, 63 insertions(+), 60 deletions(-) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index e1578369ab..eaeb303e5d 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -202,7 +202,7 @@ end function eval_variables( value_fn::F, model::MOI.ModelLike, - f::MOI.GenericVectorFunction, + f::MOI.VectorNonlinearFunction, ) where {F} return map(f.rows) do row return eval_variables(value_fn, model, row) @@ -348,10 +348,10 @@ end function map_indices( index_map::F, - f::MOI.GenericVectorFunction{T}, -) where {F<:Function,T} - return MOI.GenericVectorFunction{T}( - convert(Vector{T}, map_indices(index_map, f.rows)), + f::MOI.VectorNonlinearFunction, +) where {F<:Function} + return VectorNonlinearFunction( + convert(Vector{Any}, map_indices(index_map, f.rows)), ) end @@ -551,11 +551,11 @@ end function substitute_variables( variable_map::F, - f::MOI.GenericVectorFunction{T}, -) where {T,F<:Function} + f::VectorNonlinearFunction, +) where {F<:Function} new_rows = map(Base.Fix1(_unstable_substitute_variables, variable_map), f.rows) - return MOI.GenericVectorFunction{T}(convert(Vector{T}, new_rows)) + return VectorNonlinearFunction(convert(Vector{Any}, new_rows)) end """ @@ -818,17 +818,17 @@ function Base.getindex( end function Base.getindex( - it::ScalarFunctionIterator{F}, + it::ScalarFunctionIterator{MOI.VectorNonlinearFunction}, output_index::Integer, -) where {F<:MOI.GenericVectorFunction} - return convert(scalar_type(F), it.f.rows[output_index]) +) + return convert(MOI.ScalarNonlinearFunction, it.f.rows[output_index]) end function Base.getindex( - it::ScalarFunctionIterator{MOI.GenericVectorFunction{F}}, + it::ScalarFunctionIterator{MOI.VectorNonlinearFunction}, output_index::AbstractVector{<:Integer}, -) where {F} - return MOI.GenericVectorFunction{F}(it.f.rows[output_index]) +) + return MOI.VectorNonlinearFunction(it.f.rows[output_index]) end """ @@ -957,7 +957,7 @@ function is_canonical( _is_strictly_sorted(f.quadratic_terms) end -is_canonical(f::MOI.GenericVectorFunction) = all(is_canonical, f.rows) +is_canonical(f::MOI.VectorNonlinearFunction) = all(is_canonical, f.rows) function _is_strictly_sorted(x::Vector) if isempty(x) @@ -1036,7 +1036,7 @@ function canonicalize!(f::MOI.ScalarNonlinearFunction) return f end -function canonicalize!(f::MOI.GenericVectorFunction) +function canonicalize!(f::MOI.VectorNonlinearFunction) for (i, fi) in enumerate(f.rows) f.rows[i] = canonicalize!(fi) end @@ -2239,7 +2239,12 @@ function scalarize( return functions end -scalarize(f::MOI.GenericVectorFunction, ignore_constants::Bool = false) = f.rows +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 diff --git a/src/Utilities/operate.jl b/src/Utilities/operate.jl index 5a6a546b82..8b9e30984c 100644 --- a/src/Utilities/operate.jl +++ b/src/Utilities/operate.jl @@ -1741,7 +1741,7 @@ function operate_output_index!( op::Union{typeof(+),typeof(-)}, ::Type{T}, output_index::Integer, - f::MOI.GenericVectorFunction, + f::MOI.VectorNonlinearFunction, g::Union{T,MOI.AbstractScalarFunction}, ) where {T<:Number} f.rows[output_index] = operate!(op, T, f.rows[output_index], g) diff --git a/src/Utilities/promote_operation.jl b/src/Utilities/promote_operation.jl index af8b9af09d..2d7fbe2107 100644 --- a/src/Utilities/promote_operation.jl +++ b/src/Utilities/promote_operation.jl @@ -123,14 +123,14 @@ function promote_operation( MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, - MOI.GenericVectorFunction, + MOI.VectorNonlinearFunction, }, F2<:Union{ AbstractVector{T}, MOI.VectorOfVariables, MOI.VectorAffineFunction{T}, MOI.VectorQuadraticFunction{T}, - MOI.GenericVectorFunction, + MOI.VectorNonlinearFunction, }, } S = promote_operation(op, T, scalar_type(F1), scalar_type(F2)) @@ -177,9 +177,9 @@ end function promote_operation( ::typeof(-), ::Type{T}, - ::Type{F}, -) where {T,F<:MOI.GenericVectorFunction} - return vector_type(promote_operation(-, T, scalar_type(F))) + ::Type{MOI.VectorNonlinearFunction}, +) where {T<:Number} + return vector_type(promote_operation(-, T, MOI.ScalarNonlinearFunction)) end ### Method 3a @@ -235,9 +235,9 @@ function promote_operation( ::typeof(*), ::Type{T}, ::Type{T}, - ::Type{F}, -) where {T<:Number,F<:MOI.GenericVectorFunction} - return vector_type(promote_operation(*, T, T, scalar_type(F))) + ::Type{MOI.VectorNonlinearFunction}, +) where {T<:Number} + return vector_type(promote_operation(*, T, T, MOI.ScalarNonlinearFunction)) end ### Method 3b @@ -283,10 +283,10 @@ end function promote_operation( ::typeof(*), ::Type{T}, - ::Type{F}, + ::Type{MOI.VectorNonlinearFunction}, ::Type{T}, -) where {T,F<:MOI.GenericVectorFunction} - return vector_type(promote_operation(*, T, scalar_type(F), T)) +) where {T<:Number} + return vector_type(promote_operation(*, T, MOI.ScalarNonlinearFunction, T)) end ### Method 3c @@ -363,10 +363,10 @@ end function promote_operation( ::typeof(/), ::Type{T}, - ::Type{F}, + ::Type{MOI.VectorNonlinearFunction}, ::Type{T}, -) where {T,F<:MOI.GenericVectorFunction} - return vector_type(promote_operation(/, T, scalar_type(F), T)) +) where {T} + return vector_type(promote_operation(/, T, MOI.ScalarNonlinearFunction, T)) end ### Method 5a diff --git a/src/functions.jl b/src/functions.jl index fa4065057c..b945900f98 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -406,33 +406,6 @@ All subtypes of `AbstractVectorFunction` must implement: """ abstract type AbstractVectorFunction <: AbstractFunction end -struct GenericVectorFunction{T} <: AbstractVectorFunction - rows::Vector{T} -end - -output_dimension(f::GenericVectorFunction) = length(f.rows) - -function constant(f::GenericVectorFunction, ::Type{T}) where {T} - return zeros(T, output_dimension(f)) -end - -Base.copy(f::GenericVectorFunction) = GenericVectorFunction(copy(f.rows)) - -function Base.:(==)( - f::GenericVectorFunction{T}, - g::GenericVectorFunction{T}, -) where {T} - return f.rows == g.rows -end - -function Base.isapprox( - x::GenericVectorFunction{T}, - y::GenericVectorFunction{T}; - kwargs..., -) where {T} - return all(isapprox(xi, yi; kwargs...) for (xi, yi) in zip(x.rows, y.rows)) -end - """ VectorOfVariables(variables::Vector{VariableIndex}) <: AbstractVectorFunction @@ -731,7 +704,32 @@ julia> MOI.VectorNonlinearFunction(Any[g, x]) └ ┘ ``` """ -const VectorNonlinearFunction = GenericVectorFunction{Any} +struct VectorNonlinearFunction <: AbstractVectorFunction + rows::Vector{Any} +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 From 1baf92d8fb56154ad4b56b744e1ba541f766f440 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 4 Aug 2023 12:05:42 +1200 Subject: [PATCH 16/29] Update --- src/Utilities/functions.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index eaeb303e5d..253dbe84f2 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -551,11 +551,11 @@ end function substitute_variables( variable_map::F, - f::VectorNonlinearFunction, + f::MOI.VectorNonlinearFunction, ) where {F<:Function} new_rows = map(Base.Fix1(_unstable_substitute_variables, variable_map), f.rows) - return VectorNonlinearFunction(convert(Vector{Any}, new_rows)) + return MOI.VectorNonlinearFunction(convert(Vector{Any}, new_rows)) end """ From 565494e1f8aa215fa032d498671acaada8034e58 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 4 Aug 2023 13:10:41 +1200 Subject: [PATCH 17/29] Fix --- src/Utilities/functions.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index 253dbe84f2..6a9bd2a6b4 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -350,7 +350,7 @@ function map_indices( index_map::F, f::MOI.VectorNonlinearFunction, ) where {F<:Function} - return VectorNonlinearFunction( + return MOI.VectorNonlinearFunction( convert(Vector{Any}, map_indices(index_map, f.rows)), ) end From 492c18a98b46ed09507bcd5b1e121d8ab17e58cb Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 4 Aug 2023 15:05:55 +1200 Subject: [PATCH 18/29] Fix formatting --- src/functions.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/functions.jl b/src/functions.jl index b945900f98..dfb80d4c95 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -716,10 +716,7 @@ end Base.copy(f::VectorNonlinearFunction) = VectorNonlinearFunction(copy(f.rows)) -function Base.:(==)( - f::VectorNonlinearFunction, - g::VectorNonlinearFunction, -) +function Base.:(==)(f::VectorNonlinearFunction, g::VectorNonlinearFunction) return f.rows == g.rows end From 162b64eeb1caf6745a8636b18c518db9065cb652 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 7 Aug 2023 11:08:22 +1200 Subject: [PATCH 19/29] Update src/Bridges/Constraint/bridges/scalarize.jl --- src/Bridges/Constraint/bridges/scalarize.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bridges/Constraint/bridges/scalarize.jl b/src/Bridges/Constraint/bridges/scalarize.jl index ce8feb449f..4fbce844e9 100644 --- a/src/Bridges/Constraint/bridges/scalarize.jl +++ b/src/Bridges/Constraint/bridges/scalarize.jl @@ -263,7 +263,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintFunction, + ::MOI.ConstraintFunction, bridge::ScalarizeBridge{T,F,S}, func, ) where {T,F,S} From df8ec7d23c07bd021a2e73397dc860bd2e6682ff Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 7 Aug 2023 13:21:32 +1200 Subject: [PATCH 20/29] Update src/Bridges/Constraint/bridges/scalarize.jl --- src/Bridges/Constraint/bridges/scalarize.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bridges/Constraint/bridges/scalarize.jl b/src/Bridges/Constraint/bridges/scalarize.jl index 4fbce844e9..0038cb4553 100644 --- a/src/Bridges/Constraint/bridges/scalarize.jl +++ b/src/Bridges/Constraint/bridges/scalarize.jl @@ -263,7 +263,7 @@ end function MOI.set( model::MOI.ModelLike, - ::MOI.ConstraintFunction, + ::MOI.ConstraintFunction, bridge::ScalarizeBridge{T,F,S}, func, ) where {T,F,S} From 42b71b20d8bf938a24621c461b26ffa257ee0c96 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 9 Aug 2023 12:35:36 +1200 Subject: [PATCH 21/29] Vector{ScalarNonlinearFunction} --- src/Test/test_basic_constraint.jl | 2 +- src/Utilities/functions.jl | 10 ++++------ src/functions.jl | 6 +++--- test/Utilities/functions.jl | 17 ++++++++++++----- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Test/test_basic_constraint.jl b/src/Test/test_basic_constraint.jl index 9472da95c6..6f7dae5a41 100644 --- a/src/Test/test_basic_constraint.jl +++ b/src/Test/test_basic_constraint.jl @@ -86,7 +86,7 @@ function _function( :+, Any[MOI.ScalarNonlinearFunction(:^, Any[xi, 2]) for xi in x], ) - return MOI.VectorNonlinearFunction(Any[f; x]) + return MOI.VectorNonlinearFunction([f; x]) end # Default fallback. diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index 6a9bd2a6b4..b3d9549527 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -350,9 +350,7 @@ function map_indices( index_map::F, f::MOI.VectorNonlinearFunction, ) where {F<:Function} - return MOI.VectorNonlinearFunction( - convert(Vector{Any}, map_indices(index_map, f.rows)), - ) + return MOI.VectorNonlinearFunction(map_indices(index_map, f.rows)) end map_indices(::F, change::MOI.ScalarConstantChange) where {F<:Function} = change @@ -553,9 +551,9 @@ function substitute_variables( variable_map::F, f::MOI.VectorNonlinearFunction, ) where {F<:Function} - new_rows = - map(Base.Fix1(_unstable_substitute_variables, variable_map), f.rows) - return MOI.VectorNonlinearFunction(convert(Vector{Any}, new_rows)) + return MOI.VectorNonlinearFunction( + map(Base.Fix1(_unstable_substitute_variables, variable_map), f.rows), + ) end """ diff --git a/src/functions.jl b/src/functions.jl index dfb80d4c95..d09282de5c 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -697,15 +697,15 @@ julia> g = MOI.ScalarNonlinearFunction( ) ^(sin(MOI.VariableIndex(1)), 2.0) -julia> MOI.VectorNonlinearFunction(Any[g, x]) +julia> MOI.VectorNonlinearFunction([g, x]) ┌ ┐ │^(sin(MOI.VariableIndex(1)), 2.0)│ -│MOI.VariableIndex(1) │ +│+(MOI.VariableIndex(1)) │ └ ┘ ``` """ struct VectorNonlinearFunction <: AbstractVectorFunction - rows::Vector{Any} + rows::Vector{ScalarNonlinearFunction} end output_dimension(f::VectorNonlinearFunction) = length(f.rows) diff --git a/test/Utilities/functions.jl b/test/Utilities/functions.jl index cd991d341b..8ae2e2f7cc 100644 --- a/test/Utilities/functions.jl +++ b/test/Utilities/functions.jl @@ -338,10 +338,17 @@ function test_substitute_variables_vector_nonlinear_function() x = MOI.add_variable(model) f = MOI.ScalarNonlinearFunction(:log, Any[x]) g = MOI.VectorNonlinearFunction(Any[1.0, x, 2.0*x, f]) - h = MOI.ScalarNonlinearFunction(:log, Any[1.5*x]) @test ≈( MOI.Utilities.substitute_variables(x -> 1.5 * x, g), - MOI.VectorNonlinearFunction(Any[1.0, 1.5*x, 3.0*x, h]), + 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 @@ -353,10 +360,10 @@ function test_canonicalize_vector_nonlinear_function() fi = 1.0 * x + 1.0 * x @test length(fi.terms) == 2 g = MOI.VectorNonlinearFunction(Any[1.0, x, fi, f]) - @test g.rows[3] === fi + @test g.rows[4] === f MOI.Utilities.canonicalize!(g) - @test g.rows[3] === fi - @test length(fi.terms) == 1 + @test g.rows[4] === f + @test length(f.args[1].terms) == 1 return end From 6192b30f7144333f74ef081cc47a63118cea4f0f Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 9 Aug 2023 12:40:03 +1200 Subject: [PATCH 22/29] Update docstring --- src/functions.jl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/functions.jl b/src/functions.jl index d09282de5c..9ea0231d24 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -666,14 +666,17 @@ function Base.copy(f::VectorQuadraticFunction) end """ - VectorNonlinearFunction(args::Vector{Any}) + VectorNonlinearFunction(args::Vector{ScalarNonlinearFunction}) -The vector-valued nonlinear function composed of a vector of scalar functions. +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 one of the following: +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) @@ -703,6 +706,8 @@ julia> MOI.VectorNonlinearFunction([g, x]) │+(MOI.VariableIndex(1)) │ └ ┘ ``` + +Note the automatic conversion from `x` to `+(x)`. """ struct VectorNonlinearFunction <: AbstractVectorFunction rows::Vector{ScalarNonlinearFunction} From ce67ebd806e1480f9aeb9efeef3b695c2f8e8044 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 9 Aug 2023 13:17:08 +1200 Subject: [PATCH 23/29] Update --- src/Utilities/functions.jl | 6 +++--- test/Utilities/functions.jl | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index b3d9549527..96a497c2f2 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -819,7 +819,7 @@ function Base.getindex( it::ScalarFunctionIterator{MOI.VectorNonlinearFunction}, output_index::Integer, ) - return convert(MOI.ScalarNonlinearFunction, it.f.rows[output_index]) + return it.f.rows[output_index] end function Base.getindex( @@ -2156,8 +2156,8 @@ function vectorize( return MOI.VectorQuadraticFunction(quadratic_terms, affine_terms, constant) end -function vectorize(x::AbstractVector) - return MOI.VectorNonlinearFunction(collect(x)) +function vectorize(x::AbstractVector{MOI.ScalarNonlinearFunction}) + return MOI.VectorNonlinearFunction(x) end scalarize(f::AbstractVector, ::Bool = false) = f diff --git a/test/Utilities/functions.jl b/test/Utilities/functions.jl index 8ae2e2f7cc..b98c090783 100644 --- a/test/Utilities/functions.jl +++ b/test/Utilities/functions.jl @@ -345,7 +345,7 @@ function test_substitute_variables_vector_nonlinear_function() MOI.ScalarNonlinearFunction(:+, Any[1.5*x]), MOI.ScalarNonlinearFunction( :+, - Any[MOI.ScalarNonlinearFunction(:*, Any[2.0, 1.5 * x])], + Any[MOI.ScalarNonlinearFunction(:*, Any[2.0, 1.5*x])], ), MOI.ScalarNonlinearFunction(:log, Any[1.5*x]), ]), From d1a81d6ad17c03fb74d5a1a6c24d27711cbe57c9 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 9 Aug 2023 13:34:38 +1200 Subject: [PATCH 24/29] Fix bad rebase --- test/Bridges/Constraint/vectorize.jl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/Bridges/Constraint/vectorize.jl b/test/Bridges/Constraint/vectorize.jl index b08b9ce5b3..bd13d3acb9 100644 --- a/test/Bridges/Constraint/vectorize.jl +++ b/test/Bridges/Constraint/vectorize.jl @@ -238,6 +238,28 @@ function test_runtests() return end +MOI.Utilities.@model( + Model2179, + (), + (MOI.GreaterThan, MOI.LessThan), + (), + (), + (), + (MOI.ScalarAffineFunction,), + (), + () + ) + + function test_unsupported_ScalarNonlinearFunction() + model = MOI.instantiate(Model2179{Float64}; with_bridge_type = Float64) + MOI.supports_constraint( + model, + MOI.ScalarNonlinearFunction, + MOI.GreaterThan{Float64}, + ) + 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) From 6c377e83dae21e0e2586a355317a8ebf302f7e3b Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 9 Aug 2023 13:35:36 +1200 Subject: [PATCH 25/29] Fix formatting --- test/Bridges/Constraint/vectorize.jl | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/test/Bridges/Constraint/vectorize.jl b/test/Bridges/Constraint/vectorize.jl index bd13d3acb9..8e6fb2cbe4 100644 --- a/test/Bridges/Constraint/vectorize.jl +++ b/test/Bridges/Constraint/vectorize.jl @@ -239,26 +239,26 @@ function test_runtests() end MOI.Utilities.@model( - Model2179, - (), - (MOI.GreaterThan, MOI.LessThan), - (), - (), - (), - (MOI.ScalarAffineFunction,), - (), - () + Model2179, + (), + (MOI.GreaterThan, MOI.LessThan), + (), + (), + (), + (MOI.ScalarAffineFunction,), + (), + () ) - function test_unsupported_ScalarNonlinearFunction() - model = MOI.instantiate(Model2179{Float64}; with_bridge_type = Float64) - MOI.supports_constraint( - model, - MOI.ScalarNonlinearFunction, - MOI.GreaterThan{Float64}, - ) - return - end +function test_unsupported_ScalarNonlinearFunction() + model = MOI.instantiate(Model2179{Float64}; with_bridge_type = Float64) + MOI.supports_constraint( + model, + MOI.ScalarNonlinearFunction, + MOI.GreaterThan{Float64}, + ) + return +end function test_VectorNonlinearFunction() # We can't use the standard runtests because ScalarNonlinearFunction does From 6be382f5299fa73adc205731ceda790b20332fb2 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 9 Aug 2023 13:36:06 +1200 Subject: [PATCH 26/29] Another fix --- test/Bridges/Constraint/vectorize.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Bridges/Constraint/vectorize.jl b/test/Bridges/Constraint/vectorize.jl index 8e6fb2cbe4..fefbe0fc4a 100644 --- a/test/Bridges/Constraint/vectorize.jl +++ b/test/Bridges/Constraint/vectorize.jl @@ -248,7 +248,7 @@ MOI.Utilities.@model( (MOI.ScalarAffineFunction,), (), () - ) +) function test_unsupported_ScalarNonlinearFunction() model = MOI.instantiate(Model2179{Float64}; with_bridge_type = Float64) From 10a75b1faf30ce20e851b953c94748176ec32e8d Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 9 Aug 2023 16:33:30 +1200 Subject: [PATCH 27/29] Update code coverage --- src/Utilities/functions.jl | 3 --- test/Utilities/test_operate!.jl | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index 96a497c2f2..2e6edc2130 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -119,8 +119,6 @@ for a similar function where `value_fn` returns an """ function eval_variables end -eval_variables(::Function, x::Union{Real,AbstractVector{<:Real}}) = x - function eval_variables(value_fn::Function, t::MOI.ScalarAffineTerm) return t.coefficient * value_fn(t.variable) end @@ -1010,7 +1008,6 @@ function canonical(f::MOI.AbstractFunction) end canonicalize!(f::Union{MOI.VectorOfVariables,MOI.VariableIndex}) = f -canonicalize!(f::Union{Real,AbstractVector{<:Real}}) = f """ canonicalize!(f::Union{ScalarAffineFunction, VectorAffineFunction}) diff --git a/test/Utilities/test_operate!.jl b/test/Utilities/test_operate!.jl index 704dd3cf01..4a82333729 100644 --- a/test/Utilities/test_operate!.jl +++ b/test/Utilities/test_operate!.jl @@ -599,7 +599,7 @@ function test_operate_output_index_1a() (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 From ec10ac3da6cc55f98ea423dd8eb16c9083ddf668 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 11 Aug 2023 08:10:21 +1200 Subject: [PATCH 28/29] Update --- src/Utilities/functions.jl | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index 2e6edc2130..017b0a7421 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -486,28 +486,19 @@ function substitute_variables( return g end -function _unstable_substitute_variables( - variable_map::F, - x::MOI.VariableIndex, -) where {F<:Function} - return variable_map(x) -end - -function _unstable_substitute_variables( - variable_map::F, - x::Any, -) where {F<:Function} - return substitute_variables(variable_map, x) -end - function substitute_variables( variable_map::F, f::MOI.ScalarNonlinearFunction, ) where {F<:Function} - # TODO(odow): this uses recursion. We should remove at some point. - new_args = - map(Base.Fix1(_unstable_substitute_variables, variable_map), f.args) - return MOI.ScalarNonlinearFunction(f.head, convert(Vector{Any}, new_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( @@ -549,9 +540,8 @@ function substitute_variables( variable_map::F, f::MOI.VectorNonlinearFunction, ) where {F<:Function} - return MOI.VectorNonlinearFunction( - map(Base.Fix1(_unstable_substitute_variables, variable_map), f.rows), - ) + rows = [substitute_variables(variable_map, row) for row in f.rows] + return MOI.VectorNonlinearFunction(rows) end """ From 5d13fd7a4af8e086d728ae79768d578487dc1f5d Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 14 Aug 2023 09:15:03 +1200 Subject: [PATCH 29/29] Apply suggestions from code review Co-authored-by: Miles Lubin --- src/Test/test_multiobjective.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Test/test_multiobjective.jl b/src/Test/test_multiobjective.jl index 4a88f6eea2..c33b4d14b5 100644 --- a/src/Test/test_multiobjective.jl +++ b/src/Test/test_multiobjective.jl @@ -239,7 +239,7 @@ function test_multiobjective_vector_nonlinear( 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] + ) # [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 @@ -257,7 +257,7 @@ function test_multiobjective_vector_nonlinear_delete( 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] + ) # [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 @@ -276,7 +276,7 @@ function test_multiobjective_vector_nonlinear_delete_vector( 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] + ) # [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 @@ -296,7 +296,7 @@ function test_multiobjective_vector_nonlinear_modify( 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] + ) # [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