Skip to main content

deep_time/physics/
spacetime.rs

1//! Local spacetime state (α, β, curvature).
2
3use crate::{C_SQUARED, Drift, Position, Real, Velocity, sqrt};
4
5/// The three local spacetime quantities that fully determine how fast an observer’s
6/// proper time advances relative to coordinate time.
7///
8/// This structure holds the gravitational lapse factor, the observer’s local velocity,
9/// and the curvature information needed for the library’s unified proper-time model.
10/// It is the low-level input that `Drift` uses internally.
11#[derive(Copy, Clone, Debug, PartialEq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
14pub struct Spacetime {
15    /// Gravitational lapse (redshift) factor α.  
16    /// This is the factor by which clocks run slower in a gravitational potential.
17    pub alpha: Real,
18
19    /// Local three-velocity β = v/c measured in the coordinate rest frame.
20    pub beta: Real,
21
22    /// Kretschmann scalar (a scalar measure of spacetime curvature).  
23    /// In the weak-field regime — where |Φ|/c² ≪ 1 and the gravitational field varies
24    /// over macroscopic distances — this value is effectively zero and can safely be
25    /// left at its default. It only becomes numerically relevant in strong-field
26    /// environments such as:
27    ///
28    /// - the surface or immediate vicinity of neutron stars (where |Φ|/c² ≈ 0.15–0.25);
29    /// - regions near a black-hole event horizon (e.g. the photon rings imaged by the
30    ///   Event Horizon Telescope around M87* or Sgr A*);
31    /// - the final inspiral and merger phases of binary neutron-star or black-hole
32    ///   systems (as observed by LIGO/Virgo in events such as GW170817 or GW150914).
33    ///
34    /// In these regimes a realistic non-zero value (estimated from the local potential
35    /// and a characteristic length scale) activates the library’s intrinsic Planck-scale
36    /// saturation term.
37    pub kretschmann: Real,
38}
39
40impl Spacetime {
41    #[inline]
42    pub const fn new(alpha: Real, beta: Real, kretschmann: Real) -> Spacetime {
43        Self {
44            alpha,
45            beta,
46            kretschmann,
47        }
48    }
49
50    /// Returns the instantaneous proper-time rate `dτ/dt` from this snapshot.
51    ///
52    /// Convenience method that internally uses the same unified calculation as
53    /// `Drift::proper_time_rate`.
54    #[inline]
55    pub const fn proper_time_rate(&self) -> Real {
56        Drift::from_spacetime(self).proper_time_rate()
57    }
58
59    /// Convenience for direct gravimeter / sensor paths.
60    #[inline]
61    pub const fn from_gravitic_and_velocity(
62        alpha: Real,
63        velocity: Velocity,
64        kretschmann: Real,
65    ) -> Spacetime {
66        Self::new(alpha, velocity.beta(), kretschmann)
67    }
68
69    /// Converts the Newtonian gravitational potential Φ/c² (where Φ < 0 for bound orbits)
70    /// into the relativistic lapse factor α = √(1 + 2Φ/c²).
71    ///
72    /// This function implements the standard weak-field approximation used in general
73    /// relativity. It is valid when the dimensionless gravitational potential satisfies
74    /// |Φ|/c² ≪ 1. In this regime spacetime is nearly flat, gravitational time dilation
75    /// is a small perturbation, and higher-order curvature effects can safely be neglected.
76    /// The resulting α gives the factor by which clocks tick more slowly in a gravitational
77    /// well relative to a distant reference clock.
78    ///
79    /// This approximation is excellent for solar-system navigation, GNSS satellites,
80    /// most spacecraft operations, and any environment where |Φ|/c² remains much smaller
81    /// than ~0.01. It is exported from `deep_time::alpha_from_weak_field_potential`
82    /// and is the recommended way to obtain the lapse factor when you have the local
83    /// Newtonian potential.
84    ///
85    /// The weak-field regime breaks down in strong-gravity environments where
86    /// |Φ|/c² approaches or exceeds ~0.1. Such conditions occur near:
87    ///
88    /// - the surface or immediate vicinity of neutron stars (where |Φ|/c² ≈ 0.15–0.25);
89    /// - regions near a black-hole event horizon (e.g. the photon rings imaged by the
90    ///   Event Horizon Telescope around M87* or Sgr A*);
91    /// - the final inspiral and merger phases of binary neutron-star or black-hole
92    ///   systems (as observed by LIGO/Virgo in events such as GW170817 or GW150914).
93    ///
94    /// In those extreme regimes this function alone is no longer sufficient; a full
95    /// strong-field treatment (including curvature information passed to `Spacetime`)
96    /// is required.
97    #[inline]
98    pub const fn alpha_from_weak_field_potential(grav_potential_over_c2: Real) -> Real {
99        // gravitational_potential_over_c2 = Φ/c² < 0 → α < 1 (clocks run slower)
100        sqrt((f!(1.0) + f!(2.0) * grav_potential_over_c2).max(f!(0.0)))
101    }
102
103    /// Kretschmann scalar from total relativity
104    /// Computes the Kretschmann scalar \(\mathcal{K}\) from the total gravitational
105    /// relativity experienced by a local observer at the observer’s spacetime point.
106    ///
107    /// This is the canonical, physics-true convenience function for the master Lagrangian.
108    ///
109    /// Information on the master Lagrangian can be found
110    /// [here](https://github.com/ragardner/deep-time/blob/main/docs/relativity.md).
111    ///
112    /// It uses:
113    /// - `phi` = Φ/c² — the total local gravitational potential (redshift/gravity effect)
114    ///   felt by the observer from all masses.
115    /// - `characteristic_length_scale` — the typical length scale (in meters) over which
116    ///   the gravitational field varies at the observer’s location.
117    ///
118    /// **For existing weak-field users** (Earth orbit, GNSS, solar-system navigation):
119    /// Supply your existing `phi` value and set `characteristic_length_scale = 0.0`.
120    /// The function safely returns 0.0 (the value in double precision).
121    ///
122    /// **For strong-field / future users** (black-hole flybys, neutron stars, direct
123    /// gravimeters, or full metric evaluation):
124    /// Supply the measured or computed \(\phi\) and the real local length scale (or
125    /// the value from your metric). The function returns a physically accurate non-zero
126    /// curvature.
127    pub const fn kretschmann_from_potential_and_scale(
128        grav_potential_over_c2: Real,
129        characteristic_length_scale: Real,
130    ) -> Real {
131        if characteristic_length_scale <= f!(0.0) || grav_potential_over_c2 <= f!(0.0) {
132            return f!(0.0);
133        }
134        // Exact weak-field limit: K ≈ 48 φ² / L⁴
135        let curvature_scale = f!(2.0) * grav_potential_over_c2
136            / (characteristic_length_scale * characteristic_length_scale);
137        f!(12.0) * (curvature_scale * curvature_scale)
138    }
139
140    /// Computes both the gravitational lapse factor `α` and an estimate of the
141    /// Kretschmann scalar from the dimensionless gravitational potential Φ/c²
142    /// and a characteristic length scale.
143    ///
144    /// The lapse factor α is computed using `alpha_from_weak_field_potential`,
145    /// which is the standard weak-field expression α = √(1 + 2Φ/c²). It is valid
146    /// when the dimensionless gravitational potential satisfies |Φ|/c² ≪ 1. In
147    /// this regime spacetime is nearly flat, gravitational time dilation is a
148    /// small perturbation, and higher-order curvature effects can safely be
149    /// neglected. The resulting α gives the factor by which clocks tick more
150    /// slowly in a gravitational well relative to a distant reference clock.
151    ///
152    /// This approximation is excellent for solar-system navigation, GNSS
153    /// satellites, most spacecraft operations, and any environment where
154    /// |Φ|/c² remains much smaller than ~0.01. It is exported from
155    /// `deep_time::alpha_from_weak_field_potential` and is the recommended
156    /// way to obtain the lapse factor when you have the local Newtonian potential.
157    ///
158    /// The weak-field regime breaks down in strong-gravity environments where
159    /// |Φ|/c² approaches or exceeds ~0.1. Such conditions occur near:
160    ///
161    /// - the surface or immediate vicinity of neutron stars (where |Φ|/c² ≈ 0.15–0.25);
162    /// - regions near a black-hole event horizon (e.g. the photon rings imaged by the
163    ///   Event Horizon Telescope around M87* or Sgr A*);
164    /// - the final inspiral and merger phases of binary neutron-star or black-hole
165    ///   systems (as observed by LIGO/Virgo in events such as GW170817 or GW150914).
166    ///
167    /// In those extreme regimes this function alone is no longer sufficient; a full
168    /// strong-field treatment (including curvature information passed to `Spacetime`)
169    /// is required.
170    ///
171    /// For the `characteristic_length_scale` parameter:
172    /// - In weak-field conditions, pass `0.0`. This returns exactly the same clock
173    ///   rate as the classic relativistic formulation and sets the Kretschmann scalar
174    ///   to zero (its default value for all ordinary navigation, GNSS, or solar-system
175    ///   work).
176    /// - In strong-field conditions, supply the typical length scale (in meters) over
177    ///   which the gravitational field varies significantly at the observer’s location.
178    ///   This allows the library to estimate the Kretschmann scalar and activate the
179    ///   intrinsic Planck-scale saturation term when curvature becomes extreme.
180    pub const fn from_potential_velocity_and_scale(
181        grav_potential_over_c2: Real, // Φ/c² (total local potential)
182        velocity: Velocity,
183        characteristic_length_scale: Real,
184    ) -> Spacetime {
185        let alpha: Real = Self::alpha_from_weak_field_potential(grav_potential_over_c2);
186        let kretschmann: Real = Self::kretschmann_from_potential_and_scale(
187            grav_potential_over_c2,
188            characteristic_length_scale,
189        );
190        Self::from_gravitic_and_velocity(alpha, velocity, kretschmann)
191    }
192
193    /// Recovers the Newtonian gravitational potential Φ (m²/s²) from the
194    /// gravitational lapse factor α using the weak-field relation.
195    ///
196    /// \[
197    /// \alpha = \sqrt{1 + \frac{2\Phi}{c^2}} \quad\implies\quad
198    /// \Phi = \frac{c^2}{2}(\alpha^2 - 1)
199    /// \]
200    ///
201    /// This is the inverse of [`Spacetime::alpha_from_weak_field_potential`].
202    #[inline]
203    pub const fn grav_potential_from_alpha(alpha: Real) -> Real {
204        let alpha_sq = alpha * alpha;
205        (alpha_sq - f!(1.0)) / f!(2.0) * C_SQUARED
206    }
207
208    /// Computes the total Newtonian gravitational potential Φ at a given position
209    /// from an arbitrary collection of point-mass bodies (Sun, Earth, Moon,
210    /// planets, asteroids, etc.).
211    ///
212    /// This is the standard method used by real mission planners (Apollo,
213    /// Artemis, Mars orbiters, lunar landers) and in open-source astrodynamics
214    /// libraries (SPICE/NAIF, Orekit, GMAT, poliastro). It evaluates
215    ///
216    /// \[
217    /// \Phi = -\sum_i \frac{GM_i}{r_i}
218    /// \]
219    ///
220    /// ## Examples
221    ///
222    /// Realistic cislunar trajectory
223    ///
224    /// ```rust
225    /// use deep_time::{Position, Spacetime};
226    ///
227    /// let bodies = [
228    ///     (Position::from_au(0.0, 0.0, 0.0), 1.3271244e20),     // Sun
229    ///     (Position::from_au(1.0, 0.0, 0.0), 3.9860044e14),     // Earth
230    ///     (Position::from_au(1.00257, 0.0, 0.0), 4.9048695e12), // Moon
231    /// ];
232    ///
233    /// let position = Position::from_au(1.001, 0.001, 0.0); // e.g. spacecraft, asteroid, etc.
234    ///
235    /// let phi = Spacetime::grav_potential_from_point_masses(
236    ///     position,
237    ///     bodies.iter().copied(),
238    /// );
239    /// ```
240    pub fn grav_potential_from_point_masses<I>(position: Position, bodies: I) -> Real
241    where
242        I: IntoIterator<Item = (Position, Real)>, // (body_position, GM in m³/s²)
243    {
244        let mut phi = 0.0;
245        for (body_pos, gm) in bodies {
246            let r = position.distance_to(body_pos);
247            if r > 0.0 {
248                phi -= gm / r;
249            }
250        }
251        phi
252    }
253}
254
255#[cfg(feature = "wire")]
256impl Spacetime {
257    /// Size of the canonical wire representation in bytes (24 bytes).
258    pub const WIRE_SIZE: usize = 24;
259
260    /// Serializes this [`Spacetime`] snapshot into a fixed 24-byte buffer.
261    ///
262    /// All fields are stored as little-endian IEEE 754 `f64`.
263    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
264        let mut buf = [0u8; Self::WIRE_SIZE];
265        buf[0..8].copy_from_slice(&self.alpha.to_le_bytes());
266        buf[8..16].copy_from_slice(&self.beta.to_le_bytes());
267        buf[16..24].copy_from_slice(&self.kretschmann.to_le_bytes());
268        buf
269    }
270
271    /// Deserializes a [`Spacetime`] from exactly 24 bytes.
272    ///
273    /// ## Security
274    ///
275    /// Accepts any `f64` bit pattern (including `NaN`/`Inf`) to match the
276    /// type’s own invariants. Fixed size makes it immune to length-based
277    /// attacks. Safe for untrusted input.
278    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
279        if bytes.len() != Self::WIRE_SIZE {
280            return None;
281        }
282        let alpha = Real::from_le_bytes([
283            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
284        ]);
285        let beta = Real::from_le_bytes([
286            bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
287        ]);
288        let kretschmann = Real::from_le_bytes([
289            bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23],
290        ]);
291        Some(Self {
292            alpha,
293            beta,
294            kretschmann,
295        })
296    }
297}