Skip to content

nla_grobner: add mod_residue pattern to propagate_quotients#9597

Merged
NikolajBjorner merged 3 commits into
Z3Prover:masterfrom
1arie1:ag/mod_residue
May 27, 2026
Merged

nla_grobner: add mod_residue pattern to propagate_quotients#9597
NikolajBjorner merged 3 commits into
Z3Prover:masterfrom
1arie1:ag/mod_residue

Conversation

@1arie1

@1arie1 1arie1 commented May 22, 2026

Copy link
Copy Markdown
Contributor

Adds a new lemma pattern to nla_grobner::propagate_quotients that derives a modular-residue constraint from polynomial divisibility, filling a gap between quotient1-5 (model-value-driven case splits) and the polynomials Grobner actually produces on Skolem-encoded mod arithmetic.

Pattern

For a polynomial p with all-integer free variables and a linear monomial c_v * v (single integer var), the pattern computes M = gcd(|c_i/c_v|) over the other monomials and K = c0/c_v for the constant term. When both are integers, dividing p by c_v gives

v + M*Q + K = 0   with Q an integer

so v ≡ -K (mod M). The pattern emits the sound disjunctive lemma

(v < 0)  ∨  (v ≥ M)  ∨  (v = target)

where target = (-K) mod M ∈ [0, M-1]. This encodes "v ∈ target + M·Z" in a form the LP / SAT layer can refute against current bounds.

Motivation

QF_UFNIA verification benchmarks over fixed-prime modular arithmetic (e.g. zk applications using the BabyBear prime 2013265921) regularly produce basis polynomials of the form

-p*v_div + p*(v_a * v_b) - v_mod = 0

where v_mod is the result of (mod (* v_a v_b) p). The polynomial sits in the Grobner basis but none of quotient1-5 fires: they all require specific model-value alignments (r_value == 0, |v_value| > |r_value|, etc.) that don't hold when all variables in scope are similarly sized integers in [0, p). The proof spins on interval-tightening lemmas without ever extracting the modular conclusion.

The author of propagate_quotients flagged this gap with the comment "other division lemmas are possible" preceding the fall-through "no lemmas found" CTRACE. This patch supplies one.

Soundness

The lemma is sound regardless of v's LP bounds — the bound-negation disjuncts (v < 0) and (v ≥ M) make the disjunction unconditionally true under the polynomial identity, with v = target as the canonical residue in [0, M-1]. M is derived from the polynomial's coefficient gcd, not from any LP-side bound.

