Skip to main content

sparse_ir_basis/
gauss.rs

1//! Gauss quadrature rules for numerical integration
2//!
3//! This module provides quadrature rules for approximating integrals by weighted sums.
4//!
5//! The integral of f(x) * omega(x) is approximated by a weighted sum:
6//!
7//! sum(f(xi) * wi for (xi, wi) in zip(x, w))
8//!
9//! where we generally have superexponential convergence for smooth f(x)
10//! with the number of quadrature points.
11
12use crate::error::Error;
13use crate::matrix::Mat;
14use crate::numeric::CustomNumeric;
15use simba::scalar::ComplexField;
16use std::fmt::Debug;
17
18/// `Ok` if there is one weight per point
19fn check_weights<T>(x: &[T], w: &[T]) -> Result<(), Error> {
20    if x.len() == w.len() {
21        Ok(())
22    } else {
23        Err(Error::InvalidParameter {
24            name: "w",
25            value: format!("{} weights", w.len()),
26            reason: format!("must have one weight per point ({})", x.len()),
27        })
28    }
29}
30
31/// Quadrature rule for numerical integration.
32///
33/// Represents an approximation of an integral by a weighted sum over discrete points.
34/// The rule contains quadrature points `x`, weights `w`, and auxiliary arrays
35/// `x_forward` and `x_backward` for efficient computation.
36#[derive(Debug, Clone)]
37pub struct Rule<T> {
38    /// Quadrature points, in non-decreasing order
39    pub(crate) x: Vec<T>,
40    /// Quadrature weights
41    pub(crate) w: Vec<T>,
42    /// Distance from left endpoint: x - a
43    pub(crate) x_forward: Vec<T>,
44    /// Distance from right endpoint: b - x
45    pub(crate) x_backward: Vec<T>,
46    /// Left endpoint of integration interval
47    pub(crate) a: T,
48    /// Right endpoint of integration interval
49    pub(crate) b: T,
50}
51
52impl<T: Copy> Rule<T> {
53    /// Quadrature points, in non-decreasing order
54    pub fn x(&self) -> &[T] {
55        &self.x
56    }
57
58    /// Quadrature weights, one per point
59    pub fn w(&self) -> &[T] {
60        &self.w
61    }
62
63    /// Distance of each point from the left endpoint: `x - a`
64    pub fn x_forward(&self) -> &[T] {
65        &self.x_forward
66    }
67
68    /// Distance of each point from the right endpoint: `b - x`
69    pub fn x_backward(&self) -> &[T] {
70        &self.x_backward
71    }
72
73    /// Left endpoint of the integration interval
74    pub fn a(&self) -> T {
75        self.a
76    }
77
78    /// Right endpoint of the integration interval
79    pub fn b(&self) -> T {
80        self.b
81    }
82}
83
84impl<T> Rule<T>
85where
86    T: CustomNumeric,
87{
88    /// Create a new quadrature rule from points and weights.
89    ///
90    /// # Arguments
91    /// * `x` - Quadrature points
92    /// * `w` - Quadrature weights
93    /// * `a` - Left endpoint (default: -1.0)
94    /// * `b` - Right endpoint (default: 1.0)
95    ///
96    /// The points are not checked for order or for lying in `[a, b]`;
97    /// [`Self::validate`] does that.
98    ///
99    /// # Errors
100    /// [`Error::InvalidParameter`] if x and w have different lengths.
101    pub fn new(x: Vec<T>, w: Vec<T>, a: T, b: T) -> Result<Self, Error> {
102        check_weights(&x, &w)?;
103
104        let x_forward: Vec<T> = x.iter().map(|&xi| xi - a).collect();
105        let x_backward: Vec<T> = x.iter().map(|&xi| b - xi).collect();
106
107        Ok(Self {
108            x,
109            w,
110            x_forward,
111            x_backward,
112            a,
113            b,
114        })
115    }
116
117    /// Create a new quadrature rule from vectors.
118    ///
119    /// # Errors
120    /// The errors of [`Self::new`].
121    pub fn from_vectors(x: Vec<T>, w: Vec<T>, a: T, b: T) -> Result<Self, Error> {
122        Self::new(x, w, a, b)
123    }
124
125    /// Create a default rule with empty arrays.
126    pub fn empty() -> Self {
127        Self {
128            x: vec![],
129            w: vec![],
130            x_forward: vec![],
131            x_backward: vec![],
132            a: <T as CustomNumeric>::from_f64_unchecked(-1.0),
133            b: <T as CustomNumeric>::from_f64_unchecked(1.0),
134        }
135    }
136
137    /// Reseat the rule to a new interval [a, b].
138    ///
139    /// Scales and translates the quadrature points and weights to the new interval.
140    pub fn reseat(&self, a: T, b: T) -> Self {
141        let scaling = (b - a) / (self.b - self.a);
142        let midpoint_old = (self.b + self.a) * <T as CustomNumeric>::from_f64_unchecked(0.5);
143        let midpoint_new = (b + a) * <T as CustomNumeric>::from_f64_unchecked(0.5);
144
145        // Transform x: scaling * (xi - midpoint_old) + midpoint_new
146        let new_x: Vec<T> = self
147            .x
148            .iter()
149            .map(|&xi| scaling * (xi - midpoint_old) + midpoint_new)
150            .collect();
151        let new_w: Vec<T> = self.w.iter().map(|&wi| wi * scaling).collect();
152        let new_x_forward: Vec<T> = self.x_forward.iter().map(|&xi| xi * scaling).collect();
153        let new_x_backward: Vec<T> = self.x_backward.iter().map(|&xi| xi * scaling).collect();
154
155        Self {
156            x: new_x,
157            w: new_w,
158            x_forward: new_x_forward,
159            x_backward: new_x_backward,
160            a,
161            b,
162        }
163    }
164
165    /// Scale the weights by a factor.
166    pub fn scale(&self, factor: T) -> Self {
167        Self {
168            x: self.x.clone(),
169            w: self.w.iter().map(|&wi| wi * factor).collect(),
170            x_forward: self.x_forward.clone(),
171            x_backward: self.x_backward.clone(),
172            a: self.a,
173            b: self.b,
174        }
175    }
176
177    /// Create a piecewise rule over multiple segments.
178    ///
179    /// # Arguments
180    /// * `edges` - Segment boundaries: at least 2, finite and strictly
181    ///   increasing, with finite segment lengths and a finite sum of the ends
182    ///   of each segment (the midpoint of the segment is computed from it)
183    ///
184    /// # Errors
185    /// [`Error::InvalidParameter`] if `edges` does not meet these conditions
186    pub fn piecewise(&self, edges: &[T]) -> Result<Self, Error> {
187        if edges.len() < 2 {
188            return Err(Error::InvalidParameter {
189                name: "edges",
190                value: format!("{} edges", edges.len()),
191                reason: "must have at least 2 entries".to_string(),
192            });
193        }
194        if let Some((i, e)) = edges
195            .iter()
196            .enumerate()
197            .find(|(_, e)| !e.to_f64().is_finite())
198        {
199            return Err(Error::InvalidParameter {
200                name: "edges",
201                value: format!("{:?} at index {i}", e.to_f64()),
202                reason: "must be finite".to_string(),
203            });
204        }
205        for i in 1..edges.len() {
206            let length = (edges[i] - edges[i - 1]).to_f64();
207            if !(length > 0.0 && length.is_finite()) {
208                return Err(Error::InvalidParameter {
209                    name: "edges",
210                    value: format!(
211                        "{:?} after {:?} at index {i}",
212                        edges[i].to_f64(),
213                        edges[i - 1].to_f64()
214                    ),
215                    reason: "must be strictly increasing, with finite segment lengths".to_string(),
216                });
217            }
218            // `reseat` computes the midpoint as (a + b) * 0.5
219            if !(edges[i] + edges[i - 1]).to_f64().is_finite() {
220                return Err(Error::InvalidParameter {
221                    name: "edges",
222                    value: format!(
223                        "{:?} after {:?} at index {i}",
224                        edges[i].to_f64(),
225                        edges[i - 1].to_f64()
226                    ),
227                    reason: "must have a finite sum of the ends of each segment".to_string(),
228                });
229            }
230        }
231
232        let mut rules = Vec::new();
233        for i in 0..edges.len() - 1 {
234            let rule = self.reseat(edges[i], edges[i + 1]);
235            rules.push(rule);
236        }
237
238        Ok(Self::join(&rules))
239    }
240
241    /// Join multiple rules into a single rule.
242    ///
243    /// # Arguments
244    /// * `rules` - Vector of rules to join (must be contiguous and sorted)
245    ///
246    /// # Panics
247    /// Panics if rules are empty, not contiguous, or not sorted.
248    pub fn join(rules: &[Self]) -> Self {
249        if rules.is_empty() {
250            return Self::empty();
251        }
252
253        let a = rules[0].a;
254        let b = rules[rules.len() - 1].b;
255
256        // Check that rules are contiguous
257        for i in 1..rules.len() {
258            if (rules[i].a - rules[i - 1].b).abs_as_same_type() > T::epsilon() {
259                panic!("rules must be contiguous");
260            }
261        }
262
263        // Concatenate all arrays
264        let mut x_vec = Vec::new();
265        let mut w_vec = Vec::new();
266        let mut x_forward_vec = Vec::new();
267        let mut x_backward_vec = Vec::new();
268
269        for rule in rules {
270            // Adjust x_forward and x_backward for global coordinates
271            let x_forward_adj: Vec<T> =
272                rule.x_forward.iter().map(|&xi| xi + (rule.a - a)).collect();
273            let x_backward_adj: Vec<T> = rule
274                .x_backward
275                .iter()
276                .map(|&xi| xi + (b - rule.b))
277                .collect();
278
279            x_vec.extend(rule.x.iter().cloned());
280            w_vec.extend(rule.w.iter().cloned());
281            x_forward_vec.extend(x_forward_adj.iter().cloned());
282            x_backward_vec.extend(x_backward_adj.iter().cloned());
283        }
284
285        // Sort by x values to maintain order
286        let mut indices: Vec<usize> = (0..x_vec.len()).collect();
287        indices.sort_by(|&a, &b| x_vec[a].partial_cmp(&x_vec[b]).unwrap());
288
289        let sorted_x: Vec<T> = indices.iter().map(|&i| x_vec[i]).collect();
290        let sorted_w: Vec<T> = indices.iter().map(|&i| w_vec[i]).collect();
291
292        // Recalculate x_forward and x_backward after sorting
293        let sorted_x_forward: Vec<T> = sorted_x.iter().map(|&xi| xi - a).collect();
294        let sorted_x_backward: Vec<T> = sorted_x.iter().map(|&xi| b - xi).collect();
295
296        Self {
297            x: sorted_x,
298            w: sorted_w,
299            x_forward: sorted_x_forward,
300            x_backward: sorted_x_backward,
301            a,
302            b,
303        }
304    }
305
306    /// Convert the rule to a different numeric type.
307    pub fn convert<U>(&self) -> Rule<U>
308    where
309        U: CustomNumeric + Copy + Debug + std::fmt::Display,
310    {
311        let x: Vec<U> = self
312            .x
313            .iter()
314            .map(|&xi| <U as CustomNumeric>::from_f64_unchecked(xi.to_f64()))
315            .collect();
316        let w: Vec<U> = self
317            .w
318            .iter()
319            .map(|&wi| <U as CustomNumeric>::from_f64_unchecked(wi.to_f64()))
320            .collect();
321        let x_forward: Vec<U> = self
322            .x_forward
323            .iter()
324            .map(|&xi| <U as CustomNumeric>::from_f64_unchecked(xi.to_f64()))
325            .collect();
326        let x_backward: Vec<U> = self
327            .x_backward
328            .iter()
329            .map(|&xi| <U as CustomNumeric>::from_f64_unchecked(xi.to_f64()))
330            .collect();
331        let a = <U as CustomNumeric>::from_f64_unchecked(self.a.to_f64());
332        let b = <U as CustomNumeric>::from_f64_unchecked(self.b.to_f64());
333
334        Rule {
335            x,
336            w,
337            x_forward,
338            x_backward,
339            a,
340            b,
341        }
342    }
343
344    /// Validate the rule for consistency.
345    ///
346    /// # Returns
347    /// `true` if the rule is valid, `false` otherwise.
348    pub fn validate(&self) -> bool {
349        // Check interval validity
350        if self.a >= self.b {
351            return false;
352        }
353
354        // Check array lengths
355        if self.x.len() != self.w.len() {
356            return false;
357        }
358
359        if self.x.len() != self.x_forward.len() || self.x.len() != self.x_backward.len() {
360            return false;
361        }
362
363        // Check that all points are within [a, b]
364        for &xi in self.x.iter() {
365            if xi < self.a || xi > self.b {
366                return false;
367            }
368        }
369
370        // Check that points are sorted
371        for i in 1..self.x.len() {
372            if self.x[i] <= self.x[i - 1] {
373                return false;
374            }
375        }
376
377        // Check x_forward and x_backward consistency
378        for i in 0..self.x.len() {
379            let expected_forward = self.x[i] - self.a;
380            let expected_backward = self.b - self.x[i];
381
382            if (self.x_forward[i] - expected_forward).abs_as_same_type() > T::epsilon() {
383                return false;
384            }
385            if (self.x_backward[i] - expected_backward).abs_as_same_type() > T::epsilon() {
386                return false;
387            }
388        }
389
390        true
391    }
392}
393
394/// CustomNumeric-based implementation for f64 and Df64 support
395impl<T> Rule<T>
396where
397    T: CustomNumeric,
398{
399    /// Create a new quadrature rule from points and weights (CustomNumeric version).
400    ///
401    /// # Errors
402    /// [`Error::InvalidParameter`] if x and w have different lengths.
403    pub fn new_custom(x: Vec<T>, w: Vec<T>, a: T, b: T) -> Result<Self, Error> {
404        check_weights(&x, &w)?;
405
406        let x_forward: Vec<T> = x.iter().map(|&xi| xi - a).collect();
407        let x_backward: Vec<T> = x.iter().map(|&xi| b - xi).collect();
408
409        Ok(Self {
410            x,
411            w,
412            x_forward,
413            x_backward,
414            a,
415            b,
416        })
417    }
418
419    /// Create a new quadrature rule from vectors (CustomNumeric version).
420    ///
421    /// # Errors
422    /// The errors of [`Self::new_custom`].
423    pub fn from_vectors_custom(x: Vec<T>, w: Vec<T>, a: T, b: T) -> Result<Self, Error> {
424        Self::new_custom(x, w, a, b)
425    }
426
427    /// Reseat the rule to a new interval [a, b] (CustomNumeric version).
428    pub fn reseat_custom(&self, a: T, b: T) -> Self {
429        let scaling = (b - a) / (self.b - self.a);
430        let midpoint_old = (self.b + self.a) * <T as CustomNumeric>::from_f64_unchecked(0.5);
431        let midpoint_new = (b + a) * <T as CustomNumeric>::from_f64_unchecked(0.5);
432
433        // Transform x: scaling * (xi - midpoint_old) + midpoint_new
434        let new_x: Vec<T> = self
435            .x
436            .iter()
437            .map(|&xi| scaling * (xi - midpoint_old) + midpoint_new)
438            .collect();
439        let new_w: Vec<T> = self.w.iter().map(|&wi| wi * scaling).collect();
440        let new_x_forward: Vec<T> = self.x_forward.iter().map(|&xi| xi * scaling).collect();
441        let new_x_backward: Vec<T> = self.x_backward.iter().map(|&xi| xi * scaling).collect();
442
443        Self {
444            x: new_x,
445            w: new_w,
446            x_forward: new_x_forward,
447            x_backward: new_x_backward,
448            a,
449            b,
450        }
451    }
452
453    /// Scale the weights by a factor (CustomNumeric version).
454    pub fn scale_custom(&self, factor: T) -> Self {
455        Self {
456            x: self.x.clone(),
457            w: self.w.iter().map(|&wi| wi * factor).collect(),
458            x_forward: self.x_forward.clone(),
459            x_backward: self.x_backward.clone(),
460            a: self.a,
461            b: self.b,
462        }
463    }
464
465    /// Validate the rule for consistency (CustomNumeric version).
466    pub fn validate_custom(&self) -> bool {
467        // Check interval validity
468        if self.a >= self.b {
469            return false;
470        }
471
472        // Check array lengths
473        if self.x.len() != self.w.len() {
474            return false;
475        }
476
477        if self.x.len() != self.x_forward.len() || self.x.len() != self.x_backward.len() {
478            return false;
479        }
480
481        // Check that all points are within [a, b]
482        for &xi in self.x.iter() {
483            if xi < self.a || xi > self.b {
484                return false;
485            }
486        }
487
488        // Check that points are sorted
489        for i in 1..self.x.len() {
490            if self.x[i] <= self.x[i - 1] {
491                return false;
492            }
493        }
494
495        // Check x_forward and x_backward consistency
496        for i in 0..self.x.len() {
497            let expected_forward = self.x[i] - self.a;
498            let expected_backward = self.b - self.x[i];
499
500            if (self.x_forward[i] - expected_forward).abs_as_same_type() > T::epsilon() {
501                return false;
502            }
503            if (self.x_backward[i] - expected_backward).abs_as_same_type() > T::epsilon() {
504                return false;
505            }
506        }
507
508        true
509    }
510}
511
512/// Df64-specific implementation without ScalarOperand requirement
513impl Rule<crate::Df64> {
514    /// Create a new quadrature rule from points and weights (Df64 version).
515    ///
516    /// # Errors
517    /// [`Error::InvalidParameter`] if x and w have different lengths.
518    pub fn new_twofloat(
519        x: Vec<crate::Df64>,
520        w: Vec<crate::Df64>,
521        a: crate::Df64,
522        b: crate::Df64,
523    ) -> Result<Self, Error> {
524        check_weights(&x, &w)?;
525
526        let x_forward: Vec<crate::Df64> = x.iter().map(|&xi| xi - a).collect();
527        let x_backward: Vec<crate::Df64> = x.iter().map(|&xi| b - xi).collect();
528
529        Ok(Self {
530            x,
531            w,
532            x_forward,
533            x_backward,
534            a,
535            b,
536        })
537    }
538
539    /// Create a new quadrature rule from vectors (Df64 version).
540    ///
541    /// # Errors
542    /// The errors of [`Self::new_twofloat`].
543    pub fn from_vectors_twofloat(
544        x: Vec<crate::Df64>,
545        w: Vec<crate::Df64>,
546        a: crate::Df64,
547        b: crate::Df64,
548    ) -> Result<Self, Error> {
549        Self::new_twofloat(x, w, a, b)
550    }
551
552    /// Reseat the rule to a new interval [a, b] (Df64 version).
553    pub fn reseat_twofloat(&self, a: crate::Df64, b: crate::Df64) -> Self {
554        let scaling = (b - a) / (self.b - self.a);
555        let midpoint_old =
556            (self.b + self.a) * <crate::Df64 as CustomNumeric>::from_f64_unchecked(0.5);
557        let midpoint_new = (b + a) * <crate::Df64 as CustomNumeric>::from_f64_unchecked(0.5);
558
559        // Transform x: scaling * (xi - midpoint_old) + midpoint_new
560        let new_x: Vec<crate::Df64> = self
561            .x
562            .iter()
563            .map(|&xi| scaling * (xi - midpoint_old) + midpoint_new)
564            .collect();
565        let new_w: Vec<crate::Df64> = self.w.iter().map(|&wi| wi * scaling).collect();
566        let new_x_forward: Vec<crate::Df64> =
567            self.x_forward.iter().map(|&xi| xi * scaling).collect();
568        let new_x_backward: Vec<crate::Df64> =
569            self.x_backward.iter().map(|&xi| xi * scaling).collect();
570
571        Self {
572            x: new_x,
573            w: new_w,
574            x_forward: new_x_forward,
575            x_backward: new_x_backward,
576            a,
577            b,
578        }
579    }
580
581    /// Scale the weights by a factor (Df64 version).
582    pub fn scale_twofloat(&self, factor: crate::Df64) -> Self {
583        Self {
584            x: self.x.clone(),
585            w: self.w.iter().map(|&wi| wi * factor).collect(),
586            x_forward: self.x_forward.clone(),
587            x_backward: self.x_backward.clone(),
588            a: self.a,
589            b: self.b,
590        }
591    }
592
593    /// Validate the rule for consistency (Df64 version).
594    pub fn validate_twofloat(&self) -> bool {
595        // Check interval validity
596        if self.a >= self.b {
597            return false;
598        }
599
600        // Check array lengths
601        if self.x.len() != self.w.len() {
602            return false;
603        }
604
605        if self.x.len() != self.x_forward.len() || self.x.len() != self.x_backward.len() {
606            return false;
607        }
608
609        // Check that all points are within [a, b]
610        for &xi in self.x.iter() {
611            if xi < self.a || xi > self.b {
612                return false;
613            }
614        }
615
616        // Check that points are sorted
617        for i in 1..self.x.len() {
618            if self.x[i] <= self.x[i - 1] {
619                return false;
620            }
621        }
622
623        // Check x_forward and x_backward consistency
624        for i in 0..self.x.len() {
625            let expected_forward = self.x[i] - self.a;
626            let expected_backward = self.b - self.x[i];
627
628            if (self.x_forward[i] - expected_forward).abs() > crate::Df64::epsilon() {
629                return false;
630            }
631            if (self.x_backward[i] - expected_backward).abs() > crate::Df64::epsilon() {
632                return false;
633            }
634        }
635
636        true
637    }
638}
639
640/// Compute Gauss-Legendre quadrature nodes and weights using Newton's method.
641///
642/// This is a simplified implementation of the Gauss-Legendre quadrature rule.
643/// For production use, a more sophisticated algorithm would be preferred.
644fn gauss_legendre_nodes_weights<T>(n: usize) -> (Vec<T>, Vec<T>)
645where
646    T: CustomNumeric + Copy + Debug + std::fmt::Display + 'static,
647{
648    if n == 0 {
649        return (Vec::new(), Vec::new());
650    }
651
652    if n == 1 {
653        return (
654            vec![<T as CustomNumeric>::from_f64_unchecked(0.0)],
655            vec![<T as CustomNumeric>::from_f64_unchecked(2.0)],
656        );
657    }
658
659    let mut x = Vec::with_capacity(n);
660    let mut w = Vec::with_capacity(n);
661
662    // Use Newton's method to find roots of Legendre polynomial
663    let m = n.div_ceil(2);
664
665    // Use high-precision constants via CustomNumeric trait
666    let pi = T::pi();
667
668    for i in 0..m {
669        // Convert integers directly to avoid f64 intermediate
670        let i_val = <T as CustomNumeric>::from_f64_unchecked(i as f64);
671        let n_val = <T as CustomNumeric>::from_f64_unchecked(n as f64);
672        let three_quarters = <T as CustomNumeric>::from_f64_unchecked(0.75);
673        let half = <T as CustomNumeric>::from_f64_unchecked(0.5);
674
675        // Initial guess using Chebyshev nodes
676        let mut z = (pi * (i_val + three_quarters) / (n_val + half)).cos();
677
678        // Newton's method to refine the root
679        for _ in 0..10 {
680            let (p0, p1) = legendre_polynomial_and_derivative(n, z);
681            if p0.abs_as_same_type() < T::epsilon() {
682                break;
683            }
684            z = z - p0 / p1;
685        }
686
687        // Compute weight using high-precision constants
688        let two = <T as CustomNumeric>::from_f64_unchecked(2.0);
689        let one = <T as CustomNumeric>::from_f64_unchecked(1.0);
690        let (_, p1) = legendre_polynomial_and_derivative(n, z);
691        let weight = two / ((one - z * z) * p1 * p1);
692
693        x.push(-z);
694        w.push(weight);
695
696        if i != n - 1 - i {
697            x.push(z);
698            w.push(weight);
699        }
700    }
701
702    // Sort by x values
703    let mut indices: Vec<usize> = (0..n).collect();
704    indices.sort_by(|&a, &b| x[a].partial_cmp(&x[b]).unwrap());
705
706    let sorted_x: Vec<T> = indices.iter().map(|&i| x[i]).collect();
707    let sorted_w: Vec<T> = indices.iter().map(|&i| w[i]).collect();
708
709    (sorted_x, sorted_w)
710}
711
712/// Compute Legendre polynomial P_n(x) and its derivative using recurrence relation.
713fn legendre_polynomial_and_derivative<T>(n: usize, x: T) -> (T, T)
714where
715    T: CustomNumeric + Copy + Debug + std::fmt::Display + 'static,
716{
717    if n == 0 {
718        return (
719            <T as CustomNumeric>::from_f64_unchecked(1.0),
720            <T as CustomNumeric>::from_f64_unchecked(0.0),
721        );
722    }
723
724    if n == 1 {
725        return (x, <T as CustomNumeric>::from_f64_unchecked(1.0));
726    }
727
728    let mut p0 = <T as CustomNumeric>::from_f64_unchecked(1.0);
729    let mut p1 = x;
730    let mut dp0 = <T as CustomNumeric>::from_f64_unchecked(0.0);
731    let mut dp1 = <T as CustomNumeric>::from_f64_unchecked(1.0);
732
733    for k in 2..=n {
734        let k_f = <T as CustomNumeric>::from_f64_unchecked(k as f64);
735        let k1_f = <T as CustomNumeric>::from_f64_unchecked((k - 1) as f64);
736        let _k2_f = <T as CustomNumeric>::from_f64_unchecked((k - 2) as f64);
737
738        let p2 = ((<T as CustomNumeric>::from_f64_unchecked(2.0) * k1_f
739            + <T as CustomNumeric>::from_f64_unchecked(1.0))
740            * x
741            * p1
742            - k1_f * p0)
743            / k_f;
744        let dp2 = ((<T as CustomNumeric>::from_f64_unchecked(2.0) * k1_f
745            + <T as CustomNumeric>::from_f64_unchecked(1.0))
746            * (p1 + x * dp1)
747            - k1_f * dp0)
748            / k_f;
749
750        p0 = p1;
751        p1 = p2;
752        dp0 = dp1;
753        dp1 = dp2;
754    }
755
756    (p1, dp1)
757}
758
759/// Create a Gauss-Legendre quadrature rule with n points on [-1, 1].
760///
761/// # Arguments
762/// * `n` - Number of quadrature points
763///
764/// # Returns
765/// A Gauss-Legendre quadrature rule
766pub fn legendre<T>(n: usize) -> Rule<T>
767where
768    T: CustomNumeric + Copy + Debug + std::fmt::Display + 'static,
769{
770    if n == 0 {
771        return Rule::empty();
772    }
773
774    let (x, w) = gauss_legendre_nodes_weights(n);
775
776    Rule::from_vectors(
777        x,
778        w,
779        <T as CustomNumeric>::from_f64_unchecked(-1.0),
780        <T as CustomNumeric>::from_f64_unchecked(1.0),
781    )
782    .expect("the Gauss-Legendre nodes and weights have the same length")
783}
784
785/// Compute Gauss-Legendre quadrature nodes and weights using CustomNumeric
786fn gauss_legendre_nodes_weights_custom<T>(n: usize) -> (Vec<T>, Vec<T>)
787where
788    T: CustomNumeric,
789{
790    if n == 0 {
791        return (Vec::new(), Vec::new());
792    }
793
794    if n == 1 {
795        return (
796            vec![<T as CustomNumeric>::from_f64_unchecked(0.0)],
797            vec![<T as CustomNumeric>::from_f64_unchecked(2.0)],
798        );
799    }
800
801    let mut x = Vec::with_capacity(n);
802    let mut w = Vec::with_capacity(n);
803
804    // Use Newton's method to find roots of Legendre polynomial
805    let m = n.div_ceil(2);
806    let pi = <T as CustomNumeric>::from_f64_unchecked(std::f64::consts::PI);
807
808    for i in 0..m {
809        // Initial guess using Chebyshev nodes
810        // Note: Df64's cos() has only f64-level precision (~15-16 digits), not the full
811        // theoretical 30-digit precision. This limits Df64 interpolation accuracy to ~1e-16,
812        // not the 1e-30 that might be theoretically possible with perfect double-double arithmetic.
813        let mut z = (pi * <T as CustomNumeric>::from_f64_unchecked(i as f64 + 0.75)
814            / <T as CustomNumeric>::from_f64_unchecked(n as f64 + 0.5))
815        .cos();
816
817        // Newton's method to refine the root
818        for _ in 0..10 {
819            let (p0, p1) = legendre_polynomial_and_derivative_custom(n, z);
820            if p0.abs_as_same_type() < T::epsilon() {
821                break;
822            }
823            z = z - p0 / p1;
824        }
825
826        // Compute weight
827        let (_, p1) = legendre_polynomial_and_derivative_custom(n, z);
828        let weight = <T as CustomNumeric>::from_f64_unchecked(2.0)
829            / ((<T as CustomNumeric>::from_f64_unchecked(1.0) - z * z) * p1 * p1);
830
831        x.push(-z);
832        w.push(weight);
833
834        if i != n - 1 - i {
835            x.push(z);
836            w.push(weight);
837        }
838    }
839
840    // Sort by x values
841    let mut indices: Vec<usize> = (0..n).collect();
842    indices.sort_by(|&a, &b| x[a].partial_cmp(&x[b]).unwrap());
843
844    let sorted_x: Vec<T> = indices.iter().map(|&i| x[i]).collect();
845    let sorted_w: Vec<T> = indices.iter().map(|&i| w[i]).collect();
846
847    (sorted_x, sorted_w)
848}
849
850/// Compute Legendre polynomial P_n(x) and its derivative using CustomNumeric
851fn legendre_polynomial_and_derivative_custom<T>(n: usize, x: T) -> (T, T)
852where
853    T: CustomNumeric,
854{
855    if n == 0 {
856        return (
857            <T as CustomNumeric>::from_f64_unchecked(1.0),
858            <T as CustomNumeric>::from_f64_unchecked(0.0),
859        );
860    }
861
862    if n == 1 {
863        return (x, <T as CustomNumeric>::from_f64_unchecked(1.0));
864    }
865
866    let mut p0 = <T as CustomNumeric>::from_f64_unchecked(1.0);
867    let mut p1 = x;
868    let mut dp0 = <T as CustomNumeric>::from_f64_unchecked(0.0);
869    let mut dp1 = <T as CustomNumeric>::from_f64_unchecked(1.0);
870
871    for k in 2..=n {
872        let k_f = <T as CustomNumeric>::from_f64_unchecked(k as f64);
873        let k1_f = <T as CustomNumeric>::from_f64_unchecked((k - 1) as f64);
874        let _k2_f = <T as CustomNumeric>::from_f64_unchecked((k - 2) as f64);
875
876        let two = <T as CustomNumeric>::from_f64_unchecked(2.0);
877        let one = <T as CustomNumeric>::from_f64_unchecked(1.0);
878
879        let p2 = ((two * k1_f + one) * x * p1 - k1_f * p0) / k_f;
880        let dp2 = ((two * k1_f + one) * (p1 + x * dp1) - k1_f * dp0) / k_f;
881
882        p0 = p1;
883        p1 = p2;
884        dp0 = dp1;
885        dp1 = dp2;
886    }
887
888    (p1, dp1)
889}
890
891/// Create a Gauss-Legendre quadrature rule with n points on [-1, 1] (CustomNumeric version).
892pub fn legendre_custom<T>(n: usize) -> Rule<T>
893where
894    T: CustomNumeric,
895{
896    if n == 0 {
897        return Rule::new_custom(
898            vec![],
899            vec![],
900            <T as CustomNumeric>::from_f64_unchecked(-1.0),
901            <T as CustomNumeric>::from_f64_unchecked(1.0),
902        )
903        .expect("the Gauss-Legendre nodes and weights have the same length");
904    }
905
906    let (x, w) = gauss_legendre_nodes_weights_custom(n);
907
908    Rule::from_vectors_custom(
909        x,
910        w,
911        <T as CustomNumeric>::from_f64_unchecked(-1.0),
912        <T as CustomNumeric>::from_f64_unchecked(1.0),
913    )
914    .expect("the Gauss-Legendre nodes and weights have the same length")
915}
916
917/// Create a Gauss-Legendre quadrature rule with n points on [-1, 1] (Df64 version).
918pub fn legendre_twofloat(n: usize) -> Rule<crate::Df64> {
919    if n == 0 {
920        return Rule::new_twofloat(
921            vec![],
922            vec![],
923            <crate::Df64 as CustomNumeric>::from_f64_unchecked(-1.0),
924            <crate::Df64 as CustomNumeric>::from_f64_unchecked(1.0),
925        )
926        .expect("the Gauss-Legendre nodes and weights have the same length");
927    }
928
929    let mut x: Vec<crate::Df64> = vec![crate::Df64::ZERO; n];
930    let mut w: Vec<crate::Df64> = vec![crate::Df64::ZERO; n];
931    xprec::gauss::gauss_legendre(&mut x, &mut w);
932
933    Rule::from_vectors_twofloat(
934        x,
935        w,
936        <crate::Df64 as CustomNumeric>::from_f64_unchecked(-1.0),
937        <crate::Df64 as CustomNumeric>::from_f64_unchecked(1.0),
938    )
939    .expect("the Gauss-Legendre nodes and weights have the same length")
940}
941
942/// Create Legendre Vandermonde matrix for polynomial interpolation
943///
944/// # Arguments
945/// * `x` - Points where polynomials are evaluated
946/// * `degree` - Maximum degree of Legendre polynomials
947///
948/// # Returns
949/// Matrix V where V[i,j] = P_j(x_i), with P_j being the j-th Legendre polynomial
950pub fn legendre_vandermonde<T: CustomNumeric>(x: &[T], degree: usize) -> Mat<T> {
951    let n = x.len();
952    let mut v = Mat::<T>::from_elem([n, degree + 1], T::zero());
953
954    // First column is all ones (P_0(x) = 1)
955    for i in 0..n {
956        v[[i, 0]] = T::from_f64_unchecked(1.0);
957    }
958
959    // Second column is x (P_1(x) = x)
960    if degree > 0 {
961        for i in 0..n {
962            v[[i, 1]] = x[i];
963        }
964    }
965
966    // Recurrence relation: P_n(x) = ((2n-1)x*P_{n-1}(x) - (n-1)*P_{n-2}(x)) / n
967    for j in 2..=degree {
968        for i in 0..n {
969            let n_f64 = j as f64;
970            let term1 = T::from_f64_unchecked(2.0 * n_f64 - 1.0) * x[i] * v[[i, j - 1]];
971            let term2 = T::from_f64_unchecked(n_f64 - 1.0) * v[[i, j - 2]];
972            v[[i, j]] = (term1 - term2) / T::from_f64_unchecked(n_f64);
973        }
974    }
975
976    v
977}
978
979/// Generic Legendre Gauss quadrature rule for CustomNumeric types
980pub fn legendre_generic<T: CustomNumeric + 'static>(n: usize) -> Rule<T> {
981    if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>() {
982        // For f64, use the existing legendre function
983        let rule_f64 = legendre::<f64>(n);
984        Rule::new(
985            rule_f64
986                .x
987                .iter()
988                .map(|&x| T::from_f64_unchecked(x))
989                .collect(),
990            rule_f64
991                .w
992                .iter()
993                .map(|&w| T::from_f64_unchecked(w))
994                .collect(),
995            T::from_f64_unchecked(rule_f64.a),
996            T::from_f64_unchecked(rule_f64.b),
997        )
998        .expect("the Gauss-Legendre nodes and weights have the same length")
999    } else {
1000        // For Df64, use legendre_twofloat
1001        let rule_tf = legendre_twofloat(n);
1002        Rule::new(
1003            rule_tf.x.iter().map(|&x| T::convert_from(x)).collect(),
1004            rule_tf.w.iter().map(|&w| T::convert_from(w)).collect(),
1005            T::from_f64_unchecked(rule_tf.a.into()),
1006            T::from_f64_unchecked(rule_tf.b.into()),
1007        )
1008        .expect("the Gauss-Legendre nodes and weights have the same length")
1009    }
1010}
1011
1012#[cfg(test)]
1013#[path = "gauss_tests.rs"]
1014mod tests;