Skip to main content

amari_enumerative/
representability.rs

1//! Matroid representability over GF(2), GF(3), and GF(q).
2//!
3//! Checks whether a matroid can be represented as the column matroid of a matrix
4//! over a finite field. Provides excluded minor characterizations, exhaustive
5//! search for small matroids, and standard named matroids (Fano, dual Fano).
6
7use crate::matroid::Matroid;
8use amari_core::gf2::{GF2Matrix, GF2};
9use std::collections::BTreeSet;
10
11/// Result of a representability check.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum RepresentabilityResult {
14    /// The matroid is representable; a witnessing matrix is provided.
15    Representable(GF2Matrix),
16    /// The matroid is not representable over this field.
17    NotRepresentable,
18    /// The check was inconclusive (search space too large).
19    Inconclusive { reason: String },
20}
21
22/// Check if a matroid is representable over GF(2).
23///
24/// For small matroids (k*(n-k) ≤ 24), performs exhaustive search.
25/// Otherwise checks for U_{2,4} minor first.
26pub fn is_binary(matroid: &Matroid) -> RepresentabilityResult {
27    // Quick excluded minor check.
28    if matroid.has_excluded_minor("U24") {
29        return RepresentabilityResult::NotRepresentable;
30    }
31
32    // Try exhaustive search.
33    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
50/// Check if a matroid is representable over GF(3).
51///
52/// Uses local GF(3) arithmetic. Checks excluded minors first.
53pub fn is_ternary(matroid: &Matroid) -> RepresentabilityResult {
54    // GF(3) excluded minors: U_{2,5}, U_{3,5}, F_7^-, F_7^-* (the non-Fano and its dual)
55    // But the classical result is: M is ternary iff it has no U_{2,5} or U_{3,5} minor...
56    // Actually that's not quite right. The excluded minors for GF(3) are: U_{2,5}, U_{3,5}, F_7, F_7*.
57    // Check the two standard ones.
58    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    // For small matroids, try exhaustive GF(3) search.
66    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            // Convert to GF2Matrix representation (just for the type; entries are mod 3).
79            // We'll store the witness as a GF2 matrix with a note that it's really GF(3).
80            // For API simplicity, we return a GF2Matrix with the same shape.
81            RepresentabilityResult::Representable(matrix)
82        }
83        None => RepresentabilityResult::NotRepresentable,
84    }
85}
86
87/// Check if a matroid is representable over GF(q) for prime q.
88pub 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                // Exhaustive search over GF(q).
102                match find_gfq_representation(matroid, q) {
103                    Some(matrix) => RepresentabilityResult::Representable(matrix),
104                    None => RepresentabilityResult::NotRepresentable,
105                }
106            }
107        }
108    }
109}
110
111/// Check if a matroid is regular (representable over every field).
112///
113/// A matroid is regular iff representable over both GF(2) and GF(3).
114pub fn is_regular(matroid: &Matroid) -> bool {
115    // Regular iff no U_{2,4}, F_7, or F_7* minor.
116    !matroid.has_excluded_minor("U24")
117        && !matroid.has_excluded_minor("F7")
118        && !matroid.has_excluded_minor("F7*")
119}
120
121/// Check if a matroid has another matroid as a minor.
122///
123/// Delegates to the internal has_minor_check in matroid.rs via excluded minor interface.
124/// For arbitrary minors, this performs exhaustive search.
125pub fn has_minor(matroid: &Matroid, minor: &Matroid) -> bool {
126    // Use the matroid module's internal minor check.
127    // We check by trying all contraction/deletion sequences.
128    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    // Try deleting or contracting each element and recurse.
144    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/// The Fano matroid F_7 (matroid of the Fano plane PG(2,2)).
158///
159/// Rank 3 on 7 elements. Representable over GF(2) but not GF(3).
160#[must_use]
161pub fn fano_matroid() -> Matroid {
162    // Lines of the Fano plane: {0,1,3}, {1,2,4}, {2,3,5}, {3,4,6}, {0,4,5}, {1,5,6}, {0,2,6}
163    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/// The dual Fano matroid F_7*.
192///
193/// Rank 4 on 7 elements.
194#[must_use]
195pub fn dual_fano_matroid() -> Matroid {
196    fano_matroid().dual()
197}
198
199/// Extract the GF(2) representation matrix in standard form [I_k | A].
200///
201/// Returns None if the matroid is not binary.
202pub fn standard_representation(matroid: &Matroid) -> Option<GF2Matrix> {
203    match is_binary(matroid) {
204        RepresentabilityResult::Representable(m) => Some(m),
205        _ => None,
206    }
207}
208
209/// Given a GF(2) matrix, extract its column matroid.
210///
211/// The bases are k-element subsets of columns that are linearly independent.
212#[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
234// --- Internal helpers ---
235
236fn 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    // Build k×n matrix in RREF: identity in first k columns, free entries elsewhere.
260    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
289/// GF(3) representation search.
290fn 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    // 3^free_entries possibilities.
303    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        // Check if column matroid over GF(3) matches.
307        let candidate = column_matroid_gf3(&matrix_gf3, k, n);
308        if candidate.bases == matroid.bases {
309            // Return as GF2Matrix (shape only; caller knows it's GF(3)).
310            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    // Identity in first k columns.
327    for i in 0..k {
328        m[i * n + i] = 1;
329    }
330    // Free entries from pattern (base-3 digits).
331    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    // Extract submatrix and compute rank via Gaussian elimination mod 3.
360    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    // Gaussian elimination mod 3.
369    let mut pivot_row = 0;
370    for col in 0..m {
371        // Find pivot.
372        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        // Swap rows.
382        if pr != pivot_row {
383            for c in 0..m {
384                sub.swap(pr * m + c, pivot_row * m + c);
385            }
386        }
387
388        // Scale pivot to 1.
389        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        // Eliminate.
395        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, // undefined for 0
416    }
417}
418
419/// GF(q) representation search for prime q.
420fn 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    // Extended Euclidean algorithm for modular inverse.
531    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        // F7 should have C(7,3) - 7 = 28 bases (35 total triples - 7 lines)
653        assert_eq!(f7.bases.len(), 28);
654    }
655}