Skip to content

Commit 14981b1

Browse files
committed
Add tutorial on writing SOS solver
1 parent 5d50100 commit 14981b1

File tree

9 files changed

+291
-22
lines changed

9 files changed

+291
-22
lines changed

docs/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ GroupsCore = "d5909c97-4eac-4ecc-a3dc-fdd0858a4120"
1010
HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327"
1111
Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9"
1212
Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306"
13+
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
1314
MultivariateBases = "be282fd4-ad43-11e9-1d11-8bd9d7e43378"
1415
MultivariatePolynomials = "102ac46a-7ee4-5c85-9060-abc95bfdeaa3"
1516
MutableArithmetics = "d8a4904e-b15c-11e9-3269-09a3773c0cb0"

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const _TUTORIAL_SUBDIR = [
1313
"Noncommutative and Hermitian",
1414
"Sparsity",
1515
"Symmetry",
16+
"Extension",
1617
]
1718

1819
function literate_directory(dir)

docs/src/constraints.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ SumOfSquares.LagrangianMultipliers
324324
lagrangian_multipliers
325325
SOSDecomposition
326326
SOSDecompositionWithDomain
327+
SOSDecompositionAttribute
327328
sos_decomposition
328329
```
329330

docs/src/tutorials/Extension/tmp.jl

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
function decompose(p::MP.AbstractPolynomial, tol)
2+
vars = MP.effective_variables(p)
3+
if length(vars) != 1
4+
error("$p is not univariate")
5+
end
6+
x = first(vars)
7+
lead = MP.leadingcoefficient(p)
8+
if !isone(lead)
9+
p = p / lead
10+
end
11+
deg = MP.maxdegree(p)
12+
if isodd(deg)
13+
return
14+
end
15+
d = div(deg, 2)
16+
companion = zeros(2d, 2d)
17+
for i in 0:(2d-1)
18+
if i > 0
19+
companion[i + 1, i] = 1.0
20+
end
21+
companion[i + 1, end] = -MP.coefficient(p, x^i)
22+
end
23+
#display(companion)
24+
F = LinearAlgebra.schur(complex(companion))
25+
#display(F.Z * F.T * F.Z')
26+
σ = vcat(1:2:(2d - 1), 2:2:2d)
27+
S = LinearAlgebra.ordschur(F, isodd.(1:2d))
28+
#U = S.Z[σ, σ]
29+
#S.Z * S.T * S.Z'
30+
#display(σ)
31+
#display(S.T)
32+
#S.T[σ, σ]
33+
vt = S.Z[1, 1:d]
34+
I = 1:d
35+
J = d .+ I
36+
U11 = S.Z[I, I]
37+
U22 = S.Z[J, J]
38+
U12 = S.Z[I, J]
39+
display(S.Z)
40+
return U12
41+
U21 = S.Z[J, I]
42+
vt = U22[:, 1]
43+
U = [U11 U12; U21 U22]
44+
display(S.T)
45+
display(U * S.T * U')
46+
display(S.Z * S.T * S.Z')
47+
display(U * U')
48+
@show lead
49+
q = lead * U12' \ vt
50+
q = vec(vt' * inv(U12))
51+
@show q
52+
q1 = lead * x^d + sum([-real(q[i + 1]) * x^i for i in 0:(d-1)])
53+
q2 = sum([-imag(q[i + 1]) * x^i for i in 0:(d-1)])
54+
q_1 = Polynomial([-real.(q); 1.0])
55+
q_2 = Polynomial([-imag.(q); 0.0])
56+
@show q1^2 + q2^2
57+
@show q_1^2 + q_2^2
58+
@show Polynomials.roots(q_1 + im * q_2)
59+
@show Polynomials.roots(q_1 - im * q_2)
60+
return q1, q2
61+
end
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# # Univariate Solver
2+
3+
#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/generated/Extension/univariate_solver.ipynb)
4+
#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/generated/Extension/univariate_solver.ipynb)
5+
# **Contributed by**: Benoît Legat
6+
7+
# When using an SDP solver, the Sum-of-Squares constraint is bridged
8+
# into a semidefinite constraint. This reformulation is done only if the
9+
# solver does not support the Sum-of-Squares constraint. We show in this tutorial how to define a solver that supports such constraint.
10+
# The *same* model with be then solved with or without reformulation depending on the solver.
11+
12+
# We consider a solver that would support finding the SOS decomposition
13+
# of nonnegative univariate polynomials.
14+
15+
using Test #src
16+
17+
module MyUnivariateSolver
18+
19+
import LinearAlgebra
20+
import MathOptInterface
21+
const MOI = MathOptInterface
22+
import MultivariatePolynomials
23+
const MP = MultivariatePolynomials
24+
import SumOfSquares
25+
const SOS = SumOfSquares
26+
27+
function decompose(p::MP.AbstractPolynomial, tol=1e-6)
28+
vars = MP.effective_variables(p)
29+
if length(vars) != 1
30+
error("$p is not univariate")
31+
end
32+
x = first(vars)
33+
lead = MP.leadingcoefficient(p)
34+
if !isone(lead)
35+
p = p / lead
36+
end
37+
deg = MP.maxdegree(p)
38+
if isodd(deg)
39+
return
40+
end
41+
d = div(deg, 2)
42+
companion = zeros(2d, 2d)
43+
for i in 0:(2d-1)
44+
if i > 0
45+
companion[i + 1, i] = 1.0
46+
end
47+
companion[i + 1, end] = -MP.coefficient(p, x^i)
48+
end
49+
F = LinearAlgebra.schur(complex(companion))
50+
q = one(p)
51+
i = 1
52+
while i <= length(F.values)
53+
root = F.values[i]
54+
q *= (x - root)
55+
if !isapprox(real(root), real(F.values[i+1]), rtol=tol, atol=tol)
56+
# Cannot happen for complex conjugate root so it means that
57+
# we have a root which does not have an even multiplicity
58+
# This means that the polynomial is not nonnegative
59+
return
60+
end
61+
i += 2
62+
end
63+
q1 = MP.mapcoefficientsnz(real, q)
64+
q2 = MP.mapcoefficientsnz(imag, q)
65+
return SOS.SOSDecomposition([q1, q2])
66+
end
67+
68+
mutable struct Optimizer <: MOI.AbstractOptimizer
69+
p::Union{Nothing,MP.AbstractPolynomial}
70+
decomposition::Union{Nothing,SOS.SOSDecomposition}
71+
tol::Float64
72+
function Optimizer()
73+
return new(nothing, nothing, 1e-6)
74+
end
75+
end
76+
77+
MOI.is_empty(optimizer::Optimizer) = optimizer.p === nothing
78+
function MOI.empty!(optimizer::Optimizer)
79+
optimizer.p = nothing
80+
return
81+
end
82+
83+
function MOI.supports_constraint(::Optimizer, ::Type{<:MOI.VectorAffineFunction}, ::Type{<:SOS.SOSPolynomialSet{SOS.FullSpace}})
84+
return true
85+
end
86+
function MOI.add_constraint(optimizer::Optimizer, func::MOI.VectorAffineFunction, set::SOS.SOSPolynomialSet{SOS.FullSpace})
87+
if optimizer.p !== nothing
88+
error("Only one constraint is supported")
89+
end
90+
if !isempty(func.terms)
91+
error("Only supports constant polynomials")
92+
end
93+
optimizer.p = MP.polynomial(func.constants, set.monomials)
94+
# There will be only ever one constraint so the index does not matter.
95+
return MOI.ConstraintIndex{typeof(func),typeof(set)}(1)
96+
end
97+
98+
function MOI.optimize!(optimizer::Optimizer)
99+
optimizer.decomposition = decompose(optimizer.p, optimizer.tol)
100+
end
101+
102+
function MOI.get(optimizer::Optimizer, ::MOI.TerminationStatus)
103+
if optimizer.decomposition === nothing
104+
return MOI.INFEASIBLE
105+
else
106+
return MOI.OPTIMAL
107+
end
108+
end
109+
110+
function MOI.get(optimizer::Optimizer, ::MOI.PrimalStatus)
111+
if optimizer.decomposition === nothing
112+
return MOI.NO_SOLUTION
113+
else
114+
return MOI.FEASIBLE_POINT
115+
end
116+
end
117+
118+
function MOI.get(optimizer::Optimizer, ::SOS.SOSDecompositionAttribute, ::MOI.ConstraintIndex)
119+
return optimizer.decomposition
120+
end
121+
122+
end
123+
124+
# We define the same function both for our new solver and SDP solvers.
125+
126+
using SumOfSquares
127+
function decompose(p, solver)
128+
model = Model(solver)
129+
con = @constraint(model, p in SOSCone())
130+
optimize!(model)
131+
@assert primal_status(model) == MOI.FEASIBLE_POINT
132+
return sos_decomposition(con)
133+
end
134+
135+
# We consider the following univariate polynomial from
136+
# Example 3.35 of [BPT12].
137+
#
138+
# [BPT12] Blekherman, G.; Parrilo, P. A. & Thomas, R. R.
139+
# *Semidefinite Optimization and Convex Algebraic Geometry*.
140+
# Society for Industrial and Applied Mathematics, **2012**.
141+
142+
# Using our solver we find the following decomposition in two squares.
143+
144+
using DynamicPolynomials
145+
@polyvar x
146+
p = x^4 + 4x^3 + 6x^2 + 4x + 5
147+
dec = decompose(p, MyUnivariateSolver.Optimizer)
148+
149+
# We can verify as follows that it gives a correct decomposition:
150+
151+
@test polynomial(dec) p #src
152+
polynomial(dec)
153+
154+
# We can also use a semidefinite solver:
155+
156+
import CSDP
157+
dec = decompose(p, CSDP.Optimizer)
158+
@test length(dec.ps) == 3 #src
159+
@test polynomial(dec) p #src
160+
161+
# The decomposition is different, it is the sum of 3 squares.
162+
# However, it is also valid:
163+
164+
polynomial(dec)

src/SumOfSquares.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Reexport.@reexport using SemialgebraicSets
1717
Reexport.@reexport using MultivariateMoments
1818

1919
include("gram_matrix.jl")
20+
include("sosdec.jl")
2021

2122
using PolyJuMP
2223
abstract type SOSLikeCone <: PolyJuMP.PolynomialSet end
@@ -53,7 +54,6 @@ include("Bridges/Bridges.jl")
5354

5455
Reexport.@reexport using JuMP
5556

56-
include("sosdec.jl")
5757
include("utilities.jl")
5858
include("variable.jl")
5959
include("constraint.jl")

src/attributes.jl

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,45 @@ struct GramMatrixAttribute <: MOI.AbstractConstraintAttribute
1919
end
2020
GramMatrixAttribute() = GramMatrixAttribute(1)
2121

22+
"""
23+
struct SOSDecompositionAttribute
24+
ranktol::Real
25+
dec::MultivariateMoments.LowRankChol
26+
end
27+
28+
A constraint attribute for the [`SOSDecomposition`](@ref) of a constraint.
29+
By default, it is computed using
30+
`SOSDecomposition(gram, ranktol, dec)` where `gram` is the value of the
31+
[`GramMatrixAttribute`](@ref).
32+
"""
33+
struct SOSDecompositionAttribute <: MOI.AbstractConstraintAttribute
34+
ranktol::Real
35+
dec::MultivariateMoments.LowRankChol
36+
result_index::Int
37+
end
38+
function SOSDecompositionAttribute(ranktol::Real, dec::MultivariateMoments.LowRankChol)
39+
return SOSDecompositionAttribute(ranktol, dec, 1)
40+
end
41+
42+
function MOI.get_fallback(
43+
model::MOI.ModelLike,
44+
attr::SOSDecompositionAttribute,
45+
ci::MOI.ConstraintIndex,
46+
)
47+
gram = MOI.get(model, attr.result_index, ci)
48+
return SOSDecomposition(gram, attr.ranktol, attr.dec)
49+
end
50+
# TODO bridges should redirect to `MOI.get_fallback` as well so that
51+
# we can just use `Union{MOI.ConstraintIndex,MOI.Bridges.AbstractBridge}` above:
52+
function MOI.get(
53+
model::MOI.ModelLike,
54+
attr::SOSDecompositionAttribute,
55+
bridge::Union{Bridges.Constraint.SOSPolynomialBridge,Bridges.Constraint.SOSPolynomialInSemialgebraicSetBridge},
56+
)
57+
gram = MOI.get(model, GramMatrixAttribute(attr.result_index), bridge)
58+
return SOSDecomposition(gram, attr.ranktol, attr.dec)
59+
end
60+
2261
"""
2362
MomentMatrixAttribute(N)
2463
MomentMatrixAttribute()
@@ -49,6 +88,7 @@ LagrangianMultipliers() = LagrangianMultipliers(1)
4988
# optimize, even of `CertificateBasis` which is set befor optimize.
5089
function MOI.is_set_by_optimize(::Union{CertificateBasis,
5190
GramMatrixAttribute,
91+
SOSDecompositionAttribute,
5292
MomentMatrixAttribute,
5393
LagrangianMultipliers})
5494
return true
@@ -64,7 +104,7 @@ function MOI.Bridges.Constraint.invariant_under_function_conversion(::Union{
64104
end
65105

66106
# This is type piracy but we tolerate it.
67-
const ObjectWithoutIndex = AbstractGramMatrix{<:MOI.Utilities.ObjectWithoutIndex}
107+
const ObjectWithoutIndex = Union{AbstractGramMatrix{<:MOI.Utilities.ObjectWithoutIndex},SOSDecomposition{<:MOI.Utilities.ObjectWithoutIndex}}
68108
const ObjectOrTupleWithoutIndex = Union{ObjectWithoutIndex, Tuple{Vararg{ObjectWithoutIndex}}}
69109
const ObjectOrTupleOrArrayWithoutIndex = Union{ObjectOrTupleWithoutIndex, AbstractArray{<:ObjectOrTupleWithoutIndex}}
70110
MOI.Utilities.map_indices(::Function, x::ObjectOrTupleOrArrayWithoutIndex) = x

src/constraint.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,27 @@ function gram_matrix(cref::JuMP.ConstraintRef)
226226
return MOI.get(cref.model, GramMatrixAttribute(), cref)
227227
end
228228

229+
"""
230+
sos_decomposition(cref::JuMP.ConstraintRef)
231+
232+
Return the [`SOSDecompositionAttribute`](@ref) of `cref`.
233+
"""
234+
function sos_decomposition(cref::JuMP.ConstraintRef, ranktol::Real=0.0,
235+
dec::MultivariateMoments.LowRankChol=SVDChol())
236+
return MOI.get(cref.model, SOSDecompositionAttribute(ranktol, dec), cref)
237+
end
238+
239+
"""
240+
sos_decomposition(cref::JuMP.ConstraintRef, K<:AbstractBasicSemialgebraicSet)
241+
242+
Return representation in the quadraic module associated with K.
243+
"""
244+
function sos_decomposition(cref::JuMP.ConstraintRef, K::AbstractBasicSemialgebraicSet, args...)
245+
lm = SOSDecomposition.(lagrangian_multipliers(cref), args...)
246+
gm = sos_decomposition(cref, args...)
247+
return SOSDecompositionWithDomain(gm, lm, K)
248+
end
249+
229250
"""
230251
moment_matrix(cref::JuMP.ConstraintRef)
231252

src/sosdec.jl

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,6 @@ function MP.polynomial(decomp::SOSDecomposition, T::Type)
9393
return MP.polynomial(MP.polynomial(decomp), T)
9494
end
9595

96-
"""
97-
function sos_decomposition(cref::JuMP.ConstraintRef, ranktol::Float64, dec::MultivariateMoments.lowrankchol)
98-
99-
Return representation as a sum of squares.
100-
"""
101-
function sos_decomposition(cref::JuMP.ConstraintRef, args...)
102-
return SOSDecomposition(gram_matrix(cref), args...)
103-
end
104-
10596
"""
10697
struct SOSDecompositionWithDomain{T, PT, S}
10798
@@ -145,14 +136,3 @@ end
145136
function Base.isapprox(p::SOSDecompositionWithDomain, q::SOSDecompositionWithDomain; kwargs...)
146137
return isapprox(p.sos, q.sos) && all(isapprox.(p.sosj, q.sosj)) && p.domain == q.domain
147138
end
148-
149-
"""
150-
sos_decomposition(cref::JuMP.ConstraintRef, K<:AbstractBasicSemialgebraicSet)
151-
152-
Return representation in the quadraic module associated with K.
153-
"""
154-
function sos_decomposition(cref::JuMP.ConstraintRef, K::AbstractBasicSemialgebraicSet, args...)
155-
lm = SOSDecomposition.(lagrangian_multipliers(cref), args...)
156-
gm = sos_decomposition(cref, args...)
157-
return SOSDecompositionWithDomain(gm, lm, K)
158-
end

0 commit comments

Comments
 (0)