From 94e0893bcb43695989db19c2adc3134234173dd7 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 06:01:52 +0800 Subject: [PATCH 1/6] Add plan for #239: [Model] BalancedCompleteBipartiteSubgraph --- ...16-balanced-complete-bipartite-subgraph.md | 382 ++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 docs/plans/2026-03-16-balanced-complete-bipartite-subgraph.md diff --git a/docs/plans/2026-03-16-balanced-complete-bipartite-subgraph.md b/docs/plans/2026-03-16-balanced-complete-bipartite-subgraph.md new file mode 100644 index 000000000..4af03a458 --- /dev/null +++ b/docs/plans/2026-03-16-balanced-complete-bipartite-subgraph.md @@ -0,0 +1,382 @@ +# BalancedCompleteBipartiteSubgraph Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add `BalancedCompleteBipartiteSubgraph` as a new graph satisfaction model for the GT24 decision problem, with registry/CLI/example-db integration, paper documentation, and issue-backed tests. + +**Architecture:** Implement a concrete `BipartiteGraph`-based satisfaction problem with fields `graph: BipartiteGraph` and `k: usize`. The configuration is one binary decision per vertex: `config[0..left_size]` selects the left side of the biclique and `config[left_size..left_size + right_size]` selects the right side. A configuration is satisfying iff exactly `k` left vertices and exactly `k` right vertices are selected and every selected left/right pair is an edge of the bipartite graph. Register the model through `ProblemSchemaEntry`, `declare_variants!`, graph module exports, example-db hooks, CLI creation, and the paper. + +**Tech Stack:** Rust, serde, inventory registry, `BipartiteGraph`, brute-force solver, Typst, `pred` CLI. + +--- + +## Required workflow references + +- Implementation skill: `.claude/skills/add-model/SKILL.md` Steps 1-7 +- Execution skill: `superpowers:subagent-driven-development` +- Per-task discipline: `superpowers:test-driven-development` + +## Issue-derived decisions to preserve + +- Treat this as the **decision** problem from issue `#239`, not an optimization variant. +- Use the binary left-then-right encoding from the corrected issue text. +- Keep the model concrete over `BipartiteGraph`; do not introduce a generic graph type parameter. +- Do not edit `problemreductions-cli/src/problem_name.rs`: alias resolution is registry-backed now. +- Companion rule issue already exists: `#231 [Rule] CLIQUE to BALANCED COMPLETE BIPARTITE SUBGRAPH`. +- For the canonical paper/example-db instance, use **Issue Instance 2** so the satisfying witness is unique: + - `A = {a0,a1,a2,a3}` + - `B = {b0,b1,b2,b3}` + - edges `(0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2),(3,0),(3,1),(3,3)` + - `k = 3` + - satisfying sets `{a0,a1,a2}` and `{b0,b1,b2}` +- Keep **Issue Instance 1** as the smaller regression case: + - `k = 2` is satisfiable, for example with `{a0,a1}` and `{b0,b1}` + - `k = 3` is unsatisfiable on that same graph +- Complexity caution: the `O*(1.3803^n)` Chen et al. bound is issue-reviewed as **dense-graph-specific**. Unless the source check proves it is a valid worst-case bound for the general problem, keep `declare_variants!` conservative (`"2^num_vertices"`) and mention the dense-case algorithm only in paper prose. + +## Batch structure + +- **Batch 1:** `.claude/skills/add-model/SKILL.md` Steps 1-5.5 + - model implementation + - registry/module wiring + - example-db registration + - CLI creation support + - unit tests + - trait consistency / problem size coverage +- **Batch 2:** `.claude/skills/add-model/SKILL.md` Step 6 + - paper entry + - bibliography updates + - paper-aligned test finalization + +### Task 1: Model Semantics and Red Tests + +**Files:** +- Create: `src/models/graph/balanced_complete_bipartite_subgraph.rs` +- Create: `src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs` +- Modify: `src/models/graph/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` + +**Step 1: Write the failing model tests first** + +Add a new unit test file that covers these exact behaviors before the model exists: + +- `test_balanced_complete_bipartite_subgraph_creation` + - construct `BipartiteGraph::new(4, 4, vec![...])` + - assert `left_size() == 4`, `right_size() == 4`, `num_vertices() == 8`, `num_edges()` matches edge count, `k() == 2 or 3` + - assert `dims() == vec![2; 8]` +- `test_balanced_complete_bipartite_subgraph_evaluation_yes_instance` + - Issue Instance 1 with `k = 2` + - config `[1,1,0,0, 1,1,0,0]` must satisfy +- `test_balanced_complete_bipartite_subgraph_evaluation_no_instance` + - same graph as Instance 1 with `k = 3` + - any candidate using `{a0,a1,a2}` and `{b0,b1,b2}` must fail because edge `(1,2)` is missing +- `test_balanced_complete_bipartite_subgraph_invalid_pairing` + - choose balanced left/right sets of size `k`, but include at least one missing cross edge; `evaluate()` must return `false` +- `test_balanced_complete_bipartite_subgraph_solver_yes_instance` + - `BruteForce::find_satisfying()` returns `Some` + - `BruteForce::find_all_satisfying()` on Instance 2 returns exactly `1` solution +- `test_balanced_complete_bipartite_subgraph_solver_no_instance` + - on Instance 1 with `k = 3`, `find_satisfying()` returns `None` +- `test_balanced_complete_bipartite_subgraph_serialization` + - serde round-trip preserves `k`, partition sizes, and edges +- `test_balanced_complete_bipartite_subgraph_is_valid_solution` + - delegates to `evaluate()` +- `test_balanced_complete_bipartite_subgraph_paper_example` + - reuse the exact Issue Instance 2 canonical example + - assert the issue witness satisfies the problem + - assert `find_all_satisfying()` returns `1` + +**Step 2: Run the targeted tests to verify RED** + +Run: + +```bash +cargo test balanced_complete_bipartite_subgraph --lib +``` + +Expected: compile failure because the model module/type does not exist yet. + +**Step 3: Implement the minimal model to make the tests pass** + +Create `src/models/graph/balanced_complete_bipartite_subgraph.rs` with this shape: + +- `inventory::submit!` for `ProblemSchemaEntry` + - `name: "BalancedCompleteBipartiteSubgraph"` + - `display_name: "Balanced Complete Bipartite Subgraph"` + - `aliases: &[]` + - `dimensions: &[]` + - `description: "Decide whether a bipartite graph contains a K_{k,k} subgraph"` + - fields mirroring the user-facing bipartite input style used by `BicliqueCover`: + - `left_size` + - `right_size` + - `edges` + - `k` +- `#[derive(Debug, Clone, Serialize, Deserialize)]` +- `pub struct BalancedCompleteBipartiteSubgraph { graph: BipartiteGraph, k: usize }` +- inherent methods: + - `new(graph: BipartiteGraph, k: usize) -> Self` + - `graph(&self) -> &BipartiteGraph` + - `left_size(&self) -> usize` + - `right_size(&self) -> usize` + - `num_vertices(&self) -> usize` + - `num_edges(&self) -> usize` + - `k(&self) -> usize` + - helper to split a config into selected left/right vertex indices + - `is_valid_solution(&self, config: &[usize]) -> bool` +- `impl Problem` + - `const NAME = "BalancedCompleteBipartiteSubgraph"` + - `type Metric = bool` + - `dims() -> vec![2; self.num_vertices()]` + - `evaluate()` must: + - reject wrong config length + - reject any non-binary entry + - count selected left and right vertices separately + - require both counts equal `k` + - verify every selected pair `(left, right)` exists in the bipartite graph + - `variant() -> crate::variant_params![]` +- `impl SatisfactionProblem for BalancedCompleteBipartiteSubgraph {}` +- `crate::declare_variants! { default sat BalancedCompleteBipartiteSubgraph => "2^num_vertices", }` +- `#[cfg(feature = "example-db")] canonical_model_example_specs()` + - use `crate::example_db::specs::satisfaction_example(...)` + - encode Issue Instance 2 and its unique satisfying configuration +- test link at the bottom: + - `#[cfg(test)] #[path = "../../unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs"] mod tests;` + +**Step 4: Wire the model into graph exports** + +Update: + +- `src/models/graph/mod.rs` + - add `pub(crate) mod balanced_complete_bipartite_subgraph;` + - add `pub use balanced_complete_bipartite_subgraph::BalancedCompleteBipartiteSubgraph;` + - extend `canonical_model_example_specs()` with the new model helper +- `src/models/mod.rs` + - add `BalancedCompleteBipartiteSubgraph` to the graph re-export list +- `src/lib.rs` + - add `BalancedCompleteBipartiteSubgraph` to the `prelude` graph exports + +**Step 5: Run the same tests to verify GREEN** + +Run: + +```bash +cargo test balanced_complete_bipartite_subgraph --lib +``` + +Expected: the new model unit tests pass. + +**Step 6: Commit** + +```bash +git add src/models/graph/balanced_complete_bipartite_subgraph.rs \ + src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs \ + src/models/graph/mod.rs src/models/mod.rs src/lib.rs +git commit -m "Add BalancedCompleteBipartiteSubgraph model" +``` + +### Task 2: Registry Completeness, Example DB, Trait Checks, and CLI Creation + +**Files:** +- Modify: `src/unit_tests/trait_consistency.rs` +- Modify: `src/unit_tests/problem_size.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/src/cli.rs` + +**Step 1: Write the failing integration tests first** + +Add or extend tests before editing the CLI/create logic: + +- `src/unit_tests/trait_consistency.rs` + - add one `check_problem_trait(...)` call for `BalancedCompleteBipartiteSubgraph::new(BipartiteGraph::new(...), 2)` +- `src/unit_tests/problem_size.rs` + - add `test_problem_size_balanced_complete_bipartite_subgraph` + - assert `left_size`, `right_size`, `num_edges`, and `k` appear with the correct values +- `problemreductions-cli/src/commands/create.rs` + - add a small unit test module for `pred create BalancedCompleteBipartiteSubgraph --left 4 --right 4 --biedges ... --k 3` + - parse the emitted JSON and assert the created problem type is `BalancedCompleteBipartiteSubgraph` + - assert the serialized instance round-trips into the new model and preserves `k`, partitions, and edges + +**Step 2: Run the targeted tests to verify RED** + +Run: + +```bash +cargo test test_problem_size_balanced_complete_bipartite_subgraph --lib +cargo test test_all_problems_implement_trait_correctly --lib +cargo test -p problemreductions-cli balanced_complete_bipartite_subgraph +``` + +Expected: failures because the trait/problem-size entries and CLI create arm do not exist yet. + +**Step 3: Implement the wiring** + +Update `src/unit_tests/trait_consistency.rs`: + +- add a `check_problem_trait(...)` entry +- do **not** add a `test_direction` assertion because this is a satisfaction problem + +Update `src/unit_tests/problem_size.rs`: + +- add a new problem-size test covering the issue-backed example graph + +Update `problemreductions-cli/src/commands/create.rs`: + +- add a new match arm: + - `"BalancedCompleteBipartiteSubgraph" => { ... }` +- require: + - `--left` + - `--right` + - `--biedges` + - `--k` +- parse bipartite-local edges with the same helper used by `BicliqueCover` +- construct `BipartiteGraph::new(left, right, edges)` +- serialize `BalancedCompleteBipartiteSubgraph::new(graph, k)` +- use an error message and usage string parallel to `BicliqueCover` + +Update `problemreductions-cli/src/cli.rs`: + +- extend the “Flags by problem type” table with: + - `BalancedCompleteBipartiteSubgraph --left, --right, --biedges, --k` + +Do **not** edit `problemreductions-cli/src/problem_name.rs` unless a test proves a real alias-resolution gap. Canonical-name lookup is already case-insensitive through the registry. + +**Step 4: Run the targeted tests to verify GREEN** + +Run: + +```bash +cargo test test_problem_size_balanced_complete_bipartite_subgraph --lib +cargo test test_all_problems_implement_trait_correctly --lib +cargo test -p problemreductions-cli balanced_complete_bipartite_subgraph +``` + +Expected: all targeted tests pass. + +**Step 5: Commit** + +```bash +git add src/unit_tests/trait_consistency.rs src/unit_tests/problem_size.rs \ + problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs +git commit -m "Wire BalancedCompleteBipartiteSubgraph into CLI and registry checks" +``` + +### Task 3: Paper Entry, Citations, and Paper-Aligned Example + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `docs/paper/references.bib` +- Modify: `src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs` + +**Step 1: Write or finalize the failing paper-aligned test first** + +Before editing the paper, make sure the `test_balanced_complete_bipartite_subgraph_paper_example` test is locked to the exact canonical example chosen above: + +- use Issue Instance 2 only +- assert the witness `{a0,a1,a2}` / `{b0,b1,b2}` is satisfying +- assert `find_all_satisfying()` returns exactly `1` + +**Step 2: Run the focused test to verify RED if the example changed** + +Run: + +```bash +cargo test test_balanced_complete_bipartite_subgraph_paper_example --lib +``` + +Expected: fail if the example data or expected satisfying count is not yet aligned. + +**Step 3: Implement the paper entry** + +Update `docs/paper/reductions.typ`: + +- add display name entry: + - `"BalancedCompleteBipartiteSubgraph": [Balanced Complete Bipartite Subgraph],` +- add a new `#problem-def("BalancedCompleteBipartiteSubgraph")[ ... ][ ... ]` +- follow the structure/style of: + - `MaximumIndependentSet` for overall quality + - `GraphPartitioning` for balanced-language phrasing + - `BicliqueCover` for bipartite layout and figure structure + - `SubgraphIsomorphism` / `HamiltonianPath` for satisfaction-problem tone +- definition should state: + - input bipartite graph `G = (A, B, E)` + - integer `k` + - question whether there exist `A' subseteq A` and `B' subseteq B` with `|A'| = |B'| = k` and `A' times B' subseteq E` +- background should: + - mention the classical Garey-Johnson listing and the relationship to biclique search + - avoid over-claiming the GT24 tag if the reference check remains ambiguous +- exact algorithm sentence should: + - mention brute force / subset enumeration unconditionally + - mention Chen et al. only with a dense-graph qualifier if cited +- add a CeTZ figure using the bipartite-node layout style from `BicliqueCover` + - draw edges first + - highlight the six selected vertices and the nine biclique edges for Instance 2 + - explain why `a3` is excluded + +Update `docs/paper/references.bib` only as needed: + +- `@book{garey1979}` already exists +- add `chen2020` only if the paper prose mentions the dense exact algorithm +- do not add Lin/Manurangsi unless the final prose cites parameterized or approximation results + +**Step 4: Verify the paper entry and the aligned test** + +Run: + +```bash +cargo test test_balanced_complete_bipartite_subgraph_paper_example --lib +make paper +``` + +Expected: the focused test passes and the Typst paper builds cleanly. + +**Step 5: Commit** + +```bash +git add docs/paper/reductions.typ docs/paper/references.bib \ + src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs +git commit -m "Document BalancedCompleteBipartiteSubgraph in the paper" +``` + +### Task 4: Full Verification Before Review + +**Files:** +- No new files; verification only + +**Step 1: Run the full verification suite** + +Run: + +```bash +cargo test +make clippy +make export-schemas +make paper +``` + +Expected: + +- `cargo test` passes across the workspace +- `make clippy` passes +- `make export-schemas` updates checked-in schema/export artifacts if needed +- `make paper` passes after any schema/export changes + +**Step 2: Inspect generated diffs** + +Check for expected generated-file updates, especially: + +- `docs/src/reductions/problem_schemas.json` +- `src/example_db/fixtures/examples.json` if example-db exports were regenerated indirectly + +Only keep generated-file changes that are actually required by the new model. + +**Step 3: Commit the final implementation batch** + +```bash +git add -A +git commit -m "Finish BalancedCompleteBipartiteSubgraph integration" +``` + +**Step 4: Hand off to review** + +After this plan is executed, run the repo-local `review-implementation` skill before pushing the implementation summary comment. From 3de89bc0202fe7b9ab789b7763e5de6e9e7857ac Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 06:22:50 +0800 Subject: [PATCH 2/6] Implement #239: [Model] BalancedCompleteBipartiteSubgraph --- docs/paper/reductions.typ | 67 +++++ docs/src/reductions/problem_schemas.json | 26 ++ docs/src/reductions/reduction_graph.json | 243 +++++++++--------- problemreductions-cli/src/cli.rs | 1 + problemreductions-cli/src/commands/create.rs | 118 +++++++++ src/example_db/fixtures/examples.json | 1 + src/lib.rs | 4 +- .../balanced_complete_bipartite_subgraph.rs | 160 ++++++++++++ src/models/graph/mod.rs | 4 + src/models/mod.rs | 10 +- .../balanced_complete_bipartite_subgraph.rs | 145 +++++++++++ src/unit_tests/trait_consistency.rs | 7 + 12 files changed, 661 insertions(+), 125 deletions(-) create mode 100644 src/models/graph/balanced_complete_bipartite_subgraph.rs create mode 100644 src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index b3a286b66..6139485fe 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -89,6 +89,7 @@ "BMF": [Boolean Matrix Factorization], "PaintShop": [Paint Shop], "BicliqueCover": [Biclique Cover], + "BalancedCompleteBipartiteSubgraph": [Balanced Complete Bipartite Subgraph], "BinPacking": [Bin Packing], "ClosestVectorProblem": [Closest Vector Problem], "OptimalLinearArrangement": [Optimal Linear Arrangement], @@ -1517,6 +1518,72 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("BalancedCompleteBipartiteSubgraph") + let left-size = x.instance.graph.left_size + let right-size = x.instance.graph.right_size + let k = x.instance.k + let bip-edges = x.instance.graph.edges + let sol = x.optimal.at(0) + let left-selected = range(left-size).filter(i => sol.config.at(i) == 1) + let right-selected = range(right-size).filter(i => sol.config.at(left-size + i) == 1) + let selected-edges = bip-edges.filter(e => + left-selected.contains(e.at(0)) and right-selected.contains(e.at(1)) + ) + [ + #problem-def("BalancedCompleteBipartiteSubgraph")[ + Given a bipartite graph $G = (A, B, E)$ and an integer $k$, determine whether there exist subsets $A' subset.eq A$ and $B' subset.eq B$ such that $|A'| = |B'| = k$ and every cross pair is present: + $A' times B' subset.eq E.$ + ][ + Balanced Complete Bipartite Subgraph is a classical NP-complete bipartite containment problem from Garey and Johnson @garey1979. Unlike Biclique Cover, which asks for a collection of bicliques covering all edges, this problem asks for a _single_ balanced biclique of prescribed size. It arises naturally in biclustering, dense submatrix discovery, and pattern mining on bipartite data. A straightforward exact algorithm enumerates all $k$-subsets of $A$ and $B$ and checks whether they induce a complete bipartite graph, taking $O(binom(|A|, k) dot binom(|B|, k) dot k^2) = O^*(2^(|A| + |B|))$ time#footnote[No faster unconditional worst-case exact algorithm is asserted here for the general problem.]. + + *Example.* Consider the bipartite graph with $A = {ell_1, ell_2, ell_3, ell_4}$, $B = {r_1, r_2, r_3, r_4}$, and edges $E = {#bip-edges.map(e => $(ell_#(e.at(0) + 1), r_#(e.at(1) + 1))$).join(", ")}$. For $k = #k$, the selected sets $A' = {#left-selected.map(i => $ell_#(i + 1)$).join(", ")}$ and $B' = {#right-selected.map(i => $r_#(i + 1)$).join(", ")}$ form a balanced complete bipartite subgraph: all #selected-edges.len() required cross edges are present. Vertex $ell_4$ is excluded because $(ell_4, r_3) in.not E$, so any witness using $ell_4$ cannot realize $K_(#k,#k)$. + + #figure( + canvas(length: 1cm, { + let lpos = range(left-size).map(i => (0, left-size - 1 - i)) + let rpos = range(right-size).map(i => (2.6, right-size - 1 - i)) + for (li, rj) in bip-edges { + let selected = selected-edges.any(e => e.at(0) == li and e.at(1) == rj) + g-edge( + lpos.at(li), + rpos.at(rj), + stroke: if selected { 2pt + graph-colors.at(0) } else { 1pt + luma(180) }, + ) + } + for (idx, pos) in lpos.enumerate() { + let selected = left-selected.contains(idx) + g-node( + pos, + name: "bcbs-l" + str(idx), + fill: if selected { graph-colors.at(0) } else { luma(240) }, + label: if selected { + text(fill: white)[$ell_#(idx + 1)$] + } else { + [$ell_#(idx + 1)$] + }, + ) + } + for (idx, pos) in rpos.enumerate() { + let selected = right-selected.contains(idx) + g-node( + pos, + name: "bcbs-r" + str(idx), + fill: if selected { graph-colors.at(0) } else { luma(240) }, + label: if selected { + text(fill: white)[$r_#(idx + 1)$] + } else { + [$r_#(idx + 1)$] + }, + ) + } + }), + caption: [Balanced complete bipartite subgraph with $k = #k$: the selected vertices $A' = {#left-selected.map(i => $ell_#(i + 1)$).join(", ")}$ and $B' = {#right-selected.map(i => $r_#(i + 1)$).join(", ")}$ are blue, and the 9 edges of the induced $K_(#k,#k)$ are highlighted. The missing edge $(ell_4, r_3)$ prevents including $ell_4$.], + ) + ] + ] +} + #{ let x = load-model-example("PartitionIntoTriangles") let nv = graph-num-vertices(x.instance) diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 3a6e9df87..234378934 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -15,6 +15,32 @@ } ] }, + { + "name": "BalancedCompleteBipartiteSubgraph", + "description": "Decide whether a bipartite graph contains a K_{k,k} subgraph", + "fields": [ + { + "name": "left_size", + "type_name": "usize", + "description": "Vertices in left partition" + }, + { + "name": "right_size", + "type_name": "usize", + "description": "Vertices in right partition" + }, + { + "name": "edges", + "type_name": "Vec<(usize, usize)>", + "description": "Bipartite edges" + }, + { + "name": "k", + "type_name": "usize", + "description": "Balanced biclique size" + } + ] + }, { "name": "BicliqueCover", "description": "Cover bipartite edges with k bicliques", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index c1334cfb2..d9fc6b6a1 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -7,6 +7,13 @@ "doc_path": "models/algebraic/struct.BMF.html", "complexity": "2^(rows * rank + rank * cols)" }, + { + "name": "BalancedCompleteBipartiteSubgraph", + "variant": {}, + "category": "graph", + "doc_path": "models/graph/struct.BalancedCompleteBipartiteSubgraph.html", + "complexity": "2^num_vertices" + }, { "name": "BicliqueCover", "variant": {}, @@ -518,8 +525,8 @@ ], "edges": [ { - "source": 3, - "target": 12, + "source": 4, + "target": 13, "overhead": [ { "field": "num_vars", @@ -533,8 +540,8 @@ "doc_path": "rules/binpacking_ilp/index.html" }, { - "source": 4, - "target": 12, + "source": 5, + "target": 13, "overhead": [ { "field": "num_vars", @@ -548,8 +555,8 @@ "doc_path": "rules/circuit_ilp/index.html" }, { - "source": 4, - "target": 54, + "source": 5, + "target": 55, "overhead": [ { "field": "num_spins", @@ -563,8 +570,8 @@ "doc_path": "rules/circuit_spinglass/index.html" }, { - "source": 8, - "target": 4, + "source": 9, + "target": 5, "overhead": [ { "field": "num_variables", @@ -578,8 +585,8 @@ "doc_path": "rules/factoring_circuit/index.html" }, { - "source": 8, - "target": 13, + "source": 9, + "target": 14, "overhead": [ { "field": "num_vars", @@ -593,8 +600,8 @@ "doc_path": "rules/factoring_ilp/index.html" }, { - "source": 12, - "target": 13, + "source": 13, + "target": 14, "overhead": [ { "field": "num_vars", @@ -608,8 +615,8 @@ "doc_path": "rules/ilp_bool_ilp_i32/index.html" }, { - "source": 12, - "target": 49, + "source": 13, + "target": 50, "overhead": [ { "field": "num_vars", @@ -619,8 +626,8 @@ "doc_path": "rules/ilp_qubo/index.html" }, { - "source": 16, - "target": 19, + "source": 17, + "target": 20, "overhead": [ { "field": "num_vertices", @@ -634,8 +641,8 @@ "doc_path": "rules/kcoloring_casts/index.html" }, { - "source": 19, - "target": 12, + "source": 20, + "target": 13, "overhead": [ { "field": "num_vars", @@ -649,8 +656,8 @@ "doc_path": "rules/coloring_ilp/index.html" }, { - "source": 19, - "target": 49, + "source": 20, + "target": 50, "overhead": [ { "field": "num_vars", @@ -660,8 +667,8 @@ "doc_path": "rules/coloring_qubo/index.html" }, { - "source": 20, - "target": 22, + "source": 21, + "target": 23, "overhead": [ { "field": "num_vars", @@ -675,8 +682,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 20, - "target": 49, + "source": 21, + "target": 50, "overhead": [ { "field": "num_vars", @@ -686,8 +693,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 21, - "target": 22, + "source": 22, + "target": 23, "overhead": [ { "field": "num_vars", @@ -701,8 +708,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 21, - "target": 49, + "source": 22, + "target": 50, "overhead": [ { "field": "num_vars", @@ -712,8 +719,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 21, - "target": 56, + "source": 22, + "target": 57, "overhead": [ { "field": "num_elements", @@ -723,8 +730,8 @@ "doc_path": "rules/ksatisfiability_subsetsum/index.html" }, { - "source": 22, - "target": 51, + "source": 23, + "target": 52, "overhead": [ { "field": "num_clauses", @@ -742,8 +749,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 23, - "target": 49, + "source": 24, + "target": 50, "overhead": [ { "field": "num_vars", @@ -753,8 +760,8 @@ "doc_path": "rules/knapsack_qubo/index.html" }, { - "source": 24, - "target": 12, + "source": 25, + "target": 13, "overhead": [ { "field": "num_vars", @@ -768,8 +775,8 @@ "doc_path": "rules/longestcommonsubsequence_ilp/index.html" }, { - "source": 25, - "target": 54, + "source": 26, + "target": 55, "overhead": [ { "field": "num_spins", @@ -783,8 +790,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 27, - "target": 12, + "source": 28, + "target": 13, "overhead": [ { "field": "num_vars", @@ -798,8 +805,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 27, - "target": 31, + "source": 28, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -813,8 +820,8 @@ "doc_path": "rules/maximumclique_maximumindependentset/index.html" }, { - "source": 28, - "target": 29, + "source": 29, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -828,8 +835,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 28, - "target": 33, + "source": 29, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -843,8 +850,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 29, - "target": 34, + "source": 30, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -858,8 +865,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 28, + "source": 31, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -873,8 +880,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 30, - "target": 31, + "source": 31, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -888,8 +895,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 32, + "source": 31, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -903,8 +910,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 30, - "target": 36, + "source": 31, + "target": 37, "overhead": [ { "field": "num_sets", @@ -918,8 +925,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 27, + "source": 32, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -933,8 +940,8 @@ "doc_path": "rules/maximumindependentset_maximumclique/index.html" }, { - "source": 31, - "target": 38, + "source": 32, + "target": 39, "overhead": [ { "field": "num_sets", @@ -948,8 +955,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 45, + "source": 32, + "target": 46, "overhead": [ { "field": "num_vertices", @@ -963,8 +970,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 32, - "target": 34, + "source": 33, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -978,8 +985,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 30, + "source": 34, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -993,8 +1000,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 34, + "source": 34, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -1008,8 +1015,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 34, - "target": 31, + "source": 35, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1023,8 +1030,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 35, - "target": 12, + "source": 36, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1038,8 +1045,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 35, - "target": 38, + "source": 36, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1053,8 +1060,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 36, - "target": 30, + "source": 37, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1068,8 +1075,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 36, - "target": 38, + "source": 37, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1083,8 +1090,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 37, - "target": 49, + "source": 38, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1094,8 +1101,8 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 38, - "target": 12, + "source": 39, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1109,8 +1116,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 38, - "target": 31, + "source": 39, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1124,8 +1131,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 38, - "target": 37, + "source": 39, + "target": 38, "overhead": [ { "field": "num_sets", @@ -1139,8 +1146,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 39, - "target": 12, + "source": 40, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1154,8 +1161,8 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 42, - "target": 12, + "source": 43, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1169,8 +1176,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 45, - "target": 31, + "source": 46, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1184,8 +1191,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 45, - "target": 42, + "source": 46, + "target": 43, "overhead": [ { "field": "num_sets", @@ -1199,8 +1206,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 49, - "target": 12, + "source": 50, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1214,8 +1221,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 49, - "target": 53, + "source": 50, + "target": 54, "overhead": [ { "field": "num_spins", @@ -1225,8 +1232,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 51, - "target": 4, + "source": 52, + "target": 5, "overhead": [ { "field": "num_variables", @@ -1240,8 +1247,8 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 51, - "target": 16, + "source": 52, + "target": 17, "overhead": [ { "field": "num_vertices", @@ -1255,8 +1262,8 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 51, - "target": 21, + "source": 52, + "target": 22, "overhead": [ { "field": "num_clauses", @@ -1270,8 +1277,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 51, - "target": 30, + "source": 52, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1285,8 +1292,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 51, - "target": 39, + "source": 52, + "target": 40, "overhead": [ { "field": "num_vertices", @@ -1300,8 +1307,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 53, - "target": 49, + "source": 54, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1311,8 +1318,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 54, - "target": 25, + "source": 55, + "target": 26, "overhead": [ { "field": "num_vertices", @@ -1326,8 +1333,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 54, - "target": 53, + "source": 55, + "target": 54, "overhead": [ { "field": "num_spins", @@ -1341,8 +1348,8 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 57, - "target": 12, + "source": 58, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1356,8 +1363,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 57, - "target": 49, + "source": 58, + "target": 50, "overhead": [ { "field": "num_vars", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index c42f69c24..b73301130 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -232,6 +232,7 @@ Flags by problem type: MinimumSetCovering --universe, --sets [--weights] X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each) BicliqueCover --left, --right, --biedges, --k + BalancedCompleteBipartiteSubgraph --left, --right, --biedges, --k BMF --matrix (0/1), --rank CVP --basis, --target-vec [--bounds] OptimalLinearArrangement --graph, --bound diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index a14442240..9fc49c5ba 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -725,6 +725,37 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { (ser(BicliqueCover::new(graph, k))?, resolved_variant.clone()) } + // BalancedCompleteBipartiteSubgraph + "BalancedCompleteBipartiteSubgraph" => { + let left = args.left.ok_or_else(|| { + anyhow::anyhow!( + "BalancedCompleteBipartiteSubgraph requires --left, --right, --biedges, and --k\n\n\ + Usage: pred create BalancedCompleteBipartiteSubgraph --left 4 --right 4 --biedges 0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3 --k 3" + ) + })?; + let right = args.right.ok_or_else(|| { + anyhow::anyhow!( + "BalancedCompleteBipartiteSubgraph requires --right (right partition size)" + ) + })?; + let k = args.k.ok_or_else(|| { + anyhow::anyhow!( + "BalancedCompleteBipartiteSubgraph requires --k (balanced biclique size)" + ) + })?; + let edges_str = args.biedges.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "BalancedCompleteBipartiteSubgraph requires --biedges (e.g., 0-0,0-1,1-1)" + ) + })?; + let edges = util::parse_edge_pairs(edges_str)?; + let graph = BipartiteGraph::new(left, right, edges); + ( + ser(BalancedCompleteBipartiteSubgraph::new(graph, k))?, + resolved_variant.clone(), + ) + } + // BMF "BMF" => { let matrix = parse_bool_matrix(args)?; @@ -1098,6 +1129,93 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { emit_problem_output(&output, out) } +#[cfg(test)] +mod tests { + use super::*; + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::graph::BalancedCompleteBipartiteSubgraph; + + fn create_args_for_bcbs() -> CreateArgs { + CreateArgs { + problem: Some("BalancedCompleteBipartiteSubgraph".to_string()), + example: None, + example_target: None, + example_side: ExampleSide::Source, + graph: None, + weights: None, + edge_weights: None, + couplings: None, + fields: None, + clauses: None, + num_vars: None, + matrix: None, + k: Some(3), + 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: Some("0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3".to_string()), + left: Some(4), + right: Some(4), + rank: None, + basis: None, + target_vec: None, + bounds: None, + tree: None, + required_edges: None, + bound: None, + pattern: None, + strings: None, + arcs: None, + deadlines: None, + precedence_pairs: None, + task_lengths: None, + deadline: None, + num_processors: None, + alphabet_size: None, + } + } + + #[test] + fn test_create_balanced_complete_bipartite_subgraph() { + let args = create_args_for_bcbs(); + let output_path = + std::env::temp_dir().join(format!("bcbs-create-{}.json", std::process::id())); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = std::fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "BalancedCompleteBipartiteSubgraph"); + assert!(created.variant.is_empty()); + + let problem: BalancedCompleteBipartiteSubgraph = + serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.left_size(), 4); + assert_eq!(problem.right_size(), 4); + assert_eq!(problem.num_edges(), 12); + assert_eq!(problem.k(), 3); + + let _ = std::fs::remove_file(output_path); + } +} + /// Reject non-unit weights when the resolved variant uses `weight=One`. fn reject_nonunit_weights_for_one_variant( canonical: &str, diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index e48eef824..3a1149803 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -1,6 +1,7 @@ { "models": [ {"problem":"BMF","variant":{},"instance":{"k":2,"m":3,"matrix":[[true,true,false],[true,true,true],[false,true,true]],"n":3},"samples":[{"config":[1,0,1,1,0,1,1,1,0,0,1,1],"metric":{"Valid":0}}],"optimal":[{"config":[0,1,1,1,1,0,0,1,1,1,1,0],"metric":{"Valid":0}},{"config":[1,0,1,1,0,1,1,1,0,0,1,1],"metric":{"Valid":0}}]}, + {"problem":"BalancedCompleteBipartiteSubgraph","variant":{},"instance":{"graph":{"edges":[[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2],[3,0],[3,1],[3,3]],"left_size":4,"right_size":4},"k":3},"samples":[{"config":[1,1,1,0,1,1,1,0],"metric":true}],"optimal":[{"config":[1,1,1,0,1,1,1,0],"metric":true}]}, {"problem":"BicliqueCover","variant":{},"instance":{"graph":{"edges":[[0,0],[0,1],[1,1],[1,2]],"left_size":2,"right_size":3},"k":2},"samples":[{"config":[1,0,0,1,1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[0,1,0,1,0,1,0,1,0,1],"metric":{"Valid":5}},{"config":[1,0,1,0,1,0,1,0,1,0],"metric":{"Valid":5}}]}, {"problem":"CircuitSAT","variant":{},"instance":{"circuit":{"assignments":[{"expr":{"op":{"And":[{"op":{"Var":"x1"}},{"op":{"Var":"x2"}}]}},"outputs":["a"]},{"expr":{"op":{"Or":[{"op":{"Var":"x1"}},{"op":{"Var":"x2"}}]}},"outputs":["b"]},{"expr":{"op":{"Xor":[{"op":{"Var":"a"}},{"op":{"Var":"b"}}]}},"outputs":["c"]}]},"variables":["a","b","c","x1","x2"]},"samples":[{"config":[0,1,1,0,1],"metric":true},{"config":[0,1,1,1,0],"metric":true}],"optimal":[{"config":[0,0,0,0,0],"metric":true},{"config":[0,1,1,0,1],"metric":true},{"config":[0,1,1,1,0],"metric":true},{"config":[1,1,0,1,1],"metric":true}]}, {"problem":"ClosestVectorProblem","variant":{"weight":"i32"},"instance":{"basis":[[2,0],[1,2]],"bounds":[{"lower":-2,"upper":4},{"lower":-2,"upper":4}],"target":[2.8,1.5]},"samples":[{"config":[3,3],"metric":{"Valid":0.5385164807134505}}],"optimal":[{"config":[3,3],"metric":{"Valid":0.5385164807134505}}]}, diff --git a/src/lib.rs b/src/lib.rs index bceccfe25..054006326 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,8 +45,8 @@ pub mod prelude { pub use crate::models::algebraic::{BMF, QUBO}; pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use crate::models::graph::{ - BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, SpinGlass, - SubgraphIsomorphism, + BalancedCompleteBipartiteSubgraph, BicliqueCover, GraphPartitioning, HamiltonianPath, + IsomorphicSpanningTree, SpinGlass, SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, diff --git a/src/models/graph/balanced_complete_bipartite_subgraph.rs b/src/models/graph/balanced_complete_bipartite_subgraph.rs new file mode 100644 index 000000000..5db6c8512 --- /dev/null +++ b/src/models/graph/balanced_complete_bipartite_subgraph.rs @@ -0,0 +1,160 @@ +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::BipartiteGraph; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "BalancedCompleteBipartiteSubgraph", + display_name: "Balanced Complete Bipartite Subgraph", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Decide whether a bipartite graph contains a K_{k,k} subgraph", + fields: &[ + FieldInfo { name: "left_size", type_name: "usize", description: "Vertices in left partition" }, + FieldInfo { name: "right_size", type_name: "usize", description: "Vertices in right partition" }, + FieldInfo { name: "edges", type_name: "Vec<(usize, usize)>", description: "Bipartite edges" }, + FieldInfo { name: "k", type_name: "usize", description: "Balanced biclique size" }, + ], + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BalancedCompleteBipartiteSubgraph { + graph: BipartiteGraph, + k: usize, +} + +impl BalancedCompleteBipartiteSubgraph { + pub fn new(graph: BipartiteGraph, k: usize) -> Self { + Self { graph, k } + } + + pub fn graph(&self) -> &BipartiteGraph { + &self.graph + } + + pub fn left_size(&self) -> usize { + self.graph.left_size() + } + + pub fn right_size(&self) -> usize { + self.graph.right_size() + } + + pub fn num_vertices(&self) -> usize { + self.left_size() + self.right_size() + } + + pub fn num_edges(&self) -> usize { + self.graph.left_edges().len() + } + + pub fn k(&self) -> usize { + self.k + } + + fn selected_vertices(&self, config: &[usize]) -> Option<(Vec, Vec)> { + if config.len() != self.num_vertices() { + return None; + } + + let mut selected_left = Vec::new(); + let mut selected_right = Vec::new(); + + for (index, &value) in config.iter().enumerate() { + match value { + 0 => {} + 1 => { + if index < self.left_size() { + selected_left.push(index); + } else { + selected_right.push(index - self.left_size()); + } + } + _ => return None, + } + } + + Some((selected_left, selected_right)) + } + + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + self.evaluate(config) + } +} + +impl Problem for BalancedCompleteBipartiteSubgraph { + const NAME: &'static str = "BalancedCompleteBipartiteSubgraph"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![2; self.num_vertices()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + let Some((selected_left, selected_right)) = self.selected_vertices(config) else { + return false; + }; + + if selected_left.len() != self.k || selected_right.len() != self.k { + return false; + } + + selected_left.iter().all(|&left| { + selected_right + .iter() + .all(|&right| self.graph.left_edges().contains(&(left, right))) + }) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for BalancedCompleteBipartiteSubgraph {} + +crate::declare_variants! { + default sat BalancedCompleteBipartiteSubgraph => "2^num_vertices", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "balanced_complete_bipartite_subgraph", + build: || { + let problem = BalancedCompleteBipartiteSubgraph::new( + BipartiteGraph::new( + 4, + 4, + vec![ + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 1), + (1, 2), + (2, 0), + (2, 1), + (2, 2), + (3, 0), + (3, 1), + (3, 3), + ], + ), + 3, + ); + + crate::example_db::specs::satisfaction_example( + problem, + vec![vec![1, 1, 1, 0, 1, 1, 1, 0]], + ) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 7d76de656..eb799a1b8 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -17,12 +17,14 @@ //! - [`SpinGlass`]: Ising model Hamiltonian //! - [`HamiltonianPath`]: Hamiltonian path (simple path visiting every vertex) //! - [`BicliqueCover`]: Biclique cover on bipartite graphs +//! - [`BalancedCompleteBipartiteSubgraph`]: Balanced biclique decision problem //! - [`OptimalLinearArrangement`]: Optimal linear arrangement (total edge length at most K) //! - [`MinimumFeedbackArcSet`]: Minimum feedback arc set on directed graphs //! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median) //! - [`RuralPostman`]: Rural Postman (circuit covering required edges) //! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem) +pub(crate) mod balanced_complete_bipartite_subgraph; pub(crate) mod biclique_cover; pub(crate) mod graph_partitioning; pub(crate) mod hamiltonian_path; @@ -45,6 +47,7 @@ pub(crate) mod spin_glass; pub(crate) mod subgraph_isomorphism; pub(crate) mod traveling_salesman; +pub use balanced_complete_bipartite_subgraph::BalancedCompleteBipartiteSubgraph; pub use biclique_cover::BicliqueCover; pub use graph_partitioning::GraphPartitioning; pub use hamiltonian_path::HamiltonianPath; @@ -85,6 +88,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec BipartiteGraph { + BipartiteGraph::new( + 4, + 4, + vec![ + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 1), + (1, 3), + (2, 0), + (2, 2), + (2, 3), + (3, 1), + ], + ) +} + +fn issue_instance_2_graph() -> BipartiteGraph { + BipartiteGraph::new( + 4, + 4, + vec![ + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 1), + (1, 2), + (2, 0), + (2, 1), + (2, 2), + (3, 0), + (3, 1), + (3, 3), + ], + ) +} + +fn issue_instance_2_witness() -> Vec { + vec![1, 1, 1, 0, 1, 1, 1, 0] +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_creation() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 2); + + assert_eq!(problem.left_size(), 4); + assert_eq!(problem.right_size(), 4); + assert_eq!(problem.num_vertices(), 8); + assert_eq!(problem.num_edges(), 10); + assert_eq!(problem.k(), 2); + assert_eq!(problem.dims(), vec![2; 8]); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_evaluation_yes_instance() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 2); + + assert!(problem.evaluate(&[1, 1, 0, 0, 1, 1, 0, 0])); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_evaluation_no_instance() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 3); + + assert!(!problem.evaluate(&[1, 1, 1, 0, 1, 1, 1, 0])); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_invalid_pairing() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 2); + + assert!(!problem.evaluate(&[1, 1, 0, 0, 1, 0, 1, 0])); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_solver_yes_instance() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_2_graph(), 3); + let solver = BruteForce::new(); + + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + assert!(problem.evaluate(&solution.unwrap())); + + let all = solver.find_all_satisfying(&problem); + assert_eq!(all, vec![issue_instance_2_witness()]); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_solver_no_instance() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 3); + let solver = BruteForce::new(); + + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_serialization() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_2_graph(), 3); + + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: BalancedCompleteBipartiteSubgraph = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.left_size(), 4); + assert_eq!(deserialized.right_size(), 4); + assert_eq!(deserialized.num_edges(), 12); + assert_eq!( + deserialized.graph().left_edges(), + problem.graph().left_edges() + ); + assert_eq!(deserialized.k(), 3); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_is_valid_solution() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_2_graph(), 3); + let yes_config = issue_instance_2_witness(); + let no_config = vec![1, 1, 0, 1, 1, 1, 0, 0]; + + assert_eq!( + problem.is_valid_solution(&yes_config), + problem.evaluate(&yes_config) + ); + assert_eq!( + problem.is_valid_solution(&no_config), + problem.evaluate(&no_config) + ); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_paper_example() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_2_graph(), 3); + let witness = issue_instance_2_witness(); + let solver = BruteForce::new(); + + assert!(problem.evaluate(&witness)); + assert_eq!(solver.find_all_satisfying(&problem), vec![witness]); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index eee5dc642..03885562d 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -80,6 +80,13 @@ fn test_all_problems_implement_trait_correctly() { &BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0)]), 1), "BicliqueCover", ); + check_problem_trait( + &BalancedCompleteBipartiteSubgraph::new( + BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]), + 2, + ), + "BalancedCompleteBipartiteSubgraph", + ); check_problem_trait(&Factoring::new(6, 2, 2), "Factoring"); let circuit = Circuit::new(vec![Assignment::new( From e63ba362d65ed598324e7ee306b8bfb6924f6ab4 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 06:35:44 +0800 Subject: [PATCH 3/6] Fix review findings for #239 --- docs/src/reductions/problem_schemas.json | 16 +-- problemreductions-cli/src/cli.rs | 6 +- problemreductions-cli/src/commands/create.rs | 123 ++++++++++++------ problemreductions-cli/tests/cli_tests.rs | 16 +++ .../balanced_complete_bipartite_subgraph.rs | 4 +- .../balanced_complete_bipartite_subgraph.rs | 19 +-- src/unit_tests/problem_size.rs | 13 ++ 7 files changed, 130 insertions(+), 67 deletions(-) diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 234378934..fa8d6bef7 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -20,19 +20,9 @@ "description": "Decide whether a bipartite graph contains a K_{k,k} subgraph", "fields": [ { - "name": "left_size", - "type_name": "usize", - "description": "Vertices in left partition" - }, - { - "name": "right_size", - "type_name": "usize", - "description": "Vertices in right partition" - }, - { - "name": "edges", - "type_name": "Vec<(usize, usize)>", - "description": "Bipartite edges" + "name": "graph", + "type_name": "BipartiteGraph", + "description": "The bipartite graph G = (A, B, E)" }, { "name": "k", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index b73301130..9562e5e7f 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -347,13 +347,13 @@ pub struct CreateArgs { /// Universe size for MinimumSetCovering #[arg(long)] pub universe: Option, - /// Bipartite graph edges for BicliqueCover (e.g., "0-0,0-1,1-2" for left-right pairs) + /// Bipartite graph edges for BicliqueCover / BalancedCompleteBipartiteSubgraph (e.g., "0-0,0-1,1-2" for left-right pairs) #[arg(long)] pub biedges: Option, - /// Left partition size for BicliqueCover + /// Left partition size for BicliqueCover / BalancedCompleteBipartiteSubgraph #[arg(long)] pub left: Option, - /// Right partition size for BicliqueCover + /// Right partition size for BicliqueCover / BalancedCompleteBipartiteSubgraph #[arg(long)] pub right: Option, /// Rank for BMF diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 9fc49c5ba..d4b1605d0 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -237,6 +237,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" } + "BalancedCompleteBipartiteSubgraph" => { + "--left 4 --right 4 --biedges 0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3 --k 3" + } "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", @@ -274,6 +277,19 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> { // DirectedGraph fields use --arcs, not --graph let hint = type_format_hint(&field.type_name, graph_type); eprintln!(" --{:<16} {} ({})", "arcs", field.description, hint); + } else if field.type_name == "BipartiteGraph" { + eprintln!( + " --{:<16} {} ({})", + "left", "Vertices in the left partition", "integer" + ); + eprintln!( + " --{:<16} {} ({})", + "right", "Vertices in the right partition", "integer" + ); + eprintln!( + " --{:<16} {} ({})", + "biedges", "Bipartite edges as left-right pairs", "edge list: 0-0,0-1,1-2" + ); } else { let hint = type_format_hint(&field.type_name, graph_type); eprintln!( @@ -705,51 +721,21 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // BicliqueCover "BicliqueCover" => { - let left = args.left.ok_or_else(|| { - anyhow::anyhow!( - "BicliqueCover requires --left, --right, --biedges, and --k\n\n\ - Usage: pred create BicliqueCover --left 2 --right 2 --biedges 0-0,0-1,1-1 --k 2" - ) - })?; - let right = args.right.ok_or_else(|| { - anyhow::anyhow!("BicliqueCover requires --right (right partition size)") - })?; - let k = args.k.ok_or_else(|| { - anyhow::anyhow!("BicliqueCover requires --k (number of bicliques)") - })?; - let edges_str = args.biedges.as_deref().ok_or_else(|| { - anyhow::anyhow!("BicliqueCover requires --biedges (e.g., 0-0,0-1,1-1)") - })?; - let edges = util::parse_edge_pairs(edges_str)?; - let graph = BipartiteGraph::new(left, right, edges); + let usage = "pred create BicliqueCover --left 2 --right 2 --biedges 0-0,0-1,1-1 --k 2"; + let (graph, k) = + parse_bipartite_problem_input(args, "BicliqueCover", "number of bicliques", usage)?; (ser(BicliqueCover::new(graph, k))?, resolved_variant.clone()) } // BalancedCompleteBipartiteSubgraph "BalancedCompleteBipartiteSubgraph" => { - let left = args.left.ok_or_else(|| { - anyhow::anyhow!( - "BalancedCompleteBipartiteSubgraph requires --left, --right, --biedges, and --k\n\n\ - Usage: pred create BalancedCompleteBipartiteSubgraph --left 4 --right 4 --biedges 0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3 --k 3" - ) - })?; - let right = args.right.ok_or_else(|| { - anyhow::anyhow!( - "BalancedCompleteBipartiteSubgraph requires --right (right partition size)" - ) - })?; - let k = args.k.ok_or_else(|| { - anyhow::anyhow!( - "BalancedCompleteBipartiteSubgraph requires --k (balanced biclique size)" - ) - })?; - let edges_str = args.biedges.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "BalancedCompleteBipartiteSubgraph requires --biedges (e.g., 0-0,0-1,1-1)" - ) - })?; - let edges = util::parse_edge_pairs(edges_str)?; - let graph = BipartiteGraph::new(left, right, edges); + let usage = "pred create BalancedCompleteBipartiteSubgraph --left 4 --right 4 --biedges 0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3 --k 3"; + let (graph, k) = parse_bipartite_problem_input( + args, + "BalancedCompleteBipartiteSubgraph", + "balanced biclique size", + usage, + )?; ( ser(BalancedCompleteBipartiteSubgraph::new(graph, k))?, resolved_variant.clone(), @@ -1214,6 +1200,21 @@ mod tests { let _ = std::fs::remove_file(output_path); } + + #[test] + fn test_create_balanced_complete_bipartite_subgraph_rejects_out_of_range_biedges() { + let mut args = create_args_for_bcbs(); + args.biedges = Some("4-0".to_string()); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("out of bounds for left partition size 4")); + } } /// Reject non-unit weights when the resolved variant uses `weight=One`. @@ -1335,6 +1336,48 @@ fn variant_map(pairs: &[(&str, &str)]) -> BTreeMap { util::variant_map(pairs) } +fn parse_bipartite_problem_input( + args: &CreateArgs, + canonical: &str, + k_description: &str, + usage: &str, +) -> Result<(BipartiteGraph, usize)> { + let left = args.left.ok_or_else(|| { + anyhow::anyhow!( + "{canonical} requires --left, --right, --biedges, and --k\n\nUsage: {usage}" + ) + })?; + let right = args.right.ok_or_else(|| { + anyhow::anyhow!("{canonical} requires --right (right partition size)\n\nUsage: {usage}") + })?; + let k = args.k.ok_or_else(|| { + anyhow::anyhow!("{canonical} requires --k ({k_description})\n\nUsage: {usage}") + })?; + let edges_str = args.biedges.as_deref().ok_or_else(|| { + anyhow::anyhow!("{canonical} requires --biedges (e.g., 0-0,0-1,1-1)\n\nUsage: {usage}") + })?; + let edges = util::parse_edge_pairs(edges_str)?; + validate_bipartite_edges(canonical, left, right, &edges)?; + Ok((BipartiteGraph::new(left, right, edges), k)) +} + +fn validate_bipartite_edges( + canonical: &str, + left: usize, + right: usize, + edges: &[(usize, usize)], +) -> Result<()> { + for &(u, v) in edges { + if u >= left { + bail!("{canonical} edge {u}-{v} is out of bounds for left partition size {left}"); + } + if v >= right { + bail!("{canonical} edge {u}-{v} is out of bounds for right partition size {right}"); + } + } + Ok(()) +} + /// Parse `--graph` into a SimpleGraph, inferring num_vertices from max index. fn parse_graph(args: &CreateArgs) -> Result<(SimpleGraph, usize)> { let edges_str = args diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index f011bad11..0ef248215 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -179,6 +179,22 @@ fn test_show_includes_fields() { assert!(stdout.contains("weights")); } +#[test] +fn test_create_balanced_complete_bipartite_subgraph_help_uses_bipartite_flags() { + let output = pred() + .args(["create", "BalancedCompleteBipartiteSubgraph"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("--left"), "stderr: {stderr}"); + assert!(stderr.contains("--right"), "stderr: {stderr}"); + assert!(stderr.contains("--biedges"), "stderr: {stderr}"); + assert!(!stderr.contains("--left-size"), "stderr: {stderr}"); + assert!(!stderr.contains("--right-size"), "stderr: {stderr}"); + assert!(!stderr.contains("--edges"), "stderr: {stderr}"); +} + #[test] fn test_list_json() { let tmp = std::env::temp_dir().join("pred_test_list.json"); diff --git a/src/models/graph/balanced_complete_bipartite_subgraph.rs b/src/models/graph/balanced_complete_bipartite_subgraph.rs index 5db6c8512..1c1029cbe 100644 --- a/src/models/graph/balanced_complete_bipartite_subgraph.rs +++ b/src/models/graph/balanced_complete_bipartite_subgraph.rs @@ -12,9 +12,7 @@ inventory::submit! { module_path: module_path!(), description: "Decide whether a bipartite graph contains a K_{k,k} subgraph", fields: &[ - FieldInfo { name: "left_size", type_name: "usize", description: "Vertices in left partition" }, - FieldInfo { name: "right_size", type_name: "usize", description: "Vertices in right partition" }, - FieldInfo { name: "edges", type_name: "Vec<(usize, usize)>", description: "Bipartite edges" }, + FieldInfo { name: "graph", type_name: "BipartiteGraph", description: "The bipartite graph G = (A, B, E)" }, FieldInfo { name: "k", type_name: "usize", description: "Balanced biclique size" }, ], } diff --git a/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs b/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs index e19ab4ed2..d5cf85748 100644 --- a/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs +++ b/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs @@ -80,6 +80,14 @@ fn test_balanced_complete_bipartite_subgraph_invalid_pairing() { assert!(!problem.evaluate(&[1, 1, 0, 0, 1, 0, 1, 0])); } +#[test] +fn test_balanced_complete_bipartite_subgraph_rejects_invalid_configs() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 2); + + assert!(!problem.evaluate(&[1, 1, 0, 0, 1, 1, 0])); + assert!(!problem.evaluate(&[1, 2, 0, 0, 1, 1, 0, 0])); +} + #[test] fn test_balanced_complete_bipartite_subgraph_solver_yes_instance() { let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_2_graph(), 3); @@ -124,14 +132,9 @@ fn test_balanced_complete_bipartite_subgraph_is_valid_solution() { let yes_config = issue_instance_2_witness(); let no_config = vec![1, 1, 0, 1, 1, 1, 0, 0]; - assert_eq!( - problem.is_valid_solution(&yes_config), - problem.evaluate(&yes_config) - ); - assert_eq!( - problem.is_valid_solution(&no_config), - problem.evaluate(&no_config) - ); + assert!(problem.is_valid_solution(&yes_config)); + assert!(!problem.is_valid_solution(&no_config)); + assert!(!problem.is_valid_solution(&[1, 1, 1])); } #[test] diff --git a/src/unit_tests/problem_size.rs b/src/unit_tests/problem_size.rs index 1e40e683f..df171114a 100644 --- a/src/unit_tests/problem_size.rs +++ b/src/unit_tests/problem_size.rs @@ -194,6 +194,19 @@ fn test_problem_size_biclique_cover() { assert_eq!(size.get("rank"), Some(2)); } +#[test] +fn test_problem_size_balanced_complete_bipartite_subgraph() { + let bcbs = BalancedCompleteBipartiteSubgraph::new( + BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 2)]), + 2, + ); + let size = problem_size(&bcbs); + assert_eq!(size.get("left_size"), Some(2)); + assert_eq!(size.get("right_size"), Some(3)); + assert_eq!(size.get("num_edges"), Some(3)); + assert_eq!(size.get("k"), Some(2)); +} + #[test] fn test_problem_size_bmf() { let bmf = BMF::new(vec![vec![true, false], vec![false, true]], 2); From ce692acb956fa1c21d9ea11beb0a38f9d7e69d5b Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 06:35:58 +0800 Subject: [PATCH 4/6] chore: remove plan file after implementation --- ...16-balanced-complete-bipartite-subgraph.md | 382 ------------------ 1 file changed, 382 deletions(-) delete mode 100644 docs/plans/2026-03-16-balanced-complete-bipartite-subgraph.md diff --git a/docs/plans/2026-03-16-balanced-complete-bipartite-subgraph.md b/docs/plans/2026-03-16-balanced-complete-bipartite-subgraph.md deleted file mode 100644 index 4af03a458..000000000 --- a/docs/plans/2026-03-16-balanced-complete-bipartite-subgraph.md +++ /dev/null @@ -1,382 +0,0 @@ -# BalancedCompleteBipartiteSubgraph Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add `BalancedCompleteBipartiteSubgraph` as a new graph satisfaction model for the GT24 decision problem, with registry/CLI/example-db integration, paper documentation, and issue-backed tests. - -**Architecture:** Implement a concrete `BipartiteGraph`-based satisfaction problem with fields `graph: BipartiteGraph` and `k: usize`. The configuration is one binary decision per vertex: `config[0..left_size]` selects the left side of the biclique and `config[left_size..left_size + right_size]` selects the right side. A configuration is satisfying iff exactly `k` left vertices and exactly `k` right vertices are selected and every selected left/right pair is an edge of the bipartite graph. Register the model through `ProblemSchemaEntry`, `declare_variants!`, graph module exports, example-db hooks, CLI creation, and the paper. - -**Tech Stack:** Rust, serde, inventory registry, `BipartiteGraph`, brute-force solver, Typst, `pred` CLI. - ---- - -## Required workflow references - -- Implementation skill: `.claude/skills/add-model/SKILL.md` Steps 1-7 -- Execution skill: `superpowers:subagent-driven-development` -- Per-task discipline: `superpowers:test-driven-development` - -## Issue-derived decisions to preserve - -- Treat this as the **decision** problem from issue `#239`, not an optimization variant. -- Use the binary left-then-right encoding from the corrected issue text. -- Keep the model concrete over `BipartiteGraph`; do not introduce a generic graph type parameter. -- Do not edit `problemreductions-cli/src/problem_name.rs`: alias resolution is registry-backed now. -- Companion rule issue already exists: `#231 [Rule] CLIQUE to BALANCED COMPLETE BIPARTITE SUBGRAPH`. -- For the canonical paper/example-db instance, use **Issue Instance 2** so the satisfying witness is unique: - - `A = {a0,a1,a2,a3}` - - `B = {b0,b1,b2,b3}` - - edges `(0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2),(3,0),(3,1),(3,3)` - - `k = 3` - - satisfying sets `{a0,a1,a2}` and `{b0,b1,b2}` -- Keep **Issue Instance 1** as the smaller regression case: - - `k = 2` is satisfiable, for example with `{a0,a1}` and `{b0,b1}` - - `k = 3` is unsatisfiable on that same graph -- Complexity caution: the `O*(1.3803^n)` Chen et al. bound is issue-reviewed as **dense-graph-specific**. Unless the source check proves it is a valid worst-case bound for the general problem, keep `declare_variants!` conservative (`"2^num_vertices"`) and mention the dense-case algorithm only in paper prose. - -## Batch structure - -- **Batch 1:** `.claude/skills/add-model/SKILL.md` Steps 1-5.5 - - model implementation - - registry/module wiring - - example-db registration - - CLI creation support - - unit tests - - trait consistency / problem size coverage -- **Batch 2:** `.claude/skills/add-model/SKILL.md` Step 6 - - paper entry - - bibliography updates - - paper-aligned test finalization - -### Task 1: Model Semantics and Red Tests - -**Files:** -- Create: `src/models/graph/balanced_complete_bipartite_subgraph.rs` -- Create: `src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs` -- Modify: `src/models/graph/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` - -**Step 1: Write the failing model tests first** - -Add a new unit test file that covers these exact behaviors before the model exists: - -- `test_balanced_complete_bipartite_subgraph_creation` - - construct `BipartiteGraph::new(4, 4, vec![...])` - - assert `left_size() == 4`, `right_size() == 4`, `num_vertices() == 8`, `num_edges()` matches edge count, `k() == 2 or 3` - - assert `dims() == vec![2; 8]` -- `test_balanced_complete_bipartite_subgraph_evaluation_yes_instance` - - Issue Instance 1 with `k = 2` - - config `[1,1,0,0, 1,1,0,0]` must satisfy -- `test_balanced_complete_bipartite_subgraph_evaluation_no_instance` - - same graph as Instance 1 with `k = 3` - - any candidate using `{a0,a1,a2}` and `{b0,b1,b2}` must fail because edge `(1,2)` is missing -- `test_balanced_complete_bipartite_subgraph_invalid_pairing` - - choose balanced left/right sets of size `k`, but include at least one missing cross edge; `evaluate()` must return `false` -- `test_balanced_complete_bipartite_subgraph_solver_yes_instance` - - `BruteForce::find_satisfying()` returns `Some` - - `BruteForce::find_all_satisfying()` on Instance 2 returns exactly `1` solution -- `test_balanced_complete_bipartite_subgraph_solver_no_instance` - - on Instance 1 with `k = 3`, `find_satisfying()` returns `None` -- `test_balanced_complete_bipartite_subgraph_serialization` - - serde round-trip preserves `k`, partition sizes, and edges -- `test_balanced_complete_bipartite_subgraph_is_valid_solution` - - delegates to `evaluate()` -- `test_balanced_complete_bipartite_subgraph_paper_example` - - reuse the exact Issue Instance 2 canonical example - - assert the issue witness satisfies the problem - - assert `find_all_satisfying()` returns `1` - -**Step 2: Run the targeted tests to verify RED** - -Run: - -```bash -cargo test balanced_complete_bipartite_subgraph --lib -``` - -Expected: compile failure because the model module/type does not exist yet. - -**Step 3: Implement the minimal model to make the tests pass** - -Create `src/models/graph/balanced_complete_bipartite_subgraph.rs` with this shape: - -- `inventory::submit!` for `ProblemSchemaEntry` - - `name: "BalancedCompleteBipartiteSubgraph"` - - `display_name: "Balanced Complete Bipartite Subgraph"` - - `aliases: &[]` - - `dimensions: &[]` - - `description: "Decide whether a bipartite graph contains a K_{k,k} subgraph"` - - fields mirroring the user-facing bipartite input style used by `BicliqueCover`: - - `left_size` - - `right_size` - - `edges` - - `k` -- `#[derive(Debug, Clone, Serialize, Deserialize)]` -- `pub struct BalancedCompleteBipartiteSubgraph { graph: BipartiteGraph, k: usize }` -- inherent methods: - - `new(graph: BipartiteGraph, k: usize) -> Self` - - `graph(&self) -> &BipartiteGraph` - - `left_size(&self) -> usize` - - `right_size(&self) -> usize` - - `num_vertices(&self) -> usize` - - `num_edges(&self) -> usize` - - `k(&self) -> usize` - - helper to split a config into selected left/right vertex indices - - `is_valid_solution(&self, config: &[usize]) -> bool` -- `impl Problem` - - `const NAME = "BalancedCompleteBipartiteSubgraph"` - - `type Metric = bool` - - `dims() -> vec![2; self.num_vertices()]` - - `evaluate()` must: - - reject wrong config length - - reject any non-binary entry - - count selected left and right vertices separately - - require both counts equal `k` - - verify every selected pair `(left, right)` exists in the bipartite graph - - `variant() -> crate::variant_params![]` -- `impl SatisfactionProblem for BalancedCompleteBipartiteSubgraph {}` -- `crate::declare_variants! { default sat BalancedCompleteBipartiteSubgraph => "2^num_vertices", }` -- `#[cfg(feature = "example-db")] canonical_model_example_specs()` - - use `crate::example_db::specs::satisfaction_example(...)` - - encode Issue Instance 2 and its unique satisfying configuration -- test link at the bottom: - - `#[cfg(test)] #[path = "../../unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs"] mod tests;` - -**Step 4: Wire the model into graph exports** - -Update: - -- `src/models/graph/mod.rs` - - add `pub(crate) mod balanced_complete_bipartite_subgraph;` - - add `pub use balanced_complete_bipartite_subgraph::BalancedCompleteBipartiteSubgraph;` - - extend `canonical_model_example_specs()` with the new model helper -- `src/models/mod.rs` - - add `BalancedCompleteBipartiteSubgraph` to the graph re-export list -- `src/lib.rs` - - add `BalancedCompleteBipartiteSubgraph` to the `prelude` graph exports - -**Step 5: Run the same tests to verify GREEN** - -Run: - -```bash -cargo test balanced_complete_bipartite_subgraph --lib -``` - -Expected: the new model unit tests pass. - -**Step 6: Commit** - -```bash -git add src/models/graph/balanced_complete_bipartite_subgraph.rs \ - src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs \ - src/models/graph/mod.rs src/models/mod.rs src/lib.rs -git commit -m "Add BalancedCompleteBipartiteSubgraph model" -``` - -### Task 2: Registry Completeness, Example DB, Trait Checks, and CLI Creation - -**Files:** -- Modify: `src/unit_tests/trait_consistency.rs` -- Modify: `src/unit_tests/problem_size.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/src/cli.rs` - -**Step 1: Write the failing integration tests first** - -Add or extend tests before editing the CLI/create logic: - -- `src/unit_tests/trait_consistency.rs` - - add one `check_problem_trait(...)` call for `BalancedCompleteBipartiteSubgraph::new(BipartiteGraph::new(...), 2)` -- `src/unit_tests/problem_size.rs` - - add `test_problem_size_balanced_complete_bipartite_subgraph` - - assert `left_size`, `right_size`, `num_edges`, and `k` appear with the correct values -- `problemreductions-cli/src/commands/create.rs` - - add a small unit test module for `pred create BalancedCompleteBipartiteSubgraph --left 4 --right 4 --biedges ... --k 3` - - parse the emitted JSON and assert the created problem type is `BalancedCompleteBipartiteSubgraph` - - assert the serialized instance round-trips into the new model and preserves `k`, partitions, and edges - -**Step 2: Run the targeted tests to verify RED** - -Run: - -```bash -cargo test test_problem_size_balanced_complete_bipartite_subgraph --lib -cargo test test_all_problems_implement_trait_correctly --lib -cargo test -p problemreductions-cli balanced_complete_bipartite_subgraph -``` - -Expected: failures because the trait/problem-size entries and CLI create arm do not exist yet. - -**Step 3: Implement the wiring** - -Update `src/unit_tests/trait_consistency.rs`: - -- add a `check_problem_trait(...)` entry -- do **not** add a `test_direction` assertion because this is a satisfaction problem - -Update `src/unit_tests/problem_size.rs`: - -- add a new problem-size test covering the issue-backed example graph - -Update `problemreductions-cli/src/commands/create.rs`: - -- add a new match arm: - - `"BalancedCompleteBipartiteSubgraph" => { ... }` -- require: - - `--left` - - `--right` - - `--biedges` - - `--k` -- parse bipartite-local edges with the same helper used by `BicliqueCover` -- construct `BipartiteGraph::new(left, right, edges)` -- serialize `BalancedCompleteBipartiteSubgraph::new(graph, k)` -- use an error message and usage string parallel to `BicliqueCover` - -Update `problemreductions-cli/src/cli.rs`: - -- extend the “Flags by problem type” table with: - - `BalancedCompleteBipartiteSubgraph --left, --right, --biedges, --k` - -Do **not** edit `problemreductions-cli/src/problem_name.rs` unless a test proves a real alias-resolution gap. Canonical-name lookup is already case-insensitive through the registry. - -**Step 4: Run the targeted tests to verify GREEN** - -Run: - -```bash -cargo test test_problem_size_balanced_complete_bipartite_subgraph --lib -cargo test test_all_problems_implement_trait_correctly --lib -cargo test -p problemreductions-cli balanced_complete_bipartite_subgraph -``` - -Expected: all targeted tests pass. - -**Step 5: Commit** - -```bash -git add src/unit_tests/trait_consistency.rs src/unit_tests/problem_size.rs \ - problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs -git commit -m "Wire BalancedCompleteBipartiteSubgraph into CLI and registry checks" -``` - -### Task 3: Paper Entry, Citations, and Paper-Aligned Example - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `docs/paper/references.bib` -- Modify: `src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs` - -**Step 1: Write or finalize the failing paper-aligned test first** - -Before editing the paper, make sure the `test_balanced_complete_bipartite_subgraph_paper_example` test is locked to the exact canonical example chosen above: - -- use Issue Instance 2 only -- assert the witness `{a0,a1,a2}` / `{b0,b1,b2}` is satisfying -- assert `find_all_satisfying()` returns exactly `1` - -**Step 2: Run the focused test to verify RED if the example changed** - -Run: - -```bash -cargo test test_balanced_complete_bipartite_subgraph_paper_example --lib -``` - -Expected: fail if the example data or expected satisfying count is not yet aligned. - -**Step 3: Implement the paper entry** - -Update `docs/paper/reductions.typ`: - -- add display name entry: - - `"BalancedCompleteBipartiteSubgraph": [Balanced Complete Bipartite Subgraph],` -- add a new `#problem-def("BalancedCompleteBipartiteSubgraph")[ ... ][ ... ]` -- follow the structure/style of: - - `MaximumIndependentSet` for overall quality - - `GraphPartitioning` for balanced-language phrasing - - `BicliqueCover` for bipartite layout and figure structure - - `SubgraphIsomorphism` / `HamiltonianPath` for satisfaction-problem tone -- definition should state: - - input bipartite graph `G = (A, B, E)` - - integer `k` - - question whether there exist `A' subseteq A` and `B' subseteq B` with `|A'| = |B'| = k` and `A' times B' subseteq E` -- background should: - - mention the classical Garey-Johnson listing and the relationship to biclique search - - avoid over-claiming the GT24 tag if the reference check remains ambiguous -- exact algorithm sentence should: - - mention brute force / subset enumeration unconditionally - - mention Chen et al. only with a dense-graph qualifier if cited -- add a CeTZ figure using the bipartite-node layout style from `BicliqueCover` - - draw edges first - - highlight the six selected vertices and the nine biclique edges for Instance 2 - - explain why `a3` is excluded - -Update `docs/paper/references.bib` only as needed: - -- `@book{garey1979}` already exists -- add `chen2020` only if the paper prose mentions the dense exact algorithm -- do not add Lin/Manurangsi unless the final prose cites parameterized or approximation results - -**Step 4: Verify the paper entry and the aligned test** - -Run: - -```bash -cargo test test_balanced_complete_bipartite_subgraph_paper_example --lib -make paper -``` - -Expected: the focused test passes and the Typst paper builds cleanly. - -**Step 5: Commit** - -```bash -git add docs/paper/reductions.typ docs/paper/references.bib \ - src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs -git commit -m "Document BalancedCompleteBipartiteSubgraph in the paper" -``` - -### Task 4: Full Verification Before Review - -**Files:** -- No new files; verification only - -**Step 1: Run the full verification suite** - -Run: - -```bash -cargo test -make clippy -make export-schemas -make paper -``` - -Expected: - -- `cargo test` passes across the workspace -- `make clippy` passes -- `make export-schemas` updates checked-in schema/export artifacts if needed -- `make paper` passes after any schema/export changes - -**Step 2: Inspect generated diffs** - -Check for expected generated-file updates, especially: - -- `docs/src/reductions/problem_schemas.json` -- `src/example_db/fixtures/examples.json` if example-db exports were regenerated indirectly - -Only keep generated-file changes that are actually required by the new model. - -**Step 3: Commit the final implementation batch** - -```bash -git add -A -git commit -m "Finish BalancedCompleteBipartiteSubgraph integration" -``` - -**Step 4: Hand off to review** - -After this plan is executed, run the repo-local `review-implementation` skill before pushing the implementation summary comment. From 87446d4ecf8123c3471d54c0230f5a33f0c0bd61 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 12:15:13 +0800 Subject: [PATCH 5/6] fix review feedback for balanced biclique model --- docs/paper/reductions.typ | 2 +- docs/src/reductions/reduction_graph.json | 2 +- problemreductions-cli/src/dispatch.rs | 8 +++- problemreductions-cli/tests/cli_tests.rs | 43 +++++++++++++++++++ .../balanced_complete_bipartite_subgraph.rs | 35 +++++++++++++-- .../balanced_complete_bipartite_subgraph.rs | 11 +++++ src/unit_tests/problem_size.rs | 13 ------ 7 files changed, 94 insertions(+), 20 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 6139485fe..af393d2b4 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -1535,7 +1535,7 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS Given a bipartite graph $G = (A, B, E)$ and an integer $k$, determine whether there exist subsets $A' subset.eq A$ and $B' subset.eq B$ such that $|A'| = |B'| = k$ and every cross pair is present: $A' times B' subset.eq E.$ ][ - Balanced Complete Bipartite Subgraph is a classical NP-complete bipartite containment problem from Garey and Johnson @garey1979. Unlike Biclique Cover, which asks for a collection of bicliques covering all edges, this problem asks for a _single_ balanced biclique of prescribed size. It arises naturally in biclustering, dense submatrix discovery, and pattern mining on bipartite data. A straightforward exact algorithm enumerates all $k$-subsets of $A$ and $B$ and checks whether they induce a complete bipartite graph, taking $O(binom(|A|, k) dot binom(|B|, k) dot k^2) = O^*(2^(|A| + |B|))$ time#footnote[No faster unconditional worst-case exact algorithm is asserted here for the general problem.]. + Balanced Complete Bipartite Subgraph is a classical NP-complete bipartite containment problem from Garey and Johnson @garey1979. Unlike Biclique Cover, which asks for a collection of bicliques covering all edges, this problem asks for a _single_ balanced biclique of prescribed size. It arises naturally in biclustering, dense submatrix discovery, and pattern mining on bipartite data. Chen et al. give an exact $O^*(1.3803^n)$ algorithm for dense bipartite graphs, and the registry records that best-known bound in the catalog metadata. A straightforward baseline still enumerates all $k$-subsets of $A$ and $B$ and checks whether they induce a complete bipartite graph, taking $O(binom(|A|, k) dot binom(|B|, k) dot k^2) = O^*(2^(|A| + |B|))$ time. *Example.* Consider the bipartite graph with $A = {ell_1, ell_2, ell_3, ell_4}$, $B = {r_1, r_2, r_3, r_4}$, and edges $E = {#bip-edges.map(e => $(ell_#(e.at(0) + 1), r_#(e.at(1) + 1))$).join(", ")}$. For $k = #k$, the selected sets $A' = {#left-selected.map(i => $ell_#(i + 1)$).join(", ")}$ and $B' = {#right-selected.map(i => $r_#(i + 1)$).join(", ")}$ form a balanced complete bipartite subgraph: all #selected-edges.len() required cross edges are present. Vertex $ell_4$ is excluded because $(ell_4, r_3) in.not E$, so any witness using $ell_4$ cannot realize $K_(#k,#k)$. diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index d9fc6b6a1..21734ea6c 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -12,7 +12,7 @@ "variant": {}, "category": "graph", "doc_path": "models/graph/struct.BalancedCompleteBipartiteSubgraph.html", - "complexity": "2^num_vertices" + "complexity": "1.3803^num_vertices" }, { "name": "BicliqueCover", diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 659bba48a..99ae4d78f 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -79,8 +79,12 @@ impl LoadedProblem { } } - let reduction_path = - best_path.ok_or_else(|| anyhow::anyhow!("No reduction path from {} to ILP", name))?; + let reduction_path = best_path.ok_or_else(|| { + anyhow::anyhow!( + "No reduction path from {} to ILP. Try `pred solve --solver brute-force` for exhaustive search.", + name + ) + })?; let chain = graph .reduce_along_path(&reduction_path, self.as_any()) diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 0ef248215..3c8bc7434 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -76,6 +76,49 @@ fn test_show_variant_info() { ); } +#[test] +fn test_show_balanced_complete_bipartite_subgraph_complexity() { + let output = pred() + .args(["show", "BalancedCompleteBipartiteSubgraph"]) + .output() + .unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("1.3803^num_vertices"), + "expected updated complexity metadata, got: {stdout}" + ); +} + +#[test] +fn test_solve_balanced_complete_bipartite_subgraph_suggests_bruteforce() { + let tmp = std::env::temp_dir().join("pred_test_bcbs_problem.json"); + let create = pred() + .args([ + "create", + "--example", + "BalancedCompleteBipartiteSubgraph", + "--json", + ]) + .output() + .unwrap(); + assert!(create.status.success()); + std::fs::write(&tmp, create.stdout).unwrap(); + + let solve = pred() + .args(["solve", tmp.to_str().unwrap()]) + .output() + .unwrap(); + assert!(!solve.status.success()); + let stderr = String::from_utf8(solve.stderr).unwrap(); + assert!( + stderr.contains("--solver brute-force"), + "expected brute-force hint, got: {stderr}" + ); + + std::fs::remove_file(tmp).ok(); +} + #[test] fn test_path() { let output = pred().args(["path", "MIS", "QUBO"]).output().unwrap(); diff --git a/src/models/graph/balanced_complete_bipartite_subgraph.rs b/src/models/graph/balanced_complete_bipartite_subgraph.rs index 1c1029cbe..dd648f847 100644 --- a/src/models/graph/balanced_complete_bipartite_subgraph.rs +++ b/src/models/graph/balanced_complete_bipartite_subgraph.rs @@ -2,6 +2,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::BipartiteGraph; use crate::traits::{Problem, SatisfactionProblem}; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; inventory::submit! { ProblemSchemaEntry { @@ -19,14 +20,22 @@ inventory::submit! { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(from = "BalancedCompleteBipartiteSubgraphRepr")] pub struct BalancedCompleteBipartiteSubgraph { graph: BipartiteGraph, k: usize, + #[serde(skip)] + edge_lookup: HashSet<(usize, usize)>, } impl BalancedCompleteBipartiteSubgraph { pub fn new(graph: BipartiteGraph, k: usize) -> Self { - Self { graph, k } + let edge_lookup = Self::build_edge_lookup(&graph); + Self { + graph, + k, + edge_lookup, + } } pub fn graph(&self) -> &BipartiteGraph { @@ -53,6 +62,10 @@ impl BalancedCompleteBipartiteSubgraph { self.k } + fn build_edge_lookup(graph: &BipartiteGraph) -> HashSet<(usize, usize)> { + graph.left_edges().iter().copied().collect() + } + fn selected_vertices(&self, config: &[usize]) -> Option<(Vec, Vec)> { if config.len() != self.num_vertices() { return None; @@ -78,6 +91,10 @@ impl BalancedCompleteBipartiteSubgraph { Some((selected_left, selected_right)) } + fn has_selected_edge(&self, left: usize, right: usize) -> bool { + self.edge_lookup.contains(&(left, right)) + } + pub fn is_valid_solution(&self, config: &[usize]) -> bool { self.evaluate(config) } @@ -103,7 +120,7 @@ impl Problem for BalancedCompleteBipartiteSubgraph { selected_left.iter().all(|&left| { selected_right .iter() - .all(|&right| self.graph.left_edges().contains(&(left, right))) + .all(|&right| self.has_selected_edge(left, right)) }) } @@ -114,8 +131,20 @@ impl Problem for BalancedCompleteBipartiteSubgraph { impl SatisfactionProblem for BalancedCompleteBipartiteSubgraph {} +#[derive(Deserialize)] +struct BalancedCompleteBipartiteSubgraphRepr { + graph: BipartiteGraph, + k: usize, +} + +impl From for BalancedCompleteBipartiteSubgraph { + fn from(repr: BalancedCompleteBipartiteSubgraphRepr) -> Self { + Self::new(repr.graph, repr.k) + } +} + crate::declare_variants! { - default sat BalancedCompleteBipartiteSubgraph => "2^num_vertices", + default sat BalancedCompleteBipartiteSubgraph => "1.3803^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs b/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs index d5cf85748..843da8994 100644 --- a/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs +++ b/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs @@ -80,6 +80,15 @@ fn test_balanced_complete_bipartite_subgraph_invalid_pairing() { assert!(!problem.evaluate(&[1, 1, 0, 0, 1, 0, 1, 0])); } +#[test] +fn test_balanced_complete_bipartite_subgraph_edge_lookup() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 2); + + assert!(problem.has_selected_edge(0, 0)); + assert!(problem.has_selected_edge(1, 3)); + assert!(!problem.has_selected_edge(3, 3)); +} + #[test] fn test_balanced_complete_bipartite_subgraph_rejects_invalid_configs() { let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 2); @@ -112,6 +121,7 @@ fn test_balanced_complete_bipartite_subgraph_solver_no_instance() { #[test] fn test_balanced_complete_bipartite_subgraph_serialization() { let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_2_graph(), 3); + let witness = issue_instance_2_witness(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: BalancedCompleteBipartiteSubgraph = serde_json::from_str(&json).unwrap(); @@ -124,6 +134,7 @@ fn test_balanced_complete_bipartite_subgraph_serialization() { problem.graph().left_edges() ); assert_eq!(deserialized.k(), 3); + assert!(deserialized.evaluate(&witness)); } #[test] diff --git a/src/unit_tests/problem_size.rs b/src/unit_tests/problem_size.rs index df171114a..1e40e683f 100644 --- a/src/unit_tests/problem_size.rs +++ b/src/unit_tests/problem_size.rs @@ -194,19 +194,6 @@ fn test_problem_size_biclique_cover() { assert_eq!(size.get("rank"), Some(2)); } -#[test] -fn test_problem_size_balanced_complete_bipartite_subgraph() { - let bcbs = BalancedCompleteBipartiteSubgraph::new( - BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 2)]), - 2, - ); - let size = problem_size(&bcbs); - assert_eq!(size.get("left_size"), Some(2)); - assert_eq!(size.get("right_size"), Some(3)); - assert_eq!(size.get("num_edges"), Some(3)); - assert_eq!(size.get("k"), Some(2)); -} - #[test] fn test_problem_size_bmf() { let bmf = BMF::new(vec![vec![true, false], vec![false, true]], 2); From f71097935e9ef37639115fe7438fc1de1babe834 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Tue, 17 Mar 2026 19:35:41 +0800 Subject: [PATCH 6/6] Fix duplicate tests module in create.rs after merge Merge the PR's BalancedCompleteBipartiteSubgraph CLI tests into the existing tests module using empty_args() helper to avoid missing fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 161 +++++++------------ 1 file changed, 60 insertions(+), 101 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index f7f1d9e6e..973005907 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -1639,107 +1639,6 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { emit_problem_output(&output, out) } -#[cfg(test)] -mod tests { - use super::*; - use crate::dispatch::ProblemJsonOutput; - use problemreductions::models::graph::BalancedCompleteBipartiteSubgraph; - - fn create_args_for_bcbs() -> CreateArgs { - CreateArgs { - problem: Some("BalancedCompleteBipartiteSubgraph".to_string()), - example: None, - example_target: None, - example_side: ExampleSide::Source, - graph: None, - weights: None, - edge_weights: None, - couplings: None, - fields: None, - clauses: None, - num_vars: None, - matrix: None, - k: Some(3), - 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: Some("0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3".to_string()), - left: Some(4), - right: Some(4), - rank: None, - basis: None, - target_vec: None, - bounds: None, - tree: None, - required_edges: None, - bound: None, - pattern: None, - strings: None, - arcs: None, - deadlines: None, - precedence_pairs: None, - task_lengths: None, - deadline: None, - num_processors: None, - alphabet_size: None, - } - } - - #[test] - fn test_create_balanced_complete_bipartite_subgraph() { - let args = create_args_for_bcbs(); - let output_path = - std::env::temp_dir().join(format!("bcbs-create-{}.json", std::process::id())); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json = std::fs::read_to_string(&output_path).unwrap(); - let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); - assert_eq!(created.problem_type, "BalancedCompleteBipartiteSubgraph"); - assert!(created.variant.is_empty()); - - let problem: BalancedCompleteBipartiteSubgraph = - serde_json::from_value(created.data).unwrap(); - assert_eq!(problem.left_size(), 4); - assert_eq!(problem.right_size(), 4); - assert_eq!(problem.num_edges(), 12); - assert_eq!(problem.k(), 3); - - let _ = std::fs::remove_file(output_path); - } - - #[test] - fn test_create_balanced_complete_bipartite_subgraph_rejects_out_of_range_biedges() { - let mut args = create_args_for_bcbs(); - args.biedges = Some("4-0".to_string()); - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("out of bounds for left partition size 4")); - } -} /// Reject non-unit weights when the resolved variant uses `weight=One`. fn reject_nonunit_weights_for_one_variant( @@ -2991,4 +2890,64 @@ mod tests { std::fs::remove_file(output_path).ok(); } + + #[test] + fn test_create_balanced_complete_bipartite_subgraph() { + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::graph::BalancedCompleteBipartiteSubgraph; + + let mut args = empty_args(); + args.problem = Some("BalancedCompleteBipartiteSubgraph".to_string()); + args.biedges = Some("0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3".to_string()); + args.left = Some(4); + args.right = Some(4); + args.k = Some(3); + args.graph = None; + + let output_path = + std::env::temp_dir().join(format!("bcbs-create-{}.json", std::process::id())); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = std::fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "BalancedCompleteBipartiteSubgraph"); + assert!(created.variant.is_empty()); + + let problem: BalancedCompleteBipartiteSubgraph = + serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.left_size(), 4); + assert_eq!(problem.right_size(), 4); + assert_eq!(problem.num_edges(), 12); + assert_eq!(problem.k(), 3); + + let _ = std::fs::remove_file(output_path); + } + + #[test] + fn test_create_balanced_complete_bipartite_subgraph_rejects_out_of_range_biedges() { + let mut args = empty_args(); + args.problem = Some("BalancedCompleteBipartiteSubgraph".to_string()); + args.biedges = Some("4-0".to_string()); + args.left = Some(4); + args.right = Some(4); + args.k = Some(3); + args.graph = None; + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("out of bounds for left partition size 4")); + } }