pub mod destination;
pub mod etd;
pub mod look;
pub mod nearest_car;
pub mod reposition;
pub mod scan;
pub(crate) mod sweep;
pub use destination::{AssignedCar, DestinationDispatch};
pub use etd::EtdDispatch;
pub use look::LookDispatch;
pub use nearest_car::NearestCarDispatch;
pub use scan::ScanDispatch;
use serde::{Deserialize, Serialize};
use crate::components::{CallDirection, CarCall, HallCall};
use crate::entity::EntityId;
use crate::ids::GroupId;
use crate::world::World;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct RiderInfo {
pub id: EntityId,
pub destination: Option<EntityId>,
pub weight: f64,
pub wait_ticks: u64,
}
#[derive(Debug, Clone, Default)]
pub struct DispatchManifest {
pub waiting_at_stop: BTreeMap<EntityId, Vec<RiderInfo>>,
pub riding_to_stop: BTreeMap<EntityId, Vec<RiderInfo>>,
pub resident_count_at_stop: BTreeMap<EntityId, usize>,
pub hall_calls_at_stop: BTreeMap<EntityId, Vec<HallCall>>,
pub car_calls_by_car: BTreeMap<EntityId, Vec<CarCall>>,
}
impl DispatchManifest {
#[must_use]
pub fn waiting_count_at(&self, stop: EntityId) -> usize {
self.waiting_at_stop.get(&stop).map_or(0, Vec::len)
}
#[must_use]
pub fn total_weight_at(&self, stop: EntityId) -> f64 {
self.waiting_at_stop
.get(&stop)
.map_or(0.0, |riders| riders.iter().map(|r| r.weight).sum())
}
#[must_use]
pub fn riding_count_to(&self, stop: EntityId) -> usize {
self.riding_to_stop.get(&stop).map_or(0, Vec::len)
}
#[must_use]
pub fn has_demand(&self, stop: EntityId) -> bool {
self.waiting_count_at(stop) > 0 || self.riding_count_to(stop) > 0
}
#[must_use]
pub fn resident_count_at(&self, stop: EntityId) -> usize {
self.resident_count_at_stop.get(&stop).copied().unwrap_or(0)
}
#[must_use]
pub fn hall_call_at(&self, stop: EntityId, direction: CallDirection) -> Option<&HallCall> {
self.hall_calls_at_stop
.get(&stop)?
.iter()
.find(|c| c.direction == direction)
}
pub fn iter_hall_calls(&self) -> impl Iterator<Item = &HallCall> {
self.hall_calls_at_stop.values().flatten()
}
#[must_use]
pub fn car_calls_for(&self, car: EntityId) -> &[CarCall] {
self.car_calls_by_car.get(&car).map_or(&[], Vec::as_slice)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BuiltinStrategy {
Scan,
Look,
NearestCar,
Etd,
Destination,
Custom(String),
}
impl std::fmt::Display for BuiltinStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Scan => write!(f, "Scan"),
Self::Look => write!(f, "Look"),
Self::NearestCar => write!(f, "NearestCar"),
Self::Etd => write!(f, "Etd"),
Self::Destination => write!(f, "Destination"),
Self::Custom(name) => write!(f, "Custom({name})"),
}
}
}
impl BuiltinStrategy {
#[must_use]
pub fn instantiate(&self) -> Option<Box<dyn DispatchStrategy>> {
match self {
Self::Scan => Some(Box::new(scan::ScanDispatch::new())),
Self::Look => Some(Box::new(look::LookDispatch::new())),
Self::NearestCar => Some(Box::new(nearest_car::NearestCarDispatch::new())),
Self::Etd => Some(Box::new(etd::EtdDispatch::new())),
Self::Destination => Some(Box::new(destination::DestinationDispatch::new())),
Self::Custom(_) => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DispatchDecision {
GoToStop(EntityId),
Idle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineInfo {
entity: EntityId,
elevators: Vec<EntityId>,
serves: Vec<EntityId>,
}
impl LineInfo {
#[must_use]
pub const fn new(entity: EntityId, elevators: Vec<EntityId>, serves: Vec<EntityId>) -> Self {
Self {
entity,
elevators,
serves,
}
}
#[must_use]
pub const fn entity(&self) -> EntityId {
self.entity
}
#[must_use]
pub fn elevators(&self) -> &[EntityId] {
&self.elevators
}
#[must_use]
pub fn serves(&self) -> &[EntityId] {
&self.serves
}
pub(crate) const fn set_entity(&mut self, entity: EntityId) {
self.entity = entity;
}
pub(crate) const fn elevators_mut(&mut self) -> &mut Vec<EntityId> {
&mut self.elevators
}
pub(crate) const fn serves_mut(&mut self) -> &mut Vec<EntityId> {
&mut self.serves
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum HallCallMode {
#[default]
Classic,
Destination,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElevatorGroup {
id: GroupId,
name: String,
lines: Vec<LineInfo>,
hall_call_mode: HallCallMode,
ack_latency_ticks: u32,
elevator_entities: Vec<EntityId>,
stop_entities: Vec<EntityId>,
}
impl ElevatorGroup {
#[must_use]
pub fn new(id: GroupId, name: String, lines: Vec<LineInfo>) -> Self {
let mut group = Self {
id,
name,
lines,
hall_call_mode: HallCallMode::default(),
ack_latency_ticks: 0,
elevator_entities: Vec::new(),
stop_entities: Vec::new(),
};
group.rebuild_caches();
group
}
#[must_use]
pub const fn with_hall_call_mode(mut self, mode: HallCallMode) -> Self {
self.hall_call_mode = mode;
self
}
#[must_use]
pub const fn with_ack_latency_ticks(mut self, ticks: u32) -> Self {
self.ack_latency_ticks = ticks;
self
}
pub const fn set_hall_call_mode(&mut self, mode: HallCallMode) {
self.hall_call_mode = mode;
}
pub const fn set_ack_latency_ticks(&mut self, ticks: u32) {
self.ack_latency_ticks = ticks;
}
#[must_use]
pub const fn hall_call_mode(&self) -> HallCallMode {
self.hall_call_mode
}
#[must_use]
pub const fn ack_latency_ticks(&self) -> u32 {
self.ack_latency_ticks
}
#[must_use]
pub const fn id(&self) -> GroupId {
self.id
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn lines(&self) -> &[LineInfo] {
&self.lines
}
pub const fn lines_mut(&mut self) -> &mut Vec<LineInfo> {
&mut self.lines
}
#[must_use]
pub fn elevator_entities(&self) -> &[EntityId] {
&self.elevator_entities
}
#[must_use]
pub fn stop_entities(&self) -> &[EntityId] {
&self.stop_entities
}
pub(crate) fn push_stop(&mut self, stop: EntityId) {
if !self.stop_entities.contains(&stop) {
self.stop_entities.push(stop);
}
}
pub(crate) fn push_elevator(&mut self, elevator: EntityId) {
if !self.elevator_entities.contains(&elevator) {
self.elevator_entities.push(elevator);
}
}
pub fn rebuild_caches(&mut self) {
self.elevator_entities = self
.lines
.iter()
.flat_map(|li| li.elevators.iter().copied())
.collect();
let mut stops: Vec<EntityId> = self
.lines
.iter()
.flat_map(|li| li.serves.iter().copied())
.collect();
stops.sort_unstable();
stops.dedup();
self.stop_entities = stops;
}
}
pub trait DispatchStrategy: Send + Sync {
fn pre_dispatch(
&mut self,
_group: &ElevatorGroup,
_manifest: &DispatchManifest,
_world: &mut World,
) {
}
fn prepare_car(
&mut self,
_car: EntityId,
_car_position: f64,
_group: &ElevatorGroup,
_manifest: &DispatchManifest,
_world: &World,
) {
}
#[allow(clippy::too_many_arguments)]
fn rank(
&mut self,
car: EntityId,
car_position: f64,
stop: EntityId,
stop_position: f64,
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &World,
) -> Option<f64>;
fn fallback(
&mut self,
_car: EntityId,
_car_position: f64,
_group: &ElevatorGroup,
_manifest: &DispatchManifest,
_world: &World,
) -> DispatchDecision {
DispatchDecision::Idle
}
fn notify_removed(&mut self, _elevator: EntityId) {}
}
#[derive(Debug, Clone)]
pub struct AssignmentResult {
pub decisions: Vec<(EntityId, DispatchDecision)>,
}
const ASSIGNMENT_SENTINEL: i64 = 1 << 48;
const ASSIGNMENT_SCALE: f64 = 1_000_000.0;
fn scale_cost(cost: f64) -> i64 {
if !cost.is_finite() || cost < 0.0 {
return ASSIGNMENT_SENTINEL;
}
(cost * ASSIGNMENT_SCALE)
.round()
.clamp(0.0, (ASSIGNMENT_SENTINEL - 1) as f64) as i64
}
pub(crate) fn assign(
strategy: &mut dyn DispatchStrategy,
idle_cars: &[(EntityId, f64)],
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &World,
) -> AssignmentResult {
let pending_stops: Vec<(EntityId, f64)> = group
.stop_entities()
.iter()
.filter(|s| manifest.has_demand(**s))
.filter_map(|s| world.stop_position(*s).map(|p| (*s, p)))
.collect();
let n = idle_cars.len();
let m = pending_stops.len();
if n == 0 {
return AssignmentResult {
decisions: Vec::new(),
};
}
let mut decisions: Vec<(EntityId, DispatchDecision)> = Vec::with_capacity(n);
if m == 0 {
for &(eid, pos) in idle_cars {
let d = strategy.fallback(eid, pos, group, manifest, world);
decisions.push((eid, d));
}
return AssignmentResult { decisions };
}
let cols = n.max(m);
let mut data: Vec<i64> = vec![ASSIGNMENT_SENTINEL; n * cols];
for (i, &(car_eid, car_pos)) in idle_cars.iter().enumerate() {
strategy.prepare_car(car_eid, car_pos, group, manifest, world);
for (j, &(stop_eid, stop_pos)) in pending_stops.iter().enumerate() {
let scaled = strategy
.rank(car_eid, car_pos, stop_eid, stop_pos, group, manifest, world)
.map_or(ASSIGNMENT_SENTINEL, scale_cost);
data[i * cols + j] = scaled;
}
}
let Ok(matrix) = pathfinding::matrix::Matrix::from_vec(n, cols, data) else {
for &(car_eid, car_pos) in idle_cars {
let d = strategy.fallback(car_eid, car_pos, group, manifest, world);
decisions.push((car_eid, d));
}
return AssignmentResult { decisions };
};
let (_, assignments) = pathfinding::kuhn_munkres::kuhn_munkres_min(&matrix);
for (i, &(car_eid, car_pos)) in idle_cars.iter().enumerate() {
let col = assignments[i];
if col < m && matrix[(i, col)] < ASSIGNMENT_SENTINEL {
let (stop_eid, _) = pending_stops[col];
decisions.push((car_eid, DispatchDecision::GoToStop(stop_eid)));
} else {
let d = strategy.fallback(car_eid, car_pos, group, manifest, world);
decisions.push((car_eid, d));
}
}
AssignmentResult { decisions }
}
pub trait RepositionStrategy: Send + Sync {
fn reposition(
&mut self,
idle_elevators: &[(EntityId, f64)],
stop_positions: &[(EntityId, f64)],
group: &ElevatorGroup,
world: &World,
) -> Vec<(EntityId, EntityId)>;
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BuiltinReposition {
SpreadEvenly,
ReturnToLobby,
DemandWeighted,
NearestIdle,
Custom(String),
}
impl std::fmt::Display for BuiltinReposition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SpreadEvenly => write!(f, "SpreadEvenly"),
Self::ReturnToLobby => write!(f, "ReturnToLobby"),
Self::DemandWeighted => write!(f, "DemandWeighted"),
Self::NearestIdle => write!(f, "NearestIdle"),
Self::Custom(name) => write!(f, "Custom({name})"),
}
}
}
impl BuiltinReposition {
#[must_use]
pub fn instantiate(&self) -> Option<Box<dyn RepositionStrategy>> {
match self {
Self::SpreadEvenly => Some(Box::new(reposition::SpreadEvenly)),
Self::ReturnToLobby => Some(Box::new(reposition::ReturnToLobby::new())),
Self::DemandWeighted => Some(Box::new(reposition::DemandWeighted)),
Self::NearestIdle => Some(Box::new(reposition::NearestIdle)),
Self::Custom(_) => None,
}
}
}