Skip to main content

amari_enumerative/
matroid.rs

1//! Matroid Theory for Enumerative Geometry
2//!
3//! Implements combinatorial matroid theory with connections to tropical geometry,
4//! Grassmannians, and namespace analysis.
5//!
6//! # Key Types
7//!
8//! - [`Matroid`]: A matroid on a ground set, represented by its bases
9//! - [`ValuatedMatroid`]: A matroid with a valuation on bases (tropical Plücker vector)
10//!
11//! # Contracts
12//!
13//! - Bases satisfy the basis exchange axiom
14//! - Matroid rank is well-defined: max |B ∩ S| is the same for all bases B
15//! - Dual matroid has bases = complements of original bases
16
17use crate::namespace::{CapabilityId, Namespace};
18use std::collections::{BTreeMap, BTreeSet};
19
20/// A matroid on a ground set {0, ..., n-1}.
21///
22/// Represented by its collection of bases, which must satisfy the
23/// basis exchange axiom: for any two bases B₁, B₂ and any x ∈ B₁ \ B₂,
24/// there exists y ∈ B₂ \ B₁ such that (B₁ \ {x}) ∪ {y} is a basis.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Matroid {
27    /// Size of the ground set
28    pub ground_set_size: usize,
29    /// Collection of bases (each a k-element subset)
30    pub bases: BTreeSet<BTreeSet<usize>>,
31    /// Rank of the matroid (common size of all bases)
32    pub rank: usize,
33}
34
35impl Matroid {
36    /// Create the uniform matroid U\_{k,n}: all k-subsets of \[n\] are bases.
37    ///
38    /// # Contract
39    ///
40    /// ```text
41    /// requires: k <= n
42    /// ensures: result.bases.len() == C(n, k)
43    /// ensures: result.rank == k
44    /// ```
45    pub fn uniform(k: usize, n: usize) -> Self {
46        let bases = k_subsets_btree(n, k);
47        Self {
48            ground_set_size: n,
49            bases,
50            rank: k,
51        }
52    }
53
54    /// Create a matroid from an explicit collection of bases.
55    ///
56    /// Validates the basis exchange axiom.
57    ///
58    /// # Contract
59    ///
60    /// ```text
61    /// requires: all bases have the same cardinality
62    /// requires: basis exchange axiom holds
63    /// ensures: result.rank == bases[0].len()
64    /// ```
65    pub fn from_bases(n: usize, bases: BTreeSet<BTreeSet<usize>>) -> Result<Self, String> {
66        if bases.is_empty() {
67            return Err("Matroid must have at least one basis".to_string());
68        }
69
70        let rank = bases.iter().next().unwrap().len();
71
72        // Check all bases have the same size
73        if bases.iter().any(|b| b.len() != rank) {
74            return Err("All bases must have the same cardinality".to_string());
75        }
76
77        // Check all elements are in ground set
78        if bases.iter().any(|b| b.iter().any(|&e| e >= n)) {
79            return Err(format!("All elements must be < {n}"));
80        }
81
82        // Validate basis exchange axiom
83        for b1 in &bases {
84            for b2 in &bases {
85                let diff: Vec<usize> = b1.difference(b2).copied().collect();
86                for &x in &diff {
87                    let mut found_exchange = false;
88                    for &y in b2.difference(b1) {
89                        let mut candidate: BTreeSet<usize> = b1.clone();
90                        candidate.remove(&x);
91                        candidate.insert(y);
92                        if bases.contains(&candidate) {
93                            found_exchange = true;
94                            break;
95                        }
96                    }
97                    if !found_exchange {
98                        return Err(format!(
99                            "Basis exchange axiom fails: cannot exchange {x} from {:?} with element of {:?}",
100                            b1, b2
101                        ));
102                    }
103                }
104            }
105        }
106
107        Ok(Self {
108            ground_set_size: n,
109            bases,
110            rank,
111        })
112    }
113
114    /// Create a matroid from Plücker coordinates.
115    ///
116    /// The matroid is defined by the nonvanishing pattern of the Plücker
117    /// coordinates: a k-subset I is a basis iff p_I ≠ 0 (within tolerance).
118    ///
119    /// # Contract
120    ///
121    /// ```text
122    /// requires: coords.len() == C(n, k)
123    /// ensures: result represents the matroid of the point in Gr(k,n)
124    /// ```
125    pub fn from_plucker(
126        k: usize,
127        n: usize,
128        coords: &[f64],
129        tolerance: f64,
130    ) -> Result<Self, String> {
131        let subsets: Vec<Vec<usize>> = k_subsets_vec(n, k);
132
133        if coords.len() != subsets.len() {
134            return Err(format!(
135                "Expected C({},{}) = {} Plücker coordinates, got {}",
136                n,
137                k,
138                subsets.len(),
139                coords.len()
140            ));
141        }
142
143        let mut bases = BTreeSet::new();
144        for (i, subset) in subsets.iter().enumerate() {
145            if coords[i].abs() > tolerance {
146                bases.insert(subset.iter().copied().collect::<BTreeSet<usize>>());
147            }
148        }
149
150        if bases.is_empty() {
151            return Err("All Plücker coordinates are zero".to_string());
152        }
153
154        Ok(Self {
155            ground_set_size: n,
156            bases,
157            rank: k,
158        })
159    }
160
161    /// Rank of a subset S: max |B ∩ S| over all bases B.
162    ///
163    /// # Contract
164    ///
165    /// ```text
166    /// ensures: result <= self.rank
167    /// ensures: result <= subset.len()
168    /// ```
169    #[must_use]
170    pub fn rank_of(&self, subset: &BTreeSet<usize>) -> usize {
171        self.bases
172            .iter()
173            .map(|b| b.intersection(subset).count())
174            .max()
175            .unwrap_or(0)
176    }
177
178    /// Compute all circuits (minimal dependent sets).
179    ///
180    /// A circuit is a minimal subset C such that rank(C) < |C|.
181    ///
182    /// # Contract
183    ///
184    /// ```text
185    /// ensures: forall C in result. rank_of(C) < |C|
186    /// ensures: forall C in result. forall e in C. rank_of(C \ {e}) == |C| - 1
187    /// ```
188    #[must_use]
189    pub fn circuits(&self) -> BTreeSet<BTreeSet<usize>> {
190        let mut circuits = BTreeSet::new();
191
192        // Check subsets of increasing size
193        for size in 2..=self.ground_set_size {
194            for subset in subsets_of_size(self.ground_set_size, size) {
195                let r = self.rank_of(&subset);
196                if r < size {
197                    // Check if it's minimal (no proper subset is dependent)
198                    let is_minimal = subset.iter().all(|&e| {
199                        let mut smaller: BTreeSet<usize> = subset.clone();
200                        smaller.remove(&e);
201                        self.rank_of(&smaller) == smaller.len()
202                    });
203                    if is_minimal {
204                        circuits.insert(subset);
205                    }
206                }
207            }
208
209            // Optimization: circuits have size at most rank + 1
210            if size > self.rank + 1 {
211                break;
212            }
213        }
214
215        circuits
216    }
217
218    /// Dual matroid: bases are complements of original bases.
219    ///
220    /// # Contract
221    ///
222    /// ```text
223    /// ensures: result.rank == self.ground_set_size - self.rank
224    /// ensures: result.bases.len() == self.bases.len()
225    /// ```
226    #[must_use]
227    pub fn dual(&self) -> Self {
228        let ground: BTreeSet<usize> = (0..self.ground_set_size).collect();
229        let dual_bases: BTreeSet<BTreeSet<usize>> = self
230            .bases
231            .iter()
232            .map(|b| ground.difference(b).copied().collect())
233            .collect();
234
235        Self {
236            ground_set_size: self.ground_set_size,
237            bases: dual_bases,
238            rank: self.ground_set_size - self.rank,
239        }
240    }
241
242    /// Direct sum of two matroids (disjoint union of ground sets).
243    ///
244    /// # Contract
245    ///
246    /// ```text
247    /// ensures: result.rank == self.rank + other.rank
248    /// ensures: result.ground_set_size == self.ground_set_size + other.ground_set_size
249    /// ```
250    #[must_use]
251    pub fn direct_sum(&self, other: &Matroid) -> Self {
252        let offset = self.ground_set_size;
253        let mut bases = BTreeSet::new();
254
255        for b1 in &self.bases {
256            for b2 in &other.bases {
257                let mut combined: BTreeSet<usize> = b1.clone();
258                for &e in b2 {
259                    combined.insert(e + offset);
260                }
261                bases.insert(combined);
262            }
263        }
264
265        Self {
266            ground_set_size: self.ground_set_size + other.ground_set_size,
267            bases,
268            rank: self.rank + other.rank,
269        }
270    }
271
272    /// Matroid union: bases are maximal sets in B₁ ∨ B₂.
273    ///
274    /// This is NOT the same as the direct sum — it uses the same ground set.
275    /// A set I is independent in M₁ ∨ M₂ iff I = I₁ ∪ I₂ where I_j is
276    /// independent in M_j. The rank is min(|E|, r₁ + r₂).
277    ///
278    /// # Contract
279    ///
280    /// ```text
281    /// requires: self.ground_set_size == other.ground_set_size
282    /// ensures: result.rank <= self.rank + other.rank
283    /// ensures: result.ground_set_size == self.ground_set_size
284    /// ```
285    #[must_use]
286    pub fn matroid_union(&self, other: &Matroid) -> Self {
287        assert_eq!(
288            self.ground_set_size, other.ground_set_size,
289            "Matroid union requires same ground set"
290        );
291
292        let n = self.ground_set_size;
293        let new_rank = (self.rank + other.rank).min(n);
294
295        // Find all independent sets of the union by trying subsets of size new_rank
296        let mut bases = BTreeSet::new();
297
298        for subset in subsets_of_size(n, new_rank) {
299            if self.is_union_independent(&subset, other) {
300                bases.insert(subset);
301            }
302        }
303
304        // If no bases at this rank, try smaller ranks
305        if bases.is_empty() {
306            for r in (1..new_rank).rev() {
307                for subset in subsets_of_size(n, r) {
308                    if self.is_union_independent(&subset, other) {
309                        bases.insert(subset);
310                    }
311                }
312                if !bases.is_empty() {
313                    return Self {
314                        ground_set_size: n,
315                        bases,
316                        rank: r,
317                    };
318                }
319            }
320        }
321
322        Self {
323            ground_set_size: n,
324            bases,
325            rank: new_rank,
326        }
327    }
328
329    /// Check if a set is independent in M₁ ∨ M₂.
330    fn is_union_independent(&self, set: &BTreeSet<usize>, other: &Matroid) -> bool {
331        // I is independent in M₁ ∨ M₂ iff I = I₁ ∪ I₂ with I_j independent in M_j
332        // Equivalently: max_{A ⊆ I} (r₁(A) + r₂(I\A)) >= |I|
333        let elements: Vec<usize> = set.iter().copied().collect();
334        let n = elements.len();
335
336        // Try all subsets of the set
337        for mask in 0..(1u64 << n) {
338            let mut a: BTreeSet<usize> = BTreeSet::new();
339            let mut b: BTreeSet<usize> = BTreeSet::new();
340            for (i, &e) in elements.iter().enumerate() {
341                if mask & (1 << i) != 0 {
342                    a.insert(e);
343                } else {
344                    b.insert(e);
345                }
346            }
347            if self.rank_of(&a) >= a.len() && other.rank_of(&b) >= b.len() {
348                return true;
349            }
350        }
351        false
352    }
353
354    /// Check if an element is a loop (in no basis).
355    ///
356    /// # Contract
357    ///
358    /// ```text
359    /// ensures: result == !exists B in bases. e in B
360    /// ```
361    #[must_use]
362    pub fn is_loop(&self, e: usize) -> bool {
363        !self.bases.iter().any(|b| b.contains(&e))
364    }
365
366    /// Check if an element is a coloop (in every basis).
367    ///
368    /// # Contract
369    ///
370    /// ```text
371    /// ensures: result == forall B in bases. e in B
372    /// ```
373    #[must_use]
374    pub fn is_coloop(&self, e: usize) -> bool {
375        self.bases.iter().all(|b| b.contains(&e))
376    }
377
378    /// Delete element e: restrict to ground set \ {e}.
379    ///
380    /// # Contract
381    ///
382    /// ```text
383    /// requires: e < self.ground_set_size
384    /// ensures: result.ground_set_size == self.ground_set_size - 1
385    /// ```
386    #[must_use]
387    pub fn delete(&self, e: usize) -> Self {
388        let bases: BTreeSet<BTreeSet<usize>> = self
389            .bases
390            .iter()
391            .filter(|b| !b.contains(&e))
392            .map(|b| b.iter().map(|&x| if x > e { x - 1 } else { x }).collect())
393            .collect();
394
395        let rank = if bases.is_empty() {
396            self.rank.saturating_sub(1)
397        } else {
398            bases.iter().next().unwrap().len()
399        };
400
401        Self {
402            ground_set_size: self.ground_set_size - 1,
403            bases,
404            rank,
405        }
406    }
407
408    /// Contract element e: bases containing e, with e removed.
409    ///
410    /// # Contract
411    ///
412    /// ```text
413    /// requires: e < self.ground_set_size
414    /// ensures: result.ground_set_size == self.ground_set_size - 1
415    /// ensures: result.rank == self.rank - 1 (when e is not a loop)
416    /// ```
417    #[must_use]
418    pub fn contract(&self, e: usize) -> Self {
419        let bases: BTreeSet<BTreeSet<usize>> = self
420            .bases
421            .iter()
422            .filter(|b| b.contains(&e))
423            .map(|b| {
424                b.iter()
425                    .filter(|&&x| x != e)
426                    .map(|&x| if x > e { x - 1 } else { x })
427                    .collect()
428            })
429            .collect();
430
431        let rank = self.rank.saturating_sub(1);
432
433        Self {
434            ground_set_size: self.ground_set_size - 1,
435            bases,
436            rank,
437        }
438    }
439
440    /// Compute the Tutte polynomial T_M(x, y) via deletion-contraction.
441    ///
442    /// Returns coefficients as a map (i, j) -> coefficient of x^i y^j.
443    ///
444    /// # Contract
445    ///
446    /// ```text
447    /// ensures: T_M(1, 1) == number of bases
448    /// ensures: T_M(2, 1) == number of independent sets
449    /// ```
450    #[must_use]
451    pub fn tutte_polynomial(&self) -> BTreeMap<(usize, usize), i64> {
452        // Base case: empty ground set
453        if self.ground_set_size == 0 {
454            let mut result = BTreeMap::new();
455            if self.bases.contains(&BTreeSet::new()) {
456                result.insert((0, 0), 1);
457            }
458            return result;
459        }
460
461        // Find a non-loop, non-coloop element for deletion-contraction
462        let e = (0..self.ground_set_size).find(|&e| !self.is_loop(e) && !self.is_coloop(e));
463
464        match e {
465            Some(e) => {
466                // T_M = T_{M\e} + T_{M/e}
467                let deleted = self.delete(e);
468                let contracted = self.contract(e);
469
470                let t_del = deleted.tutte_polynomial();
471                let t_con = contracted.tutte_polynomial();
472
473                let mut result = t_del;
474                for ((i, j), coeff) in t_con {
475                    *result.entry((i, j)).or_insert(0) += coeff;
476                }
477                result
478            }
479            None => {
480                // All elements are loops or coloops
481                let loops = (0..self.ground_set_size)
482                    .filter(|&e| self.is_loop(e))
483                    .count();
484                let coloops = (0..self.ground_set_size)
485                    .filter(|&e| self.is_coloop(e))
486                    .count();
487
488                // T_M = x^coloops * y^loops
489                let mut result = BTreeMap::new();
490                result.insert((coloops, loops), 1);
491                result
492            }
493        }
494    }
495
496    /// Matroid intersection cardinality via Edmonds' augmenting path algorithm.
497    ///
498    /// Finds the maximum cardinality common independent set of two matroids
499    /// on the same ground set.
500    ///
501    /// # Contract
502    ///
503    /// ```text
504    /// requires: self.ground_set_size == other.ground_set_size
505    /// ensures: result <= min(self.rank, other.rank)
506    /// ```
507    pub fn intersection_cardinality(&self, other: &Matroid) -> usize {
508        assert_eq!(
509            self.ground_set_size, other.ground_set_size,
510            "Matroid intersection requires same ground set"
511        );
512
513        let n = self.ground_set_size;
514
515        // Start with the empty independent set
516        let mut current: BTreeSet<usize> = BTreeSet::new();
517
518        while let Some(path) = self.find_augmenting_path(&current, other, n) {
519            // Symmetric difference: toggle elements along the path
520            for &e in &path {
521                if current.contains(&e) {
522                    current.remove(&e);
523                } else {
524                    current.insert(e);
525                }
526            }
527        }
528
529        current.len()
530    }
531
532    /// Find an augmenting path for matroid intersection.
533    ///
534    /// Uses BFS on the exchange graph.
535    fn find_augmenting_path(
536        &self,
537        current: &BTreeSet<usize>,
538        other: &Matroid,
539        n: usize,
540    ) -> Option<Vec<usize>> {
541        let outside: Vec<usize> = (0..n).filter(|e| !current.contains(e)).collect();
542
543        // X1 = elements outside current that can be added to remain independent in M1
544        let x1: Vec<usize> = outside
545            .iter()
546            .copied()
547            .filter(|&e| {
548                let mut test = current.clone();
549                test.insert(e);
550                self.rank_of(&test) > current.len()
551            })
552            .collect();
553
554        // X2 = elements outside current that can be added to remain independent in M2
555        let x2: BTreeSet<usize> = outside
556            .iter()
557            .copied()
558            .filter(|&e| {
559                let mut test = current.clone();
560                test.insert(e);
561                other.rank_of(&test) > current.len()
562            })
563            .collect();
564
565        // BFS from X1 to X2 through the exchange graph
566        let mut visited: BTreeSet<usize> = BTreeSet::new();
567        let mut parent: BTreeMap<usize, Option<usize>> = BTreeMap::new();
568        let mut queue: std::collections::VecDeque<usize> = std::collections::VecDeque::new();
569
570        for &e in &x1 {
571            if !visited.contains(&e) {
572                visited.insert(e);
573                parent.insert(e, None);
574                queue.push_back(e);
575            }
576        }
577
578        while let Some(u) = queue.pop_front() {
579            if x2.contains(&u) {
580                // Found augmenting path — reconstruct it
581                let mut path = vec![u];
582                let mut cur = u;
583                while let Some(Some(p)) = parent.get(&cur) {
584                    path.push(*p);
585                    cur = *p;
586                }
587                path.reverse();
588                return Some(path);
589            }
590
591            if current.contains(&u) {
592                // u is in current set: find elements outside that could exchange with u in M1
593                for &v in &outside {
594                    if !visited.contains(&v) {
595                        let mut test = current.clone();
596                        test.remove(&u);
597                        test.insert(v);
598                        if self.rank_of(&test) >= current.len() {
599                            visited.insert(v);
600                            parent.insert(v, Some(u));
601                            queue.push_back(v);
602                        }
603                    }
604                }
605            } else {
606                // u is outside current set: find elements inside that could exchange with u in M2
607                for &v in current {
608                    if !visited.contains(&v) {
609                        let mut test = current.clone();
610                        test.remove(&v);
611                        test.insert(u);
612                        if other.rank_of(&test) >= current.len() {
613                            visited.insert(v);
614                            parent.insert(v, Some(u));
615                            queue.push_back(v);
616                        }
617                    }
618                }
619            }
620        }
621
622        None
623    }
624
625    /// Schubert matroid: the matroid associated with a Schubert cell in Gr(k, n).
626    ///
627    /// For a partition λ fitting in the k × (n-k) box, the Schubert matroid
628    /// has bases corresponding to k-subsets I such that i_j ≤ λ_j + j
629    /// (where λ is padded to length k).
630    ///
631    /// # Contract
632    ///
633    /// ```text
634    /// requires: partition fits in k × (n-k) box
635    /// ensures: result is a valid matroid
636    /// ```
637    pub fn schubert_matroid(partition: &[usize], k: usize, n: usize) -> Result<Self, String> {
638        let m = n - k;
639        // Pad partition to length k
640        let mut lambda = vec![0usize; k];
641        for (i, &p) in partition.iter().enumerate() {
642            if i >= k {
643                break;
644            }
645            if p > m {
646                return Err(format!("Partition entry {} exceeds n-k={}", p, m));
647            }
648            lambda[i] = p;
649        }
650
651        // Upper bound for position j (0-indexed): m + j - lambda[k-1-j]
652        let bounds: Vec<usize> = (0..k).map(|j| m + j - lambda[k - 1 - j]).collect();
653
654        // Bases are k-subsets {i_0 < i_1 < ... < i_{k-1}} with i_j <= bounds[j]
655        let all_subsets = k_subsets_vec(n, k);
656        let mut bases = BTreeSet::new();
657
658        for subset in all_subsets {
659            let fits = subset.iter().enumerate().all(|(j, &i_j)| i_j <= bounds[j]);
660            if fits {
661                bases.insert(subset.into_iter().collect::<BTreeSet<usize>>());
662            }
663        }
664
665        if bases.is_empty() {
666            return Err("No valid bases for Schubert matroid".to_string());
667        }
668
669        Ok(Self {
670            ground_set_size: n,
671            bases,
672            rank: k,
673        })
674    }
675}
676
677/// Combinatorial extensions for matroid theory.
678impl Matroid {
679    /// Characteristic polynomial χ_M(q) = Σ_{A ⊆ E} (−1)^|A| · q^(r − rank(A)).
680    ///
681    /// Returns coefficients [c_0, c_1, ..., c_r] where χ_M(q) = Σ cᵢ · q^i.
682    #[must_use]
683    pub fn characteristic_polynomial(&self) -> Vec<i64> {
684        let r = self.rank;
685        let n = self.ground_set_size;
686        let mut coeffs = vec![0i64; r + 1];
687
688        for mask in 0..(1u64 << n) {
689            let subset: BTreeSet<usize> = (0..n).filter(|&i| mask & (1 << i) != 0).collect();
690            let size = subset.len();
691            let rank_a = self.rank_of(&subset);
692            let power = r - rank_a;
693            let sign = if size.is_multiple_of(2) { 1i64 } else { -1 };
694            coeffs[power] += sign;
695        }
696        coeffs
697    }
698
699    /// Evaluate the characteristic polynomial at an integer q.
700    #[must_use]
701    pub fn characteristic_polynomial_eval(&self, q: i64) -> i64 {
702        let coeffs = self.characteristic_polynomial();
703        let mut result = 0i64;
704        let mut q_power = 1i64;
705        for &c in &coeffs {
706            result += c * q_power;
707            q_power *= q;
708        }
709        result
710    }
711
712    /// Beta invariant β(M) = (−1)^r · χ'_M(1).
713    #[must_use]
714    pub fn beta_invariant(&self) -> i64 {
715        let coeffs = self.characteristic_polynomial();
716        let deriv_at_1: i64 = coeffs.iter().enumerate().map(|(i, &c)| i as i64 * c).sum();
717        let sign = if self.rank.is_multiple_of(2) {
718            1i64
719        } else {
720            -1
721        };
722        sign * deriv_at_1
723    }
724
725    /// Closure operator: cl(A) = smallest flat containing A.
726    #[must_use]
727    pub fn closure(&self, subset: &BTreeSet<usize>) -> BTreeSet<usize> {
728        let mut cl = subset.clone();
729        let rank_s = self.rank_of(&cl);
730        for e in 0..self.ground_set_size {
731            if !cl.contains(&e) {
732                let mut test = cl.clone();
733                test.insert(e);
734                if self.rank_of(&test) == rank_s {
735                    cl.insert(e);
736                }
737            }
738        }
739        cl
740    }
741
742    /// Check if a subset is a flat.
743    #[must_use]
744    pub fn is_flat(&self, subset: &BTreeSet<usize>) -> bool {
745        self.closure(subset) == *subset
746    }
747
748    /// Lattice of flats grouped by rank: flats[i] = all flats of rank i.
749    #[must_use]
750    pub fn flats(&self) -> Vec<Vec<BTreeSet<usize>>> {
751        let n = self.ground_set_size;
752        let r = self.rank;
753        let mut by_rank: Vec<Vec<BTreeSet<usize>>> = vec![Vec::new(); r + 1];
754
755        // Compute closures of all subsets and collect unique flats.
756        let mut seen: BTreeSet<BTreeSet<usize>> = BTreeSet::new();
757        for mask in 0..(1u64 << n) {
758            let subset: BTreeSet<usize> = (0..n).filter(|&i| mask & (1 << i) != 0).collect();
759            let flat = self.closure(&subset);
760            if seen.insert(flat.clone()) {
761                let rk = self.rank_of(&flat);
762                by_rank[rk].push(flat);
763            }
764        }
765        by_rank
766    }
767
768    /// Möbius function μ(∅, F) for each flat F.
769    #[must_use]
770    pub fn mobius_values(&self) -> BTreeMap<BTreeSet<usize>, i64> {
771        let flats_by_rank = self.flats();
772        let mut mu: BTreeMap<BTreeSet<usize>, i64> = BTreeMap::new();
773
774        // μ(∅, ∅) = 1
775        let empty: BTreeSet<usize> = BTreeSet::new();
776        let cl_empty = self.closure(&empty);
777        mu.insert(cl_empty.clone(), 1);
778
779        // For each flat F (in increasing rank order), μ(∅,F) = -Σ_{G < F} μ(∅,G)
780        for rank_flats in &flats_by_rank {
781            for flat in rank_flats {
782                if mu.contains_key(flat) {
783                    continue;
784                }
785                let mut sum = 0i64;
786                for (g, &m) in &mu {
787                    // G < F means G is a proper subset of F and G is a flat
788                    if g.is_subset(flat) && g != flat {
789                        sum += m;
790                    }
791                }
792                mu.insert(flat.clone(), -sum);
793            }
794        }
795        mu
796    }
797
798    /// Check if the matroid is connected (no direct sum decomposition).
799    #[must_use]
800    pub fn is_connected(&self) -> bool {
801        if self.ground_set_size <= 1 {
802            return true;
803        }
804        self.connected_components().len() == 1
805    }
806
807    /// Connected components of the matroid.
808    #[must_use]
809    pub fn connected_components(&self) -> Vec<Matroid> {
810        let n = self.ground_set_size;
811        if n == 0 {
812            return vec![self.clone()];
813        }
814
815        // Two elements are in the same component if they appear in a common circuit.
816        // Use union-find approach: e, f are connected if rank({e,f}) < 2 or
817        // they're in the same circuit. More precisely, use closure:
818        // e ~ f if removing either doesn't change the rank of the closure of {e, f}.
819        let mut parent: Vec<usize> = (0..n).collect();
820
821        fn find(parent: &mut Vec<usize>, x: usize) -> usize {
822            if parent[x] != x {
823                parent[x] = find(parent, parent[x]);
824            }
825            parent[x]
826        }
827
828        fn union(parent: &mut Vec<usize>, x: usize, y: usize) {
829            let rx = find(parent, x);
830            let ry = find(parent, y);
831            if rx != ry {
832                parent[rx] = ry;
833            }
834        }
835
836        // Elements e, f are in the same component if they lie in a common circuit.
837        // Equivalently: e, f are connected if rank(E\{e}) < rank(E) and
838        // there exists a basis B containing e and another containing f.
839        // Simpler: for each pair, check if they're in the same component via circuits.
840        // For efficiency, use the criterion: e ~ f if cl({e}) and cl({f}) intersect
841        // in a circuit, or more directly, use the parallel connection criterion.
842
843        // Simple approach: e ~ f if there exists a circuit containing both.
844        let circuits = self.circuits();
845        for circuit in &circuits {
846            let elems: Vec<usize> = circuit.iter().copied().collect();
847            for i in 1..elems.len() {
848                union(&mut parent, elems[0], elems[i]);
849            }
850        }
851
852        // Also connect elements that are parallel (same closure).
853        for e in 0..n {
854            for f in (e + 1)..n {
855                let mut pair = BTreeSet::new();
856                pair.insert(e);
857                pair.insert(f);
858                if self.rank_of(&pair) < 2 {
859                    union(&mut parent, e, f);
860                }
861            }
862        }
863
864        // Group elements by component.
865        let mut components: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
866        for e in 0..n {
867            let root = find(&mut parent, e);
868            components.entry(root).or_default().push(e);
869        }
870
871        // If everything is in one component, return self.
872        if components.len() == 1 {
873            return vec![self.clone()];
874        }
875
876        // Build matroid for each component.
877        components
878            .values()
879            .map(|elems| {
880                let elem_set: BTreeSet<usize> = elems.iter().copied().collect();
881                self.restrict(&elem_set)
882            })
883            .collect()
884    }
885
886    /// f-vector: f_i = number of independent sets of size i.
887    #[must_use]
888    pub fn f_vector(&self) -> Vec<u64> {
889        let n = self.ground_set_size;
890        let r = self.rank;
891        let mut f = vec![0u64; r + 1];
892
893        for mask in 0..(1u64 << n) {
894            let subset: BTreeSet<usize> = (0..n).filter(|&i| mask & (1 << i) != 0).collect();
895            let size = subset.len();
896            if size <= r && self.rank_of(&subset) == size {
897                f[size] += 1;
898            }
899        }
900        f
901    }
902
903    /// h-vector derived from f-vector.
904    ///
905    /// h_i = Σ_j (-1)^{i-j} C(r-j, i-j) f_j
906    #[must_use]
907    pub fn h_vector(&self) -> Vec<i64> {
908        let f = self.f_vector();
909        let r = self.rank;
910        let mut h = vec![0i64; r + 1];
911
912        for (i, h_i) in h.iter_mut().enumerate() {
913            for (j, &f_j) in f.iter().enumerate().take(i + 1) {
914                let sign = if (i - j).is_multiple_of(2) { 1i64 } else { -1 };
915                let binom = binomial(r - j, i - j);
916                *h_i += sign * binom as i64 * f_j as i64;
917            }
918        }
919        h
920    }
921
922    /// Whitney numbers of the first kind: |coefficient of q^i in χ_M(q)|.
923    #[must_use]
924    pub fn whitney_numbers_first_kind(&self) -> Vec<u64> {
925        self.characteristic_polynomial()
926            .iter()
927            .map(|c| c.unsigned_abs())
928            .collect()
929    }
930
931    /// Whitney numbers of the second kind: W_i = number of flats of rank i.
932    #[must_use]
933    pub fn whitney_numbers_second_kind(&self) -> Vec<u64> {
934        self.flats().iter().map(|f| f.len() as u64).collect()
935    }
936
937    /// Restriction M|A: matroid on subset A with inherited independence.
938    #[must_use]
939    pub fn restrict(&self, subset: &BTreeSet<usize>) -> Matroid {
940        let elems: Vec<usize> = subset.iter().copied().collect();
941        let new_n = elems.len();
942
943        // Map old indices to new indices.
944        let mut index_map: BTreeMap<usize, usize> = BTreeMap::new();
945        for (new_idx, &old_idx) in elems.iter().enumerate() {
946            index_map.insert(old_idx, new_idx);
947        }
948
949        // Restrict each basis: intersect with subset and remap.
950        let restricted_bases: BTreeSet<BTreeSet<usize>> = self
951            .bases
952            .iter()
953            .map(|b| {
954                b.intersection(subset)
955                    .map(|e| index_map[e])
956                    .collect::<BTreeSet<usize>>()
957            })
958            .collect();
959
960        // The rank of the restriction is the rank of the subset.
961        let new_rank = self.rank_of(subset);
962
963        // Filter to only maximal independent sets of size new_rank.
964        let bases: BTreeSet<BTreeSet<usize>> = restricted_bases
965            .into_iter()
966            .filter(|b| b.len() == new_rank)
967            .collect();
968
969        // If no bases found at that rank, build from all independent sets.
970        if bases.is_empty() && new_rank == 0 {
971            let mut b = BTreeSet::new();
972            b.insert(BTreeSet::new());
973            return Matroid {
974                ground_set_size: new_n,
975                bases: b,
976                rank: 0,
977            };
978        }
979
980        Matroid {
981            ground_set_size: new_n,
982            bases,
983            rank: new_rank,
984        }
985    }
986
987    /// Contraction-deletion pair: returns (M/e, M\e).
988    #[must_use]
989    pub fn contraction_deletion(&self, e: usize) -> (Matroid, Matroid) {
990        (self.contract(e), self.delete(e))
991    }
992
993    /// Check for a specific excluded minor by name.
994    ///
995    /// Supported: "U24" (uniform 2,4), "F7" (Fano), "F7*" (dual Fano).
996    #[must_use]
997    pub fn has_excluded_minor(&self, name: &str) -> bool {
998        match name {
999            "U24" => {
1000                let u24 = Matroid::uniform(2, 4);
1001                has_minor_check(self, &u24)
1002            }
1003            "F7" => {
1004                let f7 = fano_matroid_internal();
1005                has_minor_check(self, &f7)
1006            }
1007            "F7*" => {
1008                let f7 = fano_matroid_internal();
1009                has_minor_check(self, &f7.dual())
1010            }
1011            _ => false,
1012        }
1013    }
1014}
1015
1016/// Check if matroid has another matroid as a minor.
1017///
1018/// M has N as a minor iff there exist disjoint C, D ⊆ E(M) such that M/C\D ≅ N.
1019fn has_minor_check(matroid: &Matroid, minor: &Matroid) -> bool {
1020    let n = matroid.ground_set_size;
1021    let m_n = minor.ground_set_size;
1022    let m_r = minor.rank;
1023
1024    if n < m_n || matroid.rank < m_r {
1025        return false;
1026    }
1027
1028    // Try all ways to select elements to contract (size = rank - m_r)
1029    // and delete (size = n - m_n - contract_size).
1030    let contract_size = matroid.rank - m_r;
1031    let delete_size = n - m_n - contract_size;
1032
1033    if contract_size + delete_size + m_n != n {
1034        return false;
1035    }
1036
1037    // Iterate over all subsets of size contract_size to contract.
1038    for contract_set in subsets_of_size(n, contract_size) {
1039        let remaining: Vec<usize> = (0..n).filter(|e| !contract_set.contains(e)).collect();
1040
1041        if remaining.len() < m_n {
1042            continue;
1043        }
1044
1045        // Try all subsets of remaining of size delete_size to delete.
1046        for del_indices in subsets_of_size_from(&remaining, delete_size) {
1047            let del_set: BTreeSet<usize> = del_indices.iter().copied().collect();
1048
1049            // Apply contraction then deletion.
1050            let mut result = matroid.clone();
1051
1052            // Contract elements (in reverse order to keep indices valid).
1053            let mut to_contract: Vec<usize> = contract_set.iter().copied().collect();
1054            to_contract.sort_unstable_by(|a, b| b.cmp(a));
1055            for &e in &to_contract {
1056                result = result.contract(e);
1057            }
1058
1059            // Adjust deletion indices after contraction.
1060            let mut to_delete: Vec<usize> = Vec::new();
1061            for &d in &del_set {
1062                let adjusted = d - contract_set.iter().filter(|&&c| c < d).count();
1063                to_delete.push(adjusted);
1064            }
1065            to_delete.sort_unstable_by(|a, b| b.cmp(a));
1066
1067            for &e in &to_delete {
1068                if e < result.ground_set_size {
1069                    result = result.delete(e);
1070                }
1071            }
1072
1073            // Check isomorphism: same rank and same bases structure.
1074            if result.ground_set_size == minor.ground_set_size
1075                && result.rank == minor.rank
1076                && result.bases == minor.bases
1077            {
1078                return true;
1079            }
1080
1081            // Also try all permutations of the remaining ground set for isomorphism.
1082            // For small ground sets only.
1083            if m_n <= 6
1084                && result.ground_set_size == m_n
1085                && result.rank == m_r
1086                && is_matroid_isomorphic(&result, minor)
1087            {
1088                return true;
1089            }
1090        }
1091    }
1092    false
1093}
1094
1095/// Check if two matroids on the same size ground set are isomorphic.
1096fn is_matroid_isomorphic(a: &Matroid, b: &Matroid) -> bool {
1097    if a.ground_set_size != b.ground_set_size || a.rank != b.rank || a.bases.len() != b.bases.len()
1098    {
1099        return false;
1100    }
1101    let n = a.ground_set_size;
1102    if n > 8 {
1103        return a.bases == b.bases;
1104    }
1105
1106    // Try all permutations.
1107    let perms = permutations(n);
1108    for perm in &perms {
1109        let mapped_bases: BTreeSet<BTreeSet<usize>> = a
1110            .bases
1111            .iter()
1112            .map(|basis| basis.iter().map(|&e| perm[e]).collect())
1113            .collect();
1114        if mapped_bases == b.bases {
1115            return true;
1116        }
1117    }
1118    false
1119}
1120
1121fn permutations(n: usize) -> Vec<Vec<usize>> {
1122    if n == 0 {
1123        return vec![vec![]];
1124    }
1125    let mut result = Vec::new();
1126    let sub = permutations(n - 1);
1127    for p in sub {
1128        for i in 0..=p.len() {
1129            let mut new_p = p.clone();
1130            new_p.insert(i, n - 1);
1131            result.push(new_p);
1132        }
1133    }
1134    result
1135}
1136
1137fn subsets_of_size_from(elements: &[usize], k: usize) -> Vec<BTreeSet<usize>> {
1138    if k == 0 {
1139        return vec![BTreeSet::new()];
1140    }
1141    if elements.len() < k {
1142        return Vec::new();
1143    }
1144    let mut result = Vec::new();
1145    let mut current = Vec::with_capacity(k);
1146    gen_subsets_from(elements, k, 0, &mut current, &mut result);
1147    result
1148}
1149
1150fn gen_subsets_from(
1151    elements: &[usize],
1152    k: usize,
1153    start: usize,
1154    current: &mut Vec<usize>,
1155    result: &mut Vec<BTreeSet<usize>>,
1156) {
1157    if current.len() == k {
1158        result.push(current.iter().copied().collect());
1159        return;
1160    }
1161    let remaining = k - current.len();
1162    if start + remaining > elements.len() {
1163        return;
1164    }
1165    for i in start..=(elements.len() - remaining) {
1166        current.push(elements[i]);
1167        gen_subsets_from(elements, k, i + 1, current, result);
1168        current.pop();
1169    }
1170}
1171
1172/// Fano matroid (internal, for excluded minor checks).
1173fn fano_matroid_internal() -> Matroid {
1174    // Fano plane PG(2,2): rank 3 on 7 elements.
1175    // Lines (dependent triples): {0,1,3}, {1,2,4}, {2,3,5}, {3,4,6}, {0,4,5}, {1,5,6}, {0,2,6}
1176    let n = 7;
1177    let k = 3;
1178    let lines: Vec<BTreeSet<usize>> = vec![
1179        [0, 1, 3].iter().copied().collect(),
1180        [1, 2, 4].iter().copied().collect(),
1181        [2, 3, 5].iter().copied().collect(),
1182        [3, 4, 6].iter().copied().collect(),
1183        [0, 4, 5].iter().copied().collect(),
1184        [1, 5, 6].iter().copied().collect(),
1185        [0, 2, 6].iter().copied().collect(),
1186    ];
1187
1188    // Bases are 3-element subsets that are NOT lines.
1189    let all_triples = k_subsets_btree(n, k);
1190    let bases: BTreeSet<BTreeSet<usize>> = all_triples
1191        .into_iter()
1192        .filter(|t| !lines.contains(t))
1193        .collect();
1194
1195    Matroid {
1196        ground_set_size: n,
1197        bases,
1198        rank: k,
1199    }
1200}
1201
1202fn binomial(n: usize, k: usize) -> u64 {
1203    if k > n {
1204        return 0;
1205    }
1206    if k == 0 || k == n {
1207        return 1;
1208    }
1209    let k = k.min(n - k);
1210    let mut result: u64 = 1;
1211    for i in 0..k {
1212        result = result * (n - i) as u64 / (i + 1) as u64;
1213    }
1214    result
1215}
1216
1217/// Matroid-based namespace analysis.
1218impl Namespace {
1219    /// Compute the capability matroid of this namespace.
1220    ///
1221    /// Capabilities are elements; a set is independent if the corresponding
1222    /// Schubert classes have a transverse intersection.
1223    ///
1224    /// # Contract
1225    ///
1226    /// ```text
1227    /// ensures: result.is_some() implies result.ground_set_size == self.capabilities.len()
1228    /// ensures: result.is_none() implies self.capabilities.is_empty()
1229    /// ```
1230    #[must_use]
1231    pub fn capability_matroid(&self) -> Option<Matroid> {
1232        let n = self.capabilities.len();
1233        if n == 0 {
1234            return None;
1235        }
1236
1237        let (k, dim_n) = self.grassmannian;
1238        let gr_dim = k * (dim_n - k);
1239
1240        // Find all maximal independent sets (bases)
1241        let mut max_size = 0;
1242        let mut candidates: Vec<BTreeSet<usize>> = Vec::new();
1243
1244        // Try all subsets
1245        for mask in 1u64..(1u64 << n) {
1246            let subset: Vec<usize> = (0..n).filter(|&i| mask & (1 << i) != 0).collect();
1247            let total_codim: usize = subset
1248                .iter()
1249                .map(|&i| self.capabilities[i].codimension())
1250                .sum();
1251
1252            if total_codim <= gr_dim {
1253                let size = subset.len();
1254                if size > max_size {
1255                    max_size = size;
1256                    candidates.clear();
1257                }
1258                if size == max_size {
1259                    candidates.push(subset.into_iter().collect());
1260                }
1261            }
1262        }
1263
1264        if candidates.is_empty() {
1265            return None;
1266        }
1267
1268        let bases: BTreeSet<BTreeSet<usize>> = candidates.into_iter().collect();
1269        Some(Matroid {
1270            ground_set_size: n,
1271            bases,
1272            rank: max_size,
1273        })
1274    }
1275
1276    /// Find redundant capabilities: those that don't affect the matroid rank.
1277    ///
1278    /// A capability is redundant if removing it doesn't change the rank
1279    /// of the capability matroid (i.e., it is a loop).
1280    ///
1281    /// # Contract
1282    ///
1283    /// ```text
1284    /// ensures: forall id in result. removing id does not change matroid rank
1285    /// ```
1286    #[must_use]
1287    pub fn redundant_capabilities(&self) -> Vec<CapabilityId> {
1288        let Some(matroid) = self.capability_matroid() else {
1289            return Vec::new();
1290        };
1291
1292        let mut redundant = Vec::new();
1293        for (i, cap) in self.capabilities.iter().enumerate() {
1294            if matroid.is_loop(i) {
1295                redundant.push(cap.id.clone());
1296            }
1297        }
1298        redundant
1299    }
1300
1301    /// Maximum number of capabilities that can be shared between two namespaces.
1302    ///
1303    /// Uses matroid intersection to find the largest common independent set.
1304    ///
1305    /// # Contract
1306    ///
1307    /// ```text
1308    /// ensures: result <= min(self.capabilities.len(), other.capabilities.len())
1309    /// ```
1310    #[must_use]
1311    pub fn max_shared_capabilities(&self, other: &Namespace) -> usize {
1312        let m1 = self.capability_matroid();
1313        let m2 = other.capability_matroid();
1314
1315        match (m1, m2) {
1316            (Some(m1), Some(m2)) if m1.ground_set_size == m2.ground_set_size => {
1317                m1.intersection_cardinality(&m2)
1318            }
1319            _ => 0,
1320        }
1321    }
1322}
1323
1324/// A matroid with a valuation on its bases (tropical Plücker vector).
1325///
1326/// The valuation v: bases → R satisfies the tropical Plücker relations.
1327#[derive(Debug, Clone)]
1328pub struct ValuatedMatroid {
1329    /// Underlying matroid
1330    pub matroid: Matroid,
1331    /// Valuation: basis → value
1332    pub valuation: BTreeMap<BTreeSet<usize>, f64>,
1333}
1334
1335impl ValuatedMatroid {
1336    /// Create a valuated matroid from tropical Plücker coordinates.
1337    ///
1338    /// # Contract
1339    ///
1340    /// ```text
1341    /// requires: coords.len() == C(n, k)
1342    /// ensures: result satisfies tropical Plücker relations (approximately)
1343    /// ```
1344    pub fn from_tropical_plucker(k: usize, n: usize, coords: &[f64]) -> Result<Self, String> {
1345        let subsets = k_subsets_vec(n, k);
1346
1347        if coords.len() != subsets.len() {
1348            return Err(format!(
1349                "Expected {} tropical Plücker coordinates, got {}",
1350                subsets.len(),
1351                coords.len()
1352            ));
1353        }
1354
1355        let mut bases = BTreeSet::new();
1356        let mut valuation = BTreeMap::new();
1357
1358        for (i, subset) in subsets.iter().enumerate() {
1359            if coords[i].is_finite() {
1360                let basis: BTreeSet<usize> = subset.iter().copied().collect();
1361                bases.insert(basis.clone());
1362                valuation.insert(basis, coords[i]);
1363            }
1364        }
1365
1366        if bases.is_empty() {
1367            return Err("No finite tropical Plücker coordinates".to_string());
1368        }
1369
1370        let rank = k;
1371        let matroid = Matroid {
1372            ground_set_size: n,
1373            bases,
1374            rank,
1375        };
1376
1377        Ok(Self { matroid, valuation })
1378    }
1379
1380    /// Check if the valuation satisfies tropical Plücker relations.
1381    ///
1382    /// The 3-term tropical Plücker relation states:
1383    /// for any (k-1)-subset S and elements a < b < c < d not in S,
1384    /// the minimum of {v(S∪{a,c}) + v(S∪{b,d}), v(S∪{a,d}) + v(S∪{b,c})}
1385    /// is attained at least twice (where v(S∪{a,b}) + v(S∪{c,d}) is included).
1386    #[must_use]
1387    pub fn satisfies_tropical_plucker(&self) -> bool {
1388        // For small matroids, just return true (full check is expensive)
1389        if self.matroid.rank <= 1 || self.matroid.ground_set_size <= 3 {
1390            return true;
1391        }
1392
1393        // Check the tropical Plücker relations
1394        let k = self.matroid.rank;
1395        let _n = self.matroid.ground_set_size;
1396
1397        if k < 2 {
1398            return true;
1399        }
1400
1401        // For each pair of k-subsets differing in exactly 2 elements
1402        for b1 in &self.matroid.bases {
1403            for b2 in &self.matroid.bases {
1404                let diff1: Vec<usize> = b1.difference(b2).copied().collect();
1405                let diff2: Vec<usize> = b2.difference(b1).copied().collect();
1406
1407                if diff1.len() != 2 || diff2.len() != 2 {
1408                    continue;
1409                }
1410
1411                // This is a basic form of the Plücker relation
1412                let v1 = self.valuation.get(b1).copied().unwrap_or(f64::INFINITY);
1413                let v2 = self.valuation.get(b2).copied().unwrap_or(f64::INFINITY);
1414
1415                // The exchange axiom in the valuated setting
1416                let i = diff1[0];
1417                let _j = diff1[1];
1418                let a = diff2[0];
1419                let b_elem = diff2[1];
1420
1421                // Check: min of the three exchange terms is attained twice
1422                let mut exchange1 = b1.clone();
1423                exchange1.remove(&i);
1424                exchange1.insert(a);
1425
1426                let mut exchange2 = b1.clone();
1427                exchange2.remove(&i);
1428                exchange2.insert(b_elem);
1429
1430                let v_ex1 = self
1431                    .valuation
1432                    .get(&exchange1)
1433                    .copied()
1434                    .unwrap_or(f64::INFINITY);
1435                let v_ex2 = self
1436                    .valuation
1437                    .get(&exchange2)
1438                    .copied()
1439                    .unwrap_or(f64::INFINITY);
1440
1441                // Basic check: finite valuations should have consistent exchanges
1442                if v1.is_finite() && v2.is_finite() && v_ex1.is_infinite() && v_ex2.is_infinite() {
1443                    return false;
1444                }
1445            }
1446        }
1447
1448        true
1449    }
1450
1451    /// Convert to a TropicalSchubertClass (when tropical-schubert feature is enabled).
1452    ///
1453    /// Extracts the valuation as tropical weights.
1454    #[cfg(feature = "tropical-schubert")]
1455    #[must_use]
1456    pub fn to_tropical_schubert(&self) -> crate::tropical_schubert::TropicalSchubertClass {
1457        // Collect valuation values as integer weights
1458        let weights: Vec<i64> = self.valuation.values().map(|&v| v as i64).collect();
1459
1460        crate::tropical_schubert::TropicalSchubertClass::new(weights)
1461    }
1462}
1463
1464// Helper functions
1465
1466fn k_subsets_btree(n: usize, k: usize) -> BTreeSet<BTreeSet<usize>> {
1467    k_subsets_vec(n, k)
1468        .into_iter()
1469        .map(|s| s.into_iter().collect())
1470        .collect()
1471}
1472
1473fn k_subsets_vec(n: usize, k: usize) -> Vec<Vec<usize>> {
1474    let mut result = Vec::new();
1475    let mut current = Vec::with_capacity(k);
1476    gen_subsets(n, k, 0, &mut current, &mut result);
1477    result
1478}
1479
1480fn gen_subsets(
1481    n: usize,
1482    k: usize,
1483    start: usize,
1484    current: &mut Vec<usize>,
1485    result: &mut Vec<Vec<usize>>,
1486) {
1487    if current.len() == k {
1488        result.push(current.clone());
1489        return;
1490    }
1491    let remaining = k - current.len();
1492    if start + remaining > n {
1493        return;
1494    }
1495    for i in start..=(n - remaining) {
1496        current.push(i);
1497        gen_subsets(n, k, i + 1, current, result);
1498        current.pop();
1499    }
1500}
1501
1502fn subsets_of_size(n: usize, k: usize) -> Vec<BTreeSet<usize>> {
1503    k_subsets_vec(n, k)
1504        .into_iter()
1505        .map(|s| s.into_iter().collect())
1506        .collect()
1507}
1508
1509/// Batch compute matroid circuits for multiple matroids in parallel.
1510#[cfg(feature = "parallel")]
1511#[must_use]
1512pub fn circuits_batch(matroids: &[Matroid]) -> Vec<BTreeSet<BTreeSet<usize>>> {
1513    use rayon::prelude::*;
1514    matroids.par_iter().map(|m| m.circuits()).collect()
1515}
1516
1517/// Batch compute Tutte polynomials for multiple matroids in parallel.
1518#[cfg(feature = "parallel")]
1519#[must_use]
1520pub fn tutte_polynomial_batch(matroids: &[Matroid]) -> Vec<BTreeMap<(usize, usize), i64>> {
1521    use rayon::prelude::*;
1522    matroids.par_iter().map(|m| m.tutte_polynomial()).collect()
1523}
1524
1525/// Batch compute matroid intersection cardinalities in parallel.
1526#[cfg(feature = "parallel")]
1527#[must_use]
1528pub fn intersection_cardinality_batch(pairs: &[(Matroid, Matroid)]) -> Vec<usize> {
1529    use rayon::prelude::*;
1530    pairs
1531        .par_iter()
1532        .map(|(m1, m2)| m1.intersection_cardinality(m2))
1533        .collect()
1534}
1535
1536#[cfg(test)]
1537mod tests {
1538    use super::*;
1539    use crate::namespace::Capability;
1540    use crate::schubert::SchubertClass;
1541
1542    #[test]
1543    fn test_uniform_matroid() {
1544        let m = Matroid::uniform(2, 4);
1545        assert_eq!(m.rank, 2);
1546        assert_eq!(m.bases.len(), 6); // C(4,2)
1547        assert_eq!(m.ground_set_size, 4);
1548    }
1549
1550    #[test]
1551    fn test_matroid_from_plucker() {
1552        // All Plücker coordinates nonzero → uniform matroid
1553        let coords = vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.0]; // C(4,2) = 6 coords
1554        let m = Matroid::from_plucker(2, 4, &coords, 0.001).unwrap();
1555        assert_eq!(m.rank, 2);
1556        assert_eq!(m.bases.len(), 6);
1557    }
1558
1559    #[test]
1560    fn test_matroid_from_plucker_partial() {
1561        // Some zero coordinates
1562        let coords = vec![1.0, 0.0, 1.0, 1.0, 0.0, 1.0];
1563        let m = Matroid::from_plucker(2, 4, &coords, 0.001).unwrap();
1564        assert_eq!(m.rank, 2);
1565        assert_eq!(m.bases.len(), 4);
1566    }
1567
1568    #[test]
1569    fn test_schubert_matroid() {
1570        // Schubert matroid for σ_1 in Gr(2,4): k=2, m=2, λ=[1,0]
1571        // bounds: j=0 → 2+0-0=2, j=1 → 2+1-1=2
1572        // Bases: {i_0 < i_1} with i_0 ≤ 2, i_1 ≤ 2 → {0,1}, {0,2}, {1,2}
1573        let m = Matroid::schubert_matroid(&[1], 2, 4).unwrap();
1574        assert_eq!(m.rank, 2);
1575        assert_eq!(m.bases.len(), 3);
1576    }
1577
1578    #[test]
1579    fn test_schubert_matroid_empty_partition() {
1580        // Empty partition σ_∅ → bounds: j=0 → 2, j=1 → 3 → all subsets are bases
1581        let m = Matroid::schubert_matroid(&[], 2, 4).unwrap();
1582        assert_eq!(m.bases.len(), 6);
1583    }
1584
1585    #[test]
1586    fn test_matroid_circuits() {
1587        // U_{2,4}: circuits are all 3-element subsets
1588        let m = Matroid::uniform(2, 4);
1589        let circuits = m.circuits();
1590        assert_eq!(circuits.len(), 4); // C(4,3) = 4
1591    }
1592
1593    #[test]
1594    fn test_matroid_dual() {
1595        let m = Matroid::uniform(2, 4);
1596        let dual = m.dual();
1597        assert_eq!(dual.rank, 2); // 4 - 2
1598        assert_eq!(dual.bases.len(), 6); // U_{2,4} is self-dual
1599    }
1600
1601    #[test]
1602    fn test_matroid_direct_sum() {
1603        let m1 = Matroid::uniform(1, 2);
1604        let m2 = Matroid::uniform(1, 2);
1605        let sum = m1.direct_sum(&m2);
1606        assert_eq!(sum.rank, 2);
1607        assert_eq!(sum.ground_set_size, 4);
1608    }
1609
1610    #[test]
1611    fn test_matroid_loop_coloop() {
1612        // U_{2,3}: no loops, no coloops
1613        let m = Matroid::uniform(2, 3);
1614        for e in 0..3 {
1615            assert!(!m.is_loop(e));
1616            assert!(!m.is_coloop(e));
1617        }
1618    }
1619
1620    #[test]
1621    fn test_matroid_delete_contract() {
1622        let m = Matroid::uniform(2, 4);
1623
1624        let deleted = m.delete(0);
1625        assert_eq!(deleted.ground_set_size, 3);
1626        assert_eq!(deleted.rank, 2);
1627
1628        let contracted = m.contract(0);
1629        assert_eq!(contracted.ground_set_size, 3);
1630        assert_eq!(contracted.rank, 1);
1631    }
1632
1633    #[test]
1634    fn test_matroid_intersection_cardinality() {
1635        let m1 = Matroid::uniform(2, 4);
1636        let m2 = Matroid::uniform(2, 4);
1637        // Intersection of two copies of U_{2,4} should be 2
1638        assert_eq!(m1.intersection_cardinality(&m2), 2);
1639    }
1640
1641    #[test]
1642    fn test_tutte_polynomial_uniform() {
1643        let m = Matroid::uniform(1, 2);
1644        let tutte = m.tutte_polynomial();
1645        // T_{U_{1,2}}(x, y) = x + y
1646        assert_eq!(tutte.get(&(1, 0)).copied().unwrap_or(0), 1);
1647        assert_eq!(tutte.get(&(0, 1)).copied().unwrap_or(0), 1);
1648    }
1649
1650    #[test]
1651    fn test_namespace_capability_matroid() {
1652        let pos = SchubertClass::new(vec![], (2, 4)).unwrap();
1653        let mut ns = Namespace::new("test", pos);
1654
1655        let cap1 = Capability::new("c1", "Cap 1", vec![1], (2, 4)).unwrap();
1656        let cap2 = Capability::new("c2", "Cap 2", vec![1], (2, 4)).unwrap();
1657        ns.grant(cap1).unwrap();
1658        ns.grant(cap2).unwrap();
1659
1660        let matroid = ns.capability_matroid();
1661        assert!(matroid.is_some());
1662        let m = matroid.unwrap();
1663        assert_eq!(m.ground_set_size, 2);
1664    }
1665
1666    #[test]
1667    fn test_valuated_matroid_tropical_plucker() {
1668        let coords = vec![0.0, 1.0, 2.0, 1.0, 2.0, 3.0]; // C(4,2)=6
1669        let vm = ValuatedMatroid::from_tropical_plucker(2, 4, &coords).unwrap();
1670        assert_eq!(vm.matroid.rank, 2);
1671        assert!(vm.satisfies_tropical_plucker());
1672    }
1673
1674    #[test]
1675    fn test_characteristic_polynomial_u24() {
1676        let m = Matroid::uniform(2, 4);
1677        let chi = m.characteristic_polynomial();
1678        // χ_{U_{2,4}}(q) = q² - 4q + 3
1679        assert_eq!(chi, vec![3, -4, 1]);
1680    }
1681
1682    #[test]
1683    fn test_characteristic_polynomial_eval_at_1() {
1684        // χ_M(1) = 0 for any matroid with at least one element
1685        let m = Matroid::uniform(2, 4);
1686        assert_eq!(m.characteristic_polynomial_eval(1), 0);
1687    }
1688
1689    #[test]
1690    fn test_beta_invariant() {
1691        let m = Matroid::uniform(2, 4);
1692        let beta = m.beta_invariant();
1693        // β(M) = (-1)^r · χ'(1)
1694        // For U_{2,4}: χ(q) = q² - 4q + 3, χ'(1) = -2, β = (-1)² · (-2) = -2
1695        assert_eq!(beta, -2);
1696    }
1697
1698    #[test]
1699    fn test_closure_idempotent() {
1700        let m = Matroid::uniform(2, 4);
1701        let s: BTreeSet<usize> = [0].iter().copied().collect();
1702        let cl = m.closure(&s);
1703        let cl2 = m.closure(&cl);
1704        assert_eq!(cl, cl2);
1705    }
1706
1707    #[test]
1708    fn test_flats_of_u24() {
1709        let m = Matroid::uniform(2, 4);
1710        let flats = m.flats();
1711        // Rank 0: just ∅
1712        assert_eq!(flats[0].len(), 1);
1713        // Rank 1: singletons {0}, {1}, {2}, {3}
1714        assert_eq!(flats[1].len(), 4);
1715        // Rank 2: just {0,1,2,3} (the whole set)
1716        assert_eq!(flats[2].len(), 1);
1717    }
1718
1719    #[test]
1720    fn test_f_vector_uniform() {
1721        let m = Matroid::uniform(2, 4);
1722        let f = m.f_vector();
1723        // f_0 = 1, f_1 = 4, f_2 = 6
1724        assert_eq!(f, vec![1, 4, 6]);
1725    }
1726
1727    #[test]
1728    fn test_whitney_second_kind() {
1729        let m = Matroid::uniform(2, 4);
1730        let w = m.whitney_numbers_second_kind();
1731        // W_0 = 1 (empty flat), W_1 = 4 (singleton flats), W_2 = 1 (full set)
1732        assert_eq!(w, vec![1, 4, 1]);
1733    }
1734
1735    #[test]
1736    fn test_restrict() {
1737        let m = Matroid::uniform(2, 4);
1738        let s: BTreeSet<usize> = [0, 1, 2].iter().copied().collect();
1739        let r = m.restrict(&s);
1740        assert_eq!(r.ground_set_size, 3);
1741        assert_eq!(r.rank, 2);
1742        // Should be U_{2,3}
1743        assert_eq!(r.bases.len(), 3);
1744    }
1745
1746    #[test]
1747    fn test_contraction_deletion_pair() {
1748        let m = Matroid::uniform(2, 4);
1749        let (contracted, deleted) = m.contraction_deletion(0);
1750        assert_eq!(contracted.ground_set_size, 3);
1751        assert_eq!(contracted.rank, 1);
1752        assert_eq!(deleted.ground_set_size, 3);
1753        assert_eq!(deleted.rank, 2);
1754    }
1755
1756    #[test]
1757    fn test_connected_uniform() {
1758        let m = Matroid::uniform(2, 4);
1759        assert!(m.is_connected());
1760    }
1761
1762    #[test]
1763    fn test_connected_components_direct_sum() {
1764        let m1 = Matroid::uniform(1, 2);
1765        let m2 = Matroid::uniform(1, 2);
1766        let sum = m1.direct_sum(&m2);
1767        let components = sum.connected_components();
1768        assert_eq!(components.len(), 2);
1769    }
1770
1771    #[test]
1772    fn test_has_excluded_minor_u24() {
1773        // U_{2,5} should contain U_{2,4} as a minor
1774        let m = Matroid::uniform(2, 5);
1775        assert!(m.has_excluded_minor("U24"));
1776        // U_{2,3} should NOT contain U_{2,4}
1777        let m2 = Matroid::uniform(2, 3);
1778        assert!(!m2.has_excluded_minor("U24"));
1779    }
1780
1781    #[cfg(feature = "parallel")]
1782    #[test]
1783    fn test_circuits_batch() {
1784        let matroids = vec![Matroid::uniform(2, 4), Matroid::uniform(1, 3)];
1785        let results = super::circuits_batch(&matroids);
1786        assert_eq!(results[0].len(), 4); // C(4,3) circuits for U_{2,4}
1787        assert_eq!(results[1].len(), 3); // C(3,2) circuits for U_{1,3}
1788    }
1789
1790    #[cfg(feature = "parallel")]
1791    #[test]
1792    fn test_tutte_polynomial_batch() {
1793        let matroids = vec![Matroid::uniform(1, 2), Matroid::uniform(1, 3)];
1794        let results = super::tutte_polynomial_batch(&matroids);
1795        assert_eq!(results.len(), 2);
1796        // U_{1,2}: T = x + y
1797        assert_eq!(results[0].get(&(1, 0)).copied().unwrap_or(0), 1);
1798        assert_eq!(results[0].get(&(0, 1)).copied().unwrap_or(0), 1);
1799    }
1800
1801    #[cfg(feature = "parallel")]
1802    #[test]
1803    fn test_intersection_cardinality_batch() {
1804        let pairs = vec![
1805            (Matroid::uniform(2, 4), Matroid::uniform(2, 4)),
1806            (Matroid::uniform(1, 3), Matroid::uniform(1, 3)),
1807        ];
1808        let results = super::intersection_cardinality_batch(&pairs);
1809        assert_eq!(results, vec![2, 1]);
1810    }
1811}