1use crate::matroid::Matroid;
8use crate::EnumerativeError;
9use amari_core::gf2::{GF2Matrix, GF2Vector, GF2};
10use num_rational::Rational64;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct BinaryCode {
17 generator: GF2Matrix,
19 n: usize,
21 k: usize,
23}
24
25impl BinaryCode {
26 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 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 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 #[must_use]
65 pub fn parameters(&self) -> (usize, usize, usize) {
66 (self.n, self.k, self.minimum_distance())
67 }
68
69 #[must_use]
71 pub fn length(&self) -> usize {
72 self.n
73 }
74
75 #[must_use]
77 pub fn dimension(&self) -> usize {
78 self.k
79 }
80
81 #[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 }
92
93 #[must_use]
95 pub fn generator_matrix(&self) -> &GF2Matrix {
96 &self.generator
97 }
98
99 #[must_use]
101 pub fn parity_check_matrix(&self) -> GF2Matrix {
102 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 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 #[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 #[must_use]
146 pub fn is_self_dual(&self) -> bool {
147 if self.k != self.n - self.k {
148 return false;
149 }
150 let gt = self.generator.transpose();
152 let product = self.generator.mul_mat(>);
153 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 pub fn codewords(&self) -> Vec<GF2Vector> {
166 assert!(self.k <= 20, "codeword enumeration limited to k ≤ 20");
167 let gt = self.generator.transpose(); 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)); }
173 words
174 }
175
176 #[must_use]
178 pub fn encode(&self, message: &GF2Vector) -> GF2Vector {
179 assert_eq!(message.dim(), self.k);
180 let gt = self.generator.transpose(); gt.mul_vec(message) }
183
184 #[must_use]
186 pub fn syndrome(&self, received: &GF2Vector) -> GF2Vector {
187 let h = self.parity_check_matrix();
188 h.mul_vec(received)
192 }
193
194 #[must_use]
196 pub fn matroid(&self) -> Matroid {
197 crate::representability::column_matroid(&self.generator)
198 }
199
200 #[must_use]
204 pub fn weight_enumerator(&self) -> Vec<u64> {
205 self.weight_distribution()
206 }
207
208 #[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 #[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
241fn 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#[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 let mut h = GF2Matrix::zero(n_minus_k, n);
281 for col in 0..n {
282 let val = col + 1; 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#[must_use]
297pub fn simplex_code(r: usize) -> BinaryCode {
298 hamming_code(r).dual()
299}
300
301pub 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 let points: Vec<Vec<u8>> = (0..n)
322 .map(|v| (0..m).map(|i| ((v >> i) & 1) as u8).collect())
323 .collect();
324
325 let mut generator_rows = Vec::new();
327 for deg in 0..=r {
328 for subset in k_subsets_vec(m, deg) {
329 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#[must_use]
347pub fn extended_golay_code() -> BinaryCode {
348 #[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 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#[must_use]
389pub fn singleton_bound(n: usize, k: usize) -> usize {
390 n - k + 1
391}
392
393#[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#[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#[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 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 assert_eq!(simp.minimum_distance(), 4);
505 }
506
507 #[test]
508 fn test_macwilliams_consistency() {
509 let ham = hamming_code(3);
510 let dual_we = ham.dual_weight_enumerator();
512 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 assert_eq!(golay.minimum_distance(), 8);
557 }
558}