rustsim-transit 0.0.1

Public-transit primitives for rustsim: stops, routes, schedules, boarding/alighting queues, dwell times, headway control
Documentation
//! Transit queue, dispatch, and dwell policies.

use crate::boarding::{Boarding, BoardingResult, Waiter};
use crate::dwell::{self, DwellParams, DwellTime};
use crate::route::Route;
use crate::stop::{Stop, StopId};
use crate::vehicle::TransitVehicle;

/// Policy for admitting passengers to a stop queue.
pub trait StopQueuePolicy {
    /// Return `true` if `waiter` may join the stop queue.
    fn can_enqueue(&self, stop: &Stop, queue: &Boarding, waiter: &Waiter) -> bool;
}

/// Stop queue policy that enforces the stop's soft capacity as a hard cap.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CapacityStopQueuePolicy;

impl StopQueuePolicy for CapacityStopQueuePolicy {
    fn can_enqueue(&self, stop: &Stop, queue: &Boarding, _waiter: &Waiter) -> bool {
        queue.len() < stop.capacity as usize
    }
}

/// Policy for deciding which queued passengers board a vehicle.
pub trait BoardingPolicy {
    /// Maximum number of passengers this boarding event may board.
    fn max_boardings(&self) -> Option<u32> {
        None
    }

    /// Return `true` if a waiter may board this vehicle.
    fn may_board(&self, waiter: &Waiter, vehicle: &TransitVehicle, served_stops: &[StopId])
        -> bool;
}

/// FIFO boarding policy with an optional per-stop-event boarding cap.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FifoBoardingPolicy {
    /// Optional maximum passengers boarded in one stop event.
    pub max_boardings: Option<u32>,
}

impl FifoBoardingPolicy {
    /// Create a FIFO policy with no event-level cap.
    pub fn unlimited() -> Self {
        Self {
            max_boardings: None,
        }
    }

    /// Create a FIFO policy capped to `max_boardings` per stop event.
    pub fn capped(max_boardings: u32) -> Self {
        Self {
            max_boardings: Some(max_boardings),
        }
    }
}

impl BoardingPolicy for FifoBoardingPolicy {
    fn max_boardings(&self) -> Option<u32> {
        self.max_boardings
    }

    fn may_board(
        &self,
        waiter: &Waiter,
        _vehicle: &TransitVehicle,
        served_stops: &[StopId],
    ) -> bool {
        served_stops.contains(&waiter.destination)
    }
}

/// Dispatch request context for one route at one simulation time.
#[derive(Debug, Clone, Copy)]
pub struct DispatchContext<'a> {
    /// Route being considered for dispatch.
    pub route: &'a Route,
    /// Current simulation time in seconds.
    pub now_s: f64,
    /// Number of currently active vehicles assigned to this route.
    pub active_vehicles: usize,
    /// Optional active-vehicle cap for this route.
    pub max_active_vehicles: Option<usize>,
}

impl<'a> DispatchContext<'a> {
    /// Create a dispatch context for `route` at `now_s`.
    pub fn new(route: &'a Route, now_s: f64) -> Self {
        Self {
            route,
            now_s,
            active_vehicles: 0,
            max_active_vehicles: None,
        }
    }

    /// Attach active-vehicle counts and an optional cap.
    pub fn with_active_vehicles(
        mut self,
        active_vehicles: usize,
        max_active_vehicles: Option<usize>,
    ) -> Self {
        self.active_vehicles = active_vehicles;
        self.max_active_vehicles = max_active_vehicles;
        self
    }
}

/// Decision returned by a dispatch policy.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DispatchDecision {
    /// A vehicle should dispatch now for this departure time.
    Dispatch {
        /// Scheduled departure time being served.
        departure_s: f64,
    },
    /// No dispatch should occur now.
    Wait {
        /// Next known departure time, if any.
        next_departure_s: Option<f64>,
        /// Seconds until the next departure, if known.
        wait_s: Option<f64>,
    },
}

