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}