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) α: {}\n\televation (out-of-plane) β: {}",
105 azimuth, elevation
106 ),
107 }
108 }
109}
110
111impl Maneuver {
112 pub fn from_impulsive(dt: Epoch, vector: Vector3<f64>, frame: LocalFrame) -> Self {
115 Self::from_time_invariant(dt, dt + Unit::Millisecond, 1.0, vector, frame)
116 }
117
118 pub fn from_time_invariant(
120 start: Epoch,
121 end: Epoch,
122 thrust_lvl: f64,
123 vector: Vector3<f64>,
124 frame: LocalFrame,
125 ) -> Self {
126 Self {
127 start,
128 end,
129 thrust_prct: thrust_lvl,
130 representation: MnvrRepr::Vector(vector),
131 frame,
132 }
133 }
134
135 pub fn vector(&self, epoch: Epoch) -> Vector3<f64> {
137 match self.representation {
138 MnvrRepr::Vector(vector) => vector,
139 MnvrRepr::Angles { azimuth, elevation } => {
140 let t = (epoch - self.start).to_seconds();
141 let alpha = azimuth.eval(t);
142 let delta = elevation.eval(t);
143 unit_vector_from_ra_dec(alpha, delta)
144 }
145 }
146 }
147
148 pub fn duration(&self) -> Duration {
150 self.end - self.start
151 }
152
153 pub fn antichronological(&self) -> bool {
155 self.duration().abs() > 1.microseconds() && self.duration() < 1.microseconds()
156 }
157
158 pub fn direction(&self) -> Vector3<f64> {
160 match self.representation {
161 MnvrRepr::Vector(vector) => vector / vector.norm(),
162 MnvrRepr::Angles { azimuth, elevation } => {
163 let alpha = azimuth.coeff_in_order(0).unwrap();
164 let delta = elevation.coeff_in_order(0).unwrap();
165 unit_vector_from_ra_dec(alpha, delta)
166 }
167 }
168 }
169
170 pub fn set_direction(&mut self, vector: Vector3<f64>) -> Result<(), GuidanceError> {
172 self.set_direction_and_rates(vector, self.rate(), self.accel())
173 }
174
175 pub fn rate(&self) -> Vector3<f64> {
177 match self.representation {
178 MnvrRepr::Vector(_) => Vector3::zeros(),
179 MnvrRepr::Angles { azimuth, elevation } => match azimuth.coeff_in_order(1) {
180 Ok(alpha) => {
181 let delta = elevation.coeff_in_order(1).unwrap();
182 unit_vector_from_ra_dec(alpha, delta)
183 }
184 Err(_) => Vector3::zeros(),
185 },
186 }
187 }
188
189 pub fn set_rate(&mut self, rate: Vector3<f64>) -> Result<(), GuidanceError> {
191 self.set_direction_and_rates(self.direction(), rate, self.accel())
192 }
193
194 pub fn accel(&self) -> Vector3<f64> {
196 match self.representation {
197 MnvrRepr::Vector(_) => Vector3::zeros(),
198 MnvrRepr::Angles { azimuth, elevation } => match azimuth.coeff_in_order(2) {
199 Ok(alpha) => {
200 let delta = elevation.coeff_in_order(2).unwrap();
201 unit_vector_from_ra_dec(alpha, delta)
202 }
203 Err(_) => Vector3::zeros(),
204 },
205 }
206 }
207
208 pub fn set_accel(&mut self, accel: Vector3<f64>) -> Result<(), GuidanceError> {
210 self.set_direction_and_rates(self.direction(), self.rate(), accel)
211 }
212
213 pub fn set_direction_and_rates(
215 &mut self,
216 dir: Vector3<f64>,
217 rate: Vector3<f64>,
218 accel: Vector3<f64>,
219 ) -> Result<(), GuidanceError> {
220 if rate.norm() < f64::EPSILON && accel.norm() < f64::EPSILON {
221 self.representation = MnvrRepr::Vector(dir)
223 } else {
224 let (alpha, delta) = ra_dec_from_unit_vector(dir);
225 if alpha.is_nan() || delta.is_nan() {
226 return Err(GuidanceError::InvalidDirection {
227 x: dir[0],
228 y: dir[1],
229 z: dir[2],
230 in_plane_deg: alpha.to_degrees(),
231 out_of_plane_deg: delta.to_degrees(),
232 });
233 }
234 if rate.norm() < f64::EPSILON && accel.norm() < f64::EPSILON {
235 self.representation = MnvrRepr::Angles {
236 azimuth: CommonPolynomial::Constant(alpha),
237 elevation: CommonPolynomial::Constant(delta),
238 };
239 } else {
240 let (alpha_dt, delta_dt) = ra_dec_from_unit_vector(rate);
241 if alpha_dt.is_nan() || delta_dt.is_nan() {
242 return Err(GuidanceError::InvalidRate {
243 x: rate[0],
244 y: rate[1],
245 z: rate[2],
246 in_plane_deg_s: alpha_dt.to_degrees(),
247 out_of_plane_deg_s: delta_dt.to_degrees(),
248 });
249 }
250 if accel.norm() < f64::EPSILON {
251 self.representation = MnvrRepr::Angles {
252 azimuth: CommonPolynomial::Linear(alpha_dt, alpha),
253 elevation: CommonPolynomial::Linear(delta_dt, delta),
254 };
255 } else {
256 let (alpha_ddt, delta_ddt) = ra_dec_from_unit_vector(accel);
257 if alpha_ddt.is_nan() || delta_ddt.is_nan() {
258 return Err(GuidanceError::InvalidAcceleration {
259 x: accel[0],
260 y: accel[1],
261 z: accel[2],
262 in_plane_deg_s2: alpha_ddt.to_degrees(),
263 out_of_plane_deg_s2: delta_ddt.to_degrees(),
264 });
265 }
266
267 self.representation = MnvrRepr::Angles {
268 azimuth: CommonPolynomial::Quadratic(alpha_ddt, alpha_dt, alpha),
269 elevation: CommonPolynomial::Quadratic(delta_ddt, delta_dt, delta),
270 };
271 }
272 }
273 }
274 Ok(())
275 }
276}
277
278impl GuidanceLaw for Maneuver {
279 fn direction(&self, osc: &Spacecraft) -> Result<Vector3<f64>, GuidanceError> {
280 match osc.mode() {
281 GuidanceMode::Thrust => match self.frame {
282 LocalFrame::Inertial => Ok(self.vector(osc.epoch())),
283 _ => Ok(self.frame.dcm_to_inertial(osc.orbit).context({
284 GuidancePhysicsSnafu {
285 action: "computing RCN frame",
286 }
287 })? * self.vector(osc.epoch())),
288 },
289 _ => Ok(Vector3::zeros()),
290 }
291 }
292
293 fn throttle(&self, osc: &Spacecraft) -> Result<f64, GuidanceError> {
294 match osc.mode() {
296 GuidanceMode::Thrust => Ok(self.thrust_prct),
297 _ => {
298 Ok(0.0)
300 }
301 }
302 }
303
304 fn next(&self, sc: &mut Spacecraft, _almanac: Arc<Almanac>) {
305 let next_mode = if sc.epoch() >= self.start && sc.epoch() < self.end {
306 GuidanceMode::Thrust
307 } else {
308 GuidanceMode::Coast
309 };
310 sc.mut_mode(next_mode);
311 }
312}
313
314#[cfg(test)]
315mod ut_mnvr {
316 use hifitime::Epoch;
317 use nalgebra::Vector3;
318
319 use crate::dynamics::guidance::LocalFrame;
320
321 use super::Maneuver;
322
323 #[test]
324 fn serde_mnvr() {
325 let epoch = Epoch::from_gregorian_utc_at_midnight(2012, 2, 29);
326 let mnvr = Maneuver::from_impulsive(epoch, Vector3::new(1.0, 1.0, 0.0), LocalFrame::RCN);
327
328 let mnvr_yml = serde_yml::to_string(&mnvr).unwrap();
329 println!("{mnvr_yml}");
330
331 let mnvr2 = serde_yml::from_str(&mnvr_yml).unwrap();
332 assert_eq!(mnvr, mnvr2);
333 }
334}