deep_time/physics/observer/light_time.rs
1//! Light-time implementations for [`Observer`].
2
3use crate::{C, C_SQUARED, Drift, Dt, Position, Real, Scale, TWO_GM_SUN_OVER_C3, Velocity, log};
4
5use super::Observer;
6
7impl Dt {
8 /// Shapiro gravitational time scale for the Sun (`2 G M_☉ / c³`).
9 ///
10 /// Recommended value for the Sun when building the `bodies` slice passed to
11 /// [`Observer::shapiro_delay`](../../struct.Observer.html#method.shapiro_delay),
12 /// [`Observer::one_way_relativistic_delay`](../../struct.Observer.html#method.one_way_relativistic_delay),
13 /// and related methods.
14 pub const SHAPIRO_SOLAR: Self = Self::from_sec_f(TWO_GM_SUN_OVER_C3, Scale::TAI, Scale::TAI);
15
16 /// Creates the Shapiro delay scale for an arbitrary central body
17 /// from its standard gravitational parameter `GM` (μ) in m³ s⁻².
18 ///
19 /// This produces the coefficient used in the Shapiro gravitational time delay
20 /// formula. It is the recommended way to create a custom Shapiro scale for
21 /// planets, stars, or other massive bodies.
22 ///
23 /// The returned value is intended to be used for the `bodies` parameter
24 /// when calling
25 /// [`Observer::shapiro_delay`](../../struct.Observer.html#method.shapiro_delay) or
26 /// [`Observer::one_way_relativistic_delay`](../../struct.Observer.html#method.one_way_relativistic_delay).
27 #[inline]
28 pub const fn shapiro_from_grav_param(gm: Real) -> Dt {
29 let sec = 2.0 * gm / (C * C_SQUARED);
30 Self::from_sec_f(sec, Scale::TAI, Scale::TAI)
31 }
32
33 /// Creates an [`Observer`] using this time value along with the
34 /// provided position, velocity, and gravitational information.
35 ///
36 /// An [`Observer`] represents a complete snapshot of an observer
37 /// (spacecraft, ground station, planet, person, etc.) at a
38 /// specific moment.
39 ///
40 /// It bundles together the time, position, velocity, and local
41 /// gravitational environment so that relativistic calculations
42 /// (light time, clock rates, Shapiro delay, etc.) can be performed.
43 ///
44 /// This method is a convenience constructor. It is useful when you
45 /// already have a [`Dt`] (a time value) and want to build an
46 /// [`Observer`] directly from it.
47 ///
48 /// ## Parameters
49 ///
50 /// - `position`: The observer’s position in meters (typically expressed
51 /// in a barycentric or heliocentric frame).
52 /// - `velocity`: The observer’s velocity in meters per second.
53 /// - `grav_potential_m2_s2`: The total Newtonian gravitational potential
54 /// (Φ) at the observer’s location, in m²/s². This is usually negative
55 /// for bound orbits and is the sum of contributions from the Sun and
56 /// planets.
57 /// - `characteristic_length_scale`: A length scale (in meters) over which
58 /// gravity varies significantly at this location. Use `0.0` for normal
59 /// solar-system and weak-field cases. Only provide a non-zero value when
60 /// working in strong gravitational fields.
61 ///
62 /// ## Examples
63 ///
64 /// ```
65 /// use deep_time::{Dt, Position, Spacetime, Velocity, from_sec_f};
66 ///
67 /// let bodies = [
68 /// (Position::from_au(0.0, 0.0, 0.0), 1.3271244e20), // Sun
69 /// (Position::from_au(1.0, 0.0, 0.0), 3.9860044e14), // Earth
70 /// (Position::from_au(1.00257, 0.0, 0.0), 4.9048695e12), // Moon
71 /// ];
72 ///
73 /// let position = Position::from_au(1.001, 0.001, 0.0); // e.g. spacecraft, asteroid, etc.
74 ///
75 /// let grav_potential = Spacetime::grav_potential_from_point_masses(
76 /// &position,
77 /// bodies.iter().cloned(),
78 /// );
79 ///
80 /// let t = from_sec_f!(1234.5);
81 ///
82 /// let state = t.to_observer(
83 /// Position::ZERO,
84 /// Velocity::ZERO,
85 /// grav_potential,
86 /// 0.0, // normal solar-system use
87 /// );
88 /// ```
89 #[inline]
90 pub const fn to_observer(
91 self,
92 position: Position,
93 velocity: Velocity,
94 grav_potential_m2_s2: Real,
95 characteristic_length_scale: Real,
96 ) -> Observer {
97 Observer {
98 time: self,
99 position,
100 velocity,
101 grav_potential_m2_s2,
102 characteristic_length_scale,
103 }
104 }
105}
106
107impl Observer {
108 /// Computes the combined one-way relativistic correction for a signal
109 /// traveling from this observer (the transmitter) to a receiver.
110 ///
111 /// This value is the **total extra time** you should add to the Newtonian
112 /// geometric light travel time (`distance / speed of light`). It includes
113 /// **two** separate relativistic effects:
114 ///
115 /// 1. The gravitational propagation delay (Shapiro delay) caused by the
116 /// Sun and other bodies slowing the signal.
117 /// 2. The differential clock-rate correction caused by the transmitter
118 /// and receiver having slightly different proper-time rates (due to
119 /// their velocities and gravitational potentials).
120 ///
121 /// In other words, this method gives you **propagation delay + clock-rate
122 /// correction** in one convenient call.
123 ///
124 /// **Important:** This is a convenience method. It is provided so you can
125 /// get the full one-way relativistic correction quickly. If you need
126 /// strict separation of the two effects (for example, to apply them at
127 /// different stages of your calculation), call
128 /// [`Observer::shapiro_delay`](#method.shapiro_delay) and
129 /// [`Observer::compute_differential_clock_correction`](#method.compute_differential_clock_correction)
130 /// individually and add the results yourself.
131 ///
132 /// ## When to use this method
133 ///
134 /// Use this when you need the complete relativistic correction for
135 /// one-way light time in a single step — for example when:
136 /// - Computing high-precision one-way range or Doppler observables
137 /// - Building simplified navigation or orbit determination models
138 /// - You want the total effect without manually combining the pieces
139 ///
140 /// ## The `bodies` parameter – which masses to include
141 ///
142 /// Pass a slice of `(shapiro_coefficient, body_position)` pairs:
143 ///
144 /// - `shapiro_coefficient`: How strong the delay from this body should be.
145 /// It equals `2GM / c³`. Use
146 /// [`Dt::SHAPIRO_SOLAR`](../../struct.Dt.html#associatedconstant.SHAPIRO_SOLAR)
147 /// for the Sun, or
148 /// [`Dt::shapiro_from_grav_param`](../../struct.Dt.html#method.shapiro_from_grav_param)
149 /// for any other body.
150 /// - `body_position`: Where the center of that body is located at the
151 /// relevant time.
152 ///
153 /// **Important: All positions must be measured the same way**
154 ///
155 /// The transmitter position (`self.position`), the receiver position
156 /// (`rx.position`), and every `body_position` you provide must all be
157 /// measured from the **same point in space**, and they must all use
158 /// the **same directions** for their X, Y, and Z axes.
159 ///
160 /// For example, if your transmitter position is measured from the center
161 /// of the solar system, then the receiver and body positions must also
162 /// be measured from the center of the solar system using the same
163 /// pointing directions for the coordinate axes.
164 ///
165 /// In most solar-system work, people use positions from JPL ephemerides
166 /// (which are measured from the center of the solar system).
167 ///
168 /// Pass an empty slice (`&[]`) to turn off the Shapiro (gravitational)
169 /// part of the correction.
170 ///
171 /// ## Parameters
172 ///
173 /// * `rx` — Receiver state at the approximate time the signal arrives.
174 /// * `bodies` — List of bodies that should contribute to the gravitational
175 /// propagation delay.
176 ///
177 /// ## Returns
178 ///
179 /// The total one-way relativistic correction (Shapiro propagation delay
180 /// plus differential clock-rate correction), expressed as a `Dt` in the
181 /// same time scale as `self.time`.
182 ///
183 /// This value should normally be **added** to the Newtonian geometric
184 /// light time.
185 pub const fn one_way_relativistic_delay(&self, rx: &Observer, bodies: &[(Dt, Position)]) -> Dt {
186 let prop = self.shapiro_delay(rx, bodies);
187 let drift = self.compute_differential_clock_correction(rx);
188 prop.add(drift)
189 }
190
191 /// Iteratively solves the one-way light-time equation in coordinate time,
192 /// including relativistic propagation corrections, until convergence.
193 ///
194 /// This solver computes the receive epoch `t_rx` such that:
195 ///
196 /// ```text
197 /// t_rx = t_tx + |r_rx(t_rx) − r_tx(t_tx)| / c + Δt_shapiro(t_tx, t_rx)
198 /// ```
199 ///
200 /// It performs fixed-point iteration using the propagation delay returned by
201 /// [`Observer::shapiro_delay`](#method.shapiro_delay).
202 /// Clock-rate and proper-time effects are **not** included in the iteration;
203 /// they should be applied separately when converting between coordinate time
204 /// and proper time or when forming observables.
205 ///
206 /// The solver is suitable for high-precision one-way light-time calculations
207 /// and works with any ephemeris source via the provided closure.
208 ///
209 /// ## Parameters
210 ///
211 /// * `rx_provider` — Closure returning the full [`Observer`] of the
212 /// receiver at a given coordinate time.
213 /// * `bodies` — Slice of `(shapiro_coefficient, body_position)` pairs
214 /// controlling the Shapiro contribution. Use `&[(Dt::SHAPIRO_SOLAR, sun_pos)]`
215 /// for solar-system work; include additional bodies for higher precision.
216 /// Pass `&[]` to disable Shapiro.
217 /// * `tolerance` — Maximum allowed change in receive time per iteration
218 /// before declaring convergence.
219 /// * `max_iter` — Maximum number of iterations. Typical values are 12–20
220 /// for solar-system geometries.
221 ///
222 /// ## Returns
223 ///
224 /// A tuple `(prop_correction, rx_time, final_state)` where:
225 /// - `prop_correction` is the converged Shapiro propagation delay,
226 /// - `rx_time` is the converged receive time (same scale as `self.time`),
227 /// - `final_state` is the receiver state at `rx_time`.
228 pub fn iterative_one_way_light_time_to<F>(
229 &self,
230 rx_provider: &mut F,
231 bodies: &[(Dt, Position)],
232 tolerance: Dt,
233 max_iter: usize,
234 ) -> (Dt, Dt, Observer)
235 where
236 F: FnMut(Dt) -> Observer,
237 {
238 // Initial geometric guess
239 let initial_rx = rx_provider(self.time);
240 let initial_r_sep = self.position.distance_to(&initial_rx.position);
241 let initial_geometric = Dt::from_sec_f(initial_r_sep / C, Scale::TAI, Scale::TAI);
242
243 let mut rx_time = self.time.add(initial_geometric);
244 let mut prop_correction = Dt::ZERO;
245
246 for _ in 0..max_iter {
247 let rx = rx_provider(rx_time);
248
249 prop_correction = self.shapiro_delay(&rx, bodies);
250
251 let r_sep = self.position.distance_to(&rx.position);
252 let geometric = Dt::from_sec_f(r_sep / C, Scale::TAI, Scale::TAI);
253 let full_delay = geometric.add(prop_correction);
254
255 let new_rx_time = self.time.add(full_delay);
256 let change = new_rx_time.to_diff_raw(rx_time);
257
258 rx_time = new_rx_time;
259
260 if change.abs() < tolerance {
261 return (prop_correction, rx_time, rx);
262 }
263 }
264
265 // Fallback after max iterations
266 let final_rx = rx_provider(rx_time);
267 (prop_correction, rx_time, final_rx)
268 }
269
270 /// Computes the total Shapiro (gravitational propagation) delay for a
271 /// complete round-trip (two-way) signal.
272 ///
273 /// This method solves the uplink and downlink legs *separately and
274 /// independently* using the iterative light-time solver. This approach
275 /// is more accurate than older combined round-trip formulas when the
276 /// two ends have significantly different velocities or are in different
277 /// gravitational environments.
278 ///
279 /// The returned value is the **sum of the uplink and downlink Shapiro
280 /// delays only**. It does **not** include clock-rate or proper-time
281 /// corrections.
282 ///
283 /// ## When to use this method
284 ///
285 /// Use this when you need the total gravitational propagation correction
286 /// for two-way (round-trip) measurements, for example:
287 /// - Two-way range or range-rate (Doppler) data
288 /// - Transponded signals from spacecraft
289 /// - Any high-precision two-way light-time calculation
290 ///
291 /// For one-way signals, use
292 /// [`Observer::shapiro_delay`](#method.shapiro_delay) or
293 /// [`Observer::one_way_relativistic_delay`](#method.one_way_relativistic_delay)
294 /// instead.
295 ///
296 /// ## How the calculation works
297 ///
298 /// 1. Solves the uplink leg (from `self` to the remote receiver) using
299 /// the `rx_provider` closure.
300 /// 2. Obtains the accurate receiver state at the uplink arrival time.
301 /// 3. Solves the downlink leg (from the receiver back to the local
302 /// transmitter) using the `tx_provider` closure.
303 ///
304 /// ## The `bodies` parameter – which masses to include
305 ///
306 /// Pass a slice of `(shapiro_coefficient, body_position)` pairs (the
307 /// same slice is used for both legs). See
308 /// [`Observer::shapiro_delay`](#method.shapiro_delay)
309 /// for details on how to build this slice.
310 ///
311 /// **Important: All states returned by the providers must be consistent**
312 /// with the same reference frame (same origin and same coordinate axes).
313 ///
314 /// ## Parameters
315 ///
316 /// * `rx_provider` — Closure that returns the full [`Observer`] of
317 /// the remote receiver (planet, spacecraft, etc.) at any given
318 /// coordinate time.
319 /// * `tx_provider` — Closure that returns the full [`Observer`] of
320 /// the local transmitter at any given coordinate time (used only for
321 /// the downlink leg).
322 /// * `bodies` — Slice of `(shapiro_coefficient, body_position)` pairs
323 /// describing the gravitating bodies.
324 /// * `tolerance` — Convergence tolerance for each leg’s iterative solver
325 /// (e.g. `Dt::from_ns_floor(1, 0, Scale::TAI)`).
326 /// * `max_iter` — Maximum number of iterations allowed per leg
327 /// (typical values are 12–20).
328 ///
329 /// ## Returns
330 ///
331 /// The total round-trip Shapiro propagation delay (uplink + downlink)
332 /// as a `Dt`, in the same time scale as `self.time`.
333 ///
334 /// This value should normally be **added** to the Newtonian geometric
335 /// round-trip light time. Clock-rate corrections must still be applied
336 /// separately (e.g. by squaring the one-way clock-rate ratio).
337 pub fn round_trip_light_time_correction<RxF, TxF>(
338 &self,
339 mut rx_provider: RxF, // remote body (planet, spacecraft, etc.)
340 mut tx_provider: TxF, // local transmitter for the return leg (can move)
341 bodies: &[(Dt, Position)],
342 tolerance: Dt,
343 max_iter: usize,
344 ) -> Dt
345 where
346 RxF: FnMut(Dt) -> Observer,
347 TxF: FnMut(Dt) -> Observer,
348 {
349 // Uplink leg: transmitter → receiver
350 let (uplink_prop, rx_time, _rx_state) =
351 self.iterative_one_way_light_time_to(&mut rx_provider, bodies, tolerance, max_iter);
352
353 // Downlink leg: receiver → transmitter
354 let return_tx = rx_provider(rx_time); // accurate state at uplink arrival
355
356 let (downlink_prop, _return_rx_time, _return_rx_state) = return_tx
357 .iterative_one_way_light_time_to(&mut tx_provider, bodies, tolerance, max_iter);
358
359 uplink_prop.add(downlink_prop)
360 }
361
362 /// Computes the one-way gravitational propagation delay (Shapiro delay)
363 /// caused by massive bodies between this observer (the transmitter) and
364 /// a receiver.
365 ///
366 /// This value is the **extra time** a radio signal takes to travel because
367 /// gravity from the Sun and planets slightly slows it down. You normally
368 /// add this delay to the ordinary geometric light travel time
369 /// (`distance / speed of light`) to get a more accurate total one-way
370 /// signal travel time.
371 ///
372 /// **Important:** This method returns **only** the gravitational
373 /// propagation delay. It does **not** include clock-rate differences
374 /// between the transmitter and receiver caused by velocity or gravity.
375 /// Those effects are available separately through
376 /// [`Observer::compute_differential_clock_correction`](#method.compute_differential_clock_correction),
377 /// [`Observer::proper_time_rate`](#method.proper_time_rate), and
378 /// [`Observer::relativistic_clock_rate_ratio`](#method.relativistic_clock_rate_ratio).
379 ///
380 /// ## When to use this method
381 ///
382 /// Use this when you need the gravitational (Shapiro) contribution to
383 /// one-way light time — for example when building high-precision range,
384 /// Doppler, or orbit determination models.
385 ///
386 /// ## The `bodies` parameter – which masses to include
387 ///
388 /// Pass a slice of `(shapiro_coefficient, body_position)` pairs:
389 ///
390 /// - `shapiro_coefficient`: How strong the delay from this body should be.
391 /// It equals `2GM / c³`. Use
392 /// [`Dt::SHAPIRO_SOLAR`](../../struct.Dt.html#associatedconstant.SHAPIRO_SOLAR)
393 /// for the Sun, or
394 /// [`Dt::shapiro_from_grav_param`](../../struct.Dt.html#method.shapiro_from_grav_param)
395 /// for any other body.
396 /// - `body_position`: Where the center of that body is located at the
397 /// relevant time.
398 ///
399 /// **Important: All positions must be measured the same way**
400 ///
401 /// The transmitter position (`self.position`), the receiver position
402 /// (`rx.position`), and every `body_position` you provide must all be
403 /// measured from the **same point in space**, and they must all use
404 /// the **same directions** for their X, Y, and Z axes.
405 ///
406 /// For example, if the transmitter position is measured from the center
407 /// of the solar system, then the receiver and body positions must also
408 /// be measured from the center of the solar system, using the same
409 /// pointing directions for the coordinate axes.
410 ///
411 /// If the positions come from different measurement systems, the
412 /// calculated delay will be wrong.
413 ///
414 /// In most solar-system work, people use positions from JPL ephemerides
415 /// (which are measured from the center of the solar system).
416 ///
417 /// Pass an empty slice (`&[]`) to turn off Shapiro delay entirely.
418 ///
419 /// ## Parameters
420 ///
421 /// * `rx` — Receiver state at the approximate time the signal arrives.
422 /// * `bodies` — List of bodies that should contribute to the delay.
423 ///
424 /// ## Returns
425 ///
426 /// The total one-way Shapiro gravitational propagation delay, in the
427 /// same time scale as `self.time`. This value should normally be
428 /// **added** to the Newtonian geometric light time.
429 pub const fn shapiro_delay(&self, rx: &Observer, bodies: &[(Dt, Position)]) -> Dt {
430 let mut total = Dt::ZERO;
431 let mut i = 0;
432
433 while i < bodies.len() {
434 let (shapiro_coeff, body_pos) = &bodies[i];
435 total = total.add(Self::shapiro_one_way_delay(
436 *shapiro_coeff,
437 &self.position,
438 &rx.position,
439 body_pos,
440 ));
441 i += 1;
442 }
443
444 total
445 }
446
447 /// Computes the first-order one-way Shapiro gravitational time delay
448 /// due to a single central body using a numerically stable formulation.
449 ///
450 /// This is the **core low-level implementation** (pub(crate) const fn).
451 /// It replaces the classic radial formula with an algebraically equivalent
452 /// but cancellation-free form that is robust even for small impact parameters
453 /// (near-grazing / conjunction geometries).
454 ///
455 /// The algorithm uses the identity:
456 ///
457 ///
458 /// ln((r_tx + r_rx + r_sep) / (r_tx + r_rx - r_sep))
459 /// ≡ 2·ln(num) − ln(denom_term)
460 ///
461 ///
462 /// where denom_term is computed from the dot-product identity
463 /// (r_tx + r_rx)² − r_sep² = 2(r_tx·r_rx + p_tx · p_rx).
464 /// This avoids the dangerous subtraction that loses precision when
465 /// the signal path passes close to the body.
466 ///
467 /// The result is equivalent (within floating-point) to the
468 /// classic Moyer/DSN-style formula while being far more stable.
469 /// Contributions from multiple bodies are summed at a higher level.
470 ///
471 /// ## Safety / Guards
472 ///
473 /// - Returns [`Dt::ZERO`](../../struct.Dt.html#associatedconstant.ZERO)
474 /// for any non-positive distance or zero Shapiro coefficient.
475 /// - Protects against invalid logarithm argument (`arg <= 1.0`).
476 /// - Designed for weak-field solar-system / cislunar use (monopole, straight-line approx).
477 pub(crate) const fn shapiro_one_way_delay(
478 shapiro: Dt,
479 tx_pos: &Position,
480 rx_pos: &Position,
481 body_pos: &Position,
482 ) -> Dt {
483 let shapiro_sec = shapiro.to_sec_f();
484
485 // Distances relative to *this specific gravitating body*
486 let r_tx = tx_pos.distance_to(body_pos);
487 let r_rx = rx_pos.distance_to(body_pos);
488 let r_sep = tx_pos.distance_to(rx_pos);
489
490 if r_tx <= f!(0.0) || r_rx <= f!(0.0) || r_sep <= f!(0.0) || shapiro_sec == f!(0.0) {
491 return Dt::ZERO;
492 }
493
494 let s = r_tx + r_rx;
495 let num = s + r_sep; // (r_tx + r_rx + r_sep)
496
497 if num <= f!(0.0) {
498 return Dt::ZERO;
499 }
500
501 // Stable computation of (r_tx + r_rx)^2 − r_sep^2
502 // = 2 × (r_tx r_rx + \vec{p_tx} · \vec{p_rx})
503 let dot_term = (r_tx * r_tx + r_rx * r_rx - r_sep * r_sep) / f!(2.0);
504 let denom_term = f!(2.0) * (r_tx * r_rx + dot_term);
505
506 if denom_term <= f!(0.0) {
507 return Dt::ZERO;
508 }
509
510 let arg = (num * num) / denom_term;
511
512 if arg <= f!(1.0) {
513 return Dt::ZERO;
514 }
515
516 let delay_sec = shapiro_sec * log(arg);
517 Dt::from_sec_f(delay_sec, Scale::TAI, Scale::TAI)
518 }
519
520 /// Computes the differential proper-time correction between `self`
521 /// (transmitter) and `rx` (receiver) over the interval between their
522 /// time tags.
523 ///
524 /// This returns the difference in proper time advance between the two
525 /// observers. It does **not** include Shapiro propagation delay.
526 ///
527 /// The result can be added to the output of
528 /// [`Observer::shapiro_delay`](#method.shapiro_delay)
529 /// or
530 /// [`Observer::iterative_one_way_light_time_to`](#method.iterative_one_way_light_time_to)
531 /// when a combined relativistic correction (propagation + clock rate) is
532 /// required.
533 ///
534 /// ## Parameters
535 ///
536 /// * `rx` — Receiver state at the approximate time of reception.
537 ///
538 /// ## Returns
539 ///
540 /// The differential clock-rate correction (`rx_proper_advance − tx_proper_advance`).
541 pub const fn compute_differential_clock_correction(&self, rx: &Observer) -> Dt {
542 let span = rx.time.to_diff_raw(self.time);
543
544 let tx_drift = Drift::from_velocity_potential_and_scale(
545 self.velocity.speed(),
546 self.grav_potential_m2_s2,
547 self.characteristic_length_scale,
548 );
549 let rx_drift = Drift::from_velocity_potential_and_scale(
550 rx.velocity.speed(),
551 rx.grav_potential_m2_s2,
552 rx.characteristic_length_scale,
553 );
554
555 rx_drift
556 .time_diff_after(&span)
557 .sub(tx_drift.time_diff_after(&span))
558 }
559}