Skip to main content

ruvector_math/optimization/
polynomial.rs

1//! Multivariate Polynomials
2//!
3//! Representation and operations for multivariate polynomials.
4
5use std::collections::HashMap;
6
7/// A monomial: product of variables with powers
8/// Represented as sorted list of (variable_index, power)
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct Monomial {
11    /// (variable_index, power) pairs, sorted by variable index
12    pub powers: Vec<(usize, usize)>,
13}
14
15impl Monomial {
16    /// Create constant monomial (1)
17    pub fn one() -> Self {
18        Self { powers: vec![] }
19    }
20
21    /// Create single variable monomial x_i
22    pub fn var(i: usize) -> Self {
23        Self {
24            powers: vec![(i, 1)],
25        }
26    }
27
28    /// Create from powers (will be sorted)
29    pub fn new(mut powers: Vec<(usize, usize)>) -> Self {
30        // Sort and merge
31        powers.sort_by_key(|&(i, _)| i);
32
33        // Merge duplicate variables
34        let mut merged = Vec::new();
35        for (i, p) in powers {
36            if p == 0 {
37                continue;
38            }
39            if let Some(&mut (last_i, ref mut last_p)) = merged.last_mut() {
40                if last_i == i {
41                    *last_p += p;
42                    continue;
43                }
44            }
45            merged.push((i, p));
46        }
47
48        Self { powers: merged }
49    }
50
51    /// Total degree
52    pub fn degree(&self) -> usize {
53        self.powers.iter().map(|&(_, p)| p).sum()
54    }
55
56    /// Is this the constant monomial?
57    pub fn is_constant(&self) -> bool {
58        self.powers.is_empty()
59    }
60
61    /// Maximum variable index (or None if constant)
62    pub fn max_var(&self) -> Option<usize> {
63        self.powers.last().map(|&(i, _)| i)
64    }
65
66    /// Multiply two monomials
67    pub fn mul(&self, other: &Monomial) -> Monomial {
68        let mut combined = self.powers.clone();
69        combined.extend(other.powers.iter().copied());
70        Monomial::new(combined)
71    }
72
73    /// Evaluate at point
74    pub fn eval(&self, x: &[f64]) -> f64 {
75        let mut result = 1.0;
76        for &(i, p) in &self.powers {
77            if i < x.len() {
78                result *= x[i].powi(p as i32);
79            }
80        }
81        result
82    }
83
84    /// Check divisibility: does self divide other?
85    pub fn divides(&self, other: &Monomial) -> bool {
86        let mut j = 0;
87        for &(i, p) in &self.powers {
88            // Find matching variable in other
89            while j < other.powers.len() && other.powers[j].0 < i {
90                j += 1;
91            }
92            if j >= other.powers.len() || other.powers[j].0 != i || other.powers[j].1 < p {
93                return false;
94            }
95            j += 1;
96        }
97        true
98    }
99}
100
101impl std::fmt::Display for Monomial {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        if self.powers.is_empty() {
104            write!(f, "1")
105        } else {
106            let parts: Vec<String> = self
107                .powers
108                .iter()
109                .map(|&(i, p)| {
110                    if p == 1 {
111                        format!("x{}", i)
112                    } else {
113                        format!("x{}^{}", i, p)
114                    }
115                })
116                .collect();
117            write!(f, "{}", parts.join("*"))
118        }
119    }
120}
121
122/// A term: coefficient times monomial
123#[derive(Debug, Clone)]
124pub struct Term {
125    /// Coefficient
126    pub coeff: f64,
127    /// Monomial
128    pub monomial: Monomial,
129}
130
131impl Term {
132    /// Create term from coefficient and powers
133    pub fn new(coeff: f64, powers: Vec<(usize, usize)>) -> Self {
134        Self {
135            coeff,
136            monomial: Monomial::new(powers),
137        }
138    }
139
140    /// Create constant term
141    pub fn constant(c: f64) -> Self {
142        Self {
143            coeff: c,
144            monomial: Monomial::one(),
145        }
146    }
147
148    /// Degree
149    pub fn degree(&self) -> usize {
150        self.monomial.degree()
151    }
152}
153
154/// Multivariate polynomial
155#[derive(Debug, Clone)]
156pub struct Polynomial {
157    /// Terms indexed by monomial
158    terms: HashMap<Monomial, f64>,
159    /// Cached degree
160    degree: usize,
161    /// Number of variables
162    num_vars: usize,
163}
164
165impl Polynomial {
166    /// Create zero polynomial
167    pub fn zero() -> Self {
168        Self {
169            terms: HashMap::new(),
170            degree: 0,
171            num_vars: 0,
172        }
173    }
174
175    /// Create constant polynomial
176    pub fn constant(c: f64) -> Self {
177        if c == 0.0 {
178            return Self::zero();
179        }
180        let mut terms = HashMap::new();
181        terms.insert(Monomial::one(), c);
182        Self {
183            terms,
184            degree: 0,
185            num_vars: 0,
186        }
187    }
188
189    /// Create single variable polynomial x_i
190    pub fn var(i: usize) -> Self {
191        let mut terms = HashMap::new();
192        terms.insert(Monomial::var(i), 1.0);
193        Self {
194            terms,
195            degree: 1,
196            num_vars: i + 1,
197        }
198    }
199
200    /// Create from terms
201    pub fn from_terms(term_list: Vec<Term>) -> Self {
202        let mut terms = HashMap::new();
203        let mut degree = 0;
204        let mut num_vars = 0;
205
206        for term in term_list {
207            if term.coeff.abs() < 1e-15 {
208                continue;
209            }
210
211            degree = degree.max(term.degree());
212            if let Some(max_v) = term.monomial.max_var() {
213                num_vars = num_vars.max(max_v + 1);
214            }
215
216            *terms.entry(term.monomial).or_insert(0.0) += term.coeff;
217        }
218
219        // Remove zero terms
220        terms.retain(|_, &mut c| c.abs() >= 1e-15);
221
222        Self {
223            terms,
224            degree,
225            num_vars,
226        }
227    }
228
229    /// Total degree
230    pub fn degree(&self) -> usize {
231        self.degree
232    }
233
234    /// Number of variables (max variable index + 1)
235    pub fn num_variables(&self) -> usize {
236        self.num_vars
237    }
238
239    /// Number of terms
240    pub fn num_terms(&self) -> usize {
241        self.terms.len()
242    }
243
244    /// Is zero polynomial?
245    pub fn is_zero(&self) -> bool {
246        self.terms.is_empty()
247    }
248
249    /// Get coefficient of monomial
250    pub fn coeff(&self, m: &Monomial) -> f64 {
251        *self.terms.get(m).unwrap_or(&0.0)
252    }
253
254    /// Get all terms
255    pub fn terms(&self) -> impl Iterator<Item = (&Monomial, &f64)> {
256        self.terms.iter()
257    }
258
259    /// Evaluate at point
260    pub fn eval(&self, x: &[f64]) -> f64 {
261        self.terms.iter().map(|(m, &c)| c * m.eval(x)).sum()
262    }
263
264    /// Add two polynomials
265    pub fn add(&self, other: &Polynomial) -> Polynomial {
266        let mut terms = self.terms.clone();
267
268        for (m, &c) in &other.terms {
269            *terms.entry(m.clone()).or_insert(0.0) += c;
270        }
271
272        terms.retain(|_, &mut c| c.abs() >= 1e-15);
273
274        let degree = terms.keys().map(|m| m.degree()).max().unwrap_or(0);
275        let num_vars = terms
276            .keys()
277            .filter_map(|m| m.max_var())
278            .max()
279            .map(|v| v + 1)
280            .unwrap_or(0);
281
282        Polynomial {
283            terms,
284            degree,
285            num_vars,
286        }
287    }
288
289    /// Subtract polynomials
290    pub fn sub(&self, other: &Polynomial) -> Polynomial {
291        self.add(&other.neg())
292    }
293
294    /// Negate polynomial
295    pub fn neg(&self) -> Polynomial {
296        Polynomial {
297            terms: self.terms.iter().map(|(m, &c)| (m.clone(), -c)).collect(),
298            degree: self.degree,
299            num_vars: self.num_vars,
300        }
301    }
302
303    /// Multiply by scalar
304    pub fn scale(&self, s: f64) -> Polynomial {
305        if s.abs() < 1e-15 {
306            return Polynomial::zero();
307        }
308
309        Polynomial {
310            terms: self
311                .terms
312                .iter()
313                .map(|(m, &c)| (m.clone(), s * c))
314                .collect(),
315            degree: self.degree,
316            num_vars: self.num_vars,
317        }
318    }
319
320    /// Multiply two polynomials
321    pub fn mul(&self, other: &Polynomial) -> Polynomial {
322        let mut terms = HashMap::new();
323
324        for (m1, &c1) in &self.terms {
325            for (m2, &c2) in &other.terms {
326                let m = m1.mul(m2);
327                *terms.entry(m).or_insert(0.0) += c1 * c2;
328            }
329        }
330
331        terms.retain(|_, &mut c| c.abs() >= 1e-15);
332
333        let degree = terms.keys().map(|m| m.degree()).max().unwrap_or(0);
334        let num_vars = terms
335            .keys()
336            .filter_map(|m| m.max_var())
337            .max()
338            .map(|v| v + 1)
339            .unwrap_or(0);
340
341        Polynomial {
342            terms,
343            degree,
344            num_vars,
345        }
346    }
347
348    /// Square polynomial
349    pub fn square(&self) -> Polynomial {
350        self.mul(self)
351    }
352
353    /// Power
354    pub fn pow(&self, n: usize) -> Polynomial {
355        if n == 0 {
356            return Polynomial::constant(1.0);
357        }
358        if n == 1 {
359            return self.clone();
360        }
361
362        let mut result = self.clone();
363        for _ in 1..n {
364            result = result.mul(self);
365        }
366        result
367    }
368
369    /// Generate all monomials up to given degree
370    pub fn monomials_up_to_degree(num_vars: usize, max_degree: usize) -> Vec<Monomial> {
371        let mut result = vec![Monomial::one()];
372
373        if max_degree == 0 || num_vars == 0 {
374            return result;
375        }
376
377        // Generate systematically using recursion
378        fn generate(
379            var: usize,
380            num_vars: usize,
381            remaining_degree: usize,
382            current: Vec<(usize, usize)>,
383            result: &mut Vec<Monomial>,
384        ) {
385            if var >= num_vars {
386                result.push(Monomial::new(current));
387                return;
388            }
389
390            for p in 0..=remaining_degree {
391                let mut next = current.clone();
392                if p > 0 {
393                    next.push((var, p));
394                }
395                generate(var + 1, num_vars, remaining_degree - p, next, result);
396            }
397        }
398
399        for d in 1..=max_degree {
400            generate(0, num_vars, d, vec![], &mut result);
401        }
402
403        // Deduplicate
404        result.sort_by(|a, b| {
405            a.degree()
406                .cmp(&b.degree())
407                .then_with(|| a.powers.cmp(&b.powers))
408        });
409        result.dedup();
410
411        // Ensure only one constant monomial
412        let const_count = result.iter().filter(|m| m.is_constant()).count();
413        if const_count > 1 {
414            let mut seen_const = false;
415            result.retain(|m| {
416                if m.is_constant() {
417                    if seen_const {
418                        return false;
419                    }
420                    seen_const = true;
421                }
422                true
423            });
424        }
425
426        result
427    }
428}
429
430impl std::fmt::Display for Polynomial {
431    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432        if self.terms.is_empty() {
433            return write!(f, "0");
434        }
435
436        let mut sorted: Vec<_> = self.terms.iter().collect();
437        sorted.sort_by(|a, b| {
438            a.0.degree()
439                .cmp(&b.0.degree())
440                .then_with(|| a.0.powers.cmp(&b.0.powers))
441        });
442
443        let parts: Vec<String> = sorted
444            .iter()
445            .map(|(m, &c)| {
446                if m.is_constant() {
447                    format!("{:.4}", c)
448                } else if (c - 1.0).abs() < 1e-10 {
449                    format!("{}", m)
450                } else if (c + 1.0).abs() < 1e-10 {
451                    format!("-{}", m)
452                } else {
453                    format!("{:.4}*{}", c, m)
454                }
455            })
456            .collect();
457
458        write!(f, "{}", parts.join(" + "))
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_monomial() {
468        let m1 = Monomial::var(0);
469        let m2 = Monomial::var(1);
470        let m3 = m1.mul(&m2);
471
472        assert_eq!(m3.degree(), 2);
473        assert_eq!(m3.powers, vec![(0, 1), (1, 1)]);
474    }
475
476    #[test]
477    fn test_polynomial_eval() {
478        // p = x² + 2xy + y²
479        let p = Polynomial::from_terms(vec![
480            Term::new(1.0, vec![(0, 2)]),
481            Term::new(2.0, vec![(0, 1), (1, 1)]),
482            Term::new(1.0, vec![(1, 2)]),
483        ]);
484
485        // At (1, 1): 1 + 2 + 1 = 4
486        assert!((p.eval(&[1.0, 1.0]) - 4.0).abs() < 1e-10);
487
488        // At (2, 3): 4 + 12 + 9 = 25 = (2+3)²
489        assert!((p.eval(&[2.0, 3.0]) - 25.0).abs() < 1e-10);
490    }
491
492    #[test]
493    fn test_polynomial_mul() {
494        // (x + y)²  = x² + 2xy + y²
495        let x = Polynomial::var(0);
496        let y = Polynomial::var(1);
497        let sum = x.add(&y);
498        let squared = sum.square();
499
500        assert!((squared.coeff(&Monomial::new(vec![(0, 2)])) - 1.0).abs() < 1e-10);
501        assert!((squared.coeff(&Monomial::new(vec![(0, 1), (1, 1)])) - 2.0).abs() < 1e-10);
502        assert!((squared.coeff(&Monomial::new(vec![(1, 2)])) - 1.0).abs() < 1e-10);
503    }
504
505    #[test]
506    fn test_monomials_generation() {
507        let monoms = Polynomial::monomials_up_to_degree(2, 2);
508
509        // Should have: 1, x0, x1, x0², x0*x1, x1²
510        assert!(monoms.len() >= 6);
511    }
512}