Skip to main content

elevator_core/
eta.rs

1//! Travel-time estimation for trapezoidal velocity profiles.
2//!
3//! Given an elevator's kinematic parameters (max speed, acceleration,
4//! deceleration), an initial speed, and a remaining distance to a target,
5//! [`travel_time`] returns the seconds required to coast in, brake, and
6//! arrive at rest. Used by [`Simulation::eta`](crate::sim::Simulation::eta)
7//! and [`Simulation::best_eta`](crate::sim::Simulation::best_eta) to walk a
8//! destination queue and sum per-leg travel plus per-stop door dwell.
9//!
10//! The profile mirrors [`movement::tick_movement`](crate::movement::tick_movement)
11//! at the closed-form level — the per-tick integrator and the closed-form
12//! solver agree to within a tick on the same inputs. ETAs are estimates,
13//! not bit-exact: load/unload time, dispatch reordering, and door commands
14//! issued mid-trip will perturb the actual arrival.
15
16/// Closed-form travel time, in seconds, for a trapezoidal/triangular
17/// velocity profile from initial speed `v0` to a full stop over `distance`.
18///
19/// All inputs are unsigned magnitudes. Returns `0.0` for non-positive
20/// `distance` or non-positive kinematic parameters (defensive: a degenerate
21/// elevator can't reach anywhere, but we'd rather return a finite zero
22/// than `NaN` or an infinity).
23///
24/// `v0` is clamped to `[0.0, v_max]`; an elevator already moving faster
25/// than its current `max_speed` (e.g. just after a runtime-upgrade lowered
26/// the cap) is treated as cruising at `v_max`.
27#[must_use]
28pub fn travel_time(distance: f64, v0: f64, v_max: f64, accel: f64, decel: f64) -> f64 {
29    if distance <= 0.0 || v_max <= 0.0 || accel <= 0.0 || decel <= 0.0 {
30        return 0.0;
31    }
32    let v0 = v0.clamp(0.0, v_max);
33
34    // If the brake distance from v0 already exceeds the remaining trip,
35    // we can't even reach v0+ε before having to slow — solve the pure
36    // deceleration leg `d = v0·t − ½·decel·t²` for the smaller root.
37    let brake_d = v0 * v0 / (2.0 * decel);
38    if brake_d >= distance {
39        let disc = (v0 * v0 - 2.0 * distance * decel).max(0.0);
40        return (v0 - disc.sqrt()) / decel;
41    }
42
43    // Triangular peak velocity (no cruise): solve d_accel(v) + d_decel(v) = d
44    // → v² = (2·d·a·decel + v0²·decel) / (a + decel)
45    let v_peak_sq = decel.mul_add(v0 * v0, 2.0 * distance * accel * decel) / (accel + decel);
46    let v_peak = v_peak_sq.sqrt();
47
48    if v_peak <= v_max {
49        // Triangular profile: accel v0→v_peak, decel v_peak→0
50        (v_peak - v0) / accel + v_peak / decel
51    } else {
52        // Trapezoidal: accel v0→v_max, cruise at v_max, decel v_max→0
53        let d_accel = v_max.mul_add(v_max, -(v0 * v0)) / (2.0 * accel);
54        let d_decel = v_max * v_max / (2.0 * decel);
55        let d_cruise = distance - d_accel - d_decel;
56        (v_max - v0) / accel + d_cruise / v_max + v_max / decel
57    }
58}