nyx_space/dynamics/guidance/
ruggiero.rs1use anise::analysis::prelude::OrbitalElement;
20use anise::prelude::Almanac;
21use log::debug;
22use serde::{Deserialize, Serialize};
23use snafu::ResultExt;
24
25use super::{
26 unit_vector_from_plane_angles, GuidStateSnafu, GuidanceError, GuidanceLaw, GuidanceMode,
27 GuidancePhysicsSnafu, NyxError, Orbit, Spacecraft, Vector3,
28};
29use crate::cosmic::eclipse::ShadowModel;
30use crate::dynamics::guidance::ObjectiveEfficiency;
31pub use crate::md::objective::Objective;
32pub use crate::md::StateParameter;
33use crate::State;
34use std::f64::consts::FRAC_PI_2 as half_pi;
35use std::fmt;
36use std::sync::Arc;
37
38#[derive(Clone, Default, Serialize, Deserialize)]
40pub struct Ruggiero {
41 pub objectives: Vec<ObjectiveEfficiency>,
43 pub max_eclipse_prct: Option<f64>,
45 pub init_state: Spacecraft,
46}
47
48impl Ruggiero {
52 pub fn simple(objectives: &[Objective], initial: Spacecraft) -> Result<Arc<Self>, NyxError> {
55 Self::from_ηthresholds(objectives, &[0.0; 5], initial)
56 }
57
58 pub fn from_ηthresholds(
62 objectives: &[Objective],
63 ηthresholds: &[f64],
64 initial: Spacecraft,
65 ) -> Result<Arc<Self>, NyxError> {
66 let mut objs = Vec::with_capacity(5);
67 if objectives.len() > 5 || objectives.is_empty() {
68 return Err(NyxError::GuidanceConfigError {
69 msg: format!(
70 "Must provide between 1 and 5 objectives (included), provided {}",
71 objectives.len()
72 ),
73 });
74 } else if objectives.len() > ηthresholds.len() {
75 return Err(NyxError::GuidanceConfigError {
76 msg: format!(
77 "Must provide at least {} efficiency threshold values, provided {}",
78 objectives.len(),
79 ηthresholds.len()
80 ),
81 });
82 }
83
84 for (obj, ηthreshold) in objectives.iter().copied().zip(ηthresholds.iter().copied()) {
85 if [
86 StateParameter::Element(OrbitalElement::SemiMajorAxis),
87 StateParameter::Element(OrbitalElement::Eccentricity),
88 StateParameter::Element(OrbitalElement::Inclination),
89 StateParameter::Element(OrbitalElement::RAAN),
90 StateParameter::Element(OrbitalElement::AoP),
91 ]
92 .contains(&obj.parameter)
93 {
94 objs.push(ObjectiveEfficiency {
95 objective: obj,
96 efficiency: ηthreshold,
97 });
98 } else {
99 return Err(NyxError::GuidanceConfigError {
100 msg: format!("Objective {} not supported in Ruggerio", obj.parameter),
101 });
102 }
103 }
104 Ok(Arc::new(Self {
105 objectives: objs,
106 init_state: initial,
107 max_eclipse_prct: None,
108 }))
109 }
110
111 pub fn from_max_eclipse(
114 objectives: &[Objective],
115 initial: Spacecraft,
116 max_eclipse: f64,
117 ) -> Result<Arc<Self>, NyxError> {
118 let mut objs = Vec::with_capacity(5);
119 if objectives.len() > 5 || objectives.is_empty() {
120 return Err(NyxError::GuidanceConfigError {
121 msg: format!(
122 "Must provide between 1 and 5 objectives (included), provided {}",
123 objectives.len()
124 ),
125 });
126 }
127 for obj in objectives {
128 if [
129 StateParameter::Element(OrbitalElement::SemiMajorAxis),
130 StateParameter::Element(OrbitalElement::Eccentricity),
131 StateParameter::Element(OrbitalElement::Inclination),
132 StateParameter::Element(OrbitalElement::RAAN),
133 StateParameter::Element(OrbitalElement::AoP),
134 ]
135 .contains(&obj.parameter)
136 {
137 objs.push(ObjectiveEfficiency {
138 objective: *obj,
139 efficiency: 0.0,
140 });
141 } else {
142 return Err(NyxError::GuidanceConfigError {
143 msg: format!("Objective {} not supported in Ruggerio", obj.parameter),
144 });
145 }
146 }
147 Ok(Arc::new(Self {
148 objectives: objs,
149 init_state: initial,
150 max_eclipse_prct: Some(max_eclipse),
151 }))
152 }
153
154 pub fn set_max_eclipse(&mut self, max_eclipse: f64) {
156 self.max_eclipse_prct = Some(max_eclipse);
157 }
158
159 pub fn efficiency(parameter: &StateParameter, osc_orbit: &Orbit) -> Result<f64, GuidanceError> {
161 let e = osc_orbit.ecc().context(GuidancePhysicsSnafu {
162 action: "computing Ruggiero efficiency",
163 })?;
164
165 let ν_ta = osc_orbit
166 .ta_deg()
167 .context(GuidancePhysicsSnafu {
168 action: "computing Ruggiero efficiency",
169 })?
170 .to_radians();
171
172 let ω = osc_orbit
173 .aop_deg()
174 .context(GuidancePhysicsSnafu {
175 action: "computing Ruggiero efficiency",
176 })?
177 .to_radians();
178
179 match parameter {
180 StateParameter::Element(OrbitalElement::SemiMajorAxis) => {
181 let a = osc_orbit.sma_km().context(GuidancePhysicsSnafu {
182 action: "computing Ruggiero efficiency",
183 })?;
184
185 let μ = osc_orbit.frame.mu_km3_s2().context(GuidancePhysicsSnafu {
186 action: "computing Ruggiero efficiency",
187 })?;
188 Ok(osc_orbit.vmag_km_s() * ((a * (1.0 - e)) / (μ * (1.0 + e))).sqrt())
189 }
190 StateParameter::Element(OrbitalElement::Eccentricity) => {
191 let num = 1.0 + 2.0 * e * ν_ta.cos() + ν_ta.cos().powi(2);
192 let denom = 1.0 + e * ν_ta.cos();
193 Ok(num / (2.0 * denom))
197 }
198 StateParameter::Element(OrbitalElement::Inclination) => {
199 let num = (ω + ν_ta).cos().abs()
200 * ((1.0 - e.powi(2) * ω.sin().powi(2)).sqrt() - e * ω.cos().abs());
201 let denom = 1.0 + e * ν_ta.cos();
202 Ok(num / denom)
203 }
204 StateParameter::Element(OrbitalElement::RAAN) => {
205 let num = (ω + ν_ta).sin().abs()
206 * ((1.0 - e.powi(2) * ω.cos().powi(2)).sqrt() - e * ω.sin().abs());
207 let denom = 1.0 + e * ν_ta.cos();
208 Ok(num / denom)
209 }
210 StateParameter::Element(OrbitalElement::AoP) => Ok(1.0),
211 _ => Err(GuidanceError::InvalidControl { param: *parameter }),
212 }
213 }
214
215 fn weighting(&self, obj: &Objective, osc_sc: &Spacecraft, η_threshold: f64) -> f64 {
217 let init = self.init_state.value(obj.parameter).unwrap();
218 let osc = osc_sc.value(obj.parameter).unwrap();
219 let target = obj.desired_value;
220 let tol = obj.tolerance;
221
222 let η = Self::efficiency(&obj.parameter, &osc_sc.orbit).unwrap();
224
225 if (osc - target).abs() < tol || η < η_threshold {
226 0.0
227 } else {
228 (target - osc)
230 / (target
231 - if (init - target).abs() < tol {
232 init + tol
233 } else {
234 init
235 })
236 .abs()
237 }
238 }
239
240 pub fn status(&self, state: &Spacecraft) -> Vec<String> {
242 self.objectives
243 .iter()
244 .map(|obj| {
245 let (ok, err) = obj.objective.assess(state).unwrap();
246 format!(
247 "{} achieved: {}\t error = {:.5} {}",
248 obj.objective,
249 ok,
250 err,
251 obj.objective.parameter.unit()
252 )
253 })
254 .collect::<Vec<String>>()
255 }
256}
257
258impl fmt::Display for Ruggiero {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 let obj_msg = self
261 .objectives
262 .iter()
263 .map(|obj| format!("{}", obj.objective))
264 .collect::<Vec<String>>();
265 write!(
266 f,
267 "Ruggiero Controller (max eclipse: {}): \n {}",
268 match self.max_eclipse_prct {
269 Some(eclp) => format!("{eclp}"),
270 None => "None".to_string(),
271 },
272 obj_msg.join("\n")
273 )
274 }
275}
276
277impl GuidanceLaw for Ruggiero {
278 fn achieved(&self, state: &Spacecraft) -> Result<bool, GuidanceError> {
280 for obj in self.objectives.iter() {
281 if !obj
282 .objective
283 .assess_value(
284 state
285 .value(obj.objective.parameter)
286 .context(GuidStateSnafu)?,
287 )
288 .0
289 {
290 return Ok(false);
291 }
292 }
293 Ok(true)
294 }
295
296 fn direction(&self, sc: &Spacecraft) -> Result<Vector3<f64>, GuidanceError> {
297 if sc.mode() == GuidanceMode::Thrust {
298 let osc = sc.orbit;
299 let mut steering = Vector3::zeros();
300 for objective in &self.objectives {
301 let obj = objective.objective;
302 let ηthreshold = objective.efficiency;
303 let weight = self.weighting(&obj, sc, ηthreshold);
304 if weight.abs() <= 0.0 {
305 continue;
306 }
307
308 let ecc = osc.ecc().context(GuidancePhysicsSnafu {
310 action: "computing Ruggiero guidance",
311 })?;
312
313 let ta_rad = osc
314 .ta_deg()
315 .context(GuidancePhysicsSnafu {
316 action: "computing Ruggiero guidance",
317 })?
318 .to_radians();
319
320 let inc_rad = osc
321 .inc_deg()
322 .context(GuidancePhysicsSnafu {
323 action: "computing Ruggiero guidance",
324 })?
325 .to_radians();
326
327 let aop_rad = osc
328 .aop_deg()
329 .context(GuidancePhysicsSnafu {
330 action: "computing Ruggiero guidance",
331 })?
332 .to_radians();
333
334 let ea_rad = osc
335 .ea_deg()
336 .context(GuidancePhysicsSnafu {
337 action: "computing Ruggiero guidance",
338 })?
339 .to_radians();
340
341 match obj.parameter {
342 StateParameter::Element(OrbitalElement::SemiMajorAxis) => {
343 let num = ecc * ta_rad.sin();
344 let denom = 1.0 + ecc * ta_rad.cos();
345 let alpha = num.atan2(denom);
346 steering += unit_vector_from_plane_angles(alpha, 0.0) * weight;
347 }
348 StateParameter::Element(OrbitalElement::Eccentricity) => {
349 let num = ta_rad.sin();
350 let denom = ta_rad.cos() + ea_rad.cos();
351 let alpha = num.atan2(denom);
352 steering += unit_vector_from_plane_angles(alpha, 0.0) * weight;
353 }
354 StateParameter::Element(OrbitalElement::Inclination) => {
355 let beta = half_pi.copysign((ta_rad + aop_rad).cos());
356 steering += unit_vector_from_plane_angles(0.0, beta) * weight;
357 }
358 StateParameter::Element(OrbitalElement::RAAN) => {
359 let beta = half_pi.copysign((ta_rad + aop_rad).sin());
360 steering += unit_vector_from_plane_angles(0.0, beta) * weight;
361 }
362 StateParameter::Element(OrbitalElement::AoP) => {
363 let oe2 = 1.0 - ecc.powi(2);
364 let e3 = ecc.powi(3);
365 let sqrt_val = (0.25 * (oe2 / e3).powi(2) + 1.0 / 27.0).sqrt();
367 let opti_ta_alpha = ((oe2 / (2.0 * e3) + sqrt_val).powf(1.0 / 3.0)
368 - (-oe2 / (2.0 * e3) + sqrt_val).powf(1.0 / 3.0)
369 - 1.0 / ecc)
370 .acos();
371 let opti_ta_beta = (-ecc * aop_rad.cos()).acos() - aop_rad;
373 if (ta_rad - opti_ta_alpha).abs() < (ta_rad - opti_ta_beta).abs() {
375 let p = osc.semi_parameter_km().context(GuidancePhysicsSnafu {
377 action: "computing Ruggiero guidance",
378 })?;
379 let (sin_ta, cos_ta) = ta_rad.sin_cos();
380 let alpha = (-p * cos_ta).atan2((p + osc.rmag_km()) * sin_ta);
381 steering += unit_vector_from_plane_angles(alpha, 0.0) * weight;
382 } else {
383 let beta = half_pi.copysign(-(ta_rad + aop_rad).sin()) * inc_rad.cos();
385 steering += unit_vector_from_plane_angles(0.0, beta) * weight;
386 };
387 }
388 _ => unreachable!(),
389 }
390 }
391
392 steering = if steering.norm() > 0.0 {
394 steering / steering.norm()
395 } else {
396 steering
397 };
398
399 Ok(osc
401 .dcm_from_rcn_to_inertial()
402 .context(GuidancePhysicsSnafu {
403 action: "computing RCN frame",
404 })?
405 * steering)
406 } else {
407 Ok(Vector3::zeros())
408 }
409 }
410
411 fn throttle(&self, sc: &Spacecraft) -> Result<f64, GuidanceError> {
413 if sc.mode() == GuidanceMode::Thrust {
414 if self.direction(sc)?.norm() > 0.0 {
415 Ok(1.0)
416 } else {
417 Ok(0.0)
418 }
419 } else {
420 Ok(0.0)
421 }
422 }
423
424 fn next(&self, sc: &mut Spacecraft, almanac: Arc<Almanac>) {
426 if sc.mode() != GuidanceMode::Inhibit {
427 if !self.achieved(sc).unwrap() {
428 if let Some(max_eclipse) = self.max_eclipse_prct {
430 let locator = ShadowModel::cislunar(almanac.clone());
431 if locator
432 .compute(sc.orbit, almanac)
433 .expect("cannot compute eclipse")
434 .percentage
435 > max_eclipse
436 {
437 sc.mode = GuidanceMode::Coast;
439 } else {
440 sc.mode = GuidanceMode::Thrust;
441 }
442 } else if sc.mode() == GuidanceMode::Coast {
443 debug!("enabling steering: {:x}", sc.orbit);
444 }
445 sc.mut_mode(GuidanceMode::Thrust);
446 } else {
447 if sc.mode() == GuidanceMode::Thrust {
448 debug!("disabling steering: {:x}", sc.orbit);
449 }
450 sc.mut_mode(GuidanceMode::Coast);
451 }
452 }
453 }
454}
455
456#[test]
457fn ruggiero_weight() {
458 use crate::time::Epoch;
459 use anise::analysis::prelude::OrbitalElement;
460 use anise::constants::frames::EARTH_J2000;
461
462 let eme2k = EARTH_J2000.with_mu_km3_s2(398_600.433);
463 let start_time = Epoch::from_gregorian_tai_at_midnight(2020, 1, 1);
464 let orbit = Orbit::keplerian(7378.1363, 0.01, 0.05, 0.0, 0.0, 1.0, start_time, eme2k);
465 let sc = Spacecraft::new(orbit, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
466
467 let objectives = &[
469 Objective::within_tolerance(
470 StateParameter::Element(OrbitalElement::SemiMajorAxis),
471 42164.0,
472 1.0,
473 ),
474 Objective::within_tolerance(
475 StateParameter::Element(OrbitalElement::Eccentricity),
476 0.01,
477 5e-5,
478 ),
479 ];
480
481 let ruggiero = Ruggiero::simple(objectives, sc).unwrap();
482 let osc = Orbit::new(
484 7_303.253_461_441_64f64,
485 127.478_714_816_381_75,
486 0.111_246_193_227_445_4,
487 -0.128_284_025_765_195_6,
488 7.422_889_151_816_439,
489 0.006_477_694_429_837_2,
490 start_time,
491 eme2k,
492 );
493
494 let mut osc_sc = Spacecraft::new(osc, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
495 osc_sc.mut_mode(GuidanceMode::Thrust);
497
498 let expected = Vector3::new(
499 -0.017_279_636_133_108_3,
500 0.999_850_315_226_803,
501 0.000_872_534_222_883_2,
502 );
503
504 let got = ruggiero.direction(&osc_sc).unwrap();
505
506 assert!(
507 dbg!(expected - got).norm() < 1e-12,
508 "incorrect direction computed"
509 );
510}