use crate::common::IntegrateFloat;
use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{array, Array1, ArrayView1};
use std::fmt::Debug;
#[derive(Debug, Clone)]
pub enum DelayType<F: IntegrateFloat> {
Constant(Vec<F>),
StateDependent,
}
#[derive(Debug, Clone)]
pub struct DDEOptions<F: IntegrateFloat> {
pub rtol: F,
pub atol: F,
pub h0: Option<F>,
pub max_step: Option<F>,
pub min_step: Option<F>,
pub max_steps: usize,
pub track_discontinuities: bool,
pub max_discontinuity_order: usize,
}
impl<F: IntegrateFloat> Default for DDEOptions<F> {
fn default() -> Self {
DDEOptions {
rtol: F::from_f64(1e-6).unwrap_or_else(|| F::epsilon()),
atol: F::from_f64(1e-9).unwrap_or_else(|| F::epsilon()),
h0: None,
max_step: None,
min_step: None,
max_steps: 100_000,
track_discontinuities: true,
max_discontinuity_order: 5,
}
}
}
#[derive(Debug, Clone)]
pub struct DDEResult<F: IntegrateFloat> {
pub t: Vec<F>,
pub y: Vec<Array1<F>>,
pub success: bool,
pub message: Option<String>,
pub n_eval: usize,
pub n_steps: usize,
pub n_accepted: usize,
pub n_rejected: usize,
pub discontinuities: Vec<F>,
}
#[derive(Debug, Clone)]
struct DenseSegment<F: IntegrateFloat> {
t_left: F,
t_right: F,
y_left: Array1<F>,
y_right: Array1<F>,
yp_left: Array1<F>,
yp_right: Array1<F>,
}
impl<F: IntegrateFloat> DenseSegment<F> {
fn evaluate(&self, t: F) -> Array1<F> {
let h = self.t_right - self.t_left;
if h.abs() < F::from_f64(1e-30).unwrap_or_else(|| F::epsilon()) {
return self.y_left.clone();
}
let s = (t - self.t_left) / h;
let s2 = s * s;
let s3 = s2 * s;
let two = F::one() + F::one();
let three = two + F::one();
let h00 = two * s3 - three * s2 + F::one();
let h10 = s3 - two * s2 + s;
let h01 = three * s2 - two * s3;
let h11 = s3 - s2;
&self.y_left * h00
+ &(&self.yp_left * (h * h10))
+ &(&self.y_right * h01)
+ &(&self.yp_right * (h * h11))
}
}
#[derive(Debug, Clone)]
struct HistoryBuffer<F: IntegrateFloat> {
segments: Vec<DenseSegment<F>>,
pre_history: Vec<(F, Array1<F>)>,
t_start: F,
}
impl<F: IntegrateFloat> HistoryBuffer<F> {
fn new(t0: F) -> Self {
HistoryBuffer {
segments: Vec::new(),
pre_history: Vec::new(),
t_start: t0,
}
}
fn add_pre_history(&mut self, t: F, y: Array1<F>) {
self.pre_history.push((t, y));
self.pre_history
.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
}
fn add_segment(&mut self, seg: DenseSegment<F>) {
self.segments.push(seg);
}
fn evaluate(&self, t: F) -> IntegrateResult<Array1<F>> {
if t < self.t_start || (t <= self.t_start && self.segments.is_empty()) {
return self.evaluate_pre_history(t);
}
for seg in &self.segments {
if t >= seg.t_left && t <= seg.t_right {
return Ok(seg.evaluate(t));
}
}
if let Some(last) = self.segments.last() {
if (t - last.t_right).abs() < F::from_f64(1e-12).unwrap_or_else(|| F::epsilon()) {
return Ok(last.y_right.clone());
}
}
Err(IntegrateError::ValueError(format!(
"Time {t} is outside the computed solution range"
)))
}
fn evaluate_pre_history(&self, t: F) -> IntegrateResult<Array1<F>> {
if self.pre_history.is_empty() {
return Err(IntegrateError::ValueError(
"No pre-history available for the requested time".into(),
));
}
if self.pre_history.len() == 1 {
return Ok(self.pre_history[0].1.clone());
}
let first_t = self.pre_history[0].0;
let last_t = self.pre_history[self.pre_history.len() - 1].0;
if t <= first_t {
return Ok(self.pre_history[0].1.clone());
}
if t >= last_t {
return Ok(self.pre_history[self.pre_history.len() - 1].1.clone());
}
for i in 0..self.pre_history.len() - 1 {
let (t_i, ref y_i) = self.pre_history[i];
let (t_ip1, ref y_ip1) = self.pre_history[i + 1];
if t >= t_i && t <= t_ip1 {
let dt = t_ip1 - t_i;
if dt.abs() < F::from_f64(1e-30).unwrap_or_else(|| F::epsilon()) {
return Ok(y_i.clone());
}
let s = (t - t_i) / dt;
return Ok(y_i * (F::one() - s) + y_ip1 * s);
}
}
Ok(self.pre_history[self.pre_history.len() - 1].1.clone())
}
}
#[derive(Debug, Clone)]
struct DiscontinuityTracker<F: IntegrateFloat> {
queue: Vec<F>,
max_order: usize,
}
impl<F: IntegrateFloat> DiscontinuityTracker<F> {
fn new(max_order: usize) -> Self {
DiscontinuityTracker {
queue: Vec::new(),
max_order,
}
}
fn seed(&mut self, t0: F, tf: F, delays: &[F]) {
for order in 0..=self.max_order {
for tau in delays {
let disc_t = t0 + F::from_usize(order + 1).unwrap_or_else(|| F::one()) * (*tau);
if disc_t > t0 && disc_t <= tf {
self.queue.push(disc_t);
}
}
}
self.queue
.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
self.queue
.dedup_by(|a, b| (*a - *b).abs() < F::from_f64(1e-12).unwrap_or_else(|| F::epsilon()));
}
fn next_after(&self, t_current: F) -> Option<F> {
let eps = F::from_f64(1e-12).unwrap_or_else(|| F::epsilon());
self.queue.iter().find(|&&td| td > t_current + eps).copied()
}
fn all_times(&self) -> &[F] {
&self.queue
}
}
pub trait DDESystem<F: IntegrateFloat> {
fn ndim(&self) -> usize;
fn rhs(&self, t: F, y: ArrayView1<F>, y_delayed: &[Array1<F>]) -> IntegrateResult<Array1<F>>;
fn delays(&self) -> Vec<F>;
fn state_dependent_delays(&self, _t: F, _y: ArrayView1<F>) -> Vec<F> {
self.delays()
}
fn history(&self, t: F) -> Array1<F>;
}
pub fn solve_dde<F: IntegrateFloat>(
sys: &dyn DDESystem<F>,
t_span: [F; 2],
options: &DDEOptions<F>,
) -> IntegrateResult<DDEResult<F>> {
let t0 = t_span[0];
let tf = t_span[1];
let n = sys.ndim();
if tf <= t0 {
return Err(IntegrateError::ValueError("tf must be > t0".into()));
}
let delays = sys.delays();
if delays.is_empty() {
return Err(IntegrateError::ValueError(
"DDE system must have at least one delay".into(),
));
}
for (i, &tau) in delays.iter().enumerate() {
if tau <= F::zero() {
return Err(IntegrateError::ValueError(format!(
"Delay {} must be positive, got {tau}",
i
)));
}
}
let mut history = HistoryBuffer::new(t0);
let max_delay = delays.iter().fold(F::zero(), |a, &b| a.max(b));
let n_pre_samples = 100;
let pre_dt = max_delay / F::from_usize(n_pre_samples).unwrap_or_else(|| F::one());
for i in 0..=n_pre_samples {
let t_pre = t0 - max_delay + F::from_usize(i).unwrap_or_else(|| F::zero()) * pre_dt;
let y_pre = sys.history(t_pre);
history.add_pre_history(t_pre, y_pre);
}
let mut disc_tracker = DiscontinuityTracker::new(if options.track_discontinuities {
options.max_discontinuity_order
} else {
0
});
if options.track_discontinuities {
disc_tracker.seed(t0, tf, &delays);
}
let y0 = sys.history(t0);
let span = tf - t0;
let mut h = match options.h0 {
Some(h0) => h0,
None => {
let h_init = span * F::from_f64(0.001).unwrap_or_else(|| F::epsilon());
if let Some(max_h) = options.max_step {
h_init.min(max_h)
} else {
h_init
}
}
};
let min_step = options
.min_step
.unwrap_or_else(|| span * F::from_f64(1e-14).unwrap_or_else(|| F::epsilon()));
let mut t_out = vec![t0];
let mut y_out = vec![y0.clone()];
let mut disc_times: Vec<F> = Vec::new();
let mut t = t0;
let mut y = y0;
let mut n_eval: usize = 0;
let mut n_steps: usize = 0;
let mut n_accepted: usize = 0;
let mut n_rejected: usize = 0;
let safety = F::from_f64(0.9).unwrap_or_else(|| F::one());
let fac_min = F::from_f64(0.2).unwrap_or_else(|| F::one());
let fac_max = F::from_f64(5.0).unwrap_or_else(|| F::one());
while t < tf && n_steps < options.max_steps {
if t + h > tf {
h = tf - t;
}
if let Some(t_disc) = disc_tracker.next_after(t) {
if t + h > t_disc {
h = t_disc - t;
let eps = F::from_f64(1e-12).unwrap_or_else(|| F::epsilon());
if (t + h - t_disc).abs() < eps {
disc_times.push(t_disc);
}
}
}
if h < min_step {
break;
}
let step_result = rk45_dde_step(sys, &history, t, h, &y, n, options.rtol, options.atol)?;
n_eval += 6;
let err_norm = step_result.error_norm;
if err_norm <= F::one() {
let t_new = t + h;
let yp_old = evaluate_rhs(sys, &history, t, &y)?;
let yp_new = evaluate_rhs(sys, &history, t_new, &step_result.y_new)?;
history.add_segment(DenseSegment {
t_left: t,
t_right: t_new,
y_left: y.clone(),
y_right: step_result.y_new.clone(),
yp_left: yp_old,
yp_right: yp_new,
});
t = t_new;
y = step_result.y_new;
n_accepted += 1;
t_out.push(t);
y_out.push(y.clone());
} else {
n_rejected += 1;
}
let factor = if err_norm > F::zero() {
safety
* (F::one() / err_norm)
.powf(F::one() / F::from_f64(5.0).unwrap_or_else(|| F::one()))
} else {
fac_max
};
let factor = factor.max(fac_min).min(fac_max);
h *= factor;
if let Some(max_h) = options.max_step {
h = h.min(max_h);
}
n_steps += 1;
}
Ok(DDEResult {
t: t_out,
y: y_out,
success: t >= tf - min_step,
message: if t >= tf - min_step {
Some("DDE integration completed successfully".into())
} else {
Some(format!("DDE integration stopped at t = {t}"))
},
n_eval,
n_steps,
n_accepted,
n_rejected,
discontinuities: disc_times,
})
}
struct RK45StepResult<F: IntegrateFloat> {
y_new: Array1<F>,
error_norm: F,
}
fn rk45_dde_step<F: IntegrateFloat>(
sys: &dyn DDESystem<F>,
history: &HistoryBuffer<F>,
t: F,
h: F,
y: &Array1<F>,
n: usize,
rtol: F,
atol: F,
) -> IntegrateResult<RK45StepResult<F>> {
let a21 = F::from_f64(1.0 / 5.0).unwrap_or_else(|| F::zero());
let a31 = F::from_f64(3.0 / 40.0).unwrap_or_else(|| F::zero());
let a32 = F::from_f64(9.0 / 40.0).unwrap_or_else(|| F::zero());
let a41 = F::from_f64(44.0 / 45.0).unwrap_or_else(|| F::zero());
let a42 = F::from_f64(-56.0 / 15.0).unwrap_or_else(|| F::zero());
let a43 = F::from_f64(32.0 / 9.0).unwrap_or_else(|| F::zero());
let a51 = F::from_f64(19372.0 / 6561.0).unwrap_or_else(|| F::zero());
let a52 = F::from_f64(-25360.0 / 2187.0).unwrap_or_else(|| F::zero());
let a53 = F::from_f64(64448.0 / 6561.0).unwrap_or_else(|| F::zero());
let a54 = F::from_f64(-212.0 / 729.0).unwrap_or_else(|| F::zero());
let a61 = F::from_f64(9017.0 / 3168.0).unwrap_or_else(|| F::zero());
let a62 = F::from_f64(-355.0 / 33.0).unwrap_or_else(|| F::zero());
let a63 = F::from_f64(46732.0 / 5247.0).unwrap_or_else(|| F::zero());
let a64 = F::from_f64(49.0 / 176.0).unwrap_or_else(|| F::zero());
let a65 = F::from_f64(-5103.0 / 18656.0).unwrap_or_else(|| F::zero());
let b1 = F::from_f64(35.0 / 384.0).unwrap_or_else(|| F::zero());
let b3 = F::from_f64(500.0 / 1113.0).unwrap_or_else(|| F::zero());
let b4 = F::from_f64(125.0 / 192.0).unwrap_or_else(|| F::zero());
let b5 = F::from_f64(-2187.0 / 6784.0).unwrap_or_else(|| F::zero());
let b6 = F::from_f64(11.0 / 84.0).unwrap_or_else(|| F::zero());
let e1 = F::from_f64(71.0 / 57600.0).unwrap_or_else(|| F::zero());
let e3 = F::from_f64(-71.0 / 16695.0).unwrap_or_else(|| F::zero());
let e4 = F::from_f64(71.0 / 1920.0).unwrap_or_else(|| F::zero());
let e5 = F::from_f64(-17253.0 / 339200.0).unwrap_or_else(|| F::zero());
let e6 = F::from_f64(22.0 / 525.0).unwrap_or_else(|| F::zero());
let e7 = F::from_f64(-1.0 / 40.0).unwrap_or_else(|| F::zero());
let c2 = F::from_f64(1.0 / 5.0).unwrap_or_else(|| F::zero());
let c3 = F::from_f64(3.0 / 10.0).unwrap_or_else(|| F::zero());
let c4 = F::from_f64(4.0 / 5.0).unwrap_or_else(|| F::zero());
let c5 = F::from_f64(8.0 / 9.0).unwrap_or_else(|| F::zero());
let k1 = evaluate_rhs(sys, history, t, y)?;
let y2 = y + &(&k1 * (h * a21));
let k2 = evaluate_rhs(sys, history, t + c2 * h, &y2)?;
let y3 = y + &(&k1 * (h * a31) + &k2 * (h * a32));
let k3 = evaluate_rhs(sys, history, t + c3 * h, &y3)?;
let y4 = y + &(&k1 * (h * a41) + &k2 * (h * a42) + &k3 * (h * a43));
let k4 = evaluate_rhs(sys, history, t + c4 * h, &y4)?;
let y5 = y + &(&k1 * (h * a51) + &k2 * (h * a52) + &k3 * (h * a53) + &k4 * (h * a54));
let k5 = evaluate_rhs(sys, history, t + c5 * h, &y5)?;
let y6 = y + &(&k1 * (h * a61)
+ &k2 * (h * a62)
+ &k3 * (h * a63)
+ &k4 * (h * a64)
+ &k5 * (h * a65));
let k6 = evaluate_rhs(sys, history, t + h, &y6)?;
let y_new =
y + &(&k1 * (h * b1) + &k3 * (h * b3) + &k4 * (h * b4) + &k5 * (h * b5) + &k6 * (h * b6));
let k7 = evaluate_rhs(sys, history, t + h, &y_new)?;
let err = &k1 * (h * e1)
+ &k3 * (h * e3)
+ &k4 * (h * e4)
+ &k5 * (h * e5)
+ &k6 * (h * e6)
+ &k7 * (h * e7);
let mut sum = F::zero();
for i in 0..n {
let scale = atol + rtol * y[i].abs().max(y_new[i].abs());
let ratio = err[i] / scale;
sum += ratio * ratio;
}
let err_norm = (sum / F::from_usize(n).unwrap_or_else(|| F::one())).sqrt();
Ok(RK45StepResult {
y_new,
error_norm: err_norm,
})
}
fn evaluate_rhs<F: IntegrateFloat>(
sys: &dyn DDESystem<F>,
history: &HistoryBuffer<F>,
t: F,
y: &Array1<F>,
) -> IntegrateResult<Array1<F>> {
let delays = sys.state_dependent_delays(t, y.view());
let mut y_delayed = Vec::with_capacity(delays.len());
for tau in &delays {
let t_delayed = t - *tau;
let y_del = history.evaluate(t_delayed)?;
y_delayed.push(y_del);
}
sys.rhs(t, y.view(), &y_delayed)
}
pub struct SimpleConstantDDE<F: IntegrateFloat> {
ndim: usize,
delay: F,
rhs_fn: Box<dyn Fn(F, ArrayView1<F>, ArrayView1<F>) -> Array1<F> + Send + Sync>,
history_fn: Box<dyn Fn(F) -> Array1<F> + Send + Sync>,
}
impl<F: IntegrateFloat> SimpleConstantDDE<F> {
pub fn new<R, H>(ndim: usize, delay: F, rhs_fn: R, history_fn: H) -> Self
where
R: Fn(F, ArrayView1<F>, ArrayView1<F>) -> Array1<F> + Send + Sync + 'static,
H: Fn(F) -> Array1<F> + Send + Sync + 'static,
{
SimpleConstantDDE {
ndim,
delay,
rhs_fn: Box::new(rhs_fn),
history_fn: Box::new(history_fn),
}
}
}
impl<F: IntegrateFloat> DDESystem<F> for SimpleConstantDDE<F> {
fn ndim(&self) -> usize {
self.ndim
}
fn rhs(&self, t: F, y: ArrayView1<F>, y_delayed: &[Array1<F>]) -> IntegrateResult<Array1<F>> {
if y_delayed.is_empty() {
return Err(IntegrateError::ValueError(
"Expected at least one delayed value".into(),
));
}
Ok((self.rhs_fn)(t, y, y_delayed[0].view()))
}
fn delays(&self) -> Vec<F> {
vec![self.delay]
}
fn history(&self, t: F) -> Array1<F> {
(self.history_fn)(t)
}
}
pub struct MultiDelayDDE<F: IntegrateFloat> {
ndim: usize,
delays_vec: Vec<F>,
rhs_fn: Box<dyn Fn(F, ArrayView1<F>, &[Array1<F>]) -> Array1<F> + Send + Sync>,
history_fn: Box<dyn Fn(F) -> Array1<F> + Send + Sync>,
}
impl<F: IntegrateFloat> MultiDelayDDE<F> {
pub fn new<R, H>(ndim: usize, delays_vec: Vec<F>, rhs_fn: R, history_fn: H) -> Self
where
R: Fn(F, ArrayView1<F>, &[Array1<F>]) -> Array1<F> + Send + Sync + 'static,
H: Fn(F) -> Array1<F> + Send + Sync + 'static,
{
MultiDelayDDE {
ndim,
delays_vec,
rhs_fn: Box::new(rhs_fn),
history_fn: Box::new(history_fn),
}
}
}
impl<F: IntegrateFloat> DDESystem<F> for MultiDelayDDE<F> {
fn ndim(&self) -> usize {
self.ndim
}
fn rhs(&self, t: F, y: ArrayView1<F>, y_delayed: &[Array1<F>]) -> IntegrateResult<Array1<F>> {
Ok((self.rhs_fn)(t, y, y_delayed))
}
fn delays(&self) -> Vec<F> {
self.delays_vec.clone()
}
fn history(&self, t: F) -> Array1<F> {
(self.history_fn)(t)
}
}
pub struct StateDependentDDE<F: IntegrateFloat> {
ndim: usize,
n_delays: usize,
rhs_fn: Box<dyn Fn(F, ArrayView1<F>, &[Array1<F>]) -> Array1<F> + Send + Sync>,
delay_fn: Box<dyn Fn(F, ArrayView1<F>) -> Vec<F> + Send + Sync>,
history_fn: Box<dyn Fn(F) -> Array1<F> + Send + Sync>,
}
impl<F: IntegrateFloat> StateDependentDDE<F> {
pub fn new<R, D, H>(ndim: usize, n_delays: usize, rhs_fn: R, delay_fn: D, history_fn: H) -> Self
where
R: Fn(F, ArrayView1<F>, &[Array1<F>]) -> Array1<F> + Send + Sync + 'static,
D: Fn(F, ArrayView1<F>) -> Vec<F> + Send + Sync + 'static,
H: Fn(F) -> Array1<F> + Send + Sync + 'static,
{
StateDependentDDE {
ndim,
n_delays,
rhs_fn: Box::new(rhs_fn),
delay_fn: Box::new(delay_fn),
history_fn: Box::new(history_fn),
}
}
}
impl<F: IntegrateFloat> DDESystem<F> for StateDependentDDE<F> {
fn ndim(&self) -> usize {
self.ndim
}
fn rhs(&self, t: F, y: ArrayView1<F>, y_delayed: &[Array1<F>]) -> IntegrateResult<Array1<F>> {
Ok((self.rhs_fn)(t, y, y_delayed))
}
fn delays(&self) -> Vec<F> {
vec![F::one(); self.n_delays]
}
fn state_dependent_delays(&self, t: F, y: ArrayView1<F>) -> Vec<F> {
(self.delay_fn)(t, y)
}
fn history(&self, t: F) -> Array1<F> {
(self.history_fn)(t)
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_simple_constant_delay() {
let sys = SimpleConstantDDE::new(
1,
1.0,
|_t, _y: ArrayView1<f64>, y_del: ArrayView1<f64>| -> Array1<f64> { array![-y_del[0]] },
|_t| array![1.0],
);
let opts = DDEOptions {
h0: Some(0.01),
rtol: 1e-6,
atol: 1e-9,
..Default::default()
};
let result = solve_dde(&sys, [0.0, 1.0], &opts).expect("DDE solve should succeed");
assert!(result.success, "DDE integration should succeed");
let y_final = result.y.last().expect("should have final state");
assert!(
(y_final[0] - 0.0).abs() < 0.05,
"y(1) = {} should be near 0",
y_final[0]
);
}
#[test]
fn test_dde_first_interval_exact() {
let sys = SimpleConstantDDE::new(
1,
1.0,
|_t, _y: ArrayView1<f64>, y_del: ArrayView1<f64>| -> Array1<f64> { array![-y_del[0]] },
|_t| array![1.0],
);
let opts = DDEOptions {
h0: Some(0.005),
rtol: 1e-8,
atol: 1e-12,
..Default::default()
};
let result = solve_dde(&sys, [0.0, 0.5], &opts).expect("DDE solve should succeed");
let y_final = result.y.last().expect("should have final state");
assert!(
(y_final[0] - 0.5).abs() < 0.01,
"y(0.5) = {} should be near 0.5",
y_final[0]
);
}
#[test]
fn test_multi_delay_dde() {
let sys = MultiDelayDDE::new(
1,
vec![0.5, 1.0],
|_t, _y: ArrayView1<f64>, y_del: &[Array1<f64>]| -> Array1<f64> {
array![-y_del[0][0] - y_del[1][0]]
},
|_t| array![1.0],
);
let opts = DDEOptions {
h0: Some(0.005),
rtol: 1e-8,
atol: 1e-12,
..Default::default()
};
let result = solve_dde(&sys, [0.0, 0.5], &opts).expect("multi-delay DDE should succeed");
let y_final = result.y.last().expect("should have final state");
assert!(
(y_final[0] - 0.0).abs() < 0.05,
"y(0.5) = {} should be near 0",
y_final[0]
);
}
#[test]
fn test_discontinuity_tracking() {
let sys = SimpleConstantDDE::new(
1,
0.5,
|_t, _y: ArrayView1<f64>, y_del: ArrayView1<f64>| -> Array1<f64> { array![-y_del[0]] },
|_t| array![1.0],
);
let opts = DDEOptions {
h0: Some(0.01),
track_discontinuities: true,
max_discontinuity_order: 3,
..Default::default()
};
let result = solve_dde(&sys, [0.0, 2.0], &opts).expect("DDE with disc tracking");
assert!(result.success);
}
#[test]
fn test_state_dependent_delay() {
let sys = StateDependentDDE::new(
1,
1,
|_t, _y: ArrayView1<f64>, y_del: &[Array1<f64>]| -> Array1<f64> {
array![-y_del[0][0]]
},
|_t, y: ArrayView1<f64>| -> Vec<f64> {
vec![y[0].abs().max(0.1)] },
|_t| array![1.0],
);
let opts = DDEOptions {
h0: Some(0.005),
rtol: 1e-5,
atol: 1e-8,
..Default::default()
};
let result = solve_dde(&sys, [0.0, 0.5], &opts).expect("state-dep DDE should succeed");
assert!(result.success);
assert!(result.y.len() > 2);
}
#[test]
fn test_dde_invalid_inputs() {
let sys = SimpleConstantDDE::new(
1,
1.0,
|_t, _y: ArrayView1<f64>, y_del: ArrayView1<f64>| -> Array1<f64> { array![-y_del[0]] },
|_t| array![1.0],
);
let opts = DDEOptions::default();
let result = solve_dde(&sys, [1.0, 0.0], &opts);
assert!(result.is_err());
}
#[test]
fn test_hermite_interpolation() {
let seg = DenseSegment {
t_left: 0.0,
t_right: 1.0,
y_left: array![1.0],
y_right: array![0.0],
yp_left: array![-1.0], yp_right: array![-1.0], };
let y_mid = seg.evaluate(0.5_f64);
assert!(
(y_mid[0] - 0.5_f64).abs() < 1e-10,
"Hermite at 0.5: {}",
y_mid[0]
);
let y0 = seg.evaluate(0.0_f64);
assert!((y0[0] - 1.0_f64).abs() < 1e-10);
let y1 = seg.evaluate(1.0_f64);
assert!((y1[0] - 0.0_f64).abs() < 1e-10);
}
#[test]
fn test_history_buffer() {
let mut buf = HistoryBuffer::new(0.0);
buf.add_pre_history(-1.0, array![2.0]);
buf.add_pre_history(-0.5, array![1.5]);
buf.add_pre_history(0.0, array![1.0]);
let y = buf.evaluate(-0.75).expect("pre-history eval");
assert!((y[0] - 1.75_f64).abs() < 1e-10, "y(-0.75) = {}", y[0]);
buf.add_segment(DenseSegment {
t_left: 0.0,
t_right: 0.5,
y_left: array![1.0],
y_right: array![0.5],
yp_left: array![-1.0],
yp_right: array![-1.0],
});
let y = buf.evaluate(0.25).expect("segment eval");
assert!((y[0] - 0.75_f64).abs() < 0.1, "y(0.25) = {}", y[0]);
}
}