use crate::config::SimConfig;
use crate::dispatch::DispatchStrategy;
use crate::error::SimError;
use crate::metrics::Metrics;
use crate::sim::Simulation;
use crate::stop::StopId;
use crate::traffic::TrafficPattern;
use rand::RngExt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimedSpawn {
pub tick: u64,
pub origin: StopId,
pub destination: StopId,
pub weight: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Condition {
AvgWaitBelow(f64),
MaxWaitBelow(u64),
ThroughputAbove(u64),
AllDeliveredByTick(u64),
AbandonmentRateBelow(f64),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Scenario {
pub name: String,
pub config: SimConfig,
pub spawns: Vec<TimedSpawn>,
pub conditions: Vec<Condition>,
pub max_ticks: u64,
}
#[derive(Debug, Clone)]
pub struct ConditionResult {
pub condition: Condition,
pub passed: bool,
pub actual_value: f64,
}
#[derive(Debug, Clone)]
pub struct ScenarioResult {
pub name: String,
pub passed: bool,
pub ticks_run: u64,
pub conditions: Vec<ConditionResult>,
pub metrics: Metrics,
}
pub struct ScenarioRunner {
sim: Simulation,
spawns: Vec<TimedSpawn>,
spawn_cursor: usize,
conditions: Vec<Condition>,
max_ticks: u64,
name: String,
skipped_spawns: u64,
}
impl ScenarioRunner {
pub fn new(
scenario: Scenario,
dispatch: impl DispatchStrategy + 'static,
) -> Result<Self, SimError> {
let sim = Simulation::new(&scenario.config, dispatch)?;
let mut spawns = scenario.spawns;
spawns.sort_by_key(|s| s.tick);
Ok(Self {
sim,
spawns,
spawn_cursor: 0,
conditions: scenario.conditions,
max_ticks: scenario.max_ticks,
name: scenario.name,
skipped_spawns: 0,
})
}
#[must_use]
pub const fn sim(&self) -> &Simulation {
&self.sim
}
pub const fn sim_mut(&mut self) -> &mut Simulation {
&mut self.sim
}
#[must_use]
pub const fn skipped_spawns(&self) -> u64 {
self.skipped_spawns
}
pub fn tick(&mut self) {
while self.spawn_cursor < self.spawns.len()
&& self.spawns[self.spawn_cursor].tick <= self.sim.current_tick()
{
let spawn = &self.spawns[self.spawn_cursor];
if self
.sim
.spawn_rider(spawn.origin, spawn.destination, spawn.weight)
.is_err()
{
self.skipped_spawns += 1;
}
self.spawn_cursor += 1;
}
self.sim.step();
}
pub fn run_to_completion(&mut self) -> ScenarioResult {
use crate::components::RiderPhase;
for _ in 0..self.max_ticks {
self.tick();
if self.spawn_cursor >= self.spawns.len() {
let all_done =
self.sim.world().iter_riders().all(|(_, r)| {
matches!(r.phase, RiderPhase::Arrived | RiderPhase::Abandoned)
});
if all_done {
break;
}
}
}
self.evaluate()
}
#[must_use]
pub fn evaluate(&self) -> ScenarioResult {
let metrics = self.sim.metrics().clone();
let condition_results: Vec<ConditionResult> = self
.conditions
.iter()
.map(|cond| evaluate_condition(cond, &metrics, self.sim.current_tick()))
.collect();
let passed = condition_results.iter().all(|r| r.passed);
ScenarioResult {
name: self.name.clone(),
passed,
ticks_run: self.sim.current_tick(),
conditions: condition_results,
metrics,
}
}
}
fn evaluate_condition(
condition: &Condition,
metrics: &Metrics,
current_tick: u64,
) -> ConditionResult {
match condition {
Condition::AvgWaitBelow(threshold) => ConditionResult {
condition: condition.clone(),
passed: metrics.avg_wait_time() < *threshold,
actual_value: metrics.avg_wait_time(),
},
Condition::MaxWaitBelow(threshold) => ConditionResult {
condition: condition.clone(),
passed: metrics.max_wait_time() < *threshold,
actual_value: metrics.max_wait_time() as f64,
},
Condition::ThroughputAbove(threshold) => ConditionResult {
condition: condition.clone(),
passed: metrics.throughput() > *threshold,
actual_value: metrics.throughput() as f64,
},
Condition::AllDeliveredByTick(deadline) => ConditionResult {
condition: condition.clone(),
passed: current_tick <= *deadline
&& metrics.total_delivered() + metrics.total_abandoned() == metrics.total_spawned(),
actual_value: current_tick as f64,
},
Condition::AbandonmentRateBelow(threshold) => ConditionResult {
condition: condition.clone(),
passed: metrics.abandonment_rate() < *threshold,
actual_value: metrics.abandonment_rate(),
},
}
}
#[derive(Debug, Clone, Default)]
pub struct SpawnSchedule {
spawns: Vec<TimedSpawn>,
}
impl SpawnSchedule {
#[must_use]
pub const fn new() -> Self {
Self { spawns: Vec::new() }
}
#[must_use]
pub fn burst(
mut self,
origin: StopId,
destination: StopId,
count: usize,
at_tick: u64,
weight: f64,
) -> Self {
self.spawns.reserve(count);
for _ in 0..count {
self.spawns.push(TimedSpawn {
tick: at_tick,
origin,
destination,
weight,
});
}
self
}
#[must_use]
pub fn staggered(
mut self,
origin: StopId,
destination: StopId,
count: usize,
start_tick: u64,
stagger_ticks: u64,
weight: f64,
) -> Self {
self.spawns.reserve(count);
for i in 0..count as u64 {
self.spawns.push(TimedSpawn {
tick: start_tick + i * stagger_ticks,
origin,
destination,
weight,
});
}
self
}
#[must_use]
pub fn from_pattern(
mut self,
pattern: TrafficPattern,
stops: &[StopId],
duration_ticks: u64,
mean_interval_ticks: u32,
weight_range: (f64, f64),
rng: &mut impl RngExt,
) -> Self {
if stops.len() < 2 || mean_interval_ticks == 0 {
return self;
}
let (wlo, whi) = if weight_range.0 > weight_range.1 {
(weight_range.1, weight_range.0)
} else {
weight_range
};
let mut tick = 0u64;
loop {
let u: f64 = rng.random_range(0.0001..1.0);
let interval = -(f64::from(mean_interval_ticks)) * u.ln();
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let step = (interval as u64).max(1);
tick = tick.saturating_add(step);
if tick >= duration_ticks {
break;
}
if let Some((origin, destination)) = pattern.sample_stop_ids(stops, rng) {
let weight = rng.random_range(wlo..=whi);
self.spawns.push(TimedSpawn {
tick,
origin,
destination,
weight,
});
}
}
self
}
#[must_use]
pub fn push(mut self, spawn: TimedSpawn) -> Self {
self.spawns.push(spawn);
self
}
#[must_use]
pub fn merge(mut self, other: Self) -> Self {
self.spawns.extend(other.spawns);
self
}
#[must_use]
pub const fn len(&self) -> usize {
self.spawns.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.spawns.is_empty()
}
#[must_use]
pub fn spawns(&self) -> &[TimedSpawn] {
&self.spawns
}
#[must_use]
pub fn into_spawns(self) -> Vec<TimedSpawn> {
self.spawns
}
}