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 let mut spawns = scenario.spawns;
120 spawns.sort_by_key(|s| s.tick);
121 Ok(Self {
122 sim,
123 spawns,
124 spawn_cursor: 0,
125 conditions: scenario.conditions,
126 max_ticks: scenario.max_ticks,
127 name: scenario.name,
128 skipped_spawns: 0,
129 })
130 }
131
132 #[must_use]
134 pub const fn sim(&self) -> &Simulation {
135 &self.sim
136 }
137
138 #[must_use]
141 pub const fn skipped_spawns(&self) -> u64 {
142 self.skipped_spawns
143 }
144
145 pub fn tick(&mut self) {
147 while self.spawn_cursor < self.spawns.len()
149 && self.spawns[self.spawn_cursor].tick <= self.sim.current_tick()
150 {
151 let spawn = &self.spawns[self.spawn_cursor];
152 if self
156 .sim
157 .spawn_rider(spawn.origin, spawn.destination, spawn.weight)
158 .is_err()
159 {
160 self.skipped_spawns += 1;
161 }
162 self.spawn_cursor += 1;
163 }
164
165 self.sim.step();
166 }
167
168 pub fn run_to_completion(&mut self) -> ScenarioResult {
170 use crate::components::RiderPhase;
171
172 for _ in 0..self.max_ticks {
173 self.tick();
174
175 if self.spawn_cursor >= self.spawns.len() {
177 let all_done =
178 self.sim.world().iter_riders().all(|(_, r)| {
179 matches!(r.phase, RiderPhase::Arrived | RiderPhase::Abandoned)
180 });
181 if all_done {
182 break;
183 }
184 }
185 }
186
187 self.evaluate()
188 }
189
190 #[must_use]
192 pub fn evaluate(&self) -> ScenarioResult {
193 let metrics = self.sim.metrics().clone();
194 let condition_results: Vec<ConditionResult> = self
195 .conditions
196 .iter()
197 .map(|cond| evaluate_condition(cond, &metrics, self.sim.current_tick()))
198 .collect();
199
200 let passed = condition_results.iter().all(|r| r.passed);
201
202 ScenarioResult {
203 name: self.name.clone(),
204 passed,
205 ticks_run: self.sim.current_tick(),
206 conditions: condition_results,
207 metrics,
208 }
209 }
210}
211
212fn evaluate_condition(
214 condition: &Condition,
215 metrics: &Metrics,
216 current_tick: u64,
217) -> ConditionResult {
218 match condition {
219 Condition::AvgWaitBelow(threshold) => ConditionResult {
220 condition: condition.clone(),
221 passed: metrics.avg_wait_time() < *threshold,
222 actual_value: metrics.avg_wait_time(),
223 },
224 Condition::MaxWaitBelow(threshold) => ConditionResult {
225 condition: condition.clone(),
226 passed: metrics.max_wait_time() < *threshold,
227 actual_value: metrics.max_wait_time() as f64,
228 },
229 Condition::ThroughputAbove(threshold) => ConditionResult {
230 condition: condition.clone(),
231 passed: metrics.throughput() > *threshold,
232 actual_value: metrics.throughput() as f64,
233 },
234 Condition::AllDeliveredByTick(deadline) => ConditionResult {
235 condition: condition.clone(),
236 passed: current_tick <= *deadline
237 && metrics.total_delivered() + metrics.total_abandoned() == metrics.total_spawned(),
238 actual_value: current_tick as f64,
239 },
240 Condition::AbandonmentRateBelow(threshold) => ConditionResult {
241 condition: condition.clone(),
242 passed: metrics.abandonment_rate() < *threshold,
243 actual_value: metrics.abandonment_rate(),
244 },
245 }
246}