PropertyT.jl/src/sos_sdps.jl

363 lines
10 KiB
Julia
Raw Normal View History

"""
sos_problem_dual(X, [u = zero(X); upper_bound=Inf])
Formulate the dual to the sum of squares decomposition problem for `X - λ·u`.
See also [sos_problem_primal](@ref).
"""
function sos_problem_dual(
elt::AlgebraElement,
order_unit::AlgebraElement=zero(elt);
lower_bound=-Inf
)
@assert parent(elt) == parent(order_unit)
algebra = parent(elt)
mstructure = if StarAlgebras._istwisted(algebra.mstructure)
algebra.mstructure
else
StarAlgebras.MTable{true}(basis(algebra), table_size=size(algebra.mstructure))
end
# 1 variable for every primal constraint
# 1 dual variable for every element of basis
# Symmetrized:
# 1 dual variable for every orbit of G acting on basis
model = Model()
@variable model y[1:length(basis(algebra))]
@constraint model λ_dual dot(order_unit, y) == 1
@constraint(model, psd, y[mstructure] in PSDCone())
if !isinf(lower_bound)
throw("Not Implemented yet")
@variable model λ_ub_dual
@objective model Min dot(elt, y) + lower_bound * λ_ub_dual
else
@objective model Min dot(elt, y)
end
return model
end
function constraints(
mstr::AbstractMatrix{<:Integer},
total_length=maximum(mstr),
)
cnstrs = [Vector{eltype(mstr)}() for _ = 1:total_length]
li = LinearIndices(mstr)
for (idx, val) in pairs(mstr)
push!(cnstrs[val], li[idx])
2018-08-20 03:50:03 +02:00
end
return cnstrs
end
function constraints(
basis::StarAlgebras.AbstractBasis,
mstr::AbstractMatrix{<:Integer};
augmented::Bool=false,
table_size=size(mstr)
)
cnstrs = [signed(eltype(mstr))[] for _ in basis]
LI = LinearIndices(table_size)
for ci in CartesianIndices(table_size)
k = LI[ci]
a_star_b = basis[mstr[k]]
push!(cnstrs[basis[a_star_b]], k)
if augmented
# (1-a_star)(1-b) = 1 - a_star - b + a_star_b
i, j = Tuple(ci)
a, b = basis[i], basis[j]
push!(cnstrs[basis[one(a)]], k)
push!(cnstrs[basis[star(a)]], -k)
push!(cnstrs[basis[b]], -k)
2017-03-13 14:49:55 +01:00
end
end
return Dict(
basis[i] => ConstraintMatrix(c, table_size..., 1) for (i, c) in pairs(cnstrs)
)
end
function constraints(A::StarAlgebra; augmented::Bool, twisted::Bool)
mstructure = if StarAlgebras._istwisted(A.mstructure) == twisted
A.mstructure
else
StarAlgebras.MTable{twisted}(basis(A), table_size=size(A.mstructure))
end
return constraints(basis(A), mstructure, augmented=augmented)
end
"""
sos_problem_primal(X, [u = zero(X); upper_bound=Inf])
Formulate sum of squares decomposition problem for `X - λ·u`.
Given element `X` of a star-algebra `A` and an order unit `u` of `Σ²A` the sum
of squares cone in `A`, fomulate sum of squares decomposition problem:
```
max: λ
subject to: X - λ·u Σ²A
```
If `upper_bound` is given a finite value, additionally `λ ≤ upper_bound` will
be added to the model. This may improve the accuracy of the solution if
`upper_bound` is less than the optimal `λ`.
The default `u = zero(X)` formulates a simple feasibility problem.
"""
function sos_problem_primal(
elt::AlgebraElement,
order_unit::AlgebraElement=zero(elt);
upper_bound=Inf,
augmented::Bool=iszero(aug(elt)) && iszero(aug(order_unit))
)
@assert parent(elt) === parent(order_unit)
N = LinearAlgebra.checksquare(parent(elt).mstructure)
model = JuMP.Model()
P = JuMP.@variable(model, P[1:N, 1:N], Symmetric)
JuMP.@constraint(model, psd, P in PSDCone())
if iszero(order_unit) && isfinite(upper_bound)
@warn "Setting `upper_bound` together with zero `order_unit` has no effect"
end
2017-03-13 14:49:55 +01:00
A = constraints(parent(elt), augmented=augmented, twisted=true)
if !iszero(order_unit)
λ = JuMP.@variable(model, λ)
if isfinite(upper_bound)
JuMP.@constraint model λ <= upper_bound
end
JuMP.@objective(model, Max, λ)
for b in basis(parent(elt))
JuMP.@constraint(model, elt(b) - λ * order_unit(b) == dot(A[b], P))
end
else
for b in basis(parent(elt))
JuMP.@constraint(model, elt(b) == dot(A[b], P))
end
end
return model
end
function invariant_constraint!(
result::AbstractMatrix,
basis::StarAlgebras.AbstractBasis,
cnstrs::AbstractDict{K,CM},
invariant_vec::SparseVector,
) where {K,CM<:ConstraintMatrix}
result .= zero(eltype(result))
for i in SparseArrays.nonzeroinds(invariant_vec)
g = basis[i]
A = cnstrs[g]
for (idx, v) in nzpairs(A)
result[idx] += invariant_vec[i] * v
end
end
return result
end
function isorth_projection(ds::SymbolicWedderburn.DirectSummand)
U = SymbolicWedderburn.image_basis(ds)
return isapprox(U * U', I)
end
sos_problem_primal(
elt::AlgebraElement,
wedderburn::WedderburnDecomposition;
kwargs...
) = sos_problem_primal(elt, zero(elt), wedderburn; kwargs...)
function sos_problem_primal(
elt::AlgebraElement,
orderunit::AlgebraElement,
wedderburn::WedderburnDecomposition;
upper_bound=Inf,
augmented=iszero(aug(elt)) && iszero(aug(orderunit))
)
@assert parent(elt) === parent(orderunit)
if any(!isorth_projection, direct_summands(wedderburn))
error("Wedderburn decomposition contains a non-orthogonal projection")
end
feasibility_problem = iszero(orderunit)
model = JuMP.Model()
if !feasibility_problem # add λ or not?
λ = JuMP.@variable(model, λ)
JuMP.@objective(model, Max, λ)
if isfinite(upper_bound)
JuMP.@constraint(model, λ <= upper_bound)
if feasibility_problem
@warn "setting `upper_bound` with zero `orderunit` has no effect"
end
end
end
2017-03-20 21:41:12 +01:00
P = map(direct_summands(wedderburn)) do ds
dim = size(ds, 1)
P = JuMP.@variable(model, [1:dim, 1:dim], Symmetric)
@constraint(model, P in PSDCone())
P
2017-03-13 14:49:55 +01:00
end
2019-04-12 23:18:48 +02:00
begin # preallocating
T = eltype(wedderburn)
M = zeros.(T, size.(P))
M_orb = zeros(T, size(parent(elt).mstructure)...)
tmps = SymbolicWedderburn._tmps(wedderburn)
end
2019-08-02 11:52:19 +02:00
X = convert(Vector{T}, coeffs(elt))
U = convert(Vector{T}, coeffs(orderunit))
2019-04-12 23:18:48 +02:00
# defining constraints based on the multiplicative structure
cnstrs = constraints(parent(elt), augmented=augmented, twisted=true)
2019-04-12 23:18:48 +02:00
@info "Adding $(length(invariant_vectors(wedderburn))) constraints"
2017-03-13 14:49:55 +01:00
for iv in invariant_vectors(wedderburn)
x = dot(X, iv)
u = dot(U, iv)
2018-09-05 09:18:38 +02:00
M_orb = invariant_constraint!(M_orb, basis(parent(elt)), cnstrs, iv)
2018-09-05 09:18:38 +02:00
M = SymbolicWedderburn.diagonalize!(M, M_orb, wedderburn, tmps)
SymbolicWedderburn.zerotol!.(M, atol=1e-12)
2018-09-05 09:18:38 +02:00
M_dot_P = sum(dot(M[π], P[π]) for π in eachindex(M) if !iszero(M[π]))
2018-09-05 09:18:38 +02:00
if feasibility_problem
JuMP.@constraint(model, x == M_dot_P)
else
JuMP.@constraint(model, x - λ * u == M_dot_P)
end
2018-09-05 09:18:38 +02:00
end
return model, P
end
2019-04-12 23:18:48 +02:00
function reconstruct(Ps, wd::WedderburnDecomposition)
N = size(first(direct_summands(wd)), 2)
P = zeros(eltype(wd), N, N)
return reconstruct!(P, Ps, wd)
2018-09-05 09:18:38 +02:00
end
2018-08-20 03:45:50 +02:00
function group_of(wd::WedderburnDecomposition)
# this is veeeery hacky... ;)
return parent(first(keys(wd.hom.cache)))
2018-09-05 09:18:38 +02:00
end
# TODO: move to SymbolicWedderburn
SymbolicWedderburn.action(wd::WedderburnDecomposition) =
SymbolicWedderburn.action(wd.hom)
2018-09-05 09:18:38 +02:00
function reconstruct!(
res::AbstractMatrix,
Ps,
wedderburn::WedderburnDecomposition,
)
G = group_of(wedderburn)
2018-08-20 03:45:50 +02:00
act = SymbolicWedderburn.action(wedderburn)
2018-09-05 09:18:38 +02:00
@assert act isa SymbolicWedderburn.ByPermutations
2019-08-02 11:52:19 +02:00
for (π, ds) in pairs(direct_summands(wedderburn))
= SymbolicWedderburn.image_basis(ds)
2018-09-05 09:18:38 +02:00
# LinearAlgebra.mul!(tmp, Uπ', P[π])
# LinearAlgebra.mul!(tmp2, tmp, Uπ)
tmp2 = ' * Ps[π] *
if eltype(res) <: AbstractFloat
SymbolicWedderburn.zerotol!(tmp2, atol=1e-12)
end
tmp2 .*= degree(ds)
2019-04-12 23:18:48 +02:00
@assert size(tmp2) == size(res)
2019-04-12 23:18:48 +02:00
for g in G
p = SymbolicWedderburn.induce(wedderburn.hom, g)
for c in axes(res, 2)
for r in axes(res, 1)
res[r, c] += tmp2[r^p, c^p]
end
end
end
2018-09-05 09:18:38 +02:00
end
res ./= order(Int, G)
2018-09-05 09:18:38 +02:00
return res
2018-09-05 09:18:38 +02:00
end
##
# Low-level solve
2018-09-05 09:18:38 +02:00
setwarmstart!(model::JuMP.Model, ::Nothing) = model
2019-04-12 23:18:48 +02:00
function setwarmstart!(model::JuMP.Model, warmstart)
constraint_map = Dict(
ct => JuMP.all_constraints(model, ct...) for
ct in JuMP.list_of_constraint_types(model)
)
JuMP.set_start_value.(JuMP.all_variables(model), warmstart.primal)
2018-08-20 03:45:50 +02:00
for (ct, idx) in pairs(constraint_map)
JuMP.set_start_value.(idx, warmstart.slack[ct])
JuMP.set_dual_start_value.(idx, warmstart.dual[ct])
end
return model
end
function getwarmstart(model::JuMP.Model)
constraint_map = Dict(
ct => JuMP.all_constraints(model, ct...) for
ct in JuMP.list_of_constraint_types(model)
)
2017-03-13 14:49:55 +01:00
primal = value.(JuMP.all_variables(model))
slack = Dict(k => value.(v) for (k, v) in constraint_map)
duals = Dict(k => JuMP.dual.(v) for (k, v) in constraint_map)
return (primal=primal, dual=duals, slack=slack)
end
2019-04-12 23:18:48 +02:00
function solve(m::JuMP.Model, optimizer, warmstart=nothing)
2019-04-12 23:18:48 +02:00
JuMP.set_optimizer(m, optimizer)
MOIU.attach_optimizer(m)
2019-04-12 23:18:48 +02:00
m = setwarmstart!(m, warmstart)
2017-03-16 09:35:32 +01:00
JuMP.optimize!(m)
Base.Libc.flush_cstdio()
status = JuMP.termination_status(m)
2017-03-13 14:49:55 +01:00
2020-10-17 02:08:51 +02:00
return status, getwarmstart(m)
2017-03-13 14:49:55 +01:00
end
2018-01-01 23:59:31 +01:00
function solve(solverlog::String, m::JuMP.Model, optimizer, warmstart=nothing)
isdir(dirname(solverlog)) || mkpath(dirname(solverlog))
2019-01-11 06:32:09 +01:00
Base.flush(Base.stdout)
Base.Libc.flush_cstdio()
status, warmstart = open(solverlog, "a+") do logfile
redirect_stdout(logfile) do
status, warmstart = solve(m, optimizer, warmstart)
status, warmstart
2018-01-01 23:59:31 +01:00
end
end
return status, warmstart
2018-01-01 23:59:31 +01:00
end