Skip to main content

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}