deep_time/physics/trajectory.rs
1//! Proper-time integration methods on [`Dt`] (see the public method docs).
2//!
3//! Overview and which-function guide:
4//! [docs/trajectory.md](https://github.com/ragardner/deep-time/blob/main/docs/trajectory.md).
5
6use crate::macros::from_sec_f;
7use crate::{C_SQUARED, Dt, DtErr, DtErrKind, Real, an_err};
8
9use super::{Drift, Spacetime, Velocity};
10
11impl Dt {
12 /// Integrate proper time along samples of time, velocity, and gravitational potential.
13 ///
14 /// Walks a list of vehicle states and estimates how much time a clock on that
15 /// path would accumulate over the **full** sample span (first time to last).
16 /// For a named arc inside a longer file, use
17 /// [`Dt::proper_time_from_states_between`](#method.proper_time_from_states_between).
18 ///
19 /// Guide: [docs/trajectory.md](https://github.com/ragardner/deep-time/blob/main/docs/trajectory.md).
20 ///
21 /// ## When to use it
22 ///
23 /// - Δτ over **exactly the samples you pass** (first sample to last).
24 /// - Not a sub-interval of a longer arc (use
25 /// [`Dt::proper_time_from_states_between`](#method.proper_time_from_states_between)).
26 ///
27 /// ## Inputs
28 ///
29 /// Each sample is `(coordinate_time, velocity, gravitational_potential)`:
30 ///
31 /// - **time** — mission / ephemeris epoch as a [`Dt`]
32 /// - **velocity** — m/s in the same frame convention you used for potential
33 /// - **potential Φ** — SI units **m²/s²** (typically negative near a planet).
34 /// Do **not** pass Φ/c² here; this API divides by \(c^2\) internally.
35 ///
36 /// Times must be non-decreasing. Empty or single-point paths yield zero.
37 /// Non-monotonic times yield [`DtErrKind::NonMonotonic`].
38 ///
39 /// ## `characteristic_length_scale`
40 ///
41 /// Pass **`0.0`** for Earth orbit, GNSS, cislunar, and similar work. That sets
42 /// curvature to zero and uses the usual weak-field clock rate from Φ and \(v\).
43 ///
44 /// Pass a positive length in meters only if you intentionally want the
45 /// library’s optional curvature estimate (see
46 /// [`Spacetime::kretschmann_from_potential_and_scale`]).
47 ///
48 /// ## Example
49 ///
50 /// ```rust
51 /// use deep_time::{Dt, Scale};
52 /// use deep_time::physics::Velocity;
53 ///
54 /// let t0 = Dt::from_sec(0, Scale::TAI, Scale::TAI);
55 /// let t1 = Dt::from_sec(3600, Scale::TAI, Scale::TAI);
56 /// // Example Earth-surface-scale |Φ| (m²/s²); use your model in production
57 /// let phi = -6.25e7;
58 /// let samples = [
59 /// (t0, Velocity::ZERO, phi),
60 /// (t1, Velocity::from_speed(0.0), phi),
61 /// ];
62 /// let dtau = Dt::proper_time_from_states(samples, 0.0).expect("monotonic");
63 /// assert!(dtau.to_sec_f() > 0.0 && dtau.to_sec_f() < 3600.0);
64 /// ```
65 ///
66 /// ## See also
67 ///
68 /// - [`Dt::proper_time_from_states_between`](#method.proper_time_from_states_between) — named interval `[start, end]`
69 /// - [`Dt::proper_time_drift_from_states`](#method.proper_time_drift_from_states) — gain/loss vs coordinate time
70 /// - [`Dt::proper_time_from_path`](#method.proper_time_from_path) — same integral if you already have [`Spacetime`]
71 pub fn proper_time_from_states<I>(
72 samples: I,
73 characteristic_length_scale: Real,
74 ) -> Result<Self, DtErr>
75 where
76 I: IntoIterator<Item = (Self, Velocity, Real)>,
77 {
78 Self::proper_time_from_path(Self::states_to_path(samples, characteristic_length_scale))
79 }
80
81 /// Proper time Δτ on a named mission arc `[start, end]`.
82 ///
83 /// Same idea as [`Dt::proper_time_from_states`](#method.proper_time_from_states), but only the window
84 /// `[start, end]` is integrated. Extra samples outside that window are
85 /// ignored except as neighbors for interpolation at the endpoints.
86 ///
87 /// Example question: how much proper time has the onboard clock accumulated
88 /// between two GET epochs when the trajectory file is longer than that arc.
89 ///
90 /// ## Coverage and errors
91 ///
92 /// Samples must **cover** `[start, end]`:
93 /// - at least one sample at or before `start`, and
94 /// - the path must reach at least as far as `end`.
95 ///
96 /// - [`DtErrKind::Incomplete`] — empty path (when `start ≠ end`) or incomplete coverage
97 /// - [`DtErrKind::OutOfRange`] — `end < start`
98 /// - [`DtErrKind::NonMonotonic`] — a later sample has an earlier time
99 ///
100 /// ## Example
101 ///
102 /// ```rust
103 /// use deep_time::{Dt, Scale};
104 /// use deep_time::physics::Velocity;
105 ///
106 /// let t0 = Dt::from_sec(0, Scale::TAI, Scale::TAI);
107 /// let t1 = Dt::from_sec(10_000, Scale::TAI, Scale::TAI);
108 /// // Flat spacetime via Φ = 0 → rate = 1
109 /// let samples = [
110 /// (t0, Velocity::ZERO, 0.0),
111 /// (t1, Velocity::ZERO, 0.0),
112 /// ];
113 /// let start = Dt::from_sec(1000, Scale::TAI, Scale::TAI);
114 /// let end = Dt::from_sec(4600, Scale::TAI, Scale::TAI);
115 /// let dtau = Dt::proper_time_from_states_between(start, end, samples, 0.0)
116 /// .expect("samples cover the arc");
117 /// assert_eq!(dtau, Dt::from_sec(3600, Scale::TAI, Scale::TAI));
118 /// ```
119 ///
120 /// ## See also
121 ///
122 /// - [`Dt::proper_time_drift_from_states`](#method.proper_time_drift_from_states) — same window, but Δτ − Δt
123 /// - [`Dt::proper_time_from_path_between`](#method.proper_time_from_path_between) — if samples are already [`Spacetime`]
124 pub fn proper_time_from_states_between<I>(
125 start: Dt,
126 end: Dt,
127 states: I,
128 characteristic_length_scale: Real,
129 ) -> Result<Dt, DtErr>
130 where
131 I: IntoIterator<Item = (Self, Velocity, Real)>,
132 {
133 Self::proper_time_from_path_between(
134 start,
135 end,
136 Self::states_to_path(states, characteristic_length_scale),
137 )
138 }
139
140 /// Clock drift vs coordinate time on `[start, end]`: Δτ − (end − start).
141 ///
142 /// Did the vehicle clock run fast or slow compared to the mission timeline
143 /// over a chosen interval?
144 ///
145 /// - **Positive** — clock accumulated more time than the coordinate interval
146 /// (ran fast).
147 /// - **Negative** — clock accumulated less (ran slow).
148 ///
149 /// Algebraically \(\int_{start}^{end}(r - 1)\,dt\). Implemented as
150 /// [`Dt::proper_time_from_states_between`](#method.proper_time_from_states_between) minus `(end − start)`.
151 ///
152 /// ## When to use it
153 ///
154 /// - Relativistic clock offset over an analysis arc
155 /// - Comparing an integrated model to a coordinate-time reference
156 /// - Not spacecraft-minus-ground (use
157 /// [`Dt::proper_time_differential_vs_rate`](#method.proper_time_differential_vs_rate) or
158 /// [`Dt::proper_time_differential_from_paths`](#method.proper_time_differential_from_paths))
159 ///
160 /// ## Inputs and errors
161 ///
162 /// Same sample layout as [`Dt::proper_time_from_states`](#method.proper_time_from_states):
163 /// `(time, velocity m/s, Φ m²/s²)`. Pass `characteristic_length_scale = 0.0`
164 /// for ordinary weak-field work. Coverage and error kinds match
165 /// [`Dt::proper_time_from_states_between`](#method.proper_time_from_states_between). `start == end` returns zero
166 /// without reading samples.
167 ///
168 /// ## Example
169 ///
170 /// ```rust
171 /// use deep_time::{Dt, Scale};
172 /// use deep_time::physics::Velocity;
173 ///
174 /// let t0 = Dt::from_sec(0, Scale::TAI, Scale::TAI);
175 /// let t1 = Dt::from_sec(86_400, Scale::TAI, Scale::TAI);
176 /// let phi = -6.25e7_f64;
177 /// let samples = [
178 /// (t0, Velocity::ZERO, phi),
179 /// (t1, Velocity::ZERO, phi),
180 /// ];
181 /// let drift = Dt::proper_time_drift_from_states(t0, t1, samples, 0.0).unwrap();
182 /// // Stationary in a potential well → clock runs slow vs coordinate time
183 /// assert!(drift.to_sec_f() < 0.0);
184 /// ```
185 pub fn proper_time_drift_from_states<I>(
186 start: Dt,
187 end: Dt,
188 states: I,
189 characteristic_length_scale: Real,
190 ) -> Result<Dt, DtErr>
191 where
192 I: IntoIterator<Item = (Self, Velocity, Real)>,
193 {
194 if start.eq(&end) {
195 return Ok(Dt::ZERO);
196 }
197 let dtau =
198 Self::proper_time_from_states_between(start, end, states, characteristic_length_scale)?;
199 Ok(dtau.sub(end.to_diff_raw(start)))
200 }
201
202 /// Integrate proper time along a path of [`Spacetime`] snapshots.
203 ///
204 /// Same as [`Dt::proper_time_from_states`](#method.proper_time_from_states), but each sample is already a
205 /// full local state `(α, β, curvature)` instead of `(v, Φ)`.
206 ///
207 /// ## When to use it
208 ///
209 /// - You already built [`Spacetime`] values (tests, precomputed rates, custom α/β).
210 /// - Prefer [`Dt::proper_time_from_states`](#method.proper_time_from_states) if you have velocity and potential.
211 ///
212 /// Integrates over the **full** sample span. For a named arc, use
213 /// [`Dt::proper_time_from_path_between`](#method.proper_time_from_path_between).
214 ///
215 /// Empty path or a single point → [`Dt::ZERO`]. Non-monotonic times →
216 /// [`DtErrKind::NonMonotonic`].
217 ///
218 /// ## Example
219 ///
220 /// ```rust
221 /// use deep_time::{Dt, Scale};
222 /// use deep_time::physics::Spacetime;
223 ///
224 /// let t0 = Dt::from_sec(0, Scale::TAI, Scale::TAI);
225 /// let t1 = Dt::from_sec(1000, Scale::TAI, Scale::TAI);
226 /// // α = 0.9, at rest → rate 0.9, Δτ = 900 s
227 /// let slow = Spacetime::new(0.9, 0.0, 0.0);
228 /// let dtau = Dt::proper_time_from_path([(t0, slow.clone()), (t1, slow)]).unwrap();
229 /// assert_eq!(dtau, Dt::from_sec(900, Scale::TAI, Scale::TAI));
230 /// ```
231 pub fn proper_time_from_path<I>(path: I) -> Result<Self, DtErr>
232 where
233 I: IntoIterator<Item = (Self, Spacetime)>,
234 {
235 let mut iter = path.into_iter();
236
237 let Some((mut prev_t, mut prev_ls)) = iter.next() else {
238 return Ok(Self::ZERO);
239 };
240
241 let mut accumulated = Self::ZERO;
242
243 for (t, ls) in iter {
244 if t.lt(&prev_t) {
245 return Err(an_err!(DtErrKind::NonMonotonic));
246 }
247
248 let rate0 = Self::rate_from_local(&prev_ls);
249 let rate1 = Self::rate_from_local(&ls);
250 accumulated = accumulated.add(Self::proper_time_segment(prev_t, rate0, t, rate1));
251
252 prev_t = t;
253 prev_ls = ls;
254 }
255
256 Ok(accumulated)
257 }
258
259 /// Proper time Δτ on `[start, end]` for a path of [`Spacetime`] samples.
260 ///
261 /// Like [`Dt::proper_time_from_path`](#method.proper_time_from_path), but only over a chosen time window.
262 /// Between samples the clock rate is treated as linear (trapezoidal rule);
263 /// if `start` or `end` falls between samples, the rate is interpolated.
264 ///
265 /// Use this when your pipeline already stores α, β, and curvature instead of
266 /// raw Φ and \(v\). Coverage and error kinds match
267 /// [`Dt::proper_time_from_states_between`](#method.proper_time_from_states_between).
268 ///
269 /// ## Example
270 ///
271 /// ```rust
272 /// use deep_time::{Dt, Scale};
273 /// use deep_time::physics::Spacetime;
274 ///
275 /// let path = [
276 /// (Dt::from_sec(0, Scale::TAI, Scale::TAI), Spacetime::new(0.9, 0.0, 0.0)),
277 /// (Dt::from_sec(1000, Scale::TAI, Scale::TAI), Spacetime::new(0.9, 0.0, 0.0)),
278 /// ];
279 /// let start = Dt::from_sec(100, Scale::TAI, Scale::TAI);
280 /// let end = Dt::from_sec(900, Scale::TAI, Scale::TAI);
281 /// // 0.9 × 800 s = 720 s
282 /// let dtau = Dt::proper_time_from_path_between(start, end, path).unwrap();
283 /// assert_eq!(dtau, Dt::from_sec(720, Scale::TAI, Scale::TAI));
284 /// ```
285 pub fn proper_time_from_path_between<I>(start: Dt, end: Dt, path: I) -> Result<Dt, DtErr>
286 where
287 I: IntoIterator<Item = (Self, Spacetime)>,
288 {
289 let rates = path
290 .into_iter()
291 .map(|(t, ls)| (t, Self::rate_from_local(&ls)));
292 Self::integrate_rates_between(start, end, rates)
293 }
294
295 /// Difference in proper time between two paths over the same interval.
296 ///
297 /// How much more (or less) time did clock A accumulate than clock B over
298 /// `[start, end]`?
299 ///
300 /// Returns \(\Delta\tau_A - \Delta\tau_B\). Positive means A’s clock ran
301 /// ahead of B’s over that coordinate interval.
302 ///
303 /// ## When to use it
304 ///
305 /// - Two vehicles or two reconstructed trajectories
306 /// - Spacecraft path vs a **sampled** ground path (both as [`Spacetime`] series)
307 ///
308 /// For spacecraft vs a **fixed** ground rate (single number), prefer
309 /// [`Dt::proper_time_differential_vs_rate`](#method.proper_time_differential_vs_rate).
310 ///
311 /// ## Errors
312 ///
313 /// Both paths must cover `[start, end]`. Same error kinds as
314 /// [`Dt::proper_time_from_path_between`](#method.proper_time_from_path_between) (`Incomplete`, `OutOfRange`,
315 /// `NonMonotonic`). `start == end` returns zero.
316 ///
317 /// ## Example
318 ///
319 /// ```rust
320 /// use deep_time::{Dt, Scale};
321 /// use deep_time::physics::Spacetime;
322 ///
323 /// let t0 = Dt::from_sec(0, Scale::TAI, Scale::TAI);
324 /// let t1 = Dt::from_sec(1000, Scale::TAI, Scale::TAI);
325 /// let high = Spacetime::new(0.95, 0.0, 0.0); // less redshifted
326 /// let low = Spacetime::new(0.90, 0.0, 0.0);
327 /// let path_a = [(t0, high.clone()), (t1, high)];
328 /// let path_b = [(t0, low.clone()), (t1, low)];
329 /// let diff = Dt::proper_time_differential_from_paths(t0, t1, path_a, path_b).unwrap();
330 /// // 950 − 900 = +50 s
331 /// assert_eq!(diff, Dt::from_sec(50, Scale::TAI, Scale::TAI));
332 /// ```
333 pub fn proper_time_differential_from_paths<Ia, Ib>(
334 start: Dt,
335 end: Dt,
336 path_a: Ia,
337 path_b: Ib,
338 ) -> Result<Dt, DtErr>
339 where
340 Ia: IntoIterator<Item = (Self, Spacetime)>,
341 Ib: IntoIterator<Item = (Self, Spacetime)>,
342 {
343 if start.eq(&end) {
344 return Ok(Dt::ZERO);
345 }
346 let dtau_a = Self::proper_time_from_path_between(start, end, path_a)?;
347 let dtau_b = Self::proper_time_from_path_between(start, end, path_b)?;
348 Ok(dtau_a.sub(dtau_b))
349 }
350
351 /// Proper time of a path minus a constant reference clock rate over `[start, end]`.
352 ///
353 /// How much did the spacecraft clock pull ahead of (or fall behind) a steady
354 /// ground or reference clock?
355 ///
356 /// Returns \(\Delta\tau_{\mathrm{path}} - r_{\mathrm{ref}}\,(end - start)\).
357 /// Positive means the path clock accumulated more proper time than the
358 /// reference over the interval.
359 ///
360 /// ## When to use it
361 ///
362 /// - Onboard vs Earth-surface rate (mission clock differentials)
363 /// - Satellite vs a fixed geoid rate
364 /// - Any reference well modeled as **constant** \(r_{\mathrm{ref}}\)
365 ///
366 /// Get \(r_{\mathrm{ref}}\) from [`Spacetime::proper_time_rate`] for a
367 /// stationary ground [`Spacetime`], or from a documented conventional value.
368 ///
369 /// ## Errors
370 ///
371 /// Path must cover `[start, end]`. Same error kinds as
372 /// [`Dt::proper_time_from_path_between`](#method.proper_time_from_path_between). `start == end` returns zero.
373 ///
374 /// ## Example
375 ///
376 /// ```rust
377 /// use deep_time::{Dt, Scale};
378 /// use deep_time::physics::Spacetime;
379 ///
380 /// let t0 = Dt::from_sec(0, Scale::TAI, Scale::TAI);
381 /// let t1 = Dt::from_sec(100_000, Scale::TAI, Scale::TAI);
382 /// // Slightly higher rate than a deeper potential well
383 /// let sc = Spacetime::new(0.999_999_999_9, 0.0, 0.0);
384 /// let ground = Spacetime::new(0.999_999_999_3, 0.0, 0.0);
385 /// let path = [(t0, sc.clone()), (t1, sc)];
386 /// let diff = Dt::proper_time_differential_vs_rate(
387 /// t0,
388 /// t1,
389 /// path,
390 /// ground.proper_time_rate(),
391 /// )
392 /// .unwrap();
393 /// assert!(diff.to_sec_f() > 0.0);
394 /// ```
395 pub fn proper_time_differential_vs_rate<I>(
396 start: Dt,
397 end: Dt,
398 path: I,
399 ref_rate: Real,
400 ) -> Result<Dt, DtErr>
401 where
402 I: IntoIterator<Item = (Self, Spacetime)>,
403 {
404 if start.eq(&end) {
405 return Ok(Dt::ZERO);
406 }
407 let dtau = Self::proper_time_from_path_between(start, end, path)?;
408 let ref_dtau = start.proper_time_between_constant_rate(end, ref_rate);
409 Ok(dtau.sub(ref_dtau))
410 }
411
412 /// Proper time when the rate \(d\tau/dt\) is constant over an interval.
413 ///
414 /// If conditions do not change (same speed, same gravity), proper time is
415 /// just **rate × elapsed coordinate time**. No sample list needed.
416 ///
417 /// ## When to use it
418 ///
419 /// - Fixed ground station
420 /// - Circular orbit approximated as constant rate
421 /// - Deep-space cruise with nearly constant \(v\) and Φ
422 /// - Building the reference leg for
423 /// [`Dt::proper_time_differential_vs_rate`](#method.proper_time_differential_vs_rate)
424 ///
425 /// Called on the **start** time: `start.proper_time_between_constant_rate(end, rate)`.
426 /// If `end` is before `self`, the result is negative.
427 ///
428 /// ## Example
429 ///
430 /// ```rust
431 /// use deep_time::{Dt, Scale};
432 /// use deep_time::physics::Spacetime;
433 ///
434 /// let t0 = Dt::from_sec(0, Scale::TAI, Scale::TAI);
435 /// let t1 = Dt::from_sec(86_400, Scale::TAI, Scale::TAI);
436 /// let ground = Spacetime::new(0.999_999_999_3, 0.0, 0.0);
437 /// let dtau = t0.proper_time_between_constant_rate(t1, ground.proper_time_rate());
438 /// assert!(dtau.to_sec_f() > 0.0 && dtau.to_sec_f() < 86_400.0);
439 /// ```
440 #[inline]
441 pub const fn proper_time_between_constant_rate(self, end: Dt, dtau_dt: Real) -> Dt {
442 let dt_sec = end.to_diff_raw(self).to_sec_f();
443 from_sec_f!(dtau_dt * dt_sec)
444 }
445
446 // -----------------------------------------------------------------------
447 // Private helpers
448 // -----------------------------------------------------------------------
449
450 /// Maps `(t, velocity, Φ)` states to `(t, Spacetime)` using the library rate model.
451 fn states_to_path<I>(
452 samples: I,
453 characteristic_length_scale: Real,
454 ) -> impl Iterator<Item = (Self, Spacetime)>
455 where
456 I: IntoIterator<Item = (Self, Velocity, Real)>,
457 {
458 samples.into_iter().map(move |(t, vel, phi)| {
459 let phi_over_c2 = phi / C_SQUARED;
460 let ls = Spacetime::from_potential_velocity_and_scale(
461 phi_over_c2,
462 vel,
463 characteristic_length_scale,
464 );
465 (t, ls)
466 })
467 }
468
469 /// Shared kernel: integrate a piecewise-linear proper-time rate series over
470 /// the closed coordinate interval `[start, end]`.
471 ///
472 /// Returns absolute Δτ (not drift). Coverage and monotonicity rules match
473 /// the public `*_between` methods.
474 fn integrate_rates_between<I>(start: Dt, end: Dt, rates: I) -> Result<Dt, DtErr>
475 where
476 I: IntoIterator<Item = (Self, Real)>,
477 {
478 if start.eq(&end) {
479 return Ok(Dt::ZERO);
480 }
481 if end.lt(&start) {
482 return Err(an_err!(DtErrKind::OutOfRange));
483 }
484
485 let mut iter = rates.into_iter();
486
487 let Some((mut prev_t, mut prev_rate)) = iter.next() else {
488 return Err(an_err!(DtErrKind::Incomplete));
489 };
490
491 // Need a sample at or before `start` to evaluate the rate on the window.
492 if prev_t.gt(&start) {
493 return Err(an_err!(DtErrKind::Incomplete));
494 }
495
496 let mut accumulated = Self::ZERO;
497 // Once true, `(prev_t, prev_rate)` is the left endpoint of an open
498 // segment still inside the window (`start <= prev_t < end`).
499 let mut active = false;
500
501 for (t, rate) in iter {
502 if t.lt(&prev_t) {
503 return Err(an_err!(DtErrKind::NonMonotonic));
504 }
505
506 if !active {
507 if t.lt(&start) {
508 // Entirely before the window; slide forward.
509 prev_t = t;
510 prev_rate = rate;
511 continue;
512 }
513
514 // prev_t <= start <= t
515 let rate_start = if prev_t.eq(&start) {
516 prev_rate
517 } else if t.eq(&start) {
518 rate
519 } else {
520 Self::lerp_rate(prev_t, prev_rate, t, rate, start)
521 };
522
523 if t.lt(&end) {
524 accumulated =
525 accumulated.add(Self::proper_time_segment(start, rate_start, t, rate));
526 active = true;
527 prev_t = t;
528 prev_rate = rate;
529 continue;
530 }
531
532 // t >= end: the whole window lies inside this bracketing segment.
533 let rate_end = if t.eq(&end) {
534 rate
535 } else {
536 Self::lerp_rate(prev_t, prev_rate, t, rate, end)
537 };
538 accumulated =
539 accumulated.add(Self::proper_time_segment(start, rate_start, end, rate_end));
540 return Ok(accumulated);
541 }
542
543 // active: integrate from prev toward end
544 if t.lt(&end) {
545 accumulated =
546 accumulated.add(Self::proper_time_segment(prev_t, prev_rate, t, rate));
547 prev_t = t;
548 prev_rate = rate;
549 continue;
550 }
551
552 // t >= end
553 let rate_end = if t.eq(&end) {
554 rate
555 } else {
556 Self::lerp_rate(prev_t, prev_rate, t, rate, end)
557 };
558 accumulated =
559 accumulated.add(Self::proper_time_segment(prev_t, prev_rate, end, rate_end));
560 return Ok(accumulated);
561 }
562
563 // Exhausted samples without reaching `end`.
564 Err(an_err!(DtErrKind::Incomplete))
565 }
566
567 /// Trapezoidal proper-time advance over one coordinate segment.
568 ///
569 /// Uses the compensated form
570 /// \(\Delta\tau = \Delta t + \tfrac12(r_0 + r_1 - 2)\,\Delta t\)
571 /// so that the large \(\approx 1\) part of the rate does not cancel against
572 /// \(\Delta t\) in floating point. Supports a negative segment
573 /// (`t1 < t0`) for symmetry; callers that enforce monotonic times only see
574 /// non-negative \(\Delta t\).
575 #[inline]
576 const fn proper_time_segment(t0: Dt, rate0: Real, t1: Dt, rate1: Real) -> Dt {
577 let dt = t1.to_diff_raw(t0);
578 if dt.is_zero() {
579 return Self::ZERO;
580 }
581
582 let sign = if dt.to_attos() < 0 { f!(-1.0) } else { f!(1.0) };
583 let dt_pos = if sign < f!(0.0) { dt.neg() } else { dt };
584 let dt_sec = dt_pos.to_sec_f();
585
586 let integral = f!(0.5) * (rate0 + rate1 - f!(2.0)) * dt_sec;
587 from_sec_f!(sign * (dt_sec + integral))
588 }
589
590 /// Linearly interpolates the proper-time rate at coordinate time `t`,
591 /// assuming a piecewise-linear rate between `(t0, rate0)` and `(t1, rate1)`.
592 ///
593 /// Caller must ensure `t0 < t1` (non-zero span) and typically
594 /// `t0 < t < t1`.
595 #[inline]
596 const fn lerp_rate(t0: Dt, rate0: Real, t1: Dt, rate1: Real, t: Dt) -> Real {
597 let span = t1.to_diff_raw(t0).to_sec_f();
598 let frac = t.to_diff_raw(t0).to_sec_f() / span;
599 rate0 + frac * (rate1 - rate0)
600 }
601
602 /// Returns the instantaneous proper-time rate (dτ/dt) from a local
603 /// spacetime state.
604 #[inline]
605 const fn rate_from_local(spacetime: &Spacetime) -> Real {
606 let drift = Drift::from_spacetime(spacetime);
607 f!(1.0) + drift.rate.to_sec_f()
608 }
609}