diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 7ab3b569..260c0384 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -30,6 +30,7 @@ "MinimumVertexCover": [Minimum Vertex Cover], "MaxCut": [Max-Cut], "GraphPartitioning": [Graph Partitioning], + "BiconnectivityAugmentation": [Biconnectivity Augmentation], "KColoring": [$k$-Coloring], "MinimumDominatingSet": [Minimum Dominating Set], "MaximumMatching": [Maximum Matching], @@ -434,6 +435,13 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co caption: [Graph with $n = 6$ vertices partitioned into $A = {v_0, v_1, v_2}$ (blue) and $B = {v_3, v_4, v_5}$ (red). The 3 crossing edges $(v_1, v_3)$, $(v_2, v_3)$, $(v_2, v_4)$ are shown in bold red; internal edges are gray.], ) ] +#problem-def("BiconnectivityAugmentation")[ + Given an undirected graph $G = (V, E)$, a set $F$ of candidate edges on $V$ with $F inter E = emptyset$, weights $w: F -> RR$, and a budget $B in RR$, find $F' subset.eq F$ such that $sum_(e in F') w(e) <= B$ and the augmented graph $G' = (V, E union F')$ is biconnected, meaning $G'$ is connected and deleting any single vertex leaves it connected. +][ +Biconnectivity augmentation is a classical network-design problem: add backup links so the graph survives any single vertex failure. The weighted candidate-edge formulation modeled here captures communication, transportation, and infrastructure planning settings where only a prescribed set of new links is feasible and each carries a cost. In this library, the exact baseline is brute-force enumeration over the $m = |F|$ candidate edges, yielding $O^*(2^m)$ time and matching the exported complexity metadata for the model. + +*Example.* Consider the path graph $v_0 - v_1 - v_2 - v_3 - v_4 - v_5$ with candidate edges $(v_0, v_2)$, $(v_0, v_3)$, $(v_0, v_4)$, $(v_1, v_3)$, $(v_1, v_4)$, $(v_1, v_5)$, $(v_2, v_4)$, $(v_2, v_5)$, $(v_3, v_5)$ carrying weights $(1, 2, 3, 1, 2, 3, 1, 2, 1)$ and budget $B = 4$. Selecting $F' = {(v_0, v_2), (v_1, v_3), (v_2, v_4), (v_3, v_5)}$ uses total weight $1 + 1 + 1 + 1 = 4$ and eliminates every articulation point: after deleting any single vertex, the remaining graph is still connected. Reducing the budget to $B = 3$ makes the instance infeasible, because one of the path endpoints remains attached through a single articulation vertex. +] #problem-def("KColoring")[ Given $G = (V, E)$ and $k$ colors, find $c: V -> {1, ..., k}$ minimizing $|{(u, v) in E : c(u) = c(v)}|$. ][ diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index ef694f9d..febac8f6 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -41,6 +41,27 @@ } ] }, + { + "name": "BiconnectivityAugmentation", + "description": "Add weighted potential edges to make a graph biconnected within budget", + "fields": [ + { + "name": "graph", + "type_name": "G", + "description": "The underlying graph G=(V,E)" + }, + { + "name": "potential_weights", + "type_name": "Vec<(usize, usize, W)>", + "description": "Potential edges with augmentation weights" + }, + { + "name": "budget", + "type_name": "W::Sum", + "description": "Maximum total augmentation weight B" + } + ] + }, { "name": "BinPacking", "description": "Assign items to bins minimizing number of bins used, subject to capacity", @@ -195,6 +216,17 @@ } ] }, + { + "name": "LongestCommonSubsequence", + "description": "Find the longest string that is a subsequence of every input string", + "fields": [ + { + "name": "strings", + "type_name": "Vec>", + "description": "The input strings" + } + ] + }, { "name": "MaxCut", "description": "Find maximum weight cut in a graph", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 59ea473f..c03e8354 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -14,6 +14,16 @@ "doc_path": "models/graph/struct.BicliqueCover.html", "complexity": "2^num_vertices" }, + { + "name": "BiconnectivityAugmentation", + "variant": { + "graph": "SimpleGraph", + "weight": "i32" + }, + "category": "graph", + "doc_path": "models/graph/struct.BiconnectivityAugmentation.html", + "complexity": "2^num_potential_edges" + }, { "name": "BinPacking", "variant": { @@ -175,6 +185,13 @@ "doc_path": "models/misc/struct.Knapsack.html", "complexity": "2^(num_items / 2)" }, + { + "name": "LongestCommonSubsequence", + "variant": {}, + "category": "misc", + "doc_path": "models/misc/struct.LongestCommonSubsequence.html", + "complexity": "2^min_string_length" + }, { "name": "MaxCut", "variant": { @@ -413,8 +430,8 @@ ], "edges": [ { - "source": 3, - "target": 9, + "source": 4, + "target": 10, "overhead": [ { "field": "num_vars", @@ -428,8 +445,8 @@ "doc_path": "rules/binpacking_ilp/index.html" }, { - "source": 4, - "target": 9, + "source": 5, + "target": 10, "overhead": [ { "field": "num_vars", @@ -443,8 +460,8 @@ "doc_path": "rules/circuit_ilp/index.html" }, { - "source": 4, - "target": 42, + "source": 5, + "target": 44, "overhead": [ { "field": "num_spins", @@ -458,8 +475,8 @@ "doc_path": "rules/circuit_spinglass/index.html" }, { - "source": 7, - "target": 4, + "source": 8, + "target": 5, "overhead": [ { "field": "num_variables", @@ -473,8 +490,8 @@ "doc_path": "rules/factoring_circuit/index.html" }, { - "source": 7, - "target": 10, + "source": 8, + "target": 11, "overhead": [ { "field": "num_vars", @@ -488,8 +505,8 @@ "doc_path": "rules/factoring_ilp/index.html" }, { - "source": 9, - "target": 10, + "source": 10, + "target": 11, "overhead": [ { "field": "num_vars", @@ -503,8 +520,8 @@ "doc_path": "rules/ilp_bool_ilp_i32/index.html" }, { - "source": 9, - "target": 39, + "source": 10, + "target": 41, "overhead": [ { "field": "num_vars", @@ -514,8 +531,8 @@ "doc_path": "rules/ilp_qubo/index.html" }, { - "source": 12, - "target": 15, + "source": 13, + "target": 16, "overhead": [ { "field": "num_vertices", @@ -529,8 +546,8 @@ "doc_path": "rules/kcoloring_casts/index.html" }, { - "source": 15, - "target": 9, + "source": 16, + "target": 10, "overhead": [ { "field": "num_vars", @@ -544,8 +561,8 @@ "doc_path": "rules/coloring_ilp/index.html" }, { - "source": 15, - "target": 39, + "source": 16, + "target": 41, "overhead": [ { "field": "num_vars", @@ -555,8 +572,8 @@ "doc_path": "rules/coloring_qubo/index.html" }, { - "source": 16, - "target": 18, + "source": 17, + "target": 19, "overhead": [ { "field": "num_vars", @@ -570,8 +587,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 16, - "target": 39, + "source": 17, + "target": 41, "overhead": [ { "field": "num_vars", @@ -581,8 +598,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 17, - "target": 18, + "source": 18, + "target": 19, "overhead": [ { "field": "num_vars", @@ -596,8 +613,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 17, - "target": 39, + "source": 18, + "target": 41, "overhead": [ { "field": "num_vars", @@ -607,8 +624,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 17, - "target": 43, + "source": 18, + "target": 45, "overhead": [ { "field": "num_elements", @@ -618,8 +635,8 @@ "doc_path": "rules/ksatisfiability_subsetsum/index.html" }, { - "source": 18, - "target": 40, + "source": 19, + "target": 42, "overhead": [ { "field": "num_clauses", @@ -637,8 +654,23 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 20, - "target": 42, + "source": 21, + "target": 10, + "overhead": [ + { + "field": "num_vars", + "formula": "num_chars_first * num_chars_second" + }, + { + "field": "num_constraints", + "formula": "num_chars_first + num_chars_second + (num_chars_first * num_chars_second)^2" + } + ], + "doc_path": "rules/longestcommonsubsequence_ilp/index.html" + }, + { + "source": 22, + "target": 44, "overhead": [ { "field": "num_spins", @@ -652,8 +684,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 22, - "target": 9, + "source": 24, + "target": 10, "overhead": [ { "field": "num_vars", @@ -667,8 +699,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 22, - "target": 26, + "source": 24, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -682,8 +714,8 @@ "doc_path": "rules/maximumclique_maximumindependentset/index.html" }, { - "source": 23, - "target": 24, + "source": 25, + "target": 26, "overhead": [ { "field": "num_vertices", @@ -697,8 +729,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 23, - "target": 28, + "source": 25, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -712,8 +744,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 24, - "target": 29, + "source": 26, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -727,8 +759,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 25, - "target": 23, + "source": 27, + "target": 25, "overhead": [ { "field": "num_vertices", @@ -742,8 +774,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 25, - "target": 26, + "source": 27, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -757,8 +789,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 25, - "target": 27, + "source": 27, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -772,8 +804,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 25, - "target": 31, + "source": 27, + "target": 33, "overhead": [ { "field": "num_sets", @@ -787,8 +819,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 26, - "target": 22, + "source": 28, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -802,8 +834,8 @@ "doc_path": "rules/maximumindependentset_maximumclique/index.html" }, { - "source": 26, - "target": 33, + "source": 28, + "target": 35, "overhead": [ { "field": "num_sets", @@ -817,8 +849,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 26, - "target": 37, + "source": 28, + "target": 39, "overhead": [ { "field": "num_vertices", @@ -832,8 +864,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 27, - "target": 29, + "source": 29, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -847,8 +879,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 28, - "target": 25, + "source": 30, + "target": 27, "overhead": [ { "field": "num_vertices", @@ -862,8 +894,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 28, - "target": 29, + "source": 30, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -877,8 +909,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 29, - "target": 26, + "source": 31, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -892,8 +924,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 9, + "source": 32, + "target": 10, "overhead": [ { "field": "num_vars", @@ -907,8 +939,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 30, - "target": 33, + "source": 32, + "target": 35, "overhead": [ { "field": "num_sets", @@ -922,8 +954,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 31, - "target": 25, + "source": 33, + "target": 27, "overhead": [ { "field": "num_vertices", @@ -937,8 +969,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 33, + "source": 33, + "target": 35, "overhead": [ { "field": "num_sets", @@ -952,8 +984,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 32, - "target": 39, + "source": 34, + "target": 41, "overhead": [ { "field": "num_vars", @@ -963,8 +995,8 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 33, - "target": 9, + "source": 35, + "target": 10, "overhead": [ { "field": "num_vars", @@ -978,8 +1010,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 33, - "target": 26, + "source": 35, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -993,8 +1025,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 33, - "target": 32, + "source": 35, + "target": 34, "overhead": [ { "field": "num_sets", @@ -1008,8 +1040,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 34, - "target": 9, + "source": 36, + "target": 10, "overhead": [ { "field": "num_vars", @@ -1023,8 +1055,8 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 36, - "target": 9, + "source": 38, + "target": 10, "overhead": [ { "field": "num_vars", @@ -1038,8 +1070,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 37, - "target": 26, + "source": 39, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -1053,8 +1085,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 37, - "target": 36, + "source": 39, + "target": 38, "overhead": [ { "field": "num_sets", @@ -1068,8 +1100,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 39, - "target": 9, + "source": 41, + "target": 10, "overhead": [ { "field": "num_vars", @@ -1083,8 +1115,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 39, - "target": 41, + "source": 41, + "target": 43, "overhead": [ { "field": "num_spins", @@ -1094,8 +1126,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 40, - "target": 4, + "source": 42, + "target": 5, "overhead": [ { "field": "num_variables", @@ -1109,8 +1141,8 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 40, - "target": 12, + "source": 42, + "target": 13, "overhead": [ { "field": "num_vertices", @@ -1124,8 +1156,8 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 40, - "target": 17, + "source": 42, + "target": 18, "overhead": [ { "field": "num_clauses", @@ -1139,8 +1171,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 40, - "target": 25, + "source": 42, + "target": 27, "overhead": [ { "field": "num_vertices", @@ -1154,8 +1186,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 40, - "target": 34, + "source": 42, + "target": 36, "overhead": [ { "field": "num_vertices", @@ -1169,8 +1201,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 41, - "target": 39, + "source": 43, + "target": 41, "overhead": [ { "field": "num_vars", @@ -1180,8 +1212,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 42, - "target": 20, + "source": 44, + "target": 22, "overhead": [ { "field": "num_vertices", @@ -1195,8 +1227,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 42, - "target": 41, + "source": 44, + "target": 43, "overhead": [ { "field": "num_spins", @@ -1210,8 +1242,8 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 44, - "target": 9, + "source": 46, + "target": 10, "overhead": [ { "field": "num_vars", @@ -1223,6 +1255,17 @@ } ], "doc_path": "rules/travelingsalesman_ilp/index.html" + }, + { + "source": 46, + "target": 41, + "overhead": [ + { + "field": "num_vars", + "formula": "num_vertices^2" + } + ], + "doc_path": "rules/travelingsalesman_qubo/index.html" } ] } \ No newline at end of file diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 6b4cbb5b..0cabceeb 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -216,6 +216,7 @@ Flags by problem type: MaximumSetPacking --sets [--weights] MinimumSetCovering --universe, --sets [--weights] BicliqueCover --left, --right, --biedges, --k + BiconnectivityAugmentation --graph, --potential-edges, --budget [--num-vertices] BMF --matrix (0/1), --rank CVP --basis, --target-vec [--bounds] LCS --strings @@ -236,6 +237,7 @@ Examples: pred create MIS/KingsSubgraph --positions \"0,0;1,0;1,1;0,1\" pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5 pred create MIS --random --num-vertices 10 --edge-prob 0.3 + pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5 pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1")] pub struct CreateArgs { /// Problem type (e.g., MIS, QUBO, SAT) @@ -337,6 +339,12 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Weighted potential augmentation edges (e.g., 0-2:3,1-3:5) + #[arg(long)] + pub potential_edges: Option, + /// Total budget for selected potential edges + #[arg(long)] + pub budget: Option, } #[derive(clap::Args)] @@ -454,3 +462,46 @@ pub fn print_subcommand_help_hint(error_msg: &str) { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_parses_biconnectivity_augmentation_flags() { + let cli = Cli::parse_from([ + "pred", + "create", + "BiconnectivityAugmentation", + "--graph", + "0-1,1-2", + "--potential-edges", + "0-2:3,1-3:5", + "--budget", + "7", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + assert_eq!(args.problem, "BiconnectivityAugmentation"); + assert_eq!(args.graph.as_deref(), Some("0-1,1-2")); + assert_eq!(args.potential_edges.as_deref(), Some("0-2:3,1-3:5")); + assert_eq!(args.budget.as_deref(), Some("7")); + } + + #[test] + fn test_create_help_mentions_biconnectivity_augmentation_flags() { + let cmd = Cli::command(); + let create = cmd.find_subcommand("create").expect("create subcommand"); + let help = create + .get_after_help() + .expect("create after_help") + .to_string(); + + assert!(help.contains("BiconnectivityAugmentation")); + assert!(help.contains("--potential-edges")); + assert!(help.contains("--budget")); + } +} diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 2b4cb04b..c19bbf40 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -7,6 +7,7 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::GraphPartitioning; use problemreductions::models::misc::{BinPacking, LongestCommonSubsequence, PaintShop, SubsetSum}; +use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ @@ -14,7 +15,7 @@ use problemreductions::topology::{ UnitDiskGraph, }; use serde::Serialize; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; /// Check if all data flags are None (no problem-specific input provided). fn all_data_flags_empty(args: &CreateArgs) -> bool { @@ -49,6 +50,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.bounds.is_none() && args.strings.is_none() && args.arcs.is_none() + && args.potential_edges.is_none() + && args.budget.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -59,9 +62,12 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { _ => "edge list: 0-1,1-2,2-3", }, "Vec" => "comma-separated: 1,2,3", + "Vec<(usize, usize, W)>" | "Vec<(usize,usize,W)>" => { + "comma-separated weighted edges: 0-2:3,1-3:5" + } "Vec" => "semicolon-separated clauses: \"1,2;-1,3\"", "Vec>" => "semicolon-separated rows: \"1,0.5;0.5,2\"", - "usize" => "integer", + "usize" | "W::Sum" => "integer", "u64" => "integer", "i64" => "integer", "Vec" => "comma-separated integers: 3,7,1,8", @@ -84,6 +90,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { "--graph 0-1,1-2,2-3 --edge-weights 1,1,1" } + "BiconnectivityAugmentation" => { + "--graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5" + } "Satisfiability" => "--num-vars 3 --clauses \"1,2;-1,3\"", "KSatisfiability" => "--num-vars 3 --clauses \"1,2,3;-1,2,-3\" --k 3", "QUBO" => "--matrix \"1,0.5;0.5,2\"", @@ -116,12 +125,12 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> { } } else { let hint = type_format_hint(&field.type_name, graph_type); - eprintln!( - " --{:<16} {} ({})", - field.name.replace('_', "-"), - field.description, - hint - ); + let flag_name = if field.name == "potential_weights" { + "potential-edges".to_string() + } else { + field.name.replace('_', "-") + }; + eprintln!(" --{:<16} {} ({})", flag_name, field.description, hint); } } } else { @@ -213,6 +222,26 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // Biconnectivity augmentation + "BiconnectivityAugmentation" => { + let (graph, _) = parse_graph(args).map_err(|e| { + anyhow::anyhow!( + "{e}\n\nUsage: pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5" + ) + })?; + let potential_edges = parse_potential_edges(args)?; + validate_potential_edges(&graph, &potential_edges)?; + let budget = parse_budget(args)?; + ( + ser(BiconnectivityAugmentation::new( + graph, + potential_edges, + budget, + ))?, + resolved_variant.clone(), + ) + } + // Graph problems with edge weights "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { let (graph, _) = parse_graph(args).map_err(|e| { @@ -653,7 +682,8 @@ fn variant_map(pairs: &[(&str, &str)]) -> BTreeMap { util::variant_map(pairs) } -/// Parse `--graph` into a SimpleGraph, inferring num_vertices from max index. +/// Parse `--graph` into a SimpleGraph, optionally preserving isolated vertices +/// via `--num-vertices`. fn parse_graph(args: &CreateArgs) -> Result<(SimpleGraph, usize)> { let edges_str = args .graph @@ -661,10 +691,12 @@ fn parse_graph(args: &CreateArgs) -> Result<(SimpleGraph, usize)> { .ok_or_else(|| anyhow::anyhow!("This problem requires --graph (e.g., 0-1,1-2,2-3)"))?; if edges_str.trim().is_empty() { - bail!( - "Empty graph string. To create a graph with isolated vertices, use:\n \ - pred create --random --num-vertices N --edge-prob 0.0" - ); + let num_vertices = args.num_vertices.ok_or_else(|| { + anyhow::anyhow!( + "Empty graph string. To create a graph with isolated vertices, pass --num-vertices N as well." + ) + })?; + return Ok((SimpleGraph::empty(num_vertices), num_vertices)); } let edges: Vec<(usize, usize)> = edges_str @@ -687,12 +719,23 @@ fn parse_graph(args: &CreateArgs) -> Result<(SimpleGraph, usize)> { }) .collect::>>()?; - let num_vertices = edges + let inferred_num_vertices = edges .iter() .flat_map(|(u, v)| [*u, *v]) .max() .map(|m| m + 1) .unwrap_or(0); + let num_vertices = match args.num_vertices { + Some(explicit) if explicit < inferred_num_vertices => { + bail!( + "--num-vertices {} is too small for the provided graph; need at least {}", + explicit, + inferred_num_vertices + ); + } + Some(explicit) => explicit, + None => inferred_num_vertices, + }; Ok((SimpleGraph::new(num_vertices, edges), num_vertices)) } @@ -933,6 +976,65 @@ fn parse_matrix(args: &CreateArgs) -> Result>> { .collect() } +fn parse_potential_edges(args: &CreateArgs) -> Result> { + let edges_str = args.potential_edges.as_deref().ok_or_else(|| { + anyhow::anyhow!("BiconnectivityAugmentation requires --potential-edges (e.g., 0-2:3,1-3:5)") + })?; + + edges_str + .split(',') + .map(|entry| { + let entry = entry.trim(); + let (edge_part, weight_part) = entry.split_once(':').ok_or_else(|| { + anyhow::anyhow!("Invalid potential edge '{entry}': expected u-v:w") + })?; + let (u_str, v_str) = edge_part.split_once('-').ok_or_else(|| { + anyhow::anyhow!("Invalid potential edge '{entry}': expected u-v:w") + })?; + let u = u_str.trim().parse::()?; + let v = v_str.trim().parse::()?; + if u == v { + bail!("Self-loop detected in potential edge {u}-{v}"); + } + let weight = weight_part.trim().parse::()?; + Ok((u, v, weight)) + }) + .collect() +} + +fn validate_potential_edges( + graph: &SimpleGraph, + potential_edges: &[(usize, usize, i32)], +) -> Result<()> { + let num_vertices = graph.num_vertices(); + let mut seen_potential_edges = BTreeSet::new(); + for &(u, v, _) in potential_edges { + if u >= num_vertices || v >= num_vertices { + bail!( + "Potential edge {u}-{v} references a vertex outside the graph (num_vertices = {num_vertices})" + ); + } + let edge = if u <= v { (u, v) } else { (v, u) }; + if graph.has_edge(edge.0, edge.1) { + bail!("Potential edge {}-{} already exists in the graph", edge.0, edge.1); + } + if !seen_potential_edges.insert(edge) { + bail!("Duplicate potential edge {}-{} is not allowed", edge.0, edge.1); + } + } + Ok(()) +} + +fn parse_budget(args: &CreateArgs) -> Result { + let budget = args + .budget + .as_deref() + .ok_or_else(|| anyhow::anyhow!("BiconnectivityAugmentation requires --budget (e.g., 5)"))?; + budget + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid budget '{budget}': {e}")) +} + /// Handle `pred create --random ...` fn create_random( args: &CreateArgs, @@ -1091,3 +1193,181 @@ fn create_random( } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + fn empty_args() -> CreateArgs { + CreateArgs { + problem: "BiconnectivityAugmentation".to_string(), + graph: None, + weights: None, + edge_weights: None, + couplings: None, + fields: None, + clauses: None, + num_vars: None, + matrix: None, + k: None, + random: false, + num_vertices: None, + edge_prob: None, + seed: None, + target: None, + m: None, + n: None, + positions: None, + radius: None, + sizes: None, + capacity: None, + sequence: None, + sets: None, + universe: None, + biedges: None, + left: None, + right: None, + rank: None, + basis: None, + target_vec: None, + bounds: None, + strings: None, + arcs: None, + potential_edges: None, + budget: None, + } + } + + #[test] + fn test_all_data_flags_empty_treats_potential_edges_as_input() { + let mut args = empty_args(); + args.potential_edges = Some("0-2:3,1-3:5".to_string()); + assert!(!all_data_flags_empty(&args)); + } + + #[test] + fn test_all_data_flags_empty_treats_budget_as_input() { + let mut args = empty_args(); + args.budget = Some("7".to_string()); + assert!(!all_data_flags_empty(&args)); + } + + #[test] + fn test_parse_potential_edges() { + let mut args = empty_args(); + args.potential_edges = Some("0-2:3,1-3:5".to_string()); + + let potential_edges = parse_potential_edges(&args).unwrap(); + + assert_eq!(potential_edges, vec![(0, 2, 3), (1, 3, 5)]); + } + + #[test] + fn test_parse_potential_edges_rejects_missing_weight() { + let mut args = empty_args(); + args.potential_edges = Some("0-2,1-3:5".to_string()); + + let err = parse_potential_edges(&args).unwrap_err().to_string(); + + assert!(err.contains("u-v:w")); + } + + #[test] + fn test_parse_budget() { + let mut args = empty_args(); + args.budget = Some("7".to_string()); + + assert_eq!(parse_budget(&args).unwrap(), 7); + } + + #[test] + fn test_parse_graph_respects_explicit_num_vertices() { + let mut args = empty_args(); + args.graph = Some("0-1".to_string()); + args.num_vertices = Some(3); + + let (graph, num_vertices) = parse_graph(&args).unwrap(); + + assert_eq!(num_vertices, 3); + assert_eq!(graph.num_vertices(), 3); + assert_eq!(graph.edges(), vec![(0, 1)]); + } + + #[test] + fn test_validate_potential_edges_rejects_existing_graph_edge() { + let err = validate_potential_edges(&SimpleGraph::path(3), &[(0, 1, 5)]) + .unwrap_err() + .to_string(); + + assert!(err.contains("already exists in the graph")); + } + + #[test] + fn test_validate_potential_edges_rejects_duplicate_edges() { + let err = validate_potential_edges(&SimpleGraph::path(4), &[(0, 3, 1), (3, 0, 2)]) + .unwrap_err() + .to_string(); + + assert!(err.contains("Duplicate potential edge")); + } + + #[test] + fn test_create_biconnectivity_augmentation_json() { + let mut args = empty_args(); + args.graph = Some("0-1,1-2,2-3".to_string()); + args.potential_edges = Some("0-2:3,0-3:4,1-3:2".to_string()); + args.budget = Some("5".to_string()); + + let output_path = std::env::temp_dir().join("pred_test_create_biconnectivity.json"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let content = std::fs::read_to_string(&output_path).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "BiconnectivityAugmentation"); + assert_eq!(json["data"]["budget"], 5); + assert_eq!( + json["data"]["potential_weights"][0], + serde_json::json!([0, 2, 3]) + ); + + std::fs::remove_file(output_path).ok(); + } + + #[test] + fn test_create_biconnectivity_augmentation_json_with_isolated_vertices() { + let mut args = empty_args(); + args.graph = Some("0-1".to_string()); + args.num_vertices = Some(3); + args.potential_edges = Some("1-2:1".to_string()); + args.budget = Some("1".to_string()); + + let output_path = + std::env::temp_dir().join("pred_test_create_biconnectivity_isolated.json"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let content = std::fs::read_to_string(&output_path).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + let problem: BiconnectivityAugmentation = + serde_json::from_value(json["data"].clone()).unwrap(); + + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.potential_weights(), &[(1, 2, 1)]); + assert_eq!(problem.budget(), &1); + + std::fs::remove_file(output_path).ok(); + } +} diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index e162efc2..367e7b1e 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,7 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; use problemreductions::models::misc::{BinPacking, Knapsack, LongestCommonSubsequence, SubsetSum}; +use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -211,6 +212,9 @@ pub fn load_problem( "MaximumMatching" => deser_opt::>(data), "MinimumDominatingSet" => deser_opt::>(data), "GraphPartitioning" => deser_opt::>(data), + "BiconnectivityAugmentation" => { + deser_sat::>(data) + } "MaxCut" => deser_opt::>(data), "MaximalIS" => deser_opt::>(data), "TravelingSalesman" => deser_opt::>(data), @@ -272,6 +276,9 @@ pub fn serialize_any_problem( "MaximumMatching" => try_ser::>(any), "MinimumDominatingSet" => try_ser::>(any), "GraphPartitioning" => try_ser::>(any), + "BiconnectivityAugmentation" => { + try_ser::>(any) + } "MaxCut" => try_ser::>(any), "MaximalIS" => try_ser::>(any), "TravelingSalesman" => try_ser::>(any), @@ -376,3 +383,61 @@ fn solve_ilp(any: &dyn Any) -> Result { let evaluation = format!("{:?}", problem.evaluate(&config)); Ok(SolveResult { config, evaluation }) } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn biconnectivity_variant() -> BTreeMap { + BTreeMap::from([ + ("graph".to_string(), "SimpleGraph".to_string()), + ("weight".to_string(), "i32".to_string()), + ]) + } + + #[test] + fn test_load_problem_biconnectivity_augmentation() { + let data = json!({ + "graph": { + "inner": { + "nodes": [null, null, null, null], + "node_holes": [], + "edge_property": "undirected", + "edges": [[0, 1, null], [1, 2, null], [2, 3, null]] + } + }, + "potential_weights": [[0, 2, 3], [0, 3, 4], [1, 3, 2]], + "budget": 5 + }); + + let problem = load_problem( + "biconnectivityaugmentation", + &biconnectivity_variant(), + data, + ) + .expect("load problem"); + + assert_eq!(problem.problem_name(), "BiconnectivityAugmentation"); + assert_eq!(problem.dims_dyn(), vec![2, 2, 2]); + } + + #[test] + fn test_serialize_any_problem_biconnectivity_augmentation() { + let problem = BiconnectivityAugmentation::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![(0, 2, 3), (0, 3, 4), (1, 3, 2)], + 5, + ); + + let data = serialize_any_problem( + "BiconnectivityAugmentation", + &biconnectivity_variant(), + &problem, + ) + .expect("serialize problem"); + + assert_eq!(data["budget"], 5); + assert_eq!(data["potential_weights"][1], json!([0, 3, 4])); + } +} diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index a595f61b..f990d741 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -52,6 +52,7 @@ pub fn resolve_alias(input: &str) -> String { "paintshop" => "PaintShop".to_string(), "bmf" => "BMF".to_string(), "bicliquecover" => "BicliqueCover".to_string(), + "biconnectivityaugmentation" => "BiconnectivityAugmentation".to_string(), "binpacking" => "BinPacking".to_string(), "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), "knapsack" => "Knapsack".to_string(), @@ -260,6 +261,10 @@ mod tests { assert_eq!(resolve_alias("3SAT"), "KSatisfiability"); assert_eq!(resolve_alias("QUBO"), "QUBO"); assert_eq!(resolve_alias("MaxCut"), "MaxCut"); + assert_eq!( + resolve_alias("biconnectivityaugmentation"), + "BiconnectivityAugmentation" + ); // Pass-through for full names assert_eq!( resolve_alias("MaximumIndependentSet"), diff --git a/src/lib.rs b/src/lib.rs index bdcbf5f3..b7206510 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,9 @@ pub mod prelude { // Problem types pub use crate::models::algebraic::{BMF, QUBO}; pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; - pub use crate::models::graph::{BicliqueCover, GraphPartitioning, SpinGlass}; + pub use crate::models::graph::{ + BicliqueCover, BiconnectivityAugmentation, GraphPartitioning, SpinGlass, + }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, TravelingSalesman, diff --git a/src/models/graph/biconnectivity_augmentation.rs b/src/models/graph/biconnectivity_augmentation.rs new file mode 100644 index 00000000..a2f181c2 --- /dev/null +++ b/src/models/graph/biconnectivity_augmentation.rs @@ -0,0 +1,290 @@ +//! Biconnectivity augmentation problem implementation. +//! +//! Given a graph, weighted potential edges, and a budget, determine whether +//! adding some subset of the potential edges can make the graph biconnected +//! without exceeding the budget. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{Problem, SatisfactionProblem}; +use crate::types::WeightElement; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; + +inventory::submit! { + ProblemSchemaEntry { + name: "BiconnectivityAugmentation", + module_path: module_path!(), + description: "Add weighted potential edges to make a graph biconnected within budget", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" }, + FieldInfo { name: "potential_weights", type_name: "Vec<(usize, usize, W)>", description: "Potential edges with augmentation weights" }, + FieldInfo { name: "budget", type_name: "W::Sum", description: "Maximum total augmentation weight B" }, + ], + } +} + +/// The Biconnectivity Augmentation problem. +/// +/// Given a graph `G = (V, E)`, weighted potential edges, and a budget `B`, +/// determine whether there exists a subset of potential edges `E'` such that: +/// - `sum_{e in E'} w(e) <= B` +/// - `(V, E union E')` is biconnected +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(bound( + serialize = "G: serde::Serialize, W: serde::Serialize, W::Sum: serde::Serialize", + deserialize = "G: serde::Deserialize<'de>, W: serde::Deserialize<'de>, W::Sum: serde::Deserialize<'de>" +))] +pub struct BiconnectivityAugmentation +where + W: WeightElement, +{ + /// The underlying graph. + graph: G, + /// Potential augmentation edges with their weights. + potential_weights: Vec<(usize, usize, W)>, + /// Maximum total weight of selected potential edges. + budget: W::Sum, +} + +impl BiconnectivityAugmentation { + /// Create a new biconnectivity augmentation instance. + /// + /// # Panics + /// Panics if any potential edge references a vertex index outside the graph, + /// is a self-loop, duplicates another candidate edge, or already exists in + /// the input graph. + pub fn new(graph: G, potential_weights: Vec<(usize, usize, W)>, budget: W::Sum) -> Self { + let num_vertices = graph.num_vertices(); + let mut seen_potential_edges = BTreeSet::new(); + for &(u, v, _) in &potential_weights { + assert!( + u < num_vertices && v < num_vertices, + "potential edge ({}, {}) references vertex >= num_vertices ({})", + u, + v, + num_vertices + ); + assert!(u != v, "potential edge ({}, {}) is a self-loop", u, v); + let edge = normalize_edge(u, v); + assert!( + !graph.has_edge(edge.0, edge.1), + "potential edge ({}, {}) already exists in the graph", + edge.0, + edge.1 + ); + assert!( + seen_potential_edges.insert(edge), + "potential edge ({}, {}) is duplicated", + edge.0, + edge.1 + ); + } + + Self { + graph, + potential_weights, + budget, + } + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &G { + &self.graph + } + + /// Get the weighted potential edges. + pub fn potential_weights(&self) -> &[(usize, usize, W)] { + &self.potential_weights + } + + /// Get the budget. + pub fn budget(&self) -> &W::Sum { + &self.budget + } + + /// Get the number of vertices in the underlying graph. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of edges in the underlying graph. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + /// Get the number of potential augmentation edges. + pub fn num_potential_edges(&self) -> usize { + self.potential_weights.len() + } + + /// Check if the problem uses a non-unit weight type. + pub fn is_weighted(&self) -> bool { + !W::IS_UNIT + } + + fn augmented_graph(&self, config: &[usize]) -> Option { + if config.len() != self.num_potential_edges() || config.iter().any(|&value| value >= 2) { + return None; + } + + let mut total = W::Sum::zero(); + let mut edges = BTreeSet::new(); + + for (u, v) in self.graph.edges() { + edges.insert(normalize_edge(u, v)); + } + + for (selected, &(u, v, ref weight)) in config.iter().zip(&self.potential_weights) { + if *selected == 1 { + total += weight.to_sum(); + if total > self.budget.clone() { + return None; + } + edges.insert(normalize_edge(u, v)); + } + } + + Some(SimpleGraph::new( + self.num_vertices(), + edges.into_iter().collect(), + )) + } +} + +impl Problem for BiconnectivityAugmentation +where + G: Graph + crate::variant::VariantParam, + W: WeightElement + crate::variant::VariantParam, +{ + const NAME: &'static str = "BiconnectivityAugmentation"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G, W] + } + + fn dims(&self) -> Vec { + vec![2; self.num_potential_edges()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.augmented_graph(config) + .is_some_and(|graph| is_biconnected(&graph)) + } +} + +impl SatisfactionProblem for BiconnectivityAugmentation +where + G: Graph + crate::variant::VariantParam, + W: WeightElement + crate::variant::VariantParam, +{ +} + +fn normalize_edge(u: usize, v: usize) -> (usize, usize) { + if u <= v { + (u, v) + } else { + (v, u) + } +} + +struct DfsState { + visited: Vec, + discovery_time: Vec, + low: Vec, + parent: Vec>, + time: usize, + has_articulation_point: bool, +} + +fn dfs_articulation_points(graph: &G, vertex: usize, state: &mut DfsState) { + if state.has_articulation_point { + return; + } + + state.visited[vertex] = true; + state.time += 1; + state.discovery_time[vertex] = state.time; + state.low[vertex] = state.time; + + let mut child_count = 0; + for neighbor in graph.neighbors(vertex) { + if !state.visited[neighbor] { + child_count += 1; + state.parent[neighbor] = Some(vertex); + dfs_articulation_points(graph, neighbor, state); + state.low[vertex] = state.low[vertex].min(state.low[neighbor]); + + if state.parent[vertex].is_none() && child_count > 1 { + state.has_articulation_point = true; + return; + } + + if state.parent[vertex].is_some() && state.low[neighbor] >= state.discovery_time[vertex] + { + state.has_articulation_point = true; + return; + } + } else if state.parent[vertex] != Some(neighbor) { + state.low[vertex] = state.low[vertex].min(state.discovery_time[neighbor]); + } + } +} + +fn is_biconnected(graph: &G) -> bool { + let num_vertices = graph.num_vertices(); + if num_vertices <= 1 { + return true; + } + + let mut state = DfsState { + visited: vec![false; num_vertices], + discovery_time: vec![0; num_vertices], + low: vec![0; num_vertices], + parent: vec![None; num_vertices], + time: 0, + has_articulation_point: false, + }; + + dfs_articulation_points(graph, 0, &mut state); + + !state.has_articulation_point && state.visited.into_iter().all(|seen| seen) +} + +crate::declare_variants! { + BiconnectivityAugmentation => "2^num_potential_edges", +} + +#[cfg(test)] +pub(crate) struct BiconnectivityAugmentationExample { + pub(crate) graph: SimpleGraph, + pub(crate) potential_edges: Vec<(usize, usize, i32)>, + pub(crate) budget: i32, + pub(crate) satisfying_config: Vec, +} + +#[cfg(test)] +pub(crate) fn canonical_model_example_specs() -> BiconnectivityAugmentationExample { + BiconnectivityAugmentationExample { + graph: SimpleGraph::path(6), + potential_edges: vec![ + (0, 2, 1), + (0, 3, 2), + (0, 4, 3), + (1, 3, 1), + (1, 4, 2), + (1, 5, 3), + (2, 4, 1), + (2, 5, 2), + (3, 5, 1), + ], + budget: 4, + satisfying_config: vec![1, 0, 0, 1, 0, 0, 1, 0, 1], + } +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/biconnectivity_augmentation.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 42f46a15..7a76d597 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -14,8 +14,10 @@ //! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle) //! - [`SpinGlass`]: Ising model Hamiltonian //! - [`BicliqueCover`]: Biclique cover on bipartite graphs +//! - [`BiconnectivityAugmentation`]: Biconnectivity augmentation with weighted potential edges pub(crate) mod biclique_cover; +pub(crate) mod biconnectivity_augmentation; pub(crate) mod graph_partitioning; pub(crate) mod kcoloring; pub(crate) mod max_cut; @@ -30,6 +32,7 @@ pub(crate) mod spin_glass; pub(crate) mod traveling_salesman; pub use biclique_cover::BicliqueCover; +pub use biconnectivity_augmentation::BiconnectivityAugmentation; pub use graph_partitioning::GraphPartitioning; pub use kcoloring::KColoring; pub use max_cut::MaxCut; diff --git a/src/models/mod.rs b/src/models/mod.rs index 6c8ac38a..7435654e 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -12,9 +12,9 @@ pub mod set; pub use algebraic::{ClosestVectorProblem, BMF, ILP, QUBO}; pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use graph::{ - BicliqueCover, GraphPartitioning, KColoring, MaxCut, MaximalIS, MaximumClique, - MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, - MinimumVertexCover, SpinGlass, TravelingSalesman, + BicliqueCover, BiconnectivityAugmentation, GraphPartitioning, KColoring, MaxCut, MaximalIS, + MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, + MinimumFeedbackVertexSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; pub use misc::{BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/graph/biconnectivity_augmentation.rs b/src/unit_tests/models/graph/biconnectivity_augmentation.rs new file mode 100644 index 00000000..81cef937 --- /dev/null +++ b/src/unit_tests/models/graph/biconnectivity_augmentation.rs @@ -0,0 +1,136 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::topology::SimpleGraph; +use crate::traits::Problem; +use crate::types::One; + +#[test] +fn test_biconnectivity_augmentation_creation() { + let graph = SimpleGraph::path(4); + let problem = BiconnectivityAugmentation::new(graph.clone(), vec![(0, 3, 2), (1, 3, 1)], 2); + + assert_eq!(problem.graph(), &graph); + assert_eq!(problem.potential_weights(), &[(0, 3, 2), (1, 3, 1)]); + assert_eq!(problem.budget(), &2); + assert_eq!(problem.num_vertices(), 4); + assert_eq!(problem.num_edges(), 3); + assert_eq!(problem.num_potential_edges(), 2); + assert_eq!(problem.dims(), vec![2, 2]); + assert_eq!(problem.num_variables(), 2); + assert!(problem.is_weighted()); + assert_eq!( + as Problem>::NAME, + "BiconnectivityAugmentation" + ); + assert_eq!( + as Problem>::variant(), + vec![("graph", "SimpleGraph"), ("weight", "i32")] + ); + + let unit_problem = + BiconnectivityAugmentation::<_, One>::new(SimpleGraph::path(3), vec![(0, 2, One)], 1); + assert!(!unit_problem.is_weighted()); +} + +#[test] +#[should_panic(expected = "references vertex >= num_vertices")] +fn test_biconnectivity_augmentation_creation_rejects_invalid_potential_edge() { + BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 4, 1)], 1); +} + +#[test] +#[should_panic(expected = "already exists in the graph")] +fn test_biconnectivity_augmentation_creation_rejects_existing_edge_candidate() { + BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(1, 2, 1)], 1); +} + +#[test] +#[should_panic(expected = "is duplicated")] +fn test_biconnectivity_augmentation_creation_rejects_duplicate_candidate() { + BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 3, 1), (3, 0, 2)], 2); +} + +#[test] +fn test_biconnectivity_augmentation_evaluation() { + let problem = BiconnectivityAugmentation::new( + SimpleGraph::path(4), + vec![(0, 2, 5), (1, 3, 1), (0, 3, 2)], + 2, + ); + + assert!(!problem.evaluate(&[0, 0, 0])); + assert!(!problem.evaluate(&[0, 1, 0])); + assert!(problem.evaluate(&[0, 0, 1])); + assert!(!problem.evaluate(&[0, 1, 1])); + assert!(!problem.evaluate(&[2, 0, 0])); + assert!(!problem.evaluate(&[1, 0])); +} + +#[test] +fn test_biconnectivity_augmentation_serialization() { + let problem = + BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 3, 2), (1, 3, 1)], 2); + + let json = serde_json::to_value(&problem).unwrap(); + let restored: BiconnectivityAugmentation = + serde_json::from_value(json).unwrap(); + + assert_eq!(restored.graph(), problem.graph()); + assert_eq!(restored.potential_weights(), problem.potential_weights()); + assert_eq!(restored.budget(), problem.budget()); +} + +#[test] +fn test_biconnectivity_augmentation_solver() { + let problem = BiconnectivityAugmentation::new( + SimpleGraph::path(4), + vec![(0, 2, 5), (1, 3, 1), (0, 3, 2)], + 2, + ); + let solver = BruteForce::new(); + + let solution = solver + .find_satisfying(&problem) + .expect("expected a satisfying augmentation"); + assert_eq!(solution, vec![0, 0, 1]); + + let all_solutions = solver.find_all_satisfying(&problem); + assert_eq!(all_solutions, vec![vec![0, 0, 1]]); +} + +#[test] +fn test_biconnectivity_augmentation_no_solution() { + let problem = BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 2, 1)], 1); + let solver = BruteForce::new(); + + assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_all_satisfying(&problem).is_empty()); +} + +#[test] +fn test_biconnectivity_augmentation_paper_example() { + let example = canonical_model_example_specs(); + let problem = BiconnectivityAugmentation::new( + example.graph.clone(), + example.potential_edges.clone(), + example.budget, + ); + let solver = BruteForce::new(); + let satisfying_solutions = solver.find_all_satisfying(&problem); + + assert!(problem.evaluate(&example.satisfying_config)); + assert!(satisfying_solutions.contains(&example.satisfying_config)); + + let over_budget_problem = + BiconnectivityAugmentation::new(example.graph, example.potential_edges, 3); + assert!(!over_budget_problem.evaluate(&example.satisfying_config)); + assert!(solver.find_satisfying(&over_budget_problem).is_none()); +} + +#[test] +fn test_is_biconnected() { + assert!(is_biconnected(&SimpleGraph::cycle(4))); + assert!(is_biconnected(&SimpleGraph::complete(3))); + assert!(!is_biconnected(&SimpleGraph::path(4))); + assert!(!is_biconnected(&SimpleGraph::new(4, vec![(0, 1), (2, 3)]))); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 7eef0660..af7aff50 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -53,6 +53,10 @@ fn test_all_problems_implement_trait_correctly() { &MaximumMatching::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32]), "MaximumMatching", ); + check_problem_trait( + &BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 3, 2)], 2), + "BiconnectivityAugmentation", + ); check_problem_trait( &Satisfiability::new(3, vec![CNFClause::new(vec![1])]), "SAT", diff --git a/tests/suites/integration.rs b/tests/suites/integration.rs index 49e43f6f..d7a256a4 100644 --- a/tests/suites/integration.rs +++ b/tests/suites/integration.rs @@ -109,6 +109,19 @@ mod all_problems_solvable { } } + #[test] + fn test_biconnectivity_augmentation_solvable() { + let problem = BiconnectivityAugmentation::new( + SimpleGraph::path(4), + vec![(0, 2, 5), (1, 3, 1), (0, 3, 2)], + 2, + ); + let solver = BruteForce::new(); + let satisfying = solver.find_all_satisfying(&problem); + assert_eq!(satisfying, vec![vec![0, 0, 1]]); + assert!(satisfying.iter().all(|config| problem.evaluate(config))); + } + #[test] fn test_satisfiability_solvable() { let problem = Satisfiability::new(