Skip to main content

nyx_space/dynamics/guidance/
mnvr.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 super::{
20    ra_dec_from_unit_vector, GuidanceError, GuidanceLaw, GuidancePhysicsSnafu, LocalFrame,
21};
22use crate::cosmic::{GuidanceMode, Spacecraft};
23use crate::dynamics::guidance::unit_vector_from_ra_dec;
24use crate::linalg::Vector3;
25use crate::polyfit::CommonPolynomial;
26use crate::time::{Epoch, Unit};
27use crate::State;
28use anise::prelude::Almanac;
29use hifitime::{Duration, TimeUnits};
30use serde::{Deserialize, Serialize};
31use serde_dhall::{SimpleType, StaticType};
32use snafu::ResultExt;
33use std::collections::HashMap;
34use std::fmt;
35use std::sync::Arc;
36
37/// Impulsive maneuver defines an instantaneous state change which causes a discontinuity in the trajectory.
38/// While useful for preliminary design, it is not typically relevant for spaceflight operations
39#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
40pub struct ImpulsiveManeuver {
41    pub local_frame: LocalFrame,
42    pub dv_km_s: Vector3<f64>,
43}
44
45impl fmt::Display for ImpulsiveManeuver {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(f, "{:.3} m/s in {:?}", self.dv_km_s * 1e3, self.local_frame)
48    }
49}
50
51impl StaticType for ImpulsiveManeuver {
52    fn static_type() -> serde_dhall::SimpleType {
53        let mut fields = HashMap::new();
54
55        let mut dv_rcrd = HashMap::new();
56        dv_rcrd.insert("_1".to_string(), f64::static_type());
57        dv_rcrd.insert("_2".to_string(), f64::static_type());
58        dv_rcrd.insert("_3".to_string(), f64::static_type());
59
60        fields.insert("local_frame".to_string(), LocalFrame::static_type());
61        fields.insert("dv_km_s".to_string(), SimpleType::Record(dv_rcrd));
62
63        SimpleType::Record(fields)
64    }
65}
66
67/// Maneuver defines a single maneuver. Direction MUST be in the VNC frame (Velocity / Normal / Cross).
68/// It may be used with a maneuver scheduler.
69#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
70pub struct Maneuver {
71    /// Start epoch of the maneuver
72    pub start: Epoch,
73    /// End epoch of the maneuver
74    pub end: Epoch,
75    /// TODO: Add a thruster group set to specify which set of thrusters to use for this maneuver, should be a key to a thruster (maybe change thruster to a hashmap actually now that I don't care about embedded stuff).
76    /// Thrust level, if 1.0 use all thruster available at full power
77    /// TODO: Convert this to a common polynomial as well to optimize throttle, throttle rate (and accel?)
78    pub thrust_prct: f64,
79    /// The representation of this maneuver.
80    pub representation: MnvrRepr,
81    /// The frame in which the maneuvers are defined.
82    pub frame: LocalFrame,
83}
84
85impl fmt::Display for Maneuver {
86    /// Prints the polynomial with the least significant coefficients first
87    #[allow(clippy::identity_op)]
88    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
89        if self.end != self.start {
90            let start_vec = self.vector(self.start);
91            let end_vec = self.vector(self.end);
92            write!(
93                f,
94                "Finite burn maneuver @ {:.2}% on {} for {} (ending on {})",
95                100.0 * self.thrust_prct,
96                self.start,
97                self.end - self.start,
98                self.end,
99            )?;
100            write!(f, "\n{}", self.representation)?;
101            write!(
102                f,
103                "\n\tinitial dir: [{:.6}, {:.6}, {:.6}]\n\tfinal dir  : [{:.6}, {:.6}, {:.6}]",
104                start_vec[0], start_vec[1], start_vec[2], end_vec[0], end_vec[1], end_vec[2]
105            )
106        } else {
107            write!(
108                f,
109                "Impulsive maneuver @ {}\n{}",
110                self.start, self.representation
111            )
112        }
113    }
114}
115
116impl StaticType for Maneuver {
117    fn static_type() -> SimpleType {
118        let mut fields = HashMap::new();
119
120        fields.insert("start".to_string(), SimpleType::Text);
121        fields.insert("end".to_string(), SimpleType::Text);
122        fields.insert("thrust_prct".to_string(), SimpleType::Double);
123        fields.insert("representation".to_string(), MnvrRepr::static_type());
124        fields.insert("frame".to_string(), LocalFrame::static_type());
125
126        SimpleType::Record(fields)
127    }
128}
129
130/// Defines the available maneuver representations.
131#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
132pub enum MnvrRepr {
133    /// Represents the maneuver as a fixed vector in the local frame.
134    Vector(Vector3<f64>),
135    /// Represents the maneuver as a polynomial of azimuth (right ascension / in-plane) and elevation (declination / out of plane)
136    Angles {
137        azimuth: CommonPolynomial,
138        elevation: CommonPolynomial,
139    },
140}
141
142impl fmt::Display for MnvrRepr {
143    /// Prints the polynomial with the least significant coefficients first
144    #[allow(clippy::identity_op)]
145    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
146        match self {
147            MnvrRepr::Vector(vector) => write!(f, "{vector}"),
148            MnvrRepr::Angles { azimuth, elevation } => write!(
149                f,
150                "\tazimuth (in-plane) α: {azimuth}\n\televation (out-of-plane) β: {elevation}"
151            ),
152        }
153    }
154}
155
156impl StaticType for MnvrRepr {
157    fn static_type() -> SimpleType {
158        let mut variants = HashMap::new();
159
160        let mut vec_repr = HashMap::new();
161
162        vec_repr.insert("_1".to_string(), f64::static_type());
163        vec_repr.insert("_2".to_string(), f64::static_type());
164        vec_repr.insert("_3".to_string(), f64::static_type());
165
166        variants.insert("Vector".to_string(), Some(SimpleType::Record(vec_repr)));
167
168        // Handle the Angles { azimuth, elevation } variant
169        let mut angles_fields = HashMap::new();
170        angles_fields.insert("azimuth".to_string(), CommonPolynomial::static_type());
171        angles_fields.insert("elevation".to_string(), CommonPolynomial::static_type());
172
173        variants.insert(
174            "Angles".to_string(),
175            Some(SimpleType::Record(angles_fields)),
176        );
177
178        SimpleType::Union(variants)
179    }
180}
181
182impl Maneuver {
183    /// Creates an impulsive maneuver whose vector is the deltaV.
184    /// TODO: This should use William's algorithm
185    pub fn from_impulsive(dt: Epoch, vector: Vector3<f64>, frame: LocalFrame) -> Self {
186        Self::from_time_invariant(dt, dt + Unit::Millisecond, 1.0, vector, frame)
187    }
188
189    /// Creates a maneuver from the provided time-invariant delta-v, in km/s
190    pub fn from_time_invariant(
191        start: Epoch,
192        end: Epoch,
193        thrust_lvl: f64,
194        vector: Vector3<f64>,
195        frame: LocalFrame,
196    ) -> Self {
197        Self {
198            start,
199            end,
200            thrust_prct: thrust_lvl,
201            representation: MnvrRepr::Vector(vector),
202            frame,
203        }
204    }
205
206    /// Return the thrust vector computed at the provided epoch
207    pub fn vector(&self, epoch: Epoch) -> Vector3<f64> {
208        match self.representation {
209            MnvrRepr::Vector(vector) => vector,
210            MnvrRepr::Angles { azimuth, elevation } => {
211                let t = (epoch - self.start).to_seconds();
212                let alpha = azimuth.eval(t);
213                let delta = elevation.eval(t);
214                unit_vector_from_ra_dec(alpha, delta)
215            }
216        }
217    }
218
219    /// Return the duration of this maneuver
220    pub fn duration(&self) -> Duration {
221        self.end - self.start
222    }
223
224    /// Return whether this is an antichronological maneuver
225    pub fn antichronological(&self) -> bool {
226        self.duration().abs() > 1.microseconds() && self.duration() < 1.microseconds()
227    }
228
229    /// Returns the direction of the burn at the start of the burn, useful for setting new angles
230    pub fn direction(&self) -> Vector3<f64> {
231        match self.representation {
232            MnvrRepr::Vector(vector) => vector / vector.norm(),
233            MnvrRepr::Angles { azimuth, elevation } => {
234                let alpha = azimuth.coeff_in_order(0).unwrap();
235                let delta = elevation.coeff_in_order(0).unwrap();
236                unit_vector_from_ra_dec(alpha, delta)
237            }
238        }
239    }
240
241    /// Set the time-invariant direction for this finite burn while keeping the other components as they are
242    pub fn set_direction(&mut self, vector: Vector3<f64>) -> Result<(), GuidanceError> {
243        self.set_direction_and_rates(vector, self.rate(), self.accel())
244    }
245
246    /// Returns the rate of direction of the burn at the start of the burn, useful for setting new angles
247    pub fn rate(&self) -> Vector3<f64> {
248        match self.representation {
249            MnvrRepr::Vector(_) => Vector3::zeros(),
250            MnvrRepr::Angles { azimuth, elevation } => match azimuth.coeff_in_order(1) {
251                Ok(alpha) => {
252                    let delta = elevation.coeff_in_order(1).unwrap();
253                    unit_vector_from_ra_dec(alpha, delta)
254                }
255                Err(_) => Vector3::zeros(),
256            },
257        }
258    }
259
260    /// Set the rate of direction for this finite burn while keeping the other components as they are
261    pub fn set_rate(&mut self, rate: Vector3<f64>) -> Result<(), GuidanceError> {
262        self.set_direction_and_rates(self.direction(), rate, self.accel())
263    }
264
265    /// Returns the acceleration of the burn at the start of the burn, useful for setting new angles
266    pub fn accel(&self) -> Vector3<f64> {
267        match self.representation {
268            MnvrRepr::Vector(_) => Vector3::zeros(),
269            MnvrRepr::Angles { azimuth, elevation } => match azimuth.coeff_in_order(2) {
270                Ok(alpha) => {
271                    let delta = elevation.coeff_in_order(2).unwrap();
272                    unit_vector_from_ra_dec(alpha, delta)
273                }
274                Err(_) => Vector3::zeros(),
275            },
276        }
277    }
278
279    /// Set the acceleration of the direction of this finite burn while keeping the other components as they are
280    pub fn set_accel(&mut self, accel: Vector3<f64>) -> Result<(), GuidanceError> {
281        self.set_direction_and_rates(self.direction(), self.rate(), accel)
282    }
283
284    /// Set the initial direction, direction rate, and direction acceleration for this finite burn
285    pub fn set_direction_and_rates(
286        &mut self,
287        dir: Vector3<f64>,
288        rate: Vector3<f64>,
289        accel: Vector3<f64>,
290    ) -> Result<(), GuidanceError> {
291        if rate.norm() < f64::EPSILON && accel.norm() < f64::EPSILON {
292            // Set as a vector
293            self.representation = MnvrRepr::Vector(dir)
294        } else {
295            let (alpha, delta) = ra_dec_from_unit_vector(dir);
296            if alpha.is_nan() || delta.is_nan() {
297                return Err(GuidanceError::InvalidDirection {
298                    x: dir[0],
299                    y: dir[1],
300                    z: dir[2],
301                    in_plane_deg: alpha.to_degrees(),
302                    out_of_plane_deg: delta.to_degrees(),
303                });
304            }
305            if rate.norm() < f64::EPSILON && accel.norm() < f64::EPSILON {
306                self.representation = MnvrRepr::Angles {
307                    azimuth: CommonPolynomial::Constant { a: alpha },
308                    elevation: CommonPolynomial::Constant { a: delta },
309                };
310            } else {
311                let (alpha_dt, delta_dt) = ra_dec_from_unit_vector(rate);
312                if alpha_dt.is_nan() || delta_dt.is_nan() {
313                    return Err(GuidanceError::InvalidRate {
314                        x: rate[0],
315                        y: rate[1],
316                        z: rate[2],
317                        in_plane_deg_s: alpha_dt.to_degrees(),
318                        out_of_plane_deg_s: delta_dt.to_degrees(),
319                    });
320                }
321                if accel.norm() < f64::EPSILON {
322                    self.representation = MnvrRepr::Angles {
323                        azimuth: CommonPolynomial::Linear {
324                            a: alpha_dt,
325                            b: alpha,
326                        },
327                        elevation: CommonPolynomial::Linear {
328                            a: delta_dt,
329                            b: delta,
330                        },
331                    };
332                } else {
333                    let (alpha_ddt, delta_ddt) = ra_dec_from_unit_vector(accel);
334                    if alpha_ddt.is_nan() || delta_ddt.is_nan() {
335                        return Err(GuidanceError::InvalidAcceleration {
336                            x: accel[0],
337                            y: accel[1],
338                            z: accel[2],
339                            in_plane_deg_s2: alpha_ddt.to_degrees(),
340                            out_of_plane_deg_s2: delta_ddt.to_degrees(),
341                        });
342                    }
343
344                    self.representation = MnvrRepr::Angles {
345                        azimuth: CommonPolynomial::Quadratic {
346                            a: alpha_ddt,
347                            b: alpha_dt,
348                            c: alpha,
349                        },
350                        elevation: CommonPolynomial::Quadratic {
351                            a: delta_ddt,
352                            b: delta_dt,
353                            c: delta,
354                        },
355                    };
356                }
357            }
358        }
359        Ok(())
360    }
361}
362
363impl GuidanceLaw for Maneuver {
364    fn direction(&self, osc: &Spacecraft) -> Result<Vector3<f64>, GuidanceError> {
365        match osc.mode() {
366            GuidanceMode::Thrust => match self.frame {
367                LocalFrame::Inertial => Ok(self.vector(osc.epoch())),
368                _ => Ok(self.frame.dcm_to_inertial(osc.orbit).context({
369                    GuidancePhysicsSnafu {
370                        action: "computing RCN frame",
371                    }
372                })? * self.vector(osc.epoch())),
373            },
374            _ => Ok(Vector3::zeros()),
375        }
376    }
377
378    fn throttle(&self, osc: &Spacecraft) -> Result<f64, GuidanceError> {
379        // match self.next(osc) {
380        match osc.mode() {
381            GuidanceMode::Thrust => Ok(self.thrust_prct),
382            _ => {
383                // We aren't in maneuver mode, so return 0% throttle
384                Ok(0.0)
385            }
386        }
387    }
388
389    fn next(&self, sc: &mut Spacecraft, _almanac: Arc<Almanac>) {
390        let next_mode = if sc.epoch() >= self.start && sc.epoch() <= self.end {
391            GuidanceMode::Thrust
392        } else {
393            GuidanceMode::Coast
394        };
395        sc.mut_mode(next_mode);
396    }
397}
398
399#[cfg(test)]
400mod ut_mnvr {
401    use hifitime::Epoch;
402    use nalgebra::Vector3;
403
404    use crate::dynamics::guidance::LocalFrame;
405
406    use super::Maneuver;
407
408    #[test]
409    fn serde_mnvr() {
410        let epoch = Epoch::from_gregorian_utc_at_midnight(2012, 2, 29);
411        let mnvr = Maneuver::from_impulsive(epoch, Vector3::new(1.0, 1.0, 0.0), LocalFrame::RCN);
412
413        let mnvr_yml = serde_yml::to_string(&mnvr).unwrap();
414        println!("{mnvr_yml}");
415
416        let mnvr2 = serde_yml::from_str(&mnvr_yml).unwrap();
417        assert_eq!(mnvr, mnvr2);
418    }
419}