use rand::rngs::StdRng;
use rand::SeedableRng;
use rustsim::prelude::*;
#[derive(Debug, Clone)]
struct Pedestrian {
id: AgentId,
position: f64, speed: f64,
spawn_time: u64,
}
impl Agent for Pedestrian {
fn id(&self) -> AgentId {
self.id
}
}
#[derive(Debug, Clone)]
struct CorridorProps {
path_length: f64, free_speed: f64, flow_capacity_per_sec: f64, min_spacing: f64, spawn_batch: usize, spawn_interval: u64, entrance_queue: Vec<AgentId>,
admission_accum: f64,
sorted_positions: Vec<f64>, agents_spawned: u64,
agents_finished: u64,
total_travel_time: u64,
}
impl CorridorProps {
fn new() -> Self {
Self {
path_length: 100.0,
free_speed: 1.3,
flow_capacity_per_sec: 50.0 / 60.0,
min_spacing: 0.8, spawn_batch: 1000,
spawn_interval: 120,
entrance_queue: Vec::new(),
admission_accum: 0.0,
sorted_positions: Vec::new(),
agents_spawned: 0,
agents_finished: 0,
total_travel_time: 0,
}
}
}
type CorridorModel = StandardModel<
rustsim_spaces::nothing::NothingSpace,
Pedestrian,
HashMapStore<Pedestrian>,
CorridorProps,
StdRng,
Fastest,
>;
fn model_step(model: &mut CorridorModel) {
let time = match model.time() {
Time::Discrete(t) => t,
Time::Continuous(t) => t as u64,
};
let interval = model.properties().spawn_interval;
let batch_size = model.properties().spawn_batch;
if interval > 0 && time % interval == 0 {
for _ in 0..batch_size {
let id = model.next_id();
let ped = Pedestrian {
id,
position: -1.0,
speed: 0.0,
spawn_time: time,
};
model.insert_agent(ped).ok();
model.properties_mut().entrance_queue.push(id);
}
model.properties_mut().agents_spawned += batch_size as u64;
}
let flow = model.properties().flow_capacity_per_sec;
model.properties_mut().admission_accum += flow;
let spacing = model.properties().min_spacing;
let mut next_pos = 0.0f64;
while model.properties().admission_accum >= 1.0 && !model.properties().entrance_queue.is_empty()
{
let id = model.properties_mut().entrance_queue.remove(0);
if let Some(mut agent) = model.agent_mut(id) {
agent.position = next_pos;
}
next_pos += spacing;
model.properties_mut().admission_accum -= 1.0;
}
let cap = flow * 2.0;
if model.properties().admission_accum > cap {
model.properties_mut().admission_accum = cap;
}
let path_length = model.properties().path_length;
let current_time = time;
let finished: Vec<(AgentId, u64)> = model
.agents()
.filter(|a| a.position >= path_length)
.map(|a| (a.id(), current_time.saturating_sub(a.spawn_time)))
.collect();
let count = finished.len() as u64;
let travel_sum: u64 = finished.iter().map(|(_, t)| *t).sum();
for (id, _) in &finished {
model.remove_agent(*id);
}
let props = model.properties_mut();
props.agents_finished += count;
props.total_travel_time += travel_sum;
let mut positions: Vec<f64> = model
.agents()
.filter(|a| a.position >= 0.0)
.map(|a| a.position)
.collect();
positions.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
model.properties_mut().sorted_positions = positions;
}
fn agent_step(
agent: &mut Pedestrian,
ctx: &mut StepContext<
'_,
rustsim_spaces::nothing::NothingSpace,
Pedestrian,
CorridorProps,
StdRng,
Fastest,
>,
) {
if agent.position < 0.0 {
agent.speed = 0.0;
return;
}
let props = ctx.properties();
let free_speed = props.free_speed;
let path_length = props.path_length;
let min_gap = props.min_spacing;
let desired_pos = agent.position + free_speed;
let sorted = &props.sorted_positions;
let my_pos = agent.position;
let nearest_ahead = match sorted.binary_search_by(|p| p.partial_cmp(&my_pos).unwrap()) {
Ok(mut idx) => {
while idx < sorted.len() && sorted[idx] <= my_pos {
idx += 1;
}
if idx < sorted.len() {
sorted[idx]
} else {
path_length + min_gap
}
}
Err(idx) => {
if idx < sorted.len() {
sorted[idx]
} else {
path_length + min_gap
}
}
};
let max_pos = (nearest_ahead - min_gap).max(my_pos);
let new_pos = desired_pos.min(max_pos).min(path_length);
agent.speed = new_pos - my_pos;
agent.position = new_pos;
}
#[test]
fn corridor_simulation() {
let store = HashMapStore::new();
let props = CorridorProps::new();
let mut model = CorridorModel::new(
store,
rustsim_spaces::nothing::NothingSpace,
Fastest::new(),
props,
StdRng::seed_from_u64(42),
Some(Box::new(agent_step)),
Some(model_step),
false, );
let total_seconds = 1200u64;
println!();
println!("=== Corridor Flow Simulation ===");
println!("Path length: 100 m (A -> B)");
println!("Spawn: 1000 pax every 2 min");
println!("Flow capacity: 50 pax/min (entrance gate)");
println!("Free walk speed: 1.3 m/s");
println!("Min spacing: 0.8 m");
println!(
"Duration: {} min ({} s)",
total_seconds / 60,
total_seconds
);
println!();
let mut snapshots: Vec<(u64, usize, usize, u64, f64)> = Vec::new();
for _ in 0..total_seconds {
model.step();
let t = match model.time() {
Time::Discrete(t) => t,
Time::Continuous(t) => t as u64,
};
if t % 60 == 0 {
let on_path = model.agents().filter(|a| a.position >= 0.0).count();
let in_queue = model.properties().entrance_queue.len();
let finished = model.properties().agents_finished;
let avg_speed = if on_path > 0 {
let total: f64 = model
.agents()
.filter(|a| a.position >= 0.0)
.map(|a| a.speed)
.sum();
total / on_path as f64
} else {
0.0
};
snapshots.push((t, on_path, in_queue, finished, avg_speed));
}
}
println!(
"{:>6} {:>8} {:>8} {:>10} {:>10}",
"Time", "On Path", "Queued", "Finished", "Avg Speed"
);
println!("{}", "-".repeat(54));
for (t, on_path, in_queue, finished, avg_speed) in &snapshots {
let mins = t / 60;
let secs = t % 60;
println!(
"{:3}:{:02} {:>8} {:>8} {:>10} {:>8.3} m/s",
mins, secs, on_path, in_queue, finished, avg_speed
);
}
let props = model.properties();
let still_on_path = model.agents().filter(|a| a.position >= 0.0).count();
let still_queued = props.entrance_queue.len();
println!();
println!("=== Final Statistics ===");
println!("Total spawned: {}", props.agents_spawned);
println!("Total finished: {}", props.agents_finished);
println!("Still on path: {}", still_on_path);
println!("Still in queue: {}", still_queued);
println!(
"Accounted for: {}",
props.agents_finished as usize + still_on_path + still_queued
);
let avg_travel = if props.agents_finished > 0 {
props.total_travel_time as f64 / props.agents_finished as f64
} else {
0.0
};
println!(
"Avg travel time: {:.1}s ({:.1} min)",
avg_travel,
avg_travel / 60.0
);
let theoretical_min = props.path_length / props.free_speed;
println!("Theoretical min: {:.1}s (at free speed)", theoretical_min);
let max_throughput = (props.flow_capacity_per_sec * total_seconds as f64) as u64;
println!(
"Max throughput: {} pax (at {:.1} pax/min for {} min)",
max_throughput,
props.flow_capacity_per_sec * 60.0,
total_seconds / 60
);
assert!(
props.agents_spawned >= 10000,
"at least 10 batches of 1000 should spawn"
);
assert!(props.agents_finished > 0, "some agents must reach vertex B");
assert!(
avg_travel >= theoretical_min - 2.0,
"avg travel ({:.1}s) should be >= theoretical min ({:.1}s)",
avg_travel,
theoretical_min
);
assert!(
props.agents_finished <= max_throughput + 200,
"throughput ({}) should not exceed capacity ({}+margin)",
props.agents_finished,
max_throughput
);
let total_remaining = still_on_path + still_queued;
assert!(
total_remaining > 0,
"demand far exceeds capacity, agents must remain"
);
let accounted = props.agents_finished as usize + still_on_path + still_queued;
assert_eq!(
accounted, props.agents_spawned as usize,
"all agents must be accounted for"
);
println!("\nAll assertions passed.");
}