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