1use anise::prelude::Almanac;
20use serde::{Deserialize, Serialize};
21use snafu::ResultExt;
22
23use super::{
24 unit_vector_from_plane_angles, GuidStateSnafu, GuidanceError, GuidanceLaw, GuidanceMode,
25 GuidancePhysicsSnafu, NyxError, Orbit, Spacecraft, Vector3,
26};
27use crate::cosmic::eclipse::EclipseLocator;
28pub use crate::md::objective::Objective;
29pub use crate::md::StateParameter;
30use crate::State;
31use std::f64::consts::FRAC_PI_2 as half_pi;
32use std::fmt;
33use std::sync::Arc;
34
35#[derive(Copy, Clone, Default, Serialize, Deserialize)]
37pub struct Ruggiero {
38 pub objectives: [Option<Objective>; 5],
40 pub ηthresholds: [f64; 5],
42 pub max_eclipse_prct: Option<f64>,
44 init_state: Spacecraft,
45}
46
47impl Ruggiero {
51 pub fn simple(objectives: &[Objective], initial: Spacecraft) -> Result<Arc<Self>, NyxError> {
54 Self::from_ηthresholds(objectives, &[0.0; 5], initial)
55 }
56
57 pub fn from_ηthresholds(
61 objectives: &[Objective],
62 ηthresholds: &[f64],
63 initial: Spacecraft,
64 ) -> Result<Arc<Self>, NyxError> {
65 let mut objs: [Option<Objective>; 5] = [None, None, None, None, None];
66 let mut eff: [f64; 5] = [0.0; 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 (i, obj) in objectives.iter().enumerate() {
85 if [
86 StateParameter::SMA,
87 StateParameter::Eccentricity,
88 StateParameter::Inclination,
89 StateParameter::RAAN,
90 StateParameter::AoP,
91 ]
92 .contains(&obj.parameter)
93 {
94 objs[i] = Some(*obj);
95 } else {
96 return Err(NyxError::GuidanceConfigError {
97 msg: format!("Objective {} not supported in Ruggerio", obj.parameter),
98 });
99 }
100 }
101 for i in 0..objectives.len() {
102 objs[i] = Some(objectives[i]);
103 eff[i] = ηthresholds[i];
104 }
105 Ok(Arc::new(Self {
106 objectives: objs,
107 init_state: initial,
108 ηthresholds: eff,
109 max_eclipse_prct: None,
110 }))
111 }
112
113 pub fn from_max_eclipse(
116 objectives: &[Objective],
117 initial: Spacecraft,
118 max_eclipse: f64,
119 ) -> Result<Arc<Self>, NyxError> {
120 let mut objs: [Option<Objective>; 5] = [None, None, None, None, None];
121 let eff: [f64; 5] = [0.0; 5];
122 if objectives.len() > 5 || objectives.is_empty() {
123 return Err(NyxError::GuidanceConfigError {
124 msg: format!(
125 "Must provide between 1 and 5 objectives (included), provided {}",
126 objectives.len()
127 ),
128 });
129 }
130
131 for (i, obj) in objectives.iter().enumerate() {
132 if [
133 StateParameter::SMA,
134 StateParameter::Eccentricity,
135 StateParameter::Inclination,
136 StateParameter::RAAN,
137 StateParameter::AoP,
138 ]
139 .contains(&obj.parameter)
140 {
141 objs[i] = Some(*obj);
142 } else {
143 return Err(NyxError::GuidanceConfigError {
144 msg: format!("Objective {} not supported in Ruggerio", obj.parameter),
145 });
146 }
147 }
148 for i in 0..objectives.len() {
149 objs[i] = Some(objectives[i]);
150 }
151 Ok(Arc::new(Self {
152 objectives: objs,
153 init_state: initial,
154 ηthresholds: eff,
155 max_eclipse_prct: Some(max_eclipse),
156 }))
157 }
158
159 pub fn set_max_eclipse(&mut self, max_eclipse: f64) {
161 self.max_eclipse_prct = Some(max_eclipse);
162 }
163
164 pub fn efficency(parameter: &StateParameter, osc_orbit: &Orbit) -> Result<f64, GuidanceError> {
166 let e = osc_orbit.ecc().context(GuidancePhysicsSnafu {
167 action: "computing Ruggiero efficency",
168 })?;
169
170 let ν_ta = osc_orbit
171 .ta_deg()
172 .context(GuidancePhysicsSnafu {
173 action: "computing Ruggiero efficency",
174 })?
175 .to_radians();
176
177 let ω = osc_orbit
178 .aop_deg()
179 .context(GuidancePhysicsSnafu {
180 action: "computing Ruggiero efficency",
181 })?
182 .to_radians();
183
184 match parameter {
185 StateParameter::SMA => {
186 let a = osc_orbit.sma_km().context(GuidancePhysicsSnafu {
187 action: "computing Ruggiero efficency",
188 })?;
189
190 let μ = osc_orbit.frame.mu_km3_s2().context(GuidancePhysicsSnafu {
191 action: "computing Ruggiero efficency",
192 })?;
193 Ok(osc_orbit.vmag_km_s() * ((a * (1.0 - e)) / (μ * (1.0 + e))).sqrt())
194 }
195 StateParameter::Eccentricity => {
196 let num = 1.0 + 2.0 * e * ν_ta.cos() + ν_ta.cos().powi(2);
197 let denom = 1.0 + e * ν_ta.cos();
198 Ok(num / (2.0 * denom))
202 }
203 StateParameter::Inclination => {
204 let num = (ω + ν_ta).cos().abs()
205 * ((1.0 - e.powi(2) * ω.sin().powi(2)).sqrt() - e * ω.cos().abs());
206 let denom = 1.0 + e * ν_ta.cos();
207 Ok(num / denom)
208 }
209 StateParameter::RAAN => {
210 let num = (ω + ν_ta).sin().abs()
211 * ((1.0 - e.powi(2) * ω.cos().powi(2)).sqrt() - e * ω.sin().abs());
212 let denom = 1.0 + e * ν_ta.cos();
213 Ok(num / denom)
214 }
215 StateParameter::AoP => Ok(1.0),
216 _ => Err(GuidanceError::InvalidControl { param: *parameter }),
217 }
218 }
219
220 fn weighting(&self, obj: &Objective, osc_sc: &Spacecraft, η_threshold: f64) -> f64 {
222 let init = self.init_state.value(obj.parameter).unwrap();
223 let osc = osc_sc.value(obj.parameter).unwrap();
224 let target = obj.desired_value;
225 let tol = obj.tolerance;
226
227 let η = Self::efficency(&obj.parameter, &osc_sc.orbit).unwrap();
229
230 if (osc - target).abs() < tol || η < η_threshold {
231 0.0
232 } else {
233 (target - osc)
235 / (target
236 - if (init - target).abs() < tol {
237 init + tol
238 } else {
239 init
240 })
241 .abs()
242 }
243 }
244
245 pub fn status(&self, state: &Spacecraft) -> Vec<String> {
247 self.objectives
248 .iter()
249 .flatten()
250 .map(|obj| {
251 let (ok, err) = obj.assess(state).unwrap();
252 format!(
253 "{} achieved: {}\t error = {:.5} {}",
254 obj,
255 ok,
256 err,
257 obj.parameter.unit()
258 )
259 })
260 .collect::<Vec<String>>()
261 }
262}
263
264impl fmt::Display for Ruggiero {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 let obj_msg = self
267 .objectives
268 .iter()
269 .flatten()
270 .map(|obj| format!("{obj}"))
271 .collect::<Vec<String>>();
272 write!(
273 f,
274 "Ruggiero Controller (max eclipse: {}): \n {}",
275 match self.max_eclipse_prct {
276 Some(eclp) => format!("{eclp}"),
277 None => "None".to_string(),
278 },
279 obj_msg.join("\n")
280 )
281 }
282}
283
284impl GuidanceLaw for Ruggiero {
285 fn achieved(&self, state: &Spacecraft) -> Result<bool, GuidanceError> {
287 for obj in self.objectives.iter().flatten() {
288 if !obj
289 .assess_value(state.value(obj.parameter).context(GuidStateSnafu)?)
290 .0
291 {
292 return Ok(false);
293 }
294 }
295 Ok(true)
296 }
297
298 fn direction(&self, sc: &Spacecraft) -> Result<Vector3<f64>, GuidanceError> {
299 if sc.mode() == GuidanceMode::Thrust {
300 let osc = sc.orbit;
301 let mut steering = Vector3::zeros();
302 for (i, obj) in self.objectives.iter().flatten().enumerate() {
303 let weight = self.weighting(obj, sc, self.ηthresholds[i]);
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::SMA => {
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::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::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::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::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 = EclipseLocator::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::constants::frames::EARTH_J2000;
460
461 let eme2k = EARTH_J2000.with_mu_km3_s2(398_600.433);
462 let start_time = Epoch::from_gregorian_tai_at_midnight(2020, 1, 1);
463 let orbit = Orbit::keplerian(7378.1363, 0.01, 0.05, 0.0, 0.0, 1.0, start_time, eme2k);
464 let sc = Spacecraft::new(orbit, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
465
466 let objectives = &[
468 Objective::within_tolerance(StateParameter::SMA, 42164.0, 1.0),
469 Objective::within_tolerance(StateParameter::Eccentricity, 0.01, 5e-5),
470 ];
471
472 let ruggiero = Ruggiero::simple(objectives, sc).unwrap();
473 let osc = Orbit::new(
475 7_303.253_461_441_64f64,
476 127.478_714_816_381_75,
477 0.111_246_193_227_445_4,
478 -0.128_284_025_765_195_6,
479 7.422_889_151_816_439,
480 0.006_477_694_429_837_2,
481 start_time,
482 eme2k,
483 );
484
485 let mut osc_sc = Spacecraft::new(osc, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
486 osc_sc.mut_mode(GuidanceMode::Thrust);
488
489 let expected = Vector3::new(
490 -0.017_279_636_133_108_3,
491 0.999_850_315_226_803,
492 0.000_872_534_222_883_2,
493 );
494
495 let got = ruggiero.direction(&osc_sc).unwrap();
496
497 assert!(
498 dbg!(expected - got).norm() < 1e-12,
499 "incorrect direction computed"
500 );
501}