Skip to main content

deep_time/physics/
spacetime.rs

1//! Local spacetime state (α, β, curvature) for proper-time rates.
2
3use crate::{C_SQUARED, Drift, Position, Real, Velocity, sqrt};
4
5/// Snapshot of the local quantities that set a clock’s rate \(d\tau/dt\).
6///
7/// Think of this as “how gravity and motion look right here, right now” for a
8/// clock:
9///
10/// - **α** — gravitational redshift factor (deeper in a well → smaller α →
11///   slower clocks).
12/// - **β** — speed as a fraction of light speed (\(v/c\)).
13/// - **kretschmann** — a curvature measure; leave at `0.0` for almost all
14///   Earth/solar-system work.
15///
16/// Trajectory APIs either take [`Spacetime`] samples directly, or build them
17/// from velocity and potential via
18/// [`Spacetime::from_potential_velocity_and_scale`].
19///
20/// Instantaneous rate: [`Spacetime::proper_time_rate`].
21#[derive(Clone, Debug, PartialEq)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
24pub struct Spacetime {
25    /// Gravitational lapse (redshift) factor α.
26    ///
27    /// Clocks run slower where gravity is stronger: α < 1 in a potential well.
28    /// In the weak field, α ≈ √(1 + 2Φ/c²) with Φ < 0.
29    pub alpha: Real,
30
31    /// Local three-velocity β = v/c in the coordinate rest frame used for the analysis.
32    pub beta: Real,
33
34    /// Kretschmann scalar (curvature invariant), in geometric units of the model.
35    ///
36    /// For solar-system, GNSS, and similar work leave this **0.0** — the
37    /// curvature correction is negligible. Non-zero values matter only in
38    /// extreme gravity (near compact objects), where you may estimate K from
39    /// potential and a length scale (see
40    /// [`Spacetime::kretschmann_from_potential_and_scale`]) or supply K from a
41    /// metric.
42    pub kretschmann: Real,
43}
44
45impl Spacetime {
46    #[inline]
47    pub const fn new(alpha: Real, beta: Real, kretschmann: Real) -> Spacetime {
48        Self {
49            alpha,
50            beta,
51            kretschmann,
52        }
53    }
54
55    /// Instantaneous proper-time rate \(d\tau/dt\) for this snapshot.
56    ///
57    /// Dimensionless: `1.0` means the clock tracks coordinate time; values a
58    /// little below `1.0` are typical when moving or sitting in a gravitational
59    /// well. Same calculation as [`Drift::proper_time_rate`] after
60    /// [`Drift::from_spacetime`].
61    #[inline]
62    pub const fn proper_time_rate(&self) -> Real {
63        Drift::from_spacetime(self).proper_time_rate()
64    }
65
66    /// Build from lapse α, a velocity vector, and Kretschmann K.
67    ///
68    /// Sets β from [`Velocity::beta`]. Pass `kretschmann = 0.0` for ordinary
69    /// weak-field work.
70    #[inline]
71    pub const fn from_gravitic_and_velocity(
72        alpha: Real,
73        velocity: Velocity,
74        kretschmann: Real,
75    ) -> Spacetime {
76        Self::new(alpha, velocity.beta(), kretschmann)
77    }
78
79    /// Weak-field lapse from dimensionless potential: α = √(1 + 2Φ/c²).
80    ///
81    /// Given how deep you are in a gravity well (as Φ/c²), return the factor by
82    /// which clocks run slow. Φ is **negative** for bound gravity, so α < 1.
83    ///
84    /// ## Validity
85    ///
86    /// Good when |Φ|/c² ≪ 1 (Earth, solar system, most spacecraft). Not
87    /// sufficient alone near neutron stars or black holes (|Φ|/c² ≳ 0.1); then
88    /// you need a strong-field metric treatment and usually a non-zero
89    /// Kretschmann on [`Spacetime`].
90    ///
91    /// ## Note on units
92    ///
93    /// Argument is **Φ/c²** (dimensionless), not Φ in m²/s². Trajectory
94    /// `*_from_states` APIs take SI Φ and divide by \(c^2\) for you.
95    #[inline]
96    pub const fn alpha_from_weak_field_potential(grav_potential_over_c2: Real) -> Real {
97        // grav_potential_over_c2 = Φ/c² < 0 → α < 1 (clocks run slower)
98        sqrt((f!(1.0) + f!(2.0) * grav_potential_over_c2).max(f!(0.0)))
99    }
100
101    /// Estimate Kretschmann scalar \(\mathcal{K} \approx 48\,\phi^2 / L^4\).
102    ///
103    /// Optional helper to guess curvature from potential strength and a length
104    /// scale. For normal flight timing you do **not** need this: pass
105    /// `characteristic_length_scale = 0.0` and get K = 0.
106    ///
107    /// ## Parameters
108    ///
109    /// - `grav_potential_over_c2` — Φ/c² (typically **negative**). The estimate
110    ///   uses φ², so the sign of φ does not matter for K.
111    /// - `characteristic_length_scale` — meters. Use **`0.0`** to disable
112    ///   (recommended default). A positive L is a curvature scale; for a single
113    ///   spherical mass the Schwarzschild match is L = r with
114    ///   |φ| = GM/(c² r). L cannot be recovered from φ alone in general.
115    ///
116    /// Background: [relativity model](https://github.com/ragardner/deep-time/blob/main/docs/relativity.md).
117    pub const fn kretschmann_from_potential_and_scale(
118        grav_potential_over_c2: Real,
119        characteristic_length_scale: Real,
120    ) -> Real {
121        // Weak-field default: no length scale → curvature term disabled.
122        // Do **not** reject negative φ: bound-system potentials are negative, and the
123        // estimate uses φ² (see below).
124        if characteristic_length_scale <= f!(0.0) {
125            return f!(0.0);
126        }
127        // Weak-field limit: K ≈ 48 φ² / L⁴
128        // (curvature_scale = 2φ/L² ⇒ 12 · (curvature_scale)² = 48 φ²/L⁴)
129        let curvature_scale = f!(2.0) * grav_potential_over_c2
130            / (characteristic_length_scale * characteristic_length_scale);
131        f!(12.0) * (curvature_scale * curvature_scale)
132    }
133
134    /// Build [`Spacetime`] from dimensionless potential Φ/c², velocity, and length scale.
135    ///
136    /// Turn “how deep in the well” and “how fast I’m moving” into the α, β, K
137    /// snapshot used for clock rates.
138    ///
139    /// ## Parameters
140    ///
141    /// - `grav_potential_over_c2` — **Φ/c²** (dimensionless), not SI Φ.
142    /// - `velocity` — m/s; only speed enters (via β).
143    /// - `characteristic_length_scale` — pass **`0.0`** for solar-system / GNSS
144    ///   work (K = 0). Positive L only if you want the optional K estimate.
145    ///
146    /// For SI potential (m²/s²), divide by \(c^2\) first, or use trajectory
147    /// `proper_time_*_from_states` which does that conversion.
148    ///
149    /// Weak-field α is valid for |Φ|/c² ≪ 1. Strong gravity needs more than
150    /// this constructor alone.
151    pub const fn from_potential_velocity_and_scale(
152        grav_potential_over_c2: Real, // Φ/c² (total local potential)
153        velocity: Velocity,
154        characteristic_length_scale: Real,
155    ) -> Spacetime {
156        let alpha: Real = Self::alpha_from_weak_field_potential(grav_potential_over_c2);
157        let kretschmann: Real = Self::kretschmann_from_potential_and_scale(
158            grav_potential_over_c2,
159            characteristic_length_scale,
160        );
161        Self::from_gravitic_and_velocity(alpha, velocity, kretschmann)
162    }
163
164    /// Recovers the Newtonian gravitational potential Φ (m²/s²) from the
165    /// gravitational lapse factor α using the weak-field relation.
166    ///
167    /// \[
168    /// \alpha = \sqrt{1 + \frac{2\Phi}{c^2}} \quad\implies\quad
169    /// \Phi = \frac{c^2}{2}(\alpha^2 - 1)
170    /// \]
171    ///
172    /// This is the inverse of [`Spacetime::alpha_from_weak_field_potential`].
173    #[inline]
174    pub const fn grav_potential_from_alpha(alpha: Real) -> Real {
175        let alpha_sq = alpha * alpha;
176        (alpha_sq - f!(1.0)) / f!(2.0) * C_SQUARED
177    }
178
179    /// Newtonian point-mass potential Φ = −Σ GMᵢ / rᵢ at a position (m²/s²).
180    ///
181    /// Sums “how much gravity well” you feel from a list of bodies treated as
182    /// point masses. The result is **negative** near masses. Use it to build
183    /// samples for trajectory proper-time APIs, or convert to α via
184    /// Φ/c² and [`Spacetime::alpha_from_weak_field_potential`].
185    ///
186    /// ## Limits
187    ///
188    /// Point masses only — no Earth \(J_2\), no tides, no extended bodies. Fine
189    /// for rough multi-body Φ or cislunar order-of-magnitude work; LEO-grade
190    /// timing usually needs multipoles from a full gravity model.
191    ///
192    /// Body positions and the evaluation point must share the same coordinate
193    /// frame.
194    ///
195    /// ## Example
196    ///
197    /// ```rust
198    /// use deep_time::{Position, Spacetime};
199    ///
200    /// let bodies = [
201    ///     (Position::from_au(0.0, 0.0, 0.0), 1.3271244e20),     // Sun GM
202    ///     (Position::from_au(1.0, 0.0, 0.0), 3.9860044e14),     // Earth GM
203    ///     (Position::from_au(1.00257, 0.0, 0.0), 4.9048695e12), // Moon GM
204    /// ];
205    /// let position = Position::from_au(1.001, 0.001, 0.0);
206    /// let phi = Spacetime::grav_potential_from_point_masses(
207    ///     &position,
208    ///     bodies.iter().cloned(),
209    /// );
210    /// assert!(phi < 0.0);
211    /// ```
212    pub fn grav_potential_from_point_masses<I>(position: &Position, bodies: I) -> Real
213    where
214        I: IntoIterator<Item = (Position, Real)>, // (body_position, GM in m³/s²)
215    {
216        let mut phi = 0.0;
217        for (body_pos, gm) in bodies {
218            let r = position.distance_to(&body_pos);
219            if r > 0.0 {
220                phi -= gm / r;
221            }
222        }
223        phi
224    }
225}
226
227#[cfg(feature = "wire")]
228impl Spacetime {
229    /// Size of the canonical wire representation in bytes (24 bytes).
230    pub const WIRE_SIZE: usize = 24;
231
232    /// Serializes this [`Spacetime`] snapshot into a fixed 24-byte buffer.
233    ///
234    /// All fields are stored as little-endian IEEE 754 `f64`.
235    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
236        let mut buf = [0u8; Self::WIRE_SIZE];
237        buf[0..8].copy_from_slice(&self.alpha.to_le_bytes());
238        buf[8..16].copy_from_slice(&self.beta.to_le_bytes());
239        buf[16..24].copy_from_slice(&self.kretschmann.to_le_bytes());
240        buf
241    }
242
243    /// Deserializes a [`Spacetime`] from exactly 24 bytes.
244    ///
245    /// ## Security
246    ///
247    /// Accepts any `f64` bit pattern (including `NaN`/`Inf`) to match the
248    /// type’s own invariants. Fixed size makes it immune to length-based
249    /// attacks. Safe for untrusted input.
250    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
251        if bytes.len() != Self::WIRE_SIZE {
252            return None;
253        }
254        let alpha = Real::from_le_bytes([
255            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
256        ]);
257        let beta = Real::from_le_bytes([
258            bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
259        ]);
260        let kretschmann = Real::from_le_bytes([
261            bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23],
262        ]);
263        Some(Self {
264            alpha,
265            beta,
266            kretschmann,
267        })
268    }
269}