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