Skip to main content

deep_time/physics/
light_time.rs

1use crate::{
2    C, C_SQUARED, Drift, Dt, Position, Real, Scale, Spacetime, TWO_GM_SUN_OVER_C3, Velocity, log,
3};
4
5impl Dt {
6    /// Shapiro gravitational time scale for the Sun (`2 G M_☉ / c³`).
7    ///
8    /// Recommended value for the Sun when building the `bodies` slice passed to
9    /// [`Observer::shapiro_delay`], [`Observer::shapiro_delay`],
10    /// and related methods.
11    pub const SHAPIRO_SOLAR: Self = Self::from_sec_f(TWO_GM_SUN_OVER_C3, Scale::TAI);
12
13    /// Creates the Shapiro delay scale for an arbitrary central body
14    /// from its standard gravitational parameter `GM` (μ) in m³ s⁻².
15    ///
16    /// This produces the coefficient used in the Shapiro gravitational time delay
17    /// formula. It is the recommended way to create a custom Shapiro scale for
18    /// planets, stars, or other massive bodies.
19    ///
20    /// The returned value is intended to be used for the `bodies` parameter
21    /// when calling [`Observer::shapiro_delay`] or
22    /// [`Observer::shapiro_delay`].
23    #[inline]
24    pub const fn shapiro_from_grav_param(gm: Real) -> Dt {
25        let secs = 2.0 * gm / (C * C_SQUARED);
26        Self::from_sec_f(secs, Scale::TAI)
27    }
28
29    /// Creates an [`Observer`] using this time value along with the
30    /// provided position, velocity, and gravitational information.
31    ///
32    /// An [`Observer`] represents a complete snapshot of an observer
33    /// (spacecraft, ground station, planet, person, etc.) at a
34    /// specific moment.
35    ///
36    /// It bundles together the time, position, velocity, and local
37    /// gravitational environment so that relativistic calculations
38    /// (light time, clock rates, Shapiro delay, etc.) can be performed.
39    ///
40    /// This method is a convenience constructor. It is useful when you
41    /// already have a [`Dt`] (a time value) and want to build an
42    /// [`Observer`] directly from it.
43    ///
44    /// ## Parameters
45    ///
46    /// - `position`: The observer’s position in meters (typically expressed
47    ///   in a barycentric or heliocentric frame).
48    /// - `velocity`: The observer’s velocity in meters per second.
49    /// - `grav_potential_m2_s2`: The total Newtonian gravitational potential
50    ///   (Φ) at the observer’s location, in m²/s². This is usually negative
51    ///   for bound orbits and is the sum of contributions from the Sun and
52    ///   planets.
53    /// - `characteristic_length_scale`: A length scale (in meters) over which
54    ///   gravity varies significantly at this location. Use `0.0` for normal
55    ///   solar-system and weak-field cases. Only provide a non-zero value when
56    ///   working in strong gravitational fields.
57    ///
58    /// ## Examples
59    ///
60    /// ```
61    /// use deep_time::{Dt, Position, Spacetime, Velocity};
62    ///
63    /// let bodies = [
64    ///     (Position::from_au(0.0, 0.0, 0.0), 1.3271244e20),     // Sun
65    ///     (Position::from_au(1.0, 0.0, 0.0), 3.9860044e14),     // Earth
66    ///     (Position::from_au(1.00257, 0.0, 0.0), 4.9048695e12), // Moon
67    /// ];
68    ///
69    /// let position = Position::from_au(1.001, 0.001, 0.0); // e.g. spacecraft, asteroid, etc.
70    ///
71    /// let grav_potential = Spacetime::grav_potential_from_point_masses(
72    ///     position,
73    ///     bodies.iter().copied(),
74    /// );
75    ///
76    /// let t = Dt::span_f(1234.5);
77    ///
78    /// let state = t.to_observer(
79    ///     Position::ZERO,
80    ///     Velocity::ZERO,
81    ///     grav_potential,
82    ///     0.0, // normal solar-system use
83    /// );
84    /// ```
85    #[inline]
86    pub const fn to_observer(
87        self,
88        position: Position,
89        velocity: Velocity,
90        grav_potential_m2_s2: Real,
91        characteristic_length_scale: Real,
92    ) -> Observer {
93        Observer {
94            time: self,
95            position,
96            velocity,
97            grav_potential_m2_s2,
98            characteristic_length_scale,
99        }
100    }
101}
102
103/// An observer at a specific instant.
104///
105/// Combines time, position, velocity, and local gravitational
106/// information. It is the main input type used by relativistic light-time
107/// methods in this library.
108#[derive(Clone, Copy, Debug, PartialEq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
111pub struct Observer {
112    /// The time of this observer.
113    ///
114    /// Any [`Scale`] is accepted. This time is treated as coordinate time
115    /// for light-time calculations.
116    pub time: Dt,
117
118    /// Position of the observer in meters.
119    ///
120    /// Typically expressed in a barycentric (solar-system barycenter) or
121    /// heliocentric frame, depending on the application.
122    pub position: Position,
123
124    /// Velocity of the observer in meters per second.
125    pub velocity: Velocity,
126
127    /// Newtonian gravitational potential Φ at the observer’s location
128    /// (in m² s⁻²).
129    ///
130    /// This value is usually negative for bound orbits. It should normally
131    /// include contributions from the Sun and all relevant planets.
132    pub grav_potential_m2_s2: Real,
133
134    /// Characteristic length scale (in meters) over which the gravitational
135    /// field varies significantly at this location.
136    ///
137    /// - Use `0.0` (the default) for all solar-system, GNSS, and weak-field
138    ///   applications.
139    /// - Provide a non-zero value only when working in strong gravitational
140    ///   fields (e.g. near neutron stars or black holes), where the library’s
141    ///   higher-order curvature terms become relevant.
142    pub characteristic_length_scale: Real,
143}
144
145impl Observer {
146    /// Creates a new `Observer` for typical solar-system, GNSS,
147    /// or weak-field use.
148    ///
149    /// This is the recommended constructor for most applications.
150    /// It sets the `characteristic_length_scale` to `0.0`, which disables
151    /// higher-order curvature terms in the proper-time model.
152    ///
153    /// ## Parameters
154    ///
155    /// - `time`: The time of the observer.
156    /// - `position`: Position in meters (usually barycentric or heliocentric).
157    /// - `velocity`: Velocity in m/s.
158    /// - `grav_potential_m2_s2`: Newtonian gravitational potential Φ
159    ///   at the location (in m²/s²).
160    #[inline]
161    pub const fn new(
162        time: Dt,
163        position: Position,
164        velocity: Velocity,
165        grav_potential_m2_s2: Real,
166    ) -> Observer {
167        Self {
168            time,
169            position,
170            velocity,
171            grav_potential_m2_s2,
172            characteristic_length_scale: 0.0,
173        }
174    }
175
176    /// Returns the instantaneous proper-time rate `dτ/dt` for this observer.
177    ///
178    /// This value indicates how fast a physical clock located at this observer
179    /// would advance relative to the time used by this `Observer`.
180    /// A returned value of `1.0` means the clock advances at the same rate
181    /// as the observer's time coordinate. Values are typically slightly different
182    /// from `1.0` due to the effects of velocity and gravitational potential.
183    ///
184    /// This rate is computed using the library’s unified proper-time model.
185    /// It is used internally for light-time corrections and Doppler calculations.
186    #[inline]
187    pub const fn proper_time_rate(&self) -> Real {
188        Spacetime::from_potential_velocity_and_scale(
189            self.grav_potential_m2_s2 / C_SQUARED,
190            self.velocity,
191            self.characteristic_length_scale,
192        )
193        .proper_time_rate()
194    }
195
196    /// Returns the ratio of proper time rates between the receiver and transmitter
197    /// for a one-way signal.
198    ///
199    /// This method computes:
200    ///
201    /// ```text
202    /// ratio = rx.proper_time_rate() / self.proper_time_rate()
203    /// ```
204    ///
205    /// ### Interpretation
206    ///
207    /// - A value of `1.0` indicates that both clocks run at the same rate.
208    /// - A value **less than `1.0`** means the receiver’s clock runs slower than
209    ///   the transmitter’s clock. The receiver will observe a lower frequency
210    ///   than was emitted.
211    /// - A value **greater than `1.0`** means the receiver’s clock runs faster
212    ///   than the transmitter’s clock. The receiver will observe a higher frequency
213    ///   than was emitted.
214    ///
215    /// The ratio captures the combined effect of special-relativistic time dilation
216    /// (due to velocity) and general-relativistic gravitational time dilation.
217    ///
218    /// ### Typical Usage (One-Way)
219    ///
220    /// This ratio is often combined with the classical kinematic Doppler term
221    /// to estimate the total one-way frequency shift:
222    ///
223    /// ```text
224    /// approximate_frequency_shift ≈ ratio * (1 - v_radial / C)
225    /// ```
226    ///
227    /// where `v_radial` is the radial velocity (positive when the receiver is
228    /// receding).
229    ///
230    /// ### Two-Way Usage
231    ///
232    /// For round-trip (two-way) measurements, square the one-way ratio:
233    ///
234    /// ```rust
235    /// use deep_time::{Dt, Observer, Position, Spacetime, Velocity};
236    ///
237    /// let bodies = [
238    ///     (Position::from_au(0.0, 0.0, 0.0), 1.3271244e20), // Sun
239    ///     (Position::from_au(1.0, 0.0, 0.0), 3.9860044e14), // Earth
240    /// ];
241    ///
242    /// let tx_pos = Position::from_au(1.0, 0.0, 0.0);
243    /// let rx_pos = Position::from_au(1.00257, 0.0, 0.0);
244    ///
245    /// let grav_potential_tx = Spacetime::grav_potential_from_point_masses(tx_pos, bodies.iter().copied());
246    /// let grav_potential_rx = Spacetime::grav_potential_from_point_masses(rx_pos, bodies.iter().copied());
247    ///
248    /// let transmitter = Observer::new(
249    ///     Dt::span_f(0.0),
250    ///     tx_pos,
251    ///     Velocity::ZERO,
252    ///     grav_potential_tx,
253    /// );
254    ///
255    /// let receiver = Observer::new(
256    ///     Dt::span_f(0.0),
257    ///     rx_pos,
258    ///     Velocity::from_speed(800.0),
259    ///     grav_potential_rx,
260    /// );
261    ///
262    /// let one_way_ratio = transmitter.relativistic_clock_rate_ratio(receiver);
263    /// let two_way_ratio = one_way_ratio * one_way_ratio;
264    /// ```
265    ///
266    /// **Note:** Squaring the one-way ratio is a common first-order approximation.
267    /// For higher precision (especially during flybys or when uplink and downlink
268    /// geometries differ significantly), consider using
269    /// [`round_trip_light_time_correction`](Self::round_trip_light_time_correction)
270    /// instead.
271    ///
272    /// This pattern is commonly used when correcting two-way Doppler (range-rate)
273    /// data for relativistic clock effects.
274    ///
275    /// ### Limitations
276    ///
277    /// - This method only accounts for the **difference in clock rates** between
278    ///   the two ends.
279    /// - It does **not** include Shapiro delay or higher-order relativistic effects
280    ///   on signal propagation.
281    /// - The combination with classical Doppler shown above is a first-order
282    ///   approximation.
283    ///
284    /// ## Parameters
285    ///
286    /// - `self` — Transmitter state at the time of transmission.
287    /// - `rx`   — Receiver state at the approximate time of reception.
288    ///
289    /// ## Examples
290    ///
291    /// ```rust
292    /// use deep_time::{Dt, Observer, Position, Spacetime, Velocity, constants::C};
293    ///
294    /// let bodies = [
295    ///     (Position::from_au(0.0, 0.0, 0.0), 1.3271244e20), // Sun
296    ///     (Position::from_au(1.0, 0.0, 0.0), 3.9860044e14), // Earth
297    /// ];
298    ///
299    /// let tx_pos = Position::from_au(1.0, 0.0, 0.0);
300    /// let rx_pos = Position::from_au(1.002, 0.0, 0.0);
301    ///
302    /// let grav_potential_tx = Spacetime::grav_potential_from_point_masses(tx_pos, bodies.iter().copied());
303    /// let grav_potential_rx = Spacetime::grav_potential_from_point_masses(rx_pos, bodies.iter().copied());
304    ///
305    /// let transmitter = Observer::new(
306    ///     Dt::span_f(0.0),
307    ///     tx_pos,
308    ///     Velocity::ZERO,
309    ///     grav_potential_tx,
310    /// );
311    ///
312    /// // Receiver receding at ~1.2 km/s (example spacecraft)
313    /// let receiver = Observer::new(
314    ///     Dt::span_f(0.0),
315    ///     rx_pos,
316    ///     Velocity::from_speed(1200.0),
317    ///     grav_potential_rx,
318    /// );
319    ///
320    /// let ratio = transmitter.relativistic_clock_rate_ratio(receiver);
321    ///
322    /// let v_radial = 1200.0; // m/s, positive if receding
323    /// let classical_doppler = 1.0 - v_radial / C;
324    ///
325    /// let approx_frequency_shift = ratio * classical_doppler;
326    /// ```
327    #[inline]
328    pub const fn relativistic_clock_rate_ratio(&self, rx: Observer) -> Real {
329        rx.proper_time_rate() / self.proper_time_rate()
330    }
331
332    /// Computes the combined one-way relativistic correction for a signal
333    /// traveling from this observer (the transmitter) to a receiver.
334    ///
335    /// This value is the **total extra time** you should add to the Newtonian
336    /// geometric light travel time (`distance / speed of light`). It includes
337    /// **two** separate relativistic effects:
338    ///
339    /// 1. The gravitational propagation delay (Shapiro delay) caused by the
340    ///    Sun and other bodies slowing the signal.
341    /// 2. The differential clock-rate correction caused by the transmitter
342    ///    and receiver having slightly different proper-time rates (due to
343    ///    their velocities and gravitational potentials).
344    ///
345    /// In other words, this method gives you **propagation delay + clock-rate
346    /// correction** in one convenient call.
347    ///
348    /// **Important:** This is a convenience method. It is provided so you can
349    /// get the full one-way relativistic correction quickly. If you need
350    /// strict separation of the two effects (for example, to apply them at
351    /// different stages of your calculation), call
352    /// [`Self::shapiro_delay`] and [`Self::compute_differential_clock_correction`]
353    /// individually and add the results yourself.
354    ///
355    /// ## When to use this method
356    ///
357    /// Use this when you need the complete relativistic correction for
358    /// one-way light time in a single step — for example when:
359    /// - Computing high-precision one-way range or Doppler observables
360    /// - Building simplified navigation or orbit determination models
361    /// - You want the total effect without manually combining the pieces
362    ///
363    /// ## The `bodies` parameter – which masses to include
364    ///
365    /// Pass a slice of `(shapiro_coefficient, body_position)` pairs:
366    ///
367    /// - `shapiro_coefficient`: How strong the delay from this body should be.
368    ///   It equals `2GM / c³`. Use [`Dt::SHAPIRO_SOLAR`] for the Sun, or
369    ///   [`Dt::shapiro_from_grav_param(gm)`] for any other body.
370    /// - `body_position`: Where the center of that body is located at the
371    ///   relevant time.
372    ///
373    /// **Important: All positions must be measured the same way**
374    ///
375    /// The transmitter position (`self.position`), the receiver position
376    /// (`rx.position`), and every `body_position` you provide must all be
377    /// measured from the **same point in space**, and they must all use
378    /// the **same directions** for their X, Y, and Z axes.
379    ///
380    /// For example, if your transmitter position is measured from the center
381    /// of the solar system, then the receiver and body positions must also
382    /// be measured from the center of the solar system using the same
383    /// pointing directions for the coordinate axes.
384    ///
385    /// In most solar-system work, people use positions from JPL ephemerides
386    /// (which are measured from the center of the solar system).
387    ///
388    /// Pass an empty slice (`&[]`) to turn off the Shapiro (gravitational)
389    /// part of the correction.
390    ///
391    /// ## Parameters
392    ///
393    /// * `rx` — Receiver state at the approximate time the signal arrives.
394    /// * `bodies` — List of bodies that should contribute to the gravitational
395    ///   propagation delay.
396    ///
397    /// ## Returns
398    ///
399    /// The total one-way relativistic correction (Shapiro propagation delay
400    /// plus differential clock-rate correction), expressed as a `Dt` in the
401    /// same time scale as `self.time`.
402    ///
403    /// This value should normally be **added** to the Newtonian geometric
404    /// light time.
405    pub const fn one_way_relativistic_delay(&self, rx: Observer, bodies: &[(Dt, Position)]) -> Dt {
406        let prop = self.shapiro_delay(rx, bodies);
407        let drift = self.compute_differential_clock_correction(rx);
408        prop.add(drift)
409    }
410
411    /// Iteratively solves the one-way light-time equation in coordinate time,
412    /// including relativistic propagation corrections, until convergence.
413    ///
414    /// This solver computes the receive epoch `t_rx` such that:
415    ///
416    /// ```text
417    /// t_rx = t_tx + |r_rx(t_rx) − r_tx(t_tx)| / c + Δt_shapiro(t_tx, t_rx)
418    /// ```
419    ///
420    /// It performs fixed-point iteration using the propagation delay returned by
421    /// [`Self::shapiro_delay`]. Clock-rate and proper-time effects
422    /// are **not** included in the iteration; they should be applied separately
423    /// when converting between coordinate time and proper time or when forming
424    /// observables.
425    ///
426    /// The solver is suitable for high-precision one-way light-time calculations
427    /// and works with any ephemeris source via the provided closure.
428    ///
429    /// ## Parameters
430    ///
431    /// * `rx_provider` — Closure returning the full [`Observer`] of the
432    ///   receiver at a given coordinate time.
433    /// * `bodies` — Slice of `(shapiro_coefficient, body_position)` pairs
434    ///   controlling the Shapiro contribution. Use `&[(Dt::SHAPIRO_SOLAR, sun_pos)]`
435    ///   for solar-system work; include additional bodies for higher precision.
436    ///   Pass `&[]` to disable Shapiro.
437    /// * `tolerance` — Maximum allowed change in receive time per iteration
438    ///   before declaring convergence (e.g. `Dt::from_ns(1, Scale::TAI)`).
439    /// * `max_iter` — Maximum number of iterations. Typical values are 12–20
440    ///   for solar-system geometries.
441    ///
442    /// ## Returns
443    ///
444    /// A tuple `(prop_correction, rx_time, final_state)` where:
445    /// - `prop_correction` is the converged Shapiro propagation delay,
446    /// - `rx_time` is the converged receive time (same scale as `self.time`),
447    /// - `final_state` is the receiver state at `rx_time`.
448    pub fn iterative_one_way_light_time_to<F>(
449        &self,
450        rx_provider: &mut F,
451        bodies: &[(Dt, Position)],
452        tolerance: Dt,
453        max_iter: usize,
454    ) -> (Dt, Dt, Observer)
455    where
456        F: FnMut(Dt) -> Observer,
457    {
458        // Initial geometric guess
459        let initial_rx = rx_provider(self.time);
460        let initial_r_sep = self.position.distance_to(initial_rx.position);
461        let initial_geometric = Dt::from_sec_f(initial_r_sep / C, Scale::TAI);
462
463        let mut rx_time = self.time.add(initial_geometric);
464        let mut prop_correction = Dt::ZERO;
465
466        for _ in 0..max_iter {
467            let rx = rx_provider(rx_time);
468
469            prop_correction = self.shapiro_delay(rx, bodies);
470
471            let r_sep = self.position.distance_to(rx.position);
472            let geometric = Dt::from_sec_f(r_sep / C, Scale::TAI);
473            let full_delay = geometric.add(prop_correction);
474
475            let new_rx_time = self.time.add(full_delay);
476            let change = new_rx_time.to_diff_raw(rx_time);
477
478            rx_time = new_rx_time;
479
480            if change.abs() < tolerance {
481                return (prop_correction, rx_time, rx);
482            }
483        }
484
485        // Fallback after max iterations
486        let final_rx = rx_provider(rx_time);
487        (prop_correction, rx_time, final_rx)
488    }
489
490    /// Computes the total Shapiro (gravitational propagation) delay for a
491    /// complete round-trip (two-way) signal.
492    ///
493    /// This method solves the uplink and downlink legs *separately and
494    /// independently* using the iterative light-time solver. This approach
495    /// is more accurate than older combined round-trip formulas when the
496    /// two ends have significantly different velocities or are in different
497    /// gravitational environments.
498    ///
499    /// The returned value is the **sum of the uplink and downlink Shapiro
500    /// delays only**. It does **not** include clock-rate or proper-time
501    /// corrections.
502    ///
503    /// ## When to use this method
504    ///
505    /// Use this when you need the total gravitational propagation correction
506    /// for two-way (round-trip) measurements, for example:
507    /// - Two-way range or range-rate (Doppler) data
508    /// - Transponded signals from spacecraft
509    /// - Any high-precision two-way light-time calculation
510    ///
511    /// For one-way signals, use [`Self::shapiro_delay`] or
512    /// [`Self::one_way_relativistic_delay`] instead.
513    ///
514    /// ## How the calculation works
515    ///
516    /// 1. Solves the uplink leg (from `self` to the remote receiver) using
517    ///    the `rx_provider` closure.
518    /// 2. Obtains the accurate receiver state at the uplink arrival time.
519    /// 3. Solves the downlink leg (from the receiver back to the local
520    ///    transmitter) using the `tx_provider` closure.
521    ///
522    /// ## The `bodies` parameter – which masses to include
523    ///
524    /// Pass a slice of `(shapiro_coefficient, body_position)` pairs (the
525    /// same slice is used for both legs). See [`Self::shapiro_delay`] for
526    /// details on how to build this slice.
527    ///
528    /// **Important: All states returned by the providers must be consistent**
529    /// with the same reference frame (same origin and same coordinate axes).
530    ///
531    /// ## Parameters
532    ///
533    /// * `rx_provider` — Closure that returns the full [`Observer`] of
534    ///   the remote receiver (planet, spacecraft, etc.) at any given
535    ///   coordinate time.
536    /// * `tx_provider` — Closure that returns the full [`Observer`] of
537    ///   the local transmitter at any given coordinate time (used only for
538    ///   the downlink leg).
539    /// * `bodies` — Slice of `(shapiro_coefficient, body_position)` pairs
540    ///   describing the gravitating bodies.
541    /// * `tolerance` — Convergence tolerance for each leg’s iterative solver
542    ///   (e.g. `Dt::from_ns(1, Scale::TAI)`).
543    /// * `max_iter` — Maximum number of iterations allowed per leg
544    ///   (typical values are 12–20).
545    ///
546    /// ## Returns
547    ///
548    /// The total round-trip Shapiro propagation delay (uplink + downlink)
549    /// as a `Dt`, in the same time scale as `self.time`.
550    ///
551    /// This value should normally be **added** to the Newtonian geometric
552    /// round-trip light time. Clock-rate corrections must still be applied
553    /// separately (e.g. by squaring the one-way clock-rate ratio).
554    pub fn round_trip_light_time_correction<RxF, TxF>(
555        &self,
556        mut rx_provider: RxF, // remote body (planet, spacecraft, etc.)
557        mut tx_provider: TxF, // local transmitter for the return leg (can move)
558        bodies: &[(Dt, Position)],
559        tolerance: Dt,
560        max_iter: usize,
561    ) -> Dt
562    where
563        RxF: FnMut(Dt) -> Observer,
564        TxF: FnMut(Dt) -> Observer,
565    {
566        // Uplink leg: transmitter → receiver
567        let (uplink_prop, rx_time, _rx_state) =
568            self.iterative_one_way_light_time_to(&mut rx_provider, bodies, tolerance, max_iter);
569
570        // Downlink leg: receiver → transmitter
571        let return_tx = rx_provider(rx_time); // accurate state at uplink arrival
572
573        let (downlink_prop, _return_rx_time, _return_rx_state) = return_tx
574            .iterative_one_way_light_time_to(&mut tx_provider, bodies, tolerance, max_iter);
575
576        uplink_prop.add(downlink_prop)
577    }
578
579    /// Computes the one-way gravitational propagation delay (Shapiro delay)
580    /// caused by massive bodies between this observer (the transmitter) and
581    /// a receiver.
582    ///
583    /// This value is the **extra time** a radio signal takes to travel because
584    /// gravity from the Sun and planets slightly slows it down. You normally
585    /// add this delay to the ordinary geometric light travel time
586    /// (`distance / speed of light`) to get a more accurate total one-way
587    /// signal travel time.
588    ///
589    /// **Important:** This method returns **only** the gravitational
590    /// propagation delay. It does **not** include clock-rate differences
591    /// between the transmitter and receiver caused by velocity or gravity.
592    /// Those effects are available separately through
593    /// [`Self::compute_differential_clock_correction`],
594    /// [`Self::proper_time_rate`], and [`Self::relativistic_clock_rate_ratio`].
595    ///
596    /// ## When to use this method
597    ///
598    /// Use this when you need the gravitational (Shapiro) contribution to
599    /// one-way light time — for example when building high-precision range,
600    /// Doppler, or orbit determination models.
601    ///
602    /// ## The `bodies` parameter – which masses to include
603    ///
604    /// Pass a slice of `(shapiro_coefficient, body_position)` pairs:
605    ///
606    /// - `shapiro_coefficient`: How strong the delay from this body should be.
607    ///   It equals `2GM / c³`. Use [`Dt::SHAPIRO_SOLAR`] for the Sun, or
608    ///   [`Dt::shapiro_from_grav_param(gm)`] for any other body.
609    /// - `body_position`: Where the center of that body is located at the
610    ///   relevant time.
611    ///
612    /// **Important: All positions must be measured the same way**
613    ///
614    /// The transmitter position (`self.position`), the receiver position
615    /// (`rx.position`), and every `body_position` you provide must all be
616    /// measured from the **same point in space**, and they must all use
617    /// the **same directions** for their X, Y, and Z axes.
618    ///
619    /// For example, if the transmitter position is measured from the center
620    /// of the solar system, then the receiver and body positions must also
621    /// be measured from the center of the solar system, using the same
622    /// pointing directions for the coordinate axes.
623    ///
624    /// If the positions come from different measurement systems, the
625    /// calculated delay will be wrong.
626    ///
627    /// In most solar-system work, people use positions from JPL ephemerides
628    /// (which are measured from the center of the solar system).
629    ///
630    /// Pass an empty slice (`&[]`) to turn off Shapiro delay entirely.
631    ///
632    /// ## Parameters
633    ///
634    /// * `rx` — Receiver state at the approximate time the signal arrives.
635    /// * `bodies` — List of bodies that should contribute to the delay.
636    ///
637    /// ## Returns
638    ///
639    /// The total one-way Shapiro gravitational propagation delay, in the
640    /// same time scale as `self.time`. This value should normally be
641    /// **added** to the Newtonian geometric light time.
642    pub const fn shapiro_delay(&self, rx: Observer, bodies: &[(Dt, Position)]) -> Dt {
643        let mut total = Dt::ZERO;
644        let mut i = 0;
645
646        while i < bodies.len() {
647            let (shapiro_coeff, body_pos) = bodies[i];
648            total = total.add(Self::shapiro_one_way_delay(
649                shapiro_coeff,
650                self.position,
651                rx.position,
652                body_pos,
653            ));
654            i += 1;
655        }
656
657        total
658    }
659
660    /// Computes the first-order one-way Shapiro gravitational time delay
661    /// due to a single central body using a numerically stable formulation.
662    ///
663    /// This is the **core low-level implementation** (pub(crate) const fn).
664    /// It replaces the classic radial formula with an algebraically equivalent
665    /// but cancellation-free form that is robust even for small impact parameters
666    /// (near-grazing / conjunction geometries).
667    ///
668    /// The algorithm uses the identity:
669    ///
670    ///
671    ///   ln((r_tx + r_rx + r_sep) / (r_tx + r_rx - r_sep))
672    ///   ≡ 2·ln(num) − ln(denom_term)
673    ///
674    ///
675    /// where denom_term is computed from the dot-product identity
676    /// (r_tx + r_rx)² − r_sep² = 2(r_tx·r_rx + p_tx · p_rx).
677    /// This avoids the dangerous subtraction that loses precision when
678    /// the signal path passes close to the body.
679    ///
680    /// The result is equivalent (within floating-point) to the
681    /// classic Moyer/DSN-style formula while being far more stable.
682    /// Contributions from multiple bodies are summed at a higher level.
683    ///
684    /// ## Safety / Guards
685    ///
686    /// - Returns [`Dt::ZERO`](../struct.Dt.html#associatedconstant.ZERO)
687    ///   for any non-positive distance or zero Shapiro coefficient.
688    /// - Protects against invalid logarithm argument (`arg <= 1.0`).
689    /// - Designed for weak-field solar-system / cislunar use (monopole, straight-line approx).
690    pub(crate) const fn shapiro_one_way_delay(
691        shapiro: Dt,
692        tx_pos: Position,
693        rx_pos: Position,
694        body_pos: Position,
695    ) -> Dt {
696        let shapiro_sec = shapiro.to_sec_f();
697
698        // Distances relative to *this specific gravitating body*
699        let r_tx = tx_pos.distance_to(body_pos);
700        let r_rx = rx_pos.distance_to(body_pos);
701        let r_sep = tx_pos.distance_to(rx_pos);
702
703        if r_tx <= f!(0.0) || r_rx <= f!(0.0) || r_sep <= f!(0.0) || shapiro_sec == f!(0.0) {
704            return Dt::ZERO;
705        }
706
707        let s = r_tx + r_rx;
708        let num = s + r_sep; // (r_tx + r_rx + r_sep)
709
710        if num <= f!(0.0) {
711            return Dt::ZERO;
712        }
713
714        // Stable computation of (r_tx + r_rx)^2 − r_sep^2
715        // = 2 × (r_tx r_rx + \vec{p_tx} · \vec{p_rx})
716        let dot_term = (r_tx * r_tx + r_rx * r_rx - r_sep * r_sep) / f!(2.0);
717        let denom_term = f!(2.0) * (r_tx * r_rx + dot_term);
718
719        if denom_term <= f!(0.0) {
720            return Dt::ZERO;
721        }
722
723        let arg = (num * num) / denom_term;
724
725        if arg <= f!(1.0) {
726            return Dt::ZERO;
727        }
728
729        let delay_sec = shapiro_sec * log(arg);
730        Dt::from_sec_f(delay_sec, Scale::TAI)
731    }
732
733    /// Computes the differential proper-time correction between `self`
734    /// (transmitter) and `rx` (receiver) over the interval between their
735    /// time tags.
736    ///
737    /// This returns the difference in proper time advance between the two
738    /// observers. It does **not** include Shapiro propagation delay.
739    ///
740    /// The result can be added to the output of [`Self::shapiro_delay`]
741    /// or [`Self::iterative_one_way_light_time_to`] when a combined
742    /// relativistic correction (propagation + clock rate) is required.
743    ///
744    /// ## Parameters
745    ///
746    /// * `rx` — Receiver state at the approximate time of reception.
747    ///
748    /// ## Returns
749    ///
750    /// The differential clock-rate correction (`rx_proper_advance − tx_proper_advance`).
751    pub const fn compute_differential_clock_correction(&self, rx: Observer) -> Dt {
752        let span = rx.time.to_diff_raw(self.time);
753
754        let tx_drift = Drift::from_velocity_potential_and_scale(
755            self.velocity.speed(),
756            self.grav_potential_m2_s2,
757            self.characteristic_length_scale,
758        );
759        let rx_drift = Drift::from_velocity_potential_and_scale(
760            rx.velocity.speed(),
761            rx.grav_potential_m2_s2,
762            rx.characteristic_length_scale,
763        );
764
765        rx_drift
766            .time_diff_after(&span)
767            .sub(tx_drift.time_diff_after(&span))
768    }
769}