use crate::arrival_log::{ArrivalLog, DestinationLog};
use crate::entity::EntityId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum TrafficMode {
#[default]
Idle,
UpPeak,
InterFloor,
DownPeak,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrafficDetector {
window_ticks: u64,
idle_rate_threshold: f64,
up_peak_fraction: f64,
down_peak_fraction: f64,
current: TrafficMode,
last_update_tick: u64,
}
impl Default for TrafficDetector {
fn default() -> Self {
Self {
window_ticks: crate::arrival_log::DEFAULT_ARRIVAL_WINDOW_TICKS,
idle_rate_threshold: 2.0 / 3600.0,
up_peak_fraction: 0.6,
down_peak_fraction: 0.6,
current: TrafficMode::Idle,
last_update_tick: 0,
}
}
}
impl TrafficDetector {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_window_ticks(mut self, window_ticks: u64) -> Self {
assert!(
window_ticks > 0,
"TrafficDetector::with_window_ticks requires a positive window"
);
self.window_ticks = window_ticks;
self
}
#[must_use]
pub fn with_idle_rate_threshold(mut self, rate: f64) -> Self {
assert!(
rate.is_finite() && rate >= 0.0,
"idle_rate_threshold must be finite and non-negative, got {rate}"
);
self.idle_rate_threshold = rate;
self
}
#[must_use]
pub fn with_up_peak_fraction(mut self, fraction: f64) -> Self {
assert!(
fraction.is_finite() && (0.0..=1.0).contains(&fraction),
"up_peak_fraction must be finite and in [0, 1], got {fraction}"
);
self.up_peak_fraction = fraction;
self
}
#[must_use]
pub fn with_down_peak_fraction(mut self, fraction: f64) -> Self {
assert!(
fraction.is_finite() && (0.0..=1.0).contains(&fraction),
"down_peak_fraction must be finite and in [0, 1], got {fraction}"
);
self.down_peak_fraction = fraction;
self
}
#[must_use]
pub const fn current_mode(&self) -> TrafficMode {
self.current
}
#[must_use]
pub const fn last_update_tick(&self) -> u64 {
self.last_update_tick
}
#[must_use]
pub const fn window_ticks(&self) -> u64 {
self.window_ticks
}
pub fn update(
&mut self,
arrivals: &ArrivalLog,
destinations: &DestinationLog,
now: u64,
stops: &[EntityId],
) {
self.last_update_tick = now;
if stops.is_empty() || self.window_ticks == 0 {
self.current = TrafficMode::Idle;
return;
}
let lobby = stops[0];
let lobby_origin_count = arrivals.arrivals_in_window(lobby, now, self.window_ticks);
let lobby_dest_count = destinations.destinations_in_window(lobby, now, self.window_ticks);
let mut total_origin: u64 = 0;
let mut total_dest: u64 = 0;
for &s in stops {
total_origin =
total_origin.saturating_add(arrivals.arrivals_in_window(s, now, self.window_ticks));
total_dest = total_dest.saturating_add(destinations.destinations_in_window(
s,
now,
self.window_ticks,
));
}
if total_origin == 0 {
self.current = TrafficMode::Idle;
return;
}
let rate_per_tick = total_origin as f64 / self.window_ticks as f64;
if rate_per_tick < self.idle_rate_threshold {
self.current = TrafficMode::Idle;
return;
}
let up_fraction = lobby_origin_count as f64 / total_origin as f64;
if up_fraction >= self.up_peak_fraction {
self.current = TrafficMode::UpPeak;
return;
}
if total_dest > 0 {
let down_fraction = lobby_dest_count as f64 / total_dest as f64;
if down_fraction >= self.down_peak_fraction {
self.current = TrafficMode::DownPeak;
return;
}
}
self.current = TrafficMode::InterFloor;
}
}