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