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
5impl Dt {
6 /// Shapiro gravitational time scale for the Sun (`2 G M_☉ / c³`).
7 ///
8 /// This is the recommended value to pass as the `shapiro` parameter to
9 /// [`ObserverState::one_way_relativistic_delay`] (and [`ObserverState::shapiro_delay_to`])
10 /// for all normal solar-system work.
11 ///
12 /// It corresponds to the one-way Shapiro coefficient for the Sun.
13 pub const SHAPIRO_SOLAR: Self = Self::from_sec_f(TWO_GM_SUN_OVER_C3);
14
15 /// Creates the Shapiro delay scale for an arbitrary central body
16 /// from its standard gravitational parameter `GM` (μ) in m³ s⁻².
17 ///
18 /// This produces the coefficient used in the Shapiro gravitational time delay
19 /// formula. It is the recommended way to create a custom Shapiro scale for
20 /// planets, stars, or other massive bodies.
21 ///
22 /// Pass the resulting value to [`ObserverState::one_way_relativistic_delay`]
23 /// or [`ObserverState::shapiro_delay_to`].
24 #[inline]
25 pub const fn shapiro_from_grav_param(gm: Real) -> Self {
26 let secs = 2.0 * gm / (C * C_SQUARED);
27 Self::from_sec_f(secs)
28 }
29
30 /// Creates an [`ObserverState`] using this time value along with the
31 /// provided position, velocity, and gravitational information.
32 ///
33 /// An `ObserverState` represents a complete snapshot of an observer
34 /// (spacecraft, ground station, planet, etc.) at a specific moment.
35 /// It bundles together the time, position, velocity, and local
36 /// gravitational environment so that relativistic calculations
37 /// (light time, clock rates, Shapiro delay, etc.) can be performed.
38 ///
39 /// This method is a convenience constructor. It is useful when you
40 /// already have a [`Dt`] (a time value) and want to build an
41 /// `ObserverState` directly from it, rather than calling
42 /// [`ObserverState::new`] or [`ObserverState::new_strong_field`].
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 /// # When to use this method
59 ///
60 /// Use this method when you already have a time value as a [`Dt`] and
61 /// want to construct an `ObserverState` in one step. It is especially
62 /// convenient when working with time values that were previously
63 /// computed or converted.
64 ///
65 /// For most normal use, [`ObserverState::new`] is simpler. Use
66 /// [`ObserverState::new_strong_field`] instead if you need to specify
67 /// a non-zero `characteristic_length_scale`.
68 ///
69 /// # Example
70 /// ```ignore
71 /// let t = Dt::from_sec(1234.5);
72 ///
73 /// let state = t.to_observer_state(
74 /// position,
75 /// velocity,
76 /// grav_potential,
77 /// 0.0, // normal solar-system use
78 /// );
79 /// ```
80 #[inline]
81 pub const fn to_observer_state(
82 self,
83 position: Position,
84 velocity: Velocity,
85 grav_potential_m2_s2: Real,
86 characteristic_length_scale: Real,
87 ) -> ObserverState {
88 ObserverState {
89 time: self,
90 position,
91 velocity,
92 grav_potential_m2_s2,
93 characteristic_length_scale,
94 }
95 }
96}
97
98/// A snapshot of an observer’s relativistic state at a specific instant.
99///
100/// `ObserverState` combines time, position, velocity, and local gravitational
101/// information. It is the main input type used by relativistic light-time
102/// methods in this library.
103#[derive(Clone, Copy, Debug, PartialEq)]
104#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105#[cfg_attr(feature = "js", derive(tsify::Tsify))]
106pub struct ObserverState {
107 /// The time of this state.
108 ///
109 /// Any [`Scale`] is accepted. This time is treated as coordinate time
110 /// for light-time calculations.
111 pub time: Dt,
112
113 /// Position of the observer in meters.
114 ///
115 /// Typically expressed in a barycentric (solar-system barycenter) or
116 /// heliocentric frame, depending on the application.
117 pub position: Position,
118
119 /// Velocity of the observer in meters per second.
120 pub velocity: Velocity,
121
122 /// Newtonian gravitational potential Φ at the observer’s location
123 /// (in m² s⁻²).
124 ///
125 /// This value is usually negative for bound orbits. It should normally
126 /// include contributions from the Sun and all relevant planets.
127 pub grav_potential_m2_s2: Real,
128
129 /// Characteristic length scale (in meters) over which the gravitational
130 /// field varies significantly at this location.
131 ///
132 /// - Use `0.0` (the default) for all solar-system, GNSS, and weak-field
133 /// applications.
134 /// - Provide a non-zero value only when working in strong gravitational
135 /// fields (e.g. near neutron stars or black holes), where the library’s
136 /// higher-order curvature terms become relevant.
137 pub characteristic_length_scale: Real,
138}
139
140impl ObserverState {
141 /// Creates a new `ObserverState` for typical solar-system, GNSS,
142 /// or weak-field use.
143 ///
144 /// This is the recommended constructor for most applications.
145 /// It sets the `characteristic_length_scale` to `0.0`, which disables
146 /// higher-order curvature terms in the proper-time model.
147 ///
148 /// # Parameters
149 /// - `time`: The time of the state.
150 /// - `position`: Position in meters (usually barycentric or heliocentric).
151 /// - `velocity`: Velocity in m/s.
152 /// - `grav_potential_m2_s2`: Newtonian gravitational potential Φ
153 /// at the location (in m²/s²).
154 #[inline]
155 pub const fn new(
156 time: Dt,
157 position: Position,
158 velocity: Velocity,
159 grav_potential_m2_s2: Real,
160 ) -> Self {
161 Self {
162 time,
163 position,
164 velocity,
165 grav_potential_m2_s2,
166 characteristic_length_scale: 0.0,
167 }
168 }
169
170 /// Creates a new `ObserverState` when strong-field effects or a
171 /// non-zero characteristic length scale are relevant.
172 ///
173 /// Use this constructor when you have gravimeter data or are working
174 /// in regions where spacetime curvature varies significantly over
175 /// short distances (e.g. near compact objects). The
176 /// `characteristic_length_scale` parameter controls whether the
177 /// library activates higher-order terms in the proper-time calculation.
178 ///
179 /// For normal solar-system work, use [`Self::new`] instead.
180 ///
181 /// # Parameters
182 /// - `time`: The time of the state.
183 /// - `position`: Position in meters.
184 /// - `velocity`: Velocity in m/s.
185 /// - `grav_potential_m2_s2`: Newtonian gravitational potential Φ
186 /// at the location (in m²/s²).
187 /// - `characteristic_length_scale`: Length scale (in meters) over which
188 /// gravity varies at this location. Must be positive to have an effect.
189 #[inline]
190 pub const fn new_strong_field(
191 time: Dt,
192 position: Position,
193 velocity: Velocity,
194 grav_potential_m2_s2: Real,
195 characteristic_length_scale: Real,
196 ) -> Self {
197 Self {
198 time,
199 position,
200 velocity,
201 grav_potential_m2_s2,
202 characteristic_length_scale,
203 }
204 }
205
206 /// Returns the instantaneous proper-time rate `dτ/dt` for this observer.
207 ///
208 /// This value indicates how fast a physical clock located at this state
209 /// would advance relative to the time used by this `ObserverState`.
210 /// A returned value of `1.0` means the clock advances at the same rate
211 /// as the state's time coordinate. Values are typically slightly different
212 /// from `1.0` due to the effects of velocity and gravitational potential.
213 ///
214 /// This rate is computed using the library’s unified proper-time model.
215 /// It is used internally for light-time corrections and Doppler calculations.
216 #[inline]
217 pub const fn proper_time_rate(&self) -> Real {
218 Spacetime::from_potential_velocity_and_scale(
219 self.grav_potential_m2_s2 / C_SQUARED,
220 self.velocity,
221 self.characteristic_length_scale,
222 )
223 .proper_time_rate()
224 }
225
226 /// Returns the ratio of proper time rates between the receiver and transmitter
227 /// for a one-way signal.
228 ///
229 /// This method computes:
230 ///
231 /// ```text
232 /// ratio = rx.proper_time_rate() / self.proper_time_rate()
233 /// ```
234 ///
235 /// ### Interpretation
236 ///
237 /// - A value of `1.0` indicates that both clocks run at the same rate.
238 /// - A value **less than `1.0`** means the receiver’s clock runs slower than
239 /// the transmitter’s clock. The receiver will observe a lower frequency
240 /// than was emitted.
241 /// - A value **greater than `1.0`** means the receiver’s clock runs faster
242 /// than the transmitter’s clock. The receiver will observe a higher frequency
243 /// than was emitted.
244 ///
245 /// The ratio captures the combined effect of special-relativistic time dilation
246 /// (due to velocity) and general-relativistic gravitational time dilation.
247 ///
248 /// ### Typical Usage (One-Way)
249 ///
250 /// This ratio is often combined with the classical kinematic Doppler term
251 /// to estimate the total one-way frequency shift:
252 ///
253 /// ```text
254 /// approximate_frequency_shift ≈ ratio * (1 - v_radial / C)
255 /// ```
256 ///
257 /// where `v_radial` is the radial velocity (positive when the receiver is
258 /// receding).
259 ///
260 /// ### Two-Way Usage
261 ///
262 /// For round-trip (two-way) measurements, square the one-way ratio:
263 ///
264 /// ```rust,ignore
265 /// let one_way_ratio = transmitter.relativistic_clock_rate_ratio(receiver);
266 /// let two_way_ratio = one_way_ratio * one_way_ratio;
267 /// ```
268 ///
269 /// This pattern is commonly used when correcting two-way Doppler (range-rate)
270 /// data for relativistic clock effects.
271 ///
272 /// ### Limitations
273 ///
274 /// - This method only accounts for the **difference in clock rates** between
275 /// the two ends.
276 /// - It does **not** include Shapiro delay or higher-order relativistic effects
277 /// on signal propagation.
278 /// - The combination with classical Doppler shown above is a first-order
279 /// approximation.
280 ///
281 /// # Parameters
282 /// - `self` — Transmitter state at the time of transmission.
283 /// - `rx` — Receiver state at the approximate time of reception.
284 ///
285 /// # Example
286 /// ```rust,ignore
287 /// let ratio = transmitter.relativistic_clock_rate_ratio(receiver);
288 ///
289 /// let v_radial = ...; // m/s, positive if receding
290 /// let classical_doppler = 1.0 - v_radial / C;
291 ///
292 /// let approx_frequency_shift = ratio * classical_doppler;
293 /// ```
294 #[inline]
295 pub const fn relativistic_clock_rate_ratio(&self, rx: ObserverState) -> Real {
296 rx.proper_time_rate() / self.proper_time_rate()
297 }
298
299 /// Computes the additional delay that must be added to the Newtonian
300 /// geometric light time `|r_rx − r_tx| / c`.
301 ///
302 /// In simple terms, this method calculates the extra time delay caused by
303 /// differences in clock rates between the transmitter and receiver (due to
304 /// their relativistic states, including velocity and gravity) plus the
305 /// gravitational Shapiro delay.
306 ///
307 /// The returned value is a **hybrid correction** consisting of two parts:
308 ///
309 /// - The difference in accumulated proper time between the transmitter and
310 /// receiver (clock-rate effects).
311 /// - The gravitational (Shapiro) delay caused by spacetime curvature near
312 /// a central mass.
313 ///
314 /// This is the primary method for relativistic light-time calculations.
315 ///
316 /// ### Optional Endpoint States
317 ///
318 /// You may optionally supply a slice of [`Spacetime`] samples. When two or
319 /// more samples are provided, only the first and last elements are used:
320 /// the first becomes the effective transmitter state, and the last becomes
321 /// the effective receiver state for the clock-rate correction.
322 ///
323 /// This is useful when you have more accurate information about the local
324 /// spacetime conditions exactly at transmission and reception.
325 ///
326 /// If fewer than two samples are provided, the method falls back to using
327 /// `self` and `rx` directly.
328 ///
329 /// **Note**: This is an endpoint-based model. It does **not** perform
330 /// numerical integration of proper time along the signal path.
331 ///
332 /// ### Custom Shapiro / Propagation Delay
333 ///
334 /// You can provide your own delay value via `custom_shapiro_delay`. When
335 /// supplied, this value is used directly instead of the internally computed
336 /// Shapiro delay.
337 ///
338 /// This allows you to pass:
339 /// - A delay computed with a different Shapiro model
340 /// - A delay that includes additional propagation effects (such as solar plasma)
341 /// - A delay obtained from an external calculation
342 ///
343 /// # Parameters
344 ///
345 /// * `rx` — Receiver state at the approximate arrival time.
346 /// * `shapiro` — Shapiro scale factor. Use [`Dt::SHAPIRO_SOLAR`] for normal
347 /// solar-system work.
348 /// * `samples` — Optional [`Spacetime`] samples. Only the first and last
349 /// are used when provided.
350 /// * `custom_shapiro_delay` — Optional custom delay to use instead of the
351 /// standard Shapiro calculation.
352 ///
353 /// # Returns
354 ///
355 /// The total relativistic correction to be added to the geometric light time.
356 ///
357 /// # Example
358 ///
359 /// ```rust,ignore
360 /// // Basic usage
361 /// let correction = tx.one_way_relativistic_delay(
362 /// rx_approx,
363 /// Dt::SHAPIRO_SOLAR,
364 /// &[],
365 /// None,
366 /// );
367 ///
368 /// // With custom endpoint states and a custom delay
369 /// let path_samples = vec![tx_spacetime, rx_spacetime];
370 /// let custom_delay = compute_custom_delay(...);
371 ///
372 /// let correction = tx.one_way_relativistic_delay(
373 /// rx_approx,
374 /// Dt::SHAPIRO_SOLAR,
375 /// &path_samples,
376 /// Some(custom_delay),
377 /// );
378 /// ```
379 pub const fn one_way_relativistic_delay(
380 &self,
381 rx: ObserverState,
382 shapiro: Dt,
383 samples: &[Spacetime],
384 custom_shapiro_delay: Option<Dt>,
385 ) -> Dt {
386 // Compute the differential clock-rate correction
387 let drift_correction = if samples.len() >= 2 {
388 let tx_local = samples[0];
389 let tx_drift = Drift::from_spacetime(&tx_local);
390
391 let rx_local = samples[samples.len() - 1];
392 let rx_drift = Drift::from_spacetime(&rx_local);
393
394 let span = rx.time.to_diff_raw(self.time);
395
396 rx_drift
397 .time_diff_after(&span)
398 .sub(tx_drift.time_diff_after(&span))
399 } else {
400 // Fallback to using self and rx directly
401 self.compute_differential_clock_correction(rx)
402 };
403
404 // Determine which Shapiro value to use
405 let shapiro_delay = match custom_shapiro_delay {
406 Some(custom) => custom,
407 None => self.shapiro_delay_to(rx, shapiro),
408 };
409
410 drift_correction.add(shapiro_delay)
411 }
412
413 /// Iteratively solves the one-way light-time equation including relativistic
414 /// corrections until the receive time converges to the requested tolerance.
415 ///
416 /// This is the recommended high-precision solver for one-way light-time
417 /// computations in modern deep-space navigation. It follows the formulation
418 /// described in Moyer (2003) and works with any ephemeris source (SPICE
419 /// kernels, numerical integrators, or analytical propagators).
420 ///
421 /// The solver performs a fixed-point iteration on the light-time equation:
422 ///
423 /// ```text
424 /// t_rx = t_tx + |r_rx(t_rx) − r_tx(t_tx)| / c + Δt_rel(t_tx, t_rx)
425 /// ```
426 ///
427 /// where `Δt_rel` is the relativistic correction returned by
428 /// [`one_way_relativistic_delay`]. The iteration continues until the change
429 /// in the estimated receive time falls below `tolerance`.
430 ///
431 /// # Parameters
432 ///
433 /// * `rx_provider` — A mutable closure that returns the full relativistic
434 /// state of the receiver at a given coordinate time. This allows the solver
435 /// to work with any ephemeris or propagator without requiring a specific
436 /// data structure.
437 /// * `shapiro` — Controls the gravitational (Shapiro) contribution. Use
438 /// [`Dt::SHAPIRO_SOLAR`] for solar-system work or
439 /// [`Dt::shapiro_from_grav_param`] for other central bodies.
440 /// * `tolerance` — Convergence tolerance on the change in receive time.
441 /// A typical value for high-precision work is `Dt::from_ns(1, Scale::TAI)`.
442 /// * `max_iter` — Maximum number of iterations before falling back. A value
443 /// of 12–20 is usually sufficient for solar-system geometries.
444 ///
445 /// # Returns
446 ///
447 /// A tuple containing:
448 /// * `rel_correction` — The final relativistic correction (clock-rate
449 /// correction + Shapiro) evaluated at convergence.
450 /// * `rx_time` — The converged receive time in the coordinate scale of
451 /// the transmitter.
452 /// * `final_state` — The receiver state at the converged receive time.
453 ///
454 /// # Examples
455 ///
456 /// ```rust,ignore
457 /// use deep_time::{Dt, ObserverState, Scale};
458 ///
459 /// # let tx = /* transmitter state */;
460 /// let tolerance = Dt::from_ns(1, Scale::TAI);
461 ///
462 /// let (correction, rx_time, rx_state) = tx.iterative_one_way_relativistic_delay_to(
463 /// &mut |t| {
464 /// // Example: call into SPICE or your own propagator
465 /// get_receiver_state_at(t)
466 /// },
467 /// Dt::SHAPIRO_SOLAR,
468 /// tolerance,
469 /// 20,
470 /// );
471 ///
472 /// // The total light time is:
473 /// let total_light_time = rx_time.to_diff_raw(tx.time);
474 /// ```
475 ///
476 /// # References
477 ///
478 /// * Moyer, T.D. (2003). *Formulation for Observed and Computed Values of
479 /// Deep Space Network Data Types for Navigation*. JPL DESCANSO Vol. 2,
480 /// Section 8 (Light-Time Solution).
481 /// * IAU Resolution B1.5 (2000) and subsequent updates on relativistic
482 /// reference systems and time scales.
483 /// * Ashby, N. (2003). "Relativity in the Global Positioning System".
484 /// *Living Reviews in Relativity*.
485 ///
486 /// # Notes
487 ///
488 /// The solver uses simple fixed-point iteration. For most solar-system
489 /// geometries, convergence occurs within 3–6 iterations. The function always
490 /// returns a result. If `max_iter` is reached, the last computed values are
491 /// returned. The returned `ObserverState` is guaranteed to be consistent with
492 /// the final `rx_time`.
493 pub fn iterative_one_way_relativistic_delay_to<F>(
494 &self,
495 rx_provider: &mut F,
496 shapiro: Dt,
497 tolerance: Dt,
498 max_iter: usize,
499 ) -> (Dt, Dt, ObserverState)
500 where
501 F: FnMut(Dt) -> ObserverState,
502 {
503 // Initial geometric guess
504 let initial_rx = rx_provider(self.time);
505 let initial_r_sep = self.position.distance_to(initial_rx.position);
506 let initial_geometric = Dt::from_sec_f(initial_r_sep / C);
507
508 let mut rx_time = self.time.add(initial_geometric);
509 let mut rel_correction = Dt::ZERO;
510
511 for _ in 0..max_iter {
512 let rx = rx_provider(rx_time);
513
514 rel_correction = self.one_way_relativistic_delay(rx, shapiro, &[], None);
515
516 let r_sep = self.position.distance_to(rx.position);
517 let geometric = Dt::from_sec_f(r_sep / C);
518 let full_delay = geometric.add(rel_correction);
519
520 let new_rx_time = self.time.add(full_delay);
521 let change = new_rx_time.to_diff_raw(rx_time);
522
523 rx_time = new_rx_time;
524
525 if change.abs() < tolerance {
526 return (rel_correction, rx_time, rx);
527 }
528 }
529
530 // Fallback after max iterations
531 let final_rx = rx_provider(rx_time);
532 (rel_correction, rx_time, final_rx)
533 }
534
535 /// Computes the total relativistic correction for a two-way round-trip
536 /// ranging measurement.
537 ///
538 /// This method solves the uplink and downlink legs **independently** using
539 /// the iterative light-time solver. This is the modern approach recommended
540 /// by Moyer (2003) and used by deep-space networks (DSN, ESA, JPL).
541 ///
542 /// Solving the legs separately is more accurate than older combined
543 /// round-trip formulas when the two ends are in different gravitational
544 /// environments or have significantly different velocities.
545 ///
546 /// The returned value must be **subtracted** from the raw measured
547 /// round-trip time to recover the geometric light time.
548 ///
549 /// # Parameters
550 ///
551 /// * `rx_provider` — Closure that returns the relativistic state of the
552 /// remote body (planet, spacecraft, etc.) at a given coordinate time.
553 /// Used for both the uplink solution and to obtain the accurate state
554 /// at uplink arrival for the downlink leg.
555 /// * `tx_provider` — Closure that returns the relativistic state of the
556 /// local transmitter at a given coordinate time (e.g. a moving ground
557 /// station or another spacecraft). Used for the downlink leg.
558 /// * `shapiro` — Shapiro scale factor applied to both legs. Use
559 /// [`Dt::SHAPIRO_SOLAR`] for normal solar-system work.
560 /// * `tolerance` — Convergence tolerance for the underlying iterative
561 /// solver (recommended: `Dt::from_ns(1, Scale::TAI)`).
562 /// * `max_iter` — Maximum iterations per leg (typically 12–20).
563 ///
564 /// # Returns
565 ///
566 /// The total relativistic correction for the complete round trip.
567 /// This value should be subtracted from the observed round-trip time.
568 ///
569 /// # Examples
570 ///
571 /// ```rust,ignore
572 /// use deep_time::{Dt, Scale};
573 ///
574 /// # let earth_station = /* local transmitter state */;
575 /// let tolerance = Dt::from_ns(1, Scale::TAI);
576 ///
577 /// let correction = earth_station.round_trip_relativistic_correction(
578 /// &mut |t| get_spacecraft_state(t), // remote body
579 /// &mut |t| get_earth_station_state(t), // local transmitter
580 /// Dt::SHAPIRO_SOLAR,
581 /// tolerance,
582 /// 15,
583 /// );
584 ///
585 /// // Geometric light time = measured_round_trip_time - correction
586 /// ```
587 ///
588 /// # References
589 ///
590 /// * Moyer, T.D. (2003). *Formulation for Observed and Computed Values of
591 /// Deep Space Network Data Types for Navigation*. JPL DESCANSO Vol. 2,
592 /// Sections 8 and 11 (Two-Way Light Time).
593 /// * IAU Resolution B1.5 (2000) and updates on relativistic reference systems.
594 /// * Modern DSN and ESA ranging implementations.
595 ///
596 /// # Notes
597 ///
598 /// This method performs two independent iterative solutions. The downlink
599 /// leg uses the precise receiver state obtained at the end of the uplink
600 /// solution, ensuring consistency between the two legs.
601 ///
602 /// The method is suitable for Earth–Mars, Jupiter, Kuiper Belt, and
603 /// interstellar-class distances.
604 pub fn round_trip_relativistic_correction<RxF, TxF>(
605 &self,
606 mut rx_provider: RxF, // remote body (planet, spacecraft, etc.)
607 mut tx_provider: TxF, // local transmitter for the return leg (can move)
608 shapiro: Dt,
609 tolerance: Dt,
610 max_iter: usize,
611 ) -> Dt
612 where
613 RxF: FnMut(Dt) -> ObserverState,
614 TxF: FnMut(Dt) -> ObserverState,
615 {
616 // Uplink leg: transmitter → receiver
617 let (uplink_rel, rx_time, _rx_state) = self.iterative_one_way_relativistic_delay_to(
618 &mut rx_provider,
619 shapiro,
620 tolerance,
621 max_iter,
622 );
623
624 // Downlink leg: receiver → transmitter
625 let return_tx = rx_provider(rx_time); // accurate state at uplink arrival
626
627 let (downlink_rel, _return_rx_time, _return_rx_state) = return_tx
628 .iterative_one_way_relativistic_delay_to(
629 &mut tx_provider,
630 shapiro,
631 tolerance,
632 max_iter,
633 );
634
635 uplink_rel.add(downlink_rel)
636 }
637
638 /// Computes **only** the gravitational (Shapiro) delay for a one-way signal.
639 ///
640 /// This method returns the classical coordinate-time Shapiro delay caused by
641 /// spacetime curvature near a central mass. It deliberately excludes any
642 /// differential proper-time (clock-rate) correction.
643 ///
644 /// Use this method when you need only the traditional relativistic propagation
645 /// delay used in most deep-space navigation, ranging, and orbit determination
646 /// systems (consistent with Moyer 2003 DSN formulations).
647 ///
648 /// If you need both the Shapiro delay **and** the differential clock-rate
649 /// correction, use [`Self::one_way_relativistic_delay`] instead.
650 ///
651 /// # Parameters
652 /// - `rx` — Receiver state at the approximate arrival time.
653 /// - `shapiro` — Shapiro scale factor. Use [`Dt::SHAPIRO_SOLAR`] for normal
654 /// solar-system work or [`Dt::shapiro_from_grav_param`] for other central
655 /// bodies. Pass `Dt::ZERO` to disable the Shapiro contribution.
656 ///
657 /// # Returns
658 /// The Shapiro delay (`Dt`) to be added to the Newtonian geometric light time.
659 ///
660 /// # Example
661 /// ```rust,ignore
662 /// let shapiro_correction = transmitter.shapiro_delay_to(receiver_approx, Dt::SHAPIRO_SOLAR);
663 /// ```
664 #[inline]
665 pub const fn shapiro_delay_to(&self, rx: ObserverState, shapiro: Dt) -> Dt {
666 let r_tx = self.position.norm();
667 let r_rx = rx.position.norm();
668 let r_sep = self.position.distance_to(rx.position);
669 Self::shapiro_one_way_delay(shapiro, r_tx, r_rx, r_sep)
670 }
671
672 /// Computes the first-order one-way Shapiro delay caused by a central point mass.
673 ///
674 /// This is an internal helper used by `shapiro_delay_to` and
675 /// `one_way_relativistic_delay`.
676 ///
677 /// The implementation uses the standard analytic approximation:
678 ///
679 /// ```text
680 /// Δt_Shapiro = (2GM/c³) × ln((r_tx + r_rx + r_sep) / (r_tx + r_rx - r_sep))
681 /// ```
682 ///
683 /// # Numerical Notes
684 ///
685 /// - Returns `Dt::ZERO` if any of `r_tx`, `r_rx`, `r_sep`, or `shapiro` are
686 /// zero or negative.
687 /// - Uses a small epsilon (`1e-6` m) as a safety floor on the denominator to
688 /// avoid division by zero or taking the logarithm of an invalid value.
689 /// - When the signal path is nearly collinear with the central body (very
690 /// small impact parameter), the result can become large. This function does
691 /// **not** model the finite size of the Sun (or other body) nor higher-order
692 /// relativistic effects.
693 /// - For high-precision work during deep solar conjunctions, consider using
694 /// a numerically integrated or impact-parameter-based Shapiro model instead.
695 pub(crate) const fn shapiro_one_way_delay(
696 shapiro: Dt,
697 r_tx: Real,
698 r_rx: Real,
699 r_sep: Real,
700 ) -> Dt {
701 let shapiro_sec = shapiro.to_sec_f();
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 // Safety floor to avoid division by zero or log of invalid argument
708 // in near-grazing geometries.
709 const EPS: Real = f!(1e-6);
710
711 let denom = (r_tx + r_rx - r_sep).max(EPS);
712 let arg = (r_tx + r_rx + r_sep) / denom;
713
714 // Additional guard against invalid logarithm argument
715 if arg <= f!(1.0) {
716 return Dt::ZERO;
717 }
718
719 let delay_sec = shapiro_sec * log(arg);
720 Dt::from_sec_f(delay_sec)
721 }
722
723 /// Computes only the differential clock-rate correction between `self`
724 /// (transmitter) and `rx` (receiver). Does **not** include any Shapiro delay.
725 ///
726 /// Internal helper.
727 pub const fn compute_differential_clock_correction(&self, rx: ObserverState) -> Dt {
728 let span = rx.time.to_diff_raw(self.time);
729
730 let tx_drift = Drift::from_velocity_potential_and_scale(
731 self.velocity.speed(),
732 self.grav_potential_m2_s2,
733 self.characteristic_length_scale,
734 );
735 let rx_drift = Drift::from_velocity_potential_and_scale(
736 rx.velocity.speed(),
737 rx.grav_potential_m2_s2,
738 rx.characteristic_length_scale,
739 );
740
741 rx_drift
742 .time_diff_after(&span)
743 .sub(tx_drift.time_diff_after(&span))
744 }
745}