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