use alloc::vec::Vec;
use crate::approx::error_estimation;
use crate::approx::fit;
use crate::core::{ChebyError, ChebyScalar, ChebySeries, ChebyTime, DifferentiateWith, Domain};
use crate::piecewise::ChebySegment;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SegmentMetadata<T> {
pub degree: usize,
pub estimated_error: Option<T>,
pub samples_used: usize,
pub depth: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AdaptiveSegmentTable<T, X, const N: usize> {
domain: Domain<X>,
segments: Vec<ChebySegment<T, X, N>>,
metadata: Vec<SegmentMetadata<f64>>,
boundaries: Vec<X>,
}
impl<T, X, const N: usize> AdaptiveSegmentTable<T, X, N>
where
T: ChebyScalar,
X: ChebyTime,
{
pub fn from_fn(
domain: Domain<X>,
f: impl Fn(X) -> T + Copy,
tolerance: f64,
max_depth: usize,
) -> Result<Self, ChebyError> {
if N < 2 {
return Err(ChebyError::InvalidDegree);
}
if !(tolerance.is_finite() && tolerance >= 0.0) {
return Err(ChebyError::NonFiniteInput);
}
let mut segments = Vec::new();
let mut metadata = Vec::new();
split::<T, X, N>(
domain,
f,
tolerance,
max_depth,
0,
&mut segments,
&mut metadata,
)?;
if segments.is_empty() {
return Err(ChebyError::EmptySegmentTable);
}
let mut boundaries = Vec::with_capacity(segments.len() + 1);
boundaries.push(segments[0].domain().start());
for (i, seg) in segments.iter().enumerate() {
if i > 0 && seg.domain().start() != segments[i - 1].domain().end() {
return Err(ChebyError::SegmentBoundariesNotContiguous);
}
boundaries.push(seg.domain().end());
}
Ok(Self {
domain,
segments,
metadata,
boundaries,
})
}
#[inline]
pub fn domain(&self) -> Domain<X> {
self.domain
}
#[inline]
pub fn len(&self) -> usize {
self.segments.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
#[inline]
pub fn segments(&self) -> &[ChebySegment<T, X, N>] {
&self.segments
}
#[inline]
pub fn metadata(&self) -> &[SegmentMetadata<f64>] {
&self.metadata
}
#[inline]
pub fn boundaries(&self) -> &[X] {
&self.boundaries
}
pub fn locate(&self, x: X) -> Option<&ChebySegment<T, X, N>> {
if self.segments.is_empty() {
return None;
}
if x < self.boundaries[0] || x > self.boundaries[self.boundaries.len() - 1] {
return None;
}
let probe = self
.boundaries
.binary_search_by(|b| b.partial_cmp(&x).unwrap_or(core::cmp::Ordering::Less));
let idx = match probe {
Ok(i) => {
if i == self.segments.len() {
self.segments.len() - 1
} else {
i
}
}
Err(i) => {
if i == 0 {
0
} else {
i - 1
}
}
};
self.segments.get(idx).filter(|s| s.contains(x))
}
pub fn evaluate(&self, x: X) -> Result<T, ChebyError> {
self.locate(x)
.ok_or(ChebyError::EvaluationOutOfDomain)?
.evaluate(x)
}
pub fn evaluate_derivative(
&self,
x: X,
) -> Result<<T as DifferentiateWith<X>>::Derivative, ChebyError>
where
T: DifferentiateWith<X>,
{
self.locate(x)
.ok_or(ChebyError::EvaluationOutOfDomain)?
.evaluate_derivative(x)
}
}
#[allow(clippy::too_many_arguments)]
fn split<T, X, const N: usize>(
domain: Domain<X>,
f: impl Fn(X) -> T + Copy,
tolerance: f64,
depth_remaining: usize,
depth_so_far: usize,
segments: &mut Vec<ChebySegment<T, X, N>>,
metadata: &mut Vec<SegmentMetadata<f64>>,
) -> Result<(), ChebyError>
where
T: ChebyScalar,
X: ChebyTime,
{
let fitted = fit::fit_from_fn_on::<T, X, N>(domain, f);
let coeffs = fitted.into_coeffs();
let err = error_estimation::estimated_truncation_error(&coeffs);
if err <= tolerance || depth_remaining == 0 {
segments.push(ChebySegment::try_new(domain, ChebySeries::new(coeffs))?);
metadata.push(SegmentMetadata {
degree: N,
estimated_error: Some(err),
samples_used: N,
depth: depth_so_far,
});
return Ok(());
}
let mid = domain.midpoint();
split(
Domain::try_new(domain.start(), mid)?,
f,
tolerance,
depth_remaining - 1,
depth_so_far + 1,
segments,
metadata,
)?;
split(
Domain::try_new(mid, domain.end())?,
f,
tolerance,
depth_remaining - 1,
depth_so_far + 1,
segments,
metadata,
)
}