differential_equations/solout/
even.rs

1//! Even Solout Implementation for evenly spaced output points.
2//!
3//! This module provides an output strategy that generates solution points at evenly
4//! spaced time intervals, regardless of the actual steps taken by the solver.
5
6use super::*;
7
8/// An output handler that provides solution points at evenly spaced time intervals.
9///
10/// # Overview
11///
12/// `EvenSolout` generates output points at strictly uniform time intervals, creating
13/// a regular grid of solution points. This is especially useful for visualization,
14/// post-processing, or when interfacing with other systems that expect uniformly
15/// sampled data.
16///
17/// Unlike `DefaultSolout` which captures the naturally occurring solver steps (which
18/// may have varying time intervals), `EvenSolout` uses interpolation to evaluate the
19/// solution at precise time points separated by a fixed interval.
20///
21/// # Example
22///
23/// ```
24/// use differential_equations::prelude::*;
25/// use differential_equations::solout::EvenSolout;
26/// use nalgebra::{Vector2, vector};
27///
28/// // Simple harmonic oscillator
29/// struct HarmonicOscillator;
30///
31/// impl ODE<f64, Vector2<f64>> for HarmonicOscillator {
32///     fn diff(&self, _t: f64, y: &Vector2<f64>, dydt: &mut Vector2<f64>) {
33///         // y[0] = position, y[1] = velocity
34///         dydt[0] = y[1];
35///         dydt[1] = -y[0];
36///     }
37/// }
38///
39/// // Create the system and solver
40/// let system = HarmonicOscillator;
41/// let t0 = 0.0;
42/// let tf = 10.0;
43/// let y0 = vector![1.0, 0.0];
44/// let mut solver = ExplicitRungeKutta::dop853().rtol(1e-6).atol(1e-8);
45///
46/// // Generate output points with a fixed interval of 0.1
47/// let mut even_output = EvenSolout::new(0.1, t0, tf);
48///
49/// // Solve with evenly spaced output
50/// let problem = ODEProblem::new(system, t0, tf, y0);
51/// let solution = problem.solout(&mut even_output).solve(&mut solver).unwrap();
52///
53/// // Note: This is equivalent to using the convenience method:
54/// let solution = problem.even(0.1).solve(&mut solver).unwrap();
55/// ```
56///
57/// # Output Characteristics
58///
59/// The output will contain points at regular intervals: t₀, t₀+dt, t₀+2dt, ..., tₙ.
60/// The final point tₙ is guaranteed to be included, even if it doesn't fall exactly on
61/// the regular grid. Any evaluation points that fall outside the integration range are ignored.
62///
63pub struct EvenSolout<T: Real> {
64    /// Fixed time interval between points
65    dt: T,
66    /// Initial time to align intervals with
67    t0: T,
68    /// Final time to ensure the last point is included
69    tf: T,
70    /// Direction of integration (positive for forward, negative for backward)
71    direction: T,
72    /// Last time point that was output
73    last_output_t: Option<T>,
74}
75
76impl<T, V, D> Solout<T, V, D> for EvenSolout<T>
77where
78    T: Real,
79    V: State<T>,
80    D: CallBackData,
81{
82    fn solout<I>(
83        &mut self,
84        t_curr: T,
85        t_prev: T,
86        y_curr: &V,
87        y_prev: &V,
88        interpolator: &mut I,
89        solution: &mut Solution<T, V, D>,
90    ) -> ControlFlag<T, V, D>
91    where
92        I: Interpolation<T, V>,
93    {
94        // Determine the alignment offset (remainder when divided by dt)
95        let offset = self.t0 % self.dt;
96
97        // Start from the last output point if available, otherwise from t_prev
98        let start_t = match self.last_output_t {
99            Some(t) => t + self.dt * self.direction,
100            None => {
101                // First time through, we need to include t0
102                if (t_prev - self.t0).abs() < T::default_epsilon() {
103                    solution.push(self.t0, *y_prev);
104                    self.last_output_t = Some(self.t0);
105                    self.t0 + self.dt * self.direction
106                } else {
107                    // Find the next aligned point after t_prev
108                    let rem = (t_prev - offset) % self.dt;
109
110                    if self.direction > T::zero() {
111                        // For forward integration
112                        if rem.abs() < T::default_epsilon() {
113                            t_prev
114                        } else {
115                            t_prev + (self.dt - rem)
116                        }
117                    } else {
118                        // For backward integration
119                        if rem.abs() < T::default_epsilon() {
120                            t_prev
121                        } else {
122                            t_prev - rem
123                        }
124                    }
125                }
126            }
127        };
128
129        let mut ti = start_t;
130
131        // Interpolate between steps
132        while (self.direction > T::zero() && ti <= t_curr)
133            || (self.direction < T::zero() && ti >= t_curr)
134        {
135            // Only output if the point falls within the current step
136            if (self.direction > T::zero() && ti >= t_prev && ti <= t_curr)
137                || (self.direction < T::zero() && ti <= t_prev && ti >= t_curr)
138            {
139                let yi = interpolator.interpolate(ti).unwrap();
140                solution.push(ti, yi);
141                self.last_output_t = Some(ti);
142            }
143
144            // Move to the next point
145            ti += self.dt * self.direction;
146        }
147
148        // Include final point if this step reaches tf and we haven't added it yet
149        if t_curr == self.tf
150            && (self.last_output_t.is_none() || self.last_output_t.unwrap() != self.tf)
151        {
152            solution.push(self.tf, *y_curr);
153            self.last_output_t = Some(self.tf);
154        }
155
156        // Continue the integration
157        ControlFlag::Continue
158    }
159}
160
161impl<T: Real> EvenSolout<T> {
162    /// Creates a new EvenSolout instance with the specified time interval.
163    ///
164    /// This output handler will generate solution points at regular intervals of `dt`
165    /// starting from `t0` and continuing through `tf`. The points will be aligned
166    /// with `t0` (i.e., t₀, t₀+dt, t₀+2dt, ...).
167    ///
168    /// # Arguments
169    /// * `dt` - The fixed time interval between output points
170    /// * `t0` - The initial time of the integration
171    /// * `tf` - The final time of the integration
172    ///
173    /// # Returns
174    /// * A new `EvenSolout` instance
175    ///
176    pub fn new(dt: T, t0: T, tf: T) -> Self {
177        EvenSolout {
178            dt,
179            t0,
180            tf,
181            direction: (tf - t0).signum(),
182            last_output_t: None,
183        }
184    }
185}