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}