Files
wifi-densepose/crates/ruvector-math/src/tropical/matrix.rs
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

366 lines
10 KiB
Rust

//! Tropical Matrices
//!
//! Matrix operations in the tropical semiring.
//! Applications:
//! - Shortest path algorithms (Floyd-Warshall)
//! - Scheduling optimization
//! - Graph eigenvalue problems
use super::semiring::{Tropical, TropicalMin};
/// Tropical matrix (max-plus)
#[derive(Debug, Clone)]
pub struct TropicalMatrix {
rows: usize,
cols: usize,
data: Vec<f64>,
}
impl TropicalMatrix {
/// Create zero matrix (all -∞)
pub fn zeros(rows: usize, cols: usize) -> Self {
Self {
rows,
cols,
data: vec![f64::NEG_INFINITY; rows * cols],
}
}
/// Create identity matrix (0 on diagonal, -∞ elsewhere)
pub fn identity(n: usize) -> Self {
let mut m = Self::zeros(n, n);
for i in 0..n {
m.set(i, i, 0.0);
}
m
}
/// Create from 2D data
pub fn from_rows(data: Vec<Vec<f64>>) -> Self {
let rows = data.len();
let cols = if rows > 0 { data[0].len() } else { 0 };
let flat: Vec<f64> = data.into_iter().flatten().collect();
Self {
rows,
cols,
data: flat,
}
}
/// Get element (returns -∞ for out of bounds)
#[inline]
pub fn get(&self, i: usize, j: usize) -> f64 {
if i >= self.rows || j >= self.cols {
return f64::NEG_INFINITY;
}
self.data[i * self.cols + j]
}
/// Set element (no-op for out of bounds)
#[inline]
pub fn set(&mut self, i: usize, j: usize, val: f64) {
if i >= self.rows || j >= self.cols {
return;
}
self.data[i * self.cols + j] = val;
}
/// Matrix dimensions
pub fn dims(&self) -> (usize, usize) {
(self.rows, self.cols)
}
/// Tropical matrix multiplication: C[i,k] = max_j(A[i,j] + B[j,k])
pub fn mul(&self, other: &Self) -> Self {
assert_eq!(self.cols, other.rows, "Dimension mismatch");
let mut result = Self::zeros(self.rows, other.cols);
for i in 0..self.rows {
for k in 0..other.cols {
let mut max_val = f64::NEG_INFINITY;
for j in 0..self.cols {
let a = self.get(i, j);
let b = other.get(j, k);
if a != f64::NEG_INFINITY && b != f64::NEG_INFINITY {
max_val = max_val.max(a + b);
}
}
result.set(i, k, max_val);
}
}
result
}
/// Tropical matrix power: A^n (n tropical multiplications)
pub fn pow(&self, n: usize) -> Self {
assert_eq!(self.rows, self.cols, "Must be square");
if n == 0 {
return Self::identity(self.rows);
}
let mut result = self.clone();
for _ in 1..n {
result = result.mul(self);
}
result
}
/// Tropical matrix closure: A* = I ⊕ A ⊕ A² ⊕ ... ⊕ A^n
/// Computes all shortest paths (min-plus version is Floyd-Warshall)
pub fn closure(&self) -> Self {
assert_eq!(self.rows, self.cols, "Must be square");
let n = self.rows;
let mut result = Self::identity(n);
let mut power = self.clone();
for _ in 0..n {
// result = result ⊕ power
for i in 0..n {
for j in 0..n {
let old = result.get(i, j);
let new = power.get(i, j);
result.set(i, j, old.max(new));
}
}
power = power.mul(self);
}
result
}
/// Find tropical eigenvalue (max cycle mean)
/// Returns the maximum average weight of any cycle
pub fn max_cycle_mean(&self) -> f64 {
assert_eq!(self.rows, self.cols, "Must be square");
let n = self.rows;
// Karp's algorithm for maximum cycle mean
let mut d = vec![vec![f64::NEG_INFINITY; n + 1]; n];
// Initialize d[i][0] = 0 for all i
for i in 0..n {
d[i][0] = 0.0;
}
// Dynamic programming
for k in 1..=n {
for i in 0..n {
for j in 0..n {
let w = self.get(i, j);
if w != f64::NEG_INFINITY && d[j][k - 1] != f64::NEG_INFINITY {
d[i][k] = d[i][k].max(w + d[j][k - 1]);
}
}
}
}
// Compute max cycle mean
let mut lambda = f64::NEG_INFINITY;
for i in 0..n {
if d[i][n] != f64::NEG_INFINITY {
let mut min_ratio = f64::INFINITY;
for k in 0..n {
// Security: prevent division by zero when k == n
if k < n && d[i][k] != f64::NEG_INFINITY {
let divisor = (n - k) as f64;
if divisor > 0.0 {
let ratio = (d[i][n] - d[i][k]) / divisor;
min_ratio = min_ratio.min(ratio);
}
}
}
lambda = lambda.max(min_ratio);
}
}
lambda
}
}
/// Tropical eigenvalue and eigenvector
#[derive(Debug, Clone)]
pub struct TropicalEigen {
/// Eigenvalue (cycle mean)
pub eigenvalue: f64,
/// Eigenvector
pub eigenvector: Vec<f64>,
}
impl TropicalEigen {
/// Compute tropical eigenpair using power iteration
/// Finds λ and v such that A ⊗ v = λ ⊗ v (i.e., max_j(A[i,j] + v[j]) = λ + v[i])
pub fn power_iteration(matrix: &TropicalMatrix, max_iters: usize) -> Option<Self> {
let n = matrix.rows;
if n == 0 {
return None;
}
// Start with uniform vector
let mut v: Vec<f64> = vec![0.0; n];
let mut eigenvalue = 0.0f64;
for _ in 0..max_iters {
// Compute A ⊗ v
let mut av = vec![f64::NEG_INFINITY; n];
for i in 0..n {
for j in 0..n {
let aij = matrix.get(i, j);
if aij != f64::NEG_INFINITY && v[j] != f64::NEG_INFINITY {
av[i] = av[i].max(aij + v[j]);
}
}
}
// Find max to normalize
let max_av = av.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
if max_av == f64::NEG_INFINITY {
return None;
}
// Eigenvalue = growth rate
let new_eigenvalue = max_av - v.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
// Normalize: v = av - max(av)
for i in 0..n {
v[i] = av[i] - max_av;
}
// Check convergence
if (new_eigenvalue - eigenvalue).abs() < 1e-10 {
return Some(TropicalEigen {
eigenvalue: new_eigenvalue,
eigenvector: v,
});
}
eigenvalue = new_eigenvalue;
}
Some(TropicalEigen {
eigenvalue,
eigenvector: v,
})
}
}
/// Min-plus matrix for shortest paths
#[derive(Debug, Clone)]
pub struct MinPlusMatrix {
rows: usize,
cols: usize,
data: Vec<f64>,
}
impl MinPlusMatrix {
/// Create from adjacency weights (+∞ for no edge)
pub fn from_adjacency(adj: Vec<Vec<f64>>) -> Self {
let rows = adj.len();
let cols = if rows > 0 { adj[0].len() } else { 0 };
let data: Vec<f64> = adj.into_iter().flatten().collect();
Self { rows, cols, data }
}
/// Get element (returns +∞ for out of bounds)
#[inline]
pub fn get(&self, i: usize, j: usize) -> f64 {
if i >= self.rows || j >= self.cols {
return f64::INFINITY;
}
self.data[i * self.cols + j]
}
/// Set element (no-op for out of bounds)
#[inline]
pub fn set(&mut self, i: usize, j: usize, val: f64) {
if i >= self.rows || j >= self.cols {
return;
}
self.data[i * self.cols + j] = val;
}
/// Floyd-Warshall all-pairs shortest paths (min-plus closure)
pub fn all_pairs_shortest_paths(&self) -> Self {
let n = self.rows;
let mut dist = self.clone();
for k in 0..n {
for i in 0..n {
for j in 0..n {
let via_k = dist.get(i, k) + dist.get(k, j);
if via_k < dist.get(i, j) {
dist.set(i, j, via_k);
}
}
}
}
dist
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tropical_matrix_mul() {
// A = [[0, 1], [-∞, 2]]
let a = TropicalMatrix::from_rows(vec![vec![0.0, 1.0], vec![f64::NEG_INFINITY, 2.0]]);
// A² = [[max(0+0, 1-∞), max(0+1, 1+2)], ...]
let a2 = a.mul(&a);
assert!((a2.get(0, 1) - 3.0).abs() < 1e-10); // max(0+1, 1+2) = 3
}
#[test]
fn test_tropical_identity() {
let i = TropicalMatrix::identity(3);
let a = TropicalMatrix::from_rows(vec![
vec![1.0, 2.0, 3.0],
vec![4.0, 5.0, 6.0],
vec![7.0, 8.0, 9.0],
]);
let ia = i.mul(&a);
for row in 0..3 {
for col in 0..3 {
assert!((ia.get(row, col) - a.get(row, col)).abs() < 1e-10);
}
}
}
#[test]
fn test_max_cycle_mean() {
// Simple cycle: 0 -> 1 (weight 3), 1 -> 0 (weight 1)
// Cycle mean = (3 + 1) / 2 = 2
let a = TropicalMatrix::from_rows(vec![
vec![f64::NEG_INFINITY, 3.0],
vec![1.0, f64::NEG_INFINITY],
]);
let mcm = a.max_cycle_mean();
assert!((mcm - 2.0).abs() < 1e-10);
}
#[test]
fn test_floyd_warshall() {
// Graph: 0 -1-> 1 -2-> 2, 0 -5-> 2
let adj = MinPlusMatrix::from_adjacency(vec![
vec![0.0, 1.0, 5.0],
vec![f64::INFINITY, 0.0, 2.0],
vec![f64::INFINITY, f64::INFINITY, 0.0],
]);
let dist = adj.all_pairs_shortest_paths();
// Shortest 0->2 is via 1: 1 + 2 = 3
assert!((dist.get(0, 2) - 3.0).abs() < 1e-10);
}
}