deep_causality_physics/kernels/propulsion/descent.rs
1/*
2 * SPDX-License-Identifier: MIT
3 * Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
4 */
5
6//! Powered-descent kinematics kernels: the closed-form stopping distance,
7//! the ignition-altitude solution, and the suicide-burn deceleration command.
8//! These are the Tier-A terminal-guidance closed forms of the retropulsion
9//! descent; Apollo polynomial guidance (Klumpp 1974) and convex
10//! powered-descent guidance (Açıkmeşe & Ploen 2007) are the named upgrade
11//! path beyond them.
12
13use crate::{Acceleration, Length, PhysicsError, Speed};
14use deep_causality_algebra::RealField;
15use deep_causality_num::FromPrimitive;
16
17/// Stopping distance under constant net deceleration
18///
19/// $$ d = \frac{v^2}{2\,a_{net}} $$
20///
21/// # Arguments
22/// * `speed` — current speed `v` (m/s, ≥ 0).
23/// * `net_deceleration` — constant net deceleration `a_net` (m/s², > 0). A
24/// vehicle with thrust-to-weight at or below one has `a_net ≤ 0` and cannot
25/// stop; that input is rejected, not extrapolated.
26///
27/// # References
28/// * Closed-form constant-acceleration kinematics; Klumpp, A. R., "Apollo
29/// Lunar Descent Guidance," Automatica 10(2), 1974, and Açıkmeşe, B., &
30/// Ploen, S. R., JGCD 30(5), 2007, as the guidance upgrade path.
31pub fn stopping_distance_kernel<R>(
32 speed: Speed<R>,
33 net_deceleration: Acceleration<R>,
34) -> Result<Length<R>, PhysicsError>
35where
36 R: RealField + FromPrimitive,
37{
38 let a = net_deceleration.value();
39 if a <= R::zero() {
40 return Err(PhysicsError::PhysicalInvariantBroken(
41 "Net deceleration must be positive; thrust-to-weight <= 1 cannot stop".into(),
42 ));
43 }
44 let v = speed.value();
45 let two = R::from_f64(2.0)
46 .ok_or_else(|| PhysicsError::NumericalInstability("R::from_f64(2.0) failed".into()))?;
47 Length::new(v * v / (two * a))
48}
49
50/// Ignition altitude for a constant-thrust vertical stopping burn
51///
52/// $$ h_{ign} = \frac{v^2}{2\,(a_T - g)} + h_{margin} $$
53///
54/// the stopping distance against the net deceleration `a_T − g`, plus the
55/// caller-supplied margin. The margin is an input by design: downstream it is
56/// sized from the weather-dispersion table's navigation-drift row, which is
57/// not this crate's business.
58///
59/// # Arguments
60/// * `speed` — descent speed at ignition `v` (m/s, ≥ 0).
61/// * `thrust_acceleration` — thrust acceleration `a_T = T/m` (m/s², must
62/// exceed `gravity`).
63/// * `gravity` — local gravitational acceleration `g` (m/s², > 0).
64/// * `margin` — additive altitude margin `h_margin` (m, ≥ 0).
65///
66/// # References
67/// * Closed-form constant-acceleration kinematics (see
68/// [`stopping_distance_kernel`]).
69pub fn ignition_altitude_kernel<R>(
70 speed: Speed<R>,
71 thrust_acceleration: Acceleration<R>,
72 gravity: Acceleration<R>,
73 margin: Length<R>,
74) -> Result<Length<R>, PhysicsError>
75where
76 R: RealField + FromPrimitive,
77{
78 let g = gravity.value();
79 if g <= R::zero() {
80 return Err(PhysicsError::Singularity(
81 "Gravitational acceleration must be positive".into(),
82 ));
83 }
84 let a_net = thrust_acceleration.value() - g;
85 if a_net <= R::zero() {
86 return Err(PhysicsError::PhysicalInvariantBroken(
87 "Thrust acceleration must exceed gravity; thrust-to-weight <= 1 cannot stop".into(),
88 ));
89 }
90 let d = stopping_distance_kernel(speed, Acceleration::new(a_net)?)?;
91 Length::new(d.value() + margin.value())
92}
93
94/// Suicide-burn deceleration command
95///
96/// $$ a_{cmd} = \frac{v^2}{2h} + g $$
97///
98/// the constant deceleration that nulls the descent speed exactly at the
99/// surface from the current speed `v` and altitude `h` — the closed-form
100/// feedback the terminal-guidance stage clamps into the safety envelope
101/// downstream.
102///
103/// # Arguments
104/// * `speed` — current descent speed `v` (m/s, ≥ 0).
105/// * `altitude` — current altitude above the surface `h` (m, > 0). Ground
106/// contact (`h ≤ 0`) is rejected.
107/// * `gravity` — local gravitational acceleration `g` (m/s², > 0).
108///
109/// # References
110/// * Closed-form constant-acceleration kinematics (see
111/// [`stopping_distance_kernel`]).
112pub fn suicide_burn_deceleration_kernel<R>(
113 speed: Speed<R>,
114 altitude: Length<R>,
115 gravity: Acceleration<R>,
116) -> Result<Acceleration<R>, PhysicsError>
117where
118 R: RealField + FromPrimitive,
119{
120 let h = altitude.value();
121 if h <= R::zero() {
122 return Err(PhysicsError::Singularity(
123 "Altitude must be positive; the vehicle is at or below ground contact".into(),
124 ));
125 }
126 let g = gravity.value();
127 if g <= R::zero() {
128 return Err(PhysicsError::Singularity(
129 "Gravitational acceleration must be positive".into(),
130 ));
131 }
132 let v = speed.value();
133 let two = R::from_f64(2.0)
134 .ok_or_else(|| PhysicsError::NumericalInstability("R::from_f64(2.0) failed".into()))?;
135 Acceleration::new(v * v / (two * h) + g)
136}