diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index f5a89d3bc..4b87601c3 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -5597,7 +5597,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let nc = x.instance.n let k = x.instance.k let A = x.instance.matrix - let dH = metric-value(x.optimal_value) + let fs = metric-value(x.optimal_value) // Decode B and C from optimal config // Config layout: B is m*k values, then C is k*n values let cfg = x.optimal_config @@ -5609,11 +5609,11 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let fmt-mat(m) = math.mat(..m.map(row => row.map(v => $#v$))) [ #problem-def("BMF")[ - Given an $m times n$ boolean matrix $A$ and rank $k$, find boolean matrices $B in {0,1}^(m times k)$ and $C in {0,1}^(k times n)$ minimizing the Hamming distance $d_H (A, B circle.tiny C)$, where the boolean product $(B circle.tiny C)_(i j) = or.big_ell (B_(i ell) and C_(ell j))$. + Given an $m times n$ boolean matrix $A$ and rank $k$, find boolean matrices $B in {0,1}^(m times k)$ and $C in {0,1}^(k times n)$ satisfying $B circle.tiny C = A$ and minimizing $|B|_1 + |C|_1$ (the total number of $1$s in $B$ and $C$), where the boolean product $(B circle.tiny C)_(i j) = or.big_ell (B_(i ell) and C_(ell j))$. An instance is infeasible when no exact factorization of rank $k$ exists. ][ - Boolean Matrix Factorization decomposes binary data into interpretable boolean factors, unlike real-valued SVD which loses the discrete structure. NP-hard even to approximate, BMF arises in data mining, text classification, and role-based access control where factors correspond to latent binary features. Practical algorithms use greedy rank-1 extraction or alternating fixed-point methods. The best known exact algorithm runs in $O^*(2^(m k + k n))$ by brute-force search over $B$ and $C$#footnote[No algorithm improving on brute-force enumeration is known for general BMF.]. + Boolean Matrix Factorization decomposes binary data into interpretable boolean factors, unlike real-valued SVD which loses the discrete structure. Deciding whether an exact factorization of a given rank exists is NP-complete (Orlin 1977); the minimum rank is the _Boolean rank_ of $A$, which coincides with the biclique edge cover number of the bipartite graph whose biadjacency matrix is $A$ (Monson, Pullman, Rees 1995). BMF arises in data mining, text classification, and role-based access control where factors correspond to latent binary features. The best known exact algorithm runs in $O^*(2^(m k + k n))$ by brute-force search over $B$ and $C$#footnote[No algorithm improving on brute-force enumeration is known for general exact BMF.]. - *Example.* Let $A = #fmt-mat(A-int)$ and $k = #k$. Set $B = #fmt-mat(B)$ and $C = #fmt-mat(C)$. Then $B circle.tiny C = #fmt-mat(A-int) = A$, achieving Hamming distance $d_H = #dH$ (exact factorization). The two boolean factors capture overlapping row/column patterns: factor 1 selects rows ${1, 2}$ and columns ${1, 2}$; factor 2 selects rows ${2, 3}$ and columns ${2, 3}$. + *Example.* Let $A = #fmt-mat(A-int)$ and $k = #k$. Set $B = #fmt-mat(B)$ and $C = #fmt-mat(C)$. Then $B circle.tiny C = #fmt-mat(A-int) = A$, so the factorization is exact with total factor size $|B|_1 + |C|_1 = #fs$. The two boolean factors capture overlapping row/column patterns: factor 1 selects rows ${1, 2}$ and columns ${1, 2}$; factor 2 selects rows ${2, 3}$ and columns ${2, 3}$. #pred-commands( "pred create --example BMF -o bmf.json", @@ -5778,9 +5778,9 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let total-size = metric-value(sol.metric) [ #problem-def("BicliqueCover")[ - Given a bipartite graph $G = (L, R, E)$ and integer $k$, find $k$ bicliques $(L_1, R_1), dots, (L_k, R_k)$ that cover all edges ($E subset.eq union.big_i L_i times R_i$) while minimizing the total size $sum_i (|L_i| + |R_i|)$. + Given a bipartite graph $G = (L, R, E)$ and integer $k$, find $k$ *sub-bicliques* of $G$, $(L_1, R_1), dots, (L_k, R_k)$ with $L_i times R_i subset.eq E$ for every $i$, whose edge sets jointly cover $E$ — i.e. $E = union.big_i L_i times R_i$. Minimize the total size $sum_i (|L_i| + |R_i|)$. A configuration that places vertices into a biclique $i$ for which $L_i times R_i$ is not a subset of $E$ (a "biclique" spanning non-edges of $G$) is infeasible. ][ - Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipartite graph as a Boolean sum of rank-1 binary matrices, connecting it to Boolean matrix rank and nondeterministic communication complexity. Applications include data compression, database optimization (covering queries with materialized views), and bioinformatics (gene expression biclustering). NP-hard even for fixed $k >= 2$. The best known algorithm runs in $O^*(2^(|L| + |R|))$ by brute-force enumeration#footnote[No algorithm improving on brute-force enumeration is known for general Biclique Cover.]. + Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipartite graph exactly as a Boolean sum of rank-1 binary matrices (Monson, Pullman, Rees 1995), so the minimum $k$ for which a cover exists equals the _Boolean rank_ of $M$. The problem connects to Boolean matrix factorization, nondeterministic communication complexity, and role-based access control; applications include database optimization (covering queries with materialized views) and bioinformatics (gene expression biclustering). NP-complete already for fixed $k >= 2$. The best known algorithm runs in $O^*(2^(|L| + |R|))$ by brute-force enumeration#footnote[No algorithm improving on brute-force enumeration is known for general Biclique Cover.]. *Example.* Consider $G = (L, R, E)$ with $L = {#range(left-size).map(i => $ell_#(i + 1)$).join(", ")}$, $R = {#range(right-size).map(i => $r_#(i + 1)$).join(", ")}$, and edges $E = {#bip-edges.map(e => $(ell_#(e.at(0) + 1), r_#(e.at(1) + 1))$).join(", ")}$. A biclique cover with $k = #k$: $(L_1, R_1) = ({ell_1}, {r_1, r_2})$ covering edges ${(ell_1, r_1), (ell_1, r_2)}$, and $(L_2, R_2) = ({ell_2}, {r_2, r_3})$ covering ${(ell_2, r_2), (ell_2, r_3)}$. Total size $= (1+2) + (1+2) = #total-size$. Merging into a single biclique is impossible since $(ell_1, r_3) in.not E$. @@ -14457,25 +14457,6 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ Output the concatenated left/right binary selection vector. ] -#reduction-rule("BicliqueCover", "ILP")[ - Use $k$ candidate bicliques, assign vertices to any of them, force every graph edge to be covered by some common biclique, and minimize the total membership size. -][ - _Construction._ Variables: binary $x_(l,b)$ for left vertices, binary $y_(r,b)$ for right vertices, and binary $z_((l,r),b)$ linearizing $x_(l,b) y_(r,b)$. The ILP is: - $ - min quad & sum_(l,b) x_(l,b) + sum_(r,b) y_(r,b) \ - "subject to" quad & z_((l,r),b) <= x_(l,b) quad forall l, r, b \ - & z_((l,r),b) <= y_(r,b) quad forall l, r, b \ - & z_((l,r),b) >= x_(l,b) + y_(r,b) - 1 quad forall l, r, b \ - & sum_b z_((l,r),b) >= 1 quad forall (l, r) in E \ - & x_(l,b) + y_(r,b) <= 1 quad forall (l, r) in.not E, b \ - & x_(l,b), y_(r,b), z_((l,r),b) in {0, 1}. - $ - - _Correctness._ ($arrow.r.double$) Any valid $k$-biclique cover assigns each covered edge to a biclique containing both endpoints, with objective equal to the total biclique size. ($arrow.l.double$) Any feasible ILP solution defines $k$ complete bipartite subgraphs whose union covers every edge, and the objective is exactly the source objective. - - _Solution extraction._ Output the flattened vertex-by-biclique membership bits and discard the coverage auxiliaries. -] - #reduction-rule("BiconnectivityAugmentation", "ILP")[ Select candidate edges under the budget and, for every deleted vertex, certify that the remaining augmented graph stays connected by a flow witness. ][ @@ -14639,26 +14620,45 @@ The following reductions to Integer Linear Programming are straightforward formu // Matrix/encoding #reduction-rule("BMF", "ILP")[ - Split the witness into binary factor matrices $B$ and $C$, reconstruct their Boolean product with McCormick auxiliaries, and minimize the Hamming distance to the target matrix. + Split the witness into binary factor matrices $B$ and $C$, reconstruct their Boolean product with McCormick auxiliaries, pin each reconstructed entry to the target, and minimize the total factor weight. ][ - _Construction._ Variables: binary $b_(i,r)$, binary $c_(r,j)$, binary $p_(i,r,j)$ linearizing $b_(i,r) c_(r,j)$, binary $w_(i,j)$ for the reconstructed entry, and nonnegative error variables $e_(i,j)$. The ILP is: + _Construction._ Variables: binary $b_(i,r)$, binary $c_(r,j)$, binary $p_(i,r,j)$ linearizing $b_(i,r) c_(r,j)$, and binary $w_(i,j)$ for the reconstructed entry. The ILP is: $ - min quad & sum_(i,j) e_(i,j) \ + min quad & sum_(i,r) b_(i,r) + sum_(r,j) c_(r,j) \ "subject to" quad & p_(i,r,j) <= b_(i,r) quad forall i, r, j \ & p_(i,r,j) <= c_(r,j) quad forall i, r, j \ & p_(i,r,j) >= b_(i,r) + c_(r,j) - 1 quad forall i, r, j \ & w_(i,j) >= p_(i,r,j) quad forall i, r, j \ & w_(i,j) <= sum_r p_(i,r,j) quad forall i, j \ - & e_(i,j) >= A_(i,j) - w_(i,j) quad forall i, j \ - & e_(i,j) >= w_(i,j) - A_(i,j) quad forall i, j \ - & b_(i,r), c_(r,j), p_(i,r,j), w_(i,j) in {0, 1}, e_(i,j) in ZZ_(>=0). + & w_(i,j) = A_(i,j) quad forall i, j \ + & b_(i,r), c_(r,j), p_(i,r,j), w_(i,j) in {0, 1}. $ - _Correctness._ ($arrow.r.double$) Any choice of factor matrices induces the same Boolean product and Hamming error in the ILP. ($arrow.l.double$) Any feasible ILP assignment determines factor matrices $B$ and $C$, and the linearization forces the objective to equal the Hamming distance between $A$ and $B dot C$. + _Correctness._ ($arrow.r.double$) Any exact factorization $B circle.tiny C = A$ gives a feasible ILP solution with objective equal to $|B|_1 + |C|_1$. ($arrow.l.double$) The McCormick constraints force $p_(i,r,j) = b_(i,r) dot c_(r,j)$; the $w$ constraints then force $w_(i,j) = or.big_r p_(i,r,j)$, so the equality $w_(i,j) = A_(i,j)$ is feasible exactly when $B circle.tiny C = A$. If no exact rank-$k$ factorization exists the ILP is infeasible, matching BMF's infeasibility signal. _Solution extraction._ Output the flattened bits of $B$ followed by the flattened bits of $C$, discarding the reconstruction auxiliaries. ] +#reduction-rule("BMF", "BicliqueCover")[ + Interpret the $m times n$ target matrix $A$ as the biadjacency matrix of a bipartite graph $G_A = (L, R, E)$ with $L = {1, dots, m}$, $R = {1, dots, n}$, and $(i, j) in E$ iff $A_(i j) = 1$, then reuse the same rank $k$. +][ + _Construction._ Given an instance $(A, k)$ of BMF, emit the BicliqueCover instance $(G_A, k)$. The vertex-membership layout transposes the BMF factor layout: column $r$ of $B$ becomes the left side of biclique $r$, and row $r$ of $C$ becomes its right side. + + _Correctness._ Each rank-1 factor $B_(dot,r) C_(r,dot)^top$ is the all-ones submatrix on ${i : B_(i,r) = 1} times {j : C_(r,j) = 1}$. Exactness of $B circle.tiny C = A$ is equivalent to (i) every such rectangle lying inside $E$ (sub-biclique of $G_A$), and (ii) the union of the $k$ rectangles exactly matching $E$ — which are precisely the two BicliqueCover feasibility conditions. The BMF objective $|B|_1 + |C|_1$ equals the total biclique size $sum_r (|L_r| + |R_r|)$, so the optimization objectives coincide (Monson, Pullman, Rees 1995). + + _Solution extraction._ Given a BicliqueCover witness (vertex-major, $"cfg"_("BC")[v k + r] in {0, 1}$), set $B_(i,r) = "cfg"_("BC")[i k + r]$ and $C_(r,j) = "cfg"_("BC")[(m + j) k + r]$. The left half is a direct copy; the right half transposes from vertex-major to biclique-row-major. +] + +#reduction-rule("BicliqueCover", "BMF")[ + The inverse of the matrix-to-graph map: read off the biadjacency matrix $A_G in {0,1}^(|L| times |R|)$ of the bipartite graph $G$ and reuse the same rank $k$. +][ + _Construction._ Given an instance $(G, k)$ of BicliqueCover, emit the BMF instance $(A_G, k)$ where $A_G[i][j] = 1$ iff $(i, j) in E(G)$. Source and target live in the same variable space, with the layout permutation described below. + + _Correctness._ Symmetric to the forward rule: the same Monson–Pullman–Rees equivalence (sub-bicliques of $G$ $<->$ rank-1 factors of $A_G$) holds in both directions, and the two objectives — total vertex memberships and $|B|_1 + |C|_1$ — agree by construction. + + _Solution extraction._ Inverse transpose of the forward map: given a BMF witness (B row-major followed by C row-major), set $"cfg"_("BC")[i k + r] = B_(i,r)$ for left vertices and $"cfg"_("BC")[(m + j) k + r] = C_(r,j)$ for right vertices. +] + #reduction-rule("ConsecutiveBlockMinimization", "ILP")[ Permute the columns with a one-hot assignment and count row-wise block starts by detecting each 0-to-1 transition after permutation. ][ diff --git a/scripts/jl/generate_testdata.jl b/scripts/jl/generate_testdata.jl index c15f02eb5..3880477a7 100644 --- a/scripts/jl/generate_testdata.jl +++ b/scripts/jl/generate_testdata.jl @@ -717,15 +717,18 @@ function main() export_setcovering(doc_sc, "doc_3subsets"), ])) - # BicliqueCover - write_fixture("biclique_cover.json", model_fixture("BicliqueCover", [ - export_biclique_cover(doc_bc_graph, [1,2,3], 2, "doc_6vertex"), - ])) - - # BMF - write_fixture("bmf.json", model_fixture("BMF", [ - export_bmf(trues(3, 3), 2, "doc_3x3_ones"), - ])) + # NOTE: BicliqueCover is no longer exported as a Julia parity fixture. + # The Rust model enforces the classical sub-biclique semantics (each + # biclique must be a complete bipartite subgraph of the input graph), + # whereas `biclique_cover_evaluate` above implements the OR-cover + # semantics used by the Julia package. Parity fixtures generated here + # would therefore disagree with Rust on configurations that cover + # non-edges. + + # NOTE: BMF is no longer exported as a Julia parity fixture. The Rust model + # was redefined as exact Boolean matrix factorization with a factor-size + # objective; the old Hamming-distance semantics implemented by + # `bmf_evaluate` / `export_bmf` below no longer match the Rust behavior. # ── Export reduction fixtures ── println("Exporting reduction fixtures...") diff --git a/src/models/algebraic/bmf.rs b/src/models/algebraic/bmf.rs index 8e3bc1f81..455514fe8 100644 --- a/src/models/algebraic/bmf.rs +++ b/src/models/algebraic/bmf.rs @@ -1,8 +1,10 @@ //! Boolean Matrix Factorization (BMF) problem implementation. //! -//! Given a boolean matrix A, find matrices B and C such that -//! the boolean product B * C approximates A. -//! The boolean product `(B * C)[i,j] = OR_k (B[i,k] AND C[k,j])`. +//! Given a boolean matrix A and rank k, find boolean matrices B (m x k) +//! and C (k x n) such that the boolean product B * C equals A exactly, +//! minimizing the total number of 1s in B and C. Configs that do not +//! produce an exact factorization evaluate to `Min(None)` (infeasible). +//! The boolean product `(B * C)[i,j] = OR_r (B[i,r] AND C[r,j])`. use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::traits::Problem; @@ -30,7 +32,8 @@ inventory::submit! { /// - B: m x k boolean matrix /// - C: k x n boolean matrix /// -/// Such that the Hamming distance between A and B*C is minimized. +/// Such that `B * C = A` exactly, minimizing the total number of 1s in B and C. +/// Configurations that do not yield an exact factorization are infeasible. /// /// # Example /// @@ -38,21 +41,16 @@ inventory::submit! { /// use problemreductions::models::algebraic::BMF; /// use problemreductions::{Problem, Solver, BruteForce}; /// -/// // 2x2 identity matrix +/// // 2x2 identity matrix — boolean rank 2 /// let a = vec![ /// vec![true, false], /// vec![false, true], /// ]; -/// let problem = BMF::new(a, 1); +/// let problem = BMF::new(a, 2); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_witnesses(&problem); -/// -/// // Check the error -/// for sol in &solutions { -/// let error = problem.hamming_distance(sol); -/// println!("Hamming error: {}", error); -/// } +/// let witness = solver.find_witness(&problem).unwrap(); +/// assert!(problem.is_exact(&witness)); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BMF { @@ -161,16 +159,14 @@ impl BMF { /// Compute the Hamming distance between the target and the product. pub fn hamming_distance(&self, config: &[usize]) -> usize { let (b, c) = self.extract_factors(config); - let product = Self::boolean_product(&b, &c); - - self.matrix - .iter() - .zip(product.iter()) - .map(|(a_row, p_row)| { - a_row - .iter() - .zip(p_row.iter()) - .filter(|(a, p)| a != p) + + (0..self.m) + .map(|i| { + (0..self.n) + .filter(|&j| { + let product_entry = (0..self.k).any(|r| b[i][r] && c[r][j]); + self.matrix[i][j] != product_entry + }) .count() }) .sum() @@ -180,6 +176,11 @@ impl BMF { pub fn is_exact(&self, config: &[usize]) -> bool { self.hamming_distance(config) == 0 } + + /// Total number of 1s in B and C (the factor size to be minimized when exact). + pub fn total_factor_size(&self, config: &[usize]) -> usize { + config.iter().filter(|&&x| x == 1).count() + } } /// Compute the boolean matrix product. @@ -213,9 +214,11 @@ impl Problem for BMF { } fn evaluate(&self, config: &[usize]) -> Min { - // Minimize Hamming distance between A and B*C. - // All configurations are valid -- the distance is the objective. - Min(Some(self.hamming_distance(config) as i32)) + // Feasible iff B*C = A exactly; objective is total factor size (|B| + |C| in 1s). + if self.hamming_distance(config) != 0 { + return Min(None); + } + Min(Some(self.total_factor_size(config) as i32)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -239,8 +242,10 @@ pub(crate) fn canonical_model_example_specs() -> Vec bool { use crate::topology::Graph; + let (left_bicliques, right_bicliques) = self.get_biclique_memberships(config); + let left_size = self.graph.left_size(); + // Every biclique must be a sub-biclique of G (no non-edges covered). + for b in 0..self.k { + for &l in &left_bicliques[b] { + for &r in &right_bicliques[b] { + // Endpoints come from get_biclique_memberships in unified + // vertex space: l < left_size, r >= left_size. + debug_assert!(l < left_size && r >= left_size); + if !self.graph.has_edge(l, r) { + return false; + } + } + } + } + // Every edge of G must be covered by at least one biclique. self.graph .edges() .iter() @@ -209,12 +241,31 @@ pub(crate) fn is_biclique_cover( left_bicliques: &[HashSet], right_bicliques: &[HashSet], ) -> bool { - edges.iter().all(|&(l, r)| { + let edge_set: HashSet<(usize, usize)> = edges + .iter() + .map(|&(u, v)| if u <= v { (u, v) } else { (v, u) }) + .collect(); + + let all_bicliques_are_subgraphs = left_bicliques .iter() .zip(right_bicliques.iter()) - .any(|(lb, rb)| lb.contains(&l) && rb.contains(&r)) - }) + .all(|(lb, rb)| { + lb.iter().all(|&l| { + rb.iter().all(|&r| { + let edge = if l <= r { (l, r) } else { (r, l) }; + edge_set.contains(&edge) + }) + }) + }); + + all_bicliques_are_subgraphs + && edges.iter().all(|&(l, r)| { + left_bicliques + .iter() + .zip(right_bicliques.iter()) + .any(|(lb, rb)| lb.contains(&l) && rb.contains(&r)) + }) } impl Problem for BicliqueCover { @@ -239,20 +290,25 @@ impl Problem for BicliqueCover { } crate::declare_variants! { - default BicliqueCover => "2^num_vertices", + default BicliqueCover => "2^(num_vertices * rank)", } #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { use crate::topology::BipartiteGraph; + // Biclique 0: L_0={ℓ_1}, R_0={r_1, r_2} — covers edges (0,0), (0,1). + // Biclique 1: L_1={ℓ_2}, R_1={r_2, r_3} — covers edges (1,1), (1,2). + // Both are sub-bicliques of G; every edge covered; total size = 6. + // Vertex-major layout with k=2: v=0 in b=0, v=1 in b=1, v=2 in b=0, + // v=3 in both, v=4 in b=1. vec![crate::example_db::specs::ModelExampleSpec { id: "biclique_cover", instance: Box::new(BicliqueCover::new( BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 1), (1, 2)]), 2, )), - optimal_config: vec![0, 1, 0, 1, 0, 1, 0, 1, 0, 1], - optimal_value: serde_json::json!(5), + optimal_config: vec![1, 0, 0, 1, 1, 0, 1, 1, 0, 1], + optimal_value: serde_json::json!(6), }] } diff --git a/src/models/graph/partition_into_paths_of_length_2.rs b/src/models/graph/partition_into_paths_of_length_2.rs index 696ddd967..717bcab97 100644 --- a/src/models/graph/partition_into_paths_of_length_2.rs +++ b/src/models/graph/partition_into_paths_of_length_2.rs @@ -175,8 +175,18 @@ pub(crate) fn canonical_model_example_specs() -> Vec &BMF { + &self.target + } + + /// Map a BMF config (B row-major, C row-major) to a BicliqueCover + /// config (vertex-major) via the inverse transpose. + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + config_bmf_to_bc(target_solution, self.m, self.n, self.k) + } +} + +#[reduction( + overhead = { + rows = "left_size", + cols = "right_size", + rank = "rank", + } +)] +impl ReduceTo for BicliqueCover { + type Result = ReductionBicliqueCoverToBMF; + + fn reduce_to(&self) -> Self::Result { + let m = self.left_size(); + let n = self.right_size(); + let k = self.k(); + let mut matrix = vec![vec![false; n]; m]; + for &(i, j) in self.graph().left_edges() { + matrix[i][j] = true; + } + let target = BMF::new(matrix, k); + ReductionBicliqueCoverToBMF { target, m, n, k } + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + use crate::export::SolutionPair; + use crate::topology::BipartiteGraph; + + vec![crate::example_db::specs::RuleExampleSpec { + id: "bicliquecover_to_bmf", + build: || { + // Single K_{2,2} biclique at rank 1 — matches the forward example. + let source = BicliqueCover::new( + BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]), + 1, + ); + crate::example_db::specs::rule_example_with_witness::<_, BMF>( + source, + SolutionPair { + // BicliqueCover (vertex-major, k=1): all 4 vertices in biclique 0 + source_config: vec![1, 1, 1, 1], + // BMF (B row-major then C row-major): B=[[1],[1]], C=[[1,1]] + target_config: vec![1, 1, 1, 1], + }, + ) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/bicliquecover_bmf.rs"] +mod tests; diff --git a/src/rules/bicliquecover_ilp.rs b/src/rules/bicliquecover_ilp.rs deleted file mode 100644 index e0a1a2c96..000000000 --- a/src/rules/bicliquecover_ilp.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! Reduction from BicliqueCover to ILP. -//! -//! Variables: binary x_{l,b} for left-vertex/biclique membership, -//! binary y_{r,b} for right-vertex/biclique membership, -//! binary z_{(l,r),b} = x_{l,b} * y_{r,b} (McCormick product). -//! Coverage: Σ_b z_{(l,r),b} ≥ 1 for every edge (l,r). -//! Objective: minimize Σ x_{l,b} + Σ y_{r,b}. - -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; -use crate::models::graph::BicliqueCover; -use crate::reduction; -use crate::rules::ilp_helpers::mccormick_product; -use crate::rules::traits::{ReduceTo, ReductionResult}; -#[derive(Debug, Clone)] -pub struct ReductionBicliqueCoverToILP { - target: ILP, - /// Number of source-problem variables (num_vertices * k). - source_vars: usize, -} - -impl ReductionResult for ReductionBicliqueCoverToILP { - type Source = BicliqueCover; - type Target = ILP; - - fn target_problem(&self) -> &ILP { - &self.target - } - - /// Extract the vertex-by-biclique membership bits, discarding z auxiliaries. - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - target_solution[..self.source_vars].to_vec() - } -} - -#[reduction( - overhead = { - num_vars = "num_vertices * rank + num_vertices * num_vertices * rank", - num_constraints = "num_vertices * num_vertices * rank + num_edges", - } -)] -impl ReduceTo> for BicliqueCover { - type Result = ReductionBicliqueCoverToILP; - - fn reduce_to(&self) -> Self::Result { - let left = self.left_size(); - let right = self.right_size(); - let n = left + right; - let k = self.k(); - let mut constraints = Vec::new(); - - // Variable layout: - // x_{l,b}: index l*k + b (left membership) [0, left*k) - // y_{r,b}: index left*k + r*k + b (right membership) [left*k, n*k) - // z_{(l,r),b}: index n*k + (l*right + r)*k + b (products) [n*k, n*k + left*right*k) - let x_idx = |l: usize, b: usize| -> usize { l * k + b }; - let y_idx = |r: usize, b: usize| -> usize { left * k + r * k + b }; - let z_idx = |l: usize, r: usize, b: usize| -> usize { n * k + (l * right + r) * k + b }; - - let num_vars = n * k + left * right * k; - let source_vars = n * k; - - // McCormick for z_{(l,r),b} = x_{l,b} * y_{r,b} - for l in 0..left { - for r in 0..right { - for b in 0..k { - constraints.extend(mccormick_product(z_idx(l, r, b), x_idx(l, b), y_idx(r, b))); - } - } - } - - // Coverage: Σ_b z_{(l,r),b} ≥ 1 for every edge - for &(l, r) in self.graph().left_edges() { - let terms: Vec<(usize, f64)> = (0..k).map(|b| (z_idx(l, r, b), 1.0)).collect(); - constraints.push(LinearConstraint::ge(terms, 1.0)); - } - - // Objective: minimize Σ x_{l,b} + Σ y_{r,b} - let mut objective: Vec<(usize, f64)> = Vec::with_capacity(n * k); - for v in 0..n { - for b in 0..k { - objective.push((v * k + b, 1.0)); - } - } - - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize); - - ReductionBicliqueCoverToILP { - target, - source_vars, - } - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; - use crate::topology::BipartiteGraph; - vec![crate::example_db::specs::RuleExampleSpec { - id: "bicliquecover_to_ilp", - build: || { - // L={0,1}, R={0,1,2}, edges: (0,0),(0,1),(1,1),(1,2), k=2 - let source = BicliqueCover::new( - BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 1), (1, 2)]), - 2, - ); - let reduction: ReductionBicliqueCoverToILP = - crate::rules::ReduceTo::>::reduce_to(&source); - let ilp_sol = crate::solvers::ILPSolver::new() - .solve(reduction.target_problem()) - .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( - source, - SolutionPair { - source_config: extracted, - target_config: ilp_sol, - }, - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/bicliquecover_ilp.rs"] -mod tests; diff --git a/src/rules/bmf_bicliquecover.rs b/src/rules/bmf_bicliquecover.rs new file mode 100644 index 000000000..bafa9b304 --- /dev/null +++ b/src/rules/bmf_bicliquecover.rs @@ -0,0 +1,134 @@ +//! Reduction from BMF (exact Boolean Matrix Factorization) to BicliqueCover. +//! +//! Classical equivalence (Monson, Pullman, Rees 1995): an m x n boolean +//! matrix `A` is the biadjacency matrix of the bipartite graph `G_A`, and +//! each rank-1 factor of `A = B ⊙ C` is exactly a (complete) biclique of +//! `G_A` — its left side is `{i : B[i][r] = 1}` and its right side is +//! `{j : C[r][j] = 1}`. Hence an exact rank-`k` factorization corresponds +//! to a cover of `E(G_A)` by `k` sub-bicliques of `G_A`, and the total +//! factor weight `|B|_1 + |C|_1` equals the total biclique size (the +//! number of vertex memberships summed over all bicliques). +//! +//! Variable-layout mapping: BMF stores `B` row-major followed by `C` +//! row-major, while BicliqueCover stores vertex memberships vertex-major. +//! `extract_solution` transposes the right-vertex half so the extracted +//! BMF config matches `B` and `C`. + +use crate::models::algebraic::BMF; +use crate::models::graph::BicliqueCover; +use crate::reduction; +use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::topology::BipartiteGraph; + +/// Convert a BicliqueCover config (vertex-major: index `v*k + b`) to a BMF +/// config (B row-major at `[0, m*k)` then C row-major at `[m*k, m*k + k*n)`). +/// +/// The left half copies unchanged; the right half transposes from +/// vertex-major `(m+j)*k + l` to biclique-row-major `m*k + l*n + j`. +pub(crate) fn config_bc_to_bmf(bc: &[usize], m: usize, n: usize, k: usize) -> Vec { + let mut bmf = vec![0usize; m * k + k * n]; + for i in 0..m { + for l in 0..k { + bmf[i * k + l] = bc[i * k + l]; + } + } + for l in 0..k { + for j in 0..n { + bmf[m * k + l * n + j] = bc[(m + j) * k + l]; + } + } + bmf +} + +/// Inverse of [`config_bc_to_bmf`]: BMF config (B row-major then C row-major) +/// to BicliqueCover config (vertex-major). +pub(crate) fn config_bmf_to_bc(bmf: &[usize], m: usize, n: usize, k: usize) -> Vec { + let mut bc = vec![0usize; (m + n) * k]; + for i in 0..m { + for l in 0..k { + bc[i * k + l] = bmf[i * k + l]; + } + } + for l in 0..k { + for j in 0..n { + bc[(m + j) * k + l] = bmf[m * k + l * n + j]; + } + } + bc +} + +/// Result of reducing BMF to BicliqueCover. +#[derive(Debug, Clone)] +pub struct ReductionBMFToBicliqueCover { + target: BicliqueCover, + m: usize, + n: usize, + k: usize, +} + +impl ReductionResult for ReductionBMFToBicliqueCover { + type Source = BMF; + type Target = BicliqueCover; + + fn target_problem(&self) -> &BicliqueCover { + &self.target + } + + /// Map a BicliqueCover config (vertex-major) back to a BMF config (B row-major, then C row-major). + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + config_bc_to_bmf(target_solution, self.m, self.n, self.k) + } +} + +#[reduction( + overhead = { + num_vertices = "rows + cols", + num_edges = "rows * cols", + rank = "rank", + } +)] +impl ReduceTo for BMF { + type Result = ReductionBMFToBicliqueCover; + + fn reduce_to(&self) -> Self::Result { + let m = self.rows(); + let n = self.cols(); + let k = self.rank(); + let mut edges = Vec::new(); + for (i, row) in self.matrix().iter().enumerate() { + for (j, &val) in row.iter().enumerate() { + if val { + edges.push((i, j)); + } + } + } + let target = BicliqueCover::new(BipartiteGraph::new(m, n, edges), k); + ReductionBMFToBicliqueCover { target, m, n, k } + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + use crate::export::SolutionPair; + + vec![crate::example_db::specs::RuleExampleSpec { + id: "bmf_to_bicliquecover", + build: || { + // 2x2 all-ones, rank 1 — a single biclique covering both sides exactly. + let source = BMF::new(vec![vec![true, true], vec![true, true]], 1); + crate::example_db::specs::rule_example_with_witness::<_, BicliqueCover>( + source, + SolutionPair { + // BMF config (B row-major, C row-major): B = [[1],[1]], C = [[1,1]] + source_config: vec![1, 1, 1, 1], + // BicliqueCover config (vertex-major, k=1): v0, v1 (left), v2, v3 (right) all in biclique 0 + target_config: vec![1, 1, 1, 1], + }, + ) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/bmf_bicliquecover.rs"] +mod tests; diff --git a/src/rules/bmf_ilp.rs b/src/rules/bmf_ilp.rs index aca0223d3..2764ef419 100644 --- a/src/rules/bmf_ilp.rs +++ b/src/rules/bmf_ilp.rs @@ -1,8 +1,8 @@ //! Reduction from BMF (Boolean Matrix Factorization) to ILP. //! //! Variables: binary b_{i,r}, c_{r,j}, McCormick product p_{i,r,j} = b_{i,r} * c_{r,j}, -//! reconstructed entry w_{i,j} = OR_r p_{i,r,j}, error e_{i,j} = |A_{i,j} - w_{i,j}|. -//! Minimize sum of errors. +//! reconstructed entry w_{i,j} = OR_r p_{i,r,j}. Pin w_{i,j} = A_{i,j} (exact factorization) +//! and minimize sum_{i,r} b_{i,r} + sum_{r,j} c_{r,j} (total factor size). use crate::models::algebraic::{LinearConstraint, ObjectiveSense, BMF, ILP}; use crate::reduction; @@ -34,8 +34,8 @@ impl ReductionResult for ReductionBMFToILP { #[reduction( overhead = { - num_vars = "rows * rank + rank * cols + rows * rank * cols + rows * cols + rows * cols", - num_constraints = "3 * rows * rank * cols + rank * rows * cols + rows * cols + 2 * rows * cols", + num_vars = "rows * rank + rank * cols + rows * rank * cols + rows * cols", + num_constraints = "3 * rows * rank * cols + rank * rows * cols + rows * cols + rows * cols", } )] impl ReduceTo> for BMF { @@ -51,13 +51,11 @@ impl ReduceTo> for BMF { // c_{r,j}: k*n variables at indices [m*k, m*k + k*n) // p_{i,r,j}: m*k*n variables at indices [m*k + k*n, m*k + k*n + m*k*n) // w_{i,j}: m*n variables at indices [m*k + k*n + m*k*n, m*k + k*n + m*k*n + m*n) - // e_{i,j}: m*n variables at indices [m*k + k*n + m*k*n + m*n, ...) let b_offset = 0; let c_offset = m * k; let p_offset = m * k + k * n; let w_offset = p_offset + m * k * n; - let e_offset = w_offset + m * n; - let num_vars = e_offset + m * n; + let num_vars = w_offset + m * n; let mut constraints = Vec::new(); @@ -73,7 +71,6 @@ impl ReduceTo> for BMF { } let w_idx = w_offset + i * n + j; - let e_idx = e_offset + i * n + j; // w_{i,j} >= p_{i,r,j} for all r for r in 0..k { @@ -89,23 +86,16 @@ impl ReduceTo> for BMF { } constraints.push(LinearConstraint::le(w_upper_terms, 0.0)); - // e_{i,j} >= A_{i,j} - w_{i,j} + // Exact factorization: w_{i,j} = A_{i,j} let a_val = if self.matrix()[i][j] { 1.0 } else { 0.0 }; - constraints.push(LinearConstraint::ge( - vec![(e_idx, 1.0), (w_idx, 1.0)], - a_val, - )); - - // e_{i,j} >= w_{i,j} - A_{i,j} - constraints.push(LinearConstraint::ge( - vec![(e_idx, 1.0), (w_idx, -1.0)], - -a_val, - )); + constraints.push(LinearConstraint::eq(vec![(w_idx, 1.0)], a_val)); } } - // Objective: minimize sum e_{i,j} - let objective: Vec<(usize, f64)> = (0..m * n).map(|idx| (e_offset + idx, 1.0)).collect(); + // Objective: minimize sum_{i,r} b_{i,r} + sum_{r,j} c_{r,j} (total factor size) + let mut objective: Vec<(usize, f64)> = + (0..m * k).map(|idx| (b_offset + idx, 1.0)).collect(); + objective.extend((0..k * n).map(|idx| (c_offset + idx, 1.0))); let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize); ReductionBMFToILP { target, m, n, k } diff --git a/src/rules/maximumindependentset_triangular.rs b/src/rules/maximumindependentset_triangular.rs index dbbed5567..6d9bd44c5 100644 --- a/src/rules/maximumindependentset_triangular.rs +++ b/src/rules/maximumindependentset_triangular.rs @@ -28,7 +28,8 @@ impl ReductionResult for ReductionISSimpleToTriangular { } fn extract_solution(&self, target_solution: &[usize]) -> Vec { - self.mapping_result.map_config_back_via_centers(target_solution) + self.mapping_result + .map_config_back_via_centers(target_solution) } } diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 8c5185851..cd59acef3 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -8,6 +8,8 @@ pub use cost::{ }; pub use registry::{EdgeCapabilities, ReductionEntry, ReductionOverhead}; +pub(crate) mod bicliquecover_bmf; +pub(crate) mod bmf_bicliquecover; pub(crate) mod circuit_sat; pub(crate) mod circuit_spinglass; mod closestvectorproblem_qubo; @@ -140,8 +142,6 @@ pub(crate) mod acyclicpartition_ilp; #[cfg(feature = "ilp-solver")] pub(crate) mod balancedcompletebipartitesubgraph_ilp; #[cfg(feature = "ilp-solver")] -pub(crate) mod bicliquecover_ilp; -#[cfg(feature = "ilp-solver")] pub(crate) mod biconnectivityaugmentation_ilp; #[cfg(feature = "ilp-solver")] pub(crate) mod binpacking_ilp; @@ -379,6 +379,8 @@ pub use traits::{ #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { let mut specs = Vec::new(); + specs.extend(bicliquecover_bmf::canonical_rule_example_specs()); + specs.extend(bmf_bicliquecover::canonical_rule_example_specs()); specs.extend(circuit_sat::canonical_rule_example_specs()); specs.extend(circuit_spinglass::canonical_rule_example_specs()); specs.extend(decisionminimumdominatingset_minmaxmulticenter::canonical_rule_example_specs()); @@ -507,7 +509,6 @@ pub(crate) fn canonical_rule_example_specs() -> Vec distance 0 + // Exact factorization -> Min(Some(total_factor_size)) = 4 (two 1s in B, two in C) let config = vec![1, 0, 0, 1, 1, 0, 0, 1]; - assert_eq!(Problem::evaluate(&problem, &config), Min(Some(0))); + assert_eq!(Problem::evaluate(&problem, &config), Min(Some(4))); - // Non-exact -> distance 2 + // Non-exact -> Min(None) let config = vec![0, 0, 0, 0, 0, 0, 0, 0]; - assert_eq!(Problem::evaluate(&problem, &config), Min(Some(2))); + assert_eq!(Problem::evaluate(&problem, &config), Min(None)); } #[test] fn test_brute_force_ones() { - // All ones matrix can be factored with rank 1 + // All-ones 2x2 factors exactly at rank 1: optimal total_factor_size = 4 + // (B = [[1],[1]] has two 1s, C = [[1,1]] has two 1s). let matrix = vec![vec![true, true], vec![true, true]]; let problem = BMF::new(matrix, 1); let solver = BruteForce::new(); - let solutions = solver.find_all_witnesses(&problem); - for sol in &solutions { - // Exact factorization has distance 0 - assert_eq!(Problem::evaluate(&problem, sol), Min(Some(0))); + let witnesses = solver.find_all_witnesses(&problem); + assert!(!witnesses.is_empty()); + for sol in &witnesses { + assert!(problem.is_exact(sol)); + assert_eq!(Problem::evaluate(&problem, sol), Min(Some(4))); } } #[test] fn test_brute_force_identity() { - // Identity matrix needs rank 2 + // Identity matrix factors exactly at rank 2. let matrix = vec![vec![true, false], vec![false, true]]; let problem = BMF::new(matrix, 2); let solver = BruteForce::new(); - let solutions = solver.find_all_witnesses(&problem); - // Should find exact factorization - for sol in &solutions { + let witnesses = solver.find_all_witnesses(&problem); + for sol in &witnesses { assert!(problem.is_exact(sol)); } } #[test] fn test_brute_force_insufficient_rank() { - // Identity matrix with rank 1 cannot be exact + // Rank-1 over the 2x2 identity admits no exact factorization, + // so every config evaluates to Min(None). let matrix = vec![vec![true, false], vec![false, true]]; let problem = BMF::new(matrix, 1); let solver = BruteForce::new(); - let solutions = solver.find_all_witnesses(&problem); - // Best approximation has distance > 0 - let best_distance = problem.hamming_distance(&solutions[0]); - // With rank 1, best we can do is distance 1 (all ones or all zeros except one) - assert!(best_distance >= 1); + let witness = solver.find_witness(&problem); + assert!( + witness.is_none() || Problem::evaluate(&problem, witness.as_ref().unwrap()) == Min(None) + ); } #[test] @@ -152,10 +151,24 @@ fn test_empty_matrix() { let matrix: Vec> = vec![]; let problem = BMF::new(matrix, 1); assert_eq!(problem.num_variables(), 0); - // Empty matrix has distance 0 + // Empty matrix factors exactly with zero factor size. assert_eq!(Problem::evaluate(&problem, &[]), Min(Some(0))); } +#[test] +fn test_rank_zero_exactness() { + let nonzero = BMF::new(vec![vec![true, false]], 0); + assert_eq!(nonzero.dims(), Vec::::new()); + assert_eq!(nonzero.hamming_distance(&[]), 1); + assert!(!nonzero.is_exact(&[])); + assert_eq!(Problem::evaluate(&nonzero, &[]), Min(None)); + + let zero = BMF::new(vec![vec![false, false]], 0); + assert_eq!(zero.hamming_distance(&[]), 0); + assert!(zero.is_exact(&[])); + assert_eq!(Problem::evaluate(&zero, &[]), Min(Some(0))); +} + #[test] fn test_is_exact() { let matrix = vec![vec![true]]; @@ -175,64 +188,24 @@ fn test_bmf_problem() { // dims: B(2*2) + C(2*2) = 8 binary variables assert_eq!(problem.dims(), vec![2; 8]); - // Exact factorization: B = I, C = I - // Config: [1,0,0,1, 1,0,0,1] + // Exact factorization: B = I, C = I — total factor size = 4 assert_eq!( Problem::evaluate(&problem, &[1, 0, 0, 1, 1, 0, 0, 1]), - Min(Some(0)) + Min(Some(4)) ); - // All zeros -> product is all zeros, distance = 2 + // All zeros -> product is all zeros, not equal to A -> infeasible assert_eq!( Problem::evaluate(&problem, &[0, 0, 0, 0, 0, 0, 0, 0]), - Min(Some(2)) + Min(None) ); - // ExtremumSense is minimize - - // Test with 1x1 matrix + // 1x1 matrix let matrix = vec![vec![true]]; let problem = BMF::new(matrix, 1); assert_eq!(problem.dims(), vec![2; 2]); // B(1*1) + C(1*1) - assert_eq!(Problem::evaluate(&problem, &[1, 1]), Min(Some(0))); // Exact - assert_eq!(Problem::evaluate(&problem, &[0, 0]), Min(Some(1))); // Distance 1 -} - -#[test] -fn test_jl_parity_evaluation() { - let data: serde_json::Value = - serde_json::from_str(include_str!("../../../../tests/data/jl/bmf.json")).unwrap(); - for instance in data["instances"].as_array().unwrap() { - let matrix_json = instance["instance"]["matrix"].as_array().unwrap(); - let matrix: Vec> = matrix_json - .iter() - .map(|row| { - row.as_array() - .unwrap() - .iter() - .map(|v| v.as_u64().unwrap() != 0) - .collect() - }) - .collect(); - let k = instance["instance"]["k"].as_u64().unwrap() as usize; - let problem = BMF::new(matrix, k); - for eval in instance["evaluations"].as_array().unwrap() { - let config = jl_parse_config(&eval["config"]); - let result = problem.evaluate(&config); - let jl_size = eval["size"].as_i64().unwrap() as i32; - // BMF always returns Min(hamming_distance). - assert_eq!( - result, - Min(Some(jl_size)), - "BMF: size mismatch for config {:?}", - config - ); - } - let best = BruteForce::new().find_all_witnesses(&problem); - let jl_best = jl_parse_configs_set(&instance["best_solutions"]); - let rust_best: HashSet> = best.into_iter().collect(); - assert_eq!(rust_best, jl_best, "BMF best solutions mismatch"); - } + assert_eq!(Problem::evaluate(&problem, &[1, 1]), Min(Some(2))); // Exact, factor size 2 + assert_eq!(Problem::evaluate(&problem, &[0, 0]), Min(None)); // Not exact } #[test] @@ -256,13 +229,12 @@ fn test_bmf_paper_example() { let problem = BMF::new(matrix, 2); // B (3x2): [[1,0],[1,1],[0,1]], C (2x3): [[1,1,0],[0,1,1]] // Config: B row-major then C row-major - // B: b00=1,b01=0, b10=1,b11=1, b20=0,b21=1 - // C: c00=1,c01=1,c02=0, c10=0,c11=1,c12=1 + // Eight 1s total -> optimal total factor size = 8. let config = vec![1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1]; - let result = Problem::evaluate(&problem, &config); - assert_eq!(result, Min(Some(0))); // exact factorization + assert!(problem.is_exact(&config)); + assert_eq!(Problem::evaluate(&problem, &config), Min(Some(8))); let solver = BruteForce::new(); let best = solver.find_witness(&problem).unwrap(); - assert_eq!(Problem::evaluate(&problem, &best), Min(Some(0))); + assert!(problem.is_exact(&best)); } diff --git a/src/unit_tests/models/graph/biclique_cover.rs b/src/unit_tests/models/graph/biclique_cover.rs index b4560be6e..6693e4fa2 100644 --- a/src/unit_tests/models/graph/biclique_cover.rs +++ b/src/unit_tests/models/graph/biclique_cover.rs @@ -4,8 +4,6 @@ use crate::topology::BipartiteGraph; use crate::traits::Problem; use crate::types::Min; -include!("../../jl_helpers.rs"); - #[test] fn test_biclique_cover_creation() { let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0)]); @@ -129,6 +127,12 @@ fn test_is_biclique_cover_function() { let right = vec![vec![2].into_iter().collect(), vec![3].into_iter().collect()]; assert!(is_biclique_cover(&edges, &left, &right)); + // A single pseudo-biclique covers both listed edges but also contains + // non-edges (0,3) and (1,2), so it is not a classical sub-biclique cover. + let left = vec![vec![0, 1].into_iter().collect()]; + let right = vec![vec![2, 3].into_iter().collect()]; + assert!(!is_biclique_cover(&edges, &left, &right)); + // Missing coverage let left = vec![vec![0].into_iter().collect()]; let right = vec![vec![2].into_iter().collect()]; @@ -161,8 +165,9 @@ fn test_biclique_problem() { // Invalid cover: only vertex 0, edge (0,2) not covered assert_eq!(problem.evaluate(&[1, 0, 0, 0]), Min(None)); - // Valid cover with all vertices -> size 4 - assert_eq!(problem.evaluate(&[1, 1, 1, 1]), Min(Some(4))); + // All vertices in biclique: biclique contains non-edges (0,3), (1,2), (1,3) + // → not a sub-biclique of G → invalid cover. + assert_eq!(problem.evaluate(&[1, 1, 1, 1]), Min(None)); // Empty config: no vertices in biclique, edge not covered assert_eq!(problem.evaluate(&[0, 0, 0, 0]), Min(None)); @@ -175,50 +180,6 @@ fn test_biclique_problem() { assert_eq!(empty_problem.evaluate(&[0, 0, 0, 0]), Min(Some(0))); } -#[test] -fn test_jl_parity_evaluation() { - let data: serde_json::Value = serde_json::from_str(include_str!( - "../../../../tests/data/jl/biclique_cover.json" - )) - .unwrap(); - for instance in data["instances"].as_array().unwrap() { - let left_size = instance["instance"]["left_size"].as_u64().unwrap() as usize; - let right_size = instance["instance"]["right_size"].as_u64().unwrap() as usize; - let unified_edges = jl_parse_edges(&instance["instance"]); - // Convert from unified coords to bipartite-local coords - let local_edges: Vec<(usize, usize)> = unified_edges - .iter() - .map(|&(l, r)| (l, r - left_size)) - .collect(); - let k = instance["instance"]["k"].as_u64().unwrap() as usize; - let graph = BipartiteGraph::new(left_size, right_size, local_edges); - let problem = BicliqueCover::new(graph, k); - for eval in instance["evaluations"].as_array().unwrap() { - let config = jl_parse_config(&eval["config"]); - let result = problem.evaluate(&config); - let jl_valid = eval["is_valid"].as_bool().unwrap(); - let jl_size = eval["size"].as_i64().unwrap() as i32; - if jl_valid { - assert_eq!( - result, - Min(Some(jl_size)), - "BicliqueCover: valid config mismatch" - ); - } else { - assert_eq!( - result, - Min(None), - "BicliqueCover: invalid config should be Invalid" - ); - } - } - let best = BruteForce::new().find_all_witnesses(&problem); - let jl_best = jl_parse_configs_set(&instance["best_solutions"]); - let rust_best: HashSet> = best.into_iter().collect(); - assert_eq!(rust_best, jl_best, "BicliqueCover best solutions mismatch"); - } -} - #[test] fn test_is_valid_solution() { use crate::topology::BipartiteGraph; @@ -242,6 +203,21 @@ fn test_size_getters() { assert_eq!(problem.rank(), 1); } +#[test] +fn test_complexity_includes_number_of_bicliques() { + let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1)]); + let problem = BicliqueCover::new(graph, 2); + let entry = inventory::iter::() + .find(|entry| entry.name == "BicliqueCover") + .expect("BicliqueCover variant should be registered"); + + assert_eq!(problem.dims().len(), 8); + assert_eq!( + (entry.complexity_eval_fn)(&problem as &dyn std::any::Any), + 256.0 + ); +} + #[test] fn test_biclique_paper_example() { // Paper: L={ℓ_1,ℓ_2}, R={r_1,r_2,r_3}, 4 edges, k=2, total size=6 diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index 399a861ad..be612b57b 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -443,8 +443,7 @@ fn test_3sat_to_mis_triangular_overhead() { let composed = graph.compose_path_overhead(&path); // 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/bicliquecover_bmf.rs b/src/unit_tests/rules/bicliquecover_bmf.rs new file mode 100644 index 000000000..30c912988 --- /dev/null +++ b/src/unit_tests/rules/bicliquecover_bmf.rs @@ -0,0 +1,91 @@ +use super::*; +use crate::models::algebraic::BMF; +use crate::models::graph::BicliqueCover; +use crate::rules::{ReduceTo, ReductionResult}; +use crate::solvers::{BruteForce, Solver}; +use crate::topology::BipartiteGraph; +use crate::traits::Problem; +use crate::types::Min; + +#[test] +fn test_bicliquecover_to_bmf_structure() { + // Graph with edges (0,0) and (1,1), k=2 → BMF target is 2x2 identity, rank 2. + let problem = BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]), 2); + let reduction: ReductionBicliqueCoverToBMF = ReduceTo::::reduce_to(&problem); + let target = reduction.target_problem(); + assert_eq!(target.rows(), 2); + assert_eq!(target.cols(), 2); + assert_eq!(target.rank(), 2); + assert_eq!(target.matrix(), &[vec![true, false], vec![false, true]][..]); +} + +#[test] +fn test_bicliquecover_to_bmf_overhead_matches_target_shape() { + let problem = BicliqueCover::new(BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 2)]), 2); + let reduction: ReductionBicliqueCoverToBMF = ReduceTo::::reduce_to(&problem); + let target = reduction.target_problem(); + + let entry = inventory::iter::() + .find(|entry| entry.source_name == "BicliqueCover" && entry.target_name == "BMF") + .expect("BicliqueCover -> BMF reduction should be registered"); + let overhead = (entry.overhead_eval_fn)(&problem as &dyn std::any::Any); + + assert_eq!(overhead.get("rows"), Some(target.rows())); + assert_eq!(overhead.get("cols"), Some(target.cols())); + assert_eq!(overhead.get("rank"), Some(target.rank())); +} + +#[test] +fn test_bicliquecover_to_bmf_closed_loop_full_biclique() { + // K_{2,2} at rank 1 — single biclique covers all 4 edges. + let problem = BicliqueCover::new( + BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]), + 1, + ); + let reduction: ReductionBicliqueCoverToBMF = ReduceTo::::reduce_to(&problem); + let target = reduction.target_problem(); + + let bf_source = BruteForce::new().solve(&problem); + let target_witness = BruteForce::new() + .find_witness(target) + .expect("target must be feasible"); + let extracted = reduction.extract_solution(&target_witness); + assert_eq!(problem.evaluate(&extracted), bf_source); +} + +#[test] +fn test_bicliquecover_to_bmf_closed_loop_identity_rank2() { + // Identity-biadjacency at rank 2 — exact factorization needs two singleton bicliques. + let problem = BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]), 2); + let reduction: ReductionBicliqueCoverToBMF = ReduceTo::::reduce_to(&problem); + let target = reduction.target_problem(); + + let bf_source = BruteForce::new().solve(&problem); + let target_witness = BruteForce::new() + .find_witness(target) + .expect("target must be feasible"); + let extracted = reduction.extract_solution(&target_witness); + assert_eq!(problem.evaluate(&extracted), bf_source); +} + +#[test] +fn test_bicliquecover_to_bmf_insufficient_rank() { + // Identity biadjacency at rank 1 — infeasible for both problems. + let problem = BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]), 1); + let reduction: ReductionBicliqueCoverToBMF = ReduceTo::::reduce_to(&problem); + let target = reduction.target_problem(); + assert_eq!(BruteForce::new().solve(&problem), Min(None)); + assert_eq!(BruteForce::new().solve(target), Min(None)); +} + +#[test] +fn test_config_roundtrip_bc_bmf() { + // The transpose helpers must invert each other. + use crate::rules::bmf_bicliquecover::{config_bc_to_bmf, config_bmf_to_bc}; + let (m, n, k) = (2, 3, 2); + let bc = vec![1, 0, 0, 1, 1, 0, 1, 1, 0, 1]; // length (m+n)*k = 10 + let bmf = config_bc_to_bmf(&bc, m, n, k); + assert_eq!(bmf.len(), m * k + k * n); + let bc_back = config_bmf_to_bc(&bmf, m, n, k); + assert_eq!(bc_back, bc); +} diff --git a/src/unit_tests/rules/bicliquecover_ilp.rs b/src/unit_tests/rules/bicliquecover_ilp.rs deleted file mode 100644 index db542b597..000000000 --- a/src/unit_tests/rules/bicliquecover_ilp.rs +++ /dev/null @@ -1,70 +0,0 @@ -use super::*; -use crate::models::algebraic::ILP; -use crate::models::graph::BicliqueCover; -use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; -use crate::rules::ReduceTo; -use crate::topology::BipartiteGraph; -use crate::traits::Problem; - -fn small_instance() -> BicliqueCover { - // L={0,1}, R={0,1,2}, edges: (0,0),(0,1),(1,1),(1,2), k=2 - BicliqueCover::new( - BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 1), (1, 2)]), - 2, - ) -} - -#[test] -fn test_bicliquecover_to_ilp_closed_loop() { - let source = small_instance(); - let reduction: ReductionBicliqueCoverToILP = ReduceTo::>::reduce_to(&source); - assert_optimization_round_trip_from_optimization_target( - &source, - &reduction, - "BicliqueCover -> ILP round trip", - ); -} - -#[test] -fn test_reduction_shape() { - let source = small_instance(); - let reduction: ReductionBicliqueCoverToILP = ReduceTo::>::reduce_to(&source); - let ilp = reduction.target_problem(); - // n=5, k=2, left=2, right=3 - // x vars: 5*2=10, z vars: 2*3*2=12, total=22 - assert_eq!(ilp.num_vars, 22); -} - -#[test] -fn test_ilp_solution_is_valid_cover() { - let source = small_instance(); - let reduction: ReductionBicliqueCoverToILP = ReduceTo::>::reduce_to(&source); - let ilp = reduction.target_problem(); - let solver = crate::solvers::ILPSolver::new(); - let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol); - let value = source.evaluate(&extracted); - assert!( - value.0.is_some(), - "extracted solution should be a valid cover" - ); -} - -#[test] -fn test_single_edge() { - // Single edge needs 1 biclique - let source = BicliqueCover::new(BipartiteGraph::new(1, 1, vec![(0, 0)]), 1); - let reduction: ReductionBicliqueCoverToILP = ReduceTo::>::reduce_to(&source); - assert_optimization_round_trip_from_optimization_target( - &source, - &reduction, - "single edge biclique cover", - ); -} - -#[test] -fn test_bicliquecover_to_ilp_bf_vs_ilp() { - let source = small_instance(); - let reduction: ReductionBicliqueCoverToILP = ReduceTo::>::reduce_to(&source); - crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); -} diff --git a/src/unit_tests/rules/bmf_bicliquecover.rs b/src/unit_tests/rules/bmf_bicliquecover.rs new file mode 100644 index 000000000..216b79be6 --- /dev/null +++ b/src/unit_tests/rules/bmf_bicliquecover.rs @@ -0,0 +1,67 @@ +use super::*; +use crate::models::algebraic::BMF; +use crate::models::graph::BicliqueCover; +use crate::rules::{ReduceTo, ReductionResult}; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; +use crate::types::Min; + +#[test] +fn test_bmf_to_bicliquecover_structure() { + // Matrix A = [[1,0],[0,1]] => bipartite graph with edges (0,0), (1,1). + let problem = BMF::new(vec![vec![true, false], vec![false, true]], 2); + let reduction: ReductionBMFToBicliqueCover = ReduceTo::::reduce_to(&problem); + let target = reduction.target_problem(); + assert_eq!(target.left_size(), 2); + assert_eq!(target.right_size(), 2); + assert_eq!(target.num_edges(), 2); + assert_eq!(target.k(), 2); +} + +#[test] +fn test_bmf_to_bicliquecover_closed_loop_all_ones() { + // All-ones 2x2 at rank 1 — exact factorization exists. + let problem = BMF::new(vec![vec![true, true], vec![true, true]], 1); + let reduction: ReductionBMFToBicliqueCover = ReduceTo::::reduce_to(&problem); + let target = reduction.target_problem(); + + let bf_source = BruteForce::new().solve(&problem); + let target_witness = BruteForce::new() + .find_witness(target) + .expect("target has feasible biclique cover"); + let extracted = reduction.extract_solution(&target_witness); + + assert_eq!(problem.evaluate(&extracted), bf_source); + assert!(problem.is_exact(&extracted)); +} + +#[test] +fn test_bmf_to_bicliquecover_closed_loop_identity() { + // 2x2 identity at rank 2 — exact factorization exists. + let problem = BMF::new(vec![vec![true, false], vec![false, true]], 2); + let reduction: ReductionBMFToBicliqueCover = ReduceTo::::reduce_to(&problem); + let target = reduction.target_problem(); + + let bf_source = BruteForce::new().solve(&problem); + let target_witness = BruteForce::new() + .find_witness(target) + .expect("target has feasible biclique cover"); + let extracted = reduction.extract_solution(&target_witness); + + assert_eq!(problem.evaluate(&extracted), bf_source); + assert!(problem.is_exact(&extracted)); +} + +#[test] +fn test_bmf_to_bicliquecover_insufficient_rank() { + // 2x2 identity at rank 1 has no exact factorization. Under classical + // sub-biclique semantics a single biclique covering both (0,0) and (1,1) + // would have to be the full K_{2,2}, which requires edges (0,1) and (1,0) + // that are not in G. So BicliqueCover is infeasible too, matching BMF. + let problem = BMF::new(vec![vec![true, false], vec![false, true]], 1); + let reduction: ReductionBMFToBicliqueCover = ReduceTo::::reduce_to(&problem); + let target = reduction.target_problem(); + + assert_eq!(BruteForce::new().solve(&problem), Min(None)); + assert_eq!(BruteForce::new().solve(target), Min(None)); +} diff --git a/src/unit_tests/rules/bmf_ilp.rs b/src/unit_tests/rules/bmf_ilp.rs index a01508087..1cbfb20ad 100644 --- a/src/unit_tests/rules/bmf_ilp.rs +++ b/src/unit_tests/rules/bmf_ilp.rs @@ -9,8 +9,8 @@ fn test_bmf_to_ilp_structure() { let problem = BMF::new(vec![vec![true, false], vec![false, true]], 1); let reduction: ReductionBMFToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); - // b: 2*1=2, c: 1*2=2, p: 2*1*2=4, w: 2*2=4, e: 2*2=4 => 16 - assert_eq!(ilp.num_vars, 16); + // b: 2*1=2, c: 1*2=2, p: 2*1*2=4, w: 2*2=4 => 12 (no error variables) + assert_eq!(ilp.num_vars, 12); assert_eq!(ilp.sense, ObjectiveSense::Minimize); } @@ -25,7 +25,8 @@ fn test_bmf_to_ilp_closed_loop() { #[test] fn test_bmf_to_ilp_bf_vs_ilp() { - let problem = BMF::new(vec![vec![true, true], vec![true, false]], 1); + // All-ones 2x2 has an exact rank-1 factorization (boolean rank 1). + let problem = BMF::new(vec![vec![true, true], vec![true, true]], 1); let reduction: ReductionBMFToILP = ReduceTo::>::reduce_to(&problem); assert_bf_vs_ilp(&problem, &reduction); } @@ -36,6 +37,6 @@ fn test_bmf_to_ilp_trivial() { let problem = BMF::new(vec![vec![true]], 1); let reduction: ReductionBMFToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); - // b: 1, c: 1, p: 1, w: 1, e: 1 => 5 - assert_eq!(ilp.num_vars, 5); + // b: 1, c: 1, p: 1, w: 1 => 4 (no error variables) + assert_eq!(ilp.num_vars, 4); } diff --git a/tests/data/jl/biclique_cover.json b/tests/data/jl/biclique_cover.json deleted file mode 100644 index 06054b5c9..000000000 --- a/tests/data/jl/biclique_cover.json +++ /dev/null @@ -1 +0,0 @@ -{"instances":[{"label":"doc_6vertex","instance":{"left_size":3,"k":2,"num_vertices":6,"right_size":3,"edges":[[0,3],[0,4],[1,3],[1,4],[2,5]]},"evaluations":[{"is_valid":false,"config":[1,0,0,0,1,1,1,1,0,1,1,0],"size":0},{"is_valid":false,"config":[0,1,1,0,0,1,0,1,0,1,1,0],"size":0},{"is_valid":true,"config":[1,1,1,1,1,1,1,1,1,1,1,1],"size":12},{"is_valid":false,"config":[1,1,1,1,0,1,0,0,0,1,1,1],"size":0},{"is_valid":false,"config":[0,0,0,0,0,0,1,0,1,1,0,0],"size":0},{"is_valid":false,"config":[0,0,0,1,1,1,1,0,1,1,0,1],"size":0},{"is_valid":false,"config":[1,1,0,1,0,0,1,0,0,1,1,0],"size":0},{"is_valid":false,"config":[0,1,1,0,1,1,1,0,1,0,1,1],"size":0},{"is_valid":false,"config":[0,0,0,0,0,0,0,0,0,0,0,0],"size":0},{"is_valid":false,"config":[0,0,1,0,0,1,0,1,0,0,0,0],"size":0}],"best_solutions":[[1,0,1,0,1,0,1,0,1,0,1,0],[0,1,0,1,1,0,0,1,0,1,1,0],[1,0,1,0,0,1,1,0,1,0,0,1],[0,1,0,1,0,1,0,1,0,1,0,1]]}],"problem_type":"BicliqueCover"} \ No newline at end of file diff --git a/tests/data/jl/bmf.json b/tests/data/jl/bmf.json deleted file mode 100644 index e92047bda..000000000 --- a/tests/data/jl/bmf.json +++ /dev/null @@ -1 +0,0 @@ -{"instances":[{"label":"doc_3x3_ones","instance":{"m":3,"k":2,"matrix":[[1,1,1],[1,1,1],[1,1,1]],"n":3},"evaluations":[{"is_valid":true,"config":[1,1,0,1,0,0,1,0,1,1,1,0],"size":4},{"is_valid":true,"config":[0,1,1,0,1,0,1,1,0,1,1,1],"size":2},{"is_valid":true,"config":[1,0,0,0,0,1,0,1,0,0,1,1],"size":6},{"is_valid":true,"config":[1,1,1,1,1,1,1,1,1,1,1,1],"size":0},{"is_valid":true,"config":[1,0,1,0,0,0,0,1,0,0,0,1],"size":7},{"is_valid":true,"config":[0,1,0,0,0,1,0,1,0,1,0,0],"size":7},{"is_valid":true,"config":[1,1,0,0,0,1,0,1,0,0,0,1],"size":6},{"is_valid":true,"config":[0,0,0,0,0,0,0,0,0,0,0,0],"size":9},{"is_valid":true,"config":[0,1,1,1,0,0,0,0,0,1,0,0],"size":7},{"is_valid":true,"config":[1,0,1,0,0,1,1,0,0,0,0,0],"size":7}],"best_solutions":[[1,0,1,0,1,0,1,1,1,0,0,0],[1,1,1,0,1,0,1,1,1,0,0,0],[1,0,1,1,1,0,1,1,1,0,0,0],[1,1,1,1,1,0,1,1,1,0,0,0],[1,0,1,0,1,1,1,1,1,0,0,0],[1,1,1,0,1,1,1,1,1,0,0,0],[1,0,1,1,1,1,1,1,1,0,0,0],[1,1,1,1,1,1,1,1,1,0,0,0],[1,1,1,1,1,1,0,1,1,1,0,0],[1,0,1,0,1,0,1,1,1,1,0,0],[1,1,1,0,1,0,1,1,1,1,0,0],[1,0,1,1,1,0,1,1,1,1,0,0],[1,1,1,1,1,0,1,1,1,1,0,0],[1,0,1,0,1,1,1,1,1,1,0,0],[1,1,1,0,1,1,1,1,1,1,0,0],[1,0,1,1,1,1,1,1,1,1,0,0],[1,1,1,1,1,1,1,1,1,1,0,0],[1,1,1,1,1,1,1,0,1,0,1,0],[1,0,1,0,1,0,1,1,1,0,1,0],[1,1,1,0,1,0,1,1,1,0,1,0],[1,0,1,1,1,0,1,1,1,0,1,0],[1,1,1,1,1,0,1,1,1,0,1,0],[1,0,1,0,1,1,1,1,1,0,1,0],[1,1,1,0,1,1,1,1,1,0,1,0],[1,0,1,1,1,1,1,1,1,0,1,0],[1,1,1,1,1,1,1,1,1,0,1,0],[1,1,1,1,1,1,0,0,1,1,1,0],[1,1,1,1,1,1,1,0,1,1,1,0],[1,1,1,1,1,1,0,1,1,1,1,0],[1,0,1,0,1,0,1,1,1,1,1,0],[1,1,1,0,1,0,1,1,1,1,1,0],[1,0,1,1,1,0,1,1,1,1,1,0],[1,1,1,1,1,0,1,1,1,1,1,0],[1,0,1,0,1,1,1,1,1,1,1,0],[1,1,1,0,1,1,1,1,1,1,1,0],[1,0,1,1,1,1,1,1,1,1,1,0],[1,1,1,1,1,1,1,1,1,1,1,0],[1,1,1,1,1,1,1,1,0,0,0,1],[1,0,1,0,1,0,1,1,1,0,0,1],[1,1,1,0,1,0,1,1,1,0,0,1],[1,0,1,1,1,0,1,1,1,0,0,1],[1,1,1,1,1,0,1,1,1,0,0,1],[1,0,1,0,1,1,1,1,1,0,0,1],[1,1,1,0,1,1,1,1,1,0,0,1],[1,0,1,1,1,1,1,1,1,0,0,1],[1,1,1,1,1,1,1,1,1,0,0,1],[1,1,1,1,1,1,0,1,0,1,0,1],[1,1,1,1,1,1,1,1,0,1,0,1],[1,1,1,1,1,1,0,1,1,1,0,1],[1,0,1,0,1,0,1,1,1,1,0,1],[1,1,1,0,1,0,1,1,1,1,0,1],[1,0,1,1,1,0,1,1,1,1,0,1],[1,1,1,1,1,0,1,1,1,1,0,1],[1,0,1,0,1,1,1,1,1,1,0,1],[1,1,1,0,1,1,1,1,1,1,0,1],[1,0,1,1,1,1,1,1,1,1,0,1],[1,1,1,1,1,1,1,1,1,1,0,1],[1,1,1,1,1,1,1,0,0,0,1,1],[1,1,1,1,1,1,1,1,0,0,1,1],[1,1,1,1,1,1,1,0,1,0,1,1],[1,0,1,0,1,0,1,1,1,0,1,1],[1,1,1,0,1,0,1,1,1,0,1,1],[1,0,1,1,1,0,1,1,1,0,1,1],[1,1,1,1,1,0,1,1,1,0,1,1],[1,0,1,0,1,1,1,1,1,0,1,1],[1,1,1,0,1,1,1,1,1,0,1,1],[1,0,1,1,1,1,1,1,1,0,1,1],[1,1,1,1,1,1,1,1,1,0,1,1],[0,1,0,1,0,1,0,0,0,1,1,1],[1,1,0,1,0,1,0,0,0,1,1,1],[0,1,1,1,0,1,0,0,0,1,1,1],[1,1,1,1,0,1,0,0,0,1,1,1],[0,1,0,1,1,1,0,0,0,1,1,1],[1,1,0,1,1,1,0,0,0,1,1,1],[0,1,1,1,1,1,0,0,0,1,1,1],[1,1,1,1,1,1,0,0,0,1,1,1],[0,1,0,1,0,1,1,0,0,1,1,1],[1,1,0,1,0,1,1,0,0,1,1,1],[0,1,1,1,0,1,1,0,0,1,1,1],[1,1,1,1,0,1,1,0,0,1,1,1],[0,1,0,1,1,1,1,0,0,1,1,1],[1,1,0,1,1,1,1,0,0,1,1,1],[0,1,1,1,1,1,1,0,0,1,1,1],[1,1,1,1,1,1,1,0,0,1,1,1],[0,1,0,1,0,1,0,1,0,1,1,1],[1,1,0,1,0,1,0,1,0,1,1,1],[0,1,1,1,0,1,0,1,0,1,1,1],[1,1,1,1,0,1,0,1,0,1,1,1],[0,1,0,1,1,1,0,1,0,1,1,1],[1,1,0,1,1,1,0,1,0,1,1,1],[0,1,1,1,1,1,0,1,0,1,1,1],[1,1,1,1,1,1,0,1,0,1,1,1],[0,1,0,1,0,1,1,1,0,1,1,1],[1,1,0,1,0,1,1,1,0,1,1,1],[0,1,1,1,0,1,1,1,0,1,1,1],[1,1,1,1,0,1,1,1,0,1,1,1],[0,1,0,1,1,1,1,1,0,1,1,1],[1,1,0,1,1,1,1,1,0,1,1,1],[0,1,1,1,1,1,1,1,0,1,1,1],[1,1,1,1,1,1,1,1,0,1,1,1],[0,1,0,1,0,1,0,0,1,1,1,1],[1,1,0,1,0,1,0,0,1,1,1,1],[0,1,1,1,0,1,0,0,1,1,1,1],[1,1,1,1,0,1,0,0,1,1,1,1],[0,1,0,1,1,1,0,0,1,1,1,1],[1,1,0,1,1,1,0,0,1,1,1,1],[0,1,1,1,1,1,0,0,1,1,1,1],[1,1,1,1,1,1,0,0,1,1,1,1],[0,1,0,1,0,1,1,0,1,1,1,1],[1,1,0,1,0,1,1,0,1,1,1,1],[0,1,1,1,0,1,1,0,1,1,1,1],[1,1,1,1,0,1,1,0,1,1,1,1],[0,1,0,1,1,1,1,0,1,1,1,1],[1,1,0,1,1,1,1,0,1,1,1,1],[0,1,1,1,1,1,1,0,1,1,1,1],[1,1,1,1,1,1,1,0,1,1,1,1],[0,1,0,1,0,1,0,1,1,1,1,1],[1,1,0,1,0,1,0,1,1,1,1,1],[0,1,1,1,0,1,0,1,1,1,1,1],[1,1,1,1,0,1,0,1,1,1,1,1],[0,1,0,1,1,1,0,1,1,1,1,1],[1,1,0,1,1,1,0,1,1,1,1,1],[0,1,1,1,1,1,0,1,1,1,1,1],[1,1,1,1,1,1,0,1,1,1,1,1],[1,0,1,0,1,0,1,1,1,1,1,1],[0,1,1,0,1,0,1,1,1,1,1,1],[1,1,1,0,1,0,1,1,1,1,1,1],[1,0,0,1,1,0,1,1,1,1,1,1],[0,1,0,1,1,0,1,1,1,1,1,1],[1,1,0,1,1,0,1,1,1,1,1,1],[1,0,1,1,1,0,1,1,1,1,1,1],[0,1,1,1,1,0,1,1,1,1,1,1],[1,1,1,1,1,0,1,1,1,1,1,1],[1,0,1,0,0,1,1,1,1,1,1,1],[0,1,1,0,0,1,1,1,1,1,1,1],[1,1,1,0,0,1,1,1,1,1,1,1],[1,0,0,1,0,1,1,1,1,1,1,1],[0,1,0,1,0,1,1,1,1,1,1,1],[1,1,0,1,0,1,1,1,1,1,1,1],[1,0,1,1,0,1,1,1,1,1,1,1],[0,1,1,1,0,1,1,1,1,1,1,1],[1,1,1,1,0,1,1,1,1,1,1,1],[1,0,1,0,1,1,1,1,1,1,1,1],[0,1,1,0,1,1,1,1,1,1,1,1],[1,1,1,0,1,1,1,1,1,1,1,1],[1,0,0,1,1,1,1,1,1,1,1,1],[0,1,0,1,1,1,1,1,1,1,1,1],[1,1,0,1,1,1,1,1,1,1,1,1],[1,0,1,1,1,1,1,1,1,1,1,1],[0,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1,1,1,1,1]]}],"problem_type":"BMF"} \ No newline at end of file diff --git a/tests/suites/integration.rs b/tests/suites/integration.rs index 6c29a4ee6..52870605a 100644 --- a/tests/suites/integration.rs +++ b/tests/suites/integration.rs @@ -283,13 +283,14 @@ mod all_problems_solvable { #[test] fn test_bmf_solvable() { + // All-ones 2x2 at rank 1 has an exact boolean factorization. let problem = BMF::new(vec![vec![true, true], vec![true, true]], 1); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { - // BMF minimizes Hamming distance, all configs are valid (no invalid marker) - let _ = problem.evaluate(sol); + // BMF evaluates to Min(Some(total_factor_size)) only when B*C = A exactly. + assert!(problem.is_exact(sol)); } } }