Skip to content

Commit bbeabc7

Browse files
authored
Refactor Newton postfilter (#287)
* Refactor Newton postfilter * Fix format * Fixes * Fix tests * Fix test * Fix format
1 parent bd2fbcd commit bbeabc7

File tree

10 files changed

+203
-120
lines changed

10 files changed

+203
-120
lines changed

docs/src/tutorials/Getting started/sum-of-squares_matrices.jl

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,15 @@ p = vec(y)' * P * vec(y)
6969
# without exploiting this multipartite structure gives the following 6 monomials.
7070

7171
X = monomials(p)
72-
@test Certificate.monomials_half_newton_polytope(X, tuple(), apply_post_filter = false) == [x * y[1], x * y[2], y[1] * y[2], x, y[1], y[2]] #src
73-
Certificate.monomials_half_newton_polytope(X, tuple(), apply_post_filter = false) #!jl
72+
unipartite = Certificate.NewtonDegreeBounds(tuple())
73+
@test Certificate.monomials_half_newton_polytope(X, unipartite) == [x * y[1], x * y[2], y[1] * y[2], x, y[1], y[2]] #src
74+
Certificate.monomials_half_newton_polytope(X, unipartite) #!jl
7475

7576
# Exploiting the multipartite structure gives 4 monomials.
7677

77-
@test Certificate.monomials_half_newton_polytope(X, ([x], y), apply_post_filter = false) == [x * y[1], x * y[2], y[1], y[2]] #src
78-
Certificate.monomials_half_newton_polytope(X, ([x], y), apply_post_filter = false) #!jl
78+
multipartite = Certificate.NewtonDegreeBounds(([x], y))
79+
@test Certificate.monomials_half_newton_polytope(X, multipartite) == [x * y[1], x * y[2], y[1], y[2]] #src
80+
Certificate.monomials_half_newton_polytope(X, multipartite) #!jl
7981

8082
# In the example above, there were only 3 monomials, where does the difference come from ?
8183
# Using the monomial basis, the only product of two monomials that is equal to
@@ -84,12 +86,12 @@ Certificate.monomials_half_newton_polytope(X, ([x], y), apply_post_filter = fals
8486
# hence the whole column and row will be zero as well.
8587
# Therefore, we can remove this monomial.
8688

87-
@test Certificate.monomials_half_newton_polytope(X, ([x], y)) == [x * y[1], x * y[2], y[1]] #src
88-
Certificate.monomials_half_newton_polytope(X, ([x], y)) #!jl
89+
@test Certificate.monomials_half_newton_polytope(X, Certificate.NewtonFilter(multipartite)) == [x * y[1], x * y[2], y[1]] #src
90+
Certificate.monomials_half_newton_polytope(X, Certificate.NewtonFilter(multipartite)) #!jl
8991

9092
# The same reasoning can be used for monomials `y[1]y[2]` and `x` therefore whether
9193
# we exploit the multipartite structure or not, we get only 3 monomials thanks
9294
# to this post filter.
9395

94-
@test Certificate.monomials_half_newton_polytope(X, tuple()) == [x * y[1], x * y[2], y[1]] #src
95-
Certificate.monomials_half_newton_polytope(X, tuple()) #!jl
96+
@test Certificate.monomials_half_newton_polytope(X, Certificate.NewtonFilter(unipartite)) == [x * y[1], x * y[2], y[1]] #src
97+
Certificate.monomials_half_newton_polytope(X, Certificate.NewtonFilter(unipartite)) #!jl

src/Certificate/ideal.jl

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,25 @@ with all variables in the same part.
9696
struct Newton{
9797
CT<:SumOfSquares.SOSLikeCone,
9898
BT<:MB.AbstractPolynomialBasis,
99-
NPT<:Tuple,
99+
N<:AbstractNewtonPolytopeApproximation,
100100
} <: SimpleIdealCertificate{CT,BT}
101101
cone::CT
102102
basis::Type{BT}
103-
variable_groups::NPT
103+
newton::N
104104
end
105+
106+
function Newton(cone, basis, variable_groups::Tuple)
107+
return Newton(
108+
cone,
109+
basis,
110+
NewtonFilter(NewtonDegreeBounds(variable_groups)),
111+
)
112+
end
113+
105114
function gram_basis(certificate::Newton{CT,B}, poly) where {CT,B}
106115
return MB.basis_covering_monomials(
107116
B,
108-
monomials_half_newton_polytope(
109-
MP.monomials(poly),
110-
certificate.variable_groups,
111-
),
117+
monomials_half_newton_polytope(MP.monomials(poly), certificate.newton),
112118
)
113119
end
114120
function gram_basis_type(::Type{<:Newton{CT,BT}}) where {CT,BT}

src/Certificate/newton_polytope.jl

Lines changed: 108 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,26 @@ function is_commutative(vars)
7373
return length(vars) < 2 || prod(vars[1:2]) == prod(reverse(vars[1:2]))
7474
end
7575

76+
abstract type AbstractNewtonPolytopeApproximation end
77+
78+
# `maxdegree` and `mindegree` on each variable separately
79+
# and on each group of variable.
80+
struct NewtonDegreeBounds{NPT} <: AbstractNewtonPolytopeApproximation
81+
variable_groups::NPT
82+
end
83+
7684
# Multipartite
7785
# TODO we might do this recursively : do 2 parts, merge them, merge with next
7886
# one and so on so that the filter at the end prunes more.
79-
function half_newton_polytope(
80-
X::AbstractVector,
81-
parts::Tuple;
82-
apply_post_filter = true,
83-
)
87+
function half_newton_polytope(X::AbstractVector, newton::NewtonDegreeBounds)
8488
if !is_commutative(MP.variables(X))
8589
throw(
8690
ArgumentError(
8791
"Multipartite Newton polytope not supported with noncommutative variables.",
8892
),
8993
)
9094
end
95+
parts = newton.variable_groups
9196
if !all(
9297
i -> all(j -> i == j || isempty(parts[i] parts[j]), 1:length(parts)),
9398
1:length(parts),
@@ -108,25 +113,23 @@ function half_newton_polytope(
108113
end
109114
if length(all_parts) == 1
110115
# all variables on same part, fallback to shortcut
111-
return half_newton_polytope(
112-
X,
113-
tuple();
114-
apply_post_filter = apply_post_filter,
115-
)
116+
return half_newton_polytope(X, NewtonDegreeBounds(tuple()))
116117
end
117118
monovecs = map(vars -> sub_half_newton_polytope(X, vars), all_parts)
118119
# Cartesian product of the newton polytopes of the different parts
119120
product = [prod(monos) for monos in Iterators.product(monovecs...)]
120121
mindeg, maxdeg = cfld(MP.extdegree(X), 2)
121122
# We know that the degree inequalities are satisfied variable-wise and
122123
# part-wise but for all variables together so we filter with that
123-
monos =
124-
MP.monovec(filter(mono -> mindeg <= MP.degree(mono) <= maxdeg, product))
125-
if apply_post_filter
126-
return post_filter(monos, X)
127-
else
128-
return monos
129-
end
124+
gram_monos = filter(mono -> mindeg <= MP.degree(mono) <= maxdeg, product)
125+
return MP.monovec(gram_monos)
126+
end
127+
128+
# Filters out points ouside the Newton polytope from the
129+
# outer approximation given by `outer_approximation`.
130+
struct NewtonFilter{N<:AbstractNewtonPolytopeApproximation} <:
131+
AbstractNewtonPolytopeApproximation
132+
outer_approximation::N
130133
end
131134

132135
function __chip(cur, i, vars, exps, n, op)
@@ -162,16 +165,12 @@ end
162165

163166
# Shortcut for more efficient `extdeg` and `exp` function in case all the
164167
# variables are in the same part
165-
function half_newton_polytope(
166-
X::AbstractVector,
167-
::Tuple{};
168-
apply_post_filter = true,
169-
)
168+
function half_newton_polytope(X::AbstractVector, ::NewtonDegreeBounds{Tuple{}})
170169
vars = MP.variables(X)
171170
if is_commutative(vars)
172171
# Commutative variables
173172
exp(mono, i) = MP.exponents(mono)[i]
174-
monos = _sub_half_newton_polytope(X, MP.extdegree(X), exp, vars)
173+
return _sub_half_newton_polytope(X, MP.extdegree(X), exp, vars)
175174
else
176175
# Non-commutative variables
177176
# We use Newton chip method of [Section 2.3, BKP16].
@@ -201,15 +200,15 @@ function half_newton_polytope(
201200
end
202201
end
203202
end
204-
monos = MP.monovec(_monos)
205-
end
206-
if apply_post_filter
207-
return post_filter(monos, X)
208-
else
209-
return monos
203+
return MP.monovec(_monos)
210204
end
211205
end
212206

207+
function half_newton_polytope(monos::AbstractVector, newton::NewtonFilter)
208+
gram_monos = half_newton_polytope(monos, newton.outer_approximation)
209+
return post_filter(gram_monos, monos)
210+
end
211+
213212
# If `mono` is such that there is no other way to have `mono^2` by multiplying
214213
# two different monomials of `monos` and `mono` is not in `X` then, the corresponding
215214
# diagonal entry of the Gram matrix will be zero hence the whole column and row
@@ -271,15 +270,10 @@ function post_filter(monos, X)
271270
end
272271

273272
function monomials_half_newton_polytope(
274-
X::AbstractVector,
275-
parts;
276-
apply_post_filter = true,
273+
monos::AbstractVector,
274+
newton::AbstractNewtonPolytopeApproximation,
277275
)
278-
return half_newton_polytope(
279-
MP.monovec(X),
280-
parts;
281-
apply_post_filter = apply_post_filter,
282-
)
276+
return half_newton_polytope(MP.monovec(monos), newton)
283277
end
284278

285279
struct DegreeBounds{M}
@@ -289,6 +283,27 @@ struct DegreeBounds{M}
289283
variablewise_maxdegree::M
290284
end
291285

286+
# TODO add to MP
287+
function _map_powers(f, mono)
288+
exps = map(f, MP.powers(mono))
289+
return _monomial(MP.variables(mono), exps)
290+
end
291+
function _map_exponents(f, mono)
292+
exps = map(f, MP.exponents(mono))
293+
return _monomial(MP.variables(mono), exps)
294+
end
295+
296+
_min_half(d::Integer) = cld(d, 2)
297+
_max_half(d::Integer) = fld(d, 2)
298+
function _half(d::DegreeBounds)
299+
return DegreeBounds(
300+
_min_half(d.mindegree),
301+
_max_half(d.maxdegree),
302+
_map_exponents(_min_half, d.variablewise_mindegree),
303+
_map_exponents(_max_half, d.variablewise_maxdegree),
304+
)
305+
end
306+
292307
function min_degree(p, v)
293308
return mapreduce(Base.Fix2(MP.degree, v), min, MP.monomials(p))
294309
end
@@ -306,35 +321,30 @@ function max_degree(p, v)
306321
end
307322

308323
function min_shift(d, shift)
309-
return max(0, cld(d - shift, 2))
310-
end
311-
function max_shift(d, shift)
312-
return fld(d - shift, 2)
324+
return max(0, d - shift)
313325
end
314326

315327
function minus_shift(
316328
deg,
317-
m::MP.AbstractMonomial,
329+
mono::MP.AbstractMonomial,
318330
p::MP.AbstractPolynomialLike,
319331
shift,
320332
)
321-
exps = map(MP.powers(m)) do (v, d)
333+
return _map_powers(mono) do (v, d)
322334
return shift(d, deg(p, v))
323335
end
324-
return _monomial(MP.variables(m), exps)
325336
end
326337

327338
function minus_shift(d::DegreeBounds, p::MP.AbstractPolynomialLike)
328339
var_mindegree =
329340
minus_shift(min_degree, d.variablewise_mindegree, p, min_shift)
330-
var_maxdegree =
331-
minus_shift(max_degree, d.variablewise_maxdegree, p, max_shift)
341+
var_maxdegree = minus_shift(max_degree, d.variablewise_maxdegree, p, -)
332342
if isnothing(var_maxdegree)
333343
return
334344
end
335345
return DegreeBounds(
336346
min_shift(d.mindegree, MP.mindegree(p)),
337-
max_shift(d.maxdegree, MP.maxdegree(p)),
347+
d.maxdegree - MP.maxdegree(p),
338348
var_mindegree,
339349
var_maxdegree,
340350
)
@@ -436,8 +446,8 @@ function putinar_degree_bounds(
436446
)
437447
mindegree = 0
438448
# TODO homogeneous case
439-
mindeg(g) = min_shift(mindegree, MP.mindegree(g))
440-
maxdeg(g) = max_shift(maxdegree, MP.maxdegree(g))
449+
mindeg(g) = _min_half(min_shift(mindegree, MP.mindegree(g)))
450+
maxdeg(g) = _max_half(maxdegree - MP.maxdegree(g))
441451
degrange(g) = mindeg(g):maxdeg(g)
442452
minus_degrange(g) = -maxdeg(g):-mindeg(g)
443453
# The multiplier will have degree `0:2fld(maxdegree - MP.maxdegree(g), 2)`
@@ -477,3 +487,52 @@ function putinar_degree_bounds(
477487
_monomial(vars, vars_maxdeg),
478488
)
479489
end
490+
491+
function multiplier_basis(g::MP.AbstractPolynomialLike, bounds::DegreeBounds)
492+
shifted = minus_shift(bounds, g)
493+
if isnothing(shifted)
494+
halved = nothing
495+
else
496+
halved = _half(shifted)
497+
end
498+
basis = MB.MonomialBasis
499+
if isnothing(halved)
500+
# TODO add `MB.empty_basis` to API
501+
return MB.maxdegree_basis(
502+
basis,
503+
MP.variables(bounds.variablewise_mindegree),
504+
-1,
505+
)
506+
else
507+
return maxdegree_gram_basis(basis, halved)
508+
end
509+
end
510+
511+
function half_newton_polytope(
512+
p::MP.AbstractPolynomialLike,
513+
gs::AbstractVector{<:MP.AbstractPolynomialLike},
514+
vars,
515+
maxdegree,
516+
::NewtonDegreeBounds,
517+
)
518+
# TODO take `variable_groups` into account
519+
bounds = putinar_degree_bounds(p, gs, vars, maxdegree)
520+
return [multiplier_basis(g, bounds) for g in gs]
521+
end
522+
523+
function half_newton_polytope(
524+
p::MP.AbstractPolynomialLike,
525+
gs::AbstractVector{<:MP.AbstractPolynomialLike},
526+
vars,
527+
maxdegree,
528+
newton::NewtonFilter{<:NewtonDegreeBounds},
529+
)
530+
# TODO
531+
return half_newton_polytope(
532+
p,
533+
gs,
534+
vars,
535+
maxdegree,
536+
newton.outer_approximation,
537+
)
538+
end

0 commit comments

Comments
 (0)