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`](crate::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}