logicaffeine_proof/gf2.rs
1//! The general linear group **GL(n,2)** and the invertibility constant — where the reciprocal
2//! SAT-threshold sum comes home.
3//!
4//! A uniformly random n×n matrix over GF(2) is invertible with probability `Π_{j=1}^n (1 − 2⁻ʲ)`, which
5//! → `φ(½) = 0.2887880951`. The reciprocal first-moment SAT-threshold sum is `−log₂ φ(½)` exactly, so the
6//! two meet at this one constant. And it is *not* a coincidence — it is a **symmetry break**:
7//!
8//! - The invertible matrices are exactly the group `GL(n,2)`.
9//! - `GL(n,2)` acts **simply transitively on ordered bases** of `GF(2)ⁿ` (a matrix is invertible iff its
10//! rows are a basis). So by orbit–stabilizer with a *trivial* stabilizer, `|GL(n,2)|` = the number of
11//! ordered bases = `Π_{i=0}^{n-1} (2ⁿ − 2ⁱ)` — each factor counting the vectors that *avoid the span
12//! built so far*. That step-by-step quotient is the symmetry-broken enumeration; dividing by `2^{n²}`
13//! gives the Euler partial product.
14
15/// `|GL(n,2)|` via the orbit–stabilizer (ordered-basis) factorization `Π_{i=0}^{n-1}(2ⁿ − 2ⁱ)`. Each
16/// factor `2ⁿ − 2ⁱ` is the count of vectors outside the i-dimensional span of the basis chosen so far —
17/// the symmetry-broken enumeration of the bases on which `GL(n,2)` acts simply transitively. Valid up to
18/// `n = 10` before the `u128` product overflows.
19pub fn gl_order(n: u32) -> u128 {
20 let full = 1u128 << n;
21 (0..n).map(|i| full - (1u128 << i)).product()
22}
23
24/// Is an n×n GF(2) matrix (each row packed as the low `n` bits of a `u64`) invertible? Gaussian
25/// elimination over GF(2): invertible iff it reduces to full rank `n`.
26pub fn is_invertible_gf2(n: u32, rows: &[u64]) -> bool {
27 let mut rows = rows.to_vec();
28 let mut rank = 0usize;
29 for col in 0..n {
30 if let Some(p) = (rank..rows.len()).find(|&r| (rows[r] >> col) & 1 == 1) {
31 rows.swap(rank, p);
32 for r in 0..rows.len() {
33 if r != rank && (rows[r] >> col) & 1 == 1 {
34 rows[r] ^= rows[rank];
35 }
36 }
37 rank += 1;
38 }
39 }
40 rank == n as usize
41}
42
43/// Brute-force count of invertible n×n GF(2) matrices, over all `2^{n²}` of them. Feasible for `n ≤ 4`.
44pub fn count_invertible_gf2_bruteforce(n: u32) -> u128 {
45 let cells = n * n;
46 let mask = (1u64 << n) - 1;
47 let mut count = 0u128;
48 for bits in 0u64..(1u64 << cells) {
49 let rows: Vec<u64> = (0..n).map(|r| (bits >> (r * n)) & mask).collect();
50 if is_invertible_gf2(n, &rows) {
51 count += 1;
52 }
53 }
54 count
55}
56
57/// The Euler partial product `Π_{j=1}^n (1 − 2⁻ʲ)` — the exact invertibility *density* `|GL(n,2)| / 2^{n²}`.
58pub fn invertibility_density(n: u32) -> f64 {
59 (1..=n).map(|j| 1.0 - 2f64.powi(-(j as i32))).product()
60}
61
62/// The **complete solution space** of a GF(2) linear system `A x = b`, in *symmetry-broken* form: one
63/// particular solution `x₀` plus a basis of the kernel. Every solution is `x₀ ⊕ (a GF(2) combination of
64/// the kernel basis)`, so the entire affine coset — all `2^{n−rank}` solutions — is generated from this
65/// compressed witness. The kernel is the **symmetry group** of the solution set (the translations that
66/// preserve `Ax`), `x₀` is one witness, and the basis are its generators: this is the exact, polynomial,
67/// linear analog of `hypercube::model_orbit`. The harder the break (the bigger the kernel), the more
68/// solutions; an *invertible* system has a trivial kernel — no symmetry, a unique witness.
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct SolutionSpace {
71 pub num_vars: usize,
72 pub particular: Vec<bool>,
73 pub kernel_basis: Vec<Vec<bool>>,
74}
75
76impl SolutionSpace {
77 /// The number of solutions: `2^{dim kernel}` (rank–nullity) — the orbit size of the solution coset.
78 pub fn count(&self) -> u128 {
79 1u128 << self.kernel_basis.len()
80 }
81
82 /// Generate **every** solution by XORing the particular solution with each subset of the kernel basis
83 /// — the orbit of `x₀` under the kernel's translation symmetry.
84 pub fn enumerate(&self) -> Vec<Vec<bool>> {
85 let k = self.kernel_basis.len();
86 (0u64..(1u64 << k))
87 .map(|mask| {
88 let mut x = self.particular.clone();
89 for b in 0..k {
90 if (mask >> b) & 1 == 1 {
91 for v in 0..self.num_vars {
92 x[v] ^= self.kernel_basis[b][v];
93 }
94 }
95 }
96 x
97 })
98 .collect()
99 }
100}
101
102/// Solve a GF(2) system `A x = b` (each row a coefficient bit-vector in the low `n` bits, `rhs` the
103/// right-hand sides) for its **entire** solution space via Gaussian elimination to reduced row echelon
104/// form. Returns the symmetry-broken [`SolutionSpace`] (particular solution + kernel basis), or `None`
105/// iff the system is inconsistent. Generalizes [`crate::xorsat::solve`], which returns just one witness.
106pub fn solve_gf2(n: usize, rows: &[u64], rhs: &[bool]) -> Option<SolutionSpace> {
107 let coeff_mask = if n == 64 { u64::MAX } else { (1u64 << n) - 1 };
108 let mut aug: Vec<u64> =
109 rows.iter().zip(rhs).map(|(&c, &b)| (c & coeff_mask) | ((b as u64) << n)).collect();
110 let mut pivot_col_of_row: Vec<usize> = Vec::new();
111 let mut rank = 0usize;
112 for col in 0..n {
113 if let Some(p) = (rank..aug.len()).find(|&r| (aug[r] >> col) & 1 == 1) {
114 aug.swap(rank, p);
115 for r in 0..aug.len() {
116 if r != rank && (aug[r] >> col) & 1 == 1 {
117 aug[r] ^= aug[rank];
118 }
119 }
120 pivot_col_of_row.push(col);
121 rank += 1;
122 }
123 }
124 // Inconsistent: a fully-reduced row with no coefficients but a 1 on the right (0 = 1).
125 for r in 0..aug.len() {
126 if aug[r] & coeff_mask == 0 && (aug[r] >> n) & 1 == 1 {
127 return None;
128 }
129 }
130 let mut is_pivot = vec![false; n];
131 for &c in &pivot_col_of_row {
132 is_pivot[c] = true;
133 }
134 // Particular solution: free variables 0, each pivot variable = its row's right-hand side.
135 let mut particular = vec![false; n];
136 for (r, &pc) in pivot_col_of_row.iter().enumerate() {
137 particular[pc] = (aug[r] >> n) & 1 == 1;
138 }
139 // Kernel basis: one vector per free column f — set x_f = 1, each pivot var = its row's f-coefficient.
140 let mut kernel_basis = Vec::new();
141 for f in 0..n {
142 if is_pivot[f] {
143 continue;
144 }
145 let mut kv = vec![false; n];
146 kv[f] = true;
147 for (r, &pc) in pivot_col_of_row.iter().enumerate() {
148 if (aug[r] >> f) & 1 == 1 {
149 kv[pc] = true;
150 }
151 }
152 kernel_basis.push(kv);
153 }
154 Some(SolutionSpace { num_vars: n, particular, kernel_basis })
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 /// **It is exactly what it ought to be: the invertible count IS `|GL(n,2)|`.** Brute force over all
162 /// `2^{n²}` matrices equals the orbit–stabilizer product `Π(2ⁿ − 2ⁱ)` — i.e. the invertible matrices
163 /// are precisely the ordered bases, the simply-transitive orbit of `GL(n,2)`. Proven exhaustively for
164 /// n = 1..4.
165 #[test]
166 fn the_invertible_count_is_the_general_linear_group() {
167 for n in 1..=4u32 {
168 let brute = count_invertible_gf2_bruteforce(n);
169 let group = gl_order(n);
170 assert_eq!(brute, group, "n={n}: brute-force invertible count must equal |GL(n,2)| = Π(2ⁿ−2ⁱ)");
171 }
172 // The factorization is the symmetry break: each factor counts vectors avoiding the span so far.
173 for n in 1..=10u32 {
174 let full = 1u128 << n;
175 let stepwise: u128 = (0..n).map(|i| full - (1u128 << i)).product();
176 assert_eq!(gl_order(n), stepwise, "|GL(n,2)| is the step-by-step basis enumeration");
177 }
178 // A couple of pinned values: |GL(2,2)| = 6 (≅ S₃), |GL(3,2)| = 168.
179 assert_eq!(gl_order(2), 6);
180 assert_eq!(gl_order(3), 168);
181 }
182
183 /// **The invertibility density is the Euler partial product, and its limit is the reciprocal
184 /// SAT-threshold sum.** `|GL(n,2)| / 2^{n²} = Π_{j=1}^n(1 − 2⁻ʲ) → φ(½)`, and `−log₂ φ(½)` equals
185 /// `Σ_{k≥1} 1/α*_k` exactly. The loop closes: the counting thresholds and the GF(2) group density are
186 /// the same constant, viewed from two sides.
187 #[test]
188 fn the_invertibility_density_is_the_reciprocal_threshold_constant() {
189 // density matches |GL|/2^{n²} for small n (where the u128 ratio is exact)
190 for n in 1..=10u32 {
191 let exact = gl_order(n) as f64 / 2f64.powi((n * n) as i32);
192 assert!((invertibility_density(n) - exact).abs() < 1e-12, "density == |GL(n,2)|/2^(n²) at n={n}");
193 }
194 // the limit is φ(½), the GF(2) invertibility constant
195 let phi_half = invertibility_density(60);
196 assert!((phi_half - 0.288_788_095_1).abs() < 1e-9, "Π(1−2⁻ʲ) → φ(½) = 0.28879");
197 // and −log₂ φ(½) is the reciprocal first-moment threshold sum, computed independently
198 let ln2 = std::f64::consts::LN_2;
199 let recip_sum: f64 = (1..=60u32).map(|k| 1.0 / crate::families::ksat_threshold_first_moment_upper(k)).sum();
200 assert!((recip_sum + phi_half.ln() / ln2).abs() < 1e-6, "Σ 1/α*_k = −log₂ φ(½) — the loop closes");
201 assert!((recip_sum - 1.791_916_824_7).abs() < 1e-6, "and it is ≈ 1.79192");
202 }
203
204 /// **Our own GF(2) substrate exhibits the constant.** Sampled random matrices are invertible at the
205 /// Euler rate `Π(1−2⁻ʲ)` — the parity rung's reality matching the closed form. (Decorrelated seeds:
206 /// never sample along SplitMix64's own increment γ, per the seed-collapse lesson.)
207 #[test]
208 fn random_gf2_matrices_are_invertible_at_the_euler_rate() {
209 let n = 6u32;
210 let trials = 4000u64;
211 let mut rng = 0x1234_5678_9ABC_DEF0u64;
212 let mut next = || {
213 rng = rng.wrapping_add(0xD1B5_4A32_D192_ED03);
214 let mut z = rng;
215 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
216 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
217 z ^ (z >> 31)
218 };
219 let mask = (1u64 << n) - 1;
220 let invertible = (0..trials)
221 .filter(|_| {
222 let rows: Vec<u64> = (0..n).map(|_| next() & mask).collect();
223 is_invertible_gf2(n, &rows)
224 })
225 .count();
226 let rate = invertible as f64 / trials as f64;
227 let expected = invertibility_density(n);
228 assert!((rate - expected).abs() < 0.03, "empirical invertibility rate {rate:.4} ≈ Π(1−2⁻ʲ) = {expected:.4}");
229 }
230
231 // A row is satisfied by x (bit-packed) iff the parity over its support matches the rhs.
232 fn satisfies(row: u64, rhs: bool, x: u64) -> bool {
233 (row & x).count_ones() % 2 == rhs as u32
234 }
235 fn pack(x: &[bool]) -> u64 {
236 x.iter().enumerate().fold(0u64, |a, (i, &b)| a | ((b as u64) << i))
237 }
238
239 /// **Symmetry-break HARD, then find every solution.** Over a fuzz of random GF(2) systems, the
240 /// symmetry-broken `SolutionSpace` (one particular solution + a kernel basis) regenerates the *exact*
241 /// full solution set — matching brute force corner-for-corner — and the count is `2^{nullity}`
242 /// (rank–nullity). All `2^{n−rank}` solutions are recovered from `1 + (n−rank)` vectors: the kernel is
243 /// the symmetry, and enumerating its orbit of the witness is "finding the solutions".
244 #[test]
245 fn the_kernel_break_recovers_every_solution() {
246 let n = 12usize;
247 let mut rng = 0xCAFE_F00D_1234_5678u64;
248 let mut next = || {
249 rng = rng.wrapping_add(0xD1B5_4A32_D192_ED03);
250 let mut z = rng;
251 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
252 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
253 z ^ (z >> 31)
254 };
255 let mask = (1u64 << n) - 1;
256 let mut saw_consistent = 0usize;
257 let mut saw_inconsistent = 0usize;
258 let mut saw_multiple = 0usize;
259 for _ in 0..400 {
260 let m = 3 + (next() as usize % 12); // 3..14 equations — under- and over-determined
261 let rows: Vec<u64> = (0..m).map(|_| next() & mask).collect();
262 let rhs: Vec<bool> = (0..m).map(|_| next() & 1 == 0).collect();
263
264 // brute-force solution set
265 let brute: std::collections::BTreeSet<u64> = (0u64..(1u64 << n))
266 .filter(|&x| rows.iter().zip(&rhs).all(|(&r, &b)| satisfies(r, b, x)))
267 .collect();
268
269 match solve_gf2(n, &rows, &rhs) {
270 None => {
271 assert!(brute.is_empty(), "solve_gf2 said inconsistent but solutions exist");
272 saw_inconsistent += 1;
273 }
274 Some(space) => {
275 saw_consistent += 1;
276 if space.kernel_basis.len() > 0 {
277 saw_multiple += 1;
278 }
279 // every generated solution is genuine
280 let gen: std::collections::BTreeSet<u64> =
281 space.enumerate().iter().map(|x| pack(x)).collect();
282 for &x in &gen {
283 assert!(rows.iter().zip(&rhs).all(|(&r, &b)| satisfies(r, b, x)), "generated non-solution");
284 }
285 // the orbit is EXACTLY the solution set, and the count is 2^nullity
286 assert_eq!(gen, brute, "kernel orbit must equal the full solution set");
287 assert_eq!(space.count(), brute.len() as u128, "count = #solutions");
288 assert_eq!(space.count(), 1u128 << space.kernel_basis.len(), "count = 2^(dim kernel)");
289 }
290 }
291 }
292 assert!(saw_consistent > 20 && saw_inconsistent > 20 && saw_multiple > 20,
293 "fuzz must exercise consistent ({saw_consistent}), inconsistent ({saw_inconsistent}), and multi-solution ({saw_multiple}) systems");
294 }
295
296 /// **No symmetry ⟹ a unique witness — and that is the φ(½) event.** When the coefficient matrix is
297 /// invertible (square, full rank), the kernel is trivial: exactly one solution, no break possible.
298 /// Rank–nullity is the orbit–stabilizer law for the solution coset: `#solutions = 2^{n−rank}`. So the
299 /// invertibility constant φ(½) is precisely the rate at which a random square system pins down a
300 /// *unique* witness — rigidity and uniqueness are the same thing, here exactly and constructively.
301 #[test]
302 fn invertible_system_has_the_unique_rigid_witness() {
303 let n = 8usize;
304 let mut rng = 0x0BADC0DE_5EED_1234u64;
305 let mut next = || {
306 rng = rng.wrapping_add(0xD1B5_4A32_D192_ED03);
307 let mut z = rng;
308 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
309 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
310 z ^ (z >> 31)
311 };
312 let mask = (1u64 << n) - 1;
313 let mut unique = 0usize;
314 let mut total = 0usize;
315 for _ in 0..2000 {
316 let rows: Vec<u64> = (0..n).map(|_| next() & mask).collect();
317 let rhs: Vec<bool> = (0..n).map(|_| next() & 1 == 0).collect();
318 if is_invertible_gf2(n as u32, &rows) {
319 // invertible ⟹ always consistent, with exactly one (rigid) solution
320 let space = solve_gf2(n, &rows, &rhs).expect("invertible ⟹ consistent");
321 assert_eq!(space.kernel_basis.len(), 0, "invertible ⟹ trivial kernel");
322 assert_eq!(space.count(), 1, "invertible ⟹ a unique, rigid witness");
323 unique += 1;
324 } else {
325 // singular ⟹ either inconsistent (None) or many solutions (nontrivial kernel)
326 match solve_gf2(n, &rows, &rhs) {
327 None => {}
328 Some(space) => {
329 assert!(!space.kernel_basis.is_empty(), "consistent singular ⟹ a symmetry to break");
330 assert!(space.count() >= 2, "consistent singular ⟹ multiple solutions");
331 }
332 }
333 }
334 total += 1;
335 }
336 // the unique-solution rate IS the GL(n,2) density φ(½) ≈ Π(1−2⁻ʲ)
337 let rate = unique as f64 / total as f64;
338 assert!((rate - invertibility_density(n as u32)).abs() < 0.03,
339 "unique-witness rate {rate:.4} ≈ φ(½) = {:.4}", invertibility_density(n as u32));
340 }
341}