1use std::collections::{BTreeMap, HashMap, HashSet};
15use std::sync::Mutex;
16
17use crate::components::{Elevator, ElevatorPhase, Line, Orientation, Position, Stop, Velocity};
18use crate::config::SimConfig;
19use crate::dispatch::{
20 BuiltinReposition, BuiltinStrategy, DispatchStrategy, ElevatorGroup, LineInfo,
21 RepositionStrategy,
22};
23use crate::door::DoorState;
24use crate::entity::EntityId;
25use crate::error::SimError;
26use crate::events::EventBus;
27use crate::hooks::{Phase, PhaseHooks};
28use crate::ids::GroupId;
29use crate::metrics::Metrics;
30use crate::rider_index::RiderIndex;
31use crate::stop::StopId;
32use crate::time::TimeAdapter;
33use crate::topology::TopologyGraph;
34use crate::world::World;
35
36use super::Simulation;
37
38type TopologyResult = (
40 Vec<ElevatorGroup>,
41 BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
42 BTreeMap<GroupId, BuiltinStrategy>,
43);
44
45impl Simulation {
46 pub fn new(
57 config: &SimConfig,
58 dispatch: impl DispatchStrategy + 'static,
59 ) -> Result<Self, SimError> {
60 let mut dispatchers = BTreeMap::new();
61 dispatchers.insert(GroupId(0), Box::new(dispatch) as Box<dyn DispatchStrategy>);
62 Self::new_with_hooks(config, dispatchers, PhaseHooks::default())
63 }
64
65 #[allow(clippy::too_many_lines)]
69 pub(crate) fn new_with_hooks(
70 config: &SimConfig,
71 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
72 hooks: PhaseHooks,
73 ) -> Result<Self, SimError> {
74 Self::validate_config(config)?;
75
76 let mut world = World::new();
77
78 let mut stop_lookup: HashMap<StopId, EntityId> = HashMap::new();
80 for sc in &config.building.stops {
81 let eid = world.spawn();
82 world.set_stop(
83 eid,
84 Stop {
85 name: sc.name.clone(),
86 position: sc.position,
87 },
88 );
89 world.set_position(eid, Position { value: sc.position });
90 stop_lookup.insert(sc.id, eid);
91 }
92
93 let mut sorted: Vec<(f64, EntityId)> = world
95 .iter_stops()
96 .map(|(eid, stop)| (stop.position, eid))
97 .collect();
98 sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
99 world.insert_resource(crate::world::SortedStops(sorted));
100
101 let (groups, dispatchers, strategy_ids) = if let Some(line_configs) = &config.building.lines
102 {
103 Self::build_explicit_topology(
104 &mut world,
105 config,
106 line_configs,
107 &stop_lookup,
108 builder_dispatchers,
109 )
110 } else {
111 Self::build_legacy_topology(&mut world, config, &stop_lookup, builder_dispatchers)
112 };
113
114 let dt = 1.0 / config.simulation.ticks_per_second;
115
116 world.insert_resource(crate::tagged_metrics::MetricTags::default());
117
118 let line_tag_info: Vec<(EntityId, String, Vec<EntityId>)> = groups
121 .iter()
122 .flat_map(|group| {
123 group.lines().iter().filter_map(|li| {
124 let line_comp = world.line(li.entity())?;
125 Some((li.entity(), line_comp.name.clone(), li.elevators().to_vec()))
126 })
127 })
128 .collect();
129
130 if let Some(tags) = world.resource_mut::<crate::tagged_metrics::MetricTags>() {
132 for (line_eid, name, elevators) in &line_tag_info {
133 let tag = format!("line:{name}");
134 tags.tag(*line_eid, tag.clone());
135 for elev_eid in elevators {
136 tags.tag(*elev_eid, tag.clone());
137 }
138 }
139 }
140
141 let mut repositioners: BTreeMap<GroupId, Box<dyn RepositionStrategy>> = BTreeMap::new();
143 let mut reposition_ids: BTreeMap<GroupId, BuiltinReposition> = BTreeMap::new();
144 if let Some(group_configs) = &config.building.groups {
145 for gc in group_configs {
146 if let Some(ref repo_id) = gc.reposition
147 && let Some(strategy) = repo_id.instantiate()
148 {
149 let gid = GroupId(gc.id);
150 repositioners.insert(gid, strategy);
151 reposition_ids.insert(gid, repo_id.clone());
152 }
153 }
154 }
155
156 Ok(Self {
157 world,
158 events: EventBus::default(),
159 pending_output: Vec::new(),
160 tick: 0,
161 dt,
162 groups,
163 stop_lookup,
164 dispatchers,
165 strategy_ids,
166 repositioners,
167 reposition_ids,
168 metrics: Metrics::new(),
169 time: TimeAdapter::new(config.simulation.ticks_per_second),
170 hooks,
171 elevator_ids_buf: Vec::new(),
172 topo_graph: Mutex::new(TopologyGraph::new()),
173 rider_index: RiderIndex::default(),
174 })
175 }
176
177 fn spawn_elevator_entity(
183 world: &mut World,
184 ec: &crate::config::ElevatorConfig,
185 line: EntityId,
186 stop_lookup: &HashMap<StopId, EntityId>,
187 start_pos_lookup: &[crate::stop::StopConfig],
188 ) -> EntityId {
189 let eid = world.spawn();
190 let start_pos = start_pos_lookup
191 .iter()
192 .find(|s| s.id == ec.starting_stop)
193 .map_or(0.0, |s| s.position);
194 world.set_position(eid, Position { value: start_pos });
195 world.set_velocity(eid, Velocity { value: 0.0 });
196 let restricted: HashSet<EntityId> = ec
197 .restricted_stops
198 .iter()
199 .filter_map(|sid| stop_lookup.get(sid).copied())
200 .collect();
201 world.set_elevator(
202 eid,
203 Elevator {
204 phase: ElevatorPhase::Idle,
205 door: DoorState::Closed,
206 max_speed: ec.max_speed,
207 acceleration: ec.acceleration,
208 deceleration: ec.deceleration,
209 weight_capacity: ec.weight_capacity,
210 current_load: 0.0,
211 riders: Vec::new(),
212 target_stop: None,
213 door_transition_ticks: ec.door_transition_ticks,
214 door_open_ticks: ec.door_open_ticks,
215 line,
216 repositioning: false,
217 restricted_stops: restricted,
218 inspection_speed_factor: ec.inspection_speed_factor,
219 going_up: true,
220 going_down: true,
221 move_count: 0,
222 door_command_queue: Vec::new(),
223 manual_target_velocity: None,
224 },
225 );
226 #[cfg(feature = "energy")]
227 if let Some(ref profile) = ec.energy_profile {
228 world.set_energy_profile(eid, profile.clone());
229 world.set_energy_metrics(eid, crate::energy::EnergyMetrics::default());
230 }
231 if let Some(mode) = ec.service_mode {
232 world.set_service_mode(eid, mode);
233 }
234 world.set_destination_queue(eid, crate::components::DestinationQueue::new());
235 eid
236 }
237
238 fn build_legacy_topology(
240 world: &mut World,
241 config: &SimConfig,
242 stop_lookup: &HashMap<StopId, EntityId>,
243 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
244 ) -> TopologyResult {
245 let all_stop_entities: Vec<EntityId> = stop_lookup.values().copied().collect();
246 let stop_positions: Vec<f64> = config.building.stops.iter().map(|s| s.position).collect();
247 let min_pos = stop_positions.iter().copied().fold(f64::INFINITY, f64::min);
248 let max_pos = stop_positions
249 .iter()
250 .copied()
251 .fold(f64::NEG_INFINITY, f64::max);
252
253 let default_line_eid = world.spawn();
254 world.set_line(
255 default_line_eid,
256 Line {
257 name: "Default".into(),
258 group: GroupId(0),
259 orientation: Orientation::Vertical,
260 position: None,
261 min_position: min_pos,
262 max_position: max_pos,
263 max_cars: None,
264 },
265 );
266
267 let mut elevator_entities = Vec::new();
268 for ec in &config.elevators {
269 let eid = Self::spawn_elevator_entity(
270 world,
271 ec,
272 default_line_eid,
273 stop_lookup,
274 &config.building.stops,
275 );
276 elevator_entities.push(eid);
277 }
278
279 let default_line_info =
280 LineInfo::new(default_line_eid, elevator_entities, all_stop_entities);
281
282 let group = ElevatorGroup::new(GroupId(0), "Default".into(), vec![default_line_info]);
283
284 let mut dispatchers = BTreeMap::new();
286 let dispatch = builder_dispatchers.into_iter().next().map_or_else(
287 || Box::new(crate::dispatch::scan::ScanDispatch::new()) as Box<dyn DispatchStrategy>,
288 |(_, d)| d,
289 );
290 dispatchers.insert(GroupId(0), dispatch);
291
292 let mut strategy_ids = BTreeMap::new();
293 strategy_ids.insert(GroupId(0), BuiltinStrategy::Scan);
294
295 (vec![group], dispatchers, strategy_ids)
296 }
297
298 #[allow(clippy::too_many_lines)]
300 fn build_explicit_topology(
301 world: &mut World,
302 config: &SimConfig,
303 line_configs: &[crate::config::LineConfig],
304 stop_lookup: &HashMap<StopId, EntityId>,
305 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
306 ) -> TopologyResult {
307 let mut line_map: HashMap<u32, (EntityId, LineInfo)> = HashMap::new();
309
310 for lc in line_configs {
311 let served_entities: Vec<EntityId> = lc
313 .serves
314 .iter()
315 .filter_map(|sid| stop_lookup.get(sid).copied())
316 .collect();
317
318 let stop_positions: Vec<f64> = lc
320 .serves
321 .iter()
322 .filter_map(|sid| {
323 config
324 .building
325 .stops
326 .iter()
327 .find(|s| s.id == *sid)
328 .map(|s| s.position)
329 })
330 .collect();
331 let auto_min = stop_positions.iter().copied().fold(f64::INFINITY, f64::min);
332 let auto_max = stop_positions
333 .iter()
334 .copied()
335 .fold(f64::NEG_INFINITY, f64::max);
336
337 let min_pos = lc.min_position.unwrap_or(auto_min);
338 let max_pos = lc.max_position.unwrap_or(auto_max);
339
340 let line_eid = world.spawn();
341 world.set_line(
344 line_eid,
345 Line {
346 name: lc.name.clone(),
347 group: GroupId(0),
348 orientation: lc.orientation,
349 position: lc.position,
350 min_position: min_pos,
351 max_position: max_pos,
352 max_cars: lc.max_cars,
353 },
354 );
355
356 let mut elevator_entities = Vec::new();
358 for ec in &lc.elevators {
359 let eid = Self::spawn_elevator_entity(
360 world,
361 ec,
362 line_eid,
363 stop_lookup,
364 &config.building.stops,
365 );
366 elevator_entities.push(eid);
367 }
368
369 let line_info = LineInfo::new(line_eid, elevator_entities, served_entities);
370 line_map.insert(lc.id, (line_eid, line_info));
371 }
372
373 let group_configs = config.building.groups.as_deref();
375 let mut groups = Vec::new();
376 let mut dispatchers = BTreeMap::new();
377 let mut strategy_ids = BTreeMap::new();
378
379 if let Some(gcs) = group_configs {
380 for gc in gcs {
381 let group_id = GroupId(gc.id);
382
383 let mut group_lines = Vec::new();
384
385 for &lid in &gc.lines {
386 if let Some((line_eid, li)) = line_map.get(&lid) {
387 if let Some(line_comp) = world.line_mut(*line_eid) {
389 line_comp.group = group_id;
390 }
391 group_lines.push(li.clone());
392 }
393 }
394
395 let mut group = ElevatorGroup::new(group_id, gc.name.clone(), group_lines);
396 if let Some(mode) = gc.hall_call_mode {
397 group.set_hall_call_mode(mode);
398 }
399 if let Some(ticks) = gc.ack_latency_ticks {
400 group.set_ack_latency_ticks(ticks);
401 }
402 groups.push(group);
403
404 let dispatch: Box<dyn DispatchStrategy> = gc
406 .dispatch
407 .instantiate()
408 .unwrap_or_else(|| Box::new(crate::dispatch::scan::ScanDispatch::new()));
409 dispatchers.insert(group_id, dispatch);
410 strategy_ids.insert(group_id, gc.dispatch.clone());
411 }
412 } else {
413 let group_id = GroupId(0);
415 let mut group_lines = Vec::new();
416
417 for (line_eid, li) in line_map.values() {
418 if let Some(line_comp) = world.line_mut(*line_eid) {
419 line_comp.group = group_id;
420 }
421 group_lines.push(li.clone());
422 }
423
424 let group = ElevatorGroup::new(group_id, "Default".into(), group_lines);
425 groups.push(group);
426
427 let dispatch: Box<dyn DispatchStrategy> =
428 Box::new(crate::dispatch::scan::ScanDispatch::new());
429 dispatchers.insert(group_id, dispatch);
430 strategy_ids.insert(group_id, BuiltinStrategy::Scan);
431 }
432
433 for (gid, d) in builder_dispatchers {
435 dispatchers.insert(gid, d);
436 }
437
438 (groups, dispatchers, strategy_ids)
439 }
440
441 #[allow(clippy::too_many_arguments)]
443 pub(crate) fn from_parts(
444 world: World,
445 tick: u64,
446 dt: f64,
447 groups: Vec<ElevatorGroup>,
448 stop_lookup: HashMap<StopId, EntityId>,
449 dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
450 strategy_ids: BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
451 metrics: Metrics,
452 ticks_per_second: f64,
453 ) -> Self {
454 let mut rider_index = RiderIndex::default();
455 rider_index.rebuild(&world);
456 Self {
457 world,
458 events: EventBus::default(),
459 pending_output: Vec::new(),
460 tick,
461 dt,
462 groups,
463 stop_lookup,
464 dispatchers,
465 strategy_ids,
466 repositioners: BTreeMap::new(),
467 reposition_ids: BTreeMap::new(),
468 metrics,
469 time: TimeAdapter::new(ticks_per_second),
470 hooks: PhaseHooks::default(),
471 elevator_ids_buf: Vec::new(),
472 topo_graph: Mutex::new(TopologyGraph::new()),
473 rider_index,
474 }
475 }
476
477 pub(crate) fn validate_config(config: &SimConfig) -> Result<(), SimError> {
479 if config.building.stops.is_empty() {
480 return Err(SimError::InvalidConfig {
481 field: "building.stops",
482 reason: "at least one stop is required".into(),
483 });
484 }
485
486 let mut seen_ids = HashSet::new();
488 for stop in &config.building.stops {
489 if !seen_ids.insert(stop.id) {
490 return Err(SimError::InvalidConfig {
491 field: "building.stops",
492 reason: format!("duplicate {}", stop.id),
493 });
494 }
495 }
496
497 let stop_ids: HashSet<StopId> = config.building.stops.iter().map(|s| s.id).collect();
498
499 if let Some(line_configs) = &config.building.lines {
500 Self::validate_explicit_topology(line_configs, &stop_ids, &config.building)?;
502 } else {
503 Self::validate_legacy_elevators(&config.elevators, &config.building)?;
505 }
506
507 if config.simulation.ticks_per_second <= 0.0 {
508 return Err(SimError::InvalidConfig {
509 field: "simulation.ticks_per_second",
510 reason: format!(
511 "must be positive, got {}",
512 config.simulation.ticks_per_second
513 ),
514 });
515 }
516
517 Ok(())
518 }
519
520 fn validate_legacy_elevators(
522 elevators: &[crate::config::ElevatorConfig],
523 building: &crate::config::BuildingConfig,
524 ) -> Result<(), SimError> {
525 if elevators.is_empty() {
526 return Err(SimError::InvalidConfig {
527 field: "elevators",
528 reason: "at least one elevator is required".into(),
529 });
530 }
531
532 for elev in elevators {
533 Self::validate_elevator_config(elev, building)?;
534 }
535
536 Ok(())
537 }
538
539 fn validate_elevator_config(
541 elev: &crate::config::ElevatorConfig,
542 building: &crate::config::BuildingConfig,
543 ) -> Result<(), SimError> {
544 if elev.max_speed <= 0.0 {
545 return Err(SimError::InvalidConfig {
546 field: "elevators.max_speed",
547 reason: format!("must be positive, got {}", elev.max_speed),
548 });
549 }
550 if elev.acceleration <= 0.0 {
551 return Err(SimError::InvalidConfig {
552 field: "elevators.acceleration",
553 reason: format!("must be positive, got {}", elev.acceleration),
554 });
555 }
556 if elev.deceleration <= 0.0 {
557 return Err(SimError::InvalidConfig {
558 field: "elevators.deceleration",
559 reason: format!("must be positive, got {}", elev.deceleration),
560 });
561 }
562 if elev.weight_capacity <= 0.0 {
563 return Err(SimError::InvalidConfig {
564 field: "elevators.weight_capacity",
565 reason: format!("must be positive, got {}", elev.weight_capacity),
566 });
567 }
568 if elev.inspection_speed_factor <= 0.0 {
569 return Err(SimError::InvalidConfig {
570 field: "elevators.inspection_speed_factor",
571 reason: format!("must be positive, got {}", elev.inspection_speed_factor),
572 });
573 }
574 if !building.stops.iter().any(|s| s.id == elev.starting_stop) {
575 return Err(SimError::InvalidConfig {
576 field: "elevators.starting_stop",
577 reason: format!("references non-existent {}", elev.starting_stop),
578 });
579 }
580 Ok(())
581 }
582
583 fn validate_explicit_topology(
585 line_configs: &[crate::config::LineConfig],
586 stop_ids: &HashSet<StopId>,
587 building: &crate::config::BuildingConfig,
588 ) -> Result<(), SimError> {
589 let mut seen_line_ids = HashSet::new();
591 for lc in line_configs {
592 if !seen_line_ids.insert(lc.id) {
593 return Err(SimError::InvalidConfig {
594 field: "building.lines",
595 reason: format!("duplicate line id {}", lc.id),
596 });
597 }
598 }
599
600 for lc in line_configs {
602 for sid in &lc.serves {
603 if !stop_ids.contains(sid) {
604 return Err(SimError::InvalidConfig {
605 field: "building.lines.serves",
606 reason: format!("line {} references non-existent {}", lc.id, sid),
607 });
608 }
609 }
610 for ec in &lc.elevators {
612 Self::validate_elevator_config(ec, building)?;
613 }
614
615 if let Some(max) = lc.max_cars
617 && lc.elevators.len() > max
618 {
619 return Err(SimError::InvalidConfig {
620 field: "building.lines.max_cars",
621 reason: format!(
622 "line {} has {} elevators but max_cars is {max}",
623 lc.id,
624 lc.elevators.len()
625 ),
626 });
627 }
628 }
629
630 let has_elevator = line_configs.iter().any(|lc| !lc.elevators.is_empty());
632 if !has_elevator {
633 return Err(SimError::InvalidConfig {
634 field: "building.lines",
635 reason: "at least one line must have at least one elevator".into(),
636 });
637 }
638
639 let served: HashSet<StopId> = line_configs
641 .iter()
642 .flat_map(|lc| lc.serves.iter().copied())
643 .collect();
644 for sid in stop_ids {
645 if !served.contains(sid) {
646 return Err(SimError::InvalidConfig {
647 field: "building.lines",
648 reason: format!("orphaned stop {sid} not served by any line"),
649 });
650 }
651 }
652
653 if let Some(group_configs) = &building.groups {
655 let line_id_set: HashSet<u32> = line_configs.iter().map(|lc| lc.id).collect();
656
657 let mut seen_group_ids = HashSet::new();
658 for gc in group_configs {
659 if !seen_group_ids.insert(gc.id) {
660 return Err(SimError::InvalidConfig {
661 field: "building.groups",
662 reason: format!("duplicate group id {}", gc.id),
663 });
664 }
665 for &lid in &gc.lines {
666 if !line_id_set.contains(&lid) {
667 return Err(SimError::InvalidConfig {
668 field: "building.groups.lines",
669 reason: format!(
670 "group {} references non-existent line id {}",
671 gc.id, lid
672 ),
673 });
674 }
675 }
676 }
677
678 let referenced_line_ids: HashSet<u32> = group_configs
680 .iter()
681 .flat_map(|g| g.lines.iter().copied())
682 .collect();
683 for lc in line_configs {
684 if !referenced_line_ids.contains(&lc.id) {
685 return Err(SimError::InvalidConfig {
686 field: "building.lines",
687 reason: format!("line {} is not assigned to any group", lc.id),
688 });
689 }
690 }
691 }
692
693 Ok(())
694 }
695
696 pub fn set_dispatch(
703 &mut self,
704 group: GroupId,
705 strategy: Box<dyn DispatchStrategy>,
706 id: crate::dispatch::BuiltinStrategy,
707 ) {
708 self.dispatchers.insert(group, strategy);
709 self.strategy_ids.insert(group, id);
710 }
711
712 pub fn set_reposition(
719 &mut self,
720 group: GroupId,
721 strategy: Box<dyn RepositionStrategy>,
722 id: BuiltinReposition,
723 ) {
724 self.repositioners.insert(group, strategy);
725 self.reposition_ids.insert(group, id);
726 }
727
728 pub fn remove_reposition(&mut self, group: GroupId) {
730 self.repositioners.remove(&group);
731 self.reposition_ids.remove(&group);
732 }
733
734 #[must_use]
736 pub fn reposition_id(&self, group: GroupId) -> Option<&BuiltinReposition> {
737 self.reposition_ids.get(&group)
738 }
739
740 pub fn add_before_hook(
747 &mut self,
748 phase: Phase,
749 hook: impl Fn(&mut World) + Send + Sync + 'static,
750 ) {
751 self.hooks.add_before(phase, Box::new(hook));
752 }
753
754 pub fn add_after_hook(
759 &mut self,
760 phase: Phase,
761 hook: impl Fn(&mut World) + Send + Sync + 'static,
762 ) {
763 self.hooks.add_after(phase, Box::new(hook));
764 }
765
766 pub fn add_before_group_hook(
768 &mut self,
769 phase: Phase,
770 group: GroupId,
771 hook: impl Fn(&mut World) + Send + Sync + 'static,
772 ) {
773 self.hooks.add_before_group(phase, group, Box::new(hook));
774 }
775
776 pub fn add_after_group_hook(
778 &mut self,
779 phase: Phase,
780 group: GroupId,
781 hook: impl Fn(&mut World) + Send + Sync + 'static,
782 ) {
783 self.hooks.add_after_group(phase, group, Box::new(hook));
784 }
785}