Skip to main content

deep_time/physics/observer/
light_time.rs

1//! Light-time implementations for [`Observer`].
2
3use crate::{C, C_SQUARED, Drift, Dt, Position, Real, Scale, TWO_GM_SUN_OVER_C3, Velocity, log};
4
5use super::Observer;
6
7impl Dt {
8    /// Shapiro gravitational time scale for the Sun (`2 G M_☉ / c³`).
9    ///
10    /// Recommended value for the Sun when building the `bodies` slice passed to
11    /// [`Observer::shapiro_delay`](../../struct.Observer.html#method.shapiro_delay),
12    /// [`Observer::one_way_relativistic_delay`](../../struct.Observer.html#method.one_way_relativistic_delay),
13    /// and related methods.
14    pub const SHAPIRO_SOLAR: Self = Self::from_sec_f(TWO_GM_SUN_OVER_C3, Scale::TAI, Scale::TAI);
15
16    /// Creates the Shapiro delay scale for an arbitrary central body
17    /// from its standard gravitational parameter `GM` (μ) in m³ s⁻².
18    ///
19    /// This produces the coefficient used in the Shapiro gravitational time delay
20    /// formula. It is the recommended way to create a custom Shapiro scale for
21    /// planets, stars, or other massive bodies.
22    ///
23    /// The returned value is intended to be used for the `bodies` parameter
24    /// when calling
25    /// [`Observer::shapiro_delay`](../../struct.Observer.html#method.shapiro_delay) or
26    /// [`Observer::one_way_relativistic_delay`](../../struct.Observer.html#method.one_way_relativistic_delay).
27    #[inline]
28    pub const fn shapiro_from_grav_param(gm: Real) -> Dt {
29        let sec = 2.0 * gm / (C * C_SQUARED);
30        Self::from_sec_f(sec, Scale::TAI, Scale::TAI)
31    }
32
33    /// Creates an [`Observer`] using this time value along with the
34    /// provided position, velocity, and gravitational information.
35    ///
36    /// An [`Observer`] represents a complete snapshot of an observer
37    /// (spacecraft, ground station, planet, person, etc.) at a
38    /// specific moment.
39    ///
40    /// It bundles together the time, position, velocity, and local
41    /// gravitational environment so that relativistic calculations
42    /// (light time, clock rates, Shapiro delay, etc.) can be performed.
43    ///
44    /// This method is a convenience constructor. It is useful when you
45    /// already have a [`Dt`] (a time value) and want to build an
46    /// [`Observer`] directly from it.
47    ///
48    /// ## Parameters
49    ///
50    /// - `position`: The observer’s position in meters (typically expressed
51    ///   in a barycentric or heliocentric frame).
52    /// - `velocity`: The observer’s velocity in meters per second.
53    /// - `grav_potential_m2_s2`: The total Newtonian gravitational potential
54    ///   (Φ) at the observer’s location, in m²/s². This is usually negative
55    ///   for bound orbits and is the sum of contributions from the Sun and
56    ///   planets.
57    /// - `characteristic_length_scale`: A length scale (in meters) over which
58    ///   gravity varies significantly at this location. Use `0.0` for normal
59    ///   solar-system and weak-field cases. Only provide a non-zero value when
60    ///   working in strong gravitational fields.
61    ///
62    /// ## Examples
63    ///
64    /// ```
65    /// use deep_time::{Dt, Position, Spacetime, Velocity, from_sec_f};
66    ///
67    /// let bodies = [
68    ///     (Position::from_au(0.0, 0.0, 0.0), 1.3271244e20),     // Sun
69    ///     (Position::from_au(1.0, 0.0, 0.0), 3.9860044e14),     // Earth
70    ///     (Position::from_au(1.00257, 0.0, 0.0), 4.9048695e12), // Moon
71    /// ];
72    ///
73    /// let position = Position::from_au(1.001, 0.001, 0.0); // e.g. spacecraft, asteroid, etc.
74    ///
75    /// let grav_potential = Spacetime::grav_potential_from_point_masses(
76    ///     &position,
77    ///     bodies.iter().cloned(),
78    /// );
79    ///
80    /// let t = from_sec_f!(1234.5);
81    ///
82    /// let state = t.to_observer(
83    ///     Position::ZERO,
84    ///     Velocity::ZERO,
85    ///     grav_potential,
86    ///     0.0, // normal solar-system use
87    /// );
88    /// ```
89    #[inline]
90    pub const fn to_observer(
91        self,
92        position: Position,
93        velocity: Velocity,
94        grav_potential_m2_s2: Real,
95        characteristic_length_scale: Real,
96    ) -> Observer {
97        Observer {
98            time: self,
99            position,
100            velocity,
101            grav_potential_m2_s2,
102            characteristic_length_scale,
103        }
104    }
105}
106
107impl Observer {
108    /// Computes the combined one-way relativistic correction for a signal
109    /// traveling from this observer (the transmitter) to a receiver.
110    ///
111    /// This value is the **total extra time** you should add to the Newtonian
112    /// geometric light travel time (`distance / speed of light`). It includes
113    /// **two** separate relativistic effects:
114    ///
115    /// 1. The gravitational propagation delay (Shapiro delay) caused by the
116    ///    Sun and other bodies slowing the signal.
117    /// 2. The differential clock-rate correction caused by the transmitter
118    ///    and receiver having slightly different proper-time rates (due to
119    ///    their velocities and gravitational potentials).
120    ///
121    /// In other words, this method gives you **propagation delay + clock-rate
122    /// correction** in one convenient call.
123    ///
124    /// **Important:** This is a convenience method. It is provided so you can
125    /// get the full one-way relativistic correction quickly. If you need
126    /// strict separation of the two effects (for example, to apply them at
127    /// different stages of your calculation), call
128    /// [`Observer::shapiro_delay`](#method.shapiro_delay) and
129    /// [`Observer::compute_differential_clock_correction`](#method.compute_differential_clock_correction)
130    /// individually and add the results yourself.
131    ///
132    /// ## When to use this method
133    ///
134    /// Use this when you need the complete relativistic correction for
135    /// one-way light time in a single step — for example when:
136    /// - Computing high-precision one-way range or Doppler observables
137    /// - Building simplified navigation or orbit determination models
138    /// - You want the total effect without manually combining the pieces
139    ///
140    /// ## The `bodies` parameter – which masses to include
141    ///
142    /// Pass a slice of `(shapiro_coefficient, body_position)` pairs:
143    ///
144    /// - `shapiro_coefficient`: How strong the delay from this body should be.
145    ///   It equals `2GM / c³`. Use
146    ///   [`Dt::SHAPIRO_SOLAR`](../../struct.Dt.html#associatedconstant.SHAPIRO_SOLAR)
147    ///   for the Sun, or
148    ///   [`Dt::shapiro_from_grav_param`](../../struct.Dt.html#method.shapiro_from_grav_param)
149    ///   for any other body.
150    /// - `body_position`: Where the center of that body is located at the
151    ///   relevant time.
152    ///
153    /// **Important: All positions must be measured the same way**
154    ///
155    /// The transmitter position (`self.position`), the receiver position
156    /// (`rx.position`), and every `body_position` you provide must all be
157    /// measured from the **same point in space**, and they must all use
158    /// the **same directions** for their X, Y, and Z axes.
159    ///
160    /// For example, if your transmitter position is measured from the center
161    /// of the solar system, then the receiver and body positions must also
162    /// be measured from the center of the solar system using the same
163    /// pointing directions for the coordinate axes.
164    ///
165    /// In most solar-system work, people use positions from JPL ephemerides
166    /// (which are measured from the center of the solar system).
167    ///
168    /// Pass an empty slice (`&[]`) to turn off the Shapiro (gravitational)
169    /// part of the correction.
170    ///
171    /// ## Parameters
172    ///
173    /// * `rx` — Receiver state at the approximate time the signal arrives.
174    /// * `bodies` — List of bodies that should contribute to the gravitational
175    ///   propagation delay.
176    ///
177    /// ## Returns
178    ///
179    /// The total one-way relativistic correction (Shapiro propagation delay
180    /// plus differential clock-rate correction), expressed as a `Dt` in the
181    /// same time scale as `self.time`.
182    ///
183    /// This value should normally be **added** to the Newtonian geometric
184    /// light time.
185    pub const fn one_way_relativistic_delay(&self, rx: &Observer, bodies: &[(Dt, Position)]) -> Dt {
186        let prop = self.shapiro_delay(rx, bodies);
187        let drift = self.compute_differential_clock_correction(rx);
188        prop.add(drift)
189    }
190
191    /// Iteratively solves the one-way light-time equation in coordinate time,
192    /// including relativistic propagation corrections, until convergence.
193    ///
194    /// This solver computes the receive epoch `t_rx` such that:
195    ///
196    /// ```text
197    /// t_rx = t_tx + |r_rx(t_rx) − r_tx(t_tx)| / c + Δt_shapiro(t_tx, t_rx)
198    /// ```
199    ///
200    /// It performs fixed-point iteration using the propagation delay returned by
201    /// [`Observer::shapiro_delay`](#method.shapiro_delay).
202    /// Clock-rate and proper-time effects are **not** included in the iteration;
203    /// they should be applied separately when converting between coordinate time
204    /// and proper time or when forming observables.
205    ///
206    /// The solver is suitable for high-precision one-way light-time calculations
207    /// and works with any ephemeris source via the provided closure.
208    ///
209    /// ## Parameters
210    ///
211    /// * `rx_provider` — Closure returning the full [`Observer`] of the
212    ///   receiver at a given coordinate time.
213    /// * `bodies` — Slice of `(shapiro_coefficient, body_position)` pairs
214    ///   controlling the Shapiro contribution. Use `&[(Dt::SHAPIRO_SOLAR, sun_pos)]`
215    ///   for solar-system work; include additional bodies for higher precision.
216    ///   Pass `&[]` to disable Shapiro.
217    /// * `tolerance` — Maximum allowed change in receive time per iteration
218    ///   before declaring convergence.
219    /// * `max_iter` — Maximum number of iterations. Typical values are 12–20
220    ///   for solar-system geometries.
221    ///
222    /// ## Returns
223    ///
224    /// A tuple `(prop_correction, rx_time, final_state)` where:
225    /// - `prop_correction` is the converged Shapiro propagation delay,
226    /// - `rx_time` is the converged receive time (same scale as `self.time`),
227    /// - `final_state` is the receiver state at `rx_time`.
228    pub fn iterative_one_way_light_time_to<F>(
229        &self,
230        rx_provider: &mut F,
231        bodies: &[(Dt, Position)],
232        tolerance: Dt,
233        max_iter: usize,
234    ) -> (Dt, Dt, Observer)
235    where
236        F: FnMut(Dt) -> Observer,
237    {
238        // Initial geometric guess
239        let initial_rx = rx_provider(self.time);
240        let initial_r_sep = self.position.distance_to(&initial_rx.position);
241        let initial_geometric = Dt::from_sec_f(initial_r_sep / C, Scale::TAI, Scale::TAI);
242
243        let mut rx_time = self.time.add(initial_geometric);
244        let mut prop_correction = Dt::ZERO;
245
246        for _ in 0..max_iter {
247            let rx = rx_provider(rx_time);
248
249            prop_correction = self.shapiro_delay(&rx, bodies);
250
251            let r_sep = self.position.distance_to(&rx.position);
252            let geometric = Dt::from_sec_f(r_sep / C, Scale::TAI, Scale::TAI);
253            let full_delay = geometric.add(prop_correction);
254
255            let new_rx_time = self.time.add(full_delay);
256            let change = new_rx_time.to_diff_raw(rx_time);
257
258            rx_time = new_rx_time;
259
260            if change.abs() < tolerance {
261                return (prop_correction, rx_time, rx);
262            }
263        }
264
265        // Fallback after max iterations
266        let final_rx = rx_provider(rx_time);
267        (prop_correction, rx_time, final_rx)
268    }
269
270    /// Computes the total Shapiro (gravitational propagation) delay for a
271    /// complete round-trip (two-way) signal.
272    ///
273    /// This method solves the uplink and downlink legs *separately and
274    /// independently* using the iterative light-time solver. This approach
275    /// is more accurate than older combined round-trip formulas when the
276    /// two ends have significantly different velocities or are in different
277    /// gravitational environments.
278    ///
279    /// The returned value is the **sum of the uplink and downlink Shapiro
280    /// delays only**. It does **not** include clock-rate or proper-time
281    /// corrections.
282    ///
283    /// ## When to use this method
284    ///
285    /// Use this when you need the total gravitational propagation correction
286    /// for two-way (round-trip) measurements, for example:
287    /// - Two-way range or range-rate (Doppler) data
288    /// - Transponded signals from spacecraft
289    /// - Any high-precision two-way light-time calculation
290    ///
291    /// For one-way signals, use
292    /// [`Observer::shapiro_delay`](#method.shapiro_delay) or
293    /// [`Observer::one_way_relativistic_delay`](#method.one_way_relativistic_delay)
294    /// instead.
295    ///
296    /// ## How the calculation works
297    ///
298    /// 1. Solves the uplink leg (from `self` to the remote receiver) using
299    ///    the `rx_provider` closure.
300    /// 2. Obtains the accurate receiver state at the uplink arrival time.
301    /// 3. Solves the downlink leg (from the receiver back to the local
302    ///    transmitter) using the `tx_provider` closure.
303    ///
304    /// ## The `bodies` parameter – which masses to include
305    ///
306    /// Pass a slice of `(shapiro_coefficient, body_position)` pairs (the
307    /// same slice is used for both legs). See
308    /// [`Observer::shapiro_delay`](#method.shapiro_delay)
309    /// for details on how to build this slice.
310    ///
311    /// **Important: All states returned by the providers must be consistent**
312    /// with the same reference frame (same origin and same coordinate axes).
313    ///
314    /// ## Parameters
315    ///
316    /// * `rx_provider` — Closure that returns the full [`Observer`] of
317    ///   the remote receiver (planet, spacecraft, etc.) at any given
318    ///   coordinate time.
319    /// * `tx_provider` — Closure that returns the full [`Observer`] of
320    ///   the local transmitter at any given coordinate time (used only for
321    ///   the downlink leg).
322    /// * `bodies` — Slice of `(shapiro_coefficient, body_position)` pairs
323    ///   describing the gravitating bodies.
324    /// * `tolerance` — Convergence tolerance for each leg’s iterative solver
325    ///   (e.g. `Dt::from_ns_floor(1, 0, Scale::TAI)`).
326    /// * `max_iter` — Maximum number of iterations allowed per leg
327    ///   (typical values are 12–20).
328    ///
329    /// ## Returns
330    ///
331    /// The total round-trip Shapiro propagation delay (uplink + downlink)
332    /// as a `Dt`, in the same time scale as `self.time`.
333    ///
334    /// This value should normally be **added** to the Newtonian geometric
335    /// round-trip light time. Clock-rate corrections must still be applied
336    /// separately (e.g. by squaring the one-way clock-rate ratio).
337    pub fn round_trip_light_time_correction<RxF, TxF>(
338        &self,
339        mut rx_provider: RxF, // remote body (planet, spacecraft, etc.)
340        mut tx_provider: TxF, // local transmitter for the return leg (can move)
341        bodies: &[(Dt, Position)],
342        tolerance: Dt,
343        max_iter: usize,
344    ) -> Dt
345    where
346        RxF: FnMut(Dt) -> Observer,
347        TxF: FnMut(Dt) -> Observer,
348    {
349        // Uplink leg: transmitter → receiver
350        let (uplink_prop, rx_time, _rx_state) =
351            self.iterative_one_way_light_time_to(&mut rx_provider, bodies, tolerance, max_iter);
352
353        // Downlink leg: receiver → transmitter
354        let return_tx = rx_provider(rx_time); // accurate state at uplink arrival
355
356        let (downlink_prop, _return_rx_time, _return_rx_state) = return_tx
357            .iterative_one_way_light_time_to(&mut tx_provider, bodies, tolerance, max_iter);
358
359        uplink_prop.add(downlink_prop)
360    }
361
362    /// Computes the one-way gravitational propagation delay (Shapiro delay)
363    /// caused by massive bodies between this observer (the transmitter) and
364    /// a receiver.
365    ///
366    /// This value is the **extra time** a radio signal takes to travel because
367    /// gravity from the Sun and planets slightly slows it down. You normally
368    /// add this delay to the ordinary geometric light travel time
369    /// (`distance / speed of light`) to get a more accurate total one-way
370    /// signal travel time.
371    ///
372    /// **Important:** This method returns **only** the gravitational
373    /// propagation delay. It does **not** include clock-rate differences
374    /// between the transmitter and receiver caused by velocity or gravity.
375    /// Those effects are available separately through
376    /// [`Observer::compute_differential_clock_correction`](#method.compute_differential_clock_correction),
377    /// [`Observer::proper_time_rate`](#method.proper_time_rate), and
378    /// [`Observer::relativistic_clock_rate_ratio`](#method.relativistic_clock_rate_ratio).
379    ///
380    /// ## When to use this method
381    ///
382    /// Use this when you need the gravitational (Shapiro) contribution to
383    /// one-way light time — for example when building high-precision range,
384    /// Doppler, or orbit determination models.
385    ///
386    /// ## The `bodies` parameter – which masses to include
387    ///
388    /// Pass a slice of `(shapiro_coefficient, body_position)` pairs:
389    ///
390    /// - `shapiro_coefficient`: How strong the delay from this body should be.
391    ///   It equals `2GM / c³`. Use
392    ///   [`Dt::SHAPIRO_SOLAR`](../../struct.Dt.html#associatedconstant.SHAPIRO_SOLAR)
393    ///   for the Sun, or
394    ///   [`Dt::shapiro_from_grav_param`](../../struct.Dt.html#method.shapiro_from_grav_param)
395    ///   for any other body.
396    /// - `body_position`: Where the center of that body is located at the
397    ///   relevant time.
398    ///
399    /// **Important: All positions must be measured the same way**
400    ///
401    /// The transmitter position (`self.position`), the receiver position
402    /// (`rx.position`), and every `body_position` you provide must all be
403    /// measured from the **same point in space**, and they must all use
404    /// the **same directions** for their X, Y, and Z axes.
405    ///
406    /// For example, if the transmitter position is measured from the center
407    /// of the solar system, then the receiver and body positions must also
408    /// be measured from the center of the solar system, using the same
409    /// pointing directions for the coordinate axes.
410    ///
411    /// If the positions come from different measurement systems, the
412    /// calculated delay will be wrong.
413    ///
414    /// In most solar-system work, people use positions from JPL ephemerides
415    /// (which are measured from the center of the solar system).
416    ///
417    /// Pass an empty slice (`&[]`) to turn off Shapiro delay entirely.
418    ///
419    /// ## Parameters
420    ///
421    /// * `rx` — Receiver state at the approximate time the signal arrives.
422    /// * `bodies` — List of bodies that should contribute to the delay.
423    ///
424    /// ## Returns
425    ///
426    /// The total one-way Shapiro gravitational propagation delay, in the
427    /// same time scale as `self.time`. This value should normally be
428    /// **added** to the Newtonian geometric light time.
429    pub const fn shapiro_delay(&self, rx: &Observer, bodies: &[(Dt, Position)]) -> Dt {
430        let mut total = Dt::ZERO;
431        let mut i = 0;
432
433        while i < bodies.len() {
434            let (shapiro_coeff, body_pos) = &bodies[i];
435            total = total.add(Self::shapiro_one_way_delay(
436                *shapiro_coeff,
437                &self.position,
438                &rx.position,
439                body_pos,
440            ));
441            i += 1;
442        }
443
444        total
445    }
446
447    /// Computes the first-order one-way Shapiro gravitational time delay
448    /// due to a single central body using a numerically stable formulation.
449    ///
450    /// This is the **core low-level implementation** (pub(crate) const fn).
451    /// It replaces the classic radial formula with an algebraically equivalent
452    /// but cancellation-free form that is robust even for small impact parameters
453    /// (near-grazing / conjunction geometries).
454    ///
455    /// The algorithm uses the identity:
456    ///
457    ///
458    ///   ln((r_tx + r_rx + r_sep) / (r_tx + r_rx - r_sep))
459    ///   ≡ 2·ln(num) − ln(denom_term)
460    ///
461    ///
462    /// where denom_term is computed from the dot-product identity
463    /// (r_tx + r_rx)² − r_sep² = 2(r_tx·r_rx + p_tx · p_rx).
464    /// This avoids the dangerous subtraction that loses precision when
465    /// the signal path passes close to the body.
466    ///
467    /// The result is equivalent (within floating-point) to the
468    /// classic Moyer/DSN-style formula while being far more stable.
469    /// Contributions from multiple bodies are summed at a higher level.
470    ///
471    /// ## Safety / Guards
472    ///
473    /// - Returns [`Dt::ZERO`](../../struct.Dt.html#associatedconstant.ZERO)
474    ///   for any non-positive distance or zero Shapiro coefficient.
475    /// - Protects against invalid logarithm argument (`arg <= 1.0`).
476    /// - Designed for weak-field solar-system / cislunar use (monopole, straight-line approx).
477    pub(crate) const fn shapiro_one_way_delay(
478        shapiro: Dt,
479        tx_pos: &Position,
480        rx_pos: &Position,
481        body_pos: &Position,
482    ) -> Dt {
483        let shapiro_sec = shapiro.to_sec_f();
484
485        // Distances relative to *this specific gravitating body*
486        let r_tx = tx_pos.distance_to(body_pos);
487        let r_rx = rx_pos.distance_to(body_pos);
488        let r_sep = tx_pos.distance_to(rx_pos);
489
490        if r_tx <= f!(0.0) || r_rx <= f!(0.0) || r_sep <= f!(0.0) || shapiro_sec == f!(0.0) {
491            return Dt::ZERO;
492        }
493
494        let s = r_tx + r_rx;
495        let num = s + r_sep; // (r_tx + r_rx + r_sep)
496
497        if num <= f!(0.0) {
498            return Dt::ZERO;
499        }
500
501        // Stable computation of (r_tx + r_rx)^2 − r_sep^2
502        // = 2 × (r_tx r_rx + \vec{p_tx} · \vec{p_rx})
503        let dot_term = (r_tx * r_tx + r_rx * r_rx - r_sep * r_sep) / f!(2.0);
504        let denom_term = f!(2.0) * (r_tx * r_rx + dot_term);
505
506        if denom_term <= f!(0.0) {
507            return Dt::ZERO;
508        }
509
510        let arg = (num * num) / denom_term;
511
512        if arg <= f!(1.0) {
513            return Dt::ZERO;
514        }
515
516        let delay_sec = shapiro_sec * log(arg);
517        Dt::from_sec_f(delay_sec, Scale::TAI, Scale::TAI)
518    }
519
520    /// Computes the differential proper-time correction between `self`
521    /// (transmitter) and `rx` (receiver) over the interval between their
522    /// time tags.
523    ///
524    /// This returns the difference in proper time advance between the two
525    /// observers. It does **not** include Shapiro propagation delay.
526    ///
527    /// The result can be added to the output of
528    /// [`Observer::shapiro_delay`](#method.shapiro_delay)
529    /// or
530    /// [`Observer::iterative_one_way_light_time_to`](#method.iterative_one_way_light_time_to)
531    /// when a combined relativistic correction (propagation + clock rate) is
532    /// required.
533    ///
534    /// ## Parameters
535    ///
536    /// * `rx` — Receiver state at the approximate time of reception.
537    ///
538    /// ## Returns
539    ///
540    /// The differential clock-rate correction (`rx_proper_advance − tx_proper_advance`).
541    pub const fn compute_differential_clock_correction(&self, rx: &Observer) -> Dt {
542        let span = rx.time.to_diff_raw(self.time);
543
544        let tx_drift = Drift::from_velocity_potential_and_scale(
545            self.velocity.speed(),
546            self.grav_potential_m2_s2,
547            self.characteristic_length_scale,
548        );
549        let rx_drift = Drift::from_velocity_potential_and_scale(
550            rx.velocity.speed(),
551            rx.grav_potential_m2_s2,
552            rx.characteristic_length_scale,
553        );
554
555        rx_drift
556            .time_diff_after(&span)
557            .sub(tx_drift.time_diff_after(&span))
558    }
559}