logicaffeine_proof/two_type.rs
1//! A genuinely **2-truncated** ∞-groupoid, had as an object: the minimal `K(A, 2)`.
2//!
3//! `kan_complex` built the nerve `BG = K(G, 1)` and proved it 1-truncated (inner horns fill *uniquely*).
4//! This is the first object *beyond* that — a Kan complex with real `π₂ ≠ 0` living inside it, not merely
5//! admitted by a crossed module or glimpsed as homology. It is the Eilenberg–MacLane space `K(A, 2)`:
6//! `π₂ = A`, every other `πₙ = 0`.
7//!
8//! The model is Dold–Kan `Γ(A[2])` for the chain complex with `A` in degree 2. Its `n`-simplices are the
9//! `A`-linear combinations of order-preserving **surjections `[n] ↠ [2]`** (since the complex is
10//! concentrated in degree 2, a face that fails to stay surjective dies — `C₁ = C₃ = 0`). Concretely an
11//! `n`-simplex is an `A`-labeling of `S_n = {surjections [n] ↠ [2]}`, and `dᵢ` pulls back along the
12//! `i`-th coface, keeping only the still-surjective terms. It is a simplicial *abelian group*, hence
13//! automatically a Kan complex — and we verify, by enumeration:
14//!
15//! - **`π₂ ≠ 0`**: the inner horn `Λ²₁` has `|A|` fillers (the `2`-cells), *not* the unique filler of a
16//! 1-type. This is the structure `K(G,1)` provably lacked.
17//! - **Kan**: every horn fills (the face-tuple map onto the compatible-horn object is surjective), checked
18//! in degrees 2, 3, 4.
19//! - **exactly 2-truncated**: degree-4 inner horns fill *uniquely* again (no `π₃`), so the higher homotopy
20//! stops at level 2.
21
22use std::collections::{HashMap, HashSet};
23
24/// All order-preserving surjections `[n] ↠ [2]`, as nondecreasing length-`(n+1)` value vectors hitting
25/// `0, 1, 2`. `S_n` — the basis of the degree-`n` part. Empty for `n < 2` (so `Γ₀ = Γ₁ = 0`).
26fn surjections(n: usize) -> Vec<Vec<u8>> {
27 fn rec(len: usize, pos: usize, last: u8, cur: &mut Vec<u8>, out: &mut Vec<Vec<u8>>) {
28 if pos == len {
29 if cur.contains(&0) && cur.contains(&1) && cur.contains(&2) {
30 out.push(cur.clone());
31 }
32 return;
33 }
34 for v in last..=2 {
35 cur.push(v);
36 rec(len, pos + 1, v, cur, out);
37 cur.pop();
38 }
39 }
40 let mut out = Vec::new();
41 rec(n + 1, 0, 0, &mut Vec::new(), &mut out);
42 out
43}
44
45fn is_surjective(v: &[u8]) -> bool {
46 v.contains(&0) && v.contains(&1) && v.contains(&2)
47}
48
49/// `σ ∘ δᵢ` — precompose a surjection with the `i`-th coface, i.e. drop coordinate `i`.
50fn drop_coord(v: &[u8], i: usize) -> Vec<u8> {
51 v.iter().enumerate().filter(|&(t, _)| t != i).map(|(_, &x)| x).collect()
52}
53
54/// The face map `dᵢ : Γ_n → Γ_{n-1}` over `A = Z/modulus`. A degree-`n` simplex is an `A`-labeling of
55/// `S_n`; `dᵢ` sends basis `σ` to `σ∘δᵢ` when that stays surjective, else to `0`, summing collisions.
56fn face(modulus: usize, n: usize, i: usize, x: &[usize]) -> Vec<usize> {
57 let sn = surjections(n);
58 let sn1 = surjections(n - 1);
59 let idx: HashMap<Vec<u8>, usize> = sn1.iter().cloned().enumerate().map(|(k, v)| (v, k)).collect();
60 let mut out = vec![0usize; sn1.len()];
61 for (k, sigma) in sn.iter().enumerate() {
62 let d = drop_coord(sigma, i);
63 if is_surjective(&d) {
64 let t = idx[&d];
65 out[t] = (out[t] + x[k]) % modulus;
66 }
67 }
68 out
69}
70
71/// Every degree-`n` simplex — all `A`-labelings of `S_n` (`|A|^{|S_n|}` of them).
72fn gamma(modulus: usize, n: usize) -> Vec<Vec<usize>> {
73 let size = surjections(n).len();
74 let mut out: Vec<Vec<usize>> = vec![vec![]];
75 for _ in 0..size {
76 let mut next = Vec::with_capacity(out.len() * modulus);
77 for t in &out {
78 for e in 0..modulus {
79 let mut u = t.clone();
80 u.push(e);
81 next.push(u);
82 }
83 }
84 out = next;
85 }
86 out
87}
88
89fn positions(n: usize, k: usize) -> Vec<usize> {
90 (0..=n).filter(|&i| i != k).collect()
91}
92
93/// The number of degree-`n` simplices filling the horn `Λⁿ_k` whose given faces are all the chosen
94/// `horn[i]` (`i ≠ k`).
95fn filler_count(modulus: usize, n: usize, k: usize, horn: &HashMap<usize, Vec<usize>>) -> usize {
96 gamma(modulus, n)
97 .iter()
98 .filter(|y| positions(n, k).iter().all(|&p| &face(modulus, n, p, y) == &horn[&p]))
99 .count()
100}
101
102/// `|ker hₖ|` — degree-`n` simplices whose faces `i ≠ k` are all zero. Equals the number of fillers of
103/// the *trivial* horn; `> 1` means a horn fills non-uniquely (genuine higher cells in that degree).
104fn kernel_size(modulus: usize, n: usize, k: usize) -> usize {
105 gamma(modulus, n)
106 .iter()
107 .filter(|y| positions(n, k).iter().all(|&p| face(modulus, n, p, y).iter().all(|&c| c == 0)))
108 .count()
109}
110
111/// `|im hₖ|` — the number of distinct face-tuples `(dᵢ y)_{i≠k}` realized by some degree-`n` simplex.
112fn image_size(modulus: usize, n: usize, k: usize) -> usize {
113 let mut images: HashSet<Vec<Vec<usize>>> = HashSet::new();
114 for y in gamma(modulus, n) {
115 images.insert(positions(n, k).iter().map(|&p| face(modulus, n, p, &y)).collect());
116 }
117 images.len()
118}
119
120/// The number of **compatible horns** `Λⁿ_k`: tuples `(x_i)_{i≠k}` of degree-`(n-1)` simplices satisfying
121/// the simplicial identities `d_a x_b = d_{b-1} x_a` (`a < b`). The horn maps `hₖ` is onto these iff Kan.
122fn compatible_horn_count(modulus: usize, n: usize, k: usize) -> usize {
123 let pos = positions(n, k);
124 let lower = gamma(modulus, n - 1);
125 let base = lower.len();
126 let total: u128 = (base as u128).pow(pos.len() as u32);
127 let mut count = 0;
128 for idx in 0..total {
129 let mut x = idx;
130 let horn: HashMap<usize, Vec<usize>> = pos
131 .iter()
132 .map(|&p| {
133 let a = (x % base as u128) as usize;
134 x /= base as u128;
135 (p, lower[a].clone())
136 })
137 .collect();
138 // simplicial identity d_a x_b = d_{b-1} x_a for every pair a < b of given positions
139 let ok = pos.iter().enumerate().all(|(ai, &a)| {
140 pos[ai + 1..].iter().all(|&b| face(modulus, n - 1, a, &horn[&b]) == face(modulus, n - 1, b - 1, &horn[&a]))
141 });
142 if ok {
143 count += 1;
144 }
145 }
146 count
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn trivial_horn(n: usize, k: usize, modulus: usize) -> HashMap<usize, Vec<usize>> {
154 // the horn whose given faces are all zero (lives in Γ_{n-1})
155 let zero = vec![0usize; surjections(n - 1).len()];
156 positions(n, k).iter().map(|&p| (p, zero.clone())).collect()
157 }
158
159 #[test]
160 fn the_2_type_has_a_nonunique_inner_filler_so_pi2_is_nonzero() {
161 // π₂ ≠ 0, INSIDE THE OBJECT. The inner horn Λ²₁ has |A| fillers — the 2-cells — not the unique
162 // filler a 1-type forces. This is precisely the structure kan_complex proved K(G,1) lacks:
163 // there every inner horn filled uniquely; here a whole copy of A fills the same horn.
164 for modulus in [2usize, 3, 4] {
165 let fillers = filler_count(modulus, 2, 1, &trivial_horn(2, 1, modulus));
166 assert_eq!(fillers, modulus, "Λ²₁ has |A| = {modulus} fillers — π₂ = A, genuinely 2-dimensional");
167 assert!(fillers > 1, "non-unique inner filler ⇒ NOT 1-truncated (this is what K(G,1) cannot do)");
168 }
169 }
170
171 #[test]
172 fn the_2_type_is_a_kan_complex_every_horn_fills() {
173 // It IS a Kan complex (an ∞-groupoid): the face-tuple map hₖ is SURJECTIVE onto the compatible
174 // horns in every degree we check (2, 3, 4) and position — so every horn fills. A simplicial
175 // abelian group is Kan; we confirm it by counting, not by appeal.
176 let modulus = 2;
177 for (n, ks) in [(2usize, vec![0, 1, 2]), (3, vec![0, 1, 2, 3])] {
178 for k in ks {
179 assert_eq!(
180 image_size(modulus, n, k),
181 compatible_horn_count(modulus, n, k),
182 "Λ^{n}_{k} fills: image of hₖ = all compatible horns"
183 );
184 }
185 }
186 // degree 4, inner horns: still surjective (Kan)
187 for k in 1..4 {
188 assert_eq!(image_size(modulus, 4, k), compatible_horn_count(modulus, 4, k), "Λ⁴_{k} fills (Kan)");
189 }
190 }
191
192 #[test]
193 fn the_2_type_realizes_the_crossed_modules_admitted_pi2_admit_equals_have() {
194 // SYMMETRY BREAKING, one level up — and the campaign's loop CLOSES. two_group's crossed module
195 // A → 1 (g = trivial, h = A, ∂ = 0) ADMITS π₂ = ker∂ = A, algebraically. This K(A,2) Kan complex
196 // HAS π₂ = A geometrically: the inner horn Λ²₁ has exactly |A| fillers. They are the SAME A — the
197 // 2-group that admits the higher symmetry is realized by the object that carries it: admit = have.
198 // And the |A| fillers form an A-TORSOR: breaking that symmetry (choosing the canonical filler) is
199 // the very π₀ orbit-collapse this whole campaign began with, now acting on 2-cells. Symmetry
200 // breaking runs from π₀ all the way up the tower.
201 use crate::two_group::{CrossedModule, FiniteGroup};
202 for m in [2usize, 3, 4] {
203 let cm = CrossedModule { g: FiniteGroup::trivial(), h: FiniteGroup::cyclic(m), partial: vec![0; m] };
204 let admitted = cm.pi_two().len(); // algebra: ker ∂ = A
205 let had = filler_count(m, 2, 1, &trivial_horn(2, 1, m)); // geometry: # fillers of Λ²₁
206 assert_eq!(admitted, had, "crossed module ADMITS π₂ = A ⟺ K(A,2) HAS π₂ = A (|A| fillers)");
207 assert_eq!(admitted, m, "and both equal |A| = {m}");
208 }
209 }
210
211 #[test]
212 fn the_2_type_is_exactly_two_truncated() {
213 // EXACTLY 2-truncated. At degree 2 the inner horn fills NON-uniquely (π₂ lives there: kernel = A).
214 // By degree 4 the inner horns fill UNIQUELY again (kernel trivial) — there is no π₃, the higher
215 // homotopy stops at level 2. So this object is a genuine 2-type: more than K(G,1), and no more.
216 let modulus = 2;
217 assert_eq!(kernel_size(modulus, 2, 1), modulus, "degree 2 inner: |ker| = |A| ⇒ π₂ = A");
218 for k in 1..4 {
219 assert_eq!(kernel_size(modulus, 4, k), 1, "degree 4 inner Λ⁴_{k}: unique filler ⇒ no π₃ (2-truncated)");
220 }
221 }
222}