nyx_space/dynamics/guidance/
mod.rs1use 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#[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 pub thrust_N: f64,
53 pub isp_s: f64,
55}
56
57#[cfg_attr(feature = "python", pymethods)]
58impl Thruster {
59 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
107pub trait GuidanceLaw: fmt::Display + Send + Sync {
111 fn direction(&self, osc_state: &Spacecraft) -> Result<Vector3<f64>, GuidanceError>;
113
114 fn throttle(&self, osc_state: &Spacecraft) -> Result<f64, GuidanceError>;
117
118 fn next(&self, next_state: &mut Spacecraft, almanac: Arc<Almanac>);
120
121 fn achieved(&self, _osc_state: &Spacecraft) -> Result<bool, GuidanceError> {
123 Err(GuidanceError::NoGuidanceObjectiveDefined)
124 }
125}
126
127fn 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
136pub fn plane_angles_from_unit_vector(vhat: Vector3<f64>) -> (f64, f64) {
138 (vhat[1].atan2(vhat[0]), vhat[2].asin())
139}
140
141pub(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
150pub(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#[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; if alpha > PI {
239 alpha = 0.0;
240 break;
241 }
242 }
243 delta += TAU * 0.1; if delta > FRAC_PI_2 {
245 break;
246 }
247 }
248}