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}