///|
pub(all) struct EtaEngine {
kalman : KalmanState
window : WindowState
route : RouteProfile
config : EtaConfig
last_observation : Observation?
} derive(Debug, Eq)
///|
pub fn EtaEngine::new(
route : RouteProfile,
initial_speed~ : Double,
config? : EtaConfig = default_eta_config(),
) -> EtaEngine {
{
kalman: KalmanState::new(initial_speed, default_kalman_config()),
window: WindowState::new(default_window_config()),
route,
config,
last_observation: None,
}
}
///|
fn blend_speed(
live : Double,
historical : Double,
config : EtaConfig,
reliability : Double,
) -> Double {
let r = clamp(reliability, config.reliability_floor, 1.0)
let live_weight = config.live_weight * r
let history_weight = config.history_weight * (2.0 - r)
let total = live_weight + history_weight
clamp(
(live * live_weight + historical * history_weight) / total,
config.min_speed,
config.max_speed,
)
}
///|
pub fn EtaEngine::observe(
self : EtaEngine,
observation : Observation,
congestion? : Double = 0.3,
) -> (EtaEngine, EtaResult) {
let route_distance = self.route.total_distance()
let remaining = clamp(
route_distance - observation.distance,
0.0,
route_distance,
)
let historical_eta = self.route.historical_eta(congestion~)
let historical_speed = safe_div(
route_distance,
historical_eta,
self.config.min_speed,
)
let (raw_speed, raw_confidence) = match self.last_observation {
None => (historical_speed, self.route.average_reliability())
Some(prev) => {
let sample = velocity_from_observation(prev, observation)
(sample.speed, sample.confidence)
}
}
let bounded_raw = clamp(
raw_speed,
self.config.min_speed,
self.config.max_speed,
)
let kalman = self.kalman.update(bounded_raw)
let window = self.window.push(kalman.estimate)
let live_speed = window.average()
let reliability = self.route.average_reliability() *
window.stability() *
clamp(raw_confidence, 0.0, 1.0)
let smoothed_speed = blend_speed(
live_speed,
historical_speed,
self.config,
reliability,
)
let eta_seconds = safe_div(remaining, smoothed_speed, 0.0)
let confidence = clamp(reliability, 0.0, 1.0)
(
{ ..self, kalman, window, last_observation: Some(observation) },
{
eta_seconds,
smoothed_speed,
raw_speed: bounded_raw,
confidence,
remaining_distance: remaining,
},
)
}
///|
pub fn forecast_route(
observations : Array[Observation],
route : RouteProfile,
) -> Array[EtaResult] {
let out : Array[EtaResult] = []
let mut engine = EtaEngine::new(route, initial_speed=8.0)
for obs in observations {
let (next, result) = engine.observe(obs)
engine = next
out.push(result)
}
out
}