nyx_space/dynamics/guidance/
mnvr.rs1use 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 snafu::ResultExt;
32use std::fmt;
33use std::sync::Arc;
34
35#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
38pub struct Maneuver {
39 pub start: Epoch,
41 pub end: Epoch,
43 pub thrust_prct: f64,
47 pub representation: MnvrRepr,
49 pub frame: LocalFrame,
51}
52
53impl fmt::Display for Maneuver {
54 #[allow(clippy::identity_op)]
56 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57 if self.end != self.start {
58 let start_vec = self.vector(self.start);
59 let end_vec = self.vector(self.end);
60 write!(
61 f,
62 "Finite burn maneuver @ {:.2}% on {} for {} (ending on {})",
63 100.0 * self.thrust_prct,
64 self.start,
65 self.end - self.start,
66 self.end,
67 )?;
68 write!(f, "\n{}", self.representation)?;
69 write!(
70 f,
71 "\n\tinitial dir: [{:.6}, {:.6}, {:.6}]\n\tfinal dir : [{:.6}, {:.6}, {:.6}]",
72 start_vec[0], start_vec[1], start_vec[2], end_vec[0], end_vec[1], end_vec[2]
73 )
74 } else {
75 write!(
76 f,
77 "Impulsive maneuver @ {}\n{}",
78 self.start, self.representation
79 )
80 }
81 }
82}
83
84#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
86pub enum MnvrRepr {
87 Vector(Vector3<f64>),
89 Angles {
91 azimuth: CommonPolynomial,
92 elevation: CommonPolynomial,
93 },
94}
95
96impl fmt::Display for MnvrRepr {
97 #[allow(clippy::identity_op)]
99 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
100 match self {
101 MnvrRepr::Vector(vector) => write!(f, "{vector}"),
102 MnvrRepr::Angles { azimuth, elevation } => write!(
103 f,
104 "\tazimuth (in-plane) α: {azimuth}\n\televation (out-of-plane) β: {elevation}"
105 ),
106 }
107 }
108}
109
110impl Maneuver {
111 pub fn from_impulsive(dt: Epoch, vector: Vector3<f64>, frame: LocalFrame) -> Self {
114 Self::from_time_invariant(dt, dt + Unit::Millisecond, 1.0, vector, frame)
115 }
116
117 pub fn from_time_invariant(
119 start: Epoch,
120 end: Epoch,
121 thrust_lvl: f64,
122 vector: Vector3<f64>,
123 frame: LocalFrame,
124 ) -> Self {
125 Self {
126 start,
127 end,
128 thrust_prct: thrust_lvl,
129 representation: MnvrRepr::Vector(vector),
130 frame,
131 }
132 }
133
134 pub fn vector(&self, epoch: Epoch) -> Vector3<f64> {
136 match self.representation {
137 MnvrRepr::Vector(vector) => vector,
138 MnvrRepr::Angles { azimuth, elevation } => {
139 let t = (epoch - self.start).to_seconds();
140 let alpha = azimuth.eval(t);
141 let delta = elevation.eval(t);
142 unit_vector_from_ra_dec(alpha, delta)
143 }
144 }
145 }
146
147 pub fn duration(&self) -> Duration {
149 self.end - self.start
150 }
151
152 pub fn antichronological(&self) -> bool {
154 self.duration().abs() > 1.microseconds() && self.duration() < 1.microseconds()
155 }
156
157 pub fn direction(&self) -> Vector3<f64> {
159 match self.representation {
160 MnvrRepr::Vector(vector) => vector / vector.norm(),
161 MnvrRepr::Angles { azimuth, elevation } => {
162 let alpha = azimuth.coeff_in_order(0).unwrap();
163 let delta = elevation.coeff_in_order(0).unwrap();
164 unit_vector_from_ra_dec(alpha, delta)
165 }
166 }
167 }
168
169 pub fn set_direction(&mut self, vector: Vector3<f64>) -> Result<(), GuidanceError> {
171 self.set_direction_and_rates(vector, self.rate(), self.accel())
172 }
173
174 pub fn rate(&self) -> Vector3<f64> {
176 match self.representation {
177 MnvrRepr::Vector(_) => Vector3::zeros(),
178 MnvrRepr::Angles { azimuth, elevation } => match azimuth.coeff_in_order(1) {
179 Ok(alpha) => {
180 let delta = elevation.coeff_in_order(1).unwrap();
181 unit_vector_from_ra_dec(alpha, delta)
182 }
183 Err(_) => Vector3::zeros(),
184 },
185 }
186 }
187
188 pub fn set_rate(&mut self, rate: Vector3<f64>) -> Result<(), GuidanceError> {
190 self.set_direction_and_rates(self.direction(), rate, self.accel())
191 }
192
193 pub fn accel(&self) -> Vector3<f64> {
195 match self.representation {
196 MnvrRepr::Vector(_) => Vector3::zeros(),
197 MnvrRepr::Angles { azimuth, elevation } => match azimuth.coeff_in_order(2) {
198 Ok(alpha) => {
199 let delta = elevation.coeff_in_order(2).unwrap();
200 unit_vector_from_ra_dec(alpha, delta)
201 }
202 Err(_) => Vector3::zeros(),
203 },
204 }
205 }
206
207 pub fn set_accel(&mut self, accel: Vector3<f64>) -> Result<(), GuidanceError> {
209 self.set_direction_and_rates(self.direction(), self.rate(), accel)
210 }
211
212 pub fn set_direction_and_rates(
214 &mut self,
215 dir: Vector3<f64>,
216 rate: Vector3<f64>,
217 accel: Vector3<f64>,
218 ) -> Result<(), GuidanceError> {
219 if rate.norm() < f64::EPSILON && accel.norm() < f64::EPSILON {
220 self.representation = MnvrRepr::Vector(dir)
222 } else {
223 let (alpha, delta) = ra_dec_from_unit_vector(dir);
224 if alpha.is_nan() || delta.is_nan() {
225 return Err(GuidanceError::InvalidDirection {
226 x: dir[0],
227 y: dir[1],
228 z: dir[2],
229 in_plane_deg: alpha.to_degrees(),
230 out_of_plane_deg: delta.to_degrees(),
231 });
232 }
233 if rate.norm() < f64::EPSILON && accel.norm() < f64::EPSILON {
234 self.representation = MnvrRepr::Angles {
235 azimuth: CommonPolynomial::Constant(alpha),
236 elevation: CommonPolynomial::Constant(delta),
237 };
238 } else {
239 let (alpha_dt, delta_dt) = ra_dec_from_unit_vector(rate);
240 if alpha_dt.is_nan() || delta_dt.is_nan() {
241 return Err(GuidanceError::InvalidRate {
242 x: rate[0],
243 y: rate[1],
244 z: rate[2],
245 in_plane_deg_s: alpha_dt.to_degrees(),
246 out_of_plane_deg_s: delta_dt.to_degrees(),
247 });
248 }
249 if accel.norm() < f64::EPSILON {
250 self.representation = MnvrRepr::Angles {
251 azimuth: CommonPolynomial::Linear(alpha_dt, alpha),
252 elevation: CommonPolynomial::Linear(delta_dt, delta),
253 };
254 } else {
255 let (alpha_ddt, delta_ddt) = ra_dec_from_unit_vector(accel);
256 if alpha_ddt.is_nan() || delta_ddt.is_nan() {
257 return Err(GuidanceError::InvalidAcceleration {
258 x: accel[0],
259 y: accel[1],
260 z: accel[2],
261 in_plane_deg_s2: alpha_ddt.to_degrees(),
262 out_of_plane_deg_s2: delta_ddt.to_degrees(),
263 });
264 }
265
266 self.representation = MnvrRepr::Angles {
267 azimuth: CommonPolynomial::Quadratic(alpha_ddt, alpha_dt, alpha),
268 elevation: CommonPolynomial::Quadratic(delta_ddt, delta_dt, delta),
269 };
270 }
271 }
272 }
273 Ok(())
274 }
275}
276
277impl GuidanceLaw for Maneuver {
278 fn direction(&self, osc: &Spacecraft) -> Result<Vector3<f64>, GuidanceError> {
279 match osc.mode() {
280 GuidanceMode::Thrust => match self.frame {
281 LocalFrame::Inertial => Ok(self.vector(osc.epoch())),
282 _ => Ok(self.frame.dcm_to_inertial(osc.orbit).context({
283 GuidancePhysicsSnafu {
284 action: "computing RCN frame",
285 }
286 })? * self.vector(osc.epoch())),
287 },
288 _ => Ok(Vector3::zeros()),
289 }
290 }
291
292 fn throttle(&self, osc: &Spacecraft) -> Result<f64, GuidanceError> {
293 match osc.mode() {
295 GuidanceMode::Thrust => Ok(self.thrust_prct),
296 _ => {
297 Ok(0.0)
299 }
300 }
301 }
302
303 fn next(&self, sc: &mut Spacecraft, _almanac: Arc<Almanac>) {
304 let next_mode = if sc.epoch() >= self.start && sc.epoch() < self.end {
305 GuidanceMode::Thrust
306 } else {
307 GuidanceMode::Coast
308 };
309 sc.mut_mode(next_mode);
310 }
311}
312
313#[cfg(test)]
314mod ut_mnvr {
315 use hifitime::Epoch;
316 use nalgebra::Vector3;
317
318 use crate::dynamics::guidance::LocalFrame;
319
320 use super::Maneuver;
321
322 #[test]
323 fn serde_mnvr() {
324 let epoch = Epoch::from_gregorian_utc_at_midnight(2012, 2, 29);
325 let mnvr = Maneuver::from_impulsive(epoch, Vector3::new(1.0, 1.0, 0.0), LocalFrame::RCN);
326
327 let mnvr_yml = serde_yml::to_string(&mnvr).unwrap();
328 println!("{mnvr_yml}");
329
330 let mnvr2 = serde_yml::from_str(&mnvr_yml).unwrap();
331 assert_eq!(mnvr, mnvr2);
332 }
333}