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