use std::collections::HashSet;
use crate::components::{ElevatorPhase, Route, TransportMode};
use crate::entity::EntityId;
use crate::world::World;
use super::{
DispatchDecision, DispatchManifest, DispatchStrategy, ElevatorGroup, LineInfo, RankContext,
};
#[derive(Debug, Clone)]
pub struct AssignmentResult {
pub decisions: Vec<(EntityId, DispatchDecision)>,
}
#[derive(Default)]
pub struct DispatchScratch {
pub cost_matrix_mx: Option<pathfinding::matrix::Matrix<i64>>,
pub servicing: Vec<(EntityId, EntityId, f64)>,
pub pending_stops: Vec<(EntityId, f64)>,
pub idle_rider_destinations: HashSet<EntityId>,
pub lines_here: Vec<EntityId>,
pub pinned_pairs: Vec<(EntityId, EntityId)>,
pub committed_pairs: Vec<(EntityId, EntityId)>,
pub idle_elevators: Vec<(EntityId, f64)>,
pub top_k_buf: Vec<(f64, usize)>,
}
impl DispatchScratch {
pub fn clear_all(&mut self) {
self.servicing.clear();
self.pending_stops.clear();
self.idle_rider_destinations.clear();
self.lines_here.clear();
self.pinned_pairs.clear();
self.committed_pairs.clear();
self.idle_elevators.clear();
}
}
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,
idle_cars: &[(EntityId, f64)],
scratch: &mut DispatchScratch,
) {
scratch.servicing.clear();
for &eid in group.elevator_entities() {
let Some(car) = world.elevator(eid) else {
continue;
};
let Some(target) = car.target_stop() else {
continue;
};
if !matches!(
car.phase(),
ElevatorPhase::MovingToStop(_)
| ElevatorPhase::DoorOpening
| ElevatorPhase::Loading
| ElevatorPhase::DoorClosing
) {
continue;
}
let remaining = car.weight_capacity().value() - car.current_load().value();
scratch.servicing.push((target, car.line(), remaining));
}
scratch.idle_rider_destinations.clear();
for &(car_eid, _) in idle_cars {
if let Some(car) = world.elevator(car_eid) {
for &rid in car.riders() {
if let Some(dest) = world.route(rid).and_then(Route::current_destination) {
scratch.idle_rider_destinations.insert(dest);
}
}
}
}
let mut lines_here: Vec<EntityId> = std::mem::take(&mut scratch.lines_here);
let servicing = &scratch.servicing;
let is_covered = |stop_eid: EntityId, lines_here: &mut Vec<EntityId>| -> bool {
lines_here.clear();
let mut capacity_here = 0.0;
for &(stop, line, rem) in servicing {
if stop == stop_eid {
lines_here.push(line);
capacity_here += rem;
}
}
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
};
scratch.pending_stops.clear();
for &stop in group.stop_entities() {
if !manifest.has_demand(stop) {
continue;
}
let keep =
scratch.idle_rider_destinations.contains(&stop) || !is_covered(stop, &mut lines_here);
if !keep {
continue;
}
if let Some(pos) = world.stop_position(stop) {
scratch.pending_stops.push((stop, pos));
}
}
scratch.lines_here = lines_here;
}
#[cfg(test)]
pub fn assign(
strategy: &mut dyn DispatchStrategy,
idle_cars: &[(EntityId, f64)],
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &World,
) -> AssignmentResult {
let mut scratch = DispatchScratch::default();
assign_with_scratch(strategy, idle_cars, group, manifest, world, &mut scratch)
}
#[allow(clippy::too_many_lines)]
pub fn assign_with_scratch(
strategy: &mut dyn DispatchStrategy,
idle_cars: &[(EntityId, f64)],
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &World,
scratch: &mut DispatchScratch,
) -> AssignmentResult {
pending_stops_minus_covered(group, manifest, world, idle_cars, scratch);
let n = idle_cars.len();
let m = scratch.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);
match &mut scratch.cost_matrix_mx {
Some(mx) if mx.rows == n && mx.columns == cols => {
mx.fill(ASSIGNMENT_SENTINEL);
}
slot => {
*slot = Some(pathfinding::matrix::Matrix::new(
n,
cols,
ASSIGNMENT_SENTINEL,
));
}
}
let matrix_ref = scratch
.cost_matrix_mx
.as_mut()
.unwrap_or_else(|| unreachable!("cost_matrix_mx populated by match above"));
let candidate_limit = strategy.candidate_limit();
let mut top_k_buf = std::mem::take(&mut scratch.top_k_buf);
{
let pending_stops = &scratch.pending_stops;
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(crate::components::Elevator::restricted_stops);
let car_serves: Option<&[EntityId]> = world
.elevator(car_eid)
.map(crate::components::Elevator::line)
.and_then(|line_eid| {
group
.lines()
.iter()
.find(|li| li.entity() == line_eid)
.map(LineInfo::serves)
});
debug_assert!(
world.elevator(car_eid).is_none() || car_serves.is_some(),
"car {car_eid:?} on line not present in its group's lines list"
);
top_k_buf.clear();
for (j, &(stop_eid, stop_pos)) in pending_stops.iter().enumerate() {
if restricted.is_some_and(|r| r.contains(&stop_eid)) {
continue;
}
if car_serves.is_some_and(|s| !s.contains(&stop_eid)) {
continue;
}
let dist = (car_pos - stop_pos).abs();
top_k_buf.push((dist, j));
}
if let Some(k) = candidate_limit
&& top_k_buf.len() > k
{
debug_assert!(
k > 0,
"candidate_limit Some(0) silently sentinels every cell — \
use None to disable pruning instead"
);
top_k_buf.select_nth_unstable_by(k - 1, |&(da, ja), &(db, jb)| {
da.partial_cmp(&db)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| pending_stops[ja].0.cmp(&pending_stops[jb].0))
});
top_k_buf.truncate(k);
}
for &(_, j) in &top_k_buf {
let (stop_eid, _) = pending_stops[j];
let ctx = RankContext {
car: car_eid,
stop: stop_eid,
group,
manifest,
world,
};
let scaled = strategy.rank(&ctx).map_or(ASSIGNMENT_SENTINEL, scale_cost);
matrix_ref[(i, j)] = scaled;
}
}
}
scratch.top_k_buf = top_k_buf;
let matrix = &*matrix_ref;
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, _) = scratch.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 }
}