nyx_space/dynamics/guidance/
kluever.rs1use anise::analysis::prelude::OrbitalElement;
20use anise::prelude::Almanac;
21use log::debug;
22use serde::{Deserialize, Serialize};
23use snafu::ResultExt;
24
25use super::{
26 GuidStateSnafu, GuidanceError, GuidanceLaw, GuidanceMode, GuidancePhysicsSnafu, Spacecraft,
27 Vector3,
28};
29use crate::cosmic::eclipse::ShadowModel;
30use crate::dynamics::guidance::ObjectiveWeight;
31pub use crate::md::objective::Objective;
32pub use crate::md::StateParameter;
33use crate::State;
34use std::fmt;
35use std::sync::Arc;
36
37#[derive(Clone, Serialize, Deserialize)]
39pub struct Kluever {
40 pub objectives: Vec<ObjectiveWeight>,
42 pub max_eclipse_prct: Option<f64>,
44}
45
46impl Kluever {
47 pub fn new(objectives: &[Objective], weights: &[f64]) -> Arc<Self> {
49 Arc::new(Self {
50 objectives: objectives
51 .iter()
52 .copied()
53 .zip(weights.iter().copied())
54 .map(|(obj, w)| ObjectiveWeight {
55 objective: obj,
56 weight: w,
57 })
58 .collect(),
59 max_eclipse_prct: None,
60 })
61 }
62
63 pub fn from_max_eclipse(
65 objectives: &[Objective],
66 weights: &[f64],
67 max_eclipse: f64,
68 ) -> Arc<Self> {
69 Arc::new(Self {
70 objectives: objectives
71 .iter()
72 .copied()
73 .zip(weights.iter().copied())
74 .map(|(obj, w)| ObjectiveWeight {
75 objective: obj,
76 weight: w,
77 })
78 .collect(),
79 max_eclipse_prct: Some(max_eclipse),
80 })
81 }
82
83 pub fn status(&self, state: &Spacecraft) -> Vec<String> {
85 self.objectives
86 .iter()
87 .map(|obj| {
88 let (ok, err) = obj.objective.assess(state).unwrap();
89 format!(
90 "{} achieved: {}\t error = {:.5} {}",
91 obj.objective,
92 ok,
93 err,
94 obj.objective.parameter.unit()
95 )
96 })
97 .collect::<Vec<String>>()
98 }
99}
100
101impl fmt::Display for Kluever {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 let obj_msg = self
104 .objectives
105 .iter()
106 .map(|objective| {
107 let obj = objective.objective;
108 let weight = objective.weight;
109 format!("{obj} (weight: {weight:.3})")
110 })
111 .collect::<Vec<String>>();
112 write!(
113 f,
114 "Kluever Guidance (max eclipse: {}): \n {}",
115 match self.max_eclipse_prct {
116 Some(eclp) => format!("{eclp}"),
117 None => "None".to_string(),
118 },
119 obj_msg.join("\n")
120 )
121 }
122}
123
124impl GuidanceLaw for Kluever {
125 fn achieved(&self, state: &Spacecraft) -> Result<bool, GuidanceError> {
127 for obj in &self.objectives {
128 if !obj
129 .objective
130 .assess_value(
131 state
132 .value(obj.objective.parameter)
133 .context(GuidStateSnafu)?,
134 )
135 .0
136 {
137 return Ok(false);
138 }
139 }
140 Ok(true)
141 }
142
143 fn direction(&self, sc: &Spacecraft) -> Result<Vector3<f64>, GuidanceError> {
144 if sc.mode() != GuidanceMode::Thrust {
145 return Ok(Vector3::zeros());
146 }
147
148 let osc = sc.orbit;
149 let mut sum_weighted_num_alpha = 0.0;
150 let mut sum_weighted_den_alpha = 0.0;
151 let mut sum_weighted_num_beta = 0.0;
152
153 let ecc = osc.ecc().context(GuidancePhysicsSnafu {
155 action: "Kluever guidance",
156 })?;
157 let ta_rad = osc
158 .ta_deg()
159 .context(GuidancePhysicsSnafu {
160 action: "Kluever guidance",
161 })?
162 .to_radians();
163 let aop_rad = osc
164 .aop_deg()
165 .context(GuidancePhysicsSnafu {
166 action: "Kluever guidance",
167 })?
168 .to_radians();
169 let raan_rad = osc
170 .raan_deg()
171 .context(GuidancePhysicsSnafu {
172 action: "Kluever guidance",
173 })?
174 .to_radians();
175
176 let u_rad = ta_rad + aop_rad;
177 let l_rad = u_rad + raan_rad;
178 let (sin_l, cos_l) = l_rad.sin_cos();
179
180 for objective in &self.objectives {
181 let obj = objective.objective;
182 let weight = objective.weight;
183 if weight == 0.0 {
184 continue;
185 }
186
187 let osc_val = sc.value(obj.parameter).context(GuidStateSnafu)?;
188 let error = obj.desired_value - osc_val;
189 if error.abs() < obj.tolerance {
190 continue;
191 }
192 let weight = weight * error.signum();
193
194 match obj.parameter {
195 StateParameter::Element(OrbitalElement::SemiMajorAxis) => {
196 sum_weighted_num_alpha += weight * (ecc * ta_rad.sin());
198 sum_weighted_den_alpha += weight * (1.0 + ecc * ta_rad.cos());
199 }
200 StateParameter::Element(OrbitalElement::Eccentricity) => {
201 let (sin_ta, cos_ta) = ta_rad.sin_cos();
203 sum_weighted_num_alpha += weight * sin_ta;
204 sum_weighted_den_alpha +=
205 weight * (cos_ta + (ecc + cos_ta) / (1.0 + ecc * cos_ta));
206 }
207 StateParameter::Element(OrbitalElement::Inclination) => {
208 let beta_opt = if u_rad.cos() >= 0.0 { 1.0 } else { -1.0 };
210 sum_weighted_num_beta += weight * beta_opt;
211 }
212 StateParameter::Element(OrbitalElement::RAAN) => {
213 let beta_opt = if u_rad.sin() >= 0.0 { 1.0 } else { -1.0 };
214 sum_weighted_num_beta += weight * beta_opt;
215 }
216
217 StateParameter::Element(OrbitalElement::EquinoctialH) => {
218 let h = sc
220 .value(StateParameter::Element(OrbitalElement::EquinoctialH))
221 .context(GuidStateSnafu)?;
222 let k = sc
223 .value(StateParameter::Element(OrbitalElement::EquinoctialK))
224 .context(GuidStateSnafu)?;
225 sum_weighted_num_alpha += weight * cos_l;
226 sum_weighted_den_alpha +=
227 weight * (sin_l + (h + sin_l) / (1.0 + h * sin_l + k * cos_l));
228 }
229
230 StateParameter::Element(OrbitalElement::EquinoctialK) => {
231 let h = sc
233 .value(StateParameter::Element(OrbitalElement::EquinoctialH))
234 .context(GuidStateSnafu)?;
235 let k = sc
236 .value(StateParameter::Element(OrbitalElement::EquinoctialK))
237 .context(GuidStateSnafu)?;
238 sum_weighted_num_alpha += weight * (-sin_l);
239 sum_weighted_den_alpha +=
240 weight * (cos_l + (k + cos_l) / (1.0 + h * sin_l + k * cos_l));
241 }
242
243 StateParameter::Element(OrbitalElement::EquinoctialP) => {
244 let beta_opt = if sin_l >= 0.0 { 1.0 } else { -1.0 };
246 sum_weighted_num_beta += weight * beta_opt;
247 }
248
249 StateParameter::Element(OrbitalElement::EquinoctialQ) => {
250 let beta_opt = if cos_l >= 0.0 { 1.0 } else { -1.0 };
252 sum_weighted_num_beta += weight * beta_opt;
253 }
254
255 StateParameter::Element(OrbitalElement::EquinoctialLambda) => {
256 sum_weighted_den_alpha += weight * 1.0;
258 }
259
260 _ => {
261 return Err(GuidanceError::InvalidControl {
262 param: obj.parameter,
263 })
264 }
265 }
266 }
267
268 let alpha = sum_weighted_num_alpha.atan2(sum_weighted_den_alpha);
270
271 let denom_beta = (sum_weighted_num_alpha.powi(2) + sum_weighted_den_alpha.powi(2)).sqrt();
273 let beta = sum_weighted_num_beta.atan2(denom_beta);
274
275 let (s_a, c_a) = alpha.sin_cos();
277 let (s_b, c_b) = beta.sin_cos();
278 let steering_rcn = Vector3::new(s_a * c_b, c_a * c_b, s_b);
279
280 Ok(osc
282 .dcm_from_rcn_to_inertial()
283 .context(GuidancePhysicsSnafu {
284 action: "RCN to Inertial",
285 })?
286 * steering_rcn)
287 }
288
289 fn throttle(&self, sc: &Spacecraft) -> Result<f64, GuidanceError> {
291 if sc.mode() == GuidanceMode::Thrust {
292 Ok(1.0)
293 } else {
294 Ok(0.0)
295 }
296 }
297
298 fn next(&self, sc: &mut Spacecraft, almanac: Arc<Almanac>) {
300 if sc.mode() != GuidanceMode::Inhibit {
301 if !self.achieved(sc).unwrap() {
302 if let Some(max_eclipse) = self.max_eclipse_prct {
304 let locator = ShadowModel::cislunar(almanac.clone());
305 if locator
306 .compute(sc.orbit, almanac)
307 .expect("cannot compute eclipse")
308 .percentage
309 > max_eclipse
310 {
311 sc.mut_mode(GuidanceMode::Coast);
313 } else {
314 sc.mut_mode(GuidanceMode::Thrust);
315 }
316 } else {
317 if sc.mode() == GuidanceMode::Coast {
318 debug!("enabling steering: {:x}", sc.orbit);
319 }
320 sc.mut_mode(GuidanceMode::Thrust);
321 }
322 } else {
323 if sc.mode() == GuidanceMode::Thrust {
324 debug!("disabling steering: {:x}", sc.orbit);
325 }
326 sc.mut_mode(GuidanceMode::Coast);
327 }
328 }
329 }
330}
331
332#[test]
333fn kluever_direction() {
334 use crate::time::Epoch;
335 use anise::analysis::prelude::OrbitalElement;
336 use anise::constants::frames::EARTH_J2000;
337
338 let eme2k = EARTH_J2000.with_mu_km3_s2(398_600.433);
339 let start_time = Epoch::from_gregorian_tai_at_midnight(2020, 1, 1);
340
341 let objectives = &[
343 Objective::within_tolerance(
344 StateParameter::Element(OrbitalElement::SemiMajorAxis),
345 42164.0,
346 1.0,
347 ),
348 Objective::within_tolerance(
349 StateParameter::Element(OrbitalElement::Eccentricity),
350 0.01,
351 5e-5,
352 ),
353 ];
354 let weights = &[1.0, 1.0];
355
356 let kluever = Kluever::new(objectives, weights);
357 let osc = crate::Orbit::new(
359 7_303.253_461_441_64f64,
360 127.478_714_816_381_75,
361 0.111_246_193_227_445_4,
362 -0.128_284_025_765_195_6,
363 7.422_889_151_816_439,
364 0.006_477_694_429_837_2,
365 start_time,
366 eme2k,
367 );
368
369 let mut osc_sc = Spacecraft::new(osc, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
370 osc_sc.mut_mode(GuidanceMode::Thrust);
372
373 let got = kluever.direction(&osc_sc).unwrap();
374
375 assert!(got.norm() > 0.0);
378 assert!((got.norm() - 1.0).abs() < 1e-12);
379
380 println!("Kluever direction: {got}");
381}