diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 6202fbde..c7cb2195 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -60,6 +60,7 @@ "SubsetSum": [Subset Sum], "MinimumFeedbackArcSet": [Minimum Feedback Arc Set], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], + "QuadraticAssignment": [Quadratic Assignment], "ShortestCommonSupersequence": [Shortest Common Supersequence], "MinimumSumMulticenter": [Minimum Sum Multicenter], "SubgraphIsomorphism": [Subgraph Isomorphism], @@ -802,6 +803,56 @@ Integer Linear Programming is a universal modeling framework: virtually every NP ) ] +#problem-def("QuadraticAssignment")[ + Given $n$ facilities and $m$ locations ($n <= m$), a flow matrix $C in ZZ^(n times n)$ representing flows between facilities, and a distance matrix $D in ZZ^(m times m)$ representing distances between locations, find an injective assignment $f: {1, dots, n} -> {1, dots, m}$ that minimizes + $ sum_(i != j) C_(i j) dot D_(f(i), f(j)). $ +][ +The Quadratic Assignment Problem was introduced by Koopmans and Beckmann (1957) to model the optimal placement of economic activities (facilities) across geographic locations, minimizing total transportation cost weighted by inter-facility flows. It is NP-hard, as shown by Sahni and Gonzalez (1976) via reduction from the Hamiltonian Circuit problem. QAP is widely regarded as one of the hardest combinatorial optimization problems: even moderate instances ($n > 20$) challenge state-of-the-art exact solvers. Best exact approaches use branch-and-bound with Gilmore--Lawler bounds or cutting-plane methods; the best known general algorithm runs in $O^*(n!)$ by exhaustive enumeration of all permutations#footnote[No algorithm significantly improving on brute-force permutation enumeration is known for general QAP.]. + +Applications include facility layout planning, keyboard and control panel design, scheduling, VLSI placement, and hospital floor planning. As a special case, when $D$ is a distance matrix on a line (i.e., $D_(k l) = |k - l|$), QAP reduces to the Optimal Linear Arrangement problem. + +*Example.* Consider $n = m = 4$ with flow matrix $C$ and distance matrix $D$: +$ C = mat(0, 3, 0, 2; 3, 0, 0, 1; 0, 0, 0, 4; 2, 1, 4, 0), quad D = mat(0, 1, 2, 3; 1, 0, 1, 2; 2, 1, 0, 1; 3, 2, 1, 0). $ +The identity assignment $f(i) = i$ gives cost $sum_(i != j) C_(i j) dot D_(i, j) = 3 dot 1 + 2 dot 3 + 3 dot 1 + 1 dot 2 + 4 dot 1 + 2 dot 3 + 1 dot 2 + 4 dot 1 = 3 + 6 + 3 + 2 + 4 + 6 + 2 + 4 = 30$. However, the assignment $f = (1, 2, 4, 3)$ — swapping facilities 3 and 4 — gives cost $3 dot 1 + 2 dot 2 + 3 dot 1 + 1 dot 1 + 4 dot 1 + 2 dot 2 + 1 dot 1 + 4 dot 1 = 3 + 4 + 3 + 1 + 4 + 4 + 1 + 4 = 24$. The optimal assignment is $f^* = (3, 4, 1, 2)$ with cost 22: it places the heavily interacting facilities 3 and 4 (flow 4) at adjacent locations. + +#figure( + canvas(length: 1cm, { + import draw: * + // Facility column (left) + let fac-x = 0 + let loc-x = 5 + let ys = (3, 2, 1, 0) + // Draw facility nodes + for i in range(4) { + circle((fac-x, ys.at(i)), radius: 0.3, fill: graph-colors.at(0), stroke: 0.8pt + graph-colors.at(0), name: "f" + str(i)) + content("f" + str(i), text(fill: white, 8pt)[$F_#(i+1)$]) + } + // Draw location nodes + for j in range(4) { + circle((loc-x, ys.at(j)), radius: 0.3, fill: graph-colors.at(1), stroke: 0.8pt + graph-colors.at(1), name: "l" + str(j)) + content("l" + str(j), text(fill: white, 8pt)[$L_#(j+1)$]) + } + // Column labels + content((fac-x, 3.7), text(9pt, weight: "bold")[Facilities]) + content((loc-x, 3.7), text(9pt, weight: "bold")[Locations]) + // Optimal assignment f* = (3, 4, 1, 2): F1→L3, F2→L4, F3→L1, F4→L2 + let assignments = ((0, 2), (1, 3), (2, 0), (3, 1)) + for (fi, li) in assignments { + line("f" + str(fi) + ".east", "l" + str(li) + ".west", + mark: (end: "straight"), stroke: 1.2pt + luma(80)) + } + // Annotate key flow: F3↔F4 have flow 4 + on-layer(-1, { + rect((-0.55, -0.55), (0.55, 1.55), + fill: graph-colors.at(0).transparentize(92%), + stroke: (dash: "dashed", paint: graph-colors.at(0).transparentize(50%), thickness: 0.6pt)) + }) + content((fac-x, -0.9), text(6pt, fill: luma(100))[flow$(F_3, F_4) = 4$]) + }), + caption: [Optimal assignment $f^* = (3, 4, 1, 2)$ for the $4 times 4$ QAP instance. Facilities (blue, left) are assigned to locations (red, right) by arrows. Facilities $F_3$ and $F_4$ (highest flow $= 4$) are assigned to adjacent locations $L_1$ and $L_2$ (distance $= 1$). Total cost $= 22$.], +) +] + #problem-def("ClosestVectorProblem")[ Given a lattice basis $bold(B) in RR^(m times n)$ (columns $bold(b)_1, dots, bold(b)_n in RR^m$ spanning lattice $cal(L)(bold(B)) = {bold(B) bold(x) : bold(x) in ZZ^n}$) and target $bold(t) in RR^m$, find $bold(x) in ZZ^n$ minimizing $norm(bold(B) bold(x) - bold(t))_2$. ][ diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index e8f6f968..25a6f0c8 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -462,6 +462,22 @@ } ] }, + { + "name": "QuadraticAssignment", + "description": "Minimize total cost of assigning facilities to locations", + "fields": [ + { + "name": "cost_matrix", + "type_name": "Vec>", + "description": "Flow/cost matrix between facilities" + }, + { + "name": "distance_matrix", + "type_name": "Vec>", + "description": "Distance matrix between locations" + } + ] + }, { "name": "RuralPostman", "description": "Find a circuit covering required edges with total length at most B (Rural Postman Problem)", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 64d98d72..3c51d06b 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -225,6 +225,7 @@ Flags by problem type: LCS --strings FAS --arcs [--weights] [--num-vertices] FVS --arcs [--weights] [--num-vertices] + QAP --matrix (cost), --distance-matrix FlowShopScheduling --task-lengths, --deadline [--num-processors] SCS --strings, --bound [--alphabet-size] ILP, CircuitSAT (via reduction only) @@ -356,6 +357,9 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Distance matrix for QuadraticAssignment (semicolon-separated rows, e.g., "0,1,2;1,0,1;2,1,0") + #[arg(long)] + pub distance_matrix: Option, /// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3") #[arg(long)] pub task_lengths: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index c7a80991..131ef530 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -56,6 +56,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.pattern.is_none() && args.strings.is_none() && args.arcs.is_none() + && args.distance_matrix.is_none() && args.task_lengths.is_none() && args.deadline.is_none() && args.num_processors.is_none() @@ -101,6 +102,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "Satisfiability" => "--num-vars 3 --clauses \"1,2;-1,3\"", "KSatisfiability" => "--num-vars 3 --clauses \"1,2,3;-1,2,-3\" --k 3", "QUBO" => "--matrix \"1,0.5;0.5,2\"", + "QuadraticAssignment" => "--matrix \"0,5;5,0\" --distance-matrix \"0,1;1,0\"", "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", "KColoring" => "--graph 0-1,1-2,2-0 --k 3", "MinimumSumMulticenter" => { @@ -375,6 +377,54 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { util::ser_ksat(num_vars, clauses, k)? } + // QuadraticAssignment + "QuadraticAssignment" => { + let cost_str = args.matrix.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "QuadraticAssignment requires --matrix (cost) and --distance-matrix\n\n\ + Usage: pred create QAP --matrix \"0,5;5,0\" --distance-matrix \"0,1;1,0\"" + ) + })?; + let dist_str = args.distance_matrix.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "QuadraticAssignment requires --distance-matrix\n\n\ + Usage: pred create QAP --matrix \"0,5;5,0\" --distance-matrix \"0,1;1,0\"" + ) + })?; + let cost_matrix = parse_i64_matrix(cost_str).context("Invalid cost matrix")?; + let distance_matrix = parse_i64_matrix(dist_str).context("Invalid distance matrix")?; + let n = cost_matrix.len(); + for (i, row) in cost_matrix.iter().enumerate() { + if row.len() != n { + bail!( + "cost matrix must be square: row {i} has {} columns, expected {n}", + row.len() + ); + } + } + let m = distance_matrix.len(); + for (i, row) in distance_matrix.iter().enumerate() { + if row.len() != m { + bail!( + "distance matrix must be square: row {i} has {} columns, expected {m}", + row.len() + ); + } + } + if n > m { + bail!("num_facilities ({n}) must be <= num_locations ({m})"); + } + ( + ser( + problemreductions::models::algebraic::QuadraticAssignment::new( + cost_matrix, + distance_matrix, + ), + )?, + resolved_variant.clone(), + ) + } + // QUBO "QUBO" => { let matrix = parse_matrix(args)?; @@ -1209,6 +1259,37 @@ fn parse_matrix(args: &CreateArgs) -> Result>> { .collect() } +/// Parse a semicolon-separated matrix of i64 values. +/// E.g., "0,5;5,0" +fn parse_i64_matrix(s: &str) -> Result>> { + let matrix: Vec> = s + .split(';') + .enumerate() + .map(|(row_idx, row)| { + row.trim() + .split(',') + .enumerate() + .map(|(col_idx, v)| { + v.trim().parse::().map_err(|e| { + anyhow::anyhow!("Invalid value at row {row_idx}, col {col_idx}: {e}") + }) + }) + .collect() + }) + .collect::>()?; + if let Some(first_len) = matrix.first().map(|r| r.len()) { + for (i, row) in matrix.iter().enumerate() { + if row.len() != first_len { + bail!( + "Ragged matrix: row {i} has {} columns, expected {first_len}", + row.len() + ); + } + } + } + Ok(matrix) +} + /// Parse `--arcs` as directed arc pairs and build a `DirectedGraph`. /// /// Returns `(graph, num_arcs)`. Infers vertex count from arc endpoints diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 2d95f3bb..36e4ef23 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Context, Result}; -use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; +use problemreductions::models::algebraic::{ClosestVectorProblem, QuadraticAssignment, ILP}; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, Knapsack, LongestCommonSubsequence, ShortestCommonSupersequence, SubsetSum, @@ -229,6 +229,7 @@ pub fn load_problem( }, "MaximumSetPacking" => deser_opt::>(data), "MinimumSetCovering" => deser_opt::>(data), + "QuadraticAssignment" => deser_opt::(data), "QUBO" => deser_opt::>(data), "SpinGlass" => match variant.get("weight").map(|s| s.as_str()) { Some("f64") => deser_opt::>(data), @@ -304,6 +305,7 @@ pub fn serialize_any_problem( _ => try_ser::>(any), }, "MinimumSetCovering" => try_ser::>(any), + "QuadraticAssignment" => try_ser::(any), "QUBO" => try_ser::>(any), "SpinGlass" => match variant.get("weight").map(|s| s.as_str()) { Some("f64") => try_ser::>(any), diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 55d1cf90..edd5caa4 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -24,6 +24,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("LCS", "LongestCommonSubsequence"), ("MaxMatching", "MaximumMatching"), ("FVS", "MinimumFeedbackVertexSet"), + ("QAP", "QuadraticAssignment"), ("SCS", "ShortestCommonSupersequence"), ("FAS", "MinimumFeedbackArcSet"), ("pmedian", "MinimumSumMulticenter"), @@ -69,6 +70,7 @@ pub fn resolve_alias(input: &str) -> String { "fas" | "minimumfeedbackarcset" => "MinimumFeedbackArcSet".to_string(), "minimumsummulticenter" | "pmedian" => "MinimumSumMulticenter".to_string(), "subsetsum" => "SubsetSum".to_string(), + "quadraticassignment" | "qap" => "QuadraticAssignment".to_string(), "scs" | "shortestcommonsupersequence" => "ShortestCommonSupersequence".to_string(), "hamiltonianpath" => "HamiltonianPath".to_string(), _ => input.to_string(), // pass-through for exact names diff --git a/src/lib.rs b/src/lib.rs index 4e0596ee..831b2235 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ pub mod variant; /// Prelude module for convenient imports. pub mod prelude { // Problem types - pub use crate::models::algebraic::{BMF, QUBO}; + pub use crate::models::algebraic::{QuadraticAssignment, BMF, QUBO}; pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use crate::models::graph::{ BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, SpinGlass, diff --git a/src/models/algebraic/mod.rs b/src/models/algebraic/mod.rs index 6cfc0069..48c3dede 100644 --- a/src/models/algebraic/mod.rs +++ b/src/models/algebraic/mod.rs @@ -5,13 +5,16 @@ //! - [`ILP`]: Integer Linear Programming //! - [`ClosestVectorProblem`]: Closest Vector Problem (minimize lattice distance) //! - [`BMF`]: Boolean Matrix Factorization +//! - [`QuadraticAssignment`]: Quadratic Assignment Problem pub(crate) mod bmf; mod closest_vector_problem; mod ilp; +mod quadratic_assignment; mod qubo; pub use bmf::BMF; pub use closest_vector_problem::{ClosestVectorProblem, VarBounds}; pub use ilp::{Comparison, LinearConstraint, ObjectiveSense, VariableDomain, ILP}; +pub use quadratic_assignment::QuadraticAssignment; pub use qubo::QUBO; diff --git a/src/models/algebraic/quadratic_assignment.rs b/src/models/algebraic/quadratic_assignment.rs new file mode 100644 index 00000000..68a4170e --- /dev/null +++ b/src/models/algebraic/quadratic_assignment.rs @@ -0,0 +1,178 @@ +//! Quadratic Assignment Problem (QAP) implementation. +//! +//! The QAP assigns facilities to locations to minimize the total cost, +//! where cost depends on both inter-facility flows and inter-location distances. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "QuadraticAssignment", + module_path: module_path!(), + description: "Minimize total cost of assigning facilities to locations", + fields: &[ + FieldInfo { name: "cost_matrix", type_name: "Vec>", description: "Flow/cost matrix between facilities" }, + FieldInfo { name: "distance_matrix", type_name: "Vec>", description: "Distance matrix between locations" }, + ], + } +} + +/// The Quadratic Assignment Problem (QAP). +/// +/// Given n facilities and m locations, a cost matrix C (n x n) representing +/// flows between facilities, and a distance matrix D (m x m) representing +/// distances between locations, find an injective assignment of facilities +/// to locations that minimizes: +/// +/// f(p) = sum_{i != j} C[i][j] * D[p(i)][p(j)] +/// +/// where p is an injective mapping from facilities to locations (a permutation when n == m). +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::algebraic::QuadraticAssignment; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// let cost_matrix = vec![ +/// vec![0, 1, 2], +/// vec![1, 0, 3], +/// vec![2, 3, 0], +/// ]; +/// let distance_matrix = vec![ +/// vec![0, 5, 8], +/// vec![5, 0, 3], +/// vec![8, 3, 0], +/// ]; +/// let problem = QuadraticAssignment::new(cost_matrix, distance_matrix); +/// +/// let solver = BruteForce::new(); +/// let best = solver.find_best(&problem); +/// assert!(best.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QuadraticAssignment { + /// Cost/flow matrix between facilities (n x n). + cost_matrix: Vec>, + /// Distance matrix between locations (m x m). + distance_matrix: Vec>, +} + +impl QuadraticAssignment { + /// Create a new Quadratic Assignment Problem. + /// + /// # Arguments + /// * `cost_matrix` - n x n matrix of flows/costs between facilities + /// * `distance_matrix` - m x m matrix of distances between locations + /// + /// # Panics + /// Panics if either matrix is not square, or if num_facilities > num_locations. + pub fn new(cost_matrix: Vec>, distance_matrix: Vec>) -> Self { + let n = cost_matrix.len(); + for row in &cost_matrix { + assert_eq!(row.len(), n, "cost_matrix must be square"); + } + let m = distance_matrix.len(); + for row in &distance_matrix { + assert_eq!(row.len(), m, "distance_matrix must be square"); + } + assert!( + n <= m, + "num_facilities ({n}) must be <= num_locations ({m})" + ); + Self { + cost_matrix, + distance_matrix, + } + } + + /// Get the cost/flow matrix. + pub fn cost_matrix(&self) -> &[Vec] { + &self.cost_matrix + } + + /// Get the distance matrix. + pub fn distance_matrix(&self) -> &[Vec] { + &self.distance_matrix + } + + /// Get the number of facilities. + pub fn num_facilities(&self) -> usize { + self.cost_matrix.len() + } + + /// Get the number of locations. + pub fn num_locations(&self) -> usize { + self.distance_matrix.len() + } +} + +impl Problem for QuadraticAssignment { + const NAME: &'static str = "QuadraticAssignment"; + type Metric = SolutionSize; + + fn dims(&self) -> Vec { + vec![self.num_locations(); self.num_facilities()] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + let n = self.num_facilities(); + let m = self.num_locations(); + + // Check config length matches number of facilities + if config.len() != n { + return SolutionSize::Invalid; + } + + // Check that all assignments are valid locations + for &loc in config { + if loc >= m { + return SolutionSize::Invalid; + } + } + + // Check injectivity: no two facilities assigned to the same location + let mut used = vec![false; m]; + for &loc in config { + if used[loc] { + return SolutionSize::Invalid; + } + used[loc] = true; + } + + // Compute objective: sum_{i != j} cost_matrix[i][j] * distance_matrix[config[i]][config[j]] + let mut total: i64 = 0; + for i in 0..n { + for j in 0..n { + if i != j { + total += self.cost_matrix[i][j] * self.distance_matrix[config[i]][config[j]]; + } + } + } + + SolutionSize::Valid(total) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl OptimizationProblem for QuadraticAssignment { + type Value = i64; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +crate::declare_variants! { + QuadraticAssignment => "factorial(num_facilities)", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/algebraic/quadratic_assignment.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index b22cc2a1..8277dd65 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -9,7 +9,7 @@ pub mod misc; pub mod set; // Re-export commonly used types -pub use algebraic::{ClosestVectorProblem, BMF, ILP, QUBO}; +pub use algebraic::{ClosestVectorProblem, QuadraticAssignment, BMF, ILP, QUBO}; pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use graph::{ BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, KColoring, MaxCut, diff --git a/src/unit_tests/models/algebraic/quadratic_assignment.rs b/src/unit_tests/models/algebraic/quadratic_assignment.rs new file mode 100644 index 00000000..07d2807a --- /dev/null +++ b/src/unit_tests/models/algebraic/quadratic_assignment.rs @@ -0,0 +1,162 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; + +/// Create a 4x4 test instance for reuse across tests. +/// +/// Cost matrix C: +/// [[0, 5, 2, 0], +/// [5, 0, 0, 3], +/// [2, 0, 0, 4], +/// [0, 3, 4, 0]] +/// +/// Distance matrix D: +/// [[0, 1, 2, 3], +/// [1, 0, 1, 2], +/// [2, 1, 0, 1], +/// [3, 2, 1, 0]] +fn make_test_instance() -> QuadraticAssignment { + let cost_matrix = vec![ + vec![0, 5, 2, 0], + vec![5, 0, 0, 3], + vec![2, 0, 0, 4], + vec![0, 3, 4, 0], + ]; + let distance_matrix = vec![ + vec![0, 1, 2, 3], + vec![1, 0, 1, 2], + vec![2, 1, 0, 1], + vec![3, 2, 1, 0], + ]; + QuadraticAssignment::new(cost_matrix, distance_matrix) +} + +#[test] +fn test_quadratic_assignment_creation() { + let qap = make_test_instance(); + assert_eq!(qap.num_facilities(), 4); + assert_eq!(qap.num_locations(), 4); + assert_eq!(qap.dims(), vec![4, 4, 4, 4]); + assert_eq!(qap.cost_matrix().len(), 4); + assert_eq!(qap.distance_matrix().len(), 4); +} + +#[test] +fn test_quadratic_assignment_evaluate_identity() { + let qap = make_test_instance(); + // Identity assignment f = (0, 1, 2, 3): + // cost = sum_{i != j} C[i][j] * D[i][j] + // = 5*1 + 2*2 + 0*3 + 5*1 + 0*1 + 3*2 + 2*2 + 0*1 + 4*1 + 0*3 + 3*2 + 4*1 + // = 5 + 4 + 0 + 5 + 0 + 6 + 4 + 0 + 4 + 0 + 6 + 4 = 38 + assert_eq!( + Problem::evaluate(&qap, &[0, 1, 2, 3]), + SolutionSize::Valid(38) + ); +} + +#[test] +fn test_quadratic_assignment_evaluate_swap() { + let qap = make_test_instance(); + // Assignment f = (0, 2, 1, 3): facility 1 -> loc 2, facility 2 -> loc 1 + // cost = sum_{i != j} C[i][j] * D[config[i]][config[j]] + // i=0,j=1: 5*D[0][2]=5*2=10 i=0,j=2: 2*D[0][1]=2*1=2 i=0,j=3: 0*D[0][3]=0 + // i=1,j=0: 5*D[2][0]=5*2=10 i=1,j=2: 0*D[2][1]=0*1=0 i=1,j=3: 3*D[2][3]=3*1=3 + // i=2,j=0: 2*D[1][0]=2*1=2 i=2,j=1: 0*D[1][2]=0*1=0 i=2,j=3: 4*D[1][3]=4*2=8 + // i=3,j=0: 0*D[3][0]=0 i=3,j=1: 3*D[3][2]=3*1=3 i=3,j=2: 4*D[3][1]=4*2=8 + // Total = 10+2+0+10+0+3+2+0+8+0+3+8 = 46 + assert_eq!( + Problem::evaluate(&qap, &[0, 2, 1, 3]), + SolutionSize::Valid(46) + ); +} + +#[test] +fn test_quadratic_assignment_evaluate_invalid() { + let qap = make_test_instance(); + // Duplicate location 0 — not injective, should be Invalid. + assert_eq!( + Problem::evaluate(&qap, &[0, 0, 1, 2]), + SolutionSize::Invalid + ); + // Out-of-range location index. + assert_eq!( + Problem::evaluate(&qap, &[0, 1, 2, 99]), + SolutionSize::Invalid + ); + // Wrong config length — too short. + assert_eq!(Problem::evaluate(&qap, &[0, 1, 2]), SolutionSize::Invalid); + // Wrong config length — too long. + assert_eq!( + Problem::evaluate(&qap, &[0, 1, 2, 3, 0]), + SolutionSize::Invalid + ); +} + +#[test] +fn test_quadratic_assignment_direction() { + let qap = make_test_instance(); + assert_eq!(qap.direction(), Direction::Minimize); +} + +#[test] +fn test_quadratic_assignment_serialization() { + let qap = make_test_instance(); + let json = serde_json::to_string(&qap).expect("serialize"); + let qap2: QuadraticAssignment = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(qap2.num_facilities(), 4); + assert_eq!(qap2.num_locations(), 4); + // Verify functional equivalence after round-trip. + assert_eq!( + Problem::evaluate(&qap, &[0, 1, 2, 3]), + Problem::evaluate(&qap2, &[0, 1, 2, 3]) + ); +} + +#[test] +fn test_quadratic_assignment_rectangular() { + // 2 facilities, 3 locations (n < m) + let cost_matrix = vec![vec![0, 3], vec![3, 0]]; + let distance_matrix = vec![vec![0, 1, 4], vec![1, 0, 2], vec![4, 2, 0]]; + let qap = QuadraticAssignment::new(cost_matrix, distance_matrix); + assert_eq!(qap.num_facilities(), 2); + assert_eq!(qap.num_locations(), 3); + assert_eq!(qap.dims(), vec![3, 3]); + // Assignment f=(0,1): cost = C[0][1]*D[0][1] + C[1][0]*D[1][0] = 3*1 + 3*1 = 6 + assert_eq!(Problem::evaluate(&qap, &[0, 1]), SolutionSize::Valid(6)); + // Assignment f=(0,2): cost = 3*D[0][2] + 3*D[2][0] = 3*4 + 3*4 = 24 + assert_eq!(Problem::evaluate(&qap, &[0, 2]), SolutionSize::Valid(24)); + // BruteForce should find optimal + let solver = BruteForce::new(); + let best = solver.find_best(&qap).unwrap(); + assert_eq!(Problem::evaluate(&qap, &best), SolutionSize::Valid(6)); +} + +#[test] +#[should_panic(expected = "cost_matrix must be square")] +fn test_quadratic_assignment_nonsquare_cost() { + QuadraticAssignment::new(vec![vec![0, 1]], vec![vec![0, 1], vec![1, 0]]); +} + +#[test] +#[should_panic(expected = "num_facilities")] +fn test_quadratic_assignment_too_many_facilities() { + // 3 facilities, 2 locations (n > m) -- should panic + let cost = vec![vec![0, 1, 2], vec![1, 0, 3], vec![2, 3, 0]]; + let dist = vec![vec![0, 1], vec![1, 0]]; + QuadraticAssignment::new(cost, dist); +} + +#[test] +fn test_quadratic_assignment_solver() { + let qap = make_test_instance(); + let solver = BruteForce::new(); + let best = solver.find_best(&qap); + assert!(best.is_some()); + let best_config = best.unwrap(); + // The brute-force solver finds the optimal assignment with cost 36. + assert_eq!( + Problem::evaluate(&qap, &best_config), + SolutionSize::Valid(36) + ); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 20fea88a..b5233865 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -77,6 +77,10 @@ fn test_all_problems_implement_trait_correctly() { "BicliqueCover", ); check_problem_trait(&Factoring::new(6, 2, 2), "Factoring"); + check_problem_trait( + &QuadraticAssignment::new(vec![vec![0, 1], vec![1, 0]], vec![vec![0, 1], vec![1, 0]]), + "QuadraticAssignment", + ); let circuit = Circuit::new(vec![Assignment::new( vec!["x".to_string()], @@ -159,6 +163,11 @@ fn test_direction() { BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0)]), 1).direction(), Direction::Minimize ); + assert_eq!( + QuadraticAssignment::new(vec![vec![0, 1], vec![1, 0]], vec![vec![0, 1], vec![1, 0]]) + .direction(), + Direction::Minimize + ); assert_eq!( MinimumFeedbackArcSet::new( DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]),