use std::collections::{HashMap, VecDeque};
use scirs2_core::ndarray::Array2;
use scirs2_core::random::{Rng, SeedableRng, StdRng};
use crate::error::{GraphError, Result};
fn extract_edges(adj: &Array2<f64>) -> Vec<(usize, usize, f64)> {
let n = adj.nrows();
let mut edges = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
let w = adj[[i, j]];
if w > 0.0 {
edges.push((i, j, w.clamp(0.0, 1.0)));
}
}
}
edges
}
fn can_reach(n: usize, edges: &[(usize, usize, f64)], active: &[bool], s: usize, t: usize) -> bool {
if s == t {
return true;
}
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for (idx, &(u, v, _)) in edges.iter().enumerate() {
if active[idx] {
adj[u].push(v);
adj[v].push(u);
}
}
let mut visited = vec![false; n];
visited[s] = true;
let mut queue = VecDeque::new();
queue.push_back(s);
while let Some(node) = queue.pop_front() {
for &nb in &adj[node] {
if nb == t {
return true;
}
if !visited[nb] {
visited[nb] = true;
queue.push_back(nb);
}
}
}
false
}
fn is_fully_connected(n: usize, edges: &[(usize, usize, f64)], active: &[bool]) -> bool {
if n <= 1 {
return true;
}
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for (idx, &(u, v, _)) in edges.iter().enumerate() {
if active[idx] {
adj[u].push(v);
adj[v].push(u);
}
}
let mut visited = vec![false; n];
visited[0] = true;
let mut queue = VecDeque::new();
queue.push_back(0usize);
let mut count = 1usize;
while let Some(node) = queue.pop_front() {
for &nb in &adj[node] {
if !visited[nb] {
visited[nb] = true;
count += 1;
queue.push_back(nb);
}
}
}
count == n
}
#[derive(Debug, Clone)]
pub struct NetworkReliability {
pub source: usize,
pub terminal: usize,
}
impl NetworkReliability {
pub fn new(source: usize, terminal: usize) -> Self {
Self { source, terminal }
}
pub fn monte_carlo(
&self,
adj: &Array2<f64>,
num_trials: usize,
seed: Option<u64>,
) -> Result<f64> {
let n = adj.nrows();
if self.source >= n {
return Err(GraphError::InvalidParameter {
param: "source".into(),
value: self.source.to_string(),
expected: format!("< {n}"),
context: "NetworkReliability::monte_carlo".into(),
});
}
if self.terminal >= n {
return Err(GraphError::InvalidParameter {
param: "terminal".into(),
value: self.terminal.to_string(),
expected: format!("< {n}"),
context: "NetworkReliability::monte_carlo".into(),
});
}
if num_trials == 0 {
return Ok(0.0);
}
let edges = extract_edges(adj);
let m = edges.len();
let mut rng: StdRng = match seed {
Some(s) => StdRng::seed_from_u64(s),
None => StdRng::from_rng(&mut scirs2_core::random::rng()),
};
let mut successes = 0u64;
let mut active = vec![false; m];
for _ in 0..num_trials {
for (idx, &(_, _, p)) in edges.iter().enumerate() {
active[idx] = rng.random::<f64>() < p;
}
if can_reach(n, &edges, &active, self.source, self.terminal) {
successes += 1;
}
}
Ok(successes as f64 / num_trials as f64)
}
pub fn monte_carlo_with_ci(
&self,
adj: &Array2<f64>,
num_trials: usize,
seed: Option<u64>,
) -> Result<(f64, f64)> {
let p_hat = self.monte_carlo(adj, num_trials, seed)?;
let half_width = if num_trials > 0 {
1.96 * (p_hat * (1.0 - p_hat) / num_trials as f64).sqrt()
} else {
1.0
};
Ok((p_hat, half_width))
}
pub fn exact(&self, adj: &Array2<f64>) -> Result<f64> {
let n = adj.nrows();
if self.source >= n || self.terminal >= n {
return Err(GraphError::InvalidParameter {
param: "source/terminal".into(),
value: format!("{}/{}", self.source, self.terminal),
expected: format!("< {n}"),
context: "NetworkReliability::exact".into(),
});
}
let edges = extract_edges(adj);
let m = edges.len();
if m > 25 {
return Err(GraphError::InvalidParameter {
param: "num_edges".into(),
value: m.to_string(),
expected: "<= 25 for exact computation".into(),
context: "NetworkReliability::exact".into(),
});
}
let mut total = 0.0_f64;
for mask in 0u32..(1u32 << m) {
let active: Vec<bool> = (0..m).map(|i| (mask >> i) & 1 == 1).collect();
let prob: f64 = edges.iter().enumerate().map(|(i, &(_, _, p))| {
if active[i] { p } else { 1.0 - p }
}).product();
if can_reach(n, &edges, &active, self.source, self.terminal) {
total += prob;
}
}
Ok(total)
}
}
#[derive(Debug, Clone, Default)]
pub struct AllTerminalReliability;
impl AllTerminalReliability {
pub fn new() -> Self {
Self
}
pub fn monte_carlo(
&self,
adj: &Array2<f64>,
num_trials: usize,
seed: Option<u64>,
) -> Result<f64> {
let n = adj.nrows();
if n == 0 {
return Err(GraphError::InvalidGraph("empty adjacency".into()));
}
if num_trials == 0 {
return Ok(0.0);
}
let edges = extract_edges(adj);
let m = edges.len();
let mut rng: StdRng = match seed {
Some(s) => StdRng::seed_from_u64(s),
None => StdRng::from_rng(&mut scirs2_core::random::rng()),
};
let mut successes = 0u64;
let mut active = vec![false; m];
for _ in 0..num_trials {
for (idx, &(_, _, p)) in edges.iter().enumerate() {
active[idx] = rng.random::<f64>() < p;
}
if is_fully_connected(n, &edges, &active) {
successes += 1;
}
}
Ok(successes as f64 / num_trials as f64)
}
pub fn exact(&self, adj: &Array2<f64>) -> Result<f64> {
let n = adj.nrows();
let edges = extract_edges(adj);
let m = edges.len();
if m > 20 {
return Err(GraphError::InvalidParameter {
param: "num_edges".into(),
value: m.to_string(),
expected: "<= 20 for exact computation".into(),
context: "AllTerminalReliability::exact".into(),
});
}
let mut total = 0.0_f64;
for mask in 0u32..(1u32 << m) {
let active: Vec<bool> = (0..m).map(|i| (mask >> i) & 1 == 1).collect();
let prob: f64 = edges.iter().enumerate().map(|(i, &(_, _, p))| {
if active[i] { p } else { 1.0 - p }
}).product();
if is_fully_connected(n, &edges, &active) {
total += prob;
}
}
Ok(total)
}
}
#[derive(Debug, Clone)]
pub struct ReliabilityPolynomial {
pub coeffs: Vec<u64>,
pub num_edges: usize,
pub num_nodes: usize,
}
impl ReliabilityPolynomial {
pub fn compute(adj: &Array2<f64>) -> Result<Self> {
let n = adj.nrows();
let edges: Vec<(usize, usize)> = (0..n)
.flat_map(|i| (i + 1..n).filter_map(move |j| if adj[[i, j]] > 0.0 { Some((i, j)) } else { None }))
.collect();
let m = edges.len();
if m > 20 {
return Err(GraphError::InvalidParameter {
param: "num_edges".into(),
value: m.to_string(),
expected: "<= 20 for polynomial computation".into(),
context: "ReliabilityPolynomial::compute".into(),
});
}
let mut coeffs = vec![0u64; m + 1];
for mask in 0u32..(1u32 << m) {
let k = mask.count_ones() as usize;
let active: Vec<bool> = (0..m).map(|i| (mask >> i) & 1 == 1).collect();
let active_edges: Vec<(usize, usize, f64)> = edges
.iter()
.map(|&(u, v)| (u, v, 1.0))
.collect();
let active_flags: Vec<bool> = (0..m).map(|i| active[i]).collect();
if is_fully_connected(n, &active_edges, &active_flags) {
coeffs[k] += 1;
}
}
Ok(Self { coeffs, num_edges: m, num_nodes: n })
}
pub fn evaluate(&self, p: f64) -> f64 {
let m = self.num_edges;
let q = 1.0 - p;
self.coeffs.iter().enumerate().map(|(k, &c)| {
if c == 0 {
0.0
} else {
c as f64 * p.powi(k as i32) * q.powi((m - k) as i32)
}
}).sum()
}
pub fn min_connected_edges(&self) -> usize {
self.coeffs.iter().position(|&c| c > 0).unwrap_or(self.num_edges)
}
pub fn total_connected_subgraphs(&self) -> u64 {
self.coeffs.iter().sum()
}
}
#[derive(Debug, Clone)]
enum BddNode {
Terminal(bool),
Internal { var: usize, low: usize, high: usize },
}
#[derive(Debug)]
pub struct BDD {
nodes: Vec<BddNode>,
unique: HashMap<(usize, usize, usize), usize>,
num_edges: usize,
num_nodes: usize,
edges: Vec<(usize, usize)>,
root: usize,
}
impl BDD {
pub fn build_all_terminal(adj: &Array2<f64>) -> Result<Self> {
let n = adj.nrows();
let edges: Vec<(usize, usize)> = (0..n)
.flat_map(|i| (i + 1..n).filter_map(move |j| if adj[[i, j]] > 0.0 { Some((i, j)) } else { None }))
.collect();
let m = edges.len();
if m > 20 {
return Err(GraphError::InvalidParameter {
param: "num_edges".into(),
value: m.to_string(),
expected: "<= 20 for BDD".into(),
context: "BDD::build_all_terminal".into(),
});
}
let mut bdd = BDD {
nodes: Vec::new(),
unique: HashMap::new(),
num_edges: m,
num_nodes: n,
edges: edges.clone(),
root: 0,
};
bdd.nodes.push(BddNode::Terminal(false));
bdd.nodes.push(BddNode::Terminal(true));
let active_mask = (1u32 << m) - 1; let root = bdd.build_node(0, active_mask, n, &edges);
bdd.root = root;
Ok(bdd)
}
fn build_node(
&mut self,
var: usize,
forced_on: u32,
n_nodes: usize,
edges: &[(usize, usize)],
) -> usize {
let m = edges.len();
if var == m {
let active: Vec<bool> = (0..m).map(|i| (forced_on >> i) & 1 == 1).collect();
let edge_data: Vec<(usize, usize, f64)> = edges.iter().map(|&(u, v)| (u, v, 1.0)).collect();
let connected = is_fully_connected(n_nodes, &edge_data, &active);
return if connected { 1 } else { 0 };
}
let key = (var, forced_on as usize, 0);
if let Some(&idx) = self.unique.get(&key) {
return idx;
}
let low_forced = forced_on & !(1u32 << var);
let low = self.build_node(var + 1, low_forced, n_nodes, edges);
let high_forced = forced_on | (1u32 << var);
let high = self.build_node(var + 1, high_forced, n_nodes, edges);
if low == high {
self.unique.insert(key, low);
return low;
}
let idx = self.nodes.len();
self.nodes.push(BddNode::Internal { var, low, high });
self.unique.insert(key, idx);
idx
}
pub fn reliability(&self, probs: &[f64]) -> Result<f64> {
if probs.len() != self.num_edges {
return Err(GraphError::InvalidParameter {
param: "probs.len()".into(),
value: probs.len().to_string(),
expected: self.num_edges.to_string(),
context: "BDD::reliability".into(),
});
}
Ok(self.eval_node(self.root, probs))
}
fn eval_node(&self, node_idx: usize, probs: &[f64]) -> f64 {
match &self.nodes[node_idx] {
BddNode::Terminal(t) => if *t { 1.0 } else { 0.0 },
BddNode::Internal { var, low, high } => {
let p = probs[*var];
let q = 1.0 - p;
q * self.eval_node(*low, probs) + p * self.eval_node(*high, probs)
}
}
}
pub fn size(&self) -> usize {
self.nodes.len()
}
pub fn num_edges(&self) -> usize {
self.num_edges
}
pub fn num_network_nodes(&self) -> usize {
self.num_nodes
}
}
#[derive(Debug, Clone)]
pub struct FailureNode {
pub failed_edges: Vec<usize>,
pub is_cut: bool,
pub probability: f64,
pub children: Vec<FailureNode>,
}
#[derive(Debug)]
pub struct ComponentFailureTree {
pub root: FailureNode,
pub minimal_cuts: Vec<Vec<usize>>,
pub num_edges: usize,
pub num_nodes: usize,
}
impl ComponentFailureTree {
pub fn build(adj: &Array2<f64>, max_depth: usize) -> Result<Self> {
let n = adj.nrows();
let edges: Vec<(usize, usize, f64)> = extract_edges(adj);
let m = edges.len();
let mut minimal_cuts = Vec::new();
let root_active = vec![true; m];
let is_cut = !is_fully_connected(n, &edges, &root_active);
let root = FailureNode {
failed_edges: Vec::new(),
is_cut,
probability: 1.0,
children: Vec::new(),
};
let mut tree = ComponentFailureTree {
root,
minimal_cuts: Vec::new(),
num_edges: m,
num_nodes: n,
};
let failed: Vec<usize> = Vec::new();
let edge_probs: Vec<f64> = edges.iter().map(|&(_, _, p)| p).collect();
failure_tree_expand_node(
&mut tree.root.children,
&failed,
0,
max_depth,
n,
&edges,
&edge_probs,
m,
&mut minimal_cuts,
);
tree.minimal_cuts = minimal_cuts;
Ok(tree)
}
pub fn minimal_cuts(&self) -> &[Vec<usize>] {
&self.minimal_cuts
}
pub fn unreliability_upper_bound(&self) -> f64 {
Self::sum_cut_probs(&self.root)
}
fn sum_cut_probs(node: &FailureNode) -> f64 {
let self_contribution = if node.is_cut { node.probability } else { 0.0 };
let child_sum: f64 = node.children.iter().map(Self::sum_cut_probs).sum();
self_contribution + child_sum
}
}
#[allow(clippy::too_many_arguments)]
fn failure_tree_expand_node(
children: &mut Vec<FailureNode>,
parent_failed: &[usize],
start_edge: usize,
remaining_depth: usize,
n: usize,
edges: &[(usize, usize, f64)],
edge_probs: &[f64],
m: usize,
minimal_cuts: &mut Vec<Vec<usize>>,
) {
if remaining_depth == 0 {
return;
}
for e in start_edge..m {
let mut failed = parent_failed.to_vec();
failed.push(e);
let prob: f64 = (0..m)
.map(|i| {
if failed.contains(&i) {
1.0 - edge_probs[i]
} else {
edge_probs[i]
}
})
.product();
let active: Vec<bool> = (0..m).map(|i| !failed.contains(&i)).collect();
let is_cut = !is_fully_connected(n, edges, &active);
if is_cut {
let parent_active: Vec<bool> = (0..m).map(|i| !parent_failed.contains(&i)).collect();
let parent_is_cut = !is_fully_connected(n, edges, &parent_active);
if !parent_is_cut {
minimal_cuts.push(failed.clone());
}
}
let mut node = FailureNode {
failed_edges: failed.clone(),
is_cut,
probability: prob,
children: Vec::new(),
};
if !is_cut && remaining_depth > 1 {
failure_tree_expand_node(
&mut node.children,
&failed,
e + 1,
remaining_depth - 1,
n,
edges,
edge_probs,
m,
minimal_cuts,
);
}
children.push(node);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn triangle_adj(p: f64) -> Array2<f64> {
let mut adj = Array2::<f64>::zeros((3, 3));
adj[[0, 1]] = p;
adj[[1, 0]] = p;
adj[[1, 2]] = p;
adj[[2, 1]] = p;
adj[[0, 2]] = p;
adj[[2, 0]] = p;
adj
}
fn path_adj_p(n: usize, p: f64) -> Array2<f64> {
let mut adj = Array2::<f64>::zeros((n, n));
for i in 0..(n - 1) {
adj[[i, i + 1]] = p;
adj[[i + 1, i]] = p;
}
adj
}
#[test]
fn test_two_terminal_exact_vs_mc() {
let p = 0.8;
let adj = path_adj_p(3, p);
let rel = NetworkReliability::new(0, 2);
let exact = rel.exact(&adj).unwrap();
assert!((exact - p * p).abs() < 1e-9, "Exact: {exact} vs {}", p * p);
let mc = rel.monte_carlo(&adj, 50000, Some(99)).unwrap();
assert!((mc - exact).abs() < 0.02, "MC: {mc} vs exact: {exact}");
}
#[test]
fn test_all_terminal_triangle_exact() {
let p = 0.9;
let adj = triangle_adj(p);
let rel = AllTerminalReliability::new();
let exact = rel.exact(&adj).unwrap();
let expected = 3.0 * p * p * (1.0 - p) + p * p * p;
assert!((exact - expected).abs() < 1e-9, "Exact: {exact} vs {expected}");
let mc = rel.monte_carlo(&adj, 50000, Some(7)).unwrap();
assert!((mc - exact).abs() < 0.02);
}
#[test]
fn test_reliability_polynomial() {
let adj = triangle_adj(1.0); let poly = ReliabilityPolynomial::compute(&adj).unwrap();
assert_eq!(poly.num_edges, 3);
let r1 = poly.evaluate(1.0);
assert!((r1 - 1.0).abs() < 1e-9, "R(1)={r1}");
let r0 = poly.evaluate(0.0);
assert!((r0 - 0.0).abs() < 1e-9, "R(0)={r0}");
}
#[test]
fn test_bdd_vs_exact() {
let p = 0.75;
let adj = triangle_adj(p);
let bdd = BDD::build_all_terminal(&adj).unwrap();
let probs = vec![p; 3];
let bdd_result = bdd.reliability(&probs).unwrap();
let exact = AllTerminalReliability::new().exact(&adj).unwrap();
assert!((bdd_result - exact).abs() < 1e-9, "BDD: {bdd_result} vs exact: {exact}");
}
#[test]
fn test_component_failure_tree() {
let adj = triangle_adj(0.9);
let tree = ComponentFailureTree::build(&adj, 2).unwrap();
assert!(!tree.minimal_cuts().is_empty());
}
#[test]
fn test_reliability_ci() {
let adj = path_adj_p(3, 0.9);
let rel = NetworkReliability::new(0, 2);
let (p_hat, hw) = rel.monte_carlo_with_ci(&adj, 10000, Some(1)).unwrap();
assert!(p_hat >= 0.0 && p_hat <= 1.0);
assert!(hw >= 0.0 && hw <= 0.1);
}
}