Skip to main content

rockets_engine/
flight.rs

1use crate::{
2    finite_vec, nonnegative, positive, ControlInput, Error, Guidance, GuidanceContext, MotorPhase,
3    Result, 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/// Integration accuracy, work budget and external control authority.
64#[derive(Debug, Clone, Copy)]
65pub struct IntegratorSettings {
66    /// Maximum internal RK4 timestep in simulation seconds. Default: 0.01 s.
67    pub max_step_s: f64,
68    /// Maximum internal substeps in a single public call. Default: 100,000.
69    pub max_substeps: usize,
70    /// Magnitude cap for additional steering acceleration in m/s². Default: zero.
71    /// This does not limit changes in the externally supplied thrust-axis orientation.
72    pub max_steering_accel_mps2: f64,
73}
74
75impl Default for IntegratorSettings {
76    fn default() -> Self {
77        Self {
78            max_step_s: 0.01,
79            max_substeps: 100_000,
80            max_steering_accel_mps2: 0.0,
81        }
82    }
83}
84
85impl IntegratorSettings {
86    fn validate(&self) -> Result<()> {
87        positive(self.max_step_s, "max_step_s")?;
88        nonnegative(self.max_steering_accel_mps2, "max_steering_accel_mps2")?;
89        if self.max_substeps == 0 {
90            return Err(Error::invalid("max_substeps", "must be nonzero"));
91        }
92        Ok(())
93    }
94}
95
96/// Result of a successful advancement. An error never overwrites the caller's input state.
97#[derive(Debug, Clone, Copy, PartialEq)]
98pub struct StepResult {
99    /// State at exactly the requested end time.
100    pub state: FlightState,
101    /// Mass and motor output at the end time (burnout has zero thrust).
102    pub rocket: RocketSample,
103    /// Number of internal RK4 steps performed.
104    pub substeps: usize,
105}
106
107/// Stateless RK4 stepper over an immutable vehicle, force model and settings.
108///
109/// Motor ignition, each thrust knot and burnout split integration intervals so no step
110/// straddles a motor discontinuity. Mass is evaluated analytically at every RK stage.
111/// Collision detection and terrain termination belong to the host application.
112#[derive(Debug, Clone)]
113pub struct Simulator<E> {
114    rocket: Rocket,
115    forces: E,
116    settings: IntegratorSettings,
117}
118
119impl<E: ForceModel> Simulator<E> {
120    /// Create a reusable simulator, validating settings before any integration.
121    pub fn new(rocket: Rocket, forces: E, settings: IntegratorSettings) -> Result<Self> {
122        settings.validate()?;
123        Ok(Self {
124            rocket,
125            forces,
126            settings,
127        })
128    }
129
130    /// The vehicle configuration needed alongside a snapshot to resume a flight.
131    pub fn rocket(&self) -> &Rocket {
132        &self.rocket
133    }
134    /// Current environmental force model.
135    pub fn forces(&self) -> &E {
136        &self.forces
137    }
138    /// Access the environment to update externally sampled weather between steps.
139    pub fn forces_mut(&mut self) -> &mut E {
140        &mut self.forces
141    }
142    /// Validated integrator settings.
143    pub fn settings(&self) -> IntegratorSettings {
144        self.settings
145    }
146
147    fn end_time(&self, state: &FlightState, duration_s: f64) -> Result<f64> {
148        state.validate()?;
149        positive(duration_s, "duration_s")?;
150        let end = state.time_s + duration_s;
151        if !end.is_finite() || end <= state.time_s {
152            return Err(Error::invalid(
153                "duration_s",
154                "must advance time to a finite, distinguishable value",
155            ));
156        }
157        if duration_s > self.settings.max_step_s * self.settings.max_substeps as f64 {
158            return Err(Error::StepLimitExceeded {
159                limit: self.settings.max_substeps,
160            });
161        }
162        Ok(end)
163    }
164
165    /// Advance using an externally supplied command, held fixed for the requested duration.
166    /// Call at a fixed cadence to obtain repeatable controller and integration behavior.
167    pub fn step(
168        &self,
169        state: &FlightState,
170        duration_s: f64,
171        control: ControlInput,
172    ) -> Result<StepResult> {
173        let end = self.end_time(state, duration_s)?;
174        let (axis, steering) = self.resolve_control(control)?;
175        let mut current = *state;
176        let mut substeps = 0;
177        let mut grid_index = 1usize;
178        while current.time_s < end {
179            if substeps >= self.settings.max_substeps {
180                return Err(Error::StepLimitExceeded {
181                    limit: self.settings.max_substeps,
182                });
183            }
184            // Anchor to the public start time instead of repeatedly adding dt. This
185            // avoids an extra near-zero step from accumulated floating-point drift.
186            // Motor events split a grid interval without resetting that grid.
187            let grid_boundary =
188                (state.time_s + grid_index as f64 * self.settings.max_step_s).min(end);
189            let mut boundary = grid_boundary;
190            if let Some(event) = self
191                .rocket
192                .motor()
193                .and_then(|m| m.next_event_after(current.time_s))
194            {
195                boundary = boundary.min(event);
196            }
197            if boundary <= current.time_s {
198                return Err(Error::NumericalFailure(
199                    "internal timestep cannot advance time",
200                ));
201            }
202            current = self.rk4(current, boundary, axis, steering)?;
203            substeps += 1;
204            if boundary == grid_boundary {
205                grid_index += 1;
206            }
207        }
208        Ok(StepResult {
209            state: current,
210            rocket: self.rocket.sample_unchecked(current.time_s),
211            substeps,
212        })
213    }
214
215    /// Evaluate guidance once, then advance under its held command.
216    /// Invalid state/duration is rejected before calling guidance. A later model error
217    /// leaves the input flight state unchanged but does not roll back the controller.
218    pub fn step_guided<G: Guidance + ?Sized>(
219        &self,
220        state: &FlightState,
221        duration_s: f64,
222        guidance: &mut G,
223    ) -> Result<StepResult> {
224        self.end_time(state, duration_s)?;
225        let command = guidance.command(GuidanceContext {
226            state,
227            rocket: self.rocket.sample_unchecked(state.time_s),
228            step_duration_s: duration_s,
229            max_steering_accel_mps2: self.settings.max_steering_accel_mps2,
230        })?;
231        self.step(state, duration_s, command)
232    }
233
234    fn resolve_control(&self, control: ControlInput) -> Result<(Vec3, Vec3)> {
235        if !finite_vec(&control.thrust_direction)
236            || !finite_vec(&control.steering_acceleration_mps2)
237        {
238            return Err(Error::invalid("control", "all components must be finite"));
239        }
240        // Scale first: ordinary norm() can overflow or underflow for finite vectors.
241        let scale = control.thrust_direction.amax();
242        if scale == 0.0 {
243            return Err(Error::invalid("thrust_direction", "must be nonzero"));
244        }
245        let scaled = control.thrust_direction / scale;
246        let axis = scaled / scaled.norm();
247        let scale = control.steering_acceleration_mps2.amax();
248        let steering = if scale == 0.0 || self.settings.max_steering_accel_mps2 == 0.0 {
249            Vec3::zeros()
250        } else {
251            let scaled = control.steering_acceleration_mps2 / scale;
252            let norm = scaled.norm();
253            if scale > self.settings.max_steering_accel_mps2 / norm {
254                scaled * (self.settings.max_steering_accel_mps2 / norm)
255            } else {
256                control.steering_acceleration_mps2
257            }
258        };
259        Ok((axis, steering))
260    }
261
262    fn derivative(
263        &self,
264        state: FlightState,
265        axis: Vec3,
266        steering: Vec3,
267        burning: bool,
268    ) -> Result<(Vec3, Vec3)> {
269        state
270            .validate()
271            .map_err(|_| Error::NumericalFailure("non-finite intermediate state"))?;
272        let mass = self.rocket.sample_unchecked(state.time_s).mass_kg;
273        let thrust = self
274            .rocket
275            .motor()
276            .map_or(0.0, |m| m.interval_thrust(state.time_s, burning));
277        let acceleration =
278            self.forces.acceleration(&state, mass)? + axis * (thrust / mass) + steering;
279        if !finite_vec(&acceleration) {
280            return Err(Error::NumericalFailure("non-finite acceleration"));
281        }
282        Ok((state.velocity_mps, acceleration))
283    }
284
285    fn rk4(&self, start: FlightState, end: f64, axis: Vec3, steering: Vec3) -> Result<FlightState> {
286        let dt = end - start.time_s;
287        let burning = self.rocket.sample_unchecked(start.time_s).motor.phase == MotorPhase::Burning;
288        let stage = |derivative: (Vec3, Vec3), delta: f64, time_s: f64| FlightState {
289            time_s,
290            position_m: start.position_m + derivative.0 * delta,
291            velocity_mps: start.velocity_mps + derivative.1 * delta,
292        };
293        let midpoint = start.time_s + dt * 0.5;
294        let k1 = self.derivative(start, axis, steering, burning)?;
295        let k2 = self.derivative(stage(k1, dt * 0.5, midpoint), axis, steering, burning)?;
296        let k3 = self.derivative(stage(k2, dt * 0.5, midpoint), axis, steering, burning)?;
297        let k4 = self.derivative(stage(k3, dt, end), axis, steering, burning)?;
298        let result = FlightState {
299            time_s: end,
300            position_m: start.position_m + (k1.0 + k2.0 * 2.0 + k3.0 * 2.0 + k4.0) * (dt / 6.0),
301            velocity_mps: start.velocity_mps + (k1.1 + k2.1 * 2.0 + k3.1 * 2.0 + k4.1) * (dt / 6.0),
302        };
303        result
304            .validate()
305            .map_err(|_| Error::NumericalFailure("non-finite output state"))?;
306        Ok(result)
307    }
308}