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 ShaperOS 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/// ShaperOS integration: matroid-based namespace analysis.
678impl Namespace {
679    /// Compute the capability matroid of this namespace.
680    ///
681    /// Capabilities are elements; a set is independent if the corresponding
682    /// Schubert classes have a transverse intersection.
683    ///
684    /// # Contract
685    ///
686    /// ```text
687    /// ensures: result.is_some() implies result.ground_set_size == self.capabilities.len()
688    /// ensures: result.is_none() implies self.capabilities.is_empty()
689    /// ```
690    #[must_use]
691    pub fn capability_matroid(&self) -> Option<Matroid> {
692        let n = self.capabilities.len();
693        if n == 0 {
694            return None;
695        }
696
697        let (k, dim_n) = self.grassmannian;
698        let gr_dim = k * (dim_n - k);
699
700        // Find all maximal independent sets (bases)
701        let mut max_size = 0;
702        let mut candidates: Vec<BTreeSet<usize>> = Vec::new();
703
704        // Try all subsets
705        for mask in 1u64..(1u64 << n) {
706            let subset: Vec<usize> = (0..n).filter(|&i| mask & (1 << i) != 0).collect();
707            let total_codim: usize = subset
708                .iter()
709                .map(|&i| self.capabilities[i].codimension())
710                .sum();
711
712            if total_codim <= gr_dim {
713                let size = subset.len();
714                if size > max_size {
715                    max_size = size;
716                    candidates.clear();
717                }
718                if size == max_size {
719                    candidates.push(subset.into_iter().collect());
720                }
721            }
722        }
723
724        if candidates.is_empty() {
725            return None;
726        }
727
728        let bases: BTreeSet<BTreeSet<usize>> = candidates.into_iter().collect();
729        Some(Matroid {
730            ground_set_size: n,
731            bases,
732            rank: max_size,
733        })
734    }
735
736    /// Find redundant capabilities: those that don't affect the matroid rank.
737    ///
738    /// A capability is redundant if removing it doesn't change the rank
739    /// of the capability matroid (i.e., it is a loop).
740    ///
741    /// # Contract
742    ///
743    /// ```text
744    /// ensures: forall id in result. removing id does not change matroid rank
745    /// ```
746    #[must_use]
747    pub fn redundant_capabilities(&self) -> Vec<CapabilityId> {
748        let Some(matroid) = self.capability_matroid() else {
749            return Vec::new();
750        };
751
752        let mut redundant = Vec::new();
753        for (i, cap) in self.capabilities.iter().enumerate() {
754            if matroid.is_loop(i) {
755                redundant.push(cap.id.clone());
756            }
757        }
758        redundant
759    }
760
761    /// Maximum number of capabilities that can be shared between two namespaces.
762    ///
763    /// Uses matroid intersection to find the largest common independent set.
764    ///
765    /// # Contract
766    ///
767    /// ```text
768    /// ensures: result <= min(self.capabilities.len(), other.capabilities.len())
769    /// ```
770    #[must_use]
771    pub fn max_shared_capabilities(&self, other: &Namespace) -> usize {
772        let m1 = self.capability_matroid();
773        let m2 = other.capability_matroid();
774
775        match (m1, m2) {
776            (Some(m1), Some(m2)) if m1.ground_set_size == m2.ground_set_size => {
777                m1.intersection_cardinality(&m2)
778            }
779            _ => 0,
780        }
781    }
782}
783
784/// A matroid with a valuation on its bases (tropical Plücker vector).
785///
786/// The valuation v: bases → R satisfies the tropical Plücker relations.
787#[derive(Debug, Clone)]
788pub struct ValuatedMatroid {
789    /// Underlying matroid
790    pub matroid: Matroid,
791    /// Valuation: basis → value
792    pub valuation: BTreeMap<BTreeSet<usize>, f64>,
793}
794
795impl ValuatedMatroid {
796    /// Create a valuated matroid from tropical Plücker coordinates.
797    ///
798    /// # Contract
799    ///
800    /// ```text
801    /// requires: coords.len() == C(n, k)
802    /// ensures: result satisfies tropical Plücker relations (approximately)
803    /// ```
804    pub fn from_tropical_plucker(k: usize, n: usize, coords: &[f64]) -> Result<Self, String> {
805        let subsets = k_subsets_vec(n, k);
806
807        if coords.len() != subsets.len() {
808            return Err(format!(
809                "Expected {} tropical Plücker coordinates, got {}",
810                subsets.len(),
811                coords.len()
812            ));
813        }
814
815        let mut bases = BTreeSet::new();
816        let mut valuation = BTreeMap::new();
817
818        for (i, subset) in subsets.iter().enumerate() {
819            if coords[i].is_finite() {
820                let basis: BTreeSet<usize> = subset.iter().copied().collect();
821                bases.insert(basis.clone());
822                valuation.insert(basis, coords[i]);
823            }
824        }
825
826        if bases.is_empty() {
827            return Err("No finite tropical Plücker coordinates".to_string());
828        }
829
830        let rank = k;
831        let matroid = Matroid {
832            ground_set_size: n,
833            bases,
834            rank,
835        };
836
837        Ok(Self { matroid, valuation })
838    }
839
840    /// Check if the valuation satisfies tropical Plücker relations.
841    ///
842    /// The 3-term tropical Plücker relation states:
843    /// for any (k-1)-subset S and elements a < b < c < d not in S,
844    /// the minimum of {v(S∪{a,c}) + v(S∪{b,d}), v(S∪{a,d}) + v(S∪{b,c})}
845    /// is attained at least twice (where v(S∪{a,b}) + v(S∪{c,d}) is included).
846    #[must_use]
847    pub fn satisfies_tropical_plucker(&self) -> bool {
848        // For small matroids, just return true (full check is expensive)
849        if self.matroid.rank <= 1 || self.matroid.ground_set_size <= 3 {
850            return true;
851        }
852
853        // Check the tropical Plücker relations
854        let k = self.matroid.rank;
855        let _n = self.matroid.ground_set_size;
856
857        if k < 2 {
858            return true;
859        }
860
861        // For each pair of k-subsets differing in exactly 2 elements
862        for b1 in &self.matroid.bases {
863            for b2 in &self.matroid.bases {
864                let diff1: Vec<usize> = b1.difference(b2).copied().collect();
865                let diff2: Vec<usize> = b2.difference(b1).copied().collect();
866
867                if diff1.len() != 2 || diff2.len() != 2 {
868                    continue;
869                }
870
871                // This is a basic form of the Plücker relation
872                let v1 = self.valuation.get(b1).copied().unwrap_or(f64::INFINITY);
873                let v2 = self.valuation.get(b2).copied().unwrap_or(f64::INFINITY);
874
875                // The exchange axiom in the valuated setting
876                let i = diff1[0];
877                let _j = diff1[1];
878                let a = diff2[0];
879                let b_elem = diff2[1];
880
881                // Check: min of the three exchange terms is attained twice
882                let mut exchange1 = b1.clone();
883                exchange1.remove(&i);
884                exchange1.insert(a);
885
886                let mut exchange2 = b1.clone();
887                exchange2.remove(&i);
888                exchange2.insert(b_elem);
889
890                let v_ex1 = self
891                    .valuation
892                    .get(&exchange1)
893                    .copied()
894                    .unwrap_or(f64::INFINITY);
895                let v_ex2 = self
896                    .valuation
897                    .get(&exchange2)
898                    .copied()
899                    .unwrap_or(f64::INFINITY);
900
901                // Basic check: finite valuations should have consistent exchanges
902                if v1.is_finite() && v2.is_finite() && v_ex1.is_infinite() && v_ex2.is_infinite() {
903                    return false;
904                }
905            }
906        }
907
908        true
909    }
910
911    /// Convert to a TropicalSchubertClass (when tropical-schubert feature is enabled).
912    ///
913    /// Extracts the valuation as tropical weights.
914    #[cfg(feature = "tropical-schubert")]
915    #[must_use]
916    pub fn to_tropical_schubert(&self) -> crate::tropical_schubert::TropicalSchubertClass {
917        // Collect valuation values as integer weights
918        let weights: Vec<i64> = self.valuation.values().map(|&v| v as i64).collect();
919
920        crate::tropical_schubert::TropicalSchubertClass::new(weights)
921    }
922}
923
924// Helper functions
925
926fn k_subsets_btree(n: usize, k: usize) -> BTreeSet<BTreeSet<usize>> {
927    k_subsets_vec(n, k)
928        .into_iter()
929        .map(|s| s.into_iter().collect())
930        .collect()
931}
932
933fn k_subsets_vec(n: usize, k: usize) -> Vec<Vec<usize>> {
934    let mut result = Vec::new();
935    let mut current = Vec::with_capacity(k);
936    gen_subsets(n, k, 0, &mut current, &mut result);
937    result
938}
939
940fn gen_subsets(
941    n: usize,
942    k: usize,
943    start: usize,
944    current: &mut Vec<usize>,
945    result: &mut Vec<Vec<usize>>,
946) {
947    if current.len() == k {
948        result.push(current.clone());
949        return;
950    }
951    let remaining = k - current.len();
952    if start + remaining > n {
953        return;
954    }
955    for i in start..=(n - remaining) {
956        current.push(i);
957        gen_subsets(n, k, i + 1, current, result);
958        current.pop();
959    }
960}
961
962fn subsets_of_size(n: usize, k: usize) -> Vec<BTreeSet<usize>> {
963    k_subsets_vec(n, k)
964        .into_iter()
965        .map(|s| s.into_iter().collect())
966        .collect()
967}
968
969/// Batch compute matroid circuits for multiple matroids in parallel.
970#[cfg(feature = "parallel")]
971#[must_use]
972pub fn circuits_batch(matroids: &[Matroid]) -> Vec<BTreeSet<BTreeSet<usize>>> {
973    use rayon::prelude::*;
974    matroids.par_iter().map(|m| m.circuits()).collect()
975}
976
977/// Batch compute Tutte polynomials for multiple matroids in parallel.
978#[cfg(feature = "parallel")]
979#[must_use]
980pub fn tutte_polynomial_batch(matroids: &[Matroid]) -> Vec<BTreeMap<(usize, usize), i64>> {
981    use rayon::prelude::*;
982    matroids.par_iter().map(|m| m.tutte_polynomial()).collect()
983}
984
985/// Batch compute matroid intersection cardinalities in parallel.
986#[cfg(feature = "parallel")]
987#[must_use]
988pub fn intersection_cardinality_batch(pairs: &[(Matroid, Matroid)]) -> Vec<usize> {
989    use rayon::prelude::*;
990    pairs
991        .par_iter()
992        .map(|(m1, m2)| m1.intersection_cardinality(m2))
993        .collect()
994}
995
996#[cfg(test)]
997mod tests {
998    use super::*;
999    use crate::namespace::Capability;
1000    use crate::schubert::SchubertClass;
1001
1002    #[test]
1003    fn test_uniform_matroid() {
1004        let m = Matroid::uniform(2, 4);
1005        assert_eq!(m.rank, 2);
1006        assert_eq!(m.bases.len(), 6); // C(4,2)
1007        assert_eq!(m.ground_set_size, 4);
1008    }
1009
1010    #[test]
1011    fn test_matroid_from_plucker() {
1012        // All Plücker coordinates nonzero → uniform matroid
1013        let coords = vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.0]; // C(4,2) = 6 coords
1014        let m = Matroid::from_plucker(2, 4, &coords, 0.001).unwrap();
1015        assert_eq!(m.rank, 2);
1016        assert_eq!(m.bases.len(), 6);
1017    }
1018
1019    #[test]
1020    fn test_matroid_from_plucker_partial() {
1021        // Some zero coordinates
1022        let coords = vec![1.0, 0.0, 1.0, 1.0, 0.0, 1.0];
1023        let m = Matroid::from_plucker(2, 4, &coords, 0.001).unwrap();
1024        assert_eq!(m.rank, 2);
1025        assert_eq!(m.bases.len(), 4);
1026    }
1027
1028    #[test]
1029    fn test_schubert_matroid() {
1030        // Schubert matroid for σ_1 in Gr(2,4): k=2, m=2, λ=[1,0]
1031        // bounds: j=0 → 2+0-0=2, j=1 → 2+1-1=2
1032        // Bases: {i_0 < i_1} with i_0 ≤ 2, i_1 ≤ 2 → {0,1}, {0,2}, {1,2}
1033        let m = Matroid::schubert_matroid(&[1], 2, 4).unwrap();
1034        assert_eq!(m.rank, 2);
1035        assert_eq!(m.bases.len(), 3);
1036    }
1037
1038    #[test]
1039    fn test_schubert_matroid_empty_partition() {
1040        // Empty partition σ_∅ → bounds: j=0 → 2, j=1 → 3 → all subsets are bases
1041        let m = Matroid::schubert_matroid(&[], 2, 4).unwrap();
1042        assert_eq!(m.bases.len(), 6);
1043    }
1044
1045    #[test]
1046    fn test_matroid_circuits() {
1047        // U_{2,4}: circuits are all 3-element subsets
1048        let m = Matroid::uniform(2, 4);
1049        let circuits = m.circuits();
1050        assert_eq!(circuits.len(), 4); // C(4,3) = 4
1051    }
1052
1053    #[test]
1054    fn test_matroid_dual() {
1055        let m = Matroid::uniform(2, 4);
1056        let dual = m.dual();
1057        assert_eq!(dual.rank, 2); // 4 - 2
1058        assert_eq!(dual.bases.len(), 6); // U_{2,4} is self-dual
1059    }
1060
1061    #[test]
1062    fn test_matroid_direct_sum() {
1063        let m1 = Matroid::uniform(1, 2);
1064        let m2 = Matroid::uniform(1, 2);
1065        let sum = m1.direct_sum(&m2);
1066        assert_eq!(sum.rank, 2);
1067        assert_eq!(sum.ground_set_size, 4);
1068    }
1069
1070    #[test]
1071    fn test_matroid_loop_coloop() {
1072        // U_{2,3}: no loops, no coloops
1073        let m = Matroid::uniform(2, 3);
1074        for e in 0..3 {
1075            assert!(!m.is_loop(e));
1076            assert!(!m.is_coloop(e));
1077        }
1078    }
1079
1080    #[test]
1081    fn test_matroid_delete_contract() {
1082        let m = Matroid::uniform(2, 4);
1083
1084        let deleted = m.delete(0);
1085        assert_eq!(deleted.ground_set_size, 3);
1086        assert_eq!(deleted.rank, 2);
1087
1088        let contracted = m.contract(0);
1089        assert_eq!(contracted.ground_set_size, 3);
1090        assert_eq!(contracted.rank, 1);
1091    }
1092
1093    #[test]
1094    fn test_matroid_intersection_cardinality() {
1095        let m1 = Matroid::uniform(2, 4);
1096        let m2 = Matroid::uniform(2, 4);
1097        // Intersection of two copies of U_{2,4} should be 2
1098        assert_eq!(m1.intersection_cardinality(&m2), 2);
1099    }
1100
1101    #[test]
1102    fn test_tutte_polynomial_uniform() {
1103        let m = Matroid::uniform(1, 2);
1104        let tutte = m.tutte_polynomial();
1105        // T_{U_{1,2}}(x, y) = x + y
1106        assert_eq!(tutte.get(&(1, 0)).copied().unwrap_or(0), 1);
1107        assert_eq!(tutte.get(&(0, 1)).copied().unwrap_or(0), 1);
1108    }
1109
1110    #[test]
1111    fn test_namespace_capability_matroid() {
1112        let pos = SchubertClass::new(vec![], (2, 4)).unwrap();
1113        let mut ns = Namespace::new("test", pos);
1114
1115        let cap1 = Capability::new("c1", "Cap 1", vec![1], (2, 4)).unwrap();
1116        let cap2 = Capability::new("c2", "Cap 2", vec![1], (2, 4)).unwrap();
1117        ns.grant(cap1).unwrap();
1118        ns.grant(cap2).unwrap();
1119
1120        let matroid = ns.capability_matroid();
1121        assert!(matroid.is_some());
1122        let m = matroid.unwrap();
1123        assert_eq!(m.ground_set_size, 2);
1124    }
1125
1126    #[test]
1127    fn test_valuated_matroid_tropical_plucker() {
1128        let coords = vec![0.0, 1.0, 2.0, 1.0, 2.0, 3.0]; // C(4,2)=6
1129        let vm = ValuatedMatroid::from_tropical_plucker(2, 4, &coords).unwrap();
1130        assert_eq!(vm.matroid.rank, 2);
1131        assert!(vm.satisfies_tropical_plucker());
1132    }
1133
1134    #[cfg(feature = "parallel")]
1135    #[test]
1136    fn test_circuits_batch() {
1137        let matroids = vec![Matroid::uniform(2, 4), Matroid::uniform(1, 3)];
1138        let results = super::circuits_batch(&matroids);
1139        assert_eq!(results[0].len(), 4); // C(4,3) circuits for U_{2,4}
1140        assert_eq!(results[1].len(), 3); // C(3,2) circuits for U_{1,3}
1141    }
1142
1143    #[cfg(feature = "parallel")]
1144    #[test]
1145    fn test_tutte_polynomial_batch() {
1146        let matroids = vec![Matroid::uniform(1, 2), Matroid::uniform(1, 3)];
1147        let results = super::tutte_polynomial_batch(&matroids);
1148        assert_eq!(results.len(), 2);
1149        // U_{1,2}: T = x + y
1150        assert_eq!(results[0].get(&(1, 0)).copied().unwrap_or(0), 1);
1151        assert_eq!(results[0].get(&(0, 1)).copied().unwrap_or(0), 1);
1152    }
1153
1154    #[cfg(feature = "parallel")]
1155    #[test]
1156    fn test_intersection_cardinality_batch() {
1157        let pairs = vec![
1158            (Matroid::uniform(2, 4), Matroid::uniform(2, 4)),
1159            (Matroid::uniform(1, 3), Matroid::uniform(1, 3)),
1160        ];
1161        let results = super::intersection_cardinality_batch(&pairs);
1162        assert_eq!(results, vec![2, 1]);
1163    }
1164}