Skip to main content

differential_equations/
solution.rs

1//! Solution container for differential equation solvers.
2
3#[cfg(feature = "polars")]
4use polars::prelude::*;
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9use crate::{
10    stats::{Evals, Steps, Timer},
11    status::Status,
12    traits::{Real, State},
13};
14
15/// The result produced by differential equation solvers.
16///
17/// # Fields
18/// * `y`              - Outputted dependent variable points.
19/// * `t`              - Outputted independent variable points.
20/// * `status`         - Status of the solver.
21/// * `evals`          - Number of function evaluations.
22/// * `steps`          - Number of steps.
23/// * `timer`          - Timer for tracking solution time.
24///
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26#[derive(Debug, Clone)]
27pub struct Solution<T, Y>
28where
29    T: Real,
30    Y: State<T>,
31{
32    /// Outputted independent variable points.
33    pub t: Vec<T>,
34
35    /// Outputted dependent variable points.
36    pub y: Vec<Y>,
37
38    /// Status of the solver.
39    pub status: Status<T, Y>,
40
41    /// Number of function, Jacobian, and related evaluations.
42    pub evals: Evals,
43
44    /// Number of steps taken during the solution.
45    pub steps: Steps,
46
47    /// Timer tracking wall-clock time. `Running` during solving, `Completed` after finalization.
48    #[cfg(not(target_arch = "wasm32"))]
49    pub timer: Timer<T>,
50}
51
52// Initial methods for the solution
53impl<T, Y> Default for Solution<T, Y>
54where
55    T: Real,
56    Y: State<T>,
57{
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl<T, Y> Solution<T, Y>
64where
65    T: Real,
66    Y: State<T>,
67{
68    /// Creates a new Solution object.
69    pub fn new() -> Self {
70        Solution {
71            t: Vec::new(),
72            y: Vec::new(),
73            status: Status::Uninitialized,
74            evals: Evals::new(),
75            steps: Steps::new(),
76            #[cfg(not(target_arch = "wasm32"))]
77            timer: Timer::Off,
78        }
79    }
80
81    /// Creates a new Solution object with pre-allocated capacity for points.
82    ///
83    /// # Arguments
84    /// * `capacity` - Initial capacity for the vectors holding time and state points.
85    pub fn new_with_capacity(capacity: usize) -> Self {
86        Solution {
87            t: Vec::with_capacity(capacity),
88            y: Vec::with_capacity(capacity),
89            status: Status::Uninitialized,
90            evals: Evals::new(),
91            steps: Steps::new(),
92            #[cfg(not(target_arch = "wasm32"))]
93            timer: Timer::Off,
94        }
95    }
96}
97
98// Methods used during solving
99impl<T, Y> Solution<T, Y>
100where
101    T: Real,
102    Y: State<T>,
103{
104    /// Push a new `(t, y)` point into the solution.
105    ///
106    /// # Arguments
107    /// * `t` - The time point.
108    /// * `y` - The state vector.
109    ///
110    pub fn push(&mut self, t: T, y: Y) {
111        self.t.push(t);
112        self.y.push(y);
113    }
114
115    /// Pop the last `(t, y)` point from the solution.
116    ///
117    /// # Returns
118    /// * `Option<(T, SMatrix<T, R, C>)>` - The last point in the solution.
119    ///
120    pub fn pop(&mut self) -> Option<(T, Y)> {
121        if self.t.is_empty() || self.y.is_empty() {
122            return None;
123        }
124        let t = self.t.pop().unwrap();
125        let y = self.y.pop().unwrap();
126        Some((t, y))
127    }
128
129    /// Truncates the solution's (t, y) points to the given index.
130    ///
131    /// # Arguments
132    /// * `index` - The index to truncate to.
133    ///
134    pub fn truncate(&mut self, index: usize) {
135        self.t.truncate(index);
136        self.y.truncate(index);
137    }
138}
139
140// Post-processing methods for the solution
141impl<T, Y> Solution<T, Y>
142where
143    T: Real,
144    Y: State<T>,
145{
146    /// Consume the solution into `(t, y)` vectors.
147    ///
148    /// Status, evaluation counters, steps, and timers are discarded.
149    ///
150    /// # Returns
151    /// * `(Vec<T>, Vec<Y)` - Tuple of time and state vectors.
152    ///
153    pub fn into_tuple(self) -> (Vec<T>, Vec<Y>) {
154        (self.t, self.y)
155    }
156
157    /// Return the last accepted step `(t, y)`.
158    ///
159    /// # Returns
160    /// * `Result<(T, Y), Box<dyn std::error::Error>>` - Result of time and state vector.
161    ///
162    pub fn last(&self) -> Result<(&T, &Y), Box<dyn std::error::Error>> {
163        let t = self.t.last().ok_or("No t steps available")?;
164        let y = self.y.last().ok_or("No y vectors available")?;
165        Ok((t, y))
166    }
167
168    /// Returns an iterator over the solution.
169    ///
170    /// # Returns
171    /// * `std::iter::Zip<std::slice::Iter<'_, T>, std::slice::Iter<'_, Y>>` - An iterator
172    ///   yielding (t, y) tuples.
173    ///
174    pub fn iter(&self) -> std::iter::Zip<std::slice::Iter<'_, T>, std::slice::Iter<'_, Y>> {
175        self.t.iter().zip(self.y.iter())
176    }
177
178    /// Write the solution to CSV using only the standard library.
179    ///
180    /// Note the columns will be named t, y0, y1, ..., yN.
181    ///
182    /// # Arguments
183    /// * `filename` - Name of the file to save the solution.
184    ///
185    /// # Returns
186    /// * `Result<(), Box<dyn std::error::Error>>` - Result of writing the file.
187    ///
188    #[cfg(not(feature = "polars"))]
189    pub fn to_csv(&self, filename: &str) -> Result<(), Box<dyn std::error::Error>> {
190        use std::io::{BufWriter, Write};
191
192        // Create file and path if it does not exist
193        let path = std::path::Path::new(filename);
194        if let Some(parent) = path.parent()
195            && !parent.exists()
196        {
197            std::fs::create_dir_all(parent)?;
198        }
199        let file = std::fs::File::create(filename)?;
200        let mut writer = BufWriter::new(file);
201
202        // Length of state vector
203        let n = self.y[0].len();
204
205        // Header
206        let mut header = String::from("t");
207        for i in 0..n {
208            header.push_str(&format!(",y{}", i));
209        }
210        writeln!(writer, "{}", header)?;
211
212        // Data rows
213        for (t, y) in self.iter() {
214            let mut row = format!("{:?}", t);
215            for i in 0..n {
216                row.push_str(&format!(",{:?}", y.get_component(i)));
217            }
218            writeln!(writer, "{}", row)?;
219        }
220
221        writer.flush()?;
222
223        Ok(())
224    }
225
226    /// Write the solution to CSV via a Polars `DataFrame`.
227    ///
228    /// Note the columns will be named t, y0, y1, ..., yN.
229    ///
230    /// # Arguments
231    /// * `filename` - Name of the file to save the solution.
232    ///
233    /// # Returns
234    /// * `Result<(), Box<dyn std::error::Error>>` - Result of writing the file.
235    ///
236    #[cfg(feature = "polars")]
237    pub fn to_csv(&self, filename: &str) -> Result<(), Box<dyn std::error::Error>> {
238        // Create file and path if it does not exist
239        let path = std::path::Path::new(filename);
240        if let Some(parent) = path.parent()
241            && !parent.exists()
242        {
243            std::fs::create_dir_all(parent)?;
244        }
245        let mut file = std::fs::File::create(filename)?;
246
247        let t = self
248            .t
249            .iter()
250            .map(simba::scalar::SupersetOf::<f64>::to_subset_unchecked)
251            .collect::<Vec<f64>>();
252        let mut columns = vec![Column::new("t".into(), t)];
253        let n = self.y[0].len();
254        for i in 0..n {
255            let header = format!("y{}", i);
256            columns.push(Column::new(
257                header.into(),
258                self.y
259                    .iter()
260                    .map(|y| {
261                        simba::scalar::SupersetOf::<f64>::to_subset_unchecked(&y.get_component(i))
262                    })
263                    .collect::<Vec<f64>>(),
264            ));
265        }
266        let mut df = DataFrame::new(self.t.len(), columns)?;
267
268        // Write the DataFrame to CSV
269        CsvWriter::new(&mut file).finish(&mut df)?;
270
271        Ok(())
272    }
273
274    /// Convert the solution to a Polars `DataFrame`.
275    ///
276    /// Requires feature "polars" to be enabled.
277    ///
278    /// Note that the columns will be named t, y0, y1, ..., yN.
279    ///
280    /// # Returns
281    /// * `Result<DataFrame, PolarsError>` - Result of creating the DataFrame.
282    ///
283    #[cfg(feature = "polars")]
284    pub fn to_polars(&self) -> Result<DataFrame, PolarsError> {
285        let t = self
286            .t
287            .iter()
288            .map(simba::scalar::SupersetOf::<f64>::to_subset_unchecked)
289            .collect::<Vec<f64>>();
290        let mut columns = vec![Column::new("t".into(), t)];
291        let n = self.y[0].len();
292        for i in 0..n {
293            let header = format!("y{}", i);
294            columns.push(Column::new(
295                header.into(),
296                self.y
297                    .iter()
298                    .map(|y| {
299                        simba::scalar::SupersetOf::<f64>::to_subset_unchecked(&y.get_component(i))
300                    })
301                    .collect::<Vec<f64>>(),
302            ));
303        }
304
305        DataFrame::new(self.t.len(), columns)
306    }
307
308    /// Convert the solution to a Polars `DataFrame` with custom column names.
309    ///
310    /// Requires feature "polars" to be enabled.
311    ///
312    /// # Arguments
313    /// * `t_name` - Custom name for the time column
314    /// * `y_names` - Custom names for the state variables
315    ///
316    /// # Returns
317    /// * `Result<DataFrame, PolarsError>` - Result of creating the DataFrame.
318    ///
319    #[cfg(feature = "polars")]
320    pub fn to_named_polars(
321        &self,
322        t_name: &str,
323        y_names: Vec<&str>,
324    ) -> Result<DataFrame, PolarsError> {
325        let t = self
326            .t
327            .iter()
328            .map(simba::scalar::SupersetOf::<f64>::to_subset_unchecked)
329            .collect::<Vec<f64>>();
330        let mut columns = vec![Column::new(t_name.into(), t)];
331
332        let n = self.y[0].len();
333
334        // Validate that we have enough names for all state variables
335        if y_names.len() != n {
336            return Err(PolarsError::ComputeError(
337                format!(
338                    "Expected {} column names for state variables, but got {}",
339                    n,
340                    y_names.len()
341                )
342                .into(),
343            ));
344        }
345
346        for (i, name) in y_names.iter().enumerate() {
347            columns.push(Column::new(
348                (*name).into(),
349                self.y
350                    .iter()
351                    .map(|y| {
352                        simba::scalar::SupersetOf::<f64>::to_subset_unchecked(&y.get_component(i))
353                    })
354                    .collect::<Vec<f64>>(),
355            ));
356        }
357
358        DataFrame::new(self.t.len(), columns)
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_into_tuple() {
368        let mut sol: Solution<f64, f64> = Solution::new();
369        sol.push(0.0, 10.0);
370        sol.push(1.0, 20.0);
371
372        let (t, y) = sol.into_tuple();
373        assert_eq!(t, vec![0.0, 1.0]);
374        assert_eq!(y, vec![10.0, 20.0]);
375    }
376
377    #[test]
378    fn test_solution_lifecycle() {
379        // Test new and new_with_capacity
380        let sol_new: Solution<f64, f64> = Solution::new();
381        assert!(sol_new.t.is_empty());
382        assert!(sol_new.y.is_empty());
383
384        let sol_cap: Solution<f64, f64> = Solution::new_with_capacity(10);
385        assert!(sol_cap.t.is_empty());
386        assert!(sol_cap.y.is_empty());
387        assert!(sol_cap.t.capacity() >= 10);
388        assert!(sol_cap.y.capacity() >= 10);
389
390        // Test push
391        let mut sol = sol_new;
392        sol.push(2.0, 30.0);
393        assert_eq!(sol.t.len(), 1);
394        assert_eq!(sol.y.len(), 1);
395        assert_eq!(sol.t[0], 2.0);
396        assert_eq!(sol.y[0], 30.0);
397
398        // Test last (non-empty)
399        let last = sol.last().unwrap();
400        assert_eq!(*last.0, 2.0);
401        assert_eq!(*last.1, 30.0);
402
403        // Test pop
404        let popped = sol.pop();
405        assert_eq!(popped, Some((2.0, 30.0)));
406        assert!(sol.t.is_empty());
407        assert!(sol.y.is_empty());
408
409        // Test last (empty)
410        assert!(sol.last().is_err());
411
412        // Test pop (empty)
413        assert_eq!(sol.pop(), None);
414
415        // Test truncate and iter
416        sol.push(0.0, 10.0);
417        sol.push(1.0, 20.0);
418        sol.push(2.0, 30.0);
419
420        let expected = vec![(0.0, 10.0), (1.0, 20.0), (2.0, 30.0)];
421        let actual: Vec<(f64, f64)> = sol.iter().map(|(&t, &y)| (t, y)).collect();
422        assert_eq!(actual, expected);
423
424        sol.truncate(1);
425        assert_eq!(sol.t.len(), 1);
426        assert_eq!(sol.y.len(), 1);
427        assert_eq!(sol.t[0], 0.0);
428        assert_eq!(sol.y[0], 10.0);
429    }
430}