Skip to main content

deep_time/physics/observer/
mod.rs

1//! Observer state at an instant.
2
3mod light_time;
4
5use crate::{C_SQUARED, Dt, Position, Real, Spacetime, Velocity};
6
7/// An observer at a specific instant.
8///
9/// Combines time, position, velocity, and local gravitational
10/// information. It is the main input type used by relativistic light-time
11/// methods in this library.
12#[derive(Clone, Copy, Debug, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
15pub struct Observer {
16    /// The time of this observer.
17    ///
18    /// Any [`Scale`] is accepted. This time is treated as coordinate time.
19    pub time: Dt,
20
21    /// Position of the observer in meters.
22    ///
23    /// Typically expressed in a barycentric (solar-system barycenter) or
24    /// heliocentric frame, depending on the application.
25    pub position: Position,
26
27    /// Velocity of the observer in meters per second.
28    pub velocity: Velocity,
29
30    /// Newtonian gravitational potential Φ at the observer’s location
31    /// (in m² s⁻²).
32    ///
33    /// This value is usually negative for bound orbits. It should normally
34    /// include contributions from the Sun and all relevant planets.
35    pub grav_potential_m2_s2: Real,
36
37    /// Characteristic length scale (in meters) over which the gravitational
38    /// field varies significantly at this location.
39    ///
40    /// - Use `0.0` (the default) for all solar-system, GNSS, and weak-field
41    ///   applications.
42    /// - Provide a non-zero value only when working in strong gravitational
43    ///   fields (e.g. near neutron stars or black holes), where the library’s
44    ///   higher-order curvature terms become relevant.
45    pub characteristic_length_scale: Real,
46}
47
48impl Observer {
49    /// Creates a new `Observer` for typical solar-system, GNSS,
50    /// or weak-field use.
51    ///
52    /// This is the recommended constructor for most applications.
53    /// It sets the `characteristic_length_scale` to `0.0`, which disables
54    /// higher-order curvature terms in the proper-time model.
55    ///
56    /// ## Parameters
57    ///
58    /// - `time`: The time of the observer.
59    /// - `position`: Position in meters (usually barycentric or heliocentric).
60    /// - `velocity`: Velocity in m/s.
61    /// - `grav_potential_m2_s2`: Newtonian gravitational potential Φ
62    ///   at the location (in m²/s²).
63    #[inline]
64    pub const fn new(
65        time: Dt,
66        position: Position,
67        velocity: Velocity,
68        grav_potential_m2_s2: Real,
69    ) -> Observer {
70        Self {
71            time,
72            position,
73            velocity,
74            grav_potential_m2_s2,
75            characteristic_length_scale: 0.0,
76        }
77    }
78
79    /// Returns the instantaneous proper-time rate `dτ/dt` for this observer.
80    ///
81    /// This value indicates how fast a physical clock located at this observer
82    /// would advance relative to the time used by this `Observer`.
83    /// A returned value of `1.0` means the clock advances at the same rate
84    /// as the observer's time coordinate. Values are typically slightly different
85    /// from `1.0` due to the effects of velocity and gravitational potential.
86    ///
87    /// This rate is computed using the library’s unified proper-time model.
88    /// It is used internally for light-time corrections and Doppler calculations.
89    #[inline]
90    pub const fn proper_time_rate(&self) -> Real {
91        Spacetime::from_potential_velocity_and_scale(
92            self.grav_potential_m2_s2 / C_SQUARED,
93            self.velocity,
94            self.characteristic_length_scale,
95        )
96        .proper_time_rate()
97    }
98
99    /// Returns the ratio of proper time rates between the receiver and transmitter
100    /// for a one-way signal.
101    ///
102    /// This method computes:
103    ///
104    /// ```text
105    /// ratio = rx.proper_time_rate() / self.proper_time_rate()
106    /// ```
107    ///
108    /// ### Interpretation
109    ///
110    /// - A value of `1.0` indicates that both clocks run at the same rate.
111    /// - A value **less than `1.0`** means the receiver’s clock runs slower than
112    ///   the transmitter’s clock. The receiver will observe a lower frequency
113    ///   than was emitted.
114    /// - A value **greater than `1.0`** means the receiver’s clock runs faster
115    ///   than the transmitter’s clock. The receiver will observe a higher frequency
116    ///   than was emitted.
117    ///
118    /// The ratio captures the combined effect of special-relativistic time dilation
119    /// (due to velocity) and general-relativistic gravitational time dilation.
120    ///
121    /// ### Typical Usage (One-Way)
122    ///
123    /// This ratio is often combined with the classical kinematic Doppler term
124    /// to estimate the total one-way frequency shift:
125    ///
126    /// ```text
127    /// approximate_frequency_shift ≈ ratio * (1 - v_radial / C)
128    /// ```
129    ///
130    /// where `v_radial` is the radial velocity (positive when the receiver is
131    /// receding).
132    ///
133    /// ### Two-Way Usage
134    ///
135    /// For round-trip (two-way) measurements, square the one-way ratio:
136    ///
137    /// ```rust
138    /// use deep_time::{Dt, Observer, Position, Spacetime, Velocity};
139    ///
140    /// let bodies = [
141    ///     (Position::from_au(0.0, 0.0, 0.0), 1.3271244e20), // Sun
142    ///     (Position::from_au(1.0, 0.0, 0.0), 3.9860044e14), // Earth
143    /// ];
144    ///
145    /// let tx_pos = Position::from_au(1.0, 0.0, 0.0);
146    /// let rx_pos = Position::from_au(1.00257, 0.0, 0.0);
147    ///
148    /// let grav_potential_tx = Spacetime::grav_potential_from_point_masses(tx_pos, bodies.iter().copied());
149    /// let grav_potential_rx = Spacetime::grav_potential_from_point_masses(rx_pos, bodies.iter().copied());
150    ///
151    /// let transmitter = Observer::new(
152    ///     Dt::span_f(0.0),
153    ///     tx_pos,
154    ///     Velocity::ZERO,
155    ///     grav_potential_tx,
156    /// );
157    ///
158    /// let receiver = Observer::new(
159    ///     Dt::span_f(0.0),
160    ///     rx_pos,
161    ///     Velocity::from_speed(800.0),
162    ///     grav_potential_rx,
163    /// );
164    ///
165    /// let one_way_ratio = transmitter.relativistic_clock_rate_ratio(receiver);
166    /// let two_way_ratio = one_way_ratio * one_way_ratio;
167    /// ```
168    ///
169    /// **Note:** Squaring the one-way ratio is a common first-order approximation.
170    /// For higher precision (especially during flybys or when uplink and downlink
171    /// geometries differ significantly), consider using
172    /// [`round_trip_light_time_correction`](Self::round_trip_light_time_correction)
173    /// instead.
174    ///
175    /// This pattern is commonly used when correcting two-way Doppler (range-rate)
176    /// data for relativistic clock effects.
177    ///
178    /// ### Limitations
179    ///
180    /// - This method only accounts for the **difference in clock rates** between
181    ///   the two ends.
182    /// - It does **not** include Shapiro delay or higher-order relativistic effects
183    ///   on signal propagation.
184    /// - The combination with classical Doppler shown above is a first-order
185    ///   approximation.
186    ///
187    /// ## Parameters
188    ///
189    /// - `self` — Transmitter state at the time of transmission.
190    /// - `rx`   — Receiver state at the approximate time of reception.
191    #[inline]
192    pub const fn relativistic_clock_rate_ratio(&self, rx: Observer) -> Real {
193        rx.proper_time_rate() / self.proper_time_rate()
194    }
195}
196
197#[cfg(feature = "wire")]
198impl Observer {
199    /// Current wire format version.
200    pub const WIRE_VERSION: u8 = 1;
201
202    /// Size of the canonical wire representation in bytes.
203    pub const WIRE_SIZE: usize = 1 + Dt::WIRE_SIZE + Position::WIRE_SIZE + Velocity::WIRE_SIZE + 16;
204
205    /// Serializes this [`Observer`] into a fixed buffer.
206    ///
207    /// Layout:
208    /// - Byte 0: Version
209    /// - Bytes [1..]: time (Dt wire) + position (24) + velocity (24) + grav_potential (8) + char_length_scale (8)
210    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
211        let mut buf = [0u8; Self::WIRE_SIZE];
212        buf[0] = Self::WIRE_VERSION;
213
214        let mut offset = 1usize;
215
216        let time = self.time.to_wire_bytes();
217        buf[offset..offset + Dt::WIRE_SIZE].copy_from_slice(&time);
218        offset += Dt::WIRE_SIZE;
219
220        let pos = self.position.to_wire_bytes();
221        buf[offset..offset + Position::WIRE_SIZE].copy_from_slice(&pos);
222        offset += Position::WIRE_SIZE;
223
224        let vel = self.velocity.to_wire_bytes();
225        buf[offset..offset + Velocity::WIRE_SIZE].copy_from_slice(&vel);
226        offset += Velocity::WIRE_SIZE;
227
228        buf[offset..offset + 8].copy_from_slice(&self.grav_potential_m2_s2.to_le_bytes());
229        offset += 8;
230
231        buf[offset..offset + 8].copy_from_slice(&self.characteristic_length_scale.to_le_bytes());
232
233        buf
234    }
235
236    /// Deserializes an [`Observer`] from exactly `WIRE_SIZE` bytes.
237    ///
238    /// Returns `None` if the version is unknown or any component is invalid.
239    ///
240    /// ## Security
241    ///
242    /// Safe for untrusted input. Fixed size with layered validation of inner types.
243    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
244        if bytes.len() != Self::WIRE_SIZE {
245            return None;
246        }
247
248        if bytes[0] != Self::WIRE_VERSION {
249            return None;
250        }
251
252        let mut offset = 1usize;
253
254        let time = Dt::from_wire_bytes(&bytes[offset..offset + Dt::WIRE_SIZE])?;
255        offset += Dt::WIRE_SIZE;
256
257        let position = Position::from_wire_bytes(&bytes[offset..offset + Position::WIRE_SIZE])?;
258        offset += Position::WIRE_SIZE;
259
260        let velocity = Velocity::from_wire_bytes(&bytes[offset..offset + Velocity::WIRE_SIZE])?;
261        offset += Velocity::WIRE_SIZE;
262
263        let grav_potential_m2_s2 = Real::from_le_bytes([
264            bytes[offset],
265            bytes[offset + 1],
266            bytes[offset + 2],
267            bytes[offset + 3],
268            bytes[offset + 4],
269            bytes[offset + 5],
270            bytes[offset + 6],
271            bytes[offset + 7],
272        ]);
273        offset += 8;
274
275        let characteristic_length_scale = Real::from_le_bytes([
276            bytes[offset],
277            bytes[offset + 1],
278            bytes[offset + 2],
279            bytes[offset + 3],
280            bytes[offset + 4],
281            bytes[offset + 5],
282            bytes[offset + 6],
283            bytes[offset + 7],
284        ]);
285
286        Some(Self {
287            time,
288            position,
289            velocity,
290            grav_potential_m2_s2,
291            characteristic_length_scale,
292        })
293    }
294}