From d53eb7eba29d93e575ccf631765ed3b9ce320c32 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 05:00:11 +0800 Subject: [PATCH 1/9] Add plan for #233: [Model] StrongConnectivityAugmentation --- ...-03-16-strong-connectivity-augmentation.md | 419 ++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 docs/plans/2026-03-16-strong-connectivity-augmentation.md diff --git a/docs/plans/2026-03-16-strong-connectivity-augmentation.md b/docs/plans/2026-03-16-strong-connectivity-augmentation.md new file mode 100644 index 00000000..00018e6d --- /dev/null +++ b/docs/plans/2026-03-16-strong-connectivity-augmentation.md @@ -0,0 +1,419 @@ +# StrongConnectivityAugmentation Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `StrongConnectivityAugmentation` model from issue #233 as a directed-graph satisfaction problem, with registry/example-db/CLI integration, paper documentation, and verification. + +**Architecture:** Reuse the repo's existing `DirectedGraph` wrapper for the base digraph, store augmentable weighted arcs explicitly, and treat each binary variable as "add this candidate arc". Evaluation should accept exactly those configurations whose selected candidate arcs stay within the bound and make the augmented digraph strongly connected. Keep the paper/example writing in a separate batch after the model, tests, exports, and fixtures are complete. + +**Tech Stack:** Rust workspace, `inventory` schema registry, `declare_variants!`, `DirectedGraph`, `BruteForce`, `pred create`, example-db fixtures, Typst paper, `make` verification targets. + +--- + +## Context And Required Decisions + +- Issue: `#233` (`[Model] StrongConnectivityAugmentation`) +- Associated rule issue exists: `#254` (`[Rule] HAMILTONIAN CIRCUIT to STRONG CONNECTIVITY AUGMENTATION`) +- Preflight guard already passed: `Good` label present +- Use the issue's 6-vertex directed example as the canonical paper/example-db instance +- Deliberate design deviation from the issue comment: use the repo-standard `DirectedGraph` wrapper instead of exposing raw `petgraph::DiGraph` in the public model schema +- CLI shape for this model: + - Base digraph: `--arcs "u>v,..."` + - Candidate weighted augmenting arcs: `--candidate-arcs "u>v:w,..."` + - Budget: reuse `--bound` +- Complexity registration: `default sat StrongConnectivityAugmentation => "2^num_potential_arcs"` + +## Batch Structure + +- **Batch 1:** Tasks 1-4 + - Implement topology primitive, model, registrations, fixtures, CLI/tests +- **Batch 2:** Task 5 + - Paper entry only, after Batch 1 outputs exist +- **Batch 3:** Task 6 + - Final verification, review, and cleanup checks + +### Task 1: Add Directed Strong-Connectivity Primitive + +**Files:** +- Modify: `src/topology/directed_graph.rs` +- Test: `src/unit_tests/topology/directed_graph.rs` + +**Step 1: Write the failing topology tests** + +Add focused tests in `src/unit_tests/topology/directed_graph.rs` for: +- a directed 3-cycle is strongly connected +- a one-way path is not strongly connected +- a single-vertex digraph is strongly connected +- an empty digraph is strongly connected (vacuous case) + +Use test names like: +- `test_is_strongly_connected_cycle` +- `test_is_strongly_connected_path` +- `test_is_strongly_connected_single_vertex` +- `test_is_strongly_connected_empty` + +**Step 2: Run the topology tests to verify RED** + +Run: +```bash +cargo test --features "ilp-highs example-db" test_is_strongly_connected --lib +``` + +Expected: +- FAIL because `DirectedGraph::is_strongly_connected()` does not exist yet + +**Step 3: Write the minimal implementation** + +In `src/topology/directed_graph.rs`: +- add `pub fn is_strongly_connected(&self) -> bool` +- treat `0` and `1` vertices as strongly connected +- implement with two traversals from vertex `0`: + - forward traversal via `successors()` + - reverse traversal via `predecessors()` +- return `true` only if both traversals visit every vertex + +Do not introduce a new graph wrapper or expose `inner`. + +**Step 4: Run the topology tests to verify GREEN** + +Run: +```bash +cargo test --features "ilp-highs example-db" test_is_strongly_connected --lib +``` + +Expected: +- PASS + +**Step 5: Commit** + +```bash +git add src/topology/directed_graph.rs src/unit_tests/topology/directed_graph.rs +git commit -m "feat: add directed strong connectivity check" +``` + +### Task 2: Implement The StrongConnectivityAugmentation Model + +**Files:** +- Create: `src/models/graph/strong_connectivity_augmentation.rs` +- Test: `src/unit_tests/models/graph/strong_connectivity_augmentation.rs` + +**Step 1: Write the failing model tests** + +Create `src/unit_tests/models/graph/strong_connectivity_augmentation.rs` with tests covering: +- creation/dims/getters +- valid issue example (`bound = 1`, exactly the `(5,2,1)` candidate chosen) +- invalid issue example (`bound = 0` or all-zero config) +- wrong-length config returns `false` +- already-strongly-connected base graph accepts the all-zero config +- serialization round-trip +- brute-force solver returns one satisfying configuration for the canonical example +- `variant()` matches `[("weight", "i32")]` +- paper-example parity test using the exact canonical instance + +Use the issue's candidate-arc order verbatim so the witness config is stable: +```text +(3,0,5), (3,1,3), (3,2,4), +(4,0,6), (4,1,2), (4,2,7), +(5,0,4), (5,1,3), (5,2,1), +(0,3,8), (0,4,3), (0,5,2), +(1,3,6), (1,4,4), (1,5,5), +(2,4,3), (2,5,7), (1,0,2) +``` + +Witness config for the YES instance: +```text +[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0] +``` + +**Step 2: Run the new model tests to verify RED** + +Run: +```bash +cargo test --features "ilp-highs example-db" strong_connectivity_augmentation --lib +``` + +Expected: +- FAIL because the model file and type do not exist yet + +**Step 3: Write the minimal model implementation** + +Create `src/models/graph/strong_connectivity_augmentation.rs` with: +- `inventory::submit!` schema entry +- `#[derive(Debug, Clone, Serialize, Deserialize)]` +- public struct: + - `graph: DirectedGraph` + - `candidate_arcs: Vec<(usize, usize, W)>` + - `bound: W` +- constructor validations: + - every candidate arc endpoint is in range + - no candidate arc duplicates an existing graph arc + - candidate arc pairs are unique +- getters/helpers: + - `graph()` + - `candidate_arcs()` + - `bound()` + - `num_vertices()` + - `num_arcs()` + - `num_potential_arcs()` + - `is_weighted()` + - `is_valid_solution()` +- `Problem` impl: + - `NAME = "StrongConnectivityAugmentation"` + - `Metric = bool` + - `variant() = crate::variant_params![W]` + - `dims() = vec![2; self.candidate_arcs.len()]` + - `evaluate()`: + - reject wrong-length configs + - sum selected candidate-arc weights and require `<= self.bound.to_sum()` + - build the augmented arc list from base arcs plus selected candidates + - return whether the augmented digraph is strongly connected +- `impl SatisfactionProblem` +- `declare_variants!` with the default `i32` variant +- `#[cfg(feature = "example-db")] canonical_model_example_specs()` using the issue example and `satisfaction_example(...)` +- test module link at the bottom + +Keep the base graph type as `DirectedGraph`; do not use raw `DiGraph` in the public schema. + +**Step 4: Run the model tests to verify GREEN** + +Run: +```bash +cargo test --features "ilp-highs example-db" strong_connectivity_augmentation --lib +``` + +Expected: +- PASS + +**Step 5: Commit** + +```bash +git add src/models/graph/strong_connectivity_augmentation.rs src/unit_tests/models/graph/strong_connectivity_augmentation.rs +git commit -m "feat: add strong connectivity augmentation model" +``` + +### Task 3: Register The Model, Example DB, And Trait Checks + +**Files:** +- Modify: `src/models/graph/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` +- Modify: `src/unit_tests/trait_consistency.rs` +- Modify: `src/unit_tests/example_db.rs` +- Modify: `src/example_db/fixtures/examples.json` + +**Step 1: Write or extend failing registration checks** + +Add or extend assertions so the new model is exercised by existing infrastructure: +- in `src/unit_tests/trait_consistency.rs`, add a `check_problem_trait(...)` case using a tiny directed example +- in `src/unit_tests/example_db.rs`, add a `find_model_example(...)` test for `StrongConnectivityAugmentation/i32` + +**Step 2: Run the focused registration tests to verify RED** + +Run: +```bash +cargo test --features "ilp-highs example-db" trait_consistency +cargo test --features "ilp-highs example-db" test_find_model_example_strong_connectivity_augmentation +``` + +Expected: +- FAIL because the model is not exported/registered everywhere yet + +**Step 3: Register the model and canonical example** + +Update: +- `src/models/graph/mod.rs` + - add module/export entries + - append `strong_connectivity_augmentation::canonical_model_example_specs()` +- `src/models/mod.rs` + - re-export `StrongConnectivityAugmentation` +- `src/lib.rs` + - add to `prelude` +- `src/unit_tests/trait_consistency.rs` + - add the new satisfaction problem instance +- `src/unit_tests/example_db.rs` + - add a lookup/assertion for the canonical example + +**Step 4: Regenerate fixtures** + +Run: +```bash +make regenerate-fixtures +``` + +Expected: +- `src/example_db/fixtures/examples.json` updates to include the new canonical model example + +**Step 5: Run the focused registration tests to verify GREEN** + +Run: +```bash +cargo test --features "ilp-highs example-db" trait_consistency +cargo test --features "ilp-highs example-db" test_find_model_example_strong_connectivity_augmentation +``` + +Expected: +- PASS + +**Step 6: Commit** + +```bash +git add src/models/graph/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/trait_consistency.rs src/unit_tests/example_db.rs src/example_db/fixtures/examples.json +git commit -m "feat: register strong connectivity augmentation" +``` + +### Task 4: Add `pred create` Support And CLI Smoke Coverage + +**Files:** +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` +- Test: `problemreductions-cli/tests/cli_tests.rs` + +**Step 1: Write the failing CLI smoke test** + +Add a new test to `problemreductions-cli/tests/cli_tests.rs` that runs something like: +```bash +pred create StrongConnectivityAugmentation \ + --arcs "0>1,1>2,2>0,3>4,4>3,2>3,4>5,5>3" \ + --candidate-arcs "3>0:5,3>1:3,3>2:4,4>0:6,4>1:2,4>2:7,5>0:4,5>1:3,5>2:1,0>3:8,0>4:3,0>5:2,1>3:6,1>4:4,1>5:5,2>4:3,2>5:7,1>0:2" \ + --bound 1 +``` + +Assert that: +- the command succeeds +- the JSON `type` is `StrongConnectivityAugmentation` +- the emitted data contains `graph`, `candidate_arcs`, and `bound` + +**Step 2: Run the CLI test to verify RED** + +Run: +```bash +cargo test -p problemreductions-cli test_create_problem_strong_connectivity_augmentation +``` + +Expected: +- FAIL because the new CLI flag/parser path does not exist yet + +**Step 3: Implement CLI support** + +In `problemreductions-cli/src/cli.rs`: +- add `candidate_arcs: Option` to `CreateArgs` +- include it in `all_data_flags_empty()` +- update the `Flags by problem type` help block + +In `problemreductions-cli/src/commands/create.rs`: +- add `type_format_hint()` support for the candidate-arc field format +- add `example_for("StrongConnectivityAugmentation", ...)` +- add a parser for `--candidate-arcs "u>v:w,..."` +- add a new `create()` match arm for `StrongConnectivityAugmentation` +- reuse `parse_directed_graph()` for the base graph and `--bound` for the budget + +Do not add a short alias unless there is a literature-standard abbreviation. + +**Step 4: Run the CLI test to verify GREEN** + +Run: +```bash +cargo test -p problemreductions-cli test_create_problem_strong_connectivity_augmentation +``` + +Expected: +- PASS + +**Step 5: Commit** + +```bash +git add problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs problemreductions-cli/tests/cli_tests.rs +git commit -m "feat: add CLI support for strong connectivity augmentation" +``` + +### Task 5: Document The Model In The Paper + +**Files:** +- Modify: `docs/paper/reductions.typ` + +**Step 1: Write the paper entry using the canonical exported example** + +Add: +- `"StrongConnectivityAugmentation": [Strong Connectivity Augmentation],` to the `display-name` dictionary +- a new `#problem-def("StrongConnectivityAugmentation")[...][...]` block + +Use the exported canonical example, not a hardcoded duplicate. The body should include: +- formal decision version with bound `B` +- short historical/application background citing Eswaran-Tarjan and Garey-Johnson +- brute-force complexity statement `2^num_potential_arcs` plus a note that no stronger exact bound is being claimed here +- the 6-vertex worked example with the single selected augmenting arc `(5,2)` +- a directed-graph figure in the style of `MinimumFeedbackVertexSet` + +**Step 2: Run the paper build to verify RED/GREEN as needed** + +Run: +```bash +make paper +``` + +Expected: +- initially FAIL until the display-name/problem-def entry is complete +- finally PASS once the paper entry is correct + +**Step 3: Commit** + +```bash +git add docs/paper/reductions.typ +git commit -m "docs: add strong connectivity augmentation to paper" +``` + +### Task 6: Final Verification And Review + +**Files:** +- No new files expected; fix anything verification or review finds + +**Step 1: Run focused model and CLI checks** + +Run: +```bash +cargo test --features "ilp-highs example-db" strong_connectivity_augmentation +cargo test -p problemreductions-cli test_create_problem_strong_connectivity_augmentation +``` + +Expected: +- PASS + +**Step 2: Run repo verification** + +Run: +```bash +make test +make clippy +``` + +Expected: +- PASS + +**Step 3: Run implementation review** + +Use the repo-local review skill directly: +```text +.claude/skills/review-implementation/SKILL.md +``` + +Auto-fix any actionable findings before moving on. + +**Step 4: If review fixes were needed, rerun verification** + +Run: +```bash +make test +make clippy +``` + +Expected: +- PASS + +**Step 5: Commit review-driven fixes if needed** + +```bash +git add -A +git commit -m "fix: address strong connectivity augmentation review findings" +``` + +Only make this commit if review or verification required additional changes. From 3abec4f4102cbf6ca1e17104caded4c98d23ed01 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 05:04:21 +0800 Subject: [PATCH 2/9] feat: add directed strong connectivity check --- src/topology/directed_graph.rs | 37 +++++++++++++++++++++++ src/unit_tests/topology/directed_graph.rs | 24 +++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/topology/directed_graph.rs b/src/topology/directed_graph.rs index 3fe0a011..5af200b3 100644 --- a/src/topology/directed_graph.rs +++ b/src/topology/directed_graph.rs @@ -143,6 +143,43 @@ impl DirectedGraph { toposort(&self.inner, None).is_ok() } + /// Returns `true` if every vertex can reach every other vertex. + pub fn is_strongly_connected(&self) -> bool { + let n = self.num_vertices(); + if n <= 1 { + return true; + } + + fn visit( + graph: &DirectedGraph, + start: usize, + neighbors: impl Fn(&DirectedGraph, usize) -> Vec, + ) -> Vec { + let mut seen = vec![false; graph.num_vertices()]; + let mut stack = vec![start]; + seen[start] = true; + + while let Some(v) = stack.pop() { + for u in neighbors(graph, v) { + if !seen[u] { + seen[u] = true; + stack.push(u); + } + } + } + + seen + } + + let forward = visit(self, 0, DirectedGraph::successors); + if forward.iter().any(|&seen| !seen) { + return false; + } + + let reverse = visit(self, 0, DirectedGraph::predecessors); + reverse.iter().all(|&seen| seen) + } + /// Check if the subgraph induced by keeping only the given arcs is acyclic (a DAG). /// /// `kept_arcs` is a boolean slice of length `num_arcs()`, where `true` means the arc is kept. diff --git a/src/unit_tests/topology/directed_graph.rs b/src/unit_tests/topology/directed_graph.rs index 859cf6e4..2a668b39 100644 --- a/src/unit_tests/topology/directed_graph.rs +++ b/src/unit_tests/topology/directed_graph.rs @@ -97,6 +97,30 @@ fn test_directed_graph_is_dag_self_loop() { assert!(!g.is_dag()); } +#[test] +fn test_is_strongly_connected_cycle() { + let g = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); + assert!(g.is_strongly_connected()); +} + +#[test] +fn test_is_strongly_connected_path() { + let g = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + assert!(!g.is_strongly_connected()); +} + +#[test] +fn test_is_strongly_connected_single_vertex() { + let g = DirectedGraph::new(1, vec![]); + assert!(g.is_strongly_connected()); +} + +#[test] +fn test_is_strongly_connected_empty() { + let g = DirectedGraph::empty(0); + assert!(g.is_strongly_connected()); +} + #[test] fn test_directed_graph_is_acyclic_subgraph() { // Cycle: 0->1->2->0 From cf0206ca9ccf57e425026b474dd30dedb340e0b8 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 05:09:10 +0800 Subject: [PATCH 3/9] feat: add strong connectivity augmentation model --- src/models/graph/mod.rs | 1 + .../graph/strong_connectivity_augmentation.rs | 235 ++++++++++++++++++ .../graph/strong_connectivity_augmentation.rs | 174 +++++++++++++ 3 files changed, 410 insertions(+) create mode 100644 src/models/graph/strong_connectivity_augmentation.rs create mode 100644 src/unit_tests/models/graph/strong_connectivity_augmentation.rs diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 7d76de65..737c19f0 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -42,6 +42,7 @@ pub(crate) mod optimal_linear_arrangement; pub(crate) mod partition_into_triangles; pub(crate) mod rural_postman; pub(crate) mod spin_glass; +pub(crate) mod strong_connectivity_augmentation; pub(crate) mod subgraph_isomorphism; pub(crate) mod traveling_salesman; diff --git a/src/models/graph/strong_connectivity_augmentation.rs b/src/models/graph/strong_connectivity_augmentation.rs new file mode 100644 index 00000000..f5337bba --- /dev/null +++ b/src/models/graph/strong_connectivity_augmentation.rs @@ -0,0 +1,235 @@ +//! Strong Connectivity Augmentation problem implementation. +//! +//! The Strong Connectivity Augmentation problem asks whether adding a bounded +//! set of weighted candidate arcs can make a directed graph strongly connected. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; +use crate::topology::DirectedGraph; +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: "StrongConnectivityAugmentation", + display_name: "Strong Connectivity Augmentation", + aliases: &[], + dimensions: &[ + VariantDimension::new("weight", "i32", &["i32"]), + ], + module_path: module_path!(), + description: "Add a bounded set of weighted candidate arcs to make a digraph strongly connected", + fields: &[ + FieldInfo { name: "graph", type_name: "DirectedGraph", description: "The initial directed graph G=(V,A)" }, + FieldInfo { name: "candidate_arcs", type_name: "Vec<(usize, usize, W)>", description: "Candidate augmenting arcs (u, v, w(u,v)) not already present in G" }, + FieldInfo { name: "bound", type_name: "W::Sum", description: "Upper bound B on the total added weight" }, + ], + } +} + +/// Strong Connectivity Augmentation. +/// +/// Given a directed graph `G = (V, A)`, weighted candidate arcs not already in +/// `A`, and a bound `B`, determine whether some subset of the candidate arcs +/// has total weight at most `B` and makes the augmented digraph strongly +/// connected. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StrongConnectivityAugmentation { + graph: DirectedGraph, + candidate_arcs: Vec<(usize, usize, W)>, + bound: W::Sum, +} + +impl StrongConnectivityAugmentation { + /// Create a new strong connectivity augmentation instance. + /// + /// # Panics + /// + /// Panics if a candidate arc endpoint is out of range, if a candidate arc + /// already exists in the base graph, or if candidate arcs contain + /// duplicates. + pub fn new(graph: DirectedGraph, candidate_arcs: Vec<(usize, usize, W)>, bound: W::Sum) -> Self { + let num_vertices = graph.num_vertices(); + let mut seen_pairs = BTreeSet::new(); + + for (u, v, _) in &candidate_arcs { + assert!( + *u < num_vertices && *v < num_vertices, + "candidate arc ({}, {}) references vertex >= num_vertices ({})", + u, + v, + num_vertices + ); + assert!( + !graph.has_arc(*u, *v), + "candidate arc ({}, {}) already exists in the base graph", + u, + v + ); + assert!( + seen_pairs.insert((*u, *v)), + "duplicate candidate arc ({}, {})", + u, + v + ); + } + + Self { + graph, + candidate_arcs, + bound, + } + } + + /// Get the base directed graph. + pub fn graph(&self) -> &DirectedGraph { + &self.graph + } + + /// Get the candidate augmenting arcs. + pub fn candidate_arcs(&self) -> &[(usize, usize, W)] { + &self.candidate_arcs + } + + /// Get the upper bound on the total added weight. + pub fn bound(&self) -> &W::Sum { + &self.bound + } + + /// Get the number of vertices in the base graph. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of arcs in the base graph. + pub fn num_arcs(&self) -> usize { + self.graph.num_arcs() + } + + /// Get the number of potential augmenting arcs. + pub fn num_potential_arcs(&self) -> usize { + self.candidate_arcs.len() + } + + /// Check whether the problem uses non-unit weights. + pub fn is_weighted(&self) -> bool { + !W::IS_UNIT + } + + /// Check whether a configuration is a satisfying augmentation. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + self.evaluate_config(config) + } + + fn evaluate_config(&self, config: &[usize]) -> bool { + if config.len() != self.candidate_arcs.len() { + return false; + } + + let mut total = W::Sum::zero(); + let mut augmented_arcs = self.graph.arcs(); + + for ((u, v, weight), &selected) in self.candidate_arcs.iter().zip(config.iter()) { + if selected > 1 { + return false; + } + if selected == 1 { + total += weight.to_sum(); + if total > self.bound { + return false; + } + augmented_arcs.push((*u, *v)); + } + } + + DirectedGraph::new(self.graph.num_vertices(), augmented_arcs).is_strongly_connected() + } +} + +impl Problem for StrongConnectivityAugmentation +where + W: WeightElement + crate::variant::VariantParam, +{ + const NAME: &'static str = "StrongConnectivityAugmentation"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![W] + } + + fn dims(&self) -> Vec { + vec![2; self.candidate_arcs.len()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.evaluate_config(config) + } +} + +impl SatisfactionProblem for StrongConnectivityAugmentation where + W: WeightElement + crate::variant::VariantParam +{ +} + +crate::declare_variants! { + default sat StrongConnectivityAugmentation => "2^num_potential_arcs", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "strong_connectivity_augmentation_i32", + build: || { + let problem = StrongConnectivityAugmentation::new( + DirectedGraph::new( + 6, + vec![ + (0, 1), + (1, 2), + (2, 0), + (3, 4), + (4, 3), + (2, 3), + (4, 5), + (5, 3), + ], + ), + vec![ + (3, 0, 5), + (3, 1, 3), + (3, 2, 4), + (4, 0, 6), + (4, 1, 2), + (4, 2, 7), + (5, 0, 4), + (5, 1, 3), + (5, 2, 1), + (0, 3, 8), + (0, 4, 3), + (0, 5, 2), + (1, 3, 6), + (1, 4, 4), + (1, 5, 5), + (2, 4, 3), + (2, 5, 7), + (1, 0, 2), + ], + 1, + ); + + crate::example_db::specs::satisfaction_example( + problem, + vec![ + vec![0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], + vec![0; 18], + ], + ) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/strong_connectivity_augmentation.rs"] +mod tests; diff --git a/src/unit_tests/models/graph/strong_connectivity_augmentation.rs b/src/unit_tests/models/graph/strong_connectivity_augmentation.rs new file mode 100644 index 00000000..9573757d --- /dev/null +++ b/src/unit_tests/models/graph/strong_connectivity_augmentation.rs @@ -0,0 +1,174 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::topology::DirectedGraph; +use crate::traits::Problem; + +fn issue_graph() -> DirectedGraph { + DirectedGraph::new( + 6, + vec![ + (0, 1), + (1, 2), + (2, 0), + (3, 4), + (4, 3), + (2, 3), + (4, 5), + (5, 3), + ], + ) +} + +fn issue_candidate_arcs() -> Vec<(usize, usize, i32)> { + vec![ + (3, 0, 5), + (3, 1, 3), + (3, 2, 4), + (4, 0, 6), + (4, 1, 2), + (4, 2, 7), + (5, 0, 4), + (5, 1, 3), + (5, 2, 1), + (0, 3, 8), + (0, 4, 3), + (0, 5, 2), + (1, 3, 6), + (1, 4, 4), + (1, 5, 5), + (2, 4, 3), + (2, 5, 7), + (1, 0, 2), + ] +} + +fn issue_example_yes() -> StrongConnectivityAugmentation { + StrongConnectivityAugmentation::new(issue_graph(), issue_candidate_arcs(), 1) +} + +fn yes_config() -> Vec { + vec![0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0] +} + +fn issue_example_already_strongly_connected() -> StrongConnectivityAugmentation { + StrongConnectivityAugmentation::new( + DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + vec![(0, 2, 5)], + 0, + ) +} + +#[test] +fn test_strong_connectivity_augmentation_creation() { + let problem = issue_example_yes(); + + assert_eq!(problem.num_vertices(), 6); + assert_eq!(problem.num_arcs(), 8); + assert_eq!(problem.num_potential_arcs(), 18); + assert_eq!(problem.candidate_arcs().len(), 18); + assert_eq!(problem.bound(), &1); + assert_eq!(problem.dims(), vec![2; 18]); + assert!(problem.is_weighted()); +} + +#[test] +fn test_strong_connectivity_augmentation_issue_example_yes() { + let problem = issue_example_yes(); + let config = yes_config(); + + assert!(problem.evaluate(&config)); + assert!(problem.is_valid_solution(&config)); +} + +#[test] +fn test_strong_connectivity_augmentation_issue_example_no() { + let problem = issue_example_yes(); + assert!(!problem.evaluate(&vec![0; 18])); +} + +#[test] +fn test_strong_connectivity_augmentation_wrong_length() { + let problem = issue_example_yes(); + assert!(!problem.evaluate(&[0, 1])); + assert!(!problem.is_valid_solution(&[0, 1])); +} + +#[test] +fn test_strong_connectivity_augmentation_already_strongly_connected() { + let problem = issue_example_already_strongly_connected(); + assert_eq!(problem.dims(), vec![2]); + assert!(problem.evaluate(&[0])); + assert!(!problem.evaluate(&[1])); +} + +#[test] +fn test_strong_connectivity_augmentation_serialization() { + let problem = issue_example_yes(); + let json = serde_json::to_string(&problem).unwrap(); + let restored: StrongConnectivityAugmentation = serde_json::from_str(&json).unwrap(); + + assert_eq!(restored.graph(), problem.graph()); + assert_eq!(restored.candidate_arcs(), problem.candidate_arcs()); + assert_eq!(restored.bound(), problem.bound()); +} + +#[test] +fn test_strong_connectivity_augmentation_solver() { + let problem = issue_example_yes(); + let solver = BruteForce::new(); + + let satisfying = solver.find_satisfying(&problem).unwrap(); + assert!(problem.evaluate(&satisfying)); + + let all_satisfying = solver.find_all_satisfying(&problem); + assert_eq!(all_satisfying, vec![yes_config()]); +} + +#[test] +fn test_strong_connectivity_augmentation_variant() { + let variant = as Problem>::variant(); + assert_eq!(variant, vec![("weight", "i32")]); +} + +#[test] +fn test_strong_connectivity_augmentation_paper_example() { + let problem = issue_example_yes(); + let config = yes_config(); + + assert!(problem.evaluate(&config)); + + let solver = BruteForce::new(); + let all_satisfying = solver.find_all_satisfying(&problem); + assert_eq!(all_satisfying.len(), 1); + assert_eq!(all_satisfying[0], config); +} + +#[test] +#[should_panic(expected = "candidate arc (0, 1) already exists in the base graph")] +fn test_strong_connectivity_augmentation_existing_arc_candidate_panics() { + StrongConnectivityAugmentation::new( + DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + vec![(0, 1, 1)], + 1, + ); +} + +#[test] +#[should_panic(expected = "duplicate candidate arc (0, 2)")] +fn test_strong_connectivity_augmentation_duplicate_candidate_arc_panics() { + StrongConnectivityAugmentation::new( + DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + vec![(0, 2, 1), (0, 2, 3)], + 3, + ); +} + +#[test] +#[should_panic(expected = "candidate arc (0, 3) references vertex >= num_vertices")] +fn test_strong_connectivity_augmentation_out_of_range_candidate_panics() { + StrongConnectivityAugmentation::new( + DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + vec![(0, 3, 1)], + 1, + ); +} From 4ffeded19e0c9f2974d6a744c02d79a1c5a1bf5d Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 05:11:36 +0800 Subject: [PATCH 4/9] feat: register strong connectivity augmentation --- src/example_db/fixtures/examples.json | 1 + src/lib.rs | 2 +- src/models/graph/mod.rs | 3 +++ src/models/mod.rs | 4 ++-- src/unit_tests/example_db.rs | 17 +++++++++++++++++ src/unit_tests/trait_consistency.rs | 8 ++++++++ 6 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index e48eef82..5db7f624 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -30,6 +30,7 @@ {"problem":"Satisfiability","variant":{},"instance":{"clauses":[{"literals":[1,2]},{"literals":[-1,3]},{"literals":[-2,-3]}],"num_vars":3},"samples":[{"config":[1,0,1],"metric":true}],"optimal":[{"config":[0,1,0],"metric":true},{"config":[1,0,1],"metric":true}]}, {"problem":"ShortestCommonSupersequence","variant":{},"instance":{"alphabet_size":3,"bound":4,"strings":[[0,1,2],[1,0,2]]},"samples":[{"config":[1,0,1,2],"metric":true}],"optimal":[{"config":[0,1,0,2],"metric":true},{"config":[1,0,1,2],"metric":true}]}, {"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1],"fields":[0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[3,4,null],[0,3,null],[1,3,null],[1,4,null],[2,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0],"metric":{"Valid":-3}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":-3}},{"config":[0,1,0,0,1],"metric":{"Valid":-3}},{"config":[0,1,0,1,0],"metric":{"Valid":-3}},{"config":[0,1,1,1,0],"metric":{"Valid":-3}},{"config":[1,0,0,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,1,0],"metric":{"Valid":-3}},{"config":[1,1,0,0,1],"metric":{"Valid":-3}}]}, + {"problem":"StrongConnectivityAugmentation","variant":{"weight":"i32"},"instance":{"bound":1,"candidate_arcs":[[3,0,5],[3,1,3],[3,2,4],[4,0,6],[4,1,2],[4,2,7],[5,0,4],[5,1,3],[5,2,1],[0,3,8],[0,4,3],[0,5,2],[1,3,6],[1,4,4],[1,5,5],[2,4,3],[2,5,7],[1,0,2]],"graph":{"inner":{"edge_property":"directed","edges":[[0,1,null],[1,2,null],[2,0,null],[3,4,null],[4,3,null],[2,3,null],[4,5,null],[5,3,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0],"metric":true},{"config":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"metric":false}],"optimal":[{"config":[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0],"metric":true}]}, {"problem":"TravelingSalesman","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,3,2,2,3,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}]} ], "rules": [ diff --git a/src/lib.rs b/src/lib.rs index bceccfe2..504c4302 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,7 +46,7 @@ pub mod prelude { pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use crate::models::graph::{ BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, SpinGlass, - SubgraphIsomorphism, + StrongConnectivityAugmentation, SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 737c19f0..d29428f7 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -21,6 +21,7 @@ //! - [`MinimumFeedbackArcSet`]: Minimum feedback arc set on directed graphs //! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median) //! - [`RuralPostman`]: Rural Postman (circuit covering required edges) +//! - [`StrongConnectivityAugmentation`]: Add weighted arcs to make a digraph strongly connected //! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem) pub(crate) mod biclique_cover; @@ -65,6 +66,7 @@ pub use optimal_linear_arrangement::OptimalLinearArrangement; pub use partition_into_triangles::PartitionIntoTriangles; pub use rural_postman::RuralPostman; pub use spin_glass::SpinGlass; +pub use strong_connectivity_augmentation::StrongConnectivityAugmentation; pub use subgraph_isomorphism::SubgraphIsomorphism; pub use traveling_salesman::TravelingSalesman; @@ -87,5 +89,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec Date: Mon, 16 Mar 2026 05:20:26 +0800 Subject: [PATCH 5/9] feat: add CLI support for strong connectivity augmentation --- problemreductions-cli/src/cli.rs | 4 + problemreductions-cli/src/commands/create.rs | 91 +++++++++++++++++--- problemreductions-cli/tests/cli_tests.rs | 29 +++++++ 3 files changed, 113 insertions(+), 11 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index c42f69c2..12beda2c 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -240,6 +240,7 @@ Flags by problem type: LCS --strings FAS --arcs [--weights] [--num-vertices] FVS --arcs [--weights] [--num-vertices] + StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices] FlowShopScheduling --task-lengths, --deadline [--num-processors] MinimumTardinessSequencing --n, --deadlines [--precedence-pairs] SCS --strings, --bound [--alphabet-size] @@ -385,6 +386,9 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Candidate weighted arcs for StrongConnectivityAugmentation (e.g., 2>0:1,2>1:3) + #[arg(long)] + pub candidate_arcs: Option, /// Deadlines for MinimumTardinessSequencing (comma-separated, e.g., "5,5,5,3,3") #[arg(long)] pub deadlines: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index a1444224..e29da035 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -1,7 +1,7 @@ use crate::cli::{CreateArgs, ExampleSide}; use crate::dispatch::ProblemJsonOutput; use crate::output::OutputConfig; -use crate::problem_name::{resolve_problem_ref, unknown_problem_error}; +use crate::problem_name::{resolve_catalog_problem_ref, resolve_problem_ref, unknown_problem_error}; use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample}; @@ -57,6 +57,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.pattern.is_none() && args.strings.is_none() && args.arcs.is_none() + && args.candidate_arcs.is_none() && args.deadlines.is_none() && args.precedence_pairs.is_none() && args.task_lengths.is_none() @@ -208,6 +209,8 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { "Vec" => "comma-separated nonnegative decimal integers: 3,7,1,8", "Vec" => "comma-separated integers: 3,7,1,8", "DirectedGraph" => "directed arcs: 0>1,1>2,2>0", + "Vec<(usize, usize, W)>" => "candidate arcs with weights: 0>2:5,2>1:3", + "W::Sum" => "integer", _ => "value", } } @@ -237,6 +240,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "MinimumSumMulticenter" => { "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2" } + "StrongConnectivityAugmentation" => { + "--arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1" + } "PartitionIntoTriangles" => "--graph 0-1,1-2,0-2", "Factoring" => "--target 15 --m 4 --n 4", "OptimalLinearArrangement" => "--graph 0-1,1-2,2-3 --bound 5", @@ -319,14 +325,13 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { let problem = args.problem.as_ref().ok_or_else(|| { anyhow::anyhow!("Missing problem type.\n\nUsage: pred create [FLAGS]") })?; - let rgraph = problemreductions::rules::ReductionGraph::new(); - let resolved = resolve_problem_ref(problem, &rgraph)?; - let canonical = &resolved.name; - let resolved_variant = resolved.variant; + let resolved = resolve_catalog_problem_ref(problem)?; + let canonical = resolved.name().to_string(); + let resolved_variant = resolved.variant().clone(); let graph_type = resolved_graph_type(&resolved_variant); if args.random { - return create_random(args, canonical, &resolved_variant, out); + return create_random(args, &canonical, &resolved_variant, out); } // ILP and CircuitSAT have complex input structures not suited for CLI flags. @@ -347,7 +352,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { } else { None }; - print_problem_help(canonical, gt)?; + print_problem_help(&canonical, gt)?; std::process::exit(2); } @@ -357,7 +362,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { | "MinimumVertexCover" | "MaximumClique" | "MinimumDominatingSet" => { - create_vertex_weight_problem(args, canonical, graph_type, &resolved_variant)? + create_vertex_weight_problem(args, &canonical, graph_type, &resolved_variant)? } // Graph partitioning (graph only, no weights) @@ -563,7 +568,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // MaximalIS — same as MIS (graph + vertex weights) "MaximalIS" => { - create_vertex_weight_problem(args, canonical, graph_type, &resolved_variant)? + create_vertex_weight_problem(args, &canonical, graph_type, &resolved_variant)? } // BinPacking @@ -930,6 +935,28 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // StrongConnectivityAugmentation + "StrongConnectivityAugmentation" => { + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "StrongConnectivityAugmentation requires --arcs\n\n\ + Usage: pred create StrongConnectivityAugmentation --arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1 [--num-vertices N]" + ) + })?; + let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; + let candidate_arcs = parse_candidate_arcs(args, graph.num_vertices())?; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!( + "StrongConnectivityAugmentation requires --bound\n\n\ + Usage: pred create StrongConnectivityAugmentation --arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1 [--num-vertices N]" + ) + })? as i32; + ( + ser(StrongConnectivityAugmentation::new(graph, candidate_arcs, bound))?, + resolved_variant.clone(), + ) + } + // MinimumSumMulticenter (p-median) "MinimumSumMulticenter" => { let (graph, n) = parse_graph(args).map_err(|e| { @@ -1086,11 +1113,11 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } - _ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)), + _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), }; let output = ProblemJsonOutput { - problem_type: canonical.to_string(), + problem_type: canonical, variant, data, }; @@ -1565,6 +1592,48 @@ fn parse_arc_weights(args: &CreateArgs, num_arcs: usize) -> Result> { } } +/// Parse `--candidate-arcs` as `u>v:w` entries for StrongConnectivityAugmentation. +fn parse_candidate_arcs(args: &CreateArgs, num_vertices: usize) -> Result> { + let arcs_str = args.candidate_arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "StrongConnectivityAugmentation requires --candidate-arcs (e.g., \"2>0:1,2>1:3\")" + ) + })?; + + arcs_str + .split(',') + .map(|entry| { + let entry = entry.trim(); + let (arc_part, weight_part) = entry.split_once(':').ok_or_else(|| { + anyhow::anyhow!( + "Invalid candidate arc '{}': expected format u>v:w (e.g., 2>0:1)", + entry + ) + })?; + let parts: Vec<&str> = arc_part.split('>').collect(); + if parts.len() != 2 { + bail!( + "Invalid candidate arc '{}': expected format u>v:w (e.g., 2>0:1)", + entry + ); + } + + let u: usize = parts[0].parse()?; + let v: usize = parts[1].parse()?; + anyhow::ensure!( + u < num_vertices && v < num_vertices, + "candidate arc ({}, {}) references vertex >= num_vertices ({})", + u, + v, + num_vertices + ); + + let weight: i32 = weight_part.parse()?; + Ok((u, v, weight)) + }) + .collect() +} + /// Handle `pred create --random ...` fn create_random( args: &CreateArgs, diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index f011bad1..5ec2e911 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -280,6 +280,35 @@ fn test_evaluate_sat() { std::fs::remove_file(&tmp).ok(); } +#[test] +fn test_create_problem_strong_connectivity_augmentation() { + let output = pred() + .args([ + "create", + "StrongConnectivityAugmentation", + "--arcs", + "0>1,1>2,2>0,3>4,4>3,2>3,4>5,5>3", + "--candidate-arcs", + "3>0:5,3>1:3,3>2:4,4>0:6,4>1:2,4>2:7,5>0:4,5>1:3,5>2:1,0>3:8,0>4:3,0>5:2,1>3:6,1>4:4,1>5:5,2>4:3,2>5:7,1>0:2", + "--bound", + "1", + ]) + .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"], "StrongConnectivityAugmentation"); + assert!(json["data"]["graph"].is_object()); + assert!(json["data"]["candidate_arcs"].is_array()); + assert_eq!(json["data"]["bound"], 1); +} + #[test] fn test_reduce() { let problem_json = r#"{ From bd73eb1ca5eeba2e1603c3df384da4220f7f1960 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 05:22:22 +0800 Subject: [PATCH 6/9] docs: add strong connectivity augmentation to paper --- docs/paper/reductions.typ | 45 ++++++++++++++++++++++++ docs/paper/references.bib | 11 ++++++ docs/src/reductions/problem_schemas.json | 21 +++++++++++ docs/src/reductions/reduction_graph.json | 15 ++++++-- 4 files changed, 89 insertions(+), 3 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index b3a286b6..2d6da26e 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -98,6 +98,7 @@ "SubsetSum": [Subset Sum], "MinimumFeedbackArcSet": [Minimum Feedback Arc Set], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], + "StrongConnectivityAugmentation": [Strong Connectivity Augmentation], "ShortestCommonSupersequence": [Shortest Common Supersequence], "MinimumSumMulticenter": [Minimum Sum Multicenter], "SubgraphIsomorphism": [Subgraph Isomorphism], @@ -892,6 +893,50 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("StrongConnectivityAugmentation") + let nv = graph-num-vertices(x.instance) + let ne = graph-num-edges(x.instance) + let arcs = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1))) + let candidates = x.instance.candidate_arcs + let bound = x.instance.bound + let sol = x.optimal.at(0) + let chosen = candidates.enumerate().filter(((i, _)) => sol.config.at(i) == 1).map(((i, arc)) => arc) + let arc = chosen.at(0) + let blue = graph-colors.at(0) + [ + #problem-def("StrongConnectivityAugmentation")[ + Given a directed graph $G = (V, A)$, a set $C subset.eq (V times V backslash A) times ZZ_(> 0)$ of weighted candidate arcs, and a bound $B in ZZ_(>= 0)$, determine whether there exists a subset $C' subset.eq C$ such that $sum_((u, v, w) in C') w <= B$ and the augmented digraph $(V, A union {(u, v) : (u, v, w) in C'})$ is strongly connected. + ][ + Strong Connectivity Augmentation models network design problems where a partially connected directed communication graph may be repaired by buying additional arcs. Eswaran and Tarjan showed that the unweighted augmentation problem is solvable in linear time, while the weighted variant is substantially harder @eswarantarjan1976. The decision version recorded as ND19 in Garey and Johnson is NP-complete @garey1979. The implementation here uses one binary variable per candidate arc, so brute-force over the candidate set yields a worst-case bound of $O^*(2^m)$ where $m = "num_potential_arcs"$. #footnote[No exact algorithm improving on brute-force is claimed here for the weighted candidate-arc formulation implemented in the codebase.] + + *Example.* The canonical instance has $n = #nv$ vertices, $|A| = #ne$ existing arcs, #candidates.len() weighted candidate arcs, and bound $B = #bound$. The base graph already contains the directed 3-cycle $v_0 -> v_1 -> v_2 -> v_0$ and the strongly connected component on ${v_3, v_4, v_5}$, with only the forward bridge $v_2 -> v_3$ between them. The unique satisfying augmentation under this bound selects the single candidate arc $(v_#arc.at(0), v_#arc.at(1)))$ of weight #arc.at(2), closing the cycle $v_2 -> v_3 -> v_4 -> v_5 -> v_2$ and making every vertex reachable from every other. The all-zero configuration is infeasible because no path returns from ${v_3, v_4, v_5}$ to ${v_0, v_1, v_2}$. + + #figure({ + let verts = ((0, 1), (1.2, 1.6), (1.2, 0.4), (3.4, 1.0), (4.6, 1.5), (4.6, 0.5)) + canvas(length: 1cm, { + for (u, v) in arcs { + draw.line(verts.at(u), verts.at(v), + stroke: 1pt + black, + mark: (end: "straight", scale: 0.4)) + } + draw.line(verts.at(arc.at(0)), verts.at(arc.at(1)), + stroke: 1.6pt + blue, + mark: (end: "straight", scale: 0.45)) + for (k, pos) in verts.enumerate() { + let highlighted = k == arc.at(0) or k == arc.at(1) + g-node(pos, name: "v" + str(k), + fill: if highlighted { blue.transparentize(65%) } else { white }, + label: [$v_#k$]) + } + }) + }, + caption: [Strong Connectivity Augmentation on a #{nv}-vertex digraph. Black arcs are present in $A$; the added candidate arc $(v_#arc.at(0), v_#arc.at(1)))$ is shown in blue. With bound $B = #bound$, this single augmentation makes the digraph strongly connected.], + ) + ] + ] +} + #{ 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 5fc78c6e..48923f57 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -64,6 +64,17 @@ @book{garey1979 year = {1979} } +@article{eswarantarjan1976, + author = {K. P. Eswaran and Robert E. Tarjan}, + title = {Augmentation Problems}, + journal = {SIAM Journal on Computing}, + volume = {5}, + number = {4}, + pages = {653--665}, + year = {1976}, + doi = {10.1137/0205044} +} + @article{gareyJohnsonStockmeyer1976, author = {Michael R. Garey and David S. Johnson and Larry Stockmeyer}, title = {Some Simplified {NP}-Complete Graph Problems}, diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 3a6e9df8..22999756 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -636,6 +636,27 @@ } ] }, + { + "name": "StrongConnectivityAugmentation", + "description": "Add a bounded set of weighted candidate arcs to make a digraph strongly connected", + "fields": [ + { + "name": "graph", + "type_name": "DirectedGraph", + "description": "The initial directed graph G=(V,A)" + }, + { + "name": "candidate_arcs", + "type_name": "Vec<(usize, usize, W)>", + "description": "Candidate augmenting arcs (u, v, w(u,v)) not already present in G" + }, + { + "name": "bound", + "type_name": "W::Sum", + "description": "Upper bound B on the total added weight" + } + ] + }, { "name": "SubgraphIsomorphism", "description": "Determine if host graph G contains a subgraph isomorphic to pattern graph H", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index c1334cfb..967593c1 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -491,6 +491,15 @@ "doc_path": "models/graph/struct.SpinGlass.html", "complexity": "2^num_spins" }, + { + "name": "StrongConnectivityAugmentation", + "variant": { + "weight": "i32" + }, + "category": "graph", + "doc_path": "models/graph/struct.StrongConnectivityAugmentation.html", + "complexity": "2^num_potential_arcs" + }, { "name": "SubgraphIsomorphism", "variant": {}, @@ -713,7 +722,7 @@ }, { "source": 21, - "target": 56, + "target": 57, "overhead": [ { "field": "num_elements", @@ -1341,7 +1350,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 57, + "source": 58, "target": 12, "overhead": [ { @@ -1356,7 +1365,7 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 57, + "source": 58, "target": 49, "overhead": [ { From b2068caef68580fd6bf2926f6612186a52e70342 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 05:44:36 +0800 Subject: [PATCH 7/9] fix: address strong connectivity augmentation review findings --- problemreductions-cli/src/commands/create.rs | 39 ++++-- problemreductions-cli/src/dispatch.rs | 24 ++++ problemreductions-cli/src/problem_name.rs | 40 +++++- problemreductions-cli/tests/cli_tests.rs | 122 ++++++++++++++++++ .../graph/strong_connectivity_augmentation.rs | 111 +++++++++++----- .../graph/strong_connectivity_augmentation.rs | 2 +- 6 files changed, 290 insertions(+), 48 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index e29da035..55238bc2 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -1,7 +1,7 @@ use crate::cli::{CreateArgs, ExampleSide}; use crate::dispatch::ProblemJsonOutput; use crate::output::OutputConfig; -use crate::problem_name::{resolve_catalog_problem_ref, resolve_problem_ref, unknown_problem_error}; +use crate::problem_name::{resolve_create_problem_ref, resolve_problem_ref, unknown_problem_error}; use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample}; @@ -325,9 +325,10 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { let problem = args.problem.as_ref().ok_or_else(|| { anyhow::anyhow!("Missing problem type.\n\nUsage: pred create [FLAGS]") })?; - let resolved = resolve_catalog_problem_ref(problem)?; - let canonical = resolved.name().to_string(); - let resolved_variant = resolved.variant().clone(); + let rgraph = problemreductions::rules::ReductionGraph::new(); + let resolved = resolve_create_problem_ref(problem, &rgraph)?; + let canonical = resolved.name; + let resolved_variant = resolved.variant; let graph_type = resolved_graph_type(&resolved_variant); if args.random { @@ -937,22 +938,21 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // StrongConnectivityAugmentation "StrongConnectivityAugmentation" => { + let usage = "Usage: pred create StrongConnectivityAugmentation --arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1 [--num-vertices N]"; let arcs_str = args.arcs.as_deref().ok_or_else(|| { anyhow::anyhow!( "StrongConnectivityAugmentation requires --arcs\n\n\ - Usage: pred create StrongConnectivityAugmentation --arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1 [--num-vertices N]" + {usage}" ) })?; let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; let candidate_arcs = parse_candidate_arcs(args, graph.num_vertices())?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "StrongConnectivityAugmentation requires --bound\n\n\ - Usage: pred create StrongConnectivityAugmentation --arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1 [--num-vertices N]" - ) - })? as i32; + let bound = parse_nonnegative_i32_bound(args, "StrongConnectivityAugmentation", usage)?; ( - ser(StrongConnectivityAugmentation::new(graph, candidate_arcs, bound))?, + ser( + StrongConnectivityAugmentation::try_new(graph, candidate_arcs, bound) + .map_err(|e| anyhow::anyhow!(e))?, + )?, resolved_variant.clone(), ) } @@ -1593,7 +1593,10 @@ fn parse_arc_weights(args: &CreateArgs, num_arcs: usize) -> Result> { } /// Parse `--candidate-arcs` as `u>v:w` entries for StrongConnectivityAugmentation. -fn parse_candidate_arcs(args: &CreateArgs, num_vertices: usize) -> Result> { +fn parse_candidate_arcs( + args: &CreateArgs, + num_vertices: usize, +) -> Result> { let arcs_str = args.candidate_arcs.as_deref().ok_or_else(|| { anyhow::anyhow!( "StrongConnectivityAugmentation requires --candidate-arcs (e.g., \"2>0:1,2>1:3\")" @@ -1634,6 +1637,16 @@ fn parse_candidate_arcs(args: &CreateArgs, num_vertices: usize) -> Result Result { + let raw_bound = args + .bound + .ok_or_else(|| anyhow::anyhow!("{problem} requires --bound\n\n{usage}"))?; + let bound = + i32::try_from(raw_bound).map_err(|_| anyhow::anyhow!("{problem} bound must fit in i32"))?; + anyhow::ensure!(bound >= 0, "{problem} bound must be nonnegative"); + Ok(bound) +} + /// Handle `pred create --random ...` fn create_random( args: &CreateArgs, diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 659bba48..cb4feda4 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -180,6 +180,7 @@ mod tests { use problemreductions::models::graph::MaximumIndependentSet; use problemreductions::models::misc::BinPacking; use problemreductions::topology::SimpleGraph; + use serde_json::json; #[test] fn test_load_problem_alias_uses_registry_dispatch() { @@ -204,6 +205,29 @@ mod tests { assert!(loaded.is_err()); } + #[test] + fn test_load_problem_rejects_invalid_strong_connectivity_augmentation_instance() { + let variant = BTreeMap::from([("weight".to_string(), "i32".to_string())]); + let data = json!({ + "graph": { + "inner": { + "edge_property": "directed", + "nodes": [null, null, null], + "node_holes": [], + "edges": [[0, 1, null], [1, 2, null]] + } + }, + "candidate_arcs": [[0, 3, 1]], + "bound": 1 + }); + + let loaded = load_problem("StrongConnectivityAugmentation", &variant, data); + assert!(loaded.is_err()); + let err = loaded.err().unwrap().to_string(); + assert!(err.contains("candidate arc"), "err: {err}"); + assert!(err.contains("num_vertices"), "err: {err}"); + } + #[test] fn test_serialize_any_problem_round_trips_bin_packing() { let problem = BinPacking::new(vec![3i32, 3, 2, 2], 5i32); diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 2f7ee789..adbea497 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -1,3 +1,4 @@ +use problemreductions::export::ProblemRef; use std::collections::{BTreeMap, BTreeSet}; use std::ffi::OsStr; @@ -39,6 +40,28 @@ pub fn resolve_catalog_problem_ref( .map_err(|e| anyhow::anyhow!("{e}")) } +/// Resolve a problem ref for `pred create`. +/// +/// Prefer reduction-graph-backed variants for problems already present in the +/// graph so `pred create` only emits loadable variants. Fall back to catalog +/// resolution for catalog-only problems that have not yet been connected by +/// reductions. +pub fn resolve_create_problem_ref( + input: &str, + graph: &problemreductions::rules::ReductionGraph, +) -> anyhow::Result { + let spec = parse_problem_spec(input)?; + if graph.variants_for(&spec.name).is_empty() { + let resolved = resolve_catalog_problem_ref(input)?; + return Ok(ProblemRef { + name: resolved.name().to_string(), + variant: resolved.variant().clone(), + }); + } + + resolve_problem_ref(input, graph) +} + /// Parse a problem spec string like "MIS/UnitDiskGraph/i32" into name + variant values. pub fn parse_problem_spec(input: &str) -> anyhow::Result { let parts: Vec<&str> = input.split('/').collect(); @@ -167,8 +190,6 @@ pub fn resolve_problem_ref( }) } -use problemreductions::export::ProblemRef; - /// A value parser that accepts any string but provides problem names as /// completion candidates for shell completion scripts. #[derive(Clone)] @@ -481,4 +502,19 @@ mod tests { ); assert_eq!(r.variant().get("weight").map(|s| s.as_str()), Some("One")); } + + #[test] + fn resolve_create_problem_ref_uses_catalog_for_problem_missing_from_graph() { + let graph = problemreductions::rules::ReductionGraph::new(); + let r = resolve_create_problem_ref("StrongConnectivityAugmentation", &graph).unwrap(); + assert_eq!(r.name, "StrongConnectivityAugmentation"); + assert_eq!(r.variant.get("weight").map(|s| s.as_str()), Some("i32")); + } + + #[test] + fn resolve_create_problem_ref_rejects_catalog_only_variant_for_graph_problem() { + let graph = problemreductions::rules::ReductionGraph::new(); + let err = resolve_create_problem_ref("MIS/TriangularSubgraph/One", &graph).unwrap_err(); + assert!(err.to_string().contains("Resolved variant")); + } } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 5ec2e911..89aed175 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -309,6 +309,128 @@ fn test_create_problem_strong_connectivity_augmentation() { assert_eq!(json["data"]["bound"], 1); } +#[test] +fn test_create_problem_strong_connectivity_augmentation_rejects_existing_candidate_arc() { + let output = pred() + .args([ + "create", + "StrongConnectivityAugmentation", + "--arcs", + "0>1,1>2", + "--candidate-arcs", + "0>1:5", + "--bound", + "1", + ]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "stdout: {}", + String::from_utf8_lossy(&output.stdout) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("already exists in the base graph"), + "stderr: {stderr}" + ); + assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); +} + +#[test] +fn test_create_problem_strong_connectivity_augmentation_rejects_negative_weight() { + let output = pred() + .args([ + "create", + "StrongConnectivityAugmentation", + "--arcs", + "0>1,1>2", + "--candidate-arcs", + "2>0:-5", + "--bound", + "0", + ]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "stdout: {}", + String::from_utf8_lossy(&output.stdout) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("must be positive"), "stderr: {stderr}"); +} + +#[test] +fn test_create_problem_strong_connectivity_augmentation_rejects_negative_bound() { + let output = pred() + .args([ + "create", + "StrongConnectivityAugmentation", + "--arcs", + "0>1,1>2", + "--candidate-arcs", + "2>0:1", + "--bound=-1", + ]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "stdout: {}", + String::from_utf8_lossy(&output.stdout) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("bound must be nonnegative"), + "stderr: {stderr}" + ); +} + +#[test] +fn test_create_problem_strong_connectivity_augmentation_rejects_oversized_bound() { + let output = pred() + .args([ + "create", + "StrongConnectivityAugmentation", + "--arcs", + "0>1,1>2", + "--candidate-arcs", + "2>0:1", + "--bound", + "3000000000", + ]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "stdout: {}", + String::from_utf8_lossy(&output.stdout) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("bound must fit in i32"), "stderr: {stderr}"); +} + +#[test] +fn test_create_rejects_catalog_only_variant_for_registered_problem() { + let output = pred() + .args([ + "create", + "MIS/TriangularSubgraph/One", + "--positions", + "0,0;1,0;0,1", + ]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "stdout: {}", + String::from_utf8_lossy(&output.stdout) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("Resolved variant"), "stderr: {stderr}"); +} + #[test] fn test_reduce() { let problem_json = r#"{ diff --git a/src/models/graph/strong_connectivity_augmentation.rs b/src/models/graph/strong_connectivity_augmentation.rs index f5337bba..39af1cde 100644 --- a/src/models/graph/strong_connectivity_augmentation.rs +++ b/src/models/graph/strong_connectivity_augmentation.rs @@ -8,7 +8,8 @@ use crate::topology::DirectedGraph; use crate::traits::{Problem, SatisfactionProblem}; use crate::types::WeightElement; use num_traits::Zero; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; +use std::cmp::Ordering; use std::collections::BTreeSet; inventory::submit! { @@ -35,7 +36,7 @@ inventory::submit! { /// `A`, and a bound `B`, determine whether some subset of the candidate arcs /// has total weight at most `B` and makes the augmented digraph strongly /// connected. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct StrongConnectivityAugmentation { graph: DirectedGraph, candidate_arcs: Vec<(usize, usize, W)>, @@ -43,44 +44,69 @@ pub struct StrongConnectivityAugmentation { } impl StrongConnectivityAugmentation { - /// Create a new strong connectivity augmentation instance. - /// - /// # Panics - /// - /// Panics if a candidate arc endpoint is out of range, if a candidate arc - /// already exists in the base graph, or if candidate arcs contain - /// duplicates. - pub fn new(graph: DirectedGraph, candidate_arcs: Vec<(usize, usize, W)>, bound: W::Sum) -> Self { + /// Fallible constructor used by CLI validation and deserialization. + pub fn try_new( + graph: DirectedGraph, + candidate_arcs: Vec<(usize, usize, W)>, + bound: W::Sum, + ) -> Result { + if !matches!( + bound.partial_cmp(&W::Sum::zero()), + Some(Ordering::Equal | Ordering::Greater) + ) { + return Err("bound must be nonnegative".to_string()); + } + let num_vertices = graph.num_vertices(); let mut seen_pairs = BTreeSet::new(); - for (u, v, _) in &candidate_arcs { - assert!( - *u < num_vertices && *v < num_vertices, - "candidate arc ({}, {}) references vertex >= num_vertices ({})", - u, - v, - num_vertices - ); - assert!( - !graph.has_arc(*u, *v), - "candidate arc ({}, {}) already exists in the base graph", - u, - v - ); - assert!( - seen_pairs.insert((*u, *v)), - "duplicate candidate arc ({}, {})", - u, - v - ); + for (u, v, weight) in &candidate_arcs { + if *u >= num_vertices || *v >= num_vertices { + return Err(format!( + "candidate arc ({}, {}) references vertex >= num_vertices ({})", + u, v, num_vertices + )); + } + if !matches!( + weight.to_sum().partial_cmp(&W::Sum::zero()), + Some(Ordering::Greater) + ) { + return Err(format!( + "candidate arc ({}, {}) weight must be positive", + u, v + )); + } + if graph.has_arc(*u, *v) { + return Err(format!( + "candidate arc ({}, {}) already exists in the base graph", + u, v + )); + } + if !seen_pairs.insert((*u, *v)) { + return Err(format!("duplicate candidate arc ({}, {})", u, v)); + } } - Self { + Ok(Self { graph, candidate_arcs, bound, - } + }) + } + + /// Create a new strong connectivity augmentation instance. + /// + /// # Panics + /// + /// Panics if a candidate arc endpoint is out of range, if a candidate arc + /// already exists in the base graph, or if candidate arcs contain + /// duplicates. + pub fn new( + graph: DirectedGraph, + candidate_arcs: Vec<(usize, usize, W)>, + bound: W::Sum, + ) -> Self { + Self::try_new(graph, candidate_arcs, bound).unwrap_or_else(|msg| panic!("{msg}")) } /// Get the base directed graph. @@ -177,6 +203,27 @@ crate::declare_variants! { default sat StrongConnectivityAugmentation => "2^num_potential_arcs", } +#[derive(Deserialize)] +struct StrongConnectivityAugmentationData { + graph: DirectedGraph, + candidate_arcs: Vec<(usize, usize, W)>, + bound: W::Sum, +} + +impl<'de, W> Deserialize<'de> for StrongConnectivityAugmentation +where + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let data = StrongConnectivityAugmentationData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.candidate_arcs, data.bound).map_err(serde::de::Error::custom) + } +} + #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { diff --git a/src/unit_tests/models/graph/strong_connectivity_augmentation.rs b/src/unit_tests/models/graph/strong_connectivity_augmentation.rs index 9573757d..d5959f2a 100644 --- a/src/unit_tests/models/graph/strong_connectivity_augmentation.rs +++ b/src/unit_tests/models/graph/strong_connectivity_augmentation.rs @@ -83,7 +83,7 @@ fn test_strong_connectivity_augmentation_issue_example_yes() { #[test] fn test_strong_connectivity_augmentation_issue_example_no() { let problem = issue_example_yes(); - assert!(!problem.evaluate(&vec![0; 18])); + assert!(!problem.evaluate(&[0; 18])); } #[test] From 8e77d0e333b105c6a35211aedc2465d8007a38d4 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 11:39:34 +0800 Subject: [PATCH 8/9] fix: address PR #652 review comments --- ...-03-16-strong-connectivity-augmentation.md | 419 ------------------ src/topology/directed_graph.rs | 36 +- .../graph/strong_connectivity_augmentation.rs | 5 - 3 files changed, 2 insertions(+), 458 deletions(-) delete mode 100644 docs/plans/2026-03-16-strong-connectivity-augmentation.md diff --git a/docs/plans/2026-03-16-strong-connectivity-augmentation.md b/docs/plans/2026-03-16-strong-connectivity-augmentation.md deleted file mode 100644 index 00018e6d..00000000 --- a/docs/plans/2026-03-16-strong-connectivity-augmentation.md +++ /dev/null @@ -1,419 +0,0 @@ -# StrongConnectivityAugmentation Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `StrongConnectivityAugmentation` model from issue #233 as a directed-graph satisfaction problem, with registry/example-db/CLI integration, paper documentation, and verification. - -**Architecture:** Reuse the repo's existing `DirectedGraph` wrapper for the base digraph, store augmentable weighted arcs explicitly, and treat each binary variable as "add this candidate arc". Evaluation should accept exactly those configurations whose selected candidate arcs stay within the bound and make the augmented digraph strongly connected. Keep the paper/example writing in a separate batch after the model, tests, exports, and fixtures are complete. - -**Tech Stack:** Rust workspace, `inventory` schema registry, `declare_variants!`, `DirectedGraph`, `BruteForce`, `pred create`, example-db fixtures, Typst paper, `make` verification targets. - ---- - -## Context And Required Decisions - -- Issue: `#233` (`[Model] StrongConnectivityAugmentation`) -- Associated rule issue exists: `#254` (`[Rule] HAMILTONIAN CIRCUIT to STRONG CONNECTIVITY AUGMENTATION`) -- Preflight guard already passed: `Good` label present -- Use the issue's 6-vertex directed example as the canonical paper/example-db instance -- Deliberate design deviation from the issue comment: use the repo-standard `DirectedGraph` wrapper instead of exposing raw `petgraph::DiGraph` in the public model schema -- CLI shape for this model: - - Base digraph: `--arcs "u>v,..."` - - Candidate weighted augmenting arcs: `--candidate-arcs "u>v:w,..."` - - Budget: reuse `--bound` -- Complexity registration: `default sat StrongConnectivityAugmentation => "2^num_potential_arcs"` - -## Batch Structure - -- **Batch 1:** Tasks 1-4 - - Implement topology primitive, model, registrations, fixtures, CLI/tests -- **Batch 2:** Task 5 - - Paper entry only, after Batch 1 outputs exist -- **Batch 3:** Task 6 - - Final verification, review, and cleanup checks - -### Task 1: Add Directed Strong-Connectivity Primitive - -**Files:** -- Modify: `src/topology/directed_graph.rs` -- Test: `src/unit_tests/topology/directed_graph.rs` - -**Step 1: Write the failing topology tests** - -Add focused tests in `src/unit_tests/topology/directed_graph.rs` for: -- a directed 3-cycle is strongly connected -- a one-way path is not strongly connected -- a single-vertex digraph is strongly connected -- an empty digraph is strongly connected (vacuous case) - -Use test names like: -- `test_is_strongly_connected_cycle` -- `test_is_strongly_connected_path` -- `test_is_strongly_connected_single_vertex` -- `test_is_strongly_connected_empty` - -**Step 2: Run the topology tests to verify RED** - -Run: -```bash -cargo test --features "ilp-highs example-db" test_is_strongly_connected --lib -``` - -Expected: -- FAIL because `DirectedGraph::is_strongly_connected()` does not exist yet - -**Step 3: Write the minimal implementation** - -In `src/topology/directed_graph.rs`: -- add `pub fn is_strongly_connected(&self) -> bool` -- treat `0` and `1` vertices as strongly connected -- implement with two traversals from vertex `0`: - - forward traversal via `successors()` - - reverse traversal via `predecessors()` -- return `true` only if both traversals visit every vertex - -Do not introduce a new graph wrapper or expose `inner`. - -**Step 4: Run the topology tests to verify GREEN** - -Run: -```bash -cargo test --features "ilp-highs example-db" test_is_strongly_connected --lib -``` - -Expected: -- PASS - -**Step 5: Commit** - -```bash -git add src/topology/directed_graph.rs src/unit_tests/topology/directed_graph.rs -git commit -m "feat: add directed strong connectivity check" -``` - -### Task 2: Implement The StrongConnectivityAugmentation Model - -**Files:** -- Create: `src/models/graph/strong_connectivity_augmentation.rs` -- Test: `src/unit_tests/models/graph/strong_connectivity_augmentation.rs` - -**Step 1: Write the failing model tests** - -Create `src/unit_tests/models/graph/strong_connectivity_augmentation.rs` with tests covering: -- creation/dims/getters -- valid issue example (`bound = 1`, exactly the `(5,2,1)` candidate chosen) -- invalid issue example (`bound = 0` or all-zero config) -- wrong-length config returns `false` -- already-strongly-connected base graph accepts the all-zero config -- serialization round-trip -- brute-force solver returns one satisfying configuration for the canonical example -- `variant()` matches `[("weight", "i32")]` -- paper-example parity test using the exact canonical instance - -Use the issue's candidate-arc order verbatim so the witness config is stable: -```text -(3,0,5), (3,1,3), (3,2,4), -(4,0,6), (4,1,2), (4,2,7), -(5,0,4), (5,1,3), (5,2,1), -(0,3,8), (0,4,3), (0,5,2), -(1,3,6), (1,4,4), (1,5,5), -(2,4,3), (2,5,7), (1,0,2) -``` - -Witness config for the YES instance: -```text -[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0] -``` - -**Step 2: Run the new model tests to verify RED** - -Run: -```bash -cargo test --features "ilp-highs example-db" strong_connectivity_augmentation --lib -``` - -Expected: -- FAIL because the model file and type do not exist yet - -**Step 3: Write the minimal model implementation** - -Create `src/models/graph/strong_connectivity_augmentation.rs` with: -- `inventory::submit!` schema entry -- `#[derive(Debug, Clone, Serialize, Deserialize)]` -- public struct: - - `graph: DirectedGraph` - - `candidate_arcs: Vec<(usize, usize, W)>` - - `bound: W` -- constructor validations: - - every candidate arc endpoint is in range - - no candidate arc duplicates an existing graph arc - - candidate arc pairs are unique -- getters/helpers: - - `graph()` - - `candidate_arcs()` - - `bound()` - - `num_vertices()` - - `num_arcs()` - - `num_potential_arcs()` - - `is_weighted()` - - `is_valid_solution()` -- `Problem` impl: - - `NAME = "StrongConnectivityAugmentation"` - - `Metric = bool` - - `variant() = crate::variant_params![W]` - - `dims() = vec![2; self.candidate_arcs.len()]` - - `evaluate()`: - - reject wrong-length configs - - sum selected candidate-arc weights and require `<= self.bound.to_sum()` - - build the augmented arc list from base arcs plus selected candidates - - return whether the augmented digraph is strongly connected -- `impl SatisfactionProblem` -- `declare_variants!` with the default `i32` variant -- `#[cfg(feature = "example-db")] canonical_model_example_specs()` using the issue example and `satisfaction_example(...)` -- test module link at the bottom - -Keep the base graph type as `DirectedGraph`; do not use raw `DiGraph` in the public schema. - -**Step 4: Run the model tests to verify GREEN** - -Run: -```bash -cargo test --features "ilp-highs example-db" strong_connectivity_augmentation --lib -``` - -Expected: -- PASS - -**Step 5: Commit** - -```bash -git add src/models/graph/strong_connectivity_augmentation.rs src/unit_tests/models/graph/strong_connectivity_augmentation.rs -git commit -m "feat: add strong connectivity augmentation model" -``` - -### Task 3: Register The Model, Example DB, And Trait Checks - -**Files:** -- Modify: `src/models/graph/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` -- Modify: `src/unit_tests/trait_consistency.rs` -- Modify: `src/unit_tests/example_db.rs` -- Modify: `src/example_db/fixtures/examples.json` - -**Step 1: Write or extend failing registration checks** - -Add or extend assertions so the new model is exercised by existing infrastructure: -- in `src/unit_tests/trait_consistency.rs`, add a `check_problem_trait(...)` case using a tiny directed example -- in `src/unit_tests/example_db.rs`, add a `find_model_example(...)` test for `StrongConnectivityAugmentation/i32` - -**Step 2: Run the focused registration tests to verify RED** - -Run: -```bash -cargo test --features "ilp-highs example-db" trait_consistency -cargo test --features "ilp-highs example-db" test_find_model_example_strong_connectivity_augmentation -``` - -Expected: -- FAIL because the model is not exported/registered everywhere yet - -**Step 3: Register the model and canonical example** - -Update: -- `src/models/graph/mod.rs` - - add module/export entries - - append `strong_connectivity_augmentation::canonical_model_example_specs()` -- `src/models/mod.rs` - - re-export `StrongConnectivityAugmentation` -- `src/lib.rs` - - add to `prelude` -- `src/unit_tests/trait_consistency.rs` - - add the new satisfaction problem instance -- `src/unit_tests/example_db.rs` - - add a lookup/assertion for the canonical example - -**Step 4: Regenerate fixtures** - -Run: -```bash -make regenerate-fixtures -``` - -Expected: -- `src/example_db/fixtures/examples.json` updates to include the new canonical model example - -**Step 5: Run the focused registration tests to verify GREEN** - -Run: -```bash -cargo test --features "ilp-highs example-db" trait_consistency -cargo test --features "ilp-highs example-db" test_find_model_example_strong_connectivity_augmentation -``` - -Expected: -- PASS - -**Step 6: Commit** - -```bash -git add src/models/graph/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/trait_consistency.rs src/unit_tests/example_db.rs src/example_db/fixtures/examples.json -git commit -m "feat: register strong connectivity augmentation" -``` - -### Task 4: Add `pred create` Support And CLI Smoke Coverage - -**Files:** -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` -- Test: `problemreductions-cli/tests/cli_tests.rs` - -**Step 1: Write the failing CLI smoke test** - -Add a new test to `problemreductions-cli/tests/cli_tests.rs` that runs something like: -```bash -pred create StrongConnectivityAugmentation \ - --arcs "0>1,1>2,2>0,3>4,4>3,2>3,4>5,5>3" \ - --candidate-arcs "3>0:5,3>1:3,3>2:4,4>0:6,4>1:2,4>2:7,5>0:4,5>1:3,5>2:1,0>3:8,0>4:3,0>5:2,1>3:6,1>4:4,1>5:5,2>4:3,2>5:7,1>0:2" \ - --bound 1 -``` - -Assert that: -- the command succeeds -- the JSON `type` is `StrongConnectivityAugmentation` -- the emitted data contains `graph`, `candidate_arcs`, and `bound` - -**Step 2: Run the CLI test to verify RED** - -Run: -```bash -cargo test -p problemreductions-cli test_create_problem_strong_connectivity_augmentation -``` - -Expected: -- FAIL because the new CLI flag/parser path does not exist yet - -**Step 3: Implement CLI support** - -In `problemreductions-cli/src/cli.rs`: -- add `candidate_arcs: Option` to `CreateArgs` -- include it in `all_data_flags_empty()` -- update the `Flags by problem type` help block - -In `problemreductions-cli/src/commands/create.rs`: -- add `type_format_hint()` support for the candidate-arc field format -- add `example_for("StrongConnectivityAugmentation", ...)` -- add a parser for `--candidate-arcs "u>v:w,..."` -- add a new `create()` match arm for `StrongConnectivityAugmentation` -- reuse `parse_directed_graph()` for the base graph and `--bound` for the budget - -Do not add a short alias unless there is a literature-standard abbreviation. - -**Step 4: Run the CLI test to verify GREEN** - -Run: -```bash -cargo test -p problemreductions-cli test_create_problem_strong_connectivity_augmentation -``` - -Expected: -- PASS - -**Step 5: Commit** - -```bash -git add problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs problemreductions-cli/tests/cli_tests.rs -git commit -m "feat: add CLI support for strong connectivity augmentation" -``` - -### Task 5: Document The Model In The Paper - -**Files:** -- Modify: `docs/paper/reductions.typ` - -**Step 1: Write the paper entry using the canonical exported example** - -Add: -- `"StrongConnectivityAugmentation": [Strong Connectivity Augmentation],` to the `display-name` dictionary -- a new `#problem-def("StrongConnectivityAugmentation")[...][...]` block - -Use the exported canonical example, not a hardcoded duplicate. The body should include: -- formal decision version with bound `B` -- short historical/application background citing Eswaran-Tarjan and Garey-Johnson -- brute-force complexity statement `2^num_potential_arcs` plus a note that no stronger exact bound is being claimed here -- the 6-vertex worked example with the single selected augmenting arc `(5,2)` -- a directed-graph figure in the style of `MinimumFeedbackVertexSet` - -**Step 2: Run the paper build to verify RED/GREEN as needed** - -Run: -```bash -make paper -``` - -Expected: -- initially FAIL until the display-name/problem-def entry is complete -- finally PASS once the paper entry is correct - -**Step 3: Commit** - -```bash -git add docs/paper/reductions.typ -git commit -m "docs: add strong connectivity augmentation to paper" -``` - -### Task 6: Final Verification And Review - -**Files:** -- No new files expected; fix anything verification or review finds - -**Step 1: Run focused model and CLI checks** - -Run: -```bash -cargo test --features "ilp-highs example-db" strong_connectivity_augmentation -cargo test -p problemreductions-cli test_create_problem_strong_connectivity_augmentation -``` - -Expected: -- PASS - -**Step 2: Run repo verification** - -Run: -```bash -make test -make clippy -``` - -Expected: -- PASS - -**Step 3: Run implementation review** - -Use the repo-local review skill directly: -```text -.claude/skills/review-implementation/SKILL.md -``` - -Auto-fix any actionable findings before moving on. - -**Step 4: If review fixes were needed, rerun verification** - -Run: -```bash -make test -make clippy -``` - -Expected: -- PASS - -**Step 5: Commit review-driven fixes if needed** - -```bash -git add -A -git commit -m "fix: address strong connectivity augmentation review findings" -``` - -Only make this commit if review or verification required additional changes. diff --git a/src/topology/directed_graph.rs b/src/topology/directed_graph.rs index 5af200b3..3e136146 100644 --- a/src/topology/directed_graph.rs +++ b/src/topology/directed_graph.rs @@ -13,7 +13,7 @@ //! [`MinimumFeedbackVertexSet`]: crate::models::graph::MinimumFeedbackVertexSet //! [`MinimumFeedbackArcSet`]: crate::models::graph::MinimumFeedbackArcSet -use petgraph::algo::toposort; +use petgraph::algo::{kosaraju_scc, toposort}; use petgraph::graph::{DiGraph, NodeIndex}; use petgraph::visit::EdgeRef; use serde::{Deserialize, Serialize}; @@ -145,39 +145,7 @@ impl DirectedGraph { /// Returns `true` if every vertex can reach every other vertex. pub fn is_strongly_connected(&self) -> bool { - let n = self.num_vertices(); - if n <= 1 { - return true; - } - - fn visit( - graph: &DirectedGraph, - start: usize, - neighbors: impl Fn(&DirectedGraph, usize) -> Vec, - ) -> Vec { - let mut seen = vec![false; graph.num_vertices()]; - let mut stack = vec![start]; - seen[start] = true; - - while let Some(v) = stack.pop() { - for u in neighbors(graph, v) { - if !seen[u] { - seen[u] = true; - stack.push(u); - } - } - } - - seen - } - - let forward = visit(self, 0, DirectedGraph::successors); - if forward.iter().any(|&seen| !seen) { - return false; - } - - let reverse = visit(self, 0, DirectedGraph::predecessors); - reverse.iter().all(|&seen| seen) + kosaraju_scc(&self.inner).len() <= 1 } /// Check if the subgraph induced by keeping only the given arcs is acyclic (a DAG). diff --git a/src/unit_tests/models/graph/strong_connectivity_augmentation.rs b/src/unit_tests/models/graph/strong_connectivity_augmentation.rs index d5959f2a..faa4763c 100644 --- a/src/unit_tests/models/graph/strong_connectivity_augmentation.rs +++ b/src/unit_tests/models/graph/strong_connectivity_augmentation.rs @@ -136,11 +136,6 @@ fn test_strong_connectivity_augmentation_paper_example() { let config = yes_config(); assert!(problem.evaluate(&config)); - - let solver = BruteForce::new(); - let all_satisfying = solver.find_all_satisfying(&problem); - assert_eq!(all_satisfying.len(), 1); - assert_eq!(all_satisfying[0], config); } #[test] From 93a6dd4ab9a2936e71f513aab175f1d560d3885b Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 12:16:23 +0800 Subject: [PATCH 9/9] docs: improve StrongConnectivityAugmentation CLI guidance --- README.md | 7 +++++++ docs/src/cli.md | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/README.md b/README.md index 94d4cb1f..0667e7cd 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,13 @@ cd problem-reductions make cli # builds target/release/pred ``` +If you prefer a workspace-local build without installing into your global Cargo bin directory: + +```bash +cargo build -p problemreductions-cli --release +./target/release/pred --version +``` + See the [Getting Started](https://codingthrust.github.io/problem-reductions/getting-started.html) guide for usage examples, the reduction workflow, and [CLI usage](https://codingthrust.github.io/problem-reductions/cli.html). ## MCP Server (AI Integration) diff --git a/docs/src/cli.md b/docs/src/cli.md index 6507c61f..d7ebd9f9 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -18,6 +18,13 @@ cd problem-reductions make cli # builds target/release/pred ``` +If you prefer a workspace-local build without installing into your global Cargo bin directory: + +```bash +cargo build -p problemreductions-cli --release +./target/release/pred --version +``` + Verify the installation: ```bash @@ -276,6 +283,7 @@ pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json pred create X3C --universe 9 --sets "0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8" -o x3c.json pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 --precedence-pairs "0>3,1>3,1>4,2>4" -o mts.json +pred create StrongConnectivityAugmentation --arcs "0>1,1>2,2>0,3>4,4>3,2>3,4>5,5>3" --candidate-arcs "3>0:5,3>1:3,3>2:4,4>0:6,4>1:2,4>2:7,5>0:4,5>1:3,5>2:1,0>3:8,0>4:3,0>5:2,1>3:6,1>4:4,1>5:5,2>4:3,2>5:7,1>0:2" --bound 1 -o sca.json ``` Canonical examples are useful when you want a known-good instance from the paper/example database.