Skip to main content

rockets_engine/
flight.rs

1use crate::{
2    finite_vec, nonnegative, positive, ControlInput, Error, Guidance, GuidanceContext, Result,
3    Rocket, RocketSample, Vec3,
4};
5
6/// Complete translational state. Motor phase and fuel are derived from time and configuration.
7#[derive(Debug, Clone, Copy, PartialEq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub struct FlightState {
10    /// Seconds since the simulation epoch used by the motor schedule.
11    pub time_s: f64,
12    /// World position in metres; Y is up.
13    pub position_m: Vec3,
14    /// World velocity in metres per second.
15    pub velocity_mps: Vec3,
16}
17
18impl FlightState {
19    /// Check a state created directly or restored from a snapshot.
20    pub fn validate(&self) -> Result<()> {
21        nonnegative(self.time_s, "state.time_s")?;
22        if !finite_vec(&self.position_m) || !finite_vec(&self.velocity_mps) {
23            return Err(Error::invalid("state vectors", "must be finite"));
24        }
25        Ok(())
26    }
27}
28
29/// Environmental acceleration hook (gravity, drag, wind, etc.), without motor or controls.
30///
31/// Called at every RK4 stage with the stage's position, velocity, time and current mass.
32/// Implementations must be deterministic and side-effect free for reproducible integration.
33pub trait ForceModel {
34    /// Acceleration in world-frame metres per second squared.
35    fn acceleration(&self, state: &FlightState, mass_kg: f64) -> Result<Vec3>;
36}
37
38impl<F> ForceModel for F
39where
40    F: Fn(&FlightState, f64) -> Result<Vec3>,
41{
42    fn acceleration(&self, state: &FlightState, mass_kg: f64) -> Result<Vec3> {
43        self(state, mass_kg)
44    }
45}
46
47/// Constant gravity with no atmosphere. Use a zero vector for an inertial vacuum.
48#[derive(Debug, Clone, Copy)]
49pub struct UniformGravity(pub Vec3);
50
51impl Default for UniformGravity {
52    fn default() -> Self {
53        Self(Vec3::new(0.0, -9.80665, 0.0))
54    }
55}
56
57impl ForceModel for UniformGravity {
58    fn acceleration(&self, _state: &FlightState, _mass_kg: f64) -> Result<Vec3> {
59        Ok(self.0)
60    }
61}
62
63/// Standard gravitational parameter of Earth, in cubic metres per second squared.
64pub const EARTH_MU_M3_S2: f64 = 3.986_004_418e14;
65
66/// Inverse-square gravity toward a fixed center, for planet-scale flight.
67///
68/// Unlike [`UniformGravity`], both magnitude and direction vary with position,
69/// which matters once trajectories span hundreds of kilometres or climb above
70/// the atmosphere. Positions are read in the caller's frame; place `center_m`
71/// at the attracting body's center (for an Earth-centered inertial coast, the
72/// origin). There is no atmosphere here: pair with an [`AltitudeSwitch`] or a
73/// custom model when drag still applies.
74#[derive(Debug, Clone, Copy)]
75pub struct CentralGravity {
76    mu_m3_s2: f64,
77    center_m: Vec3,
78}
79
80impl CentralGravity {
81    /// A central field with gravitational parameter `mu_m3_s2` about `center_m`.
82    pub fn new(mu_m3_s2: f64, center_m: Vec3) -> Result<Self> {
83        positive(mu_m3_s2, "mu_m3_s2")?;
84        if !finite_vec(&center_m) {
85            return Err(Error::invalid("center_m", "must be finite"));
86        }
87        Ok(Self { mu_m3_s2, center_m })
88    }
89
90    /// Earth's field ([`EARTH_MU_M3_S2`]) about `center_m`.
91    pub fn earth(center_m: Vec3) -> Result<Self> {
92        Self::new(EARTH_MU_M3_S2, center_m)
93    }
94
95    /// Gravitational parameter in m³/s².
96    pub fn mu_m3_s2(&self) -> f64 {
97        self.mu_m3_s2
98    }
99    /// Attracting center in the caller's frame.
100    pub fn center_m(&self) -> Vec3 {
101        self.center_m
102    }
103}
104
105impl ForceModel for CentralGravity {
106    fn acceleration(&self, state: &FlightState, _mass_kg: f64) -> Result<Vec3> {
107        let offset = state.position_m - self.center_m;
108        let distance_squared = offset.norm_squared();
109        if !distance_squared.is_finite() || distance_squared == 0.0 {
110            return Err(Error::NumericalFailure(
111                "central gravity is singular at its center",
112            ));
113        }
114        let acceleration = offset * (-self.mu_m3_s2 / (distance_squared * distance_squared.sqrt()));
115        if !finite_vec(&acceleration) {
116            return Err(Error::NumericalFailure("non-finite central gravity"));
117        }
118        Ok(acceleration)
119    }
120}
121
122/// Routes to one of two force models depending on altitude (position Y).
123///
124/// Use it to bound a dense-atmosphere model: at and below the cutoff, forces
125/// come from e.g. `BallisticForces`; above it, from a vacuum model such as
126/// [`UniformGravity`] or [`CentralGravity`]. The Y coordinate follows the
127/// crate's world-frame convention; with the ballistics adapter it is absolute
128/// altitude above sea level.
129#[derive(Debug, Clone)]
130pub struct AltitudeSwitch<Below, Above> {
131    below: Below,
132    above: Above,
133    cutoff_altitude_m: f64,
134}
135
136impl<Below, Above> AltitudeSwitch<Below, Above> {
137    /// Switch from `below` to `above` at `cutoff_altitude_m` (inclusive below).
138    pub fn new(below: Below, above: Above, cutoff_altitude_m: f64) -> Result<Self> {
139        if !cutoff_altitude_m.is_finite() {
140            return Err(Error::invalid("cutoff_altitude_m", "must be finite"));
141        }
142        Ok(Self {
143            below,
144            above,
145            cutoff_altitude_m,
146        })
147    }
148    /// Model used at and below the cutoff.
149    pub fn below(&self) -> &Below {
150        &self.below
151    }
152    /// Model used above the cutoff.
153    pub fn above(&self) -> &Above {
154        &self.above
155    }
156    /// Handoff altitude in metres.
157    pub fn cutoff_altitude_m(&self) -> f64 {
158        self.cutoff_altitude_m
159    }
160}
161
162impl<Below: ForceModel, Above: ForceModel> ForceModel for AltitudeSwitch<Below, Above> {
163    fn acceleration(&self, state: &FlightState, mass_kg: f64) -> Result<Vec3> {
164        if state.position_m.y <= self.cutoff_altitude_m {
165            self.below.acceleration(state, mass_kg)
166        } else {
167            self.above.acceleration(state, mass_kg)
168        }
169    }
170}
171
172/// Integration accuracy, work budget and external control authority.
173#[derive(Debug, Clone, Copy)]
174pub struct IntegratorSettings {
175    /// Maximum internal RK4 timestep in simulation seconds. Default: 0.01 s.
176    pub max_step_s: f64,
177    /// Maximum internal substeps in a single public call. Default: 100,000.
178    pub max_substeps: usize,
179    /// Magnitude cap for additional steering acceleration in m/s². Default: zero.
180    /// This does not limit changes in the externally supplied thrust-axis orientation.
181    pub max_steering_accel_mps2: f64,
182}
183
184impl Default for IntegratorSettings {
185    fn default() -> Self {
186        Self {
187            max_step_s: 0.01,
188            max_substeps: 100_000,
189            max_steering_accel_mps2: 0.0,
190        }
191    }
192}
193
194impl IntegratorSettings {
195    fn validate(&self) -> Result<()> {
196        positive(self.max_step_s, "max_step_s")?;
197        nonnegative(self.max_steering_accel_mps2, "max_steering_accel_mps2")?;
198        if self.max_substeps == 0 {
199            return Err(Error::invalid("max_substeps", "must be nonzero"));
200        }
201        Ok(())
202    }
203}
204
205/// Result of a successful advancement. An error never overwrites the caller's input state.
206#[derive(Debug, Clone, Copy, PartialEq)]
207pub struct StepResult {
208    /// State at exactly the requested end time.
209    pub state: FlightState,
210    /// Mass and motor output at the end time (burnout has zero thrust).
211    pub rocket: RocketSample,
212    /// Number of internal RK4 steps performed.
213    pub substeps: usize,
214}
215
216/// Stateless RK4 stepper over an immutable vehicle, force model and settings.
217///
218/// Motor ignition, each thrust knot and burnout split integration intervals so no step
219/// straddles a motor discontinuity. Mass is evaluated analytically at every RK stage.
220/// Collision detection and terrain termination belong to the host application.
221#[derive(Debug, Clone)]
222pub struct Simulator<E> {
223    rocket: Rocket,
224    forces: E,
225    settings: IntegratorSettings,
226}
227
228impl<E: ForceModel> Simulator<E> {
229    /// Create a reusable simulator, validating settings before any integration.
230    pub fn new(rocket: Rocket, forces: E, settings: IntegratorSettings) -> Result<Self> {
231        settings.validate()?;
232        Ok(Self {
233            rocket,
234            forces,
235            settings,
236        })
237    }
238
239    /// The vehicle configuration needed alongside a snapshot to resume a flight.
240    pub fn rocket(&self) -> &Rocket {
241        &self.rocket
242    }
243    /// Current environmental force model.
244    pub fn forces(&self) -> &E {
245        &self.forces
246    }
247    /// Access the environment to update externally sampled weather between steps.
248    pub fn forces_mut(&mut self) -> &mut E {
249        &mut self.forces
250    }
251    /// Validated integrator settings.
252    pub fn settings(&self) -> IntegratorSettings {
253        self.settings
254    }
255
256    fn end_time(&self, state: &FlightState, duration_s: f64) -> Result<f64> {
257        state.validate()?;
258        positive(duration_s, "duration_s")?;
259        let end = state.time_s + duration_s;
260        if !end.is_finite() || end <= state.time_s {
261            return Err(Error::invalid(
262                "duration_s",
263                "must advance time to a finite, distinguishable value",
264            ));
265        }
266        if duration_s > self.settings.max_step_s * self.settings.max_substeps as f64 {
267            return Err(Error::StepLimitExceeded {
268                limit: self.settings.max_substeps,
269            });
270        }
271        Ok(end)
272    }
273
274    /// Advance using an externally supplied command, held fixed for the requested duration.
275    /// Call at a fixed cadence to obtain repeatable controller and integration behavior.
276    pub fn step(
277        &self,
278        state: &FlightState,
279        duration_s: f64,
280        control: ControlInput,
281    ) -> Result<StepResult> {
282        let end = self.end_time(state, duration_s)?;
283        let (axis, steering) = self.resolve_control(control)?;
284        let mut current = *state;
285        let mut substeps = 0;
286        let mut grid_index = 1usize;
287        while current.time_s < end {
288            if substeps >= self.settings.max_substeps {
289                return Err(Error::StepLimitExceeded {
290                    limit: self.settings.max_substeps,
291                });
292            }
293            // Anchor to the public start time instead of repeatedly adding dt. This
294            // avoids an extra near-zero step from accumulated floating-point drift.
295            // Motor events split a grid interval without resetting that grid.
296            let grid_boundary =
297                (state.time_s + grid_index as f64 * self.settings.max_step_s).min(end);
298            let mut boundary = grid_boundary;
299            if let Some(event) = self.rocket.next_event_after(current.time_s) {
300                boundary = boundary.min(event);
301            }
302            if boundary <= current.time_s {
303                return Err(Error::NumericalFailure(
304                    "internal timestep cannot advance time",
305                ));
306            }
307            current = self.rk4(current, boundary, axis, steering)?;
308            substeps += 1;
309            if boundary == grid_boundary {
310                grid_index += 1;
311            }
312        }
313        Ok(StepResult {
314            state: current,
315            rocket: self.rocket.sample_unchecked(current.time_s),
316            substeps,
317        })
318    }
319
320    /// Evaluate guidance once, then advance under its held command.
321    /// Invalid state/duration is rejected before calling guidance. A later model error
322    /// leaves the input flight state unchanged but does not roll back the controller.
323    pub fn step_guided<G: Guidance + ?Sized>(
324        &self,
325        state: &FlightState,
326        duration_s: f64,
327        guidance: &mut G,
328    ) -> Result<StepResult> {
329        self.end_time(state, duration_s)?;
330        let command = guidance.command(GuidanceContext {
331            state,
332            rocket: self.rocket.sample_unchecked(state.time_s),
333            step_duration_s: duration_s,
334            max_steering_accel_mps2: self.settings.max_steering_accel_mps2,
335        })?;
336        self.step(state, duration_s, command)
337    }
338
339    fn resolve_control(&self, control: ControlInput) -> Result<(Vec3, Vec3)> {
340        if !finite_vec(&control.thrust_direction)
341            || !finite_vec(&control.steering_acceleration_mps2)
342        {
343            return Err(Error::invalid("control", "all components must be finite"));
344        }
345        // Scale first: ordinary norm() can overflow or underflow for finite vectors.
346        let scale = control.thrust_direction.amax();
347        if scale == 0.0 {
348            return Err(Error::invalid("thrust_direction", "must be nonzero"));
349        }
350        let scaled = control.thrust_direction / scale;
351        let axis = scaled / scaled.norm();
352        let scale = control.steering_acceleration_mps2.amax();
353        let steering = if scale == 0.0 || self.settings.max_steering_accel_mps2 == 0.0 {
354            Vec3::zeros()
355        } else {
356            let scaled = control.steering_acceleration_mps2 / scale;
357            let norm = scaled.norm();
358            if scale > self.settings.max_steering_accel_mps2 / norm {
359                scaled * (self.settings.max_steering_accel_mps2 / norm)
360            } else {
361                control.steering_acceleration_mps2
362            }
363        };
364        Ok((axis, steering))
365    }
366
367    fn derivative(
368        &self,
369        state: FlightState,
370        axis: Vec3,
371        steering: Vec3,
372        stages: &[crate::motor::IntervalStage],
373    ) -> Result<(Vec3, Vec3)> {
374        state
375            .validate()
376            .map_err(|_| Error::NumericalFailure("non-finite intermediate state"))?;
377        let mass = self.rocket.interval_mass(state.time_s, stages);
378        let thrust = self.rocket.interval_thrust(state.time_s, stages);
379        let acceleration =
380            self.forces.acceleration(&state, mass)? + axis * (thrust / mass) + steering;
381        if !finite_vec(&acceleration) {
382            return Err(Error::NumericalFailure("non-finite acceleration"));
383        }
384        Ok((state.velocity_mps, acceleration))
385    }
386
387    fn rk4(&self, start: FlightState, end: f64, axis: Vec3, steering: Vec3) -> Result<FlightState> {
388        let dt = end - start.time_s;
389        let stages = self.rocket.interval_stages(start.time_s);
390        let stage = |derivative: (Vec3, Vec3), delta: f64, time_s: f64| FlightState {
391            time_s,
392            position_m: start.position_m + derivative.0 * delta,
393            velocity_mps: start.velocity_mps + derivative.1 * delta,
394        };
395        let midpoint = start.time_s + dt * 0.5;
396        let k1 = self.derivative(start, axis, steering, &stages)?;
397        let k2 = self.derivative(stage(k1, dt * 0.5, midpoint), axis, steering, &stages)?;
398        let k3 = self.derivative(stage(k2, dt * 0.5, midpoint), axis, steering, &stages)?;
399        let k4 = self.derivative(stage(k3, dt, end), axis, steering, &stages)?;
400        let result = FlightState {
401            time_s: end,
402            position_m: start.position_m + (k1.0 + k2.0 * 2.0 + k3.0 * 2.0 + k4.0) * (dt / 6.0),
403            velocity_mps: start.velocity_mps + (k1.1 + k2.1 * 2.0 + k3.1 * 2.0 + k4.1) * (dt / 6.0),
404        };
405        result
406            .validate()
407            .map_err(|_| Error::NumericalFailure("non-finite output state"))?;
408        Ok(result)
409    }
410}