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