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