amari_tropical/
types.rs

1//! Core tropical algebra types
2//!
3//! This module defines the fundamental types for tropical (max-plus) algebra:
4//! - TropicalNumber: Scalar values in the tropical semiring
5//! - TropicalMatrix: Matrices over the tropical semiring
6//! - TropicalMultivector: Geometric algebra multivectors in tropical space
7//!
8//! ## Tropical Semiring
9//!
10//! The tropical semiring replaces standard arithmetic operations with:
11//! - Tropical addition: max(a, b)
12//! - Tropical multiplication: a + b
13//! - Tropical zero: -∞
14//! - Tropical one: 0
15//!
16//! This structure is isomorphic to (ℝ ∪ {-∞}, max, +) and has applications in:
17//! - Optimization and shortest path algorithms
18//! - Machine learning (max-plus neural networks)
19//! - Discrete event systems
20//! - Phylogenetics and computational biology
21
22use core::fmt;
23use num_traits::Float;
24
25#[cfg(feature = "std")]
26use std::ops::{Add, Mul};
27
28#[cfg(not(feature = "std"))]
29use core::ops::{Add, Mul};
30
31use crate::TropicalError;
32
33/// A tropical number in the max-plus semiring
34///
35/// Represents a value in tropical algebra where:
36/// - Addition is max(a, b)
37/// - Multiplication is a + b
38/// - Zero element is -∞
39/// - Unit element is 0
40///
41/// Generic over any floating-point type supporting the `Float` trait.
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub struct TropicalNumber<T: Float> {
44    value: T,
45}
46
47impl<T: Float> TropicalNumber<T> {
48    /// Create a new tropical number
49    ///
50    /// # Example
51    /// ```
52    /// use amari_tropical::TropicalNumber;
53    ///
54    /// let t = TropicalNumber::new(3.5);
55    /// assert_eq!(t.value(), 3.5);
56    /// ```
57    #[inline]
58    pub fn new(value: T) -> Self {
59        Self { value }
60    }
61
62    /// Get the underlying value
63    #[inline]
64    pub fn value(&self) -> T {
65        self.value
66    }
67
68    /// Create tropical zero (-∞)
69    ///
70    /// The additive identity in tropical algebra
71    #[inline]
72    pub fn zero() -> Self {
73        Self {
74            value: T::neg_infinity(),
75        }
76    }
77
78    /// Alias for `zero()` - tropical additive identity
79    ///
80    /// Returns negative infinity, the additive identity in tropical algebra
81    #[inline]
82    pub fn neg_infinity() -> Self {
83        Self::zero()
84    }
85
86    /// Alias for `zero()` - tropical additive identity
87    #[inline]
88    pub fn tropical_zero() -> Self {
89        Self::zero()
90    }
91
92    /// Create tropical one (0)
93    ///
94    /// The multiplicative identity in tropical algebra
95    #[inline]
96    pub fn one() -> Self {
97        Self { value: T::zero() }
98    }
99
100    /// Alias for `one()` - tropical multiplicative identity
101    #[inline]
102    pub fn tropical_one() -> Self {
103        Self::one()
104    }
105
106    /// Check if this is tropical zero (-∞)
107    #[inline]
108    pub fn is_zero(&self) -> bool {
109        self.value == T::neg_infinity()
110    }
111
112    /// Check if this is tropical one (0)
113    #[inline]
114    pub fn is_one(&self) -> bool {
115        self.value == T::zero()
116    }
117
118    /// Check if this value represents infinity (tropical zero)
119    ///
120    /// Alias for `is_zero()` - checks if value is negative infinity
121    #[inline]
122    pub fn is_infinity(&self) -> bool {
123        self.is_zero()
124    }
125
126    /// Tropical addition: max(self, other)
127    #[inline]
128    pub fn tropical_add(&self, other: &Self) -> Self {
129        Self {
130            value: self.value.max(other.value),
131        }
132    }
133
134    /// Tropical multiplication: self + other
135    #[inline]
136    pub fn tropical_mul(&self, other: &Self) -> Self {
137        Self {
138            value: self.value + other.value,
139        }
140    }
141
142    /// Tropical power: self * k (standard multiplication)
143    #[inline]
144    pub fn tropical_pow(&self, k: T) -> Self {
145        Self {
146            value: self.value * k,
147        }
148    }
149}
150
151impl<T: Float + fmt::Display> fmt::Display for TropicalNumber<T> {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        write!(f, "Tropical({})", self.value)
154    }
155}
156
157// Standard arithmetic operators using tropical operations
158impl<T: Float> Add for TropicalNumber<T> {
159    type Output = Self;
160
161    /// Implements tropical addition as max
162    #[inline]
163    fn add(self, other: Self) -> Self {
164        self.tropical_add(&other)
165    }
166}
167
168impl<T: Float> Mul for TropicalNumber<T> {
169    type Output = Self;
170
171    /// Implements tropical multiplication as addition
172    #[inline]
173    fn mul(self, other: Self) -> Self {
174        self.tropical_mul(&other)
175    }
176}
177
178/// A matrix in tropical algebra
179///
180/// Stores elements as a 2D vector with dimensions (rows × cols).
181/// Matrix operations follow tropical semiring rules:
182/// - Matrix addition: element-wise max
183/// - Matrix multiplication: tropical matrix product
184#[derive(Debug, Clone, PartialEq)]
185pub struct TropicalMatrix<T: Float> {
186    pub rows: usize,
187    pub cols: usize,
188    pub data: Vec<Vec<TropicalNumber<T>>>,
189}
190
191impl<T: Float> TropicalMatrix<T> {
192    /// Create a new tropical matrix with given dimensions
193    ///
194    /// Initializes all elements to tropical zero (-∞)
195    pub fn new(rows: usize, cols: usize) -> Self {
196        let data = vec![vec![TropicalNumber::zero(); cols]; rows];
197        Self { rows, cols, data }
198    }
199
200    /// Create a tropical matrix from raw 2D data
201    ///
202    /// Data must be a Vec of rows, where each row is a Vec of values
203    pub fn from_vec(data: Vec<Vec<T>>) -> Result<Self, TropicalError> {
204        if data.is_empty() {
205            return Ok(Self::new(0, 0));
206        }
207
208        let rows = data.len();
209        let cols = data[0].len();
210
211        // Verify all rows have the same length
212        for (i, row) in data.iter().enumerate() {
213            if row.len() != cols {
214                return Err(TropicalError::DimensionMismatch(format!(
215                    "Row {} has {} elements, expected {}",
216                    i,
217                    row.len(),
218                    cols
219                )));
220            }
221        }
222
223        let tropical_data: Vec<Vec<TropicalNumber<T>>> = data
224            .into_iter()
225            .map(|row| row.into_iter().map(TropicalNumber::new).collect())
226            .collect();
227
228        Ok(Self {
229            rows,
230            cols,
231            data: tropical_data,
232        })
233    }
234
235    /// Get matrix dimensions (rows, cols)
236    #[inline]
237    pub fn dims(&self) -> (usize, usize) {
238        (self.rows, self.cols)
239    }
240
241    /// Get number of rows
242    #[inline]
243    pub fn rows(&self) -> usize {
244        self.rows
245    }
246
247    /// Get number of columns
248    #[inline]
249    pub fn cols(&self) -> usize {
250        self.cols
251    }
252
253    /// Get element at (row, col)
254    pub fn get(&self, row: usize, col: usize) -> Result<TropicalNumber<T>, TropicalError> {
255        if row >= self.rows || col >= self.cols {
256            return Err(TropicalError::IndexOutOfBounds(format!(
257                "Index ({}, {}) out of bounds for {}×{} matrix",
258                row, col, self.rows, self.cols
259            )));
260        }
261        Ok(self.data[row][col])
262    }
263
264    /// Set element at (row, col)
265    pub fn set(
266        &mut self,
267        row: usize,
268        col: usize,
269        value: TropicalNumber<T>,
270    ) -> Result<(), TropicalError> {
271        if row >= self.rows || col >= self.cols {
272            return Err(TropicalError::IndexOutOfBounds(format!(
273                "Index ({}, {}) out of bounds for {}×{} matrix",
274                row, col, self.rows, self.cols
275            )));
276        }
277        self.data[row][col] = value;
278        Ok(())
279    }
280
281    /// Create tropical identity matrix
282    ///
283    /// Diagonal elements are tropical one (0), off-diagonal are tropical zero (-∞)
284    pub fn identity(size: usize) -> Self {
285        let mut matrix = Self::new(size, size);
286        for i in 0..size {
287            matrix.data[i][i] = TropicalNumber::one();
288        }
289        matrix
290    }
291
292    /// Create tropical matrix from log probabilities
293    ///
294    /// In tropical algebra, log probabilities are natural since:
295    /// - log(p1 * p2) = log(p1) + log(p2) (tropical multiplication)
296    /// - log(max(p1, p2)) ≈ max(log(p1), log(p2)) (tropical addition)
297    ///
298    /// This method converts a 2D array of log probabilities into a tropical matrix.
299    pub fn from_log_probs(log_probs: &[Vec<T>]) -> Self {
300        if log_probs.is_empty() {
301            return Self::new(0, 0);
302        }
303
304        let rows = log_probs.len();
305        let cols = log_probs.first().map(|r| r.len()).unwrap_or(0);
306
307        let data: Vec<Vec<TropicalNumber<T>>> = log_probs
308            .iter()
309            .map(|row| row.iter().map(|&val| TropicalNumber::new(val)).collect())
310            .collect();
311
312        Self { rows, cols, data }
313    }
314
315    /// Tropical matrix multiplication
316    ///
317    /// (A ⊗ B)[i,j] = max_k(A[i,k] + B[k,j])
318    pub fn tropical_matmul(&self, other: &Self) -> Result<Self, TropicalError> {
319        if self.cols != other.rows {
320            return Err(TropicalError::DimensionMismatch(format!(
321                "Cannot multiply {}×{} matrix with {}×{} matrix",
322                self.rows, self.cols, other.rows, other.cols
323            )));
324        }
325
326        let mut result = Self::new(self.rows, other.cols);
327
328        for i in 0..self.rows {
329            for j in 0..other.cols {
330                let mut sum = TropicalNumber::zero();
331                for k in 0..self.cols {
332                    let a = self.data[i][k];
333                    let b = other.data[k][j];
334                    sum = sum.tropical_add(&a.tropical_mul(&b));
335                }
336                result.data[i][j] = sum;
337            }
338        }
339
340        Ok(result)
341    }
342
343    /// Get the underlying data as a 2D vector
344    pub fn data(&self) -> &Vec<Vec<TropicalNumber<T>>> {
345        &self.data
346    }
347
348    /// Get mutable access to the underlying data
349    pub fn data_mut(&mut self) -> &mut Vec<Vec<TropicalNumber<T>>> {
350        &mut self.data
351    }
352}
353
354impl<T: Float + fmt::Display> fmt::Display for TropicalMatrix<T> {
355    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356        writeln!(f, "TropicalMatrix [{}×{}]:", self.rows, self.cols)?;
357        for row in &self.data {
358            write!(f, "  [")?;
359            for (j, val) in row.iter().enumerate() {
360                if j > 0 {
361                    write!(f, ", ")?;
362                }
363                write!(f, "{}", val.value)?;
364            }
365            writeln!(f, "]")?;
366        }
367        Ok(())
368    }
369}
370
371/// A geometric algebra multivector in tropical space
372///
373/// Combines geometric algebra with tropical semiring operations.
374/// Generic over:
375/// - T: Scalar type (must implement Float)
376/// - P, Q, R: Metric signature (p positive, q negative, r null dimensions)
377#[derive(Debug, Clone, PartialEq)]
378pub struct TropicalMultivector<T: Float, const P: usize, const Q: usize, const R: usize> {
379    /// Multivector components in tropical algebra
380    ///
381    /// Total dimension is 2^(P+Q+R) for the full geometric algebra
382    components: Vec<TropicalNumber<T>>,
383}
384
385impl<T: Float, const P: usize, const Q: usize, const R: usize> TropicalMultivector<T, P, Q, R> {
386    /// Create a new tropical multivector
387    ///
388    /// Initializes all components to tropical zero (-∞)
389    pub fn new() -> Self {
390        let dim = 1 << (P + Q + R); // 2^(P+Q+R)
391        Self {
392            components: vec![TropicalNumber::zero(); dim],
393        }
394    }
395
396    /// Create from component values
397    pub fn from_components(components: Vec<T>) -> Result<Self, TropicalError> {
398        let dim = 1 << (P + Q + R);
399        if components.len() != dim {
400            return Err(TropicalError::DimensionMismatch(format!(
401                "Expected {} components for Cl({},{},{}), got {}",
402                dim,
403                P,
404                Q,
405                R,
406                components.len()
407            )));
408        }
409
410        Ok(Self {
411            components: components.into_iter().map(TropicalNumber::new).collect(),
412        })
413    }
414
415    /// Get total number of components
416    #[inline]
417    pub fn dim(&self) -> usize {
418        self.components.len()
419    }
420
421    /// Get component at index
422    pub fn get(&self, index: usize) -> Result<TropicalNumber<T>, TropicalError> {
423        self.components.get(index).copied().ok_or_else(|| {
424            TropicalError::IndexOutOfBounds(format!(
425                "Index {} out of bounds for multivector with {} components",
426                index,
427                self.components.len()
428            ))
429        })
430    }
431
432    /// Set component at index
433    pub fn set(&mut self, index: usize, value: TropicalNumber<T>) -> Result<(), TropicalError> {
434        if index >= self.components.len() {
435            return Err(TropicalError::IndexOutOfBounds(format!(
436                "Index {} out of bounds for multivector with {} components",
437                index,
438                self.components.len()
439            )));
440        }
441        self.components[index] = value;
442        Ok(())
443    }
444
445    /// Get slice of all components
446    pub fn components(&self) -> &[TropicalNumber<T>] {
447        &self.components
448    }
449
450    /// Tropical multivector addition (grade-wise max)
451    pub fn tropical_add(&self, other: &Self) -> Self {
452        let components: Vec<_> = self
453            .components
454            .iter()
455            .zip(&other.components)
456            .map(|(a, b)| a.tropical_add(b))
457            .collect();
458
459        Self { components }
460    }
461
462    /// Geometric product in tropical algebra
463    ///
464    /// This is a simplified implementation for tropical geometric algebra.
465    /// In tropical algebra, the geometric product combines grade-wise operations.
466    pub fn geometric_product(&self, other: &Self) -> Self {
467        // Simplified tropical geometric product
468        // For now, use component-wise tropical multiplication as approximation
469        let components: Vec<_> = self
470            .components
471            .iter()
472            .zip(&other.components)
473            .map(|(a, b)| a.tropical_mul(b))
474            .collect();
475
476        Self { components }
477    }
478
479    /// Compute tropical norm
480    ///
481    /// In tropical algebra, the norm is the maximum absolute value
482    /// of all components (tropical addition of all components)
483    pub fn tropical_norm(&self) -> TropicalNumber<T> {
484        let mut max_val = TropicalNumber::zero();
485        for &comp in &self.components {
486            max_val = max_val.tropical_add(&comp);
487        }
488        max_val
489    }
490
491    /// Get scalar part (grade-0 component)
492    #[inline]
493    pub fn scalar(&self) -> TropicalNumber<T> {
494        self.components[0]
495    }
496}
497
498impl<T: Float, const P: usize, const Q: usize, const R: usize> Default
499    for TropicalMultivector<T, P, Q, R>
500{
501    fn default() -> Self {
502        Self::new()
503    }
504}
505
506impl<T: Float + fmt::Display, const P: usize, const Q: usize, const R: usize> fmt::Display
507    for TropicalMultivector<T, P, Q, R>
508{
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510        write!(f, "TropicalMultivector<{},{},{}>([", P, Q, R)?;
511        for (i, comp) in self.components.iter().enumerate() {
512            if i > 0 {
513                write!(f, ", ")?;
514            }
515            write!(f, "{}", comp.value)?;
516        }
517        write!(f, "])")
518    }
519}
520
521// Precision type aliases for tropical numbers
522/// Standard-precision tropical number (f64)
523pub type StandardTropical = TropicalNumber<f64>;
524
525/// Extended-precision tropical number (uses extended precision float from amari-core)
526#[cfg(feature = "high-precision")]
527pub type ExtendedTropical = TropicalNumber<crate::ExtendedFloat>;
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532
533    #[test]
534    fn test_tropical_number_basics() {
535        let a = TropicalNumber::new(3.0f64);
536        let b = TropicalNumber::new(5.0f64);
537
538        // Tropical addition is max
539        let sum = a.tropical_add(&b);
540        assert_eq!(sum.value(), 5.0);
541
542        // Tropical multiplication is addition
543        let product = a.tropical_mul(&b);
544        assert_eq!(product.value(), 8.0);
545    }
546
547    #[test]
548    fn test_tropical_identities() {
549        let zero = TropicalNumber::<f64>::zero();
550        let one = TropicalNumber::<f64>::one();
551        let x = TropicalNumber::new(3.0);
552
553        // Tropical zero is -∞, is additive identity
554        assert!(zero.is_zero());
555        assert_eq!(x.tropical_add(&zero), x);
556
557        // Tropical one is 0, is multiplicative identity
558        assert!(one.is_one());
559        assert_eq!(x.tropical_mul(&one), x);
560    }
561
562    #[test]
563    fn test_tropical_matrix_creation() {
564        let mat = TropicalMatrix::<f64>::new(2, 3);
565        assert_eq!(mat.dims(), (2, 3));
566        assert_eq!(mat.rows(), 2);
567        assert_eq!(mat.cols(), 3);
568    }
569
570    #[test]
571    fn test_tropical_matrix_identity() {
572        let id = TropicalMatrix::<f64>::identity(3);
573        assert_eq!(id.dims(), (3, 3));
574
575        // Diagonal should be tropical one (0)
576        for i in 0..3 {
577            assert!(id.get(i, i).unwrap().is_one());
578        }
579
580        // Off-diagonal should be tropical zero (-∞)
581        assert!(id.get(0, 1).unwrap().is_zero());
582        assert!(id.get(1, 2).unwrap().is_zero());
583    }
584
585    #[test]
586    fn test_tropical_matrix_multiplication() {
587        let mut a = TropicalMatrix::<f64>::new(2, 2);
588        a.set(0, 0, TropicalNumber::new(1.0)).unwrap();
589        a.set(0, 1, TropicalNumber::new(2.0)).unwrap();
590        a.set(1, 0, TropicalNumber::new(3.0)).unwrap();
591        a.set(1, 1, TropicalNumber::new(4.0)).unwrap();
592
593        let id = TropicalMatrix::<f64>::identity(2);
594
595        // Multiplying by identity should preserve matrix
596        let result = a.tropical_matmul(&id).unwrap();
597        assert_eq!(result.get(0, 0).unwrap().value(), 1.0);
598        assert_eq!(result.get(0, 1).unwrap().value(), 2.0);
599        assert_eq!(result.get(1, 0).unwrap().value(), 3.0);
600        assert_eq!(result.get(1, 1).unwrap().value(), 4.0);
601    }
602
603    #[test]
604    fn test_tropical_multivector_creation() {
605        let mv = TropicalMultivector::<f64, 3, 0, 0>::new();
606        assert_eq!(mv.dim(), 8); // 2^3 = 8 components for Cl(3,0,0)
607    }
608
609    #[test]
610    fn test_tropical_multivector_addition() {
611        let mut a = TropicalMultivector::<f64, 2, 0, 0>::new();
612        let mut b = TropicalMultivector::<f64, 2, 0, 0>::new();
613
614        a.set(0, TropicalNumber::new(1.0)).unwrap();
615        a.set(1, TropicalNumber::new(2.0)).unwrap();
616
617        b.set(0, TropicalNumber::new(3.0)).unwrap();
618        b.set(1, TropicalNumber::new(1.5)).unwrap();
619
620        let sum = a.tropical_add(&b);
621
622        // Tropical addition is max
623        assert_eq!(sum.get(0).unwrap().value(), 3.0);
624        assert_eq!(sum.get(1).unwrap().value(), 2.0);
625    }
626}