ruvector_math/tropical/
polynomial.rs

1//! Tropical Polynomials
2//!
3//! A tropical polynomial p(x) = ⊕_i (a_i ⊗ x^i) = max_i(a_i + i*x)
4//! represents a piecewise linear function.
5//!
6//! Key property: The number of linear pieces = number of "bends" in the graph.
7
8use super::semiring::Tropical;
9
10/// A monomial in tropical arithmetic: a ⊗ x^k = a + k*x
11#[derive(Debug, Clone, Copy)]
12pub struct TropicalMonomial {
13    /// Coefficient (tropical)
14    pub coeff: f64,
15    /// Exponent
16    pub exp: i32,
17}
18
19impl TropicalMonomial {
20    /// Create new monomial
21    pub fn new(coeff: f64, exp: i32) -> Self {
22        Self { coeff, exp }
23    }
24
25    /// Evaluate at point x: coeff + exp * x
26    #[inline]
27    pub fn eval(&self, x: f64) -> f64 {
28        if self.coeff == f64::NEG_INFINITY {
29            f64::NEG_INFINITY
30        } else {
31            self.coeff + self.exp as f64 * x
32        }
33    }
34
35    /// Multiply monomials (add coefficients, add exponents)
36    pub fn mul(&self, other: &Self) -> Self {
37        Self {
38            coeff: self.coeff + other.coeff,
39            exp: self.exp + other.exp,
40        }
41    }
42}
43
44/// Tropical polynomial: max_i(a_i + i*x)
45///
46/// Represents a piecewise linear convex function.
47#[derive(Debug, Clone)]
48pub struct TropicalPolynomial {
49    /// Monomials (sorted by exponent)
50    terms: Vec<TropicalMonomial>,
51}
52
53impl TropicalPolynomial {
54    /// Create polynomial from coefficients (index = exponent)
55    pub fn from_coeffs(coeffs: &[f64]) -> Self {
56        let terms: Vec<TropicalMonomial> = coeffs
57            .iter()
58            .enumerate()
59            .filter(|(_, &c)| c != f64::NEG_INFINITY)
60            .map(|(i, &c)| TropicalMonomial::new(c, i as i32))
61            .collect();
62
63        Self { terms }
64    }
65
66    /// Create from explicit monomials
67    pub fn from_monomials(terms: Vec<TropicalMonomial>) -> Self {
68        let mut sorted = terms;
69        sorted.sort_by_key(|m| m.exp);
70        Self { terms: sorted }
71    }
72
73    /// Number of terms
74    pub fn num_terms(&self) -> usize {
75        self.terms.len()
76    }
77
78    /// Evaluate polynomial at x: max_i(a_i + i*x)
79    pub fn eval(&self, x: f64) -> f64 {
80        self.terms
81            .iter()
82            .map(|m| m.eval(x))
83            .fold(f64::NEG_INFINITY, f64::max)
84    }
85
86    /// Find roots (bend points) of the tropical polynomial
87    /// These are x values where two linear pieces meet
88    pub fn roots(&self) -> Vec<f64> {
89        if self.terms.len() < 2 {
90            return vec![];
91        }
92
93        let mut roots = Vec::new();
94
95        // Find intersections between consecutive dominant pieces
96        for i in 0..self.terms.len() - 1 {
97            for j in i + 1..self.terms.len() {
98                let m1 = &self.terms[i];
99                let m2 = &self.terms[j];
100
101                // Solve: a1 + e1*x = a2 + e2*x
102                // x = (a1 - a2) / (e2 - e1)
103                if m1.exp != m2.exp {
104                    let x = (m1.coeff - m2.coeff) / (m2.exp - m1.exp) as f64;
105
106                    // Check if this is actually a root (both pieces achieve max here)
107                    let val = m1.eval(x);
108                    let max_val = self.eval(x);
109
110                    if (val - max_val).abs() < 1e-10 {
111                        roots.push(x);
112                    }
113                }
114            }
115        }
116
117        roots.sort_by(|a, b| a.partial_cmp(b).unwrap());
118        roots.dedup_by(|a, b| (*a - *b).abs() < 1e-10);
119        roots
120    }
121
122    /// Count linear regions (pieces) of the tropical polynomial
123    /// This equals 1 + number of roots
124    pub fn num_linear_regions(&self) -> usize {
125        1 + self.roots().len()
126    }
127
128    /// Tropical multiplication: (⊕_i a_i x^i) ⊗ (⊕_j b_j x^j) = ⊕_{i,j} (a_i + b_j) x^{i+j}
129    pub fn mul(&self, other: &Self) -> Self {
130        let mut new_terms = Vec::new();
131
132        for m1 in &self.terms {
133            for m2 in &other.terms {
134                new_terms.push(m1.mul(m2));
135            }
136        }
137
138        // Simplify: keep only dominant terms for each exponent
139        new_terms.sort_by_key(|m| m.exp);
140
141        let mut simplified = Vec::new();
142        let mut i = 0;
143        while i < new_terms.len() {
144            let exp = new_terms[i].exp;
145            let mut max_coeff = new_terms[i].coeff;
146
147            while i < new_terms.len() && new_terms[i].exp == exp {
148                max_coeff = max_coeff.max(new_terms[i].coeff);
149                i += 1;
150            }
151
152            simplified.push(TropicalMonomial::new(max_coeff, exp));
153        }
154
155        Self { terms: simplified }
156    }
157
158    /// Tropical addition: max of two polynomials
159    pub fn add(&self, other: &Self) -> Self {
160        let mut combined: Vec<TropicalMonomial> = Vec::new();
161        combined.extend(self.terms.iter().cloned());
162        combined.extend(other.terms.iter().cloned());
163
164        combined.sort_by_key(|m| m.exp);
165
166        // Keep max coefficient for each exponent
167        let mut simplified = Vec::new();
168        let mut i = 0;
169        while i < combined.len() {
170            let exp = combined[i].exp;
171            let mut max_coeff = combined[i].coeff;
172
173            while i < combined.len() && combined[i].exp == exp {
174                max_coeff = max_coeff.max(combined[i].coeff);
175                i += 1;
176            }
177
178            simplified.push(TropicalMonomial::new(max_coeff, exp));
179        }
180
181        Self { terms: simplified }
182    }
183}
184
185/// Multivariate tropical polynomial
186/// Represents piecewise linear functions in multiple variables
187#[derive(Debug, Clone)]
188pub struct MultivariateTropicalPolynomial {
189    /// Number of variables
190    nvars: usize,
191    /// Terms: (coefficient, exponent vector)
192    terms: Vec<(f64, Vec<i32>)>,
193}
194
195impl MultivariateTropicalPolynomial {
196    /// Create from terms
197    pub fn new(nvars: usize, terms: Vec<(f64, Vec<i32>)>) -> Self {
198        Self { nvars, terms }
199    }
200
201    /// Evaluate at point x
202    pub fn eval(&self, x: &[f64]) -> f64 {
203        assert_eq!(x.len(), self.nvars);
204
205        self.terms
206            .iter()
207            .map(|(coeff, exp)| {
208                if *coeff == f64::NEG_INFINITY {
209                    f64::NEG_INFINITY
210                } else {
211                    let linear: f64 = exp.iter().zip(x.iter()).map(|(&e, &xi)| e as f64 * xi).sum();
212                    coeff + linear
213                }
214            })
215            .fold(f64::NEG_INFINITY, f64::max)
216    }
217
218    /// Number of terms
219    pub fn num_terms(&self) -> usize {
220        self.terms.len()
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_tropical_polynomial_eval() {
230        // p(x) = max(2 + 0x, 1 + 1x, -1 + 2x) = max(2, 1+x, -1+2x)
231        let p = TropicalPolynomial::from_coeffs(&[2.0, 1.0, -1.0]);
232
233        assert!((p.eval(0.0) - 2.0).abs() < 1e-10);  // max(2, 1, -1) = 2
234        assert!((p.eval(1.0) - 2.0).abs() < 1e-10);  // max(2, 2, 1) = 2
235        assert!((p.eval(3.0) - 5.0).abs() < 1e-10);  // max(2, 4, 5) = 5
236    }
237
238    #[test]
239    fn test_tropical_roots() {
240        // p(x) = max(0, x) has root at x=0
241        let p = TropicalPolynomial::from_coeffs(&[0.0, 0.0]);
242        let roots = p.roots();
243
244        assert_eq!(roots.len(), 1);
245        assert!(roots[0].abs() < 1e-10);
246    }
247
248    #[test]
249    fn test_tropical_mul() {
250        let p = TropicalPolynomial::from_coeffs(&[1.0, 2.0]); // max(1, 2+x)
251        let q = TropicalPolynomial::from_coeffs(&[0.0, 1.0]); // max(0, 1+x)
252
253        let pq = p.mul(&q);
254
255        // At x=0: p(0)=2, q(0)=1, pq(0) should be max of products
256        // We expect max(1+0, 2+1, 1+1, 2+0) for appropriate exponents
257        assert!(pq.num_terms() > 0);
258    }
259
260    #[test]
261    fn test_multivariate() {
262        // p(x,y) = max(0, x, y)
263        let p = MultivariateTropicalPolynomial::new(2, vec![
264            (0.0, vec![0, 0]),
265            (0.0, vec![1, 0]),
266            (0.0, vec![0, 1]),
267        ]);
268
269        assert!((p.eval(&[1.0, 2.0]) - 2.0).abs() < 1e-10);
270        assert!((p.eval(&[3.0, 1.0]) - 3.0).abs() < 1e-10);
271    }
272}