Skip to main content

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::{
11    ATTOS_PER_SEC_I128, C_SQUARED, Dt, PLANCK_LENGTH_4, Position, Real, Scale, Velocity, sqrt,
12};
13
14/// The three local spacetime quantities that fully determine how fast an observer’s
15/// proper time advances relative to coordinate time.
16///
17/// This structure holds the gravitational lapse factor, the observer’s local velocity,
18/// and the curvature information needed for the library’s unified proper-time model.
19/// It is the low-level input that `Drift` uses internally.
20#[derive(Copy, Clone, Debug, PartialEq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
23pub struct Spacetime {
24    /// Gravitational lapse (redshift) factor α.  
25    /// This is the factor by which clocks run slower in a gravitational potential.
26    pub alpha: Real,
27
28    /// Local three-velocity β = v/c measured in the coordinate rest frame.
29    pub beta: Real,
30
31    /// Kretschmann scalar (a scalar measure of spacetime curvature).  
32    /// In the weak-field regime — where |Φ|/c² ≪ 1 and the gravitational field varies
33    /// over macroscopic distances — this value is effectively zero and can safely be
34    /// left at its default. It only becomes numerically relevant in strong-field
35    /// environments such as:
36    ///
37    /// - the surface or immediate vicinity of neutron stars (where |Φ|/c² ≈ 0.15–0.25);
38    /// - regions near a black-hole event horizon (e.g. the photon rings imaged by the
39    ///   Event Horizon Telescope around M87* or Sgr A*);
40    /// - the final inspiral and merger phases of binary neutron-star or black-hole
41    ///   systems (as observed by LIGO/Virgo in events such as GW170817 or GW150914).
42    ///
43    /// In these regimes a realistic non-zero value (estimated from the local potential
44    /// and a characteristic length scale) activates the library’s intrinsic Planck-scale
45    /// saturation term.
46    pub kretschmann: Real,
47}
48
49impl Spacetime {
50    #[inline]
51    pub const fn new(alpha: Real, beta: Real, kretschmann: Real) -> Spacetime {
52        Self {
53            alpha,
54            beta,
55            kretschmann,
56        }
57    }
58
59    /// Returns the instantaneous proper-time rate `dτ/dt` from this snapshot.
60    ///
61    /// Convenience method that internally uses the same unified calculation as
62    /// `Drift::proper_time_rate`.
63    #[inline]
64    pub const fn proper_time_rate(&self) -> Real {
65        Drift::from_spacetime(self).proper_time_rate()
66    }
67
68    /// Convenience for direct gravimeter / sensor paths.
69    #[inline]
70    pub const fn from_gravitic_and_velocity(
71        alpha: Real,
72        velocity: Velocity,
73        kretschmann: Real,
74    ) -> Spacetime {
75        Self::new(alpha, velocity.beta(), kretschmann)
76    }
77
78    /// Converts the Newtonian gravitational potential Φ/c² (where Φ < 0 for bound orbits)
79    /// into the relativistic lapse factor α = √(1 + 2Φ/c²).
80    ///
81    /// This function implements the standard weak-field approximation used in general
82    /// relativity. It is valid when the dimensionless gravitational potential satisfies
83    /// |Φ|/c² ≪ 1. In this regime spacetime is nearly flat, gravitational time dilation
84    /// is a small perturbation, and higher-order curvature effects can safely be neglected.
85    /// The resulting α gives the factor by which clocks tick more slowly in a gravitational
86    /// well relative to a distant reference clock.
87    ///
88    /// This approximation is excellent for solar-system navigation, GNSS satellites,
89    /// most spacecraft operations, and any environment where |Φ|/c² remains much smaller
90    /// than ~0.01. It is exported from `deep_time::alpha_from_weak_field_potential`
91    /// and is the recommended way to obtain the lapse factor when you have the local
92    /// Newtonian potential.
93    ///
94    /// The weak-field regime breaks down in strong-gravity environments where
95    /// |Φ|/c² approaches or exceeds ~0.1. Such conditions occur near:
96    ///
97    /// - the surface or immediate vicinity of neutron stars (where |Φ|/c² ≈ 0.15–0.25);
98    /// - regions near a black-hole event horizon (e.g. the photon rings imaged by the
99    ///   Event Horizon Telescope around M87* or Sgr A*);
100    /// - the final inspiral and merger phases of binary neutron-star or black-hole
101    ///   systems (as observed by LIGO/Virgo in events such as GW170817 or GW150914).
102    ///
103    /// In those extreme regimes this function alone is no longer sufficient; a full
104    /// strong-field treatment (including curvature information passed to `Spacetime`)
105    /// is required.
106    #[inline]
107    pub const fn alpha_from_weak_field_potential(grav_potential_over_c2: Real) -> Real {
108        // gravitational_potential_over_c2 = Φ/c² < 0 → α < 1 (clocks run slower)
109        sqrt((f!(1.0) + f!(2.0) * grav_potential_over_c2).max(f!(0.0)))
110    }
111
112    /// Kretschmann scalar from total relativity
113    /// Computes the Kretschmann scalar \(\mathcal{K}\) from the total gravitational
114    /// relativity experienced by a local observer at the observer’s spacetime point.
115    ///
116    /// This is the canonical, physics-true convenience function for the master Lagrangian.
117    ///
118    /// Information on the master Lagrangian can be found
119    /// [here](https://github.com/ragardner/deep-time/blob/main/docs/relativity.md).
120    ///
121    /// It uses:
122    /// - `phi` = Φ/c² — the total local gravitational potential (redshift/gravity effect)
123    ///   felt by the observer from all masses.
124    /// - `characteristic_length_scale` — the typical length scale (in meters) over which
125    ///   the gravitational field varies at the observer’s location.
126    ///
127    /// **For existing weak-field users** (Earth orbit, GNSS, solar-system navigation):
128    /// Supply your existing `phi` value and set `characteristic_length_scale = 0.0`.
129    /// The function safely returns 0.0 (the value in double precision).
130    ///
131    /// **For strong-field / future users** (black-hole flybys, neutron stars, direct
132    /// gravimeters, or full metric evaluation):
133    /// Supply the measured or computed \(\phi\) and the real local length scale (or
134    /// the value from your metric). The function returns a physically accurate non-zero
135    /// curvature.
136    pub const fn kretschmann_from_potential_and_scale(
137        grav_potential_over_c2: Real,
138        characteristic_length_scale: Real,
139    ) -> Real {
140        if characteristic_length_scale <= f!(0.0) || grav_potential_over_c2 <= f!(0.0) {
141            return f!(0.0);
142        }
143        // Exact weak-field limit: K ≈ 48 φ² / L⁴
144        let curvature_scale = f!(2.0) * grav_potential_over_c2
145            / (characteristic_length_scale * characteristic_length_scale);
146        f!(12.0) * (curvature_scale * curvature_scale)
147    }
148
149    /// Recommended constructor for most users.
150    ///
151    /// Computes both the gravitational lapse factor `α` and an estimate of the
152    /// Kretschmann scalar from the dimensionless gravitational potential Φ/c²
153    /// and a characteristic length scale.
154    ///
155    /// The lapse factor α is computed using `alpha_from_weak_field_potential`,
156    /// which is the standard weak-field expression α = √(1 + 2Φ/c²). It is valid
157    /// when the dimensionless gravitational potential satisfies |Φ|/c² ≪ 1. In
158    /// this regime spacetime is nearly flat, gravitational time dilation is a
159    /// small perturbation, and higher-order curvature effects can safely be
160    /// neglected. The resulting α gives the factor by which clocks tick more
161    /// slowly in a gravitational well relative to a distant reference clock.
162    ///
163    /// This approximation is excellent for solar-system navigation, GNSS
164    /// satellites, most spacecraft operations, and any environment where
165    /// |Φ|/c² remains much smaller than ~0.01. It is exported from
166    /// `deep_time::alpha_from_weak_field_potential` and is the recommended
167    /// way to obtain the lapse factor when you have the local Newtonian potential.
168    ///
169    /// The weak-field regime breaks down in strong-gravity environments where
170    /// |Φ|/c² approaches or exceeds ~0.1. Such conditions occur near:
171    ///
172    /// - the surface or immediate vicinity of neutron stars (where |Φ|/c² ≈ 0.15–0.25);
173    /// - regions near a black-hole event horizon (e.g. the photon rings imaged by the
174    ///   Event Horizon Telescope around M87* or Sgr A*);
175    /// - the final inspiral and merger phases of binary neutron-star or black-hole
176    ///   systems (as observed by LIGO/Virgo in events such as GW170817 or GW150914).
177    ///
178    /// In those extreme regimes this function alone is no longer sufficient; a full
179    /// strong-field treatment (including curvature information passed to `Spacetime`)
180    /// is required.
181    ///
182    /// For the `characteristic_length_scale` parameter:
183    /// - In weak-field conditions, pass `0.0`. This returns exactly the same clock
184    ///   rate as the classic relativistic formulation and sets the Kretschmann scalar
185    ///   to zero (its default value for all ordinary navigation, GNSS, or solar-system
186    ///   work).
187    /// - In strong-field conditions, supply the typical length scale (in meters) over
188    ///   which the gravitational field varies significantly at the observer’s location.
189    ///   This allows the library to estimate the Kretschmann scalar and activate the
190    ///   intrinsic Planck-scale saturation term when curvature becomes extreme.
191    pub const fn from_potential_velocity_and_scale(
192        grav_potential_over_c2: Real, // Φ/c² (total local potential)
193        velocity: Velocity,
194        characteristic_length_scale: Real,
195    ) -> Spacetime {
196        let alpha: Real = Self::alpha_from_weak_field_potential(grav_potential_over_c2);
197        let kretschmann: Real = Self::kretschmann_from_potential_and_scale(
198            grav_potential_over_c2,
199            characteristic_length_scale,
200        );
201        Self::from_gravitic_and_velocity(alpha, velocity, kretschmann)
202    }
203
204    /// Recovers the Newtonian gravitational potential Φ (m²/s²) from the
205    /// gravitational lapse factor α using the weak-field relation.
206    ///
207    /// \[
208    /// \alpha = \sqrt{1 + \frac{2\Phi}{c^2}} \quad\implies\quad
209    /// \Phi = \frac{c^2}{2}(\alpha^2 - 1)
210    /// \]
211    ///
212    /// This is the inverse of [`Spacetime::alpha_from_weak_field_potential`].
213    #[inline]
214    pub const fn grav_potential_from_alpha(alpha: Real) -> Real {
215        let alpha_sq = alpha * alpha;
216        (alpha_sq - f!(1.0)) / f!(2.0) * C_SQUARED
217    }
218
219    /// Computes the total Newtonian gravitational potential Φ at a given position
220    /// from an arbitrary collection of point-mass bodies (Sun, Earth, Moon,
221    /// planets, asteroids, etc.).
222    ///
223    /// This is the standard method used by real mission planners (Apollo,
224    /// Artemis, Mars orbiters, lunar landers) and in open-source astrodynamics
225    /// libraries (SPICE/NAIF, Orekit, GMAT, poliastro). It evaluates
226    ///
227    /// \[
228    /// \Phi = -\sum_i \frac{GM_i}{r_i}
229    /// \]
230    ///
231    /// ## Examples
232    ///
233    /// Realistic cislunar trajectory
234    ///
235    /// ```rust
236    /// use deep_time::{Position, Spacetime};
237    ///
238    /// let bodies = [
239    ///     (Position::from_au(0.0, 0.0, 0.0), 1.3271244e20),     // Sun
240    ///     (Position::from_au(1.0, 0.0, 0.0), 3.9860044e14),     // Earth
241    ///     (Position::from_au(1.00257, 0.0, 0.0), 4.9048695e12), // Moon
242    /// ];
243    ///
244    /// let position = Position::from_au(1.001, 0.001, 0.0); // e.g. spacecraft, asteroid, etc.
245    ///
246    /// let phi = Spacetime::grav_potential_from_point_masses(
247    ///     position,
248    ///     bodies.iter().copied(),
249    /// );
250    /// ```
251    pub fn grav_potential_from_point_masses<I>(position: Position, bodies: I) -> Real
252    where
253        I: IntoIterator<Item = (Position, Real)>, // (body_position, GM in m³/s²)
254    {
255        let mut phi = 0.0;
256        for (body_pos, gm) in bodies {
257            let r = position.distance_to(body_pos);
258            if r > 0.0 {
259                phi -= gm / r;
260            }
261        }
262        phi
263    }
264}
265
266/// Quadratic polynomial that describes the accumulated difference between an
267/// observer’s proper time (the time measured by a real clock moving through
268/// spacetime) and a chosen coordinate time such as TT, TAI, or any other
269/// `Scale`.
270///
271/// The polynomial follows the classic form  
272/// Δt = constant + rate·Δt + accel·(Δt)²  
273/// where the three coefficients capture any fixed offset, constant drift, and
274/// quadratic acceleration of the clock. This structure is used throughout
275/// spacecraft navigation, GNSS systems, and relativistic timing pipelines to
276/// steer clocks, predict time offsets, and maintain synchronization over long
277/// durations.
278///
279/// All three coefficients are stored using [`Dt`].
280#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
281#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
282#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
283pub struct Drift {
284    /// Constant term a₀ expressed in seconds.  
285    /// This represents any fixed time offset between the observer’s proper time
286    /// and the chosen coordinate time.
287    pub constant: Dt,
288
289    /// Linear drift rate a₁ expressed in seconds per second.  
290    /// This term captures a steady fractional rate difference (for example, a
291    /// clock that runs consistently fast or slow).
292    pub rate: Dt,
293
294    /// Quadratic acceleration term a₂ expressed in seconds per second squared.  
295    /// This term accounts for any changing drift rate, such as the gradual
296    /// acceleration caused by relativistic effects or hardware aging.
297    pub accel: Dt,
298}
299
300impl Drift {
301    /// Creates a new `Drift` polynomial from its three coefficients.
302    #[inline]
303    pub const fn new(constant: Dt, rate: Dt, accel: Dt) -> Drift {
304        Self {
305            constant,
306            rate,
307            accel,
308        }
309    }
310
311    /// The zero polynomial representing no correction at all.
312    ///
313    /// Use this when the observer’s clock is already perfectly synchronized with
314    /// the chosen coordinate time.
315    pub const ZERO: Self = Self::new(Dt::ZERO, Dt::ZERO, Dt::ZERO);
316
317    /// Creates a [`Drift`] consisting of a pure constant offset.
318    ///
319    /// This is the most common constructor when only a fixed time bias is known
320    /// (for example, after a one-time clock synchronization or leap-second
321    /// adjustment).
322    #[inline]
323    pub const fn from_constant(c: Dt) -> Drift {
324        Self::new(c, Dt::ZERO, Dt::ZERO)
325    }
326
327    /// Creates a [`Drift`] consisting of a constant offset together with a
328    /// constant linear drift rate.  
329    ///
330    /// This form is very common for GNSS receivers and spacecraft clock steering,
331    /// where a steady fractional frequency offset must be corrected in addition
332    /// to any fixed bias.
333    #[inline]
334    pub const fn from_offset_and_rate(offset: Dt, rate: Dt) -> Drift {
335        Self::new(offset, rate, Dt::ZERO)
336    }
337
338    /// Returns the instantaneous proper-time rate `dτ/dt` (dimensionless).
339    ///
340    /// This value tells you how fast a real physical clock (such as a spacecraft
341    /// onboard clock) is advancing compared to coordinate time. A value of
342    /// `1.0` means the clock runs at the normal rate. Values slightly below `1.0`
343    /// are typical when the clock is moving or sitting in a gravitational well.
344    ///
345    /// The rate includes special-relativistic velocity effects, gravitational
346    /// time dilation, and the library’s built-in Planck-scale saturation term.
347    #[inline]
348    pub const fn proper_time_rate(&self) -> Real {
349        f!(1.0) + self.rate.to_sec_f()
350    }
351
352    /// Evaluates the polynomial at the given elapsed coordinate time span.  
353    ///
354    /// Returns the accumulated time difference (in seconds) between proper
355    /// time and coordinate time after the interval span has passed.
356    pub const fn time_diff_after(&self, span: &Dt) -> Dt {
357        let dt_attos = span.to_attos();
358        let mut total_attos = self.constant.to_attos();
359
360        if !self.rate.is_zero() || !self.accel.is_zero() {
361            // Linear term: rate * dt
362            let rate_attos: i128 = self.rate.to_attos();
363            let rate_term = rate_attos.wrapping_mul(dt_attos) / ATTOS_PER_SEC_I128;
364            total_attos = total_attos.wrapping_add(rate_term);
365
366            // Quadratic term: accel * dt²
367            let accel_attos: i128 = self.accel.to_attos();
368            let accel_dt = accel_attos.wrapping_mul(dt_attos) / ATTOS_PER_SEC_I128;
369            let accel_term = accel_dt.wrapping_mul(dt_attos) / ATTOS_PER_SEC_I128;
370            total_attos = total_attos.saturating_add(accel_term);
371        }
372
373        Dt::span(total_attos)
374    }
375
376    /// Evaluates the deterministic relativistic/polynomial correction **and**
377    /// adds a user-supplied stochastic offset (in seconds).
378    ///
379    /// This is the single production method for realistic stochastic clock
380    /// modeling. In real mission pipelines the deterministic part (this
381    /// polynomial) is kept perfectly clean; stochastic noise (white phase noise,
382    /// random-walk frequency noise, Monte-Carlo realizations, Kalman process
383    /// noise, measured clock residuals, etc.) is added at evaluation time.
384    ///
385    /// Pass `0.0` (or simply call the original `time_diff_after`) when you
386    /// want purely deterministic behavior.
387    #[inline]
388    pub fn time_diff_after_with_noise(&self, span: &Dt, stochastic_offset_sec: Real) -> Dt {
389        self.time_diff_after(span)
390            .add(Dt::from_sec_f(stochastic_offset_sec, Scale::TAI))
391    }
392
393    /// Creates a `Drift` directly from an observer’s velocity and total
394    /// local gravitational potential using the library’s unified master-Lagrangian
395    /// proper-time rate.  
396    ///
397    /// It automatically computes the relativistic clock rate that includes both
398    /// special-relativistic velocity effects and gravitational time dilation,
399    /// then returns a [`Drift`] that can be evaluated at any future time.
400    ///
401    /// The `characteristic_length_scale` parameter controls whether the
402    /// weak-field or strong-field formulation is used:
403    ///
404    /// - In the weak-field regime (where |Φ|/c² ≪ 1), simply pass
405    ///   `characteristic_length_scale = 0.0`. This returns the same
406    ///   relativistic clock rate used by JPL, ESA, GNSS systems, and all modern
407    ///   solar-system navigation pipelines.
408    /// - In strong-field conditions, supply a non-zero length scale (in meters)
409    ///   over which the gravitational potential changes at the observer’s
410    ///   location. This activates the library’s intrinsic Planck-scale saturation
411    ///   term when spacetime curvature becomes extreme.
412    pub const fn from_velocity_potential_and_scale(
413        velocity_m_s: Real,
414        grav_potential_m2_s2: Real,
415        characteristic_length_scale: Real,
416    ) -> Drift {
417        let phi = grav_potential_m2_s2 / C_SQUARED;
418        let velocity = Velocity::from_speed(velocity_m_s);
419        let spacetime = Spacetime::from_potential_velocity_and_scale(
420            phi,
421            velocity,
422            characteristic_length_scale,
423        );
424        Self::from_spacetime(&spacetime)
425    }
426
427    /// Canonical low-level constructor that implements the library's general
428    /// relativity formula.
429    ///
430    /// This function is the single source of truth for the proper-time rate
431    /// calculation used throughout the library. Most users will never call it
432    /// directly; the high-level constructors `from_velocity_potential_and_scale`
433    /// and `from_spacetime` are the intended entry points.
434    ///
435    /// The internal expression is  
436    /// K_eff = [δ(1 + x) + x(1−δ)²] / (1 + x)  
437    /// where δ = α²(1−β²) and x = ℓ_Pl⁴ 𝒦.
438    ///
439    /// The returned rate offset is then applied as a linear term in the `Drift`
440    /// polynomial.
441    pub const fn from_unified_proper_time_rate(u: Real, kretschmann: Real) -> Drift {
442        let delta = u.max(f!(0.0));
443        let x = PLANCK_LENGTH_4 * kretschmann.max(f!(0.0));
444
445        let one_minus_delta = f!(1.0) - delta;
446        let num = delta * (f!(1.0) + x) + x * (one_minus_delta * one_minus_delta);
447        let k_eff = num / (f!(1.0) + x);
448
449        let rate_factor = sqrt(k_eff).max(f!(0.0));
450        let rate_offset = rate_factor - f!(1.0);
451
452        Self::from_offset_and_rate(Dt::ZERO, Dt::from_sec_f(rate_offset, Scale::TAI))
453    }
454
455    /// Creates a `Drift` from a fully resolved `Spacetime` snapshot.  
456    ///
457    /// This is the canonical high-level entry point when you already hold a
458    /// `Spacetime` object containing the gravitational lapse factor α, the
459    /// local velocity β, and the Kretschmann scalar. It internally computes the
460    /// unified proper-time rate and packages the result as a `Drift`
461    /// polynomial ready for evaluation at any future time.
462    #[inline]
463    pub const fn from_spacetime(spacetime: &Spacetime) -> Drift {
464        let u = spacetime.alpha * spacetime.alpha * (f!(1.0) - spacetime.beta * spacetime.beta);
465        Self::from_unified_proper_time_rate(u, spacetime.kretschmann)
466    }
467}
468
469impl Dt {
470    /// Builds a clock-drift model in which this [`Dt`] is treated as the
471    /// initial fixed time difference between the observer’s proper time and
472    /// the chosen coordinate time.
473    ///
474    /// In practice you often compute or measure a one-time offset (for example
475    /// after a clock synchronization or a leap-second jump) and then want to
476    /// combine it with a steady rate difference and any quadratic change.
477    /// This method lets you do that directly from a [`Dt`] without having to
478    /// call the more verbose [`Drift::new`].
479    ///
480    /// The other two arguments describe how the difference between the two
481    /// clocks will evolve:
482    /// - `rate` — the constant fractional speed difference (how much faster or
483    ///   slower one clock runs compared with the other).
484    /// - `accel` — how quickly that speed difference itself is changing (for
485    ///   example because the spacecraft is moving through a varying gravitational
486    ///   field).
487    ///
488    /// See [`Drift`] and [`Drift::from_offset_and_rate`] for more background on
489    /// why these three numbers are used to model real clocks.
490    #[inline]
491    pub const fn to_drift_as_constant(self, rate: Dt, accel: Dt) -> Drift {
492        Drift::new(self, rate, accel)
493    }
494
495    /// Builds a clock-drift model in which this [`Dt`] supplies the constant
496    /// fractional rate difference between the observer’s proper time and the
497    /// chosen coordinate time.
498    ///
499    /// If you have already calculated (or measured) a steady rate offset as a
500    /// [`Dt`], you can use this method to attach an initial time offset and a
501    /// quadratic term and obtain a complete [`Drift`] polynomial.
502    ///
503    /// Physically, the rate term captures the fact that two clocks that are
504    /// moving at different velocities or sitting at different gravitational
505    /// potentials will accumulate a steadily growing time difference. The
506    /// other two parameters let you also describe any starting bias and any
507    /// change in that rate over time.
508    ///
509    /// See the documentation on [`Drift`] for the meaning of the three
510    /// coefficients in a relativistic timing context.
511    #[inline]
512    pub const fn to_drift_as_rate(self, constant: Dt, accel: Dt) -> Drift {
513        Drift::new(constant, self, accel)
514    }
515
516    /// Builds a clock-drift model in which this [`Dt`] supplies the quadratic
517    /// term that describes how the rate difference itself is changing.
518    ///
519    /// Some situations (a spacecraft on a highly elliptical orbit, a clock
520    /// whose frequency is aging, or a trajectory that takes it through regions
521    /// of changing gravitational potential) cause the *rate* at which two
522    /// clocks diverge to change over time. If you have computed that changing
523    /// rate as a [`Dt`], this method lets you combine it with an initial offset
524    /// and a base rate to form a full [`Drift`].
525    ///
526    /// The other two arguments are:
527    /// - `constant` — any fixed time bias present at the start.
528    /// - `rate` — the base fractional rate difference that will itself be
529    ///   modified by the quadratic term supplied by `self`.
530    ///
531    /// See [`Drift`] for more explanation of why a quadratic model is used for
532    /// relativistic clock predictions.
533    #[inline]
534    pub const fn to_drift_as_accel(self, constant: Dt, rate: Dt) -> Drift {
535        Drift::new(constant, rate, self)
536    }
537}