nyx_space/propagators/
options.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use std::fmt;
20
21use crate::time::{Duration, Unit};
22
23use super::ErrorControl;
24use anise::frames::Frame;
25use serde::{Deserialize, Serialize};
26use typed_builder::TypedBuilder;
27
28/// Stores the integrator options, including the minimum and maximum step sizes, and the central body to perform the integration.
29///
30/// Note that different step sizes and max errors are only used for adaptive
31/// methods. To use a fixed step integrator, initialize the options using `with_fixed_step`, and
32/// use whichever adaptive step integrator is desired.  For example, initializing an RK45 with
33/// fixed step options will lead to an RK4 being used instead of an RK45.
34#[derive(Clone, Copy, Debug, TypedBuilder, Serialize, Deserialize, PartialEq)]
35#[builder(doc)]
36pub struct IntegratorOptions {
37    #[builder(default_code = "60.0 * Unit::Second")]
38    pub init_step: Duration,
39    #[builder(default_code = "0.001 * Unit::Second")]
40    pub min_step: Duration,
41    #[builder(default_code = "2700.0 * Unit::Second")]
42    pub max_step: Duration,
43    #[builder(default = 1e-12)]
44    pub tolerance: f64,
45    #[builder(default = 50)]
46    pub attempts: u8,
47    #[builder(default = false)]
48    pub fixed_step: bool,
49    #[builder(default)]
50    pub error_ctrl: ErrorControl,
51    /// If a frame is specified and the propagator state is in a different frame, it it changed to this frame prior to integration.
52    /// Note, when setting this, it's recommended to call `strip` on the Frame.
53    #[builder(default, setter(strip_option))]
54    pub integration_frame: Option<Frame>,
55}
56
57impl IntegratorOptions {
58    /// `with_adaptive_step` initializes an `PropOpts` such that the integrator is used with an
59    ///  adaptive step size. The number of attempts is currently fixed to 50 (as in GMAT).
60    pub fn with_adaptive_step(
61        min_step: Duration,
62        max_step: Duration,
63        tolerance: f64,
64        error_ctrl: ErrorControl,
65    ) -> Self {
66        IntegratorOptions {
67            init_step: max_step,
68            min_step,
69            max_step,
70            tolerance,
71            attempts: 50,
72            fixed_step: false,
73            error_ctrl,
74            integration_frame: None,
75        }
76    }
77
78    pub fn with_adaptive_step_s(
79        min_step: f64,
80        max_step: f64,
81        tolerance: f64,
82        error_ctrl: ErrorControl,
83    ) -> Self {
84        Self::with_adaptive_step(
85            min_step * Unit::Second,
86            max_step * Unit::Second,
87            tolerance,
88            error_ctrl,
89        )
90    }
91
92    /// `with_fixed_step` initializes an `PropOpts` such that the integrator is used with a fixed
93    ///  step size.
94    pub fn with_fixed_step(step: Duration) -> Self {
95        IntegratorOptions {
96            init_step: step,
97            min_step: step,
98            max_step: step,
99            tolerance: 0.0,
100            fixed_step: true,
101            attempts: 0,
102            error_ctrl: ErrorControl::RSSCartesianStep,
103            integration_frame: None,
104        }
105    }
106
107    pub fn with_fixed_step_s(step: f64) -> Self {
108        Self::with_fixed_step(step * Unit::Second)
109    }
110
111    /// Returns the default options with a specific tolerance.
112    #[allow(clippy::field_reassign_with_default)]
113    pub fn with_tolerance(tolerance: f64) -> Self {
114        let mut opts = Self::default();
115        opts.tolerance = tolerance;
116        opts
117    }
118
119    /// Creates a propagator with the provided max step, and sets the initial step to that value as well.
120    #[allow(clippy::field_reassign_with_default)]
121    pub fn with_max_step(max_step: Duration) -> Self {
122        let mut opts = Self::default();
123        opts.set_max_step(max_step);
124        opts
125    }
126
127    /// Returns a string with the information about these options
128    pub fn info(&self) -> String {
129        format!("{self}")
130    }
131
132    /// Set the maximum step size and sets the initial step to that value if currently greater
133    pub fn set_max_step(&mut self, max_step: Duration) {
134        if self.init_step > max_step {
135            self.init_step = max_step;
136        }
137        self.max_step = max_step;
138    }
139
140    /// Set the minimum step size and sets the initial step to that value if currently smaller
141    pub fn set_min_step(&mut self, min_step: Duration) {
142        if self.init_step < min_step {
143            self.init_step = min_step;
144        }
145        self.min_step = min_step;
146    }
147}
148
149impl fmt::Display for IntegratorOptions {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        if self.fixed_step {
152            write!(f, "fixed step: {:e}", self.min_step,)
153        } else {
154            write!(
155                f,
156                "min_step: {:e}, max_step: {:e}, tol: {:e}, attempts: {}",
157                self.min_step, self.max_step, self.tolerance, self.attempts,
158            )
159        }
160    }
161}
162
163impl Default for IntegratorOptions {
164    /// `default` returns the same default options as GMAT.
165    fn default() -> IntegratorOptions {
166        IntegratorOptions {
167            init_step: 60.0 * Unit::Second,
168            min_step: 0.001 * Unit::Second,
169            max_step: 2700.0 * Unit::Second,
170            tolerance: 1e-12,
171            attempts: 50,
172            fixed_step: false,
173            error_ctrl: ErrorControl::RSSCartesianStep,
174            integration_frame: None,
175        }
176    }
177}
178
179#[cfg(test)]
180mod ut_integr_opts {
181    use hifitime::Unit;
182
183    use crate::propagators::{ErrorControl, IntegratorOptions};
184
185    #[test]
186    fn test_options() {
187        let opts = IntegratorOptions::with_fixed_step_s(1e-1);
188        assert_eq!(opts.min_step, 1e-1 * Unit::Second);
189        assert_eq!(opts.max_step, 1e-1 * Unit::Second);
190        assert!(opts.tolerance.abs() < f64::EPSILON);
191        assert!(opts.fixed_step);
192
193        let opts =
194            IntegratorOptions::with_adaptive_step_s(1e-2, 10.0, 1e-12, ErrorControl::RSSStep);
195        assert_eq!(opts.min_step, 1e-2 * Unit::Second);
196        assert_eq!(opts.max_step, 10.0 * Unit::Second);
197        assert!((opts.tolerance - 1e-12).abs() < f64::EPSILON);
198        assert!(!opts.fixed_step);
199
200        let opts: IntegratorOptions = Default::default();
201        assert_eq!(opts.init_step, 60.0 * Unit::Second);
202        assert_eq!(opts.min_step, 0.001 * Unit::Second);
203        assert_eq!(opts.max_step, 2700.0 * Unit::Second);
204        assert!((opts.tolerance - 1e-12).abs() < f64::EPSILON);
205        assert_eq!(opts.attempts, 50);
206        assert!(!opts.fixed_step);
207
208        let opts = IntegratorOptions::with_max_step(1.0 * Unit::Second);
209        assert_eq!(opts.init_step, 1.0 * Unit::Second);
210        assert_eq!(opts.min_step, 0.001 * Unit::Second);
211        assert_eq!(opts.max_step, 1.0 * Unit::Second);
212        assert!((opts.tolerance - 1e-12).abs() < f64::EPSILON);
213        assert_eq!(opts.attempts, 50);
214        assert!(!opts.fixed_step);
215    }
216
217    #[test]
218    fn test_serde() {
219        let opts = IntegratorOptions::default();
220        let serialized = toml::to_string(&opts).unwrap();
221        println!("{serialized}");
222        let deserd: IntegratorOptions = toml::from_str(&serialized).unwrap();
223        assert_eq!(deserd, opts);
224    }
225}