1use crate::matroid::Matroid;
8use amari_core::gf2::{GF2Matrix, GF2};
9use std::collections::BTreeSet;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum RepresentabilityResult {
14 Representable(GF2Matrix),
16 NotRepresentable,
18 Inconclusive { reason: String },
20}
21
22pub fn is_binary(matroid: &Matroid) -> RepresentabilityResult {
27 if matroid.has_excluded_minor("U24") {
29 return RepresentabilityResult::NotRepresentable;
30 }
31
32 match find_gf2_representation(matroid) {
34 Some(matrix) => RepresentabilityResult::Representable(matrix),
35 None => {
36 let k = matroid.rank;
37 let n = matroid.ground_set_size;
38 let free = k * (n - k);
39 if free > 24 {
40 RepresentabilityResult::Inconclusive {
41 reason: format!("search space 2^{} too large", free),
42 }
43 } else {
44 RepresentabilityResult::NotRepresentable
45 }
46 }
47 }
48}
49
50pub fn is_ternary(matroid: &Matroid) -> RepresentabilityResult {
54 if matroid.has_excluded_minor("F7") {
59 return RepresentabilityResult::NotRepresentable;
60 }
61 if matroid.has_excluded_minor("F7*") {
62 return RepresentabilityResult::NotRepresentable;
63 }
64
65 let k = matroid.rank;
67 let n = matroid.ground_set_size;
68 let free = k * (n - k);
69
70 if free > 16 {
71 return RepresentabilityResult::Inconclusive {
72 reason: format!("GF(3) search space 3^{} too large", free),
73 };
74 }
75
76 match find_gf3_representation(matroid) {
77 Some(matrix) => {
78 RepresentabilityResult::Representable(matrix)
82 }
83 None => RepresentabilityResult::NotRepresentable,
84 }
85}
86
87pub fn is_representable_over_gfq(matroid: &Matroid, q: u64) -> RepresentabilityResult {
89 match q {
90 2 => is_binary(matroid),
91 3 => is_ternary(matroid),
92 _ => {
93 let k = matroid.rank;
94 let n = matroid.ground_set_size;
95 let free = k * (n - k);
96 if free > 12 {
97 RepresentabilityResult::Inconclusive {
98 reason: format!("GF({}) search space {}^{} too large", q, q, free),
99 }
100 } else {
101 match find_gfq_representation(matroid, q) {
103 Some(matrix) => RepresentabilityResult::Representable(matrix),
104 None => RepresentabilityResult::NotRepresentable,
105 }
106 }
107 }
108 }
109}
110
111pub fn is_regular(matroid: &Matroid) -> bool {
115 !matroid.has_excluded_minor("U24")
117 && !matroid.has_excluded_minor("F7")
118 && !matroid.has_excluded_minor("F7*")
119}
120
121pub fn has_minor(matroid: &Matroid, minor: &Matroid) -> bool {
126 check_has_minor(matroid, minor)
129}
130
131fn check_has_minor(matroid: &Matroid, minor: &Matroid) -> bool {
132 let n = matroid.ground_set_size;
133 let m_n = minor.ground_set_size;
134 let m_r = minor.rank;
135
136 if n < m_n || matroid.rank < m_r {
137 return false;
138 }
139 if n == m_n {
140 return matroid.bases == minor.bases;
141 }
142
143 for e in 0..n {
145 let deleted = matroid.delete(e);
146 if check_has_minor(&deleted, minor) {
147 return true;
148 }
149 let contracted = matroid.contract(e);
150 if check_has_minor(&contracted, minor) {
151 return true;
152 }
153 }
154 false
155}
156
157#[must_use]
161pub fn fano_matroid() -> Matroid {
162 let n = 7;
164 let k = 3;
165 let lines: Vec<BTreeSet<usize>> = vec![
166 [0, 1, 3].iter().copied().collect(),
167 [1, 2, 4].iter().copied().collect(),
168 [2, 3, 5].iter().copied().collect(),
169 [3, 4, 6].iter().copied().collect(),
170 [0, 4, 5].iter().copied().collect(),
171 [1, 5, 6].iter().copied().collect(),
172 [0, 2, 6].iter().copied().collect(),
173 ];
174
175 let all_triples: Vec<BTreeSet<usize>> = k_subsets_vec(n, k)
176 .into_iter()
177 .map(|v| v.into_iter().collect())
178 .collect();
179 let bases: BTreeSet<BTreeSet<usize>> = all_triples
180 .into_iter()
181 .filter(|t| !lines.contains(t))
182 .collect();
183
184 Matroid {
185 ground_set_size: n,
186 bases,
187 rank: k,
188 }
189}
190
191#[must_use]
195pub fn dual_fano_matroid() -> Matroid {
196 fano_matroid().dual()
197}
198
199pub fn standard_representation(matroid: &Matroid) -> Option<GF2Matrix> {
203 match is_binary(matroid) {
204 RepresentabilityResult::Representable(m) => Some(m),
205 _ => None,
206 }
207}
208
209#[must_use]
213pub fn column_matroid(matrix: &GF2Matrix) -> Matroid {
214 let k = matrix.nrows();
215 let n = matrix.ncols();
216
217 let mut bases = BTreeSet::new();
218
219 for subset in k_subsets_vec(n, k) {
220 let sub = extract_columns(matrix, &subset);
221 if sub.rank() == k {
222 let basis: BTreeSet<usize> = subset.into_iter().collect();
223 bases.insert(basis);
224 }
225 }
226
227 Matroid {
228 ground_set_size: n,
229 bases,
230 rank: k,
231 }
232}
233
234fn find_gf2_representation(matroid: &Matroid) -> Option<GF2Matrix> {
237 let k = matroid.rank;
238 let n = matroid.ground_set_size;
239 if k == 0 {
240 return Some(GF2Matrix::zero(0, n));
241 }
242
243 let free_entries = k * (n - k);
244 if free_entries > 24 {
245 return None;
246 }
247
248 for bits in 0..(1u64 << free_entries) {
249 let matrix = build_rref_matrix_gf2(k, n, bits);
250 let candidate = column_matroid(&matrix);
251 if candidate.bases == matroid.bases {
252 return Some(matrix);
253 }
254 }
255 None
256}
257
258fn build_rref_matrix_gf2(k: usize, n: usize, bits: u64) -> GF2Matrix {
259 let mut m = GF2Matrix::zero(k, n);
261 for i in 0..k {
262 m.set(i, i, GF2::ONE);
263 }
264
265 let mut bit_idx = 0;
266 for i in 0..k {
267 for j in k..n {
268 if (bits >> bit_idx) & 1 == 1 {
269 m.set(i, j, GF2::ONE);
270 }
271 bit_idx += 1;
272 }
273 }
274 m
275}
276
277fn extract_columns(matrix: &GF2Matrix, cols: &[usize]) -> GF2Matrix {
278 let k = matrix.nrows();
279 let new_n = cols.len();
280 let mut sub = GF2Matrix::zero(k, new_n);
281 for (new_j, &old_j) in cols.iter().enumerate() {
282 for i in 0..k {
283 sub.set(i, new_j, matrix.get(i, old_j));
284 }
285 }
286 sub
287}
288
289fn find_gf3_representation(matroid: &Matroid) -> Option<GF2Matrix> {
291 let k = matroid.rank;
292 let n = matroid.ground_set_size;
293 if k == 0 {
294 return Some(GF2Matrix::zero(0, n));
295 }
296
297 let free_entries = k * (n - k);
298 if free_entries > 16 {
299 return None;
300 }
301
302 let total = 3u64.pow(free_entries as u32);
304 for pattern in 0..total {
305 let matrix_gf3 = build_rref_matrix_gf3(k, n, pattern);
306 let candidate = column_matroid_gf3(&matrix_gf3, k, n);
308 if candidate.bases == matroid.bases {
309 let mut result = GF2Matrix::zero(k, n);
311 for i in 0..k {
312 for j in 0..n {
313 if matrix_gf3[i * n + j] != 0 {
314 result.set(i, j, GF2::ONE);
315 }
316 }
317 }
318 return Some(result);
319 }
320 }
321 None
322}
323
324fn build_rref_matrix_gf3(k: usize, n: usize, pattern: u64) -> Vec<u8> {
325 let mut m = vec![0u8; k * n];
326 for i in 0..k {
328 m[i * n + i] = 1;
329 }
330 let mut p = pattern;
332 for i in 0..k {
333 for j in k..n {
334 m[i * n + j] = (p % 3) as u8;
335 p /= 3;
336 }
337 }
338 m
339}
340
341fn column_matroid_gf3(matrix: &[u8], k: usize, n: usize) -> Matroid {
342 let mut bases = BTreeSet::new();
343
344 for subset in k_subsets_vec(n, k) {
345 if gf3_columns_independent(matrix, k, n, &subset) {
346 let basis: BTreeSet<usize> = subset.into_iter().collect();
347 bases.insert(basis);
348 }
349 }
350
351 Matroid {
352 ground_set_size: n,
353 bases,
354 rank: k,
355 }
356}
357
358fn gf3_columns_independent(matrix: &[u8], k: usize, n: usize, cols: &[usize]) -> bool {
359 let m = cols.len();
361 let mut sub = vec![0u8; k * m];
362 for (new_j, &old_j) in cols.iter().enumerate() {
363 for i in 0..k {
364 sub[i * m + new_j] = matrix[i * n + old_j];
365 }
366 }
367
368 let mut pivot_row = 0;
370 for col in 0..m {
371 let mut found = None;
373 for row in pivot_row..k {
374 if sub[row * m + col] != 0 {
375 found = Some(row);
376 break;
377 }
378 }
379 let Some(pr) = found else { continue };
380
381 if pr != pivot_row {
383 for c in 0..m {
384 sub.swap(pr * m + c, pivot_row * m + c);
385 }
386 }
387
388 let inv = gf3_inv(sub[pivot_row * m + col]);
390 for c in 0..m {
391 sub[pivot_row * m + c] = (sub[pivot_row * m + c] * inv) % 3;
392 }
393
394 for row in 0..k {
396 if row != pivot_row && sub[row * m + col] != 0 {
397 let factor = sub[row * m + col];
398 for c in 0..m {
399 sub[row * m + c] =
400 (sub[row * m + c] + 3 - (factor * sub[pivot_row * m + c]) % 3) % 3;
401 }
402 }
403 }
404
405 pivot_row += 1;
406 }
407
408 pivot_row == k
409}
410
411fn gf3_inv(x: u8) -> u8 {
412 match x % 3 {
413 1 => 1,
414 2 => 2,
415 _ => 0, }
417}
418
419fn find_gfq_representation(matroid: &Matroid, q: u64) -> Option<GF2Matrix> {
421 let k = matroid.rank;
422 let n = matroid.ground_set_size;
423 if k == 0 {
424 return Some(GF2Matrix::zero(0, n));
425 }
426
427 let free_entries = k * (n - k);
428 let total = q.checked_pow(free_entries as u32)?;
429
430 if total > 1_000_000 {
431 return None;
432 }
433
434 for pattern in 0..total {
435 let matrix = build_rref_matrix_gfq(k, n, pattern, q);
436 let candidate = column_matroid_gfq(&matrix, k, n, q);
437 if candidate.bases == matroid.bases {
438 let mut result = GF2Matrix::zero(k, n);
439 for i in 0..k {
440 for j in 0..n {
441 if matrix[i * n + j] != 0 {
442 result.set(i, j, GF2::ONE);
443 }
444 }
445 }
446 return Some(result);
447 }
448 }
449 None
450}
451
452fn build_rref_matrix_gfq(k: usize, n: usize, pattern: u64, q: u64) -> Vec<u64> {
453 let mut m = vec![0u64; k * n];
454 for i in 0..k {
455 m[i * n + i] = 1;
456 }
457 let mut p = pattern;
458 for i in 0..k {
459 for j in k..n {
460 m[i * n + j] = p % q;
461 p /= q;
462 }
463 }
464 m
465}
466
467fn column_matroid_gfq(matrix: &[u64], k: usize, n: usize, q: u64) -> Matroid {
468 let mut bases = BTreeSet::new();
469 for subset in k_subsets_vec(n, k) {
470 if gfq_columns_independent(matrix, k, n, &subset, q) {
471 let basis: BTreeSet<usize> = subset.into_iter().collect();
472 bases.insert(basis);
473 }
474 }
475 Matroid {
476 ground_set_size: n,
477 bases,
478 rank: k,
479 }
480}
481
482fn gfq_columns_independent(matrix: &[u64], k: usize, n: usize, cols: &[usize], q: u64) -> bool {
483 let m = cols.len();
484 let mut sub: Vec<u64> = vec![0; k * m];
485 for (new_j, &old_j) in cols.iter().enumerate() {
486 for i in 0..k {
487 sub[i * m + new_j] = matrix[i * n + old_j];
488 }
489 }
490
491 let mut pivot_row = 0;
492 for col in 0..m {
493 let mut found = None;
494 for row in pivot_row..k {
495 if sub[row * m + col] != 0 {
496 found = Some(row);
497 break;
498 }
499 }
500 let Some(pr) = found else { continue };
501
502 if pr != pivot_row {
503 for c in 0..m {
504 sub.swap(pr * m + c, pivot_row * m + c);
505 }
506 }
507
508 let inv = gfq_inv(sub[pivot_row * m + col], q);
509 for c in 0..m {
510 sub[pivot_row * m + c] = (sub[pivot_row * m + c] * inv) % q;
511 }
512
513 for row in 0..k {
514 if row != pivot_row && sub[row * m + col] != 0 {
515 let factor = sub[row * m + col];
516 for c in 0..m {
517 sub[row * m + c] =
518 (sub[row * m + c] + q - (factor * sub[pivot_row * m + c]) % q) % q;
519 }
520 }
521 }
522
523 pivot_row += 1;
524 }
525
526 pivot_row == k
527}
528
529fn gfq_inv(x: u64, q: u64) -> u64 {
530 if x == 0 {
532 return 0;
533 }
534 let mut a = x as i64;
535 let mut b = q as i64;
536 let mut x0 = 0i64;
537 let mut x1 = 1i64;
538 while a > 1 {
539 let quotient = a / b;
540 let temp = b;
541 b = a % b;
542 a = temp;
543 let temp = x0;
544 x0 = x1 - quotient * x0;
545 x1 = temp;
546 }
547 ((x1 % q as i64 + q as i64) % q as i64) as u64
548}
549
550fn k_subsets_vec(n: usize, k: usize) -> Vec<Vec<usize>> {
551 let mut result = Vec::new();
552 let mut current = Vec::with_capacity(k);
553 gen_subsets_vec(n, k, 0, &mut current, &mut result);
554 result
555}
556
557fn gen_subsets_vec(
558 n: usize,
559 k: usize,
560 start: usize,
561 current: &mut Vec<usize>,
562 result: &mut Vec<Vec<usize>>,
563) {
564 if current.len() == k {
565 result.push(current.clone());
566 return;
567 }
568 let remaining = k - current.len();
569 if start + remaining > n {
570 return;
571 }
572 for i in start..=(n - remaining) {
573 current.push(i);
574 gen_subsets_vec(n, k, i + 1, current, result);
575 current.pop();
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 #[test]
584 fn test_u24_not_binary() {
585 let m = Matroid::uniform(2, 4);
586 assert_eq!(is_binary(&m), RepresentabilityResult::NotRepresentable);
587 }
588
589 #[test]
590 fn test_u23_is_binary() {
591 let m = Matroid::uniform(2, 3);
592 match is_binary(&m) {
593 RepresentabilityResult::Representable(matrix) => {
594 assert_eq!(matrix.nrows(), 2);
595 assert_eq!(matrix.ncols(), 3);
596 }
597 other => panic!("Expected Representable, got {:?}", other),
598 }
599 }
600
601 #[test]
602 fn test_fano_is_binary() {
603 let f7 = fano_matroid();
604 match is_binary(&f7) {
605 RepresentabilityResult::Representable(_) => {}
606 other => panic!("F7 should be binary, got {:?}", other),
607 }
608 }
609
610 #[test]
611 fn test_fano_not_ternary() {
612 let f7 = fano_matroid();
613 match is_ternary(&f7) {
614 RepresentabilityResult::NotRepresentable => {}
615 other => panic!("F7 should not be ternary, got {:?}", other),
616 }
617 }
618
619 #[test]
620 fn test_column_matroid_roundtrip() {
621 let m = Matroid::uniform(2, 3);
622 if let RepresentabilityResult::Representable(matrix) = is_binary(&m) {
623 let recovered = column_matroid(&matrix);
624 assert_eq!(recovered.bases, m.bases);
625 }
626 }
627
628 #[test]
629 fn test_is_regular_u23() {
630 let m = Matroid::uniform(2, 3);
631 assert!(is_regular(&m));
632 }
633
634 #[test]
635 fn test_is_not_regular_u24() {
636 let m = Matroid::uniform(2, 4);
637 assert!(!is_regular(&m));
638 }
639
640 #[test]
641 fn test_dual_fano() {
642 let f7star = dual_fano_matroid();
643 assert_eq!(f7star.rank, 4);
644 assert_eq!(f7star.ground_set_size, 7);
645 }
646
647 #[test]
648 fn test_fano_matroid_properties() {
649 let f7 = fano_matroid();
650 assert_eq!(f7.rank, 3);
651 assert_eq!(f7.ground_set_size, 7);
652 assert_eq!(f7.bases.len(), 28);
654 }
655}