Skip to main content

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}