/// Policy that decides whether a route should dispatch a vehicle.
pub trait DispatchPolicy {
    /// Return the dispatch decision for `context`.
    fn decide(&self, context: DispatchContext<'_>) -> DispatchDecision;
}

/// Schedule-following dispatch policy with optional active-vehicle caps.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ScheduledDispatchPolicy;

impl DispatchPolicy for ScheduledDispatchPolicy {
    fn decide(&self, context: DispatchContext<'_>) -> DispatchDecision {
        if context
            .max_active_vehicles
            .is_some_and(|cap| context.active_vehicles >= cap)
        {
            return DispatchDecision::Wait {
                next_departure_s: context.route.schedule.next_departure(context.now_s),
                wait_s: None,
            };
        }

        match context.route.schedule.next_departure(context.now_s) {
            Some(departure_s) if departure_s <= context.now_s => {
                DispatchDecision::Dispatch { departure_s }
            }
            Some(departure_s) => DispatchDecision::Wait {
                next_departure_s: Some(departure_s),
                wait_s: Some((departure_s - context.now_s).max(0.0)),
            },
            None => DispatchDecision::Wait {
                next_departure_s: None,
                wait_s: None,
            },
        }
    }
}

/// Policy for computing stop dwell times from passenger exchange counts.
pub trait DwellPolicy {
    /// Compute dwell time for a stop event.
    fn dwell_time(&self, alighters: u32, boarders: u32) -> DwellTime;
}

/// Linear dwell policy backed by [`DwellParams`].
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct LinearDwellPolicy {
    /// Dwell parameters used by this policy.
    pub params: DwellParams,
}

impl LinearDwellPolicy {
    /// Create a linear dwell policy from explicit parameters.
    pub fn new(params: DwellParams) -> Self {
        Self { params }
    }
}

impl DwellPolicy for LinearDwellPolicy {
    fn dwell_time(&self, alighters: u32, boarders: u32) -> DwellTime {
        dwell::compute(alighters, boarders, &self.params)
    }
}

/// Apply a boarding policy to a queue and vehicle.
pub fn board_with_policy<P: BoardingPolicy>(
    queue: &mut Boarding,
    vehicle: &mut TransitVehicle,
    served_stops: &[StopId],
    passenger_destinations: &mut Vec<StopId>,
    policy: &P,
) -> BoardingResult {
    queue.board_vehicle_with_policy(vehicle, served_stops, passenger_destinations, policy)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Boarding, Route, Schedule, TransitVehicle, Waiter};

    #[test]
    fn capacity_stop_queue_policy_rejects_full_stop() {
        let stop = Stop::at_ground(1, "A", 0.0, 0.0, 1);
        let mut queue = Boarding::new();
        let first = Waiter {
            passenger: 1,
            destination: 2,
            arrived_at: 0.0,
        };
        let second = Waiter {
            passenger: 2,
            destination: 2,
            arrived_at: 1.0,
        };
        assert!(CapacityStopQueuePolicy.can_enqueue(&stop, &queue, &first));
        queue.enqueue(first);
        assert!(!CapacityStopQueuePolicy.can_enqueue(&stop, &queue, &second));
    }

    #[test]
    fn capped_fifo_boarding_limits_one_stop_event() {
        let mut queue = Boarding::new();
        for passenger in 1..=3 {
            queue.enqueue(Waiter {
                passenger,
                destination: 9,
                arrived_at: passenger as f64,
            });
        }
        let mut vehicle = TransitVehicle::idle(10, 5, 10);
        let mut destinations = Vec::new();

        let result = queue.board_vehicle_with_policy(
            &mut vehicle,
            &[9],
            &mut destinations,
            &FifoBoardingPolicy::capped(2),
        );

        assert_eq!(result.boarded, 2);
        assert_eq!(vehicle.passengers, vec![1, 2]);
        assert_eq!(destinations, vec![9, 9]);
        assert_eq!(queue.len(), 1);
    }

    #[test]
    fn scheduled_dispatch_waits_dispatches_and_respects_active_cap() {
        let route = Route::new(
            1,
            "Blue",
            vec![1, 2],
            vec![60.0],
            Schedule::fixed_headway(300.0),
        );
        let policy = ScheduledDispatchPolicy;

        assert_eq!(
            policy.decide(DispatchContext::new(&route, 100.0)),
            DispatchDecision::Wait {
                next_departure_s: Some(300.0),
                wait_s: Some(200.0)
            }
        );
        assert_eq!(
            policy.decide(DispatchContext::new(&route, 300.0)),
            DispatchDecision::Dispatch { departure_s: 300.0 }
        );
        assert_eq!(
            policy.decide(DispatchContext::new(&route, 300.0).with_active_vehicles(2, Some(2))),
            DispatchDecision::Wait {
                next_departure_s: Some(300.0),
                wait_s: None
            }
        );
    }

    #[test]
    fn linear_dwell_policy_matches_dwell_formula() {
        let dwell = LinearDwellPolicy::default().dwell_time(2, 3);
        assert!((dwell.total - (8.0 + 2.0 * 1.8 + 3.0 * 2.6)).abs() < 1e-9);
    }
}