deep_time/light_time.rs
1use crate::{
2 C, C_SQUARED, ClockDrift, Dt, LocalSpacetime, Position, Real, TSpan, TWO_GM_SUN_OVER_C3,
3 Velocity, log,
4};
5
6/// Configuration for the **Shapiro delay** — the extra time light (or radio signals)
7/// takes to travel near a massive body because gravity curves spacetime.
8///
9/// In simple terms: when a light beam or radar pulse passes close to a star or planet,
10/// general relativity makes the path take a tiny bit longer than it would in flat space.
11/// This struct holds the single number that controls how strong that delay is for a
12/// particular central body (the Sun, a planet, another star, etc.).
13///
14/// Used in high-precision light-time calculations, spacecraft ranging, pulsar timing,
15/// and very-long-baseline interferometry (VLBI).
16#[derive(Copy, Clone, Debug, PartialEq)]
17pub struct LightContext {
18 /// 2GM/c³ in seconds — the characteristic gravitational time scale used in the
19 /// one-way Shapiro delay formula.
20 ///
21 /// This is mathematically equal to `2 * GM / c³`, where:
22 /// - `G` is Newton’s gravitational constant,
23 /// - **M** is the mass of the central body,
24 /// - `c` is the speed of light.
25 ///
26 /// “GM” (often written as a single value called the **standard gravitational parameter** or μ)
27 /// is the product of G and M. In real-world astronomy it is measured very accurately as one
28 /// combined number (in m³/s²), so we keep it together here.
29 ///
30 /// It tells the light-propagation code “how strong the gravitational slowing is”
31 /// for this body. Bigger value = stronger delay effect.
32 ///
33 /// Most users never set this manually — just use `SOLAR` or `from_grav_param()`.
34 pub two_grav_param_over_c3: f64,
35}
36
37impl LightContext {
38 /// Ready-to-use value for our own Sun, based on the exact IAU 2015 recommended constants.
39 ///
40 /// Use this for any typical solar-system light-propagation calculation (radar to
41 /// planets, spacecraft tracking, etc.).
42 pub const SOLAR: Self = Self {
43 two_grav_param_over_c3: TWO_GM_SUN_OVER_C3,
44 };
45
46 /// No gravitational delay at all (flat spacetime approximation).
47 ///
48 /// Perfect for:
49 /// - interstellar or intergalactic distances,
50 /// - quick “ignore gravity” calculations,
51 /// - or when you want to apply your own custom gravitational model elsewhere.
52 pub const FLAT: Self = Self {
53 two_grav_param_over_c3: 0.0,
54 };
55
56 /// Creates a `LightContext` for any central body (planet, star, black hole, etc.).
57 ///
58 /// # Arguments
59 ///
60 /// * `grav_param` — the body’s **standard gravitational parameter** (GM or μ)
61 /// in m³/s². This is the product of Newton’s gravitational constant and the body’s mass.
62 pub const fn from_grav_param(grav_param: f64) -> Self {
63 Self {
64 two_grav_param_over_c3: 2.0 * grav_param / (C * C_SQUARED),
65 }
66 }
67}
68
69/// A complete relativistic state of an observer (spacecraft, ground station,
70/// planet, etc.) at a specific instant.
71///
72/// This is the natural input type for all relativistic light-time calculations
73/// in the library. It bundles position, velocity, gravitational potential, and
74/// an optional length scale in convenient SI units.
75#[derive(Clone, Copy, Debug, PartialEq)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77#[cfg_attr(feature = "js", derive(tsify::Tsify))]
78pub struct ObserverState {
79 /// Dt of this state (any [`Scale`] is accepted).
80 pub time: Dt,
81 /// Position in meters (typically barycentric or heliocentric).
82 pub position: Position,
83 /// Velocity in meters per second.
84 pub velocity: Velocity,
85 /// Local gravitational potential Φ in m² s⁻² (negative for bound orbits).
86 /// Usually the sum of contributions from the Sun and planets.
87 pub grav_potential_m2_s2: f64,
88 /// Characteristic length scale (in meters) over which gravity varies
89 /// significantly at the observer’s location.
90 /// Pass `0.0` (the default) for all solar-system, GNSS, and weak-field cases.
91 pub characteristic_length_scale: f64,
92}
93
94impl ObserverState {
95 /// Creates a new state for typical solar-system or GNSS use.
96 #[inline]
97 pub const fn new(
98 time: Dt,
99 position: Position,
100 velocity: Velocity,
101 grav_potential_m2_s2: f64,
102 ) -> Self {
103 Self {
104 time,
105 position,
106 velocity,
107 grav_potential_m2_s2,
108 characteristic_length_scale: 0.0,
109 }
110 }
111
112 /// Creates a new state when strong-field or gravimeter data is available.
113 #[inline]
114 pub const fn new_strong_field(
115 time: Dt,
116 position: Position,
117 velocity: Velocity,
118 grav_potential_m2_s2: f64,
119 characteristic_length_scale: f64,
120 ) -> Self {
121 Self {
122 time,
123 position,
124 velocity,
125 grav_potential_m2_s2,
126 characteristic_length_scale,
127 }
128 }
129
130 /// Returns the instantaneous proper-time rate `dτ/dt` for this observer.
131 ///
132 /// This is the exact rate at which a real clock at the given position,
133 /// velocity, and gravitational environment would advance compared to
134 /// coordinate time. It is used internally by the library for proper-time
135 /// integration, light-time corrections, and Doppler calculations.
136 #[inline]
137 pub const fn proper_time_rate(&self) -> Real {
138 let ls = LocalSpacetime::from_potential_velocity_and_scale(
139 self.grav_potential_m2_s2 / C_SQUARED,
140 self.velocity,
141 self.characteristic_length_scale,
142 );
143 ls.proper_time_rate()
144 }
145
146 /// Returns the relativistic clock-rate Doppler factor for a one-way signal
147 /// sent from this transmitter to the given receiver.
148 ///
149 /// The factor is the ratio of the receiver’s proper-time rate to the
150 /// transmitter’s proper-time rate. It accounts for the fact that clocks
151 /// at the two locations run at slightly different speeds due to motion
152 /// and gravity.
153 ///
154 /// To obtain the full observed frequency shift, multiply this factor by
155 /// the classical kinematic Doppler term `(1 - v_radial / C)`, where
156 /// `v_radial` is the line-of-sight component of the relative velocity
157 /// (positive when the transmitter and receiver are moving apart).
158 ///
159 /// This value is used for deep-space tracking, GNSS range-rate measurements,
160 /// and pulsar timing.
161 ///
162 /// # Parameters
163 /// - `self` – transmitter state (position, velocity, gravitational potential,
164 /// and length scale at the moment the signal is sent)
165 /// - `rx` – receiver state (same information, evaluated at the approximate
166 /// arrival time)
167 ///
168 /// # Example
169 /// ```rust
170 /// use deep_time::{ObserverState, Position, Velocity, Dt};
171 /// use deep_time::constants::C;
172 ///
173 /// # let tx_time = Dt::default();
174 /// # let rx_time = Dt::default();
175 /// # let tx_pos = Position::ZERO;
176 /// # let rx_pos = Position::ZERO;
177 /// # let tx_vel = Velocity::ZERO;
178 /// # let rx_vel = Velocity::ZERO;
179 /// # let phi = 0.0_f64; // gravitational potential
180 ///
181 /// let tx = ObserverState::new(tx_time, tx_pos, tx_vel, phi);
182 /// let rx = ObserverState::new(rx_time, rx_pos, rx_vel, phi);
183 ///
184 /// let factor = tx.relativistic_clock_doppler_factor(rx);
185 ///
186 /// // Full observed frequency shift (example only)
187 /// let v_radial = 0.0; // m/s, positive if receding
188 /// let classical_doppler = 1.0 - v_radial / C;
189 /// let total_frequency_shift = 1.0 * factor * classical_doppler;
190 /// ```
191 #[inline]
192 pub const fn relativistic_clock_doppler_factor(&self, rx: ObserverState) -> Real {
193 rx.proper_time_rate() / self.proper_time_rate()
194 }
195
196 /// Returns the two-way relativistic clock-rate Doppler factor for round-trip
197 /// ranging (transmit → receive → immediate transponder reply).
198 ///
199 /// This is the product of the one-way factors for the complete round trip
200 /// and is the value needed by deep-space networks when correcting measured
201 /// range-rate data.
202 #[inline]
203 pub const fn two_way_relativistic_doppler_factor(&self, rx: ObserverState) -> Real {
204 let one_way = self.relativistic_clock_doppler_factor(rx);
205 one_way * one_way
206 }
207
208 /// Computes the total relativistic correction that must be added to the Newtonian
209 /// geometric light time (`|r_rx − r_tx| / c`) for a one-way signal.
210 ///
211 /// This function accounts for two physical effects:
212 /// - Differential clock-rate drift between transmitter and receiver (special-relativistic
213 /// velocity + general-relativistic gravitational time dilation) using the library’s
214 /// unified master-Lagrangian proper-time model.
215 /// - Gravitational (Shapiro) delay caused by spacetime curvature near a central mass.
216 ///
217 /// Use cases include:
218 /// - Deep-space tracking and ranging (DSN, ESA, JPL)
219 /// - GNSS and satellite navigation
220 /// - Pulsar timing arrays
221 /// - Laser communication or ranging to distant spacecraft
222 /// - Future interstellar missions where signals pass near other stars or black holes
223 ///
224 /// # Parameters
225 /// - `self` – the transmitter’s full relativistic state at the moment the signal is sent
226 /// - `rx` – the receiver’s relativistic state at the approximate arrival time
227 /// - `context` – controls the gravitational (Shapiro) contribution. Use `LightContext::SOLAR`
228 /// for solar-system work, `LightContext::FLAT` when you want no central-mass delay,
229 /// or `LightContext::from_grav_param(your_gravitational_parameter)` for any other star, planet,
230 /// or black hole.
231 ///
232 /// # Returns
233 /// A [`TSpan`] (in seconds) to be **added** to the Newtonian geometric light time.
234 ///
235 /// # Examples
236 ///
237 /// Basic usage for a solar-system one-way light-time correction (e.g. Earth to Mars):
238 ///
239 /// ```no_run
240 /// use deep_time::{
241 /// ObserverState, Position, Velocity, Dt, TSpan, LightContext,
242 /// // Assume you have ephemeris functions or constants available
243 /// };
244 ///
245 /// # let tx_time: Dt = todo!();
246 /// # let tx_pos: Position = todo!();
247 /// # let tx_vel: Velocity = todo!();
248 /// # let tx_potential: f64 = todo!();
249 /// # let rx_approx_time: Dt = todo!();
250 /// # let rx_pos: Position = todo!();
251 /// # let rx_vel: Velocity = todo!();
252 /// # let rx_potential: f64 = todo!();
253 ///
254 /// let transmitter = ObserverState::new(
255 /// tx_time,
256 /// tx_pos,
257 /// tx_vel,
258 /// tx_potential,
259 /// );
260 ///
261 /// let receiver_approx = ObserverState::new(
262 /// rx_approx_time,
263 /// rx_pos,
264 /// rx_vel,
265 /// rx_potential,
266 /// );
267 ///
268 /// // Use SOLAR for Sun-centered calculations
269 /// let correction: TSpan = transmitter
270 /// .one_way_relativistic_delay_to(receiver_approx, LightContext::SOLAR);
271 ///
272 /// // The result should be added to the Newtonian geometric delay `r_sep / C`
273 /// ```
274 ///
275 /// For a custom body (e.g. Jupiter):
276 ///
277 /// ```ignore
278 /// let jupiter_context = LightContext::from_grav_param(jupiter_gm); // GM in m³/s²
279 /// let correction = tx.one_way_relativistic_delay_to(rx, jupiter_context);
280 /// ```
281 ///
282 /// # Multi-body and exotic environments
283 ///
284 /// This function models the Shapiro delay from only a **single central mass**
285 /// via the supplied `LightContext`. For signals that pass near multiple massive
286 /// bodies (e.g. two stars, a star and a planet, or a binary black-hole system)
287 /// or in highly dynamic/strong-field regimes, the single-body approximation may
288 /// not be sufficient.
289 ///
290 /// In those cases consider using [`one_way_relativistic_delay_integrated`] instead,
291 /// which lets you supply your own full spacetime model along the entire path.
292 /// Alternatively, you can compute individual Shapiro contributions from each body
293 /// (using the helper `shapiro_one_way_for_body` if you add it) and manually combine
294 /// them with the result of this function.
295 pub const fn one_way_relativistic_delay_to(
296 &self,
297 rx: ObserverState,
298 context: LightContext,
299 ) -> TSpan {
300 let span = rx.time.to_tai_since(self.time);
301
302 let tx_drift = ClockDrift::from_velocity_potential_and_scale(
303 self.velocity.speed(),
304 self.grav_potential_m2_s2,
305 self.characteristic_length_scale,
306 );
307 let rx_drift = ClockDrift::from_velocity_potential_and_scale(
308 rx.velocity.speed(),
309 rx.grav_potential_m2_s2,
310 rx.characteristic_length_scale,
311 );
312
313 let drift_correction = rx_drift
314 .time_diff_after(&span)
315 .sub(tx_drift.time_diff_after(&span));
316
317 let r_tx = self.position.norm();
318 let r_rx = rx.position.norm();
319 let r_sep = self.position.distance_to(rx.position);
320 let shapiro = Self::shapiro_one_way_delay(context, r_tx, r_rx, r_sep);
321
322 drift_correction.add(shapiro)
323 }
324
325 /// Iteratively solves for the true receive time and the corresponding relativistic
326 /// correction for a one-way signal.
327 ///
328 /// Because the exact arrival time depends on the relativistic correction itself,
329 /// an iterative approach is required. The function typically converges in 3–5
330 /// iterations to sub-nanosecond accuracy, even for outer-solar-system distances.
331 ///
332 /// # Parameters
333 /// - `self` – the transmitter’s relativistic state (fixed)
334 /// - `rx_provider` – a closure that, given a guessed receive [`Dt`], returns
335 /// the full [`ObserverState`] of the receiver at that time. You usually create
336 /// this closure by calling your ephemeris/orbit propagator.
337 /// - `context` – gravitational context (`LightContext::SOLAR`, `LightContext::FLAT`,
338 /// or a custom value). See [`one_way_relativistic_delay_to`] for details.
339 /// - `tolerance` – maximum allowed change in receive time between iterations
340 /// (recommended `TSpan::from_ns(1)` or tighter)
341 /// - `max_iter` – safety limit on the number of iterations (recommended 8–12)
342 ///
343 /// # Returns
344 /// A tuple `(correction, final_rx_time)` where:
345 /// - `correction` is the relativistic delay (same as returned by [`one_way_relativistic_delay_to`])
346 /// - `final_rx_time` is the converged receive [`Dt`]
347 ///
348 /// # Examples
349 ///
350 /// ```no_run
351 /// use deep_time::{ObserverState, Dt, TSpan, LightContext, Position, Velocity};
352 ///
353 /// # // Assume these exist in your code (e.g. from an ephemeris library)
354 /// # let tx_time: Dt = todo!();
355 /// # let tx_pos: Position = todo!();
356 /// # let tx_vel: Velocity = todo!();
357 /// # let tx_potential: f64 = todo!();
358 /// # fn get_receiver_state_at(t: Dt) -> (Position, Velocity, f64) { todo!() }
359 ///
360 /// let transmitter = ObserverState::new(
361 /// tx_time,
362 /// tx_pos,
363 /// tx_vel,
364 /// tx_potential,
365 /// );
366 ///
367 /// let (correction, final_rx_time) = transmitter.iterative_one_way_relativistic_delay_to(
368 /// |guessed_rx_time| {
369 /// // Call your ephemeris / orbit propagator here
370 /// let (pos, vel, potential) = get_receiver_state_at(guessed_rx_time);
371 /// ObserverState::new(guessed_rx_time, pos, vel, potential)
372 /// },
373 /// LightContext::SOLAR,
374 /// TSpan::from_ns(1), // 1 nanosecond tolerance (recommended)
375 /// 12, // safety limit (recommended)
376 /// );
377 ///
378 /// // `correction` is the total relativistic delay (clock drift + Shapiro)
379 /// // to add to the Newtonian geometric light time.
380 /// // `final_rx_time` is the accurately converged signal arrival time.
381 /// ```
382 ///
383 /// Using a custom central body (e.g. near Jupiter) and a tighter tolerance:
384 ///
385 /// ```ignore
386 /// let jupiter_gm = 1.26686534e17_f64; // m³/s²
387 /// let context = LightContext::from_grav_param(jupiter_gm);
388 ///
389 /// let (correction, rx_time) = tx.iterative_one_way_relativistic_delay_to(
390 /// rx_provider, context, TSpan::from_ns(0.1), 10
391 /// );
392 /// ```
393 pub fn iterative_one_way_relativistic_delay_to<F>(
394 &self,
395 mut rx_provider: F,
396 context: LightContext,
397 tolerance: TSpan,
398 max_iter: usize,
399 ) -> (TSpan, Dt)
400 where
401 F: FnMut(Dt) -> ObserverState,
402 {
403 let mut rx = rx_provider(self.time);
404 let mut rel_correction = TSpan::ZERO;
405
406 for _ in 0..max_iter {
407 rel_correction = self.one_way_relativistic_delay_to(rx, context);
408
409 let r_sep = self.position.distance_to(rx.position);
410 let geometric = TSpan::from_sec_f(r_sep / C);
411 let full_delay = geometric.add(rel_correction);
412
413 let new_rx_time = self.time.add(full_delay);
414 let change = new_rx_time.to_tai_since(rx.time);
415
416 rx = rx_provider(new_rx_time);
417 rx.time = new_rx_time;
418
419 if change < tolerance {
420 return (rel_correction, new_rx_time);
421 }
422 }
423 (rel_correction, rx.time)
424 }
425
426 /// Computes the relativistic correction using numerical quadrature (Simpson’s rule)
427 /// of the relative clock-rate offset along the entire straight-line light path.
428 ///
429 /// This is the most accurate method when the clock-rate offset varies continuously
430 /// along the path (long baselines, interstellar distances, or strong-field regions).
431 ///
432 /// # Parameters
433 /// - `self` – the transmitter’s relativistic state
434 /// - `rx` – the receiver’s relativistic state
435 /// - `context` – gravitational context for the Shapiro delay (see [`one_way_relativistic_delay_to`])
436 /// - `samples` – a slice of [`LocalSpacetime`] snapshots uniformly spaced along the
437 /// path (λ ∈ [0, 1]). You build this slice by evaluating your spacetime model
438 /// at several points between transmitter and receiver. Even 9–21 samples give
439 /// excellent accuracy.
440 ///
441 /// # Returns
442 /// A [`TSpan`] containing the integrated clock-drift correction plus the Shapiro
443 /// delay from the supplied `context`.
444 ///
445 /// # Example of building `samples`
446 /// ```ignore
447 /// let samples: Vec<LocalSpacetime> = (0..=15)
448 /// .map(|i| {
449 /// let lambda = i as f64 / 15.0;
450 /// let point = tx.position.lerp(rx.position, lambda);
451 /// let phi_over_c2 = compute_total_potential_at(point); // your model
452 /// LocalSpacetime::from_potential_velocity_and_scale(
453 /// phi_over_c2,
454 /// Velocity::ZERO,
455 /// 0.0, // weak-field
456 /// )
457 /// })
458 /// .collect();
459 /// ```
460 ///
461 /// # Examples
462 ///
463 /// Full usage example for high-accuracy one-way light-time correction
464 /// (e.g. interstellar distances or strong gravitational fields):
465 ///
466 /// ```no_run
467 /// use deep_time::{
468 /// ObserverState, LocalSpacetime, Position, Velocity, Dt,
469 /// TSpan, LightContext,
470 /// };
471 ///
472 /// # let tx_time: Dt = todo!();
473 /// # let tx_pos: Position = todo!();
474 /// # let tx_vel: Velocity = todo!();
475 /// # let tx_potential: f64 = todo!();
476 /// # let rx_time: Dt = todo!();
477 /// # let rx_pos: Position = todo!();
478 /// # let rx_vel: Velocity = todo!();
479 /// # let rx_potential: f64 = todo!();
480 /// # fn compute_total_potential_at(pos: Position) -> f64 { todo!() }
481 ///
482 /// let transmitter = ObserverState::new(
483 /// tx_time, tx_pos, tx_vel, tx_potential,
484 /// );
485 ///
486 /// let receiver = ObserverState::new(
487 /// rx_time, rx_pos, rx_vel, rx_potential,
488 /// );
489 ///
490 /// // Build uniformly spaced samples along the straight-line path.
491 /// // 9–21 points are usually sufficient; use more for interstellar/strong-field cases.
492 /// let samples: Vec<LocalSpacetime> = (0..=21)
493 /// .map(|i| {
494 /// let lambda = i as f64 / 21.0;
495 /// let point = transmitter.position.lerp(receiver.position, lambda);
496 /// let phi_over_c2 = compute_total_potential_at(point);
497 ///
498 /// LocalSpacetime::from_potential_velocity_and_scale(
499 /// phi_over_c2,
500 /// Velocity::ZERO, // light itself carries no rest-mass velocity
501 /// 0.0, // weak-field approximation
502 /// )
503 /// })
504 /// .collect();
505 ///
506 /// let total_correction: TSpan = transmitter.one_way_relativistic_delay_integrated(
507 /// receiver,
508 /// LightContext::SOLAR,
509 /// &samples,
510 /// );
511 ///
512 /// // `total_correction` is the integrated clock-drift + Shapiro delay
513 /// // to be added to the Newtonian geometric light time.
514 /// ```
515 ///
516 /// Using a custom central body (e.g. near another star or planet):
517 ///
518 /// ```ignore
519 /// let custom_context = LightContext::from_grav_param(star_gm); // GM in m³/s²
520 /// let correction = tx.one_way_relativistic_delay_integrated(
521 /// rx, custom_context, &samples
522 /// );
523 /// ```
524 ///
525 /// # Multi-body and exotic environments
526 ///
527 /// This function is the recommended choice when a signal passes near multiple
528 /// massive bodies (two or more stars, a star and a planet, binary black holes,
529 /// etc.) or when operating in strong gravitational fields or highly dynamic
530 /// spacetimes. Because you supply your own [`LocalSpacetime`] snapshots, each
531 /// sample can incorporate the combined gravitational potential, velocity, and
532 /// curvature from every relevant body in your model.
533 ///
534 /// In contrast, the faster [`one_way_relativistic_delay_to`] function only
535 /// supports a single central mass via `LightContext`. For complex geometries
536 /// or high-fidelity simulations, this integrated method provides greater
537 /// accuracy and flexibility.
538 pub const fn one_way_relativistic_delay_integrated(
539 &self,
540 rx: ObserverState,
541 context: LightContext,
542 samples: &[LocalSpacetime],
543 ) -> TSpan {
544 if samples.is_empty() {
545 return self.one_way_relativistic_delay_to(rx, context);
546 }
547
548 let dt_sec = rx.time.to_tai_since(self.time).to_sec_f();
549
550 let num_samples = samples.len();
551 let n = f!(num_samples);
552 let h = f!(1.0) / n;
553 let mut s = f!(0.0);
554
555 let mut i = 0;
556 while i < num_samples {
557 let local = samples[i];
558 let drift = ClockDrift::from_local_spacetime(&local);
559 let rate_offset = drift.rate().to_sec_f();
560
561 let coeff = if i == 0 || i == num_samples - 1 {
562 f!(1.0)
563 } else if i % 2 == 0 {
564 f!(2.0)
565 } else {
566 f!(4.0)
567 };
568 s += coeff * rate_offset;
569
570 i += 1;
571 }
572
573 let integrated_drift_sec = (h / f!(3.0)) * s * dt_sec;
574
575 let r_tx = self.position.norm();
576 let r_rx = rx.position.norm();
577 let r_sep = self.position.distance_to(rx.position);
578 let shapiro = Self::shapiro_one_way_delay(context, r_tx, r_rx, r_sep);
579
580 TSpan::from_sec_f(integrated_drift_sec).add(shapiro)
581 }
582
583 /// Computes the relativistic correction for a two-way round-trip ranging measurement
584 /// (transmit → receive → immediate transponder reply).
585 ///
586 /// Deep-space networks measure distance by timing a round-trip signal. This function
587 /// returns the tiny relativistic adjustment that must be **subtracted** from the raw
588 /// measured round-trip time to recover the true geometric distance.
589 ///
590 /// # Parameters
591 /// - `self` – the transmitter’s relativistic state at send time
592 /// - `round_trip_measured` – the raw measured round-trip duration (in seconds)
593 /// - `rx` – the receiver’s relativistic state (its `time` field is ignored)
594 /// - `context` – gravitational context for the Shapiro delay (see [`one_way_relativistic_delay_to`])
595 ///
596 /// # Returns
597 /// A [`TSpan`] (in seconds) that must be **subtracted** from the measured round-trip time.
598 ///
599 /// # Examples
600 ///
601 /// Typical usage for deep-space two-way ranging (e.g. Earth to spacecraft or planet via DSN):
602 ///
603 /// ```no_run
604 /// use deep_time::{
605 /// ObserverState, Position, Velocity, Dt, TSpan, LightContext,
606 /// };
607 ///
608 /// # let tx_time: Dt = todo!();
609 /// # let tx_pos: Position = todo!();
610 /// # let tx_vel: Velocity = todo!();
611 /// # let tx_potential: f64 = todo!();
612 /// # let rx_pos: Position = todo!();
613 /// # let rx_vel: Velocity = todo!();
614 /// # let rx_potential: f64 = todo!();
615 /// # let measured_round_trip: TSpan = todo!(); // from your ranging hardware / DSN
616 ///
617 /// let transmitter = ObserverState::new(
618 /// tx_time,
619 /// tx_pos,
620 /// tx_vel,
621 /// tx_potential,
622 /// );
623 ///
624 /// // Receiver state at approximate arrival time (its `.time` field is ignored)
625 /// let receiver_approx = ObserverState::new(
626 /// Dt::default(), // dummy time - will be ignored
627 /// rx_pos,
628 /// rx_vel,
629 /// rx_potential,
630 /// );
631 ///
632 /// let relativistic_correction = transmitter.round_trip_relativistic_correction(
633 /// measured_round_trip,
634 /// receiver_approx,
635 /// LightContext::SOLAR,
636 /// );
637 ///
638 /// // Correct the measured round-trip time:
639 /// let corrected_round_trip = measured_round_trip.sub(relativistic_correction);
640 ///
641 /// // Then the true geometric one-way light time and distance can be computed from
642 /// // `corrected_round_trip / 2`.
643 /// ```
644 ///
645 /// Using a custom gravitational context (e.g. ranging to a probe near Jupiter):
646 ///
647 /// ```ignore
648 /// let jupiter_context = LightContext::from_grav_param(jupiter_gm); // GM in m³/s²
649 /// let correction = tx.round_trip_relativistic_correction(
650 /// measured, rx_approx, jupiter_context
651 /// );
652 /// ```
653 pub const fn round_trip_relativistic_correction(
654 &self,
655 round_trip_measured: TSpan,
656 rx: ObserverState,
657 context: LightContext,
658 ) -> TSpan {
659 let one_way_approx = round_trip_measured.div_by_2();
660 let rx_approx = ObserverState {
661 time: self.time.add(one_way_approx),
662 ..rx
663 };
664
665 let one_way_delay = self.one_way_relativistic_delay_to(rx_approx, context);
666 one_way_delay.add(one_way_delay)
667 }
668
669 /// First-order one-way Shapiro delay (gravitational light-time delay) caused by a
670 /// central point mass.
671 ///
672 /// This is an internal helper used by the public delay functions. It implements the
673 /// standard logarithmic formula used in solar-system navigation and pulsar timing.
674 const fn shapiro_one_way_delay(
675 context: LightContext,
676 r_tx: Real,
677 r_rx: Real,
678 r_sep: Real,
679 ) -> TSpan {
680 if context.two_grav_param_over_c3 == f!(0.0)
681 || r_tx <= f!(0.0)
682 || r_rx <= f!(0.0)
683 || r_sep <= f!(0.0)
684 {
685 return TSpan::ZERO;
686 }
687
688 let arg = (r_tx + r_rx + r_sep) / (r_tx + r_rx - r_sep).max(f!(1.0));
689 let delay_sec = context.two_grav_param_over_c3 * log(arg);
690
691 TSpan::from_sec_f(delay_sec)
692 }
693}