use crate::network::topology::PowerNetwork;
use crate::optimize::restoration::black_start::{EnergizationPath, LoadBlock, RestorationPlan};
use std::collections::{HashSet, VecDeque};
pub struct RestorationSequencer {
pub max_parallel_paths: usize,
}
impl RestorationSequencer {
pub fn new(max_parallel_paths: usize) -> Self {
Self {
max_parallel_paths: max_parallel_paths.max(1),
}
}
pub fn compute_parallel_paths(
&self,
network: &PowerNetwork,
black_start_buses: &[usize],
) -> Vec<Vec<EnergizationPath>> {
let mut groups: Vec<Vec<EnergizationPath>> = Vec::new();
for &bs_bus in black_start_buses {
let subtree_paths = self.bfs_subtree(network, bs_bus);
if !subtree_paths.is_empty() {
groups.push(subtree_paths);
}
}
groups.truncate(self.max_parallel_paths);
groups
}
pub fn order_load_blocks(
&self,
blocks: &[LoadBlock],
available_mw: f64,
reserve_mw: f64,
) -> Vec<usize> {
let headroom = (available_mw - reserve_mw).max(0.0);
let mut remaining_headroom = headroom;
let mut ordered: Vec<usize> = Vec::new();
let mut priorities: Vec<usize> = blocks.iter().map(|b| b.priority).collect();
priorities.sort_unstable();
priorities.dedup();
for prio in priorities {
let mut tier: Vec<&LoadBlock> = blocks.iter().filter(|b| b.priority == prio).collect();
tier.sort_by(|a, b| {
let a_nd = !a.can_defer as u8;
let b_nd = !b.can_defer as u8;
b_nd.cmp(&a_nd)
.then_with(|| {
a.base_demand_mw
.partial_cmp(&b.base_demand_mw)
.unwrap_or(std::cmp::Ordering::Equal)
})
.then(a.block_id.cmp(&b.block_id))
});
for block in tier {
let clp = block.base_demand_mw * block.cold_load_pickup_factor;
if clp <= remaining_headroom || !block.can_defer {
ordered.push(block.block_id);
remaining_headroom = (remaining_headroom - clp).max(0.0);
}
}
}
ordered
}
fn bfs_subtree(&self, network: &PowerNetwork, root_bus: usize) -> Vec<EnergizationPath> {
let mut paths: Vec<EnergizationPath> = Vec::new();
let mut visited: HashSet<usize> = HashSet::new();
let mut queue: VecDeque<(usize, Vec<usize>, f64)> = VecDeque::new();
visited.insert(root_bus);
queue.push_back((root_bus, Vec::new(), 0.0));
while let Some((current, branch_path, dist)) = queue.pop_front() {
for (bi, branch) in network.branches.iter().enumerate() {
if !branch.status {
continue;
}
let neighbor = if branch.from_bus == current {
branch.to_bus
} else if branch.to_bus == current {
branch.from_bus
} else {
continue;
};
if visited.contains(&neighbor) {
continue;
}
visited.insert(neighbor);
let z_pu = (branch.r * branch.r + branch.x * branch.x).sqrt();
let length_km = (z_pu / 0.3).max(0.5);
let new_dist = dist + length_km;
let mut new_path = branch_path.clone();
new_path.push(bi);
let charging: f64 = new_path
.iter()
.map(|&idx| network.branches[idx].b * 0.5 * 100.0)
.sum();
paths.push(EnergizationPath {
from_bus: root_bus,
to_bus: neighbor,
branch_sequence: new_path.clone(),
total_length_km: new_dist,
charging_current_mvar: charging,
can_energize_at_t: 0.0,
});
queue.push_back((neighbor, new_path, new_dist));
}
}
paths
}
}
pub struct RestorationMetrics {
pub total_customers: usize,
pub outage_start_min: f64,
}
impl RestorationMetrics {
pub fn new(total_customers: usize, outage_start_min: f64) -> Self {
Self {
total_customers,
outage_start_min,
}
}
pub fn compute_saidi(&self, plan: &RestorationPlan, customers_per_block: &[usize]) -> f64 {
use crate::optimize::restoration::black_start::RestorationAction;
if self.total_customers == 0 {
return 0.0;
}
let mut sum = 0.0_f64;
for step in &plan.steps {
if let RestorationAction::PickupLoadBlock { block_id, .. } = &step.action {
let customers = customers_per_block.get(*block_id).copied().unwrap_or(0);
let duration_min = step.time_min - self.outage_start_min;
sum += (customers as f64) * duration_min.max(0.0);
}
}
sum / (self.total_customers as f64)
}
pub fn compute_saifi(&self, plan: &RestorationPlan, customers_per_block: &[usize]) -> f64 {
use crate::optimize::restoration::black_start::RestorationAction;
if self.total_customers == 0 {
return 0.0;
}
let mut interrupted = 0usize;
for step in &plan.steps {
if let RestorationAction::PickupLoadBlock { block_id, .. } = &step.action {
interrupted += customers_per_block.get(*block_id).copied().unwrap_or(0);
}
}
interrupted as f64 / self.total_customers as f64
}
pub fn compute_ens(&self, plan: &RestorationPlan, blocks: &[LoadBlock]) -> f64 {
use crate::optimize::restoration::black_start::RestorationAction;
let mut ens = 0.0_f64;
for step in &plan.steps {
if let RestorationAction::PickupLoadBlock { block_id, .. } = &step.action {
if let Some(block) = blocks.iter().find(|b| b.block_id == *block_id) {
let duration_h = (step.time_min - self.outage_start_min).max(0.0) / 60.0;
ens += block.base_demand_mw * duration_h;
}
}
}
for block in blocks {
let restored = plan.steps.iter().any(|s| {
matches!(&s.action, RestorationAction::PickupLoadBlock { block_id, .. }
if *block_id == block.block_id)
});
if !restored {
let duration_h = (plan.total_time_min - self.outage_start_min).max(0.0) / 60.0;
ens += block.base_demand_mw * duration_h;
}
}
ens
}
}