diff --git a/src/Bridges/Constraint/vectorize.jl b/src/Bridges/Constraint/vectorize.jl index 44666d941d..4080089fc9 100644 --- a/src/Bridges/Constraint/vectorize.jl +++ b/src/Bridges/Constraint/vectorize.jl @@ -3,55 +3,82 @@ Transforms a constraint `G`-in-`scalar_set_type(S, T)` where `S <: VectorLinearSet` to `F`-in-`S`. + +## Examples + +The constraint `SingleVariable`-in-`LessThan{Float64}` becomes +`VectorAffineFunction{Float64}`-in-`Nonpositives`, where `T = Float64`, +`F = VectorAffineFunction{Float64}`, `S = Nonpositives`, and +`G = SingleVariable`. """ mutable struct VectorizeBridge{T, F, S, G} <: AbstractBridge vector_constraint::CI{F, S} set_constant::T # constant in scalar set end -function bridge_constraint(::Type{VectorizeBridge{T, F, S, G}}, - model::MOI.ModelLike, g::G, - set::MOIU.ScalarLinearSet{T}) where {T, F, S, G} - set_constant = MOI.constant(set) - h = MOIU.operate(-, T, g, set_constant) - if -set_constant != MOI.constant(h)[1] - # This means the constant in `f` was not zero - constant = MOI.constant(h)[1] + set_constant - throw(MOI.ScalarFunctionConstantNotZero{typeof(constant), G, - typeof(set)}(constant)) + +function bridge_constraint( + ::Type{VectorizeBridge{T, F, S, G}}, + model::MOI.ModelLike, + scalar_f::G, + set::MOIU.ScalarLinearSet{T} +) where {T, F, S, G} + scalar_const = MOI.constant(scalar_f, T) + if !iszero(scalar_const) + throw( + MOI.ScalarFunctionConstantNotZero{ + typeof(scalar_const), G, typeof(set) + }(scalar_const) + ) end - f = MOIU.operate(vcat, T, h) - vector_constraint = MOI.add_constraint(model, f, S(1)) - return VectorizeBridge{T, F, S, G}(vector_constraint, set_constant) + vector_f = convert(F, scalar_f) + set_const = MOI.constant(set) + MOIU.operate_output_index!(-, T, 1, vector_f, set_const) + vector_constraint = MOI.add_constraint(model, vector_f, S(1)) + return VectorizeBridge{T, F, S, G}(vector_constraint, set_const) end -function MOI.supports_constraint(::Type{VectorizeBridge{T}}, - ::Type{<:MOI.AbstractScalarFunction}, - ::Type{<:MOIU.ScalarLinearSet{T}}) where T +function MOI.supports_constraint( + ::Type{VectorizeBridge{T}}, + ::Type{<:MOI.AbstractScalarFunction}, + ::Type{<:MOIU.ScalarLinearSet{T}}, +) where {T} return true end -MOIB.added_constrained_variable_types(::Type{<:VectorizeBridge}) = Tuple{DataType}[] -function MOIB.added_constraint_types(::Type{<:VectorizeBridge{T, F, S}}) where {T, F, S} + +function MOIB.added_constrained_variable_types(::Type{<:VectorizeBridge}) + return Tuple{DataType}[] +end + +function MOIB.added_constraint_types( + ::Type{<:VectorizeBridge{T, F, S}} +) where {T, F, S} return [(F, S)] end -function concrete_bridge_type(::Type{<:VectorizeBridge{T}}, - G::Type{<:MOI.AbstractScalarFunction}, - S::Type{<:MOIU.ScalarLinearSet{T}}) where T + +function concrete_bridge_type( + ::Type{<:VectorizeBridge{T}}, + G::Type{<:MOI.AbstractScalarFunction}, + S::Type{<:MOIU.ScalarLinearSet{T}}, +) where {T} H = MOIU.promote_operation(-, T, G, T) F = MOIU.promote_operation(vcat, T, H) return VectorizeBridge{T, F, MOIU.vector_set_type(S), G} end # Attributes, Bridge acting as a model -function MOI.get(::VectorizeBridge{T, F, S}, - ::MOI.NumberOfConstraints{F, S}) where {T, F, S} + +function MOI.get( + ::VectorizeBridge{T, F, S}, ::MOI.NumberOfConstraints{F, S} +) where {T, F, S} return 1 end -function MOI.get(bridge::VectorizeBridge{T, F, S}, - ::MOI.ListOfConstraintIndices{F, S}) where {T, F, S} + +function MOI.get( + bridge::VectorizeBridge{T, F, S}, ::MOI.ListOfConstraintIndices{F, S} +) where {T, F, S} return [bridge.vector_constraint] end -# References function MOI.delete(model::MOI.ModelLike, bridge::VectorizeBridge) MOI.delete(model, bridge.vector_constraint) end @@ -61,62 +88,112 @@ end function MOI.supports( ::MOI.ModelLike, ::Union{MOI.ConstraintPrimalStart, MOI.ConstraintDualStart}, - ::Type{<:VectorizeBridge}) - + ::Type{<:VectorizeBridge}, +) return true end -function MOI.get(model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimal, MOI.ConstraintPrimalStart}, - bridge::VectorizeBridge) + +function MOI.get( + model::MOI.ModelLike, + attr::MOI.ConstraintPrimalStart, + bridge::VectorizeBridge, +) x = MOI.get(model, attr, bridge.vector_constraint) @assert length(x) == 1 - y = x[1] - if !(attr isa MOI.ConstraintPrimal && - MOIU.is_ray(MOI.get(model, MOI.PrimalStatus(attr.N)))) + return x[1] + bridge.set_constant +end + +function MOI.get( + model::MOI.ModelLike, + attr::MOI.ConstraintPrimal, + bridge::VectorizeBridge, +) + x = MOI.get(model, attr, bridge.vector_constraint) + @assert length(x) == 1 + if MOIU.is_ray(MOI.get(model, MOI.PrimalStatus(attr.N))) # If it is an infeasibility certificate, it is a ray and satisfies the # homogenized problem, see https://github.com/JuliaOpt/MathOptInterface.jl/issues/433 + return x[1] + else # Otherwise, we need to add the set constant since the ConstraintPrimal # is defined as the value of the function and the set_constant was # removed from the original function - y += bridge.set_constant - end - return y + return x[1] + bridge.set_constant + end end -function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintPrimalStart, - bridge::VectorizeBridge, value) - MOI.set(model, attr, bridge.vector_constraint, [value - bridge.set_constant]) + +function MOI.set( + model::MOI.ModelLike, + attr::MOI.ConstraintPrimalStart, + bridge::VectorizeBridge, + value, +) + MOI.set( + model, attr, bridge.vector_constraint, [value - bridge.set_constant] + ) + return end -function MOI.get(model::MOI.ModelLike, - attr::Union{MOI.ConstraintDual, MOI.ConstraintDualStart}, - bridge::VectorizeBridge) + +function MOI.get( + model::MOI.ModelLike, + attr::Union{MOI.ConstraintDual, MOI.ConstraintDualStart}, + bridge::VectorizeBridge, +) x = MOI.get(model, attr, bridge.vector_constraint) @assert length(x) == 1 return x[1] end -function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintDualStart, - bridge::VectorizeBridge, value) + +function MOI.set( + model::MOI.ModelLike, + attr::MOI.ConstraintDualStart, + bridge::VectorizeBridge, + value, +) MOI.set(model, attr, bridge.vector_constraint, [value]) + return end -function MOI.modify(model::MOI.ModelLike, bridge::VectorizeBridge, - change::MOI.ScalarCoefficientChange) - MOI.modify(model, bridge.vector_constraint, - MOI.MultirowChange(change.variable, - [(1, change.new_coefficient)])) + +function MOI.modify( + model::MOI.ModelLike, + bridge::VectorizeBridge, + change::MOI.ScalarCoefficientChange, +) + MOI.modify( + model, + bridge.vector_constraint, + MOI.MultirowChange(change.variable, [(1, change.new_coefficient)]), + ) + return end -function MOI.set(model::MOI.ModelLike, ::MOI.ConstraintSet, - bridge::VectorizeBridge, new_set::MOIU.ScalarLinearSet) + +function MOI.set( + model::MOI.ModelLike, + ::MOI.ConstraintSet, + bridge::VectorizeBridge, + new_set::MOIU.ScalarLinearSet, +) bridge.set_constant = MOI.constant(new_set) - MOI.modify(model, bridge.vector_constraint, - MOI.VectorConstantChange([-bridge.set_constant])) + MOI.modify( + model, + bridge.vector_constraint, + MOI.VectorConstantChange([-bridge.set_constant]), + ) + return end -function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintFunction, - bridge::VectorizeBridge{T, F, S, G}) where {T, F, S, G} + +function MOI.get( + model::MOI.ModelLike, + attr::MOI.ConstraintFunction, + bridge::VectorizeBridge{T, F, S, G} +) where {T, F, S, G} f = MOIU.scalarize(MOI.get(model, attr, bridge.vector_constraint), true) - @assert isone(length(f)) - # If `G` is `MOI.SingleVariable`, `f` will be `MOI.ScalarAffineFunction`. + @assert length(f) == 1 return convert(G, f[1]) end -function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintSet, - bridge::VectorizeBridge{T, F, S}) where {T, F, S} + +function MOI.get( + model::MOI.ModelLike, ::MOI.ConstraintSet, bridge::VectorizeBridge{T, F, S} +) where {T, F, S} return MOIU.scalar_set_type(S, T)(bridge.set_constant) end diff --git a/src/functions.jl b/src/functions.jl index 2409793f56..b67feb0162 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -428,26 +428,36 @@ end # Conversion between scalar functions # Conversion to SingleVariable function Base.convert(::Type{SingleVariable}, f::ScalarAffineFunction) - if !iszero(f.constant) || !isone(length(f.terms)) || !isone(f.terms[1].coefficient) + if ( + !iszero(f.constant) || + !isone(length(f.terms)) || + !isone(f.terms[1].coefficient) + ) throw(InexactError(:convert, SingleVariable, f)) end return SingleVariable(f.terms[1].variable_index) end -function Base.convert(::Type{SingleVariable}, - f::ScalarQuadraticFunction{T}) where T + +function Base.convert( + ::Type{SingleVariable}, f::ScalarQuadraticFunction{T} +) where {T} return convert(SingleVariable, convert(ScalarAffineFunction{T}, f)) end # Conversion to ScalarAffineFunction -function Base.convert(::Type{ScalarAffineFunction{T}}, α::T) where T +function Base.convert(::Type{ScalarAffineFunction{T}}, α::T) where {T} return ScalarAffineFunction{T}(ScalarAffineTerm{T}[], α) end -function Base.convert(::Type{ScalarAffineFunction{T}}, - f::SingleVariable) where T + +function Base.convert( + ::Type{ScalarAffineFunction{T}}, f::SingleVariable +) where {T} return ScalarAffineFunction{T}(f) end -function Base.convert(::Type{ScalarAffineFunction{T}}, - f::ScalarQuadraticFunction{T}) where T + +function Base.convert( + ::Type{ScalarAffineFunction{T}}, f::ScalarQuadraticFunction{T} +) where {T} if !Base.isempty(f.quadratic_terms) throw(InexactError(:convert, ScalarAffineFunction{T}, f)) end @@ -455,15 +465,86 @@ function Base.convert(::Type{ScalarAffineFunction{T}}, end # Conversion to ScalarQuadraticFunction -function Base.convert(::Type{ScalarQuadraticFunction{T}}, α::T) where T - return ScalarQuadraticFunction{T}(ScalarAffineTerm{T}[], ScalarQuadraticTerm{T}[], α) -end -function Base.convert(::Type{ScalarQuadraticFunction{T}}, - f::SingleVariable) where T - convert(ScalarQuadraticFunction{T}, convert(ScalarAffineFunction{T}, f)) -end -function Base.convert(::Type{ScalarQuadraticFunction{T}}, - f::ScalarAffineFunction{T}) where T - return ScalarQuadraticFunction{T}(f.terms, ScalarQuadraticTerm{T}[], - f.constant) +function Base.convert( + ::Type{ScalarQuadraticFunction{T}}, α::T +) where {T} + return ScalarQuadraticFunction{T}( + ScalarAffineTerm{T}[], ScalarQuadraticTerm{T}[], α + ) +end + +function Base.convert( + ::Type{ScalarQuadraticFunction{T}}, f::SingleVariable +) where {T} + return convert( + ScalarQuadraticFunction{T}, convert(ScalarAffineFunction{T}, f) + ) +end + +function Base.convert( + ::Type{ScalarQuadraticFunction{T}}, f::ScalarAffineFunction{T} +) where {T} + return ScalarQuadraticFunction{T}( + f.terms, ScalarQuadraticTerm{T}[], f.constant + ) +end + +function Base.convert(::Type{VectorOfVariables}, g::SingleVariable) + return VectorOfVariables([g.variable]) +end + +function Base.convert( + ::Type{VectorAffineFunction{T}}, g::SingleVariable +) where {T} + return VectorAffineFunction{T}( + [VectorAffineTerm(1, ScalarAffineTerm(one(T), g.variable))], + [zero(T)], + ) +end + +function Base.convert( + ::Type{VectorQuadraticFunction{T}}, g::SingleVariable +) where {T} + return VectorQuadraticFunction{T}( + [VectorAffineTerm(1, ScalarAffineTerm(one(T), g.variable))], + VectorQuadraticTerm{T}[], + [zero(T)], + ) +end + +function Base.convert( + ::Type{VectorAffineFunction{T}}, g::ScalarAffineFunction +) where {T} + return VectorAffineFunction{T}( + VectorAffineTerm{T}[ + VectorAffineTerm(1, term) for term in g.terms + ], + [g.constant], + ) +end + +function Base.convert( + ::Type{VectorQuadraticFunction{T}}, g::ScalarAffineFunction +) where {T} + return VectorQuadraticFunction{T}( + VectorAffineTerm{T}[ + VectorAffineTerm(1, term) for term in g.terms + ], + VectorQuadraticTerm{T}[], + [g.constant], + ) +end + +function Base.convert( + ::Type{VectorQuadraticFunction{T}}, g::ScalarQuadraticFunction +) where {T} + return VectorQuadraticFunction{T}( + VectorAffineTerm{T}[ + VectorAffineTerm(1, term) for term in g.affine_terms + ], + VectorQuadraticTerm{T}[ + VectorQuadraticTerm(1, term) for term in g.quadratic_terms + ], + [g.constant], + ) end diff --git a/test/Bridges/Constraint/vectorize.jl b/test/Bridges/Constraint/vectorize.jl index af3e6108c7..1a32fc7ffc 100644 --- a/test/Bridges/Constraint/vectorize.jl +++ b/test/Bridges/Constraint/vectorize.jl @@ -1,74 +1,106 @@ using Test -using MathOptInterface +import MathOptInterface const MOI = MathOptInterface const MOIT = MathOptInterface.Test const MOIU = MathOptInterface.Utilities const MOIB = MathOptInterface.Bridges -include("../utilities.jl") +include(joinpath(dirname(@__DIR__), "utilities.jl")) mock = MOIU.MockOptimizer(MOIU.UniversalFallback(MOIU.Model{Float64}())) config = MOIT.TestConfig() @testset "Vectorize" begin - bridged_mock = MOIB.Constraint.Vectorize{Float64}(mock) + bridged_mock = MOI.Bridges.Constraint.Vectorize{Float64}(mock) MOIT.scalar_function_constant_not_zero(bridged_mock) - MOIT.basic_constraint_tests(bridged_mock, config, - include=Iterators.product( - [MOI.SingleVariable, - MOI.ScalarAffineFunction{Float64}], - # TODO add it when operate(vcat, ...) - # is implemented for quadratic - #MOI.ScalarQuadraticFunction{Float64}], - [MOI.EqualTo{Float64}, - MOI.GreaterThan{Float64}, - MOI.LessThan{Float64}])) + MOIT.basic_constraint_tests( + bridged_mock, + config, + include = Iterators.product( + [ + MOI.SingleVariable, + MOI.ScalarAffineFunction{Float64}, + # TODO: add when operate(vcat, ...) is implemented for quadratic + # MOI.ScalarQuadraticFunction{Float64}, + ], [ + MOI.EqualTo{Float64}, + MOI.GreaterThan{Float64}, + MOI.LessThan{Float64}, + ], + ) + ) - MOIU.set_mock_optimize!(mock, - (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1, 0], + MOIU.set_mock_optimize!( + mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!( + mock, + [1, 0], (MOI.VectorAffineFunction{Float64}, MOI.Nonpositives) => [[-1]], - (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) => [[0], [1]])) + (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) => [[0], [1]], + ) + ) MOIT.linear2test(bridged_mock, config) - MOIU.set_mock_optimize!(mock, + MOIU.set_mock_optimize!( + mock, (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0, 0]), (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, 0]), - (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, -100])) + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, -100]), + ) MOIT.linear4test(bridged_mock, config) - MOIU.set_mock_optimize!(mock, - (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [4/3, 4/3]), - (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [2, 0]), - (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [4, 0]), - (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [2])) + MOIU.set_mock_optimize!( + mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [4/3, 4/3]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [2, 0]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [4, 0]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [2]), + ) MOIT.linear5test(mock, config) - MOIU.set_mock_optimize!(mock, + MOIU.set_mock_optimize!( + mock, (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0, 0]), (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, 0]), - (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, -100])) + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [100, -100]), + ) MOIT.linear6test(mock, config) - MOIU.set_mock_optimize!(mock, - (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0, 1/2, 1], - (MOI.VectorAffineFunction{Float64}, MOI.Nonpositives) => [[-1], [-2]], - (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) => [[2], [0], [0]]), - (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1], - (MOI.VectorAffineFunction{Float64}, MOI.Nonpositives) => [[-1]], - (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) => [[0]])) + MOIU.set_mock_optimize!( + mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!( + mock, + [0, 1/2, 1], + (MOI.VectorAffineFunction{Float64}, MOI.Nonpositives) => [[-1], [-2]], + (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) => [[2], [0], [0]]), + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!( + mock, + [1], + (MOI.VectorAffineFunction{Float64}, MOI.Nonpositives) => [[-1]], + (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) => [[0]], + ) + ) # linear14 has double variable bounds for the z variable mock.eval_variable_constraint_dual = false MOIT.linear14test(bridged_mock, config) mock.eval_variable_constraint_dual = true - mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, ones(3), - (MOI.VectorAffineFunction{Float64}, MOI.Zeros) => [[2]]) + mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!( + mock, ones(3), (MOI.VectorAffineFunction{Float64}, MOI.Zeros) => [[2]] + ) MOIT.psdt0vtest(bridged_mock, config) - ci = first(MOI.get(bridged_mock, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}}())) + ci = first( + MOI.get( + bridged_mock, + MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64} + }() + ) + ) @testset "$attr" for attr in [MOI.ConstraintPrimalStart(), MOI.ConstraintDualStart()] @test MOI.supports(bridged_mock, attr, typeof(ci)) @@ -76,7 +108,10 @@ config = MOIT.TestConfig() @test MOI.get(bridged_mock, attr, ci) == 2.0 end - test_delete_bridge(bridged_mock, ci, 3, - ((MOI.VectorAffineFunction{Float64}, - MOI.Zeros, 0),)) + test_delete_bridge( + bridged_mock, + ci, + 3, + ((MOI.VectorAffineFunction{Float64}, MOI.Zeros, 0),) + ) end diff --git a/test/functions.jl b/test/functions.jl index 03c404b555..5f11fd12ad 100644 --- a/test/functions.jl +++ b/test/functions.jl @@ -24,3 +24,58 @@ const MOI = MathOptInterface end end end + +@testset "Base.convert" begin + @testset "SingleVariable" begin + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + f = MOI.SingleVariable(x) + f_vov = convert(MOI.VectorOfVariables, f) + @test f_vov ≈ MOI.VectorOfVariables([x]) + f_vaf = convert(MOI.VectorAffineFunction{Float64}, f) + @test f_vaf ≈ MOI.VectorAffineFunction( + [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x))], [0.0] + ) + f_vqf = convert(MOI.VectorQuadraticFunction{Float64}, f) + @test f_vqf ≈ MOI.VectorQuadraticFunction( + [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x))], + MOI.VectorQuadraticTerm{Float64}[], + [0.0], + ) + end + @testset "ScalarAffineFunction" begin + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(2.0, x)], 1.0) + @test_throws(MethodError, convert(MOI.VectorOfVariables, f)) + f_vaf = convert(MOI.VectorAffineFunction{Float64}, f) + @test f_vaf ≈ MOI.VectorAffineFunction( + [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(2.0, x))], [1.0] + ) + f_vqf = convert(MOI.VectorQuadraticFunction{Float64}, f) + @test f_vqf ≈ MOI.VectorQuadraticFunction( + [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(2.0, x))], + MOI.VectorQuadraticTerm{Float64}[], + [1.0], + ) + end + @testset "ScalarQuadraticFunction" begin + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + f = MOI.ScalarQuadraticFunction( + [MOI.ScalarAffineTerm(2.0, x)], + [MOI.ScalarQuadraticTerm(3.0, x, x)], + 1.0, + ) + @test_throws(MethodError, convert(MOI.VectorOfVariables, f)) + @test_throws(MethodError, convert(MOI.VectorAffineFunction{Float64}, f)) + f_vqf = convert(MOI.VectorQuadraticFunction{Float64}, f) + @test f_vqf ≈ MOI.VectorQuadraticFunction( + [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(2.0, x))], + MOI.VectorQuadraticTerm{Float64}[ + MOI.VectorQuadraticTerm(1, MOI.ScalarQuadraticTerm(3.0, x, x)) + ], + [1.0], + ) + end +end