Groebner.jl: Fast Gröbner Tracing in Julia

This page mirrors the code snippets from the paper Groebner.jl: Fast Gröbner Tracing in Julia that appeared in the proceedings of the International Congress on Mathematical Software 2026.

Setup

using Pkg; Pkg.add("Groebner"); Pkg.add("Nemo")

Generic coefficient arithmetic

using Groebner, Nemo

K, t = rational_function_field(QQ, "t")
R, (x, y, z) = K["x", "y", "z"]

F = [t * x^2 * y + y^3, x * y^2 + 3 // t * z]

G = groebner(F);
@assert isgroebner(G)
nothing
┌ Warning: Groebner.jl does not have a native implementation for the given field: Rational function field over QQ.
│ Falling back to a generic implementation (may be slow).
│ If this is unexpected, please consider submitting a GitHub issue.
└ @ Groebner ~/work/Groebner.jl/Groebner.jl/src/input_output/AbstractAlgebra.jl:78
┌ Warning: Groebner.jl does not have a native implementation for the given field: Rational function field over QQ.
│ Falling back to a generic implementation (may be slow).
│ If this is unexpected, please consider submitting a GitHub issue.
└ @ Groebner ~/work/Groebner.jl/Groebner.jl/src/input_output/AbstractAlgebra.jl:78

Learn and apply

using Groebner, Nemo

kat = Groebner.Examples.katsuran(10)

kat_zp1 = map(f -> change_base_ring(GF(2^30 + 3), f), kat)
kat_zp2 = map(f -> change_base_ring(GF(2^30 + 7), f), kat)

trace, _ = groebner_learn(kat_zp1)
success, gb = groebner_apply!(trace, kat_zp2)

@assert success && isgroebner(gb)
nothing

Timing the apply phase

The paper reports these timings using BenchmarkTools. Here we run one timing pass without rendering benchmark output into the page so the examples remain lightweight in CI.

trace_timing, _ = groebner_learn(kat_zp1)

groebner_time = @elapsed groebner(kat_zp2)
apply_time = @elapsed groebner_apply!(trace_timing, kat_zp2)

@assert groebner_time >= 0
@assert apply_time >= 0
nothing

Batched application over several primes

kat_zp3 = map(f -> change_base_ring(GF(2^30 + 9), f), kat)
kat_zp4 = map(f -> change_base_ring(GF(2^30 + 15), f), kat)
kat_zp5 = map(f -> change_base_ring(GF(2^30 + 19), f), kat)

success, (gb2, gb3, gb4, gb5) = groebner_apply!(trace, (kat_zp2, kat_zp3, kat_zp4, kat_zp5))

@assert success && all(isgroebner, (gb2, gb3, gb4, gb5))
nothing

Hybrid exact-numeric coefficients

import Base: +, -, *, zero, one, inv, iszero, isone

const M = 256
setprecision(BigFloat, M)

struct Hybrid{Zp, Float}
    a::Zp
    b::Float
end

iszero(x::Hybrid) = iszero(x.a)
isone(x::Hybrid) = isone(x.a)
zero(x::Hybrid) = Hybrid(zero(x.a), zero(x.b))
one(x::Hybrid) = Hybrid(one(x.a), one(x.b))

+(x::Hybrid, y::Hybrid) = Hybrid(x.a + y.a, x.b + y.b)
-(x::Hybrid, y::Hybrid) = Hybrid(x.a - y.a, x.b - y.b)
*(x::Hybrid, y::Hybrid) = Hybrid(x.a * y.a, x.b * y.b)
inv(x::Hybrid) = Hybrid(inv(x.a), inv(x.b))
inv (generic function with 130 methods)
using Groebner, Nemo

to_hybrid(q) = Hybrid(GF(2^30 + 3)(q), BigFloat(q))

sys = Groebner.Examples.hexapod(k = QQ)

sys_exps = map(f -> collect(exponent_vectors(f)), sys)
sys_cfs = map(f -> collect(coefficients(f)), sys)

cfs_hybrid = map(c -> Groebner.CoeffGeneric.(to_hybrid.(c)), sys_cfs)

ring = Groebner.PolyRing(6, DegRevLex(), 0, :generic)
gb_exps, gb_cfs = groebner(ring, sys_exps, cfs_hybrid);

@assert !isempty(gb_exps)
@assert length(gb_exps) == length(gb_cfs)
nothing