11 Algorithms for dealing with Gröbner bases. Everything is from Cox,
12 Little, O'Shea "Ideals, Varieties, and Algorithms" (4th ed.), which
13 is optimized for being readable, not for speed.
17 const groebner_basis_of : (F : polynomial#[:] -> polynomial#[:])
19 const positive_monomial_division : (a : monomial#, b : monomial# -> std.option(monomial))
21 impl lcm_struct polynomial#
25 As of this moment, I don't want to encourage using other monomial
26 orderings. That's strictly a hack for lcm calculation via
29 Also, this really wouldn't work with negative exponents.
31 const lex_order = {a : monomial, b : monomial
32 var ak = a.vars.len - 1
33 var bk = b.vars.len - 1
35 while ak >= 0 && bk >= 0
36 match strnumcmp(a.vars[ak].variable, b.vars[bk].variable)
37 | `std.Before: -> `std.Before
38 | `std.After : -> `std.After
40 match t.cmp(a.vars[ak].power, b.vars[bk].power)
41 | `std.Before: -> `std.Before
42 | `std.After : -> `std.After
62 impl lcm_struct polynomial# =
65 Early case. If f or g are just constants, return the other, or maybe 1
67 var f_trivial = f.terms.len == 1 && f.terms[0].vars.len == 0
68 var g_trivial = g.terms.len == 1 && g.terms[0].vars.len == 0
69 if f_trivial && g_trivial
82 Slightly more involved case. Evaluate f and g with a
83 bunch of random constants, then see if they have an
84 lcm. If not, then just return f*g.
86 if no_chance_of_lcm(f, g)
94 Build t and (1-t) to multiply f and g. In particular,
95 we need t to be large enough that the standard
96 grevlex ordering we use has the same effect as lex
97 ordering: any monomial containing t is larger than
98 any monomial not containing t.
100 var fake_var : byte[:] = make_var_larger_than(f, g)
101 var vp_t : var_power = [
102 .variable = fake_var,
103 .power = rid(), // gadd(auto deg(f), auto deg(g)),
105 var mon_t : monomial = [
107 .vars = std.sldup([ vp_t ][:])
109 var mon_one : monomial = [
114 mul_poly_mon_ip(ft, &mon_t)
117 var poly_1mt : polynomial = [
118 .terms = [ mon_one, mon_t ][:]
122 rmul_ip(g1mt, &poly_1mt)
124 auto mon_t // This kills fake_var as well
127 Force lex ordering. This will percolate all the way
128 through the Gröbner basis calculation, since we do it
131 ft.monomial_cmp = `std.Some lex_order
132 g1mt.monomial_cmp = `std.Some lex_order
136 /* Now, compute a Gröbner basis for ⟨ ft, g(1-t) ⟩. */
137 var groebner_basis = groebner_basis_of( [ ft, g1mt ][:] )
140 The promise is that, if we remove all the elements of
141 the groebner basis that contain fake_var, we'll be
142 left with at most one element, and it will be the
145 for var j = 0; j < groebner_basis.len; ++j
146 if contains_var(groebner_basis[j], fake_var)
147 __dispose__(groebner_basis[j])
148 std.sldel(&groebner_basis, j)
152 if groebner_basis.len == 1
153 var ret = groebner_basis[0]
154 std.slfree(groebner_basis)
155 ret.monomial_cmp = `std.None
159 elif groebner_basis.len == 0
160 std.slfree(groebner_basis)
166 /* This should be impossible */
167 var ret = groebner_basis[0]
168 for var j = 1; j < groebner_basis.len; ++j
169 __dispose__(groebner_basis[j])
171 std.slfree(groebner_basis)
172 ret.monomial_cmp = `std.None
179 const no_chance_of_lcm = {f, g
180 /* If f or g aren't entirely positive, the following fails */
181 for var j = 0; j < f.terms.len; ++j
182 if f.terms[j].coeff.p.sign < 0
186 for var j = 0; j < g.terms.len; ++j
187 if g.terms[j].coeff.p.sign < 0
193 Now, assuming f and g are totally positive, suppose they factor into
198 All pi and qi are also positive, so evaluated on numbers
199 (strictly greater than 1), they'll return results >= 2.
200 Therefore, if f and g share a common factor pi = qj,
201 evaluating f and g will result in numbers with a common
204 So, if we evaluate f and g on some random numbers and they
205 don't share any common factors numerically, they can't share
208 The number 5 was chosen after about three minutes of testing
209 with one specific program. It should probably be calculated
210 based on number of terms and variables.
212 for var j = 0; j < 5; ++j
213 var rng : std.rng# = std.mksrng(j)
214 var int_from_var : std.htab(byte[:], int)# = std.mkht()
216 var fq : yakmo.Q# = eval_polynomial_by_seed(f, rng, int_from_var)
217 var gq : yakmo.Q# = eval_polynomial_by_seed(g, rng, int_from_var)
222 var fzb : std.bigint# = std.bigdup(fq.p)
223 std.bigmul(fzb, gq.q)
224 var fz : yakmo.Z# = (fzb : yakmo.Z#)
225 var gzb : std.bigint# = std.bigdup(gq.p)
226 std.bigmul(gzb, fq.q)
227 var gz : yakmo.Z# = (gzb : yakmo.Z#)
232 for (k, _) : std.byhtkeyvals(int_from_var)
236 std.htfree(int_from_var)
238 var fgz = yakmo.rmul(fz, gz)
239 var fglcm = yakmo.lcm(fz, gz)
243 if std.eq(fgz, fglcm)
252 TODO: all of this needs to be subsumed in some sort of "evaluable"
253 trait. But traits are borked right now.
255 const eval_polynomial_by_seed = { f : yakmo.polynomial#, rng : std.rng#, vars : std.htab(byte[:], int)#
256 var ret : yakmo.Q# = yakmo.gid()
258 for var j = 0; j < f.terms.len; ++j
259 var temp : yakmo.Q# = eval_monomial_by_seed(f.terms[j], rng, vars)
261 yakmo.gadd_ip(ret, temp)
267 const eval_monomial_by_seed = { m : yakmo.monomial, rng : std.rng#, vars : std.htab(byte[:], int)#
268 var ret : yakmo.Q# = t.dup(m.coeff)
270 for var j = 0; j < m.vars.len; ++j
271 var temp : yakmo.Q# = eval_var_power_by_seed(m.vars[j], rng, vars)
273 yakmo.rmul_ip(ret, temp)
279 const primes : int[:] = [
280 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59,
281 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127,
282 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193,
283 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269,
284 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349,
285 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431,
286 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499
289 const eval_var_power_by_seed = {vp : yakmo.var_power, rng : std.rng#, vars : std.htab(byte[:], int)#
290 var var_name : byte[:] = vp.variable
291 var var_value : int = 0
292 var zret : std.bigint# = std.mkbigint(1)
294 match std.htget(vars, var_name)
296 var_value = primes[std.rngrand(rng, 0, primes.len)]
297 std.htput(vars, std.sldup(var_name), var_value)
298 | `std.Some z: var_value = z
301 var np : std.bigint# = std.bigdup((vp.power : std.bigint#))
302 var double = std.mkbigint(var_value)
303 while !std.bigiszero(np)
304 if !std.bigiseven(np)
305 std.bigmul(zret, double)
308 std.bigmul(double, double)
314 var qret : yakmo.Q = [ .p = zret, .q = std.mkbigint(1) ]
319 const make_var_larger_than = {a, b
322 for var j = 0; j < a.terms.len; ++j
323 for var k = 0; k < a.terms[j].vars.len; ++k
325 (ct, st) = std.charstep(a.terms[j].vars[k].variable)
326 c = std.max(c, std.max(ct, ct + 1))
329 (ct, st) = std.charstep(st)
334 for var j = 0; j < b.terms.len; ++j
335 for var k = 0; k < b.terms[j].vars.len; ++k
337 (ct, st) = std.charstep(b.terms[j].vars[k].variable)
338 c = std.max(c, std.max(ct, ct + 1))
341 (ct, st) = std.charstep(st)
346 var sb : std.strbuf# = std.mksb()
356 Return whether p contains a variable with given name, given the
357 assumption that such a variable is the largest variable occuring in
358 p, and that it only occurs with powers large enough to place it in
361 const contains_var = {p : polynomial#, name : byte[:]
366 var mon : yakmo.monomial# = &p.terms[p.terms.len - 1]
371 -> std.eq(mon.vars[mon.vars.len - 1].variable, name)
375 Buchberger's Algorithm, specifically as described in [CLO] Chapter 2,
376 Section 9, Theorem 9, with the incorporated reduction definition.
378 const groebner_basis_of = {F : polynomial#[:]
379 var G : polynomial#[:] = t.dup(F)
382 Initial reduction step. Kill every monomial of any polynomial
383 that appears as the leading term of any other polynomial.
386 reduce_ideal(&G, false)
389 /* Now run Buchberger's */
390 var test_these : (std.size, std.size)[:] = [][:]
391 for var j = 0; j < G.len; ++j
392 for var k = 0; k < j; ++k
393 std.slpush(&test_these, (j, k))
397 while test_these.len != 0
399 (j, k) = std.slpop(&test_these)
400 if !common_factors_in_LT(G[j], G[k])
404 if proposition_8_criterion(G, test_these, j, k)
408 var r = S_bar(G, G[j], G[k])
413 reduce_ideal(&G, true)
414 for var i = 0; i < G.len - 1; ++i
415 std.slpush(&test_these, (i, G.len - 1))
419 std.slfree(test_these)
422 We now have a Gröbner basis. Make it slightly prettier by
423 making all entries monic.
425 for var j = 0; j < G.len; ++j
431 /* Now, remove redundant entries */
433 TODO: I'm pretty sure this is handled by the reduction step
434 during the algorithm, but I'm too tired to check.
436 minimize_groebner_basis(&G)
441 const ensure_monic = {p : polynomial#
443 /* How did this get in here? */
447 if eqQ(p.terms[p.terms.len - 1].coeff, 1, 1)
451 match finv(p.terms[p.terms.len - 1].coeff)
454 for var k = 0; k < p.terms.len; ++k
455 rmul_ip(p.terms[k].coeff, ci)
461 const reduce_ideal = {ps : polynomial#[:]#, only_check_end : bool
463 Start at the end, because in the process of Buchberger's,
464 that's where the new, interesting polynomials get slotted.
466 var test_these = [][:]
468 std.slpush(&test_these, ps#.len - 1)
470 for var k = 0; k < ps#.len - 1; ++k
471 std.slpush(&test_these, k)
474 while test_these.len != 0
475 var j = std.slpop(&test_these)
483 /* Apply everything to p */
484 for var k = 0; k < ps#.len; ++k
494 var ltq = &q.terms[q.terms.len - 1]
497 We want to loop down through p, from the top.
498 If we ever change p, then we might add/remove
499 terms and change the indexing. But 1) we don't
500 want to check the top terms over and over,
501 and 2) as soon as we skip through a term, we
502 know it will never be affected by any future
506 for var l = p.terms.len - 1; l >= 0; --l
507 match positive_monomial_division(&p.terms[l], ltq)
508 | `std.None: good_terms++
511 var summand = t.dup(q)
512 mul_poly_mon_ip(summand, &r)
516 l = p.terms.len - 1 // good_terms
526 /* Now apply p to everything else */
527 var ltp : monomial# = &p.terms[p.terms.len - 1]
528 for var k = 0; k < ps#.len; ++k
540 for var l = q.terms.len - 1; l >= 0; --l
541 match positive_monomial_division(&q.terms[l], ltp)
545 var summand = t.dup(p)
546 mul_poly_mon_ip(summand, &r)
550 l = q.terms.len - 1 // good_terms
551 ensure_contains(&test_these, k)
557 std.slfree(test_these)
560 const ensure_contains = {s, e
563 This can only happen if we're in the second loop for
564 a particular q, and in that case, the index for q has
565 already been added to the list.
569 for var k = 0; k < s#.len; ++k
577 /* Weed out zeroes */
578 const remove_trivial = {ps : polynomial#[:]#
579 for var j = 0; j < ps#.len; ++j
580 if ps#[j].terms.len == 0
588 /* True if LCM(LT(f), LT(g)) != LT(f) LT(g) */
589 const common_factors_in_LT = {f : polynomial#, g : polynomial#
590 if f.terms.len == 0 || g.terms.len == 0
593 var ltf = &f.terms[f.terms.len - 1]
594 var ltg = &f.terms[f.terms.len - 1]
596 for var j = 0; j < ltf.vars.len; ++j
597 for var k = 0; k < ltg.vars.len; ++k
598 match strnumcmp(ltg.vars[k].variable, ltf.vars[j].variable)
600 | `std.Equal: -> true
610 True if there is some l such that
614 - both (j, l) and (k, l) are not in B (up to ordering)
616 - LT(f_l) divides lcm(LT(f_j), LT(f_k))
618 const proposition_8_criterion = {G : polynomial#[:], B : (std.size, std.size)[:], j : std.size, k : std.size
621 if fj.terms.len == 0 || fk.terms.len == 0
625 var ltj = &fj.terms[fj.terms.len - 1]
626 var ltk = &fk.terms[fk.terms.len - 1]
628 for var l = 0; l < G.len; ++l
633 var already_in_b = false
634 var jl1 = std.min(j, l)
635 var jl2 = std.max(j, l)
636 var kl1 = std.min(k, l)
637 var kl2 = std.max(k, l)
639 if (s == jl1 && t == jl2) || (s == kl1 && t == kl2)
654 var ltl = &fl.terms[fl.terms.len - 1]
655 var this_l_works = true
658 var this_var_in_lcm = false
660 match strnumcmp(vpj.variable, vpl.variable)
663 match t.cmp(vpj.power, vpl.power)
666 this_var_in_lcm = true
678 match strnumcmp(vpk.variable, vpl.variable)
681 match t.cmp(vpk.power, vpl.power)
684 this_var_in_lcm = true
706 Compute \overbar{S(f, g)}^{F}; the remainder of S(f, g) when reducing
709 TODO: maybe break this out into a general polynomial div_maybe or div
710 or reduce or something?
712 const S_bar = {F, f, g
713 var Sfg : polynomial# = S_polynomial(f, g)
718 for var j = 0; j < F.len; ++j
719 if F[j].terms.len == 0
723 if Sfg.terms.len == 0
727 match positive_monomial_division(&Sfg.terms[Sfg.terms.len - 1], &F[j].terms[F[j].terms.len - 1])
731 var summand = t.dup(F[j])
732 mul_poly_mon_ip(summand, &q)
735 gadd_ip(Sfg, summand)
745 Let X be the lcm of the leading terms of f and g (ignoring coefficients). Then
747 S(f, g) := X/LT(f) * f - X/LT(g) * g
749 const S_polynomial = {f : polynomial#, g : polynomial#
750 var ltf : monomial# = &f.terms[f.terms.len - 1]
751 var ltg : monomial# = &g.terms[g.terms.len - 1]
752 var f_factor : monomial = [ .coeff = rid(), .vars = [][:] ]
753 var g_factor : monomial = [ .coeff = rid(), .vars = [][:] ]
756 First, construct X/LT(f) and X/LT(g). ltf.vars and ltg.vars
757 are ordered, so we may walk through LT(f) and LT(g). A power
758 of a variable which we can prove occurs only in LT(f) should
759 be placed in X/LT(g), and vice-versa.
763 while fj < ltf.vars.len && gj < ltg.vars.len
764 var fx = <f.vars[fj]
765 var gx = <g.vars[gj]
766 match strnumcmp(fx.variable, gx.variable)
768 std.slpush(&g_factor.vars, t.dup(fx#))
771 std.slpush(&f_factor.vars, t.dup(gx#))
774 /* x^a and x^b. Add x^|a-b| to one side or the other */
775 var ngp : Z# = gneg(gx.power)
776 var p = gadd(fx.power, ngp)
779 | `std.Equal: __dispose__(p)
782 std.slpush(&f_factor.vars, [
783 .variable = std.sldup(fx.variable),
787 std.slpush(&g_factor.vars, [
788 .variable = std.sldup(gx.variable),
797 /* Clean up any extra, trailing terms */
798 while fj < ltf.vars.len
799 std.slpush(&g_factor.vars, t.dup(ltf.vars[fj]))
803 while gj < ltg.vars.len
804 std.slpush(&f_factor.vars, t.dup(ltg.vars[gj]))
808 /* Now construct (X / LT(f)) * f - (X / LT(g)) * g */
809 var f_term = t.dup(f)
810 var g_term = auto t.dup(g)
811 mul_poly_mon_ip(f_term, &f_factor)
812 __dispose__(f_factor)
813 mul_poly_mon_ip(g_term, &g_factor)
814 __dispose__(g_factor)
816 gadd_ip(f_term, g_term)
822 Assuming a and b have only non-negative exponents, try and build a/b,
823 similarly with non-negative exponents.
825 This is not a division_struct monomial because monomials in general
826 are allowed to have negative exponents.
828 const positive_monomial_division = { a : monomial#, b : monomial#
829 var quotient : monomial = [ .coeff = t.dup(a.coeff), .vars = [][:] ]
833 rmul_ip(quotient.coeff, bci)
836 __dispose__(quotient)
842 while ak < a.vars.len && bk < b.vars.len
845 match strnumcmp(ax.variable, bx.variable)
847 std.slpush("ient.vars, t.dup(ax#))
850 __dispose__(quotient)
853 match t.cmp(ax.power, bx.power)
855 __dispose__(quotient)
860 var bp : Z# = t.dup(bx.power)
863 std.slpush("ient.vars, [
864 .variable = std.sldup(ax.variable),
865 .power = gadd(ax.power, bp)
873 __dispose__(quotient)
877 while ak < a.vars.len
878 std.slpush("ient.vars, t.dup(a.vars[ak]))
882 reducemonomial("ient)
883 -> `std.Some quotient
886 const minimize_groebner_basis = { G : polynomial#[:]#
888 Following [CLO] Chapter 2, Section 7, Lemma 3, try to remove
889 any p in G such that LT(p) is generated by the leading terms
893 for var j = G#.len - 1; j >= 0; --j
894 var lt = t.dup(G#[j].terms[G#[j].terms.len - 1])
895 for var k = 0; k < G#.len; ++k
900 match positive_monomial_division(<, &G#[k].terms[G#[k].terms.len - 1])