use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum ReliabilityError {
InvalidConfig(String),
AssessmentFailed(String),
}
impl fmt::Display for ReliabilityError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidConfig(s) => write!(f, "invalid reliability config: {s}"),
Self::AssessmentFailed(s) => write!(f, "reliability assessment failed: {s}"),
}
}
}
impl std::error::Error for ReliabilityError {}
struct LcgRng {
state: u64,
}
impl LcgRng {
fn new(seed: u64) -> Self {
Self { state: seed }
}
fn next_f64(&mut self) -> f64 {
self.state = self
.state
.wrapping_mul(6_364_136_223_846_793_005u64)
.wrapping_add(1_442_695_040_888_963_407u64);
(self.state >> 11) as f64 / (1u64 << 53) as f64
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComponentType {
TransmissionLine,
Transformer,
Generator,
Bus,
CircuitBreaker,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentReliability {
pub component_type: ComponentType,
pub failure_rate_per_year: f64,
pub repair_time_hours: f64,
pub availability: f64,
}
impl ComponentReliability {
pub fn new(
component_type: ComponentType,
failure_rate_per_year: f64,
repair_time_hours: f64,
) -> Self {
let mttf_h = if failure_rate_per_year > 0.0 {
8760.0 / failure_rate_per_year
} else {
f64::INFINITY
};
let availability = if mttf_h.is_infinite() {
1.0
} else {
mttf_h / (mttf_h + repair_time_hours)
};
Self {
component_type,
failure_rate_per_year,
repair_time_hours,
availability: availability.clamp(0.0, 1.0),
}
}
pub fn perfect(component_type: ComponentType) -> Self {
Self {
component_type,
failure_rate_per_year: 0.0,
repair_time_hours: 0.0,
availability: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoadPointReliability {
pub bus: usize,
pub load_mw: f64,
pub saifi: f64,
pub saidi_hours: f64,
pub caidi_hours: f64,
pub eens_mwh: f64,
pub ens_cost_usd: f64,
pub availability_pct: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemReliability {
pub load_points: Vec<LoadPointReliability>,
pub system_saifi: f64,
pub system_saidi_hours: f64,
pub system_eens_mwh: f64,
pub system_eic_usd: f64,
pub n_states_sampled: usize,
pub convergence_achieved: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReliabilityConfig {
pub n_buses: usize,
pub load_points: Vec<usize>,
pub n_monte_carlo: usize,
pub mission_time_hours: f64,
pub convergence_criterion: f64,
}
impl ReliabilityConfig {
pub fn default_annual(n_buses: usize, load_points: Vec<usize>) -> Self {
Self {
n_buses,
load_points,
n_monte_carlo: 10_000,
mission_time_hours: 8760.0,
convergence_criterion: 1e-3,
}
}
}
pub struct ReliabilityAssessor {
config: ReliabilityConfig,
generators: Vec<(usize, f64, ComponentReliability)>,
branches: Vec<(usize, usize, f64, ComponentReliability)>,
load_mw: Vec<f64>,
}
impl ReliabilityAssessor {
pub fn new(config: ReliabilityConfig) -> Self {
let n = config.n_buses;
Self {
config,
generators: Vec::new(),
branches: Vec::new(),
load_mw: vec![0.0; n],
}
}
pub fn add_generator(&mut self, bus: usize, p_mw: f64, rel: ComponentReliability) {
self.generators.push((bus, p_mw, rel));
}
pub fn add_branch(
&mut self,
from: usize,
to: usize,
rating_mw: f64,
rel: ComponentReliability,
) {
self.branches.push((from, to, rating_mw, rel));
}
pub fn set_load(&mut self, load_mw: Vec<f64>) {
self.load_mw = load_mw;
}
fn is_supplied(&self, load_bus: usize, gen_avail: &[bool], branch_avail: &[bool]) -> bool {
let n = self.config.n_buses;
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for (idx, &(from, to, _, _)) in self.branches.iter().enumerate() {
if branch_avail[idx] && from < n && to < n {
adj[from].push(to);
adj[to].push(from);
}
}
let mut visited = vec![false; n];
let mut queue = std::collections::VecDeque::new();
if load_bus < n {
visited[load_bus] = true;
queue.push_back(load_bus);
}
while let Some(current) = queue.pop_front() {
for &neighbor in &adj[current] {
if !visited[neighbor] {
visited[neighbor] = true;
queue.push_back(neighbor);
}
}
}
for (idx, &(bus, _p_mw, _)) in self.generators.iter().enumerate() {
if gen_avail[idx] && bus < n && visited[bus] {
return true;
}
}
false
}
fn outage_duration_hours(
&self,
load_bus: usize,
gen_avail: &[bool],
branch_avail: &[bool],
) -> f64 {
let n = self.config.n_buses;
let mut min_repair = f64::INFINITY;
for (idx, &(bus, _p_mw, ref rel)) in self.generators.iter().enumerate() {
if !gen_avail[idx] && bus < n && bus == load_bus {
min_repair = min_repair.min(rel.repair_time_hours);
}
}
for (idx, &(from, to, _, ref rel)) in self.branches.iter().enumerate() {
if !branch_avail[idx] && (from == load_bus || to == load_bus) {
if from < n && to < n {
min_repair = min_repair.min(rel.repair_time_hours);
}
}
}
if min_repair.is_infinite() {
0.0
} else {
min_repair
}
}
pub fn assess(&self) -> Result<SystemReliability, ReliabilityError> {
if self.config.n_buses == 0 {
return Err(ReliabilityError::InvalidConfig(
"n_buses must be >= 1".to_string(),
));
}
if self.config.n_monte_carlo == 0 {
return Err(ReliabilityError::InvalidConfig(
"n_monte_carlo must be >= 1".to_string(),
));
}
let n_lp = self.config.load_points.len();
let mut failure_count = vec![0u64; n_lp];
let mut total_outage_hours = vec![0.0_f64; n_lp];
let mut prev_eens = vec![0.0_f64; n_lp];
let mut convergence_achieved = false;
let mut rng = LcgRng::new(42);
let mut n_sampled = 0usize;
for sample in 0..self.config.n_monte_carlo {
let gen_avail: Vec<bool> = self
.generators
.iter()
.map(|(_, _, rel)| rng.next_f64() < rel.availability)
.collect();
let branch_avail: Vec<bool> = self
.branches
.iter()
.map(|(_, _, _, rel)| rng.next_f64() < rel.availability)
.collect();
for (lp_idx, &bus) in self.config.load_points.iter().enumerate() {
if !self.is_supplied(bus, &gen_avail, &branch_avail) {
failure_count[lp_idx] += 1;
let outage_h = self.outage_duration_hours(bus, &gen_avail, &branch_avail);
total_outage_hours[lp_idx] += outage_h;
}
}
n_sampled += 1;
if (sample + 1) % 100 == 0 && sample > 0 {
let mut converged = true;
for lp_idx in 0..n_lp {
let load_mw = if self.config.load_points[lp_idx] < self.load_mw.len() {
self.load_mw[self.config.load_points[lp_idx]]
} else {
1.0
};
let saidi = total_outage_hours[lp_idx] / n_sampled as f64;
let eens = saidi * load_mw;
let rel_change = if prev_eens[lp_idx].abs() > 1e-9 {
(eens - prev_eens[lp_idx]).abs() / prev_eens[lp_idx].abs()
} else {
if eens.abs() > 1e-9 {
1.0
} else {
0.0
}
};
if rel_change > self.config.convergence_criterion {
converged = false;
}
prev_eens[lp_idx] = eens;
}
if converged {
convergence_achieved = true;
break;
}
}
}
let voll = 10.0_f64; let n_s = n_sampled as f64;
let mut lp_results = Vec::with_capacity(n_lp);
for (lp_idx, &bus) in self.config.load_points.iter().enumerate() {
let load_mw = if bus < self.load_mw.len() {
self.load_mw[bus]
} else {
0.0
};
let saifi = failure_count[lp_idx] as f64 / n_s;
let saidi_hours = total_outage_hours[lp_idx] / n_s;
let caidi_hours = if saifi > 1e-12 {
saidi_hours / saifi
} else {
0.0
};
let saidi_annual =
saidi_hours * self.config.mission_time_hours / self.config.mission_time_hours;
let eens_mwh = saidi_annual * load_mw;
let ens_cost_usd = eens_mwh * voll;
let availability_pct =
(1.0 - saidi_annual / self.config.mission_time_hours.max(1.0)) * 100.0;
lp_results.push(LoadPointReliability {
bus,
load_mw,
saifi,
saidi_hours: saidi_annual,
caidi_hours,
eens_mwh,
ens_cost_usd,
availability_pct: availability_pct.clamp(0.0, 100.0),
});
}
let total_load: f64 = lp_results.iter().map(|lp| lp.load_mw).sum();
let n_lp_f = n_lp.max(1) as f64;
let system_saifi = lp_results.iter().map(|lp| lp.saifi).sum::<f64>() / n_lp_f;
let system_saidi_hours = lp_results.iter().map(|lp| lp.saidi_hours).sum::<f64>() / n_lp_f;
let system_eens_mwh = lp_results.iter().map(|lp| lp.eens_mwh).sum();
let system_eic_usd = system_eens_mwh * voll;
let _ = total_load;
Ok(SystemReliability {
load_points: lp_results,
system_saifi,
system_saidi_hours,
system_eens_mwh,
system_eic_usd,
n_states_sampled: n_sampled,
convergence_achieved,
})
}
pub fn assess_analytical(&self) -> Result<SystemReliability, ReliabilityError> {
if self.config.n_buses == 0 {
return Err(ReliabilityError::InvalidConfig(
"n_buses must be >= 1".to_string(),
));
}
let n_lp = self.config.load_points.len();
let voll = 10.0_f64;
let mut lp_saifi = vec![0.0_f64; n_lp];
let mut lp_eens_mwh = vec![0.0_f64; n_lp];
for (g_idx, &(_, _p_mw, ref rel)) in self.generators.iter().enumerate() {
let p_fail = 1.0 - rel.availability;
if p_fail < 1e-15 {
continue;
}
let gen_avail: Vec<bool> = (0..self.generators.len()).map(|i| i != g_idx).collect();
let branch_avail = vec![true; self.branches.len()];
for (lp_idx, &bus) in self.config.load_points.iter().enumerate() {
if !self.is_supplied(bus, &gen_avail, &branch_avail) {
let load_mw = if bus < self.load_mw.len() {
self.load_mw[bus]
} else {
0.0
};
let outage_h = rel.repair_time_hours;
let eens_contribution =
p_fail * outage_h * load_mw / 8760.0 * self.config.mission_time_hours;
lp_eens_mwh[lp_idx] += eens_contribution;
lp_saifi[lp_idx] += p_fail * rel.failure_rate_per_year;
}
}
}
for (b_idx, (_, _, _, rel)) in self.branches.iter().enumerate() {
let p_fail = 1.0 - rel.availability;
if p_fail < 1e-15 {
continue;
}
let gen_avail = vec![true; self.generators.len()];
let branch_avail: Vec<bool> = (0..self.branches.len()).map(|i| i != b_idx).collect();
for (lp_idx, &bus) in self.config.load_points.iter().enumerate() {
if !self.is_supplied(bus, &gen_avail, &branch_avail) {
let load_mw = if bus < self.load_mw.len() {
self.load_mw[bus]
} else {
0.0
};
let outage_h = rel.repair_time_hours;
let eens_contribution =
p_fail * outage_h * load_mw / 8760.0 * self.config.mission_time_hours;
lp_eens_mwh[lp_idx] += eens_contribution;
lp_saifi[lp_idx] += p_fail * rel.failure_rate_per_year;
}
}
}
let mut lp_results = Vec::with_capacity(n_lp);
for (lp_idx, &bus) in self.config.load_points.iter().enumerate() {
let load_mw = if bus < self.load_mw.len() {
self.load_mw[bus]
} else {
0.0
};
let eens_mwh = lp_eens_mwh[lp_idx];
let saifi = lp_saifi[lp_idx];
let saidi_hours = if load_mw > 1e-9 {
eens_mwh / load_mw
} else {
0.0
};
let caidi_hours = if saifi > 1e-12 {
saidi_hours / saifi
} else {
0.0
};
let ens_cost_usd = eens_mwh * voll;
let availability_pct =
(1.0 - saidi_hours / self.config.mission_time_hours.max(1.0)) * 100.0;
lp_results.push(LoadPointReliability {
bus,
load_mw,
saifi,
saidi_hours,
caidi_hours,
eens_mwh,
ens_cost_usd,
availability_pct: availability_pct.clamp(0.0, 100.0),
});
}
let n_lp_f = n_lp.max(1) as f64;
let system_saifi = lp_results.iter().map(|lp| lp.saifi).sum::<f64>() / n_lp_f;
let system_saidi_hours = lp_results.iter().map(|lp| lp.saidi_hours).sum::<f64>() / n_lp_f;
let system_eens_mwh = lp_results.iter().map(|lp| lp.eens_mwh).sum();
let system_eic_usd = system_eens_mwh * voll;
Ok(SystemReliability {
load_points: lp_results,
system_saifi,
system_saidi_hours,
system_eens_mwh,
system_eic_usd,
n_states_sampled: self.generators.len() + self.branches.len(),
convergence_achieved: true,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_two_bus_system(
gen_rel: ComponentReliability,
branch_rel: ComponentReliability,
) -> ReliabilityAssessor {
let config = ReliabilityConfig {
n_buses: 2,
load_points: vec![1],
n_monte_carlo: 2000,
mission_time_hours: 8760.0,
convergence_criterion: 1e-3,
};
let mut ra = ReliabilityAssessor::new(config);
ra.add_generator(0, 100.0, gen_rel);
ra.add_branch(0, 1, 100.0, branch_rel);
ra.set_load(vec![0.0, 50.0]); ra
}
#[test]
fn test_perfectly_reliable_zero_eens() {
let gen = ComponentReliability::perfect(ComponentType::Generator);
let branch = ComponentReliability::perfect(ComponentType::TransmissionLine);
let ra = make_two_bus_system(gen, branch);
let result = ra.assess().expect("assessment must succeed");
assert!(
result.system_eens_mwh < 1e-9,
"perfectly reliable system must have zero EENS, got {:.6}",
result.system_eens_mwh
);
}
#[test]
fn test_unreliable_line_positive_eens() {
let gen = ComponentReliability::perfect(ComponentType::Generator);
let branch = ComponentReliability::new(ComponentType::TransmissionLine, 10.0, 8.0);
let ra = make_two_bus_system(gen, branch);
let result = ra
.assess_analytical()
.expect("analytical assessment must succeed");
assert!(
result.system_eens_mwh > 0.0,
"unreliable line must produce positive EENS (analytical), got {:.6}",
result.system_eens_mwh
);
}
#[test]
fn test_convergence_within_budget() {
let gen = ComponentReliability::perfect(ComponentType::Generator);
let branch = ComponentReliability::new(ComponentType::TransmissionLine, 5.0, 4.0);
let ra = make_two_bus_system(gen, branch);
let result = ra.assess().expect("assessment must succeed");
assert!(
result.n_states_sampled <= 2000,
"samples must not exceed n_monte_carlo=2000, got {}",
result.n_states_sampled
);
}
#[test]
fn test_saidi_eens_relationship() {
let gen = ComponentReliability::perfect(ComponentType::Generator);
let branch = ComponentReliability::new(ComponentType::TransmissionLine, 8.0, 6.0);
let ra = make_two_bus_system(gen, branch);
let result = ra.assess().expect("assessment must succeed");
let lp = &result.load_points[0];
if lp.load_mw > 0.0 {
let expected_eens = lp.saidi_hours * lp.load_mw;
assert!(
(lp.eens_mwh - expected_eens).abs() < 1e-9,
"EENS ({:.6}) should equal SAIDI×load ({:.6})",
lp.eens_mwh,
expected_eens
);
}
}
#[test]
fn test_analytical_nonzero_eens() {
let gen = ComponentReliability::perfect(ComponentType::Generator);
let branch = ComponentReliability::new(ComponentType::TransmissionLine, 5.0, 8.0);
let ra = make_two_bus_system(gen, branch);
let result = ra
.assess_analytical()
.expect("analytical assessment must succeed");
assert!(
result.system_eens_mwh > 0.0,
"analytical EENS must be positive with unreliable branch, got {:.4}",
result.system_eens_mwh
);
}
#[test]
fn test_analytical_vs_monte_carlo_consistency() {
let gen = ComponentReliability::perfect(ComponentType::Generator);
let branch = ComponentReliability::new(ComponentType::TransmissionLine, 1.0, 8.0);
let ra = make_two_bus_system(gen, branch);
let mc = ra.assess().expect("MC assessment");
let anal = ra.assess_analytical().expect("analytical assessment");
if anal.system_eens_mwh > 0.0 && mc.system_eens_mwh > 0.0 {
let ratio = anal.system_eens_mwh / mc.system_eens_mwh;
assert!(
ratio > 0.01 && ratio < 100.0,
"analytical ({:.4}) and MC ({:.4}) EENS should be within 2 orders of magnitude",
anal.system_eens_mwh,
mc.system_eens_mwh
);
}
}
#[test]
fn test_three_bus_radial_reliability() {
let config = ReliabilityConfig {
n_buses: 3,
load_points: vec![1, 2],
n_monte_carlo: 1000,
mission_time_hours: 8760.0,
convergence_criterion: 1e-3,
};
let mut ra = ReliabilityAssessor::new(config);
ra.add_generator(
0,
200.0,
ComponentReliability::perfect(ComponentType::Generator),
);
ra.add_branch(
0,
1,
100.0,
ComponentReliability::new(ComponentType::TransmissionLine, 2.0, 4.0),
);
ra.add_branch(
1,
2,
100.0,
ComponentReliability::new(ComponentType::TransmissionLine, 2.0, 4.0),
);
ra.set_load(vec![0.0, 30.0, 30.0]);
let result = ra.assess().expect("three-bus assessment must succeed");
assert_eq!(result.load_points.len(), 2);
assert!(
result.load_points[1].eens_mwh >= result.load_points[0].eens_mwh * 0.5,
"far bus should have at least comparable EENS to near bus"
);
}
#[test]
fn test_zero_load_zero_eens() {
let gen = ComponentReliability::new(ComponentType::Generator, 10.0, 8.0);
let branch = ComponentReliability::new(ComponentType::TransmissionLine, 5.0, 4.0);
let config = ReliabilityConfig {
n_buses: 2,
load_points: vec![1],
n_monte_carlo: 500,
mission_time_hours: 8760.0,
convergence_criterion: 1e-3,
};
let mut ra = ReliabilityAssessor::new(config);
ra.add_generator(0, 100.0, gen);
ra.add_branch(0, 1, 100.0, branch);
ra.set_load(vec![0.0, 0.0]);
let result = ra.assess().expect("zero-load assessment must succeed");
assert!(
result.system_eens_mwh < 1e-9,
"zero load → zero EENS, got {:.6}",
result.system_eens_mwh
);
}
}