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 reposition_buf: Vec::new(),
173 topo_graph: Mutex::new(TopologyGraph::new()),
174 rider_index: RiderIndex::default(),
175 })
176 }
177
178 fn spawn_elevator_entity(
184 world: &mut World,
185 ec: &crate::config::ElevatorConfig,
186 line: EntityId,
187 stop_lookup: &HashMap<StopId, EntityId>,
188 start_pos_lookup: &[crate::stop::StopConfig],
189 ) -> EntityId {
190 let eid = world.spawn();
191 let start_pos = start_pos_lookup
192 .iter()
193 .find(|s| s.id == ec.starting_stop)
194 .map_or(0.0, |s| s.position);
195 world.set_position(eid, Position { value: start_pos });
196 world.set_velocity(eid, Velocity { value: 0.0 });
197 let restricted: HashSet<EntityId> = ec
198 .restricted_stops
199 .iter()
200 .filter_map(|sid| stop_lookup.get(sid).copied())
201 .collect();
202 world.set_elevator(
203 eid,
204 Elevator {
205 phase: ElevatorPhase::Idle,
206 door: DoorState::Closed,
207 max_speed: ec.max_speed,
208 acceleration: ec.acceleration,
209 deceleration: ec.deceleration,
210 weight_capacity: ec.weight_capacity,
211 current_load: crate::components::Weight::ZERO,
212 riders: Vec::new(),
213 target_stop: None,
214 door_transition_ticks: ec.door_transition_ticks,
215 door_open_ticks: ec.door_open_ticks,
216 line,
217 repositioning: false,
218 restricted_stops: restricted,
219 inspection_speed_factor: ec.inspection_speed_factor,
220 going_up: true,
221 going_down: true,
222 move_count: 0,
223 door_command_queue: Vec::new(),
224 manual_target_velocity: None,
225 },
226 );
227 #[cfg(feature = "energy")]
228 if let Some(ref profile) = ec.energy_profile {
229 world.set_energy_profile(eid, profile.clone());
230 world.set_energy_metrics(eid, crate::energy::EnergyMetrics::default());
231 }
232 if let Some(mode) = ec.service_mode {
233 world.set_service_mode(eid, mode);
234 }
235 world.set_destination_queue(eid, crate::components::DestinationQueue::new());
236 eid
237 }
238
239 fn build_legacy_topology(
241 world: &mut World,
242 config: &SimConfig,
243 stop_lookup: &HashMap<StopId, EntityId>,
244 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
245 ) -> TopologyResult {
246 let all_stop_entities: Vec<EntityId> = stop_lookup.values().copied().collect();
247 let stop_positions: Vec<f64> = config.building.stops.iter().map(|s| s.position).collect();
248 let min_pos = stop_positions.iter().copied().fold(f64::INFINITY, f64::min);
249 let max_pos = stop_positions
250 .iter()
251 .copied()
252 .fold(f64::NEG_INFINITY, f64::max);
253
254 let default_line_eid = world.spawn();
255 world.set_line(
256 default_line_eid,
257 Line {
258 name: "Default".into(),
259 group: GroupId(0),
260 orientation: Orientation::Vertical,
261 position: None,
262 min_position: min_pos,
263 max_position: max_pos,
264 max_cars: None,
265 },
266 );
267
268 let mut elevator_entities = Vec::new();
269 for ec in &config.elevators {
270 let eid = Self::spawn_elevator_entity(
271 world,
272 ec,
273 default_line_eid,
274 stop_lookup,
275 &config.building.stops,
276 );
277 elevator_entities.push(eid);
278 }
279
280 let default_line_info =
281 LineInfo::new(default_line_eid, elevator_entities, all_stop_entities);
282
283 let group = ElevatorGroup::new(GroupId(0), "Default".into(), vec![default_line_info]);
284
285 let mut dispatchers = BTreeMap::new();
287 let dispatch = builder_dispatchers.into_iter().next().map_or_else(
288 || Box::new(crate::dispatch::scan::ScanDispatch::new()) as Box<dyn DispatchStrategy>,
289 |(_, d)| d,
290 );
291 dispatchers.insert(GroupId(0), dispatch);
292
293 let mut strategy_ids = BTreeMap::new();
294 strategy_ids.insert(GroupId(0), BuiltinStrategy::Scan);
295
296 (vec![group], dispatchers, strategy_ids)
297 }
298
299 #[allow(clippy::too_many_lines)]
301 fn build_explicit_topology(
302 world: &mut World,
303 config: &SimConfig,
304 line_configs: &[crate::config::LineConfig],
305 stop_lookup: &HashMap<StopId, EntityId>,
306 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
307 ) -> TopologyResult {
308 let mut line_map: HashMap<u32, (EntityId, LineInfo)> = HashMap::new();
310
311 for lc in line_configs {
312 let served_entities: Vec<EntityId> = lc
314 .serves
315 .iter()
316 .filter_map(|sid| stop_lookup.get(sid).copied())
317 .collect();
318
319 let stop_positions: Vec<f64> = lc
321 .serves
322 .iter()
323 .filter_map(|sid| {
324 config
325 .building
326 .stops
327 .iter()
328 .find(|s| s.id == *sid)
329 .map(|s| s.position)
330 })
331 .collect();
332 let auto_min = stop_positions.iter().copied().fold(f64::INFINITY, f64::min);
333 let auto_max = stop_positions
334 .iter()
335 .copied()
336 .fold(f64::NEG_INFINITY, f64::max);
337
338 let min_pos = lc.min_position.unwrap_or(auto_min);
339 let max_pos = lc.max_position.unwrap_or(auto_max);
340
341 let line_eid = world.spawn();
342 world.set_line(
345 line_eid,
346 Line {
347 name: lc.name.clone(),
348 group: GroupId(0),
349 orientation: lc.orientation,
350 position: lc.position,
351 min_position: min_pos,
352 max_position: max_pos,
353 max_cars: lc.max_cars,
354 },
355 );
356
357 let mut elevator_entities = Vec::new();
359 for ec in &lc.elevators {
360 let eid = Self::spawn_elevator_entity(
361 world,
362 ec,
363 line_eid,
364 stop_lookup,
365 &config.building.stops,
366 );
367 elevator_entities.push(eid);
368 }
369
370 let line_info = LineInfo::new(line_eid, elevator_entities, served_entities);
371 line_map.insert(lc.id, (line_eid, line_info));
372 }
373
374 let group_configs = config.building.groups.as_deref();
376 let mut groups = Vec::new();
377 let mut dispatchers = BTreeMap::new();
378 let mut strategy_ids = BTreeMap::new();
379
380 if let Some(gcs) = group_configs {
381 for gc in gcs {
382 let group_id = GroupId(gc.id);
383
384 let mut group_lines = Vec::new();
385
386 for &lid in &gc.lines {
387 if let Some((line_eid, li)) = line_map.get(&lid) {
388 if let Some(line_comp) = world.line_mut(*line_eid) {
390 line_comp.group = group_id;
391 }
392 group_lines.push(li.clone());
393 }
394 }
395
396 let mut group = ElevatorGroup::new(group_id, gc.name.clone(), group_lines);
397 if let Some(mode) = gc.hall_call_mode {
398 group.set_hall_call_mode(mode);
399 }
400 if let Some(ticks) = gc.ack_latency_ticks {
401 group.set_ack_latency_ticks(ticks);
402 }
403 groups.push(group);
404
405 let dispatch: Box<dyn DispatchStrategy> = gc
407 .dispatch
408 .instantiate()
409 .unwrap_or_else(|| Box::new(crate::dispatch::scan::ScanDispatch::new()));
410 dispatchers.insert(group_id, dispatch);
411 strategy_ids.insert(group_id, gc.dispatch.clone());
412 }
413 } else {
414 let group_id = GroupId(0);
416 let mut group_lines = Vec::new();
417
418 for (line_eid, li) in line_map.values() {
419 if let Some(line_comp) = world.line_mut(*line_eid) {
420 line_comp.group = group_id;
421 }
422 group_lines.push(li.clone());
423 }
424
425 let group = ElevatorGroup::new(group_id, "Default".into(), group_lines);
426 groups.push(group);
427
428 let dispatch: Box<dyn DispatchStrategy> =
429 Box::new(crate::dispatch::scan::ScanDispatch::new());
430 dispatchers.insert(group_id, dispatch);
431 strategy_ids.insert(group_id, BuiltinStrategy::Scan);
432 }
433
434 for (gid, d) in builder_dispatchers {
436 dispatchers.insert(gid, d);
437 }
438
439 (groups, dispatchers, strategy_ids)
440 }
441
442 #[allow(clippy::too_many_arguments)]
444 pub(crate) fn from_parts(
445 world: World,
446 tick: u64,
447 dt: f64,
448 groups: Vec<ElevatorGroup>,
449 stop_lookup: HashMap<StopId, EntityId>,
450 dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
451 strategy_ids: BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
452 metrics: Metrics,
453 ticks_per_second: f64,
454 ) -> Self {
455 let mut rider_index = RiderIndex::default();
456 rider_index.rebuild(&world);
457 Self {
458 world,
459 events: EventBus::default(),
460 pending_output: Vec::new(),
461 tick,
462 dt,
463 groups,
464 stop_lookup,
465 dispatchers,
466 strategy_ids,
467 repositioners: BTreeMap::new(),
468 reposition_ids: BTreeMap::new(),
469 metrics,
470 time: TimeAdapter::new(ticks_per_second),
471 hooks: PhaseHooks::default(),
472 elevator_ids_buf: Vec::new(),
473 reposition_buf: Vec::new(),
474 topo_graph: Mutex::new(TopologyGraph::new()),
475 rider_index,
476 }
477 }
478
479 pub(crate) fn validate_config(config: &SimConfig) -> Result<(), SimError> {
481 if config.building.stops.is_empty() {
482 return Err(SimError::InvalidConfig {
483 field: "building.stops",
484 reason: "at least one stop is required".into(),
485 });
486 }
487
488 let mut seen_ids = HashSet::new();
490 for stop in &config.building.stops {
491 if !seen_ids.insert(stop.id) {
492 return Err(SimError::InvalidConfig {
493 field: "building.stops",
494 reason: format!("duplicate {}", stop.id),
495 });
496 }
497 if !stop.position.is_finite() {
498 return Err(SimError::InvalidConfig {
499 field: "building.stops.position",
500 reason: format!("{} has non-finite position {}", stop.id, stop.position),
501 });
502 }
503 }
504
505 let stop_ids: HashSet<StopId> = config.building.stops.iter().map(|s| s.id).collect();
506
507 if let Some(line_configs) = &config.building.lines {
508 Self::validate_explicit_topology(line_configs, &stop_ids, &config.building)?;
510 } else {
511 Self::validate_legacy_elevators(&config.elevators, &config.building)?;
513 }
514
515 if config.simulation.ticks_per_second <= 0.0 {
516 return Err(SimError::InvalidConfig {
517 field: "simulation.ticks_per_second",
518 reason: format!(
519 "must be positive, got {}",
520 config.simulation.ticks_per_second
521 ),
522 });
523 }
524
525 Ok(())
526 }
527
528 fn validate_legacy_elevators(
530 elevators: &[crate::config::ElevatorConfig],
531 building: &crate::config::BuildingConfig,
532 ) -> Result<(), SimError> {
533 if elevators.is_empty() {
534 return Err(SimError::InvalidConfig {
535 field: "elevators",
536 reason: "at least one elevator is required".into(),
537 });
538 }
539
540 for elev in elevators {
541 Self::validate_elevator_config(elev, building)?;
542 }
543
544 Ok(())
545 }
546
547 fn validate_elevator_config(
549 elev: &crate::config::ElevatorConfig,
550 building: &crate::config::BuildingConfig,
551 ) -> Result<(), SimError> {
552 if elev.max_speed.value() <= 0.0 {
553 return Err(SimError::InvalidConfig {
554 field: "elevators.max_speed",
555 reason: format!("must be positive, got {}", elev.max_speed.value()),
556 });
557 }
558 if elev.acceleration.value() <= 0.0 {
559 return Err(SimError::InvalidConfig {
560 field: "elevators.acceleration",
561 reason: format!("must be positive, got {}", elev.acceleration.value()),
562 });
563 }
564 if elev.deceleration.value() <= 0.0 {
565 return Err(SimError::InvalidConfig {
566 field: "elevators.deceleration",
567 reason: format!("must be positive, got {}", elev.deceleration.value()),
568 });
569 }
570 if elev.weight_capacity.value() <= 0.0 {
571 return Err(SimError::InvalidConfig {
572 field: "elevators.weight_capacity",
573 reason: format!("must be positive, got {}", elev.weight_capacity.value()),
574 });
575 }
576 if elev.inspection_speed_factor <= 0.0 {
577 return Err(SimError::InvalidConfig {
578 field: "elevators.inspection_speed_factor",
579 reason: format!("must be positive, got {}", elev.inspection_speed_factor),
580 });
581 }
582 if elev.door_transition_ticks == 0 {
583 return Err(SimError::InvalidConfig {
584 field: "elevators.door_transition_ticks",
585 reason: "must be > 0".into(),
586 });
587 }
588 if elev.door_open_ticks == 0 {
589 return Err(SimError::InvalidConfig {
590 field: "elevators.door_open_ticks",
591 reason: "must be > 0".into(),
592 });
593 }
594 if !building.stops.iter().any(|s| s.id == elev.starting_stop) {
595 return Err(SimError::InvalidConfig {
596 field: "elevators.starting_stop",
597 reason: format!("references non-existent {}", elev.starting_stop),
598 });
599 }
600 Ok(())
601 }
602
603 fn validate_explicit_topology(
605 line_configs: &[crate::config::LineConfig],
606 stop_ids: &HashSet<StopId>,
607 building: &crate::config::BuildingConfig,
608 ) -> Result<(), SimError> {
609 let mut seen_line_ids = HashSet::new();
611 for lc in line_configs {
612 if !seen_line_ids.insert(lc.id) {
613 return Err(SimError::InvalidConfig {
614 field: "building.lines",
615 reason: format!("duplicate line id {}", lc.id),
616 });
617 }
618 }
619
620 for lc in line_configs {
622 if lc.serves.is_empty() {
623 return Err(SimError::InvalidConfig {
624 field: "building.lines.serves",
625 reason: format!("line {} has no stops", lc.id),
626 });
627 }
628 for sid in &lc.serves {
629 if !stop_ids.contains(sid) {
630 return Err(SimError::InvalidConfig {
631 field: "building.lines.serves",
632 reason: format!("line {} references non-existent {}", lc.id, sid),
633 });
634 }
635 }
636 for ec in &lc.elevators {
638 Self::validate_elevator_config(ec, building)?;
639 }
640
641 if let Some(max) = lc.max_cars
643 && lc.elevators.len() > max
644 {
645 return Err(SimError::InvalidConfig {
646 field: "building.lines.max_cars",
647 reason: format!(
648 "line {} has {} elevators but max_cars is {max}",
649 lc.id,
650 lc.elevators.len()
651 ),
652 });
653 }
654 }
655
656 let has_elevator = line_configs.iter().any(|lc| !lc.elevators.is_empty());
658 if !has_elevator {
659 return Err(SimError::InvalidConfig {
660 field: "building.lines",
661 reason: "at least one line must have at least one elevator".into(),
662 });
663 }
664
665 let served: HashSet<StopId> = line_configs
667 .iter()
668 .flat_map(|lc| lc.serves.iter().copied())
669 .collect();
670 for sid in stop_ids {
671 if !served.contains(sid) {
672 return Err(SimError::InvalidConfig {
673 field: "building.lines",
674 reason: format!("orphaned stop {sid} not served by any line"),
675 });
676 }
677 }
678
679 if let Some(group_configs) = &building.groups {
681 let line_id_set: HashSet<u32> = line_configs.iter().map(|lc| lc.id).collect();
682
683 let mut seen_group_ids = HashSet::new();
684 for gc in group_configs {
685 if !seen_group_ids.insert(gc.id) {
686 return Err(SimError::InvalidConfig {
687 field: "building.groups",
688 reason: format!("duplicate group id {}", gc.id),
689 });
690 }
691 for &lid in &gc.lines {
692 if !line_id_set.contains(&lid) {
693 return Err(SimError::InvalidConfig {
694 field: "building.groups.lines",
695 reason: format!(
696 "group {} references non-existent line id {}",
697 gc.id, lid
698 ),
699 });
700 }
701 }
702 }
703
704 let referenced_line_ids: HashSet<u32> = group_configs
706 .iter()
707 .flat_map(|g| g.lines.iter().copied())
708 .collect();
709 for lc in line_configs {
710 if !referenced_line_ids.contains(&lc.id) {
711 return Err(SimError::InvalidConfig {
712 field: "building.lines",
713 reason: format!("line {} is not assigned to any group", lc.id),
714 });
715 }
716 }
717 }
718
719 Ok(())
720 }
721
722 pub fn set_dispatch(
729 &mut self,
730 group: GroupId,
731 strategy: Box<dyn DispatchStrategy>,
732 id: crate::dispatch::BuiltinStrategy,
733 ) {
734 self.dispatchers.insert(group, strategy);
735 self.strategy_ids.insert(group, id);
736 }
737
738 pub fn set_reposition(
745 &mut self,
746 group: GroupId,
747 strategy: Box<dyn RepositionStrategy>,
748 id: BuiltinReposition,
749 ) {
750 self.repositioners.insert(group, strategy);
751 self.reposition_ids.insert(group, id);
752 }
753
754 pub fn remove_reposition(&mut self, group: GroupId) {
756 self.repositioners.remove(&group);
757 self.reposition_ids.remove(&group);
758 }
759
760 #[must_use]
762 pub fn reposition_id(&self, group: GroupId) -> Option<&BuiltinReposition> {
763 self.reposition_ids.get(&group)
764 }
765
766 pub fn add_before_hook(
773 &mut self,
774 phase: Phase,
775 hook: impl Fn(&mut World) + Send + Sync + 'static,
776 ) {
777 self.hooks.add_before(phase, Box::new(hook));
778 }
779
780 pub fn add_after_hook(
785 &mut self,
786 phase: Phase,
787 hook: impl Fn(&mut World) + Send + Sync + 'static,
788 ) {
789 self.hooks.add_after(phase, Box::new(hook));
790 }
791
792 pub fn add_before_group_hook(
794 &mut self,
795 phase: Phase,
796 group: GroupId,
797 hook: impl Fn(&mut World) + Send + Sync + 'static,
798 ) {
799 self.hooks.add_before_group(phase, group, Box::new(hook));
800 }
801
802 pub fn add_after_group_hook(
804 &mut self,
805 phase: Phase,
806 group: GroupId,
807 hook: impl Fn(&mut World) + Send + Sync + 'static,
808 ) {
809 self.hooks.add_after_group(phase, group, Box::new(hook));
810 }
811}