use std::{borrow::Cow, ops::RangeInclusive};
use crate::{
basis::{
Basis, CriticalPoint, DifferentialBasis, IntegralBasis, IntoMonomialBasis, OrthogonalBasis,
Root, RootFindingBasis, RootFindingMethod,
},
display::PolynomialDisplay,
error::Result,
statistics,
value::{CoordExt, FloatClampedCast, SteppedValues, Value},
};
pub type LogarithmicPolynomial<'data, T = f64> =
Polynomial<'data, crate::basis::LogarithmicBasis<T>, T>;
pub type LaguerrePolynomial<'data, T = f64> = Polynomial<'data, crate::basis::LaguerreBasis<T>, T>;
pub type PhysicistsHermitePolynomial<'data, T = f64> =
Polynomial<'data, crate::basis::PhysicistsHermiteBasis<T>, T>;
pub type ProbabilistsHermitePolynomial<'data, T = f64> =
Polynomial<'data, crate::basis::ProbabilistsHermiteBasis<T>, T>;
pub type LegendrePolynomial<'data, T = f64> = Polynomial<'data, crate::basis::LegendreBasis<T>, T>;
pub type FourierPolynomial<'data, T = f64> = Polynomial<'data, crate::basis::FourierBasis<T>, T>;
pub type LinearAugmentedFourierPolynomial<'data, T = f64> =
Polynomial<'data, crate::basis::LinearAugmentedFourierBasis<T>, T>;
pub type ChebyshevPolynomial<'data, T = f64> =
Polynomial<'data, crate::basis::ChebyshevBasis<T>, T>;
pub type MonomialPolynomial<'data, T = f64> = Polynomial<'data, crate::basis::MonomialBasis<T>, T>;
#[derive(Debug, Clone, PartialEq)]
pub struct Polynomial<'a, B, T: Value = f64>
where
B: Basis<T>,
B: PolynomialDisplay<T>,
{
degree: usize,
basis: B,
coefficients: Cow<'a, [T]>,
}
impl<'a, B, T: Value> Polynomial<'a, B, T>
where
B: Basis<T>,
B: PolynomialDisplay<T>,
{
pub const unsafe fn from_raw(basis: B, coefficients: Cow<'a, [T]>, degree: usize) -> Self {
Self {
degree,
basis,
coefficients,
}
}
pub fn from_basis(basis: B, coefficients: impl Into<Cow<'a, [T]>>) -> Result<Self> {
let coefficients = coefficients.into();
let degree = basis.degree(coefficients.len()).ok_or(
crate::error::Error::InvalidNumberOfParameters(coefficients.len()),
)?;
Ok(unsafe { Self::from_raw(basis, coefficients, degree) })
}
pub fn into_inner(self) -> (B, Cow<'a, [T]>, usize) {
(self.basis, self.coefficients, self.degree)
}
pub(crate) fn basis(&self) -> &B {
&self.basis
}
pub fn into_owned(self) -> Polynomial<'static, B, T> {
Polynomial {
degree: self.degree,
basis: self.basis,
coefficients: Cow::Owned(self.coefficients.into_owned()),
}
}
#[must_use]
pub fn coefficients(&self) -> &[T] {
&self.coefficients
}
#[must_use]
pub fn coefficients_mut(&mut self) -> &mut [T] {
self.coefficients.to_mut()
}
#[must_use]
pub fn degree(&self) -> usize {
self.degree
}
pub fn abs(&mut self) {
for c in self.coefficients.to_mut().iter_mut() {
*c = Value::abs(*c);
}
}
pub fn scale(&mut self, factor: T) {
for c in self.coefficients.to_mut().iter_mut() {
*c *= factor;
}
}
pub fn y(&self, x: T) -> T {
let x = self.basis.normalize_x(x);
self.basis.solve(x, self.coefficients())
}
pub fn solve(&self, x: impl IntoIterator<Item = T>) -> Vec<(T, T)> {
x.into_iter().map(|x| (x, self.y(x))).collect()
}
pub fn solve_range(&self, range: RangeInclusive<T>, step: T) -> Vec<(T, T)> {
self.solve(SteppedValues::new(range, step))
}
pub fn r_squared(&self, data: &[(T, T)]) -> T {
let x = data.x_iter();
let y = data.y_iter();
let y_fit = self.solve(x).into_iter().map(|(_, y)| y);
statistics::r_squared(y, y_fit)
}
pub fn remove_leading_zeros(&mut self) {
while self.degree > 0
&& !self.coefficients.is_empty()
&& Value::abs(self.coefficients[self.degree]) <= T::epsilon()
{
self.degree -= 1;
self.coefficients.to_mut().pop();
}
if self.coefficients.is_empty() {
self.coefficients.to_mut().push(T::zero());
}
}
pub fn leading_coefficient(&self) -> T {
for c in self.coefficients.iter().rev() {
if Value::abs(*c) > T::epsilon() {
return *c;
}
}
T::zero()
}
pub fn derivative(&self) -> Result<Polynomial<'static, B::B2, T>>
where
B: DifferentialBasis<T>,
{
let new_degree = if self.degree == 0 { 0 } else { self.degree - 1 };
let (db, dc) = self.basis.derivative(&self.coefficients)?;
let derivative = unsafe { Polynomial::from_raw(db, dc.into(), new_degree) };
Ok(derivative)
}
pub fn critical_points(&self, x_range: RangeInclusive<T>) -> Result<Vec<CriticalPoint<T>>>
where
B: DifferentialBasis<T>,
B::B2: DifferentialBasis<T>,
B::B2: RootFindingBasis<T>,
{
self.critical_points_from_roots(x_range, |f, x| {
f.roots(x).map(|roots| {
roots
.into_iter()
.filter_map(|r| if let Root::Real(x) = r { Some(x) } else { None })
.collect()
})
})
}
pub fn iterative_critical_points(
&self,
x_range: RangeInclusive<T>,
) -> Result<Vec<CriticalPoint<T>>>
where
B: DifferentialBasis<T>,
B::B2: RootFindingBasis<T>,
{
self.critical_points_from_roots(x_range, |f, x| {
f.iterative_roots(x).map(|roots| {
roots
.into_iter()
.filter_map(|r| if let Root::Real(x) = r { Some(x) } else { None })
.collect()
})
})
}
fn critical_points_from_roots<F>(
&self,
x_range: RangeInclusive<T>,
root_finder: F,
) -> Result<Vec<CriticalPoint<T>>>
where
B: DifferentialBasis<T>,
B::B2: DifferentialBasis<T>,
F: Fn(&Polynomial<B::B2, T>, RangeInclusive<T>) -> Result<Vec<T>>,
{
let dx = self.derivative()?;
let ddx = dx.derivative()?;
let roots = root_finder(&dx, x_range)?;
let mut points = Vec::with_capacity(roots.len());
for x in roots {
let curvature = ddx.y(x);
let y = self.y(x);
match curvature {
c if c > T::zero() => points.push(CriticalPoint::Minima(x, y)),
c if c < T::zero() => points.push(CriticalPoint::Maxima(x, y)),
_ => points.push(CriticalPoint::Inflection(x, y)),
}
}
Ok(points)
}
pub fn roots(&self, x_range: RangeInclusive<T>) -> Result<Vec<Root<T>>>
where
B: RootFindingBasis<T>,
{
self.basis.roots(self.coefficients(), x_range)
}
pub fn root_finding_method(&self) -> RootFindingMethod
where
B: RootFindingBasis<T>,
{
self.basis.root_finding_method()
}
pub fn iterative_roots(&self, x_range: RangeInclusive<T>) -> Result<Vec<Root<T>>>
where
B: RootFindingBasis<T>,
{
let default_max_iterations = B::DEFAULT_ROOT_FINDING_MAX_ITERATIONS;
let leading_coef = self.leading_coefficient();
let coef_scalar = Value::min(Value::abs(leading_coef.log10()), T::one());
let precision_scalar = T::epsilon() / f64::EPSILON.clamped_cast::<T>();
let max = T::from_positive_int(default_max_iterations) * coef_scalar * precision_scalar;
let recommended_max = max.as_usize().unwrap_or(default_max_iterations);
let max_iterations = recommended_max.max(default_max_iterations);
self.basis.roots_iterative(
&self.coefficients,
x_range,
B::DEFAULT_ROOT_FINDING_SAMPLES,
max_iterations,
)
}
pub fn integral(&self, constant: Option<T>) -> Result<Polynomial<'static, B::B2, T>>
where
B: IntegralBasis<T>,
{
let constant = constant.unwrap_or(T::zero());
let new_degree = self.degree + 1;
let (ib, ic) = self.basis.integral(&self.coefficients, constant)?;
let integral = unsafe { Polynomial::from_raw(ib, ic.into(), new_degree) };
Ok(integral)
}
pub fn area_under_curve(&self, x_min: T, x_max: T, constant: Option<T>) -> Result<T>
where
B: IntegralBasis<T>,
{
let integral = self.integral(constant)?;
Ok(integral.y(x_max) - integral.y(x_min))
}
pub fn iterative_monotonicity_violations(&self, x_range: RangeInclusive<T>) -> Result<Vec<T>>
where
B: DifferentialBasis<T>,
B::B2: RootFindingBasis<T>,
{
self.monotonicity_violations_from_roots(x_range, |f, x| {
f.iterative_roots(x).map(|roots| {
roots
.into_iter()
.filter_map(|r| if let Root::Real(x) = r { Some(x) } else { None })
.collect()
})
})
}
pub fn monotonicity_violations(&self, x_range: RangeInclusive<T>) -> Result<Vec<T>>
where
B: DifferentialBasis<T>,
B::B2: RootFindingBasis<T>,
{
self.monotonicity_violations_from_roots(x_range, |f, x| {
f.roots(x).map(|roots| {
roots
.into_iter()
.filter_map(|r| if let Root::Real(x) = r { Some(x) } else { None })
.collect()
})
})
}
fn monotonicity_violations_from_roots<F>(
&self,
x_range: RangeInclusive<T>,
root_finder: F,
) -> Result<Vec<T>>
where
F: Fn(&Polynomial<B::B2, T>, RangeInclusive<T>) -> Result<Vec<T>>,
B: DifferentialBasis<T>,
{
let dx = self.derivative()?;
let mut roots = root_finder(&dx, x_range.clone())?;
roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
if roots.is_empty() {
return Ok(vec![]);
}
let tol = T::epsilon().sqrt();
let mut violated_at = vec![];
for x in roots {
let tol = tol * (T::one() + Value::abs(x));
let left = dx.y(x - tol);
let right = dx.y(x + tol);
let left_sign = left.f_signum();
let right_sign = right.f_signum();
if left_sign == T::zero() && right_sign == T::zero() {
continue;
}
if left_sign == T::zero() || right_sign == T::zero() {
if left * right < T::zero() {
violated_at.push(x);
}
continue;
}
if left_sign != right_sign {
violated_at.push(x);
}
}
Ok(violated_at)
}
pub fn as_monomial(&self) -> Result<MonomialPolynomial<'static, T>>
where
B: IntoMonomialBasis<T>,
{
let mut coefficients = self.coefficients().to_vec();
self.basis().as_monomial(&mut coefficients)?;
Ok(MonomialPolynomial::owned(coefficients))
}
pub fn project<B2: Basis<T> + PolynomialDisplay<T>>(
&self,
x_range: RangeInclusive<T>,
) -> Result<Polynomial<'static, B2, T>> {
let samples = self.coefficients.len() * 15; let step = (*x_range.end() - *x_range.start()) / T::try_cast(samples)?;
let points = self.solve_range(x_range, step);
let fit = crate::CurveFit::<B2, T>::new(&points, self.degree())?;
Ok(fit.into_polynomial())
}
pub fn project_orthogonal<B2>(
&self,
x_range: RangeInclusive<T>,
target_degree: usize,
) -> Result<Polynomial<'static, B2, T>>
where
B2: Basis<T> + PolynomialDisplay<T> + OrthogonalBasis<T>,
{
let b2 = B2::from_range(x_range.clone());
let k = b2.k(target_degree);
let mut coeffs = vec![T::zero(); k];
let nodes = b2.gauss_nodes(k);
for i in 0..k {
let mut numerator = T::zero();
let mut denominator = T::zero();
for j in 0..k {
let x2 = b2.denormalize_x(nodes[j].0);
let w_j = nodes[j].1;
let y_i = self.y(x2); let phi_i = b2.solve_function(i, nodes[j].0);
numerator += w_j * y_i * phi_i;
denominator += w_j * phi_i * phi_i;
}
coeffs[i] = numerator / denominator;
}
let poly = unsafe { Polynomial::from_raw(b2, coeffs.into(), target_degree) };
Ok(poly)
}
pub fn coefficient_energies(&self) -> Vec<T>
where
B: OrthogonalBasis<T>,
{
self.coefficients()
.iter()
.enumerate()
.map(|(degree, &c)| {
let norm = self.basis.gauss_normalization(degree);
c * c * norm
})
.collect()
}
pub fn smoothness(&self) -> T
where
B: OrthogonalBasis<T>,
{
let energies = self.coefficient_energies();
let mut smoothness = T::zero();
let mut total_energy = T::zero();
for (degree, &energy) in energies.iter().enumerate() {
let k = T::from_positive_int(degree);
smoothness += k * k * energy;
total_energy += energy;
}
if total_energy.is_near_zero() {
return T::zero();
}
smoothness / total_energy
}
pub fn spectral_energy_filter(&mut self)
where
B: OrthogonalBasis<T>,
{
let n = self.coefficients().len();
let energies = self.coefficient_energies();
let mut total_energy = T::zero();
for &e in &energies {
total_energy += e;
}
if total_energy.is_near_zero() || n <= 1 {
return; }
let mut suffix = vec![T::zero(); n + 1];
for i in (0..n).rev() {
suffix[i] = suffix[i + 1] + energies[i];
}
let mut best_score = T::infinity();
let mut best_k = None;
for k in 1..n {
let total = suffix[0] - suffix[k];
let kt = T::from_positive_int(k);
let score = total / (kt * kt);
if score < best_score {
best_k = Some(k);
best_score = score;
}
}
let Some(k_keep) = best_k else {
return;
};
let m = T::from_positive_int(k_keep + 1);
for k in 1..=k_keep.saturating_sub(1) {
let x = T::from_positive_int(k) / m;
let sinc = if x.is_near_zero() {
T::one()
} else {
(T::pi() * x).sin() / (T::pi() * x)
};
self.coefficients_mut()[k] = self.coefficients()[k] * sinc;
}
for i in (k_keep + 1)..n {
self.coefficients_mut()[i] = T::zero();
}
}
#[expect(clippy::missing_panics_doc, reason = "Infallible operation")]
#[must_use]
pub fn equation(&self) -> String {
let mut output = String::new();
self.basis
.format_polynomial(&mut output, self.coefficients())
.expect("String should be infallible");
output
}
}
impl<'a, B, T: Value> AsRef<Polynomial<'a, B, T>> for Polynomial<'a, B, T>
where
B: Basis<T>,
B: PolynomialDisplay<T>,
{
fn as_ref(&self) -> &Polynomial<'a, B, T> {
self
}
}
impl<B, T: Value> std::fmt::Display for Polynomial<'_, B, T>
where
B: Basis<T>,
B: PolynomialDisplay<T>,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.equation())
}
}
impl<B: Basis<T> + PolynomialDisplay<T>, T: Value> std::ops::Mul<T> for Polynomial<'_, B, T> {
type Output = Polynomial<'static, B, T>;
fn mul(self, rhs: T) -> Self::Output {
let mut result = self.clone().into_owned();
result.scale(rhs);
result
}
}
impl<B: Basis<T> + PolynomialDisplay<T>, T: Value> std::ops::MulAssign<T> for Polynomial<'_, B, T> {
fn mul_assign(&mut self, rhs: T) {
self.scale(rhs);
}
}
impl<B: Basis<T> + PolynomialDisplay<T>, T: Value> std::ops::Div<T> for Polynomial<'_, B, T> {
type Output = Polynomial<'static, B, T>;
fn div(self, rhs: T) -> Self::Output {
let mut result = self.clone().into_owned();
result.scale(T::one() / rhs);
result
}
}
impl<B: Basis<T> + PolynomialDisplay<T>, T: Value> std::ops::DivAssign<T> for Polynomial<'_, B, T> {
fn div_assign(&mut self, rhs: T) {
self.scale(T::one() / rhs);
}
}
#[cfg(test)]
mod tests {
use crate::{assert_all_close, assert_close, assert_y};
use super::*;
#[test]
fn test_y() {
function!(test(x) = 8.0 + 7.0 x^1 + 6.0 x^2);
assert_y!(&test, 0.0, 8.0);
assert_y!(&test, 1.0, 21.0);
assert_y!(&test, 2.0, 46.0);
}
#[test]
fn test_solve() {
function!(test(x) = 8.0 + 7.0 x^1 + 6.0 x^2);
let points: Vec<_> = test.solve(vec![0.0, 1.0, 2.0]).y();
assert_all_close!(points, &[8.0, 21.0, 46.0]);
}
#[test]
fn test_solve_range() {
function!(test(x) = 8.0 + 7.0 x^1 + 6.0 x^2);
let points = test.solve_range(0.0..=3.0, 1.0).y();
assert_all_close!(points, &[8.0, 21.0, 46.0, 83.0]);
}
#[test]
fn test_area_under_curve() {
function!(test(x) = 8.0 + 7.0 x^1 + 6.0 x^2);
let area = test
.area_under_curve(0.0, 3.0, None)
.expect("Failed to compute area");
assert_close!(area, 109.5);
}
}