diff --git a/Project.toml b/Project.toml index 83754b12e..613024753 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ CliqueTrees = "60701a23-6482-424a-84db-faee86b9b1f8" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +LowRankOpt = "607ca3ad-272e-43c8-bcbe-fc71b56c935c" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" MultivariateBases = "be282fd4-ad43-11e9-1d11-8bd9d7e43378" MultivariateMoments = "f4abf1af-0426-5881-a0da-e2f168889b5e" @@ -24,6 +25,7 @@ SymbolicWedderburn = "858aa9a9-4c7c-4c62-b466-2421203962a2" CliqueTrees = "1" DataStructures = "0.19" JuMP = "1.10" +LowRankOpt = "0.2" MathOptInterface = "1.13" MultivariateBases = "0.3" MultivariateMoments = "0.5" diff --git a/src/Bridges/Variable/Variable.jl b/src/Bridges/Variable/Variable.jl index 4a154fd2d..223c3ed26 100644 --- a/src/Bridges/Variable/Variable.jl +++ b/src/Bridges/Variable/Variable.jl @@ -6,11 +6,13 @@ import StarAlgebras as SA import MultivariateBases as MB import MathOptInterface as MOI import MultivariatePolynomials as MP +import LowRankOpt as LRO import SumOfSquares as SOS include("psd2x2.jl") include("scaled_diagonally_dominant.jl") include("copositive_inner.jl") include("kernel.jl") +include("lowrank.jl") end diff --git a/src/Bridges/Variable/kernel.jl b/src/Bridges/Variable/kernel.jl index def2a0022..58c6aa93d 100644 --- a/src/Bridges/Variable/kernel.jl +++ b/src/Bridges/Variable/kernel.jl @@ -37,9 +37,11 @@ end function MOI.Bridges.Variable.supports_constrained_variable( ::Type{<:KernelBridge}, - ::Type{<:SOS.WeightedSOSCone}, -) - return true + ::Type{<:SOS.WeightedSOSCone{M,B}}, +) where {M,B} + # Could be made to work but doesn't work yet so it's best to use + # `LowRankBridge` which can then be bridged to classical PSD by MOI's bridge + return !(B <: MB.LagrangeBasis) end function MOI.Bridges.added_constrained_variable_types( diff --git a/src/Bridges/Variable/lowrank.jl b/src/Bridges/Variable/lowrank.jl new file mode 100644 index 000000000..3fc4a9b0e --- /dev/null +++ b/src/Bridges/Variable/lowrank.jl @@ -0,0 +1,173 @@ +struct LowRankBridge{T,M} <: MOI.Bridges.Variable.AbstractBridge + affine::Vector{MOI.ScalarAffineFunction{T}} + variables::Vector{Vector{MOI.VariableIndex}} + constraints::Vector{MOI.ConstraintIndex{MOI.VectorOfVariables}} + set::SOS.WeightedSOSCone{M} +end + +import LinearAlgebra + +function MOI.Bridges.Variable.bridge_constrained_variable( + ::Type{LowRankBridge{T,M}}, + model::MOI.ModelLike, + set::SOS.WeightedSOSCone{M}, +) where {T,M} + variables = Vector{Vector{MOI.VariableIndex}}(undef, length(set.gram_bases)) + constraints = Vector{MOI.ConstraintIndex{MOI.VectorOfVariables}}( + undef, + length(set.gram_bases), + ) + for i in eachindex(set.gram_bases) + U = MB.transformation_to(set.gram_bases[i], set.basis) + weights = SA.coeffs(set.weights[i], set.basis) + variables[i], constraints[i] = MOI.add_constrained_variables( + model, + LRO.SetDotProducts{LRO.WITHOUT_SET}( + SOS.matrix_cone(M, length(set.gram_bases[i])), + [ + LRO.TriangleVectorization( + LRO.Factorization( + Matrix{T}(reshape(U[j, :], size(U, 2), 1)), + T[weights[j]], + ), + ) for j in eachindex(set.basis) + ], + ), + ) + end + return LowRankBridge{T,M}( + [ + MOI.ScalarAffineFunction( + [ + MOI.ScalarAffineTerm(one(T), variables[i][j]) for + i in eachindex(set.gram_bases) + ], + zero(T), + ) for j in eachindex(set.basis) + ], + variables, + constraints, + set, + ) +end + +function MOI.Bridges.Variable.supports_constrained_variable( + ::Type{<:LowRankBridge}, + ::Type{<:SOS.WeightedSOSCone{M,B}}, +) where {M,B} + # Could be made to work for non-LagrangeBasis but it's not low rank in + # we we'll need a high bridge cost for them so that it's only UnsafeAddMul + # if the other bridges are removed + return B <: MB.LagrangeBasis +end + +function MOI.Bridges.added_constrained_variable_types( + ::Type{LowRankBridge{T,M}}, +) where {T,M} + return Tuple{Type}[ + ( + LRO.SetDotProducts{ + LRO.WITHOUT_SET, + S[1], + LRO.TriangleVectorization{ + T, + LRO.Factorization{T,Matrix{T},Vector{T}}, + }, + }, + ) for S in SOS.Bridges.Constraint.constrained_variable_types(M) if + S[1] == MOI.PositiveSemidefiniteConeTriangle # FIXME hack + ] +end + +function MOI.Bridges.added_constraint_types(::Type{<:LowRankBridge}) + return Tuple{Type,Type}[] +end + +function MOI.Bridges.Variable.concrete_bridge_type( + ::Type{<:LowRankBridge{T}}, + ::Type{<:SOS.WeightedSOSCone{M}}, +) where {T,M} + return LowRankBridge{T,M} +end + +# Attributes, Bridge acting as a model +function MOI.get(bridge::LowRankBridge, ::MOI.NumberOfVariables) + return sum(length, bridge.variables) +end + +function MOI.get(bridge::LowRankBridge, ::MOI.ListOfVariableIndices) + return reduce(vcat, bridge.variables) +end + +function MOI.get( + bridge::LowRankBridge, + ::MOI.NumberOfConstraints{MOI.VectorOfVariables,S}, +) where {S<:MOI.AbstractVectorSet} + return count(bridge.constraints) do ci + return ci isa MOI.ConstraintIndex{MOI.VectorOfVariables,S} + end +end + +function MOI.get( + bridge::LowRankBridge, + ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables,S}, +) where {S} + return [ + ci for ci in bridge.constraints if + ci isa MOI.ConstraintIndex{MOI.VectorOfVariables,S} + ] +end + +# Indices +function MOI.delete(model::MOI.ModelLike, bridge::LowRankBridge) + for vars in bridge.variables + MOI.delete(model, vars) + end + return +end + +# Attributes, Bridge acting as a constraint + +function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet, bridge::LowRankBridge) + return bridge.set +end + +function MOI.get( + model::MOI.ModelLike, + attr::MOI.ConstraintPrimal, + bridge::LowRankBridge, +) + return [ + MOI.get( + model, + MOI.VariablePrimal(attr.result_index), + bridge, + MOI.Bridges.IndexInVector(i), + ) for i in eachindex(bridge.affine) + ] +end + +function MOI.get( + model::MOI.ModelLike, + attr::MOI.VariablePrimal, + bridge::LowRankBridge, + i::MOI.Bridges.IndexInVector, +) + return MOI.Utilities.eval_variables(bridge.affine[i.value]) do vi + return MOI.get(model, MOI.VariablePrimal(attr.result_index), vi) + end +end + +function MOI.Bridges.bridged_function( + bridge::LowRankBridge, + i::MOI.Bridges.IndexInVector, +) + return bridge.affine[i.value] +end + +function MOI.Bridges.Variable.unbridged_map( + ::LowRankBridge, + ::Vector{MOI.VariableIndex}, +) + return nothing +end diff --git a/src/variables.jl b/src/variables.jl index 58499a80b..2a7fee536 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -9,10 +9,11 @@ function _bridge_coefficient_type(::Type{SOSPolynomialSet{D,B,C}}) where {D,B,C} end function PolyJuMP.bridges(S::Type{<:WeightedSOSCone}) - return Tuple{Type,Type}[( - Bridges.Variable.KernelBridge, - _bridge_coefficient_type(S), - )] + T = _bridge_coefficient_type(S) + return Tuple{Type,Type}[ + (Bridges.Variable.KernelBridge, T), + (Bridges.Variable.LowRankBridge, T), + ] end function PolyJuMP.bridges(::Type{<:PositiveSemidefinite2x2ConeTriangle}) diff --git a/test/Bridges/Variable/lowrank.jl b/test/Bridges/Variable/lowrank.jl new file mode 100644 index 000000000..86eb925cd --- /dev/null +++ b/test/Bridges/Variable/lowrank.jl @@ -0,0 +1,223 @@ +module TestVariableLowRank + +using Test +import MultivariateBases as MB +import LowRankOpt as LRO +import Hypatia +using DynamicPolynomials +using SumOfSquares + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +# 1 variable, 3 Lagrange points, gram basis [1, x], weight 1 +# U = transformation_to([1, x], LagrangeBasis at [0, 1, 2]) = [1 0; 1 1; 1 2] +# weights at points = [1, 1, 1] +function test_runtests() + @polyvar x + lag = MB.LagrangeBasis((x,), [[0.0], [1.0], [2.0]]) + MOI.Bridges.runtests( + SumOfSquares.Bridges.Variable.LowRankBridge, + model -> begin + p, _ = MOI.add_constrained_variables( + model, + SumOfSquares.WeightedSOSCone{ + MOI.PositiveSemidefiniteConeTriangle, + }( + lag, + [MB.SubBasis{MB.Monomial}([x^0, x])], + [MB.algebra_element(1.0 * x^0)], + ), + ) + a = float.(1:length(p)) + MOI.add_constraint( + model, + MOI.Utilities.vectorize([a' * p]), + MOI.Zeros(1), + ) + end, + model -> begin + q, _ = MOI.add_constrained_variables( + model, + LRO.SetDotProducts{LRO.WITHOUT_SET}( + SumOfSquares.PositiveSemidefinite2x2ConeTriangle(), + [ + LRO.TriangleVectorization( + LRO.Factorization(reshape([1.0, 0.0], 2, 1), [1.0]), + ), + LRO.TriangleVectorization( + LRO.Factorization(reshape([1.0, 1.0], 2, 1), [1.0]), + ), + LRO.TriangleVectorization( + LRO.Factorization(reshape([1.0, 2.0], 2, 1), [1.0]), + ), + ], + ), + ) + MOI.add_constraint( + model, + MOI.Utilities.vectorize([1.0 * q[1] + 2.0 * q[2] + 3.0 * q[3]]), + MOI.Zeros(1), + ) + end; + cannot_unbridge = true, + ) + return +end + +# Same as above but with weight 2 +function test_runtests_weighted() + @polyvar x + lag = MB.LagrangeBasis((x,), [[0.0], [1.0], [2.0]]) + MOI.Bridges.runtests( + SumOfSquares.Bridges.Variable.LowRankBridge, + model -> begin + p, _ = MOI.add_constrained_variables( + model, + SumOfSquares.WeightedSOSCone{ + MOI.PositiveSemidefiniteConeTriangle, + }( + lag, + [MB.SubBasis{MB.Monomial}([x^0, x])], + [MB.algebra_element(2.0 * x^0)], + ), + ) + a = float.(1:length(p)) + MOI.add_constraint( + model, + MOI.Utilities.vectorize([a' * p]), + MOI.Zeros(1), + ) + end, + model -> begin + q, _ = MOI.add_constrained_variables( + model, + LRO.SetDotProducts{LRO.WITHOUT_SET}( + SumOfSquares.PositiveSemidefinite2x2ConeTriangle(), + [ + LRO.TriangleVectorization( + LRO.Factorization(reshape([1.0, 0.0], 2, 1), [2.0]), + ), + LRO.TriangleVectorization( + LRO.Factorization(reshape([1.0, 1.0], 2, 1), [2.0]), + ), + LRO.TriangleVectorization( + LRO.Factorization(reshape([1.0, 2.0], 2, 1), [2.0]), + ), + ], + ), + ) + MOI.add_constraint( + model, + MOI.Utilities.vectorize([1.0 * q[1] + 2.0 * q[2] + 3.0 * q[3]]), + MOI.Zeros(1), + ) + end; + cannot_unbridge = true, + ) + return +end + +function _bridged_model_and_vars() + @polyvar x + lag = MB.LagrangeBasis((x,), [[0.0], [1.0], [2.0]]) + inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + model = MOI.Bridges.Variable.SingleBridgeOptimizer{ + SumOfSquares.Bridges.Variable.LowRankBridge{Float64}, + }( + inner, + ) + set = SumOfSquares.WeightedSOSCone{MOI.PositiveSemidefiniteConeTriangle}( + lag, + [MB.SubBasis{MB.Monomial}([x^0, x])], + [MB.algebra_element(1.0 * x^0)], + ) + p, ci = MOI.add_constrained_variables(model, set) + return model, inner, p, ci +end + +function test_delete() + model, _, p, _ = _bridged_model_and_vars() + @test MOI.get(model, MOI.NumberOfVariables()) == 3 + MOI.delete(model, p) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + return +end + +function test_primal() + @polyvar x + lag = MB.LagrangeBasis((x,), [[0.0], [1.0], [2.0]]) + mock = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ) + model = MOI.Bridges.Variable.SingleBridgeOptimizer{ + SumOfSquares.Bridges.Variable.LowRankBridge{Float64}, + }( + mock, + ) + set = SumOfSquares.WeightedSOSCone{MOI.PositiveSemidefiniteConeTriangle}( + lag, + [MB.SubBasis{MB.Monomial}([x^0, x])], + [MB.algebra_element(1.0 * x^0)], + ) + p, ci = MOI.add_constrained_variables(model, set) + # Mock an optimization result: all inner variables = 1.0 + inner_vars = MOI.get(mock, MOI.ListOfVariableIndices()) + MOI.Utilities.mock_optimize!( + mock, + MOI.OPTIMAL, + (MOI.FEASIBLE_POINT, ones(length(inner_vars))), + ) + # VariablePrimal through bridge (line 158) + for vi in p + @test MOI.get(model, MOI.VariablePrimal(), vi) == 1.0 + end + # ConstraintPrimal through bridge (line 141) + cp = MOI.get(model, MOI.ConstraintPrimal(), ci) + @test length(cp) == 3 + @test all(cp .== 1.0) + return +end + +# Verify that with Hypatia, LagrangeBasis uses SetDotProducts (not PSD) +function test_hypatia_uses_setdotproducts() + @polyvar x + optimizer = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + Hypatia.Optimizer{Float64}(), + ) + MOI.set(optimizer, MOI.Silent(), true) + model = MOI.Bridges.full_bridge_optimizer(optimizer, Float64) + MOI.Bridges.add_bridge( + model, + SumOfSquares.Bridges.Variable.LowRankBridge{Float64}, + ) + LRO.Bridges.Variable.add_all_bridges(model, Float64) + lag = MB.LagrangeBasis((x,), [[0.0], [1.0], [2.0], [3.0], [4.0]]) + set = SumOfSquares.WeightedSOSCone{MOI.PositiveSemidefiniteConeTriangle}( + lag, + [MB.SubBasis{MB.Monomial}([x^0, x, x^2])], + [MB.algebra_element(1.0 * x^0)], + ) + MOI.add_constrained_variables(model, set) + cache = optimizer.model_cache + constraint_types = MOI.get(cache, MOI.ListOfConstraintTypesPresent()) + @test any(((F, S),) -> S <: LRO.SetDotProducts, constraint_types) + @test !any( + ((F, S),) -> S <: MOI.PositiveSemidefiniteConeTriangle, + constraint_types, + ) + return +end + +end # module + +TestVariableLowRank.runtests() diff --git a/test/Project.toml b/test/Project.toml index c674ebf11..20887718f 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -2,8 +2,10 @@ Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" DynamicPolynomials = "7c1d4256-1411-5781-91ec-d7bc3513ac07" +Hypatia = "b99e6be6-89ff-11e8-14f8-45c827f4f8f2" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +LowRankOpt = "607ca3ad-272e-43c8-bcbe-fc71b56c935c" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" MultivariateBases = "be282fd4-ad43-11e9-1d11-8bd9d7e43378" MultivariateMoments = "f4abf1af-0426-5881-a0da-e2f168889b5e"