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, ElevatorPhase, HallCall, Route, TransportMode, Weight,
};
use crate::entity::EntityId;
use crate::ids::GroupId;
use crate::world::World;
use std::collections::BTreeMap;
#[must_use]
pub fn pair_can_do_work(ctx: &RankContext<'_>) -> bool {
let Some(car) = ctx.world.elevator(ctx.car) else {
return false;
};
let can_exit_here = car
.riders()
.iter()
.any(|&rid| ctx.world.route(rid).and_then(Route::current_destination) == Some(ctx.stop));
if can_exit_here {
return true;
}
if bypass_in_current_direction(car, ctx) {
return false;
}
let remaining_capacity = car.weight_capacity.value() - car.current_load.value();
if remaining_capacity <= 0.0 {
return false;
}
let waiting = ctx.manifest.waiting_riders_at(ctx.stop);
if !waiting.is_empty() {
return waiting
.iter()
.any(|r| rider_can_board(r, car, ctx, remaining_capacity));
}
ctx.manifest
.hall_calls_at_stop
.get(&ctx.stop)
.is_some_and(|calls| calls.iter().any(|c| c.pending_riders.is_empty()))
}
fn rider_can_board(
rider: &RiderInfo,
car: &crate::components::Elevator,
ctx: &RankContext<'_>,
remaining_capacity: f64,
) -> bool {
if rider.weight.value() > remaining_capacity {
return false;
}
let Some(dest) = rider.destination else {
return true;
};
let Some(dest_pos) = ctx.world.stop_position(dest) else {
return true;
};
if dest_pos > ctx.stop_position && !car.going_up() {
return false;
}
if dest_pos < ctx.stop_position && !car.going_down() {
return false;
}
true
}
fn bypass_in_current_direction(car: &crate::components::Elevator, ctx: &RankContext<'_>) -> bool {
let Some(target) = car.phase().moving_target() else {
return false;
};
let Some(target_pos) = ctx.world.stop_position(target) else {
return false;
};
let going_up = target_pos > ctx.car_position;
let going_down = target_pos < ctx.car_position;
if !going_up && !going_down {
return false;
}
let threshold = if going_up {
car.bypass_load_up_pct()
} else {
car.bypass_load_down_pct()
};
let Some(pct) = threshold else {
return false;
};
let capacity = car.weight_capacity().value();
if capacity <= 0.0 {
return false;
}
let load_ratio = car.current_load().value() / capacity;
if load_ratio < pct {
return false;
}
let stop_above = ctx.stop_position > ctx.car_position;
let stop_below = ctx.stop_position < ctx.car_position;
(going_up && stop_above) || (going_down && stop_below)
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct RiderInfo {
pub id: EntityId,
pub destination: Option<EntityId>,
pub weight: Weight,
pub wait_ticks: u64,
}
#[derive(Debug, Clone, Default)]
pub struct DispatchManifest {
pub(crate) waiting_at_stop: BTreeMap<EntityId, Vec<RiderInfo>>,
pub(crate) riding_to_stop: BTreeMap<EntityId, Vec<RiderInfo>>,
pub(crate) resident_count_at_stop: BTreeMap<EntityId, usize>,
pub(crate) hall_calls_at_stop: BTreeMap<EntityId, Vec<HallCall>>,
pub(crate) car_calls_by_car: BTreeMap<EntityId, Vec<CarCall>>,
pub(crate) arrivals_at_stop: BTreeMap<EntityId, u64>,
pub(crate) arrival_window_ticks: u64,
}
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.value()).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
|| self
.hall_calls_at_stop
.get(&stop)
.is_some_and(|calls| calls.iter().any(|c| c.pending_riders.is_empty()))
}
#[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 arrivals_at(&self, stop: EntityId) -> u64 {
self.arrivals_at_stop.get(&stop).copied().unwrap_or(0)
}
#[must_use]
pub const fn arrival_window_ticks(&self) -> u64 {
self.arrival_window_ticks
}
#[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)
}
#[must_use]
pub fn waiting_riders_at(&self, stop: EntityId) -> &[RiderInfo] {
self.waiting_at_stop.get(&stop).map_or(&[], Vec::as_slice)
}
pub fn iter_waiting_stops(&self) -> impl Iterator<Item = (&EntityId, &[RiderInfo])> {
self.waiting_at_stop
.iter()
.map(|(stop, riders)| (stop, riders.as_slice()))
}
#[must_use]
pub fn riding_riders_to(&self, stop: EntityId) -> &[RiderInfo] {
self.riding_to_stop.get(&stop).map_or(&[], Vec::as_slice)
}
pub fn iter_riding_stops(&self) -> impl Iterator<Item = (&EntityId, &[RiderInfo])> {
self.riding_to_stop
.iter()
.map(|(stop, riders)| (stop, riders.as_slice()))
}
pub fn iter_hall_call_stops(&self) -> impl Iterator<Item = (&EntityId, &[HallCall])> {
self.hall_calls_at_stop
.iter()
.map(|(stop, calls)| (stop, calls.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
}
#[must_use]
pub fn accepts_leg(&self, leg: &crate::components::RouteLeg) -> bool {
match leg.via {
crate::components::TransportMode::Group(g) => g == self.id,
crate::components::TransportMode::Line(l) => {
self.lines.iter().any(|li| li.entity() == l)
}
crate::components::TransportMode::Walk => false,
}
}
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;
}
}
#[non_exhaustive]
pub struct RankContext<'a> {
pub car: EntityId,
pub car_position: f64,
pub stop: EntityId,
pub stop_position: f64,
pub group: &'a ElevatorGroup,
pub manifest: &'a DispatchManifest,
pub world: &'a World,
}
impl std::fmt::Debug for RankContext<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RankContext")
.field("car", &self.car)
.field("car_position", &self.car_position)
.field("stop", &self.stop)
.field("stop_position", &self.stop_position)
.field("group", &self.group)
.field("manifest", &self.manifest)
.field("world", &"World { .. }")
.finish()
}
}
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,
) {
}
fn rank(&mut self, ctx: &RankContext<'_>) -> 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 {
debug_assert!(
cost.is_finite() && cost >= 0.0,
"DispatchStrategy::rank() returned invalid cost {cost}; must be finite and non-negative"
);
return ASSIGNMENT_SENTINEL;
}
(cost * ASSIGNMENT_SCALE)
.round()
.clamp(0.0, (ASSIGNMENT_SENTINEL - 1) as f64) as i64
}
fn pending_stops_minus_covered(
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &World,
) -> Vec<(EntityId, f64)> {
let servicing: Vec<(EntityId, EntityId, f64)> = group
.elevator_entities()
.iter()
.filter_map(|&eid| {
let car = world.elevator(eid)?;
let target = car.target_stop()?;
matches!(
car.phase(),
ElevatorPhase::DoorOpening | ElevatorPhase::Loading | ElevatorPhase::DoorClosing
)
.then(|| {
let remaining = car.weight_capacity().value() - car.current_load().value();
(target, car.line(), remaining)
})
})
.collect();
let is_covered = |stop_eid: EntityId| {
let (lines_here, capacity_here): (Vec<EntityId>, f64) =
servicing
.iter()
.fold((Vec::new(), 0.0), |(mut lines, cap), &(stop, line, rem)| {
if stop == stop_eid {
lines.push(line);
(lines, cap + rem)
} else {
(lines, cap)
}
});
if lines_here.is_empty() {
return false;
}
let mut total_weight = 0.0;
for rider in manifest.waiting_riders_at(stop_eid) {
let required_line = world
.route(rider.id)
.and_then(Route::current)
.and_then(|leg| match leg.via {
TransportMode::Line(l) => Some(l),
_ => None,
});
if let Some(required) = required_line
&& !lines_here.contains(&required)
{
return false;
}
total_weight += rider.weight.value();
}
total_weight <= capacity_here
};
group
.stop_entities()
.iter()
.filter(|s| manifest.has_demand(**s) && !is_covered(**s))
.filter_map(|s| world.stop_position(*s).map(|p| (*s, p)))
.collect()
}
pub(crate) fn assign(
strategy: &mut dyn DispatchStrategy,
idle_cars: &[(EntityId, f64)],
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &World,
) -> AssignmentResult {
let pending_stops = pending_stops_minus_covered(group, manifest, world);
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);
let restricted = world
.elevator(car_eid)
.map(|c| c.restricted_stops().clone())
.unwrap_or_default();
for (j, &(stop_eid, stop_pos)) in pending_stops.iter().enumerate() {
if restricted.contains(&stop_eid) {
continue; }
let ctx = RankContext {
car: car_eid,
car_position: car_pos,
stop: stop_eid,
stop_position: stop_pos,
group,
manifest,
world,
};
let scaled = strategy.rank(&ctx).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,
out: &mut Vec<(EntityId, EntityId)>,
);
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BuiltinReposition {
SpreadEvenly,
ReturnToLobby,
DemandWeighted,
NearestIdle,
PredictiveParking,
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::PredictiveParking => write!(f, "PredictiveParking"),
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::PredictiveParking => Some(Box::new(reposition::PredictiveParking::new())),
Self::Custom(_) => None,
}
}
}