Skip to content

Commit bf5225a

Browse files
authored
Merge pull request #106 from rprebet/paramcurve
Add rational parametrization for algebraic curves
2 parents 41afe86 + 616cacb commit bf5225a

File tree

10 files changed

+387
-15
lines changed

10 files changed

+387
-15
lines changed

docs/src/solvers.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,14 @@ The underlying engine is provided by msolve.
5353
info_level::Int=0,
5454
precision::Int=32
5555
)
56+
57+
rational_curve_parametrization(
58+
I::Ideal{P} where P<:QQMPolyRingElem;
59+
info_level::Int=0,
60+
use_lfs::Bool = false,
61+
cfs_lfs::Vector{Vector{ZZRingElem}} = Vector{ZZRingElem}[],
62+
nr_thrds::Int=1,
63+
check_gen::Bool = true
64+
)
5665
```
5766

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/param-curve.jl")
1718
include("algorithms/hilbert.jl")
1819
#= siggb =#
1920
include("siggb/siggb.jl")

src/algorithms/groebner-bases.jl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,8 @@ function groebner_basis(
123123
truncate_lifting::Int=0,
124124
info_level::Int=0
125125
)
126-
127126
return get!(I.gb, eliminate) do
128-
_core_groebner_basis(I, initial_hts = initial_hts, nr_thrds = nr_thrds,
127+
_core_groebner_basis(I, initial_hts = initial_hts, nr_thrds = nr_thrds,
129128
max_nr_pairs = max_nr_pairs, la_option = la_option,
130129
eliminate = eliminate, intersect = intersect,
131130
complete_reduction = complete_reduction,
@@ -148,7 +147,6 @@ function _core_groebner_basis(
148147
truncate_lifting::Int=0,
149148
info_level::Int=0
150149
)
151-
152150
F = I.gens
153151

154152
if iszero(F)

src/algorithms/param-curve.jl

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
@doc Markdown.doc"""
2+
rational_curve_parametrization(I::Ideal{T} where T <: MPolyRingElem, <keyword arguments>)
3+
4+
Given a **radical** ideal `I` with solution set X being of dimension 1 over the complex numbers,
5+
return a rational curve parametrization of the one-dimensional irreducible components of X.
6+
7+
In the output, the variables `x`,`y` of the parametrization correspond to the last two
8+
entries of the `vars` attribute, in that order.
9+
10+
**Note**: At the moment only QQ is supported as ground field. If the dimension of the ideal
11+
is not one an ErrorException is thrown.
12+
13+
# Arguments
14+
- `I::Ideal{T} where T <: QQMPolyRingElem`: input generators.
15+
- `info_level::Int=0`: info level printout: off (`0`, default), summary (`1`), detailed (`2`).
16+
- `use_lfs::Bool=false`: add new variables (_Z2, _Z1) + 2 generic linear forms
17+
- `cfs_lfs::Vector{Vector{ZZRingElem}} = []`: coefficients for the above linear forms
18+
- `nr_thrds::Int=1`: number of threads for msolve
19+
- `check_gen::Bool = true`: perform some genericity position checks on the last two variables
20+
21+
# Examples
22+
```jldoctest
23+
julia> using AlgebraicSolving
24+
25+
julia> R, (x1,x2,x3) = polynomial_ring(QQ, ["x1","x2","x3"])
26+
(Multivariate polynomial ring in 3 variables over QQ, QQMPolyRingElem[x1, x2, x3])
27+
28+
julia> I = Ideal([x1+2*x2+2*x3-1, x1^2+2*x2^2+2*x3^2-x1])
29+
QQMPolyRingElem[x1 + 2*x2 + 2*x3 - 1, x1^2 - x1 + 2*x2^2 + 2*x3^2]
30+
31+
julia> rational_curve_parametrization(I)
32+
AlgebraicSolving.RationalCurveParametrization([:x1, :x2, :x3], Vector{ZZRingElem}[], x^2 + 4//3*x*y - 1//3*x + y^2 - 1//3*y, 4//3*x + 2*y - 1//3, QQMPolyRingElem[4//3*x^2 - 4//3*x*y + 2//3*x + 4//3*y - 1//3])
33+
34+
julia> rational_curve_parametrization(I, cfs_lfs=map.(Ref(ZZ),[[-8,2,2,-1,-8], [8,-7,-5,8,-7]]))
35+
AlgebraicSolving.RationalCurveParametrization([:x1, :x2, :x3, :_Z2, :_Z1], Vector{ZZRingElem}[[-8, 2, 2, -1, -8], [8, -7, -5, 8, -7]], 4963//30508*x^2 - 6134//7627*x*y - 647//7627*x + y^2 + 1640//7627*y + 88//7627, -6134//7627*x + 2*y + 1640//7627, QQMPolyRingElem[8662//22881*x^2 - 21442//22881*x*y - 2014//7627*x + 9458//22881*y + 1016//22881, -2769//30508*x^2 + 4047//15254*x*y - 875//7627*x + 3224//7627*y + 344//7627, -9017//91524*x^2 + 9301//45762*x*y - 1185//7627*x + 8480//22881*y + 920//22881])
36+
```
37+
"""
38+
function rational_curve_parametrization(
39+
I::Ideal{P} where P<:QQMPolyRingElem; # input generators
40+
info_level::Int=0, # info level for print outs
41+
use_lfs::Bool = false, # add generic variables
42+
cfs_lfs::Vector{Vector{ZZRingElem}} = Vector{ZZRingElem}[], # coeffs of linear forms
43+
nr_thrds::Int=1, # number of threads (msolve)
44+
check_gen::Bool = true # perform genericity check
45+
)
46+
info_level>0 && println("Compute ideal data" * (check_gen ? " and genericity check" : ""))
47+
lucky_prime = _generate_lucky_primes(I.gens, one(ZZ)<<30, one(ZZ)<<31-1, 1) |> first
48+
Itest = Ideal(change_base_ring.(Ref(GF(lucky_prime)), I.gens))
49+
Itest.dim = I.dim
50+
dimension(Itest)
51+
if Itest.dim == -1
52+
T = polynomial_ring(QQ, [:x,:y])[1]
53+
I.dim = -1
54+
I.rat_param = RationalCurveParametrization(Symbol[], Vector{ZZRingElem}[], T(-1), T(-1), QQMPolyRingElem[])
55+
return I.rat_param
56+
end
57+
@assert(Itest.dim==1, "I must define a curve or an empty set")
58+
if nvars(parent(I)) == 1
59+
T, (x,y) = polynomial_ring(QQ, [:x,:y])
60+
I.dim = 1
61+
I.rat_param = RationalCurveParametrization(Symbol[:x, :y], Vector{ZZRingElem}[[0,1]], y, T(1), QQMPolyRingElem[])
62+
return I.rat_param
63+
end
64+
65+
DEG = hilbert_degree(Itest)
66+
# If generic variables must be added
67+
F, cfs_lfs = use_lfs ? _add_genvars(I.gens, max(2, length(cfs_lfs)), cfs_lfs) :
68+
!isempty(cfs_lfs) ? _add_genvars(I.gens, length(cfs_lfs), cfs_lfs) :
69+
(I.gens, Vector{ZZRingElem}[])
70+
R = parent(first(F))
71+
N = nvars(R)
72+
if check_gen let
73+
val = [ZZ(), ZZ()]
74+
# Bound on bifurcation set degree (e.g. Jelonek & Kurdyka, 2005)
75+
bif_bound = ZZ(1) << ( N * floor(Int, log2(maximum(total_degree.(F)))) + 1 )
76+
while any(is_divisible_by.(val, Ref(lucky_prime)))
77+
val = rand(-bif_bound:bif_bound, 2)
78+
end
79+
for ivar in [N-1, N]
80+
Fnew = vcat(F, val[1]*gens(R)[ivar] + val[2])
81+
new_lucky_prime = _generate_lucky_primes(Fnew, one(ZZ)<<30, one(ZZ)<<31-1, 1) |> first
82+
local INEW = Ideal(change_base_ring.(Ref(GF(new_lucky_prime)), Fnew))
83+
@assert(dimension(INEW) == 0 && hilbert_degree(INEW) == DEG, "The curve is not in generic position")
84+
end
85+
end end
86+
87+
# Compute DEG+2 evaluations of x in the param (whose total deg is bounded by DEG)
88+
PARAM = Vector{Vector{QQPolyRingElem}}(undef, DEG+2)
89+
_values = Vector{ZZRingElem}(undef, DEG+2)
90+
i = 1
91+
free_ind = collect(1:DEG+2)
92+
used_ind = zeros(Bool, DEG+2)
93+
lc = nothing
94+
while length(free_ind) > 0
95+
if i > 2*(DEG+2)
96+
error("Too many bad specializations. Check radicality, else permute variables or use_lfs=true")
97+
end
98+
# Evaluation of the generator at values x s.t. 0 <= |x|-i <= length(free_ind)/2
99+
# plus one point at -(length(free_ind)+1)/2 if the length if odd.
100+
# This reduces a bit the bitsize of the evaluation
101+
curr_values = ZZ.([-(i-1+(length(free_ind)+1)÷2):-i;i:(i-1+length(free_ind)÷2)])
102+
LFeval = Ideal.(_evalvar(F, N-1, curr_values))
103+
# Compute parametrization of each evaluation
104+
Lr = Vector{RationalParametrization}(undef, length(free_ind))
105+
for j in 1:length(free_ind)
106+
info_level>0 && print("Evaluated parametrizations: $(j+DEG+2-length(free_ind))/$(DEG+2)", "\r")
107+
Lr[j] = rational_parametrization(LFeval[j], nr_thrds=nr_thrds)
108+
end
109+
info_level>0 && println()
110+
for j in 1:length(free_ind)
111+
# Specialization checks: same vars order, generic degree
112+
if Lr[j].vars == [symbols(R)[1:N-2]; symbols(R)[N]] && degree(Lr[j].elim) == DEG
113+
if isnothing(lc)
114+
lc = leading_coefficient(Lr[j].elim)
115+
rr = [ p for p in vcat(Lr[j].elim, Lr[j].denom, Lr[j].param) ]
116+
else
117+
# Adjust when the rat_param is multiplied by some constant factor
118+
fact = lc / leading_coefficient(Lr[j].elim)
119+
rr = [ p*fact for p in vcat(Lr[j].elim, Lr[j].denom, Lr[j].param) ]
120+
end
121+
PARAM[j] = rr
122+
_values[j] = curr_values[j]
123+
used_ind[j] = true
124+
else
125+
info_level>0 && println("bad specialization: ", i+j-1)
126+
end
127+
end
128+
i += length(free_ind)
129+
free_ind = [ free_ind[j] for j in eachindex(free_ind) if !used_ind[j] ]
130+
used_ind = zeros(Bool, length(free_ind))
131+
end
132+
133+
# Interpolate each coefficient of each poly in the param
134+
T = polynomial_ring(QQ, [:x,:y])[1]
135+
A = polynomial_ring(QQ)[1]
136+
137+
POLY_PARAM = Vector{QQMPolyRingElem}(undef,N)
138+
for count in 1:N
139+
info_level>0 && print("Interpolate parametrizations: $count/$N\r")
140+
COEFFS = Vector{QQPolyRingElem}(undef, DEG+1)
141+
for deg in 0:DEG
142+
_evals = [coeff(PARAM[i][count], deg) for i in eachindex(PARAM)]
143+
# Remove denominators for faster interpolation with FLINT
144+
den = foldl(lcm, denominator.(_evals))
145+
scaled_evals = [ZZ(_evals[i] * den) for i in eachindex(_evals)]
146+
COEFFS[deg+1] = interpolate(A, _values, scaled_evals) / (lc*den)
147+
end
148+
ctx = MPolyBuildCtx(T)
149+
for (i, c) in enumerate(COEFFS)
150+
for (j, coeff) in enumerate(coefficients(c))
151+
!iszero(coeff) && push_term!(ctx, coeff, [j-1, i-1])
152+
end
153+
end
154+
POLY_PARAM[count] = finish(ctx)
155+
end
156+
info_level>0 && println()
157+
# Output: [vars, linear forms, elim, denom, [nums_param]]
158+
I.dim = 1 # If we reached here, I has necessarily dimension 1
159+
I.rat_param = RationalCurveParametrization( symbols(R), cfs_lfs, POLY_PARAM[1],
160+
POLY_PARAM[2], POLY_PARAM[3:end] )
161+
return I.rat_param
162+
end
163+
164+
165+
# Inject polynomials in F in a polynomial ring with ngenvars new variables
166+
# return these polynomials
167+
# + newvars linear forms provided by coefficients in cfs_lfs + random ones
168+
function _add_genvars(
169+
F::Vector{P} where P<:MPolyRingElem,
170+
ngenvars::Int,
171+
cfs_lfs::Vector{Vector{ZZRingElem}} = Vector{ZZRingElem}[]
172+
)
173+
if length(cfs_lfs) > ngenvars
174+
error("Too many linear forms provided ($(length(cfs_lfs))>$(ngenvars))")
175+
end
176+
R = parent(first(F))
177+
N = nvars(R)
178+
# Add new variables (reverse alphabetical order)
179+
newS = vcat(symbols(R), Symbol.(["_Z$i" for i in ngenvars:-1:1]))
180+
R_ext, all_vars = polynomial_ring(base_ring(R), newS)
181+
# Inject F in this new ring
182+
Fnew = Vector{MPolyRingElem}(undef, length(F))
183+
ctx = MPolyBuildCtx(R_ext)
184+
for i in eachindex(F)
185+
for (e, c) in zip(exponent_vectors(F[i]), coefficients(F[i]))
186+
push_term!(ctx, c, vcat(e, zeros(Int,ngenvars)))
187+
end
188+
Fnew[i] = finish(ctx)
189+
end
190+
191+
# Complete possible incomplete provided linear forms
192+
for i in eachindex(cfs_lfs)
193+
if N+ngenvars < length(cfs_lfs[i])
194+
error("Too many coeffs ($(length(cfs_lfs[i]))>$(N+ngenvars)) for the $(i)th linear form")
195+
else
196+
append!(cfs_lfs[i], rand(ZZ.(setdiff(-100:100,0)), N+ngenvars - length(cfs_lfs[i])))
197+
end
198+
end
199+
# Add missing linear forms if needed
200+
append!(cfs_lfs, [rand(ZZ.(setdiff(-100:100,0)), N+ngenvars) for _ in 1:ngenvars-length(cfs_lfs)])
201+
# Construct and append linear forms
202+
append!(Fnew, [transpose(cfs_lf) * all_vars for cfs_lf in cfs_lfs])
203+
204+
return Fnew, cfs_lfs
205+
end
206+
207+
# for each a in La, evaluate each poly in F in x_i=a
208+
function _evalvar(
209+
F::Vector{P} where P<:MPolyRingElem,
210+
i::Int,
211+
La::Vector{T} where T<:RingElem
212+
)
213+
R = parent(first(F))
214+
indnewvars = setdiff(1:nvars(R), i)
215+
C, = polynomial_ring(base_ring(R), symbols(R)[indnewvars])
216+
LFeval = Vector{typeof(zero(C))}[]
217+
ctx = MPolyBuildCtx(C)
218+
219+
for a in La
220+
powa = Dict(0=>one(parent(a)), 1=>a) #no recompute powers
221+
push!(LFeval, typeof(zero(C))[])
222+
for f in F
223+
for (e,c) in zip(exponent_vectors(f), coefficients(f))
224+
aei = get!(powa, e[i]) do
225+
a^e[i]
226+
end
227+
push_term!(ctx, c*aei, [e[j] for j in indnewvars ])
228+
end
229+
push!(LFeval[end], finish(ctx))
230+
end
231+
end
232+
return LFeval
233+
end
234+
235+
# Generate N primes > start that do not divide any numerator/denominator
236+
# of any coefficient in polynomials from LP
237+
function _generate_lucky_primes(
238+
LF::Vector{P} where P<:MPolyRingElem,
239+
low::ZZRingElem,
240+
up::ZZRingElem,
241+
N::Int64
242+
)
243+
# Avoid repetitive enumeration and redundant divisibility check
244+
CF = ZZRingElem[]
245+
for f in LF, c in coefficients(f), part in (numerator(c), denominator(c))
246+
if !isone(part)
247+
push!(CF, part)
248+
end
249+
end
250+
sort!(CF, rev=true)
251+
unique!(CF)
252+
253+
# Test primes
254+
Lprim = ZZRingElem[]
255+
while length(Lprim) < N
256+
cur_prim = next_prime(rand(low:up))
257+
is_lucky = !(cur_prim in Lprim)
258+
i = firstindex(CF)
259+
# Exploit decreasing order of CF
260+
while is_lucky && i <= lastindex(CF) && CF[i] > cur_prim
261+
is_lucky = !is_divisible_by(CF[i], cur_prim)
262+
i += 1
263+
end
264+
is_lucky && push!(Lprim, cur_prim)
265+
end
266+
return Lprim
267+
end

src/algorithms/solvers.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function _get_rational_parametrization(
3131
ctr += lens[2]
3232

3333
size = nr-2
34-
p = Vector{PolyRingElem}(undef, size)
34+
p = Vector{QQPolyRingElem}(undef, size)
3535
k = 1
3636
for i in 3:nr
3737
p[k] = C([unsafe_load(cfs, j+ctr) for j in 1:lens[i]-1])
@@ -135,7 +135,7 @@ function _core_msolve(
135135
if jl_dquot == 0
136136
I.dim = -1
137137
C,x = polynomial_ring(QQ,"x")
138-
I.rat_param = RationalParametrization(Symbol[], ZZRingElem[], C(-1), C(-1), PolyRingElem[])
138+
I.rat_param = RationalParametrization(Symbol[], ZZRingElem[], C(-1), C(-1), QQPolyRingElem[])
139139
I.real_sols = QQFieldElem[]
140140
I.inter_sols = Vector{QQFieldElem}[]
141141
return I.rat_param, I.real_sols
@@ -147,11 +147,12 @@ function _core_msolve(
147147
jl_vnames = Base.unsafe_wrap(Array, res_vnames[], jl_rp_nr_vars)
148148
vsymbols = [Symbol(unsafe_string(jl_vnames[i])) for i in 1:jl_rp_nr_vars]
149149
#= get possible variable permutation, ignoring additional variables=#
150-
perm = findfirst.(isequal.(R.S), Ref(vsymbols))
150+
perm = indexin(R.S, vsymbols)
151151

152152
rat_param = _get_rational_parametrization(jl_ld, jl_len,
153153
jl_cf, jl_cf_lf, jl_rp_nr_vars)
154154

155+
155156
I.rat_param = RationalParametrization(vsymbols, rat_param[1],rat_param[2],
156157
rat_param[3], rat_param[4])
157158
if get_param == 2
@@ -239,7 +240,7 @@ julia> I = Ideal([x1+2*x2+2*x3-1, x1^2+2*x2^2+2*x3^2-x1, 2*x1*x2+2*x2*x3-x2])
239240
QQMPolyRingElem[x1 + 2*x2 + 2*x3 - 1, x1^2 - x1 + 2*x2^2 + 2*x3^2, 2*x1*x2 + 2*x2*x3 - x2]
240241
241242
julia> rational_parametrization(I)
242-
AlgebraicSolving.RationalParametrization([:x1, :x2, :x3], ZZRingElem[], 84*x^4 - 40*x^3 + x^2 + x, 336*x^3 - 120*x^2 + 2*x + 1, AbstractAlgebra.PolyRingElem[184*x^3 - 80*x^2 + 4*x + 1, 36*x^3 - 18*x^2 + 2*x])
243+
AlgebraicSolving.RationalParametrization([:x1, :x2, :x3], ZZRingElem[], 84*x^4 - 40*x^3 + x^2 + x, 336*x^3 - 120*x^2 + 2*x + 1, Nemo.QQPolyRingElem[184*x^3 - 80*x^2 + 4*x + 1, 36*x^3 - 18*x^2 + 2*x])
243244
```
244245
"""
245246
function rational_parametrization(

src/exports.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export polynomial_ring, MPolyRing, GFElem, MPolyRingElem,
44
QQMPolyRingElem, base_ring, coefficient_ring, evaluate,
55
prime_field, sig_groebner_basis, cyclic, leading_coefficient,
66
equidimensional_decomposition, homogenize, dimension, FqMPolyRingElem,
7-
hilbert_series, hilbert_dimension, hilbert_degree, hilbert_polynomial
7+
hilbert_series, hilbert_dimension, hilbert_degree, hilbert_polynomial,
8+
rational_curve_parametrization

0 commit comments

Comments
 (0)