mathhook_core/functions/polynomials/
evaluation.rs

1//! Polynomial Recurrence Evaluation Engine
2//!
3//! Generic three-term recurrence evaluation for all orthogonal polynomial families.
4//! Implements numerically stable evaluation using recurrence relations from properties.
5
6use crate::core::Expression;
7use crate::functions::properties::PolynomialProperties;
8use std::collections::HashMap;
9
10/// Evaluate polynomial using three-term recurrence relation
11///
12/// Generic implementation for all orthogonal polynomials that uses the
13/// recurrence relation defined in PolynomialProperties. This is mathematically
14/// the most stable approach for polynomial evaluation.
15///
16/// # Recurrence Form
17///
18/// For orthogonal polynomials, the three-term recurrence has the form:
19/// ```text
20/// P_{n+1}(x) = (alpha_n * x + beta_n) * P_n(x) + gamma_n * P_{n-1}(x)
21/// ```
22///
23/// # Arguments
24///
25/// * `properties` - Polynomial properties containing recurrence coefficients
26/// * `n` - Polynomial degree (must be non-negative)
27/// * `x` - Evaluation point
28///
29/// # Returns
30///
31/// Numerical value of P_n(x) computed using recurrence relation
32///
33/// # Examples
34///
35/// ```rust
36/// use mathhook_core::functions::polynomials::evaluation::evaluate_recurrence;
37/// use mathhook_core::functions::polynomials::legendre::LegendreIntelligence;
38/// use mathhook_core::functions::properties::FunctionProperties;
39///
40/// let legendre = LegendreIntelligence::new();
41/// let props = legendre.get_properties();
42/// if let Some(FunctionProperties::Polynomial(legendre_props)) = props.get("legendre_p") {
43///     let result = evaluate_recurrence(legendre_props, 5, 0.5);
44///     assert!((result - 0.08984375).abs() < 1e-10);
45/// }
46/// ```
47pub fn evaluate_recurrence(properties: &PolynomialProperties, n: usize, x: f64) -> f64 {
48    if n == 0 {
49        return eval_expr_at_x_internal(&properties.recurrence.initial_conditions.0, x);
50    }
51    if n == 1 {
52        return eval_expr_at_x_internal(&properties.recurrence.initial_conditions.1, x);
53    }
54
55    let mut p_prev = eval_expr_at_x_internal(&properties.recurrence.initial_conditions.0, x);
56    let mut p_curr = eval_expr_at_x_internal(&properties.recurrence.initial_conditions.1, x);
57
58    for i in 1..n {
59        let alpha = eval_coeff_at_n_internal(&properties.recurrence.alpha_coeff, i, x);
60        let beta = eval_coeff_at_n_internal(&properties.recurrence.beta_coeff, i, x);
61        let gamma = eval_coeff_at_n_internal(&properties.recurrence.gamma_coeff, i, x);
62
63        let p_next = (alpha * x + beta) * p_curr + gamma * p_prev;
64        p_prev = p_curr;
65        p_curr = p_next;
66    }
67
68    p_curr
69}
70
71/// Internal domain-specific evaluation for polynomial coefficients
72///
73/// Handles symbolic coefficients that depend on n (e.g., (2n+1)/(n+1) for Legendre)
74/// and evaluates them to f64 for numerical computation.
75///
76/// This function substitutes 'n' and 'x' symbols with their values, then uses
77/// Expression::evaluate_to_f64() for numerical conversion.
78///
79/// # Arguments
80///
81/// * `expr` - Coefficient expression (may contain 'n' symbol)
82/// * `n` - Current iteration index
83/// * `x` - Evaluation point (needed for some coefficient expressions)
84///
85/// # Returns
86///
87/// Numerical value of coefficient at given n
88fn eval_coeff_at_n_internal(expr: &Expression, n: usize, x: f64) -> f64 {
89    let mut substitutions = HashMap::new();
90    substitutions.insert("n".to_owned(), Expression::integer(n as i64));
91    substitutions.insert("x".to_owned(), Expression::float(x));
92
93    expr.substitute(&substitutions)
94        .evaluate_to_f64()
95        .unwrap_or_else(|_| eval_func_coeff_internal(expr, n))
96}
97
98/// Internal fallback for special coefficient functions
99///
100/// Handles special coefficient functions like legendre_alpha(n), hermite_gamma(n), etc.
101/// that cannot be handled by standard expression evaluation.
102fn eval_func_coeff_internal(expr: &Expression, n: usize) -> f64 {
103    match expr {
104        Expression::Function { name, .. } => {
105            let n_f64 = n as f64;
106            match name.as_ref() {
107                "legendre_alpha" => (2.0 * n_f64 + 1.0) / (n_f64 + 1.0),
108                "legendre_gamma" => -n_f64 / (n_f64 + 1.0),
109                "laguerre_alpha" => -(1.0 / (n_f64 + 1.0)),
110                "laguerre_beta" => (2.0 * n_f64 + 1.0) / (n_f64 + 1.0),
111                "laguerre_gamma" => -n_f64 / (n_f64 + 1.0),
112                _ => 0.0,
113            }
114        }
115        _ => 0.0,
116    }
117}
118
119/// Internal domain-specific evaluation for polynomial expressions
120///
121/// Used for initial conditions and constant expressions in recurrence.
122/// Substitutes 'x' symbol with the evaluation point and converts to f64.
123///
124/// # Arguments
125///
126/// * `expr` - Expression to evaluate
127/// * `x` - Evaluation point
128///
129/// # Returns
130///
131/// Numerical value of expression at x
132fn eval_expr_at_x_internal(expr: &Expression, x: f64) -> f64 {
133    let mut substitutions = HashMap::new();
134    substitutions.insert("x".to_owned(), Expression::float(x));
135
136    expr.substitute(&substitutions)
137        .evaluate_to_f64()
138        .unwrap_or(0.0)
139}
140
141/// Numerical evaluator function for Legendre polynomials
142///
143/// Expected args: [n, x] where n is polynomial degree and x is evaluation point
144pub fn evaluate_legendre_numerical(args: &[f64]) -> Vec<f64> {
145    if args.len() != 2 {
146        return vec![0.0];
147    }
148    let n = args[0] as usize;
149    let x = args[1];
150
151    vec![evaluate_legendre(n, x)]
152}
153
154/// Direct Legendre polynomial evaluation using recurrence relation
155///
156/// (n+1)P_{n+1}(x) = (2n+1)x P_n(x) - n P_{n-1}(x)
157fn evaluate_legendre(n: usize, x: f64) -> f64 {
158    if n == 0 {
159        return 1.0;
160    }
161    if n == 1 {
162        return x;
163    }
164
165    let mut p_prev = 1.0;
166    let mut p_curr = x;
167
168    for i in 1..n {
169        let i_f64 = i as f64;
170        let alpha = (2.0 * i_f64 + 1.0) / (i_f64 + 1.0);
171        let gamma = -i_f64 / (i_f64 + 1.0);
172
173        let p_next = alpha * x * p_curr + gamma * p_prev;
174        p_prev = p_curr;
175        p_curr = p_next;
176    }
177
178    p_curr
179}
180
181/// Numerical evaluator function for Hermite polynomials
182///
183/// Expected args: [n, x] where n is polynomial degree and x is evaluation point
184pub fn evaluate_hermite_numerical(args: &[f64]) -> Vec<f64> {
185    if args.len() != 2 {
186        return vec![0.0];
187    }
188    let n = args[0] as usize;
189    let x = args[1];
190
191    vec![evaluate_hermite(n, x)]
192}
193
194/// Direct Hermite polynomial evaluation using recurrence relation
195///
196/// H_{n+1}(x) = 2x H_n(x) - 2n H_{n-1}(x)
197fn evaluate_hermite(n: usize, x: f64) -> f64 {
198    if n == 0 {
199        return 1.0;
200    }
201    if n == 1 {
202        return 2.0 * x;
203    }
204
205    let mut p_prev = 1.0;
206    let mut p_curr = 2.0 * x;
207
208    for i in 1..n {
209        let i_f64 = i as f64;
210        let alpha = 2.0 * x;
211        let gamma = -2.0 * i_f64;
212
213        let p_next = alpha * p_curr + gamma * p_prev;
214        p_prev = p_curr;
215        p_curr = p_next;
216    }
217
218    p_curr
219}
220
221/// Numerical evaluator function for Laguerre polynomials
222///
223/// Expected args: [n, x] where n is polynomial degree and x is evaluation point
224pub fn evaluate_laguerre_numerical(args: &[f64]) -> Vec<f64> {
225    if args.len() != 2 {
226        return vec![0.0];
227    }
228    let n = args[0] as usize;
229    let x = args[1];
230
231    vec![evaluate_laguerre(n, x)]
232}
233
234/// Direct Laguerre polynomial evaluation using recurrence relation
235///
236/// (n+1)L_{n+1}(x) = (2n+1-x)L_n(x) - nL_{n-1}(x)
237fn evaluate_laguerre(n: usize, x: f64) -> f64 {
238    if n == 0 {
239        return 1.0;
240    }
241    if n == 1 {
242        return 1.0 - x;
243    }
244
245    let mut p_prev = 1.0;
246    let mut p_curr = 1.0 - x;
247
248    for i in 1..n {
249        let i_f64 = i as f64;
250        let alpha = -(1.0 / (i_f64 + 1.0));
251        let beta = (2.0 * i_f64 + 1.0) / (i_f64 + 1.0);
252        let gamma = -i_f64 / (i_f64 + 1.0);
253
254        let p_next = (alpha * x + beta) * p_curr + gamma * p_prev;
255        p_prev = p_curr;
256        p_curr = p_next;
257    }
258
259    p_curr
260}
261
262/// Numerical evaluator function for Chebyshev first kind polynomials T_n(x)
263///
264/// Expected args: [n, x] where n is polynomial degree and x is evaluation point
265pub fn evaluate_chebyshev_first_numerical(args: &[f64]) -> Vec<f64> {
266    if args.len() != 2 {
267        return vec![0.0];
268    }
269    let n = args[0] as usize;
270    let x = args[1];
271
272    vec![evaluate_chebyshev_first(n, x)]
273}
274
275/// Direct Chebyshev first kind polynomial evaluation using recurrence relation
276///
277/// T_{n+1}(x) = 2x T_n(x) - T_{n-1}(x)
278fn evaluate_chebyshev_first(n: usize, x: f64) -> f64 {
279    if n == 0 {
280        return 1.0;
281    }
282    if n == 1 {
283        return x;
284    }
285
286    let mut p_prev = 1.0;
287    let mut p_curr = x;
288
289    for _ in 1..n {
290        let p_next = 2.0 * x * p_curr - p_prev;
291        p_prev = p_curr;
292        p_curr = p_next;
293    }
294
295    p_curr
296}
297
298/// Numerical evaluator function for Chebyshev second kind polynomials U_n(x)
299///
300/// Expected args: [n, x] where n is polynomial degree and x is evaluation point
301pub fn evaluate_chebyshev_second_numerical(args: &[f64]) -> Vec<f64> {
302    if args.len() != 2 {
303        return vec![0.0];
304    }
305    let n = args[0] as usize;
306    let x = args[1];
307
308    vec![evaluate_chebyshev_second(n, x)]
309}
310
311/// Direct Chebyshev second kind polynomial evaluation using recurrence relation
312///
313/// U_{n+1}(x) = 2x U_n(x) - U_{n-1}(x)
314fn evaluate_chebyshev_second(n: usize, x: f64) -> f64 {
315    if n == 0 {
316        return 1.0;
317    }
318    if n == 1 {
319        return 2.0 * x;
320    }
321
322    let mut p_prev = 1.0;
323    let mut p_curr = 2.0 * x;
324
325    for _ in 1..n {
326        let p_next = 2.0 * x * p_curr - p_prev;
327        p_prev = p_curr;
328        p_curr = p_next;
329    }
330
331    p_curr
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use crate::functions::polynomials::legendre::LegendreIntelligence;
338    use crate::functions::properties::FunctionProperties;
339    use crate::{expr, symbol};
340
341    #[test]
342    fn test_evaluate_recurrence_legendre_low_order() {
343        let legendre = LegendreIntelligence::new();
344        let props = legendre.get_properties();
345
346        if let Some(FunctionProperties::Polynomial(legendre_props)) = props.get("legendre_p") {
347            assert!((evaluate_recurrence(legendre_props, 0, 0.5) - 1.0).abs() < 1e-10);
348            assert!((evaluate_recurrence(legendre_props, 1, 0.5) - 0.5).abs() < 1e-10);
349        }
350    }
351
352    #[test]
353    fn test_coefficient_evaluation() {
354        let n_sym = symbol!(n);
355        let expr_2n_plus_1 = expr!((2 * n) + 1);
356
357        assert!((eval_coeff_at_n_internal(&Expression::symbol(n_sym), 5, 0.0) - 5.0).abs() < 1e-10);
358        assert!((eval_coeff_at_n_internal(&expr_2n_plus_1, 5, 0.0) - 11.0).abs() < 1e-10);
359    }
360
361    #[test]
362    fn test_expression_evaluation() {
363        let x_sym = symbol!(x);
364        let expr_1 = expr!(1);
365        let expr_x = Expression::symbol(x_sym);
366        let expr_2x = expr!(2 * x);
367
368        assert!((eval_expr_at_x_internal(&expr_1, 0.5) - 1.0).abs() < 1e-10);
369        assert!((eval_expr_at_x_internal(&expr_x, 0.5) - 0.5).abs() < 1e-10);
370        assert!((eval_expr_at_x_internal(&expr_2x, 0.5) - 1.0).abs() < 1e-10);
371    }
372}