1use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
8use crate::topology::{Graph, SimpleGraph};
9use crate::traits::Problem;
10use crate::types::WeightElement;
11use num_traits::Zero;
12use petgraph::algo::{articulation_points::articulation_points, connected_components};
13use petgraph::graph::{NodeIndex, UnGraph};
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeSet;
16
17inventory::submit! {
18 ProblemSchemaEntry {
19 name: "BiconnectivityAugmentation",
20 display_name: "Biconnectivity Augmentation",
21 aliases: &[],
22 dimensions: &[
23 VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
24 VariantDimension::new("weight", "i64", &["i64"]),
25 ],
26 category: crate::registry::ProblemCategory::Graph,
27 module_path: module_path!(),
28 description: "Add weighted potential edges to make a graph biconnected within budget",
29 fields: BiconnectivityAugmentationCreateSpec::FIELDS,
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(bound(serialize = "G: serde::Serialize, W: serde::Serialize, W::Sum: serde::Serialize"))]
41#[serde(try_from = "BiconnectivityAugmentationData<G, W>")]
42#[serde(bound(
43 deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>"
44))]
45pub struct BiconnectivityAugmentation<G, W>
46where
47 W: WeightElement,
48{
49 graph: G,
51 potential_weights: Vec<(usize, usize, W)>,
53 budget: W::Sum,
55}
56
57#[derive(Deserialize)]
58#[serde(bound(
59 deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>"
60))]
61struct BiconnectivityAugmentationData<G, W: WeightElement> {
62 graph: G,
63 potential_weights: Vec<(usize, usize, W)>,
64 budget: W::Sum,
65}
66
67impl<G, W> TryFrom<BiconnectivityAugmentationData<G, W>> for BiconnectivityAugmentation<G, W>
68where
69 G: Graph,
70 W: WeightElement,
71{
72 type Error = crate::registry::ConstructionError;
73
74 fn try_from(data: BiconnectivityAugmentationData<G, W>) -> Result<Self, Self::Error> {
75 Self::try_new(data.graph, data.potential_weights, data.budget)
76 }
77}
78
79#[derive(Debug, Deserialize, crate::CreateSpec)]
80struct BiconnectivityAugmentationCreateSpec {
81 #[create(codec = "edge-list")]
82 graph: Vec<(usize, usize)>,
83 num_vertices: Option<usize>,
84 potential_weights: Vec<(usize, usize, i64)>,
85 budget: i64,
86}
87
88impl TryFrom<BiconnectivityAugmentationCreateSpec>
89 for BiconnectivityAugmentation<SimpleGraph, i64>
90{
91 type Error = crate::registry::ConstructionError;
92 fn try_from(spec: BiconnectivityAugmentationCreateSpec) -> Result<Self, Self::Error> {
93 if spec.graph.is_empty() && spec.num_vertices.is_none() {
94 return Err("num_vertices is required for an empty graph".into());
95 }
96 for &(u, v) in &spec.graph {
97 if u == v {
98 return Err(format!("self-loop {u}-{v} is not allowed").into());
99 }
100 }
101 let inferred = spec
102 .graph
103 .iter()
104 .flat_map(|&(u, v)| [u, v])
105 .max()
106 .map(|v| v.checked_add(1).ok_or("vertex count overflows usize"))
107 .transpose()?
108 .unwrap_or(0);
109 let count = spec.num_vertices.unwrap_or(inferred);
110 if count < inferred {
111 return Err("num_vertices is too small for graph endpoints".into());
112 }
113 let graph = SimpleGraph::new(count, spec.graph);
114 Self::try_new(graph, spec.potential_weights, spec.budget)
115 }
116}
117
118impl<G: Graph, W: WeightElement> BiconnectivityAugmentation<G, W> {
119 pub fn new(graph: G, potential_weights: Vec<(usize, usize, W)>, budget: W::Sum) -> Self {
126 Self::try_new(graph, potential_weights, budget).unwrap_or_else(|error| panic!("{error}"))
127 }
128
129 fn try_new(
130 graph: G,
131 potential_weights: Vec<(usize, usize, W)>,
132 budget: W::Sum,
133 ) -> Result<Self, crate::registry::ConstructionError> {
134 let num_vertices = graph.num_vertices();
135 let mut seen_potential_edges = BTreeSet::new();
136 for &(u, v, _) in &potential_weights {
137 if u >= num_vertices || v >= num_vertices {
138 return Err(format!(
139 "potential edge ({}, {}) references vertex >= num_vertices ({})",
140 u, v, num_vertices
141 )
142 .into());
143 }
144 if u == v {
145 return Err(format!("potential edge ({}, {}) is a self-loop", u, v).into());
146 }
147 let edge = normalize_edge(u, v);
148 if graph.has_edge(edge.0, edge.1) {
149 return Err(format!(
150 "potential edge ({}, {}) already exists in the graph",
151 edge.0, edge.1
152 )
153 .into());
154 }
155 if !seen_potential_edges.insert(edge) {
156 return Err(
157 format!("potential edge ({}, {}) is duplicated", edge.0, edge.1).into(),
158 );
159 }
160 }
161
162 Ok(Self {
163 graph,
164 potential_weights,
165 budget,
166 })
167 }
168
169 pub fn graph(&self) -> &G {
171 &self.graph
172 }
173
174 pub fn potential_weights(&self) -> &[(usize, usize, W)] {
176 &self.potential_weights
177 }
178
179 pub fn budget(&self) -> &W::Sum {
181 &self.budget
182 }
183
184 pub fn max_numeric_magnitude_bits(&self) -> u64 {
186 crate::types::max_numeric_magnitude_bits(
187 self.potential_weights
188 .iter()
189 .map(|(_, _, weight)| weight.to_sum())
190 .chain(std::iter::once(self.budget.clone())),
191 )
192 }
193
194 pub fn num_vertices(&self) -> usize {
196 self.graph.num_vertices()
197 }
198
199 pub fn num_edges(&self) -> usize {
201 self.graph.num_edges()
202 }
203
204 pub fn num_potential_edges(&self) -> usize {
206 self.potential_weights.len()
207 }
208
209 pub fn is_weighted(&self) -> bool {
211 !W::IS_UNIT
212 }
213
214 fn augmented_graph(
215 &self,
216 config: &[bool],
217 ) -> Result<Option<UnGraph<(), ()>>, crate::traits::EvaluationError> {
218 if config.len() != self.num_potential_edges() {
219 return Ok(None);
220 }
221
222 let mut total = W::Sum::zero();
223 let mut edges = BTreeSet::new();
224
225 for (u, v) in self.graph.edges() {
226 edges.insert(normalize_edge(u, v));
227 }
228
229 for (selected, &(u, v, ref weight)) in config.iter().zip(&self.potential_weights) {
230 if *selected {
231 total = W::checked_add_to_sum(
232 total,
233 weight.to_sum(),
234 "summing biconnectivity augmentation weights",
235 )?;
236 edges.insert(normalize_edge(u, v));
237 }
238 }
239 if total > self.budget.clone() {
240 return Ok(None);
241 }
242
243 let mut graph = UnGraph::new_undirected();
244 for _ in 0..self.num_vertices() {
245 graph.add_node(());
246 }
247 for (u, v) in edges {
248 graph.add_edge(NodeIndex::new(u), NodeIndex::new(v), ());
249 }
250 Ok(Some(graph))
251 }
252}
253
254impl<G, W> Problem for BiconnectivityAugmentation<G, W>
255where
256 G: Graph + crate::variant::VariantParam,
257 W: WeightElement + crate::variant::VariantParam,
258{
259 const NAME: &'static str = "BiconnectivityAugmentation";
260 type Solution = Vec<bool>;
261 type Value = crate::types::Or;
262
263 crate::problem_parameters![
264 ("max_numeric_magnitude_bits", max_numeric_magnitude_bits),
265 ("num_edges", num_edges),
266 ("num_potential_edges", num_potential_edges),
267 ("num_vertices", num_vertices),
268 ];
269
270 fn variant() -> Vec<(&'static str, &'static str)> {
271 crate::variant_params![G, W]
272 }
273
274 fn evaluate(
275 &self,
276 config: &Self::Solution,
277 ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
278 if config.len() != self.num_potential_edges() {
279 return Err(crate::traits::EvaluationError::InvalidConfiguration(
280 "edge-selection length does not match the candidate edges".into(),
281 ));
282 }
283 Ok({
284 crate::types::Or({
285 self.augmented_graph(config)?
286 .is_some_and(|graph| is_biconnected(&graph))
287 })
288 })
289 }
290}
291
292impl<G, W> crate::solvers::BruteForceProblem for BiconnectivityAugmentation<G, W>
293where
294 G: Graph + crate::variant::VariantParam,
295 W: WeightElement + crate::variant::VariantParam,
296{
297 fn dimensions(&self) -> Vec<usize> {
298 vec![2; self.num_potential_edges()]
299 }
300}
301
302fn normalize_edge(u: usize, v: usize) -> (usize, usize) {
303 if u <= v {
304 (u, v)
305 } else {
306 (v, u)
307 }
308}
309
310fn is_biconnected(graph: &UnGraph<(), ()>) -> bool {
311 graph.node_count() <= 1
312 || (connected_components(graph) == 1 && articulation_points(graph).is_empty())
313}
314
315crate::declare_variants! {
316 default BiconnectivityAugmentation<SimpleGraph, i64> => "2^num_potential_edges" create BiconnectivityAugmentationCreateSpec,
317}
318
319crate::register_brute_force! {
320 BiconnectivityAugmentation<SimpleGraph, i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
321}
322
323#[cfg(feature = "example-db")]
324pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
325 vec![crate::example_db::specs::ModelExampleSpec {
326 id: "biconnectivity_augmentation",
327 instance: Box::new(BiconnectivityAugmentation::new(
328 SimpleGraph::path(6),
329 vec![
330 (0, 2, 1),
331 (0, 3, 2),
332 (0, 4, 3),
333 (1, 3, 1),
334 (1, 4, 2),
335 (1, 5, 3),
336 (2, 4, 1),
337 (2, 5, 2),
338 (3, 5, 1),
339 ],
340 4,
341 )),
342 optimal_config: serde_json::json!(vec![
343 true, false, false, true, false, false, true, false, true
344 ]),
345 optimal_value: serde_json::json!(true),
346 }]
347}
348
349#[cfg(test)]
350pub(crate) fn example_instance() -> BiconnectivityAugmentation<SimpleGraph, i64> {
351 BiconnectivityAugmentation::new(
352 SimpleGraph::path(6),
353 vec![
354 (0, 2, 1),
355 (0, 3, 2),
356 (0, 4, 3),
357 (1, 3, 1),
358 (1, 4, 2),
359 (1, 5, 3),
360 (2, 4, 1),
361 (2, 5, 2),
362 (3, 5, 1),
363 ],
364 4,
365 )
366}
367
368#[cfg(test)]
369#[path = "../../unit_tests/models/graph/biconnectivity_augmentation.rs"]
370mod tests;