1use crate::{
2 finite_vec, nonnegative, positive, ControlInput, Error, Guidance, GuidanceContext, Result,
3 Rocket, RocketSample, Vec3,
4};
5
6#[derive(Debug, Clone, Copy, PartialEq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub struct FlightState {
10 pub time_s: f64,
12 pub position_m: Vec3,
14 pub velocity_mps: Vec3,
16}
17
18impl FlightState {
19 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
29pub trait ForceModel {
34 fn acceleration(&self, state: &FlightState, mass_kg: f64) -> Result<Vec3>;
36
37 fn acceleration_with_axis(
42 &self,
43 state: &FlightState,
44 mass_kg: f64,
45 _body_axis: Vec3,
46 ) -> Result<Vec3> {
47 self.acceleration(state, mass_kg)
48 }
49}
50
51impl<F> ForceModel for F
52where
53 F: Fn(&FlightState, f64) -> Result<Vec3>,
54{
55 fn acceleration(&self, state: &FlightState, mass_kg: f64) -> Result<Vec3> {
56 self(state, mass_kg)
57 }
58}
59
60pub struct BoxedForces(pub Box<dyn ForceModel>);
64
65impl std::fmt::Debug for BoxedForces {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("BoxedForces").finish_non_exhaustive()
68 }
69}
70
71impl ForceModel for BoxedForces {
72 fn acceleration(&self, state: &FlightState, mass_kg: f64) -> Result<Vec3> {
73 self.0.acceleration(state, mass_kg)
74 }
75
76 fn acceleration_with_axis(
77 &self,
78 state: &FlightState,
79 mass_kg: f64,
80 body_axis: Vec3,
81 ) -> Result<Vec3> {
82 self.0.acceleration_with_axis(state, mass_kg, body_axis)
83 }
84}
85
86#[derive(Debug, Clone, Copy)]
88pub struct UniformGravity(pub Vec3);
89
90impl Default for UniformGravity {
91 fn default() -> Self {
92 Self(Vec3::new(0.0, -9.80665, 0.0))
93 }
94}
95
96impl ForceModel for UniformGravity {
97 fn acceleration(&self, _state: &FlightState, _mass_kg: f64) -> Result<Vec3> {
98 Ok(self.0)
99 }
100}
101
102pub const EARTH_MU_M3_S2: f64 = 3.986_004_418e14;
104
105#[derive(Debug, Clone, Copy)]
115pub struct CentralGravity {
116 mu_m3_s2: f64,
117 center_m: Vec3,
118}
119
120impl CentralGravity {
121 pub fn new(mu_m3_s2: f64, center_m: Vec3) -> Result<Self> {
123 positive(mu_m3_s2, "mu_m3_s2")?;
124 if !finite_vec(¢er_m) {
125 return Err(Error::invalid("center_m", "must be finite"));
126 }
127 Ok(Self { mu_m3_s2, center_m })
128 }
129
130 pub fn earth(center_m: Vec3) -> Result<Self> {
132 Self::new(EARTH_MU_M3_S2, center_m)
133 }
134
135 pub fn mu_m3_s2(&self) -> f64 {
137 self.mu_m3_s2
138 }
139 pub fn center_m(&self) -> Vec3 {
141 self.center_m
142 }
143}
144
145impl ForceModel for CentralGravity {
146 fn acceleration(&self, state: &FlightState, _mass_kg: f64) -> Result<Vec3> {
147 let offset = state.position_m - self.center_m;
148 let distance_squared = offset.norm_squared();
149 if !distance_squared.is_finite() || distance_squared == 0.0 {
150 return Err(Error::NumericalFailure(
151 "central gravity is singular at its center",
152 ));
153 }
154 let acceleration = offset * (-self.mu_m3_s2 / (distance_squared * distance_squared.sqrt()));
155 if !finite_vec(&acceleration) {
156 return Err(Error::NumericalFailure("non-finite central gravity"));
157 }
158 Ok(acceleration)
159 }
160}
161
162#[derive(Debug, Clone)]
176pub struct AltitudeSwitch<Below, Above> {
177 below: Below,
178 above: Above,
179 cutoff_altitude_m: f64,
180}
181
182impl<Below, Above> AltitudeSwitch<Below, Above> {
183 pub fn new(below: Below, above: Above, cutoff_altitude_m: f64) -> Result<Self> {
187 if !cutoff_altitude_m.is_finite() {
188 return Err(Error::invalid("cutoff_altitude_m", "must be finite"));
189 }
190 Ok(Self {
191 below,
192 above,
193 cutoff_altitude_m,
194 })
195 }
196 pub fn below(&self) -> &Below {
198 &self.below
199 }
200 pub fn above(&self) -> &Above {
202 &self.above
203 }
204 pub fn cutoff_altitude_m(&self) -> f64 {
207 self.cutoff_altitude_m
208 }
209}
210
211impl<Below: ForceModel, Above: ForceModel> ForceModel for AltitudeSwitch<Below, Above> {
212 fn acceleration(&self, state: &FlightState, mass_kg: f64) -> Result<Vec3> {
213 if state.position_m.y <= self.cutoff_altitude_m {
214 self.below.acceleration(state, mass_kg)
215 } else {
216 self.above.acceleration(state, mass_kg)
217 }
218 }
219}
220
221#[derive(Debug, Clone)]
231pub struct RailForces<F> {
232 inner: F,
233 origin_m: Vec3,
234 direction: Vec3,
235 length_m: f64,
236}
237
238impl<F> RailForces<F> {
239 pub fn new(inner: F, origin_m: Vec3, direction: Vec3, length_m: f64) -> Result<Self> {
242 if !finite_vec(&origin_m) || !finite_vec(&direction) || direction.amax() == 0.0 {
243 return Err(Error::invalid(
244 "rail",
245 "origin must be finite, direction nonzero",
246 ));
247 }
248 positive(length_m, "rail length_m")?;
249 Ok(Self {
250 inner,
251 origin_m,
252 direction: direction.normalize(),
253 length_m,
254 })
255 }
256
257 fn constrain(&self, state: &FlightState, acceleration: Vec3) -> Vec3 {
258 let along = (state.position_m - self.origin_m).dot(&self.direction);
259 if along < self.length_m {
260 self.direction * acceleration.dot(&self.direction)
261 } else {
262 acceleration
263 }
264 }
265}
266
267impl<F: ForceModel> ForceModel for RailForces<F> {
268 fn acceleration(&self, state: &FlightState, mass_kg: f64) -> Result<Vec3> {
269 let a = self.inner.acceleration(state, mass_kg)?;
270 Ok(self.constrain(state, a))
271 }
272
273 fn acceleration_with_axis(
274 &self,
275 state: &FlightState,
276 mass_kg: f64,
277 body_axis: Vec3,
278 ) -> Result<Vec3> {
279 let a = self
280 .inner
281 .acceleration_with_axis(state, mass_kg, body_axis)?;
282 Ok(self.constrain(state, a))
283 }
284}
285
286#[derive(Debug, Clone, Copy)]
288pub struct IntegratorSettings {
289 pub max_step_s: f64,
291 pub max_substeps: usize,
293 pub max_steering_accel_mps2: f64,
296}
297
298impl Default for IntegratorSettings {
299 fn default() -> Self {
300 Self {
301 max_step_s: 0.01,
302 max_substeps: 100_000,
303 max_steering_accel_mps2: 0.0,
304 }
305 }
306}
307
308impl IntegratorSettings {
309 fn validate(&self) -> Result<()> {
310 positive(self.max_step_s, "max_step_s")?;
311 nonnegative(self.max_steering_accel_mps2, "max_steering_accel_mps2")?;
312 if self.max_substeps == 0 {
313 return Err(Error::invalid("max_substeps", "must be nonzero"));
314 }
315 Ok(())
316 }
317}
318
319#[derive(Debug, Clone, Copy, PartialEq)]
321pub struct StepResult {
322 pub state: FlightState,
324 pub rocket: RocketSample,
326 pub substeps: usize,
328}
329
330#[derive(Debug, Clone)]
336pub struct Simulator<E> {
337 rocket: Rocket,
338 forces: E,
339 settings: IntegratorSettings,
340}
341
342impl<E: ForceModel> Simulator<E> {
343 pub fn new(rocket: Rocket, forces: E, settings: IntegratorSettings) -> Result<Self> {
345 settings.validate()?;
346 Ok(Self {
347 rocket,
348 forces,
349 settings,
350 })
351 }
352
353 pub fn rocket(&self) -> &Rocket {
355 &self.rocket
356 }
357 pub fn forces(&self) -> &E {
359 &self.forces
360 }
361 pub fn forces_mut(&mut self) -> &mut E {
363 &mut self.forces
364 }
365 pub fn settings(&self) -> IntegratorSettings {
367 self.settings
368 }
369
370 fn end_time(&self, state: &FlightState, duration_s: f64) -> Result<f64> {
371 state.validate()?;
372 positive(duration_s, "duration_s")?;
373 let end = state.time_s + duration_s;
374 if !end.is_finite() || end <= state.time_s {
375 return Err(Error::invalid(
376 "duration_s",
377 "must advance time to a finite, distinguishable value",
378 ));
379 }
380 if duration_s > self.settings.max_step_s * self.settings.max_substeps as f64 {
381 return Err(Error::StepLimitExceeded {
382 limit: self.settings.max_substeps,
383 });
384 }
385 Ok(end)
386 }
387
388 pub fn step(
391 &self,
392 state: &FlightState,
393 duration_s: f64,
394 control: ControlInput,
395 ) -> Result<StepResult> {
396 let end = self.end_time(state, duration_s)?;
397 let (axis, steering) = self.resolve_control(control)?;
398 let mut current = *state;
399 let mut substeps = 0;
400 let mut grid_index = 1usize;
401 while current.time_s < end {
402 if substeps >= self.settings.max_substeps {
403 return Err(Error::StepLimitExceeded {
404 limit: self.settings.max_substeps,
405 });
406 }
407 let grid_boundary =
411 (state.time_s + grid_index as f64 * self.settings.max_step_s).min(end);
412 let mut boundary = grid_boundary;
413 if let Some(event) = self.rocket.next_event_after(current.time_s) {
414 boundary = boundary.min(event);
415 }
416 if boundary <= current.time_s {
417 return Err(Error::NumericalFailure(
418 "internal timestep cannot advance time",
419 ));
420 }
421 current = self.rk4(current, boundary, axis, steering)?;
422 substeps += 1;
423 if boundary == grid_boundary {
424 grid_index += 1;
425 }
426 }
427 Ok(StepResult {
428 state: current,
429 rocket: self.rocket.sample_unchecked(current.time_s),
430 substeps,
431 })
432 }
433
434 pub fn step_guided<G: Guidance + ?Sized>(
438 &self,
439 state: &FlightState,
440 duration_s: f64,
441 guidance: &mut G,
442 ) -> Result<StepResult> {
443 self.end_time(state, duration_s)?;
444 let command = guidance.command(GuidanceContext {
445 state,
446 rocket: self.rocket.sample_unchecked(state.time_s),
447 step_duration_s: duration_s,
448 max_steering_accel_mps2: self.settings.max_steering_accel_mps2,
449 })?;
450 self.step(state, duration_s, command)
451 }
452
453 fn resolve_control(&self, control: ControlInput) -> Result<(Vec3, Vec3)> {
454 if !finite_vec(&control.thrust_direction)
455 || !finite_vec(&control.steering_acceleration_mps2)
456 {
457 return Err(Error::invalid("control", "all components must be finite"));
458 }
459 let scale = control.thrust_direction.amax();
461 if scale == 0.0 {
462 return Err(Error::invalid("thrust_direction", "must be nonzero"));
463 }
464 let scaled = control.thrust_direction / scale;
465 let axis = scaled / scaled.norm();
466 let scale = control.steering_acceleration_mps2.amax();
467 let steering = if scale == 0.0 || self.settings.max_steering_accel_mps2 == 0.0 {
468 Vec3::zeros()
469 } else {
470 let scaled = control.steering_acceleration_mps2 / scale;
471 let norm = scaled.norm();
472 if scale > self.settings.max_steering_accel_mps2 / norm {
473 scaled * (self.settings.max_steering_accel_mps2 / norm)
474 } else {
475 control.steering_acceleration_mps2
476 }
477 };
478 Ok((axis, steering))
479 }
480
481 fn derivative(
482 &self,
483 state: FlightState,
484 axis: Vec3,
485 steering: Vec3,
486 stages: &[crate::motor::IntervalStage],
487 ) -> Result<(Vec3, Vec3)> {
488 state
489 .validate()
490 .map_err(|_| Error::NumericalFailure("non-finite intermediate state"))?;
491 let mass = self.rocket.interval_mass(state.time_s, stages);
492 let thrust = self.rocket.interval_thrust(state.time_s, stages);
493 let acceleration = self.forces.acceleration_with_axis(&state, mass, axis)?
494 + axis * (thrust / mass)
495 + steering;
496 if !finite_vec(&acceleration) {
497 return Err(Error::NumericalFailure("non-finite acceleration"));
498 }
499 Ok((state.velocity_mps, acceleration))
500 }
501
502 fn rk4(&self, start: FlightState, end: f64, axis: Vec3, steering: Vec3) -> Result<FlightState> {
503 let dt = end - start.time_s;
504 let stages = self.rocket.interval_stages(start.time_s);
505 let stage = |derivative: (Vec3, Vec3), delta: f64, time_s: f64| FlightState {
506 time_s,
507 position_m: start.position_m + derivative.0 * delta,
508 velocity_mps: start.velocity_mps + derivative.1 * delta,
509 };
510 let midpoint = start.time_s + dt * 0.5;
511 let k1 = self.derivative(start, axis, steering, &stages)?;
512 let k2 = self.derivative(stage(k1, dt * 0.5, midpoint), axis, steering, &stages)?;
513 let k3 = self.derivative(stage(k2, dt * 0.5, midpoint), axis, steering, &stages)?;
514 let k4 = self.derivative(stage(k3, dt, end), axis, steering, &stages)?;
515 let result = FlightState {
516 time_s: end,
517 position_m: start.position_m + (k1.0 + k2.0 * 2.0 + k3.0 * 2.0 + k4.0) * (dt / 6.0),
518 velocity_mps: start.velocity_mps + (k1.1 + k2.1 * 2.0 + k3.1 * 2.0 + k4.1) * (dt / 6.0),
519 };
520 result
521 .validate()
522 .map_err(|_| Error::NumericalFailure("non-finite output state"))?;
523 Ok(result)
524 }
525}