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}