Skip to content

Commit 3a244a9

Browse files
authored
Add exponent iterator (#312)
* Add exponent iterator * Add docstrings * Improve doctest * Fix doctest setup * Add tests * Fix format * Rename * Fix tests * Fix format * Fix * Fix ambiguity * Fix format * Remove unused * Fix docstring
1 parent 9fc5d7d commit 3a244a9

File tree

6 files changed

+296
-37
lines changed

6 files changed

+296
-37
lines changed

docs/make.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
using Documenter, MultivariatePolynomials
22

3+
DocMeta.setdocmeta!(
4+
MultivariatePolynomials,
5+
:DocTestSetup,
6+
:(using MultivariatePolynomials);
7+
recursive=true,
8+
)
9+
310
makedocs(
411
sitename = "MultivariatePolynomials",
512

docs/src/types.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ LexOrder
5252
InverseLexOrder
5353
Graded
5454
Reverse
55+
ExponentsIterator
5556
```
5657

5758
## Terms

src/comparison.jl

Lines changed: 208 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -200,25 +200,10 @@ Springer Science & Business Media, **2013**.
200200
"""
201201
struct LexOrder <: AbstractMonomialOrdering end
202202

203-
function compare(
204-
exp1::AbstractVector{T},
205-
exp2::AbstractVector{T},
206-
::Type{LexOrder},
207-
) where {T}
208-
if eachindex(exp1) != eachindex(exp2)
209-
throw(
210-
ArgumentError(
211-
"Cannot compare exponent vectors `$exp1` and `$exp2` of different indices.",
212-
),
213-
)
214-
end
215-
@inbounds for i in eachindex(exp1)
216-
Δ = exp1[i] - exp2[i]
217-
if !iszero(Δ)
218-
return Δ
219-
end
220-
end
221-
return zero(T)
203+
const _TupleOrVector = Union{Tuple,AbstractVector}
204+
205+
function compare(exp1::_TupleOrVector, exp2::_TupleOrVector, ::Type{LexOrder})
206+
return Base.cmp(exp1, exp2)
222207
end
223208

224209
"""
@@ -239,25 +224,16 @@ SIAM Journal on Matrix Analysis and Applications, 34(1), 102-125, *2013*.
239224
"""
240225
struct InverseLexOrder <: AbstractMonomialOrdering end
241226

227+
# We can't use `Iterators.Reverse` because it's not an `AbstractVector`
228+
# so not `cmp` methods is defined for it.
229+
_rev(v::AbstractVector) = view(v, lastindex(v):-1:firstindex(v))
230+
_rev(t::Tuple) = reverse(t)
242231
function compare(
243-
exp1::AbstractVector{T},
244-
exp2::AbstractVector{T},
232+
exp1::_TupleOrVector,
233+
exp2::_TupleOrVector,
245234
::Type{InverseLexOrder},
246-
) where {T}
247-
if eachindex(exp1) != eachindex(exp2)
248-
throw(
249-
ArgumentError(
250-
"Cannot compare exponent vectors `$exp1` and `$exp2` of different indices.",
251-
),
252-
)
253-
end
254-
@inbounds for i in Iterators.Reverse(eachindex(exp1))
255-
Δ = exp1[i] - exp2[i]
256-
if !iszero(Δ)
257-
return Δ
258-
end
259-
end
260-
return zero(T)
235+
)
236+
return compare(_rev(exp1), _rev(exp2), LexOrder)
261237
end
262238

263239
"""
@@ -316,3 +292,199 @@ compare(a, b, ::Type{Reverse{O}}) where {O} = compare(b, a, O)
316292
Returns the [`AbstractMonomialOrdering`](@ref) used for the monomials of `p`.
317293
"""
318294
function ordering end
295+
296+
_last_lex_index(n, ::Type{LexOrder}) = n
297+
_prev_lex_index(i, ::Type{LexOrder}) = i - 1
298+
_not_first_indices(n, ::Type{LexOrder}) = n:-1:2
299+
_last_lex_index(_, ::Type{InverseLexOrder}) = 1
300+
_prev_lex_index(i, ::Type{InverseLexOrder}) = i + 1
301+
_not_first_indices(n, ::Type{InverseLexOrder}) = 1:(n-1)
302+
_last_lex_index(n, ::Type{Graded{M}}) where {M} = _last_lex_index(n, M)
303+
_prev_lex_index(i, ::Type{Graded{M}}) where {M} = _prev_lex_index(i, M)
304+
_not_first_indices(n, ::Type{Graded{M}}) where {M} = _not_first_indices(n, M)
305+
306+
"""
307+
struct ExponentsIterator{M}(
308+
object;
309+
mindegree::Int = 0,
310+
maxdegree::Union{Nothing,Int} = nothing,
311+
inline::Bool = false,
312+
)
313+
314+
An iterator for generating monomial exponents for monomial
315+
ordering `M`. The type of the vector of exponents is the type of
316+
`object` and is length (i.e., the number of variables) is `length(object)`.
317+
318+
Note that `object` does not have to be zero, it just needs to implement
319+
`copy` and `setindex!` methods (except for `Tuple` which we handle with a
320+
special case).
321+
322+
See also [`monomials`](@ref).
323+
324+
### Examples
325+
326+
The following example shows how to generate all exponents of
327+
monomials of 2 variables up to degree 2.
328+
```jldoctest
329+
julia> collect(ExponentsIterator{Graded{LexOrder}}((0, 0), maxdegree = 2))
330+
6-element Vector{Tuple{Int64, Int64}}:
331+
(0, 0)
332+
(0, 1)
333+
(1, 0)
334+
(0, 2)
335+
(1, 1)
336+
(2, 0)
337+
```
338+
Note that you can easily generate the tuple of exponents
339+
of arbitrary length using `ntuple` as follows:
340+
```jldoctest
341+
julia> collect(ExponentsIterator{Graded{LexOrder}}(ntuple(zero, 3), mindegree = 2, maxdegree = 2))
342+
6-element Vector{Tuple{Int64, Int64, Int64}}:
343+
(0, 0, 2)
344+
(0, 1, 1)
345+
(0, 2, 0)
346+
(1, 0, 1)
347+
(1, 1, 0)
348+
(2, 0, 0)
349+
```
350+
You can also change the monomial ordering and use `Vector` instead of `Tuple` as follows:
351+
```jldoctest
352+
julia> collect(ExponentsIterator{LexOrder}(zeros(Int, 2), mindegree = 2, maxdegree = 3))
353+
7-element Vector{Vector{Int64}}:
354+
[0, 2]
355+
[0, 3]
356+
[1, 1]
357+
[1, 2]
358+
[2, 0]
359+
[2, 1]
360+
[3, 0]
361+
```
362+
"""
363+
struct ExponentsIterator{M,D<:Union{Nothing,Int},O}
364+
object::O # Used to get number of variables and get new zero elements
365+
mindegree::Int
366+
maxdegree::D
367+
inline::Bool
368+
end
369+
370+
function ExponentsIterator{M}(
371+
object;
372+
mindegree::Int = 0,
373+
maxdegree::Union{Nothing,Int} = nothing,
374+
inline::Bool = false,
375+
) where {M}
376+
if length(object) == 0 && isnothing(maxdegree)
377+
# Otherwise, it will incorrectly think that the iterator is infinite
378+
# while it actually has zero elements
379+
maxdegree = mindegree
380+
end
381+
return ExponentsIterator{M,typeof(maxdegree),typeof(object)}(
382+
object,
383+
mindegree,
384+
maxdegree,
385+
inline,
386+
)
387+
end
388+
389+
Base.eltype(::Type{ExponentsIterator{M,D,O}}) where {M,D,O} = O
390+
function Base.IteratorSize(::Type{<:ExponentsIterator{M,Nothing}}) where {M}
391+
return Base.IsInfinite()
392+
end
393+
function Base.IteratorSize(::Type{<:ExponentsIterator{M,Int}}) where {M}
394+
return Base.HasLength()
395+
end
396+
397+
function Base.length(it::ExponentsIterator{M,Int}) where {M}
398+
len = binomial(nvariables(it) + it.maxdegree, nvariables(it))
399+
if it.mindegree > 0
400+
if it.maxdegree < it.mindegree
401+
return 0
402+
end
403+
len -= binomial(nvariables(it) + it.mindegree - 1, nvariables(it))
404+
end
405+
return len
406+
end
407+
408+
nvariables(it::ExponentsIterator) = length(it.object)
409+
410+
_increase_degree(it::ExponentsIterator{<:Graded,Nothing}, _) = false
411+
_increase_degree(it::ExponentsIterator{<:Graded,Int}, _) = false
412+
_increase_degree(it::ExponentsIterator{M,Nothing}, _) where {M} = true
413+
function _increase_degree(it::ExponentsIterator{M,Int}, deg) where {M}
414+
return deg < it.maxdegree
415+
end
416+
417+
# We just changed the degree by removing `Δ`,
418+
# In graded ordering, we just add `Δ` to maintain the same degree
419+
_adjust_degree(::ExponentsIterator{<:Graded}, _, Δ) = Δ
420+
# Otherwise, we just need the degree to stay above `it.mindegree`,
421+
# so we need to add `it.mindegree - deg`
422+
_adjust_degree(it::ExponentsIterator, deg, _) = max(0, it.mindegree - deg)
423+
424+
_setindex!(x, v, i) = Base.setindex!(x, v, i)
425+
_setindex!(x::Tuple, v, i) = Base.setindex(x, v, i)
426+
_increment!(x, i) = _setindex!(x, x[i] + 1, i)
427+
428+
_zero(x) = zero(x)
429+
_zero(x::Tuple) = zero.(x)
430+
431+
_zero!(x) = fill!(x, 0)
432+
_zero!(x::Tuple) = _zero(x)
433+
434+
_copy(x) = copy(x)
435+
_copy(x::Tuple) = x
436+
437+
function _iterate!(it::ExponentsIterator{M}, z, deg) where {M}
438+
if _increase_degree(it, deg)
439+
z = _increment!(z, _last_lex_index(nvariables(it), M))
440+
return z, deg + 1
441+
end
442+
I = _not_first_indices(nvariables(it), M)
443+
i = findfirst(i -> !iszero(z[i]), I)
444+
if isnothing(i)
445+
if !isnothing(it.maxdegree) && deg == it.maxdegree
446+
return
447+
end
448+
z = _zero!(z)
449+
z = _setindex!(z, deg + 1, _last_lex_index(nvariables(it), M))
450+
return z, deg + 1
451+
end
452+
j = I[i]
453+
Δ = z[j] - 1
454+
z = _setindex!(z, 0, j)
455+
j = I[i]
456+
deg -= Δ
457+
Δ = _adjust_degree(it, deg, Δ)
458+
deg += Δ
459+
z = _setindex!(z, Δ, _last_lex_index(nvariables(it), M))
460+
j = I[i]
461+
z = _increment!(z, _prev_lex_index(j, M))
462+
return z, deg
463+
end
464+
465+
function Base.iterate(it::ExponentsIterator{M}) where {M}
466+
z = _zero(it.object)
467+
if it.mindegree > 0
468+
if nvariables(it) == 0 ||
469+
(!isnothing(it.maxdegree) && it.maxdegree < it.mindegree)
470+
return
471+
end
472+
z = _setindex!(z, it.mindegree, _last_lex_index(nvariables(it), M))
473+
end
474+
return z, (z, it.mindegree)
475+
end
476+
477+
function Base.iterate(it::ExponentsIterator, state)
478+
if nvariables(it) == 0
479+
return # There cannot be more than 1 element
480+
end
481+
z, deg = state
482+
if !it.inline
483+
z = _copy(z)
484+
end
485+
state = _iterate!(it, z, deg)
486+
if isnothing(state)
487+
return
488+
end
489+
return state[1], state
490+
end

src/polynomial.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,12 @@ end
252252
253253
Returns an iterator over the monomials of `p` of the nonzero terms of the polynomial sorted in the decreasing order.
254254
255-
monomials(vars::Tuple, degs::AbstractVector{Int}, filter::Function = m -> true)
255+
monomials(vars::Union{Vector{<:AbstractVariable},Tuple}, degs::AbstractVector{Int}, filter::Function = m -> true)
256256
257257
Builds the vector of all the monomial_vector `m` with variables `vars` such that the degree `degree(m)` is in `degs` and `filter(m)` is `true`.
258258
259+
See also [`ExponentsIterator`](@ref).
260+
259261
### Examples
260262
261263
Calling `monomials` on ``4x^2y + xy + 2x`` should return an iterator of ``[x^2y, xy, x]``.

test/comparison.jl

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
module TestComparison
2+
3+
using Test
4+
using MultivariatePolynomials
5+
6+
function _test(object, M; kws...)
7+
it = ExponentsIterator{M}(object; kws...)
8+
v = collect(Iterators.take(it, 20))
9+
@test issorted(v, lt = (a, b) -> compare(a, b, M) < 0)
10+
end
11+
12+
function _test(nvars::Int, M; kws...)
13+
_test(zeros(Int, nvars), M; inline = false, kws...)
14+
_test(zeros(Int, nvars), M; inline = true, kws...)
15+
_test(ntuple(zero, nvars), M; inline = false, kws...)
16+
return
17+
end
18+
19+
function test_exponents_iterator()
20+
@testset "nvariables = $nvars" for nvars in 0:3
21+
@testset "mindegree = $mindegree" for mindegree in 0:3
22+
@testset "maxdegree = $maxdegree" for maxdegree in
23+
vcat(nothing, 0:3)
24+
for L in [LexOrder, InverseLexOrder]
25+
@testset "M = $M" for M in [L, Graded{L}]
26+
_test(nvars, M; mindegree, maxdegree)
27+
end
28+
end
29+
end
30+
end
31+
end
32+
end
33+
34+
function test_compare()
35+
lex = LexOrder
36+
grlex = Graded{lex}
37+
rinvlex = Reverse{InverseLexOrder}
38+
grevlex = Graded{rinvlex}
39+
@test compare([1, 0, 1], [1, 1, 0], grlex) == -1
40+
@test compare([1, 1, 0], [1, 0, 1], grlex) == 1
41+
# [CLO13, p. 58]
42+
@test compare(1:3, [3, 2, 0], lex) < 0
43+
@test compare(1:3, [3, 2, 0], grlex) > 0
44+
@test compare(1:3, [3, 2, 0], rinvlex) < 0
45+
@test compare(1:3, [3, 2, 0], grevlex) > 0
46+
@test compare([1, 2, 4], [1, 1, 5], lex) > 0
47+
@test compare([1, 2, 4], [1, 1, 5], grlex) > 0
48+
@test compare([1, 2, 4], [1, 1, 5], rinvlex) > 0
49+
@test compare([1, 2, 4], [1, 1, 5], grevlex) > 0
50+
# [CLO13, p. 59]
51+
@test compare((5, 1, 1), (4, 1, 2), lex) > 0
52+
@test compare((5, 1, 1), (4, 1, 2), grlex) > 0
53+
@test compare((5, 1, 1), (4, 1, 2), rinvlex) > 0
54+
@test compare((5, 1, 1), (4, 1, 2), grevlex) > 0
55+
# [CLO13] Cox, D., Little, J., & OShea, D.
56+
# *Ideals, varieties, and algorithms: an introduction to computational algebraic geometry and commutative algebra*.
57+
# Springer Science & Business Media, **2013**.
58+
59+
end
60+
61+
function runtests()
62+
for name in names(@__MODULE__; all = true)
63+
if startswith("$(name)", "test_")
64+
@testset "$(name)" begin
65+
getfield(@__MODULE__, name)()
66+
end
67+
end
68+
end
69+
return
70+
end
71+
72+
end # module
73+
74+
TestComparison.runtests()

test/runtests.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ using LinearAlgebra
44
using MultivariatePolynomials
55
const MP = MultivariatePolynomials
66

7+
# Tests that do not need any specific polynomial implementation
8+
include("comparison.jl")
9+
710
include("utils.jl")
811

912
# Taken from JuMP/test/solvers.jl

0 commit comments

Comments
 (0)