diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 05ae126f..a2d91c80 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -133,6 +133,7 @@ "MultipleChoiceBranching": [Multiple Choice Branching], "MultipleCopyFileAllocation": [Multiple Copy File Allocation], "MultiprocessorScheduling": [Multiprocessor Scheduling], + "NetworkReliability": [Network Reliability], "PartitionIntoPathsOfLength2": [Partition into Paths of Length 2], "PartitionIntoTriangles": [Partition Into Triangles], "PrecedenceConstrainedScheduling": [Precedence Constrained Scheduling], @@ -1617,6 +1618,56 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#{ + let x = load-model-example("NetworkReliability") + let nv = graph-num-vertices(x.instance) + let edges = x.instance.graph.edges + let ne = edges.len() + let terminals = x.instance.terminals + let failure-prob = x.instance.failure_probs.at(0) + let threshold = x.instance.threshold + let witness-config = x.optimal_config + let witness-edge-indices = witness-config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let witness-edges = witness-edge-indices.map(i => edges.at(i)) + let witness-verts = witness-edges.fold((), (acc, e) => { + let acc2 = if acc.contains(e.at(0)) { acc } else { acc + (e.at(0),) } + if acc2.contains(e.at(1)) { acc2 } else { acc2 + (e.at(1),) } + }) + let exact-reliability = 0.96842547 + let connected-count = 91 + let disconnected-count = 165 + [ + #problem-def("NetworkReliability")[ + Given an undirected graph $G = (V, E)$, a terminal set $T subset.eq V$ with $|T| >= 2$, independent edge failure probabilities $p: E -> [0, 1]$, and a threshold $q in [0, 1]$, determine whether the probability that every pair of terminals in $T$ remains connected by a path of surviving edges is at least $q$. + ][ + Network Reliability captures whether a fault-prone communication network keeps its terminals connected despite independent edge failures. Garey and Johnson list the decision version as ND20 and mark it with an asterisk because membership in NP was not known for the general formulation @garey1979[App.~A2, ND20]. Valiant showed that the related reliability counting problems are \#P-complete, and Ball surveyed the resulting hardness landscape for two-terminal, all-terminal, and $k$-terminal variants @valiant1979 @ball1986. The implementation here uses one binary variable per edge, so exact reliability on the represented graphs is obtained by enumerating all $2^m$ survival patterns and checking terminal connectivity in $O(2^m dot (m + n))$ time. #footnote[No better exact worst-case bound is claimed here for the general graph family implemented in the codebase.] + + *Example.* Consider the graph $G$ with $n = #nv$ vertices, $m = #ne$ edges, terminals $T = {#terminals.map(i => $v_#i$).join(", ")}$, uniform failure probability $p(e) = #failure-prob$ on every edge, and threshold $q = #threshold$. The highlighted witness path #witness-edges.map(e => $(v_#(e.at(0)), v_#(e.at(1)))$).join(", ") is one surviving-edge configuration for which `evaluate` returns true. Summing the probabilities of all #connected-count terminal-connecting survival patterns yields $R(G, T, p) = #exact-reliability > #threshold$, while the remaining #disconnected-count patterns disconnect the terminals, so this is a YES instance. + + #figure({ + let verts = ((0, 1.2), (1.6, 1.8), (1.6, 0.4), (3.2, 1.1), (3.2, -0.3), (4.8, 0.4)) + canvas(length: 1cm, { + for (idx, (u, v)) in edges.enumerate() { + let on-witness = witness-edge-indices.contains(idx) + g-edge(verts.at(u), verts.at(v), + stroke: if on-witness { 2pt + graph-colors.at(0) } else { 1pt + luma(200) }) + } + for (k, pos) in verts.enumerate() { + let is-terminal = terminals.contains(k) + let on-witness = witness-verts.contains(k) + g-node(pos, name: "v" + str(k), + fill: if is-terminal { graph-colors.at(0) } else if on-witness { graph-colors.at(0).transparentize(70%) } else { white }, + stroke: if is-terminal { none } else { 1pt + graph-colors.at(0) }, + label: text(fill: if is-terminal { white } else { black })[$v_#k$]) + } + }) + }, + caption: [Network Reliability example with terminals $T = {#terminals.map(i => $v_#i$).join(", ")}$ (blue). The highlighted witness path survives in one connected configuration, while the exact reliability over all $2^#ne$ survival patterns is $#exact-reliability > #threshold$.], + ) + ] + ] +} + #{ let x = load-model-example("MinimumSumMulticenter") let nv = graph-num-vertices(x.instance) diff --git a/docs/paper/references.bib b/docs/paper/references.bib index bce4d90d..76eb5f90 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -180,6 +180,28 @@ @book{garey1979 year = {1979} } +@article{valiant1979, + author = {Leslie G. Valiant}, + title = {The Complexity of Enumeration and Reliability Problems}, + journal = {SIAM Journal on Computing}, + volume = {8}, + number = {3}, + pages = {410--421}, + year = {1979}, + doi = {10.1137/0208032} +} + +@article{ball1986, + author = {Michael O. Ball}, + title = {Computational Complexity of Network Reliability Analysis: An Overview}, + journal = {IEEE Transactions on Reliability}, + volume = {R-35}, + number = {3}, + pages = {230--239}, + year = {1986}, + doi = {10.1109/TR.1986.4335422} +} + @article{bruckerGareyJohnson1977, author = {Peter Brucker and Michael R. Garey and David S. Johnson}, title = {Scheduling equal-length tasks under tree-like precedence constraints to minimize maximum lateness}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 29240024..c6474820 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -217,6 +217,7 @@ TIP: Run `pred create ` (no other flags) to see problem-specific help. Flags by problem type: MIS, MVC, MaxClique, MinDomSet --graph, --weights MaxCut, MaxMatching, TSP --graph, --edge-weights + NetworkReliability --graph, --terminals, --failure-probs, --threshold ShortestWeightConstrainedPath --graph, --edge-lengths, --edge-weights, --source-vertex, --target-vertex, --length-bound, --weight-bound MaximalIS --graph, --weights SAT, NAESAT --num-vars, --clauses @@ -345,6 +346,9 @@ pub struct CreateArgs { /// Edge weights (e.g., 2,3,1) [default: all 1s] #[arg(long)] pub edge_weights: Option, + /// Edge failure probabilities for NetworkReliability (e.g., 0.1,0.2,0.1) + #[arg(long)] + pub failure_probs: Option, /// Edge lengths (e.g., 2,3,1) [default: all 1s] #[arg(long)] pub edge_lengths: Option, @@ -499,6 +503,9 @@ pub struct CreateArgs { /// Upper bound or length bound (for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleCopyFileAllocation, MultipleChoiceBranching, OptimalLinearArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection) #[arg(long, allow_hyphen_values = true)] pub bound: Option, + /// Reliability threshold q for NetworkReliability + #[arg(long)] + pub threshold: Option, /// Upper bound on total path length #[arg(long)] pub length_bound: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index be56d6f5..53c0da4e 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -47,6 +47,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { args.graph.is_none() && args.weights.is_none() && args.edge_weights.is_none() + && args.failure_probs.is_none() && args.edge_lengths.is_none() && args.capacities.is_none() && args.source.is_none() @@ -98,6 +99,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.tree.is_none() && args.required_edges.is_none() && args.bound.is_none() + && args.threshold.is_none() && args.length_bound.is_none() && args.weight_bound.is_none() && args.pattern.is_none() @@ -539,6 +541,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "ShortestWeightConstrainedPath" => { "--graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,1-4 --edge-lengths 2,4,3,1,5,4,2,6 --edge-weights 5,1,2,3,2,3,1,1 --source-vertex 0 --target-vertex 5 --length-bound 10 --weight-bound 8" } + "NetworkReliability" => { + "--graph 0-1,0-2,1-3,2-3,1-4,3-4,3-5,4-5 --terminals 0,5 --failure-probs 0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1 --threshold 0.95" + } "SteinerTreeInGraphs" => "--graph 0-1,1-2,2-3 --edge-weights 1,1,1 --terminals 0,3", "BiconnectivityAugmentation" => { "--graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5" @@ -654,6 +659,7 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String { match (canonical, field_name) { ("BoundedComponentSpanningForest", "max_components") => return "k".to_string(), ("BoundedComponentSpanningForest", "max_weight") => return "bound".to_string(), + ("NetworkReliability", "threshold") => return "threshold".to_string(), ("FlowShopScheduling", "num_processors") | ("SchedulingWithIndividualDeadlines", "num_processors") => { return "num-processors/--m".to_string(); @@ -726,6 +732,8 @@ fn help_flag_hint( } ("ShortestCommonSupersequence", "strings") => "symbol lists: \"0,1,2;1,2,0\"", ("MultipleChoiceBranching", "partition") => "semicolon-separated groups: \"0,1;2,3\"", + ("NetworkReliability", "failure_probs") => "comma-separated probabilities: 0.1,0.1,0.2", + ("NetworkReliability", "threshold") => "number in [0,1]", ("ConsistencyOfDatabaseFrequencyTables", "attribute_domains") => { "comma-separated domain sizes: 2,3,2" } @@ -1466,6 +1474,25 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // NetworkReliability (graph + terminals + failure probabilities + threshold) + "NetworkReliability" => { + let usage = "Usage: pred create NetworkReliability --graph 0-1,0-2,1-3,2-3,1-4,3-4,3-5,4-5 --terminals 0,5 --failure-probs 0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1 --threshold 0.95"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let terminals = parse_terminals(args, graph.num_vertices()) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let failure_probs = parse_failure_probs(args, graph.num_edges(), usage)?; + let threshold = parse_network_reliability_threshold(args, usage)?; + ( + ser(NetworkReliability::new( + graph, + terminals, + failure_probs, + threshold, + ))?, + resolved_variant.clone(), + ) + } + // RuralPostman "RuralPostman" => { reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; @@ -3930,6 +3957,47 @@ fn parse_edge_weights(args: &CreateArgs, num_edges: usize) -> Result> { parse_i32_edge_values(args.edge_weights.as_ref(), num_edges, "edge weight") } +fn parse_failure_probs(args: &CreateArgs, num_edges: usize, usage: &str) -> Result> { + let raw = args + .failure_probs + .as_deref() + .ok_or_else(|| anyhow::anyhow!("NetworkReliability requires --failure-probs\n\n{usage}"))?; + let failure_probs: Vec = raw + .split(',') + .map(|entry| { + let trimmed = entry.trim(); + trimmed + .parse::() + .with_context(|| format!("invalid failure probability `{trimmed}`\n\n{usage}")) + }) + .collect::>>()?; + if failure_probs.len() != num_edges { + bail!( + "Expected {} failure probabilities but got {}\n\n{}", + num_edges, + failure_probs.len(), + usage + ); + } + if failure_probs + .iter() + .any(|prob| !prob.is_finite() || !(0.0..=1.0).contains(prob)) + { + bail!("All failure probabilities must be finite values in [0,1]\n\n{usage}"); + } + Ok(failure_probs) +} + +fn parse_network_reliability_threshold(args: &CreateArgs, usage: &str) -> Result { + let threshold = args + .threshold + .ok_or_else(|| anyhow::anyhow!("NetworkReliability requires --threshold\n\n{usage}"))?; + if !threshold.is_finite() || !(0.0..=1.0).contains(&threshold) { + bail!("NetworkReliability threshold must be in [0,1]\n\n{usage}"); + } + Ok(threshold) +} + fn validate_vertex_index( label: &str, vertex: usize, @@ -5590,6 +5658,7 @@ mod tests { graph: None, weights: None, edge_weights: None, + failure_probs: None, edge_lengths: None, capacities: None, source: None, @@ -5641,6 +5710,7 @@ mod tests { tree: None, required_edges: None, bound: None, + threshold: None, length_bound: None, weight_bound: None, pattern: None, diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 646a76e9..72321bc7 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -3015,6 +3015,43 @@ fn test_create_steiner_tree_rejects_duplicate_terminals() { assert!(stderr.contains("terminals must be distinct"), "{stderr}"); } +#[test] +fn test_create_network_reliability() { + let output_file = std::env::temp_dir().join("pred_test_create_network_reliability.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "NetworkReliability", + "--graph", + "0-1,0-2,1-3,2-3,1-4,3-4,3-5,4-5", + "--terminals", + "0,5", + "--failure-probs", + "0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1", + "--threshold", + "0.95", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "NetworkReliability"); + assert_eq!(json["data"]["terminals"], serde_json::json!([0, 5])); + assert_eq!( + json["data"]["failure_probs"], + serde_json::json!([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]) + ); + assert_eq!(json["data"]["threshold"], serde_json::json!(0.95)); + std::fs::remove_file(&output_file).ok(); +} + #[test] fn test_create_sequencing_to_minimize_weighted_completion_time() { let output_file = std::env::temp_dir() @@ -3265,6 +3302,24 @@ fn test_create_model_example_steiner_tree() { assert_eq!(json["variant"]["weight"], "i32"); } +#[test] +fn test_create_model_example_network_reliability() { + let output = pred() + .args(["create", "--example", "NetworkReliability"]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "NetworkReliability"); + assert_eq!(json["data"]["terminals"], serde_json::json!([0, 5])); + assert_eq!(json["data"]["threshold"], serde_json::json!(0.95)); +} + #[test] fn test_create_missing_model_example() { let output = pred() diff --git a/src/lib.rs b/src/lib.rs index 89ce43d8..85db90a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,9 +58,9 @@ pub mod prelude { KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinMaxMulticenter, MinimumCutIntoBoundedSets, MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, - MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, - PartitionIntoPathsOfLength2, PartitionIntoTriangles, RuralPostman, - ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman, + MultipleChoiceBranching, MultipleCopyFileAllocation, NetworkReliability, + OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, + RuralPostman, ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index c8801748..9fc6c637 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -36,6 +36,7 @@ //! - [`MultipleChoiceBranching`]: Directed branching with partition constraints //! - [`LengthBoundedDisjointPaths`]: Length-bounded internally disjoint s-t paths //! - [`RuralPostman`]: Rural Postman (circuit covering required edges) +//! - [`NetworkReliability`]: Terminal connectivity under independent edge failures //! - [`SteinerTree`]: Minimum-weight tree spanning all required terminals //! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem) //! - [`DirectedTwoCommodityIntegralFlow`]: Directed two-commodity integral flow (satisfaction) @@ -71,6 +72,7 @@ pub(crate) mod minimum_sum_multicenter; pub(crate) mod minimum_vertex_cover; pub(crate) mod multiple_choice_branching; pub(crate) mod multiple_copy_file_allocation; +pub(crate) mod network_reliability; pub(crate) mod optimal_linear_arrangement; pub(crate) mod partition_into_paths_of_length_2; pub(crate) mod partition_into_triangles; @@ -113,6 +115,7 @@ pub use minimum_sum_multicenter::MinimumSumMulticenter; pub use minimum_vertex_cover::MinimumVertexCover; pub use multiple_choice_branching::MultipleChoiceBranching; pub use multiple_copy_file_allocation::MultipleCopyFileAllocation; +pub use network_reliability::NetworkReliability; pub use optimal_linear_arrangement::OptimalLinearArrangement; pub use partition_into_paths_of_length_2::PartitionIntoPathsOfLength2; pub use partition_into_triangles::PartitionIntoTriangles; @@ -147,6 +150,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Terminal vertices T subset of V that must remain connected" }, + FieldInfo { name: "failure_probs", type_name: "Vec", description: "Independent edge failure probabilities p: E -> [0,1]" }, + FieldInfo { name: "threshold", type_name: "f64", description: "Required reliability threshold q in [0,1]" }, + ], + } +} + +/// The Network Reliability decision problem. +/// +/// Each binary configuration indicates which edges survive: +/// - `0`: the edge fails +/// - `1`: the edge survives +/// +/// `evaluate(config)` checks whether the surviving-edge subgraph keeps all +/// terminal vertices connected. The overall decision question +/// `R(G, T, p) >= q` is exposed via [`NetworkReliability::reliability`] and +/// [`NetworkReliability::meets_threshold`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkReliability { + graph: SimpleGraph, + terminals: Vec, + failure_probs: Vec, + threshold: f64, +} + +impl NetworkReliability { + /// Create a Network Reliability instance. + /// + /// # Panics + /// Panics if the edge probability vector length does not match `num_edges`, + /// terminals are invalid, or any probability/threshold lies outside `[0, 1]`. + pub fn new( + graph: SimpleGraph, + terminals: Vec, + failure_probs: Vec, + threshold: f64, + ) -> Self { + assert_eq!( + failure_probs.len(), + graph.num_edges(), + "failure_probs length must match num_edges" + ); + + let n = graph.num_vertices(); + let distinct_terminals: BTreeSet<_> = terminals.iter().copied().collect(); + assert_eq!( + distinct_terminals.len(), + terminals.len(), + "terminals must be distinct" + ); + assert!(terminals.len() >= 2, "at least 2 terminals required"); + for &terminal in &terminals { + assert!( + terminal < n, + "terminal {} out of range (num_vertices = {})", + terminal, + n + ); + } + + for (index, &prob) in failure_probs.iter().enumerate() { + assert!( + prob.is_finite() && (0.0..=1.0).contains(&prob), + "failure probability at edge {} must be in [0, 1]", + index + ); + } + assert!( + threshold.is_finite() && (0.0..=1.0).contains(&threshold), + "threshold must be in [0, 1]" + ); + + Self { + graph, + terminals, + failure_probs, + threshold, + } + } + + /// Get the underlying graph. + pub fn graph(&self) -> &SimpleGraph { + &self.graph + } + + /// Get the terminal vertices. + pub fn terminals(&self) -> &[usize] { + &self.terminals + } + + /// Get the independent edge failure probabilities. + pub fn failure_probs(&self) -> &[f64] { + &self.failure_probs + } + + /// Get the reliability threshold. + pub fn threshold(&self) -> f64 { + self.threshold + } + + /// Get the number of vertices. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of edges. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + /// Get the number of terminals. + pub fn num_terminals(&self) -> usize { + self.terminals.len() + } + + /// Check whether a configuration keeps all terminals connected. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + self.evaluate(config) + } + + /// Compute the probability mass of a single edge-survival configuration. + pub fn configuration_probability(&self, config: &[usize]) -> f64 { + if config.len() != self.num_edges() || config.iter().any(|&bit| bit > 1) { + return 0.0; + } + + config + .iter() + .zip(self.failure_probs.iter()) + .map(|(&bit, &failure_prob)| { + if bit == 1 { + 1.0 - failure_prob + } else { + failure_prob + } + }) + .product() + } + + /// Sum the probabilities of all surviving-edge patterns that connect the terminals. + pub fn reliability(&self) -> f64 { + DimsIterator::new(self.dims()) + .filter(|config| self.evaluate(config)) + .map(|config| self.configuration_probability(&config)) + .sum() + } + + /// Return whether the exact reliability meets the instance threshold. + pub fn meets_threshold(&self) -> bool { + const EPSILON: f64 = 1e-12; + self.reliability() + EPSILON >= self.threshold + } +} + +impl Problem for NetworkReliability { + const NAME: &'static str = "NetworkReliability"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![2; self.graph.num_edges()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + terminals_connected_with_surviving_edges(&self.graph, &self.terminals, config) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for NetworkReliability {} + +fn terminals_connected_with_surviving_edges( + graph: &SimpleGraph, + terminals: &[usize], + config: &[usize], +) -> bool { + if config.len() != graph.num_edges() || config.iter().any(|&bit| bit > 1) { + return false; + } + + let mut adjacency = vec![Vec::new(); graph.num_vertices()]; + for ((u, v), &bit) in graph.edges().iter().zip(config.iter()) { + if bit == 1 { + adjacency[*u].push(*v); + adjacency[*v].push(*u); + } + } + + let start = terminals[0]; + let mut visited = vec![false; graph.num_vertices()]; + let mut queue = VecDeque::from([start]); + visited[start] = true; + + while let Some(vertex) = queue.pop_front() { + for &neighbor in &adjacency[vertex] { + if !visited[neighbor] { + visited[neighbor] = true; + queue.push_back(neighbor); + } + } + } + + terminals.iter().all(|&terminal| visited[terminal]) +} + +crate::declare_variants! { + default sat NetworkReliability => "2^num_edges * (num_edges + num_vertices)", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "network_reliability", + instance: Box::new(NetworkReliability::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (1, 4), + (3, 4), + (3, 5), + (4, 5), + ], + ), + vec![0, 5], + vec![0.1; 8], + 0.95, + )), + optimal_config: vec![1, 0, 1, 0, 0, 0, 1, 0], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/network_reliability.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index a2be59f0..b9858517 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -25,9 +25,9 @@ pub use graph::{ MaximumIndependentSet, MaximumMatching, MinMaxMulticenter, MinimumCutIntoBoundedSets, MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching, MultipleCopyFileAllocation, - OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, RuralPostman, - ShortestWeightConstrainedPath, SpinGlass, SteinerTree, SteinerTreeInGraphs, - StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, + NetworkReliability, OptimalLinearArrangement, PartitionIntoPathsOfLength2, + PartitionIntoTriangles, RuralPostman, ShortestWeightConstrainedPath, SpinGlass, SteinerTree, + SteinerTreeInGraphs, StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; pub use misc::PartiallyOrderedKnapsack; diff --git a/src/unit_tests/models/graph/network_reliability.rs b/src/unit_tests/models/graph/network_reliability.rs new file mode 100644 index 00000000..d960ae48 --- /dev/null +++ b/src/unit_tests/models/graph/network_reliability.rs @@ -0,0 +1,137 @@ +use crate::config::DimsIterator; +use crate::models::graph::NetworkReliability; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::Problem; + +fn issue_235_example_with_threshold(threshold: f64) -> NetworkReliability { + NetworkReliability::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (1, 4), + (3, 4), + (3, 5), + (4, 5), + ], + ), + vec![0, 5], + vec![0.1; 8], + threshold, + ) +} + +fn issue_235_example() -> NetworkReliability { + issue_235_example_with_threshold(0.95) +} + +#[test] +fn test_network_reliability_creation_and_getters() { + let problem = issue_235_example(); + + assert_eq!(problem.graph().num_vertices(), 6); + assert_eq!(problem.num_vertices(), 6); + assert_eq!(problem.num_edges(), 8); + assert_eq!(problem.num_terminals(), 2); + assert_eq!(problem.terminals(), &[0, 5]); + assert_eq!(problem.failure_probs(), &[0.1; 8]); + assert_eq!(problem.threshold(), 0.95); + assert_eq!(problem.dims(), vec![2; 8]); +} + +#[test] +fn test_network_reliability_evaluate_terminal_connectivity() { + let problem = issue_235_example(); + + let connected_config = vec![1, 0, 1, 0, 0, 0, 1, 0]; + let disconnected_config = vec![1, 0, 0, 0, 0, 0, 1, 0]; + + assert!(problem.evaluate(&connected_config)); + assert!(problem.is_valid_solution(&connected_config)); + assert!(!problem.evaluate(&disconnected_config)); + assert!(!problem.is_valid_solution(&disconnected_config)); +} + +#[test] +fn test_network_reliability_exact_reliability_matches_issue_example() { + let problem = issue_235_example(); + + let reliability = problem.reliability(); + let connected_count = DimsIterator::new(problem.dims()) + .filter(|config| problem.evaluate(config)) + .count(); + assert!((reliability - 0.968425).abs() < 1e-6); + assert_eq!(connected_count, 91); + assert_eq!((1usize << problem.num_edges()) - connected_count, 165); + assert!(problem.meets_threshold()); +} + +#[cfg(feature = "example-db")] +#[test] +fn test_network_reliability_paper_example() { + let problem = issue_235_example(); + let witness_config = vec![1, 0, 1, 0, 0, 0, 1, 0]; + + assert!(problem.evaluate(&witness_config)); + assert!((problem.reliability() - 0.96842547).abs() < 1e-9); + assert!(problem.meets_threshold()); + + let specs = super::canonical_model_example_specs(); + assert_eq!(specs.len(), 1); + assert_eq!(specs[0].optimal_config, witness_config); + assert_eq!(specs[0].optimal_value, serde_json::json!(true)); +} + +#[test] +fn test_network_reliability_threshold_decision_is_not_single_config_validity() { + let problem = issue_235_example_with_threshold(0.99); + let witness_config = vec![1, 0, 1, 0, 0, 0, 1, 0]; + + assert!(problem.evaluate(&witness_config)); + assert!(!problem.meets_threshold()); +} + +#[test] +#[should_panic(expected = "failure_probs length must match num_edges")] +fn test_network_reliability_rejects_bad_probability_length() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let _ = NetworkReliability::new(graph, vec![0, 2], vec![0.1], 0.5); +} + +#[test] +#[should_panic(expected = "failure probability")] +fn test_network_reliability_rejects_probability_out_of_range() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let _ = NetworkReliability::new(graph, vec![0, 2], vec![0.1, 1.2], 0.5); +} + +#[test] +#[should_panic(expected = "threshold must be in [0, 1]")] +fn test_network_reliability_rejects_threshold_out_of_range() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let _ = NetworkReliability::new(graph, vec![0, 2], vec![0.1, 0.2], 1.1); +} + +#[test] +#[should_panic(expected = "terminals must be distinct")] +fn test_network_reliability_rejects_duplicate_terminals() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let _ = NetworkReliability::new(graph, vec![0, 0], vec![0.1, 0.2], 0.5); +} + +#[test] +#[should_panic(expected = "terminal 3 out of range")] +fn test_network_reliability_rejects_terminal_out_of_bounds() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let _ = NetworkReliability::new(graph, vec![0, 3], vec![0.1, 0.2], 0.5); +} + +#[test] +#[should_panic(expected = "at least 2 terminals required")] +fn test_network_reliability_rejects_too_few_terminals() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let _ = NetworkReliability::new(graph, vec![0], vec![0.1, 0.2], 0.5); +}