863 lines
23 KiB
Python
863 lines
23 KiB
Python
|
"""Groebner bases algorithms. """
|
||
|
|
||
|
|
||
|
from sympy.core.symbol import Dummy
|
||
|
from sympy.polys.monomials import monomial_mul, monomial_lcm, monomial_divides, term_div
|
||
|
from sympy.polys.orderings import lex
|
||
|
from sympy.polys.polyerrors import DomainError
|
||
|
from sympy.polys.polyconfig import query
|
||
|
|
||
|
def groebner(seq, ring, method=None):
|
||
|
"""
|
||
|
Computes Groebner basis for a set of polynomials in `K[X]`.
|
||
|
|
||
|
Wrapper around the (default) improved Buchberger and the other algorithms
|
||
|
for computing Groebner bases. The choice of algorithm can be changed via
|
||
|
``method`` argument or :func:`sympy.polys.polyconfig.setup`, where
|
||
|
``method`` can be either ``buchberger`` or ``f5b``.
|
||
|
|
||
|
"""
|
||
|
if method is None:
|
||
|
method = query('groebner')
|
||
|
|
||
|
_groebner_methods = {
|
||
|
'buchberger': _buchberger,
|
||
|
'f5b': _f5b,
|
||
|
}
|
||
|
|
||
|
try:
|
||
|
_groebner = _groebner_methods[method]
|
||
|
except KeyError:
|
||
|
raise ValueError("'%s' is not a valid Groebner bases algorithm (valid are 'buchberger' and 'f5b')" % method)
|
||
|
|
||
|
domain, orig = ring.domain, None
|
||
|
|
||
|
if not domain.is_Field or not domain.has_assoc_Field:
|
||
|
try:
|
||
|
orig, ring = ring, ring.clone(domain=domain.get_field())
|
||
|
except DomainError:
|
||
|
raise DomainError("Cannot compute a Groebner basis over %s" % domain)
|
||
|
else:
|
||
|
seq = [ s.set_ring(ring) for s in seq ]
|
||
|
|
||
|
G = _groebner(seq, ring)
|
||
|
|
||
|
if orig is not None:
|
||
|
G = [ g.clear_denoms()[1].set_ring(orig) for g in G ]
|
||
|
|
||
|
return G
|
||
|
|
||
|
def _buchberger(f, ring):
|
||
|
"""
|
||
|
Computes Groebner basis for a set of polynomials in `K[X]`.
|
||
|
|
||
|
Given a set of multivariate polynomials `F`, finds another
|
||
|
set `G`, such that Ideal `F = Ideal G` and `G` is a reduced
|
||
|
Groebner basis.
|
||
|
|
||
|
The resulting basis is unique and has monic generators if the
|
||
|
ground domains is a field. Otherwise the result is non-unique
|
||
|
but Groebner bases over e.g. integers can be computed (if the
|
||
|
input polynomials are monic).
|
||
|
|
||
|
Groebner bases can be used to choose specific generators for a
|
||
|
polynomial ideal. Because these bases are unique you can check
|
||
|
for ideal equality by comparing the Groebner bases. To see if
|
||
|
one polynomial lies in an ideal, divide by the elements in the
|
||
|
base and see if the remainder vanishes.
|
||
|
|
||
|
They can also be used to solve systems of polynomial equations
|
||
|
as, by choosing lexicographic ordering, you can eliminate one
|
||
|
variable at a time, provided that the ideal is zero-dimensional
|
||
|
(finite number of solutions).
|
||
|
|
||
|
Notes
|
||
|
=====
|
||
|
|
||
|
Algorithm used: an improved version of Buchberger's algorithm
|
||
|
as presented in T. Becker, V. Weispfenning, Groebner Bases: A
|
||
|
Computational Approach to Commutative Algebra, Springer, 1993,
|
||
|
page 232.
|
||
|
|
||
|
References
|
||
|
==========
|
||
|
|
||
|
.. [1] [Bose03]_
|
||
|
.. [2] [Giovini91]_
|
||
|
.. [3] [Ajwa95]_
|
||
|
.. [4] [Cox97]_
|
||
|
|
||
|
"""
|
||
|
order = ring.order
|
||
|
|
||
|
monomial_mul = ring.monomial_mul
|
||
|
monomial_div = ring.monomial_div
|
||
|
monomial_lcm = ring.monomial_lcm
|
||
|
|
||
|
def select(P):
|
||
|
# normal selection strategy
|
||
|
# select the pair with minimum LCM(LM(f), LM(g))
|
||
|
pr = min(P, key=lambda pair: order(monomial_lcm(f[pair[0]].LM, f[pair[1]].LM)))
|
||
|
return pr
|
||
|
|
||
|
def normal(g, J):
|
||
|
h = g.rem([ f[j] for j in J ])
|
||
|
|
||
|
if not h:
|
||
|
return None
|
||
|
else:
|
||
|
h = h.monic()
|
||
|
|
||
|
if h not in I:
|
||
|
I[h] = len(f)
|
||
|
f.append(h)
|
||
|
|
||
|
return h.LM, I[h]
|
||
|
|
||
|
def update(G, B, ih):
|
||
|
# update G using the set of critical pairs B and h
|
||
|
# [BW] page 230
|
||
|
h = f[ih]
|
||
|
mh = h.LM
|
||
|
|
||
|
# filter new pairs (h, g), g in G
|
||
|
C = G.copy()
|
||
|
D = set()
|
||
|
|
||
|
while C:
|
||
|
# select a pair (h, g) by popping an element from C
|
||
|
ig = C.pop()
|
||
|
g = f[ig]
|
||
|
mg = g.LM
|
||
|
LCMhg = monomial_lcm(mh, mg)
|
||
|
|
||
|
def lcm_divides(ip):
|
||
|
# LCM(LM(h), LM(p)) divides LCM(LM(h), LM(g))
|
||
|
m = monomial_lcm(mh, f[ip].LM)
|
||
|
return monomial_div(LCMhg, m)
|
||
|
|
||
|
# HT(h) and HT(g) disjoint: mh*mg == LCMhg
|
||
|
if monomial_mul(mh, mg) == LCMhg or (
|
||
|
not any(lcm_divides(ipx) for ipx in C) and
|
||
|
not any(lcm_divides(pr[1]) for pr in D)):
|
||
|
D.add((ih, ig))
|
||
|
|
||
|
E = set()
|
||
|
|
||
|
while D:
|
||
|
# select h, g from D (h the same as above)
|
||
|
ih, ig = D.pop()
|
||
|
mg = f[ig].LM
|
||
|
LCMhg = monomial_lcm(mh, mg)
|
||
|
|
||
|
if not monomial_mul(mh, mg) == LCMhg:
|
||
|
E.add((ih, ig))
|
||
|
|
||
|
# filter old pairs
|
||
|
B_new = set()
|
||
|
|
||
|
while B:
|
||
|
# select g1, g2 from B (-> CP)
|
||
|
ig1, ig2 = B.pop()
|
||
|
mg1 = f[ig1].LM
|
||
|
mg2 = f[ig2].LM
|
||
|
LCM12 = monomial_lcm(mg1, mg2)
|
||
|
|
||
|
# if HT(h) does not divide lcm(HT(g1), HT(g2))
|
||
|
if not monomial_div(LCM12, mh) or \
|
||
|
monomial_lcm(mg1, mh) == LCM12 or \
|
||
|
monomial_lcm(mg2, mh) == LCM12:
|
||
|
B_new.add((ig1, ig2))
|
||
|
|
||
|
B_new |= E
|
||
|
|
||
|
# filter polynomials
|
||
|
G_new = set()
|
||
|
|
||
|
while G:
|
||
|
ig = G.pop()
|
||
|
mg = f[ig].LM
|
||
|
|
||
|
if not monomial_div(mg, mh):
|
||
|
G_new.add(ig)
|
||
|
|
||
|
G_new.add(ih)
|
||
|
|
||
|
return G_new, B_new
|
||
|
# end of update ################################
|
||
|
|
||
|
if not f:
|
||
|
return []
|
||
|
|
||
|
# replace f with a reduced list of initial polynomials; see [BW] page 203
|
||
|
f1 = f[:]
|
||
|
|
||
|
while True:
|
||
|
f = f1[:]
|
||
|
f1 = []
|
||
|
|
||
|
for i in range(len(f)):
|
||
|
p = f[i]
|
||
|
r = p.rem(f[:i])
|
||
|
|
||
|
if r:
|
||
|
f1.append(r.monic())
|
||
|
|
||
|
if f == f1:
|
||
|
break
|
||
|
|
||
|
I = {} # ip = I[p]; p = f[ip]
|
||
|
F = set() # set of indices of polynomials
|
||
|
G = set() # set of indices of intermediate would-be Groebner basis
|
||
|
CP = set() # set of pairs of indices of critical pairs
|
||
|
|
||
|
for i, h in enumerate(f):
|
||
|
I[h] = i
|
||
|
F.add(i)
|
||
|
|
||
|
#####################################
|
||
|
# algorithm GROEBNERNEWS2 in [BW] page 232
|
||
|
|
||
|
while F:
|
||
|
# select p with minimum monomial according to the monomial ordering
|
||
|
h = min([f[x] for x in F], key=lambda f: order(f.LM))
|
||
|
ih = I[h]
|
||
|
F.remove(ih)
|
||
|
G, CP = update(G, CP, ih)
|
||
|
|
||
|
# count the number of critical pairs which reduce to zero
|
||
|
reductions_to_zero = 0
|
||
|
|
||
|
while CP:
|
||
|
ig1, ig2 = select(CP)
|
||
|
CP.remove((ig1, ig2))
|
||
|
|
||
|
h = spoly(f[ig1], f[ig2], ring)
|
||
|
# ordering divisors is on average more efficient [Cox] page 111
|
||
|
G1 = sorted(G, key=lambda g: order(f[g].LM))
|
||
|
ht = normal(h, G1)
|
||
|
|
||
|
if ht:
|
||
|
G, CP = update(G, CP, ht[1])
|
||
|
else:
|
||
|
reductions_to_zero += 1
|
||
|
|
||
|
######################################
|
||
|
# now G is a Groebner basis; reduce it
|
||
|
Gr = set()
|
||
|
|
||
|
for ig in G:
|
||
|
ht = normal(f[ig], G - {ig})
|
||
|
|
||
|
if ht:
|
||
|
Gr.add(ht[1])
|
||
|
|
||
|
Gr = [f[ig] for ig in Gr]
|
||
|
|
||
|
# order according to the monomial ordering
|
||
|
Gr = sorted(Gr, key=lambda f: order(f.LM), reverse=True)
|
||
|
|
||
|
return Gr
|
||
|
|
||
|
def spoly(p1, p2, ring):
|
||
|
"""
|
||
|
Compute LCM(LM(p1), LM(p2))/LM(p1)*p1 - LCM(LM(p1), LM(p2))/LM(p2)*p2
|
||
|
This is the S-poly provided p1 and p2 are monic
|
||
|
"""
|
||
|
LM1 = p1.LM
|
||
|
LM2 = p2.LM
|
||
|
LCM12 = ring.monomial_lcm(LM1, LM2)
|
||
|
m1 = ring.monomial_div(LCM12, LM1)
|
||
|
m2 = ring.monomial_div(LCM12, LM2)
|
||
|
s1 = p1.mul_monom(m1)
|
||
|
s2 = p2.mul_monom(m2)
|
||
|
s = s1 - s2
|
||
|
return s
|
||
|
|
||
|
# F5B
|
||
|
|
||
|
# convenience functions
|
||
|
|
||
|
|
||
|
def Sign(f):
|
||
|
return f[0]
|
||
|
|
||
|
|
||
|
def Polyn(f):
|
||
|
return f[1]
|
||
|
|
||
|
|
||
|
def Num(f):
|
||
|
return f[2]
|
||
|
|
||
|
|
||
|
def sig(monomial, index):
|
||
|
return (monomial, index)
|
||
|
|
||
|
|
||
|
def lbp(signature, polynomial, number):
|
||
|
return (signature, polynomial, number)
|
||
|
|
||
|
# signature functions
|
||
|
|
||
|
|
||
|
def sig_cmp(u, v, order):
|
||
|
"""
|
||
|
Compare two signatures by extending the term order to K[X]^n.
|
||
|
|
||
|
u < v iff
|
||
|
- the index of v is greater than the index of u
|
||
|
or
|
||
|
- the index of v is equal to the index of u and u[0] < v[0] w.r.t. order
|
||
|
|
||
|
u > v otherwise
|
||
|
"""
|
||
|
if u[1] > v[1]:
|
||
|
return -1
|
||
|
if u[1] == v[1]:
|
||
|
#if u[0] == v[0]:
|
||
|
# return 0
|
||
|
if order(u[0]) < order(v[0]):
|
||
|
return -1
|
||
|
return 1
|
||
|
|
||
|
|
||
|
def sig_key(s, order):
|
||
|
"""
|
||
|
Key for comparing two signatures.
|
||
|
|
||
|
s = (m, k), t = (n, l)
|
||
|
|
||
|
s < t iff [k > l] or [k == l and m < n]
|
||
|
s > t otherwise
|
||
|
"""
|
||
|
return (-s[1], order(s[0]))
|
||
|
|
||
|
|
||
|
def sig_mult(s, m):
|
||
|
"""
|
||
|
Multiply a signature by a monomial.
|
||
|
|
||
|
The product of a signature (m, i) and a monomial n is defined as
|
||
|
(m * t, i).
|
||
|
"""
|
||
|
return sig(monomial_mul(s[0], m), s[1])
|
||
|
|
||
|
# labeled polynomial functions
|
||
|
|
||
|
|
||
|
def lbp_sub(f, g):
|
||
|
"""
|
||
|
Subtract labeled polynomial g from f.
|
||
|
|
||
|
The signature and number of the difference of f and g are signature
|
||
|
and number of the maximum of f and g, w.r.t. lbp_cmp.
|
||
|
"""
|
||
|
if sig_cmp(Sign(f), Sign(g), Polyn(f).ring.order) < 0:
|
||
|
max_poly = g
|
||
|
else:
|
||
|
max_poly = f
|
||
|
|
||
|
ret = Polyn(f) - Polyn(g)
|
||
|
|
||
|
return lbp(Sign(max_poly), ret, Num(max_poly))
|
||
|
|
||
|
|
||
|
def lbp_mul_term(f, cx):
|
||
|
"""
|
||
|
Multiply a labeled polynomial with a term.
|
||
|
|
||
|
The product of a labeled polynomial (s, p, k) by a monomial is
|
||
|
defined as (m * s, m * p, k).
|
||
|
"""
|
||
|
return lbp(sig_mult(Sign(f), cx[0]), Polyn(f).mul_term(cx), Num(f))
|
||
|
|
||
|
|
||
|
def lbp_cmp(f, g):
|
||
|
"""
|
||
|
Compare two labeled polynomials.
|
||
|
|
||
|
f < g iff
|
||
|
- Sign(f) < Sign(g)
|
||
|
or
|
||
|
- Sign(f) == Sign(g) and Num(f) > Num(g)
|
||
|
|
||
|
f > g otherwise
|
||
|
"""
|
||
|
if sig_cmp(Sign(f), Sign(g), Polyn(f).ring.order) == -1:
|
||
|
return -1
|
||
|
if Sign(f) == Sign(g):
|
||
|
if Num(f) > Num(g):
|
||
|
return -1
|
||
|
#if Num(f) == Num(g):
|
||
|
# return 0
|
||
|
return 1
|
||
|
|
||
|
|
||
|
def lbp_key(f):
|
||
|
"""
|
||
|
Key for comparing two labeled polynomials.
|
||
|
"""
|
||
|
return (sig_key(Sign(f), Polyn(f).ring.order), -Num(f))
|
||
|
|
||
|
# algorithm and helper functions
|
||
|
|
||
|
|
||
|
def critical_pair(f, g, ring):
|
||
|
"""
|
||
|
Compute the critical pair corresponding to two labeled polynomials.
|
||
|
|
||
|
A critical pair is a tuple (um, f, vm, g), where um and vm are
|
||
|
terms such that um * f - vm * g is the S-polynomial of f and g (so,
|
||
|
wlog assume um * f > vm * g).
|
||
|
For performance sake, a critical pair is represented as a tuple
|
||
|
(Sign(um * f), um, f, Sign(vm * g), vm, g), since um * f creates
|
||
|
a new, relatively expensive object in memory, whereas Sign(um *
|
||
|
f) and um are lightweight and f (in the tuple) is a reference to
|
||
|
an already existing object in memory.
|
||
|
"""
|
||
|
domain = ring.domain
|
||
|
|
||
|
ltf = Polyn(f).LT
|
||
|
ltg = Polyn(g).LT
|
||
|
lt = (monomial_lcm(ltf[0], ltg[0]), domain.one)
|
||
|
|
||
|
um = term_div(lt, ltf, domain)
|
||
|
vm = term_div(lt, ltg, domain)
|
||
|
|
||
|
# The full information is not needed (now), so only the product
|
||
|
# with the leading term is considered:
|
||
|
fr = lbp_mul_term(lbp(Sign(f), Polyn(f).leading_term(), Num(f)), um)
|
||
|
gr = lbp_mul_term(lbp(Sign(g), Polyn(g).leading_term(), Num(g)), vm)
|
||
|
|
||
|
# return in proper order, such that the S-polynomial is just
|
||
|
# u_first * f_first - u_second * f_second:
|
||
|
if lbp_cmp(fr, gr) == -1:
|
||
|
return (Sign(gr), vm, g, Sign(fr), um, f)
|
||
|
else:
|
||
|
return (Sign(fr), um, f, Sign(gr), vm, g)
|
||
|
|
||
|
|
||
|
def cp_cmp(c, d):
|
||
|
"""
|
||
|
Compare two critical pairs c and d.
|
||
|
|
||
|
c < d iff
|
||
|
- lbp(c[0], _, Num(c[2]) < lbp(d[0], _, Num(d[2])) (this
|
||
|
corresponds to um_c * f_c and um_d * f_d)
|
||
|
or
|
||
|
- lbp(c[0], _, Num(c[2]) >< lbp(d[0], _, Num(d[2])) and
|
||
|
lbp(c[3], _, Num(c[5])) < lbp(d[3], _, Num(d[5])) (this
|
||
|
corresponds to vm_c * g_c and vm_d * g_d)
|
||
|
|
||
|
c > d otherwise
|
||
|
"""
|
||
|
zero = Polyn(c[2]).ring.zero
|
||
|
|
||
|
c0 = lbp(c[0], zero, Num(c[2]))
|
||
|
d0 = lbp(d[0], zero, Num(d[2]))
|
||
|
|
||
|
r = lbp_cmp(c0, d0)
|
||
|
|
||
|
if r == -1:
|
||
|
return -1
|
||
|
if r == 0:
|
||
|
c1 = lbp(c[3], zero, Num(c[5]))
|
||
|
d1 = lbp(d[3], zero, Num(d[5]))
|
||
|
|
||
|
r = lbp_cmp(c1, d1)
|
||
|
|
||
|
if r == -1:
|
||
|
return -1
|
||
|
#if r == 0:
|
||
|
# return 0
|
||
|
return 1
|
||
|
|
||
|
|
||
|
def cp_key(c, ring):
|
||
|
"""
|
||
|
Key for comparing critical pairs.
|
||
|
"""
|
||
|
return (lbp_key(lbp(c[0], ring.zero, Num(c[2]))), lbp_key(lbp(c[3], ring.zero, Num(c[5]))))
|
||
|
|
||
|
|
||
|
def s_poly(cp):
|
||
|
"""
|
||
|
Compute the S-polynomial of a critical pair.
|
||
|
|
||
|
The S-polynomial of a critical pair cp is cp[1] * cp[2] - cp[4] * cp[5].
|
||
|
"""
|
||
|
return lbp_sub(lbp_mul_term(cp[2], cp[1]), lbp_mul_term(cp[5], cp[4]))
|
||
|
|
||
|
|
||
|
def is_rewritable_or_comparable(sign, num, B):
|
||
|
"""
|
||
|
Check if a labeled polynomial is redundant by checking if its
|
||
|
signature and number imply rewritability or comparability.
|
||
|
|
||
|
(sign, num) is comparable if there exists a labeled polynomial
|
||
|
h in B, such that sign[1] (the index) is less than Sign(h)[1]
|
||
|
and sign[0] is divisible by the leading monomial of h.
|
||
|
|
||
|
(sign, num) is rewritable if there exists a labeled polynomial
|
||
|
h in B, such thatsign[1] is equal to Sign(h)[1], num < Num(h)
|
||
|
and sign[0] is divisible by Sign(h)[0].
|
||
|
"""
|
||
|
for h in B:
|
||
|
# comparable
|
||
|
if sign[1] < Sign(h)[1]:
|
||
|
if monomial_divides(Polyn(h).LM, sign[0]):
|
||
|
return True
|
||
|
|
||
|
# rewritable
|
||
|
if sign[1] == Sign(h)[1]:
|
||
|
if num < Num(h):
|
||
|
if monomial_divides(Sign(h)[0], sign[0]):
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
|
||
|
def f5_reduce(f, B):
|
||
|
"""
|
||
|
F5-reduce a labeled polynomial f by B.
|
||
|
|
||
|
Continuously searches for non-zero labeled polynomial h in B, such
|
||
|
that the leading term lt_h of h divides the leading term lt_f of
|
||
|
f and Sign(lt_h * h) < Sign(f). If such a labeled polynomial h is
|
||
|
found, f gets replaced by f - lt_f / lt_h * h. If no such h can be
|
||
|
found or f is 0, f is no further F5-reducible and f gets returned.
|
||
|
|
||
|
A polynomial that is reducible in the usual sense need not be
|
||
|
F5-reducible, e.g.:
|
||
|
|
||
|
>>> from sympy.polys.groebnertools import lbp, sig, f5_reduce, Polyn
|
||
|
>>> from sympy.polys import ring, QQ, lex
|
||
|
|
||
|
>>> R, x,y,z = ring("x,y,z", QQ, lex)
|
||
|
|
||
|
>>> f = lbp(sig((1, 1, 1), 4), x, 3)
|
||
|
>>> g = lbp(sig((0, 0, 0), 2), x, 2)
|
||
|
|
||
|
>>> Polyn(f).rem([Polyn(g)])
|
||
|
0
|
||
|
>>> f5_reduce(f, [g])
|
||
|
(((1, 1, 1), 4), x, 3)
|
||
|
|
||
|
"""
|
||
|
order = Polyn(f).ring.order
|
||
|
domain = Polyn(f).ring.domain
|
||
|
|
||
|
if not Polyn(f):
|
||
|
return f
|
||
|
|
||
|
while True:
|
||
|
g = f
|
||
|
|
||
|
for h in B:
|
||
|
if Polyn(h):
|
||
|
if monomial_divides(Polyn(h).LM, Polyn(f).LM):
|
||
|
t = term_div(Polyn(f).LT, Polyn(h).LT, domain)
|
||
|
if sig_cmp(sig_mult(Sign(h), t[0]), Sign(f), order) < 0:
|
||
|
# The following check need not be done and is in general slower than without.
|
||
|
#if not is_rewritable_or_comparable(Sign(gp), Num(gp), B):
|
||
|
hp = lbp_mul_term(h, t)
|
||
|
f = lbp_sub(f, hp)
|
||
|
break
|
||
|
|
||
|
if g == f or not Polyn(f):
|
||
|
return f
|
||
|
|
||
|
|
||
|
def _f5b(F, ring):
|
||
|
"""
|
||
|
Computes a reduced Groebner basis for the ideal generated by F.
|
||
|
|
||
|
f5b is an implementation of the F5B algorithm by Yao Sun and
|
||
|
Dingkang Wang. Similarly to Buchberger's algorithm, the algorithm
|
||
|
proceeds by computing critical pairs, computing the S-polynomial,
|
||
|
reducing it and adjoining the reduced S-polynomial if it is not 0.
|
||
|
|
||
|
Unlike Buchberger's algorithm, each polynomial contains additional
|
||
|
information, namely a signature and a number. The signature
|
||
|
specifies the path of computation (i.e. from which polynomial in
|
||
|
the original basis was it derived and how), the number says when
|
||
|
the polynomial was added to the basis. With this information it
|
||
|
is (often) possible to decide if an S-polynomial will reduce to
|
||
|
0 and can be discarded.
|
||
|
|
||
|
Optimizations include: Reducing the generators before computing
|
||
|
a Groebner basis, removing redundant critical pairs when a new
|
||
|
polynomial enters the basis and sorting the critical pairs and
|
||
|
the current basis.
|
||
|
|
||
|
Once a Groebner basis has been found, it gets reduced.
|
||
|
|
||
|
References
|
||
|
==========
|
||
|
|
||
|
.. [1] Yao Sun, Dingkang Wang: "A New Proof for the Correctness of F5
|
||
|
(F5-Like) Algorithm", https://arxiv.org/abs/1004.0084 (specifically
|
||
|
v4)
|
||
|
|
||
|
.. [2] Thomas Becker, Volker Weispfenning, Groebner bases: A computational
|
||
|
approach to commutative algebra, 1993, p. 203, 216
|
||
|
"""
|
||
|
order = ring.order
|
||
|
|
||
|
# reduce polynomials (like in Mario Pernici's implementation) (Becker, Weispfenning, p. 203)
|
||
|
B = F
|
||
|
while True:
|
||
|
F = B
|
||
|
B = []
|
||
|
|
||
|
for i in range(len(F)):
|
||
|
p = F[i]
|
||
|
r = p.rem(F[:i])
|
||
|
|
||
|
if r:
|
||
|
B.append(r)
|
||
|
|
||
|
if F == B:
|
||
|
break
|
||
|
|
||
|
# basis
|
||
|
B = [lbp(sig(ring.zero_monom, i + 1), F[i], i + 1) for i in range(len(F))]
|
||
|
B.sort(key=lambda f: order(Polyn(f).LM), reverse=True)
|
||
|
|
||
|
# critical pairs
|
||
|
CP = [critical_pair(B[i], B[j], ring) for i in range(len(B)) for j in range(i + 1, len(B))]
|
||
|
CP.sort(key=lambda cp: cp_key(cp, ring), reverse=True)
|
||
|
|
||
|
k = len(B)
|
||
|
|
||
|
reductions_to_zero = 0
|
||
|
|
||
|
while len(CP):
|
||
|
cp = CP.pop()
|
||
|
|
||
|
# discard redundant critical pairs:
|
||
|
if is_rewritable_or_comparable(cp[0], Num(cp[2]), B):
|
||
|
continue
|
||
|
if is_rewritable_or_comparable(cp[3], Num(cp[5]), B):
|
||
|
continue
|
||
|
|
||
|
s = s_poly(cp)
|
||
|
|
||
|
p = f5_reduce(s, B)
|
||
|
|
||
|
p = lbp(Sign(p), Polyn(p).monic(), k + 1)
|
||
|
|
||
|
if Polyn(p):
|
||
|
# remove old critical pairs, that become redundant when adding p:
|
||
|
indices = []
|
||
|
for i, cp in enumerate(CP):
|
||
|
if is_rewritable_or_comparable(cp[0], Num(cp[2]), [p]):
|
||
|
indices.append(i)
|
||
|
elif is_rewritable_or_comparable(cp[3], Num(cp[5]), [p]):
|
||
|
indices.append(i)
|
||
|
|
||
|
for i in reversed(indices):
|
||
|
del CP[i]
|
||
|
|
||
|
# only add new critical pairs that are not made redundant by p:
|
||
|
for g in B:
|
||
|
if Polyn(g):
|
||
|
cp = critical_pair(p, g, ring)
|
||
|
if is_rewritable_or_comparable(cp[0], Num(cp[2]), [p]):
|
||
|
continue
|
||
|
elif is_rewritable_or_comparable(cp[3], Num(cp[5]), [p]):
|
||
|
continue
|
||
|
|
||
|
CP.append(cp)
|
||
|
|
||
|
# sort (other sorting methods/selection strategies were not as successful)
|
||
|
CP.sort(key=lambda cp: cp_key(cp, ring), reverse=True)
|
||
|
|
||
|
# insert p into B:
|
||
|
m = Polyn(p).LM
|
||
|
if order(m) <= order(Polyn(B[-1]).LM):
|
||
|
B.append(p)
|
||
|
else:
|
||
|
for i, q in enumerate(B):
|
||
|
if order(m) > order(Polyn(q).LM):
|
||
|
B.insert(i, p)
|
||
|
break
|
||
|
|
||
|
k += 1
|
||
|
|
||
|
#print(len(B), len(CP), "%d critical pairs removed" % len(indices))
|
||
|
else:
|
||
|
reductions_to_zero += 1
|
||
|
|
||
|
# reduce Groebner basis:
|
||
|
H = [Polyn(g).monic() for g in B]
|
||
|
H = red_groebner(H, ring)
|
||
|
|
||
|
return sorted(H, key=lambda f: order(f.LM), reverse=True)
|
||
|
|
||
|
|
||
|
def red_groebner(G, ring):
|
||
|
"""
|
||
|
Compute reduced Groebner basis, from BeckerWeispfenning93, p. 216
|
||
|
|
||
|
Selects a subset of generators, that already generate the ideal
|
||
|
and computes a reduced Groebner basis for them.
|
||
|
"""
|
||
|
def reduction(P):
|
||
|
"""
|
||
|
The actual reduction algorithm.
|
||
|
"""
|
||
|
Q = []
|
||
|
for i, p in enumerate(P):
|
||
|
h = p.rem(P[:i] + P[i + 1:])
|
||
|
if h:
|
||
|
Q.append(h)
|
||
|
|
||
|
return [p.monic() for p in Q]
|
||
|
|
||
|
F = G
|
||
|
H = []
|
||
|
|
||
|
while F:
|
||
|
f0 = F.pop()
|
||
|
|
||
|
if not any(monomial_divides(f.LM, f0.LM) for f in F + H):
|
||
|
H.append(f0)
|
||
|
|
||
|
# Becker, Weispfenning, p. 217: H is Groebner basis of the ideal generated by G.
|
||
|
return reduction(H)
|
||
|
|
||
|
|
||
|
def is_groebner(G, ring):
|
||
|
"""
|
||
|
Check if G is a Groebner basis.
|
||
|
"""
|
||
|
for i in range(len(G)):
|
||
|
for j in range(i + 1, len(G)):
|
||
|
s = spoly(G[i], G[j], ring)
|
||
|
s = s.rem(G)
|
||
|
if s:
|
||
|
return False
|
||
|
|
||
|
return True
|
||
|
|
||
|
|
||
|
def is_minimal(G, ring):
|
||
|
"""
|
||
|
Checks if G is a minimal Groebner basis.
|
||
|
"""
|
||
|
order = ring.order
|
||
|
domain = ring.domain
|
||
|
|
||
|
G.sort(key=lambda g: order(g.LM))
|
||
|
|
||
|
for i, g in enumerate(G):
|
||
|
if g.LC != domain.one:
|
||
|
return False
|
||
|
|
||
|
for h in G[:i] + G[i + 1:]:
|
||
|
if monomial_divides(h.LM, g.LM):
|
||
|
return False
|
||
|
|
||
|
return True
|
||
|
|
||
|
|
||
|
def is_reduced(G, ring):
|
||
|
"""
|
||
|
Checks if G is a reduced Groebner basis.
|
||
|
"""
|
||
|
order = ring.order
|
||
|
domain = ring.domain
|
||
|
|
||
|
G.sort(key=lambda g: order(g.LM))
|
||
|
|
||
|
for i, g in enumerate(G):
|
||
|
if g.LC != domain.one:
|
||
|
return False
|
||
|
|
||
|
for term in g.terms():
|
||
|
for h in G[:i] + G[i + 1:]:
|
||
|
if monomial_divides(h.LM, term[0]):
|
||
|
return False
|
||
|
|
||
|
return True
|
||
|
|
||
|
def groebner_lcm(f, g):
|
||
|
"""
|
||
|
Computes LCM of two polynomials using Groebner bases.
|
||
|
|
||
|
The LCM is computed as the unique generator of the intersection
|
||
|
of the two ideals generated by `f` and `g`. The approach is to
|
||
|
compute a Groebner basis with respect to lexicographic ordering
|
||
|
of `t*f` and `(1 - t)*g`, where `t` is an unrelated variable and
|
||
|
then filtering out the solution that does not contain `t`.
|
||
|
|
||
|
References
|
||
|
==========
|
||
|
|
||
|
.. [1] [Cox97]_
|
||
|
|
||
|
"""
|
||
|
if f.ring != g.ring:
|
||
|
raise ValueError("Values should be equal")
|
||
|
|
||
|
ring = f.ring
|
||
|
domain = ring.domain
|
||
|
|
||
|
if not f or not g:
|
||
|
return ring.zero
|
||
|
|
||
|
if len(f) <= 1 and len(g) <= 1:
|
||
|
monom = monomial_lcm(f.LM, g.LM)
|
||
|
coeff = domain.lcm(f.LC, g.LC)
|
||
|
return ring.term_new(monom, coeff)
|
||
|
|
||
|
fc, f = f.primitive()
|
||
|
gc, g = g.primitive()
|
||
|
|
||
|
lcm = domain.lcm(fc, gc)
|
||
|
|
||
|
f_terms = [ ((1,) + monom, coeff) for monom, coeff in f.terms() ]
|
||
|
g_terms = [ ((0,) + monom, coeff) for monom, coeff in g.terms() ] \
|
||
|
+ [ ((1,) + monom,-coeff) for monom, coeff in g.terms() ]
|
||
|
|
||
|
t = Dummy("t")
|
||
|
t_ring = ring.clone(symbols=(t,) + ring.symbols, order=lex)
|
||
|
|
||
|
F = t_ring.from_terms(f_terms)
|
||
|
G = t_ring.from_terms(g_terms)
|
||
|
|
||
|
basis = groebner([F, G], t_ring)
|
||
|
|
||
|
def is_independent(h, j):
|
||
|
return not any(monom[j] for monom in h.monoms())
|
||
|
|
||
|
H = [ h for h in basis if is_independent(h, 0) ]
|
||
|
|
||
|
h_terms = [ (monom[1:], coeff*lcm) for monom, coeff in H[0].terms() ]
|
||
|
h = ring.from_terms(h_terms)
|
||
|
|
||
|
return h
|
||
|
|
||
|
def groebner_gcd(f, g):
|
||
|
"""Computes GCD of two polynomials using Groebner bases. """
|
||
|
if f.ring != g.ring:
|
||
|
raise ValueError("Values should be equal")
|
||
|
domain = f.ring.domain
|
||
|
|
||
|
if not domain.is_Field:
|
||
|
fc, f = f.primitive()
|
||
|
gc, g = g.primitive()
|
||
|
gcd = domain.gcd(fc, gc)
|
||
|
|
||
|
H = (f*g).quo([groebner_lcm(f, g)])
|
||
|
|
||
|
if len(H) != 1:
|
||
|
raise ValueError("Length should be 1")
|
||
|
h = H[0]
|
||
|
|
||
|
if not domain.is_Field:
|
||
|
return gcd*h
|
||
|
else:
|
||
|
return h.monic()
|