Skip to main content

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 serde_dhall::{SimpleType, StaticType};
20use std::collections::HashMap;
21use std::fmt;
22
23use crate::time::{Duration, Unit};
24
25use super::ErrorControl;
26use anise::frames::Frame;
27use serde::{Deserialize, Serialize};
28use typed_builder::TypedBuilder;
29
30#[cfg(feature = "python")]
31use pyo3::prelude::*;
32
33/// Stores the integrator options, including the minimum and maximum step sizes, and the central body to perform the integration.
34///
35/// Note that different step sizes and max errors are only used for adaptive
36/// methods. To use a fixed step integrator, initialize the options using `with_fixed_step`, and
37/// use whichever adaptive step integrator is desired.  For example, initializing an RK45 with
38/// fixed step options will lead to an RK4 being used instead of an RK45.
39///
40/// :type min_step: Duration | None
41/// :type max_step: Duration | None
42/// :type tolerance: float | None
43#[derive(Clone, Copy, Debug, TypedBuilder, Serialize, Deserialize, PartialEq)]
44#[cfg_attr(feature = "python", pyclass(from_py_object))]
45#[builder(doc)]
46pub struct IntegratorOptions {
47    #[builder(default_code = "60.0 * Unit::Second")]
48    pub init_step: Duration,
49    #[builder(default_code = "0.001 * Unit::Second")]
50    pub min_step: Duration,
51    #[builder(default_code = "2700.0 * Unit::Second")]
52    pub max_step: Duration,
53    #[builder(default = 1e-12)]
54    pub tolerance: f64,
55    #[builder(default = 50)]
56    pub attempts: u8,
57    #[builder(default = false)]
58    pub fixed_step: bool,
59    #[builder(default)]
60    pub error_ctrl: ErrorControl,
61    /// If a frame is specified and the propagator state is in a different frame, it it changed to this frame prior to integration.
62    /// Note, when setting this, it's recommended to call `strip` on the Frame.
63    #[builder(default, setter(strip_option))]
64    pub integration_frame: Option<Frame>,
65}
66
67impl IntegratorOptions {
68    /// `with_adaptive_step` initializes an `PropOpts` such that the integrator is used with an
69    ///  adaptive step size. The number of attempts is currently fixed to 50 (as in GMAT).
70    pub fn with_adaptive_step(
71        min_step: Duration,
72        max_step: Duration,
73        tolerance: f64,
74        error_ctrl: ErrorControl,
75    ) -> Self {
76        IntegratorOptions {
77            init_step: max_step,
78            min_step,
79            max_step,
80            tolerance,
81            attempts: 50,
82            fixed_step: false,
83            error_ctrl,
84            integration_frame: None,
85        }
86    }
87
88    pub fn with_adaptive_step_s(
89        min_step: f64,
90        max_step: f64,
91        tolerance: f64,
92        error_ctrl: ErrorControl,
93    ) -> Self {
94        Self::with_adaptive_step(
95            min_step * Unit::Second,
96            max_step * Unit::Second,
97            tolerance,
98            error_ctrl,
99        )
100    }
101
102    /// `with_fixed_step` initializes an `PropOpts` such that the integrator is used with a fixed
103    ///  step size.
104    pub fn with_fixed_step(step: Duration) -> Self {
105        IntegratorOptions {
106            init_step: step,
107            min_step: step,
108            max_step: step,
109            tolerance: 0.0,
110            fixed_step: true,
111            attempts: 0,
112            error_ctrl: ErrorControl::RSSCartesianStep,
113            integration_frame: None,
114        }
115    }
116
117    pub fn with_fixed_step_s(step: f64) -> Self {
118        Self::with_fixed_step(step * Unit::Second)
119    }
120
121    /// Returns the default options with a specific tolerance.
122    #[allow(clippy::field_reassign_with_default)]
123    pub fn with_tolerance(tolerance: f64) -> Self {
124        let mut opts = Self::default();
125        opts.tolerance = tolerance;
126        opts
127    }
128
129    /// Creates a propagator with the provided max step, and sets the initial step to that value as well.
130    #[allow(clippy::field_reassign_with_default)]
131    pub fn with_max_step(max_step: Duration) -> Self {
132        let mut opts = Self::default();
133        opts.set_max_step(max_step);
134        opts
135    }
136}
137
138#[cfg_attr(feature = "python", pymethods)]
139impl IntegratorOptions {
140    /// Returns a string with the information about these options
141    /// :rtype: str
142    pub fn info(&self) -> String {
143        format!("{self}")
144    }
145
146    /// Set the maximum step size and sets the initial step to that value if currently greater
147    ///
148    /// :type max_step: Duration
149    /// :rtype: None
150    pub fn set_max_step(&mut self, max_step: Duration) {
151        if self.init_step > max_step {
152            self.init_step = max_step;
153        }
154        self.max_step = max_step;
155    }
156
157    /// Set the minimum step size and sets the initial step to that value if currently smaller
158    ///
159    /// :type min_step: Duration
160    /// :rtype: None
161    pub fn set_min_step(&mut self, min_step: Duration) {
162        if self.init_step < min_step {
163            self.init_step = min_step;
164        }
165        self.min_step = min_step;
166    }
167}
168
169impl fmt::Display for IntegratorOptions {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        if self.fixed_step {
172            write!(f, "fixed step: {:e}", self.min_step)
173        } else {
174            write!(
175                f,
176                "min_step: {:e}, max_step: {:e}, tol: {:e}, attempts: {}",
177                self.min_step, self.max_step, self.tolerance, self.attempts,
178            )
179        }
180    }
181}
182
183impl Default for IntegratorOptions {
184    /// `default` returns the same default options as GMAT.
185    fn default() -> IntegratorOptions {
186        IntegratorOptions {
187            init_step: 60.0 * Unit::Second,
188            min_step: 0.001 * Unit::Second,
189            max_step: 2700.0 * Unit::Second,
190            tolerance: 1e-12,
191            attempts: 50,
192            fixed_step: false,
193            error_ctrl: ErrorControl::RSSCartesianStep,
194            integration_frame: None,
195        }
196    }
197}
198
199impl StaticType for IntegratorOptions {
200    fn static_type() -> SimpleType {
201        let mut fields = HashMap::new();
202
203        // Duration fields (handled as strings/Text)
204        fields.insert("init_step".to_string(), SimpleType::Text);
205        fields.insert("min_step".to_string(), SimpleType::Text);
206        fields.insert("max_step".to_string(), SimpleType::Text);
207
208        // Standard scalars
209        fields.insert("tolerance".to_string(), SimpleType::Double);
210        fields.insert("attempts".to_string(), SimpleType::Natural);
211        fields.insert("fixed_step".to_string(), SimpleType::Bool);
212
213        // Nested types
214        // Note: ErrorControl must also implement StaticType
215        fields.insert("error_ctrl".to_string(), ErrorControl::static_type());
216
217        // Optional field
218        fields.insert(
219            "integration_frame".to_string(),
220            SimpleType::Optional(Box::new(Frame::static_type())),
221        );
222
223        SimpleType::Record(fields)
224    }
225}
226#[cfg(test)]
227mod ut_integr_opts {
228    use hifitime::Unit;
229
230    use crate::propagators::{ErrorControl, IntegratorOptions};
231
232    #[test]
233    fn test_options() {
234        let opts = IntegratorOptions::with_fixed_step_s(1e-1);
235        assert_eq!(opts.min_step, 1e-1 * Unit::Second);
236        assert_eq!(opts.max_step, 1e-1 * Unit::Second);
237        assert!(opts.tolerance.abs() < f64::EPSILON);
238        assert!(opts.fixed_step);
239
240        let opts =
241            IntegratorOptions::with_adaptive_step_s(1e-2, 10.0, 1e-12, ErrorControl::RSSStep);
242        assert_eq!(opts.min_step, 1e-2 * Unit::Second);
243        assert_eq!(opts.max_step, 10.0 * Unit::Second);
244        assert!((opts.tolerance - 1e-12).abs() < f64::EPSILON);
245        assert!(!opts.fixed_step);
246
247        let opts: IntegratorOptions = Default::default();
248        assert_eq!(opts.init_step, 60.0 * Unit::Second);
249        assert_eq!(opts.min_step, 0.001 * Unit::Second);
250        assert_eq!(opts.max_step, 2700.0 * Unit::Second);
251        assert!((opts.tolerance - 1e-12).abs() < f64::EPSILON);
252        assert_eq!(opts.attempts, 50);
253        assert!(!opts.fixed_step);
254
255        let opts = IntegratorOptions::with_max_step(1.0 * Unit::Second);
256        assert_eq!(opts.init_step, 1.0 * Unit::Second);
257        assert_eq!(opts.min_step, 0.001 * Unit::Second);
258        assert_eq!(opts.max_step, 1.0 * Unit::Second);
259        assert!((opts.tolerance - 1e-12).abs() < f64::EPSILON);
260        assert_eq!(opts.attempts, 50);
261        assert!(!opts.fixed_step);
262    }
263
264    #[test]
265    fn test_serde() {
266        let opts = IntegratorOptions::default();
267        let serialized = toml::to_string(&opts).unwrap();
268        println!("{serialized}");
269        let deserd: IntegratorOptions = toml::from_str(&serialized).unwrap();
270        assert_eq!(deserd, opts);
271    }
272}