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}