Skip to main content

nyx_space/dynamics/guidance/
mod.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 crate::cosmic::{GuidanceMode, Orbit, Spacecraft, STD_GRAVITY};
20use crate::errors::{NyxError, StateError};
21use crate::linalg::Vector3;
22use anise::astro::PhysicsResult;
23use anise::errors::PhysicsError;
24use anise::math::rotation::DCM;
25use anise::prelude::Almanac;
26use der::{Decode, Encode, Reader};
27use serde::{Deserialize, Serialize};
28use serde_dhall::StaticType;
29
30pub mod mnvr;
31pub use mnvr::{Maneuver, MnvrRepr};
32
33mod ruggiero;
34pub use ruggiero::{Objective, Ruggiero, StateParameter};
35
36mod kluever;
37pub use kluever::Kluever;
38use snafu::Snafu;
39
40use std::fmt;
41use std::sync::Arc;
42
43#[cfg(feature = "python")]
44use pyo3::prelude::*;
45
46/// Defines a thruster with a maximum isp and a maximum thrust.
47#[allow(non_snake_case)]
48#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
49#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, StaticType)]
50pub struct Thruster {
51    /// The thrust is to be provided in Newtons
52    pub thrust_N: f64,
53    /// The Isp is to be provided in seconds
54    pub isp_s: f64,
55}
56
57#[cfg_attr(feature = "python", pymethods)]
58impl Thruster {
59    /// Returns the exhaust velocity v_e in meters per second
60    pub fn exhaust_velocity_m_s(&self) -> f64 {
61        self.isp_s * STD_GRAVITY
62    }
63}
64
65#[cfg(feature = "python")]
66#[cfg_attr(feature = "python", pymethods)]
67impl Thruster {
68    #[allow(non_snake_case)]
69    #[new]
70    fn py_new(thrust_N: f64, isp_s: f64) -> Self {
71        Self { thrust_N, isp_s }
72    }
73}
74
75impl Encode for Thruster {
76    fn encoded_len(&self) -> der::Result<der::Length> {
77        self.thrust_N.encoded_len()? + self.isp_s.encoded_len()?
78    }
79
80    fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
81        self.thrust_N.encode(encoder)?;
82        self.isp_s.encode(encoder)
83    }
84}
85
86impl<'a> Decode<'a> for Thruster {
87    fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
88        Ok(Self {
89            thrust_N: decoder.decode()?,
90            isp_s: decoder.decode()?,
91        })
92    }
93}
94
95#[derive(Clone, Debug, Serialize, Deserialize, StaticType)]
96pub struct ObjectiveEfficiency {
97    pub objective: Objective,
98    pub efficiency: f64,
99}
100
101#[derive(Clone, Debug, Serialize, Deserialize, StaticType)]
102pub struct ObjectiveWeight {
103    pub objective: Objective,
104    pub weight: f64,
105}
106
107/// The `GuidanceLaw` trait handles guidance laws, optimizations, and other such methods for
108/// controlling the overall thrust direction when tied to a `Spacecraft`. For delta V control,
109/// tie the DeltaVctrl to a MissionArc.
110pub trait GuidanceLaw: fmt::Display + Send + Sync {
111    /// Returns a unit vector corresponding to the thrust direction in the inertial frame.
112    fn direction(&self, osc_state: &Spacecraft) -> Result<Vector3<f64>, GuidanceError>;
113
114    /// Returns a number between [0;1] corresponding to the engine throttle level.
115    /// For example, 0 means coasting, i.e. no thrusting, and 1 means maximum thrusting.
116    fn throttle(&self, osc_state: &Spacecraft) -> Result<f64, GuidanceError>;
117
118    /// Updates the state of the BaseSpacecraft for the next maneuver, e.g. prepares the controller for the next maneuver
119    fn next(&self, next_state: &mut Spacecraft, almanac: Arc<Almanac>);
120
121    /// Returns whether this thrust control has been achieved, if it has an objective
122    fn achieved(&self, _osc_state: &Spacecraft) -> Result<bool, GuidanceError> {
123        Err(GuidanceError::NoGuidanceObjectiveDefined)
124    }
125}
126
127/// Converts the alpha (in-plane) and beta (out-of-plane) angles in the RCN frame to the unit vector in the RCN frame
128fn unit_vector_from_plane_angles(alpha: f64, beta: f64) -> Vector3<f64> {
129    Vector3::new(
130        alpha.sin() * beta.cos(),
131        alpha.cos() * beta.cos(),
132        beta.sin(),
133    )
134}
135
136/// Converts the provided unit vector into in-plane and out-of-plane angles in the RCN frame, returned in radians
137pub fn plane_angles_from_unit_vector(vhat: Vector3<f64>) -> (f64, f64) {
138    (vhat[1].atan2(vhat[0]), vhat[2].asin())
139}
140
141/// Converts the alpha (in-plane) and beta (out-of-plane) angles in the RCN frame to the unit vector in the RCN frame
142pub(crate) fn unit_vector_from_ra_dec(alpha: f64, delta: f64) -> Vector3<f64> {
143    Vector3::new(
144        delta.cos() * alpha.cos(),
145        delta.cos() * alpha.sin(),
146        delta.sin(),
147    )
148}
149
150/// Converts the provided unit vector into in-plane and out-of-plane angles in the RCN frame, returned in radians
151pub(crate) fn ra_dec_from_unit_vector(vhat: Vector3<f64>) -> (f64, f64) {
152    let alpha = vhat[1].atan2(vhat[0]);
153    let delta = vhat[2].asin();
154    (alpha, delta)
155}
156
157#[derive(Debug, PartialEq, Snafu)]
158pub enum GuidanceError {
159    #[snafu(display("No thruster attached to spacecraft"))]
160    NoThrustersDefined,
161    #[snafu(display("Throttle is not between 0.0 and 1.0: {ratio}"))]
162    ThrottleRatio { ratio: f64 },
163    #[snafu(display("Invalid finite burn control direction u = [{x}, {y}, {z}] => i-plane = {in_plane_deg} deg, Delta = {out_of_plane_deg} deg",))]
164    InvalidDirection {
165        x: f64,
166        y: f64,
167        z: f64,
168        in_plane_deg: f64,
169        out_of_plane_deg: f64,
170    },
171    #[snafu(display("Invalid finite burn control rate u = [{x}, {y}, {z}] => in-plane = {in_plane_deg_s} deg/s, out of plane = {out_of_plane_deg_s} deg/s",))]
172    InvalidRate {
173        x: f64,
174        y: f64,
175        z: f64,
176        in_plane_deg_s: f64,
177        out_of_plane_deg_s: f64,
178    },
179    #[snafu(display("Invalid finite burn control acceleration u = [{x}, {y}, {z}] => in-plane = {in_plane_deg_s2} deg/s^2, out of plane = {out_of_plane_deg_s2} deg/s^2",))]
180    InvalidAcceleration {
181        x: f64,
182        y: f64,
183        z: f64,
184        in_plane_deg_s2: f64,
185        out_of_plane_deg_s2: f64,
186    },
187    #[snafu(display("when {action} encountered {source}"))]
188    GuidancePhysicsError {
189        action: &'static str,
190        source: PhysicsError,
191    },
192    #[snafu(display(
193        "An objective based analysis or control was attempted, but no objective was defined"
194    ))]
195    NoGuidanceObjectiveDefined,
196    #[snafu(display("{param} is not a control variable in this guidance law"))]
197    InvalidControl { param: StateParameter },
198    #[snafu(display("guidance encountered {source}"))]
199    GuidState { source: StateError },
200}
201
202/// Local frame options, used notably for guidance laws.
203/// TODO: Replace with ANISE enum, which is identical
204#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, StaticType)]
205pub enum LocalFrame {
206    Inertial,
207    RIC,
208    VNC,
209    RCN,
210}
211
212impl LocalFrame {
213    pub fn dcm_to_inertial(&self, state: Orbit) -> PhysicsResult<DCM> {
214        match self {
215            LocalFrame::Inertial => Ok(DCM::identity(
216                state.frame.orientation_id,
217                state.frame.orientation_id,
218            )),
219            LocalFrame::RIC => state.dcm_from_ric_to_inertial(),
220            LocalFrame::VNC => state.dcm_from_vnc_to_inertial(),
221            LocalFrame::RCN => state.dcm_from_rcn_to_inertial(),
222        }
223    }
224}
225
226#[test]
227fn ra_dec_from_vec() {
228    use std::f64::consts::{FRAC_PI_2, PI, TAU};
229    let mut delta = -FRAC_PI_2;
230    let mut alpha = 0.0;
231    loop {
232        loop {
233            let unit_v = unit_vector_from_ra_dec(alpha, delta);
234            let (alpha2, delta2) = ra_dec_from_unit_vector(unit_v);
235            assert!((alpha - alpha2).abs() < f64::EPSILON);
236            assert!((delta - delta2).abs() < f64::EPSILON);
237            alpha += TAU * 0.1; // Increment right ascension by one tenth of a circle
238            if alpha > PI {
239                alpha = 0.0;
240                break;
241            }
242        }
243        delta += TAU * 0.1; // Increment declination by one tenth of a circle
244        if delta > FRAC_PI_2 {
245            break;
246        }
247    }
248}