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