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 set_max_eclipse(&mut self, max_eclipse: f64) {
65 self.max_eclipse_prct = Some(max_eclipse);
66 }
67}
68
69impl fmt::Display for Kluever {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 let obj_msg = self
72 .objectives
73 .iter()
74 .map(|objective| {
75 let obj = objective.objective;
76 let weight = objective.weight;
77 format!("{obj} (weight: {weight:.3})")
78 })
79 .collect::<Vec<String>>();
80 write!(
81 f,
82 "Kluever Guidance (max eclipse: {}): \n {}",
83 match self.max_eclipse_prct {
84 Some(eclp) => format!("{eclp}"),
85 None => "None".to_string(),
86 },
87 obj_msg.join("\n")
88 )
89 }
90}
91
92impl GuidanceLaw for Kluever {
93 fn achieved(&self, state: &Spacecraft) -> Result<bool, GuidanceError> {
95 for obj in &self.objectives {
96 if !obj
97 .objective
98 .assess_value(
99 state
100 .value(obj.objective.parameter)
101 .context(GuidStateSnafu)?,
102 )
103 .0
104 {
105 return Ok(false);
106 }
107 }
108 Ok(true)
109 }
110
111 fn direction(&self, sc: &Spacecraft) -> Result<Vector3<f64>, GuidanceError> {
112 if sc.mode() != GuidanceMode::Thrust {
113 return Ok(Vector3::zeros());
114 }
115
116 let osc = sc.orbit;
117 let mut sum_weighted_num_alpha = 0.0;
118 let mut sum_weighted_den_alpha = 0.0;
119 let mut sum_weighted_num_beta = 0.0;
120
121 let ecc = osc.ecc().context(GuidancePhysicsSnafu {
123 action: "Kluever guidance",
124 })?;
125 let ta_rad = osc
126 .ta_deg()
127 .context(GuidancePhysicsSnafu {
128 action: "Kluever guidance",
129 })?
130 .to_radians();
131 let aop_rad = osc
132 .aop_deg()
133 .context(GuidancePhysicsSnafu {
134 action: "Kluever guidance",
135 })?
136 .to_radians();
137 let raan_rad = osc
138 .raan_deg()
139 .context(GuidancePhysicsSnafu {
140 action: "Kluever guidance",
141 })?
142 .to_radians();
143
144 let u_rad = ta_rad + aop_rad;
145 let l_rad = u_rad + raan_rad;
146 let (sin_l, cos_l) = l_rad.sin_cos();
147
148 for objective in &self.objectives {
149 let obj = objective.objective;
150 let weight = objective.weight;
151 if weight == 0.0 {
152 continue;
153 }
154
155 let osc_val = sc.value(obj.parameter).context(GuidStateSnafu)?;
156 let error = obj.desired_value - osc_val;
157 if error.abs() < obj.tolerance {
158 continue;
159 }
160 let weight = weight * error.signum();
161
162 match obj.parameter {
163 StateParameter::Element(OrbitalElement::SemiMajorAxis) => {
164 sum_weighted_num_alpha += weight * (ecc * ta_rad.sin());
166 sum_weighted_den_alpha += weight * (1.0 + ecc * ta_rad.cos());
167 }
168 StateParameter::Element(OrbitalElement::Eccentricity) => {
169 let (sin_ta, cos_ta) = ta_rad.sin_cos();
171 sum_weighted_num_alpha += weight * sin_ta;
172 sum_weighted_den_alpha +=
173 weight * (cos_ta + (ecc + cos_ta) / (1.0 + ecc * cos_ta));
174 }
175 StateParameter::Element(OrbitalElement::Inclination) => {
176 let beta_opt = if u_rad.cos() >= 0.0 { 1.0 } else { -1.0 };
178 sum_weighted_num_beta += weight * beta_opt;
179 }
180 StateParameter::Element(OrbitalElement::RAAN) => {
181 let beta_opt = if u_rad.sin() >= 0.0 { 1.0 } else { -1.0 };
182 sum_weighted_num_beta += weight * beta_opt;
183 }
184
185 StateParameter::Element(OrbitalElement::EquinoctialH) => {
186 let h = sc
188 .value(StateParameter::Element(OrbitalElement::EquinoctialH))
189 .context(GuidStateSnafu)?;
190 let k = sc
191 .value(StateParameter::Element(OrbitalElement::EquinoctialK))
192 .context(GuidStateSnafu)?;
193 sum_weighted_num_alpha += weight * cos_l;
194 sum_weighted_den_alpha +=
195 weight * (sin_l + (h + sin_l) / (1.0 + h * sin_l + k * cos_l));
196 }
197
198 StateParameter::Element(OrbitalElement::EquinoctialK) => {
199 let h = sc
201 .value(StateParameter::Element(OrbitalElement::EquinoctialH))
202 .context(GuidStateSnafu)?;
203 let k = sc
204 .value(StateParameter::Element(OrbitalElement::EquinoctialK))
205 .context(GuidStateSnafu)?;
206 sum_weighted_num_alpha += weight * (-sin_l);
207 sum_weighted_den_alpha +=
208 weight * (cos_l + (k + cos_l) / (1.0 + h * sin_l + k * cos_l));
209 }
210
211 StateParameter::Element(OrbitalElement::EquinoctialP) => {
212 let beta_opt = if sin_l >= 0.0 { 1.0 } else { -1.0 };
214 sum_weighted_num_beta += weight * beta_opt;
215 }
216
217 StateParameter::Element(OrbitalElement::EquinoctialQ) => {
218 let beta_opt = if cos_l >= 0.0 { 1.0 } else { -1.0 };
220 sum_weighted_num_beta += weight * beta_opt;
221 }
222
223 StateParameter::Element(OrbitalElement::EquinoctialLambda) => {
224 sum_weighted_den_alpha += weight * 1.0;
226 }
227
228 _ => {
229 return Err(GuidanceError::InvalidControl {
230 param: obj.parameter,
231 })
232 }
233 }
234 }
235
236 let alpha = sum_weighted_num_alpha.atan2(sum_weighted_den_alpha);
238
239 let denom_beta = (sum_weighted_num_alpha.powi(2) + sum_weighted_den_alpha.powi(2)).sqrt();
241 let beta = sum_weighted_num_beta.atan2(denom_beta);
242
243 let (s_a, c_a) = alpha.sin_cos();
245 let (s_b, c_b) = beta.sin_cos();
246 let steering_rcn = Vector3::new(s_a * c_b, c_a * c_b, s_b);
247
248 Ok(osc
250 .dcm_from_rcn_to_inertial()
251 .context(GuidancePhysicsSnafu {
252 action: "RCN to Inertial",
253 })?
254 * steering_rcn)
255 }
256
257 fn throttle(&self, sc: &Spacecraft) -> Result<f64, GuidanceError> {
259 if sc.mode() == GuidanceMode::Thrust {
260 Ok(1.0)
261 } else {
262 Ok(0.0)
263 }
264 }
265
266 fn next(&self, sc: &mut Spacecraft, almanac: Arc<Almanac>) {
268 if sc.mode() != GuidanceMode::Inhibit {
269 if !self.achieved(sc).unwrap() {
270 if let Some(max_eclipse) = self.max_eclipse_prct {
272 let locator = ShadowModel::cislunar(almanac.clone());
273 if locator
274 .compute(sc.orbit, almanac)
275 .expect("cannot compute eclipse")
276 .percentage
277 > max_eclipse
278 {
279 sc.mut_mode(GuidanceMode::Coast);
281 } else {
282 sc.mut_mode(GuidanceMode::Thrust);
283 }
284 } else {
285 if sc.mode() == GuidanceMode::Coast {
286 debug!("enabling steering: {:x}", sc.orbit);
287 }
288 sc.mut_mode(GuidanceMode::Thrust);
289 }
290 } else {
291 if sc.mode() == GuidanceMode::Thrust {
292 debug!("disabling steering: {:x}", sc.orbit);
293 }
294 sc.mut_mode(GuidanceMode::Coast);
295 }
296 }
297 }
298}
299
300#[test]
301fn kluever_direction() {
302 use crate::time::Epoch;
303 use anise::analysis::prelude::OrbitalElement;
304 use anise::constants::frames::EARTH_J2000;
305
306 let eme2k = EARTH_J2000.with_mu_km3_s2(398_600.433);
307 let start_time = Epoch::from_gregorian_tai_at_midnight(2020, 1, 1);
308
309 let objectives = &[
311 Objective::within_tolerance(
312 StateParameter::Element(OrbitalElement::SemiMajorAxis),
313 42164.0,
314 1.0,
315 ),
316 Objective::within_tolerance(
317 StateParameter::Element(OrbitalElement::Eccentricity),
318 0.01,
319 5e-5,
320 ),
321 ];
322 let weights = &[1.0, 1.0];
323
324 let kluever = Kluever::new(objectives, weights);
325 let osc = crate::Orbit::new(
327 7_303.253_461_441_64f64,
328 127.478_714_816_381_75,
329 0.111_246_193_227_445_4,
330 -0.128_284_025_765_195_6,
331 7.422_889_151_816_439,
332 0.006_477_694_429_837_2,
333 start_time,
334 eme2k,
335 );
336
337 let mut osc_sc = Spacecraft::new(osc, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
338 osc_sc.mut_mode(GuidanceMode::Thrust);
340
341 let got = kluever.direction(&osc_sc).unwrap();
342
343 assert!(got.norm() > 0.0);
346 assert!((got.norm() - 1.0).abs() < 1e-12);
347
348 println!("Kluever direction: {got}");
349}