Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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$.],
) <fig:network-reliability-example>
]
]
}

#{
let x = load-model-example("MinimumSumMulticenter")
let nv = graph-num-vertices(x.instance)
Expand Down
22 changes: 22 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
7 changes: 7 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ TIP: Run `pred create <PROBLEM>` (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
Expand Down Expand Up @@ -345,6 +346,9 @@ pub struct CreateArgs {
/// Edge weights (e.g., 2,3,1) [default: all 1s]
#[arg(long)]
pub edge_weights: Option<String>,
/// Edge failure probabilities for NetworkReliability (e.g., 0.1,0.2,0.1)
#[arg(long)]
pub failure_probs: Option<String>,
/// Edge lengths (e.g., 2,3,1) [default: all 1s]
#[arg(long)]
pub edge_lengths: Option<String>,
Expand Down Expand Up @@ -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<i64>,
/// Reliability threshold q for NetworkReliability
#[arg(long)]
pub threshold: Option<f64>,
/// Upper bound on total path length
#[arg(long)]
pub length_bound: Option<i32>,
Expand Down
70 changes: 70 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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"
}
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -3930,6 +3957,47 @@ fn parse_edge_weights(args: &CreateArgs, num_edges: usize) -> Result<Vec<i32>> {
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<Vec<f64>> {
let raw = args
.failure_probs
.as_deref()
.ok_or_else(|| anyhow::anyhow!("NetworkReliability requires --failure-probs\n\n{usage}"))?;
let failure_probs: Vec<f64> = raw
.split(',')
.map(|entry| {
let trimmed = entry.trim();
trimmed
.parse::<f64>()
.with_context(|| format!("invalid failure probability `{trimmed}`\n\n{usage}"))
})
.collect::<Result<Vec<_>>>()?;
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<f64> {
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,
Expand Down Expand Up @@ -5590,6 +5658,7 @@ mod tests {
graph: None,
weights: None,
edge_weights: None,
failure_probs: None,
edge_lengths: None,
capacities: None,
source: None,
Expand Down Expand Up @@ -5641,6 +5710,7 @@ mod tests {
tree: None,
required_edges: None,
bound: None,
threshold: None,
length_bound: None,
weight_bound: None,
pattern: None,
Expand Down
55 changes: 55 additions & 0 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
4 changes: 4 additions & 0 deletions src/models/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -147,6 +150,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(maximal_is::canonical_model_example_specs());
specs.extend(minimum_cut_into_bounded_sets::canonical_model_example_specs());
specs.extend(multiple_copy_file_allocation::canonical_model_example_specs());
specs.extend(network_reliability::canonical_model_example_specs());
specs.extend(minimum_feedback_vertex_set::canonical_model_example_specs());
specs.extend(min_max_multicenter::canonical_model_example_specs());
specs.extend(minimum_multiway_cut::canonical_model_example_specs());
Expand Down
Loading
Loading