Skip to main content

amari_enumerative/
weight_enumerator.rs

1//! Weight enumerators for binary linear codes.
2//!
3//! Provides the `BinaryCode` type for [n, k, d] binary linear codes,
4//! weight enumerator computation, MacWilliams identity, and standard codes
5//! (Hamming, simplex, Reed-Muller).
6
7use crate::matroid::Matroid;
8use crate::EnumerativeError;
9use amari_core::gf2::{GF2Matrix, GF2Vector, GF2};
10use num_rational::Rational64;
11
12/// A binary linear code [n, k, d].
13///
14/// k-dimensional subspace of GF(2)^n with minimum distance d.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct BinaryCode {
17    /// Generator matrix (k × n), in systematic form if possible.
18    generator: GF2Matrix,
19    /// Code length.
20    n: usize,
21    /// Code dimension.
22    k: usize,
23}
24
25impl BinaryCode {
26    /// Create a code from its generator matrix.
27    ///
28    /// The matrix is reduced to RREF. Its rank determines k.
29    pub fn from_generator(matrix: GF2Matrix) -> Result<Self, EnumerativeError> {
30        let n = matrix.ncols();
31        let mut gen = matrix;
32        gen.reduced_row_echelon();
33        let k = gen.rank();
34        if k == 0 {
35            return Err(EnumerativeError::CodeError(
36                "generator matrix has rank 0".to_string(),
37            ));
38        }
39        // Keep only the k nonzero rows.
40        let rows: Vec<GF2Vector> = (0..k).map(|i| gen.row(i).clone()).collect();
41        let gen = GF2Matrix::from_rows(rows);
42        Ok(Self {
43            generator: gen,
44            n,
45            k,
46        })
47    }
48
49    /// Create a code from its parity check matrix H.
50    ///
51    /// The code is C = ker(H).
52    pub fn from_parity_check(h: GF2Matrix) -> Result<Self, EnumerativeError> {
53        let null = h.null_space();
54        if null.is_empty() {
55            return Err(EnumerativeError::CodeError(
56                "parity check matrix has trivial null space".to_string(),
57            ));
58        }
59        let gen = GF2Matrix::from_rows(null);
60        Self::from_generator(gen)
61    }
62
63    /// Code parameters [n, k, d].
64    #[must_use]
65    pub fn parameters(&self) -> (usize, usize, usize) {
66        (self.n, self.k, self.minimum_distance())
67    }
68
69    /// Code length n.
70    #[must_use]
71    pub fn length(&self) -> usize {
72        self.n
73    }
74
75    /// Code dimension k.
76    #[must_use]
77    pub fn dimension(&self) -> usize {
78        self.k
79    }
80
81    /// Minimum distance d (minimum Hamming weight of nonzero codewords).
82    #[must_use]
83    pub fn minimum_distance(&self) -> usize {
84        let dist = self.weight_distribution();
85        for (i, &count) in dist.iter().enumerate() {
86            if i > 0 && count > 0 {
87                return i;
88            }
89        }
90        self.n // shouldn't happen for a valid code
91    }
92
93    /// Generator matrix.
94    #[must_use]
95    pub fn generator_matrix(&self) -> &GF2Matrix {
96        &self.generator
97    }
98
99    /// Parity check matrix H such that H * c = 0 for all codewords c.
100    #[must_use]
101    pub fn parity_check_matrix(&self) -> GF2Matrix {
102        // We need H such that for all codewords c, H * c = 0.
103        // Equivalently, the rows of H span the orthogonal complement of the row space of G.
104        // If G is in RREF [I_k | P], then H = [P^T | I_{n-k}] (columns reordered to match pivots).
105
106        // Get RREF and pivots.
107        let mut rref = self.generator.clone();
108        let pivots = rref.reduced_row_echelon();
109        let n_k = self.n - self.k;
110
111        if n_k == 0 {
112            return GF2Matrix::zero(0, self.n);
113        }
114
115        let free_cols: Vec<usize> = (0..self.n).filter(|c| !pivots.contains(c)).collect();
116
117        // Build H: (n-k) × n matrix.
118        // For each free column f_j (j=0..n-k-1):
119        //   Row j of H has 1 at position f_j (identity part)
120        //   and for each pivot column p_i, H[j][p_i] = RREF[i][f_j]
121        let mut h = GF2Matrix::zero(n_k, self.n);
122        for (j, &fc) in free_cols.iter().enumerate() {
123            h.set(j, fc, GF2::ONE);
124            for (i, &pc) in pivots.iter().enumerate() {
125                let val = rref.get(i, fc);
126                h.set(j, pc, val);
127            }
128        }
129
130        h
131    }
132
133    /// The dual code C^⊥.
134    #[must_use]
135    pub fn dual(&self) -> BinaryCode {
136        let h = self.parity_check_matrix();
137        BinaryCode {
138            n: self.n,
139            k: self.n - self.k,
140            generator: h,
141        }
142    }
143
144    /// Whether the code is self-dual (C = C^⊥).
145    #[must_use]
146    pub fn is_self_dual(&self) -> bool {
147        if self.k != self.n - self.k {
148            return false;
149        }
150        // C is self-dual iff G·G^T = 0 (all codewords are mutually orthogonal)
151        let gt = self.generator.transpose();
152        let product = self.generator.mul_mat(&gt);
153        // Check all entries are zero
154        for i in 0..product.nrows() {
155            for j in 0..product.ncols() {
156                if product.get(i, j).value() != 0 {
157                    return false;
158                }
159            }
160        }
161        true
162    }
163
164    /// Enumerate all 2^k codewords.
165    pub fn codewords(&self) -> Vec<GF2Vector> {
166        assert!(self.k <= 20, "codeword enumeration limited to k ≤ 20");
167        let gt = self.generator.transpose(); // n × k
168        let mut words = Vec::with_capacity(1 << self.k);
169        for msg in 0..(1u64 << self.k) {
170            let message = GF2Vector::from_u64(self.k, msg);
171            words.push(gt.mul_vec(&message)); // n × k times k-vec = n-vec
172        }
173        words
174    }
175
176    /// Encode a k-bit message into an n-bit codeword.
177    #[must_use]
178    pub fn encode(&self, message: &GF2Vector) -> GF2Vector {
179        assert_eq!(message.dim(), self.k);
180        let gt = self.generator.transpose(); // n × k
181        gt.mul_vec(message) // n × k times k-vec = n-vec
182    }
183
184    /// Syndrome of a received vector.
185    #[must_use]
186    pub fn syndrome(&self, received: &GF2Vector) -> GF2Vector {
187        let h = self.parity_check_matrix();
188        // Syndrome = H * r (treating r as column vector)
189        // But our mul_vec does M * v, so we need H * received.
190        // Since H is (n-k) × n and received is n-dimensional, this works.
191        h.mul_vec(received)
192    }
193
194    /// The column matroid of the generator matrix.
195    #[must_use]
196    pub fn matroid(&self) -> Matroid {
197        crate::representability::column_matroid(&self.generator)
198    }
199
200    /// Weight enumerator coefficients A_0, A_1, ..., A_n.
201    ///
202    /// A_i = number of codewords of Hamming weight i.
203    #[must_use]
204    pub fn weight_enumerator(&self) -> Vec<u64> {
205        self.weight_distribution()
206    }
207
208    /// Weight distribution: A_i = number of codewords of Hamming weight i.
209    #[must_use]
210    pub fn weight_distribution(&self) -> Vec<u64> {
211        let mut dist = vec![0u64; self.n + 1];
212        for codeword in self.codewords() {
213            dist[codeword.weight()] += 1;
214        }
215        dist
216    }
217
218    /// MacWilliams identity: dual weight enumerator computed from transform.
219    ///
220    /// W_{C⊥}[j] = (1/2^k) · Σ_i W_C[i] · K_j(i; n)
221    /// where K_j(i; n) is the Krawtchouk polynomial.
222    #[must_use]
223    pub fn dual_weight_enumerator(&self) -> Vec<Rational64> {
224        let w = self.weight_distribution();
225        let n = self.n;
226        let size = Rational64::from(1i64 << self.k);
227
228        let mut dual_w = vec![Rational64::from(0); n + 1];
229        for (j, dw_j) in dual_w.iter_mut().enumerate() {
230            let mut sum = Rational64::from(0);
231            for (i, &wi) in w.iter().enumerate() {
232                let k_val = krawtchouk(j, i, n);
233                sum += Rational64::from(wi as i64) * Rational64::from(k_val);
234            }
235            *dw_j = sum / size;
236        }
237        dual_w
238    }
239}
240
241/// Krawtchouk polynomial K_j(i; n) = Σ_s (-1)^s C(i,s) C(n-i, j-s).
242fn krawtchouk(j: usize, i: usize, n: usize) -> i64 {
243    let mut sum = 0i64;
244    for s in 0..=j.min(i) {
245        if j >= s && n >= i && (n - i) >= (j - s) {
246            let sign = if s % 2 == 0 { 1i64 } else { -1 };
247            let c1 = binomial(i, s) as i64;
248            let c2 = binomial(n - i, j - s) as i64;
249            sum += sign * c1 * c2;
250        }
251    }
252    sum
253}
254
255fn binomial(n: usize, k: usize) -> u64 {
256    if k > n {
257        return 0;
258    }
259    if k == 0 || k == n {
260        return 1;
261    }
262    let k = k.min(n - k);
263    let mut result: u64 = 1;
264    for i in 0..k {
265        result = result * (n - i) as u64 / (i + 1) as u64;
266    }
267    result
268}
269
270/// Construct the Hamming code Ham(r, 2).
271///
272/// Parameters: [2^r − 1, 2^r − 1 − r, 3].
273#[must_use]
274pub fn hamming_code(r: usize) -> BinaryCode {
275    assert!((2..=10).contains(&r), "Hamming code r must be in [2, 10]");
276    let n = (1 << r) - 1;
277    let n_minus_k = r;
278
279    // Parity check matrix: all nonzero r-bit vectors as columns.
280    let mut h = GF2Matrix::zero(n_minus_k, n);
281    for col in 0..n {
282        let val = col + 1; // nonzero r-bit value
283        for bit in 0..r {
284            if (val >> bit) & 1 == 1 {
285                h.set(bit, col, GF2::ONE);
286            }
287        }
288    }
289
290    BinaryCode::from_parity_check(h).expect("Hamming code construction should not fail")
291}
292
293/// Construct the simplex code (dual of Hamming code).
294///
295/// Parameters: [2^r − 1, r, 2^(r−1)].
296#[must_use]
297pub fn simplex_code(r: usize) -> BinaryCode {
298    hamming_code(r).dual()
299}
300
301/// Construct the Reed-Muller code RM(r, m).
302///
303/// Parameters: [2^m, Σ_{i=0}^{r} C(m,i), 2^(m−r)].
304pub fn reed_muller_code(r: usize, m: usize) -> Result<BinaryCode, EnumerativeError> {
305    if r > m {
306        return Err(EnumerativeError::CodeError(format!(
307            "Reed-Muller order r={} must be <= m={}",
308            r, m
309        )));
310    }
311    if m > 10 {
312        return Err(EnumerativeError::CodeError(format!(
313            "Reed-Muller m={} too large (max 10)",
314            m
315        )));
316    }
317
318    let n = 1usize << m;
319
320    // Evaluation points: all m-bit vectors.
321    let points: Vec<Vec<u8>> = (0..n)
322        .map(|v| (0..m).map(|i| ((v >> i) & 1) as u8).collect())
323        .collect();
324
325    // Monomials of degree ≤ r: subsets of {0,...,m-1} of size ≤ r.
326    let mut generator_rows = Vec::new();
327    for deg in 0..=r {
328        for subset in k_subsets_vec(m, deg) {
329            // Evaluate monomial x_{i1} · x_{i2} · ... at each point.
330            let mut row = GF2Vector::zero(n);
331            for (j, point) in points.iter().enumerate() {
332                let val: u8 = subset.iter().map(|&i| point[i]).product();
333                if val == 1 {
334                    row.set(j, GF2::ONE);
335                }
336            }
337            generator_rows.push(row);
338        }
339    }
340
341    let gen = GF2Matrix::from_rows(generator_rows);
342    BinaryCode::from_generator(gen)
343}
344
345/// Construct the extended Golay code [24, 12, 8].
346#[must_use]
347pub fn extended_golay_code() -> BinaryCode {
348    // The extended binary Golay code generator matrix.
349    // P matrix (12×12) from the standard construction.
350    #[rustfmt::skip]
351    let p: [[u8; 12]; 12] = [
352        [1,1,0,1,1,1,0,0,0,1,0,1],
353        [1,0,1,1,1,0,0,0,1,0,1,1],
354        [0,1,1,1,0,0,0,1,0,1,1,1],
355        [1,1,1,0,0,0,1,0,1,1,0,1],
356        [1,1,0,0,0,1,0,1,1,0,1,1],
357        [1,0,0,0,1,0,1,1,0,1,1,1],
358        [0,0,0,1,0,1,1,0,1,1,1,1],
359        [0,0,1,0,1,1,0,1,1,1,0,1],
360        [0,1,0,1,1,0,1,1,1,0,0,1],
361        [1,0,1,1,0,1,1,1,0,0,0,1],
362        [0,1,1,0,1,1,1,0,0,0,1,1],
363        [1,1,1,1,1,1,1,1,1,1,1,0],
364    ];
365
366    let k = 12;
367    let n = 24;
368    let mut gen = GF2Matrix::zero(k, n);
369
370    // [I_12 | P]
371    for (i, p_row) in p.iter().enumerate().take(k) {
372        gen.set(i, i, GF2::ONE);
373        for (j, &p_val) in p_row.iter().enumerate() {
374            if p_val == 1 {
375                gen.set(i, k + j, GF2::ONE);
376            }
377        }
378    }
379
380    BinaryCode {
381        generator: gen,
382        n,
383        k,
384    }
385}
386
387/// Singleton bound: d ≤ n − k + 1.
388#[must_use]
389pub fn singleton_bound(n: usize, k: usize) -> usize {
390    n - k + 1
391}
392
393/// Hamming bound: maximum number of correctable errors for [n, k] code.
394///
395/// t such that Σ_{i=0}^{t} C(n, i) ≤ 2^(n−k).
396#[must_use]
397pub fn hamming_bound(n: usize, k: usize) -> usize {
398    let sphere_cap = 1u64 << (n - k);
399    let mut sum = 0u64;
400    for t in 0..=n {
401        sum += binomial(n, t);
402        if sum > sphere_cap {
403            return if t > 0 { t - 1 } else { 0 };
404        }
405    }
406    n
407}
408
409/// Plotkin bound: d ≤ 2^(k−1) · n / (2^k − 1) for d even.
410///
411/// Returns None if the bound doesn't apply.
412#[must_use]
413pub fn plotkin_bound(n: usize, k: usize) -> Option<usize> {
414    if k == 0 {
415        return Some(n);
416    }
417    let two_k = 1u64 << k;
418    let two_k_minus_1 = 1u64 << (k - 1);
419    let bound = two_k_minus_1 * n as u64 / (two_k - 1);
420    Some(bound as usize)
421}
422
423/// Gilbert-Varshamov bound: maximum guaranteed minimum distance.
424///
425/// Largest d such that Σ_{i=0}^{d−2} C(n−1, i) < 2^(n−k).
426#[must_use]
427pub fn gilbert_varshamov_bound(n: usize, k: usize) -> usize {
428    if n <= k {
429        return 1;
430    }
431    let target = 1u64 << (n - k);
432    let mut sum = 0u64;
433    for d in 1..=n {
434        if d >= 2 {
435            sum += binomial(n - 1, d - 2);
436        }
437        if sum >= target {
438            return d - 1;
439        }
440    }
441    n
442}
443
444fn k_subsets_vec(n: usize, k: usize) -> Vec<Vec<usize>> {
445    let mut result = Vec::new();
446    let mut current = Vec::with_capacity(k);
447    gen_subsets_vec(n, k, 0, &mut current, &mut result);
448    result
449}
450
451fn gen_subsets_vec(
452    n: usize,
453    k: usize,
454    start: usize,
455    current: &mut Vec<usize>,
456    result: &mut Vec<Vec<usize>>,
457) {
458    if current.len() == k {
459        result.push(current.clone());
460        return;
461    }
462    let remaining = k - current.len();
463    if start + remaining > n {
464        return;
465    }
466    for i in start..=(n - remaining) {
467        current.push(i);
468        gen_subsets_vec(n, k, i + 1, current, result);
469        current.pop();
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn test_hamming_code_parameters() {
479        let ham = hamming_code(3);
480        let (n, k, d) = ham.parameters();
481        assert_eq!(n, 7);
482        assert_eq!(k, 4);
483        assert_eq!(d, 3);
484    }
485
486    #[test]
487    fn test_hamming_weight_distribution() {
488        let ham = hamming_code(3);
489        let dist = ham.weight_distribution();
490        // [7,4,3] Hamming code: A_0=1, A_3=7, A_4=7, A_7=1
491        assert_eq!(dist[0], 1);
492        assert_eq!(dist[3], 7);
493        assert_eq!(dist[4], 7);
494        assert_eq!(dist[7], 1);
495    }
496
497    #[test]
498    fn test_simplex_is_dual_of_hamming() {
499        let _ham = hamming_code(3);
500        let simp = simplex_code(3);
501        assert_eq!(simp.n, 7);
502        assert_eq!(simp.k, 3);
503        // Simplex code minimum distance = 2^(r-1) = 4
504        assert_eq!(simp.minimum_distance(), 4);
505    }
506
507    #[test]
508    fn test_macwilliams_consistency() {
509        let ham = hamming_code(3);
510        // Compute dual weight enumerator via MacWilliams.
511        let dual_we = ham.dual_weight_enumerator();
512        // Compute dual weight enumerator directly.
513        let dual = ham.dual();
514        let direct_we = dual.weight_distribution();
515
516        for (j, &d) in direct_we.iter().enumerate() {
517            let from_transform = dual_we[j];
518            assert_eq!(
519                from_transform,
520                Rational64::from(d as i64),
521                "MacWilliams mismatch at weight {}",
522                j
523            );
524        }
525    }
526
527    #[test]
528    fn test_reed_muller_rm1_3() {
529        let rm = reed_muller_code(1, 3).unwrap();
530        let (n, k, d) = rm.parameters();
531        assert_eq!(n, 8);
532        assert_eq!(k, 4);
533        assert_eq!(d, 4);
534    }
535
536    #[test]
537    fn test_singleton_bound() {
538        assert_eq!(singleton_bound(7, 4), 4);
539    }
540
541    #[test]
542    fn test_encode_syndrome_roundtrip() {
543        let ham = hamming_code(3);
544        let msg = GF2Vector::from_bits(&[1, 0, 1, 1]);
545        let codeword = ham.encode(&msg);
546        let syn = ham.syndrome(&codeword);
547        assert!(syn.is_zero(), "syndrome of valid codeword should be zero");
548    }
549
550    #[test]
551    fn test_extended_golay_code() {
552        let golay = extended_golay_code();
553        assert_eq!(golay.length(), 24);
554        assert_eq!(golay.dimension(), 12);
555        // Minimum distance should be 8.
556        assert_eq!(golay.minimum_distance(), 8);
557    }
558}