Validated under smt.arith.validate=true on the mod-factor-propagation reproducers (PR #9235 follow-up), zk verifier benchmarks, and a broader QF_UFNIA sample — 50+ files total, zero validate_conflict() assertion violations.

Performance

A model-value gate (skip emission when v's current value already satisfies one of the disjuncts) prevents the pattern from short-circuiting the propagate_quotients || propagate_gcd_test || propagate_eqs || propagate_factorization || propagate_linear_equations chain with redundant emissions. Without the gate, a single (v, M, target) triple can re-emit each Grobner round and starve the downstream propagators — observed in regression testing as thousands of identical emissions on a small benchmark, turning a sub-second closure into a timeout.

On six small mod-factor-propagation reproducers, the patch closes four cases that previously timed out at 30 s (~1 s typical under the Grobner-ramped config: smt.arith.nl.gr_q=50,
smt.arith.nl.grobner_eqs_growth=50, smt.arith.nl.grobner_exp_delay=false, smt.arith.nl.grobner_frequency=1). The two remaining timeouts in that set are attributable to different gaps (Boolean-disjunction propagation, and the multi-bounded-mod-result polynomial shape that needs Grobner over Z/pZ), not to mod_residue itself.

Diagnostics

TRACE under the existing 'grobner' tag emits one line per lemma emission, recording v, M, c_v, c0, and target.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends Z3’s NLA Gröbner-based quotient propagation (nla::grobner::propagate_quotients) with a new “mod_residue” lemma pattern that detects when a linear integer variable’s value is determined modulo a computed gcd, and emits a disjunctive clause of the form (v < 0) ∨ (v ≥ M) ∨ (v = target) to enable bound-based refutations in the LP/SAT layers.

Changes:

  • Add a new modular-residue detection pattern over integer polynomials in propagate_quotients, deriving v ≡ -K (mod M) from polynomial divisibility conditions.
  • Emit a guarded disjunctive lemma encoding the residue class via two “out-of-range” disjuncts plus the canonical residue equality.
  • Add tracing for the new lemma emissions under the existing grobner TRACE tag.

Comment thread src/math/lp/nla_grobner.cpp Outdated
// integer (call it K). Let M = gcd(|c_i/c_v|) over non-v monomials.
// Then p/c_v gives v + M*Q + K = 0 with Q integer, so v ≡ -K (mod M).
// With target = (-K) mod M ∈ [0, M-1], emit
// (v < 0) ∨ (v ≥ M) ∨ (v = target).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Emit:

dependencies => (v < 0) ∨ (v ≥ M) ∨ (v = target).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread src/math/lp/nla_grobner.cpp Outdated
// emission would be redundant. Without this guard, the lemma
// re-emits every Grobner round on the same polynomial.
rational v_val = c().val(vv);
if (v_val < rational::zero() || v_val >= M || v_val == target)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can just write v_val < 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread src/math/lp/nla_grobner.cpp Outdated
if (!K.is_int())
continue;
rational target = mod(-K, M); // Euclidean: result in [0, M-1].
SASSERT(target >= rational::zero() && target < M);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can just write target >= 0, target == 0, etc

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread src/math/lp/nla_grobner.cpp Outdated
M = M.is_zero() ? a : gcd(M, a);
if (M.is_one()) { ok = false; break; } // trivial modulus, abort
}
if (!ok || M.is_zero())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can just write M == 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread src/math/lp/nla_grobner.cpp Outdated
rational a = abs(quot);
SASSERT(!a.is_zero());
M = M.is_zero() ? a : gcd(M, a);
if (M.is_one()) { ok = false; break; } // trivial modulus, abort

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

M == 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

1arie1 and others added 2 commits May 26, 2026 17:26
Adds a new lemma pattern to nla_grobner::propagate_quotients that
derives a modular-residue constraint from polynomial divisibility,
filling a gap between quotient1-5 (model-value-driven case splits)
and the polynomials Grobner actually produces on Skolem-encoded mod
arithmetic.

Pattern
-------

For a polynomial p with all-integer free variables and a linear
monomial c_v * v (single integer var), the pattern computes
M = gcd(|c_i/c_v|) over the other monomials and K = c0/c_v for the
constant term. When both are integers, dividing p by c_v gives

    v + M*Q + K = 0   with Q an integer

so v ≡ -K (mod M). The pattern emits the sound disjunctive lemma

    (v < 0)  ∨  (v ≥ M)  ∨  (v = target)

where target = (-K) mod M ∈ [0, M-1]. This encodes "v ∈ target + M·Z"
in a form the LP / SAT layer can refute against current bounds.

Motivation
----------

QF_UFNIA verification benchmarks over fixed-prime modular arithmetic
(e.g. zk applications using the BabyBear prime 2013265921) regularly
produce basis polynomials of the form

    -p*v_div + p*(v_a * v_b) - v_mod = 0

where v_mod is the result of (mod (* v_a v_b) p). The polynomial
sits in the Grobner basis but none of quotient1-5 fires: they all
require specific model-value alignments (r_value == 0,
|v_value| > |r_value|, etc.) that don't hold when all variables in
scope are similarly sized integers in [0, p). The proof spins on
interval-tightening lemmas without ever extracting the modular
conclusion.

The author of propagate_quotients flagged this gap with the comment
\"other division lemmas are possible\" preceding the fall-through
\"no lemmas found\" CTRACE. This patch supplies one.

Soundness
---------

The lemma is sound regardless of v's LP bounds — the bound-negation
disjuncts (v < 0) and (v ≥ M) make the disjunction unconditionally
true under the polynomial identity, with v = target as the canonical
residue in [0, M-1]. M is derived from the polynomial's coefficient
gcd, not from any LP-side bound.

Validated under smt.arith.validate=true on the mod-factor-propagation
reproducers (PR Z3Prover#9235 follow-up), zk verifier benchmarks, and a
broader QF_UFNIA sample — 50+ files total, zero validate_conflict()
assertion violations.

Performance
-----------

A model-value gate (skip emission when v's current value already
satisfies one of the disjuncts) prevents the pattern from
short-circuiting the propagate_quotients || propagate_gcd_test ||
propagate_eqs || propagate_factorization || propagate_linear_equations
chain with redundant emissions. Without the gate, a single (v, M,
target) triple can re-emit each Grobner round and starve the
downstream propagators — observed in regression testing as thousands
of identical emissions on a small benchmark, turning a sub-second
closure into a timeout.

On six small mod-factor-propagation reproducers, the patch closes
four cases that previously timed out at 30 s (~1 s typical under
the Grobner-ramped config: smt.arith.nl.gr_q=50,
smt.arith.nl.grobner_eqs_growth=50, smt.arith.nl.grobner_exp_delay=false,
smt.arith.nl.grobner_frequency=1). The two remaining timeouts in
that set are attributable to different gaps (Boolean-disjunction
propagation, and the multi-bounded-mod-result polynomial shape that
needs Grobner over Z/pZ), not to mod_residue itself.

Diagnostics
-----------

TRACE under the existing 'grobner' tag emits one line per lemma
emission, recording v, M, c_v, c0, and target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use literal rational comparisons (M == 1, M == 0, target >= 0,
v_val < 0) and reframe the docstring Emit lines as
"dependencies => disjunction" to make the lemma form explicit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants