problemreductions/models/graph/
acyclic_partition.rs1use 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, Deserialize)]
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(Debug, Deserialize, crate::CreateSpec)]
42struct AcyclicPartitionCreateSpec {
43 #[create(codec = "arc-list")]
44 arcs: Vec<(usize, usize)>,
45 num_vertices: Option<usize>,
46 #[create(codec = "comma-separated")]
47 weights: Option<Vec<i64>>,
48 #[create(name = "arc_costs", codec = "comma-separated")]
49 arc_weights: Option<Vec<i64>>,
50 weight_bound: i64,
51 cost_bound: i64,
52}
53
54impl TryFrom<AcyclicPartitionCreateSpec> for AcyclicPartition<i64> {
55 type Error = crate::registry::ConstructionError;
56
57 fn try_from(spec: AcyclicPartitionCreateSpec) -> Result<Self, Self::Error> {
58 if spec.arcs.is_empty() && spec.num_vertices.is_none() {
59 return Err("num_vertices is required for an empty arc list"
60 .to_string()
61 .into());
62 }
63 let inferred = spec
64 .arcs
65 .iter()
66 .flat_map(|&(u, v)| [u, v])
67 .max()
68 .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
69 .transpose()?
70 .unwrap_or(0);
71 let num_vertices = spec.num_vertices.unwrap_or(inferred);
72 if num_vertices < inferred {
73 return Err(format!(
74 "num_vertices {num_vertices} is too small for arc endpoints; need at least {inferred}"
75 ).into());
76 }
77 let graph = DirectedGraph::new(num_vertices, spec.arcs);
78 let vertex_weights = spec.weights.unwrap_or_else(|| vec![1; num_vertices]);
79 if vertex_weights.len() != num_vertices {
80 return Err(format!(
81 "weights has length {}, expected {num_vertices}",
82 vertex_weights.len()
83 )
84 .into());
85 }
86 let arc_costs = spec
87 .arc_weights
88 .unwrap_or_else(|| vec![1; graph.num_arcs()]);
89 if arc_costs.len() != graph.num_arcs() {
90 return Err(format!(
91 "arc_weights has length {}, expected {}",
92 arc_costs.len(),
93 graph.num_arcs()
94 )
95 .into());
96 }
97 Ok(Self::new(
98 graph,
99 vertex_weights,
100 arc_costs,
101 spec.weight_bound,
102 spec.cost_bound,
103 ))
104 }
105}
106
107impl<W: WeightElement> AcyclicPartition<W> {
108 pub fn new(
110 graph: DirectedGraph,
111 vertex_weights: Vec<W>,
112 arc_costs: Vec<W>,
113 weight_bound: W::Sum,
114 cost_bound: W::Sum,
115 ) -> Self {
116 assert_eq!(
117 vertex_weights.len(),
118 graph.num_vertices(),
119 "vertex_weights length must match graph num_vertices"
120 );
121 assert_eq!(
122 arc_costs.len(),
123 graph.num_arcs(),
124 "arc_costs length must match graph num_arcs"
125 );
126 Self {
127 graph,
128 vertex_weights,
129 arc_costs,
130 weight_bound,
131 cost_bound,
132 }
133 }
134
135 pub fn graph(&self) -> &DirectedGraph {
137 &self.graph
138 }
139
140 pub fn vertex_weights(&self) -> &[W] {
142 &self.vertex_weights
143 }
144
145 pub fn arc_costs(&self) -> &[W] {
147 &self.arc_costs
148 }
149
150 pub fn set_vertex_weights(&mut self, vertex_weights: Vec<W>) {
152 assert_eq!(
153 vertex_weights.len(),
154 self.graph.num_vertices(),
155 "vertex_weights length must match graph num_vertices"
156 );
157 self.vertex_weights = vertex_weights;
158 }
159
160 pub fn set_arc_costs(&mut self, arc_costs: Vec<W>) {
162 assert_eq!(
163 arc_costs.len(),
164 self.graph.num_arcs(),
165 "arc_costs length must match graph num_arcs"
166 );
167 self.arc_costs = arc_costs;
168 }
169
170 pub fn weight_bound(&self) -> &W::Sum {
172 &self.weight_bound
173 }
174
175 pub fn cost_bound(&self) -> &W::Sum {
177 &self.cost_bound
178 }
179
180 pub fn is_weighted(&self) -> bool {
182 !W::IS_UNIT
183 }
184
185 pub fn num_vertices(&self) -> usize {
187 self.graph.num_vertices()
188 }
189
190 pub fn num_arcs(&self) -> usize {
192 self.graph.num_arcs()
193 }
194
195 pub fn is_valid_solution(
197 &self,
198 config: &[usize],
199 ) -> Result<bool, crate::traits::EvaluationError> {
200 is_valid_acyclic_partition(
201 &self.graph,
202 &self.vertex_weights,
203 &self.arc_costs,
204 &self.weight_bound,
205 &self.cost_bound,
206 config,
207 )
208 }
209}
210
211impl<W> Problem for AcyclicPartition<W>
212where
213 W: WeightElement + crate::variant::VariantParam,
214{
215 const NAME: &'static str = "AcyclicPartition";
216 type Solution = Vec<usize>;
217 type Value = crate::types::Or;
218
219 crate::problem_parameters![("num_arcs", num_arcs), ("num_vertices", num_vertices),];
220
221 fn variant() -> Vec<(&'static str, &'static str)> {
222 crate::variant_params![W]
223 }
224
225 fn evaluate(
226 &self,
227 config: &Self::Solution,
228 ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
229 let n = self.graph.num_vertices();
230 if config.len() != n {
231 return Err(crate::traits::EvaluationError::InvalidConfiguration(
232 "partition assignment length does not match the graph vertices".into(),
233 ));
234 }
235 if config.iter().any(|&part| part >= n) {
236 return Err(crate::traits::EvaluationError::InvalidConfiguration(
237 "partition assignment contains an out-of-range part".into(),
238 ));
239 }
240 Ok({
241 crate::types::Or({
242 is_valid_acyclic_partition(
243 &self.graph,
244 &self.vertex_weights,
245 &self.arc_costs,
246 &self.weight_bound,
247 &self.cost_bound,
248 config,
249 )?
250 })
251 })
252 }
253}
254
255impl<W> crate::solvers::BruteForceProblem for AcyclicPartition<W>
256where
257 W: WeightElement + crate::variant::VariantParam,
258{
259 fn dimensions(&self) -> Vec<usize> {
260 vec![self.graph.num_vertices(); self.graph.num_vertices()]
261 }
262}
263
264fn is_valid_acyclic_partition<W: WeightElement>(
265 graph: &DirectedGraph,
266 vertex_weights: &[W],
267 arc_costs: &[W],
268 weight_bound: &W::Sum,
269 cost_bound: &W::Sum,
270 config: &[usize],
271) -> Result<bool, crate::traits::EvaluationError> {
272 let num_vertices = graph.num_vertices();
273 if config.len() != num_vertices {
274 return Ok(false);
275 }
276 if vertex_weights.len() != num_vertices || arc_costs.len() != graph.num_arcs() {
277 return Ok(false);
278 }
279 if config.iter().any(|&label| label >= num_vertices) {
280 return Ok(false);
281 }
282
283 let mut partition_weights = vec![W::Sum::zero(); num_vertices];
284 let mut used_labels = vec![false; num_vertices];
285 for (vertex, &label) in config.iter().enumerate() {
286 used_labels[label] = true;
287 partition_weights[label] = W::checked_add_to_sum(
288 partition_weights[label].clone(),
289 vertex_weights[vertex].to_sum(),
290 "summing acyclic partition vertex weights",
291 )?;
292 if partition_weights[label] > *weight_bound {
293 return Ok(false);
294 }
295 }
296
297 let mut dense_label = vec![usize::MAX; num_vertices];
298 let mut next_dense = 0usize;
299 for (label, used) in used_labels.iter().enumerate() {
300 if *used {
301 dense_label[label] = next_dense;
302 next_dense += 1;
303 }
304 }
305
306 let mut total_cost = W::Sum::zero();
307 let mut quotient_arcs = BTreeSet::new();
308 for ((source, target), cost) in graph.arcs().iter().zip(arc_costs.iter()) {
309 let source_label = config[*source];
310 let target_label = config[*target];
311 if source_label == target_label {
312 continue;
313 }
314 total_cost = W::checked_add_to_sum(
315 total_cost,
316 cost.to_sum(),
317 "summing acyclic partition arc costs",
318 )?;
319 if total_cost > *cost_bound {
320 return Ok(false);
321 }
322 quotient_arcs.insert((dense_label[source_label], dense_label[target_label]));
323 }
324
325 Ok(DirectedGraph::new(next_dense, quotient_arcs.into_iter().collect()).is_dag())
326}
327
328crate::declare_variants! {
329 default AcyclicPartition<i64> => "num_vertices^num_vertices" create AcyclicPartitionCreateSpec,
330}
331
332crate::register_brute_force! {
333 AcyclicPartition<i64>,
334}
335
336#[cfg(feature = "example-db")]
337pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
338 vec![crate::example_db::specs::ModelExampleSpec {
339 id: "acyclic_partition",
340 instance: Box::new(AcyclicPartition::new(
341 DirectedGraph::new(
342 6,
343 vec![
344 (0, 1),
345 (0, 2),
346 (1, 3),
347 (1, 4),
348 (2, 4),
349 (2, 5),
350 (3, 5),
351 (4, 5),
352 ],
353 ),
354 vec![2, 3, 2, 1, 3, 1],
355 vec![1; 8],
356 5,
357 5,
358 )),
359 optimal_config: serde_json::json!(vec![0, 1, 0, 2, 2, 2]),
360 optimal_value: serde_json::json!(true),
361 }]
362}
363
364#[cfg(test)]
365#[path = "../../unit_tests/models/graph/acyclic_partition.rs"]
366mod tests;