Skip to main content

hpr_sim/
recorder.rs

1//! Watching a flight: the [`Observer`] trait, the [`Sample`] of derived quantities at an instant,
2//! and the [`Recorder`], which keeps a chosen set of [`Channel`]s at a fixed interval.
3
4use hpr_core::DVec3;
5use serde::{Deserialize, Serialize};
6
7use crate::dynamics::Phase;
8use crate::error::SimError;
9use crate::flight::FlightEvent;
10use crate::metrics::Stability;
11use crate::state::State;
12
13/// The flight's state and the quantities derived from it at one instant.
14#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
15pub struct Sample {
16    /// Time since launch, s: the flight's clock, from which each motor's ignition is counted.
17    pub time_s: f64,
18    /// The phase the flight was in.
19    pub phase: Phase,
20    /// The integrated state.
21    pub state: State,
22    /// The center of mass in the launch frame, m.
23    pub cg_enu_m: DVec3,
24    /// The center of mass's velocity relative to the launch frame, m/s.
25    pub cg_velocity_enu_m_s: DVec3,
26    /// The center of mass's ellipsoidal height above the launch site, m.
27    pub height_above_ground_m: f64,
28    /// The rate of that height, m/s.
29    pub vertical_speed_m_s: f64,
30    /// The nose tip's acceleration relative to the launch frame, m/s².
31    pub acceleration_enu_m_s2: DVec3,
32    /// The airspeed of the center of mass, m/s.
33    pub airspeed_m_s: f64,
34    /// Its Mach number.
35    pub mach: f64,
36    /// The total angle of attack at the center of mass, rad.
37    pub angle_of_attack_rad: f64,
38    /// The dynamic pressure, Pa.
39    pub dynamic_pressure_pa: f64,
40    /// The axial force coefficient `C_A` on the reference area.
41    pub axial_coefficient: f64,
42    /// The thrust, N.
43    pub thrust_n: f64,
44    /// The mass, kg.
45    pub mass_kg: f64,
46    /// The drag area `C_D S` of the open recovery devices, m² (zero before any deploys).
47    pub recovery_drag_area_m2: f64,
48}
49
50/// What a recorder can keep. Vector channels take three columns (east, north, up or x, y, z).
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53#[non_exhaustive]
54pub enum Channel {
55    /// Time since launch, s.
56    Time,
57    /// The nose tip's position in the launch frame, m.
58    Position,
59    /// The nose tip's velocity, m/s.
60    Velocity,
61    /// The attitude quaternion `(w, x, y, z)`, body to launch frame.
62    Attitude,
63    /// The body rates, rad/s.
64    BodyRates,
65    /// The center of mass's position, m.
66    CgPosition,
67    /// The center of mass's height above the site, m.
68    HeightAboveGround,
69    /// The vertical speed, m/s.
70    VerticalSpeed,
71    /// The nose tip's acceleration, m/s².
72    Acceleration,
73    /// The airspeed, m/s.
74    Airspeed,
75    /// The Mach number.
76    Mach,
77    /// The total angle of attack, rad.
78    AngleOfAttack,
79    /// The dynamic pressure, Pa.
80    DynamicPressure,
81    /// The axial force coefficient.
82    AxialCoefficient,
83    /// The thrust, N.
84    Thrust,
85    /// The mass, kg.
86    Mass,
87    /// The open recovery devices' drag area `C_D S`, m².
88    RecoveryDragArea,
89}
90
91impl Channel {
92    /// Every channel, in column order.
93    pub const ALL: &'static [Channel] = &[
94        Channel::Time,
95        Channel::Position,
96        Channel::Velocity,
97        Channel::Attitude,
98        Channel::BodyRates,
99        Channel::CgPosition,
100        Channel::HeightAboveGround,
101        Channel::VerticalSpeed,
102        Channel::Acceleration,
103        Channel::Airspeed,
104        Channel::Mach,
105        Channel::AngleOfAttack,
106        Channel::DynamicPressure,
107        Channel::AxialCoefficient,
108        Channel::Thrust,
109        Channel::Mass,
110        Channel::RecoveryDragArea,
111    ];
112
113    /// The column names, with units.
114    #[must_use]
115    pub fn columns(self) -> Vec<String> {
116        let three = |base: &str, unit: &str, axes: [&str; 3]| {
117            axes.iter()
118                .map(|axis| format!("{base}_{axis}_{unit}"))
119                .collect()
120        };
121        let one = |name: &str| vec![name.to_owned()];
122        let enu = ["east", "north", "up"];
123        let xyz = ["x", "y", "z"];
124        match self {
125            Self::Time => one("time_s"),
126            Self::Position => three("position", "m", enu),
127            Self::Velocity => three("velocity", "m_s", enu),
128            Self::Attitude => ["attitude_w", "attitude_x", "attitude_y", "attitude_z"]
129                .iter()
130                .map(|s| (*s).to_owned())
131                .collect(),
132            Self::BodyRates => three("body_rate", "rad_s", xyz),
133            Self::CgPosition => three("cg", "m", enu),
134            Self::HeightAboveGround => one("height_above_ground_m"),
135            Self::VerticalSpeed => one("vertical_speed_m_s"),
136            Self::Acceleration => three("acceleration", "m_s2", enu),
137            Self::Airspeed => one("airspeed_m_s"),
138            Self::Mach => one("mach"),
139            Self::AngleOfAttack => one("angle_of_attack_rad"),
140            Self::DynamicPressure => one("dynamic_pressure_pa"),
141            Self::AxialCoefficient => one("axial_coefficient"),
142            Self::Thrust => one("thrust_n"),
143            Self::Mass => one("mass_kg"),
144            Self::RecoveryDragArea => one("recovery_drag_area_m2"),
145        }
146    }
147
148    /// Appends this channel's values from `sample` to `row`.
149    pub fn push(self, sample: &Sample, row: &mut Vec<f64>) {
150        let s = &sample.state;
151        match self {
152            Self::Time => row.push(sample.time_s),
153            Self::Position => row.extend(s.position_enu_m.to_array()),
154            Self::Velocity => row.extend(s.velocity_enu_m_s.to_array()),
155            Self::Attitude => {
156                let q = s.unit_attitude();
157                row.extend([q.w, q.x, q.y, q.z]);
158            }
159            Self::BodyRates => row.extend(s.body_rate_rad_s.to_array()),
160            Self::CgPosition => row.extend(sample.cg_enu_m.to_array()),
161            Self::HeightAboveGround => row.push(sample.height_above_ground_m),
162            Self::VerticalSpeed => row.push(sample.vertical_speed_m_s),
163            Self::Acceleration => row.extend(sample.acceleration_enu_m_s2.to_array()),
164            Self::Airspeed => row.push(sample.airspeed_m_s),
165            Self::Mach => row.push(sample.mach),
166            Self::AngleOfAttack => row.push(sample.angle_of_attack_rad),
167            Self::DynamicPressure => row.push(sample.dynamic_pressure_pa),
168            Self::AxialCoefficient => row.push(sample.axial_coefficient),
169            Self::Thrust => row.push(sample.thrust_n),
170            Self::Mass => row.push(sample.mass_kg),
171            Self::RecoveryDragArea => row.push(sample.recovery_drag_area_m2),
172        }
173    }
174}
175
176/// One accepted integration step of a flight, with its dense output.
177pub trait FlightStep {
178    /// The phase.
179    fn phase(&self) -> Phase;
180    /// When the step starts, s.
181    fn start_s(&self) -> f64;
182    /// When it ends, s.
183    fn end_s(&self) -> f64;
184    /// The state at `t` in `[start_s, end_s]`, from the dense output.
185    fn state_at(&self, t_s: f64) -> State;
186    /// The sample at `t` in `[start_s, end_s]`: the dense output's state and one evaluation of
187    /// the equations of motion there.
188    ///
189    /// # Errors
190    ///
191    /// Whatever the evaluation returns (it succeeded at the step's own stages).
192    fn sample(&self, t_s: f64) -> Result<Sample, SimError>;
193
194    /// The rocket's stability at `t` in `[start_s, end_s]`: its static margin and its margin at
195    /// the flight's Mach number there, both with the air along the axis, from the aerodynamic
196    /// model flying (the sustainer's after a powered separation) and the center of mass at `t`
197    /// ([`crate::metrics::stability`]).
198    ///
199    /// # Errors
200    ///
201    /// Whatever the evaluation or the aerodynamic model returns.
202    fn stability(&self, t_s: f64) -> Result<Stability, SimError>;
203}
204
205/// Watches a flight as it runs. Both methods do nothing by default; `()` watches nothing.
206pub trait Observer {
207    /// Sees every accepted integration step, in order.
208    ///
209    /// # Errors
210    ///
211    /// An error stops the flight and is returned from it.
212    fn step(&mut self, step: &dyn FlightStep) -> Result<(), SimError> {
213        let _ = step;
214        Ok(())
215    }
216
217    /// Sees every flight event as it is recorded.
218    fn event(&mut self, event: &FlightEvent) {
219        let _ = event;
220    }
221}
222
223impl Observer for () {}
224
225/// An observer lent to a flight, so the caller keeps it.
226impl<T: Observer + ?Sized> Observer for &mut T {
227    fn step(&mut self, step: &dyn FlightStep) -> Result<(), SimError> {
228        (**self).step(step)
229    }
230
231    fn event(&mut self, event: &FlightEvent) {
232        (**self).event(event);
233    }
234}
235
236/// Two observers watch one flight: each sees every step and event, the first before the second.
237impl<A: Observer, B: Observer> Observer for (A, B) {
238    fn step(&mut self, step: &dyn FlightStep) -> Result<(), SimError> {
239        self.0.step(step)?;
240        self.1.step(step)
241    }
242
243    fn event(&mut self, event: &FlightEvent) {
244        self.0.event(event);
245        self.1.event(event);
246    }
247}
248
249/// Records chosen channels, one row per sample.
250///
251/// - With an interval, a row at every multiple of `interval_s` after launch that falls within the
252///   flight, and a row at every event (unless the last row already has that time).
253/// - Without one, a row at the flight's first step's start and at every step's end; events end
254///   steps, so they are among the rows.
255///
256/// A recorder keeps one flight: [`Recorder::clear`] it before recording another. It serializes its
257/// settings and rows for inspection; it is built with [`Recorder::new`], not deserialized.
258#[derive(Debug, Clone, PartialEq, Serialize)]
259pub struct Recorder {
260    channels: Vec<Channel>,
261    interval_s: Option<f64>,
262    /// The next interval sample is at `next_index · interval_s`.
263    next_index: u64,
264    rows: Vec<Vec<f64>>,
265    samples: usize,
266    last_time_s: Option<f64>,
267}
268
269impl Recorder {
270    /// A recorder of `channels`, sampling every `interval_s` seconds (`None`: every step's end).
271    ///
272    /// # Errors
273    ///
274    /// [`SimError::Domain`] for an interval that isn't finite and positive;
275    /// [`SimError::Unsupported`] for a channel listed twice, which would give two columns one name.
276    pub fn new(channels: Vec<Channel>, interval_s: Option<f64>) -> Result<Self, SimError> {
277        if let Some(dt) = interval_s
278            && !(dt.is_finite() && dt > 0.0)
279        {
280            return Err(SimError::Domain {
281                what: "recorder interval",
282                value: dt,
283            });
284        }
285        if channels
286            .iter()
287            .enumerate()
288            .any(|(i, channel)| channels[..i].contains(channel))
289        {
290            return Err(SimError::Unsupported {
291                what: "a recorder channel listed twice",
292            });
293        }
294        Ok(Self {
295            channels,
296            interval_s,
297            next_index: 0,
298            rows: Vec::new(),
299            samples: 0,
300            last_time_s: None,
301        })
302    }
303
304    /// A recorder holding `rows` as if it had recorded them, for tests of what reads its rows.
305    #[cfg(test)]
306    pub(crate) fn with_rows(channels: Vec<Channel>, rows: Vec<Vec<f64>>) -> Self {
307        Self {
308            channels,
309            interval_s: None,
310            next_index: 0,
311            samples: rows.len(),
312            last_time_s: None,
313            rows,
314        }
315    }
316
317    /// Forgets the recorded rows, ready for another flight.
318    pub fn clear(&mut self) {
319        self.next_index = 0;
320        self.rows.clear();
321        self.samples = 0;
322        self.last_time_s = None;
323    }
324
325    /// The column names.
326    #[must_use]
327    pub fn columns(&self) -> Vec<String> {
328        self.channels.iter().flat_map(|c| c.columns()).collect()
329    }
330
331    /// The recorded rows.
332    #[must_use]
333    pub fn rows(&self) -> &[Vec<f64>] {
334        &self.rows
335    }
336
337    fn record(&mut self, sample: &Sample) {
338        let mut row = Vec::new();
339        for channel in &self.channels {
340            channel.push(sample, &mut row);
341        }
342        self.rows.push(row);
343        self.samples += 1;
344        self.last_time_s = Some(sample.time_s);
345    }
346}
347
348impl Observer for Recorder {
349    fn step(&mut self, step: &dyn FlightStep) -> Result<(), SimError> {
350        match self.interval_s {
351            None => {
352                if self.samples == 0 {
353                    self.record(&step.sample(step.start_s())?);
354                }
355                self.record(&step.sample(step.end_s())?);
356            }
357            Some(dt) => {
358                // A flight that starts after launch samples from its start.
359                let first = (step.start_s() / dt).ceil();
360                if (self.next_index as f64) < first {
361                    self.next_index = first as u64;
362                }
363                if !(dt.is_finite() && dt > 0.0) {
364                    return Err(SimError::Domain {
365                        what: "recorder interval",
366                        value: dt,
367                    });
368                }
369                loop {
370                    let t = self.next_index as f64 * dt;
371                    if t > step.end_s() {
372                        break;
373                    }
374                    let sample = step.sample(t)?;
375                    self.record(&sample);
376                    self.next_index += 1;
377                }
378            }
379        }
380        Ok(())
381    }
382
383    fn event(&mut self, event: &FlightEvent) {
384        if self.interval_s.is_some() && self.last_time_s != Some(event.sample.time_s) {
385            self.record(&event.sample);
386        }
387    }
388}