deep_time/dt/trajectory.rs
1use crate::{C_SQUARED, Drift, Dt, DtErr, DtErrKind, Real, Spacetime, Velocity, an_err};
2
3impl Dt {
4 /// Computes the accumulated proper time along a trajectory given a sequence
5 /// of physical states.
6 ///
7 /// This function accepts samples expressed in terms of directly observable
8 /// quantities — coordinate time, velocity, and gravitational potential —
9 /// and integrates the proper time (Δτ) along the path. It is a convenience
10 /// wrapper around the core [`proper_time_from_path`] routine.
11 ///
12 /// The integration is performed using the trapezoidal rule applied to the
13 /// instantaneous proper-time rate between consecutive samples. This approach
14 /// is standard for high-precision clock modeling in astrodynamics and
15 /// relativistic timing applications.
16 ///
17 /// A single sample, or multiple samples at identical times, produces a result
18 /// of zero (no time has elapsed). An empty iterator also returns zero.
19 ///
20 /// ## Parameters
21 ///
22 /// - `samples`: Iterator yielding `(coordinate_time, velocity, gravitational_potential)`
23 /// triples. The coordinate times must be monotonically non-decreasing.
24 /// **It is the caller’s responsibility** to supply samples that cover the
25 /// desired time interval. The function does not validate that the first or
26 /// last sample exactly matches any particular start or end time.
27 /// - `characteristic_length_scale`: Controls whether the weak-field or
28 /// strong-field formulation is used when constructing the local spacetime
29 /// state.
30 ///
31 /// Pass `0.0` (the normal choice) for all conventional weak-field work
32 /// (Earth orbit, GNSS, solar-system navigation, most spacecraft). This
33 /// produces exactly the classic relativistic clock rate used by JPL, ESA,
34 /// and GNSS systems, with the Kretschmann scalar set to zero.
35 ///
36 /// Supply a positive value (in meters) only when you need the library’s
37 /// intrinsic Planck-scale saturation term. The value should represent the
38 /// characteristic length scale over which the gravitational field varies
39 /// significantly at the observer’s location. This is intended for strong-field
40 /// regimes such as the vicinity of neutron stars or black-hole event horizons.
41 ///
42 /// ## Returns
43 ///
44 /// `Ok(total_proper_time)` — the total proper time (Δτ) that has accumulated
45 /// for an observer following the trajectory defined by the supplied samples,
46 /// returned as a [`Dt`].
47 ///
48 /// This value represents the actual time that would have elapsed on a physical
49 /// clock moving along the path, including all relativistic effects (velocity
50 /// and gravitational time dilation, plus the Planck-scale saturation term when
51 /// active). It is **not** a drift or difference relative to coordinate time.
52 /// If you need the difference between proper time and coordinate time
53 /// (Δτ − Δt), use [`proper_time_drift_from_states`] instead.
54 ///
55 /// `Err(DtErr)` — if the coordinate times are not monotonically non-decreasing.
56 pub fn proper_time_from_states<I>(
57 samples: I,
58 characteristic_length_scale: Real,
59 ) -> Result<Self, DtErr>
60 where
61 I: IntoIterator<Item = (Self, Velocity, Real)>,
62 {
63 let path_iter = samples.into_iter().map(|(t, vel, phi)| {
64 let phi_over_c2 = phi / C_SQUARED;
65 let ls = Spacetime::from_potential_velocity_and_scale(
66 phi_over_c2,
67 vel,
68 characteristic_length_scale,
69 );
70 (t, ls)
71 });
72
73 Self::proper_time_from_path(path_iter)
74 }
75
76 /// Computes the relativistic clock drift (proper time minus coordinate time)
77 /// over a specific interval.
78 ///
79 /// This returns how much a physical clock has gained or lost time compared
80 /// with coordinate time between `start` and `end`.
81 ///
82 /// - A positive result means the onboard clock ran **fast** (it accumulated
83 /// more proper time than the coordinate interval).
84 /// - A negative result means the onboard clock ran **slow** (it accumulated
85 /// less proper time than the coordinate interval).
86 ///
87 /// This is the higher-level function most callers should use when they need
88 /// the net drift over a well-defined time interval. It internally calls
89 /// [`proper_time_from_states`] to integrate proper time along the supplied
90 /// trajectory and then subtracts the requested coordinate time span.
91 ///
92 /// ## Parameters
93 ///
94 /// - `start`: Starting coordinate time of the interval.
95 /// - `end`: Ending coordinate time of the interval.
96 /// - `states`: Iterator of physical states in the form
97 /// `(coordinate_time, velocity, gravitational_potential)`.
98 /// Coordinate times must be monotonically **non-decreasing**.
99 /// **It is the caller’s responsibility** to ensure the provided states
100 /// cover the time range from `start` to `end`. The function integrates
101 /// proper time over whatever samples are supplied and subtracts the
102 /// requested coordinate interval (`end - start`). Exact matching of the
103 /// first and last state times to `start` and `end` is **not** validated.
104 /// - `characteristic_length_scale`: Controls the weak-field vs strong-field
105 /// formulation when constructing local spacetime states (see
106 /// [`proper_time_from_states`] for full details). Pass `0.0` for all normal
107 /// weak-field work (GNSS, Earth orbit, solar-system navigation). Supply a
108 /// positive length (in meters) only when strong-field Planck-scale
109 /// saturation effects are required.
110 ///
111 /// ## Returns
112 ///
113 /// `Ok(drift)` — the accumulated drift (`Δτ − Δt`) as a [`Dt`].
114 ///
115 /// `Err(DtErr)` — if the coordinate times in `states` are not monotonically
116 /// non-decreasing.
117 pub fn proper_time_drift_from_states<I>(
118 start: Dt,
119 end: Dt,
120 states: I,
121 characteristic_length_scale: Real,
122 ) -> Result<Dt, DtErr>
123 where
124 I: IntoIterator<Item = (Self, Velocity, Real)>,
125 {
126 if start.eq(&end) {
127 return Ok(Dt::ZERO);
128 }
129 let dtau = Self::proper_time_from_states(states, characteristic_length_scale)?;
130 Ok(dtau.sub(end.to_diff_raw(start)))
131 }
132
133 /// Computes accumulated proper time along an arbitrary trajectory.
134 ///
135 /// This is the core integration function of the library. It walks the
136 /// supplied path segment by segment and applies the trapezoidal rule
137 /// to the instantaneous proper-time rate at each step.
138 ///
139 /// This approach is commonly used when integrating clock rates along
140 /// sampled trajectories in astrodynamics and high-precision timing work.
141 ///
142 /// The function enforces that coordinate times are monotonically
143 /// non-decreasing (equal times are allowed). It performs a single pass
144 /// with no heap allocation.
145 ///
146 /// ## Parameters
147 ///
148 /// - `path`: An iterator of `(coordinate_time, Spacetime)` pairs.
149 /// Coordinate times must be monotonically non-decreasing.
150 ///
151 /// ## Returns
152 ///
153 /// `Ok(total_proper_time)` — the accumulated proper time (Δτ) as a [`Dt`].
154 /// Returns `ZERO` if the iterator is empty (no time elapsed).
155 ///
156 /// `Err(DtErr)` — if the path contains any decrease in coordinate time
157 /// (i.e., a later sample has a strictly earlier coordinate time than a
158 /// previous sample).
159 pub fn proper_time_from_path<I>(path: I) -> Result<Self, DtErr>
160 where
161 I: IntoIterator<Item = (Self, Spacetime)>,
162 {
163 let mut iter = path.into_iter();
164
165 let Some((mut prev_t, mut prev_ls)) = iter.next() else {
166 return Ok(Self::ZERO);
167 };
168
169 let mut accumulated = Self::ZERO;
170
171 for (t, ls) in iter {
172 if t.lt(&prev_t) {
173 return Err(an_err!(
174 DtErrKind::InvalidInput,
175 "proper_time_from_path requires monotonically non-decreasing coordinate times"
176 ));
177 }
178
179 let dt = t.to_diff_raw(prev_t);
180 if !dt.is_zero() {
181 let sign = if dt.to_attos() < 0 { f!(-1.0) } else { f!(1.0) };
182 let dt_pos = if sign < f!(0.0) { dt.neg() } else { dt };
183 let dt_sec = dt_pos.to_sec_f();
184
185 let rate0 = Self::rate_from_local(&prev_ls);
186 let rate1 = Self::rate_from_local(&ls);
187
188 let integral = f!(0.5) * (rate0 + rate1 - f!(2.0)) * dt_sec;
189 let dtau_segment = Dt::span_f(sign * (dt_sec + integral));
190
191 accumulated = accumulated.add(dtau_segment);
192 }
193
194 prev_t = t;
195 prev_ls = ls;
196 }
197
198 Ok(accumulated)
199 }
200
201 /// Computes proper time advance over an interval when the proper-time rate
202 /// is constant.
203 ///
204 /// This method is intended for trajectory segments where the physical
205 /// conditions remain unchanged, such as:
206 ///
207 /// - a fixed ground station,
208 /// - a circular orbit, or
209 /// - a deep-space cruise phase with constant velocity and gravitational potential.
210 ///
211 /// It is mathematically equivalent to integrating a constant rate using
212 /// the trapezoidal rule in [`proper_time_from_path`], but is more efficient
213 /// and makes the caller's intent explicit.
214 ///
215 /// The method is called on the starting coordinate time (`self`). It
216 /// calculates the coordinate time interval to `end` and multiplies it by
217 /// the supplied constant rate `dtau_dt`.
218 ///
219 /// ## Parameters
220 ///
221 /// - `end`: Ending coordinate time of the interval.
222 /// - `dtau_dt`: Constant proper-time rate (dimensionless). In relativistic
223 /// contexts this value is typically slightly less than `1.0`. The caller
224 /// is responsible for providing an appropriate rate (for example, from
225 /// `Drift::proper_time_rate` or a precomputed constant).
226 ///
227 /// ## Returns
228 ///
229 /// The accumulated proper time advance (Δτ) over the interval as a [`Dt`].
230 ///
231 /// If `end` occurs before `self`, the result will be negative.
232 #[inline]
233 pub const fn proper_time_between_constant_rate(self, end: Dt, dtau_dt: Real) -> Dt {
234 let dt_sec = end.to_diff_raw(self).to_sec_f();
235 Dt::span_f(dtau_dt * dt_sec)
236 }
237
238 /// Returns the instantaneous proper-time rate (dτ/dt) from a local
239 /// spacetime state.
240 ///
241 /// This is a private helper used by the integration routines.
242 #[inline]
243 const fn rate_from_local(spacetime: &Spacetime) -> Real {
244 let drift = Drift::from_spacetime(spacetime);
245 f!(1.0) + drift.rate.to_sec_f()
246 }
247}