///|
/// Settings controlling a complete evacuation run.
pub struct SimulationConfig {
maximum_steps : Int
reroute_interval : Int
snapshot_interval : Int
maximum_route_risk : Double
stop_when_evacuated : Bool
hazard : HazardConfig
} derive(Eq, Debug)
///|
/// A movable cohort. Cohorts split when passage capacity is insufficient.
struct Cohort {
group_index : Int
mut count : Int
mut place_index : Int
mut waiting_steps : Int
mut stalled_steps : Int
mut exposure : Double
mut last_route_step : Int
mut route : Route
} derive(Eq, Debug)
///|
/// Aggregate state captured for reports and regression tests.
pub struct SimulationSnapshot {
step : Int
remaining : Int
evacuated : Int
stalled : Int
maximum_risk : Double
mean_risk : Double
passage_queues : Array[Int]
place_occupancy : Array[Int]
} derive(Eq, Debug)
///|
/// Final group-level metrics.
pub struct GroupOutcome {
group_id : String
initial_count : Int
evacuated_count : Int
remaining_count : Int
first_exit_step : Int
last_exit_step : Int
accumulated_exposure : Double
reroutes : Int
} derive(Eq, Debug)
///|
/// Complete deterministic result.
pub struct SimulationResult {
plan_name : String
completed : Bool
steps : Int
total_people : Int
evacuated_people : Int
remaining_people : Int
peak_stalled_people : Int
peak_risk : Double
mean_evacuation_step : Double
snapshots : Array[SimulationSnapshot]
group_outcomes : Array[GroupOutcome]
passage_peak_queues : Array[Int]
event_log : Array[String]
} derive(Eq, Debug)
///|
/// A state-aware edge used internally by the simulation router.
struct LiveEdge {
passage_index : Int
to_index : Int
cost : Double
} derive(Eq, Debug)
///|
/// Practical deterministic defaults.
pub fn SimulationConfig::default() -> SimulationConfig {
{
maximum_steps: 600,
reroute_interval: 5,
snapshot_interval: 10,
maximum_route_risk: 0.85,
stop_when_evacuated: true,
hazard: HazardConfig::default(),
}
}
///|
/// Short configuration useful for tests.
pub fn SimulationConfig::short_run() -> SimulationConfig {
{
maximum_steps: 120,
reroute_interval: 2,
snapshot_interval: 1,
maximum_route_risk: 0.9,
stop_when_evacuated: true,
hazard: HazardConfig::slow(),
}
}
///|
fn int_min(a : Int, b : Int) -> Int {
if a < b {
a
} else {
b
}
}
///|
fn int_max(a : Int, b : Int) -> Int {
if a > b {
a
} else {
b
}
}
///|
fn live_edge_cost(
plan : EvacuationPlan,
state : ScenarioState,
passage_index : Int,
from_index : Int,
to_index : Int,
queues : Array[Int],
mobility : Mobility,
) -> Double {
let passage = plan.passages[passage_index]
let risk = if to_index < state.risks.length() {
state.risks[to_index]
} else {
0.0
}
let queue = if passage_index < queues.length() {
queues[passage_index]
} else {
0
}
let queue_cost = queue.to_double() /
(if passage.flow_per_second > 0.1 { passage.flow_per_second } else { 0.1 })
let accessibility_cost = if mobility.requires_accessible() &&
!passage.accessible {
1000000000.0
} else {
0.0
}
let stair_cost = if passage.kind == Stairway {
8.0 / mobility.speed_factor()
} else {
0.0
}
let direction_cost = if plan.places[from_index].floor !=
plan.places[to_index].floor {
2.0
} else {
0.0
}
passage.length_m / mobility.speed_factor() +
risk * 100.0 +
queue_cost * 5.0 +
accessibility_cost +
stair_cost +
direction_cost
}
///|
fn live_outgoing_edges(
plan : EvacuationPlan,
state : ScenarioState,
from_index : Int,
queues : Array[Int],
mobility : Mobility,
maximum_risk : Double,
) -> Array[LiveEdge] {
let result : Array[LiveEdge] = []
if from_index < 0 || from_index >= plan.places.length() {
return result
}
let from_id = plan.places[from_index].id
for i = 0; i < plan.passages.length(); i = i + 1 {
let passage = plan.passages[i]
let active = passage.enabled &&
(i >= state.passage_enabled.length() || state.passage_enabled[i])
if !active {
continue
}
if mobility.requires_accessible() && !passage.accessible {
continue
}
if passage.from_id == from_id {
let target = plan.place_index(passage.to_id)
if target >= 0 &&
(target >= state.risks.length() || state.risks[target] <= maximum_risk) {
result.push({
passage_index: i,
to_index: target,
cost: live_edge_cost(
plan, state, i, from_index, target, queues, mobility,
),
})
}
}
if passage.bidirectional && passage.to_id == from_id {
let target = plan.place_index(passage.from_id)
if target >= 0 &&
(target >= state.risks.length() || state.risks[target] <= maximum_risk) {
result.push({
passage_index: i,
to_index: target,
cost: live_edge_cost(
plan, state, i, from_index, target, queues, mobility,
),
})
}
}
}
result
}
///|
fn live_minimum(distance : Array[Double], visited : Array[Bool]) -> Int {
let mut selected = -1
let mut value = 1000000000000.0
for i = 0; i < distance.length(); i = i + 1 {
if !visited[i] && distance[i] < value {
selected = i
value = distance[i]
}
}
selected
}
///|
fn rebuild_live_route(
plan : EvacuationPlan,
start : Int,
target : Int,
distance : Array[Double],
previous_place : Array[Int],
previous_passage : Array[Int],
risks : Array[Double],
) -> Route {
if target < 0 ||
target >= distance.length() ||
distance[target] >= 999999999999.0 {
return Route::no_route()
}
let reverse_places : Array[Int] = []
let reverse_passages : Array[Int] = []
let mut cursor = target
let mut hops = 0
while cursor >= 0 && hops <= plan.places.length() {
reverse_places.push(cursor)
if cursor == start {
break
}
let passage = previous_passage[cursor]
if passage < 0 {
return Route::no_route()
}
reverse_passages.push(passage)
cursor = previous_place[cursor]
hops = hops + 1
}
if reverse_places.length() == 0 ||
reverse_places[reverse_places.length() - 1] != start {
return Route::no_route()
}
let places : Array[Int] = []
let passages : Array[Int] = []
let mut route_distance = 0.0
let mut route_risk = 0.0
for i = reverse_places.length() - 1; i >= 0; i = i - 1 {
let index = reverse_places[i]
places.push(index)
if index < risks.length() && risks[index] > route_risk {
route_risk = risks[index]
}
}
for i = reverse_passages.length() - 1; i >= 0; i = i - 1 {
let index = reverse_passages[i]
passages.push(index)
route_distance = route_distance + plan.passages[index].length_m
}
{
place_indices: places,
passage_indices: passages,
total_cost: distance[target],
distance_m: route_distance,
maximum_risk: route_risk,
reachable: true,
}
}
///|
fn live_route_to_exit(
plan : EvacuationPlan,
state : ScenarioState,
start : Int,
queues : Array[Int],
mobility : Mobility,
maximum_risk : Double,
) -> Route {
let n = plan.places.length()
if start < 0 || start >= n {
return Route::no_route()
}
let distance = Array::make(n, 1000000000000.0)
let visited = Array::make(n, false)
let previous_place = Array::make(n, -1)
let previous_passage = Array::make(n, -1)
distance[start] = 0.0
let mut target = -1
for iteration = 0; iteration < n; iteration = iteration + 1 {
let current = live_minimum(distance, visited)
if current < 0 {
break
}
if plan.places[current].kind == Exit {
target = current
break
}
visited[current] = true
for
edge in live_outgoing_edges(
plan, state, current, queues, mobility, maximum_risk,
) {
let candidate = distance[current] + edge.cost
if candidate < distance[edge.to_index] {
distance[edge.to_index] = candidate
previous_place[edge.to_index] = current
previous_passage[edge.to_index] = edge.passage_index
}
}
}
rebuild_live_route(
plan,
start,
target,
distance,
previous_place,
previous_passage,
state.risks,
)
}
///|
fn make_cohorts(plan : EvacuationPlan) -> Array[Cohort] {
let result : Array[Cohort] = []
for i = 0; i < plan.populations.length(); i = i + 1 {
let group = plan.populations[i]
let start = plan.place_index(group.place_id)
result.push({
group_index: i,
count: group.count,
place_index: start,
waiting_steps: group.response_delay,
stalled_steps: 0,
exposure: 0.0,
last_route_step: -100000,
route: Route::no_route(),
})
}
result
}
///|
fn cohort_people(cohorts : Array[Cohort]) -> Int {
let mut total = 0
for cohort in cohorts {
if cohort.count > 0 {
total = total + cohort.count
}
}
total
}
///|
fn place_occupancy(
plan : EvacuationPlan,
cohorts : Array[Cohort],
) -> Array[Int] {
let result = Array::make(plan.places.length(), 0)
for cohort in cohorts {
if cohort.count > 0 &&
cohort.place_index >= 0 &&
cohort.place_index < result.length() {
result[cohort.place_index] = result[cohort.place_index] + cohort.count
}
}
result
}
///|
fn desired_passage(
plan : EvacuationPlan,
state : ScenarioState,
cohort : Cohort,
queues : Array[Int],
step : Int,
config : SimulationConfig,
) -> Int {
if cohort.place_index < 0 || cohort.place_index >= plan.places.length() {
return -1
}
if plan.places[cohort.place_index].kind == Exit {
return -1
}
let group = plan.populations[cohort.group_index]
let route_expired = step - cohort.last_route_step >= config.reroute_interval
let route_invalid = !cohort.route.reachable ||
cohort.route.passage_indices.length() == 0
if route_expired || route_invalid {
cohort.route = live_route_to_exit(
plan,
state,
cohort.place_index,
queues,
group.mobility,
config.maximum_route_risk,
)
cohort.last_route_step = step
}
if cohort.route.reachable && cohort.route.passage_indices.length() > 0 {
cohort.route.passage_indices[0]
} else {
-1
}
}
///|
fn route_next_place(
plan : EvacuationPlan,
passage_index : Int,
current : Int,
) -> Int {
if passage_index < 0 ||
passage_index >= plan.passages.length() ||
current < 0 ||
current >= plan.places.length() {
return -1
}
let passage = plan.passages[passage_index]
let current_id = plan.places[current].id
if passage.from_id == current_id {
plan.place_index(passage.to_id)
} else if passage.bidirectional && passage.to_id == current_id {
plan.place_index(passage.from_id)
} else {
-1
}
}
///|
fn passage_step_capacity(
plan : EvacuationPlan,
timestep : Double,
) -> Array[Int] {
let result : Array[Int] = []
for passage in plan.passages {
let raw = (passage.flow_per_second * timestep).to_int()
result.push(int_max(1, raw))
}
result
}
///|
fn build_queues(
plan : EvacuationPlan,
state : ScenarioState,
cohorts : Array[Cohort],
step : Int,
config : SimulationConfig,
) -> Array[Int] {
let queues = Array::make(plan.passages.length(), 0)
for cohort in cohorts {
if cohort.count > 0 && cohort.waiting_steps <= 0 {
let passage = desired_passage(plan, state, cohort, queues, step, config)
if passage >= 0 && passage < queues.length() {
queues[passage] = queues[passage] + cohort.count
}
}
}
queues
}
///|
fn copy_int_array(values : Array[Int]) -> Array[Int] {
let result : Array[Int] = []
for value in values {
result.push(value)
}
result
}
///|
fn record_snapshot(
plan : EvacuationPlan,
state : ScenarioState,
cohorts : Array[Cohort],
queues : Array[Int],
total_people : Int,
) -> SimulationSnapshot {
let remaining = cohort_people(cohorts)
let mut stalled = 0
for cohort in cohorts {
if cohort.stalled_steps > 0 {
stalled = stalled + cohort.count
}
}
let risk = observe_risk(state)
{
step: state.step,
remaining,
evacuated: total_people - remaining,
stalled,
maximum_risk: risk.maximum_risk,
mean_risk: risk.mean_risk,
passage_queues: copy_int_array(queues),
place_occupancy: place_occupancy(plan, cohorts),
}
}
///|
fn advance_cohorts(
plan : EvacuationPlan,
state : ScenarioState,
cohorts : Array[Cohort],
queues : Array[Int],
capacity : Array[Int],
step : Int,
config : SimulationConfig,
group_first_exit : Array[Int],
group_last_exit : Array[Int],
group_evacuated : Array[Int],
group_reroutes : Array[Int],
) -> Unit {
let initial_length = cohorts.length()
for i = 0; i < initial_length; i = i + 1 {
let cohort = cohorts[i]
if cohort.count <= 0 {
continue
}
if cohort.place_index < 0 || cohort.place_index >= plan.places.length() {
cohort.stalled_steps = cohort.stalled_steps + 1
continue
}
let risk = if cohort.place_index < state.risks.length() {
state.risks[cohort.place_index]
} else {
0.0
}
cohort.exposure = cohort.exposure +
risk * config.hazard.spread_rate * cohort.count.to_double()
if plan.places[cohort.place_index].kind == Exit {
let group_index = cohort.group_index
group_evacuated[group_index] = group_evacuated[group_index] + cohort.count
if group_first_exit[group_index] < 0 {
group_first_exit[group_index] = step
}
group_last_exit[group_index] = step
cohort.count = 0
continue
}
if cohort.waiting_steps > 0 {
cohort.waiting_steps = cohort.waiting_steps - 1
continue
}
let previous_route_step = cohort.last_route_step
let passage = desired_passage(plan, state, cohort, queues, step, config)
if cohort.last_route_step != previous_route_step &&
previous_route_step > -100000 {
group_reroutes[cohort.group_index] = group_reroutes[cohort.group_index] +
1
}
if passage < 0 || passage >= capacity.length() {
cohort.stalled_steps = cohort.stalled_steps + 1
continue
}
let next_place = route_next_place(plan, passage, cohort.place_index)
if next_place < 0 {
cohort.stalled_steps = cohort.stalled_steps + 1
cohort.route = Route::no_route()
continue
}
let allowed = int_min(cohort.count, capacity[passage])
if allowed <= 0 {
cohort.stalled_steps = cohort.stalled_steps + 1
continue
}
capacity[passage] = capacity[passage] - allowed
if allowed == cohort.count {
cohort.place_index = next_place
cohort.stalled_steps = 0
cohort.route = Route::no_route()
} else {
cohort.count = cohort.count - allowed
cohort.stalled_steps = cohort.stalled_steps + 1
cohorts.push({
group_index: cohort.group_index,
count: allowed,
place_index: next_place,
waiting_steps: 0,
stalled_steps: 0,
exposure: cohort.exposure *
allowed.to_double() /
(cohort.count + allowed).to_double(),
last_route_step: -100000,
route: Route::no_route(),
})
}
}
}
///|
fn update_peak_queues(current : Array[Int], peaks : Array[Int]) -> Unit {
let count = int_min(current.length(), peaks.length())
for i = 0; i < count; i = i + 1 {
if current[i] > peaks[i] {
peaks[i] = current[i]
}
}
}
///|
fn build_group_outcomes(
plan : EvacuationPlan,
cohorts : Array[Cohort],
first_exit : Array[Int],
last_exit : Array[Int],
evacuated : Array[Int],
reroutes : Array[Int],
) -> Array[GroupOutcome] {
let result : Array[GroupOutcome] = []
let remaining = Array::make(plan.populations.length(), 0)
let exposures = Array::make(plan.populations.length(), 0.0)
for cohort in cohorts {
if cohort.group_index >= 0 && cohort.group_index < remaining.length() {
remaining[cohort.group_index] = remaining[cohort.group_index] +
cohort.count
exposures[cohort.group_index] = exposures[cohort.group_index] +
cohort.exposure
}
}
for i = 0; i < plan.populations.length(); i = i + 1 {
let group = plan.populations[i]
result.push({
group_id: group.id,
initial_count: group.count,
evacuated_count: evacuated[i],
remaining_count: remaining[i],
first_exit_step: first_exit[i],
last_exit_step: last_exit[i],
accumulated_exposure: exposures[i],
reroutes: reroutes[i],
})
}
result
}
///|
fn mean_evacuation_step(outcomes : Array[GroupOutcome]) -> Double {
let mut weighted = 0.0
let mut evacuated = 0
for outcome in outcomes {
if outcome.evacuated_count > 0 && outcome.last_exit_step >= 0 {
weighted = weighted +
outcome.last_exit_step.to_double() * outcome.evacuated_count.to_double()
evacuated = evacuated + outcome.evacuated_count
}
}
if evacuated > 0 {
weighted / evacuated.to_double()
} else {
0.0
}
}
///|
/// Run a complete deterministic evacuation simulation.
pub fn run_simulation(
plan : EvacuationPlan,
config : SimulationConfig,
) -> SimulationResult {
let validation = validate_plan(plan)
if !validation_ok(validation) {
return {
plan_name: plan.name,
completed: false,
steps: 0,
total_people: plan.population_count(),
evacuated_people: 0,
remaining_people: plan.population_count(),
peak_stalled_people: plan.population_count(),
peak_risk: 0.0,
mean_evacuation_step: 0.0,
snapshots: [],
group_outcomes: [],
passage_peak_queues: Array::make(plan.passages.length(), 0),
event_log: ["simulation rejected because plan validation failed"],
}
}
let state = ScenarioState::from_plan(plan)
let cohorts = make_cohorts(plan)
let total_people = plan.population_count()
let snapshots : Array[SimulationSnapshot] = []
let peak_queues = Array::make(plan.passages.length(), 0)
let first_exit = Array::make(plan.populations.length(), -1)
let last_exit = Array::make(plan.populations.length(), -1)
let evacuated = Array::make(plan.populations.length(), 0)
let reroutes = Array::make(plan.populations.length(), 0)
let mut peak_stalled = 0
let mut peak_risk = 0.0
let mut executed_steps = 0
let mut last_queues = Array::make(plan.passages.length(), 0)
for step = 0; step < config.maximum_steps; step = step + 1 {
advance_hazard(plan, state, config.hazard)
update_hazard_blocks(plan, state, config.hazard)
let queues = build_queues(plan, state, cohorts, step, config)
last_queues = queues
update_peak_queues(queues, peak_queues)
let capacity = passage_step_capacity(plan, plan.timestep_seconds)
advance_cohorts(
plan, state, cohorts, queues, capacity, step, config, first_exit, last_exit,
evacuated, reroutes,
)
executed_steps = step + 1
let observation = observe_risk(state)
if observation.maximum_risk > peak_risk {
peak_risk = observation.maximum_risk
}
let snapshot_every = int_max(1, config.snapshot_interval)
if step % snapshot_every == 0 || cohort_people(cohorts) == 0 {
let snapshot = record_snapshot(plan, state, cohorts, queues, total_people)
if snapshot.stalled > peak_stalled {
peak_stalled = snapshot.stalled
}
snapshots.push(snapshot)
}
if config.stop_when_evacuated && cohort_people(cohorts) == 0 {
break
}
}
if snapshots.length() == 0 ||
snapshots[snapshots.length() - 1].step != state.step {
let final_snapshot = record_snapshot(
plan, state, cohorts, last_queues, total_people,
)
if final_snapshot.stalled > peak_stalled {
peak_stalled = final_snapshot.stalled
}
snapshots.push(final_snapshot)
}
let outcomes = build_group_outcomes(
plan, cohorts, first_exit, last_exit, evacuated, reroutes,
)
let remaining = cohort_people(cohorts)
let evacuated_people = total_people - remaining
{
plan_name: plan.name,
completed: remaining == 0,
steps: executed_steps,
total_people,
evacuated_people,
remaining_people: remaining,
peak_stalled_people: peak_stalled,
peak_risk,
mean_evacuation_step: mean_evacuation_step(outcomes),
snapshots,
group_outcomes: outcomes,
passage_peak_queues: peak_queues,
event_log: state.event_log,
}
}
///|
/// Compare two results with a stable lower-is-better score.
pub fn result_score(result : SimulationResult) -> Double {
let incomplete_penalty = result.remaining_people.to_double() * 10000.0
let time_penalty = result.steps.to_double()
let stall_penalty = result.peak_stalled_people.to_double() * 5.0
let risk_penalty = result.peak_risk * 1000.0
incomplete_penalty + time_penalty + stall_penalty + risk_penalty
}
///|
/// Return the name of the better scenario, or "tie".
pub fn compare_results(
left_name : String,
left : SimulationResult,
right_name : String,
right : SimulationResult,
) -> String {
let left_score = result_score(left)
let right_score = result_score(right)
if left_score < right_score {
left_name
} else if right_score < left_score {
right_name
} else {
"tie"
}
}
///|
/// Count total people at risk at or above a threshold in one snapshot.
pub fn snapshot_people_in_risky_places(
snapshot : SimulationSnapshot,
risks : Array[Double],
threshold : Double,
) -> Int {
let count = int_min(snapshot.place_occupancy.length(), risks.length())
let mut total = 0
for i = 0; i < count; i = i + 1 {
if risks[i] >= threshold {
total = total + snapshot.place_occupancy[i]
}
}
total
}