///|
pub(all) struct OutageSimulationResult {
failed_node_id : String
power_blackout_nodes : Array[String]
plc_timeout_nodes : Array[String]
safety_tripped_nodes : Array[String]
starved_workstations : Array[String]
blocked_workstations : Array[String]
all_impacted_nodes : Array[String]
production_capacity_loss_percent : Double
estimated_hourly_loss_cny : Double
affected_workstations_count : Int
} derive(Debug, Eq)
///|
pub fn simulate_device_outage(
graph : @graph.FactoryGraph,
failed_node_id : String,
hourly_downtime_cost? : Double = 50000.0,
) -> OutageSimulationResult {
let power_blackout : Array[String] = []
let plc_timeout : Array[String] = []
let safety_tripped : Array[String] = []
let starved : Array[String] = []
let blocked : Array[String] = []
let all_impacted : Map[String, Bool] = Map([])
all_impacted[failed_node_id] = true
let failed_node = graph.get_node(failed_node_id)
let failed_nt = match failed_node {
Some(n) => n.node_type()
None => @types.NodeType::Machine
}
// 1. Power blackout cascade: If PowerSource fails, all reachable via EnergyFlow lose power
if failed_nt == @types.NodeType::PowerSource {
let q = [failed_node_id]
let visited_pwr : Map[String, Bool] = Map([])
visited_pwr[failed_node_id] = true
while q.length() > 0 {
let u = q.remove(0)
let out_e = graph.get_layer_out_edges(@types.FlowType::EnergyFlow, u)
for e in out_e {
let v = e.target_id()
if !visited_pwr.contains(v) {
visited_pwr[v] = true
power_blackout.push(v)
all_impacted[v] = true
q.push(v)
}
}
}
}
// 2. PLC Controller timeout: If PLC fails, all controlled nodes trip
if failed_nt == @types.NodeType::PLCController {
let q = [failed_node_id]
let visited_ctl : Map[String, Bool] = Map([])
visited_ctl[failed_node_id] = true
while q.length() > 0 {
let u = q.remove(0)
let out_e = graph.get_layer_out_edges(@types.FlowType::ControlFlow, u)
for e in out_e {
let v = e.target_id()
if !visited_ctl.contains(v) {
visited_ctl[v] = true
plc_timeout.push(v)
all_impacted[v] = true
q.push(v)
}
}
}
}
// 3. Safety Interlock zone trip: All nodes in the same safety zone trip
match failed_node {
Some(n) => {
let sz = n.safety_zone_id()
if sz.length() > 0 && sz != "SZ-0" {
let sz_nodes = graph.get_nodes_in_safety_zone(sz)
for target_node in sz_nodes {
let tid = target_node.id()
if tid != failed_node_id && !all_impacted.contains(tid) {
safety_tripped.push(tid)
all_impacted[tid] = true
}
}
}
}
None => ()
}
// 4. Downstream Material Starvation: Nodes that only receive material from failed nodes
let q_mat = []
for nid, _ in all_impacted {
q_mat.push(nid)
}
let visited_starve : Map[String, Bool] = Map([])
while q_mat.length() > 0 {
let u = q_mat.remove(0)
let out_mat = graph.get_layer_out_edges(@types.FlowType::MaterialFlow, u)
for e in out_mat {
let v = e.target_id()
if !visited_starve.contains(v) && !all_impacted.contains(v) {
// Check if all in-edges for material come from impacted nodes
let in_mat = graph.get_layer_in_edges(@types.FlowType::MaterialFlow, v)
let mut has_unimpacted_supplier = false
for in_e in in_mat {
let sup = in_e.source_id()
if !all_impacted.contains(sup) {
has_unimpacted_supplier = true
break
}
}
if !has_unimpacted_supplier {
visited_starve[v] = true
starved.push(v)
all_impacted[v] = true
q_mat.push(v)
}
}
}
}
// 5. Upstream Material Blockage: Nodes upstream that have nowhere to send WIP
let q_block = []
for nid, _ in all_impacted {
q_block.push(nid)
}
let visited_block : Map[String, Bool] = Map([])
while q_block.length() > 0 {
let u = q_block.remove(0)
let in_mat = graph.get_layer_in_edges(@types.FlowType::MaterialFlow, u)
for e in in_mat {
let v = e.source_id()
if !visited_block.contains(v) && !all_impacted.contains(v) {
let out_mat = graph.get_layer_out_edges(
@types.FlowType::MaterialFlow,
v,
)
let mut has_unimpacted_consumer = false
for out_e in out_mat {
let cons = out_e.target_id()
if !all_impacted.contains(cons) {
has_unimpacted_consumer = true
break
}
}
if !has_unimpacted_consumer {
visited_block[v] = true
blocked.push(v)
all_impacted[v] = true
q_block.push(v)
}
}
}
}
let total_nodes = graph.node_count()
let impacted_count = all_impacted.length()
let loss_pct = if total_nodes > 0 {
impacted_count.to_double() / total_nodes.to_double() * 100.0
} else {
0.0
}
let mut ws_count = 0
let all_impacted_arr = []
for nid, _ in all_impacted {
all_impacted_arr.push(nid)
match graph.get_node(nid) {
Some(n) => {
let nt = n.node_type()
if nt == @types.NodeType::Workstation ||
nt == @types.NodeType::Machine ||
nt == @types.NodeType::RobotArm {
ws_count = ws_count + 1
}
}
None => ()
}
}
let financial_loss = hourly_downtime_cost * (loss_pct / 100.0)
{
failed_node_id,
power_blackout_nodes: power_blackout,
plc_timeout_nodes: plc_timeout,
safety_tripped_nodes: safety_tripped,
starved_workstations: starved,
blocked_workstations: blocked,
all_impacted_nodes: all_impacted_arr,
production_capacity_loss_percent: loss_pct,
estimated_hourly_loss_cny: financial_loss,
affected_workstations_count: ws_count,
}
}