1use crate::config::SimConfig;
4use crate::dispatch::DispatchStrategy;
5use crate::error::SimError;
6use crate::metrics::Metrics;
7use crate::sim::Simulation;
8use crate::stop::StopId;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct TimedSpawn {
14 pub tick: u64,
16 pub origin: StopId,
18 pub destination: StopId,
20 pub weight: f64,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26#[non_exhaustive]
27pub enum Condition {
28 AvgWaitBelow(f64),
30 MaxWaitBelow(u64),
32 ThroughputAbove(u64),
34 AllDeliveredByTick(u64),
39 AbandonmentRateBelow(f64),
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Scenario {
46 pub name: String,
48 pub config: SimConfig,
50 pub spawns: Vec<TimedSpawn>,
52 pub conditions: Vec<Condition>,
54 pub max_ticks: u64,
56}
57
58#[derive(Debug, Clone)]
60pub struct ConditionResult {
61 pub condition: Condition,
63 pub passed: bool,
65 pub actual_value: f64,
67}
68
69#[derive(Debug, Clone)]
71pub struct ScenarioResult {
72 pub name: String,
74 pub passed: bool,
76 pub ticks_run: u64,
78 pub conditions: Vec<ConditionResult>,
80 pub metrics: Metrics,
82}
83
84pub struct ScenarioRunner {
86 sim: Simulation,
88 spawns: Vec<TimedSpawn>,
90 spawn_cursor: usize,
92 conditions: Vec<Condition>,
94 max_ticks: u64,
96 name: String,
98 skipped_spawns: u64,
100}
101
102impl ScenarioRunner {
103 pub fn new(
111 scenario: Scenario,
112 dispatch: impl DispatchStrategy + 'static,
113 ) -> Result<Self, SimError> {
114 let sim = Simulation::new(&scenario.config, dispatch)?;
115 Ok(Self {
116 sim,
117 spawns: scenario.spawns,
118 spawn_cursor: 0,
119 conditions: scenario.conditions,
120 max_ticks: scenario.max_ticks,
121 name: scenario.name,
122 skipped_spawns: 0,
123 })
124 }
125
126 #[must_use]
128 pub const fn sim(&self) -> &Simulation {
129 &self.sim
130 }
131
132 #[must_use]
135 pub const fn skipped_spawns(&self) -> u64 {
136 self.skipped_spawns
137 }
138
139 pub fn tick(&mut self) {
141 while self.spawn_cursor < self.spawns.len()
143 && self.spawns[self.spawn_cursor].tick <= self.sim.current_tick()
144 {
145 let spawn = &self.spawns[self.spawn_cursor];
146 if self
150 .sim
151 .spawn_rider(spawn.origin, spawn.destination, spawn.weight)
152 .is_err()
153 {
154 self.skipped_spawns += 1;
155 }
156 self.spawn_cursor += 1;
157 }
158
159 self.sim.step();
160 }
161
162 pub fn run_to_completion(&mut self) -> ScenarioResult {
164 use crate::components::RiderPhase;
165
166 for _ in 0..self.max_ticks {
167 self.tick();
168
169 if self.spawn_cursor >= self.spawns.len() {
171 let all_done =
172 self.sim.world().iter_riders().all(|(_, r)| {
173 matches!(r.phase, RiderPhase::Arrived | RiderPhase::Abandoned)
174 });
175 if all_done {
176 break;
177 }
178 }
179 }
180
181 self.evaluate()
182 }
183
184 #[must_use]
186 pub fn evaluate(&self) -> ScenarioResult {
187 let metrics = self.sim.metrics().clone();
188 let condition_results: Vec<ConditionResult> = self
189 .conditions
190 .iter()
191 .map(|cond| evaluate_condition(cond, &metrics, self.sim.current_tick()))
192 .collect();
193
194 let passed = condition_results.iter().all(|r| r.passed);
195
196 ScenarioResult {
197 name: self.name.clone(),
198 passed,
199 ticks_run: self.sim.current_tick(),
200 conditions: condition_results,
201 metrics,
202 }
203 }
204}
205
206fn evaluate_condition(
208 condition: &Condition,
209 metrics: &Metrics,
210 current_tick: u64,
211) -> ConditionResult {
212 match condition {
213 Condition::AvgWaitBelow(threshold) => ConditionResult {
214 condition: condition.clone(),
215 passed: metrics.avg_wait_time() < *threshold,
216 actual_value: metrics.avg_wait_time(),
217 },
218 Condition::MaxWaitBelow(threshold) => ConditionResult {
219 condition: condition.clone(),
220 passed: metrics.max_wait_time() < *threshold,
221 actual_value: metrics.max_wait_time() as f64,
222 },
223 Condition::ThroughputAbove(threshold) => ConditionResult {
224 condition: condition.clone(),
225 passed: metrics.throughput() > *threshold,
226 actual_value: metrics.throughput() as f64,
227 },
228 Condition::AllDeliveredByTick(deadline) => ConditionResult {
229 condition: condition.clone(),
230 passed: current_tick <= *deadline
231 && metrics.total_delivered() + metrics.total_abandoned() == metrics.total_spawned(),
232 actual_value: current_tick as f64,
233 },
234 Condition::AbandonmentRateBelow(threshold) => ConditionResult {
235 condition: condition.clone(),
236 passed: metrics.abandonment_rate() < *threshold,
237 actual_value: metrics.abandonment_rate(),
238 },
239 }
240}