nyx_space/cosmic/
bplane.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use 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/// Stores a B-Plane
36#[derive(Copy, Clone, Debug)]
37
38pub struct BPlane {
39    /// The $B_T$ component, in kilometers
40    pub b_t_km: OrbitPartial,
41    /// The $B_R$ component, in kilometers
42    pub b_r_km: OrbitPartial,
43    /// The Linearized Time of Flight
44    pub ltof_s: OrbitPartial,
45    /// The B-Plane rotation matrix
46    pub str_dcm: Matrix3<f64>,
47    /// The frame in which this B Plane was computed
48    pub frame: Frame,
49    /// The time of computation
50    pub epoch: Epoch,
51}
52
53impl BPlane {
54    /// Returns a newly define B-Plane if the orbit is hyperbolic and already in Dual form
55    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        // The reals implementation (which was initially validated) was:
70        // let s = e_hat / orbit.ecc() + (1.0 - (1.0 / orbit.ecc()).powi(2)).sqrt() * n_hat;
71        // let s_hat = s / s.norm();
72
73        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); // Just to make sure to renormalize everything
84
85        // The reals implementation (which was initially validated) was:
86        // let b_vec = orbit.semi_minor_axis()
87        //     * ((1.0 - (1.0 / orbit.ecc()).powi(2)).sqrt() * e_hat
88        //         - (1.0 / orbit.ecc() * n_hat));
89        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        // Build the rotation matrix from inertial to B Plane
105        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    /// Returns a newly defined B-Plane if the orbit is hyperbolic.
137    pub fn new(orbit: Orbit) -> Result<Self, AstroError> {
138        // Convert to OrbitDual so we can target it
139        Self::from_dual(OrbitDual::from(orbit))
140    }
141
142    /// Returns the DCM to convert to the B Plane from the inertial frame
143    pub fn inertial_to_bplane(&self) -> Matrix3<f64> {
144        self.str_dcm
145    }
146
147    /// Returns the Jacobian of the B plane (BR, BT, LTOF) with respect to the velocity
148    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    /// Returns the Jacobian of the B plane (BR, BT) with respect to two of the velocity components
163    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    /// Returns the B dot T, in km
190    pub fn b_dot_t_km(&self) -> f64 {
191        self.b_t_km.real()
192    }
193
194    /// Returns the B dot R, in km
195    pub fn b_dot_r_km(&self) -> f64 {
196        self.b_r_km.real()
197    }
198
199    /// Returns the linearized time of flight, in seconds
200    ///
201    /// NOTE:
202    /// Although the LTOF should allow to build a 3x3 Jacobian for a differential corrector
203    /// it's historically super finicky and one will typically have better results by cascading
204    /// DCs which target different parameters instead of trying to use the LTOF.
205    pub fn ltof(&self) -> Duration {
206        self.ltof_s.real() * Unit::Second
207    }
208
209    /// Returns the B plane angle in degrees between -180 and 180
210    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    /// Returns the B plane vector magnitude, in kilometers
215    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    /// The $B_T$ component, in kilometers
237    pub b_t_km: f64,
238    /// The $B_R$ component, in kilometers
239    pub b_r_km: f64,
240    /// The Linearized Time of Flight, in seconds
241    pub ltof_s: f64,
242
243    /// The tolerance on the $B_T$ component, in kilometers
244    pub tol_b_t_km: f64,
245    /// The tolerance on the $B_R$ component, in kilometers
246    pub tol_b_r_km: f64,
247    /// The tolerance on the Linearized Time of Flight, in seconds
248    pub tol_ltof_s: f64,
249}
250
251impl BPlaneTarget {
252    /// Initializes a new B Plane target with only the targets and the default tolerances.
253    /// Default tolerances are 1 millimeter in positions and 1 second in LTOF
254    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    /// Initializes a new B Plane target with only the B Plane targets (not LTOF constraint) and the default tolerances.
267    /// Default tolerances are 1 millimeter in positions. Here, the LTOF tolerance is set to 100 days.
268    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    /// Includes the linearized time of flight as an objective
296    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
318/// Returns the Delta V (in km/s) needed to achieve the B Plane specified by B dot R and B dot T.
319/// If no LTOF target is set, this method will fix VX, VY and VZ successively and use the minimum of those as a seed for the LTOF variation finding.
320/// If the 3x3 search is worse than any of the 2x2s, then a 2x2 will be returned.
321/// This uses the hyperdual formulation of the Jacobian and will also vary the linearize time of flight (LTOF).
322pub 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        // If no LTOF is targeted, we'll solve this with a least squared approach.
335        loop {
336            if attempt_no > max_iter {
337                return Err(TargetingError::TooManyIterations);
338            }
339
340            // Build current B Plane
341            let b_plane = BPlane::new(real_orbit).context(AstroSnafu)?;
342
343            // Check convergence
344            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            // Build the error vector
352            let b_plane_err = Vector2::new(br_err, bt_err);
353
354            if b_plane_err.norm() >= prev_b_plane_err {
355                // If the error is not going down, we'll raise an error
356                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            // Grab the first two rows of the Jacobian (discard the rest).
365            let full_jac = b_plane.jacobian();
366            let jac = full_jac.fixed_rows::<2>(0);
367            // Solve the Least Squares / compute the delta-v
368            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            // Rebuild a new orbit
375            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        // The LTOF targeting seems to break often, but it's still implemented
383        loop {
384            if attempt_no > max_iter {
385                return Err(TargetingError::TooManyIterations);
386            }
387
388            // Build current B Plane
389            let b_plane = BPlane::new(real_orbit).context(AstroSnafu)?;
390
391            // Check convergence
392            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            // Build the error vector
404            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            // Compute the delta-v
416            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            // Rebuild a new orbit
423            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}