pub(crate) mod assignment;
pub mod destination;
pub mod etd;
pub mod look;
pub mod manifest;
pub mod nearest_car;
pub mod reposition;
pub mod rsr;
pub mod scan;
pub mod scratch;
pub(crate) mod sweep;
pub use assignment::AssignmentResult;
#[cfg(test)]
pub(crate) use assignment::assign;
pub(crate) use assignment::{DispatchScratch, assign_with_scratch};
pub use destination::{AssignedCar, DestinationDispatch};
pub use etd::EtdDispatch;
pub use look::LookDispatch;
pub use manifest::{DispatchManifest, RiderInfo};
pub use nearest_car::NearestCarDispatch;
pub use rsr::RsrDispatch;
pub use scan::ScanDispatch;
pub use scratch::PrepareScratch;
use serde::{Deserialize, Serialize};
use crate::components::Route;
use crate::entity::EntityId;
use crate::ids::GroupId;
use crate::world::World;
#[must_use]
pub fn pair_is_useful(ctx: &RankContext<'_>, respect_aboard_path: bool) -> 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);
let servable = if waiting.is_empty() {
ctx.manifest
.hall_calls_at_stop
.get(&ctx.stop)
.is_some_and(|calls| calls.iter().any(|c| c.pending_riders.is_empty()))
} else {
waiting
.iter()
.any(|r| rider_can_board(r, car, ctx, remaining_capacity))
};
if !servable {
return false;
}
if !respect_aboard_path || car.riders().is_empty() {
return true;
}
let has_routed_rider = car.riders().iter().any(|&rid| {
ctx.world
.route(rid)
.and_then(Route::current_destination)
.is_some()
});
if !has_routed_rider {
return true;
}
let to_cand = ctx.stop_position() - ctx.car_position();
car.riders().iter().any(|&rid| {
let Some(dest) = ctx.world.route(rid).and_then(Route::current_destination) else {
return false;
};
let Some(dest_pos) = ctx.world.stop_position(dest) else {
return false;
};
let to_dest = dest_pos - ctx.car_position();
to_dest * to_cand >= 0.0 && to_cand.abs() <= to_dest.abs()
})
}
#[must_use]
pub(crate) fn wait_ticks_sum(riders: &[RiderInfo]) -> f64 {
riders.iter().map(|r| r.wait_ticks as f64).sum()
}
#[must_use]
pub(crate) fn wait_ticks_squared_sum(riders: &[RiderInfo]) -> f64 {
riders
.iter()
.map(|r| {
let w = r.wait_ticks as f64;
w * w
})
.sum()
}
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, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BuiltinStrategy {
Scan,
Look,
NearestCar,
Etd,
Destination,
Rsr,
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::Rsr => write!(f, "Rsr"),
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::default())),
Self::Destination => Some(Box::new(destination::DestinationDispatch::new())),
Self::Rsr => Some(Box::new(rsr::RsrDispatch::default())),
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) fn add_elevator(&mut self, elevator: EntityId) -> bool {
if self.elevators.contains(&elevator) {
false
} else {
self.elevators.push(elevator);
true
}
}
pub(crate) fn remove_elevator(&mut self, elevator: EntityId) -> bool {
let len_before = self.elevators.len();
self.elevators.retain(|&e| e != elevator);
self.elevators.len() != len_before
}
pub(crate) fn add_stop(&mut self, stop: EntityId) -> bool {
if self.serves.contains(&stop) {
false
} else {
self.serves.push(stop);
true
}
}
pub(crate) fn remove_stop(&mut self, stop: EntityId) -> bool {
let len_before = self.serves.len();
self.serves.retain(|&s| s != stop);
self.serves.len() != len_before
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum HallCallMode {
#[default]
Classic,
Destination,
}
impl std::fmt::Display for HallCallMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Classic => f.write_str("classic"),
Self::Destination => f.write_str("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;
}
}
#[must_use]
pub fn elevator_line_serves<'a>(
world: &World,
groups: &'a [ElevatorGroup],
elevator: EntityId,
) -> Option<&'a [EntityId]> {
let line_eid = world.elevator(elevator)?.line();
groups
.iter()
.flat_map(ElevatorGroup::lines)
.find(|li| li.entity() == line_eid)
.map(LineInfo::serves)
}
pub type LineServesIndex<'a> = std::collections::HashMap<EntityId, &'a [EntityId]>;
#[must_use]
pub fn build_line_serves_index(groups: &[ElevatorGroup]) -> LineServesIndex<'_> {
let mut idx: LineServesIndex<'_> = std::collections::HashMap::new();
for li in groups.iter().flat_map(ElevatorGroup::lines) {
idx.insert(li.entity(), li.serves());
}
idx
}
#[must_use]
pub fn elevator_line_serves_indexed<'a>(
world: &World,
index: &LineServesIndex<'a>,
elevator: EntityId,
) -> Option<&'a [EntityId]> {
let line_eid = world.elevator(elevator)?.line();
index.get(&line_eid).copied()
}
#[non_exhaustive]
pub struct RankContext<'a> {
pub car: EntityId,
pub stop: EntityId,
pub group: &'a ElevatorGroup,
pub manifest: &'a DispatchManifest,
pub world: &'a World,
}
impl RankContext<'_> {
#[must_use]
pub fn car_position(&self) -> f64 {
self.world.position(self.car).map_or(0.0, |p| p.value)
}
#[must_use]
pub fn stop_position(&self) -> f64 {
self.world.stop_position(self.stop).unwrap_or(0.0)
}
}
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(&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) {}
#[must_use]
fn builtin_id(&self) -> Option<BuiltinStrategy> {
None
}
#[must_use]
fn snapshot_config(&self) -> Option<String> {
None
}
fn restore_config(&mut self, _serialized: &str) -> Result<(), String> {
Ok(())
}
}
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)>,
);
#[must_use]
fn builtin_id(&self) -> Option<BuiltinReposition> {
None
}
#[must_use]
fn min_arrival_log_window(&self) -> u64 {
0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BuiltinReposition {
SpreadEvenly,
ReturnToLobby,
DemandWeighted,
NearestIdle,
PredictiveParking,
Adaptive,
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::Adaptive => write!(f, "Adaptive"),
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::Adaptive => Some(Box::new(reposition::AdaptiveParking::new())),
Self::Custom(_) => None,
}
}
}