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