1use anise::prelude::{Frame, Orbit};
20
21use super::{AstroError, AstroPhysicsSnafu, OrbitDual, OrbitPartial};
22use crate::cosmic::NotHyperbolicSnafu;
23use crate::linalg::{Matrix2, Matrix3, Vector2, Vector3};
24use crate::md::objective::Objective;
25use crate::md::{AstroSnafu, StateParameter, TargetingError};
26use crate::time::{Duration, Epoch, Unit};
27use crate::utils::between_pm_180;
28use hyperdual::linalg::norm;
29use hyperdual::{Float, OHyperdual};
30
31use snafu::{ensure, ResultExt};
32use std::convert::From;
33use std::fmt;
34
35#[derive(Copy, Clone, Debug)]
37
38pub struct BPlane {
39 pub b_t_km: OrbitPartial,
41 pub b_r_km: OrbitPartial,
43 pub ltof_s: OrbitPartial,
45 pub str_dcm: Matrix3<f64>,
47 pub frame: Frame,
49 pub epoch: Epoch,
51}
52
53impl BPlane {
54 pub fn from_dual(orbit: OrbitDual) -> Result<Self, AstroError> {
56 ensure!(
57 orbit.ecc().context(AstroPhysicsSnafu)?.real() > 1.0,
58 NotHyperbolicSnafu
59 );
60
61 let one = OHyperdual::from(1.0);
62 let zero = OHyperdual::from(0.0);
63
64 let e_hat =
65 orbit.evec().context(AstroPhysicsSnafu)? / orbit.ecc().context(AstroPhysicsSnafu)?.dual;
66 let h_hat = orbit.hvec() / orbit.hmag().dual;
67 let n_hat = h_hat.cross(&e_hat);
68
69 let ecc = orbit.ecc().context(AstroPhysicsSnafu)?;
74
75 let incoming_asymptote_fact = (one - (one / ecc.dual).powi(2)).sqrt();
76
77 let s = Vector3::new(
78 e_hat[0] / ecc.dual + incoming_asymptote_fact * n_hat[0],
79 e_hat[1] / ecc.dual + incoming_asymptote_fact * n_hat[1],
80 e_hat[2] / ecc.dual + incoming_asymptote_fact * n_hat[2],
81 );
82
83 let s_hat = s / norm(&s); let semi_minor_axis = orbit.semi_minor_axis_km().context(AstroPhysicsSnafu)?;
90
91 let b_vec = Vector3::new(
92 semi_minor_axis.dual
93 * (incoming_asymptote_fact * e_hat[0] - ((one / ecc.dual) * n_hat[0])),
94 semi_minor_axis.dual
95 * (incoming_asymptote_fact * e_hat[1] - ((one / ecc.dual) * n_hat[1])),
96 semi_minor_axis.dual
97 * (incoming_asymptote_fact * e_hat[2] - ((one / ecc.dual) * n_hat[2])),
98 );
99
100 let t = s_hat.cross(&Vector3::new(zero, zero, one));
101 let t_hat = t / norm(&t);
102 let r_hat = s_hat.cross(&t_hat);
103
104 let str_rot = Matrix3::new(
106 s_hat[0].real(),
107 s_hat[1].real(),
108 s_hat[2].real(),
109 t_hat[0].real(),
110 t_hat[1].real(),
111 t_hat[2].real(),
112 r_hat[0].real(),
113 r_hat[1].real(),
114 r_hat[2].real(),
115 );
116
117 Ok(BPlane {
118 b_r_km: OrbitPartial {
119 dual: b_vec.dot(&r_hat),
120 param: StateParameter::BdotR,
121 },
122 b_t_km: OrbitPartial {
123 dual: b_vec.dot(&t_hat),
124 param: StateParameter::BdotT,
125 },
126 ltof_s: OrbitPartial {
127 dual: b_vec.dot(&s_hat) / orbit.vmag_km_s().dual,
128 param: StateParameter::BLTOF,
129 },
130 str_dcm: str_rot,
131 frame: orbit.frame,
132 epoch: orbit.dt,
133 })
134 }
135
136 pub fn new(orbit: Orbit) -> Result<Self, AstroError> {
138 Self::from_dual(OrbitDual::from(orbit))
140 }
141
142 pub fn inertial_to_bplane(&self) -> Matrix3<f64> {
144 self.str_dcm
145 }
146
147 pub fn jacobian(&self) -> Matrix3<f64> {
149 Matrix3::new(
150 self.b_r_km.wtr_vx(),
151 self.b_r_km.wtr_vy(),
152 self.b_r_km.wtr_vz(),
153 self.b_t_km.wtr_vx(),
154 self.b_t_km.wtr_vy(),
155 self.b_t_km.wtr_vz(),
156 self.ltof_s.wtr_vx(),
157 self.ltof_s.wtr_vy(),
158 self.ltof_s.wtr_vz(),
159 )
160 }
161
162 pub fn jacobian2(&self, invariant: StateParameter) -> Result<Matrix2<f64>, AstroError> {
164 match invariant {
165 StateParameter::VX => Ok(Matrix2::new(
166 self.b_r_km.wtr_vy(),
167 self.b_r_km.wtr_vz(),
168 self.b_t_km.wtr_vy(),
169 self.b_t_km.wtr_vz(),
170 )),
171 StateParameter::VY => Ok(Matrix2::new(
172 self.b_r_km.wtr_vx(),
173 self.b_r_km.wtr_vz(),
174 self.b_t_km.wtr_vx(),
175 self.b_t_km.wtr_vz(),
176 )),
177 StateParameter::VZ => Ok(Matrix2::new(
178 self.b_r_km.wtr_vx(),
179 self.b_r_km.wtr_vy(),
180 self.b_t_km.wtr_vx(),
181 self.b_t_km.wtr_vy(),
182 )),
183 _ => Err(AstroError::BPlaneInvariant),
184 }
185 }
186}
187
188impl BPlane {
189 pub fn b_dot_t_km(&self) -> f64 {
191 self.b_t_km.real()
192 }
193
194 pub fn b_dot_r_km(&self) -> f64 {
196 self.b_r_km.real()
197 }
198
199 pub fn ltof(&self) -> Duration {
206 self.ltof_s.real() * Unit::Second
207 }
208
209 pub fn angle_deg(&self) -> f64 {
211 between_pm_180(self.b_dot_r_km().atan2(self.b_dot_t_km()).to_degrees())
212 }
213
214 pub fn magnitude_km(&self) -> f64 {
216 (self.b_dot_t_km().powi(2) + self.b_dot_r_km().powi(2)).sqrt()
217 }
218}
219
220impl fmt::Display for BPlane {
221 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
222 write!(
223 f,
224 "[{}] {} B-Plane: B∙R = {:.3} km\tB∙T = {:.3} km\tAngle = {:.3} deg",
225 self.frame,
226 self.epoch,
227 self.b_dot_r_km(),
228 self.b_dot_t_km(),
229 self.angle_deg()
230 )
231 }
232}
233
234#[derive(Copy, Clone, Debug)]
235pub struct BPlaneTarget {
236 pub b_t_km: f64,
238 pub b_r_km: f64,
240 pub ltof_s: f64,
242
243 pub tol_b_t_km: f64,
245 pub tol_b_r_km: f64,
247 pub tol_ltof_s: f64,
249}
250
251impl BPlaneTarget {
252 pub fn from_targets(b_t_km: f64, b_r_km: f64, ltof: Duration) -> Self {
255 let tol_ltof: Duration = 6.0 * Unit::Hour;
256 Self {
257 b_t_km,
258 b_r_km,
259 ltof_s: ltof.to_seconds(),
260 tol_b_t_km: 1e-6,
261 tol_b_r_km: 1e-6,
262 tol_ltof_s: tol_ltof.to_seconds(),
263 }
264 }
265
266 pub fn from_bt_br(b_t_km: f64, b_r_km: f64) -> Self {
269 let ltof_tol: Duration = 100 * Unit::Day;
270 Self {
271 b_t_km,
272 b_r_km,
273 ltof_s: 0.0,
274 tol_b_t_km: 1e-6,
275 tol_b_r_km: 1e-6,
276 tol_ltof_s: ltof_tol.to_seconds(),
277 }
278 }
279
280 pub fn ltof_target_set(&self) -> bool {
281 self.ltof_s.abs() > 1e-10
282 }
283
284 pub fn to_objectives(self) -> [Objective; 2] {
285 self.to_objectives_with_tolerance(1.0)
286 }
287
288 pub fn to_objectives_with_tolerance(self, tol_km: f64) -> [Objective; 2] {
289 [
290 Objective::within_tolerance(StateParameter::BdotR, self.b_r_km, tol_km),
291 Objective::within_tolerance(StateParameter::BdotT, self.b_t_km, tol_km),
292 ]
293 }
294
295 pub fn to_all_objectives_with_tolerance(self, tol_km: f64) -> [Objective; 3] {
297 [
298 Objective::within_tolerance(StateParameter::BdotR, self.b_r_km, tol_km),
299 Objective::within_tolerance(StateParameter::BdotT, self.b_t_km, tol_km),
300 Objective::within_tolerance(StateParameter::BLTOF, self.ltof_s, self.tol_ltof_s * 1e5),
301 ]
302 }
303}
304
305impl fmt::Display for BPlaneTarget {
306 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
307 write!(
308 f,
309 "B-Plane target: B∙R = {:.3} km (+/- {:.1} m)\tB∙T = {:.3} km (+/- {:.1} m)",
310 self.b_r_km,
311 self.tol_b_r_km * 1e-3,
312 self.b_t_km,
313 self.tol_b_t_km * 1e-3,
314 )
315 }
316}
317
318pub fn try_achieve_b_plane(
323 orbit: Orbit,
324 target: BPlaneTarget,
325) -> Result<(Vector3<f64>, BPlane), TargetingError> {
326 let mut total_dv = Vector3::zeros();
327 let mut attempt_no = 0;
328 let max_iter = 10;
329
330 let mut real_orbit = orbit;
331 let mut prev_b_plane_err = f64::INFINITY;
332
333 if !target.ltof_target_set() {
334 loop {
336 if attempt_no > max_iter {
337 return Err(TargetingError::TooManyIterations);
338 }
339
340 let b_plane = BPlane::new(real_orbit).context(AstroSnafu)?;
342
343 let br_err = target.b_r_km - b_plane.b_dot_r_km();
345 let bt_err = target.b_t_km - b_plane.b_dot_t_km();
346
347 if br_err.abs() < target.tol_b_r_km && bt_err.abs() < target.tol_b_t_km {
348 return Ok((total_dv, b_plane));
349 }
350
351 let b_plane_err = Vector2::new(br_err, bt_err);
353
354 if b_plane_err.norm() >= prev_b_plane_err {
355 return Err(TargetingError::CorrectionIneffective {
357 prev_val: prev_b_plane_err,
358 cur_val: b_plane_err.norm(),
359 action: "Delta-V correction is ineffective at reducing the B-Plane error",
360 });
361 }
362 prev_b_plane_err = b_plane_err.norm();
363
364 let full_jac = b_plane.jacobian();
366 let jac = full_jac.fixed_rows::<2>(0);
367 let dv = jac.transpose() * (jac * jac.transpose()).try_inverse().unwrap() * b_plane_err;
369
370 total_dv[0] += dv[0];
371 total_dv[1] += dv[1];
372 total_dv[2] += dv[2];
373
374 real_orbit.velocity_km_s.x += dv[0];
376 real_orbit.velocity_km_s.y += dv[1];
377 real_orbit.velocity_km_s.z += dv[2];
378
379 attempt_no += 1;
380 }
381 } else {
382 loop {
384 if attempt_no > max_iter {
385 return Err(TargetingError::TooManyIterations);
386 }
387
388 let b_plane = BPlane::new(real_orbit).context(AstroSnafu)?;
390
391 let br_err = target.b_r_km - b_plane.b_dot_r_km();
393 let bt_err = target.b_t_km - b_plane.b_dot_t_km();
394 let ltof_err = target.ltof_s - b_plane.ltof_s.real();
395
396 if br_err.abs() < target.tol_b_r_km
397 && bt_err.abs() < target.tol_b_t_km
398 && ltof_err.abs() < target.tol_ltof_s
399 {
400 return Ok((total_dv, b_plane));
401 }
402
403 let b_plane_err = Vector3::new(br_err, bt_err, ltof_err);
405
406 if b_plane_err.norm() >= prev_b_plane_err {
407 return Err(TargetingError::CorrectionIneffective {
408 prev_val: prev_b_plane_err,
409 cur_val: b_plane_err.norm(),
410 action: "LTOF enabled correction is failing. Try to not set an LTOF target.",
411 });
412 }
413 prev_b_plane_err = b_plane_err.norm();
414
415 let dv = b_plane.jacobian() * b_plane_err;
417
418 total_dv[0] += dv[0];
419 total_dv[1] += dv[1];
420 total_dv[2] += dv[2];
421
422 real_orbit.velocity_km_s.x += dv[0];
424 real_orbit.velocity_km_s.y += dv[1];
425 real_orbit.velocity_km_s.z += dv[2];
426
427 attempt_no += 1;
428 }
429 }
430}