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}