1use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
9use crate::topology::DirectedGraph;
10use crate::traits::Problem;
11use crate::types::WeightElement;
12use num_traits::Zero;
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeSet;
15
16inventory::submit! {
17 ProblemSchemaEntry {
18 name: "AcyclicPartition",
19 display_name: "Acyclic Partition",
20 aliases: &[],
21 dimensions: &[
22 VariantDimension::new("weight", "i64", &["i64"]),
23 ],
24 category: crate::registry::ProblemCategory::Graph,
25 module_path: module_path!(),
26 description: "Partition a directed graph into bounded-weight groups with an acyclic quotient graph and bounded inter-partition cost",
27 fields: AcyclicPartitionCreateSpec::FIELDS,
28 }
29}
30
31#[derive(Debug, Clone, Serialize)]
33pub struct AcyclicPartition<W: WeightElement> {
34 graph: DirectedGraph,
35 vertex_weights: Vec<W>,
36 arc_costs: Vec<W>,
37 weight_bound: W::Sum,
38 cost_bound: W::Sum,
39}
40
41#[derive(Deserialize)]
42#[serde(bound(deserialize = "W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>"))]
43struct AcyclicPartitionData<W: WeightElement> {
44 graph: DirectedGraph,
45 vertex_weights: Vec<W>,
46 arc_costs: Vec<W>,
47 weight_bound: W::Sum,
48 cost_bound: W::Sum,
49}
50
51impl<'de, W> Deserialize<'de> for AcyclicPartition<W>
52where
53 W: WeightElement + Deserialize<'de>,
54 W::Sum: Deserialize<'de>,
55{
56 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
57 let data = AcyclicPartitionData::<W>::deserialize(deserializer)?;
58 Self::try_new(
59 data.graph,
60 data.vertex_weights,
61 data.arc_costs,
62 data.weight_bound,
63 data.cost_bound,
64 )
65 .map_err(serde::de::Error::custom)
66 }
67}
68
69#[derive(Debug, Deserialize, crate::CreateSpec)]
70struct AcyclicPartitionCreateSpec {
71 #[create(codec = "arc-list")]
72 arcs: Vec<(usize, usize)>,
73 num_vertices: Option<usize>,
74 #[create(codec = "comma-separated")]
75 weights: Option<Vec<i64>>,
76 #[create(name = "arc_costs", codec = "comma-separated")]
77 arc_weights: Option<Vec<i64>>,
78 weight_bound: i64,
79 cost_bound: i64,
80}
81
82impl TryFrom<AcyclicPartitionCreateSpec> for AcyclicPartition<i64> {
83 type Error = crate::registry::ConstructionError;
84
85 fn try_from(spec: AcyclicPartitionCreateSpec) -> Result<Self, Self::Error> {
86 if spec.arcs.is_empty() && spec.num_vertices.is_none() {
87 return Err("num_vertices is required for an empty arc list"
88 .to_string()
89 .into());
90 }
91 let inferred = spec
92 .arcs
93 .iter()
94 .flat_map(|&(u, v)| [u, v])
95 .max()
96 .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
97 .transpose()?
98 .unwrap_or(0);
99 let num_vertices = spec.num_vertices.unwrap_or(inferred);
100 if num_vertices < inferred {
101 return Err(format!(
102 "num_vertices {num_vertices} is too small for arc endpoints; need at least {inferred}"
103 ).into());
104 }
105 let graph = DirectedGraph::new(num_vertices, spec.arcs);
106 let vertex_weights = spec.weights.unwrap_or_else(|| vec![1; num_vertices]);
107 let arc_costs = spec
108 .arc_weights
109 .unwrap_or_else(|| vec![1; graph.num_arcs()]);
110 Self::try_new(
111 graph,
112 vertex_weights,
113 arc_costs,
114 spec.weight_bound,
115 spec.cost_bound,
116 )
117 }
118}
119
120impl<W: WeightElement> AcyclicPartition<W> {
121 pub fn new(
123 graph: DirectedGraph,
124 vertex_weights: Vec<W>,
125 arc_costs: Vec<W>,
126 weight_bound: W::Sum,
127 cost_bound: W::Sum,
128 ) -> Self {
129 Self::try_new(graph, vertex_weights, arc_costs, weight_bound, cost_bound)
130 .unwrap_or_else(|error| panic!("{error}"))
131 }
132
133 fn try_new(
134 graph: DirectedGraph,
135 vertex_weights: Vec<W>,
136 arc_costs: Vec<W>,
137 weight_bound: W::Sum,
138 cost_bound: W::Sum,
139 ) -> Result<Self, crate::registry::ConstructionError> {
140 Self::check_vertex_weights(&graph, &vertex_weights)?;
141 Self::check_arc_costs(&graph, &arc_costs)?;
142 Ok(Self {
143 graph,
144 vertex_weights,
145 arc_costs,
146 weight_bound,
147 cost_bound,
148 })
149 }
150
151 pub fn graph(&self) -> &DirectedGraph {
153 &self.graph
154 }
155
156 pub fn vertex_weights(&self) -> &[W] {
158 &self.vertex_weights
159 }
160
161 pub fn arc_costs(&self) -> &[W] {
163 &self.arc_costs
164 }
165
166 pub fn set_vertex_weights(&mut self, vertex_weights: Vec<W>) {
168 Self::check_vertex_weights(&self.graph, &vertex_weights)
169 .unwrap_or_else(|error| panic!("{error}"));
170 self.vertex_weights = vertex_weights;
171 }
172
173 fn check_vertex_weights(
174 graph: &DirectedGraph,
175 vertex_weights: &[W],
176 ) -> Result<(), crate::registry::ConstructionError> {
177 if vertex_weights.len() != graph.num_vertices() {
178 return Err(crate::registry::ConstructionError::length_mismatch(
179 "vertex_weights",
180 vertex_weights.len(),
181 graph.num_vertices(),
182 ));
183 }
184 Ok(())
185 }
186
187 pub fn set_arc_costs(&mut self, arc_costs: Vec<W>) {
189 Self::check_arc_costs(&self.graph, &arc_costs).unwrap_or_else(|error| panic!("{error}"));
190 self.arc_costs = arc_costs;
191 }
192
193 fn check_arc_costs(
194 graph: &DirectedGraph,
195 arc_costs: &[W],
196 ) -> Result<(), crate::registry::ConstructionError> {
197 if arc_costs.len() != graph.num_arcs() {
198 return Err(crate::registry::ConstructionError::length_mismatch(
199 "arc_costs",
200 arc_costs.len(),
201 graph.num_arcs(),
202 ));
203 }
204 Ok(())
205 }
206
207 pub fn weight_bound(&self) -> &W::Sum {
209 &self.weight_bound
210 }
211
212 pub fn cost_bound(&self) -> &W::Sum {
214 &self.cost_bound
215 }
216
217 pub fn is_weighted(&self) -> bool {
219 !W::IS_UNIT
220 }
221
222 pub fn max_numeric_magnitude_bits(&self) -> u64 {
224 crate::types::max_numeric_magnitude_bits(
225 self.vertex_weights
226 .iter()
227 .chain(&self.arc_costs)
228 .map(|value| value.to_sum())
229 .chain([self.weight_bound.clone(), self.cost_bound.clone()]),
230 )
231 }
232
233 pub fn num_vertices(&self) -> usize {
235 self.graph.num_vertices()
236 }
237
238 pub fn num_arcs(&self) -> usize {
240 self.graph.num_arcs()
241 }
242
243 pub fn is_valid_solution(
245 &self,
246 config: &[usize],
247 ) -> Result<bool, crate::traits::EvaluationError> {
248 is_valid_acyclic_partition(
249 &self.graph,
250 &self.vertex_weights,
251 &self.arc_costs,
252 &self.weight_bound,
253 &self.cost_bound,
254 config,
255 )
256 }
257}
258
259impl<W> Problem for AcyclicPartition<W>
260where
261 W: WeightElement + crate::variant::VariantParam,
262{
263 const NAME: &'static str = "AcyclicPartition";
264 type Solution = Vec<usize>;
265 type Value = crate::types::Or;
266
267 crate::problem_parameters![
268 ("max_numeric_magnitude_bits", max_numeric_magnitude_bits),
269 ("num_arcs", num_arcs),
270 ("num_vertices", num_vertices),
271 ];
272
273 fn variant() -> Vec<(&'static str, &'static str)> {
274 crate::variant_params![W]
275 }
276
277 fn evaluate(
278 &self,
279 config: &Self::Solution,
280 ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
281 let n = self.graph.num_vertices();
282 if config.len() != n {
283 return Err(crate::traits::EvaluationError::InvalidConfiguration(
284 "partition assignment length does not match the graph vertices".into(),
285 ));
286 }
287 if config.iter().any(|&part| part >= n) {
288 return Err(crate::traits::EvaluationError::InvalidConfiguration(
289 "partition assignment contains an out-of-range part".into(),
290 ));
291 }
292 Ok({
293 crate::types::Or({
294 is_valid_acyclic_partition(
295 &self.graph,
296 &self.vertex_weights,
297 &self.arc_costs,
298 &self.weight_bound,
299 &self.cost_bound,
300 config,
301 )?
302 })
303 })
304 }
305}
306
307impl<W> crate::solvers::BruteForceProblem for AcyclicPartition<W>
308where
309 W: WeightElement + crate::variant::VariantParam,
310{
311 fn dimensions(&self) -> Vec<usize> {
312 vec![self.graph.num_vertices(); self.graph.num_vertices()]
313 }
314}
315
316fn is_valid_acyclic_partition<W: WeightElement>(
317 graph: &DirectedGraph,
318 vertex_weights: &[W],
319 arc_costs: &[W],
320 weight_bound: &W::Sum,
321 cost_bound: &W::Sum,
322 config: &[usize],
323) -> Result<bool, crate::traits::EvaluationError> {
324 let num_vertices = graph.num_vertices();
325 if config.len() != num_vertices {
326 return Ok(false);
327 }
328 if vertex_weights.len() != num_vertices || arc_costs.len() != graph.num_arcs() {
329 return Ok(false);
330 }
331 if config.iter().any(|&label| label >= num_vertices) {
332 return Ok(false);
333 }
334
335 let mut partition_weights = vec![W::Sum::zero(); num_vertices];
336 let mut used_labels = vec![false; num_vertices];
337 for (vertex, &label) in config.iter().enumerate() {
338 used_labels[label] = true;
339 partition_weights[label] = W::checked_add_to_sum(
340 partition_weights[label].clone(),
341 vertex_weights[vertex].to_sum(),
342 "summing acyclic partition vertex weights",
343 )?;
344 }
345 if partition_weights
346 .iter()
347 .zip(&used_labels)
348 .any(|(weight, used)| *used && weight > weight_bound)
349 {
350 return Ok(false);
351 }
352
353 let mut dense_label = vec![usize::MAX; num_vertices];
354 let mut next_dense = 0usize;
355 for (label, used) in used_labels.iter().enumerate() {
356 if *used {
357 dense_label[label] = next_dense;
358 next_dense += 1;
359 }
360 }
361
362 let mut total_cost = W::Sum::zero();
363 let mut quotient_arcs = BTreeSet::new();
364 for ((source, target), cost) in graph.arcs().iter().zip(arc_costs.iter()) {
365 let source_label = config[*source];
366 let target_label = config[*target];
367 if source_label == target_label {
368 continue;
369 }
370 total_cost = W::checked_add_to_sum(
371 total_cost,
372 cost.to_sum(),
373 "summing acyclic partition arc costs",
374 )?;
375 quotient_arcs.insert((dense_label[source_label], dense_label[target_label]));
376 }
377
378 Ok(total_cost <= *cost_bound
379 && DirectedGraph::new(next_dense, quotient_arcs.into_iter().collect()).is_dag())
380}
381
382crate::declare_variants! {
383 default AcyclicPartition<i64> => "num_vertices^num_vertices" create AcyclicPartitionCreateSpec,
384}
385
386crate::register_brute_force! {
387 AcyclicPartition<i64>,
388}
389
390#[cfg(feature = "example-db")]
391pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
392 vec![crate::example_db::specs::ModelExampleSpec {
393 id: "acyclic_partition",
394 instance: Box::new(AcyclicPartition::new(
395 DirectedGraph::new(
396 6,
397 vec![
398 (0, 1),
399 (0, 2),
400 (1, 3),
401 (1, 4),
402 (2, 4),
403 (2, 5),
404 (3, 5),
405 (4, 5),
406 ],
407 ),
408 vec![2, 3, 2, 1, 3, 1],
409 vec![1; 8],
410 5,
411 5,
412 )),
413 optimal_config: serde_json::json!(vec![0, 1, 0, 2, 2, 2]),
414 optimal_value: serde_json::json!(true),
415 }]
416}
417
418#[cfg(test)]
419#[path = "../../unit_tests/models/graph/acyclic_partition.rs"]
420mod tests;