Skip to main content

ruvector_math/tropical/
matrix.rs

1//! Tropical Matrices
2//!
3//! Matrix operations in the tropical semiring.
4//! Applications:
5//! - Shortest path algorithms (Floyd-Warshall)
6//! - Scheduling optimization
7//! - Graph eigenvalue problems
8
9use super::semiring::{Tropical, TropicalMin};
10
11/// Tropical matrix (max-plus)
12#[derive(Debug, Clone)]
13pub struct TropicalMatrix {
14    rows: usize,
15    cols: usize,
16    data: Vec<f64>,
17}
18
19impl TropicalMatrix {
20    /// Create zero matrix (all -∞)
21    pub fn zeros(rows: usize, cols: usize) -> Self {
22        Self {
23            rows,
24            cols,
25            data: vec![f64::NEG_INFINITY; rows * cols],
26        }
27    }
28
29    /// Create identity matrix (0 on diagonal, -∞ elsewhere)
30    pub fn identity(n: usize) -> Self {
31        let mut m = Self::zeros(n, n);
32        for i in 0..n {
33            m.set(i, i, 0.0);
34        }
35        m
36    }
37
38    /// Create from 2D data
39    pub fn from_rows(data: Vec<Vec<f64>>) -> Self {
40        let rows = data.len();
41        let cols = if rows > 0 { data[0].len() } else { 0 };
42        let flat: Vec<f64> = data.into_iter().flatten().collect();
43        Self {
44            rows,
45            cols,
46            data: flat,
47        }
48    }
49
50    /// Get element (returns -∞ for out of bounds)
51    #[inline]
52    pub fn get(&self, i: usize, j: usize) -> f64 {
53        if i >= self.rows || j >= self.cols {
54            return f64::NEG_INFINITY;
55        }
56        self.data[i * self.cols + j]
57    }
58
59    /// Set element (no-op for out of bounds)
60    #[inline]
61    pub fn set(&mut self, i: usize, j: usize, val: f64) {
62        if i >= self.rows || j >= self.cols {
63            return;
64        }
65        self.data[i * self.cols + j] = val;
66    }
67
68    /// Matrix dimensions
69    pub fn dims(&self) -> (usize, usize) {
70        (self.rows, self.cols)
71    }
72
73    /// Tropical matrix multiplication: C[i,k] = max_j(A[i,j] + B[j,k])
74    pub fn mul(&self, other: &Self) -> Self {
75        assert_eq!(self.cols, other.rows, "Dimension mismatch");
76
77        let mut result = Self::zeros(self.rows, other.cols);
78
79        for i in 0..self.rows {
80            for k in 0..other.cols {
81                let mut max_val = f64::NEG_INFINITY;
82                for j in 0..self.cols {
83                    let a = self.get(i, j);
84                    let b = other.get(j, k);
85
86                    if a != f64::NEG_INFINITY && b != f64::NEG_INFINITY {
87                        max_val = max_val.max(a + b);
88                    }
89                }
90                result.set(i, k, max_val);
91            }
92        }
93
94        result
95    }
96
97    /// Tropical matrix power: A^n (n tropical multiplications)
98    pub fn pow(&self, n: usize) -> Self {
99        assert_eq!(self.rows, self.cols, "Must be square");
100
101        if n == 0 {
102            return Self::identity(self.rows);
103        }
104
105        let mut result = self.clone();
106        for _ in 1..n {
107            result = result.mul(self);
108        }
109        result
110    }
111
112    /// Tropical matrix closure: A* = I ⊕ A ⊕ A² ⊕ ... ⊕ A^n
113    /// Computes all shortest paths (min-plus version is Floyd-Warshall)
114    pub fn closure(&self) -> Self {
115        assert_eq!(self.rows, self.cols, "Must be square");
116        let n = self.rows;
117
118        let mut result = Self::identity(n);
119        let mut power = self.clone();
120
121        for _ in 0..n {
122            // result = result ⊕ power
123            for i in 0..n {
124                for j in 0..n {
125                    let old = result.get(i, j);
126                    let new = power.get(i, j);
127                    result.set(i, j, old.max(new));
128                }
129            }
130            power = power.mul(self);
131        }
132
133        result
134    }
135
136    /// Find tropical eigenvalue (max cycle mean)
137    /// Returns the maximum average weight of any cycle
138    pub fn max_cycle_mean(&self) -> f64 {
139        assert_eq!(self.rows, self.cols, "Must be square");
140        let n = self.rows;
141
142        // Karp's algorithm for maximum cycle mean
143        let mut d = vec![vec![f64::NEG_INFINITY; n + 1]; n];
144
145        // Initialize d[i][0] = 0 for all i
146        for i in 0..n {
147            d[i][0] = 0.0;
148        }
149
150        // Dynamic programming
151        for k in 1..=n {
152            for i in 0..n {
153                for j in 0..n {
154                    let w = self.get(i, j);
155                    if w != f64::NEG_INFINITY && d[j][k - 1] != f64::NEG_INFINITY {
156                        d[i][k] = d[i][k].max(w + d[j][k - 1]);
157                    }
158                }
159            }
160        }
161
162        // Compute max cycle mean
163        let mut lambda = f64::NEG_INFINITY;
164        for i in 0..n {
165            if d[i][n] != f64::NEG_INFINITY {
166                let mut min_ratio = f64::INFINITY;
167                for k in 0..n {
168                    // Security: prevent division by zero when k == n
169                    if k < n && d[i][k] != f64::NEG_INFINITY {
170                        let divisor = (n - k) as f64;
171                        if divisor > 0.0 {
172                            let ratio = (d[i][n] - d[i][k]) / divisor;
173                            min_ratio = min_ratio.min(ratio);
174                        }
175                    }
176                }
177                lambda = lambda.max(min_ratio);
178            }
179        }
180
181        lambda
182    }
183}
184
185/// Tropical eigenvalue and eigenvector
186#[derive(Debug, Clone)]
187pub struct TropicalEigen {
188    /// Eigenvalue (cycle mean)
189    pub eigenvalue: f64,
190    /// Eigenvector
191    pub eigenvector: Vec<f64>,
192}
193
194impl TropicalEigen {
195    /// Compute tropical eigenpair using power iteration
196    /// Finds λ and v such that A ⊗ v = λ ⊗ v (i.e., max_j(A[i,j] + v[j]) = λ + v[i])
197    pub fn power_iteration(matrix: &TropicalMatrix, max_iters: usize) -> Option<Self> {
198        let n = matrix.rows;
199        if n == 0 {
200            return None;
201        }
202
203        // Start with uniform vector
204        let mut v: Vec<f64> = vec![0.0; n];
205        let mut eigenvalue = 0.0f64;
206
207        for _ in 0..max_iters {
208            // Compute A ⊗ v
209            let mut av = vec![f64::NEG_INFINITY; n];
210            for i in 0..n {
211                for j in 0..n {
212                    let aij = matrix.get(i, j);
213                    if aij != f64::NEG_INFINITY && v[j] != f64::NEG_INFINITY {
214                        av[i] = av[i].max(aij + v[j]);
215                    }
216                }
217            }
218
219            // Find max to normalize
220            let max_av = av.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
221            if max_av == f64::NEG_INFINITY {
222                return None;
223            }
224
225            // Eigenvalue = growth rate
226            let new_eigenvalue = max_av - v.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
227
228            // Normalize: v = av - max(av)
229            for i in 0..n {
230                v[i] = av[i] - max_av;
231            }
232
233            // Check convergence
234            if (new_eigenvalue - eigenvalue).abs() < 1e-10 {
235                return Some(TropicalEigen {
236                    eigenvalue: new_eigenvalue,
237                    eigenvector: v,
238                });
239            }
240
241            eigenvalue = new_eigenvalue;
242        }
243
244        Some(TropicalEigen {
245            eigenvalue,
246            eigenvector: v,
247        })
248    }
249}
250
251/// Min-plus matrix for shortest paths
252#[derive(Debug, Clone)]
253pub struct MinPlusMatrix {
254    rows: usize,
255    cols: usize,
256    data: Vec<f64>,
257}
258
259impl MinPlusMatrix {
260    /// Create from adjacency weights (+∞ for no edge)
261    pub fn from_adjacency(adj: Vec<Vec<f64>>) -> Self {
262        let rows = adj.len();
263        let cols = if rows > 0 { adj[0].len() } else { 0 };
264        let data: Vec<f64> = adj.into_iter().flatten().collect();
265        Self { rows, cols, data }
266    }
267
268    /// Get element (returns +∞ for out of bounds)
269    #[inline]
270    pub fn get(&self, i: usize, j: usize) -> f64 {
271        if i >= self.rows || j >= self.cols {
272            return f64::INFINITY;
273        }
274        self.data[i * self.cols + j]
275    }
276
277    /// Set element (no-op for out of bounds)
278    #[inline]
279    pub fn set(&mut self, i: usize, j: usize, val: f64) {
280        if i >= self.rows || j >= self.cols {
281            return;
282        }
283        self.data[i * self.cols + j] = val;
284    }
285
286    /// Floyd-Warshall all-pairs shortest paths (min-plus closure)
287    pub fn all_pairs_shortest_paths(&self) -> Self {
288        let n = self.rows;
289        let mut dist = self.clone();
290
291        for k in 0..n {
292            for i in 0..n {
293                for j in 0..n {
294                    let via_k = dist.get(i, k) + dist.get(k, j);
295                    if via_k < dist.get(i, j) {
296                        dist.set(i, j, via_k);
297                    }
298                }
299            }
300        }
301
302        dist
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn test_tropical_matrix_mul() {
312        // A = [[0, 1], [-∞, 2]]
313        let a = TropicalMatrix::from_rows(vec![vec![0.0, 1.0], vec![f64::NEG_INFINITY, 2.0]]);
314
315        // A² = [[max(0+0, 1-∞), max(0+1, 1+2)], ...]
316        let a2 = a.mul(&a);
317
318        assert!((a2.get(0, 1) - 3.0).abs() < 1e-10); // max(0+1, 1+2) = 3
319    }
320
321    #[test]
322    fn test_tropical_identity() {
323        let i = TropicalMatrix::identity(3);
324        let a = TropicalMatrix::from_rows(vec![
325            vec![1.0, 2.0, 3.0],
326            vec![4.0, 5.0, 6.0],
327            vec![7.0, 8.0, 9.0],
328        ]);
329
330        let ia = i.mul(&a);
331        for row in 0..3 {
332            for col in 0..3 {
333                assert!((ia.get(row, col) - a.get(row, col)).abs() < 1e-10);
334            }
335        }
336    }
337
338    #[test]
339    fn test_max_cycle_mean() {
340        // Simple cycle: 0 -> 1 (weight 3), 1 -> 0 (weight 1)
341        // Cycle mean = (3 + 1) / 2 = 2
342        let a = TropicalMatrix::from_rows(vec![
343            vec![f64::NEG_INFINITY, 3.0],
344            vec![1.0, f64::NEG_INFINITY],
345        ]);
346
347        let mcm = a.max_cycle_mean();
348        assert!((mcm - 2.0).abs() < 1e-10);
349    }
350
351    #[test]
352    fn test_floyd_warshall() {
353        // Graph: 0 -1-> 1 -2-> 2, 0 -5-> 2
354        let adj = MinPlusMatrix::from_adjacency(vec![
355            vec![0.0, 1.0, 5.0],
356            vec![f64::INFINITY, 0.0, 2.0],
357            vec![f64::INFINITY, f64::INFINITY, 0.0],
358        ]);
359
360        let dist = adj.all_pairs_shortest_paths();
361
362        // Shortest 0->2 is via 1: 1 + 2 = 3
363        assert!((dist.get(0, 2) - 3.0).abs() < 1e-10);
364    }
365}