diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index e64bb909..3678eb7f 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -11145,56 +11145,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m ] } -#{ - let ss-ca = load-example("SubsetSum", "CapacityAssignment") - let ss-ca-sol = ss-ca.solutions.at(0) - let ss-ca-sizes = ss-ca.source.instance.sizes.map(int) - let ss-ca-target = int(ss-ca.source.instance.target) - let ss-ca-n = ss-ca-sizes.len() - let ss-ca-S = ss-ca-sizes.fold(0, (a, b) => a + b) - let ss-ca-J = ss-ca-S - ss-ca-target - let ss-ca-selected = ss-ca-sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) - let ss-ca-selected-sizes = ss-ca-selected.map(i => ss-ca-sizes.at(i)) - let ss-ca-selected-sum = ss-ca-selected-sizes.fold(0, (a, b) => a + b) - let ss-ca-not-selected = ss-ca-sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => i) - let ss-ca-delay-sum = ss-ca-not-selected.map(i => ss-ca-sizes.at(i)).fold(0, (a, b) => a + b) - [ - #reduction-rule("SubsetSum", "CapacityAssignment", - example: true, - example-caption: [#ss-ca-n elements, target sum $B = #ss-ca-target$], - extra: [ - #pred-commands( - "pred create --example SubsetSum -o subsetsum.json", - "pred reduce subsetsum.json --to " + target-spec(ss-ca) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate subsetsum.json --config " + ss-ca-sol.source_config.map(str).join(","), - ) - *Step 1 -- Source instance.* The canonical Subset Sum instance has sizes $(#ss-ca-sizes.map(str).join(", "))$ and target $B = #ss-ca-target$. The total sum is $S = #ss-ca-S$. - - *Step 2 -- Build the Capacity Assignment instance.* The reduction creates #ss-ca-n communication links with two capacities ${1, 2}$. For each link $c_i$ with element value $a_i$: cost row $(0, a_i)$ and delay row $(a_i, 0)$. The delay budget is $J = S - B = #ss-ca-S - #ss-ca-target = #ss-ca-J$. - - *Step 3 -- Verify the canonical witness.* The fixture stores source config $(#ss-ca-sol.source_config.map(str).join(", "))$, selecting elements at indices $#ss-ca-selected.map(str).join(", ")$ with values $#ss-ca-selected-sizes.map(str).join(" + ") = #ss-ca-selected-sum = B$. In the target, these links get high capacity (index 1) with total cost $#ss-ca-selected-sum$ and the remaining links get low capacity (index 0) with total delay $#ss-ca-delay-sum <= #ss-ca-J = J$. - - *Witness semantics.* The example DB stores one canonical witness. Other subsets summing to $B$ would also yield valid witnesses. - ], - )[ - This $O(n)$ reduction from Subset Sum to Capacity Assignment follows the original NP-completeness proof of Van Sickle and Chandy @vansicklechandy1977 (GJ SR7 @garey1979). Each element becomes a communication link with two capacity levels; the cost/delay duality encodes complementary subset selection. - ][ - _Construction._ Given sizes $a_1, dots, a_n in ZZ^+$ and target $B$, let $S = sum_(i=1)^n a_i$. Create $n$ communication links with capacity set $M = {1, 2}$. For each link $c_i$: - - Cost: $g(c_i, 1) = 0$, $g(c_i, 2) = a_i$ (non-decreasing since $0 <= a_i$). - - Delay: $d(c_i, 1) = a_i$, $d(c_i, 2) = 0$ (non-increasing since $a_i >= 0$). - Set the delay budget $J = S - B$. - - _Correctness._ For any assignment $sigma$, the total cost is $sum_(i: sigma(c_i)=2) a_i$ and the total delay is $sum_(i: sigma(c_i)=1) a_i$. Since every element contributes to exactly one of these sums, cost $+$ delay $= S$. - - ($arrow.r.double$) If $A' subset.eq A$ sums to $B$, assign $sigma(c_i) = 2$ for $a_i in A'$ and $sigma(c_i) = 1$ otherwise. Total cost $= B$, total delay $= S - B = J$. - - ($arrow.l.double$) The delay constraint forces delay $<= S - B$, so cost $>= S - (S - B) = B$. If the optimal cost equals $B$, the high-capacity links form a subset summing to exactly $B$. If no such subset exists, the minimum cost is strictly greater than $B$. - - _Solution extraction._ Return the target configuration unchanged: capacity index 1 (high) for link $c_i$ means element $a_i$ is selected. - ] - ] -} +// Removed: SubsetSum → CapacityAssignment (unsound reduction, #1006) #reduction-rule("ILP", "QUBO")[ A binary ILP optimizes a linear objective over binary variables subject to linear constraints. The penalty method converts each equality constraint $bold(a)_k^top bold(x) = b_k$ into the quadratic penalty $(bold(a)_k^top bold(x) - b_k)^2$, which is zero if and only if the constraint is satisfied. Inequality constraints are first converted to equalities using binary slack variables with powers-of-two coefficients. The resulting unconstrained quadratic over binary variables is a QUBO whose matrix $Q$ combines the negated objective (as diagonal terms) with the expanded constraint penalties (as a Gram matrix $A^top A$). @@ -11314,24 +11265,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Solution extraction._ Return the same binary selection vector: element $i$ is in the partition subset if and only if it is selected in the Subset Sum witness. ] -#reduction-rule("Partition", "ShortestWeightConstrainedPath")[ - Build a chain of $n + 1$ vertices with two parallel edges per layer; the "include" and "exclude" edges swap length and weight so that a balanced partition corresponds to a shortest weight-constrained $s$-$t$ path. -][ - _Construction._ Given positive sizes $s_0, dots, s_(n-1)$ with total sum $S$, build a multigraph on vertices $v_0, dots, v_n$. For each element $i$, add two parallel edges from $v_i$ to $v_(i+1)$: - - _Include edge:_ length $= s_i + 1$, weight $= 1$. - - _Exclude edge:_ length $= 1$, weight $= s_i + 1$. - - Set source $= v_0$, target $= v_n$, and weight bound $W = S\/2 + n$. The objective minimizes path length. - - _Correctness._ Every $s$-$t$ path selects exactly one edge per layer, so the path length and weight sum decompose as - $ - L = sum_(i in A) (s_i + 1) + sum_(i in.not A) 1 = sum_(i in A) s_i + n, quad - W' = sum_(i in.not A) (s_i + 1) + sum_(i in A) 1 = (S - sum_(i in A) s_i) + n, - $ - where $A$ is the set of layers choosing the include edge. Note $L + W' = S + 2n$, so the weight constraint $W' <= S\/2 + n$ is equivalent to $L >= S\/2 + n$. A balanced partition with $sum_(i in A) s_i = S\/2$ achieves $L = S\/2 + n$ (the minimum) and $W' = S\/2 + n = W$ (tight). ($arrow.r.double$) A balanced partition gives the shortest feasible path. ($arrow.l.double$) Any shortest feasible path must have $L = S\/2 + n$, implying $sum_(i in A) s_i = S\/2$. - - _Solution extraction._ For each layer $i$, if the exclude edge is selected, element $i$ goes to subset $A_2$ (config $= 1$); otherwise element $i$ goes to subset $A_1$ (config $= 0$). -] +// Removed: Partition → ShortestWeightConstrainedPath (unsound reduction, #1006) #let ks_qubo = load-example("Knapsack", "QUBO") #let ks_qubo_sol = ks_qubo.solutions.at(0) @@ -15828,37 +15762,7 @@ The following table shows concrete variable overhead for example instances, take _Solution extraction._ Return the first $n$ ILP coordinates $(t_0, dots, t_(n-1))$ as the vertex evaluation positions. ] -#let part_swi = load-example("Partition", "SequencingWithinIntervals") -#let part_swi_sol = part_swi.solutions.at(0) -#reduction-rule("Partition", "SequencingWithinIntervals", - example: true, - example-caption: [$n = #part_swi.source.instance.sizes.len()$ elements, $S = #part_swi.source.instance.sizes.sum()$], - extra: [ - #pred-commands( - "pred create --example " + problem-spec(part_swi.source) + " -o partition.json", - "pred reduce partition.json --to " + target-spec(part_swi) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate partition.json --config " + part_swi_sol.source_config.map(str).join(","), - ) - - *Step 1 -- Source instance.* The Partition instance has $n = #part_swi.source.instance.sizes.len()$ elements with sizes $(#part_swi.source.instance.sizes.map(str).join(", "))$ and total sum $S = #part_swi.source.instance.sizes.sum()$. The half-sum is $h = floor(S\/2) = #calc.floor(part_swi.source.instance.sizes.sum() / 2)$. - - *Step 2 -- Construct tasks.* The reduction creates $n + 1 = #part_swi.target.instance.lengths.len()$ tasks. Each element $a_i$ becomes a task with release time $r_i = 0$, deadline $d_i = S + 1 = #part_swi.target.instance.deadlines.at(0)$, and length $p_i = a_i$. An enforcer task is pinned at the midpoint: $r = #part_swi.target.instance.release_times.at(part_swi.source.instance.sizes.len())$, $d = #part_swi.target.instance.deadlines.at(part_swi.source.instance.sizes.len())$, $p = 1$. This enforcer occupies $[h, h+1)$, splitting the timeline into two blocks of size $h = #calc.floor(part_swi.source.instance.sizes.sum() / 2)$ each. - - *Step 3 -- Verify a solution.* The canonical partition assigns elements to subsets via $(#part_swi_sol.source_config.map(str).join(", "))$: subset 0 = $\{#part_swi_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => str(part_swi.source.instance.sizes.at(i))).join(", ")\}$ (sum #part_swi_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => part_swi.source.instance.sizes.at(i)).sum()), subset 1 = $\{#part_swi_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(part_swi.source.instance.sizes.at(i))).join(", ")\}$ (sum #part_swi_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => part_swi.source.instance.sizes.at(i)).sum()). Both subsets sum to $S\/2 = #calc.floor(part_swi.source.instance.sizes.sum() / 2)$ #sym.checkmark \ - The target schedule has start-time offsets $(#part_swi_sol.target_config.map(str).join(", "))$: subset-0 tasks fill $[0, h)$ and subset-1 tasks fill $[h+1, S+1)$, with the enforcer at $[h, h+1)$ #sym.checkmark - - *Multiplicity:* The fixture stores one canonical witness. Other valid partitions (e.g.\ swapping the two subsets) exist but are symmetric. - ], -)[ - A unit-length enforcer task pinned at $[floor(S/2), floor(S/2)+1)$ splits the timeline into two blocks. A valid schedule exists iff the elements partition into two equal-sum subsets. -][ - _Construction._ Let $A = {a_1, dots, a_n}$ with $S = sum a_i$ and $h = floor(S/2)$. Create $n+1$ tasks: element tasks with $r_i = 0$, $d_i = S+1$, $p_i = a_i$; enforcer task with $r = h$, $d = h+1$, $p = 1$. - - _Correctness._ ($arrow.r.double$) A balanced partition places one subset's tasks in $[0, h)$ and the other in $[h+1, S+1)$. ($arrow.l.double$) The enforcer at $[h, h+1)$ splits usable time into two blocks of size $h = S/2$; since element tasks fill both blocks exactly, the assignment gives a balanced partition. - - _Solution extraction._ Task $t_i$ starting at time $s_i$: assign to subset 0 if $s_i <= h$, else subset 1. -] +// Removed: Partition → SequencingWithinIntervals (unsound reduction, #1006) #let mvc_mfas = load-example("MinimumVertexCover", "MinimumFeedbackArcSet") #let mvc_mfas_sol = mvc_mfas.solutions.at(0) @@ -16351,39 +16255,7 @@ The following table shows concrete variable overhead for example instances, take _Solution extraction._ The QAP permutation $gamma$ is the Hamiltonian circuit visit order directly. ] -#let hp_cos = load-example("HamiltonianPath", "ConsecutiveOnesSubmatrix") -#let hp_cos_sol = hp_cos.solutions.at(0) -#reduction-rule("HamiltonianPath", "ConsecutiveOnesSubmatrix", - example: true, - example-caption: [Path graph $P_#hp_cos.source.instance.graph.num_vertices$ ($n = #hp_cos.source.instance.graph.num_vertices$, $|E| = #hp_cos.source.instance.graph.edges.len()$)], - extra: [ - #pred-commands( - "pred create --example " + problem-spec(hp_cos.source) + " -o hp.json", - "pred reduce hp.json --to " + target-spec(hp_cos) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate hp.json --config " + hp_cos_sol.source_config.map(str).join(","), - ) - - *Step 1 -- Source instance.* The graph $G$ has $n = #hp_cos.source.instance.graph.num_vertices$ vertices and $m = #hp_cos.source.instance.graph.edges.len()$ edges: ${#hp_cos.source.instance.graph.edges.map(e => "(" + str(e.at(0)) + "," + str(e.at(1)) + ")").join(", ")}$. - - *Step 2 -- Build the incidence matrix.* Construct the $n times m = #hp_cos.target.instance.matrix.len() times #hp_cos.target.instance.matrix.at(0).len()$ vertex-edge incidence matrix $A$ where $A_(i,j) = 1$ iff vertex $i$ is an endpoint of edge $j$. Set bound $K = n - 1 = #hp_cos.target.instance.bound$. The matrix is: - $ A = mat( - #hp_cos.target.instance.matrix.map(row => row.map(v => if v { "1" } else { "0" }).join(", ")).join(";\n ") - ) $ - - *Step 3 -- Verify a solution.* The canonical Hamiltonian path visits vertices in order $(#hp_cos_sol.source_config.map(str).join(", "))$. The target configuration $(#hp_cos_sol.target_config.map(str).join(", "))$ selects #hp_cos_sol.target_config.filter(x => x == 1).len() columns (all $K = #hp_cos.target.instance.bound$ edges). Ordering the selected columns by path position, each row has at most two consecutive ones #sym.checkmark - - *Multiplicity:* The fixture stores one canonical witness. The path $P_#hp_cos.source.instance.graph.num_vertices$ has exactly 2 Hamiltonian paths (forward and reverse); the canonical one is $(#hp_cos_sol.source_config.map(str).join(", "))$. - ], -)[ - The vertex-edge incidence matrix has the consecutive-ones property on a selected subset of $n-1$ columns iff those columns correspond to a Hamiltonian path. -][ - _Construction._ Given $G = (V, E)$ with $|V| = n$, $|E| = m$, build $n times m$ Boolean matrix $A$ with $A_(i,j) = 1$ iff vertex $i$ is an endpoint of edge $j$. Set bound $K = n - 1$. - - _Correctness._ ($arrow.r.double$) A Hamiltonian path's $n-1$ edges, ordered by path position, give each row at most two consecutive ones. ($arrow.l.double$) $n-1$ columns with the C1P property form a subgraph where each vertex has degree $<= 2$; with $n-1$ edges spanning $n$ vertices, this is a Hamiltonian path. - - _Solution extraction._ Selected columns identify edges; walk from a degree-1 vertex to recover the path ordering. -] +// Removed: HamiltonianPath → ConsecutiveOnesSubmatrix (unsound reduction, #1006) #let part_bp = load-example("Partition", "BinPacking") #let part_bp_sol = part_bp.solutions.at(0) @@ -17047,46 +16919,7 @@ The following table shows concrete variable overhead for example instances, take _Solution extraction._ The Subset Sum configuration is the Integer Expression Membership configuration: $x_i = 1$ (right branch) means element $i$ is selected. ] -// 9. ExactCoverBy3Sets → MinimumWeightSolutionToLinearEquations (#860) -#let x3c_mwle = load-example("ExactCoverBy3Sets", "MinimumWeightSolutionToLinearEquations") -#let x3c_mwle_sol = x3c_mwle.solutions.at(0) -#reduction-rule("ExactCoverBy3Sets", "MinimumWeightSolutionToLinearEquations", - example: true, - example-caption: [$|U| = #x3c_mwle.source.instance.universe_size$, $|cal(C)| = #x3c_mwle.source.instance.subsets.len()$ subsets], - extra: [ - #pred-commands( - "pred create --example " + problem-spec(x3c_mwle.source) + " -o x3c.json", - "pred reduce x3c.json --to " + target-spec(x3c_mwle) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate x3c.json --config " + x3c_mwle_sol.source_config.map(str).join(","), - ) - - #{ - let rows = x3c_mwle.target.instance.matrix - let y = x3c_mwle_sol.target_config - let q = x3c_mwle.source.instance.universe_size / 3 - [ - *Step 1 -- Source instance.* The X3C fixture has subsets $C_0 = {#x3c_mwle.source.instance.subsets.at(0).map(str).join(", ")}$, $C_1 = {#x3c_mwle.source.instance.subsets.at(1).map(str).join(", ")}$, and $C_2 = {#x3c_mwle.source.instance.subsets.at(2).map(str).join(", ")}$ over a universe of size $#x3c_mwle.source.instance.universe_size$, so $q = #q$. - - *Step 2 -- Build the incidence matrix.* The three columns correspond to $C_0$, $C_1$, and $C_2$. The six rows are $r_0 = (#rows.at(0).map(str).join(", "))$, $r_1 = (#rows.at(1).map(str).join(", "))$, $r_2 = (#rows.at(2).map(str).join(", "))$, $r_3 = (#rows.at(3).map(str).join(", "))$, $r_4 = (#rows.at(4).map(str).join(", "))$, and $r_5 = (#rows.at(5).map(str).join(", "))$, with right-hand side $b = (#x3c_mwle.target.instance.rhs.map(str).join(", "))$. - - *Step 3 -- Check the linear equations on the witness.* With $y = (#y.map(str).join(", "))$, the row products are $r_0 dot y = 1$, $r_1 dot y = 1$, $r_2 dot y = 1$, $r_3 dot y = 1$, $r_4 dot y = 1$, and $r_5 dot y = 1$. Hence $A y = b$ for the stored target witness. - - *Step 4 -- Check weight and extraction.* The vector $y$ has #y.filter(x => x != 0).len() nonzero entries, exactly the required $q = #q$. Those two nonzero positions select $C_0$ and $C_1$, so the target witness encodes the same exact cover as the source configuration $(#x3c_mwle_sol.source_config.map(str).join(", "))$ #sym.checkmark. - ] - } - - *Multiplicity:* The fixture stores one canonical witness. - ], -)[ - This $O(q n)$ reduction @garey1979 builds the $3q times n$ incidence matrix $A$ where $A_(i,j) = 1$ iff element $u_i in C_j$. Set right-hand side $b = (1, dots, 1)^top$ and weight bound $K = q = |X|\/3$. An exact cover corresponds to a binary solution of weight exactly $q$. -][ - _Construction._ Let $(X, cal(C))$ be an X3C instance with $|X| = 3q$ and $cal(C) = {C_1, dots, C_n}$. Define $n$ variables $y_1, dots, y_n$. Build matrix $A in {0,1}^(3q times n)$ with $A_(i,j) = 1$ iff $u_i in C_j$. Each column has exactly 3 ones. Set $b = bold(1) in ZZ^(3q)$ and $K = q$. - - _Correctness._ ($arrow.r.double$) An exact cover selects $q$ sets, each covering 3 elements with no overlap, giving $A y = b$ with $y in {0,1}^n$ and weight $q = K$. ($arrow.l.double$) If $y$ has at most $K = q$ nonzero entries and $A y = b$, summing all equations gives $3 sum_j y_j = 3q$, so $sum y_j = q$. With $|"support"| <= q$ and each column contributing 3 incidences, every row is hit by exactly one selected column, forcing each nonzero $y_j = 1$. The selected sets form an exact cover. - - _Solution extraction._ Select subset $j$ iff $y_j != 0$. -] +// Removed: ExactCoverBy3Sets → MinimumWeightSolutionToLinearEquations (unsound reduction, #1006) // 11. KSatisfiability → SimultaneousIncongruences (#554) #let ksat_si = load-example("KSatisfiability", "SimultaneousIncongruences") @@ -17461,90 +17294,11 @@ The following table shows concrete variable overhead for example instances, take _Solution extraction._ Decode the Lehmer code, simulate the schedule tracking start times; assign elements to groups by $floor("start" / (B+1))$. ] -#let tp_smwt = load-example("ThreePartition", "SequencingToMinimizeWeightedTardiness") -#let tp_smwt_sol = tp_smwt.solutions.at(0) -#reduction-rule("ThreePartition", "SequencingToMinimizeWeightedTardiness", - example: true, - example-caption: [$m = #(tp_smwt.source.instance.sizes.len() / 3)$ groups, $B = #tp_smwt.source.instance.bound$, $3m = #tp_smwt.source.instance.sizes.len()$ elements], - extra: [ - #pred-commands( - "pred create --example " + problem-spec(tp_smwt.source) + " -o threepartition.json", - "pred reduce threepartition.json --to " + target-spec(tp_smwt) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate threepartition.json --config " + tp_smwt_sol.source_config.map(str).join(","), - ) - - #{ - let sizes = tp_smwt.source.instance.sizes - let B = tp_smwt.source.instance.bound - let m = sizes.len() / 3 - let n = sizes.len() - let H = m * B + (m - 1) - let Wf = m * B + 1 - let tgt = tp_smwt.target.instance - - [*Step 1 -- Source instance.* The canonical ThreePartition instance has $3m = #n$ elements with sizes $(#sizes.map(str).join(", "))$ and bound $B = #B$. The total sum is $#sizes.fold(0, (a, b) => a + b) = m B = #m times #B$. Each element satisfies $B\/4 < a_i < B\/2$, i.e.\ $#(calc.floor(B / 4) + 1) <= a_i <= #(calc.ceil(B / 2) - 1)$.] - - [*Step 2 -- Construct target tasks.* The horizon is $H = m B + (m - 1) = #H$, and the filler weight is $W_f = m B + 1 = #Wf$. The reduction creates #tgt.lengths.len() tasks: - - *#n element tasks* with lengths $(#tgt.lengths.slice(0, n).map(str).join(", "))$, weights $(#tgt.weights.slice(0, n).map(str).join(", "))$, and deadlines $(#tgt.deadlines.slice(0, n).map(str).join(", "))$ (all equal to $H$). - - *#(m - 1) filler task#if m - 1 != 1 [s]* with length $#tgt.lengths.at(n)$, weight $W_f = #tgt.weights.at(n)$, and deadline $#tgt.deadlines.at(n)$ (tight: $(1) dot B + 1 = #tgt.deadlines.at(n)$). - The tardiness bound is $K = #tgt.bound$.] - - [*Step 3 -- Verify a solution.* The source assignment $(#tp_smwt_sol.source_config.map(str).join(", "))$ places elements ${0, 1, 2}$ (sizes $#sizes.at(0), #sizes.at(1), #sizes.at(2)$, sum $= #(sizes.at(0) + sizes.at(1) + sizes.at(2))$) in group 0 and elements ${3, 4, 5}$ (sizes $#sizes.at(3), #sizes.at(4), #sizes.at(5)$, sum $= #(sizes.at(3) + sizes.at(4) + sizes.at(5))$) in group 1. Both groups sum to $B = #B$ #sym.checkmark. The target Lehmer code is $(#tp_smwt_sol.target_config.map(str).join(", "))$, which decodes to the schedule: tasks $0, 1, 2$ (slot 0, total length $#B$), then filler (length 1, completes at $#(B + 1) <= #tgt.deadlines.at(n)$ #sym.checkmark), then tasks $3, 4, 5$ (slot 1, completes at $#H$). All element deadlines are $#H$ #sym.checkmark, and the filler meets its tight deadline. Zero tardiness achieved #sym.checkmark.] - - [*Multiplicity:* The fixture stores one canonical witness. This instance admits other valid orderings within each slot (e.g.\ permuting elements 0, 1, 2 within slot 0), but the group assignment is unique up to slot relabeling.] - } - ], -)[ - High-weight filler tasks with tight deadlines force zero-tardiness schedules to leave exactly $m$ slots of width $B$ for element tasks. Size constraints ensure 3 elements per slot. -][ - _Construction._ Given $(S, B)$ with $|S| = 3m$. Horizon $H = m B + (m-1)$, filler weight $W_f = m B + 1$. Element tasks: $p_i = a_i$, $w_i = 1$, $d_i = H$. Filler tasks ($m-1$): $p = 1$, $w = W_f$, $d_j = (j+1)B + (j+1)$. Tardiness bound $K = 0$. - - _Correctness._ ($arrow.r.double$) A valid 3-partition schedules each triple in a slot between fillers, achieving zero tardiness. ($arrow.l.double$) Zero tardiness forces fillers to their tight deadlines, creating $m$ slots of width $B$; element tasks fill them exactly with 3 per slot. - - _Solution extraction._ Decode Lehmer code; scan left to right incrementing group index at each filler. -] - -#let tp_jss = load-example("ThreePartition", "JobShopScheduling") -#let tp_jss_sol = tp_jss.solutions.at(0) -#reduction-rule("ThreePartition", "JobShopScheduling", - example: true, - example-caption: [$3m = #tp_jss.source.instance.sizes.len()$ elements, $B = #tp_jss.source.instance.bound$, #tp_jss.target.instance.num_processors machines], - extra: [ - #pred-commands( - "pred create --example " + problem-spec(tp_jss.source) + " -o threepartition.json", - "pred reduce threepartition.json --to " + target-spec(tp_jss) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate threepartition.json --config " + tp_jss_sol.source_config.map(str).join(","), - ) - - *Step 1 -- Source instance.* The canonical 3-Partition instance has $3m = #tp_jss.source.instance.sizes.len()$ elements with sizes $S = (#tp_jss.source.instance.sizes.map(str).join(", "))$ and bound $B = #tp_jss.source.instance.bound$. Since $m = #{ let n = tp_jss.source.instance.sizes.len(); str(calc.div-euclid(n, 3)) }$, there are $m - 1 = #{ let n = tp_jss.source.instance.sizes.len(); str(calc.div-euclid(n, 3) - 1) }$ separators, separator length $L = m B + 1 = #{ let n = tp_jss.source.instance.sizes.len(); let m = calc.div-euclid(n, 3); str(m * tp_jss.source.instance.bound + 1) }$, and deadline $D = m B + (m-1) L = #{ let n = tp_jss.source.instance.sizes.len(); let m = calc.div-euclid(n, 3); let B = tp_jss.source.instance.bound; let L = m * B + 1; str(m * B + (m - 1) * L) }$. - - *Step 2 -- Construct jobs.* Each element $a_i$ becomes an element job with two tasks: $(text("machine") 0, a_i)$ then $(text("machine") 1, a_i)$. This gives #tp_jss.target.instance.jobs.len() jobs on #tp_jss.target.instance.num_processors processors. The target JSS instance has jobs: #{ let jobs = tp_jss.target.instance.jobs; let descs = jobs.map(j => { let tasks = j.map(t => "(" + str(t.at(0)) + "," + str(t.at(1)) + ")"); "[" + tasks.join(", ") + "]" }); descs.join("; ") }. - - *Step 3 -- Verify a solution.* The source config $(#tp_jss_sol.source_config.map(str).join(", "))$ assigns all #tp_jss.source.instance.sizes.len() elements to group 0. With $m = 1$ and no separators, any ordering of the #tp_jss.source.instance.sizes.len() element tasks on each machine is valid. The target Lehmer code $(#tp_jss_sol.target_config.map(str).join(", "))$ encodes identity orderings on both machines. The resulting makespan is $sum a_i = #{ tp_jss.source.instance.sizes.sum() } = B = #tp_jss.source.instance.bound <= D$ #sym.checkmark, and all #tp_jss.source.instance.sizes.len() elements land in a single processor slot containing exactly 3 elements #sym.checkmark. - - *Multiplicity:* The fixture stores one canonical witness. With $m = 1$ there is only one valid group assignment (all elements in group 0); the $3! times 3!$ task orderings on the two machines yield multiple target configs, but only one source partition. - ], -)[ - On 2 machines, $m-1$ long separator jobs on machine 0 force element jobs into $m$ windows of width $B$. Size constraints ensure 3 elements per window. -][ - _Construction._ Given $(S, B)$ with $|S| = 3m$, $L = m B + 1$, $D = m B + (m-1)L$. Element jobs ($3m$): two tasks $(text("machine") 0, a_i)$ then $(text("machine") 1, a_i)$. Separator jobs ($m-1$): single task $(text("machine") 0, L)$. - - _Correctness._ ($arrow.r.double$) A valid 3-partition interleaves element groups with separators on machine 0, achieving makespan $D$. ($arrow.l.double$) Separators of length $L > m B$ create impassable barriers; remaining budget $m B$ split into $m$ windows; size constraints force 3 elements per window. +// Removed: ThreePartition → SequencingToMinimizeWeightedTardiness (unsound reduction, #1006) - _Solution extraction._ Decode machine 0 Lehmer code; walk permutation incrementing group at each separator. -] +// Removed: ThreePartition → JobShopScheduling (unsound reduction, #1006) -#reduction-rule("ThreePartition", "FlowShopScheduling")[ - On 3 machines, $m - 1$ separator jobs with a large middle-machine task force element jobs into $m$ groups of width $B$, so a valid 3-partition exists iff the flow-shop schedule meets its deadline. -][ - _Construction._ Given $(S, B)$ with $|S| = 3m$ and sizes $s(a_0), dots, s(a_(3m - 1))$. Set $L = m B + 1$. Create $3m$ element jobs: job $i$ has task lengths $(s(a_i), s(a_i), s(a_i))$ (identical on all 3 machines). Create $m - 1$ separator jobs with task lengths $(0, L, 0)$. Compute the deadline $D$ as the makespan of a canonical schedule that interleaves element triples with separators: $[a_0, a_1, a_2, "sep"_1, a_3, a_4, a_5, "sep"_2, dots]$. - - _Correctness._ ($arrow.r.double$) A valid 3-partition with group sums all equal to $B$ yields a schedule that places each triple between consecutive separators. Since each element job has equal task length on all machines, the pipeline delay between machines matches exactly, and the interleaved schedule achieves makespan $D$. ($arrow.l.double$) Each separator has a middle-machine task of length $L = m B + 1 > m B$, which blocks machine 2 for longer than the total element processing time on that machine. This forces element jobs to occupy exactly $m$ windows of width $B$ between separators. Since $B\/4 < s(a_i) < B\/2$ (the standard strong-sense 3-Partition assumption), each window contains exactly 3 elements whose sizes sum to $B$. - - _Solution extraction._ Decode the Lehmer code to a job permutation. Walk left to right, incrementing the group index at each separator job. Each element job's group index gives the source configuration. -] +// Removed: ThreePartition → FlowShopScheduling (unsound reduction, #1006) #let mc_mcbs = load-example("MaxCut", "MinimumCutIntoBoundedSets") #let mc_mcbs_sol = mc_mcbs.solutions.at(0) diff --git a/src/rules/circuit_spinglass.rs b/src/rules/circuit_spinglass.rs index 42857d92..da080d92 100644 --- a/src/rules/circuit_spinglass.rs +++ b/src/rules/circuit_spinglass.rs @@ -414,8 +414,8 @@ where #[reduction( overhead = { - num_spins = "num_assignments", - num_interactions = "num_assignments", + num_spins = "num_assignments * num_variables", + num_interactions = "num_assignments * num_variables", } )] impl ReduceTo> for CircuitSAT { diff --git a/src/rules/exactcoverby3sets_minimumweightsolutiontolinearequations.rs b/src/rules/exactcoverby3sets_minimumweightsolutiontolinearequations.rs deleted file mode 100644 index 752c43d0..00000000 --- a/src/rules/exactcoverby3sets_minimumweightsolutiontolinearequations.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Reduction from ExactCoverBy3Sets to MinimumWeightSolutionToLinearEquations. -//! -//! The incidence matrix has one row per universe element and one column per set. -//! Selecting a set corresponds to setting its variable to 1. The equation -//! system enforces that each universe element is covered exactly once, and the -//! sparsity bound enforces that at most `|U|/3` sets are selected. - -use crate::models::algebraic::MinimumWeightSolutionToLinearEquations; -use crate::models::set::ExactCoverBy3Sets; -use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; - -#[derive(Debug, Clone)] -pub struct ReductionX3CToMinimumWeightSolutionToLinearEquations { - target: MinimumWeightSolutionToLinearEquations, -} - -impl ReductionResult for ReductionX3CToMinimumWeightSolutionToLinearEquations { - type Source = ExactCoverBy3Sets; - type Target = MinimumWeightSolutionToLinearEquations; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - target_solution.to_vec() - } -} - -#[reduction(overhead = { - num_variables = "num_sets", - num_equations = "universe_size", -})] -impl ReduceTo for ExactCoverBy3Sets { - type Result = ReductionX3CToMinimumWeightSolutionToLinearEquations; - - fn reduce_to(&self) -> Self::Result { - let mut coefficients = vec![vec![0i64; self.num_sets()]; self.universe_size()]; - for (set_index, set) in self.sets().iter().enumerate() { - for &element in set { - coefficients[element][set_index] = 1; - } - } - - ReductionX3CToMinimumWeightSolutionToLinearEquations { - target: MinimumWeightSolutionToLinearEquations::new( - coefficients, - vec![1; self.universe_size()], - ), - } - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "exactcoverby3sets_to_minimumweightsolutiontolinearequations", - build: || { - crate::example_db::specs::rule_example_with_witness::< - _, - MinimumWeightSolutionToLinearEquations, - >( - ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]), - SolutionPair { - source_config: vec![1, 1, 0], - target_config: vec![1, 1, 0], - }, - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/exactcoverby3sets_minimumweightsolutiontolinearequations.rs"] -mod tests; diff --git a/src/rules/hamiltonianpath_consecutiveonessubmatrix.rs b/src/rules/hamiltonianpath_consecutiveonessubmatrix.rs deleted file mode 100644 index 4880aa86..00000000 --- a/src/rules/hamiltonianpath_consecutiveonessubmatrix.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! Reduction from HamiltonianPath to ConsecutiveOnesSubmatrix. -//! -//! Given a Hamiltonian Path instance G = (V, E) with n vertices and m edges, -//! we construct a ConsecutiveOnesSubmatrix instance as follows (Booth 1975, -//! Garey & Johnson SR14): -//! -//! 1. Build the vertex-edge incidence matrix A of size n × m: -//! a_{i,j} = 1 iff vertex i is an endpoint of edge j. -//! 2. Set bound K = n − 1 (number of edges in a Hamiltonian path). -//! -//! G has a Hamiltonian path iff K columns of A can be permuted so that each -//! row has all its 1's consecutive (the consecutive ones property). -//! -//! Overhead: num_rows = num_vertices, num_cols = num_edges, bound = num_vertices − 1. - -use crate::models::algebraic::ConsecutiveOnesSubmatrix; -use crate::models::graph::HamiltonianPath; -use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::topology::{Graph, SimpleGraph}; - -/// Result of reducing HamiltonianPath to ConsecutiveOnesSubmatrix. -/// -/// Stores the target problem, the original graph edge list (for solution -/// extraction), and the number of original vertices. -#[derive(Debug, Clone)] -pub struct ReductionHamiltonianPathToConsecutiveOnesSubmatrix { - target: ConsecutiveOnesSubmatrix, - /// Edges of the original graph, indexed the same as columns in the matrix. - edges: Vec<(usize, usize)>, - /// Number of vertices in the original graph. - num_vertices: usize, -} - -impl ReductionResult for ReductionHamiltonianPathToConsecutiveOnesSubmatrix { - type Source = HamiltonianPath; - type Target = ConsecutiveOnesSubmatrix; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - let n = self.num_vertices; - if n == 0 { - return vec![]; - } - if n == 1 { - return vec![0]; - } - - // target_solution is a binary vector over columns (edges). - // Selected columns correspond to edges forming the Hamiltonian path. - // Guard: in the Tucker fallback branch, edges may be shorter than target columns. - let selected_edges: Vec<(usize, usize)> = target_solution - .iter() - .enumerate() - .filter(|(_, &v)| v == 1) - .filter_map(|(j, _)| self.edges.get(j).copied()) - .collect(); - - if selected_edges.len() != n - 1 { - return vec![0; n]; - } - - // Build adjacency list from selected edges. - let mut adj: Vec> = vec![vec![]; n]; - for &(u, v) in &selected_edges { - adj[u].push(v); - adj[v].push(u); - } - - // Find the path endpoints (degree-1 vertices in the selected subgraph). - let endpoints: Vec = (0..n).filter(|&v| adj[v].len() == 1).collect(); - if endpoints.len() != 2 { - // Not a valid path — fallback. - return vec![0; n]; - } - - // Walk the path from one endpoint. - let mut path = Vec::with_capacity(n); - let mut current = endpoints[0]; - let mut prev = usize::MAX; - for _ in 0..n { - path.push(current); - let next = adj[current].iter().copied().find(|&nb| nb != prev); - prev = current; - match next { - Some(nx) => current = nx, - None => break, - } - } - - if path.len() != n { - return vec![0; n]; - } - - path - } -} - -#[reduction( - overhead = { - num_rows = "num_vertices", - num_cols = "num_edges", - } -)] -impl ReduceTo for HamiltonianPath { - type Result = ReductionHamiltonianPathToConsecutiveOnesSubmatrix; - - fn reduce_to(&self) -> Self::Result { - let n = self.num_vertices(); - let edges = self.graph().edges(); - let m = edges.len(); - - // K = n - 1 (but at least 0 for degenerate cases). - let bound = if n > 0 { (n - 1) as i64 } else { 0 }; - - // If there are fewer edges than required (m < n-1), a Hamiltonian path - // is impossible. Construct a trivially unsatisfiable C1P instance: - // a 3×3 Tucker-style matrix with bound = 3 that has no valid column - // permutation satisfying C1P. - if n > 1 && m < n - 1 { - let tucker = vec![ - vec![true, true, false], - vec![true, false, true], - vec![false, true, true], - ]; - let target = ConsecutiveOnesSubmatrix::new(tucker, 3); - return ReductionHamiltonianPathToConsecutiveOnesSubmatrix { - target, - edges, - num_vertices: n, - }; - } - - // Build n × m vertex-edge incidence matrix. - let mut matrix = vec![vec![false; m]; n]; - for (j, &(u, v)) in edges.iter().enumerate() { - matrix[u][j] = true; - matrix[v][j] = true; - } - - let target = ConsecutiveOnesSubmatrix::new(matrix, bound); - - ReductionHamiltonianPathToConsecutiveOnesSubmatrix { - target, - edges, - 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: "hamiltonianpath_to_consecutiveonessubmatrix", - build: || { - // Path graph: 0-1-2-3 (has a Hamiltonian path: 0,1,2,3) - let source = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); - - // Edges: [(0,1), (1,2), (2,3)] — all 3 edges are in the path. - // K = 3 = n-1, so target_config selects all columns. - let target_config = vec![1, 1, 1]; - - crate::example_db::specs::rule_example_with_witness::<_, ConsecutiveOnesSubmatrix>( - source, - SolutionPair { - source_config: vec![0, 1, 2, 3], - target_config, - }, - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/hamiltonianpath_consecutiveonessubmatrix.rs"] -mod tests; diff --git a/src/rules/maximumindependentset_triangular.rs b/src/rules/maximumindependentset_triangular.rs index 0f57af8e..dbbed556 100644 --- a/src/rules/maximumindependentset_triangular.rs +++ b/src/rules/maximumindependentset_triangular.rs @@ -28,7 +28,7 @@ impl ReductionResult for ReductionISSimpleToTriangular { } fn extract_solution(&self, target_solution: &[usize]) -> Vec { - self.mapping_result.map_config_back(target_solution) + self.mapping_result.map_config_back_via_centers(target_solution) } } diff --git a/src/rules/minimumvertexcover_ensemblecomputation.rs b/src/rules/minimumvertexcover_ensemblecomputation.rs index 3a3f4113..158fc65d 100644 --- a/src/rules/minimumvertexcover_ensemblecomputation.rs +++ b/src/rules/minimumvertexcover_ensemblecomputation.rs @@ -42,13 +42,20 @@ impl ReductionResult for ReductionVCToEC { /// - z_i = {a₀} ∪ {v} — vertex v is in the cover /// - z_j = {u} ∪ z_k — edge {u, v_r} is covered by v_r /// - /// We collect all vertices that appear as singleton operands (index < |V|). - /// In a minimum-length witness, exactly the cover vertices appear this way. + /// We collect all vertices that appear as singleton operands (index < |V|) + /// in the meaningful steps only (before all required subsets are covered). + /// Padding steps beyond the coverage point are ignored. fn extract_solution(&self, target_solution: &[usize]) -> Vec { - let budget = self.target.budget(); + use crate::traits::Problem; + use crate::types::Min; + + let meaningful_steps = match self.target.evaluate(target_solution) { + Min(Some(n)) => n, + _ => return vec![0; self.num_vertices], + }; let mut cover = vec![0usize; self.num_vertices]; - for step in 0..budget { + for step in 0..meaningful_steps { let left = target_solution[2 * step]; let right = target_solution[2 * step + 1]; @@ -121,7 +128,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec Vec Vec Vec Vec Vec &Self::Target { - &self.target - } - - /// Extract a Partition assignment from a SequencingWithinIntervals solution. - /// - /// The target config encodes start-time offsets from release times. - /// For regular tasks (release = 0), the offset is the start time itself. - /// Tasks starting before `half` belong to subset 0; tasks starting at or - /// after `half + 1` belong to subset 1. - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - (0..self.num_elements) - .map(|i| { - let start = target_solution[i] as u64; // release = 0, so offset = start - if start > self.half { - 1 - } else { - 0 - } - }) - .collect() - } -} - -#[reduction(overhead = { - num_tasks = "num_elements + 1", -})] -impl ReduceTo for Partition { - type Result = ReductionPartitionToSWI; - - fn reduce_to(&self) -> Self::Result { - let n = self.num_elements(); - let s = self.total_sum(); - let half = s / 2; - - // Regular tasks: one per element, release=0, deadline=S+1, length=a_i - let mut release_times = vec![0u64; n]; - let mut deadlines = vec![s + 1; n]; - let mut lengths: Vec = self.sizes().to_vec(); - - // Enforcer task: release=S/2, deadline=S/2+1, length=1 - // The enforcer is pinned at time S/2, splitting the timeline into two equal blocks. - release_times.push(half); - deadlines.push(half + 1); - lengths.push(1); - - ReductionPartitionToSWI { - target: SequencingWithinIntervals::new(release_times, deadlines, lengths), - num_elements: n, - half, - } - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "partition_to_sequencingwithinintervals", - build: || { - // sizes [1, 2, 3, 4], sum=10, half=5 - // partition: {2,3} in subset 0 (before enforcer), {1,4} in subset 1 (after enforcer) - // Schedule: tasks 1,2 (lengths 2,3) fill [0,5), enforcer at [5,6), tasks 0,3 (lengths 1,4) fill [6,11) - // Target config = start time offsets: task0=6, task1=0, task2=2, task3=7, enforcer=0 - crate::example_db::specs::rule_example_with_witness::<_, SequencingWithinIntervals>( - Partition::new(vec![1, 2, 3, 4]), - SolutionPair { - source_config: vec![1, 0, 0, 1], - target_config: vec![6, 0, 2, 7, 0], - }, - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/partition_sequencingwithinintervals.rs"] -mod tests; diff --git a/src/rules/partition_shortestweightconstrainedpath.rs b/src/rules/partition_shortestweightconstrainedpath.rs deleted file mode 100644 index 02db41e2..00000000 --- a/src/rules/partition_shortestweightconstrainedpath.rs +++ /dev/null @@ -1,141 +0,0 @@ -//! Reduction from Partition to ShortestWeightConstrainedPath. -//! -//! Constructs a chain of n+1 vertices with two parallel edges per layer. -//! A balanced partition corresponds to a shortest weight-constrained s-t path. - -use crate::models::graph::ShortestWeightConstrainedPath; -use crate::models::misc::Partition; -use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::topology::SimpleGraph; - -/// Result of reducing Partition to ShortestWeightConstrainedPath. -#[derive(Debug, Clone)] -pub struct ReductionPartitionToShortestWeightConstrainedPath { - target: ShortestWeightConstrainedPath, - n: usize, -} - -impl ReductionResult for ReductionPartitionToShortestWeightConstrainedPath { - type Source = Partition; - type Target = ShortestWeightConstrainedPath; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - // Target edges are ordered: for layer i, edge 2*i is "include", - // edge 2*i+1 is "exclude". - // Include edge chosen → element in subset A_1 (config = 0). - // Exclude edge chosen → element in subset A_2 (config = 1). - (0..self.n) - .map(|i| { - let include_edge = 2 * i; - let exclude_edge = 2 * i + 1; - if target_solution[exclude_edge] == 1 { - 1 - } else { - debug_assert_eq!( - target_solution[include_edge], 1, - "layer {i}: neither include nor exclude edge selected" - ); - 0 - } - }) - .collect() - } -} - -fn partition_size_to_i32(value: u64) -> i32 { - i32::try_from(value).expect( - "Partition -> ShortestWeightConstrainedPath requires all sizes and weight_bound to fit in i32", - ) -} - -#[reduction(overhead = { - num_vertices = "num_elements + 1", - num_edges = "2 * num_elements", -})] -impl ReduceTo> for Partition { - type Result = ReductionPartitionToShortestWeightConstrainedPath; - - fn reduce_to(&self) -> Self::Result { - let n = self.num_elements(); - let num_vertices = n + 1; - - // Build edges: for each layer i (0..n), two parallel edges (v_i, v_{i+1}). - let mut edges = Vec::with_capacity(2 * n); - let mut edge_lengths = Vec::with_capacity(2 * n); - let mut edge_weights = Vec::with_capacity(2 * n); - - for i in 0..n { - let a_i = partition_size_to_i32(self.sizes()[i]); - let a_i_plus_1 = a_i.checked_add(1).expect("a_i + 1 overflows i32"); - - // "Include" edge: length = a_i + 1, weight = 1 - edges.push((i, i + 1)); - edge_lengths.push(a_i_plus_1); - edge_weights.push(1); - - // "Exclude" edge: length = 1, weight = a_i + 1 - edges.push((i, i + 1)); - edge_lengths.push(1); - edge_weights.push(a_i_plus_1); - } - - let total_sum = partition_size_to_i32(self.total_sum()); - let weight_bound = (total_sum / 2) - .checked_add(partition_size_to_i32(n as u64)) - .expect("weight_bound overflows i32"); - - let graph = SimpleGraph::new(num_vertices, edges); - - ReductionPartitionToShortestWeightConstrainedPath { - target: ShortestWeightConstrainedPath::new( - graph, - edge_lengths, - edge_weights, - 0, - n, - weight_bound, - ), - n, - } - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "partition_to_shortestweightconstrainedpath", - build: || { - // Partition {3, 1, 1, 2, 2, 1}: balanced split {3,2} vs {1,1,2,1}. - // Source config: [1,0,0,1,0,0] means elements 0,3 in A_2. - // Target: include edges for layers where config=0, exclude for config=1. - // Layer 0 (a=3): exclude (config=1) → target edge 1 - // Layer 1 (a=1): include (config=0) → target edge 2 - // Layer 2 (a=1): include (config=0) → target edge 4 - // Layer 3 (a=2): exclude (config=1) → target edge 7 - // Layer 4 (a=2): include (config=0) → target edge 8 - // Layer 5 (a=1): include (config=0) → target edge 10 - // Target config (12 edges): [0,1, 1,0, 1,0, 0,1, 1,0, 1,0] - crate::example_db::specs::rule_example_with_witness::< - _, - ShortestWeightConstrainedPath, - >( - Partition::new(vec![3, 1, 1, 2, 2, 1]), - SolutionPair { - source_config: vec![1, 0, 0, 1, 0, 0], - target_config: vec![0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0], - }, - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/partition_shortestweightconstrainedpath.rs"] -mod tests; diff --git a/src/rules/sat_circuitsat.rs b/src/rules/sat_circuitsat.rs index 7b93744a..f0a2eb5a 100644 --- a/src/rules/sat_circuitsat.rs +++ b/src/rules/sat_circuitsat.rs @@ -8,6 +8,7 @@ use crate::models::formula::{Assignment, BooleanExpr, Circuit, CircuitSAT}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::Problem; +use std::collections::HashSet; /// Result of reducing SAT to CircuitSAT. #[derive(Debug, Clone)] @@ -35,8 +36,8 @@ impl ReductionResult for ReductionSATToCircuit { #[reduction( overhead = { - num_variables = "num_vars + num_clauses + 1", - num_assignments = "num_clauses + 2", + num_variables = "num_vars + num_clauses", + num_assignments = "num_vars + num_clauses", } )] impl ReduceTo for Satisfiability { @@ -95,6 +96,22 @@ impl ReduceTo for Satisfiability { BooleanExpr::constant(true), )); + // Add identity assignments for variables that don't appear in any clause, + // so they are present in Circuit::variables() for index mapping. + let used_vars: HashSet = clauses + .iter() + .flat_map(|c| c.literals.iter().map(|&lit| lit.unsigned_abs() as usize)) + .collect(); + for i in 1..=num_vars { + if !used_vars.contains(&i) { + let var_name = format!("x{}", i); + assignments.push(Assignment::new( + vec![format!("__unused_{}", i)], + BooleanExpr::var(&var_name), + )); + } + } + let circuit = Circuit::new(assignments); let target = CircuitSAT::new(circuit); diff --git a/src/rules/spinglass_maxcut.rs b/src/rules/spinglass_maxcut.rs index e056be62..e3ed5a41 100644 --- a/src/rules/spinglass_maxcut.rs +++ b/src/rules/spinglass_maxcut.rs @@ -133,7 +133,7 @@ where #[reduction( overhead = { num_vertices = "num_spins", - num_edges = "num_interactions", + num_edges = "num_interactions + num_spins", } )] impl ReduceTo> for SpinGlass { diff --git a/src/rules/subsetsum_capacityassignment.rs b/src/rules/subsetsum_capacityassignment.rs deleted file mode 100644 index 85d5f1d5..00000000 --- a/src/rules/subsetsum_capacityassignment.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Reduction from SubsetSum to CapacityAssignment. -//! -//! Each element becomes a communication link with two capacity levels. -//! Choosing the high capacity (index 1) corresponds to including the element -//! in the subset. The delay budget constraint enforces that enough elements -//! are included to make the total cost equal to the target sum B. - -use crate::models::misc::{CapacityAssignment, SubsetSum}; -use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; - -/// Result of reducing SubsetSum to CapacityAssignment. -#[derive(Debug, Clone)] -pub struct ReductionSubsetSumToCapacityAssignment { - target: CapacityAssignment, -} - -impl ReductionResult for ReductionSubsetSumToCapacityAssignment { - type Source = SubsetSum; - type Target = CapacityAssignment; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - /// Solution extraction: capacity index 1 (high) means the element is selected. - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - target_solution.to_vec() - } -} - -#[reduction(overhead = { - num_links = "num_elements", - num_capacities = "2", -})] -impl ReduceTo for SubsetSum { - type Result = ReductionSubsetSumToCapacityAssignment; - - fn reduce_to(&self) -> Self::Result { - let n = self.num_elements(); - - // Capacities: {1, 2} - let capacities = vec![1, 2]; - - // For each element a_i: - // cost(c_i, 1) = 0 (low capacity = not selected) - // cost(c_i, 2) = a_i (high capacity = selected, costs a_i) - // delay(c_i, 1) = a_i (low capacity incurs delay a_i) - // delay(c_i, 2) = 0 (high capacity has zero delay) - let mut cost = Vec::with_capacity(n); - let mut delay = Vec::with_capacity(n); - - for size in self.sizes() { - let a_i: u64 = size - .try_into() - .expect("SubsetSum element must fit in u64 for CapacityAssignment reduction"); - cost.push(vec![0, a_i]); - delay.push(vec![a_i, 0]); - } - - // Delay budget J = S - B, where S = sum of all elements - let total_sum: u64 = self - .sizes() - .iter() - .map(|s| -> u64 { - s.try_into() - .expect("SubsetSum element must fit in u64 for CapacityAssignment reduction") - }) - .sum(); - let target_val: u64 = self - .target() - .try_into() - .expect("SubsetSum target must fit in u64 for CapacityAssignment reduction"); - // Use saturating subtraction to avoid underflow when target_val > total_sum. - // In that case, treat the delay budget as 0 so the reduction remains sound. - let delay_budget = total_sum.saturating_sub(target_val); - - ReductionSubsetSumToCapacityAssignment { - target: CapacityAssignment::new(capacities, cost, delay, delay_budget), - } - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "subsetsum_to_capacityassignment", - build: || { - // SubsetSum: sizes = [3, 7, 1, 8, 2, 4], target = 11 - // Solution: select elements 0 and 3 (values 3 and 8), sum = 11. - // In CapacityAssignment: config [1, 0, 0, 1, 0, 0] means - // links 0,3 get high capacity (index 1), others get low (index 0). - crate::example_db::specs::rule_example_with_witness::<_, CapacityAssignment>( - SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32), - SolutionPair { - source_config: vec![1, 0, 0, 1, 0, 0], - target_config: vec![1, 0, 0, 1, 0, 0], - }, - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/subsetsum_capacityassignment.rs"] -mod tests; diff --git a/src/rules/threepartition_flowshopscheduling.rs b/src/rules/threepartition_flowshopscheduling.rs deleted file mode 100644 index efa72dcb..00000000 --- a/src/rules/threepartition_flowshopscheduling.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! Reduction from ThreePartition to FlowShopScheduling. -//! -//! Given a 3-Partition instance with 3m elements of sizes s(a_i) and bound B, -//! construct a 3-machine flow-shop scheduling instance: -//! -//! - 3m "element jobs": job i has task_lengths = [s(a_i), s(a_i), s(a_i)] -//! - (m-1) "separator jobs": task_lengths = [0, L, 0] where L = m*B + 1 -//! - Deadline D = makespan of a canonical schedule (computed via compute_makespan) -//! -//! A valid 3-partition exists iff the flow-shop schedule meets deadline D. -//! The large separator tasks on machine 2 force exactly 3 element jobs -//! (summing to B) between consecutive separators. -//! -//! Solution extraction: decode Lehmer code to job order, count separators -//! to determine which group each element job belongs to. - -use crate::models::misc::{FlowShopScheduling, ThreePartition}; -use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; - -/// Result of reducing ThreePartition to FlowShopScheduling. -#[derive(Debug, Clone)] -pub struct ReductionThreePartitionToFSS { - target: FlowShopScheduling, - /// Number of elements (3m) in the source problem. - num_elements: usize, -} - -impl ReductionResult for ReductionThreePartitionToFSS { - type Source = ThreePartition; - type Target = FlowShopScheduling; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - /// Extract source solution from target solution. - /// - /// The target config is a Lehmer code encoding a job permutation. - /// Decode to job order, then walk through counting separators - /// to assign each element job to a group. - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - let n = self.target.num_jobs(); - let job_order = - crate::models::misc::decode_lehmer(target_solution, n).expect("valid Lehmer code"); - - let mut config = vec![0usize; self.num_elements]; - let mut current_group = 0; - - for &job in &job_order { - if job < self.num_elements { - // Element job: assign to current group - config[job] = current_group; - } else { - // Separator job: advance to next group - current_group += 1; - } - } - - config - } -} - -#[reduction(overhead = { - num_jobs = "num_elements + num_groups - 1", -})] -impl ReduceTo for ThreePartition { - type Result = ReductionThreePartitionToFSS; - - fn reduce_to(&self) -> Self::Result { - let num_elements = self.num_elements(); - let num_groups = self.num_groups(); - let bound = self.bound(); - - // L = m * B + 1 — large enough to force grouping - let big_l = (num_groups as u64) * bound + 1; - - // Build task_lengths: element jobs first, then separator jobs - let mut task_lengths = Vec::with_capacity(num_elements + num_groups - 1); - - // Element jobs: identical task length on all 3 machines - for &size in self.sizes() { - task_lengths.push(vec![size, size, size]); - } - - // Separator jobs: [0, L, 0] - for _ in 0..num_groups.saturating_sub(1) { - task_lengths.push(vec![0, big_l, 0]); - } - - // Compute deadline from canonical schedule. - // Canonical order: group1 elements, sep1, group2 elements, sep2, ... - // We use a valid partition ordering to compute the achievable makespan. - let canonical_order: Vec = { - let mut order = Vec::with_capacity(num_elements + num_groups - 1); - for g in 0..num_groups { - // Add 3 element jobs per group (in natural order) - for i in 0..3 { - order.push(g * 3 + i); - } - // Add separator after each group except the last - if g < num_groups - 1 { - order.push(num_elements + g); - } - } - order - }; - - let target_no_deadline = FlowShopScheduling::new(3, task_lengths.clone(), u64::MAX); - let deadline = target_no_deadline.compute_makespan(&canonical_order); - - let target = FlowShopScheduling::new(3, task_lengths, deadline); - - ReductionThreePartitionToFSS { - target, - num_elements, - } - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "threepartition_to_flowshopscheduling", - build: || { - // ThreePartition: sizes [4, 5, 6, 4, 6, 5], bound=15, m=2 - // Valid partition: {4,5,6} and {4,6,5} - let source = ThreePartition::new(vec![4, 5, 6, 4, 6, 5], 15); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - // Canonical order: elements [0,1,2], separator [6], elements [3,4,5] - // Lehmer encode: job order [0,1,2,6,3,4,5] - // For Lehmer encoding of [0,1,2,6,3,4,5]: - // available=[0,1,2,3,4,5,6], pick 0 -> index 0; available=[1,2,3,4,5,6] - // available=[1,2,3,4,5,6], pick 1 -> index 0; available=[2,3,4,5,6] - // available=[2,3,4,5,6], pick 2 -> index 0; available=[3,4,5,6] - // available=[3,4,5,6], pick 6 -> index 3; available=[3,4,5] - // available=[3,4,5], pick 3 -> index 0; available=[4,5] - // available=[4,5], pick 4 -> index 0; available=[5] - // available=[5], pick 5 -> index 0; - let target_config = vec![0, 0, 0, 3, 0, 0, 0]; - - // Source config: element 0,1,2 -> group 0; element 3,4,5 -> group 1 - let source_config = vec![0, 0, 0, 1, 1, 1]; - - crate::example_db::specs::assemble_rule_example( - &source, - target, - vec![SolutionPair { - source_config, - target_config, - }], - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/threepartition_flowshopscheduling.rs"] -mod tests; diff --git a/src/rules/threepartition_jobshopscheduling.rs b/src/rules/threepartition_jobshopscheduling.rs deleted file mode 100644 index 766e5589..00000000 --- a/src/rules/threepartition_jobshopscheduling.rs +++ /dev/null @@ -1,215 +0,0 @@ -//! Reduction from ThreePartition to JobShopScheduling. -//! -//! Given a 3-Partition instance with 3m positive integers (each strictly between -//! B/4 and B/2) that must be partitioned into m triples summing to B, construct a -//! Job-Shop Scheduling instance on 2 processors: -//! -//! - **Element jobs** (3m jobs): job i has tasks [(0, s(a_i)), (1, s(a_i))]. -//! - **Separator jobs** (m-1 jobs): job k has a single task [(0, L)] where L = m*B + 1. -//! -//! The separators force m windows of size B on processor 0. A valid 3-partition -//! exists iff the optimal makespan equals the threshold D = m*B + (m-1)*L. -//! -//! Solution extraction: decode the processor-0 Lehmer code to find the task -//! ordering, locate the separator boundaries, and assign each element to the -//! group (window) it occupies. -//! -//! Reference: Garey, Johnson & Sethi (1976). "The complexity of flowshop and -//! jobshop scheduling." Mathematics of Operations Research 1, pp. 117-129. - -use crate::models::misc::{JobShopScheduling, ThreePartition}; -use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; - -/// Result of reducing ThreePartition to JobShopScheduling. -#[derive(Debug, Clone)] -pub struct ReductionThreePartitionToJSS { - target: JobShopScheduling, - /// Number of elements (3m) in the source problem. - num_elements: usize, - /// Number of groups (m) in the source problem. - num_groups: usize, - /// The makespan threshold: schedules achieving this makespan correspond - /// to valid 3-partitions. - threshold: u64, -} - -impl ReductionThreePartitionToJSS { - /// The makespan threshold D: a valid 3-partition exists iff the optimal - /// makespan of the target JSS instance equals D. - pub fn threshold(&self) -> u64 { - self.threshold - } - - /// Compute the makespan threshold D = m*B + (m-1)*L where L = m*B + 1. - fn compute_threshold(num_groups: usize, bound: u64) -> u64 { - let m = num_groups as u64; - let b = bound; - let l = m * b + 1; - m * b + (m - 1) * l - } - - /// Compute the separator length L = m*B + 1. - fn separator_length(num_groups: usize, bound: u64) -> u64 { - (num_groups as u64) * bound + 1 - } -} - -impl ReductionResult for ReductionThreePartitionToJSS { - type Source = ThreePartition; - type Target = JobShopScheduling; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - // The target config encodes Lehmer codes for each machine's tasks. - // Machine 0 has: 3m element tasks (task index 0 of each element job) - // + (m-1) separator tasks - // = 3m + (m-1) tasks total - // Machine 1 has: 3m element tasks (task index 1 of each element job) - // - // The config layout is [machine_0_lehmer..., machine_1_lehmer...]. - // machine_0_lehmer has length (3m + m - 1) = 4m - 1. - // - // We decode machine 0's ordering to find which group each element - // belongs to: elements between separators k-1 and k form group k. - - let num_elem = self.num_elements; - let m = self.num_groups; - - // Number of tasks on machine 0: element tasks + separator tasks - let machine0_len = num_elem + (m - 1); - - // Decode machine 0 Lehmer code - let machine0_lehmer = &target_solution[..machine0_len]; - let machine0_order = crate::models::misc::decode_lehmer(machine0_lehmer, machine0_len) - .expect("valid Lehmer code for machine 0"); - - // Task IDs on machine 0: - // - Element job i contributes task at flat index 2*i (first task of job i). - // - Separator job k contributes task at flat index 2*num_elem + k. - // - // Build mapping: flat task ID -> element index or separator marker. - let separator_task_ids: Vec = (0..m - 1).map(|k| 2 * num_elem + k).collect(); - - // machine0_order gives the order of task indices assigned to machine 0. - // The flatten_tasks() in JobShopScheduling assigns IDs sequentially: - // job 0 tasks get ids [0, 1], job 1 tasks get [2, 3], ... - // Element job i (2 tasks): ids [2*i, 2*i+1] - // Separator job k (1 task): id [2*num_elem + k] - // - // Machine 0 tasks are: element task 2*i (for i in 0..num_elem) and - // separator task 2*num_elem+k (for k in 0..m-1). - // Machine 1 tasks are: element task 2*i+1 (for i in 0..num_elem). - // - // The machine_task_ids for machine 0 are ordered by job index (since - // flatten_tasks iterates jobs in order): [0, 2, 4, ..., 2*(num_elem-1), - // 2*num_elem, 2*num_elem+1, ...]. - // - // machine0_order[j] gives the j-th machine-local index in the Lehmer - // permutation, which maps to machine_task_ids[machine0_order[j]]. - - // Build the machine 0 task id list in the same order as flatten_tasks - let mut machine0_task_ids: Vec = Vec::with_capacity(machine0_len); - for i in 0..num_elem { - machine0_task_ids.push(2 * i); // element job i, task 0 (on machine 0) - } - for k in 0..m - 1 { - machine0_task_ids.push(2 * num_elem + k); // separator job k - } - - // The actual ordering of tasks on machine 0: - let ordered_task_ids: Vec = machine0_order - .iter() - .map(|&local_idx| machine0_task_ids[local_idx]) - .collect(); - - // Now assign groups: walk through ordered_task_ids, incrementing group - // at each separator. - let mut config = vec![0usize; num_elem]; - let mut current_group = 0usize; - - for &task_id in &ordered_task_ids { - if separator_task_ids.contains(&task_id) { - current_group += 1; - } else { - // This is an element task with flat id 2*i => element i - let element_index = task_id / 2; - config[element_index] = current_group; - } - } - - config - } -} - -#[reduction(overhead = { - num_jobs = "num_elements + num_groups - 1", - num_tasks = "2 * num_elements + num_groups - 1", -})] -impl ReduceTo for ThreePartition { - type Result = ReductionThreePartitionToJSS; - - fn reduce_to(&self) -> Self::Result { - let num_elements = self.num_elements(); - let m = self.num_groups(); - let bound = self.bound(); - let l = ReductionThreePartitionToJSS::separator_length(m, bound); - let threshold = ReductionThreePartitionToJSS::compute_threshold(m, bound); - - // Build jobs - let mut jobs: Vec> = Vec::with_capacity(num_elements + m - 1); - - // Element jobs: 2 tasks each, one on each processor - for &size in self.sizes() { - jobs.push(vec![(0, size), (1, size)]); - } - - // Separator jobs: 1 task each, on processor 0 - for _ in 0..m.saturating_sub(1) { - jobs.push(vec![(0, l)]); - } - - ReductionThreePartitionToJSS { - target: JobShopScheduling::new(2, jobs), - num_elements, - num_groups: m, - threshold, - } - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "threepartition_to_jobshopscheduling", - build: || { - // m=1: sizes [4, 5, 6], bound=15, one group - // 3 element jobs, 0 separators => 3 jobs, 6 tasks - // All elements go to group 0: config = [0, 0, 0] - let source = ThreePartition::new(vec![4, 5, 6], 15); - let _reduction = ReduceTo::::reduce_to(&source); - - // For m=1, any ordering works. Use identity ordering on both machines. - // Machine 0: 3 tasks => Lehmer [0, 0, 0] - // Machine 1: 3 tasks => Lehmer [0, 0, 0] - let target_config = vec![0, 0, 0, 0, 0, 0]; - - crate::example_db::specs::rule_example_with_witness::<_, JobShopScheduling>( - source, - SolutionPair { - source_config: vec![0, 0, 0], - target_config, - }, - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/threepartition_jobshopscheduling.rs"] -mod tests; diff --git a/src/rules/threepartition_sequencingtominimizeweightedtardiness.rs b/src/rules/threepartition_sequencingtominimizeweightedtardiness.rs deleted file mode 100644 index 66344236..00000000 --- a/src/rules/threepartition_sequencingtominimizeweightedtardiness.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! Reduction from ThreePartition to SequencingToMinimizeWeightedTardiness. -//! -//! Given a 3-PARTITION instance with 3m elements, bound B, and sizes s(a_i) -//! with B/4 < s(a_i) < B/2 and total sum = mB, construct a weighted tardiness -//! scheduling instance using the filler-task approach (Garey & Johnson, A5.1). -//! -//! - 3m element tasks: length = s(a_i), weight = 1, deadline = mB + (m-1) -//! - (m-1) filler tasks: length = 1, weight = mB + 1, deadline = (j+1)B + (j+1) -//! - Bound K = 0 -//! -//! Filler weights force zero tardiness, creating m slots of width B separated -//! by unit gaps. Exactly 3 element tasks must fill each slot. - -use crate::models::misc::{SequencingToMinimizeWeightedTardiness, ThreePartition}; -use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; - -/// Result of reducing ThreePartition to SequencingToMinimizeWeightedTardiness. -#[derive(Debug, Clone)] -pub struct ReductionThreePartitionToSMWT { - target: SequencingToMinimizeWeightedTardiness, - /// Number of element tasks (3m) — indices 0..num_elements are element tasks, - /// indices num_elements.. are filler tasks. - num_elements: usize, -} - -impl ReductionResult for ReductionThreePartitionToSMWT { - type Source = ThreePartition; - type Target = SequencingToMinimizeWeightedTardiness; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - /// Extract a ThreePartition group assignment from a target Lehmer-code solution. - /// - /// Decode the Lehmer code into a permutation, then count how many filler - /// tasks have been seen before each element task. The filler count gives - /// the group (slot) index for that element. - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - let n = self.target.num_tasks(); - let schedule = crate::models::misc::decode_lehmer(target_solution, n) - .expect("target solution must be a valid Lehmer code"); - - let mut assignment = vec![0usize; self.num_elements]; - let mut filler_count = 0usize; - - for &job in &schedule { - if job < self.num_elements { - // Element task — assign to current group (= number of fillers seen so far) - assignment[job] = filler_count; - } else { - // Filler task — advance to next group - filler_count += 1; - } - } - - assignment - } -} - -#[reduction(overhead = { - num_tasks = "num_elements + num_groups - 1", -})] -impl ReduceTo for ThreePartition { - type Result = ReductionThreePartitionToSMWT; - - fn reduce_to(&self) -> Self::Result { - let m = self.num_groups(); - let b = self.bound(); - let n = self.num_elements(); - let horizon = (m as u64) * b + (m as u64 - 1); - let filler_weight = (m as u64) * b + 1; - - let total_tasks = n + m.saturating_sub(1); - let mut lengths = Vec::with_capacity(total_tasks); - let mut weights = Vec::with_capacity(total_tasks); - let mut deadlines = Vec::with_capacity(total_tasks); - - // Element tasks: length = s(a_i), weight = 1, deadline = horizon - for &size in self.sizes() { - lengths.push(size); - weights.push(1); - deadlines.push(horizon); - } - - // Filler tasks: length = 1, weight = mB+1, deadline = (j+1)*B + (j+1) - for j in 0..m.saturating_sub(1) { - lengths.push(1); - weights.push(filler_weight); - let deadline = ((j + 1) as u64) * b + (j + 1) as u64; - deadlines.push(deadline); - } - - ReductionThreePartitionToSMWT { - target: SequencingToMinimizeWeightedTardiness::new(lengths, weights, deadlines, 0), - num_elements: n, - } - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "threepartition_to_sequencingtominimizeweightedtardiness", - build: || { - // m=2, B=20, sizes=[7,7,6,7,7,6], sum=40=2*20 - // B/4=5, B/2=10 => all sizes strictly between 5 and 10 - // Partition: {7,7,6} in slot 0 and {7,7,6} in slot 1 - // Schedule: t0(7) t1(7) t2(6) f0(1) t3(7) t4(7) t5(6) - // Permutation: [0,1,2,6,3,4,5] - // Lehmer for [0,1,2,6,3,4,5]: - // pos 0: job 0 in [0,1,2,3,4,5,6] -> index 0 - // pos 1: job 1 in [1,2,3,4,5,6] -> index 0 - // pos 2: job 2 in [2,3,4,5,6] -> index 0 - // pos 3: job 6 in [3,4,5,6] -> index 3 - // pos 4: job 3 in [3,4,5] -> index 0 - // pos 5: job 4 in [4,5] -> index 0 - // pos 6: job 5 in [5] -> index 0 - crate::example_db::specs::rule_example_with_witness::< - _, - SequencingToMinimizeWeightedTardiness, - >( - ThreePartition::new(vec![7, 7, 6, 7, 7, 6], 20), - SolutionPair { - source_config: vec![0, 0, 0, 1, 1, 1], - target_config: vec![0, 0, 0, 3, 0, 0, 0], - }, - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/threepartition_sequencingtominimizeweightedtardiness.rs"] -mod tests; diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 536f999a..43dc121f 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -836,21 +836,6 @@ fn test_find_rule_example_ksatisfiability_to_minimumvertexcover() { assert_eq!(example.target.problem, "MinimumVertexCover"); } -#[test] -fn test_find_rule_example_partition_to_sequencingwithinintervals() { - let source = ProblemRef { - name: "Partition".to_string(), - variant: BTreeMap::new(), - }; - let target = ProblemRef { - name: "SequencingWithinIntervals".to_string(), - variant: BTreeMap::new(), - }; - let example = find_rule_example(&source, &target).unwrap(); - assert_eq!(example.source.problem, "Partition"); - assert_eq!(example.target.problem, "SequencingWithinIntervals"); -} - #[test] fn test_find_rule_example_minimumvertexcover_to_minimumfeedbackarcset() { let source = ProblemRef { @@ -950,24 +935,6 @@ fn test_find_rule_example_hamiltoniancircuit_to_ruralpostman() { assert_eq!(example.target.problem, "RuralPostman"); } -#[test] -fn test_find_rule_example_partition_to_shortestweightconstrainedpath() { - let source = ProblemRef { - name: "Partition".to_string(), - variant: BTreeMap::new(), - }; - let target = ProblemRef { - name: "ShortestWeightConstrainedPath".to_string(), - variant: BTreeMap::from([ - ("graph".to_string(), "SimpleGraph".to_string()), - ("weight".to_string(), "i32".to_string()), - ]), - }; - let example = find_rule_example(&source, &target).unwrap(); - assert_eq!(example.source.problem, "Partition"); - assert_eq!(example.target.problem, "ShortestWeightConstrainedPath"); -} - #[test] fn test_find_rule_example_maximumindependentset_to_integralflowbundles() { let source = ProblemRef { @@ -1001,21 +968,6 @@ fn test_find_rule_example_hamiltoniancircuit_to_quadraticassignment() { assert_eq!(example.target.problem, "QuadraticAssignment"); } -#[test] -fn test_find_rule_example_hamiltonianpath_to_consecutiveonessubmatrix() { - let source = ProblemRef { - name: "HamiltonianPath".to_string(), - variant: BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]), - }; - let target = ProblemRef { - name: "ConsecutiveOnesSubmatrix".to_string(), - variant: BTreeMap::new(), - }; - let example = find_rule_example(&source, &target).unwrap(); - assert_eq!(example.source.problem, "HamiltonianPath"); - assert_eq!(example.target.problem, "ConsecutiveOnesSubmatrix"); -} - // PR #804 rules #[test] @@ -1120,21 +1072,6 @@ fn test_find_rule_example_rootedtreearrangement_to_rootedtreestorageassignment() assert_eq!(example.target.problem, "RootedTreeStorageAssignment"); } -#[test] -fn test_find_rule_example_subsetsum_to_capacityassignment() { - let source = ProblemRef { - name: "SubsetSum".to_string(), - variant: BTreeMap::new(), - }; - let target = ProblemRef { - name: "CapacityAssignment".to_string(), - variant: BTreeMap::new(), - }; - let example = find_rule_example(&source, &target).unwrap(); - assert_eq!(example.source.problem, "SubsetSum"); - assert_eq!(example.target.problem, "CapacityAssignment"); -} - #[test] fn test_find_rule_example_longestcommonsubsequence_to_maximumindependentset() { let source = ProblemRef { @@ -1302,54 +1239,6 @@ fn test_find_rule_example_threepartition_to_sequencingwithreleasetimesanddeadlin ); } -#[test] -fn test_find_rule_example_threepartition_to_sequencingtominimizeweightedtardiness() { - let source = ProblemRef { - name: "ThreePartition".to_string(), - variant: BTreeMap::new(), - }; - let target = ProblemRef { - name: "SequencingToMinimizeWeightedTardiness".to_string(), - variant: BTreeMap::new(), - }; - let example = find_rule_example(&source, &target).unwrap(); - assert_eq!(example.source.problem, "ThreePartition"); - assert_eq!( - example.target.problem, - "SequencingToMinimizeWeightedTardiness" - ); -} - -#[test] -fn test_find_rule_example_threepartition_to_flowshopscheduling() { - let source = ProblemRef { - name: "ThreePartition".to_string(), - variant: BTreeMap::new(), - }; - let target = ProblemRef { - name: "FlowShopScheduling".to_string(), - variant: BTreeMap::new(), - }; - let example = find_rule_example(&source, &target).unwrap(); - assert_eq!(example.source.problem, "ThreePartition"); - assert_eq!(example.target.problem, "FlowShopScheduling"); -} - -#[test] -fn test_find_rule_example_threepartition_to_jobshopscheduling() { - let source = ProblemRef { - name: "ThreePartition".to_string(), - variant: BTreeMap::new(), - }; - let target = ProblemRef { - name: "JobShopScheduling".to_string(), - variant: BTreeMap::new(), - }; - let example = find_rule_example(&source, &target).unwrap(); - assert_eq!(example.source.problem, "ThreePartition"); - assert_eq!(example.target.problem, "JobShopScheduling"); -} - #[test] fn test_find_rule_example_maxcut_to_minimumcutintoboundedsets() { let source = ProblemRef { diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index ff32ddb6..399a861a 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -424,7 +424,7 @@ fn test_3sat_to_mis_triangular_overhead() { assert_eq!(edges[2].get("num_edges").unwrap().eval(&test_size), 36.0); // Edge 3: MIS{SimpleGraph,One} → MIS{TriangularSubgraph,i32} - // num_vertices = num_vertices^2, num_edges = num_vertices^2 + // num_vertices = num_vertices², num_edges = num_vertices² assert_eq!( edges[3].get("num_vertices").unwrap().eval(&test_size), 100.0 @@ -441,9 +441,10 @@ fn test_3sat_to_mis_triangular_overhead() { // // Composed: num_vertices = L², num_edges = L² let composed = graph.compose_path_overhead(&path); - // Evaluate composed at input: L=6, so L^2=36 + // Evaluate composed at input: L=6, so L²=36 assert_eq!(composed.get("num_vertices").unwrap().eval(&test_size), 36.0); - assert_eq!(composed.get("num_edges").unwrap().eval(&test_size), 36.0); + assert_eq!(composed.get("num_edges").unwrap().eval(&test_size), 36.0 + ); } // ---- k-neighbor BFS ---- diff --git a/src/unit_tests/rules/exactcoverby3sets_minimumweightsolutiontolinearequations.rs b/src/unit_tests/rules/exactcoverby3sets_minimumweightsolutiontolinearequations.rs deleted file mode 100644 index b6e98c9f..00000000 --- a/src/unit_tests/rules/exactcoverby3sets_minimumweightsolutiontolinearequations.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::models::algebraic::MinimumWeightSolutionToLinearEquations; -use crate::models::set::ExactCoverBy3Sets; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; -use crate::rules::{ReduceTo, ReductionResult}; - -#[test] -fn test_exactcoverby3sets_to_minimumweightsolutiontolinearequations_closed_loop() { - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); - let reduction = ReduceTo::::reduce_to(&source); - - assert_satisfaction_round_trip_from_satisfaction_target( - &source, - &reduction, - "ExactCoverBy3Sets -> MinimumWeightSolutionToLinearEquations closed loop", - ); -} - -#[test] -fn test_exactcoverby3sets_to_minimumweightsolutiontolinearequations_structure() { - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - assert_eq!( - target.matrix(), - &[ - vec![1, 0, 1], - vec![1, 0, 0], - vec![1, 0, 0], - vec![0, 1, 1], - vec![0, 1, 1], - vec![0, 1, 0], - ] - ); - assert_eq!(target.rhs(), &[1, 1, 1, 1, 1, 1]); - assert_eq!(target.num_variables(), 3); - assert_eq!(target.num_equations(), 6); -} - -#[test] -fn test_exactcoverby3sets_to_minimumweightsolutiontolinearequations_extract_solution_is_identity() { - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); - let reduction = ReduceTo::::reduce_to(&source); - - assert_eq!(reduction.extract_solution(&[1, 0, 1]), vec![1, 0, 1]); -} diff --git a/src/unit_tests/rules/hamiltonianpath_consecutiveonessubmatrix.rs b/src/unit_tests/rules/hamiltonianpath_consecutiveonessubmatrix.rs deleted file mode 100644 index 4ee5d323..00000000 --- a/src/unit_tests/rules/hamiltonianpath_consecutiveonessubmatrix.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::models::algebraic::ConsecutiveOnesSubmatrix; -use crate::models::graph::HamiltonianPath; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; -use crate::rules::ReduceTo; -use crate::rules::ReductionResult; -use crate::solvers::BruteForce; -use crate::topology::SimpleGraph; -use crate::Problem; - -/// Helper: build a path graph 0-1-2-3 (has HP). -fn path4() -> HamiltonianPath { - HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])) -} - -#[test] -fn test_hamiltonianpath_to_consecutiveonessubmatrix_closed_loop() { - let source = path4(); - let reduction = ReduceTo::::reduce_to(&source); - - assert_satisfaction_round_trip_from_satisfaction_target( - &source, - &reduction, - "HamiltonianPath -> ConsecutiveOnesSubmatrix", - ); -} - -#[test] -fn test_hamiltonianpath_to_consecutiveonessubmatrix_structure() { - let source = path4(); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - // 4 vertices -> 4 rows - assert_eq!(target.num_rows(), 4); - // 3 edges -> 3 columns - assert_eq!(target.num_cols(), 3); - // K = n - 1 = 3 - assert_eq!(target.bound(), 3); - - // Check incidence matrix structure: - // Edge 0: (0,1), Edge 1: (1,2), Edge 2: (2,3) - let matrix = target.matrix(); - // Vertex 0 is endpoint of edge 0 only - assert_eq!(matrix[0], vec![true, false, false]); - // Vertex 1 is endpoint of edges 0 and 1 - assert_eq!(matrix[1], vec![true, true, false]); - // Vertex 2 is endpoint of edges 1 and 2 - assert_eq!(matrix[2], vec![false, true, true]); - // Vertex 3 is endpoint of edge 2 only - assert_eq!(matrix[3], vec![false, false, true]); -} - -#[test] -fn test_hamiltonianpath_to_consecutiveonessubmatrix_extract_solution() { - let source = path4(); - let reduction = ReduceTo::::reduce_to(&source); - - // Select all 3 edges (columns) — they form the Hamiltonian path. - let target_config = vec![1, 1, 1]; - let extracted = reduction.extract_solution(&target_config); - - assert_eq!(extracted.len(), 4); - assert!( - source.evaluate(&extracted).0, - "extracted solution must be a valid Hamiltonian path" - ); -} - -#[test] -fn test_hamiltonianpath_to_consecutiveonessubmatrix_no_path_few_edges() { - // Disconnected graph: 0-1, 2-3 (2 edges < n-1 = 3, no Hamiltonian path). - // The reduction detects m < n-1 and produces a Tucker unsatisfiable instance. - let source = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)])); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - let solver = BruteForce::new(); - let witness = solver.find_witness(target); - assert!( - witness.is_none(), - "disconnected graph with too few edges should be unsatisfiable" - ); -} - -#[test] -fn test_hamiltonianpath_to_consecutiveonessubmatrix_no_path_disconnected() { - // Two disjoint triangles: 6 vertices, 6 edges, no HP (disconnected). - let source = HamiltonianPath::new(SimpleGraph::new( - 6, - vec![(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5)], - )); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - assert_eq!(target.num_rows(), 6); - assert_eq!(target.num_cols(), 6); - assert_eq!(target.bound(), 5); - - let solver = BruteForce::new(); - let witness = solver.find_witness(target); - assert!( - witness.is_none(), - "two disjoint triangles should have no Hamiltonian path" - ); -} - -#[test] -fn test_hamiltonianpath_to_consecutiveonessubmatrix_triangle() { - // Triangle: 0-1, 1-2, 0-2 (has HP, e.g. 0-1-2) - let source = HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)])); - let reduction = ReduceTo::::reduce_to(&source); - - assert_satisfaction_round_trip_from_satisfaction_target( - &source, - &reduction, - "HamiltonianPath(triangle) -> ConsecutiveOnesSubmatrix", - ); -} - -#[test] -fn test_hamiltonianpath_to_consecutiveonessubmatrix_single_vertex() { - // Single vertex, no edges — trivially has HP (path of length 0). - let source = HamiltonianPath::new(SimpleGraph::new(1, vec![])); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - assert_eq!(target.num_rows(), 1); - assert_eq!(target.num_cols(), 0); - assert_eq!(target.bound(), 0); - - // K=0 is vacuously satisfiable; empty config selects 0 columns. - let solver = BruteForce::new(); - let witness = solver.find_witness(target); - assert!(witness.is_some(), "single vertex should be satisfiable"); -} - -#[test] -fn test_hamiltonianpath_to_consecutiveonessubmatrix_cycle5() { - // 5-cycle: 0-1-2-3-4-0 (has HP, e.g. 0-1-2-3-4) - let source = HamiltonianPath::new(SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4)], - )); - let reduction = ReduceTo::::reduce_to(&source); - - assert_eq!(reduction.target_problem().num_cols(), 5); - assert_eq!(reduction.target_problem().bound(), 4); - - assert_satisfaction_round_trip_from_satisfaction_target( - &source, - &reduction, - "HamiltonianPath(C5) -> ConsecutiveOnesSubmatrix", - ); -} diff --git a/src/unit_tests/rules/partition_sequencingwithinintervals.rs b/src/unit_tests/rules/partition_sequencingwithinintervals.rs deleted file mode 100644 index 700c3831..00000000 --- a/src/unit_tests/rules/partition_sequencingwithinintervals.rs +++ /dev/null @@ -1,140 +0,0 @@ -use super::*; -use crate::models::misc::Partition; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; -use crate::solvers::BruteForce; -use crate::traits::Problem; - -fn reduce_partition(sizes: &[u64]) -> (Partition, ReductionPartitionToSWI) { - let source = Partition::new(sizes.to_vec()); - let reduction = ReduceTo::::reduce_to(&source); - (source, reduction) -} - -fn assert_satisfiability_matches( - source: &Partition, - target: &SequencingWithinIntervals, - expected: bool, -) { - let solver = BruteForce::new(); - assert_eq!(solver.find_witness(source).is_some(), expected); - assert_eq!(solver.find_witness(target).is_some(), expected); -} - -#[test] -fn test_partition_to_sequencingwithinintervals_closed_loop() { - // sizes [1, 2, 3, 4], sum=10, partition: {2,3} and {1,4} - let (source, reduction) = reduce_partition(&[1, 2, 3, 4]); - - assert_satisfaction_round_trip_from_satisfaction_target( - &source, - &reduction, - "Partition -> SequencingWithinIntervals closed loop", - ); -} - -#[test] -fn test_partition_to_sequencingwithinintervals_structure() { - let (source, reduction) = reduce_partition(&[1, 2, 3, 4]); - let target = reduction.target_problem(); - - // n+1 tasks (4 regular + 1 enforcer) - assert_eq!(target.num_tasks(), source.num_elements() + 1); - - // Regular tasks: release=0, deadline=11, lengths=[1,2,3,4] - let n = source.num_elements(); - for i in 0..n { - assert_eq!(target.release_times()[i], 0); - assert_eq!(target.deadlines()[i], 11); // S+1 = 10+1 - } - assert_eq!(&target.lengths()[..n], source.sizes()); - - // Enforcer: release=5, deadline=6, length=1 - assert_eq!(target.release_times()[n], 5); - assert_eq!(target.deadlines()[n], 6); - assert_eq!(target.lengths()[n], 1); -} - -#[test] -fn test_partition_to_sequencingwithinintervals_odd_sum() { - // sum = 2+4+5 = 11 (odd), no valid partition - let (source, reduction) = reduce_partition(&[2, 4, 5]); - let target = reduction.target_problem(); - - // Enforcer: release=5, deadline=6, length=1 - assert_eq!(target.release_times()[3], 5); - assert_eq!(target.deadlines()[3], 6); - assert_eq!(target.lengths()[3], 1); - - // Source is infeasible (odd sum, no balanced partition). - // Note: Target may be feasible (unequal windows allow scheduling) - // but any extracted witness would not satisfy Partition (forward-only reduction). - let solver = BruteForce::new(); - assert!(solver.find_witness(&source).is_none()); -} - -#[test] -fn test_partition_to_sequencingwithinintervals_equal_elements() { - // [3, 3, 3, 3], sum=12, half=6 - let (source, reduction) = reduce_partition(&[3, 3, 3, 3]); - let target = reduction.target_problem(); - - assert_eq!(target.num_tasks(), 5); - assert_satisfiability_matches(&source, target, true); - - assert_satisfaction_round_trip_from_satisfaction_target( - &source, - &reduction, - "Partition -> SWI equal elements", - ); -} - -#[test] -fn test_partition_to_sequencingwithinintervals_solution_extraction() { - let (source, reduction) = reduce_partition(&[1, 2, 3, 4]); - let target = reduction.target_problem(); - - let solver = BruteForce::new(); - let target_solutions = solver.find_all_witnesses(target); - - for sol in &target_solutions { - let extracted = reduction.extract_solution(sol); - // Extracted config should have length = num_elements (no enforcer) - assert_eq!(extracted.len(), source.num_elements()); - // If the target solution is valid, extracted should satisfy source - let target_valid = target.evaluate(sol); - let source_valid = source.evaluate(&extracted); - if target_valid.0 { - assert!( - source_valid.0, - "Valid SWI solution should yield valid Partition" - ); - } - } -} - -#[test] -fn test_partition_to_sequencingwithinintervals_two_elements() { - // [5, 5], sum=10, half=5 - let (source, reduction) = reduce_partition(&[5, 5]); - let target = reduction.target_problem(); - - assert_eq!(target.num_tasks(), 3); - assert_satisfiability_matches(&source, target, true); - - assert_satisfaction_round_trip_from_satisfaction_target( - &source, - &reduction, - "Partition -> SWI two elements", - ); -} - -#[test] -fn test_partition_to_sequencingwithinintervals_single_element() { - // [4], sum=4, half=2 - // Not partitionable (only one element) - let (source, reduction) = reduce_partition(&[4]); - let target = reduction.target_problem(); - - assert_eq!(target.num_tasks(), 2); - assert_satisfiability_matches(&source, target, false); -} diff --git a/src/unit_tests/rules/partition_shortestweightconstrainedpath.rs b/src/unit_tests/rules/partition_shortestweightconstrainedpath.rs deleted file mode 100644 index 1310c577..00000000 --- a/src/unit_tests/rules/partition_shortestweightconstrainedpath.rs +++ /dev/null @@ -1,92 +0,0 @@ -use super::*; -use crate::models::graph::ShortestWeightConstrainedPath; -use crate::models::misc::Partition; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; -use crate::solvers::BruteForce; -use crate::topology::SimpleGraph; -use crate::traits::Problem; - -#[test] -fn test_partition_to_shortestweightconstrainedpath_closed_loop() { - let source = Partition::new(vec![3, 1, 1, 2, 2, 1]); - let reduction = ReduceTo::>::reduce_to(&source); - - assert_satisfaction_round_trip_from_optimization_target( - &source, - &reduction, - "Partition -> ShortestWeightConstrainedPath closed loop", - ); -} - -#[test] -fn test_partition_to_shortestweightconstrainedpath_structure() { - let source = Partition::new(vec![3, 1, 1, 2, 2, 1]); - let reduction = ReduceTo::>::reduce_to(&source); - let target = reduction.target_problem(); - - // n=6 elements → 7 vertices, 12 edges - assert_eq!(target.num_vertices(), 7); - assert_eq!(target.num_edges(), 12); - assert_eq!(target.source_vertex(), 0); - assert_eq!(target.target_vertex(), 6); - - // total_sum = 10, weight_bound = floor(10/2) + 6 = 11 - assert_eq!(*target.weight_bound(), 11); - - // Check edge lengths and weights for first layer (a_0 = 3): - // Include edge: length=4, weight=1; Exclude edge: length=1, weight=4 - assert_eq!(target.edge_lengths()[0], 4); // include - assert_eq!(target.edge_weights()[0], 1); - assert_eq!(target.edge_lengths()[1], 1); // exclude - assert_eq!(target.edge_weights()[1], 4); -} - -#[test] -fn test_partition_to_shortestweightconstrainedpath_unsatisfiable() { - // Odd total sum → no balanced partition exists - let source = Partition::new(vec![2, 4, 5]); - let reduction = ReduceTo::>::reduce_to(&source); - let target = reduction.target_problem(); - - // total_sum = 11, weight_bound = floor(11/2) + 3 = 8 - assert_eq!(*target.weight_bound(), 8); - - // The SWCP optimal path exists, but extracting it should not satisfy Partition. - let best = BruteForce::new() - .find_witness(target) - .expect("SWCP target should have an optimal solution"); - let extracted = reduction.extract_solution(&best); - assert!(!source.evaluate(&extracted)); -} - -#[test] -fn test_partition_to_shortestweightconstrainedpath_small() { - // Two elements: [3, 3] → balanced partition exists - let source = Partition::new(vec![3, 3]); - let reduction = ReduceTo::>::reduce_to(&source); - let target = reduction.target_problem(); - - assert_eq!(target.num_vertices(), 3); - assert_eq!(target.num_edges(), 4); - // total_sum = 6, weight_bound = 3 + 2 = 5 - assert_eq!(*target.weight_bound(), 5); - - assert_satisfaction_round_trip_from_optimization_target( - &source, - &reduction, - "Partition [3,3] -> SWCP", - ); -} - -#[test] -fn test_partition_to_shortestweightconstrainedpath_single_element() { - // Single element → no balanced partition (odd total) - let source = Partition::new(vec![4]); - let reduction = ReduceTo::>::reduce_to(&source); - let target = reduction.target_problem(); - - assert_eq!(target.num_vertices(), 2); - assert_eq!(target.num_edges(), 2); - // total_sum = 4, weight_bound = 2 + 1 = 3 - assert_eq!(*target.weight_bound(), 3); -} diff --git a/src/unit_tests/rules/sat_circuitsat.rs b/src/unit_tests/rules/sat_circuitsat.rs index 6fb021b0..77b27e20 100644 --- a/src/unit_tests/rules/sat_circuitsat.rs +++ b/src/unit_tests/rules/sat_circuitsat.rs @@ -65,3 +65,16 @@ fn test_sat_to_circuitsat_single_literal_clause() { let extracted = result.extract_solution(&target_solution); assert_eq!(extracted, vec![1, 1]); } + +#[test] +fn test_sat_to_circuitsat_unused_variables() { + // 5 variables but only x1 and x2 appear in clauses; x3..x5 are unused. + // Previously panicked because unused variables were missing from CircuitSAT. + let sat = Satisfiability::new(5, vec![CNFClause::new(vec![1, 2])]); + let result = ReduceTo::::reduce_to(&sat); + assert_satisfaction_round_trip_from_satisfaction_target( + &sat, + &result, + "SAT->CircuitSAT unused variables", + ); +} diff --git a/src/unit_tests/rules/subsetsum_capacityassignment.rs b/src/unit_tests/rules/subsetsum_capacityassignment.rs deleted file mode 100644 index fa23929b..00000000 --- a/src/unit_tests/rules/subsetsum_capacityassignment.rs +++ /dev/null @@ -1,116 +0,0 @@ -use super::*; -use crate::models::misc::{CapacityAssignment, SubsetSum}; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; -use crate::solvers::BruteForce; -use crate::traits::Problem; - -#[test] -fn test_subsetsum_to_capacityassignment_closed_loop() { - // YES instance: {3, 7, 1, 8, 2, 4}, target 11 → subset {3, 8} sums to 11 - let source = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); - let reduction = ReduceTo::::reduce_to(&source); - - assert_satisfaction_round_trip_from_optimization_target( - &source, - &reduction, - "SubsetSum -> CapacityAssignment closed loop", - ); -} - -#[test] -fn test_subsetsum_to_capacityassignment_structure() { - let source = SubsetSum::new(vec![3u32, 7, 1, 8, 4, 12], 15u32); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - // 6 elements → 6 links, 2 capacities - assert_eq!(target.num_links(), 6); - assert_eq!(target.num_capacities(), 2); - assert_eq!(target.capacities(), &[1, 2]); - - // Check cost/delay for first link (a_0 = 3): - // cost(c_0, low) = 0, cost(c_0, high) = 3 - assert_eq!(target.cost()[0], vec![0, 3]); - // delay(c_0, low) = 3, delay(c_0, high) = 0 - assert_eq!(target.delay()[0], vec![3, 0]); - - // Delay budget = S - B = 35 - 15 = 20 - assert_eq!(target.delay_budget(), 20); -} - -#[test] -fn test_subsetsum_to_capacityassignment_no_instance() { - // NO instance: {1, 5, 11, 6}, target 4 → no subset sums to 4 - let source = SubsetSum::new(vec![1u32, 5, 11, 6], 4u32); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - // S = 23, B = 4, delay_budget = 19 - assert_eq!(target.delay_budget(), 19); - - // The optimal CapacityAssignment cost should be > 4 (since no subset sums to 4) - let best = BruteForce::new() - .find_witness(target) - .expect("CapacityAssignment should have a feasible solution"); - let extracted = reduction.extract_solution(&best); - // The extracted config should NOT satisfy SubsetSum - assert!(!source.evaluate(&extracted)); -} - -#[test] -fn test_subsetsum_to_capacityassignment_small() { - // Two elements: {3, 3}, target 3 → subset {3} sums to 3 - let source = SubsetSum::new(vec![3u32, 3], 3u32); - let reduction = ReduceTo::::reduce_to(&source); - - assert_satisfaction_round_trip_from_optimization_target( - &source, - &reduction, - "SubsetSum [3,3] target 3 -> CapacityAssignment", - ); -} - -#[test] -fn test_subsetsum_to_capacityassignment_target_exceeds_sum() { - // target > sum(sizes): unsatisfiable, should not panic from underflow - let source = SubsetSum::new(vec![1u32, 2, 3], 100u32); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - // delay_budget = saturating_sub(6, 100) = 0 - assert_eq!(target.delay_budget(), 0); - - // No subset sums to 100, so source is unsatisfiable - let solver = BruteForce::new(); - let witness = solver.find_witness(target); - if let Some(config) = witness { - let extracted = reduction.extract_solution(&config); - assert!( - !source.evaluate(&extracted), - "source should be unsatisfiable" - ); - } -} - -#[test] -fn test_subsetsum_to_capacityassignment_monotonicity() { - // Verify cost non-decreasing and delay non-increasing for all links - let source = SubsetSum::new(vec![5u32, 10, 15], 20u32); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - for (link, cost_row) in target.cost().iter().enumerate() { - let cost_row: &[u64] = cost_row; - assert!( - cost_row.windows(2).all(|w| w[0] <= w[1]), - "cost row {link} must be non-decreasing" - ); - } - for (link, delay_row) in target.delay().iter().enumerate() { - let delay_row: &[u64] = delay_row; - assert!( - delay_row.windows(2).all(|w| w[0] >= w[1]), - "delay row {link} must be non-increasing" - ); - } -} diff --git a/src/unit_tests/rules/threepartition_flowshopscheduling.rs b/src/unit_tests/rules/threepartition_flowshopscheduling.rs deleted file mode 100644 index 6e45ae91..00000000 --- a/src/unit_tests/rules/threepartition_flowshopscheduling.rs +++ /dev/null @@ -1,185 +0,0 @@ -use super::*; -use crate::models::misc::ThreePartition; -use crate::solvers::BruteForce; -use crate::traits::Problem; - -fn reduce_three_partition( - sizes: Vec, - bound: u64, -) -> (ThreePartition, ReductionThreePartitionToFSS) { - let source = ThreePartition::new(sizes, bound); - let reduction = ReduceTo::::reduce_to(&source); - (source, reduction) -} - -/// Encode a job order (permutation) as a Lehmer code. -fn encode_lehmer(job_order: &[usize]) -> Vec { - let n = job_order.len(); - let mut available: Vec = (0..n).collect(); - let mut lehmer = Vec::with_capacity(n); - for &job in job_order { - let pos = available.iter().position(|&x| x == job).unwrap(); - lehmer.push(pos); - available.remove(pos); - } - lehmer -} - -#[test] -fn test_threepartition_to_flowshopscheduling_closed_loop() { - // ThreePartition: sizes [4, 5, 6, 4, 6, 5], bound=15, m=2 - // Valid partition: group 0 = {4,5,6} (indices 0,1,2), group 1 = {4,6,5} (indices 3,4,5) - let (source, reduction) = reduce_three_partition(vec![4, 5, 6, 4, 6, 5], 15); - let target = reduction.target_problem(); - - // Verify source is satisfiable - let solver = BruteForce::new(); - assert!( - solver.find_witness(&source).is_some(), - "Source 3-Partition should be satisfiable" - ); - - // Verify target is satisfiable - assert!( - solver.find_witness(target).is_some(), - "Target FlowShopScheduling should be satisfiable" - ); - - // Canonical ordering: [0,1,2, sep(6), 3,4,5] -- group 1 elements, separator, group 2 elements - let canonical_order = vec![0, 1, 2, 6, 3, 4, 5]; - let canonical_lehmer = encode_lehmer(&canonical_order); - assert_eq!(canonical_lehmer, vec![0, 0, 0, 3, 0, 0, 0]); - - // Verify canonical ordering satisfies the target - let target_value = target.evaluate(&canonical_lehmer); - assert!(target_value.0, "Canonical ordering should meet deadline"); - - // Extract and verify: elements before separator -> group 0, after -> group 1 - let extracted = reduction.extract_solution(&canonical_lehmer); - assert_eq!(extracted, vec![0, 0, 0, 1, 1, 1]); - assert!( - source.evaluate(&extracted).0, - "Extracted solution should be a valid 3-partition" - ); - - // Test another valid ordering: group 2 first, then group 1 - let alt_order = vec![3, 4, 5, 6, 0, 1, 2]; - let alt_lehmer = encode_lehmer(&alt_order); - let alt_value = target.evaluate(&alt_lehmer); - assert!( - alt_value.0, - "Alternative valid ordering should meet deadline" - ); - let alt_extracted = reduction.extract_solution(&alt_lehmer); - assert_eq!(alt_extracted, vec![1, 1, 1, 0, 0, 0]); - assert!( - source.evaluate(&alt_extracted).0, - "Alternative extraction should be a valid 3-partition" - ); - - // Verify all valid-partition orderings extract correctly - // A valid partition groups elements into triples summing to B=15. - // For this instance: one triple from each of {4,5,6} values. - // Elements by value: val 4 at {0,3}, val 5 at {1,5}, val 6 at {2,4} - let target_witnesses = solver.find_all_witnesses(target); - let mut valid_extraction_count = 0; - for w in &target_witnesses { - let extracted = reduction.extract_solution(w); - if source.evaluate(&extracted).0 { - valid_extraction_count += 1; - } - } - assert!( - valid_extraction_count > 0, - "At least some target witnesses should extract to valid source solutions" - ); -} - -#[test] -fn test_threepartition_to_flowshopscheduling_structure() { - let (source, reduction) = reduce_three_partition(vec![4, 5, 6, 4, 6, 5], 15); - let target = reduction.target_problem(); - - // 3 machines - assert_eq!(target.num_processors(), 3); - // 6 element jobs + 1 separator = 7 total jobs - assert_eq!(target.num_jobs(), 7); - assert_eq!( - target.num_jobs(), - source.num_elements() + source.num_groups() - 1 - ); - - // Check element job task lengths - let task_lengths = target.task_lengths(); - for (i, tasks) in task_lengths.iter().enumerate().take(6) { - let size = source.sizes()[i]; - assert_eq!(*tasks, vec![size, size, size]); - } - - // Check separator job task lengths: [0, L, 0] where L = m*B+1 = 2*15+1 = 31 - let big_l = 2 * 15 + 1; - assert_eq!(task_lengths[6], vec![0, big_l, 0]); - - // Deadline should be positive - assert!(target.deadline() > 0); -} - -#[test] -fn test_threepartition_to_flowshopscheduling_solution_extraction() { - let (source, reduction) = reduce_three_partition(vec![4, 5, 6, 4, 6, 5], 15); - - // Test extraction for canonical orderings where elements are properly grouped - // Ordering: indices 0,1,2 (group 0), separator 6, indices 3,4,5 (group 1) - let lehmer = encode_lehmer(&[0, 1, 2, 6, 3, 4, 5]); - let extracted = reduction.extract_solution(&lehmer); - assert_eq!(extracted.len(), source.num_elements()); - assert_eq!(extracted, vec![0, 0, 0, 1, 1, 1]); - assert!(source.evaluate(&extracted).0); - - // Different valid grouping: {0,4,5}=group 0, {1,2,3}=group 1 - // 4+6+5=15 and 5+6+4=15 - let lehmer2 = encode_lehmer(&[0, 4, 5, 6, 1, 2, 3]); - let extracted2 = reduction.extract_solution(&lehmer2); - assert_eq!(extracted2[0], 0); // element 0 in group 0 - assert_eq!(extracted2[4], 0); // element 4 in group 0 - assert_eq!(extracted2[5], 0); // element 5 in group 0 - assert_eq!(extracted2[1], 1); // element 1 in group 1 - assert_eq!(extracted2[2], 1); // element 2 in group 1 - assert_eq!(extracted2[3], 1); // element 3 in group 1 - assert!(source.evaluate(&extracted2).0); -} - -#[test] -fn test_threepartition_to_flowshopscheduling_dims() { - let (_source, reduction) = reduce_three_partition(vec![4, 5, 6, 4, 6, 5], 15); - let target = reduction.target_problem(); - - // Lehmer code dims: [7, 6, 5, 4, 3, 2, 1] - let dims = target.dims(); - assert_eq!(dims, vec![7, 6, 5, 4, 3, 2, 1]); -} - -#[test] -fn test_threepartition_to_flowshopscheduling_canonical_makespan() { - let (source, reduction) = reduce_three_partition(vec![4, 5, 6, 4, 6, 5], 15); - let target = reduction.target_problem(); - - // The canonical ordering should achieve exactly the deadline - let canonical_order = vec![0, 1, 2, 6, 3, 4, 5]; - let makespan = target.compute_makespan(&canonical_order); - assert_eq!(makespan, target.deadline()); - - // Verify the deadline computation: - // m=2, B=15, L=31 - // Canonical schedule on M2: first element starts at s(a_0)=4, - // group1 takes B=15, separator takes L=31, group2 takes B=15 - // M2 finishes at 4 + 15 + 31 + 15 = 65 - // M3 lags behind M2 by one element's processing time at the end - assert!(target.deadline() > 0); - - // The number of elements + groups - 1 should equal num_jobs - assert_eq!( - source.num_elements() + source.num_groups() - 1, - target.num_jobs() - ); -} diff --git a/src/unit_tests/rules/threepartition_jobshopscheduling.rs b/src/unit_tests/rules/threepartition_jobshopscheduling.rs deleted file mode 100644 index 2cbb12ab..00000000 --- a/src/unit_tests/rules/threepartition_jobshopscheduling.rs +++ /dev/null @@ -1,218 +0,0 @@ -use super::*; -use crate::models::misc::ThreePartition; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; -use crate::traits::Problem; -use crate::types::Min; - -/// m=1: 3 elements, bound=15, sizes=[4, 5, 6]. Only 1 group, no separators. -/// 3 jobs, 6 tasks. dims = [3,2,1,3,2,1] => 36 configs. Fast for brute force. -#[test] -fn test_threepartition_to_jobshopscheduling_closed_loop() { - let source = ThreePartition::new(vec![4, 5, 6], 15); - let reduction = ReduceTo::::reduce_to(&source); - - assert_satisfaction_round_trip_from_optimization_target( - &source, - &reduction, - "ThreePartition -> JobShopScheduling closed loop (m=1)", - ); -} - -/// Verify the target problem structure for m=1. -#[test] -fn test_threepartition_to_jss_structure_m1() { - let source = ThreePartition::new(vec![4, 5, 6], 15); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - // m=1: 3 element jobs, 0 separator jobs - assert_eq!(target.num_processors(), 2); - assert_eq!(target.num_jobs(), 3); - assert_eq!(target.num_tasks(), 6); - - // Each element job has 2 tasks - for (i, job) in target.jobs().iter().enumerate() { - assert_eq!(job.len(), 2, "element job {i} should have 2 tasks"); - assert_eq!( - job[0].0, 0, - "element job {i} task 0 should be on processor 0" - ); - assert_eq!( - job[1].0, 1, - "element job {i} task 1 should be on processor 1" - ); - // Tasks have equal length = source size - assert_eq!(job[0].1, job[1].1); - } - - let sizes = source.sizes(); - assert_eq!(target.jobs()[0][0].1, sizes[0]); - assert_eq!(target.jobs()[1][0].1, sizes[1]); - assert_eq!(target.jobs()[2][0].1, sizes[2]); -} - -/// Verify the target problem structure for m=2. -#[test] -fn test_threepartition_to_jss_structure_m2() { - // m=2: 6 elements, bound=20, sizes satisfy B/4 < s < B/2 - let source = ThreePartition::new(vec![6, 7, 7, 6, 8, 6], 20); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - // m=2: 6 element jobs + 1 separator job = 7 jobs - assert_eq!(target.num_processors(), 2); - assert_eq!(target.num_jobs(), 7); // 6 + 2 - 1 - assert_eq!(target.num_tasks(), 13); // 2*6 + 2 - 1 - - // Element jobs (0..5): 2 tasks each - for i in 0..6 { - let job = &target.jobs()[i]; - assert_eq!(job.len(), 2); - assert_eq!(job[0].0, 0); - assert_eq!(job[1].0, 1); - } - - // Separator job (index 6): 1 task on processor 0 - let separator = &target.jobs()[6]; - assert_eq!(separator.len(), 1); - assert_eq!(separator[0].0, 0); - - // Separator length L = m*B + 1 = 2*20 + 1 = 41 - assert_eq!(separator[0].1, 41); -} - -/// Verify that the threshold is correct for m=2. -#[test] -fn test_threepartition_to_jss_threshold_m2() { - let source = ThreePartition::new(vec![6, 7, 7, 6, 8, 6], 20); - let reduction = ReduceTo::::reduce_to(&source); - - // D = m*B + (m-1)*L = 2*20 + 1*41 = 81 - assert_eq!(reduction.threshold(), 81); -} - -/// For m=2, manually construct a valid schedule config and verify extraction. -#[test] -fn test_threepartition_to_jss_extraction_m2() { - // sizes = [6, 7, 7, 6, 8, 6], bound = 20, m = 2 - // Valid partition: group 0 = {7, 7, 6} (indices 1,2,3), group 1 = {6, 8, 6} (indices 0,4,5) - let source = ThreePartition::new(vec![6, 7, 7, 6, 8, 6], 20); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - // Machine 0 tasks (local indices 0..6): - // local 0 -> element 0 (task id 0) - // local 1 -> element 1 (task id 2) - // local 2 -> element 2 (task id 4) - // local 3 -> element 3 (task id 6) - // local 4 -> element 4 (task id 8) - // local 5 -> element 5 (task id 10) - // local 6 -> separator 0 (task id 12) - // - // We want machine 0 order: elem1, elem2, elem3, separator0, elem0, elem4, elem5 - // That's local indices: [1, 2, 3, 6, 0, 4, 5] - // - // Lehmer encoding of permutation [1, 2, 3, 6, 0, 4, 5]: - // available = [0,1,2,3,4,5,6] - // pick 1 from [0,1,2,3,4,5,6] -> index 1, remaining [0,2,3,4,5,6] - // pick 2 from [0,2,3,4,5,6] -> index 1, remaining [0,3,4,5,6] - // pick 3 from [0,3,4,5,6] -> index 1, remaining [0,4,5,6] - // pick 6 from [0,4,5,6] -> index 3, remaining [0,4,5] - // pick 0 from [0,4,5] -> index 0, remaining [4,5] - // pick 4 from [4,5] -> index 0, remaining [5] - // pick 5 from [5] -> index 0 - let machine0_lehmer = vec![1, 1, 1, 3, 0, 0, 0]; - - // Machine 1 tasks (local indices 0..5): - // local 0 -> element 0 (task id 1) - // local 1 -> element 1 (task id 3) - // local 2 -> element 2 (task id 5) - // local 3 -> element 3 (task id 7) - // local 4 -> element 4 (task id 9) - // local 5 -> element 5 (task id 11) - // - // Any valid ordering; use identity: [0,1,2,3,4,5] => Lehmer [0,0,0,0,0,0] - let machine1_lehmer = vec![0, 0, 0, 0, 0, 0]; - - let mut config = machine0_lehmer; - config.extend(machine1_lehmer); - - // Verify the schedule produces a valid makespan - let value = target.evaluate(&config); - assert!(value.0.is_some(), "config should produce a valid schedule"); - - // Extract and verify source solution - let source_config = reduction.extract_solution(&config); - assert_eq!(source_config.len(), 6); - - // Elements 1,2,3 should be in group 0 (before separator) - // Elements 0,4,5 should be in group 1 (after separator) - assert_eq!(source_config[1], 0); // element 1 in group 0 - assert_eq!(source_config[2], 0); // element 2 in group 0 - assert_eq!(source_config[3], 0); // element 3 in group 0 - assert_eq!(source_config[0], 1); // element 0 in group 1 - assert_eq!(source_config[4], 1); // element 4 in group 1 - assert_eq!(source_config[5], 1); // element 5 in group 1 - - // Verify the extracted solution is a valid 3-partition - let source_value = source.evaluate(&source_config); - assert!( - source_value.0, - "extracted solution should be a valid 3-partition" - ); -} - -/// For m=1, verify that optimal makespan equals the sum of all sizes. -#[test] -fn test_threepartition_to_jss_makespan_m1() { - let source = ThreePartition::new(vec![4, 5, 6], 15); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - // With m=1, no separators, threshold = 1*15 + 0 = 15 - assert_eq!(reduction.threshold(), 15); - - // Identity ordering: Lehmer [0,0,0] for machine 0, [0,0,0] for machine 1 - let config = vec![0, 0, 0, 0, 0, 0]; - let value = target.evaluate(&config); - - // Tasks on machine 0: 4, 5, 6 (total 15) - // Tasks on machine 1: must wait for respective machine 0 tasks - // Machine 0: [0,4], [4,9], [9,15] - // Machine 1: [4,8], [9,14], [15,21] - // Makespan = 21 - assert_eq!(value, Min(Some(21))); -} - -/// Verify overhead expressions are correct. -#[test] -fn test_threepartition_to_jss_overhead() { - let source = ThreePartition::new(vec![4, 5, 6], 15); - let reduction = ReduceTo::::reduce_to(&source); - let target = reduction.target_problem(); - - // num_jobs = num_elements + num_groups - 1 = 3 + 1 - 1 = 3 - assert_eq!( - target.num_jobs(), - source.num_elements() + source.num_groups() - 1 - ); - // num_tasks = 2 * num_elements + num_groups - 1 = 6 + 0 = 6 - assert_eq!( - target.num_tasks(), - 2 * source.num_elements() + source.num_groups() - 1 - ); - - // Also check for m=2 - let source2 = ThreePartition::new(vec![6, 7, 7, 6, 8, 6], 20); - let reduction2 = ReduceTo::::reduce_to(&source2); - let target2 = reduction2.target_problem(); - - assert_eq!( - target2.num_jobs(), - source2.num_elements() + source2.num_groups() - 1 - ); - assert_eq!( - target2.num_tasks(), - 2 * source2.num_elements() + source2.num_groups() - 1 - ); -} diff --git a/src/unit_tests/rules/threepartition_sequencingtominimizeweightedtardiness.rs b/src/unit_tests/rules/threepartition_sequencingtominimizeweightedtardiness.rs deleted file mode 100644 index dca72e3f..00000000 --- a/src/unit_tests/rules/threepartition_sequencingtominimizeweightedtardiness.rs +++ /dev/null @@ -1,176 +0,0 @@ -use super::*; -use crate::models::misc::{SequencingToMinimizeWeightedTardiness, ThreePartition}; -use crate::solvers::BruteForce; -use crate::traits::Problem; - -fn reduce(sizes: Vec, bound: u64) -> (ThreePartition, ReductionThreePartitionToSMWT) { - let source = ThreePartition::new(sizes, bound); - let reduction = ReduceTo::::reduce_to(&source); - (source, reduction) -} - -fn assert_satisfiability_matches( - source: &ThreePartition, - target: &SequencingToMinimizeWeightedTardiness, - expected: bool, -) { - let solver = BruteForce::new(); - assert_eq!( - solver.find_witness(source).is_some(), - expected, - "source satisfiability mismatch" - ); - assert_eq!( - solver.find_witness(target).is_some(), - expected, - "target satisfiability mismatch" - ); -} - -/// Verify the decision-level round trip: source is satisfiable iff target is, -/// and at least one target witness extracts to a valid source witness. -fn assert_decision_round_trip( - source: &ThreePartition, - reduction: &ReductionThreePartitionToSMWT, - context: &str, -) { - let solver = BruteForce::new(); - let target = reduction.target_problem(); - let source_sat = solver.find_witness(source).is_some(); - let target_witnesses = solver.find_all_witnesses(target); - let target_sat = !target_witnesses.is_empty(); - assert_eq!( - source_sat, target_sat, - "{context}: satisfiability mismatch (source={source_sat}, target={target_sat})" - ); - - if source_sat { - // At least one target witness must extract to a valid source witness - let found_valid = target_witnesses.iter().any(|tw| { - let extracted = reduction.extract_solution(tw); - source.evaluate(&extracted).0 - }); - assert!( - found_valid, - "{context}: no target witness extracted to a valid source solution" - ); - } -} - -#[test] -fn test_threepartition_to_sequencingtominimizeweightedtardiness_closed_loop() { - // m=2, B=20, sizes with B/4 < s < B/2 (i.e., 5 < s < 10) - // sizes: [7, 7, 6, 7, 7, 6], sum = 40 = 2*20 - // Valid partition: {7,7,6} and {7,7,6} - let (source, reduction) = reduce(vec![7, 7, 6, 7, 7, 6], 20); - - assert_decision_round_trip(&source, &reduction, "ThreePartition -> SMWT closed loop"); -} - -#[test] -fn test_threepartition_to_sequencingtominimizeweightedtardiness_structure() { - // m=2, B=20, 6 elements + 1 filler = 7 tasks - let (source, reduction) = reduce(vec![7, 7, 6, 7, 7, 6], 20); - let target = reduction.target_problem(); - - let m = source.num_groups(); - let b = source.bound(); - - // Total tasks: 3m + (m-1) = 6 + 1 = 7 - assert_eq!(target.num_tasks(), 7); - assert_eq!(target.num_tasks(), source.num_elements() + m - 1); - - // Element task lengths match source sizes - let lengths = target.lengths(); - for (len, &size) in lengths.iter().zip(source.sizes()) { - assert_eq!(*len, size); - } - - // Filler task length = 1 - for &len in &lengths[source.num_elements()..] { - assert_eq!(len, 1); - } - - // Element task weights = 1 - let weights = target.weights(); - for &w in &weights[..source.num_elements()] { - assert_eq!(w, 1); - } - - // Filler task weight = mB + 1 - let filler_weight = (m as u64) * b + 1; - for &w in &weights[source.num_elements()..] { - assert_eq!(w, filler_weight); - } - - // Element task deadlines = mB + (m-1) = horizon - let horizon = (m as u64) * b + (m as u64 - 1); - let deadlines = target.deadlines(); - for &d in &deadlines[..source.num_elements()] { - assert_eq!(d, horizon); - } - - // Filler deadlines: (j+1)*B + (j+1) - for (j, &d) in deadlines[source.num_elements()..].iter().enumerate() { - let expected = ((j + 1) as u64) * b + (j + 1) as u64; - assert_eq!(d, expected); - } - - // Bound = 0 - assert_eq!(target.bound(), 0); -} - -#[test] -fn test_threepartition_to_sequencingtominimizeweightedtardiness_m1() { - // m=1, B=20, 3 elements, no fillers - // sizes: [7, 7, 6], sum = 20 = 1*20 - let (source, reduction) = reduce(vec![7, 7, 6], 20); - let target = reduction.target_problem(); - - // 3 element tasks, 0 filler tasks - assert_eq!(target.num_tasks(), 3); - assert_eq!(target.bound(), 0); - - // All deadlines = 1*20 + 0 = 20 - for &d in target.deadlines() { - assert_eq!(d, 20); - } - - // m=1: any permutation of 3 tasks should satisfy (sum = B, all fit by deadline) - assert_decision_round_trip(&source, &reduction, "ThreePartition -> SMWT m=1"); -} - -#[test] -fn test_threepartition_to_sequencingtominimizeweightedtardiness_solution_extraction() { - let (source, reduction) = reduce(vec![7, 7, 6, 7, 7, 6], 20); - let target = reduction.target_problem(); - - let solver = BruteForce::new(); - let target_solutions = solver.find_all_witnesses(target); - assert!(!target_solutions.is_empty(), "target should be satisfiable"); - - // Verify that at least one target solution extracts to a valid source solution - let mut found_valid = false; - for sol in &target_solutions { - let extracted = reduction.extract_solution(sol); - assert_eq!(extracted.len(), source.num_elements()); - if source.evaluate(&extracted).0 { - found_valid = true; - } - } - assert!( - found_valid, - "at least one extraction must yield a valid 3-partition" - ); -} - -#[test] -fn test_threepartition_to_sequencingtominimizeweightedtardiness_satisfiability_match() { - // Feasible instance: m=2, B=20 - let (source, reduction) = reduce(vec![7, 7, 6, 7, 7, 6], 20); - assert_satisfiability_matches(&source, reduction.target_problem(), true); - - // m=1: always feasible (3 elements sum to B) - let (source1, reduction1) = reduce(vec![7, 7, 6], 20); - assert_satisfiability_matches(&source1, reduction1.target_problem(), true); -}