A residue accumulated from coprime moduli. value is the symmetric
representative of their common solution modulo modulus.
Constructor
Hex.Modular.Crt.mk
The Sunzi Suanjing, a Chinese text of the third to fifth century, asks for
a number that leaves remainder 2 when counted by threes, 3 when counted by
fives, and 2 when counted by sevens. A Hex.Modular.Crt holds the
answer so far: its modulus, which is the product of the moduli pushed so far,
and the symmetric representative of the solution modulo it. Hex.Modular.Crt.init is the
state modulo 1, and each Hex.Modular.Crt.push adds one more
condition:
def sunzi : Option Crt := do
let c ← Crt.init.push 2 3
let c ← c.push 3 5
c.push 2 7
#eval sunzi.map fun c => (c.value, c.modulus)
A residue accumulated from coprime moduli. value is the symmetric
representative of their common solution modulo modulus.
Constructor
Hex.Modular.Crt.mk
Fold the residue r modulo m into a scalar CRT accumulation using one
Garner mixed-radix step. Moduli zero and one and moduli not coprime to the
accumulated modulus are rejected.
The moduli must be pairwise coprime. push checks this as it goes, and
returns none when the new modulus shares a factor with the product so far,
even when the conditions are consistent: 1 mod 4 and 3 mod 6 are both
satisfied by 9, but 4 and 6 are not coprime.
#guard (Crt.init.push 1 4 >>= (·.push 3 6)).isNone
Suppose the state holds v modulo M, and r modulo m is pushed, with
M and m coprime. Let s be the inverse of M modulo m. Then
δ = symMod ((r - v) s) m and the new value is symMod (v + M δ) (M m).
Adding a multiple of M does not change the residue modulo M, and modulo
m the sum is v + (r - v) = r. This is Garner's mixed-radix step. The
inverse s comes from the extended Euclidean algorithm applied to
M mod m and m, so its operands are no larger than m, whatever the size
of M. When m fits in a word, the other operations (additions,
multiplications by numbers smaller than m, and reductions) each take time
linear in the bit length of M.
The final reduction is needed. Pushing 1 mod 3 and then 0 mod 2 gives
δ = 1 and v + M δ = 4, which is outside (-3, 3]; the stored value is
-2:
#eval (Crt.init.push 1 3 >>= (·.push 0 2)).map
fun c => (c.value, c.modulus)
Three theorems describe a successful push. The new modulus is the product, the new value has the new residue, and it keeps the old residue modulo every divisor of the old modulus. By induction, the value after a sequence of pushes has every residue that was pushed.
Hex.Modular.Crt.push_modulus {c c' : Hex.Modular.Crt} {r : ℤ} {m : ℕ} (h : c.push r m = some c') : c'.modulus = c.modulus * mHex.Modular.Crt.push_modulus {c c' : Hex.Modular.Crt} {r : ℤ} {m : ℕ} (h : c.push r m = some c') : c'.modulus = c.modulus * m
A successful scalar push multiplies the accumulated modulus by the new one.
Hex.Modular.Crt.push_congr_new {c c' : Hex.Modular.Crt} {r : ℤ} {m : ℕ} (h : c.push r m = some c') : c'.value % ↑m = r % ↑mHex.Modular.Crt.push_congr_new {c c' : Hex.Modular.Crt} {r : ℤ} {m : ℕ} (h : c.push r m = some c') : c'.value % ↑m = r % ↑m
A successful scalar push records the requested new residue.
Hex.Modular.Crt.push_congr_old {c c' : Hex.Modular.Crt} {r : ℤ} {m d : ℕ} (hd : d ∣ c.modulus) (h : c.push r m = some c') : c'.value % ↑d = c.value % ↑dHex.Modular.Crt.push_congr_old {c c' : Hex.Modular.Crt} {r : ℤ} {m d : ℕ} (hd : d ∣ c.modulus) (h : c.push r m = some c') : c'.value % ↑d = c.value % ↑d
A successful scalar push preserves the accumulated value modulo every divisor of the old accumulated modulus.
To recover an integer x, push residues of x until 2 |x| is less than the
modulus. The value then has the same residue as x modulo the whole modulus,
and it is the symmetric representative of that residue, so it is x by
symMod_unique. The same argument gives the uniqueness of strictly bounded
solutions:
Hex.Modular.crt_unique {c : Hex.Modular.Crt} {x y : ℤ} (h : 2 * x.natAbs < c.modulus) (h' : 2 * y.natAbs < c.modulus) (hx : x % ↑c.modulus = y % ↑c.modulus) : x = yHex.Modular.crt_unique {c : Hex.Modular.Crt} {x y : ℤ} (h : 2 * x.natAbs < c.modulus) (h' : 2 * y.natAbs < c.modulus) (hx : x % ↑c.modulus = y % ↑c.modulus) : x = y
Two integers strictly smaller than half the accumulated modulus and congruent modulo that modulus are equal.
Recovering a polynomial or a matrix means recovering many integers modulo
the same primes. A CrtVec k (Hex.Modular.CrtVec) holds k values
sharing one modulus. Its Hex.Modular.CrtVec.push computes the inverse s once and
uses it for every coordinate, so a push costs one extended Euclidean
algorithm whatever k is. Here the second coordinate solves 1 mod 3,
0 mod 5 and -1 mod 7:
def lanes : Option (CrtVec 2) := do
let c ← (CrtVec.init 2).push #v[2, 1] 3
let c ← c.push #v[3, 0] 5
c.push #v[2, -1] 7
#eval lanes.map fun c => (c.value, c.modulus)
CrtVec has the same theorems as Crt, for every coordinate, together with
Hex.Modular.CrtVec.push_congr, which combines the old and new
residues into one congruence modulo the product, and
Hex.Modular.CrtVec.eq_of_congr, the analogue of crt_unique.
When every modulus is known before any residue, the moduli can be prepared
once and used for many reconstructions.
Hex.Modular.CrtPlan.build? checks that the moduli are greater than
one and pairwise coprime, and arranges them as the leaves of a balanced
binary tree. Each internal node stores the inverse of the product of its left
subtree modulo the product of its right subtree.
Hex.Modular.CrtPlan.reconstruct? then combines the residues from the
leaves up, applying Garner's step at each node with its stored inverse, so it
runs no extended Euclidean algorithm.
Hex.Modular.CrtPlan.reconstructVec? does the same for k values at
once: its input is an Array (Vector Int k) with one vector of residues per
modulus, in the order of the moduli, and it uses each stored inverse for every
coordinate. Both return none if the number of residues differs from the
number of moduli, and both check the result against every residue before
returning it.
A plan for the five primes above recovers the same residue of H₅₀ as the
incremental computation:
def plan? : Option CrtPlan := CrtPlan.build? primes.toArray
def viaPlan : Option Int := plan?.bind fun plan =>
plan.reconstruct? (primes.toArray.map (harmonicMod 50))
def viaPush : Option Int :=
(primes.foldlM (fun (c : Crt) p =>
c.push (harmonicMod 50 p) p) Crt.init).map (·.value)
#guard viaPlan.isSome
#guard viaPlan = viaPush
#guard (CrtPlan.build? #[6, 10]).isNone
The result is the symmetric representative modulo the product of the moduli, so it recovers every integer that small:
Hex.Modular.CrtPlan.reconstruct?_eq_candidate {plan : Hex.Modular.CrtPlan} {residues : Array ℤ} {value candidate : ℤ} (hresult : plan.reconstruct? residues = some value) (hbound : 2 * candidate.natAbs < plan.modulus) (hcongr : ∀ (i : Fin plan.moduli.size), candidate % ↑plan.moduli[i] = residues.getD (↑i) 0 % ↑plan.moduli[i]) : value = candidateHex.Modular.CrtPlan.reconstruct?_eq_candidate {plan : Hex.Modular.CrtPlan} {residues : Array ℤ} {value candidate : ℤ} (hresult : plan.reconstruct? residues = some value) (hbound : 2 * candidate.natAbs < plan.modulus) (hcongr : ∀ (i : Fin plan.moduli.size), candidate % ↑plan.moduli[i] = residues.getD (↑i) 0 % ↑plan.moduli[i]) : value = candidate
Strictly half-bounded candidates that match every input residue are recovered exactly by scalar reconstruction.
The incremental form is the one to use when the number of moduli is not known in advance, because a computation can stop as soon as it has enough. The section Costs and limits compares the two.