Skip to content

Commit 012d83b

Browse files
authored
Merge pull request #98 from rprebet/hilbert
Add functions for computation of Hilbert series
2 parents 419fefd + b8a0bf1 commit 012d83b

File tree

8 files changed

+376
-12
lines changed

8 files changed

+376
-12
lines changed

docs/src/hilbert.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
```@meta
2+
CurrentModule = AlgebraicSolving
3+
DocTestSetup = quote
4+
using AlgebraicSolving
5+
end
6+
```
7+
8+
```@setup algebraicsolving
9+
using AlgebraicSolving
10+
```
11+
12+
```@contents
13+
Pages = ["hilbert.md"]
14+
```
15+
16+
# Hilbert series of an ideal
17+
18+
## Introduction
19+
20+
AlgebraicSolving allows to compute the Hilbert series for the ideal spanned
21+
by given input generators over finite fields of characteristic smaller
22+
$2^{31}$ and over the rationals.
23+
24+
The underlying engine is provided by msolve.
25+
26+
## Functionality
27+
28+
```@docs
29+
hilbert_series(I::Ideal{T}) where T <: MPolyRingElem
30+
```
31+
32+
In addition, from the same input, AlgebraicSolving can also compute the dimension and degree of the ideal, as well as the Hilbert polynomial and index of regularity.
33+
34+
```@docs
35+
hilbert_dimension(I::Ideal{T}) where T <: MPolyRingElem
36+
37+
hilbert_degree(I::Ideal{T}) where T <: MPolyRingElem
38+
39+
hilbert_polynomial(I::Ideal{T}) where T <: MPolyRingElem
40+
```

src/AlgebraicSolving.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ include("algorithms/normal-forms.jl")
1414
include("algorithms/solvers.jl")
1515
include("algorithms/dimension.jl")
1616
include("algorithms/decomposition.jl")
17+
include("algorithms/hilbert.jl")
1718
#= siggb =#
1819
include("siggb/siggb.jl")
1920
#= examples =#

