#![cfg(feature = "piecewise-polynomial")]
use alloc::vec::Vec;
use core::convert::TryFrom;
use core::ops::RangeInclusive;
use slatec_sys::FortranInteger;
use crate::runtime::{lock_native, permit_recoverable_native_statuses};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PiecewisePolynomialError {
TooFewBreakpoints,
InvalidOrder,
NonFiniteInput,
BreakpointsNotStrictlyIncreasing {
index: usize,
},
CoefficientCountMismatch {
expected: usize,
actual: usize,
},
OutOfDomain,
DerivativeOrderTooHigh {
requested: usize,
maximum: usize,
},
OutputLengthMismatch {
expected: usize,
actual: usize,
},
DimensionOverflow,
AllocationFailed,
NativeContractViolation {
detail: &'static str,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Extrapolation {
Error,
Clamp,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PiecewisePolynomial<T> {
breakpoints: Vec<T>,
coefficients: Vec<T>,
order: usize,
}
impl<T> PiecewisePolynomial<T> {
#[must_use]
pub const fn order(&self) -> usize {
self.order
}
#[must_use]
pub const fn degree(&self) -> usize {
self.order - 1
}
#[must_use]
pub fn piece_count(&self) -> usize {
self.breakpoints.len() - 1
}
#[must_use]
pub fn breakpoints(&self) -> &[T] {
&self.breakpoints
}
#[must_use]
pub fn coefficients_for_piece(&self, index: usize) -> Option<&[T]> {
let start = index.checked_mul(self.order)?;
let end = start.checked_add(self.order)?;
self.coefficients.get(start..end)
}
#[must_use]
pub fn domain(&self) -> RangeInclusive<T>
where
T: Copy,
{
self.breakpoints[0]..=self.breakpoints[self.piece_count()]
}
}
mod sealed {
pub trait Sealed {}
impl Sealed for f32 {}
impl Sealed for f64 {}
}
trait PpScalar: sealed::Sealed + Copy + Default + PartialOrd {
fn finite(self) -> bool;
unsafe fn evaluate_native(
leading_dimension: *const FortranInteger,
coefficients: *const Self,
breakpoints: *const Self,
piece_count: *const FortranInteger,
order: *const FortranInteger,
derivative_order: *const FortranInteger,
point: *const Self,
interval_state: *mut FortranInteger,
) -> Self;
unsafe fn integrate_native(
leading_dimension: *const FortranInteger,
coefficients: *const Self,
breakpoints: *const Self,
piece_count: *const FortranInteger,
order: *const FortranInteger,
lower: *const Self,
upper: *const Self,
integral: *mut Self,
);
#[cfg(feature = "bspline")]
unsafe fn convert_bspline_native(
knots: *const Self,
coefficients: *const Self,
coefficient_count: *const FortranInteger,
order: *const FortranInteger,
leading_dimension: *const FortranInteger,
pp_coefficients: *mut Self,
breakpoints: *mut Self,
piece_count: *mut FortranInteger,
workspace: *mut Self,
);
}
impl PpScalar for f32 {
fn finite(self) -> bool {
self.is_finite()
}
unsafe fn evaluate_native(
leading_dimension: *const FortranInteger,
coefficients: *const Self,
breakpoints: *const Self,
piece_count: *const FortranInteger,
order: *const FortranInteger,
derivative_order: *const FortranInteger,
point: *const Self,
interval_state: *mut FortranInteger,
) -> Self {
unsafe {
slatec_sys::piecewise_polynomial::ppval(
leading_dimension,
coefficients,
breakpoints,
piece_count,
order,
derivative_order,
point,
interval_state,
)
}
}
unsafe fn integrate_native(
leading_dimension: *const FortranInteger,
coefficients: *const Self,
breakpoints: *const Self,
piece_count: *const FortranInteger,
order: *const FortranInteger,
lower: *const Self,
upper: *const Self,
integral: *mut Self,
) {
unsafe {
slatec_sys::piecewise_polynomial::ppqad(
leading_dimension,
coefficients,
breakpoints,
piece_count,
order,
lower,
upper,
integral,
)
}
}
#[cfg(feature = "bspline")]
unsafe fn convert_bspline_native(
knots: *const Self,
coefficients: *const Self,
coefficient_count: *const FortranInteger,
order: *const FortranInteger,
leading_dimension: *const FortranInteger,
pp_coefficients: *mut Self,
breakpoints: *mut Self,
piece_count: *mut FortranInteger,
workspace: *mut Self,
) {
unsafe {
slatec_sys::piecewise_polynomial::bsppp(
knots,
coefficients,
coefficient_count,
order,
leading_dimension,
pp_coefficients,
breakpoints,
piece_count,
workspace,
)
}
}
}
impl PpScalar for f64 {
fn finite(self) -> bool {
self.is_finite()
}
unsafe fn evaluate_native(
leading_dimension: *const FortranInteger,
coefficients: *const Self,
breakpoints: *const Self,
piece_count: *const FortranInteger,
order: *const FortranInteger,
derivative_order: *const FortranInteger,
point: *const Self,
interval_state: *mut FortranInteger,
) -> Self {
unsafe {
slatec_sys::piecewise_polynomial::dppval(
leading_dimension,
coefficients,
breakpoints,
piece_count,
order,
derivative_order,
point,
interval_state,
)
}
}
unsafe fn integrate_native(
leading_dimension: *const FortranInteger,
coefficients: *const Self,
breakpoints: *const Self,
piece_count: *const FortranInteger,
order: *const FortranInteger,
lower: *const Self,
upper: *const Self,
integral: *mut Self,
) {
unsafe {
slatec_sys::piecewise_polynomial::dppqad(
leading_dimension,
coefficients,
breakpoints,
piece_count,
order,
lower,
upper,
integral,
)
}
}
#[cfg(feature = "bspline")]
unsafe fn convert_bspline_native(
knots: *const Self,
coefficients: *const Self,
coefficient_count: *const FortranInteger,
order: *const FortranInteger,
leading_dimension: *const FortranInteger,
pp_coefficients: *mut Self,
breakpoints: *mut Self,
piece_count: *mut FortranInteger,
workspace: *mut Self,
) {
unsafe {
slatec_sys::piecewise_polynomial::dbsppp(
knots,
coefficients,
coefficient_count,
order,
leading_dimension,
pp_coefficients,
breakpoints,
piece_count,
workspace,
)
}
}
}
#[allow(private_bounds)]
impl<T: PpScalar> PiecewisePolynomial<T> {
fn from_parts_impl(
breakpoints: Vec<T>,
coefficients: Vec<T>,
order: usize,
) -> Result<Self, PiecewisePolynomialError> {
validate_parts(&breakpoints, &coefficients, order)?;
Ok(Self {
breakpoints,
coefficients,
order,
})
}
fn evaluate_impl(&self, point: T) -> Result<T, PiecewisePolynomialError> {
self.evaluate_with_extrapolation_impl(point, Extrapolation::Error)
}
fn evaluate_with_extrapolation_impl(
&self,
point: T,
extrapolation: Extrapolation,
) -> Result<T, PiecewisePolynomialError> {
self.derivative_with_extrapolation_impl(point, 0, extrapolation)
}
fn derivative_impl(
&self,
point: T,
derivative_order: usize,
) -> Result<T, PiecewisePolynomialError> {
self.derivative_with_extrapolation_impl(point, derivative_order, Extrapolation::Error)
}
fn derivative_with_extrapolation_impl(
&self,
point: T,
derivative_order: usize,
extrapolation: Extrapolation,
) -> Result<T, PiecewisePolynomialError> {
if derivative_order >= self.order {
return Err(PiecewisePolynomialError::DerivativeOrderTooHigh {
requested: derivative_order,
maximum: self.order - 1,
});
}
let point = self.validate_domain(point, extrapolation)?;
let leading_dimension = native_len(self.order)?;
let piece_count = native_len(self.piece_count())?;
let order = native_len(self.order)?;
let derivative_order = native_len(derivative_order)?;
let mut interval_state = 1;
let _native = lock_native();
let _xerror = permit_recoverable_native_statuses();
let value = unsafe {
T::evaluate_native(
&leading_dimension,
self.coefficients.as_ptr(),
self.breakpoints.as_ptr(),
&piece_count,
&order,
&derivative_order,
&point,
&mut interval_state,
)
};
if interval_state < 1
|| usize::try_from(interval_state)
.ok()
.is_some_and(|index| index > self.piece_count())
{
return Err(PiecewisePolynomialError::NativeContractViolation {
detail: "PPVAL returned an invalid interval-search state",
});
}
Ok(value)
}
fn evaluate_into_impl(
&self,
points: &[T],
output: &mut [T],
extrapolation: Extrapolation,
) -> Result<(), PiecewisePolynomialError> {
if points.len() != output.len() {
return Err(PiecewisePolynomialError::OutputLengthMismatch {
expected: points.len(),
actual: output.len(),
});
}
native_len(points.len())?;
for (&point, value) in points.iter().zip(output.iter_mut()) {
*value = self.evaluate_with_extrapolation_impl(point, extrapolation)?;
}
Ok(())
}
fn integrate_impl(
&self,
lower: T,
upper: T,
extrapolation: Extrapolation,
) -> Result<T, PiecewisePolynomialError> {
let lower = self.validate_domain(lower, extrapolation)?;
let upper = self.validate_domain(upper, extrapolation)?;
let leading_dimension = native_len(self.order)?;
let piece_count = native_len(self.piece_count())?;
let order = native_len(self.order)?;
let mut integral = T::default();
let _native = lock_native();
let _xerror = permit_recoverable_native_statuses();
unsafe {
T::integrate_native(
&leading_dimension,
self.coefficients.as_ptr(),
self.breakpoints.as_ptr(),
&piece_count,
&order,
&lower,
&upper,
&mut integral,
)
};
Ok(integral)
}
fn validate_domain(
&self,
point: T,
extrapolation: Extrapolation,
) -> Result<T, PiecewisePolynomialError> {
if !point.finite() {
return Err(PiecewisePolynomialError::NonFiniteInput);
}
let first = self.breakpoints[0];
let last = self.breakpoints[self.piece_count()];
if point < first {
return match extrapolation {
Extrapolation::Error => Err(PiecewisePolynomialError::OutOfDomain),
Extrapolation::Clamp => Ok(first),
};
}
if point > last {
return match extrapolation {
Extrapolation::Error => Err(PiecewisePolynomialError::OutOfDomain),
Extrapolation::Clamp => Ok(last),
};
}
Ok(point)
}
#[cfg(feature = "bspline")]
fn from_bspline_parts(
knots: &[T],
coefficients: &[T],
order: usize,
) -> Result<Self, PiecewisePolynomialError> {
let coefficient_count = coefficients.len();
let maximum_pieces = coefficient_count
.checked_sub(order)
.and_then(|value| value.checked_add(1))
.ok_or(PiecewisePolynomialError::DimensionOverflow)?;
let breakpoint_capacity = maximum_pieces
.checked_add(1)
.ok_or(PiecewisePolynomialError::DimensionOverflow)?;
let coefficient_capacity = order
.checked_mul(maximum_pieces)
.ok_or(PiecewisePolynomialError::DimensionOverflow)?;
let workspace_capacity = order
.checked_mul(
coefficient_count
.checked_add(3)
.ok_or(PiecewisePolynomialError::DimensionOverflow)?,
)
.ok_or(PiecewisePolynomialError::DimensionOverflow)?;
let mut pp_coefficients = zeroed::<T>(coefficient_capacity)?;
let mut breakpoints = zeroed::<T>(breakpoint_capacity)?;
let mut workspace = zeroed::<T>(workspace_capacity)?;
let coefficient_count_native = native_len(coefficient_count)?;
let order_native = native_len(order)?;
let leading_dimension = order_native;
let mut piece_count = 0;
let _native = lock_native();
let _xerror = permit_recoverable_native_statuses();
unsafe {
T::convert_bspline_native(
knots.as_ptr(),
coefficients.as_ptr(),
&coefficient_count_native,
&order_native,
&leading_dimension,
pp_coefficients.as_mut_ptr(),
breakpoints.as_mut_ptr(),
&mut piece_count,
workspace.as_mut_ptr(),
)
};
let piece_count = usize::try_from(piece_count).map_err(|_| {
PiecewisePolynomialError::NativeContractViolation {
detail: "BSPPP returned a negative piece count",
}
})?;
if piece_count == 0 || piece_count > maximum_pieces {
return Err(PiecewisePolynomialError::NativeContractViolation {
detail: "BSPPP returned an out-of-range piece count",
});
}
let used_coefficients = order.checked_mul(piece_count).ok_or(
PiecewisePolynomialError::NativeContractViolation {
detail: "BSPPP result dimensions overflowed",
},
)?;
let used_breakpoints = piece_count.checked_add(1).ok_or(
PiecewisePolynomialError::NativeContractViolation {
detail: "BSPPP breakpoint count overflowed",
},
)?;
pp_coefficients.truncate(used_coefficients);
breakpoints.truncate(used_breakpoints);
Self::from_parts_impl(breakpoints, pp_coefficients, order).map_err(|_| {
PiecewisePolynomialError::NativeContractViolation {
detail: "BSPPP returned an invalid PP representation",
}
})
}
}
macro_rules! impl_public_precision {
($scalar:ty) => {
impl PiecewisePolynomial<$scalar> {
pub fn from_parts(
breakpoints: Vec<$scalar>,
coefficients: Vec<$scalar>,
order: usize,
) -> Result<Self, PiecewisePolynomialError> {
Self::from_parts_impl(breakpoints, coefficients, order)
}
pub fn evaluate(&self, point: $scalar) -> Result<$scalar, PiecewisePolynomialError> {
self.evaluate_impl(point)
}
pub fn evaluate_with_extrapolation(
&self,
point: $scalar,
extrapolation: Extrapolation,
) -> Result<$scalar, PiecewisePolynomialError> {
self.evaluate_with_extrapolation_impl(point, extrapolation)
}
pub fn derivative(
&self,
point: $scalar,
derivative_order: usize,
) -> Result<$scalar, PiecewisePolynomialError> {
self.derivative_impl(point, derivative_order)
}
pub fn derivative_with_extrapolation(
&self,
point: $scalar,
derivative_order: usize,
extrapolation: Extrapolation,
) -> Result<$scalar, PiecewisePolynomialError> {
self.derivative_with_extrapolation_impl(point, derivative_order, extrapolation)
}
pub fn evaluate_into(
&self,
points: &[$scalar],
output: &mut [$scalar],
) -> Result<(), PiecewisePolynomialError> {
self.evaluate_into_impl(points, output, Extrapolation::Error)
}
pub fn evaluate_into_with_extrapolation(
&self,
points: &[$scalar],
output: &mut [$scalar],
extrapolation: Extrapolation,
) -> Result<(), PiecewisePolynomialError> {
self.evaluate_into_impl(points, output, extrapolation)
}
pub fn integrate(
&self,
lower: $scalar,
upper: $scalar,
) -> Result<$scalar, PiecewisePolynomialError> {
self.integrate_impl(lower, upper, Extrapolation::Error)
}
pub fn integrate_with_extrapolation(
&self,
lower: $scalar,
upper: $scalar,
extrapolation: Extrapolation,
) -> Result<$scalar, PiecewisePolynomialError> {
self.integrate_impl(lower, upper, extrapolation)
}
}
};
}
impl_public_precision!(f32);
impl_public_precision!(f64);
#[cfg(feature = "bspline")]
macro_rules! impl_bspline_conversion {
($scalar:ty) => {
impl crate::bspline::BSpline<$scalar> {
pub fn to_piecewise_polynomial(
&self,
) -> Result<PiecewisePolynomial<$scalar>, PiecewisePolynomialError> {
let (knots, coefficients, order) = self.native_parts();
PiecewisePolynomial::from_bspline_parts(knots, coefficients, order)
}
}
};
}
#[cfg(feature = "bspline")]
impl_bspline_conversion!(f32);
#[cfg(feature = "bspline")]
impl_bspline_conversion!(f64);
fn validate_parts<T: PpScalar>(
breakpoints: &[T],
coefficients: &[T],
order: usize,
) -> Result<(), PiecewisePolynomialError> {
if order == 0 || native_len(order).is_err() {
return Err(PiecewisePolynomialError::InvalidOrder);
}
if breakpoints.len() < 2 {
return Err(PiecewisePolynomialError::TooFewBreakpoints);
}
let piece_count = breakpoints.len() - 1;
native_len(piece_count)?;
let expected = order
.checked_mul(piece_count)
.ok_or(PiecewisePolynomialError::DimensionOverflow)?;
if coefficients.len() != expected {
return Err(PiecewisePolynomialError::CoefficientCountMismatch {
expected,
actual: coefficients.len(),
});
}
for (index, &breakpoint) in breakpoints.iter().enumerate() {
if !breakpoint.finite() {
return Err(PiecewisePolynomialError::NonFiniteInput);
}
if index > 0 && !(breakpoint > breakpoints[index - 1]) {
return Err(PiecewisePolynomialError::BreakpointsNotStrictlyIncreasing { index });
}
}
if coefficients
.iter()
.any(|&coefficient| !coefficient.finite())
{
return Err(PiecewisePolynomialError::NonFiniteInput);
}
Ok(())
}
fn native_len(length: usize) -> Result<FortranInteger, PiecewisePolynomialError> {
FortranInteger::try_from(length).map_err(|_| PiecewisePolynomialError::DimensionOverflow)
}
#[cfg(feature = "bspline")]
fn zeroed<T: Copy + Default>(length: usize) -> Result<Vec<T>, PiecewisePolynomialError> {
let mut values = Vec::new();
values
.try_reserve_exact(length)
.map_err(|_| PiecewisePolynomialError::AllocationFailed)?;
values.resize(length, T::default());
Ok(values)
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
#[test]
fn validates_native_pp_storage_before_entry() {
assert_eq!(
PiecewisePolynomial::<f64>::from_parts(vec![0.0], vec![], 1),
Err(PiecewisePolynomialError::TooFewBreakpoints)
);
assert_eq!(
PiecewisePolynomial::<f64>::from_parts(vec![0.0, 1.0], vec![1.0], 0),
Err(PiecewisePolynomialError::InvalidOrder)
);
assert_eq!(
PiecewisePolynomial::<f64>::from_parts(vec![0.0, 0.0], vec![1.0], 1),
Err(PiecewisePolynomialError::BreakpointsNotStrictlyIncreasing { index: 1 })
);
}
}