mathhook_core/matrices/eigenvalues/
characteristic.rs

1use crate::core::expression::Expression;
2use crate::core::symbol::Symbol;
3use crate::simplify::Simplify;
4
5/// Characteristic polynomial of a matrix
6///
7/// Represents the polynomial det(A - λI) where A is a matrix and λ is a variable.
8/// The roots of this polynomial are the eigenvalues of the matrix.
9///
10/// # Mathematical Definition
11///
12/// For an n×n matrix A, the characteristic polynomial is:
13/// $$p(\lambda) = \det(A - \lambda I)$$
14///
15/// This expands to a polynomial of degree n:
16/// $$p(\lambda) = c_0 + c_1\lambda + c_2\lambda^2 + \cdots + c_n\lambda^n$$
17///
18/// # Properties
19///
20/// - Degree equals matrix dimension
21/// - Coefficients are polynomial expressions in matrix entries
22/// - Roots (eigenvalues) may be real or complex
23/// - Leading coefficient is (-1)^n
24/// - Constant term is det(A)
25///
26/// # Examples
27///
28/// ```rust
29/// use mathhook_core::{expr, symbol, Expression};
30/// use mathhook_core::matrices::eigenvalues::characteristic::CharacteristicPolynomial;
31///
32/// let lambda = symbol!(lambda);
33///
34/// // 2×2 matrix characteristic polynomial: λ² - trace·λ + det
35/// let poly = CharacteristicPolynomial::new(
36///     vec![
37///         Expression::add(vec![
38///            expr!(a * d),
39///          Expression::mul(vec![expr!(-1), expr!(b * c)]),
40///         ]), // det(A)
41///         Expression::mul(vec![expr!(-1), expr!(a + d)]),            // -trace(A)
42///         expr!(1),                   // leading coefficient
43///     ],
44///     lambda.clone()
45/// );
46/// ```
47#[derive(Debug, Clone)]
48pub struct CharacteristicPolynomial {
49    /// Coefficients of the polynomial [c₀, c₁, c₂, ..., cₙ]
50    /// where p(λ) = c₀ + c₁λ + c₂λ² + ... + cₙλⁿ
51    pub coefficients: Vec<Expression>,
52    /// Variable symbol (typically λ or lambda)
53    pub variable: Symbol,
54}
55
56impl CharacteristicPolynomial {
57    /// Creates new characteristic polynomial
58    ///
59    /// # Arguments
60    ///
61    /// * `coefficients` - Polynomial coefficients [c₀, c₁, ..., cₙ]
62    /// * `variable` - Variable symbol (typically λ)
63    ///
64    /// # Examples
65    ///
66    /// ```rust
67    /// use mathhook_core::{expr, symbol};
68    /// use mathhook_core::matrices::eigenvalues::characteristic::CharacteristicPolynomial;
69    ///
70    /// let lambda = symbol!(lambda);
71    /// let poly = CharacteristicPolynomial::new(
72    ///     vec![expr!(6), expr!(-5), expr!(1)],  // λ² - 5λ + 6
73    ///     lambda
74    /// );
75    /// ```
76    pub fn new(coefficients: Vec<Expression>, variable: Symbol) -> Self {
77        Self {
78            coefficients,
79            variable,
80        }
81    }
82
83    /// Returns the degree of the polynomial
84    ///
85    /// # Examples
86    ///
87    /// ```rust
88    /// use mathhook_core::{expr, symbol};
89    /// use mathhook_core::matrices::eigenvalues::characteristic::CharacteristicPolynomial;
90    ///
91    /// let lambda = symbol!(lambda);
92    /// let poly = CharacteristicPolynomial::new(
93    ///     vec![expr!(1), expr!(2), expr!(3)],
94    ///     lambda
95    /// );
96    /// assert_eq!(poly.degree(), 2);
97    /// ```
98    pub fn degree(&self) -> usize {
99        if self.coefficients.is_empty() {
100            0
101        } else {
102            self.coefficients.len() - 1
103        }
104    }
105
106    /// Converts polynomial to expression form
107    ///
108    /// Returns: c₀ + c₁λ + c₂λ² + ... + cₙλⁿ
109    ///
110    /// # Examples
111    ///
112    /// ```rust
113    /// use mathhook_core::{expr, symbol};
114    /// use mathhook_core::matrices::eigenvalues::characteristic::CharacteristicPolynomial;
115    ///
116    /// let lambda = symbol!(lambda);
117    /// let poly = CharacteristicPolynomial::new(
118    ///     vec![expr!(6), expr!(-5), expr!(1)],
119    ///     lambda.clone()
120    /// );
121    ///
122    /// let expr = poly.to_expression();
123    /// // Represents: 6 - 5λ + λ²
124    /// ```
125    pub fn to_expression(&self) -> Expression {
126        if self.coefficients.is_empty() {
127            return Expression::integer(0);
128        }
129
130        let mut terms = Vec::new();
131
132        for (power, coeff) in self.coefficients.iter().enumerate() {
133            if coeff.is_zero() {
134                continue;
135            }
136
137            let term = if power == 0 {
138                coeff.clone()
139            } else if power == 1 {
140                Expression::mul(vec![
141                    coeff.clone(),
142                    Expression::symbol(self.variable.clone()),
143                ])
144            } else {
145                Expression::mul(vec![
146                    coeff.clone(),
147                    Expression::pow(
148                        Expression::symbol(self.variable.clone()),
149                        Expression::integer(power as i64),
150                    ),
151                ])
152            };
153
154            terms.push(term);
155        }
156
157        if terms.is_empty() {
158            Expression::integer(0)
159        } else if terms.len() == 1 {
160            terms[0].clone()
161        } else {
162            Expression::add(terms).simplify()
163        }
164    }
165
166    /// Evaluates polynomial at given value
167    ///
168    /// Uses Horner's method for efficient evaluation.
169    ///
170    /// # Arguments
171    ///
172    /// * `value` - Value to substitute for variable
173    ///
174    /// # Examples
175    ///
176    /// ```rust
177    /// use mathhook_core::{expr, symbol};
178    /// use mathhook_core::matrices::eigenvalues::characteristic::CharacteristicPolynomial;
179    ///
180    /// let lambda = symbol!(lambda);
181    /// let poly = CharacteristicPolynomial::new(
182    ///     vec![expr!(6), expr!(-5), expr!(1)],  // λ² - 5λ + 6
183    ///     lambda
184    /// );
185    ///
186    /// let result = poly.evaluate(&expr!(2));  // 2² - 5(2) + 6 = 0
187    /// assert_eq!(result, expr!(0));
188    /// ```
189    pub fn evaluate(&self, value: &Expression) -> Expression {
190        if self.coefficients.is_empty() {
191            return Expression::integer(0);
192        }
193
194        // Horner's method: p(x) = c₀ + x(c₁ + x(c₂ + ... + x·cₙ))
195        let mut result = self.coefficients.last().unwrap().clone();
196
197        for coeff in self.coefficients.iter().rev().skip(1) {
198            result = Expression::add(vec![
199                coeff.clone(),
200                Expression::mul(vec![value.clone(), result]),
201            ])
202            .simplify();
203        }
204
205        result
206    }
207
208    /// Adds two characteristic polynomials
209    ///
210    /// Note: This is polynomial addition, not matrix addition.
211    /// Both polynomials must use the same variable.
212    ///
213    /// # Arguments
214    ///
215    /// * `poly1` - First polynomial
216    /// * `poly2` - Second polynomial
217    ///
218    /// # Returns
219    ///
220    /// Sum of the two polynomials
221    ///
222    /// # Examples
223    ///
224    /// ```rust
225    /// use mathhook_core::{expr, symbol};
226    /// use mathhook_core::matrices::eigenvalues::characteristic::{CharacteristicPolynomial, CharacteristicPolynomialBuilder};
227    ///
228    /// let lambda = symbol!(lambda);
229    /// let builder = CharacteristicPolynomialBuilder;
230    ///
231    /// let poly1 = CharacteristicPolynomial::new(
232    ///     vec![expr!(1), expr!(2)],  // 1 + 2λ
233    ///     lambda.clone()
234    /// );
235    /// let poly2 = CharacteristicPolynomial::new(
236    ///     vec![expr!(3), expr!(4)],  // 3 + 4λ
237    ///     lambda.clone()
238    /// );
239    ///
240    /// let sum = builder.add(&poly1, &poly2);  // 4 + 6λ
241    /// assert_eq!(sum.coefficients.len(), 2);
242    /// ```
243    pub fn add(&self, other: &CharacteristicPolynomial) -> CharacteristicPolynomial {
244        let max_len = self.coefficients.len().max(other.coefficients.len());
245        let mut coefficients = vec![Expression::integer(0); max_len];
246
247        for (i, coeff) in coefficients.iter_mut().enumerate().take(max_len) {
248            let coeff1 = if i < self.coefficients.len() {
249                self.coefficients[i].clone()
250            } else {
251                Expression::integer(0)
252            };
253
254            let coeff2 = if i < other.coefficients.len() {
255                other.coefficients[i].clone()
256            } else {
257                Expression::integer(0)
258            };
259
260            *coeff = Expression::add(vec![coeff1, coeff2]).simplify();
261        }
262
263        CharacteristicPolynomial {
264            coefficients,
265            variable: self.variable.clone(),
266        }
267    }
268
269    /// Multiplies two characteristic polynomials
270    ///
271    /// # Arguments
272    ///
273    /// * `poly1` - First polynomial
274    /// * `poly2` - Second polynomial
275    ///
276    /// # Returns
277    ///
278    /// Product of the two polynomials
279    ///
280    /// # Examples
281    ///
282    /// ```rust
283    /// use mathhook_core::{expr, symbol};
284    /// use mathhook_core::matrices::eigenvalues::characteristic::CharacteristicPolynomial;
285    ///
286    /// let lambda = symbol!(lambda);
287    ///
288    /// let poly1 = CharacteristicPolynomial::new(
289    ///     vec![expr!(1), expr!(1)],  // 1 + λ
290    ///     lambda.clone()
291    /// );
292    /// let poly2 = CharacteristicPolynomial::new(
293    ///     vec![expr!(2), expr!(1)],  // 2 + λ
294    ///     lambda.clone()
295    /// );
296    ///
297    /// let product = poly1.multiply(&poly2);  // 2 + 3λ + λ²
298    /// assert_eq!(product.degree(), 2);
299    /// ```
300    pub fn multiply(&self, other: &CharacteristicPolynomial) -> CharacteristicPolynomial {
301        if self.coefficients.is_empty() || other.coefficients.is_empty() {
302            return CharacteristicPolynomial::new(vec![], self.variable.clone());
303        }
304
305        let result_len = self.coefficients.len() + other.coefficients.len() - 1;
306        let mut result_coeffs = vec![Expression::integer(0); result_len];
307
308        for (i, c1) in self.coefficients.iter().enumerate() {
309            for (j, c2) in other.coefficients.iter().enumerate() {
310                let product = Expression::mul(vec![c1.clone(), c2.clone()]).simplify();
311                result_coeffs[i + j] =
312                    Expression::add(vec![result_coeffs[i + j].clone(), product]).simplify();
313            }
314        }
315
316        CharacteristicPolynomial::new(result_coeffs, self.variable.clone())
317    }
318
319    /// Formats polynomial as human-readable string
320    ///
321    /// # Examples
322    ///
323    /// ```rust
324    /// use mathhook_core::{expr, symbol};
325    /// use mathhook_core::matrices::eigenvalues::characteristic::CharacteristicPolynomial;
326    ///
327    /// let lambda = symbol!(lambda);
328    /// let poly = CharacteristicPolynomial::new(
329    ///     vec![expr!(6), expr!(-5), expr!(1)],
330    ///     lambda
331    /// );
332    ///
333    /// let formatted = poly.format();
334    /// // Output: "6 + (-5)·λ + λ²"
335    /// ```
336    pub fn format(&self) -> String {
337        if self.coefficients.is_empty() {
338            return "0".to_owned();
339        }
340
341        let mut parts = Vec::new();
342
343        for (power, coeff) in self.coefficients.iter().enumerate() {
344            if coeff.is_zero() {
345                continue;
346            }
347
348            let mut part = String::new();
349
350            if !coeff.is_one() || power == 0 {
351                part.push_str(&coeff.to_string());
352            }
353
354            if power > 0 {
355                if !coeff.is_one() {
356                    part.push('·');
357                }
358                part.push_str(self.variable.name.as_ref());
359
360                if power > 1 {
361                    part.push('^');
362                    part.push_str(&power.to_string());
363                }
364            }
365
366            parts.push(part);
367        }
368
369        if parts.is_empty() {
370            "0".to_owned()
371        } else {
372            parts.join(" + ")
373        }
374    }
375}
376
377/// Builder for characteristic polynomials
378///
379/// Provides methods for constructing characteristic polynomials from matrices.
380pub struct CharacteristicPolynomialBuilder;
381
382impl CharacteristicPolynomialBuilder {
383    /// Adds two characteristic polynomials
384    ///
385    /// # Arguments
386    ///
387    /// * `poly1` - First polynomial
388    /// * `poly2` - Second polynomial
389    ///
390    /// # Returns
391    ///
392    /// Sum of the two polynomials (same as `CharacteristicPolynomial::add`)
393    pub fn add(
394        &self,
395        poly1: &CharacteristicPolynomial,
396        poly2: &CharacteristicPolynomial,
397    ) -> CharacteristicPolynomial {
398        let max_len = poly1.coefficients.len().max(poly2.coefficients.len());
399        let mut coefficients = vec![Expression::integer(0); max_len];
400
401        for (i, coeff) in coefficients.iter_mut().enumerate().take(max_len) {
402            let coeff1 = if i < poly1.coefficients.len() {
403                poly1.coefficients[i].clone()
404            } else {
405                Expression::integer(0)
406            };
407
408            let coeff2 = if i < poly2.coefficients.len() {
409                poly2.coefficients[i].clone()
410            } else {
411                Expression::integer(0)
412            };
413
414            *coeff = Expression::add(vec![coeff1, coeff2]).simplify();
415        }
416
417        CharacteristicPolynomial {
418            coefficients,
419            variable: poly1.variable.clone(),
420        }
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427    use crate::{expr, symbol};
428
429    #[test]
430    fn test_new_polynomial() {
431        let lambda = symbol!(lambda);
432        let poly =
433            CharacteristicPolynomial::new(vec![expr!(1), expr!(2), expr!(3)], lambda.clone());
434
435        assert_eq!(poly.coefficients.len(), 3);
436        assert_eq!(poly.degree(), 2);
437    }
438
439    #[test]
440    fn test_polynomial_degree() {
441        let lambda = symbol!(lambda);
442
443        let poly1 = CharacteristicPolynomial::new(vec![expr!(1)], lambda.clone());
444        assert_eq!(poly1.degree(), 0);
445
446        let poly2 = CharacteristicPolynomial::new(vec![expr!(1), expr!(2)], lambda.clone());
447        assert_eq!(poly2.degree(), 1);
448
449        let poly3 =
450            CharacteristicPolynomial::new(vec![expr!(1), expr!(2), expr!(3)], lambda.clone());
451        assert_eq!(poly3.degree(), 2);
452
453        let poly_empty = CharacteristicPolynomial::new(vec![], lambda);
454        assert_eq!(poly_empty.degree(), 0);
455    }
456
457    #[test]
458    fn test_polynomial_to_expression() {
459        let lambda = symbol!(lambda);
460        let poly = CharacteristicPolynomial::new(vec![expr!(6), expr!(-5), expr!(1)], lambda);
461
462        let expr = poly.to_expression();
463        // Should represent: 6 - 5λ + λ²
464        assert!(!expr.is_zero());
465    }
466
467    #[test]
468    fn test_polynomial_evaluate() {
469        let lambda = symbol!(lambda);
470        let poly = CharacteristicPolynomial::new(vec![expr!(6), expr!(-5), expr!(1)], lambda);
471
472        // Test at λ = 2: 6 - 5(2) + 2² = 6 - 10 + 4 = 0
473        let result = poly.evaluate(&expr!(2));
474        assert_eq!(result.simplify(), expr!(0));
475
476        // Test at λ = 3: 6 - 5(3) + 3² = 6 - 15 + 9 = 0
477        let result = poly.evaluate(&expr!(3));
478        assert_eq!(result.simplify(), expr!(0));
479    }
480
481    #[test]
482    fn test_polynomial_addition() {
483        let lambda = symbol!(lambda);
484        let poly1 = CharacteristicPolynomial::new(vec![expr!(1), expr!(2)], lambda.clone());
485        let poly2 = CharacteristicPolynomial::new(vec![expr!(3), expr!(4)], lambda.clone());
486
487        let sum = poly1.add(&poly2);
488
489        assert_eq!(sum.coefficients.len(), 2);
490        assert_eq!(sum.coefficients[0].simplify(), expr!(4));
491        assert_eq!(sum.coefficients[1].simplify(), expr!(6));
492    }
493
494    #[test]
495    fn test_polynomial_addition_different_lengths() {
496        let lambda = symbol!(lambda);
497        let poly1 =
498            CharacteristicPolynomial::new(vec![expr!(1), expr!(2), expr!(3)], lambda.clone());
499        let poly2 = CharacteristicPolynomial::new(vec![expr!(4), expr!(5)], lambda.clone());
500
501        let sum = poly1.add(&poly2);
502
503        assert_eq!(sum.coefficients.len(), 3);
504        assert_eq!(sum.coefficients[0].simplify(), expr!(5));
505        assert_eq!(sum.coefficients[1].simplify(), expr!(7));
506        assert_eq!(sum.coefficients[2].simplify(), expr!(3));
507    }
508
509    #[test]
510    fn test_polynomial_multiplication() {
511        let lambda = symbol!(lambda);
512        let poly1 = CharacteristicPolynomial::new(vec![expr!(1), expr!(1)], lambda.clone());
513        let poly2 = CharacteristicPolynomial::new(vec![expr!(2), expr!(1)], lambda.clone());
514
515        let product = poly1.multiply(&poly2);
516
517        // (1 + λ)(2 + λ) = 2 + 3λ + λ²
518        assert_eq!(product.degree(), 2);
519        assert_eq!(product.coefficients[0].simplify(), expr!(2));
520        assert_eq!(product.coefficients[1].simplify(), expr!(3));
521        assert_eq!(product.coefficients[2].simplify(), expr!(1));
522    }
523
524    #[test]
525    fn test_polynomial_format() {
526        let lambda = symbol!(lambda);
527        let poly = CharacteristicPolynomial::new(vec![expr!(6), expr!(-5), expr!(1)], lambda);
528
529        let formatted = poly.format();
530        assert!(formatted.contains(poly.variable.name.as_ref()));
531    }
532
533    #[test]
534    fn test_builder_add() {
535        let lambda = symbol!(lambda);
536        let builder = CharacteristicPolynomialBuilder;
537
538        let poly1 = CharacteristicPolynomial::new(vec![expr!(1), expr!(2)], lambda.clone());
539        let poly2 = CharacteristicPolynomial::new(vec![expr!(3), expr!(4)], lambda.clone());
540
541        let sum = builder.add(&poly1, &poly2);
542
543        assert_eq!(sum.coefficients.len(), 2);
544        assert_eq!(sum.coefficients[0].simplify(), expr!(4));
545        assert_eq!(sum.coefficients[1].simplify(), expr!(6));
546    }
547}