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)]
114pub struct CentralGravity {
115 mu_m3_s2: f64,
116 center_m: Vec3,
117}
118
119impl CentralGravity {
120 pub fn new(mu_m3_s2: f64, center_m: Vec3) -> Result<Self> {
122 positive(mu_m3_s2, "mu_m3_s2")?;
123 if !finite_vec(¢er_m) {
124 return Err(Error::invalid("center_m", "must be finite"));
125 }
126 Ok(Self { mu_m3_s2, center_m })
127 }
128
129 pub fn earth(center_m: Vec3) -> Result<Self> {
131 Self::new(EARTH_MU_M3_S2, center_m)
132 }
133
134 pub fn mu_m3_s2(&self) -> f64 {
136 self.mu_m3_s2
137 }
138 pub fn center_m(&self) -> Vec3 {
140 self.center_m
141 }
142}
143
144impl ForceModel for CentralGravity {
145 fn acceleration(&self, state: &FlightState, _mass_kg: f64) -> Result<Vec3> {
146 let offset = state.position_m - self.center_m;
147 let distance_squared = offset.norm_squared();
148 if !distance_squared.is_finite() || distance_squared == 0.0 {
149 return Err(Error::NumericalFailure(
150 "central gravity is singular at its center",
151 ));
152 }
153 let acceleration = offset * (-self.mu_m3_s2 / (distance_squared * distance_squared.sqrt()));
154 if !finite_vec(&acceleration) {
155 return Err(Error::NumericalFailure("non-finite central gravity"));
156 }
157 Ok(acceleration)
158 }
159}
160
161#[derive(Debug, Clone)]
169pub struct AltitudeSwitch<Below, Above> {
170 below: Below,
171 above: Above,
172 cutoff_altitude_m: f64,
173}
174
175impl<Below, Above> AltitudeSwitch<Below, Above> {
176 pub fn new(below: Below, above: Above, cutoff_altitude_m: f64) -> Result<Self> {
178 if !cutoff_altitude_m.is_finite() {
179 return Err(Error::invalid("cutoff_altitude_m", "must be finite"));
180 }
181 Ok(Self {
182 below,
183 above,
184 cutoff_altitude_m,
185 })
186 }
187 pub fn below(&self) -> &Below {
189 &self.below
190 }
191 pub fn above(&self) -> &Above {
193 &self.above
194 }
195 pub fn cutoff_altitude_m(&self) -> f64 {
197 self.cutoff_altitude_m
198 }
199}
200
201impl<Below: ForceModel, Above: ForceModel> ForceModel for AltitudeSwitch<Below, Above> {
202 fn acceleration(&self, state: &FlightState, mass_kg: f64) -> Result<Vec3> {
203 if state.position_m.y <= self.cutoff_altitude_m {
204 self.below.acceleration(state, mass_kg)
205 } else {
206 self.above.acceleration(state, mass_kg)
207 }
208 }
209}
210
211#[derive(Debug, Clone)]
221pub struct RailForces<F> {
222 inner: F,
223 origin_m: Vec3,
224 direction: Vec3,
225 length_m: f64,
226}
227
228impl<F> RailForces<F> {
229 pub fn new(inner: F, origin_m: Vec3, direction: Vec3, length_m: f64) -> Result<Self> {
232 if !finite_vec(&origin_m) || !finite_vec(&direction) || direction.amax() == 0.0 {
233 return Err(Error::invalid(
234 "rail",
235 "origin must be finite, direction nonzero",
236 ));
237 }
238 positive(length_m, "rail length_m")?;
239 Ok(Self {
240 inner,
241 origin_m,
242 direction: direction.normalize(),
243 length_m,
244 })
245 }
246
247 fn constrain(&self, state: &FlightState, acceleration: Vec3) -> Vec3 {
248 let along = (state.position_m - self.origin_m).dot(&self.direction);
249 if along < self.length_m {
250 self.direction * acceleration.dot(&self.direction)
251 } else {
252 acceleration
253 }
254 }
255}
256
257impl<F: ForceModel> ForceModel for RailForces<F> {
258 fn acceleration(&self, state: &FlightState, mass_kg: f64) -> Result<Vec3> {
259 let a = self.inner.acceleration(state, mass_kg)?;
260 Ok(self.constrain(state, a))
261 }
262
263 fn acceleration_with_axis(
264 &self,
265 state: &FlightState,
266 mass_kg: f64,
267 body_axis: Vec3,
268 ) -> Result<Vec3> {
269 let a = self
270 .inner
271 .acceleration_with_axis(state, mass_kg, body_axis)?;
272 Ok(self.constrain(state, a))
273 }
274}
275
276#[derive(Debug, Clone, Copy)]
278pub struct IntegratorSettings {
279 pub max_step_s: f64,
281 pub max_substeps: usize,
283 pub max_steering_accel_mps2: f64,
286}
287
288impl Default for IntegratorSettings {
289 fn default() -> Self {
290 Self {
291 max_step_s: 0.01,
292 max_substeps: 100_000,
293 max_steering_accel_mps2: 0.0,
294 }
295 }
296}
297
298impl IntegratorSettings {
299 fn validate(&self) -> Result<()> {
300 positive(self.max_step_s, "max_step_s")?;
301 nonnegative(self.max_steering_accel_mps2, "max_steering_accel_mps2")?;
302 if self.max_substeps == 0 {
303 return Err(Error::invalid("max_substeps", "must be nonzero"));
304 }
305 Ok(())
306 }
307}
308
309#[derive(Debug, Clone, Copy, PartialEq)]
311pub struct StepResult {
312 pub state: FlightState,
314 pub rocket: RocketSample,
316 pub substeps: usize,
318}
319
320#[derive(Debug, Clone)]
326pub struct Simulator<E> {
327 rocket: Rocket,
328 forces: E,
329 settings: IntegratorSettings,
330}
331
332impl<E: ForceModel> Simulator<E> {
333 pub fn new(rocket: Rocket, forces: E, settings: IntegratorSettings) -> Result<Self> {
335 settings.validate()?;
336 Ok(Self {
337 rocket,
338 forces,
339 settings,
340 })
341 }
342
343 pub fn rocket(&self) -> &Rocket {
345 &self.rocket
346 }
347 pub fn forces(&self) -> &E {
349 &self.forces
350 }
351 pub fn forces_mut(&mut self) -> &mut E {
353 &mut self.forces
354 }
355 pub fn settings(&self) -> IntegratorSettings {
357 self.settings
358 }
359
360 fn end_time(&self, state: &FlightState, duration_s: f64) -> Result<f64> {
361 state.validate()?;
362 positive(duration_s, "duration_s")?;
363 let end = state.time_s + duration_s;
364 if !end.is_finite() || end <= state.time_s {
365 return Err(Error::invalid(
366 "duration_s",
367 "must advance time to a finite, distinguishable value",
368 ));
369 }
370 if duration_s > self.settings.max_step_s * self.settings.max_substeps as f64 {
371 return Err(Error::StepLimitExceeded {
372 limit: self.settings.max_substeps,
373 });
374 }
375 Ok(end)
376 }
377
378 pub fn step(
381 &self,
382 state: &FlightState,
383 duration_s: f64,
384 control: ControlInput,
385 ) -> Result<StepResult> {
386 let end = self.end_time(state, duration_s)?;
387 let (axis, steering) = self.resolve_control(control)?;
388 let mut current = *state;
389 let mut substeps = 0;
390 let mut grid_index = 1usize;
391 while current.time_s < end {
392 if substeps >= self.settings.max_substeps {
393 return Err(Error::StepLimitExceeded {
394 limit: self.settings.max_substeps,
395 });
396 }
397 let grid_boundary =
401 (state.time_s + grid_index as f64 * self.settings.max_step_s).min(end);
402 let mut boundary = grid_boundary;
403 if let Some(event) = self.rocket.next_event_after(current.time_s) {
404 boundary = boundary.min(event);
405 }
406 if boundary <= current.time_s {
407 return Err(Error::NumericalFailure(
408 "internal timestep cannot advance time",
409 ));
410 }
411 current = self.rk4(current, boundary, axis, steering)?;
412 substeps += 1;
413 if boundary == grid_boundary {
414 grid_index += 1;
415 }
416 }
417 Ok(StepResult {
418 state: current,
419 rocket: self.rocket.sample_unchecked(current.time_s),
420 substeps,
421 })
422 }
423
424 pub fn step_guided<G: Guidance + ?Sized>(
428 &self,
429 state: &FlightState,
430 duration_s: f64,
431 guidance: &mut G,
432 ) -> Result<StepResult> {
433 self.end_time(state, duration_s)?;
434 let command = guidance.command(GuidanceContext {
435 state,
436 rocket: self.rocket.sample_unchecked(state.time_s),
437 step_duration_s: duration_s,
438 max_steering_accel_mps2: self.settings.max_steering_accel_mps2,
439 })?;
440 self.step(state, duration_s, command)
441 }
442
443 fn resolve_control(&self, control: ControlInput) -> Result<(Vec3, Vec3)> {
444 if !finite_vec(&control.thrust_direction)
445 || !finite_vec(&control.steering_acceleration_mps2)
446 {
447 return Err(Error::invalid("control", "all components must be finite"));
448 }
449 let scale = control.thrust_direction.amax();
451 if scale == 0.0 {
452 return Err(Error::invalid("thrust_direction", "must be nonzero"));
453 }
454 let scaled = control.thrust_direction / scale;
455 let axis = scaled / scaled.norm();
456 let scale = control.steering_acceleration_mps2.amax();
457 let steering = if scale == 0.0 || self.settings.max_steering_accel_mps2 == 0.0 {
458 Vec3::zeros()
459 } else {
460 let scaled = control.steering_acceleration_mps2 / scale;
461 let norm = scaled.norm();
462 if scale > self.settings.max_steering_accel_mps2 / norm {
463 scaled * (self.settings.max_steering_accel_mps2 / norm)
464 } else {
465 control.steering_acceleration_mps2
466 }
467 };
468 Ok((axis, steering))
469 }
470
471 fn derivative(
472 &self,
473 state: FlightState,
474 axis: Vec3,
475 steering: Vec3,
476 stages: &[crate::motor::IntervalStage],
477 ) -> Result<(Vec3, Vec3)> {
478 state
479 .validate()
480 .map_err(|_| Error::NumericalFailure("non-finite intermediate state"))?;
481 let mass = self.rocket.interval_mass(state.time_s, stages);
482 let thrust = self.rocket.interval_thrust(state.time_s, stages);
483 let acceleration = self.forces.acceleration_with_axis(&state, mass, axis)?
484 + axis * (thrust / mass)
485 + steering;
486 if !finite_vec(&acceleration) {
487 return Err(Error::NumericalFailure("non-finite acceleration"));
488 }
489 Ok((state.velocity_mps, acceleration))
490 }
491
492 fn rk4(&self, start: FlightState, end: f64, axis: Vec3, steering: Vec3) -> Result<FlightState> {
493 let dt = end - start.time_s;
494 let stages = self.rocket.interval_stages(start.time_s);
495 let stage = |derivative: (Vec3, Vec3), delta: f64, time_s: f64| FlightState {
496 time_s,
497 position_m: start.position_m + derivative.0 * delta,
498 velocity_mps: start.velocity_mps + derivative.1 * delta,
499 };
500 let midpoint = start.time_s + dt * 0.5;
501 let k1 = self.derivative(start, axis, steering, &stages)?;
502 let k2 = self.derivative(stage(k1, dt * 0.5, midpoint), axis, steering, &stages)?;
503 let k3 = self.derivative(stage(k2, dt * 0.5, midpoint), axis, steering, &stages)?;
504 let k4 = self.derivative(stage(k3, dt, end), axis, steering, &stages)?;
505 let result = FlightState {
506 time_s: end,
507 position_m: start.position_m + (k1.0 + k2.0 * 2.0 + k3.0 * 2.0 + k4.0) * (dt / 6.0),
508 velocity_mps: start.velocity_mps + (k1.1 + k2.1 * 2.0 + k3.1 * 2.0 + k4.1) * (dt / 6.0),
509 };
510 result
511 .validate()
512 .map_err(|_| Error::NumericalFailure("non-finite output state"))?;
513 Ok(result)
514 }
515}