From 5899373481993b480bcf5efea930e0421df3a7f6 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 17:03:43 +0800 Subject: [PATCH 1/6] Add plan for #235: [Model] NetworkReliability --- docs/plans/2026-03-21-network-reliability.md | 287 +++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 docs/plans/2026-03-21-network-reliability.md diff --git a/docs/plans/2026-03-21-network-reliability.md b/docs/plans/2026-03-21-network-reliability.md new file mode 100644 index 00000000..3133b886 --- /dev/null +++ b/docs/plans/2026-03-21-network-reliability.md @@ -0,0 +1,287 @@ +# NetworkReliability Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `NetworkReliability` model, exact reliability helpers, CLI/example-db integration, tests, and paper documentation for issue #235. + +**Architecture:** Implement `NetworkReliability` as a `SatisfactionProblem` whose binary configuration marks which graph edges survive. `evaluate()` should answer the per-configuration predicate "are all terminals connected in the surviving subgraph?", while separate model-local helpers perform the exact probability summation required for the real decision question `R(G, T, p) >= q`. Keep the model non-variant (`SimpleGraph`, `Vec`, `f64`) and wire it through the existing registry, CLI, example-db, and paper patterns used by other graph models with terminal sets. + +**Tech Stack:** Rust workspace (`problemreductions`, `problemreductions-cli`), `inventory`/`declare_variants!`, `DimsIterator` for exact enumeration, Typst paper docs, cargo/make verification. + +--- + +## Batch 1: Model + Integration (add-model Steps 1-5.5) + +### Task 1: Lock down the model contract with failing tests + +**Files:** +- Create: `src/unit_tests/models/graph/network_reliability.rs` +- Reference: `src/models/graph/steiner_tree_in_graphs.rs` +- Reference: `src/unit_tests/models/graph/steiner_tree_in_graphs.rs` + +**Step 1: Write the failing tests** + +Add focused tests that describe the intended behavior before any production code exists: + +```rust +#[test] +fn test_network_reliability_creation_and_getters() { + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let problem = NetworkReliability::new(graph, vec![0, 3], vec![0.1, 0.2, 0.3], 0.8); + assert_eq!(problem.num_vertices(), 4); + assert_eq!(problem.num_edges(), 3); + assert_eq!(problem.num_terminals(), 2); + assert_eq!(problem.dims(), vec![2; 3]); + assert_eq!(problem.threshold(), 0.8); +} + +#[test] +fn test_network_reliability_evaluate_checks_terminal_connectivity() { + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]); + let problem = NetworkReliability::new(graph, vec![0, 2], vec![0.1; 4], 0.5); + assert!(problem.evaluate(&[1, 1, 0, 0])); + assert!(!problem.evaluate(&[1, 0, 0, 0])); +} + +#[test] +fn test_network_reliability_exact_probability_matches_issue_example() { + let problem = issue_235_example(); + let reliability = problem.reliability(); + assert!((reliability - 0.968425).abs() < 1e-9); + assert!(problem.meets_threshold()); +} +``` + +Also cover invalid constructor inputs: +- `failure_probs.len() != num_edges` +- duplicate or out-of-bounds terminals +- fewer than 2 terminals +- failure probability outside `[0.0, 1.0]` +- threshold outside `[0.0, 1.0]` + +**Step 2: Run the test target and verify RED** + +Run: + +```bash +cargo test network_reliability --lib +``` + +Expected: compile failure because `NetworkReliability` and its test module do not exist yet. + +**Step 3: Write the minimal implementation to satisfy the tests** + +Create `src/models/graph/network_reliability.rs` with: +- `ProblemSchemaEntry` describing `graph`, `terminals`, `failure_probs`, and `threshold` +- `#[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetworkReliability` +- constructor validation for terminal distinctness/bounds and probability/threshold ranges +- accessors: `graph()`, `terminals()`, `failure_probs()`, `threshold()` +- size getters: `num_vertices()`, `num_edges()`, `num_terminals()` +- helpers: + - `is_valid_solution(&self, config: &[usize]) -> bool` + - `configuration_probability(&self, config: &[usize]) -> f64` + - `reliability(&self) -> f64` + - `meets_threshold(&self) -> bool` +- internal connectivity check for terminals in the surviving-edge subgraph +- `Problem` + `SatisfactionProblem` +- `crate::declare_variants! { default sat NetworkReliability => "2^num_edges * num_vertices" }` +- `canonical_model_example_specs()` for the issue example, with a representative connected configuration for example-db even though the issue’s real expected outcome is the aggregate reliability +- `#[cfg(test)]` link to the new test file + +Register the new model in: +- `src/models/graph/mod.rs` +- `src/models/mod.rs` +- `src/lib.rs` + +**Step 4: Run the tests and verify GREEN** + +Run: + +```bash +cargo test network_reliability --lib +``` + +Expected: the new model/unit tests pass. + +**Step 5: Commit the model slice** + +```bash +git add src/models/graph/network_reliability.rs src/models/graph/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/models/graph/network_reliability.rs +git commit -m "feat: add NetworkReliability model" +``` + +### Task 2: Add CLI creation and example-db plumbing + +**Files:** +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/tests/cli_tests.rs` +- Modify: `src/models/graph/mod.rs` +- Reference: `problemreductions-cli/src/commands/create.rs` + +**Step 1: Write the failing CLI tests** + +Add CLI tests that exercise both direct creation and canonical example creation: + +```rust +#[test] +fn test_create_network_reliability() { + // pred create NetworkReliability --graph ... --terminals 0,5 --failure-probs 0.1,... --threshold 0.95 +} + +#[test] +fn test_create_example_network_reliability() { + // pred create --example NetworkReliability +} +``` + +Assertions should verify: +- JSON `"type"` is `"NetworkReliability"` +- `failure_probs` round-trip as floats +- `threshold` round-trips as `0.95` +- `terminals` round-trip correctly + +**Step 2: Run the CLI tests and verify RED** + +Run: + +```bash +cargo test -p problemreductions-cli network_reliability +``` + +Expected: failure because the CLI has no `NetworkReliability` arm or `--failure-probs` / `--threshold` support yet. + +**Step 3: Implement the CLI path** + +Update `problemreductions-cli/src/cli.rs`: +- add `NetworkReliability --graph, --terminals, --failure-probs, --threshold` to the `after_help` table +- add `failure_probs: Option` and `threshold: Option` to `CreateArgs` +- include the new flags in `all_data_flags_empty()` + +Update `problemreductions-cli/src/commands/create.rs`: +- add example text for `NetworkReliability` +- add a parser helper for `--failure-probs` as a comma-separated `Vec` matching `num_edges` +- add a `NetworkReliability` match arm that parses graph, terminals, failure probabilities, and threshold +- prefer `--threshold` over reusing integer `--bound`, since this problem’s threshold is rational and should remain a `f64` +- keep alias resolution unchanged unless tests prove the registry-driven lookup is insufficient + +Keep the example-db registration consistent by ensuring the new `canonical_model_example_specs()` is included in the graph example chain. + +**Step 4: Run the CLI tests and targeted example-db coverage** + +Run: + +```bash +cargo test -p problemreductions-cli network_reliability +cargo test example_db --lib +``` + +Expected: CLI creation and example-db validation both pass. + +**Step 5: Commit the integration slice** + +```bash +git add problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs problemreductions-cli/tests/cli_tests.rs src/models/graph/mod.rs +git commit -m "feat: wire NetworkReliability through CLI" +``` + +## Batch 2: Paper + Final Verification (add-model Steps 6-7) + +### Task 3: Document the model and lock the paper example + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `src/unit_tests/models/graph/network_reliability.rs` + +**Step 1: Add the paper regression test first** + +Extend `src/unit_tests/models/graph/network_reliability.rs` with a paper-example test that: +- constructs the issue #235 example instance +- checks the representative connected configuration used by example-db/paper +- checks `reliability()` is `0.968425` within tolerance +- checks `meets_threshold()` is `true` + +**Step 2: Run the targeted test and verify RED if the paper/example wiring is still absent** + +Run: + +```bash +cargo test network_reliability_example --lib +``` + +Expected: fail until the final example and paper narrative are aligned. + +**Step 3: Write the paper entry** + +Update `docs/paper/reductions.typ`: +- add `"NetworkReliability": [Network Reliability]` to the display-name dictionary +- add a `#problem-def("NetworkReliability")[...][...]` block near the graph problems +- explain the model carefully: + - the configuration space is surviving/failing edge patterns + - `evaluate()` checks terminal connectivity for one pattern + - `reliability()` performs the exact weighted sum over all patterns + - the decision question compares that sum to `q` +- include the issue example with the exact value `0.968425` and a small graph figure +- cite the literature from the issue/comments (`@garey1979`, `@valiant1979`, `@ball1986`, and the Rosenthal reference already available in the bibliography if present) + +**Step 4: Run the paper and model checks** + +Run: + +```bash +cargo test network_reliability --lib +make paper +``` + +Expected: the paper builds cleanly and the example tests stay green. + +**Step 5: Commit the documentation slice** + +```bash +git add docs/paper/reductions.typ src/unit_tests/models/graph/network_reliability.rs +git commit -m "docs: add NetworkReliability paper entry" +``` + +### Task 4: Full verification before handoff + +**Files:** +- Modify only if verification exposes defects + +**Step 1: Run formatting and regression checks** + +Run: + +```bash +cargo fmt --all +cargo test network_reliability --lib +cargo test example_db --lib +cargo test -p problemreductions-cli network_reliability +make paper +``` + +If any command fails, fix the issue and re-run the affected command before moving on. + +**Step 2: Run a broader workspace confidence check** + +Run: + +```bash +make check +``` + +Expected: formatting, clippy, and workspace tests pass. + +**Step 3: Prepare for issue-to-pr cleanup** + +After implementation succeeds: +- ensure all code changes are committed +- leave `docs/plans/2026-03-21-network-reliability.md` in place for the initial plan PR commit only +- let the outer `issue-to-pr` workflow remove this file in its dedicated cleanup commit + +**Step 4: Final implementation summary inputs** + +Capture for the PR summary comment: +- model files added/changed +- CLI flags introduced (`--failure-probs`, `--threshold`) +- exact example result (`0.968425 > 0.95`) +- explicit design deviation: the standard solver still finds connected configurations, while exact reliability comparison lives in model-local helpers From 186f955534f9d6a7c44d73ddbeb66b5f0de41f8c Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 17:10:30 +0800 Subject: [PATCH 2/6] feat: add NetworkReliability model --- src/lib.rs | 6 +- src/models/graph/mod.rs | 4 + src/models/graph/network_reliability.rs | 265 ++++++++++++++++++ src/models/mod.rs | 6 +- .../models/graph/network_reliability.rs | 102 +++++++ 5 files changed, 377 insertions(+), 6 deletions(-) create mode 100644 src/models/graph/network_reliability.rs create mode 100644 src/unit_tests/models/graph/network_reliability.rs 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_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..548f9157 --- /dev/null +++ b/src/unit_tests/models/graph/network_reliability.rs @@ -0,0 +1,102 @@ +use crate::models::graph::NetworkReliability; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::Problem; + +fn issue_235_example() -> 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], + 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(); + assert!((reliability - 0.968425).abs() < 1e-6); + 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); +} From ce8842d14dbb7ed8d75739960f38857ce44802b9 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 17:13:54 +0800 Subject: [PATCH 3/6] feat: wire NetworkReliability through CLI --- problemreductions-cli/src/cli.rs | 7 ++ problemreductions-cli/src/commands/create.rs | 70 ++++++++++++++++++++ problemreductions-cli/tests/cli_tests.rs | 55 +++++++++++++++ 3 files changed, 132 insertions(+) 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() From f748aba1dab53f94d5417807e437cafb00def725 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 17:34:35 +0800 Subject: [PATCH 4/6] docs: add NetworkReliability paper entry --- docs/paper/reductions.typ | 51 +++++++++++++++++++ docs/paper/references.bib | 22 ++++++++ .../models/graph/network_reliability.rs | 16 ++++++ 3 files changed, 89 insertions(+) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 05ae126f..1aa8b7cb 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 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/src/unit_tests/models/graph/network_reliability.rs b/src/unit_tests/models/graph/network_reliability.rs index 548f9157..167d1804 100644 --- a/src/unit_tests/models/graph/network_reliability.rs +++ b/src/unit_tests/models/graph/network_reliability.rs @@ -59,6 +59,22 @@ fn test_network_reliability_exact_reliability_matches_issue_example() { 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] #[should_panic(expected = "failure_probs length must match num_edges")] fn test_network_reliability_rejects_bad_probability_length() { From 4adc7e7fafbb9bc59867cd26e791b2a3ab663362 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 17:34:40 +0800 Subject: [PATCH 5/6] chore: remove plan file after implementation --- docs/plans/2026-03-21-network-reliability.md | 287 ------------------- 1 file changed, 287 deletions(-) delete mode 100644 docs/plans/2026-03-21-network-reliability.md diff --git a/docs/plans/2026-03-21-network-reliability.md b/docs/plans/2026-03-21-network-reliability.md deleted file mode 100644 index 3133b886..00000000 --- a/docs/plans/2026-03-21-network-reliability.md +++ /dev/null @@ -1,287 +0,0 @@ -# NetworkReliability Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `NetworkReliability` model, exact reliability helpers, CLI/example-db integration, tests, and paper documentation for issue #235. - -**Architecture:** Implement `NetworkReliability` as a `SatisfactionProblem` whose binary configuration marks which graph edges survive. `evaluate()` should answer the per-configuration predicate "are all terminals connected in the surviving subgraph?", while separate model-local helpers perform the exact probability summation required for the real decision question `R(G, T, p) >= q`. Keep the model non-variant (`SimpleGraph`, `Vec`, `f64`) and wire it through the existing registry, CLI, example-db, and paper patterns used by other graph models with terminal sets. - -**Tech Stack:** Rust workspace (`problemreductions`, `problemreductions-cli`), `inventory`/`declare_variants!`, `DimsIterator` for exact enumeration, Typst paper docs, cargo/make verification. - ---- - -## Batch 1: Model + Integration (add-model Steps 1-5.5) - -### Task 1: Lock down the model contract with failing tests - -**Files:** -- Create: `src/unit_tests/models/graph/network_reliability.rs` -- Reference: `src/models/graph/steiner_tree_in_graphs.rs` -- Reference: `src/unit_tests/models/graph/steiner_tree_in_graphs.rs` - -**Step 1: Write the failing tests** - -Add focused tests that describe the intended behavior before any production code exists: - -```rust -#[test] -fn test_network_reliability_creation_and_getters() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let problem = NetworkReliability::new(graph, vec![0, 3], vec![0.1, 0.2, 0.3], 0.8); - assert_eq!(problem.num_vertices(), 4); - assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.num_terminals(), 2); - assert_eq!(problem.dims(), vec![2; 3]); - assert_eq!(problem.threshold(), 0.8); -} - -#[test] -fn test_network_reliability_evaluate_checks_terminal_connectivity() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]); - let problem = NetworkReliability::new(graph, vec![0, 2], vec![0.1; 4], 0.5); - assert!(problem.evaluate(&[1, 1, 0, 0])); - assert!(!problem.evaluate(&[1, 0, 0, 0])); -} - -#[test] -fn test_network_reliability_exact_probability_matches_issue_example() { - let problem = issue_235_example(); - let reliability = problem.reliability(); - assert!((reliability - 0.968425).abs() < 1e-9); - assert!(problem.meets_threshold()); -} -``` - -Also cover invalid constructor inputs: -- `failure_probs.len() != num_edges` -- duplicate or out-of-bounds terminals -- fewer than 2 terminals -- failure probability outside `[0.0, 1.0]` -- threshold outside `[0.0, 1.0]` - -**Step 2: Run the test target and verify RED** - -Run: - -```bash -cargo test network_reliability --lib -``` - -Expected: compile failure because `NetworkReliability` and its test module do not exist yet. - -**Step 3: Write the minimal implementation to satisfy the tests** - -Create `src/models/graph/network_reliability.rs` with: -- `ProblemSchemaEntry` describing `graph`, `terminals`, `failure_probs`, and `threshold` -- `#[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetworkReliability` -- constructor validation for terminal distinctness/bounds and probability/threshold ranges -- accessors: `graph()`, `terminals()`, `failure_probs()`, `threshold()` -- size getters: `num_vertices()`, `num_edges()`, `num_terminals()` -- helpers: - - `is_valid_solution(&self, config: &[usize]) -> bool` - - `configuration_probability(&self, config: &[usize]) -> f64` - - `reliability(&self) -> f64` - - `meets_threshold(&self) -> bool` -- internal connectivity check for terminals in the surviving-edge subgraph -- `Problem` + `SatisfactionProblem` -- `crate::declare_variants! { default sat NetworkReliability => "2^num_edges * num_vertices" }` -- `canonical_model_example_specs()` for the issue example, with a representative connected configuration for example-db even though the issue’s real expected outcome is the aggregate reliability -- `#[cfg(test)]` link to the new test file - -Register the new model in: -- `src/models/graph/mod.rs` -- `src/models/mod.rs` -- `src/lib.rs` - -**Step 4: Run the tests and verify GREEN** - -Run: - -```bash -cargo test network_reliability --lib -``` - -Expected: the new model/unit tests pass. - -**Step 5: Commit the model slice** - -```bash -git add src/models/graph/network_reliability.rs src/models/graph/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/models/graph/network_reliability.rs -git commit -m "feat: add NetworkReliability model" -``` - -### Task 2: Add CLI creation and example-db plumbing - -**Files:** -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/tests/cli_tests.rs` -- Modify: `src/models/graph/mod.rs` -- Reference: `problemreductions-cli/src/commands/create.rs` - -**Step 1: Write the failing CLI tests** - -Add CLI tests that exercise both direct creation and canonical example creation: - -```rust -#[test] -fn test_create_network_reliability() { - // pred create NetworkReliability --graph ... --terminals 0,5 --failure-probs 0.1,... --threshold 0.95 -} - -#[test] -fn test_create_example_network_reliability() { - // pred create --example NetworkReliability -} -``` - -Assertions should verify: -- JSON `"type"` is `"NetworkReliability"` -- `failure_probs` round-trip as floats -- `threshold` round-trips as `0.95` -- `terminals` round-trip correctly - -**Step 2: Run the CLI tests and verify RED** - -Run: - -```bash -cargo test -p problemreductions-cli network_reliability -``` - -Expected: failure because the CLI has no `NetworkReliability` arm or `--failure-probs` / `--threshold` support yet. - -**Step 3: Implement the CLI path** - -Update `problemreductions-cli/src/cli.rs`: -- add `NetworkReliability --graph, --terminals, --failure-probs, --threshold` to the `after_help` table -- add `failure_probs: Option` and `threshold: Option` to `CreateArgs` -- include the new flags in `all_data_flags_empty()` - -Update `problemreductions-cli/src/commands/create.rs`: -- add example text for `NetworkReliability` -- add a parser helper for `--failure-probs` as a comma-separated `Vec` matching `num_edges` -- add a `NetworkReliability` match arm that parses graph, terminals, failure probabilities, and threshold -- prefer `--threshold` over reusing integer `--bound`, since this problem’s threshold is rational and should remain a `f64` -- keep alias resolution unchanged unless tests prove the registry-driven lookup is insufficient - -Keep the example-db registration consistent by ensuring the new `canonical_model_example_specs()` is included in the graph example chain. - -**Step 4: Run the CLI tests and targeted example-db coverage** - -Run: - -```bash -cargo test -p problemreductions-cli network_reliability -cargo test example_db --lib -``` - -Expected: CLI creation and example-db validation both pass. - -**Step 5: Commit the integration slice** - -```bash -git add problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs problemreductions-cli/tests/cli_tests.rs src/models/graph/mod.rs -git commit -m "feat: wire NetworkReliability through CLI" -``` - -## Batch 2: Paper + Final Verification (add-model Steps 6-7) - -### Task 3: Document the model and lock the paper example - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `src/unit_tests/models/graph/network_reliability.rs` - -**Step 1: Add the paper regression test first** - -Extend `src/unit_tests/models/graph/network_reliability.rs` with a paper-example test that: -- constructs the issue #235 example instance -- checks the representative connected configuration used by example-db/paper -- checks `reliability()` is `0.968425` within tolerance -- checks `meets_threshold()` is `true` - -**Step 2: Run the targeted test and verify RED if the paper/example wiring is still absent** - -Run: - -```bash -cargo test network_reliability_example --lib -``` - -Expected: fail until the final example and paper narrative are aligned. - -**Step 3: Write the paper entry** - -Update `docs/paper/reductions.typ`: -- add `"NetworkReliability": [Network Reliability]` to the display-name dictionary -- add a `#problem-def("NetworkReliability")[...][...]` block near the graph problems -- explain the model carefully: - - the configuration space is surviving/failing edge patterns - - `evaluate()` checks terminal connectivity for one pattern - - `reliability()` performs the exact weighted sum over all patterns - - the decision question compares that sum to `q` -- include the issue example with the exact value `0.968425` and a small graph figure -- cite the literature from the issue/comments (`@garey1979`, `@valiant1979`, `@ball1986`, and the Rosenthal reference already available in the bibliography if present) - -**Step 4: Run the paper and model checks** - -Run: - -```bash -cargo test network_reliability --lib -make paper -``` - -Expected: the paper builds cleanly and the example tests stay green. - -**Step 5: Commit the documentation slice** - -```bash -git add docs/paper/reductions.typ src/unit_tests/models/graph/network_reliability.rs -git commit -m "docs: add NetworkReliability paper entry" -``` - -### Task 4: Full verification before handoff - -**Files:** -- Modify only if verification exposes defects - -**Step 1: Run formatting and regression checks** - -Run: - -```bash -cargo fmt --all -cargo test network_reliability --lib -cargo test example_db --lib -cargo test -p problemreductions-cli network_reliability -make paper -``` - -If any command fails, fix the issue and re-run the affected command before moving on. - -**Step 2: Run a broader workspace confidence check** - -Run: - -```bash -make check -``` - -Expected: formatting, clippy, and workspace tests pass. - -**Step 3: Prepare for issue-to-pr cleanup** - -After implementation succeeds: -- ensure all code changes are committed -- leave `docs/plans/2026-03-21-network-reliability.md` in place for the initial plan PR commit only -- let the outer `issue-to-pr` workflow remove this file in its dedicated cleanup commit - -**Step 4: Final implementation summary inputs** - -Capture for the PR summary comment: -- model files added/changed -- CLI flags introduced (`--failure-probs`, `--threshold`) -- exact example result (`0.968425 > 0.95`) -- explicit design deviation: the standard solver still finds connected configurations, while exact reliability comparison lives in model-local helpers From 027a896c551372a503dbd7777ad4e35d6dbba668 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 17:39:36 +0800 Subject: [PATCH 6/6] fix: address NetworkReliability review feedback --- docs/paper/reductions.typ | 2 +- src/models/graph/network_reliability.rs | 2 +- .../models/graph/network_reliability.rs | 23 +++++++++++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 1aa8b7cb..a2d91c80 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -1640,7 +1640,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], #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 n)$ time. #footnote[No better exact worst-case bound is claimed here for the general graph family implemented in the codebase.] + 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. diff --git a/src/models/graph/network_reliability.rs b/src/models/graph/network_reliability.rs index eb4b327f..599f5b17 100644 --- a/src/models/graph/network_reliability.rs +++ b/src/models/graph/network_reliability.rs @@ -230,7 +230,7 @@ fn terminals_connected_with_surviving_edges( } crate::declare_variants! { - default sat NetworkReliability => "2^num_edges * num_vertices", + default sat NetworkReliability => "2^num_edges * (num_edges + num_vertices)", } #[cfg(feature = "example-db")] diff --git a/src/unit_tests/models/graph/network_reliability.rs b/src/unit_tests/models/graph/network_reliability.rs index 167d1804..d960ae48 100644 --- a/src/unit_tests/models/graph/network_reliability.rs +++ b/src/unit_tests/models/graph/network_reliability.rs @@ -1,8 +1,9 @@ +use crate::config::DimsIterator; use crate::models::graph::NetworkReliability; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; -fn issue_235_example() -> NetworkReliability { +fn issue_235_example_with_threshold(threshold: f64) -> NetworkReliability { NetworkReliability::new( SimpleGraph::new( 6, @@ -19,10 +20,14 @@ fn issue_235_example() -> NetworkReliability { ), vec![0, 5], vec![0.1; 8], - 0.95, + 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(); @@ -55,7 +60,12 @@ 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()); } @@ -75,6 +85,15 @@ fn test_network_reliability_paper_example() { 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() {