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