src/algorithms/dimension.jl

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
Compute the Krull dimension of a given polynomial ideal `I`.
55
6-
**Note**: This requires a Gröbner basis of `I`, which is computed internally if not alraedy known.
6+
**Note**: This requires a Gröbner basis of `I`, which is computed internally if not already known.
77
88
# Examples
99
```jldoctest
@@ -26,13 +26,16 @@ function dimension(I::Ideal{T}) where T <: MPolyRingElem
2626
R = parent(first(gb))
2727

2828
res = Set([trues(ngens(R))])
29-
lead_exps = (_drl_lead_exp).(gb)
29+
lead_exps = Vector{Vector{Int}}(undef, length(gb))
30+
for i in eachindex(gb)
31+
lead_exps[i] = _lead_exp_ord(gb[i], :degrevlex)
32+
end
3033
for lexp in lead_exps
3134
nz_exps = (!iszero).(lexp)
3235
nz_exps_ind = findall(nz_exps)
3336
next_res = Set{BitVector}()
3437
for mis in res
35-
if all_lesseq(nz_exps, mis)
38+
if _all_lesseq(nz_exps, mis)
3639
@inbounds for j in nz_exps_ind
3740
new_mis = copy(mis)
3841
new_mis[j] = false
@@ -49,7 +52,7 @@ function dimension(I::Ideal{T}) where T <: MPolyRingElem
4952
return I.dim
5053
end
5154

52-
function all_lesseq(a::BitVector, b::BitVector)::Bool
55+
function _all_lesseq(a::BitVector, b::BitVector)::Bool
5356
@inbounds for i in eachindex(a)
5457
if a[i] && !b[i]
5558
return false
@@ -58,12 +61,15 @@ function all_lesseq(a::BitVector, b::BitVector)::Bool
5861
return true
5962
end
6063

61-
function _drl_exp_vector(u::Vector{Int})
62-
return [sum(u), -reverse(u)...]
63-
end
64+
function _lead_exp_ord(p::MPolyRingElem, order::Symbol)
65+
R = parent(p)
66+
internal_ordering(R)==order && return first(exponent_vectors(p))
6467

65-
function _drl_lead_exp(p::MPolyRingElem)
66-
exps = collect(Nemo.exponent_vectors(p))
67-
_, i = findmax(_drl_exp_vector.(exps))
68-
return exps[i]
68+
A = base_ring(R)
69+
R1, _ = polynomial_ring(A, symbols(R), internal_ordering=order)
70+
ctx = MPolyBuildCtx(R1)
71+
for e in exponent_vectors(p)
72+
push_term!(ctx, one(A), e)
73+
end
74+
return first(exponent_vectors(finish(ctx)))
6975
end

src/algorithms/hilbert.jl

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
@doc Markdown.doc"""
2+
hilbert_series(I::Ideal{T}) where T <: MPolyRingElem
3+
4+
Compute the Hilbert series of a given polynomial ideal `I`.
5+
6+
Based on: Anna M. Bigatti, Computation of Hilbert-Poincaré series,
7+
Journal of Pure and Applied Algebra, 1997.
8+
9+
**Notes**:
10+
* This requires a Gröbner basis of `I`, which is computed internally if not already known.
11+
* Significantly faster when internal_ordering is :degrevlex.
12+
13+
# Examples
14+
```jldoctest
15+
julia> using AlgebraicSolving
16+
17+
julia> R, (x, y, z) = polynomial_ring(QQ, ["x", "y", "z"]);
18+
19+
julia> I = Ideal([x*y,x*z,y*z]);
20+
21+
julia> hilbert_series(I)
22+
(-2*t - 1)//(t - 1)
23+
```
24+
"""
25+
function hilbert_series(I::Ideal{T}) where T <: MPolyRingElem
26+
27+
gb = get!(I.gb, 0) do
28+
groebner_basis(I, complete_reduction = true)
29+
end
30+
lead_exps = Vector{Vector{Int}}(undef, length(gb))
31+
for i in eachindex(gb)
32+
lead_exps[i] = _lead_exp_ord(gb[i], :degrevlex)
33+
end
34+
return _hilbert_series_mono(lead_exps)
35+
end
36+
37+
@doc Markdown.doc"""
38+
hilbert_degree(I::Ideal{T}) where T <: MPolyRingElem
39+
40+
Compute the degree of a given polynomial ideal `I` by first computing its Hilbert series.
41+
42+
**Note**: This requires a Gröbner basis of `I`, which is computed internally if not already known.
43+
44+
# Examples
45+
```jldoctest
46+
julia> using AlgebraicSolving
47+
48+
julia> R, (x, y, z) = polynomial_ring(QQ, ["x", "y", "z"]);
49+
50+
julia> I = Ideal([x*y,x*z,y*z]);
51+
52+
julia> hilbert_degree(I)
53+
3
54+
```
55+
"""
56+
function hilbert_degree(I::Ideal{T}) where T <: MPolyRingElem
57+
58+
!isnothing(I.deg) && return I.deg
59+
I.deg = numerator(hilbert_series(I))(1) |> abs
60+
return I.deg
61+
end
62+
63+
@doc Markdown.doc"""
64+
hilbert_dimension(I::Ideal{T}) where T <: MPolyRingElem
65+
66+
Compute the Krull dimension of a given polynomial ideal `I` by first computing its Hilbert series.
67+
68+
**Note**: This requires a Gröbner basis of `I`, which is computed internally if not already known.
69+
70+
# Examples
71+
```jldoctest
72+
julia> using AlgebraicSolving
73+
74+
julia> R, (x, y, z) = polynomial_ring(QQ, ["x", "y", "z"]);
75+
76+
julia> I = Ideal([x*y,x*z,y*z]);
77+
78+
julia> hilbert_dimension(I)
79+
1
80+
```
81+
"""
82+
function hilbert_dimension(I::Ideal{T}) where T <: MPolyRingElem
83+
84+
H = hilbert_series(I)
85+
I.dim = iszero(H) ? -1 : degree(denominator(H))
86+
return I.dim
87+
end
88+
89+
@doc Markdown.doc"""
90+
hilbert_polynomial(I::Ideal{T}) where T <: MPolyRingElem
91+
92+
Compute the Hilbert polynomial and the index of regularity of a given polynomial ideal `I`
93+
by first computing its Hilbert series. The index of regularity is the smallest integer such that
94+
the Hilbert function and polynomial match.
95+
96+
Note that the Hilbert polynomial of I has leading term (e/d!)*t^d, where e and d are respectively
97+
the degree and Krull dimension of I.
98+
99+
**Note**: This requires a Gröbner basis of `I`, which is computed internally if not already known.
100+
101+
# Examples
102+
```jldoctest
103+
julia> using AlgebraicSolving
104+
105+
julia> R, (x, y, z) = polynomial_ring(QQ, ["x", "y", "z"]);
106+
107+
julia> I = Ideal([x*y,x*z,y*z]);
108+
109+
julia> hilbert_polynomial(I)
110+
(3*s + 3, 1)
111+
```
112+
"""
113+
function hilbert_polynomial(I::Ideal{T}) where T <: MPolyRingElem
114+
115+
A, s = polynomial_ring(QQ, :s)
116+
H = hilbert_series(I)
117+
dim = degree(denominator(H))
118+
num = iseven(dim) ? numerator(H) : -numerator(H)
119+
dim==0 && return num(s), 0
120+
121+
t = gen(parent(num))
122+
La = Vector{ZZPolyRingElem}(undef, dim)
123+
while dim>0
124+
num, La[dim] = divrem(num, 1-t)
125+
dim -= 1
126+
end
127+
128+
Hpolyfct = d->sum(La[i](0)*binomial(i+d, i) for i in 1:length(La))
129+
dim = degree(denominator(H))
130+
Hpoly = interpolate(A, QQ.(0:dim+1), [QQ(Hpolyfct(d)) for d in 0:dim+1])
131+
@assert(degree(Hpoly)==dim, "Degree of poly does not match the dimension")
132+
# Hilbert poly, index of regularity
133+
return Hpoly, degree(num)+1
134+
end
135+
136+
# Computes hilbert series of a monomial ideal on input list of exponents
137+
function _hilbert_series_mono(exps::Vector{Vector{Int}})
138+
139+
h = _num_hilbert_series_mono(exps)
140+
t = gen(parent(h))
141+
return h//(1-t)^length(first(exps))
142+
end
143+
144+
# Computes numerator hilbert series of a monomial ideal on input list of exponents
145+
function _num_hilbert_series_mono(exps::Vector{Vector{Int}})
146+
147+
A, t = polynomial_ring(ZZ, 't')
148+
r = length(exps)
149+
r == 0 && return one(A)
150+
N = length(first(exps))
151+
## Base cases ##
152+
r == 1 && return (1-t^sum(first(exps)))
153+
supp = findall.(Ref(!iszero), exps)
154+
pow_supp = findall(s->length(s)==1, supp)
155+
# If exps is a product of simple powers
156+
if length(pow_supp) == r
157+
#println("Simple power")
158+
return prod(1-t^(exps[i][supp[i][1]]) for i in pow_supp)
159+
# Only one non-simple power P
160+
elseif length(pow_supp) == r-1
161+
#println("Mixed pow")
162+
inpow = setdiff(eachindex(exps), pow_supp) |> first
163+
# P has disjoint support with other powers
164+
if all(iszero(exps[inpow][supp[i][1]]) for i in pow_supp)
165+
return (1-t^sum(exps[inpow]))*prod(1-t^(exps[i][supp[i][1]]) for i in pow_supp)
166+
else
167+
return prod(1-t^(exps[i][supp[i][1]]) for i in pow_supp) - t^sum(exps[inpow]) *
168+
prod(1-t^(exps[i][supp[i][1]]-exps[inpow][supp[i][1]]) for i in pow_supp)
169+
end
170+
end
171+
172+
# Variable index occuring the most in exps
173+
counts = sum(x->x .> 0, eachcol(reduce(hcat, exps)))
174+
ivarmax = argmax(counts)
175+
176+
## Splitting recursive cases ##
177+
# Monomials have disjoint supports
178+
if counts[ivarmax] == 1
179+
return prod(1-t^sum(mono) for mono in exps)
180+
# Heuristic where general splitting is useful
181+
elseif 8 <= r <= N
182+
# Finest partition of monomial supports
183+
LV, h = _monomial_support_partition(exps), one(A)
184+
rem_mon = collect(1:r)
185+
# If we are indeed splitting
186+
if length(LV) > 1
187+
for V in LV
188+
JV, iJV = Vector{Vector{Int}}(), Int[]
189+
for (k, i) in enumerate(rem_mon)
190+
mono = exps[i]
191+
if any(mono[j] != 0 for j in V)
192+
push!(iJV, k)
193+
push!(JV, mono)
194+
end
195+
end
196+
h *= _num_hilbert_series_mono(JV)
197+
# Avoid re-check monomials
198+
deleteat!(rem_mon, iJV)
199+
end
200+
return h
201+
end
202+
end
203+
204+
## Pivot recursive case ##
205+
# Exponent of ivarmax in gcd of two random generators
206+
pivexp = max(1, minimum(mon[ivarmax] for mon in rand(exps, 2)))
207+
h = zero(A)
208+
#Compute and partition gens of (exps):pivot
209+
Lquo = [Vector{Int}[] for _ in 1:pivexp+2]
210+
trivialquo = false
211+
for mono in exps
212+
if mono[ivarmax] <= pivexp
213+
monoquo = vcat(mono[1:ivarmax-1], 0, mono[ivarmax+1:end])
214+
if iszero(monoquo)
215+
trivialquo = true
216+
break
217+
end
218+
push!(Lquo[mono[ivarmax]+1], monoquo)
219+
else
220+
push!(Lquo[pivexp+2],
221+
vcat(mono[1:ivarmax-1], mono[ivarmax]-pivexp, mono[ivarmax+1:end]))
222+
end
223+
end
224+
if !trivialquo
225+
# Interreduce generators based on partition
226+
@inbounds for i in pivexp+1:-1:1
227+
non_min = [ k for (k,mono) in enumerate(Lquo[i]) if
228+
any(_all_lesseq(mini, mono) for j in i+1:pivexp+1 for mini in Lquo[j])]
229+
deleteat!(Lquo[i], non_min)
230+
end
231+
# Merge all partitions
232+
h += _num_hilbert_series_mono(vcat(Lquo...))*t^pivexp
233+
end
234+
# Interreduce (exps) + pivot
235+
filter!(e->(pivexp > e[ivarmax]), exps)
236+
push!(exps,[zeros(Int64,ivarmax-1); pivexp; zeros(Int64,N-ivarmax)])
237+
h += _num_hilbert_series_mono(exps)
238+
239+
return h
240+
end
241+
242+
function _all_lesseq(a::Vector{Int}, b::Vector{Int})::Bool
243+
@inbounds for i in eachindex(a)
244+
if a[i] > b[i]
245+
return false
246+
end
247+
end
248+
return true
249+
end
250+
251+
# Build adjacency graph: connect variables that appear together in a monomial
252+
function _monomial_support_partition(L::Vector{Vector{Int}})
253+
254+
n = length(first(L))
255+
adj = [Set{Int}() for _ in 1:n]
256+
active = falses(n)
257+
258+
for mono in L
259+
support = findall(!=(0), mono)
260+
foreach(i -> active[i] = true, support)
261+
for i in support, j in support
262+
i != j && push!(adj[i], j)
263+
end
264+
end
265+
266+
visited = falses(n)
267+
components = Vector{Vector{Int}}()
268+
269+
function dfs(u, comp)
270+
visited[u] = true
271+
push!(comp, u)
272+
foreach(v -> !visited[v] && dfs(v, comp), adj[u])
273+
end
274+
275+
for v in 1:n
276+
if active[v] && !visited[v]
277+
comp = Int[]
278+
dfs(v, comp)
279+
push!(components, comp)
280+
end
281+
end
282+
283+
return components
284+
end

0 commit comments

Comments
 (0)