Skip to main content

deep_time/physics/
spacetime.rs

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