///|
/// Validate a complete plan and return every deterministic diagnostic.
pub fn validate_plan(plan : EvacuationPlan) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
validate_plan_header(plan, diagnostics)
validate_places(plan, diagnostics)
validate_passages(plan, diagnostics)
validate_populations(plan, diagnostics)
validate_connectivity(plan, diagnostics)
validate_exit_redundancy(plan, diagnostics)
diagnostics
}
///|
fn validate_plan_header(
plan : EvacuationPlan,
diagnostics : Array[Diagnostic],
) -> Unit {
if plan.name.length() == 0 {
diagnostics.push(
diagnostic(
"PLAN_NAME_EMPTY",
Error,
"plan",
"The evacuation plan has no name.",
"Give the plan a short human-readable name.",
),
)
}
if plan.timestep_seconds <= 0.0 {
diagnostics.push(
diagnostic(
"TIMESTEP_INVALID",
Error,
"plan",
"The simulation timestep must be positive.",
"Use a timestep between 0.1 and 5 seconds.",
),
)
}
if plan.places.length() == 0 {
diagnostics.push(
diagnostic(
"PLAN_EMPTY",
Error,
"plan",
"The plan contains no places.",
"Add at least one occupied room and one exit.",
),
)
}
if plan.place_kind_count(Exit) == 0 {
diagnostics.push(
diagnostic(
"NO_EXIT",
Error,
"plan",
"The plan contains no exit.",
"Mark at least one place as an exit.",
),
)
}
}
///|
fn validate_places(
plan : EvacuationPlan,
diagnostics : Array[Diagnostic],
) -> Unit {
for i = 0; i < plan.places.length(); i = i + 1 {
let place = plan.places[i]
if place.id.length() == 0 {
diagnostics.push(
diagnostic(
"PLACE_ID_EMPTY",
Error,
"place[\{i}]",
"A place has an empty identifier.",
"Use a stable identifier such as room-a101.",
),
)
}
if place.label.length() == 0 {
diagnostics.push(
diagnostic(
"PLACE_LABEL_EMPTY",
Warning,
place.id,
"A place has no display label.",
"Add a label that responders can recognize.",
),
)
}
if place.capacity <= 0 {
diagnostics.push(
diagnostic(
"PLACE_CAPACITY_INVALID",
Error,
place.id,
"The holding capacity must be positive.",
"Set a conservative positive capacity.",
),
)
}
if place.initial_risk < 0.0 || place.initial_risk > 1.0 {
diagnostics.push(
diagnostic(
"PLACE_RISK_RANGE",
Error,
place.id,
"Initial risk is outside the range 0 to 1.",
"Clamp risk values to the inclusive range 0 to 1.",
),
)
}
for j = i + 1; j < plan.places.length(); j = j + 1 {
if plan.places[j].id == place.id {
diagnostics.push(
diagnostic(
"PLACE_ID_DUPLICATE",
Error,
place.id,
"Two places use the same identifier.",
"Assign a unique identifier to every place.",
),
)
}
}
}
}
///|
fn validate_passages(
plan : EvacuationPlan,
diagnostics : Array[Diagnostic],
) -> Unit {
for i = 0; i < plan.passages.length(); i = i + 1 {
let passage = plan.passages[i]
if passage.id.length() == 0 {
diagnostics.push(
diagnostic(
"PASSAGE_ID_EMPTY",
Error,
"passage[\{i}]",
"A passage has an empty identifier.",
"Use a stable identifier such as door-a101-east.",
),
)
}
if plan.place_index(passage.from_id) < 0 {
diagnostics.push(
diagnostic(
"PASSAGE_FROM_UNKNOWN",
Error,
passage.id,
"The passage starts at an unknown place.",
"Correct from_id or add the missing place.",
),
)
}
if plan.place_index(passage.to_id) < 0 {
diagnostics.push(
diagnostic(
"PASSAGE_TO_UNKNOWN",
Error,
passage.id,
"The passage ends at an unknown place.",
"Correct to_id or add the missing place.",
),
)
}
if passage.from_id == passage.to_id {
diagnostics.push(
diagnostic(
"PASSAGE_SELF_LOOP",
Warning,
passage.id,
"The passage connects a place to itself.",
"Remove the self-loop unless it represents a deliberate delay.",
),
)
}
if passage.length_m <= 0.0 {
diagnostics.push(
diagnostic(
"PASSAGE_LENGTH_INVALID",
Error,
passage.id,
"Passage length must be positive.",
"Measure or estimate a positive travel length.",
),
)
}
if passage.width_m <= 0.0 {
diagnostics.push(
diagnostic(
"PASSAGE_WIDTH_INVALID",
Error,
passage.id,
"Passage width must be positive.",
"Measure the narrowest usable width.",
),
)
}
if passage.flow_per_second <= 0.0 {
diagnostics.push(
diagnostic(
"PASSAGE_FLOW_INVALID",
Error,
passage.id,
"Passage flow rate must be positive.",
"Use a conservative people-per-second rate.",
),
)
}
if passage.kind == Stairway && passage.accessible {
diagnostics.push(
diagnostic(
"STAIR_MARKED_ACCESSIBLE",
Warning,
passage.id,
"A stairway is marked as accessible.",
"Confirm an evacuation chair or accessible alternative exists.",
),
)
}
for j = i + 1; j < plan.passages.length(); j = j + 1 {
if plan.passages[j].id == passage.id {
diagnostics.push(
diagnostic(
"PASSAGE_ID_DUPLICATE",
Error,
passage.id,
"Two passages use the same identifier.",
"Assign a unique identifier to every passage.",
),
)
}
}
}
}
///|
fn validate_populations(
plan : EvacuationPlan,
diagnostics : Array[Diagnostic],
) -> Unit {
for i = 0; i < plan.populations.length(); i = i + 1 {
let group = plan.populations[i]
if group.id.length() == 0 {
diagnostics.push(
diagnostic(
"GROUP_ID_EMPTY",
Error,
"population[\{i}]",
"A population group has an empty identifier.",
"Use a stable group identifier.",
),
)
}
if plan.place_index(group.place_id) < 0 {
diagnostics.push(
diagnostic(
"GROUP_PLACE_UNKNOWN",
Error,
group.id,
"A population group starts at an unknown place.",
"Correct place_id or add the missing place.",
),
)
}
if group.count <= 0 {
diagnostics.push(
diagnostic(
"GROUP_COUNT_INVALID",
Error,
group.id,
"Population count must be positive.",
"Remove empty groups or set a positive count.",
),
)
}
if group.response_delay < 0 {
diagnostics.push(
diagnostic(
"GROUP_DELAY_INVALID",
Error,
group.id,
"Response delay cannot be negative.",
"Use zero for immediate response.",
),
)
}
if group.familiarity < 0.0 || group.familiarity > 1.0 {
diagnostics.push(
diagnostic(
"GROUP_FAMILIARITY_RANGE",
Error,
group.id,
"Familiarity is outside the range 0 to 1.",
"Clamp familiarity to the inclusive range 0 to 1.",
),
)
}
}
}
///|
fn direct_neighbors(plan : EvacuationPlan, place_index : Int) -> Array[Int] {
let result : Array[Int] = []
if place_index < 0 || place_index >= plan.places.length() {
return result
}
let place_id = plan.places[place_index].id
for passage in plan.passages {
if passage.enabled && passage.from_id == place_id {
let target = plan.place_index(passage.to_id)
if target >= 0 {
result.push(target)
}
}
if passage.enabled && passage.bidirectional && passage.to_id == place_id {
let target = plan.place_index(passage.from_id)
if target >= 0 {
result.push(target)
}
}
}
result
}
///|
fn can_reach_any_exit(plan : EvacuationPlan, start : Int) -> Bool {
if start < 0 || start >= plan.places.length() {
return false
}
let visited = Array::make(plan.places.length(), false)
let queue : Array[Int] = [start]
visited[start] = true
let mut cursor = 0
while cursor < queue.length() {
let current = queue[cursor]
cursor = cursor + 1
if plan.places[current].kind == Exit {
return true
}
for next in direct_neighbors(plan, current) {
if !visited[next] {
visited[next] = true
queue.push(next)
}
}
}
false
}
///|
fn validate_connectivity(
plan : EvacuationPlan,
diagnostics : Array[Diagnostic],
) -> Unit {
for i = 0; i < plan.places.length(); i = i + 1 {
let place = plan.places[i]
if place.kind != Exit && !can_reach_any_exit(plan, i) {
diagnostics.push(
diagnostic(
"EXIT_UNREACHABLE",
Error,
place.id,
"This place cannot reach an enabled exit.",
"Add or enable a directed route to an exit.",
),
)
}
}
}
///|
fn count_reachable_exits(plan : EvacuationPlan, start : Int) -> Int {
if start < 0 || start >= plan.places.length() {
return 0
}
let visited = Array::make(plan.places.length(), false)
let queue : Array[Int] = [start]
visited[start] = true
let mut cursor = 0
let mut exits = 0
while cursor < queue.length() {
let current = queue[cursor]
cursor = cursor + 1
if plan.places[current].kind == Exit {
exits = exits + 1
}
for next in direct_neighbors(plan, current) {
if !visited[next] {
visited[next] = true
queue.push(next)
}
}
}
exits
}
///|
fn validate_exit_redundancy(
plan : EvacuationPlan,
diagnostics : Array[Diagnostic],
) -> Unit {
if plan.exit_indices().length() < 2 && plan.population_count() > 20 {
diagnostics.push(
diagnostic(
"SINGLE_EXIT_GLOBAL",
Warning,
"plan",
"A populated plan has fewer than two exits.",
"Evaluate whether an independent alternative exit is required.",
),
)
}
for group in plan.populations {
let start = plan.place_index(group.place_id)
if start >= 0 && count_reachable_exits(plan, start) == 1 && group.count > 10 {
diagnostics.push(
diagnostic(
"SINGLE_EXIT_GROUP",
Warning,
group.id,
"This group can reach only one exit.",
"Add an independent route or document the accepted limitation.",
),
)
}
}
}
///|
/// Count diagnostics of one severity.
pub fn diagnostic_count(
diagnostics : Array[Diagnostic],
severity : Severity,
) -> Int {
let mut count = 0
for item in diagnostics {
if item.severity == severity {
count = count + 1
}
}
count
}
///|
/// Whether validation found no errors.
pub fn validation_ok(diagnostics : Array[Diagnostic]) -> Bool {
diagnostic_count(diagnostics, Error) == 0
}