deep_time/physics/drift.rs
1//! Quadratic polynomial for relativistic corrections, clock drift, and custom timescale steering.
2//!
3//! Used to model the accumulated difference between Proper time (τ)
4//! and a coordinate time such as TT (or any other `Scale`).
5//!
6//! Information on the underlying physical model (the master Lagrangian, different
7//! regimes of behavior, and its relationship to general relativity) can be found
8//! [here](https://github.com/ragardner/deep-time/blob/main/docs/relativity.md).
9
10use crate::{ATTOS_PER_SEC_I128, C_SQUARED, Dt, PLANCK_LENGTH_4, Real, Scale, dt, sqrt};
11
12use super::{Spacetime, Velocity};
13
14/// Quadratic polynomial that describes the accumulated difference between an
15/// observer’s proper time (the time measured by a real clock moving through
16/// spacetime) and a chosen coordinate time such as TT, TAI, or any other
17/// `Scale`.
18///
19/// The polynomial follows the classic form
20/// Δt = constant + rate·Δt + accel·(Δt)²
21/// where the three coefficients capture any fixed offset, constant drift, and
22/// quadratic acceleration of the clock. This structure is used throughout
23/// spacecraft navigation, GNSS systems, and relativistic timing pipelines to
24/// steer clocks, predict time offsets, and maintain synchronization over long
25/// durations.
26///
27/// All three coefficients are stored using [`Dt`].
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
30#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub struct Drift {
32 /// Constant term a₀ expressed in seconds.
33 /// This represents any fixed time offset between the observer’s proper time
34 /// and the chosen coordinate time.
35 pub constant: Dt,
36
37 /// Linear drift rate a₁ expressed in seconds per second.
38 /// This term captures a steady fractional rate difference (for example, a
39 /// clock that runs consistently fast or slow).
40 pub rate: Dt,
41
42 /// Quadratic acceleration term a₂ expressed in seconds per second squared.
43 /// This term accounts for any changing drift rate, such as the gradual
44 /// acceleration caused by relativistic effects or hardware aging.
45 pub accel: Dt,
46}
47
48impl Drift {
49 /// Creates a new `Drift` polynomial from its three coefficients.
50 #[inline]
51 pub const fn new(constant: Dt, rate: Dt, accel: Dt) -> Drift {
52 Self {
53 constant,
54 rate,
55 accel,
56 }
57 }
58
59 /// The zero polynomial representing no correction at all.
60 ///
61 /// Use this when the observer’s clock is already perfectly synchronized with
62 /// the chosen coordinate time.
63 pub const ZERO: Self = Self::new(Dt::ZERO, Dt::ZERO, Dt::ZERO);
64
65 /// Creates a [`Drift`] consisting of a pure constant offset.
66 ///
67 /// This is the most common constructor when only a fixed time bias is known
68 /// (for example, after a one-time clock synchronization or leap-second
69 /// adjustment).
70 #[inline]
71 pub const fn from_constant(c: Dt) -> Drift {
72 Self::new(c, Dt::ZERO, Dt::ZERO)
73 }
74
75 /// Creates a [`Drift`] consisting of a constant offset together with a
76 /// constant linear drift rate.
77 ///
78 /// This form is very common for GNSS receivers and spacecraft clock steering,
79 /// where a steady fractional frequency offset must be corrected in addition
80 /// to any fixed bias.
81 #[inline]
82 pub const fn from_offset_and_rate(offset: Dt, rate: Dt) -> Drift {
83 Self::new(offset, rate, Dt::ZERO)
84 }
85
86 /// Returns the instantaneous proper-time rate `dτ/dt` (dimensionless).
87 ///
88 /// This value tells you how fast a real physical clock (such as a spacecraft
89 /// onboard clock) is advancing compared to coordinate time. A value of
90 /// `1.0` means the clock runs at the normal rate. Values slightly below `1.0`
91 /// are typical when the clock is moving or sitting in a gravitational well.
92 ///
93 /// The rate includes special-relativistic velocity effects, gravitational
94 /// time dilation, and the library’s built-in Planck-scale saturation term.
95 #[inline]
96 pub const fn proper_time_rate(&self) -> Real {
97 f!(1.0) + self.rate.to_sec_f()
98 }
99
100 /// Evaluates the polynomial at the given elapsed coordinate time span.
101 ///
102 /// Returns the accumulated time difference (in seconds) between proper
103 /// time and coordinate time after the interval span has passed.
104 ///
105 /// Uses saturating attosecond arithmetic (same policy as [`Dt`] add/mul).
106 /// Scaled products `(a·b)/10¹⁸` avoid wrapping or early-clamping the
107 /// intermediate `a·b` when it exceeds `i128` but the result still fits.
108 pub const fn time_diff_after(&self, span: &Dt) -> Dt {
109 let dt_attos = span.to_attos();
110 let mut total_attos = self.constant.to_attos();
111
112 if !self.rate.is_zero() || !self.accel.is_zero() {
113 // Linear: rate * dt → (rate_attos * dt_attos) / 10¹⁸
114 let rate_term = saturating_mul_div_attos_per_sec(self.rate.to_attos(), dt_attos);
115 total_attos = total_attos.saturating_add(rate_term);
116
117 // Quadratic: accel * dt² → two successive scaled multiplies
118 let accel_dt = saturating_mul_div_attos_per_sec(self.accel.to_attos(), dt_attos);
119 let accel_term = saturating_mul_div_attos_per_sec(accel_dt, dt_attos);
120 total_attos = total_attos.saturating_add(accel_term);
121 }
122
123 dt!(total_attos)
124 }
125
126 /// Evaluates the deterministic relativistic/polynomial correction **and**
127 /// adds a user-supplied stochastic offset (in seconds).
128 ///
129 /// This is the single production method for realistic stochastic clock
130 /// modeling. In real mission pipelines the deterministic part (this
131 /// polynomial) is kept perfectly clean; stochastic noise (white phase noise,
132 /// random-walk frequency noise, Monte-Carlo realizations, Kalman process
133 /// noise, measured clock residuals, etc.) is added at evaluation time.
134 ///
135 /// Pass `0.0` (or simply call the original `time_diff_after`) when you
136 /// want purely deterministic behavior.
137 #[inline]
138 pub fn time_diff_after_with_noise(&self, span: &Dt, stochastic_offset_sec: Real) -> Dt {
139 self.time_diff_after(span).add(Dt::from_sec_f(
140 stochastic_offset_sec,
141 Scale::TAI,
142 Scale::TAI,
143 ))
144 }
145
146 /// Build a linear-rate [`Drift`] from speed (m/s) and SI potential Φ (m²/s²).
147 ///
148 /// Given how fast you move and how deep you sit in gravity, return a
149 /// [`Drift`] whose rate term matches the library’s proper-time model
150 /// (special-relativistic and gravitational effects). Useful when you want
151 /// the rate as a polynomial coefficient rather than integrating a path.
152 ///
153 /// ## `characteristic_length_scale`
154 ///
155 /// Pass **`0.0`** for ordinary weak-field work (Earth orbit, solar system):
156 /// Kretschmann is zero and the rate is the first-order weak-field form.
157 /// Pass a positive length (meters) only if you want the optional curvature
158 /// estimate (see [`Spacetime::kretschmann_from_potential_and_scale`]).
159 pub const fn from_velocity_potential_and_scale(
160 velocity_m_s: Real,
161 grav_potential_m2_s2: Real,
162 characteristic_length_scale: Real,
163 ) -> Drift {
164 let phi = grav_potential_m2_s2 / C_SQUARED;
165 let velocity = Velocity::from_speed(velocity_m_s);
166 let spacetime = Spacetime::from_potential_velocity_and_scale(
167 phi,
168 velocity,
169 characteristic_length_scale,
170 );
171 Self::from_spacetime(&spacetime)
172 }
173
174 /// Canonical low-level constructor that implements the library's general
175 /// relativity formula.
176 ///
177 /// This function is the single source of truth for the proper-time rate
178 /// calculation used throughout the library. Most users will never call it
179 /// directly; the high-level constructors `from_velocity_potential_and_scale`
180 /// and `from_spacetime` are the intended entry points.
181 ///
182 /// The internal expression is
183 /// K_eff = [δ(1 + x) + x(1−δ)²] / (1 + x)
184 /// where δ = α²(1−β²) and x = ℓ_Pl⁴ 𝒦.
185 ///
186 /// The returned rate offset is then applied as a linear term in the `Drift`
187 /// polynomial.
188 pub const fn from_unified_proper_time_rate(u: Real, kretschmann: Real) -> Drift {
189 let delta = u.max(f!(0.0));
190 let x = PLANCK_LENGTH_4 * kretschmann.max(f!(0.0));
191
192 let one_minus_delta = f!(1.0) - delta;
193 let num = delta * (f!(1.0) + x) + x * (one_minus_delta * one_minus_delta);
194 let k_eff = num / (f!(1.0) + x);
195
196 let rate_factor = sqrt(k_eff).max(f!(0.0));
197 let rate_offset = rate_factor - f!(1.0);
198
199 Self::from_offset_and_rate(
200 Dt::ZERO,
201 Dt::from_sec_f(rate_offset, Scale::TAI, Scale::TAI),
202 )
203 }
204
205 /// Creates a `Drift` from a fully resolved `Spacetime` snapshot.
206 ///
207 /// This is the canonical high-level entry point when you already hold a
208 /// `Spacetime` object containing the gravitational lapse factor α, the
209 /// local velocity β, and the Kretschmann scalar. It internally computes the
210 /// unified proper-time rate and packages the result as a `Drift`
211 /// polynomial ready for evaluation at any future time.
212 #[inline]
213 pub const fn from_spacetime(spacetime: &Spacetime) -> Drift {
214 let u = spacetime.alpha * spacetime.alpha * (f!(1.0) - spacetime.beta * spacetime.beta);
215 Self::from_unified_proper_time_rate(u, spacetime.kretschmann)
216 }
217}
218
219impl Dt {
220 /// Builds a clock-drift model in which this [`Dt`] is treated as the
221 /// initial fixed time difference between the observer’s proper time and
222 /// the chosen coordinate time.
223 ///
224 /// In practice you often compute or measure a one-time offset (for example
225 /// after a clock synchronization or a leap-second jump) and then want to
226 /// combine it with a steady rate difference and any quadratic change.
227 /// This method lets you do that directly from a [`Dt`] without having to
228 /// call the more verbose [`Drift::new`].
229 ///
230 /// The other two arguments describe how the difference between the two
231 /// clocks will evolve:
232 /// - `rate` — the constant fractional speed difference (how much faster or
233 /// slower one clock runs compared with the other).
234 /// - `accel` — how quickly that speed difference itself is changing (for
235 /// example because the spacecraft is moving through a varying gravitational
236 /// field).
237 ///
238 /// See [`Drift`] and [`Drift::from_offset_and_rate`] for more background on
239 /// why these three numbers are used to model real clocks.
240 #[inline]
241 pub const fn to_drift_as_constant(self, rate: Dt, accel: Dt) -> Drift {
242 Drift::new(self, rate, accel)
243 }
244
245 /// Builds a clock-drift model in which this [`Dt`] supplies the constant
246 /// fractional rate difference between the observer’s proper time and the
247 /// chosen coordinate time.
248 ///
249 /// If you have already calculated (or measured) a steady rate offset as a
250 /// [`Dt`], you can use this method to attach an initial time offset and a
251 /// quadratic term and obtain a complete [`Drift`] polynomial.
252 ///
253 /// Physically, the rate term captures the fact that two clocks that are
254 /// moving at different velocities or sitting at different gravitational
255 /// potentials will accumulate a steadily growing time difference. The
256 /// other two parameters let you also describe any starting bias and any
257 /// change in that rate over time.
258 ///
259 /// See the documentation on [`Drift`] for the meaning of the three
260 /// coefficients in a relativistic timing context.
261 #[inline]
262 pub const fn to_drift_as_rate(self, constant: Dt, accel: Dt) -> Drift {
263 Drift::new(constant, self, accel)
264 }
265
266 /// Builds a clock-drift model in which this [`Dt`] supplies the quadratic
267 /// term that describes how the rate difference itself is changing.
268 ///
269 /// Some situations (a spacecraft on a highly elliptical orbit, a clock
270 /// whose frequency is aging, or a trajectory that takes it through regions
271 /// of changing gravitational potential) cause the *rate* at which two
272 /// clocks diverge to change over time. If you have computed that changing
273 /// rate as a [`Dt`], this method lets you combine it with an initial offset
274 /// and a base rate to form a full [`Drift`].
275 ///
276 /// The other two arguments are:
277 /// - `constant` — any fixed time bias present at the start.
278 /// - `rate` — the base fractional rate difference that will itself be
279 /// modified by the quadratic term supplied by `self`.
280 ///
281 /// See [`Drift`] for more explanation of why a quadratic model is used for
282 /// relativistic clock predictions.
283 #[inline]
284 pub const fn to_drift_as_accel(self, constant: Dt, rate: Dt) -> Drift {
285 Drift::new(constant, rate, self)
286 }
287
288 /// Advances this `Dt` by the given elapsed duration while applying the relativistic proper-time correction
289 /// derived from the supplied `Spacetime` model.
290 ///
291 /// - This method is intended for simulation of remote clocks (e.g., Earth time as observed from a spacecraft).
292 /// - For a local hardware proper-time clock, use the plain `add` methods instead.
293 #[inline]
294 pub const fn adjusted_advance(&mut self, elapsed: &Dt, spacetime: &Spacetime) {
295 let dtau = elapsed.add(Drift::from_spacetime(spacetime).time_diff_after(elapsed));
296 *self = self.add(dtau);
297 }
298
299 /// Advances this `Dt` by the given elapsed duration while applying the relativistic proper-time correction
300 /// from a pre-computed `Drift` value.
301 ///
302 /// - This is an optimized variant of [`Dt::adjusted_advance`](../struct.Dt.html#method.adjusted_advance)
303 /// for callers that already hold a [`Drift`] instance.
304 /// - This method is intended for simulation of remote clocks (e.g., Earth time as observed from a spacecraft).
305 /// - For a local hardware proper-time clock, use the plain `add` methods instead.
306 #[inline]
307 pub const fn adjusted_advance_using_drift(&mut self, elapsed: &Dt, drift: &Drift) {
308 let dtau = elapsed.add(drift.time_diff_after(elapsed));
309 *self = self.add(dtau);
310 }
311
312 /// Converts this instant to any other [`Scale`] while applying an exact quadratic relativistic
313 /// or clock-drift correction defined by a [`Drift`] model relative to a reference instant.
314 pub const fn convert_using_drift(self, reference: Dt, drift: &Drift) -> Dt {
315 let span = self.to_diff_raw(reference);
316 let correction = drift.time_diff_after(&span);
317 self.add(correction)
318 }
319
320 /// Performs the inverse conversion of
321 /// [`Dt::convert_using_drift`](../struct.Dt.html#method.convert_using_drift),
322 /// recovering the original proper
323 /// time on the source clock scale.
324 ///
325 /// A fixed-point iteration (at most 16 steps) is used to solve the implicit equation. For the common
326 /// case of a pure constant offset the function returns immediately without iteration.
327 pub const fn convert_back_using_drift(self, reference: Dt, drift: &Drift) -> Dt {
328 if drift.rate.is_zero() && drift.accel.is_zero() {
329 return self.sub(drift.constant);
330 }
331 let mut guess = self;
332 let mut i = 0u32;
333 while i < 16 {
334 let span = guess.to_diff_raw(reference);
335 let correction = drift.time_diff_after(&span);
336 guess = self.sub(correction);
337 i += 1;
338 }
339 guess
340 }
341}
342
343/// Fixed-point product `(a * b) / ATTOS_PER_SEC`, saturating on true result overflow.
344///
345/// Drift coefficients and spans are both attosecond-scaled, so applying rate or
346/// accel needs `(a·b)/10¹⁸`. The raw product `a·b` can exceed `i128` even when
347/// that scaled result still fits; this helper avoids wrapping or early clamp.
348///
349/// 1. Uses `checked_mul` when the intermediate product fits (common path).
350/// 2. Otherwise splits `a = a_hi·D + a_lo` so
351/// `(a·b)/D = a_hi·b + (a_lo·b)/D`, with a second split on `b` if needed.
352/// 3. Combines parts with saturating arithmetic so extreme inputs clamp like
353/// the rest of [`Dt`] rather than wrapping.
354const fn saturating_mul_div_attos_per_sec(a: i128, b: i128) -> i128 {
355 if a == 0 || b == 0 {
356 return 0;
357 }
358
359 if let Some(product) = a.checked_mul(b) {
360 return product / ATTOS_PER_SEC_I128;
361 }
362
363 // a = a_hi * D + a_lo (Rust truncating division; identity holds for negatives)
364 let a_hi = a / ATTOS_PER_SEC_I128;
365 let a_lo = a % ATTOS_PER_SEC_I128;
366 // (a_hi * D + a_lo) * b / D = a_hi * b + (a_lo * b) / D
367 let hi = a_hi.saturating_mul(b);
368
369 let lo = match a_lo.checked_mul(b) {
370 Some(product) => product / ATTOS_PER_SEC_I128,
371 None => {
372 // |a_lo| < D; split b the same way:
373 // a_lo * b / D = a_lo * b_hi + (a_lo * b_lo) / D
374 // |a_lo * b_lo| < D² = 10³⁶ < i128::MAX, so the cross term is exact.
375 let b_hi = b / ATTOS_PER_SEC_I128;
376 let b_lo = b % ATTOS_PER_SEC_I128;
377 let cross = (a_lo * b_lo) / ATTOS_PER_SEC_I128;
378 a_lo.saturating_mul(b_hi).saturating_add(cross)
379 }
380 };
381
382 hi.saturating_add(lo)
383}
384
385#[cfg(feature = "wire")]
386impl Drift {
387 /// Wire format version for this type’s outer envelope.
388 ///
389 /// Independent of nested
390 /// [`Dt::WIRE_VERSION`](../struct.Dt.html#associatedconstant.WIRE_VERSION).
391 pub const WIRE_VERSION: u8 = 1;
392
393 /// Size of the canonical wire representation in bytes.
394 ///
395 /// One version byte plus three
396 /// [`Dt::WIRE_SIZE`](../struct.Dt.html#associatedconstant.WIRE_SIZE)
397 /// records (`constant`, `rate`, `accel`).
398 pub const WIRE_SIZE: usize = 1 + 3 * Dt::WIRE_SIZE;
399
400 /// Serializes this polynomial into a fixed buffer.
401 ///
402 /// ## Wire format
403 ///
404 /// - Byte `0`: [`WIRE_VERSION`](Self::WIRE_VERSION)
405 /// - Next [`Dt::WIRE_SIZE`](../struct.Dt.html#associatedconstant.WIRE_SIZE)
406 /// bytes: `constant`
407 /// - Next [`Dt::WIRE_SIZE`](../struct.Dt.html#associatedconstant.WIRE_SIZE)
408 /// bytes: `rate`
409 /// - Next [`Dt::WIRE_SIZE`](../struct.Dt.html#associatedconstant.WIRE_SIZE)
410 /// bytes: `accel`
411 pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
412 let mut buf = [0u8; Self::WIRE_SIZE];
413 buf[0] = Self::WIRE_VERSION;
414
415 let n = Dt::WIRE_SIZE;
416 let c = self.constant.to_wire_bytes();
417 let r = self.rate.to_wire_bytes();
418 let a = self.accel.to_wire_bytes();
419
420 buf[1..1 + n].copy_from_slice(&c);
421 buf[1 + n..1 + 2 * n].copy_from_slice(&r);
422 buf[1 + 2 * n..1 + 3 * n].copy_from_slice(&a);
423 buf
424 }
425
426 /// Deserializes from exactly [`WIRE_SIZE`](Self::WIRE_SIZE) bytes.
427 ///
428 /// ## Errors
429 ///
430 /// Returns `None` only when:
431 /// - `bytes` is not exactly [`WIRE_SIZE`](Self::WIRE_SIZE) long,
432 /// - the version byte is not [`WIRE_VERSION`](Self::WIRE_VERSION), or
433 /// - any nested
434 /// [`Dt::from_wire_bytes`](../struct.Dt.html#method.from_wire_bytes)
435 /// fails (unknown
436 /// [`Dt::WIRE_VERSION`](../struct.Dt.html#associatedconstant.WIRE_VERSION)).
437 ///
438 /// Nested scale/target bytes never fail decode (see
439 /// [`Dt::from_wire_bytes`](../struct.Dt.html#method.from_wire_bytes)).
440 ///
441 /// ## Security
442 ///
443 /// Composes the safety guarantees of
444 /// [`Dt::from_wire_bytes`](../struct.Dt.html#method.from_wire_bytes).
445 /// Safe for untrusted input.
446 pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
447 if bytes.len() != Self::WIRE_SIZE {
448 return None;
449 }
450
451 if bytes[0] != Self::WIRE_VERSION {
452 return None;
453 }
454
455 let n = Dt::WIRE_SIZE;
456 let constant = Dt::from_wire_bytes(&bytes[1..1 + n])?;
457 let rate = Dt::from_wire_bytes(&bytes[1 + n..1 + 2 * n])?;
458 let accel = Dt::from_wire_bytes(&bytes[1 + 2 * n..1 + 3 * n])?;
459
460 Some(Self::new(constant, rate, accel))
461 }
462}