use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LosGrade {
A,
B,
C,
D,
E,
F,
}
impl std::fmt::Display for LosGrade {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::A => write!(f, "A"),
Self::B => write!(f, "B"),
Self::C => write!(f, "C"),
Self::D => write!(f, "D"),
Self::E => write!(f, "E"),
Self::F => write!(f, "F"),
}
}
}
pub trait LosCriteria {
fn classify(&self, value: f64) -> LosGrade;
fn name(&self) -> &'static str;
fn unit(&self) -> &'static str;
}
#[derive(Debug, Clone, Copy)]
pub struct PedestrianWalkway;
impl LosCriteria for PedestrianWalkway {
fn classify(&self, density: f64) -> LosGrade {
if density <= 0.31 {
LosGrade::A
} else if density <= 0.43 {
LosGrade::B
} else if density <= 0.72 {
LosGrade::C
} else if density <= 1.08 {
LosGrade::D
} else if density <= 2.17 {
LosGrade::E
} else {
LosGrade::F
}
}
fn name(&self) -> &'static str {
"Pedestrian Walkway"
}
fn unit(&self) -> &'static str {
"pax/m2"
}
}
#[derive(Debug, Clone, Copy)]
pub struct PedestrianStairway;
impl LosCriteria for PedestrianStairway {
fn classify(&self, density: f64) -> LosGrade {
if density <= 0.54 {
LosGrade::A
} else if density <= 0.72 {
LosGrade::B
} else if density <= 1.08 {
LosGrade::C
} else if density <= 1.54 {
LosGrade::D
} else if density <= 2.70 {
LosGrade::E
} else {
LosGrade::F
}
}
fn name(&self) -> &'static str {
"Pedestrian Stairway"
}
fn unit(&self) -> &'static str {
"pax/m2"
}
}
#[derive(Debug, Clone, Copy)]
pub struct PedestrianQueuing;
impl LosCriteria for PedestrianQueuing {
fn classify(&self, density: f64) -> LosGrade {
if density <= 0.83 {
LosGrade::A
} else if density <= 1.11 {
LosGrade::B
} else if density <= 1.43 {
LosGrade::C
} else if density <= 3.33 {
LosGrade::D
} else if density <= 5.00 {
LosGrade::E
} else {
LosGrade::F
}
}
fn name(&self) -> &'static str {
"Pedestrian Queuing"
}
fn unit(&self) -> &'static str {
"pax/m2"
}
}
#[derive(Debug, Clone, Copy)]
pub struct VehicularFreeway;
impl LosCriteria for VehicularFreeway {
fn classify(&self, density_pc_km_ln: f64) -> LosGrade {
if density_pc_km_ln <= 7.0 {
LosGrade::A
} else if density_pc_km_ln <= 11.0 {
LosGrade::B
} else if density_pc_km_ln <= 16.0 {
LosGrade::C
} else if density_pc_km_ln <= 22.0 {
LosGrade::D
} else if density_pc_km_ln <= 28.0 {
LosGrade::E
} else {
LosGrade::F
}
}
fn name(&self) -> &'static str {
"Vehicular Freeway"
}
fn unit(&self) -> &'static str {
"pc/km/ln"
}
}
#[derive(Debug, Clone, Copy)]
pub struct VehicularUrbanStreet;
impl LosCriteria for VehicularUrbanStreet {
fn classify(&self, delay_s: f64) -> LosGrade {
if delay_s <= 10.0 {
LosGrade::A
} else if delay_s <= 20.0 {
LosGrade::B
} else if delay_s <= 35.0 {
LosGrade::C
} else if delay_s <= 55.0 {
LosGrade::D
} else if delay_s <= 80.0 {
LosGrade::E
} else {
LosGrade::F
}
}
fn name(&self) -> &'static str {
"Vehicular Urban Street"
}
fn unit(&self) -> &'static str {
"s/veh"
}
}
#[derive(Debug, Clone, Copy)]
pub struct VehicularUnsignalized;
impl LosCriteria for VehicularUnsignalized {
fn classify(&self, delay_s: f64) -> LosGrade {
if delay_s <= 10.0 {
LosGrade::A
} else if delay_s <= 15.0 {
LosGrade::B
} else if delay_s <= 25.0 {
LosGrade::C
} else if delay_s <= 35.0 {
LosGrade::D
} else if delay_s <= 50.0 {
LosGrade::E
} else {
LosGrade::F
}
}
fn name(&self) -> &'static str {
"Vehicular Unsignalized Intersection"
}
fn unit(&self) -> &'static str {
"s/veh"
}
}
#[derive(Debug, Clone, Copy)]
pub struct BicycleFacility;
impl LosCriteria for BicycleFacility {
fn classify(&self, events_per_min: f64) -> LosGrade {
if events_per_min <= 10.0 {
LosGrade::A
} else if events_per_min <= 20.0 {
LosGrade::B
} else if events_per_min <= 30.0 {
LosGrade::C
} else if events_per_min <= 40.0 {
LosGrade::D
} else if events_per_min <= 60.0 {
LosGrade::E
} else {
LosGrade::F
}
}
fn name(&self) -> &'static str {
"Bicycle Facility"
}
fn unit(&self) -> &'static str {
"events/min"
}
}
#[derive(Debug, Clone, Copy)]
pub struct TransitCapacity;
impl LosCriteria for TransitCapacity {
fn classify(&self, load_factor: f64) -> LosGrade {
if load_factor <= 0.50 {
LosGrade::A
} else if load_factor <= 0.75 {
LosGrade::B
} else if load_factor <= 1.00 {
LosGrade::C
} else if load_factor <= 1.25 {
LosGrade::D
} else if load_factor <= 1.50 {
LosGrade::E
} else {
LosGrade::F
}
}
fn name(&self) -> &'static str {
"Transit Capacity"
}
fn unit(&self) -> &'static str {
"load factor"
}
}
#[derive(Debug, Clone, Copy, PartialEq, Error)]
pub enum LosCriteriaConfigError {
#[error("thresholds must be in strictly ascending order")]
NonAscendingThresholds,
}
#[derive(Debug, Clone)]
pub struct CustomLosCriteria {
name: &'static str,
unit: &'static str,
thresholds: [f64; 5],
}
impl CustomLosCriteria {
pub fn new(
name: &'static str,
unit: &'static str,
thresholds: [f64; 5],
) -> Result<Self, LosCriteriaConfigError> {
for i in 1..5 {
if thresholds[i] <= thresholds[i - 1] {
return Err(LosCriteriaConfigError::NonAscendingThresholds);
}
}
Ok(Self {
name,
unit,
thresholds,
})
}
}
impl LosCriteria for CustomLosCriteria {
fn classify(&self, value: f64) -> LosGrade {
if value <= self.thresholds[0] {
LosGrade::A
} else if value <= self.thresholds[1] {
LosGrade::B
} else if value <= self.thresholds[2] {
LosGrade::C
} else if value <= self.thresholds[3] {
LosGrade::D
} else if value <= self.thresholds[4] {
LosGrade::E
} else {
LosGrade::F
}
}
fn name(&self) -> &'static str {
self.name
}
fn unit(&self) -> &'static str {
self.unit
}
}
#[derive(Debug, Clone)]
pub struct DensityStatistics {
pub max_density: f64,
pub mean_density: f64,
pub occupied_cells: usize,
pub total_agents: usize,
pub worst_los: LosGrade,
pub los_distribution: [usize; 6],
}
#[derive(Debug, Clone, Copy, PartialEq, Error)]
pub enum DensityGridError {
#[error("extent must be positive")]
InvalidExtent,
#[error("cell_size must be positive")]
InvalidCellSize,
}
#[derive(Debug, Clone)]
pub struct DensityGrid {
extent_x: f64,
extent_y: f64,
cell_size: f64,
cell_area: f64,
grid_w: usize,
grid_h: usize,
counts: Vec<u32>,
total_agents: usize,
}
impl DensityGrid {
pub fn new(extent_x: f64, extent_y: f64, cell_size: f64) -> Result<Self, DensityGridError> {
if extent_x <= 0.0 || extent_y <= 0.0 {
return Err(DensityGridError::InvalidExtent);
}
if cell_size <= 0.0 {
return Err(DensityGridError::InvalidCellSize);
}
let grid_w = (extent_x / cell_size).ceil() as usize;
let grid_h = (extent_y / cell_size).ceil() as usize;
Ok(Self {
extent_x,
extent_y,
cell_size,
cell_area: cell_size * cell_size,
grid_w,
grid_h,
counts: vec![0; grid_w * grid_h],
total_agents: 0,
})
}
pub fn grid_dimensions(&self) -> (usize, usize) {
(self.grid_w, self.grid_h)
}
pub fn cell_size(&self) -> f64 {
self.cell_size
}
pub fn extent(&self) -> (f64, f64) {
(self.extent_x, self.extent_y)
}
pub fn add_position(&mut self, x: f64, y: f64) {
let cx = ((x / self.cell_size).floor() as usize).min(self.grid_w.saturating_sub(1));
let cy = ((y / self.cell_size).floor() as usize).min(self.grid_h.saturating_sub(1));
let idx = cy * self.grid_w + cx;
self.counts[idx] += 1;
self.total_agents += 1;
}
pub fn add_positions(&mut self, positions: impl IntoIterator<Item = (f64, f64)>) {
for (x, y) in positions {
self.add_position(x, y);
}
}
pub fn count_at(&self, x: f64, y: f64) -> u32 {
let cx = ((x / self.cell_size).floor() as usize).min(self.grid_w.saturating_sub(1));
let cy = ((y / self.cell_size).floor() as usize).min(self.grid_h.saturating_sub(1));
self.counts[cy * self.grid_w + cx]
}
pub fn density_at(&self, x: f64, y: f64) -> f64 {
self.count_at(x, y) as f64 / self.cell_area
}
pub fn los_at(&self, x: f64, y: f64, criteria: &dyn LosCriteria) -> LosGrade {
criteria.classify(self.density_at(x, y))
}
pub fn density_at_cell(&self, cx: usize, cy: usize) -> f64 {
if cx >= self.grid_w || cy >= self.grid_h {
return 0.0;
}
self.counts[cy * self.grid_w + cx] as f64 / self.cell_area
}
pub fn count_at_cell(&self, cx: usize, cy: usize) -> u32 {
if cx >= self.grid_w || cy >= self.grid_h {
return 0;
}
self.counts[cy * self.grid_w + cx]
}
pub fn max_density(&self) -> f64 {
self.counts.iter().copied().max().unwrap_or(0) as f64 / self.cell_area
}
pub fn mean_density_occupied(&self) -> f64 {
let occupied: Vec<u32> = self.counts.iter().copied().filter(|&c| c > 0).collect();
if occupied.is_empty() {
return 0.0;
}
let sum: u32 = occupied.iter().sum();
(sum as f64 / occupied.len() as f64) / self.cell_area
}
pub fn mean_density_all(&self) -> f64 {
let total_area = self.grid_w as f64 * self.grid_h as f64 * self.cell_area;
if total_area == 0.0 {
return 0.0;
}
self.total_agents as f64 / total_area
}
pub fn statistics(&self, criteria: &dyn LosCriteria) -> DensityStatistics {
let mut max_count: u32 = 0;
let mut occupied_cells = 0usize;
let mut los_distribution = [0usize; 6];
for &count in &self.counts {
if count > 0 {
occupied_cells += 1;
if count > max_count {
max_count = count;
}
let density = count as f64 / self.cell_area;
let los = criteria.classify(density);
let idx = match los {
LosGrade::A => 0,
LosGrade::B => 1,
LosGrade::C => 2,
LosGrade::D => 3,
LosGrade::E => 4,
LosGrade::F => 5,
};
los_distribution[idx] += 1;
}
}
let max_density = max_count as f64 / self.cell_area;
let worst_los = criteria.classify(max_density);
let mean_density = if occupied_cells > 0 {
(self.total_agents as f64 / occupied_cells as f64) / self.cell_area
} else {
0.0
};
DensityStatistics {
max_density,
mean_density,
occupied_cells,
total_agents: self.total_agents,
worst_los,
los_distribution,
}
}
pub fn counts(&self) -> &[u32] {
&self.counts
}
pub fn total_agents(&self) -> usize {
self.total_agents
}
pub fn clear(&mut self) {
self.counts.fill(0);
self.total_agents = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pedestrian_walkway_thresholds() {
let c = PedestrianWalkway;
assert_eq!(c.classify(0.0), LosGrade::A);
assert_eq!(c.classify(0.31), LosGrade::A);
assert_eq!(c.classify(0.32), LosGrade::B);
assert_eq!(c.classify(0.43), LosGrade::B);
assert_eq!(c.classify(0.44), LosGrade::C);
assert_eq!(c.classify(0.72), LosGrade::C);
assert_eq!(c.classify(0.73), LosGrade::D);
assert_eq!(c.classify(1.08), LosGrade::D);
assert_eq!(c.classify(1.09), LosGrade::E);
assert_eq!(c.classify(2.17), LosGrade::E);
assert_eq!(c.classify(2.18), LosGrade::F);
assert_eq!(c.classify(10.0), LosGrade::F);
}
#[test]
fn pedestrian_stairway_thresholds() {
let c = PedestrianStairway;
assert_eq!(c.classify(0.5), LosGrade::A);
assert_eq!(c.classify(0.55), LosGrade::B);
assert_eq!(c.classify(0.73), LosGrade::C);
assert_eq!(c.classify(1.1), LosGrade::D);
assert_eq!(c.classify(1.55), LosGrade::E);
assert_eq!(c.classify(3.0), LosGrade::F);
}
#[test]
fn pedestrian_queuing_thresholds() {
let c = PedestrianQueuing;
assert_eq!(c.classify(0.5), LosGrade::A);
assert_eq!(c.classify(0.9), LosGrade::B);
assert_eq!(c.classify(1.2), LosGrade::C);
assert_eq!(c.classify(2.0), LosGrade::D);
assert_eq!(c.classify(4.0), LosGrade::E);
assert_eq!(c.classify(6.0), LosGrade::F);
}
#[test]
fn vehicular_freeway_thresholds() {
let c = VehicularFreeway;
assert_eq!(c.classify(5.0), LosGrade::A);
assert_eq!(c.classify(9.0), LosGrade::B);
assert_eq!(c.classify(14.0), LosGrade::C);
assert_eq!(c.classify(20.0), LosGrade::D);
assert_eq!(c.classify(26.0), LosGrade::E);
assert_eq!(c.classify(35.0), LosGrade::F);
}
#[test]
fn vehicular_urban_street_thresholds() {
let c = VehicularUrbanStreet;
assert_eq!(c.classify(5.0), LosGrade::A);
assert_eq!(c.classify(15.0), LosGrade::B);
assert_eq!(c.classify(30.0), LosGrade::C);
assert_eq!(c.classify(45.0), LosGrade::D);
assert_eq!(c.classify(70.0), LosGrade::E);
assert_eq!(c.classify(100.0), LosGrade::F);
}
#[test]
fn vehicular_unsignalized_thresholds() {
let c = VehicularUnsignalized;
assert_eq!(c.classify(5.0), LosGrade::A);
assert_eq!(c.classify(12.0), LosGrade::B);
assert_eq!(c.classify(20.0), LosGrade::C);
assert_eq!(c.classify(30.0), LosGrade::D);
assert_eq!(c.classify(45.0), LosGrade::E);
assert_eq!(c.classify(60.0), LosGrade::F);
}
#[test]
fn bicycle_facility_thresholds() {
let c = BicycleFacility;
assert_eq!(c.classify(5.0), LosGrade::A);
assert_eq!(c.classify(15.0), LosGrade::B);
assert_eq!(c.classify(25.0), LosGrade::C);
assert_eq!(c.classify(35.0), LosGrade::D);
assert_eq!(c.classify(55.0), LosGrade::E);
assert_eq!(c.classify(70.0), LosGrade::F);
}
#[test]
fn transit_capacity_thresholds() {
let c = TransitCapacity;
assert_eq!(c.classify(0.3), LosGrade::A);
assert_eq!(c.classify(0.6), LosGrade::B);
assert_eq!(c.classify(0.9), LosGrade::C);
assert_eq!(c.classify(1.1), LosGrade::D);
assert_eq!(c.classify(1.4), LosGrade::E);
assert_eq!(c.classify(2.0), LosGrade::F);
}
#[test]
fn custom_criteria() {
let c = CustomLosCriteria::new("Test", "units", [1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
assert_eq!(c.classify(0.5), LosGrade::A);
assert_eq!(c.classify(1.5), LosGrade::B);
assert_eq!(c.classify(2.5), LosGrade::C);
assert_eq!(c.classify(3.5), LosGrade::D);
assert_eq!(c.classify(4.5), LosGrade::E);
assert_eq!(c.classify(5.5), LosGrade::F);
assert_eq!(c.name(), "Test");
assert_eq!(c.unit(), "units");
}
#[test]
fn custom_criteria_bad_order() {
let err = CustomLosCriteria::new("Bad", "x", [1.0, 3.0, 2.0, 4.0, 5.0]).unwrap_err();
assert_eq!(err, LosCriteriaConfigError::NonAscendingThresholds);
}
#[test]
fn basic_density() {
let mut grid = DensityGrid::new(10.0, 10.0, 1.0).unwrap();
grid.add_position(0.5, 0.5);
grid.add_position(0.1, 0.9);
grid.add_position(0.3, 0.3);
grid.add_position(0.9, 0.1);
assert_eq!(grid.count_at(0.5, 0.5), 4);
assert!((grid.density_at(0.5, 0.5) - 4.0).abs() < 1e-9);
assert_eq!(grid.los_at(0.5, 0.5, &PedestrianWalkway), LosGrade::F);
}
#[test]
fn empty_cell_density() {
let grid = DensityGrid::new(10.0, 10.0, 1.0).unwrap();
assert_eq!(grid.count_at(5.0, 5.0), 0);
assert!((grid.density_at(5.0, 5.0)).abs() < 1e-9);
assert_eq!(grid.los_at(5.0, 5.0, &PedestrianWalkway), LosGrade::A);
}
#[test]
fn statistics_with_walkway_criteria() {
let mut grid = DensityGrid::new(10.0, 10.0, 2.0).unwrap();
grid.add_position(1.0, 1.0);
grid.add_position(1.5, 1.5);
grid.add_position(5.0, 5.0);
let stats = grid.statistics(&PedestrianWalkway);
assert_eq!(stats.total_agents, 3);
assert_eq!(stats.occupied_cells, 2);
assert!((stats.max_density - 0.5).abs() < 1e-9);
assert_eq!(stats.worst_los, LosGrade::C);
assert_eq!(stats.los_distribution[0], 1); assert_eq!(stats.los_distribution[2], 1); }
#[test]
fn statistics_with_queuing_criteria() {
let mut grid = DensityGrid::new(10.0, 10.0, 2.0).unwrap();
grid.add_position(1.0, 1.0);
grid.add_position(1.5, 1.5);
grid.add_position(5.0, 5.0);
let stats = grid.statistics(&PedestrianQueuing);
assert_eq!(stats.worst_los, LosGrade::A); assert_eq!(stats.los_distribution[0], 2); }
#[test]
fn different_criteria_same_density() {
assert_eq!(PedestrianWalkway.classify(0.6), LosGrade::C);
assert_eq!(PedestrianStairway.classify(0.6), LosGrade::B);
assert_eq!(PedestrianQueuing.classify(0.6), LosGrade::A);
}
#[test]
fn clear_resets() {
let mut grid = DensityGrid::new(10.0, 10.0, 1.0).unwrap();
grid.add_position(0.5, 0.5);
assert_eq!(grid.total_agents(), 1);
grid.clear();
assert_eq!(grid.total_agents(), 0);
assert_eq!(grid.count_at(0.5, 0.5), 0);
}
#[test]
fn add_positions_batch() {
let mut grid = DensityGrid::new(10.0, 10.0, 1.0).unwrap();
let positions = vec![(0.5, 0.5), (0.1, 0.1), (5.0, 5.0)];
grid.add_positions(positions);
assert_eq!(grid.total_agents(), 3);
assert_eq!(grid.count_at(0.5, 0.5), 2);
assert_eq!(grid.count_at(5.0, 5.0), 1);
}
#[test]
fn clamping_out_of_bounds() {
let mut grid = DensityGrid::new(10.0, 10.0, 1.0).unwrap();
grid.add_position(-5.0, -5.0);
assert_eq!(grid.count_at(0.0, 0.0), 1);
grid.add_position(100.0, 100.0);
assert_eq!(grid.count_at(9.9, 9.9), 1);
}
#[test]
fn los_grade_ordering() {
assert!(LosGrade::A < LosGrade::B);
assert!(LosGrade::B < LosGrade::C);
assert!(LosGrade::E < LosGrade::F);
}
#[test]
fn los_grade_display() {
assert_eq!(format!("{}", LosGrade::A), "A");
assert_eq!(format!("{}", LosGrade::F), "F");
}
#[test]
fn criteria_metadata() {
assert_eq!(PedestrianWalkway.name(), "Pedestrian Walkway");
assert_eq!(PedestrianWalkway.unit(), "pax/m2");
assert_eq!(VehicularUrbanStreet.name(), "Vehicular Urban Street");
assert_eq!(VehicularUrbanStreet.unit(), "s/veh");
assert_eq!(TransitCapacity.name(), "Transit Capacity");
assert_eq!(TransitCapacity.unit(), "load factor");
}
}