Skip to main content

deep_time/
light_time.rs

1use crate::{
2    C, C_SQUARED, Drift, Dt, Position, Real, Spacetime, TWO_GM_SUN_OVER_C3, Velocity, log,
3};
4
5/// Configuration for the **Shapiro delay** — the extra time light (or radio signals)
6/// takes to travel near a massive body because gravity curves spacetime.
7///
8/// In simple terms: when a light beam or radar pulse passes close to a star or planet,
9/// general relativity makes the path take a tiny bit longer than it would in flat space.
10/// This struct holds the single number that controls how strong that delay is for a
11/// particular central body (the Sun, a planet, another star, etc.).
12///
13/// Used in high-precision light-time calculations, spacecraft ranging, pulsar timing,
14/// and very-long-baseline interferometry (VLBI).
15#[derive(Copy, Clone, Debug, PartialEq)]
16pub struct LightContext {
17    /// 2GM/c³ in seconds — the characteristic gravitational time scale used in the
18    /// one-way Shapiro delay formula.
19    ///
20    /// This is mathematically equal to `2 * GM / c³`, where:
21    /// - `G` is Newton’s gravitational constant,
22    /// - **M** is the mass of the central body,
23    /// - `c` is the speed of light.
24    ///
25    /// “GM” (often written as a single value called the **standard gravitational parameter** or μ)
26    /// is the product of G and M. In real-world astronomy it is measured very accurately as one
27    /// combined number (in m³/s²), so we keep it together here.
28    ///
29    /// It tells the light-propagation code “how strong the gravitational slowing is”
30    /// for this body. Bigger value = stronger delay effect.
31    ///
32    /// Most users never set this manually — just use `SOLAR` or `from_grav_param()`.
33    pub two_grav_param_over_c3: f64,
34}
35
36impl LightContext {
37    /// Ready-to-use value for our own Sun, based on the exact IAU 2015 recommended constants.
38    ///
39    /// Use this for any typical solar-system light-propagation calculation (radar to
40    /// planets, spacecraft tracking, etc.).
41    pub const SOLAR: Self = Self {
42        two_grav_param_over_c3: TWO_GM_SUN_OVER_C3,
43    };
44
45    /// No gravitational delay at all (flat spacetime approximation).
46    ///
47    /// Perfect for:
48    /// - interstellar or intergalactic distances,
49    /// - quick “ignore gravity” calculations,
50    /// - or when you want to apply your own custom gravitational model elsewhere.
51    pub const FLAT: Self = Self {
52        two_grav_param_over_c3: 0.0,
53    };
54
55    /// Creates a `LightContext` for any central body (planet, star, black hole, etc.).
56    ///
57    /// # Arguments
58    ///
59    /// * `grav_param` — the body’s **standard gravitational parameter** (GM or μ)
60    ///   in m³/s². This is the product of Newton’s gravitational constant and the body’s mass.
61    pub const fn from_grav_param(grav_param: f64) -> Self {
62        Self {
63            two_grav_param_over_c3: 2.0 * grav_param / (C * C_SQUARED),
64        }
65    }
66}
67
68/// A complete relativistic state of an observer (spacecraft, ground station,
69/// planet, etc.) at a specific instant.
70///
71/// This is the natural input type for all relativistic light-time calculations
72/// in the library. It bundles position, velocity, gravitational potential, and
73/// an optional length scale in convenient SI units.
74#[derive(Clone, Copy, Debug, PartialEq)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76#[cfg_attr(feature = "js", derive(tsify::Tsify))]
77pub struct ObserverState {
78    /// Dt of this state (any [`Scale`] is accepted).
79    pub time: Dt,
80    /// Position in meters (typically barycentric or heliocentric).
81    pub position: Position,
82    /// Velocity in meters per second.
83    pub velocity: Velocity,
84    /// Local gravitational potential Φ in m² s⁻² (negative for bound orbits).
85    /// Usually the sum of contributions from the Sun and planets.
86    pub grav_potential_m2_s2: f64,
87    /// Characteristic length scale (in meters) over which gravity varies
88    /// significantly at the observer’s location.  
89    /// Pass `0.0` (the default) for all solar-system, GNSS, and weak-field cases.
90    pub characteristic_length_scale: f64,
91}
92
93impl ObserverState {
94    /// Creates a new state for typical solar-system or GNSS use.
95    #[inline]
96    pub const fn new(
97        time: Dt,
98        position: Position,
99        velocity: Velocity,
100        grav_potential_m2_s2: f64,
101    ) -> Self {
102        Self {
103            time,
104            position,
105            velocity,
106            grav_potential_m2_s2,
107            characteristic_length_scale: 0.0,
108        }
109    }
110
111    /// Creates a new state when strong-field or gravimeter data is available.
112    #[inline]
113    pub const fn new_strong_field(
114        time: Dt,
115        position: Position,
116        velocity: Velocity,
117        grav_potential_m2_s2: f64,
118        characteristic_length_scale: f64,
119    ) -> Self {
120        Self {
121            time,
122            position,
123            velocity,
124            grav_potential_m2_s2,
125            characteristic_length_scale,
126        }
127    }
128
129    /// Returns the instantaneous proper-time rate `dτ/dt` for this observer.
130    ///
131    /// This is the exact rate at which a real clock at the given position,
132    /// velocity, and gravitational environment would advance compared to
133    /// coordinate time. It is used internally by the library for proper-time
134    /// integration, light-time corrections, and Doppler calculations.
135    #[inline]
136    pub const fn proper_time_rate(&self) -> Real {
137        let ls = Spacetime::from_potential_velocity_and_scale(
138            self.grav_potential_m2_s2 / C_SQUARED,
139            self.velocity,
140            self.characteristic_length_scale,
141        );
142        ls.proper_time_rate()
143    }
144
145    /// Returns the relativistic clock-rate Doppler factor for a one-way signal
146    /// sent from this transmitter to the given receiver.
147    ///
148    /// The factor is the ratio of the receiver’s proper-time rate to the
149    /// transmitter’s proper-time rate. It accounts for the fact that clocks
150    /// at the two locations run at slightly different speeds due to motion
151    /// and gravity.
152    ///
153    /// To obtain the full observed frequency shift, multiply this factor by
154    /// the classical kinematic Doppler term `(1 - v_radial / C)`, where
155    /// `v_radial` is the line-of-sight component of the relative velocity
156    /// (positive when the transmitter and receiver are moving apart).
157    ///
158    /// This value is used for deep-space tracking, GNSS range-rate measurements,
159    /// and pulsar timing.
160    ///
161    /// # Parameters
162    /// - `self` – transmitter state (position, velocity, gravitational potential,
163    ///   and length scale at the moment the signal is sent)
164    /// - `rx`   – receiver state (same information, evaluated at the approximate
165    ///   arrival time)
166    ///
167    /// # Example
168    /// ```rust
169    /// use deep_time::{ObserverState, Position, Velocity, Dt};
170    /// use deep_time::constants::C;
171    ///
172    /// # let tx_time = Dt::default();
173    /// # let rx_time = Dt::default();
174    /// # let tx_pos = Position::ZERO;
175    /// # let rx_pos = Position::ZERO;
176    /// # let tx_vel = Velocity::ZERO;
177    /// # let rx_vel = Velocity::ZERO;
178    /// # let phi = 0.0_f64; // gravitational potential
179    ///
180    /// let tx = ObserverState::new(tx_time, tx_pos, tx_vel, phi);
181    /// let rx = ObserverState::new(rx_time, rx_pos, rx_vel, phi);
182    ///
183    /// let factor = tx.relativistic_clock_doppler_factor(rx);
184    ///
185    /// // Full observed frequency shift (example only)
186    /// let v_radial = 0.0; // m/s, positive if receding
187    /// let classical_doppler = 1.0 - v_radial / C;
188    /// let total_frequency_shift = 1.0 * factor * classical_doppler;
189    /// ```
190    #[inline]
191    pub const fn relativistic_clock_doppler_factor(&self, rx: ObserverState) -> Real {
192        rx.proper_time_rate() / self.proper_time_rate()
193    }
194
195    /// Returns the two-way relativistic clock-rate Doppler factor for round-trip
196    /// ranging (transmit → receive → immediate transponder reply).
197    ///
198    /// This is the product of the one-way factors for the complete round trip
199    /// and is the value needed by deep-space networks when correcting measured
200    /// range-rate data.
201    #[inline]
202    pub const fn two_way_relativistic_doppler_factor(&self, rx: ObserverState) -> Real {
203        let one_way = self.relativistic_clock_doppler_factor(rx);
204        one_way * one_way
205    }
206
207    /// Computes the total relativistic correction that must be added to the Newtonian
208    /// geometric light time (`|r_rx − r_tx| / c`) for a one-way signal.
209    ///
210    /// This function accounts for two physical effects:
211    /// - Differential clock-rate drift between transmitter and receiver (special-relativistic
212    ///   velocity + general-relativistic gravitational time dilation) using the library’s
213    ///   unified master-Lagrangian proper-time model.
214    /// - Gravitational (Shapiro) delay caused by spacetime curvature near a central mass.
215    ///
216    /// Use cases include:
217    /// - Deep-space tracking and ranging (DSN, ESA, JPL)
218    /// - GNSS and satellite navigation
219    /// - Pulsar timing arrays
220    /// - Laser communication or ranging to distant spacecraft
221    /// - Future interstellar missions where signals pass near other stars or black holes
222    ///
223    /// # Parameters
224    /// - `self` – the transmitter’s full relativistic state at the moment the signal is sent
225    /// - `rx` – the receiver’s relativistic state at the approximate arrival time
226    /// - `context` – controls the gravitational (Shapiro) contribution. Use `LightContext::SOLAR`
227    ///   for solar-system work, `LightContext::FLAT` when you want no central-mass delay,
228    ///   or `LightContext::from_grav_param(your_gravitational_parameter)` for any other star, planet,
229    ///   or black hole.
230    ///
231    /// # Returns
232    /// A [`Dt`] (in seconds) to be **added** to the Newtonian geometric light time.
233    ///
234    /// # Examples
235    ///
236    /// Basic usage for a solar-system one-way light-time correction (e.g. Earth to Mars):
237    ///
238    /// ```no_run
239    /// use deep_time::{
240    ///     ObserverState, Position, Velocity, Dt, LightContext,
241    ///     // Assume you have ephemeris functions or constants available
242    /// };
243    ///
244    /// # let tx_time: Dt = todo!();
245    /// # let tx_pos: Position = todo!();
246    /// # let tx_vel: Velocity = todo!();
247    /// # let tx_potential: f64 = todo!();
248    /// # let rx_approx_time: Dt = todo!();
249    /// # let rx_pos: Position = todo!();
250    /// # let rx_vel: Velocity = todo!();
251    /// # let rx_potential: f64 = todo!();
252    ///
253    /// let transmitter = ObserverState::new(
254    ///     tx_time,
255    ///     tx_pos,
256    ///     tx_vel,
257    ///     tx_potential,
258    /// );
259    ///
260    /// let receiver_approx = ObserverState::new(
261    ///     rx_approx_time,
262    ///     rx_pos,
263    ///     rx_vel,
264    ///     rx_potential,
265    /// );
266    ///
267    /// // Use SOLAR for Sun-centered calculations
268    /// let correction: Dt = transmitter
269    ///     .one_way_relativistic_delay_to(receiver_approx, LightContext::SOLAR);
270    ///
271    /// // The result should be added to the Newtonian geometric delay `r_sep / C`
272    /// ```
273    ///
274    /// For a custom body (e.g. Jupiter):
275    ///
276    /// ```ignore
277    /// let jupiter_context = LightContext::from_grav_param(jupiter_gm);  // GM in m³/s²
278    /// let correction = tx.one_way_relativistic_delay_to(rx, jupiter_context);
279    /// ```
280    ///
281    /// # Multi-body and exotic environments
282    ///
283    /// This function models the Shapiro delay from only a **single central mass**
284    /// via the supplied `LightContext`. For signals that pass near multiple massive
285    /// bodies (e.g. two stars, a star and a planet, or a binary black-hole system)
286    /// or in highly dynamic/strong-field regimes, the single-body approximation may
287    /// not be sufficient.
288    ///
289    /// In those cases consider using [`one_way_relativistic_delay_integrated`] instead,
290    /// which lets you supply your own full spacetime model along the entire path.
291    /// Alternatively, you can compute individual Shapiro contributions from each body
292    /// (using the helper `shapiro_one_way_for_body` if you add it) and manually combine
293    /// them with the result of this function.
294    pub const fn one_way_relativistic_delay_to(
295        &self,
296        rx: ObserverState,
297        context: LightContext,
298    ) -> Dt {
299        let span = rx.time.to_diff_raw(self.time);
300
301        let tx_drift = Drift::from_velocity_potential_and_scale(
302            self.velocity.speed(),
303            self.grav_potential_m2_s2,
304            self.characteristic_length_scale,
305        );
306        let rx_drift = Drift::from_velocity_potential_and_scale(
307            rx.velocity.speed(),
308            rx.grav_potential_m2_s2,
309            rx.characteristic_length_scale,
310        );
311
312        let drift_correction = rx_drift
313            .time_diff_after(&span)
314            .sub(tx_drift.time_diff_after(&span));
315
316        let r_tx = self.position.norm();
317        let r_rx = rx.position.norm();
318        let r_sep = self.position.distance_to(rx.position);
319        let shapiro = Self::shapiro_one_way_delay(context, r_tx, r_rx, r_sep);
320
321        drift_correction.add(shapiro)
322    }
323
324    /// Iteratively solves the one-way light-time equation including relativistic corrections
325    /// until the receive time converges to the requested tolerance.
326    ///
327    /// This is the recommended high-precision solver for one-way light-time computations
328    /// in modern deep-space navigation. It follows the formulation described in
329    /// Moyer (2003) and is suitable for use with any ephemeris source (SPICE kernels,
330    /// numerical integrators, or analytical propagators).
331    ///
332    /// The solver performs a fixed-point iteration on the light-time equation:
333    ///
334    /// ```text
335    /// t_rx = t_tx + |r_rx(t_rx) − r_tx(t_tx)| / c + Δt_rel(t_tx, t_rx)
336    /// ```
337    ///
338    /// where `Δt_rel` is the relativistic correction returned by
339    /// [`one_way_relativistic_delay_to`]. The iteration continues until the change
340    /// in the estimated receive time falls below `tolerance`.
341    ///
342    /// # Parameters
343    ///
344    /// * `rx_provider` — A mutable closure that returns the full relativistic state
345    ///   of the receiver at a given coordinate time. This allows the solver to work
346    ///   seamlessly with any ephemeris or propagator without requiring a specific
347    ///   data structure.
348    /// * `context` — Controls the gravitational (Shapiro) contribution. Use
349    ///   [`LightContext::SOLAR`] for solar-system work or
350    ///   [`LightContext::from_grav_param`] for other central bodies.
351    /// * `tolerance` — Convergence tolerance on the change in receive time.
352    ///   A typical value for high-precision work is `Dt::from_ns(1, Scale::TAI)`.
353    /// * `max_iter` — Maximum number of iterations before falling back. A value of
354    ///   12–20 is usually sufficient for solar-system geometries.
355    ///
356    /// # Returns
357    ///
358    /// A tuple containing:
359    /// * `rel_correction` — The final relativistic correction (clock drift + Shapiro)
360    ///   evaluated at convergence.
361    /// * `rx_time` — The converged receive time in the coordinate scale of the
362    ///   transmitter.
363    /// * `final_state` — The receiver state at the converged receive time.
364    ///
365    /// # Examples
366    ///
367    /// ```rust,ignore
368    /// use deep_time::{Dt, LightContext, ObserverState, Scale};
369    ///
370    /// # let tx = /* transmitter state */;
371    /// let tolerance = Dt::from_ns(1, Scale::TAI);
372    ///
373    /// let (correction, rx_time, rx_state) = tx.iterative_one_way_relativistic_delay_to(
374    ///     &mut |t| {
375    ///         // Example: call into SPICE or your own propagator
376    ///         get_receiver_state_at(t)
377    ///     },
378    ///     LightContext::SOLAR,
379    ///     tolerance,
380    ///     20,
381    /// );
382    ///
383    /// // The total light time is:
384    /// let total_light_time = rx_time.to_diff_raw(tx.time);
385    /// ```
386    ///
387    /// # References
388    ///
389    /// * Moyer, T.D. (2003). *Formulation for Observed and Computed Values of
390    ///   Deep Space Network Data Types for Navigation*. JPL DESCANSO Vol. 2,
391    ///   Section 8 (Light-Time Solution).
392    /// * IAU Resolution B1.5 (2000) and subsequent updates on relativistic
393    ///   reference systems and time scales.
394    /// * Ashby, N. (2003). "Relativity in the Global Positioning System".
395    ///   *Living Reviews in Relativity*.
396    ///
397    /// # Notes
398    ///
399    /// The solver uses a simple fixed-point iteration. For most solar-system
400    /// geometries convergence occurs within 3–6 iterations. The function always
401    /// returns a result; if `max_iter` is reached, the last computed values are
402    /// returned. The returned `ObserverState` is guaranteed to be consistent with
403    /// the final `rx_time`.
404    pub fn iterative_one_way_relativistic_delay_to<F>(
405        &self,
406        rx_provider: &mut F,
407        context: LightContext,
408        tolerance: Dt,
409        max_iter: usize,
410    ) -> (Dt, Dt, ObserverState)
411    where
412        F: FnMut(Dt) -> ObserverState,
413    {
414        // Initial geometric guess
415        let initial_rx = rx_provider(self.time);
416        let initial_r_sep = self.position.distance_to(initial_rx.position);
417        let initial_geometric = Dt::from_sec_f(initial_r_sep / C);
418
419        let mut rx_time = self.time.add(initial_geometric);
420        let mut rel_correction = Dt::ZERO;
421
422        for _ in 0..max_iter {
423            let rx = rx_provider(rx_time);
424
425            rel_correction = self.one_way_relativistic_delay_to(rx, context);
426
427            let r_sep = self.position.distance_to(rx.position);
428            let geometric = Dt::from_sec_f(r_sep / C);
429            let full_delay = geometric.add(rel_correction);
430
431            let new_rx_time = self.time.add(full_delay);
432            let change = new_rx_time.to_diff_raw(rx_time);
433
434            rx_time = new_rx_time;
435
436            if change.abs() < tolerance {
437                return (rel_correction, rx_time, rx);
438            }
439        }
440
441        // Fallback after max iterations
442        let final_rx = rx_provider(rx_time);
443        (rel_correction, rx_time, final_rx)
444    }
445
446    /// Computes the one-way relativistic correction using the standard
447    /// endpoint-differential model with optional path sampling.
448    ///
449    /// This function implements the modern formulation used by deep-space
450    /// navigation systems (Moyer 2003 / DSN standard). It returns the total
451    /// correction that must be added to the Newtonian geometric light time
452    /// `|r_rx − r_tx| / c`.
453    ///
454    /// The correction consists of two physically distinct contributions:
455    /// - **Differential clock-rate correction**: The difference in accumulated
456    ///   proper time between the transmitter (at transmission) and receiver
457    ///   (at reception) over the coordinate time span, computed from the
458    ///   library’s unified master-Lagrangian model.
459    /// - **Gravitational (Shapiro) delay**: The extra coordinate time required
460    ///   for the signal to propagate near a massive body, evaluated using the
461    ///   supplied [`LightContext`].
462    ///
463    /// When a non-empty `samples` slice is provided, the first element is used
464    /// as the effective transmitter state and the last element as the effective
465    /// receiver state. This allows callers to supply a high-resolution model
466    /// of how the local spacetime (gravitational potential and velocity) varies
467    /// along the straight-line path, yielding more accurate endpoint rates when
468    /// the environment changes significantly between transmission and reception.
469    ///
470    /// If fewer than two samples are supplied, the function falls back to the
471    /// direct endpoint evaluation using the provided `rx` state.
472    ///
473    /// # Parameters
474    ///
475    /// * `rx` — The receiver state evaluated at an approximate arrival time.
476    ///   This state is used for the Shapiro calculation and as a fallback when
477    ///   no samples are provided.
478    /// * `context` — Controls the gravitational contribution via the Shapiro
479    ///   delay formula. Use [`LightContext::SOLAR`] for solar-system work or
480    ///   [`LightContext::from_grav_param`] for custom central bodies.
481    /// * `samples` — Optional sequence of [`Spacetime`] states sampled
482    ///   along the straight-line path from transmitter to receiver. When
483    ///   non-empty, the first sample defines the transmitter clock rate and
484    ///   the last sample defines the receiver clock rate.
485    ///
486    /// # Returns
487    ///
488    /// The total relativistic correction (`Dt`) to be added to the Newtonian
489    /// geometric light time.
490    ///
491    /// # Examples
492    ///
493    /// ```rust,ignore
494    /// use deep_time::{Dt, LightContext, Spacetime, ObserverState};
495    ///
496    /// # let tx = /* transmitter state */;
497    /// # let rx_approx = /* approximate receiver state */;
498    ///
499    /// // Basic usage (no path sampling)
500    /// let correction = tx.one_way_relativistic_delay_integrated(
501    ///     rx_approx,
502    ///     LightContext::SOLAR,
503    ///     &[],
504    /// );
505    ///
506    /// // High-fidelity usage with path sampling
507    /// let path_samples: Vec<Spacetime> = /* sample along the light path */;
508    /// let correction = tx.one_way_relativistic_delay_integrated(
509    ///     rx_approx,
510    ///     LightContext::SOLAR,
511    ///     &path_samples,
512    /// );
513    /// ```
514    ///
515    /// # References
516    ///
517    /// * Moyer, T.D. (2003). *Formulation for Observed and Computed Values of
518    ///   Deep Space Network Data Types for Navigation*. JPL DESCANSO Vol. 2,
519    ///   Sections 8 and 11.
520    /// * IAU Resolution B1.5 (2000) and subsequent updates on relativistic
521    ///   reference systems.
522    /// * Ashby, N. (2003). "Relativity in the Global Positioning System".
523    ///   *Living Reviews in Relativity*.
524    ///
525    /// # Notes
526    ///
527    /// This function always uses the **differential** proper-time accumulation
528    /// between the effective transmitter and receiver states. It does **not**
529    /// perform an absolute integration of `(dτ/dt − 1)` along the path.
530    /// The Shapiro delay term is always computed from the endpoint positions
531    /// using the analytic logarithmic formula.
532    pub const fn one_way_relativistic_delay_integrated(
533        &self,
534        rx: ObserverState,
535        context: LightContext,
536        samples: &[Spacetime],
537    ) -> Dt {
538        if samples.len() < 2 {
539            return self.one_way_relativistic_delay_to(rx, context);
540        }
541
542        // Effective transmitter drift from first sample (or self if you prefer)
543        let tx_local = samples[0];
544        let tx_drift = Drift::from_spacetime(&tx_local);
545
546        // Effective receiver drift from last sample (path-informed rx rate)
547        let rx_local = samples[samples.len() - 1];
548        let rx_drift = Drift::from_spacetime(&rx_local);
549
550        let span = rx.time.to_diff_raw(self.time);
551        let drift_correction = rx_drift
552            .time_diff_after(&span)
553            .sub(tx_drift.time_diff_after(&span));
554
555        let r_tx = self.position.norm();
556        let r_rx = rx.position.norm();
557        let r_sep = self.position.distance_to(rx.position);
558        let shapiro = Self::shapiro_one_way_delay(context, r_tx, r_rx, r_sep);
559
560        drift_correction.add(shapiro)
561    }
562
563    /// Computes the total relativistic correction for a two-way round-trip
564    /// ranging measurement by independently solving the uplink and downlink
565    /// legs using the full iterative light-time solver.
566    ///
567    /// This function implements the modern formulation recommended by
568    /// Moyer (2003) and used by deep-space networks (DSN, ESA, JPL) for
569    /// high-accuracy two-way ranging. It solves the uplink leg (transmitter
570    /// to remote body) and the downlink leg (remote body back to transmitter)
571    /// as two separate one-way problems, each with its own iterative solution.
572    /// This approach is more accurate than older combined round-trip formulations
573    /// when the transmitter and receiver are in different gravitational
574    /// environments or moving at different velocities.
575    ///
576    /// The returned correction must be **subtracted** from the raw measured
577    /// round-trip time to recover the geometric light time.
578    ///
579    /// # Parameters
580    ///
581    /// * `rx_provider` — Closure that returns the relativistic state of the
582    ///   remote body (planet, spacecraft, etc.) at a given coordinate time.
583    ///   This is used for both the uplink solution and to obtain the accurate
584    ///   state at uplink arrival for the downlink leg.
585    /// * `tx_provider` — Closure that returns the relativistic state of the
586    ///   local transmitter at a given coordinate time. This is used for the
587    ///   downlink leg and may represent a moving Earth station or another
588    ///   spacecraft.
589    /// * `context` — Controls the gravitational (Shapiro) contribution for
590    ///   both legs. Use [`LightContext::SOLAR`] for solar-system work.
591    /// * `tolerance` — Convergence tolerance passed to the underlying
592    ///   iterative solver (recommended: `Dt::from_ns(1, Scale::TAI)`).
593    /// * `max_iter` — Maximum iterations for each leg (typically 12–20).
594    ///
595    /// # Returns
596    ///
597    /// The total relativistic correction (`Dt`) for the complete round-trip.
598    /// This value should be subtracted from the observed round-trip time.
599    ///
600    /// # Examples
601    ///
602    /// ```rust,ignore
603    /// use deep_time::{Dt, LightContext, Scale};
604    ///
605    /// # let earth_station = /* local transmitter state */;
606    /// let tolerance = Dt::from_ns(1, Scale::TAI);
607    ///
608    /// let correction = earth_station.round_trip_relativistic_correction(
609    ///     &mut |t| get_spacecraft_state(t),      // remote body
610    ///     &mut |t| get_earth_station_state(t),   // local transmitter
611    ///     LightContext::SOLAR,
612    ///     tolerance,
613    ///     15,
614    /// );
615    ///
616    /// // Geometric light time = measured_round_trip_time - correction
617    /// ```
618    ///
619    /// # References
620    ///
621    /// * Moyer, T.D. (2003). *Formulation for Observed and Computed Values of
622    ///   Deep Space Network Data Types for Navigation*. JPL DESCANSO Vol. 2,
623    ///   Sections 8 and 11 (Two-Way Light Time).
624    /// * IAU Resolution B1.5 (2000) and updates on relativistic reference systems.
625    /// * Modern DSN and ESA ranging implementations (post-2005).
626    ///
627    /// # Notes
628    ///
629    /// This function performs two independent iterative solutions. The downlink
630    /// leg uses the precise receiver state obtained at the end of the uplink
631    /// solution, ensuring consistency. The method is suitable for Earth–Mars,
632    /// Jupiter, Kuiper Belt, and interstellar-class distances.
633    pub fn round_trip_relativistic_correction<RxF, TxF>(
634        &self,
635        mut rx_provider: RxF, // remote body (planet, spacecraft, etc.)
636        mut tx_provider: TxF, // local transmitter for the return leg (can move)
637        context: LightContext,
638        tolerance: Dt,
639        max_iter: usize,
640    ) -> Dt
641    where
642        RxF: FnMut(Dt) -> ObserverState,
643        TxF: FnMut(Dt) -> ObserverState,
644    {
645        // Uplink leg: transmitter → receiver
646        let (uplink_rel, rx_time, _rx_state) = self.iterative_one_way_relativistic_delay_to(
647            &mut rx_provider,
648            context,
649            tolerance,
650            max_iter,
651        );
652
653        // Downlink leg: receiver → transmitter
654        let return_tx = rx_provider(rx_time); // accurate state at uplink arrival
655
656        let (downlink_rel, _return_rx_time, _return_rx_state) = return_tx
657            .iterative_one_way_relativistic_delay_to(
658                &mut tx_provider,
659                context,
660                tolerance,
661                max_iter,
662            );
663
664        uplink_rel.add(downlink_rel)
665    }
666
667    /// First-order one-way Shapiro delay (gravitational light-time delay) caused by a
668    /// central point mass.
669    ///
670    /// This is an internal helper used by the public delay functions. It implements the
671    /// standard logarithmic formula used in solar-system navigation and pulsar timing.
672    const fn shapiro_one_way_delay(
673        context: LightContext,
674        r_tx: Real,
675        r_rx: Real,
676        r_sep: Real,
677    ) -> Dt {
678        if context.two_grav_param_over_c3 == f!(0.0)
679            || r_tx <= f!(0.0)
680            || r_rx <= f!(0.0)
681            || r_sep <= f!(0.0)
682        {
683            return Dt::ZERO;
684        }
685
686        let arg = (r_tx + r_rx + r_sep) / (r_tx + r_rx - r_sep).max(f!(1.0));
687        let delay_sec = context.two_grav_param_over_c3 * log(arg);
688
689        Dt::from_sec_f(delay_sec)
690    }
691}