From f29a8db551316d1c11f6bc12a2359f4cd59d28b4 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Fri, 20 Mar 2026 13:25:13 +0800 Subject: [PATCH 1/6] Add plan for #119: [Rule] GraphPartitioning to QUBO --- .../2026-03-20-graphpartitioning-to-qubo.md | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 docs/plans/2026-03-20-graphpartitioning-to-qubo.md diff --git a/docs/plans/2026-03-20-graphpartitioning-to-qubo.md b/docs/plans/2026-03-20-graphpartitioning-to-qubo.md new file mode 100644 index 000000000..16d27d364 --- /dev/null +++ b/docs/plans/2026-03-20-graphpartitioning-to-qubo.md @@ -0,0 +1,215 @@ +# GraphPartitioning to QUBO Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `GraphPartitioning -> QUBO` reduction, its closed-loop tests, canonical example fixture, and paper entry for issue #119. + +**Architecture:** Implement a direct penalty-method QUBO in a new `src/rules/graphpartitioning_qubo.rs` module. The reduction keeps one binary variable per source vertex, uses the source partition bit-vector as the extracted solution unchanged, and chooses `P = num_edges + 1` so any imbalanced assignment is dominated by every balanced assignment. The paper entry should reuse the canonical exported fixture for the 6-vertex issue example instead of hardcoding duplicated numbers. + +**Tech Stack:** Rust, cargo, existing reduction macros/traits, Typst paper, GitHub pipeline scripts + +--- + +## Batch 1: Rule + tests + exported example + +### Task 1: Add the failing rule tests first + +**Files:** +- Create: `src/unit_tests/rules/graphpartitioning_qubo.rs` +- Modify: `src/rules/mod.rs` +- Create: `src/rules/graphpartitioning_qubo.rs` + +**Step 1: Write the failing tests** + +Create `src/unit_tests/rules/graphpartitioning_qubo.rs` with these concrete checks: +- `test_graphpartitioning_to_qubo_closed_loop`: build the 6-vertex issue graph, reduce with `ReduceTo::>::reduce_to`, solve the target with `BruteForce`, and verify every optimal QUBO solution extracts to a `GraphPartitioning` optimum with cut `3`. +- `test_graphpartitioning_to_qubo_matrix_matches_issue_example`: assert `num_vars == 6`, diagonal entries `[-48.0, -47.0, -46.0, -46.0, -47.0, -48.0]`, edge off-diagonals `18.0`, and non-edge off-diagonals `20.0`. +- `test_graphpartitioning_to_qubo_canonical_example_spec` behind `#[cfg(feature = "example-db")]`: verify the canonical rule example exports `GraphPartitioning` -> `QUBO` with `num_vars == 6` and at least one witness. + +Wire the new rule module into `src/rules/mod.rs` and add only the minimal new `src/rules/graphpartitioning_qubo.rs` skeleton needed for the tests to compile against the module path. + +**Step 2: Run the targeted test to verify RED** + +Run: +```bash +cargo test --features "ilp-highs example-db" graphpartitioning_qubo -- --include-ignored +``` + +Expected: +- FAIL because the `ReduceTo> for GraphPartitioning` implementation and canonical example are still missing/incomplete. + +**Step 3: Commit the red test scaffold** + +```bash +git add src/unit_tests/rules/graphpartitioning_qubo.rs src/rules/mod.rs src/rules/graphpartitioning_qubo.rs +git commit -m "test: add failing GraphPartitioning to QUBO coverage" +``` + +### Task 2: Implement the reduction module + +**Files:** +- Modify: `src/rules/graphpartitioning_qubo.rs` +- Modify: `src/rules/mod.rs` + +**Step 1: Write the minimal implementation** + +Implement `ReductionGraphPartitioningToQUBO` with: +- `target: QUBO` +- `ReductionResult::extract_solution()` returning `target_solution[..num_vertices].to_vec()` or the full vector if the target has exactly one bit per vertex +- `#[reduction(overhead = { num_vars = "num_vertices" })]` +- `ReduceTo> for GraphPartitioning` + +Construct the upper-triangular Q matrix exactly from the issue: +- `P = num_edges as f64 + 1.0` +- diagonal `Q[i][i] = degree(i) as f64 + P * (1.0 - n as f64)` +- off-diagonal `Q[i][j] += 2P` for every `i < j` +- subtract `2.0` from `Q[u][v]` for each graph edge `(u, v)` after normalizing to upper-triangular coordinates + +Add the canonical example spec in the same rule file under `#[cfg(feature = "example-db")]` using the issue’s 6-vertex graph and witness `source_config == target_config == vec![0, 0, 0, 1, 1, 1]`. + +**Step 2: Run the targeted test to verify GREEN** + +Run: +```bash +cargo test --features "ilp-highs example-db" graphpartitioning_qubo -- --include-ignored +``` + +Expected: +- PASS for the new rule tests. + +**Step 3: Refactor only if needed** + +Keep the implementation small. If repeated upper-triangular updates are awkward, extract one tiny local helper in the rule module and rerun the same targeted command. + +**Step 4: Commit** + +```bash +git add src/rules/graphpartitioning_qubo.rs src/rules/mod.rs src/unit_tests/rules/graphpartitioning_qubo.rs +git commit -m "feat: add GraphPartitioning to QUBO reduction" +``` + +### Task 3: Regenerate exported data and verify the canonical example + +**Files:** +- Modify: `docs/src/data/examples.json` +- Modify: generated graph/schema exports if changed by the commands below + +**Step 1: Regenerate the fixture/export artifacts** + +Run: +```bash +cargo run --features "example-db" --example export_examples +cargo run --example export_graph +cargo run --example export_schemas +``` + +Expected: +- The canonical `GraphPartitioning -> QUBO` example appears in the checked-in example export. +- Reduction graph/schema exports include the new primitive rule. + +**Step 2: Verify the example-driven data is stable** + +Run: +```bash +git status --short +``` + +Expected: +- Only the new rule/test/module changes plus expected generated exports are modified. + +**Step 3: Commit** + +```bash +git add docs/src/data/examples.json +git add . +git commit -m "chore: export GraphPartitioning to QUBO fixtures" +``` + +## Batch 2: Paper entry + +### Task 4: Document the reduction in the paper + +**Files:** +- Modify: `docs/paper/reductions.typ` + +**Step 1: Write the failing documentation build** + +Add a `#let gp_qubo = load-example("GraphPartitioning", "QUBO")` block and a `#reduction-rule("GraphPartitioning", "QUBO", ...)` entry in the penalty-method QUBO section near the other QUBO reductions. Use the exported fixture data for the 6-vertex example instead of duplicating the matrix constants by hand. + +The theorem/proof must cover: +- binary variable mapping `x_i in {0,1}` +- cut-counting term `sum_(uv in E) (x_u + x_v - 2 x_u x_v)` +- balance penalty `P (sum_i x_i - n/2)^2` +- explicit QUBO coefficients `Q_(ii) = deg(i) + P(1 - n)` and `Q_(ij) = 2P - 2` on edges / `2P` otherwise +- correctness argument that `P > m` forces balance and the remaining objective equals the cut size +- solution extraction as the identity map from the QUBO bit-vector back to the partition encoding + +**Step 2: Run the documentation build** + +Run: +```bash +make paper +``` + +Expected: +- PASS with the new reduction-rule entry and no completeness warnings for `GraphPartitioning -> QUBO`. + +**Step 3: Commit** + +```bash +git add docs/paper/reductions.typ +git commit -m "docs: add GraphPartitioning to QUBO paper entry" +``` + +## Batch 3: Full verification and cleanup + +### Task 5: Run fresh verification before claiming completion + +**Files:** +- Verify only; no planned edits + +**Step 1: Re-run focused tests** + +Run: +```bash +cargo test --features "ilp-highs example-db" graphpartitioning_qubo -- --include-ignored +``` + +Expected: +- PASS. + +**Step 2: Run repository verification** + +Run: +```bash +make fmt +make test +make clippy +``` + +Expected: +- All commands pass. + +**Step 3: Inspect the working tree before final push** + +Run: +```bash +git status --short +``` + +Expected: +- No unexpected tracked changes remain. + +**Step 4: Commit any final formatting/export deltas** + +```bash +git add -A +git commit -m "chore: finalize GraphPartitioning to QUBO implementation" +``` + +**Step 5: Remove the plan file after implementation** + +```bash +git rm docs/plans/2026-03-20-graphpartitioning-to-qubo.md +git commit -m "chore: remove plan file after implementation" +``` From e4275498b2df174d3804f410b3ecf6a9cc7c9ac2 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Fri, 20 Mar 2026 13:28:30 +0800 Subject: [PATCH 2/6] test: add failing GraphPartitioning to QUBO coverage --- src/rules/graphpartitioning_qubo.rs | 47 +++++++++++ src/rules/mod.rs | 2 + .../rules/graphpartitioning_qubo.rs | 82 +++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 src/rules/graphpartitioning_qubo.rs create mode 100644 src/unit_tests/rules/graphpartitioning_qubo.rs diff --git a/src/rules/graphpartitioning_qubo.rs b/src/rules/graphpartitioning_qubo.rs new file mode 100644 index 000000000..5caf19b87 --- /dev/null +++ b/src/rules/graphpartitioning_qubo.rs @@ -0,0 +1,47 @@ +//! Reduction from GraphPartitioning to QUBO. +//! +//! This file is intentionally scaffolded for TDD. The concrete QUBO +//! construction is added in the implementation task. + +use crate::models::algebraic::QUBO; +use crate::models::graph::GraphPartitioning; +use crate::reduction; +use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::topology::SimpleGraph; + +/// Result of reducing GraphPartitioning to QUBO. +#[derive(Debug, Clone)] +pub struct ReductionGraphPartitioningToQUBO { + target: QUBO, +} + +impl ReductionResult for ReductionGraphPartitioningToQUBO { + type Source = GraphPartitioning; + type Target = QUBO; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + target_solution.to_vec() + } +} + +#[reduction(overhead = { num_vars = "num_vertices" })] +impl ReduceTo> for GraphPartitioning { + type Result = ReductionGraphPartitioningToQUBO; + + fn reduce_to(&self) -> Self::Result { + todo!("GraphPartitioning -> QUBO reduction not implemented yet") + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + Vec::new() +} + +#[cfg(test)] +#[path = "../unit_tests/rules/graphpartitioning_qubo.rs"] +mod tests; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 4740bd9bd..0d09a9b4c 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -9,6 +9,7 @@ pub use registry::{ReductionEntry, ReductionOverhead}; pub(crate) mod circuit_spinglass; pub(crate) mod coloring_qubo; pub(crate) mod factoring_circuit; +pub(crate) mod graphpartitioning_qubo; mod graph; mod kcoloring_casts; mod knapsack_qubo; @@ -87,6 +88,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec GraphPartitioning { + GraphPartitioning::new(SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (3, 5), + (4, 5), + ], + )) +} + +#[test] +fn test_graphpartitioning_to_qubo_closed_loop() { + let source = example_problem(); + let reduction = ReduceTo::>::reduce_to(&source); + + assert_optimization_round_trip_from_optimization_target( + &source, + &reduction, + "GraphPartitioning->QUBO closed loop", + ); +} + +#[test] +fn test_graphpartitioning_to_qubo_matrix_matches_issue_example() { + let source = example_problem(); + let reduction = ReduceTo::>::reduce_to(&source); + let qubo = reduction.target_problem(); + + assert_eq!(qubo.num_vars(), 6); + + let expected_diagonal = [-48.0, -47.0, -46.0, -46.0, -47.0, -48.0]; + for (index, expected) in expected_diagonal.into_iter().enumerate() { + assert_eq!(qubo.get(index, index), Some(&expected)); + } + + let edge_pairs = [ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (3, 5), + (4, 5), + ]; + for &(u, v) in &edge_pairs { + assert_eq!(qubo.get(u, v), Some(&18.0), "edge ({u}, {v})"); + } + + let non_edge_pairs = [(0, 3), (0, 4), (0, 5), (1, 4), (1, 5), (2, 5)]; + for &(u, v) in &non_edge_pairs { + assert_eq!(qubo.get(u, v), Some(&20.0), "non-edge ({u}, {v})"); + } +} + +#[cfg(feature = "example-db")] +#[test] +fn test_graphpartitioning_to_qubo_canonical_example_spec() { + let spec = canonical_rule_example_specs() + .into_iter() + .find(|spec| spec.id == "graphpartitioning_to_qubo") + .expect("missing canonical GraphPartitioning -> QUBO example spec"); + let example = (spec.build)(); + + assert_eq!(example.source.problem, "GraphPartitioning"); + assert_eq!(example.target.problem, "QUBO"); + assert_eq!(example.target.instance["num_vars"], 6); + assert!(!example.solutions.is_empty()); +} From eefe92152db0ac3b350d8f95c809c77f431a3be6 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Fri, 20 Mar 2026 13:29:48 +0800 Subject: [PATCH 3/6] feat: add GraphPartitioning to QUBO reduction --- src/rules/graphpartitioning_qubo.rs | 65 ++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/src/rules/graphpartitioning_qubo.rs b/src/rules/graphpartitioning_qubo.rs index 5caf19b87..a15e03f57 100644 --- a/src/rules/graphpartitioning_qubo.rs +++ b/src/rules/graphpartitioning_qubo.rs @@ -1,13 +1,14 @@ //! Reduction from GraphPartitioning to QUBO. //! -//! This file is intentionally scaffolded for TDD. The concrete QUBO -//! construction is added in the implementation task. +//! Uses the penalty-method QUBO +//! H = sum_(u,v in E) (x_u + x_v - 2 x_u x_v) + P (sum_i x_i - n/2)^2 +//! with P = |E| + 1 so any imbalanced partition is dominated by a balanced one. use crate::models::algebraic::QUBO; use crate::models::graph::GraphPartitioning; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::topology::SimpleGraph; +use crate::topology::{Graph, SimpleGraph}; /// Result of reducing GraphPartitioning to QUBO. #[derive(Debug, Clone)] @@ -33,13 +34,67 @@ impl ReduceTo> for GraphPartitioning { type Result = ReductionGraphPartitioningToQUBO; fn reduce_to(&self) -> Self::Result { - todo!("GraphPartitioning -> QUBO reduction not implemented yet") + let n = self.num_vertices(); + let penalty = self.num_edges() as f64 + 1.0; + let mut matrix = vec![vec![0.0f64; n]; n]; + let mut degrees = vec![0usize; n]; + let edges = self.graph().edges(); + + for &(u, v) in &edges { + degrees[u] += 1; + degrees[v] += 1; + } + + for i in 0..n { + matrix[i][i] = degrees[i] as f64 + penalty * (1.0 - n as f64); + } + + for i in 0..n { + for j in (i + 1)..n { + matrix[i][j] = 2.0 * penalty; + } + } + + for (u, v) in edges { + let (lo, hi) = if u < v { (u, v) } else { (v, u) }; + matrix[lo][hi] -= 2.0; + } + + ReductionGraphPartitioningToQUBO { + target: QUBO::from_matrix(matrix), + } } } #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { - Vec::new() + use crate::export::SolutionPair; + + vec![crate::example_db::specs::RuleExampleSpec { + id: "graphpartitioning_to_qubo", + build: || { + crate::example_db::specs::rule_example_with_witness::<_, QUBO>( + GraphPartitioning::new(SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (3, 5), + (4, 5), + ], + )), + SolutionPair { + source_config: vec![0, 0, 0, 1, 1, 1], + target_config: vec![0, 0, 0, 1, 1, 1], + }, + ) + }, + }] } #[cfg(test)] From 760eb5369824762dbf16a02ecba8d0e1016d7958 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Fri, 20 Mar 2026 13:33:45 +0800 Subject: [PATCH 4/6] docs: add GraphPartitioning to QUBO paper entry --- docs/paper/reductions.typ | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 09da8adb3..e3fa35528 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -4250,6 +4250,48 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Solution extraction._ For each vertex $u$, find terminal position $t$ with $x_(u,t) = 1$. For each edge $(u,v)$, output 1 (cut) if $u$ and $v$ are in different components, 0 otherwise. ] +#let gp_qubo = load-example("GraphPartitioning", "QUBO") +#let gp_qubo_sol = gp_qubo.solutions.at(0) +#let gp_qubo_edges = gp_qubo.source.instance.graph.edges.map(e => (e.at(0), e.at(1))) +#let gp_qubo_n = gp_qubo.source.instance.graph.num_vertices +#let gp_qubo_m = gp_qubo_edges.len() +#let gp_qubo_penalty = gp_qubo_m + 1 +#let gp_qubo_diag = range(0, gp_qubo_n).map(i => gp_qubo.target.instance.matrix.at(i).at(i)) +#let gp_qubo_cut_edges = gp_qubo_edges.filter(e => gp_qubo_sol.source_config.at(e.at(0)) != gp_qubo_sol.source_config.at(e.at(1))) +#let gp_qubo_cut_size = gp_qubo_cut_edges.len() +#reduction-rule("GraphPartitioning", "QUBO", + example: true, + example-caption: [6-vertex balanced partition instance ($n = #gp_qubo_n$, $|E| = #gp_qubo_m$)], + extra: [ + *Step 1 -- Binary partition variables.* Introduce one binary variable per vertex: $x_i = 0$ means vertex $i$ is in the left block, $x_i = 1$ means it is in the right block. For the canonical instance, this gives $n = #gp_qubo_n$ QUBO variables: + $ x_0, x_1, x_2, x_3, x_4, x_5 $ + + *Step 2 -- Choose the balance penalty.* The source graph has $m = #gp_qubo_m$ edges, so the construction uses $P = m + 1 = #gp_qubo_penalty$. Any imbalance contributes at least $P$, which is already larger than the maximum possible cut size of any balanced partition. + + *Step 3 -- Fill the QUBO matrix.* The diagonal entries are $Q_(i i) = deg(i) + P(1 - n)$, which evaluates here to $(#gp_qubo_diag.map(str).join(", "))$. For every pair $i < j$, start from $Q_(i j) = 2P = #(2 * gp_qubo_penalty)$, then subtract $2$ when $(i,j)$ is an edge. Hence edge coefficients become $18$ while non-edge coefficients stay $20$; the exported upper-triangular matrix matches the issue example exactly.\ + + *Step 4 -- Verify a solution.* The exported witness is $bold(x) = (#gp_qubo_sol.target_config.map(str).join(", "))$, which is also the source partition encoding. The cut edges are #gp_qubo_cut_edges.map(e => "(" + str(e.at(0)) + "," + str(e.at(1)) + ")").join(", "), so the cut size is $#gp_qubo_cut_size$ and the balance penalty vanishes because exactly #(gp_qubo_n / 2) vertices are assigned to the right block #sym.checkmark. + ], +)[ + Graph Partitioning (minimum bisection) asks for a balanced bipartition minimizing the number of crossing edges. Lucas's Ising formulation @lucas2014 translates directly to a QUBO by combining a cut-counting quadratic objective with a quadratic equality penalty enforcing $sum_i x_i = n / 2$. The reduction uses one binary variable per source vertex, so the QUBO has exactly $n$ variables. +][ + _Construction._ Given an undirected graph $G = (V, E)$ with even $n = |V|$ and $m = |E|$, introduce binary variables $x_i in {0,1}$ for each vertex $i in V$. Interpret $x_i = 0$ as $i in A$ and $x_i = 1$ as $i in B$. The cut objective is: + $ H_"cut" = sum_((u,v) in E) (x_u + x_v - 2 x_u x_v) $ + because the term equals $1$ exactly when edge $(u,v)$ crosses the partition. To enforce balance, add: + $ H_"bal" = P (sum_i x_i - n/2)^2 $ + with penalty $P = m + 1$. The QUBO objective is $H = H_"cut" + H_"bal"$. + + Expanding $H_"bal"$ with $x_i^2 = x_i$ gives: + $ H_"bal" = P (1 - n) sum_i x_i + 2 P sum_(i < j) x_i x_j + P n^2 / 4 $ + so the upper-triangular QUBO coefficients are: + $ Q_(i i) = deg(i) + P (1 - n) $ + and for $i < j$, $Q_(i j) = 2 P$ for every pair, then subtract $2$ whenever $(i,j) in E$. Equivalently, edge pairs have coefficient $2P - 2$ and non-edge pairs have coefficient $2P$. The additive constant $P n^2 / 4$ does not affect the minimizer. + + _Correctness._ ($arrow.r.double$) If $bold(x)$ encodes a balanced partition, then $sum_i x_i = n/2$ and $H_"bal" = 0$, so the QUBO objective equals the cut size exactly. ($arrow.l.double$) If $bold(x)$ is imbalanced, then $|sum_i x_i - n/2| >= 1$, hence $H_"bal" >= P = m + 1$. Every balanced partition has cut size at most $m$, so any imbalanced assignment has objective strictly larger than at least one balanced assignment. Therefore every QUBO minimizer is balanced, and among balanced assignments minimizing $H$ is identical to minimizing the cut size. + + _Solution extraction._ Return the QUBO bit-vector directly: the same binary assignment already records the source partition. +] + #let qubo_ilp = load-example("QUBO", "ILP") #let qubo_ilp_sol = qubo_ilp.solutions.at(0) #reduction-rule("QUBO", "ILP", From fef3e108deeefd00905b03ab54534a8d4680a22d Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Fri, 20 Mar 2026 13:45:32 +0800 Subject: [PATCH 5/6] chore: finalize GraphPartitioning to QUBO pipeline work --- .../2026-03-20-graphpartitioning-to-qubo.md | 215 ------------------ src/rules/graphpartitioning_qubo.rs | 11 +- src/rules/mod.rs | 2 +- 3 files changed, 5 insertions(+), 223 deletions(-) delete mode 100644 docs/plans/2026-03-20-graphpartitioning-to-qubo.md diff --git a/docs/plans/2026-03-20-graphpartitioning-to-qubo.md b/docs/plans/2026-03-20-graphpartitioning-to-qubo.md deleted file mode 100644 index 16d27d364..000000000 --- a/docs/plans/2026-03-20-graphpartitioning-to-qubo.md +++ /dev/null @@ -1,215 +0,0 @@ -# GraphPartitioning to QUBO Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `GraphPartitioning -> QUBO` reduction, its closed-loop tests, canonical example fixture, and paper entry for issue #119. - -**Architecture:** Implement a direct penalty-method QUBO in a new `src/rules/graphpartitioning_qubo.rs` module. The reduction keeps one binary variable per source vertex, uses the source partition bit-vector as the extracted solution unchanged, and chooses `P = num_edges + 1` so any imbalanced assignment is dominated by every balanced assignment. The paper entry should reuse the canonical exported fixture for the 6-vertex issue example instead of hardcoding duplicated numbers. - -**Tech Stack:** Rust, cargo, existing reduction macros/traits, Typst paper, GitHub pipeline scripts - ---- - -## Batch 1: Rule + tests + exported example - -### Task 1: Add the failing rule tests first - -**Files:** -- Create: `src/unit_tests/rules/graphpartitioning_qubo.rs` -- Modify: `src/rules/mod.rs` -- Create: `src/rules/graphpartitioning_qubo.rs` - -**Step 1: Write the failing tests** - -Create `src/unit_tests/rules/graphpartitioning_qubo.rs` with these concrete checks: -- `test_graphpartitioning_to_qubo_closed_loop`: build the 6-vertex issue graph, reduce with `ReduceTo::>::reduce_to`, solve the target with `BruteForce`, and verify every optimal QUBO solution extracts to a `GraphPartitioning` optimum with cut `3`. -- `test_graphpartitioning_to_qubo_matrix_matches_issue_example`: assert `num_vars == 6`, diagonal entries `[-48.0, -47.0, -46.0, -46.0, -47.0, -48.0]`, edge off-diagonals `18.0`, and non-edge off-diagonals `20.0`. -- `test_graphpartitioning_to_qubo_canonical_example_spec` behind `#[cfg(feature = "example-db")]`: verify the canonical rule example exports `GraphPartitioning` -> `QUBO` with `num_vars == 6` and at least one witness. - -Wire the new rule module into `src/rules/mod.rs` and add only the minimal new `src/rules/graphpartitioning_qubo.rs` skeleton needed for the tests to compile against the module path. - -**Step 2: Run the targeted test to verify RED** - -Run: -```bash -cargo test --features "ilp-highs example-db" graphpartitioning_qubo -- --include-ignored -``` - -Expected: -- FAIL because the `ReduceTo> for GraphPartitioning` implementation and canonical example are still missing/incomplete. - -**Step 3: Commit the red test scaffold** - -```bash -git add src/unit_tests/rules/graphpartitioning_qubo.rs src/rules/mod.rs src/rules/graphpartitioning_qubo.rs -git commit -m "test: add failing GraphPartitioning to QUBO coverage" -``` - -### Task 2: Implement the reduction module - -**Files:** -- Modify: `src/rules/graphpartitioning_qubo.rs` -- Modify: `src/rules/mod.rs` - -**Step 1: Write the minimal implementation** - -Implement `ReductionGraphPartitioningToQUBO` with: -- `target: QUBO` -- `ReductionResult::extract_solution()` returning `target_solution[..num_vertices].to_vec()` or the full vector if the target has exactly one bit per vertex -- `#[reduction(overhead = { num_vars = "num_vertices" })]` -- `ReduceTo> for GraphPartitioning` - -Construct the upper-triangular Q matrix exactly from the issue: -- `P = num_edges as f64 + 1.0` -- diagonal `Q[i][i] = degree(i) as f64 + P * (1.0 - n as f64)` -- off-diagonal `Q[i][j] += 2P` for every `i < j` -- subtract `2.0` from `Q[u][v]` for each graph edge `(u, v)` after normalizing to upper-triangular coordinates - -Add the canonical example spec in the same rule file under `#[cfg(feature = "example-db")]` using the issue’s 6-vertex graph and witness `source_config == target_config == vec![0, 0, 0, 1, 1, 1]`. - -**Step 2: Run the targeted test to verify GREEN** - -Run: -```bash -cargo test --features "ilp-highs example-db" graphpartitioning_qubo -- --include-ignored -``` - -Expected: -- PASS for the new rule tests. - -**Step 3: Refactor only if needed** - -Keep the implementation small. If repeated upper-triangular updates are awkward, extract one tiny local helper in the rule module and rerun the same targeted command. - -**Step 4: Commit** - -```bash -git add src/rules/graphpartitioning_qubo.rs src/rules/mod.rs src/unit_tests/rules/graphpartitioning_qubo.rs -git commit -m "feat: add GraphPartitioning to QUBO reduction" -``` - -### Task 3: Regenerate exported data and verify the canonical example - -**Files:** -- Modify: `docs/src/data/examples.json` -- Modify: generated graph/schema exports if changed by the commands below - -**Step 1: Regenerate the fixture/export artifacts** - -Run: -```bash -cargo run --features "example-db" --example export_examples -cargo run --example export_graph -cargo run --example export_schemas -``` - -Expected: -- The canonical `GraphPartitioning -> QUBO` example appears in the checked-in example export. -- Reduction graph/schema exports include the new primitive rule. - -**Step 2: Verify the example-driven data is stable** - -Run: -```bash -git status --short -``` - -Expected: -- Only the new rule/test/module changes plus expected generated exports are modified. - -**Step 3: Commit** - -```bash -git add docs/src/data/examples.json -git add . -git commit -m "chore: export GraphPartitioning to QUBO fixtures" -``` - -## Batch 2: Paper entry - -### Task 4: Document the reduction in the paper - -**Files:** -- Modify: `docs/paper/reductions.typ` - -**Step 1: Write the failing documentation build** - -Add a `#let gp_qubo = load-example("GraphPartitioning", "QUBO")` block and a `#reduction-rule("GraphPartitioning", "QUBO", ...)` entry in the penalty-method QUBO section near the other QUBO reductions. Use the exported fixture data for the 6-vertex example instead of duplicating the matrix constants by hand. - -The theorem/proof must cover: -- binary variable mapping `x_i in {0,1}` -- cut-counting term `sum_(uv in E) (x_u + x_v - 2 x_u x_v)` -- balance penalty `P (sum_i x_i - n/2)^2` -- explicit QUBO coefficients `Q_(ii) = deg(i) + P(1 - n)` and `Q_(ij) = 2P - 2` on edges / `2P` otherwise -- correctness argument that `P > m` forces balance and the remaining objective equals the cut size -- solution extraction as the identity map from the QUBO bit-vector back to the partition encoding - -**Step 2: Run the documentation build** - -Run: -```bash -make paper -``` - -Expected: -- PASS with the new reduction-rule entry and no completeness warnings for `GraphPartitioning -> QUBO`. - -**Step 3: Commit** - -```bash -git add docs/paper/reductions.typ -git commit -m "docs: add GraphPartitioning to QUBO paper entry" -``` - -## Batch 3: Full verification and cleanup - -### Task 5: Run fresh verification before claiming completion - -**Files:** -- Verify only; no planned edits - -**Step 1: Re-run focused tests** - -Run: -```bash -cargo test --features "ilp-highs example-db" graphpartitioning_qubo -- --include-ignored -``` - -Expected: -- PASS. - -**Step 2: Run repository verification** - -Run: -```bash -make fmt -make test -make clippy -``` - -Expected: -- All commands pass. - -**Step 3: Inspect the working tree before final push** - -Run: -```bash -git status --short -``` - -Expected: -- No unexpected tracked changes remain. - -**Step 4: Commit any final formatting/export deltas** - -```bash -git add -A -git commit -m "chore: finalize GraphPartitioning to QUBO implementation" -``` - -**Step 5: Remove the plan file after implementation** - -```bash -git rm docs/plans/2026-03-20-graphpartitioning-to-qubo.md -git commit -m "chore: remove plan file after implementation" -``` diff --git a/src/rules/graphpartitioning_qubo.rs b/src/rules/graphpartitioning_qubo.rs index a15e03f57..8e32846c9 100644 --- a/src/rules/graphpartitioning_qubo.rs +++ b/src/rules/graphpartitioning_qubo.rs @@ -45,13 +45,10 @@ impl ReduceTo> for GraphPartitioning { degrees[v] += 1; } - for i in 0..n { - matrix[i][i] = degrees[i] as f64 + penalty * (1.0 - n as f64); - } - - for i in 0..n { - for j in (i + 1)..n { - matrix[i][j] = 2.0 * penalty; + for (i, row) in matrix.iter_mut().enumerate() { + row[i] = degrees[i] as f64 + penalty * (1.0 - n as f64); + for value in row.iter_mut().skip(i + 1) { + *value = 2.0 * penalty; } } diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 0d09a9b4c..9a6156d49 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -9,8 +9,8 @@ pub use registry::{ReductionEntry, ReductionOverhead}; pub(crate) mod circuit_spinglass; pub(crate) mod coloring_qubo; pub(crate) mod factoring_circuit; -pub(crate) mod graphpartitioning_qubo; mod graph; +pub(crate) mod graphpartitioning_qubo; mod kcoloring_casts; mod knapsack_qubo; mod ksatisfiability_casts; From b98b61f782cca2cae6aec20f26d9157a0e85bbe9 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 12:01:21 +0800 Subject: [PATCH 6/6] Add GraphPartitioning->QUBO to dominated-rules allow-list After merging main (which added GraphPartitioning->MaxCut), the indirect path GraphPartitioning->MaxCut->SpinGlass->QUBO dominates the direct GraphPartitioning->QUBO reduction. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/unit_tests/rules/analysis.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/unit_tests/rules/analysis.rs b/src/unit_tests/rules/analysis.rs index 020b89b66..97b37b19b 100644 --- a/src/unit_tests/rules/analysis.rs +++ b/src/unit_tests/rules/analysis.rs @@ -245,6 +245,11 @@ fn test_find_dominated_rules_returns_known_set() { ("Factoring", "ILP {variable: \"i32\"}"), // K3-SAT → QUBO via SAT → CircuitSAT → SpinGlass chain ("KSatisfiability {k: \"K3\"}", "QUBO {weight: \"f64\"}"), + // GraphPartitioning -> MaxCut -> SpinGlass -> QUBO is better + ( + "GraphPartitioning {graph: \"SimpleGraph\"}", + "QUBO {weight: \"f64\"}", + ), // Knapsack -> ILP -> QUBO is better than the direct penalty reduction ("Knapsack", "QUBO {weight: \"f64\"}"), // MaxMatching → MaxSetPacking → ILP is better than direct MaxMatching → ILP