Skip to main content

hpr_sim/
state.rs

1//! The flight state: the body origin's position and velocity, the attitude and the body rates.
2
3use hpr_core::{DQuat, DVec3};
4use serde::{Deserialize, Serialize};
5
6/// The number of components in a [`State`] array.
7pub const STATE_LEN: usize = 13;
8
9/// A rigid body's state in the launch frame `L` (`docs/physics/frames.md`).
10///
11/// The reference point is the body origin, the nose tip (the decision record on the design tree,
12/// [ADR-007][adr-007]), which is fixed in the body; the center of mass moves relative to it as
13/// propellant burns. As an array the order is position, velocity, attitude `(w, x, y, z)` and body
14/// rates.
15///
16/// [adr-007]: https://github.com/nrdptel/hpr-sim/blob/main/docs/DECISIONS.md#adr-007-design-tree-stations-placement-automatic-radii-overrides-motors-and-checks-2026-09-17
17#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
18pub struct State {
19    /// The nose tip's position in `L`, m.
20    pub position_enu_m: DVec3,
21    /// The nose tip's velocity relative to `L`, m/s.
22    pub velocity_enu_m_s: DVec3,
23    /// The attitude, body to launch frame. The integrated quaternion's norm drifts slightly; every
24    /// use normalizes it.
25    pub attitude: DQuat,
26    /// The body's angular velocity relative to `L`, in body axes, rad/s.
27    pub body_rate_rad_s: DVec3,
28}
29
30impl State {
31    /// The state as the integrator's array.
32    #[must_use]
33    pub fn to_array(&self) -> [f64; STATE_LEN] {
34        let (p, v, q, w) = (
35            self.position_enu_m,
36            self.velocity_enu_m_s,
37            self.attitude,
38            self.body_rate_rad_s,
39        );
40        [
41            p.x, p.y, p.z, v.x, v.y, v.z, q.w, q.x, q.y, q.z, w.x, w.y, w.z,
42        ]
43    }
44
45    /// The state from the integrator's array.
46    #[must_use]
47    pub fn from_array(y: &[f64; STATE_LEN]) -> Self {
48        Self {
49            position_enu_m: DVec3::new(y[0], y[1], y[2]),
50            velocity_enu_m_s: DVec3::new(y[3], y[4], y[5]),
51            attitude: DQuat::from_xyzw(y[7], y[8], y[9], y[6]),
52            body_rate_rad_s: DVec3::new(y[10], y[11], y[12]),
53        }
54    }
55
56    /// The attitude scaled to unit length.
57    #[must_use]
58    pub fn unit_attitude(&self) -> DQuat {
59        self.attitude.normalize()
60    }
61
62    /// A point given in body axes (m from the nose tip), in `L`.
63    #[must_use]
64    pub fn point_enu_m(&self, body_m: DVec3) -> DVec3 {
65        self.position_enu_m + self.unit_attitude().mul_vec3(body_m)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn arrays_round_trip() {
75        let state = State {
76            position_enu_m: DVec3::new(1.0, 2.0, 3.0),
77            velocity_enu_m_s: DVec3::new(4.0, 5.0, 6.0),
78            attitude: DQuat::from_xyzw(0.1, 0.2, 0.3, 0.9),
79            body_rate_rad_s: DVec3::new(7.0, 8.0, 9.0),
80        };
81        assert_eq!(State::from_array(&state.to_array()), state);
82        assert_eq!(state.to_array()[6], 0.9);
83    }
84}