#[cfg(not(feature = "std"))]
use alloc::{boxed::Box, vec::Vec};
use crate::Scalar;
pub trait Signal<S: Scalar>: Send + Sync {
fn eval(&self, t: S) -> S;
fn eval_derivative(&self, t: S) -> S {
let h = S::EPSILON.cbrt() * (S::ONE + t.abs());
(self.eval(t + h) - self.eval(t - h)) / (S::TWO * h)
}
}
#[derive(Clone, Debug)]
pub struct Harmonic<S: Scalar> {
pub amplitude: S,
pub frequency: S,
pub phase: S,
}
impl<S: Scalar> Harmonic<S> {
pub fn new(amplitude: S, frequency: S, phase: S) -> Self {
Self {
amplitude,
frequency,
phase,
}
}
}
impl<S: Scalar> Signal<S> for Harmonic<S> {
#[inline]
fn eval(&self, t: S) -> S {
let omega_t = S::TWO * S::PI * self.frequency * t + self.phase;
self.amplitude * omega_t.sin()
}
#[inline]
fn eval_derivative(&self, t: S) -> S {
let omega = S::TWO * S::PI * self.frequency;
let omega_t = omega * t + self.phase;
self.amplitude * omega * omega_t.cos()
}
}
#[derive(Clone, Debug)]
pub struct Step<S: Scalar> {
pub magnitude: S,
pub time: S,
pub smoothing: Option<S>,
}
impl<S: Scalar> Step<S> {
pub fn new(magnitude: S, time: S) -> Self {
Self {
magnitude,
time,
smoothing: None,
}
}
pub fn smooth(magnitude: S, time: S, smoothing: S) -> Self {
Self {
magnitude,
time,
smoothing: Some(smoothing),
}
}
}
impl<S: Scalar> Signal<S> for Step<S> {
fn eval(&self, t: S) -> S {
match self.smoothing {
None => {
if t >= self.time {
self.magnitude
} else {
S::ZERO
}
}
Some(k) => {
let x = (t - self.time) / k;
self.magnitude * S::HALF * (S::ONE + x.tanh())
}
}
}
}
#[derive(Clone, Debug)]
pub struct Ramp<S: Scalar> {
pub start_value: S,
pub end_value: S,
pub start_time: S,
pub end_time: S,
}
impl<S: Scalar> Ramp<S> {
pub fn new(start_value: S, end_value: S, start_time: S, end_time: S) -> Self {
Self {
start_value,
end_value,
start_time,
end_time,
}
}
pub fn from_rate(rate: S, start_time: S, end_time: S) -> Self {
let duration = end_time - start_time;
Self {
start_value: S::ZERO,
end_value: rate * duration,
start_time,
end_time,
}
}
}
impl<S: Scalar> Signal<S> for Ramp<S> {
fn eval(&self, t: S) -> S {
if t <= self.start_time {
self.start_value
} else if t >= self.end_time {
self.end_value
} else {
let alpha = (t - self.start_time) / (self.end_time - self.start_time);
self.start_value + alpha * (self.end_value - self.start_value)
}
}
fn eval_derivative(&self, t: S) -> S {
if t > self.start_time && t < self.end_time {
(self.end_value - self.start_value) / (self.end_time - self.start_time)
} else {
S::ZERO
}
}
}
#[derive(Clone, Debug)]
pub struct Pulse<S: Scalar> {
pub magnitude: S,
pub start: S,
pub duration: S,
}
impl<S: Scalar> Pulse<S> {
pub fn new(magnitude: S, start: S, duration: S) -> Self {
Self {
magnitude,
start,
duration,
}
}
}
impl<S: Scalar> Signal<S> for Pulse<S> {
fn eval(&self, t: S) -> S {
if t >= self.start && t <= self.start + self.duration {
self.magnitude
} else {
S::ZERO
}
}
}
#[derive(Clone, Debug)]
pub struct Chirp<S: Scalar> {
pub amplitude: S,
pub f0: S, pub f1: S, pub t1: S, }
impl<S: Scalar> Chirp<S> {
pub fn new(amplitude: S, f0: S, f1: S, t1: S) -> Self {
Self {
amplitude,
f0,
f1,
t1,
}
}
}
impl<S: Scalar> Signal<S> for Chirp<S> {
fn eval(&self, t: S) -> S {
if t < S::ZERO || t > self.t1 {
return S::ZERO;
}
let k = (self.f1 - self.f0) / self.t1;
let phase = S::TWO * S::PI * (self.f0 * t + S::HALF * k * t * t);
self.amplitude * phase.sin()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Interpolation {
#[default]
Linear,
Nearest,
Cubic,
}
#[derive(Clone, Debug)]
pub struct Tabulated<S: Scalar> {
pub times: Vec<S>,
pub values: Vec<S>,
pub interp: Interpolation,
spline_d2: Option<Vec<S>>,
}
impl<S: Scalar> Tabulated<S> {
pub fn new(times: Vec<S>, values: Vec<S>, interp: Interpolation) -> Self {
assert_eq!(
times.len(),
values.len(),
"times and values must have same length"
);
assert!(!times.is_empty(), "times must not be empty");
let spline_d2 = if interp == Interpolation::Cubic && times.len() >= 2 {
Some(Self::compute_spline_coefficients(×, &values))
} else {
None
};
Self {
times,
values,
interp,
spline_d2,
}
}
pub fn from_pairs(pairs: &[(S, S)], interp: Interpolation) -> Self {
let (times, values): (Vec<_>, Vec<_>) = pairs.iter().cloned().unzip();
Self::new(times, values, interp)
}
fn compute_spline_coefficients(times: &[S], values: &[S]) -> Vec<S> {
let n = times.len();
if n < 2 {
return vec![S::ZERO; n];
}
if n == 2 {
return vec![S::ZERO, S::ZERO];
}
let mut d2 = vec![S::ZERO; n];
let mut c_prime = vec![S::ZERO; n - 2]; let mut d_prime = vec![S::ZERO; n - 2];
let mut h = Vec::with_capacity(n - 1);
for i in 0..n - 1 {
h.push(times[i + 1] - times[i]);
}
for i in 1..n - 1 {
let h_prev = h[i - 1];
let h_curr = h[i];
let diag = S::TWO * (h_prev + h_curr);
let slope_curr = (values[i + 1] - values[i]) / h_curr;
let slope_prev = (values[i] - values[i - 1]) / h_prev;
let rhs = S::from_f64(6.0) * (slope_curr - slope_prev);
let idx = i - 1;
if idx == 0 {
c_prime[idx] = h_curr / diag;
d_prime[idx] = rhs / diag;
} else {
let m = diag - h_prev * c_prime[idx - 1];
c_prime[idx] = h_curr / m;
d_prime[idx] = (rhs - h_prev * d_prime[idx - 1]) / m;
}
}
let last_idx = n - 3;
d2[n - 2] = d_prime[last_idx];
for i in (1..n - 2).rev() {
let idx = i - 1;
d2[i] = d_prime[idx] - c_prime[idx] * d2[i + 1];
}
d2[0] = S::ZERO;
d2[n - 1] = S::ZERO;
d2
}
pub fn value_at(&self, t: S) -> S {
self.interpolate_at(t)
}
fn interpolate_at(&self, t: S) -> S {
let n = self.times.len();
if t <= self.times[0] {
return self.values[0];
}
if t >= self.times[n - 1] {
return self.values[n - 1];
}
let i = self.find_interval(t);
let t0 = self.times[i];
let t1 = self.times[i + 1];
let v0 = self.values[i];
let v1 = self.values[i + 1];
match self.interp {
Interpolation::Nearest => {
if t - t0 < t1 - t {
v0
} else {
v1
}
}
Interpolation::Linear => {
let alpha = (t - t0) / (t1 - t0);
v0 + alpha * (v1 - v0)
}
Interpolation::Cubic => self.cubic_spline_interp(t, i),
}
}
fn cubic_spline_interp(&self, t: S, i: usize) -> S {
let d2 = match &self.spline_d2 {
Some(d2) => d2,
None => {
let t0 = self.times[i];
let t1 = self.times[i + 1];
let alpha = (t - t0) / (t1 - t0);
return self.values[i] + alpha * (self.values[i + 1] - self.values[i]);
}
};
let t0 = self.times[i];
let t1 = self.times[i + 1];
let h = t1 - t0;
let y0 = self.values[i];
let y1 = self.values[i + 1];
let d2_0 = d2[i];
let d2_1 = d2[i + 1];
let u = (t - t0) / h;
let one_minus_u = S::ONE - u;
let u3 = u * u * u;
let omu3 = one_minus_u * one_minus_u * one_minus_u;
let h2_6 = h * h / S::from_f64(6.0);
one_minus_u * y0 + u * y1 + h2_6 * ((u3 - u) * d2_1 + (omu3 - one_minus_u) * d2_0)
}
fn find_interval(&self, t: S) -> usize {
let n = self.times.len();
if t <= self.times[0] {
return 0;
}
if t >= self.times[n - 1] {
return n - 2;
}
let mut lo = 0;
let mut hi = n - 1;
while hi - lo > 1 {
let mid = (lo + hi) / 2;
if t < self.times[mid] {
hi = mid;
} else {
lo = mid;
}
}
lo
}
}
impl<S: Scalar> Signal<S> for Tabulated<S> {
fn eval(&self, t: S) -> S {
self.interpolate_at(t)
}
}
#[cfg(feature = "std")]
#[derive(Clone, Debug)]
pub struct FromFile<S: Scalar> {
tabulated: Tabulated<S>,
path: std::string::String,
}
#[cfg(feature = "std")]
impl<S: Scalar + std::str::FromStr> FromFile<S> {
pub fn load<P: AsRef<std::path::Path>>(path: P) -> Result<Self, std::string::String> {
Self::load_with_interpolation(path, Interpolation::Linear)
}
pub fn load_with_interpolation<P: AsRef<std::path::Path>>(
path: P,
interp: Interpolation,
) -> Result<Self, std::string::String> {
use std::fs::File;
use std::io::{BufRead, BufReader};
let path_ref = path.as_ref();
let file = File::open(path_ref)
.map_err(|e| format!("Failed to open file '{}': {}", path_ref.display(), e))?;
let reader = BufReader::new(file);
let mut times = Vec::new();
let mut values = Vec::new();
for (line_num, line_result) in reader.lines().enumerate() {
let line =
line_result.map_err(|e| format!("Failed to read line {}: {}", line_num + 1, e))?;
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
if parts.len() < 2 {
return Err(format!(
"Line {} has fewer than 2 columns: '{}'",
line_num + 1,
line
));
}
let t: S = parts[0].parse().map_err(|_| {
format!(
"Failed to parse time on line {}: '{}'",
line_num + 1,
parts[0]
)
})?;
let v: S = parts[1].parse().map_err(|_| {
format!(
"Failed to parse value on line {}: '{}'",
line_num + 1,
parts[1]
)
})?;
times.push(t);
values.push(v);
}
if times.is_empty() {
return Err("No data found in file".to_string());
}
let tabulated = Tabulated::new(times, values, interp);
let path_string = path_ref.to_string_lossy().into_owned();
Ok(Self {
tabulated,
path: path_string,
})
}
pub fn from_csv_string(
content: &str,
interp: Interpolation,
) -> Result<Self, std::string::String> {
let mut times = Vec::new();
let mut values = Vec::new();
for (line_num, line) in content.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
if parts.len() < 2 {
return Err(format!(
"Line {} has fewer than 2 columns: '{}'",
line_num + 1,
line
));
}
let t: S = parts[0].parse().map_err(|_| {
format!(
"Failed to parse time on line {}: '{}'",
line_num + 1,
parts[0]
)
})?;
let v: S = parts[1].parse().map_err(|_| {
format!(
"Failed to parse value on line {}: '{}'",
line_num + 1,
parts[1]
)
})?;
times.push(t);
values.push(v);
}
if times.is_empty() {
return Err("No data found in content".to_string());
}
let tabulated = Tabulated::new(times, values, interp);
Ok(Self {
tabulated,
path: "<from_string>".to_string(),
})
}
pub fn len(&self) -> usize {
self.tabulated.times.len()
}
pub fn is_empty(&self) -> bool {
self.tabulated.times.is_empty()
}
pub fn time_range(&self) -> (S, S) {
let times = &self.tabulated.times;
(times[0], times[times.len() - 1])
}
pub fn path(&self) -> &str {
&self.path
}
pub fn as_tabulated(&self) -> &Tabulated<S> {
&self.tabulated
}
}
#[cfg(feature = "std")]
impl<S: Scalar + std::str::FromStr> Signal<S> for FromFile<S> {
fn eval(&self, t: S) -> S {
self.tabulated.eval(t)
}
fn eval_derivative(&self, t: S) -> S {
self.tabulated.eval_derivative(t)
}
}
#[derive(Clone, Debug)]
pub struct Piecewise<S: Scalar> {
segments: Vec<(S, S)>,
default: S,
}
impl<S: Scalar> Piecewise<S> {
pub fn new(segments: Vec<(S, S)>, default: S) -> Self {
Self { segments, default }
}
}
impl<S: Scalar> Signal<S> for Piecewise<S> {
fn eval(&self, t: S) -> S {
for &(end_time, value) in &self.segments {
if t < end_time {
return value;
}
}
self.default
}
}
#[derive(Clone, Debug)]
pub struct Sum<S: Scalar, A: Signal<S>, B: Signal<S>> {
pub a: A,
pub b: B,
_marker: core::marker::PhantomData<S>,
}
impl<S: Scalar, A: Signal<S>, B: Signal<S>> Sum<S, A, B> {
pub fn new(a: A, b: B) -> Self {
Self {
a,
b,
_marker: core::marker::PhantomData,
}
}
}
impl<S: Scalar, A: Signal<S>, B: Signal<S>> Signal<S> for Sum<S, A, B> {
#[inline]
fn eval(&self, t: S) -> S {
self.a.eval(t) + self.b.eval(t)
}
#[inline]
fn eval_derivative(&self, t: S) -> S {
self.a.eval_derivative(t) + self.b.eval_derivative(t)
}
}
#[derive(Clone, Debug)]
pub struct Product<S: Scalar, A: Signal<S>, B: Signal<S>> {
pub a: A,
pub b: B,
_marker: core::marker::PhantomData<S>,
}
impl<S: Scalar, A: Signal<S>, B: Signal<S>> Product<S, A, B> {
pub fn new(a: A, b: B) -> Self {
Self {
a,
b,
_marker: core::marker::PhantomData,
}
}
}
impl<S: Scalar, A: Signal<S>, B: Signal<S>> Signal<S> for Product<S, A, B> {
#[inline]
fn eval(&self, t: S) -> S {
self.a.eval(t) * self.b.eval(t)
}
#[inline]
fn eval_derivative(&self, t: S) -> S {
self.a.eval_derivative(t) * self.b.eval(t) + self.a.eval(t) * self.b.eval_derivative(t)
}
}
#[derive(Clone, Debug)]
pub struct Scaled<S: Scalar, Inner: Signal<S>> {
pub scale: S,
pub inner: Inner,
}
impl<S: Scalar, Inner: Signal<S>> Scaled<S, Inner> {
pub fn new(scale: S, inner: Inner) -> Self {
Self { scale, inner }
}
}
impl<S: Scalar, Inner: Signal<S>> Signal<S> for Scaled<S, Inner> {
#[inline]
fn eval(&self, t: S) -> S {
self.scale * self.inner.eval(t)
}
#[inline]
fn eval_derivative(&self, t: S) -> S {
self.scale * self.inner.eval_derivative(t)
}
}
#[derive(Clone, Debug)]
pub struct Constant<S: Scalar> {
pub value: S,
}
impl<S: Scalar> Constant<S> {
pub fn new(value: S) -> Self {
Self { value }
}
}
impl<S: Scalar> Signal<S> for Constant<S> {
#[inline]
fn eval(&self, _t: S) -> S {
self.value
}
#[inline]
fn eval_derivative(&self, _t: S) -> S {
S::ZERO
}
}
#[derive(Clone, Debug, Default)]
pub struct Zero<S: Scalar> {
_marker: core::marker::PhantomData<S>,
}
impl<S: Scalar> Zero<S> {
pub fn new() -> Self {
Self {
_marker: core::marker::PhantomData,
}
}
}
impl<S: Scalar> Signal<S> for Zero<S> {
#[inline]
fn eval(&self, _t: S) -> S {
S::ZERO
}
#[inline]
fn eval_derivative(&self, _t: S) -> S {
S::ZERO
}
}
impl<S: Scalar> Signal<S> for Box<dyn Signal<S>> {
fn eval(&self, t: S) -> S {
(**self).eval(t)
}
fn eval_derivative(&self, t: S) -> S {
(**self).eval_derivative(t)
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-10;
#[test]
fn test_harmonic() {
let h = Harmonic::new(2.0, 1.0, 0.0);
assert!(h.eval(0.0).abs() < TOL);
assert!((h.eval(0.25) - 2.0).abs() < TOL);
assert!(h.eval(0.5).abs() < TOL);
assert!((h.eval(0.75) + 2.0).abs() < TOL);
}
#[test]
fn test_harmonic_derivative() {
let h = Harmonic::new(1.0, 1.0, 0.0);
let deriv = h.eval_derivative(0.0);
assert!((deriv - 2.0 * core::f64::consts::PI).abs() < TOL);
}
#[test]
fn test_step_sharp() {
let s = Step::new(1.0, 2.0);
assert!(s.eval(1.0).abs() < TOL);
assert!(s.eval(1.999).abs() < TOL);
assert!((s.eval(2.0) - 1.0).abs() < TOL);
assert!((s.eval(3.0) - 1.0).abs() < TOL);
}
#[test]
fn test_step_smooth() {
let s = Step::smooth(1.0, 2.0, 0.1);
assert!(s.eval(0.0).abs() < 0.01);
assert!((s.eval(2.0) - 0.5).abs() < TOL);
assert!((s.eval(4.0) - 1.0).abs() < 0.01);
}
#[test]
fn test_ramp() {
let r = Ramp::new(0.0, 2.0, 1.0, 3.0);
assert!(r.eval(0.5).abs() < TOL);
assert!(r.eval(1.0).abs() < TOL);
assert!((r.eval(2.0) - 1.0).abs() < TOL);
assert!((r.eval(3.0) - 2.0).abs() < TOL);
assert!((r.eval(4.0) - 2.0).abs() < TOL);
}
#[test]
fn test_ramp_derivative() {
let r = Ramp::new(0.0, 4.0, 1.0, 3.0);
assert!((r.eval_derivative(2.0) - 2.0).abs() < TOL);
assert!(r.eval_derivative(0.5).abs() < TOL);
assert!(r.eval_derivative(4.0).abs() < TOL);
}
#[test]
fn test_pulse() {
let p = Pulse::new(5.0, 1.0, 2.0);
assert!(p.eval(0.5).abs() < TOL);
assert!((p.eval(1.0) - 5.0).abs() < TOL);
assert!((p.eval(2.0) - 5.0).abs() < TOL);
assert!((p.eval(3.0) - 5.0).abs() < TOL);
assert!(p.eval(3.5).abs() < TOL);
}
#[test]
fn test_chirp() {
let c = Chirp::new(1.0, 1.0, 10.0, 5.0);
assert!(c.eval(-1.0).abs() < TOL);
assert!(c.eval(6.0).abs() < TOL);
assert!(c.eval(2.5).abs() <= 1.0 + TOL);
assert!(c.eval(0.0).abs() < TOL);
}
#[test]
fn test_tabulated_linear() {
let times = vec![0.0, 1.0, 2.0, 3.0];
let values = vec![0.0, 2.0, 1.0, 3.0];
let t = Tabulated::new(times, values, Interpolation::Linear);
assert!(t.eval(0.0).abs() < TOL);
assert!((t.eval(1.0) - 2.0).abs() < TOL);
assert!((t.eval(2.0) - 1.0).abs() < TOL);
assert!((t.eval(3.0) - 3.0).abs() < TOL);
assert!((t.eval(0.5) - 1.0).abs() < TOL); assert!((t.eval(1.5) - 1.5).abs() < TOL);
assert!(t.eval(-1.0).abs() < TOL);
assert!((t.eval(5.0) - 3.0).abs() < TOL);
}
#[test]
fn test_tabulated_nearest() {
let times = vec![0.0, 1.0, 2.0];
let values = vec![0.0, 1.0, 2.0];
let t = Tabulated::new(times, values, Interpolation::Nearest);
assert!(t.eval(0.3).abs() < TOL);
assert!((t.eval(0.6) - 1.0).abs() < TOL);
assert!((t.eval(1.4) - 1.0).abs() < TOL);
assert!((t.eval(1.6) - 2.0).abs() < TOL);
}
#[test]
fn test_tabulated_cubic_spline() {
let times = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let values = vec![0.0, 1.0, 0.0, 1.0, 0.0];
let t = Tabulated::new(times.clone(), values.clone(), Interpolation::Cubic);
for (i, (&ti, &vi)) in times.iter().zip(values.iter()).enumerate() {
let y = t.value_at(ti);
assert!(
(y - vi).abs() < 1e-10,
"Failed at point {}: expected {}, got {}",
i,
vi,
y
);
}
let mid_12 = t.value_at(1.5);
assert!(
mid_12 < 1.0 && mid_12 > -0.5,
"Cubic spline midpoint 1.5 out of range: {}",
mid_12
);
let eps = 0.001;
let deriv_left = (t.value_at(1.5) - t.value_at(1.5 - eps)) / eps;
let deriv_right = (t.value_at(1.5 + eps) - t.value_at(1.5)) / eps;
assert!(
(deriv_left - deriv_right).abs() < 0.1,
"Derivative discontinuity at 1.5: left={}, right={}",
deriv_left,
deriv_right
);
}
#[test]
fn test_tabulated_cubic_vs_linear() {
let times = vec![0.0, 1.0, 2.0, 3.0];
let values = vec![0.0, 1.0, 0.5, 1.5];
let linear = Tabulated::new(times.clone(), values.clone(), Interpolation::Linear);
let cubic = Tabulated::new(times.clone(), values.clone(), Interpolation::Cubic);
for (&ti, &vi) in times.iter().zip(values.iter()) {
assert!((linear.value_at(ti) - vi).abs() < 1e-10);
assert!((cubic.value_at(ti) - vi).abs() < 1e-10);
}
let lin_mid = linear.value_at(1.5);
let cub_mid = cubic.value_at(1.5);
assert!((lin_mid - 0.75).abs() < 1e-10);
assert!(
cub_mid > 0.0 && cub_mid < 2.0,
"Cubic value {} out of reasonable range",
cub_mid
);
}
#[test]
fn test_piecewise() {
let p = Piecewise::new(vec![(1.0, 10.0), (3.0, 20.0), (5.0, 30.0)], 0.0);
assert!((p.eval(0.5) - 10.0).abs() < TOL);
assert!((p.eval(2.0) - 20.0).abs() < TOL);
assert!((p.eval(4.0) - 30.0).abs() < TOL);
assert!(p.eval(6.0).abs() < TOL);
}
#[test]
fn test_sum() {
let a = Constant::new(3.0);
let b = Constant::new(5.0);
let sum = Sum::new(a, b);
assert!((sum.eval(0.0) - 8.0).abs() < TOL);
assert!((sum.eval(100.0) - 8.0).abs() < TOL);
}
#[test]
fn test_product() {
let a = Constant::new(3.0);
let b = Constant::new(5.0);
let prod = Product::new(a, b);
assert!((prod.eval(0.0) - 15.0).abs() < TOL);
}
#[test]
fn test_scaled() {
let h = Harmonic::new(1.0, 1.0, 0.0);
let scaled = Scaled::new(2.0, h);
assert!((scaled.eval(0.25) - 2.0).abs() < TOL);
}
#[test]
fn test_signal_derivative_f32() {
let times: Vec<f32> = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let values: Vec<f32> = vec![0.0, 2.0, 4.0, 6.0, 8.0];
let s = Tabulated::new(times, values, Interpolation::Linear);
let d = s.eval_derivative(2.0_f32);
assert!(d != 0.0, "FD step quantised to zero on f32");
assert!(
(d - 2.0_f32).abs() < 1e-2,
"central-FD derivative on f32 too inaccurate: d={}",
d
);
}
#[test]
fn test_constant_and_zero() {
let c = Constant::new(42.0);
let z: Zero<f64> = Zero::new();
assert!((c.eval(0.0) - 42.0).abs() < TOL);
assert!((c.eval(1000.0) - 42.0).abs() < TOL);
assert!(c.eval_derivative(0.0).abs() < TOL);
assert!(z.eval(0.0).abs() < TOL);
assert!(z.eval(1000.0).abs() < TOL);
}
#[test]
fn test_composite_signals() {
let harmonic = Harmonic::new(2.0, 1.0, 0.0);
let step = Step::new(1.0, 0.5);
let composite = Sum::new(harmonic, step);
assert!(composite.eval(0.0).abs() < TOL);
assert!((composite.eval(0.25) - 2.0).abs() < TOL);
assert!((composite.eval(0.75) + 1.0).abs() < TOL);
}
#[cfg(feature = "std")]
#[test]
fn test_from_file_csv_string() {
let csv = r#"
# Test data
0.0, 0.0
1.0, 2.0
2.0, 1.0
3.0, 3.0
"#;
let signal: super::FromFile<f64> =
super::FromFile::from_csv_string(csv, Interpolation::Linear).unwrap();
assert_eq!(signal.len(), 4);
assert!(!signal.is_empty());
let (t_min, t_max) = signal.time_range();
assert!(t_min.abs() < TOL);
assert!((t_max - 3.0).abs() < TOL);
assert!(signal.eval(0.0).abs() < TOL);
assert!((signal.eval(1.0) - 2.0).abs() < TOL);
assert!((signal.eval(0.5) - 1.0).abs() < TOL); }
#[cfg(feature = "std")]
#[test]
fn test_from_file_empty_error() {
let csv = "# Only comments\n# No data\n";
let result: Result<super::FromFile<f64>, _> =
super::FromFile::from_csv_string(csv, Interpolation::Linear);
assert!(result.is_err());
}
#[cfg(feature = "std")]
#[test]
fn test_from_file_parse_error() {
let csv = "0.0, abc\n"; let result: Result<super::FromFile<f64>, _> =
super::FromFile::from_csv_string(csv, Interpolation::Linear);
assert!(result.is_err());
}
#[cfg(feature = "std")]
#[test]
fn test_from_file_too_few_columns() {
let csv = "0.0\n"; let result: Result<super::FromFile<f64>, _> =
super::FromFile::from_csv_string(csv, Interpolation::Linear);
assert!(result.is_err());
}
}