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, Y> Solout<T, Y> for EvenSolout<T>
77where
78    T: Real,
79    Y: State<T>,
80{
81    fn solout<I>(
82        &mut self,
83        t_curr: T,
84        t_prev: T,
85        y_curr: &Y,
86        y_prev: &Y,
87        interpolator: &mut I,
88        solution: &mut Solution<T, Y>,
89    ) -> ControlFlag<T, Y>
90    where
91        I: Interpolation<T, Y>,
92    {
93        // Determine the alignment offset (remainder when divided by dt)
94        let offset = self.t0 % self.dt;
95
96        // Tolerance for comparing time points to avoid near-duplicates from FP error
97        // Scales with dt and also includes a small absolute epsilon
98        let tol = self.dt.abs() * T::from_f64(1e-12).unwrap_or(T::default_epsilon())
99            + T::default_epsilon() * T::from_f64(10.0).unwrap_or(T::one());
100
101        // Start from the last output point if available, otherwise from t_prev
102        let start_t = match self.last_output_t {
103            Some(t) => t + self.dt * self.direction,
104            None => {
105                // First time through, we need to include t0
106                if (t_prev - self.t0).abs() < T::default_epsilon() {
107                    solution.push(self.t0, *y_prev);
108                    self.last_output_t = Some(self.t0);
109                    self.t0 + self.dt * self.direction
110                } else {
111                    // Find the next aligned point after t_prev
112                    let rem = (t_prev - offset) % self.dt;
113
114                    if self.direction > T::zero() {
115                        // For forward integration
116                        if rem.abs() < T::default_epsilon() {
117                            t_prev
118                        } else {
119                            t_prev + (self.dt - rem)
120                        }
121                    } else {
122                        // For backward integration
123                        if rem.abs() < T::default_epsilon() {
124                            t_prev
125                        } else {
126                            t_prev - rem
127                        }
128                    }
129                }
130            }
131        };
132
133        let mut ti = start_t;
134
135        // Interpolate between steps
136        while (self.direction > T::zero() && ti <= t_curr)
137            || (self.direction < T::zero() && ti >= t_curr)
138        {
139            // Only output if the point falls within the current step
140            if (self.direction > T::zero() && ti >= t_prev && ti <= t_curr)
141                || (self.direction < T::zero() && ti <= t_prev && ti >= t_curr)
142            {
143                // Skip if this ti is essentially the same as the last output
144                if self
145                    .last_output_t
146                    .map(|t_last| (ti - t_last).abs() <= tol)
147                    .unwrap_or(false)
148                {
149                    // Do nothing; advance to next ti
150                } else {
151                    let yi = interpolator.interpolate(ti).unwrap();
152                    solution.push(ti, yi);
153                    self.last_output_t = Some(ti);
154                }
155            }
156
157            // Move to the next point
158            ti += self.dt * self.direction;
159        }
160
161        // Include final point if this step reaches tf and we haven't added it yet
162        if t_curr == self.tf {
163            match self.last_output_t {
164                Some(t_last) => {
165                    if (t_last - self.tf).abs() <= tol {
166                        // Replace the near-duplicate last point with the exact final time
167                        let _ = solution.pop();
168                        solution.push(self.tf, *y_curr);
169                        self.last_output_t = Some(self.tf);
170                    } else if t_last != self.tf {
171                        solution.push(self.tf, *y_curr);
172                        self.last_output_t = Some(self.tf);
173                    }
174                }
175                None => {
176                    solution.push(self.tf, *y_curr);
177                    self.last_output_t = Some(self.tf);
178                }
179            }
180        }
181
182        // Continue the integration
183        ControlFlag::Continue
184    }
185}
186
187impl<T: Real> EvenSolout<T> {
188    /// Creates a new EvenSolout instance with the specified time interval.
189    ///
190    /// This output handler will generate solution points at regular intervals of `dt`
191    /// starting from `t0` and continuing through `tf`. The points will be aligned
192    /// with `t0` (i.e., t₀, t₀+dt, t₀+2dt, ...).
193    ///
194    /// # Arguments
195    /// * `dt` - The fixed time interval between output points
196    /// * `t0` - The initial time of the integration
197    /// * `tf` - The final time of the integration
198    ///
199    /// # Returns
200    /// * A new `EvenSolout` instance
201    ///
202    pub fn new(dt: T, t0: T, tf: T) -> Self {
203        EvenSolout {
204            dt,
205            t0,
206            tf,
207            direction: (tf - t0).signum(),
208            last_output_t: None,
209        }
210    }
211}