elevator_core/
scenario.rs1use 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),
36 AbandonmentRateBelow(f64),
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Scenario {
43 pub name: String,
45 pub config: SimConfig,
47 pub spawns: Vec<TimedSpawn>,
49 pub conditions: Vec<Condition>,
51 pub max_ticks: u64,
53}
54
55#[derive(Debug, Clone)]
57pub struct ConditionResult {
58 pub condition: Condition,
60 pub passed: bool,
62 pub actual_value: f64,
64}
65
66#[derive(Debug, Clone)]
68pub struct ScenarioResult {
69 pub name: String,
71 pub passed: bool,
73 pub ticks_run: u64,
75 pub conditions: Vec<ConditionResult>,
77 pub metrics: Metrics,
79}
80
81pub struct ScenarioRunner {
83 sim: Simulation,
85 spawns: Vec<TimedSpawn>,
87 spawn_cursor: usize,
89 conditions: Vec<Condition>,
91 max_ticks: u64,
93 name: String,
95}
96
97impl ScenarioRunner {
98 pub fn new(
106 scenario: Scenario,
107 dispatch: impl DispatchStrategy + 'static,
108 ) -> Result<Self, SimError> {
109 let sim = Simulation::new(&scenario.config, dispatch)?;
110 Ok(Self {
111 sim,
112 spawns: scenario.spawns,
113 spawn_cursor: 0,
114 conditions: scenario.conditions,
115 max_ticks: scenario.max_ticks,
116 name: scenario.name,
117 })
118 }
119
120 #[must_use]
122 pub const fn sim(&self) -> &Simulation {
123 &self.sim
124 }
125
126 pub fn tick(&mut self) {
128 while self.spawn_cursor < self.spawns.len()
130 && self.spawns[self.spawn_cursor].tick <= self.sim.current_tick()
131 {
132 let spawn = &self.spawns[self.spawn_cursor];
133 let _ = self
137 .sim
138 .spawn_rider(spawn.origin, spawn.destination, spawn.weight);
139 self.spawn_cursor += 1;
140 }
141
142 self.sim.step();
143 }
144
145 pub fn run_to_completion(&mut self) -> ScenarioResult {
147 use crate::components::RiderPhase;
148
149 for _ in 0..self.max_ticks {
150 self.tick();
151
152 if self.spawn_cursor >= self.spawns.len() {
154 let all_done =
155 self.sim.world().iter_riders().all(|(_, r)| {
156 matches!(r.phase, RiderPhase::Arrived | RiderPhase::Abandoned)
157 });
158 if all_done {
159 break;
160 }
161 }
162 }
163
164 self.evaluate()
165 }
166
167 #[must_use]
169 pub fn evaluate(&self) -> ScenarioResult {
170 let metrics = self.sim.metrics().clone();
171 let condition_results: Vec<ConditionResult> = self
172 .conditions
173 .iter()
174 .map(|cond| evaluate_condition(cond, &metrics, self.sim.current_tick()))
175 .collect();
176
177 let passed = condition_results.iter().all(|r| r.passed);
178
179 ScenarioResult {
180 name: self.name.clone(),
181 passed,
182 ticks_run: self.sim.current_tick(),
183 conditions: condition_results,
184 metrics,
185 }
186 }
187}
188
189fn evaluate_condition(
191 condition: &Condition,
192 metrics: &Metrics,
193 current_tick: u64,
194) -> ConditionResult {
195 match condition {
196 Condition::AvgWaitBelow(threshold) => ConditionResult {
197 condition: condition.clone(),
198 passed: metrics.avg_wait_time() < *threshold,
199 actual_value: metrics.avg_wait_time(),
200 },
201 Condition::MaxWaitBelow(threshold) => ConditionResult {
202 condition: condition.clone(),
203 passed: metrics.max_wait_time() < *threshold,
204 actual_value: metrics.max_wait_time() as f64,
205 },
206 Condition::ThroughputAbove(threshold) => ConditionResult {
207 condition: condition.clone(),
208 passed: metrics.throughput() > *threshold,
209 actual_value: metrics.throughput() as f64,
210 },
211 Condition::AllDeliveredByTick(deadline) => ConditionResult {
212 condition: condition.clone(),
213 passed: current_tick <= *deadline
214 && metrics.total_delivered() == metrics.total_spawned(),
215 actual_value: current_tick as f64,
216 },
217 Condition::AbandonmentRateBelow(threshold) => ConditionResult {
218 condition: condition.clone(),
219 passed: metrics.abandonment_rate() < *threshold,
220 actual_value: metrics.abandonment_rate(),
221 },
222 }
223}