problemreductions/models/misc/
clustering.rs1use crate::registry::{FieldInfo, ProblemSchemaEntry};
9use crate::traits::Problem;
10use serde::{Deserialize, Serialize};
11
12inventory::submit! {
13 ProblemSchemaEntry {
14 name: "Clustering",
15 display_name: "Clustering",
16 aliases: &[],
17 dimensions: &[],
18 category: crate::registry::ProblemCategory::Misc,
19 module_path: module_path!(),
20 description: "Partition elements into at most K clusters where all intra-cluster distances are at most B",
21 fields: &[
22 FieldInfo { name: "distances", type_name: "Vec<Vec<i64>>", description: "Symmetric distance matrix with zero diagonal" },
23 FieldInfo { name: "num_clusters", type_name: "usize", description: "Maximum number of clusters K" },
24 FieldInfo { name: "diameter_bound", type_name: "i64", description: "Maximum allowed intra-cluster pairwise distance B" },
25 ],
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(try_from = "ClusteringData")]
63pub struct Clustering {
64 distances: Vec<Vec<i64>>,
66 num_clusters: usize,
68 diameter_bound: i64,
70}
71
72#[derive(Deserialize)]
73struct ClusteringData {
74 distances: Vec<Vec<i64>>,
75 num_clusters: usize,
76 diameter_bound: i64,
77}
78
79impl TryFrom<ClusteringData> for Clustering {
80 type Error = crate::registry::ConstructionError;
81
82 fn try_from(data: ClusteringData) -> Result<Self, Self::Error> {
83 Self::try_new(data.distances, data.num_clusters, data.diameter_bound)
84 }
85}
86
87impl Clustering {
88 pub fn new(distances: Vec<Vec<i64>>, num_clusters: usize, diameter_bound: i64) -> Self {
99 Self::try_new(distances, num_clusters, diameter_bound)
100 .unwrap_or_else(|error| panic!("{error}"))
101 }
102
103 fn try_new(
104 distances: Vec<Vec<i64>>,
105 num_clusters: usize,
106 diameter_bound: i64,
107 ) -> Result<Self, crate::registry::ConstructionError> {
108 let n = distances.len();
109 if n == 0 {
110 return Err("Clustering requires at least one element".into());
111 }
112 if num_clusters == 0 {
113 return Err("num_clusters must be at least 1".into());
114 }
115 for (i, row) in distances.iter().enumerate() {
116 if row.len() != n {
117 return Err(format!(
118 "Distance matrix must be square: row {i} has {} columns, expected {n}",
119 row.len()
120 )
121 .into());
122 }
123 if distances[i][i] != 0 {
124 return Err(format!("Diagonal entry distances[{i}][{i}] must be 0").into());
125 }
126 }
127 for (i, row_i) in distances.iter().enumerate() {
128 for j in (i + 1)..n {
129 if row_i[j] != distances[j][i] {
130 return Err(format!("Distance matrix must be symmetric: distances[{i}][{j}] = {} != distances[{j}][{i}] = {}", row_i[j], distances[j][i]).into());
131 }
132 }
133 }
134 Ok(Self {
135 distances,
136 num_clusters,
137 diameter_bound,
138 })
139 }
140
141 pub fn distances(&self) -> &[Vec<i64>] {
143 &self.distances
144 }
145
146 pub fn num_elements(&self) -> usize {
148 self.distances.len()
149 }
150
151 pub fn num_clusters(&self) -> usize {
153 self.num_clusters
154 }
155
156 pub fn diameter_bound(&self) -> i64 {
158 self.diameter_bound
159 }
160
161 fn is_valid_partition(&self, config: &[usize]) -> bool {
163 let n = self.num_elements();
164 if config.len() != n {
165 return false;
166 }
167 if config.iter().any(|&c| c >= self.num_clusters) {
168 return false;
169 }
170 let mut clusters: Vec<Vec<usize>> = vec![vec![]; self.num_clusters];
172 for (i, &c) in config.iter().enumerate() {
173 clusters[c].push(i);
174 }
175 for members in &clusters {
177 for a in 0..members.len() {
178 for b in (a + 1)..members.len() {
179 if self.distances[members[a]][members[b]] > self.diameter_bound {
180 return false;
181 }
182 }
183 }
184 }
185 true
186 }
187}
188
189impl Problem for Clustering {
190 const NAME: &'static str = "Clustering";
191 type Solution = Vec<usize>;
192 type Value = crate::types::Or;
193
194 crate::problem_parameters![
195 ("num_clusters", num_clusters),
196 ("num_elements", num_elements),
197 ];
198
199 fn variant() -> Vec<(&'static str, &'static str)> {
200 crate::variant_params![]
201 }
202
203 fn evaluate(
204 &self,
205 config: &Self::Solution,
206 ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
207 if config.len() != self.num_elements() {
208 return Err(crate::traits::EvaluationError::InvalidConfiguration(
209 "cluster assignment length does not match the elements".into(),
210 ));
211 }
212 if config.iter().any(|&cluster| cluster >= self.num_clusters) {
213 return Err(crate::traits::EvaluationError::InvalidConfiguration(
214 "cluster assignment contains an out-of-range cluster".into(),
215 ));
216 }
217 Ok(crate::types::Or(self.is_valid_partition(config)))
218 }
219}
220
221impl crate::solvers::BruteForceProblem for Clustering {
222 fn dimensions(&self) -> Vec<usize> {
223 vec![self.num_clusters; self.num_elements()]
224 }
225}
226
227crate::declare_variants! {
228 default Clustering => "num_clusters^num_elements",
229}
230
231crate::register_brute_force! {
232 Clustering,
233}
234
235#[cfg(feature = "example-db")]
236pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
237 let distances = vec![
241 vec![0, 1, 1, 3, 3, 3],
242 vec![1, 0, 1, 3, 3, 3],
243 vec![1, 1, 0, 3, 3, 3],
244 vec![3, 3, 3, 0, 1, 1],
245 vec![3, 3, 3, 1, 0, 1],
246 vec![3, 3, 3, 1, 1, 0],
247 ];
248 vec![crate::example_db::specs::ModelExampleSpec {
249 id: "clustering",
250 instance: Box::new(Clustering::new(distances, 2, 1)),
251 optimal_config: serde_json::json!(vec![0, 0, 0, 1, 1, 1]),
252 optimal_value: serde_json::json!(true),
253 }]
254}
255
256#[cfg(test)]
257#[path = "../../unit_tests/models/misc/clustering.rs"]
258mod tests;