diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index f74d8bd51..9c3ae9339 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -110,6 +110,7 @@ "SubsetSum": [Subset Sum], "MinimumFeedbackArcSet": [Minimum Feedback Arc Set], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], + "QuadraticAssignment": [Quadratic Assignment], "SequencingWithReleaseTimesAndDeadlines": [Sequencing with Release Times and Deadlines], "MultipleChoiceBranching": [Multiple Choice Branching], "ShortestCommonSupersequence": [Shortest Common Supersequence], @@ -1850,6 +1851,93 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("QuadraticAssignment") + let C = x.instance.cost_matrix + let D = x.instance.distance_matrix + let n = C.len() + let m = D.len() + let sol = (config: x.optimal_config, metric: x.optimal_value) + let fstar = sol.config + let cost-star = sol.metric.Valid + // Convert integer matrix to math.mat content + let to-mat(m) = math.mat(..m.map(row => row.map(v => $#v$))) + // Compute identity assignment cost + let id-cost = range(n).fold(0, (acc, i) => + range(n).fold(acc, (acc2, j) => + if i != j { acc2 + C.at(i).at(j) * D.at(i).at(j) } else { acc2 } + ) + ) + // Format optimal assignment as 1-indexed + let fstar-display = fstar.map(v => str(v + 1)).join(", ") + // Find the highest-flow off-diagonal pair + let max-flow = 0 + let max-fi = 0 + let max-fj = 0 + for i in range(n) { + for j in range(i + 1, n) { + if C.at(i).at(j) > max-flow { + max-flow = C.at(i).at(j) + max-fi = i + max-fj = j + } + } + } + let assigned-li = fstar.at(max-fi) + let assigned-lj = fstar.at(max-fj) + let dist-between = D.at(assigned-li).at(assigned-lj) + [ + #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 = #n$ with flow matrix $C$ and distance matrix $D$: + $ C = #to-mat(C), quad D = #to-mat(D). $ + The identity assignment $f(i) = i$ gives cost #id-cost. The optimal assignment is $f^* = (#fstar-display)$ with cost #cost-star: it places the heavily interacting facilities $F_#(max-fi + 1)$ and $F_#(max-fj + 1)$ (highest flow $= #max-flow$) at locations $L_#(assigned-li + 1)$ and $L_#(assigned-lj + 1)$ (distance $= #dist-between$). + + #figure( + canvas(length: 1cm, { + import draw: * + let fac-x = 0 + let loc-x = 5 + let ys = range(n).rev() + // Draw facility nodes + for i in range(n) { + 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(m) { + 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)$]) + } + content((fac-x, n - 0.3), text(9pt, weight: "bold")[Facilities]) + content((loc-x, m - 0.3), text(9pt, weight: "bold")[Locations]) + // Draw optimal assignment arrows + for (fi, li) in fstar.enumerate() { + line("f" + str(fi) + ".east", "l" + str(li) + ".west", + mark: (end: "straight"), stroke: 1.2pt + luma(80)) + } + // Highlight highest-flow pair + on-layer(-1, { + let y0 = calc.min(ys.at(max-fi), ys.at(max-fj)) - 0.55 + let y1 = calc.max(ys.at(max-fi), ys.at(max-fj)) + 0.55 + rect((-0.55, y0), (0.55, y1), + 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_#(max-fi + 1), F_#(max-fj + 1)) = #max-flow$]) + }), + caption: [Optimal assignment $f^* = (#fstar-display)$ for the $#n times #m$ QAP instance. Facilities (blue, left) are assigned to locations (red, right) by arrows. Facilities $F_#(max-fi + 1)$ and $F_#(max-fj + 1)$ (highest flow $= #max-flow$) are assigned to locations $L_#(assigned-li + 1)$ and $L_#(assigned-lj + 1)$ (distance $= #dist-between$). Total cost $= #cost-star$.], + ) + ] + ] +} + #{ let x = load-model-example("ClosestVectorProblem") let basis = x.instance.basis diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 8a80850fb..3456c550a 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -257,6 +257,7 @@ Flags by problem type: LCS --strings, --bound [--alphabet-size] FAS --arcs [--weights] [--num-vertices] FVS --arcs [--weights] [--num-vertices] + QAP --matrix (cost), --distance-matrix StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices] FlowShopScheduling --task-lengths, --deadline [--num-processors] StaffScheduling --schedules, --requirements, --num-workers, --k @@ -467,6 +468,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, /// Weighted potential augmentation edges (e.g., 0-2:3,1-3:5) #[arg(long)] pub potential_edges: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 2a14d8a5c..1e1372f67 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -84,6 +84,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.candidate_arcs.is_none() && args.potential_edges.is_none() && args.budget.is_none() @@ -298,6 +299,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", "HamiltonianCircuit" => "--graph 0-1,1-2,2-3,3-0", @@ -1010,6 +1012,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)?; @@ -2878,6 +2928,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) +} + fn parse_potential_edges(args: &CreateArgs) -> Result> { let edges_str = args.potential_edges.as_deref().ok_or_else(|| { anyhow::anyhow!("BiconnectivityAugmentation requires --potential-edges (e.g., 0-2:3,1-3:5)") @@ -3552,6 +3633,7 @@ mod tests { pattern: None, strings: None, arcs: None, + distance_matrix: None, potential_edges: None, budget: None, candidate_arcs: None, diff --git a/src/lib.rs b/src/lib.rs index bba9b1ea6..517f59434 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,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::{ BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation, diff --git a/src/models/algebraic/mod.rs b/src/models/algebraic/mod.rs index a4945487d..fcbba87f3 100644 --- a/src/models/algebraic/mod.rs +++ b/src/models/algebraic/mod.rs @@ -5,15 +5,18 @@ //! - [`ILP`]: Integer Linear Programming //! - [`ClosestVectorProblem`]: Closest Vector Problem (minimize lattice distance) //! - [`BMF`]: Boolean Matrix Factorization +//! - [`QuadraticAssignment`]: Quadratic Assignment Problem pub(crate) mod bmf; pub(crate) mod closest_vector_problem; pub(crate) mod ilp; +pub(crate) mod quadratic_assignment; pub(crate) 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; #[cfg(feature = "example-db")] @@ -23,5 +26,6 @@ pub(crate) fn canonical_model_example_specs() -> 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! { + default opt QuadraticAssignment => "factorial(num_facilities)", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "quadratic_assignment", + instance: Box::new(QuadraticAssignment::new( + vec![ + vec![0, 5, 2, 0], + vec![5, 0, 0, 3], + vec![2, 0, 0, 4], + vec![0, 3, 4, 0], + ], + vec![ + vec![0, 4, 1, 1], + vec![4, 0, 3, 4], + vec![1, 3, 0, 4], + vec![1, 4, 4, 0], + ], + )), + optimal_config: vec![3, 0, 1, 2], + optimal_value: serde_json::json!({"Valid": 56}), + }] +} + +#[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 15f9cc7a5..14216872f 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::{ BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation, 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 000000000..703321938 --- /dev/null +++ b/src/unit_tests/models/algebraic/quadratic_assignment.rs @@ -0,0 +1,164 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; + +/// Create a 4x4 test instance matching issue #300's example. +/// +/// Cost matrix C (flows between 4 facilities): +/// [[0, 5, 2, 0], +/// [5, 0, 0, 3], +/// [2, 0, 0, 4], +/// [0, 3, 4, 0]] +/// +/// Distance matrix D (distances between 4 locations): +/// [[0, 4, 1, 1], +/// [4, 0, 3, 4], +/// [1, 3, 0, 4], +/// [1, 4, 4, 0]] +/// +/// Optimal assignment: f* = (3, 0, 1, 2) with cost 56. +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, 4, 1, 1], + vec![4, 0, 3, 4], + vec![1, 3, 0, 4], + vec![1, 4, 4, 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*4 + 2*1 + 0*1 + 5*4 + 0*3 + 3*4 + 2*1 + 0*3 + 4*4 + 0*1 + 3*4 + 4*4 + // = 20 + 2 + 0 + 20 + 0 + 12 + 2 + 0 + 16 + 0 + 12 + 16 = 100 + assert_eq!( + Problem::evaluate(&qap, &[0, 1, 2, 3]), + SolutionSize::Valid(100) + ); +} + +#[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*1=5 i=0,j=2: 2*D[0][1]=2*4=8 i=0,j=3: 0*D[0][3]=0 + // i=1,j=0: 5*D[2][0]=5*1=5 i=1,j=2: 0*D[2][1]=0*3=0 i=1,j=3: 3*D[2][3]=3*4=12 + // i=2,j=0: 2*D[1][0]=2*4=8 i=2,j=1: 0*D[1][2]=0*3=0 i=2,j=3: 4*D[1][3]=4*4=16 + // i=3,j=0: 0*D[3][0]=0 i=3,j=1: 3*D[3][2]=3*4=12 i=3,j=2: 4*D[3][1]=4*4=16 + // Total = 5+8+0+5+0+12+8+0+16+0+12+16 = 82 + assert_eq!( + Problem::evaluate(&qap, &[0, 2, 1, 3]), + SolutionSize::Valid(82) + ); +} + +#[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 f* = (3, 0, 1, 2) with cost 56. + assert_eq!( + Problem::evaluate(&qap, &best_config), + SolutionSize::Valid(56) + ); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 04f4a9195..13599ecc8 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -88,6 +88,10 @@ fn test_all_problems_implement_trait_correctly() { "BalancedCompleteBipartiteSubgraph", ); 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()], @@ -187,6 +191,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 + ); // Maximization problems assert_eq!(