///|
/// Descriptive statistics used by reports and scenario comparisons.
pub struct DistributionStats {
count : Int
minimum : Double
maximum : Double
mean : Double
median : Double
percentile_90 : Double
} derive(Eq, Debug)
///|
/// Static planned use of one exit.
pub struct ExitUsage {
exit_id : String
assigned_groups : Int
assigned_people : Int
share : Double
} derive(Eq, Debug)
///|
/// Resilience observation derived from topology and planned routes.
pub struct ResilienceFinding {
code : String
severity : Severity
subject : String
value : Double
message : String
} derive(Eq, Debug)
///|
/// Pairwise comparison of two simulation results.
pub struct ScenarioDelta {
baseline_name : String
candidate_name : String
evacuation_step_delta : Int
remaining_delta : Int
peak_stalled_delta : Int
peak_risk_delta : Double
score_delta : Double
improved : Bool
} derive(Eq, Debug)
///|
/// Ranked scenario entry.
pub struct ScenarioRanking {
name : String
score : Double
completed : Bool
rank : Int
} derive(Eq, Debug)
///|
fn copy_doubles(values : Array[Double]) -> Array[Double] {
let result : Array[Double] = []
for value in values {
result.push(value)
}
result
}
///|
fn sort_doubles(values : Array[Double]) -> Array[Double] {
let result = copy_doubles(values)
for i = 1; i < result.length(); i = i + 1 {
let value = result[i]
let mut j = i - 1
while j >= 0 && result[j] > value {
result[j + 1] = result[j]
j = j - 1
}
result[j + 1] = value
}
result
}
///|
/// Calculate a linearly interpolated percentile from numeric values.
pub fn percentile(values : Array[Double], probability : Double) -> Double {
if values.length() == 0 {
return 0.0
}
let sorted = sort_doubles(values)
let p = if probability < 0.0 {
0.0
} else if probability > 1.0 {
1.0
} else {
probability
}
let position = p * (sorted.length() - 1).to_double()
let lower = position.to_int()
let upper = if lower + 1 < sorted.length() { lower + 1 } else { lower }
let fraction = position - lower.to_double()
sorted[lower] * (1.0 - fraction) + sorted[upper] * fraction
}
///|
/// Compute descriptive statistics for an array.
pub fn distribution_stats(values : Array[Double]) -> DistributionStats {
if values.length() == 0 {
return {
count: 0,
minimum: 0.0,
maximum: 0.0,
mean: 0.0,
median: 0.0,
percentile_90: 0.0,
}
}
let mut minimum = values[0]
let mut maximum = values[0]
let mut sum = 0.0
for value in values {
if value < minimum {
minimum = value
}
if value > maximum {
maximum = value
}
sum = sum + value
}
{
count: values.length(),
minimum,
maximum,
mean: sum / values.length().to_double(),
median: percentile(values, 0.5),
percentile_90: percentile(values, 0.9),
}
}
///|
/// Convert integer observations to descriptive statistics.
pub fn integer_distribution_stats(values : Array[Int]) -> DistributionStats {
let converted : Array[Double] = []
for value in values {
converted.push(value.to_double())
}
distribution_stats(converted)
}
///|
/// Find the first snapshot meeting an evacuation percentage.
pub fn evacuation_threshold_step(
result : SimulationResult,
fraction : Double,
) -> Int {
if result.total_people <= 0 {
return 0
}
let target = result.total_people.to_double() *
(if fraction < 0.0 { 0.0 } else if fraction > 1.0 { 1.0 } else { fraction })
for snapshot in result.snapshots {
if snapshot.evacuated.to_double() >= target {
return snapshot.step
}
}
-1
}
///|
/// Estimate evacuation progress at an arbitrary step using the last observation.
pub fn evacuation_progress_at(result : SimulationResult, step : Int) -> Double {
if result.total_people <= 0 {
return 1.0
}
let mut evacuated = 0
for snapshot in result.snapshots {
if snapshot.step <= step {
evacuated = snapshot.evacuated
}
}
evacuated.to_double() / result.total_people.to_double()
}
///|
/// Collect snapshot queue totals.
pub fn total_queue_timeline(result : SimulationResult) -> Array[Int] {
let totals : Array[Int] = []
for snapshot in result.snapshots {
let mut total = 0
for queue in snapshot.passage_queues {
total = total + queue
}
totals.push(total)
}
totals
}
///|
/// Collect maximum passage queue at every snapshot.
pub fn worst_queue_timeline(result : SimulationResult) -> Array[Int] {
let values : Array[Int] = []
for snapshot in result.snapshots {
let mut maximum = 0
for queue in snapshot.passage_queues {
if queue > maximum {
maximum = queue
}
}
values.push(maximum)
}
values
}
///|
/// Compute planned exit usage before dynamic events occur.
pub fn analyze_exit_usage(plan : EvacuationPlan) -> Array[ExitUsage] {
let exits = plan.exit_indices()
let people = Array::make(exits.length(), 0)
let groups = Array::make(exits.length(), 0)
let routes = planned_group_routes(
plan,
Array::make(plan.places.length(), 0.0),
Array::make(plan.passages.length(), 0.0),
)
let count = if routes.length() < plan.populations.length() {
routes.length()
} else {
plan.populations.length()
}
for i = 0; i < count; i = i + 1 {
if routes[i].reachable && routes[i].place_indices.length() > 0 {
let destination = routes[i].place_indices[routes[i].place_indices.length() -
1]
for e = 0; e < exits.length(); e = e + 1 {
if exits[e] == destination {
groups[e] = groups[e] + 1
people[e] = people[e] + plan.populations[i].count
}
}
}
}
let result : Array[ExitUsage] = []
let total = plan.population_count()
for i = 0; i < exits.length(); i = i + 1 {
result.push({
exit_id: plan.places[exits[i]].id,
assigned_groups: groups[i],
assigned_people: people[i],
share: if total > 0 {
people[i].to_double() / total.to_double()
} else {
0.0
},
})
}
result
}
///|
/// Shannon entropy of exit use, normalized to 0..1.
pub fn exit_use_balance(usages : Array[ExitUsage]) -> Double {
if usages.length() <= 1 {
return 0.0
}
let mut concentration = 0.0
for usage in usages {
concentration = concentration + usage.share * usage.share
}
let minimum_concentration = 1.0 / usages.length().to_double()
let denominator = 1.0 - minimum_concentration
if denominator <= 0.0 {
0.0
} else {
(1.0 - concentration) / denominator
}
}
///|
/// Perform static resilience checks that complement validation.
pub fn resilience_findings(plan : EvacuationPlan) -> Array[ResilienceFinding] {
let result : Array[ResilienceFinding] = []
let usages = analyze_exit_usage(plan)
let balance = exit_use_balance(usages)
if usages.length() < 2 {
result.push({
code: "EXIT_COUNT_LOW",
severity: Warning,
subject: "plan",
value: usages.length().to_double(),
message: "The plan has fewer than two usable exits.",
})
}
if usages.length() >= 2 && balance < 0.35 {
result.push({
code: "EXIT_USE_IMBALANCED",
severity: Warning,
subject: "plan",
value: balance,
message: "Planned routes concentrate most occupants at one exit.",
})
}
for usage in usages {
if usage.share > 0.7 {
result.push({
code: "EXIT_OVERCONCENTRATION",
severity: Warning,
subject: usage.exit_id,
value: usage.share,
message: "This exit receives more than seventy percent of planned occupants.",
})
}
if usage.assigned_people == 0 {
result.push({
code: "EXIT_UNUSED",
severity: Info,
subject: usage.exit_id,
value: 0.0,
message: "No group selects this exit under baseline costs.",
})
}
}
let critical = critical_places(plan)
for place_id in critical {
result.push({
code: "CRITICAL_PLACE",
severity: Warning,
subject: place_id,
value: 1.0,
message: "Removing this place disconnects at least one occupied origin from all exits.",
})
}
let routes = planned_group_routes(
plan,
Array::make(plan.places.length(), 0.0),
Array::make(plan.passages.length(), 0.0),
)
let pressures = analyze_passage_pressure(plan, routes, 90.0)
for pressure in pressures {
if pressure.critical {
result.push({
code: "PASSAGE_PRESSURE_HIGH",
severity: Warning,
subject: pressure.passage_id,
value: pressure.utilization,
message: "Baseline assigned flow exceeds the configured ninety-second service window.",
})
}
}
result
}
///|
/// Compare a candidate result against a named baseline.
pub fn scenario_delta(
baseline_name : String,
baseline : SimulationResult,
candidate_name : String,
candidate : SimulationResult,
) -> ScenarioDelta {
let delta = result_score(candidate) - result_score(baseline)
{
baseline_name,
candidate_name,
evacuation_step_delta: candidate.steps - baseline.steps,
remaining_delta: candidate.remaining_people - baseline.remaining_people,
peak_stalled_delta: candidate.peak_stalled_people -
baseline.peak_stalled_people,
peak_risk_delta: candidate.peak_risk - baseline.peak_risk,
score_delta: delta,
improved: delta < 0.0,
}
}
///|
/// Rank named scenario results using the stable result score.
pub fn rank_scenarios(
names : Array[String],
results : Array[SimulationResult],
) -> Array[ScenarioRanking] {
let count = if names.length() < results.length() {
names.length()
} else {
results.length()
}
let ranking : Array[ScenarioRanking] = []
for i = 0; i < count; i = i + 1 {
ranking.push({
name: names[i],
score: result_score(results[i]),
completed: results[i].completed,
rank: 0,
})
}
for i = 1; i < ranking.length(); i = i + 1 {
let value = ranking[i]
let mut j = i - 1
while j >= 0 && ranking[j].score > value.score {
ranking[j + 1] = ranking[j]
j = j - 1
}
ranking[j + 1] = value
}
let final_ranking : Array[ScenarioRanking] = []
for i = 0; i < ranking.length(); i = i + 1 {
let entry = ranking[i]
final_ranking.push({
name: entry.name,
score: entry.score,
completed: entry.completed,
rank: i + 1,
})
}
final_ranking
}
///|
/// Create a concise Markdown resilience section.
pub fn resilience_markdown(plan : EvacuationPlan) -> String {
let usages = analyze_exit_usage(plan)
let findings = resilience_findings(plan)
let mut text = "# MoonEvac resilience analysis\n\n"
text = text + "- Plan: **\{plan.name}**\n"
text = text + "- Exit balance score: \{exit_use_balance(usages)}\n"
text = text + "- Findings: \{findings.length()}\n\n"
text = text + "## Planned exit use\n\n"
text = text + "| Exit | Groups | People | Share |\n|---|---:|---:|---:|\n"
for usage in usages {
text = text + "| \{usage.exit_id} | \{usage.assigned_groups} | "
text = text + "\{usage.assigned_people} | \{usage.share} |\n"
}
text = text + "\n## Findings\n\n"
if findings.length() == 0 {
text = text + "No static resilience findings.\n"
} else {
for finding in findings {
text = text + "- **\{finding.code}** (`\{finding.subject}`): "
text = text + "\{finding.message} Value=\{finding.value}.\n"
}
}
text
}