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