sim_lib_discrete_algebra/closure.rs
1//! Kleene closure `A* = I + A + A^2 + ...` over a closed semiring.
2//!
3//! This is the single derivation point for several graph algorithms:
4//! boolean closure yields transitive reachability, min-plus closure yields
5//! all-pairs shortest paths, and so on. The graph crate wraps this; it never
6//! re-implements Floyd-Warshall or Warshall directly.
7
8use crate::error::AlgebraError;
9use crate::matrix::{AlgebraLimits, Matrix};
10use crate::semiring::Semiring;
11
12impl<S: Semiring> Matrix<S> {
13 /// Compute the Kleene closure `A* = I + A + A^2 + ...`.
14 ///
15 /// Uses the generalized Floyd-Warshall (Lehmann) asteration: for each
16 /// pivot `k`, paths may pass through `k` and loop there `star(A[k][k])`
17 /// times. The identity is added at the end so `A*` includes the empty path
18 /// on the diagonal (distance-0 for min-plus, reflexive for boolean).
19 ///
20 /// Returns [`AlgebraError::NoStar`] when a pivot's diagonal star does not
21 /// converge (e.g. a negative cycle in min-plus, any directed cycle in
22 /// counting, or a semiring with no `star` such as `RealF64`).
23 ///
24 /// # Examples
25 ///
26 /// Boolean closure of a directed chain `0 -> 1 -> 2` yields reflexive
27 /// reachability: node `i` reaches node `j` exactly when `j >= i`.
28 ///
29 /// ```
30 /// use sim_lib_discrete_algebra::{AlgebraLimits, BoolRing, Matrix};
31 ///
32 /// let mut a = Matrix::new(3, 3);
33 /// a.set(0, 1, BoolRing(true)).unwrap();
34 /// a.set(1, 2, BoolRing(true)).unwrap();
35 ///
36 /// let reach = a.closure(AlgebraLimits::default()).unwrap();
37 /// assert_eq!(reach.get(0, 2).unwrap(), &BoolRing(true)); // 0 reaches 2
38 /// assert_eq!(reach.get(1, 1).unwrap(), &BoolRing(true)); // reflexive
39 /// assert_eq!(reach.get(2, 0).unwrap(), &BoolRing(false)); // 2 cannot reach 0
40 /// ```
41 pub fn closure(&self, limits: AlgebraLimits) -> Result<Self, AlgebraError> {
42 self.validate()?;
43 if !self.is_square() {
44 return Err(AlgebraError::ShapeMismatch(format!(
45 "closure requires a square matrix, got {}x{}",
46 self.rows, self.cols
47 )));
48 }
49 let n = self.rows;
50 if n > limits.max_dim {
51 return Err(AlgebraError::LimitExceeded(format!(
52 "closure: dimension {n} exceeds max_dim {}",
53 limits.max_dim
54 )));
55 }
56 let mut c = self.clone();
57 for k in 0..n {
58 let s = c.data[k * n + k].star().ok_or(AlgebraError::NoStar)?;
59 let mut next = c.clone();
60 for i in 0..n {
61 let cik = c.data[i * n + k].mul(&s);
62 for j in 0..n {
63 let via = cik.mul(&c.data[k * n + j]);
64 next.data[i * n + j] = c.data[i * n + j].add(&via);
65 }
66 }
67 c = next;
68 }
69 // Add the identity so the closure contains the empty path A^0 = I.
70 for i in 0..n {
71 let updated = c.data[i * n + i].add(&S::one());
72 c.data[i * n + i] = updated;
73 }
74 Ok(c)
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::boolean::BoolRing;
82 use crate::tropical_min::MinPlus;
83
84 #[test]
85 fn boolean_closure_of_directed_chain_is_upper_triangular_reachability() {
86 // Directed chain 0->1->2->3. Reachability is the reflexive upper
87 // triangle: i reaches j iff j >= i.
88 let mut a = Matrix::new(4, 4);
89 a.set(0, 1, BoolRing(true)).unwrap();
90 a.set(1, 2, BoolRing(true)).unwrap();
91 a.set(2, 3, BoolRing(true)).unwrap();
92 let star = a.closure(AlgebraLimits::default()).unwrap();
93 for i in 0..4 {
94 for j in 0..4 {
95 let expected = BoolRing(j >= i);
96 assert_eq!(star.get(i, j).unwrap(), &expected, "reachability ({i},{j})");
97 }
98 }
99 }
100
101 #[test]
102 fn min_plus_closure_matches_floyd_warshall() {
103 // Directed weighted graph: 0->1 (1), 1->2 (2), 0->2 (5), 2->3 (1).
104 let mut a = Matrix::filled(4, 4, MinPlus::Inf);
105 a.set(0, 1, MinPlus::Fin(1)).unwrap();
106 a.set(1, 2, MinPlus::Fin(2)).unwrap();
107 a.set(0, 2, MinPlus::Fin(5)).unwrap();
108 a.set(2, 3, MinPlus::Fin(1)).unwrap();
109 let star = a.closure(AlgebraLimits::default()).unwrap();
110 // shortest 0->2 is via 1: 1+2=3, beating the direct edge 5.
111 assert_eq!(star.get(0, 2).unwrap(), &MinPlus::Fin(3));
112 // 0->3 = 1+2+1 = 4.
113 assert_eq!(star.get(0, 3).unwrap(), &MinPlus::Fin(4));
114 // diagonal is 0 (empty path); unreachable stays Inf.
115 assert_eq!(star.get(0, 0).unwrap(), &MinPlus::Fin(0));
116 assert_eq!(star.get(3, 0).unwrap(), &MinPlus::Inf);
117 }
118
119 #[test]
120 fn closure_without_star_reports_no_star() {
121 use crate::real::RealF64;
122 let a: Matrix<RealF64> = Matrix::filled(2, 2, RealF64(0.5));
123 assert_eq!(
124 a.closure(AlgebraLimits::default()).unwrap_err(),
125 AlgebraError::NoStar
126 );
127 }
128}