diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index d9ac82a60..fe69cb2d9 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -81,7 +81,6 @@ "MaximumIndependentSet": [Maximum Independent Set], "MinimumVertexCover": [Minimum Vertex Cover], "MaxCut": [Max-Cut], - "GraphPartitioning": [Graph Partitioning], "GeneralizedHex": [Generalized Hex], "HamiltonianCircuit": [Hamiltonian Circuit], "BiconnectivityAugmentation": [Biconnectivity Augmentation], @@ -610,70 +609,6 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] ] } -#{ - let x = load-model-example("GraphPartitioning") - let nv = graph-num-vertices(x.instance) - let ne = graph-num-edges(x.instance) - let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1))) - let config = x.optimal_config - let cut-val = metric-value(x.optimal_value) - let side-a = range(nv).filter(i => config.at(i) == 0) - let side-b = range(nv).filter(i => config.at(i) == 1) - let cut-edges = edges.filter(e => config.at(e.at(0)) != config.at(e.at(1))) - [ - #problem-def("GraphPartitioning")[ - Given an undirected graph $G = (V, E)$ with $|V| = n$ (even), find a partition of $V$ into two disjoint sets $A$ and $B$ with $|A| = |B| = n slash 2$ that minimizes the number of edges crossing the partition: - $ "cut"(A, B) = |{(u, v) in E : u in A, v in B}|. $ - ][ - Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel computing, and scientific simulation, where balanced workload distribution with minimal communication is essential. Closely related to Max-Cut (which _maximizes_ rather than _minimizes_ the cut) and to the Ising Spin Glass model. NP-completeness was proved by Garey, Johnson and Stockmeyer @garey1976. Arora, Rao and Vazirani @arora2009 gave an $O(sqrt(log n))$-approximation algorithm. The best known unconditional exact algorithm is brute-force enumeration of all $binom(n, n slash 2) = O^*(2^n)$ balanced partitions; no faster worst-case algorithm is known. Cygan et al. @cygan2014 showed that Minimum Bisection is fixed-parameter tractable in $O(2^(O(k^3)) dot n^3 log^3 n)$ time parameterized by bisection width $k$. Standard partitioning tools include METIS, KaHIP, and Scotch. - - *Example.* Consider the graph $G$ with $n = #nv$ vertices and #ne edges. The optimal balanced partition is $A = {#side-a.map(i => $v_#i$).join($,$)}$, $B = {#side-b.map(i => $v_#i$).join($,$)}$, with cut value #cut-val. - - #pred-commands( - "pred create --example GraphPartitioning -o graph-partitioning.json", - "pred solve graph-partitioning.json", - "pred evaluate graph-partitioning.json --config " + x.optimal_config.map(str).join(","), - ) - - #figure( - canvas(length: 1cm, { - // Two-column layout for balanced partition - let half = int(nv / 2) - let verts = ( - ..range(half).map(i => (0, (half - 1 - i))), - ..range(half).map(i => (2.5, (half - 1 - i))), - ) - // Draw edges - for (u, v) in edges { - let crossing = config.at(u) != config.at(v) - g-edge(verts.at(u), verts.at(v), - stroke: if crossing { 2pt + graph-colors.at(1) } else { 1pt + luma(180) }) - } - // Draw partition regions - import draw: * - on-layer(-1, { - rect((-0.5, -0.5), (0.5, half - 0.5), - fill: graph-colors.at(0).transparentize(90%), - stroke: (dash: "dashed", paint: graph-colors.at(0), thickness: 0.8pt)) - content((0, half - 0.2), text(8pt, fill: graph-colors.at(0))[$A$]) - rect((2.0, -0.5), (3.0, half - 0.5), - fill: graph-colors.at(1).transparentize(90%), - stroke: (dash: "dashed", paint: graph-colors.at(1), thickness: 0.8pt)) - content((2.5, half - 0.2), text(8pt, fill: graph-colors.at(1))[$B$]) - }) - // Draw nodes - for (k, pos) in verts.enumerate() { - let in-a = config.at(k) == 0 - g-node(pos, name: "v" + str(k), - fill: if in-a { graph-colors.at(0) } else { graph-colors.at(1) }, - label: text(fill: white)[$v_#k$]) - } - }), - caption: [Graph with $n = #nv$ vertices partitioned into $A$ (blue) and $B$ (red). The #cut-val crossing edges are shown in bold red; internal edges are gray.], - ) - ] - ] -} #problem-def("MinimumCutIntoBoundedSets")[ Given an undirected graph $G = (V, E)$ with edge weights $w: E -> ZZ^+$, designated vertices $s, t in V$, and a positive integer $B <= |V|$, find a partition of $V$ into disjoint sets $V_1$ and $V_2$ such that $s in V_1$, $t in V_2$, $|V_1| <= B$, $|V_2| <= B$, that minimizes the total cut weight $ sum_({u,v} in E: u in V_1, v in V_2) w({u,v}). $ @@ -6503,57 +6438,6 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead _Solution extraction._ For VC solution $C$, return $S = V backslash C$, i.e.\ flip each variable: $s_v = 1 - c_v$. ] -#let gp_mc = load-example("GraphPartitioning", "MaxCut") -#let gp_mc_sol = gp_mc.solutions.at(0) -#let gp_mc_source_edges = gp_mc.source.instance.graph.edges.map(e => (e.at(0), e.at(1))) -#let gp_mc_target_edges = gp_mc.target.instance.graph.edges.map(e => (e.at(0), e.at(1))) -#let gp_mc_weights = gp_mc.target.instance.edge_weights -#let gp_mc_nv = gp_mc.source.instance.graph.num_vertices -#let gp_mc_ne = gp_mc_source_edges.len() -#let gp_mc_penalty = gp_mc_ne + 1 -#let gp_mc_side_a = range(gp_mc_nv).filter(i => gp_mc_sol.source_config.at(i) == 0) -#let gp_mc_side_b = range(gp_mc_nv).filter(i => gp_mc_sol.source_config.at(i) == 1) -#let gp_mc_weight_lo = gp_mc_target_edges.enumerate().filter(((i, e)) => gp_mc_weights.at(i) == gp_mc_penalty - 1).map(((i, e)) => e) -#let gp_mc_weight_hi = gp_mc_target_edges.enumerate().filter(((i, e)) => gp_mc_weights.at(i) == gp_mc_penalty).map(((i, e)) => e) -#let gp_mc_source_cross = gp_mc_source_edges.filter(e => gp_mc_sol.source_config.at(e.at(0)) != gp_mc_sol.source_config.at(e.at(1))) -#let gp_mc_cut_lo = gp_mc_target_edges.enumerate().filter(((i, e)) => - gp_mc_weights.at(i) == gp_mc_penalty - 1 and - gp_mc_sol.target_config.at(e.at(0)) != gp_mc_sol.target_config.at(e.at(1)) -).map(((i, e)) => e) -#let gp_mc_cut_hi = gp_mc_target_edges.enumerate().filter(((i, e)) => - gp_mc_weights.at(i) == gp_mc_penalty and - gp_mc_sol.target_config.at(e.at(0)) != gp_mc_sol.target_config.at(e.at(1)) -).map(((i, e)) => e) -#let gp_mc_cut_value = gp_mc_target_edges.enumerate().filter(((i, e)) => - gp_mc_sol.target_config.at(e.at(0)) != gp_mc_sol.target_config.at(e.at(1)) -).map(((i, e)) => gp_mc_weights.at(i)).sum(default: 0) -#reduction-rule("GraphPartitioning", "MaxCut", - example: true, - example-caption: [6-vertex minimum bisection to weighted Max-Cut], - extra: [ - #pred-commands( - "pred create --example GraphPartitioning -o graphpartitioning.json", - "pred reduce graphpartitioning.json --to " + target-spec(gp_mc) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate graphpartitioning.json --config " + gp_mc_sol.source_config.map(str).join(","), - ) - Here $m = #gp_mc_ne$, so $P = m + 1 = #gp_mc_penalty$ \ - Weight $#(gp_mc_penalty - 1)$ edges (original edges): {#gp_mc_weight_lo.map(e => $(v_#(e.at(0)), v_#(e.at(1)))$).join(", ")} \ - Weight $#gp_mc_penalty$ edges (non-edges): {#gp_mc_weight_hi.map(e => $(v_#(e.at(0)), v_#(e.at(1)))$).join(", ")} \ - Canonical witness $A = {#gp_mc_side_a.map(i => $v_#i$).join(", ")}$, $B = {#gp_mc_side_b.map(i => $v_#i$).join(", ")}$ cuts source edges {#gp_mc_source_cross.map(e => $(v_#(e.at(0)), v_#(e.at(1)))$).join(", ")} and attains weighted cut $#gp_mc_cut_lo.len() * #(gp_mc_penalty - 1) + #gp_mc_cut_hi.len() * #gp_mc_penalty = #gp_mc_cut_value$ #sym.checkmark - ], -)[ - @garey1976 Graph Partitioning minimizes cut edges subject to a perfect-balance constraint, while Max-Cut maximizes a weighted cut without any balance constraint. A standard folklore construction in combinatorial optimization removes that constraint by rewarding every cross-pair equally and then subtracting one unit on original edges. The resulting weighted complete graph forces every optimum to be balanced first, and among balanced cuts it exactly minimizes the original bisection width. -][ - _Construction._ Given a Graph Partitioning instance $G = (V, E)$ with $n = |V|$ and $m = |E|$, set $P = m + 1$. Build the complete graph $G' = (V, E')$ on the same vertex set, where $E'$ contains every unordered pair $\{u, v\}$ with $u != v$. Assign weight $w'_(u, v) = P - 1$ when $(u, v) in E$, and $w'_(u, v) = P$ otherwise. For any partition $(A, B)$ of $V$, the weighted cut in $G'$ is - $ "cut"_(G')(A, B) = P |A| |B| - "cut"_G(A, B). $ - - _Correctness._ ($arrow.r.double$) Let $(A, B)$ be a maximum cut of $G'$. If it were unbalanced, then $|A| |B|$ would be at least one smaller than for a balanced partition $(A', B')$. Hence - $ "cut"_(G')(A', B') - "cut"_(G')(A, B) >= P - ("cut"_G(A', B') - "cut"_G(A, B)) >= P - m > 0, $ - because $0 <= "cut"_G(·, ·) <= m$ and $P = m + 1$. Therefore every maximum cut of $G'$ is balanced. Among balanced partitions, $P |A| |B| = P (n slash 2)^2$ is constant, so maximizing $"cut"_(G')(A, B)$ is equivalent to minimizing $"cut"_G(A, B)$. ($arrow.l.double$) Conversely, every minimum bisection of $G$ is balanced and therefore maximizes $P |A| |B| - "cut"_G(A, B)$ in $G'$. - - _Solution extraction._ Read off the same partition vector on the original vertex set: the Max-Cut bit for vertex $v$ is already the Graph Partitioning bit for $v$. -] #let mis_clique = load-example("MaximumIndependentSet", "MaximumClique") #let mis_clique_sol = mis_clique.solutions.at(0) @@ -7074,53 +6958,6 @@ 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: [ - #pred-commands( - "pred create --example GraphPartitioning -o graphpartitioning.json", - "pred reduce graphpartitioning.json --to " + target-spec(gp_qubo) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate graphpartitioning.json --config " + gp_qubo_sol.source_config.map(str).join(","), - ) - *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) @@ -7655,55 +7492,6 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ $K = {v : x_v = 1}$. ] -#let gp_ilp = load-example("GraphPartitioning", "ILP") -#let gp_ilp_sol = gp_ilp.solutions.at(0) -#let gp_n = graph-num-vertices(gp_ilp.source.instance) -#let gp_edges = gp_ilp.source.instance.graph.edges -#let gp_m = gp_edges.len() -#let gp_part_a = range(gp_n).filter(i => gp_ilp_sol.source_config.at(i) == 0) -#let gp_part_b = range(gp_n).filter(i => gp_ilp_sol.source_config.at(i) == 1) -#let gp_crossing = range(gp_m).filter(i => gp_ilp_sol.target_config.at(gp_n + i) == 1) -#let gp_crossing_edges = gp_crossing.map(i => gp_edges.at(i)) -#reduction-rule("GraphPartitioning", "ILP", - example: true, - example-caption: [Two triangles linked by three crossing edges encoded as a 15-variable ILP.], - extra: [ - #pred-commands( - "pred create --example GraphPartitioning -o graphpartitioning.json", - "pred reduce graphpartitioning.json --to " + target-spec(gp_ilp) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate graphpartitioning.json --config " + gp_ilp_sol.source_config.map(str).join(","), - ) - *Step 1 -- Balanced partition variables.* Introduce $x_v in {0,1}$ for each vertex. In the canonical witness, $A = {#gp_part_a.map(str).join(", ")}$ and $B = {#gp_part_b.map(str).join(", ")}$, so $bold(x) = (#gp_ilp_sol.source_config.map(str).join(", "))$.\ - - *Step 2 -- Crossing indicators.* Add one binary variable per edge, so the target has $#gp_ilp.target.instance.num_vars$ binary variables and #gp_ilp.target.instance.constraints.len() constraints in total. The three active crossing indicators correspond to edges $\{#gp_crossing_edges.map(e => "(" + str(e.at(0)) + "," + str(e.at(1)) + ")").join(", ")\}$.\ - - *Step 3 -- Verify the objective.* The target witness $bold(z) = (#gp_ilp_sol.target_config.map(str).join(", "))$ sets exactly #gp_crossing.len() edge-indicator variables to 1, so the ILP objective equals the bisection width #gp_crossing.len() #sym.checkmark - ], -)[ - The node-and-edge integer-programming formulation of Chopra and Rao @chopra1993 models a balanced cut with one binary variable per vertex and one binary crossing indicator per edge. A single balance equality enforces the bisection, and two linear inequalities per edge linearize $|x_u - x_v|$ so that the objective can minimize the number of crossing edges directly. -][ - _Construction._ Given graph $G = (V, E)$ with $n = |V|$ and $m = |E|$: - - _Variables._ Binary $x_v in {0, 1}$ for each $v in V$, where $x_v = 1$ means vertex $v$ is placed in side $B$. For each edge $e = (u, v) in E$, binary $y_e in {0, 1}$ indicates whether $e$ crosses the partition. Total: $n + m$ variables. - - _Constraints._ (1) Balance: $sum_(v in V) x_v = n / 2$. If $n$ is odd, the right-hand side is fractional, so the ILP is infeasible exactly when Graph Partitioning has no valid balanced partition. (2) For each edge $e = (u, v)$: $y_e >= x_u - x_v$ and $y_e >= x_v - x_u$. Since $y_e$ is binary and the objective minimizes $sum_e y_e$, these inequalities force $y_e = 1$ exactly for crossing edges. Total: $2m + 1$ constraints. - - _Objective._ Minimize $sum_(e in E) y_e$. - - The ILP is: - $ - min quad & sum_(e in E) y_e \ - "subject to" quad & sum_(v in V) x_v = n / 2 \ - & y_e >= x_u - x_v quad forall e = (u, v) in E \ - & y_e >= x_v - x_u quad forall e = (u, v) in E \ - & x_v in {0, 1}, y_e in {0, 1} - $. - - _Correctness._ ($arrow.r.double$) Given a balanced partition $(A, B)$, set $x_v = 1$ iff $v in B$, and set $y_e = 1$ iff edge $e$ has one endpoint in each side. The balance constraint holds because $|B| = n / 2$, and the linking inequalities hold because $|x_u - x_v| = 1$ exactly on crossing edges. The objective is therefore the cut size. ($arrow.l.double$) Any feasible ILP solution satisfies the balance equation, so exactly half the vertices have $x_v = 1$ when $n$ is even. For each edge, the linking inequalities imply $y_e >= |x_u - x_v|$; minimization therefore chooses $y_e = |x_u - x_v|$, making the objective count precisely the crossing edges of the extracted partition. - - _Solution extraction._ Return the first $n$ variables $(x_v)_(v in V)$ as the Graph Partitioning configuration; the edge-indicator variables are auxiliary. -] #let ks_ilp = load-example("Knapsack", "ILP") #let ks_ilp_sol = ks_ilp.solutions.at(0) diff --git a/docs/src/cli.md b/docs/src/cli.md index f799801ff..b168cb9e4 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -135,7 +135,6 @@ Registered problems: 50 types, 59 reductions, 69 variant nodes ExactCoverBy3Sets * X3C O(2^universe_size) Factoring * 2 O(exp((m + n)^0.3333333333333333 * log(m + n)^0.6666666666666666)) FlowShopScheduling * O(factorial(num_jobs)) - GraphPartitioning/SimpleGraph * O(2^num_vertices) HamiltonianPath/SimpleGraph * O(1.657^num_vertices) ILP/bool * 2 O(2^num_vars) ILP/i32 O(num_vars^num_vars) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index c18a94ade..95781bc02 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -228,7 +228,6 @@ Flags by problem type: KClique --graph, --k MinimumMultiwayCut --graph, --terminals, --edge-weights PartitionIntoTriangles --graph - GraphPartitioning --graph GeneralizedHex --graph, --source, --sink IntegralFlowWithMultipliers --arcs, --capacities, --source, --sink, --multipliers, --requirement MinimumCutIntoBoundedSets --graph, --edge-weights, --source, --sink, --size-bound diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 900aafe19..125284030 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -13,8 +13,8 @@ use problemreductions::models::algebraic::{ }; use problemreductions::models::formula::Quantifier; use problemreductions::models::graph::{ - DisjointConnectingPaths, GeneralizedHex, GraphPartitioning, HamiltonianCircuit, - HamiltonianPath, IntegralFlowBundles, LengthBoundedDisjointPaths, LongestCircuit, LongestPath, + DisjointConnectingPaths, GeneralizedHex, HamiltonianCircuit, HamiltonianPath, + IntegralFlowBundles, LengthBoundedDisjointPaths, LongestCircuit, LongestPath, MinimumCutIntoBoundedSets, MinimumDummyActivitiesPert, MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching, PathConstrainedNetworkFlow, RootedTreeArrangement, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, @@ -537,7 +537,6 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { _ => "--graph 0-1,1-2,2-3 --weights 1,1,1,1", }, "KClique" => "--graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3", - "GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3", "GeneralizedHex" => "--graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5", "IntegralFlowBundles" => { "--arcs \"0>1,0>2,1>3,2>3,1>2,2>1\" --bundles \"0,1;2,5;3,4\" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4" @@ -1135,19 +1134,6 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { (data, resolved_variant.clone()) } - // Graph partitioning (graph only, no weights) - "GraphPartitioning" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create GraphPartitioning --graph 0-1,1-2,2-3,0-2,1-3,0-3" - ) - })?; - ( - ser(GraphPartitioning::new(graph))?, - resolved_variant.clone(), - ) - } - // Generalized Hex (graph + source + sink) "GeneralizedHex" => { let usage = @@ -5905,27 +5891,6 @@ fn create_random( ) } - // GraphPartitioning (graph only, no weights; requires even vertex count) - "GraphPartitioning" => { - let num_vertices = if num_vertices % 2 != 0 { - eprintln!( - "Warning: GraphPartitioning requires even vertex count; rounding {} up to {}", - num_vertices, - num_vertices + 1 - ); - num_vertices + 1 - } else { - num_vertices - }; - let edge_prob = args.edge_prob.unwrap_or(0.5); - if !(0.0..=1.0).contains(&edge_prob) { - bail!("--edge-prob must be between 0.0 and 1.0"); - } - let graph = util::create_random_graph(num_vertices, edge_prob, args.seed); - let variant = variant_map(&[("graph", "SimpleGraph")]); - (ser(GraphPartitioning::new(graph))?, variant) - } - // Hamiltonian Circuit (graph only, no weights) "HamiltonianCircuit" => { let edge_prob = args.edge_prob.unwrap_or(0.5); diff --git a/src/lib.rs b/src/lib.rs index 2eca06109..4a4dfae7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,10 +53,10 @@ pub mod prelude { AcyclicPartition, BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation, BottleneckTravelingSalesman, BoundedComponentSpanningForest, DirectedTwoCommodityIntegralFlow, DisjointConnectingPaths, GeneralizedHex, - GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IntegralFlowBundles, - IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, IsomorphicSpanningTree, KClique, - KthBestSpanningTree, LengthBoundedDisjointPaths, LongestPath, MixedChinesePostman, - SpinGlass, SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, + HamiltonianCircuit, HamiltonianPath, IntegralFlowBundles, IntegralFlowHomologousArcs, + IntegralFlowWithMultipliers, IsomorphicSpanningTree, KClique, KthBestSpanningTree, + LengthBoundedDisjointPaths, LongestPath, MixedChinesePostman, SpinGlass, SteinerTree, + StrongConnectivityAugmentation, SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, LongestCircuit, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, diff --git a/src/models/graph/graph_partitioning.rs b/src/models/graph/graph_partitioning.rs deleted file mode 100644 index a2ea1f0f6..000000000 --- a/src/models/graph/graph_partitioning.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! GraphPartitioning problem implementation. -//! -//! The Graph Partitioning (Minimum Bisection) problem asks for a balanced partition -//! of vertices into two equal halves minimizing the number of crossing edges. - -use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::topology::{Graph, SimpleGraph}; -use crate::traits::Problem; -use crate::types::Min; -use serde::{Deserialize, Serialize}; - -inventory::submit! { - ProblemSchemaEntry { - name: "GraphPartitioning", - display_name: "Graph Partitioning", - aliases: &[], - dimensions: &[ - VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), - ], - module_path: module_path!(), - description: "Find minimum cut balanced bisection of a graph", - fields: &[ - FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" }, - ], - } -} - -/// The Graph Partitioning (Minimum Bisection) problem. -/// -/// Given an undirected graph G = (V, E) with |V| = n (even), -/// partition V into two disjoint sets A and B with |A| = |B| = n/2, -/// minimizing the number of edges crossing the partition. -/// -/// # Type Parameters -/// -/// * `G` - The graph type (e.g., `SimpleGraph`) -/// -/// # Example -/// -/// ``` -/// use problemreductions::models::graph::GraphPartitioning; -/// use problemreductions::topology::SimpleGraph; -/// use problemreductions::types::Min; -/// use problemreductions::{Problem, Solver, BruteForce}; -/// -/// // Square graph: 0-1, 1-2, 2-3, 3-0 -/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); -/// let problem = GraphPartitioning::new(graph); -/// -/// let solver = BruteForce::new(); -/// let solutions = solver.find_all_witnesses(&problem); -/// -/// // Minimum bisection of a 4-cycle: cut = 2 -/// for sol in solutions { -/// let size = problem.evaluate(&sol); -/// assert_eq!(size, Min(Some(2))); -/// } -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GraphPartitioning { - /// The underlying graph structure. - graph: G, -} - -impl GraphPartitioning { - /// Create a GraphPartitioning problem from a graph. - /// - /// # Arguments - /// * `graph` - The undirected graph to partition - pub fn new(graph: G) -> Self { - Self { graph } - } - - /// Get a reference to the underlying graph. - pub fn graph(&self) -> &G { - &self.graph - } - - /// Get the number of vertices in the underlying graph. - pub fn num_vertices(&self) -> usize { - self.graph.num_vertices() - } - - /// Get the number of edges in the underlying graph. - pub fn num_edges(&self) -> usize { - self.graph.num_edges() - } -} - -impl Problem for GraphPartitioning -where - G: Graph + crate::variant::VariantParam, -{ - const NAME: &'static str = "GraphPartitioning"; - type Value = Min; - - fn variant() -> Vec<(&'static str, &'static str)> { - crate::variant_params![G] - } - - fn dims(&self) -> Vec { - vec![2; self.graph.num_vertices()] - } - - fn evaluate(&self, config: &[usize]) -> Min { - let n = self.graph.num_vertices(); - if config.len() != n { - return Min(None); - } - // Balanced bisection requires even n - if !n.is_multiple_of(2) { - return Min(None); - } - // Check balanced: exactly n/2 vertices in partition 1 - let count_ones = config.iter().filter(|&&x| x == 1).count(); - if count_ones != n / 2 { - return Min(None); - } - // Count crossing edges - let mut cut = 0i32; - for (u, v) in self.graph.edges() { - if config[u] != config[v] { - cut += 1; - } - } - Min(Some(cut)) - } -} - -crate::declare_variants! { - default GraphPartitioning => "2^num_vertices", -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_model_example_specs() -> Vec { - use crate::topology::SimpleGraph; - // Two triangles connected by 3 edges; balanced cut = 3 - vec![crate::example_db::specs::ModelExampleSpec { - id: "graph_partitioning", - instance: Box::new(GraphPartitioning::new(SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 2), - (1, 3), - (2, 3), - (2, 4), - (3, 4), - (3, 5), - (4, 5), - ], - ))), - optimal_config: vec![0, 0, 0, 1, 1, 1], - optimal_value: serde_json::json!(3), - }] -} - -#[cfg(test)] -#[path = "../../unit_tests/models/graph/graph_partitioning.rs"] -mod tests; diff --git a/src/models/graph/max_cut.rs b/src/models/graph/max_cut.rs index b3c22b0db..efaa632ac 100644 --- a/src/models/graph/max_cut.rs +++ b/src/models/graph/max_cut.rs @@ -14,7 +14,7 @@ inventory::submit! { ProblemSchemaEntry { name: "MaxCut", display_name: "Max Cut", - aliases: &[], + aliases: &["GraphPartitioning"], dimensions: &[ VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), VariantDimension::new("weight", "i32", &["i32"]), diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 71d258e05..36e98e3d2 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -9,7 +9,6 @@ //! - [`MinimumFeedbackVertexSet`]: Minimum weight feedback vertex set in a directed graph //! - [`MaximumClique`]: Maximum weight clique //! - [`MaxCut`]: Maximum cut on weighted graphs -//! - [`GraphPartitioning`]: Minimum bisection (balanced graph partitioning) //! - [`MinimumCutIntoBoundedSets`]: Minimum cut into bounded sets (Garey & Johnson ND17) //! - [`MinimumDummyActivitiesPert`]: Minimum dummy activities in activity-on-arc PERT networks //! - [`HamiltonianCircuit`]: Hamiltonian circuit (decision problem) @@ -64,7 +63,6 @@ pub(crate) mod bounded_component_spanning_forest; pub(crate) mod directed_two_commodity_integral_flow; pub(crate) mod disjoint_connecting_paths; pub(crate) mod generalized_hex; -pub(crate) mod graph_partitioning; pub(crate) mod hamiltonian_circuit; pub(crate) mod hamiltonian_path; pub(crate) mod integral_flow_bundles; @@ -120,7 +118,6 @@ pub use bounded_component_spanning_forest::BoundedComponentSpanningForest; pub use directed_two_commodity_integral_flow::DirectedTwoCommodityIntegralFlow; pub use disjoint_connecting_paths::DisjointConnectingPaths; pub use generalized_hex::GeneralizedHex; -pub use graph_partitioning::GraphPartitioning; pub use hamiltonian_circuit::HamiltonianCircuit; pub use hamiltonian_path::HamiltonianPath; pub use integral_flow_bundles::IntegralFlowBundles; @@ -218,7 +215,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec, - num_vertices: usize, -} - -impl ReductionResult for ReductionGraphPartitioningToILP { - type Source = GraphPartitioning; - type Target = ILP; - - fn target_problem(&self) -> &ILP { - &self.target - } - - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - target_solution[..self.num_vertices].to_vec() - } -} - -#[reduction( - overhead = { - num_vars = "num_vertices + num_edges", - num_constraints = "2 * num_edges + 1", - } -)] -impl ReduceTo> for GraphPartitioning { - type Result = ReductionGraphPartitioningToILP; - - fn reduce_to(&self) -> Self::Result { - let n = self.num_vertices(); - let edges = self.graph().edges(); - let m = edges.len(); - let num_vars = n + m; - - let mut constraints = Vec::with_capacity(2 * m + 1); - - let balance_terms: Vec<(usize, f64)> = (0..n).map(|v| (v, 1.0)).collect(); - constraints.push(LinearConstraint::eq(balance_terms, n as f64 / 2.0)); - - for (edge_idx, (u, v)) in edges.iter().enumerate() { - let y_var = n + edge_idx; - constraints.push(LinearConstraint::ge( - vec![(y_var, 1.0), (*u, -1.0), (*v, 1.0)], - 0.0, - )); - constraints.push(LinearConstraint::ge( - vec![(y_var, 1.0), (*u, 1.0), (*v, -1.0)], - 0.0, - )); - } - - let objective: Vec<(usize, f64)> = (0..m).map(|edge_idx| (n + edge_idx, 1.0)).collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize); - - ReductionGraphPartitioningToILP { - target, - num_vertices: n, - } - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "graphpartitioning_to_ilp", - build: || { - let source = GraphPartitioning::new(SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 2), - (1, 3), - (2, 3), - (2, 4), - (3, 4), - (3, 5), - (4, 5), - ], - )); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( - source, - SolutionPair { - source_config: vec![0, 0, 0, 1, 1, 1], - target_config: vec![0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0], - }, - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/graphpartitioning_ilp.rs"] -mod tests; diff --git a/src/rules/graphpartitioning_maxcut.rs b/src/rules/graphpartitioning_maxcut.rs deleted file mode 100644 index 0bf31b369..000000000 --- a/src/rules/graphpartitioning_maxcut.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Reduction from GraphPartitioning to MaxCut on a weighted complete graph. - -use crate::models::graph::{GraphPartitioning, MaxCut}; -use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::topology::{Graph, SimpleGraph}; - -/// Result of reducing GraphPartitioning to MaxCut. -#[derive(Debug, Clone)] -pub struct ReductionGPToMaxCut { - target: MaxCut, -} - -#[cfg(any(test, feature = "example-db"))] -const ISSUE_EXAMPLE_WITNESS: [usize; 6] = [0, 0, 0, 1, 1, 1]; - -impl ReductionResult for ReductionGPToMaxCut { - type Source = GraphPartitioning; - type Target = MaxCut; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - target_solution.to_vec() - } -} - -#[cfg(any(test, feature = "example-db"))] -fn issue_example() -> 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), - ], - )) -} - -fn complete_graph_edges_and_weights(graph: &SimpleGraph) -> (Vec<(usize, usize)>, Vec) { - let num_vertices = graph.num_vertices(); - let p = penalty_weight(graph.num_edges()); - let mut edges = Vec::new(); - let mut weights = Vec::new(); - - for u in 0..num_vertices { - for v in (u + 1)..num_vertices { - edges.push((u, v)); - weights.push(if graph.has_edge(u, v) { p - 1 } else { p }); - } - } - - (edges, weights) -} - -fn penalty_weight(num_edges: usize) -> i32 { - i32::try_from(num_edges) - .ok() - .and_then(|num_edges| num_edges.checked_add(1)) - .expect("GraphPartitioning -> MaxCut penalty exceeds i32 range") -} - -#[reduction( - overhead = { - num_vertices = "num_vertices", - num_edges = "num_vertices * (num_vertices - 1) / 2", - } -)] -impl ReduceTo> for GraphPartitioning { - type Result = ReductionGPToMaxCut; - - fn reduce_to(&self) -> Self::Result { - let (edges, weights) = complete_graph_edges_and_weights(self.graph()); - let target = MaxCut::new(SimpleGraph::new(self.num_vertices(), edges), weights); - - ReductionGPToMaxCut { target } - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "graphpartitioning_to_maxcut", - build: || { - crate::example_db::specs::rule_example_with_witness::<_, MaxCut>( - issue_example(), - SolutionPair { - source_config: ISSUE_EXAMPLE_WITNESS.to_vec(), - target_config: ISSUE_EXAMPLE_WITNESS.to_vec(), - }, - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/graphpartitioning_maxcut.rs"] -mod tests; diff --git a/src/rules/graphpartitioning_qubo.rs b/src/rules/graphpartitioning_qubo.rs deleted file mode 100644 index 8e32846c9..000000000 --- a/src/rules/graphpartitioning_qubo.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Reduction from GraphPartitioning to QUBO. -//! -//! 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::{Graph, 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 { - 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, 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; - } - } - - 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 { - 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)] -#[path = "../unit_tests/rules/graphpartitioning_qubo.rs"] -mod tests; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 31b4f163e..1d9c5e77b 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -11,8 +11,6 @@ mod closestvectorproblem_qubo; pub(crate) mod coloring_qubo; pub(crate) mod factoring_circuit; mod graph; -pub(crate) mod graphpartitioning_maxcut; -pub(crate) mod graphpartitioning_qubo; pub(crate) mod hamiltoniancircuit_travelingsalesman; mod kcoloring_casts; mod knapsack_qubo; @@ -92,8 +90,6 @@ pub(crate) mod factoring_ilp; #[cfg(feature = "ilp-solver")] pub(crate) mod flowshopscheduling_ilp; #[cfg(feature = "ilp-solver")] -pub(crate) mod graphpartitioning_ilp; -#[cfg(feature = "ilp-solver")] pub(crate) mod hamiltonianpath_ilp; #[cfg(feature = "ilp-solver")] mod ilp_bool_ilp_i32; @@ -239,8 +235,6 @@ pub(crate) fn canonical_rule_example_specs() -> Vec Vec GraphPartitioning { - let graph = SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 2), - (1, 3), - (2, 3), - (2, 4), - (3, 4), - (3, 5), - (4, 5), - ], - ); - GraphPartitioning::new(graph) -} - -#[test] -fn test_graphpartitioning_basic() { - let problem = issue_example(); - - // Check dims: 6 binary variables - assert_eq!(problem.dims(), vec![2, 2, 2, 2, 2, 2]); - - // Evaluate a valid balanced partition: A={0,1,2}, B={3,4,5} - // config: [0, 0, 0, 1, 1, 1] - // Crossing edges: (1,3), (2,3), (2,4) => cut = 3 - let config = vec![0, 0, 0, 1, 1, 1]; - let result = problem.evaluate(&config); - assert_eq!(result, Min(Some(3))); -} - -#[test] -fn test_graphpartitioning_serialization() { - let problem = issue_example(); - let json = serde_json::to_string(&problem).unwrap(); - let deserialized: GraphPartitioning = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.graph().num_vertices(), 6); - assert_eq!(deserialized.graph().num_edges(), 9); - - // Verify evaluation is consistent after round-trip - let config = vec![0, 0, 0, 1, 1, 1]; - assert_eq!(problem.evaluate(&config), deserialized.evaluate(&config)); -} - -#[test] -fn test_graphpartitioning_solver() { - let problem = issue_example(); - let solver = BruteForce::new(); - let best = solver.find_witness(&problem).unwrap(); - let size = problem.evaluate(&best); - assert_eq!(size, Min(Some(3))); - - // All optimal solutions should have cut = 3 - let all_best = solver.find_all_witnesses(&problem); - assert!(!all_best.is_empty()); - for sol in &all_best { - assert_eq!(problem.evaluate(sol), Min(Some(3))); - } -} - -#[test] -fn test_graphpartitioning_odd_vertices() { - // 3 vertices: all configs must be Invalid since n is odd - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = GraphPartitioning::new(graph); - - // Every possible config should be Invalid - for a in 0..2 { - for b in 0..2 { - for c in 0..2 { - assert_eq!( - problem.evaluate(&[a, b, c]), - Min(None), - "Expected Invalid for odd n, config [{}, {}, {}]", - a, - b, - c - ); - } - } - } -} - -#[test] -fn test_graphpartitioning_unbalanced_invalid() { - // 4 vertices: only configs with exactly 2 ones are valid - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]); - let problem = GraphPartitioning::new(graph); - - // All zeros: 0 ones, not balanced - assert_eq!(problem.evaluate(&[0, 0, 0, 0]), Min(None)); - - // All ones: 4 ones, not balanced - assert_eq!(problem.evaluate(&[1, 1, 1, 1]), Min(None)); - - // One vertex in partition 1: not balanced - assert_eq!(problem.evaluate(&[1, 0, 0, 0]), Min(None)); - - // Three vertices in partition 1: not balanced - assert_eq!(problem.evaluate(&[1, 1, 1, 0]), Min(None)); - - // Two vertices in partition 1: balanced, should be Valid - // 4-cycle edges: (0,1),(1,2),(2,3),(0,3). Config [1,1,0,0] cuts (1,2) and (0,3) => cut=2 - assert_eq!(problem.evaluate(&[1, 1, 0, 0]), Min(Some(2))); -} - -#[test] -fn test_graphpartitioning_size_getters() { - let problem = issue_example(); - assert_eq!(problem.num_vertices(), 6); - assert_eq!(problem.num_edges(), 9); -} - -#[test] -fn test_graphpartitioning_square_graph() { - // Square graph: 0-1, 1-2, 2-3, 3-0 (the doctest example) - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); - let problem = GraphPartitioning::new(graph); - - let solver = BruteForce::new(); - let all_best = solver.find_all_witnesses(&problem); - - // Minimum bisection of a 4-cycle: cut = 2 - for sol in &all_best { - assert_eq!(problem.evaluate(sol), Min(Some(2))); - } -} - -#[test] -fn test_graphpartitioning_problem_name() { - assert_eq!( - as Problem>::NAME, - "GraphPartitioning" - ); -} - -#[test] -fn test_graphpartitioning_graph_accessor() { - let problem = issue_example(); - let graph = problem.graph(); - assert_eq!(graph.num_vertices(), 6); - assert_eq!(graph.num_edges(), 9); -} - -#[test] -fn test_graphpartitioning_empty_graph() { - // 4 vertices, no edges: any balanced partition has cut = 0 - let graph = SimpleGraph::new(4, vec![]); - let problem = GraphPartitioning::new(graph); - - let config = vec![0, 0, 1, 1]; - assert_eq!(problem.evaluate(&config), Min(Some(0))); -} diff --git a/src/unit_tests/rules/analysis.rs b/src/unit_tests/rules/analysis.rs index 97b37b19b..020b89b66 100644 --- a/src/unit_tests/rules/analysis.rs +++ b/src/unit_tests/rules/analysis.rs @@ -245,11 +245,6 @@ 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 diff --git a/src/unit_tests/rules/graphpartitioning_ilp.rs b/src/unit_tests/rules/graphpartitioning_ilp.rs deleted file mode 100644 index bb0ec4e4e..000000000 --- a/src/unit_tests/rules/graphpartitioning_ilp.rs +++ /dev/null @@ -1,132 +0,0 @@ -use super::*; -use crate::models::algebraic::{Comparison, ObjectiveSense}; -use crate::models::graph::GraphPartitioning; -use crate::solvers::{BruteForce, ILPSolver}; -use crate::topology::SimpleGraph; -use crate::traits::Problem; -use crate::types::Min; - -fn canonical_instance() -> GraphPartitioning { - let graph = SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 2), - (1, 3), - (2, 3), - (2, 4), - (3, 4), - (3, 5), - (4, 5), - ], - ); - GraphPartitioning::new(graph) -} - -#[test] -fn test_reduction_creates_valid_ilp() { - let problem = canonical_instance(); - let reduction: ReductionGraphPartitioningToILP = ReduceTo::>::reduce_to(&problem); - let ilp = reduction.target_problem(); - - assert_eq!(ilp.num_vars, 15); - assert_eq!(ilp.constraints.len(), 19); - assert_eq!(ilp.sense, ObjectiveSense::Minimize); - assert_eq!( - ilp.objective, - vec![ - (6, 1.0), - (7, 1.0), - (8, 1.0), - (9, 1.0), - (10, 1.0), - (11, 1.0), - (12, 1.0), - (13, 1.0), - (14, 1.0), - ] - ); -} - -#[test] -fn test_reduction_constraint_shape() { - let problem = GraphPartitioning::new(SimpleGraph::new(2, vec![(0, 1)])); - let reduction: ReductionGraphPartitioningToILP = ReduceTo::>::reduce_to(&problem); - let ilp = reduction.target_problem(); - - assert_eq!(ilp.num_vars, 3); - assert_eq!(ilp.constraints.len(), 3); - - let balance = &ilp.constraints[0]; - assert_eq!(balance.cmp, Comparison::Eq); - assert_eq!(balance.terms, vec![(0, 1.0), (1, 1.0)]); - assert_eq!(balance.rhs, 1.0); - - let first_link = &ilp.constraints[1]; - assert_eq!(first_link.cmp, Comparison::Ge); - assert_eq!(first_link.terms, vec![(2, 1.0), (0, -1.0), (1, 1.0)]); - assert_eq!(first_link.rhs, 0.0); - - let second_link = &ilp.constraints[2]; - assert_eq!(second_link.cmp, Comparison::Ge); - assert_eq!(second_link.terms, vec![(2, 1.0), (0, 1.0), (1, -1.0)]); - assert_eq!(second_link.rhs, 0.0); -} - -#[test] -fn test_graphpartitioning_to_ilp_closed_loop() { - let problem = canonical_instance(); - let reduction: ReductionGraphPartitioningToILP = ReduceTo::>::reduce_to(&problem); - let ilp = reduction.target_problem(); - - let bf = BruteForce::new(); - let ilp_solver = ILPSolver::new(); - - let bf_solutions = bf.find_all_witnesses(&problem); - let bf_obj = problem.evaluate(&bf_solutions[0]); - - let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution); - let ilp_obj = problem.evaluate(&extracted); - - assert_eq!(bf_obj, Min(Some(3))); - assert_eq!(ilp_obj, Min(Some(3))); -} - -#[test] -fn test_odd_vertices_reduce_to_infeasible_ilp() { - let problem = GraphPartitioning::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); - let reduction: ReductionGraphPartitioningToILP = ReduceTo::>::reduce_to(&problem); - let ilp = reduction.target_problem(); - - assert_eq!(ilp.constraints[0].cmp, Comparison::Eq); - assert_eq!(ilp.constraints[0].rhs, 1.5); - - let solver = ILPSolver::new(); - assert_eq!(solver.solve(ilp), None); -} - -#[test] -fn test_solution_extraction() { - let problem = canonical_instance(); - let reduction: ReductionGraphPartitioningToILP = ReduceTo::>::reduce_to(&problem); - - let ilp_solution = vec![0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0]; - let extracted = reduction.extract_solution(&ilp_solution); - - assert_eq!(extracted, vec![0, 0, 0, 1, 1, 1]); - assert_eq!(problem.evaluate(&extracted), Min(Some(3))); -} - -#[test] -fn test_solve_reduced() { - let problem = canonical_instance(); - - let ilp_solver = ILPSolver::new(); - let solution = ilp_solver - .solve_reduced(&problem) - .expect("solve_reduced should work"); - - assert_eq!(problem.evaluate(&solution), Min(Some(3))); -} diff --git a/src/unit_tests/rules/graphpartitioning_maxcut.rs b/src/unit_tests/rules/graphpartitioning_maxcut.rs deleted file mode 100644 index ca9acf458..000000000 --- a/src/unit_tests/rules/graphpartitioning_maxcut.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::models::graph::{GraphPartitioning, MaxCut}; -use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; -use crate::rules::{ReduceTo, ReductionResult}; -use crate::topology::{Graph, SimpleGraph}; - -fn issue_example() -> GraphPartitioning { - super::issue_example() -} - -#[test] -fn test_graphpartitioning_to_maxcut_closed_loop() { - let source = issue_example(); - let reduction = ReduceTo::>::reduce_to(&source); - - assert_optimization_round_trip_from_optimization_target( - &source, - &reduction, - "GraphPartitioning->MaxCut closed loop", - ); -} - -#[test] -fn test_graphpartitioning_to_maxcut_target_structure() { - let source = issue_example(); - let reduction = ReduceTo::>::reduce_to(&source); - let target = reduction.target_problem(); - let num_vertices = source.num_vertices(); - let penalty = i32::try_from(source.num_edges()).unwrap() + 1; - - assert_eq!(target.num_vertices(), num_vertices); - assert_eq!(target.num_edges(), num_vertices * (num_vertices - 1) / 2); - - for u in 0..num_vertices { - for v in (u + 1)..num_vertices { - let expected_weight = if source.graph().has_edge(u, v) { - penalty - 1 - } else { - penalty - }; - assert_eq!( - target.edge_weight(u, v), - Some(&expected_weight), - "unexpected weight on edge ({u}, {v})" - ); - } - } -} - -#[test] -fn test_graphpartitioning_to_maxcut_extract_solution_identity() { - let source = issue_example(); - let reduction = ReduceTo::>::reduce_to(&source); - let target_solution = super::ISSUE_EXAMPLE_WITNESS.to_vec(); - - assert_eq!( - reduction.extract_solution(&target_solution), - target_solution - ); -} - -#[test] -fn test_graphpartitioning_to_maxcut_penalty_overflow_panics() { - let result = std::panic::catch_unwind(|| super::penalty_weight(i32::MAX as usize)); - assert!(result.is_err()); -} diff --git a/src/unit_tests/rules/graphpartitioning_qubo.rs b/src/unit_tests/rules/graphpartitioning_qubo.rs deleted file mode 100644 index 779eea84f..000000000 --- a/src/unit_tests/rules/graphpartitioning_qubo.rs +++ /dev/null @@ -1,82 +0,0 @@ -use super::*; -use crate::models::algebraic::QUBO; -use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; -use crate::topology::SimpleGraph; - -fn example_problem() -> 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()); -}