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}