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