1use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
7use crate::topology::DirectedGraph;
8use crate::traits::Problem;
9use crate::types::WeightElement;
10use num_traits::Zero;
11use serde::{Deserialize, Deserializer, Serialize};
12use std::cmp::Ordering;
13use std::collections::BTreeSet;
14
15inventory::submit! {
16 ProblemSchemaEntry {
17 name: "StrongConnectivityAugmentation",
18 display_name: "Strong Connectivity Augmentation",
19 aliases: &[],
20 dimensions: &[
21 VariantDimension::new("weight", "i64", &["i64"]),
22 ],
23 category: crate::registry::ProblemCategory::Graph,
24 module_path: module_path!(),
25 description: "Add a bounded set of weighted candidate arcs to make a digraph strongly connected",
26 fields: &[
27 FieldInfo { name: "graph", type_name: "DirectedGraph", description: "The initial directed graph G=(V,A)" },
28 FieldInfo { name: "candidate_arcs", type_name: "Vec<(usize, usize, W)>", description: "Candidate augmenting arcs (u, v, w(u,v)) not already present in G" },
29 FieldInfo { name: "bound", type_name: "W::Sum", description: "Upper bound B on the total added weight" },
30 ],
31 }
32}
33
34#[derive(Debug, Clone, Serialize)]
41pub struct StrongConnectivityAugmentation<W: WeightElement> {
42 graph: DirectedGraph,
43 candidate_arcs: Vec<(usize, usize, W)>,
44 bound: W::Sum,
45}
46
47impl<W: WeightElement> StrongConnectivityAugmentation<W> {
48 pub fn try_new(
50 graph: DirectedGraph,
51 candidate_arcs: Vec<(usize, usize, W)>,
52 bound: W::Sum,
53 ) -> Result<Self, crate::registry::ConstructionError> {
54 if !matches!(
55 bound.partial_cmp(&W::Sum::zero()),
56 Some(Ordering::Equal | Ordering::Greater)
57 ) {
58 return Err("bound must be nonnegative".to_string().into());
59 }
60
61 let num_vertices = graph.num_vertices();
62 let mut seen_pairs = BTreeSet::new();
63
64 for (u, v, weight) in &candidate_arcs {
65 if *u >= num_vertices || *v >= num_vertices {
66 return Err(format!(
67 "candidate arc ({}, {}) references vertex >= num_vertices ({})",
68 u, v, num_vertices
69 )
70 .into());
71 }
72 if !matches!(
73 weight.to_sum().partial_cmp(&W::Sum::zero()),
74 Some(Ordering::Greater)
75 ) {
76 return Err(format!("candidate arc ({}, {}) weight must be positive", u, v).into());
77 }
78 if graph.has_arc(*u, *v) {
79 return Err(format!(
80 "candidate arc ({}, {}) already exists in the base graph",
81 u, v
82 )
83 .into());
84 }
85 if !seen_pairs.insert((*u, *v)) {
86 return Err(format!("duplicate candidate arc ({}, {})", u, v).into());
87 }
88 }
89
90 Ok(Self {
91 graph,
92 candidate_arcs,
93 bound,
94 })
95 }
96
97 pub fn new(
105 graph: DirectedGraph,
106 candidate_arcs: Vec<(usize, usize, W)>,
107 bound: W::Sum,
108 ) -> Self {
109 Self::try_new(graph, candidate_arcs, bound).unwrap_or_else(|msg| panic!("{msg}"))
110 }
111
112 pub fn graph(&self) -> &DirectedGraph {
114 &self.graph
115 }
116
117 pub fn candidate_arcs(&self) -> &[(usize, usize, W)] {
119 &self.candidate_arcs
120 }
121
122 pub fn bound(&self) -> &W::Sum {
124 &self.bound
125 }
126
127 pub fn max_numeric_magnitude_bits(&self) -> u64 {
129 crate::types::max_numeric_magnitude_bits(
130 self.candidate_arcs
131 .iter()
132 .map(|(_, _, weight)| weight.to_sum())
133 .chain(std::iter::once(self.bound.clone())),
134 )
135 }
136
137 pub fn num_vertices(&self) -> usize {
139 self.graph.num_vertices()
140 }
141
142 pub fn num_arcs(&self) -> usize {
144 self.graph.num_arcs()
145 }
146
147 pub fn num_potential_arcs(&self) -> usize {
149 self.candidate_arcs.len()
150 }
151
152 pub fn is_weighted(&self) -> bool {
154 !W::IS_UNIT
155 }
156
157 pub fn is_valid_solution(
159 &self,
160 config: &[bool],
161 ) -> Result<bool, crate::traits::EvaluationError> {
162 self.evaluate_config(config)
163 }
164
165 fn evaluate_config(&self, config: &[bool]) -> Result<bool, crate::traits::EvaluationError> {
166 if config.len() != self.candidate_arcs.len() {
167 return Ok(false);
168 }
169
170 let mut total = W::Sum::zero();
171 let mut augmented_arcs = self.graph.arcs();
172
173 for ((u, v, weight), &selected) in self.candidate_arcs.iter().zip(config.iter()) {
174 if selected {
175 total = W::checked_add_to_sum(
176 total,
177 weight.to_sum(),
178 "summing strong-connectivity augmentation weights",
179 )?;
180 if total > self.bound {
181 return Ok(false);
182 }
183 augmented_arcs.push((*u, *v));
184 }
185 }
186
187 Ok(DirectedGraph::new(self.graph.num_vertices(), augmented_arcs).is_strongly_connected())
188 }
189}
190
191impl<W> Problem for StrongConnectivityAugmentation<W>
192where
193 W: WeightElement + crate::variant::VariantParam,
194{
195 const NAME: &'static str = "StrongConnectivityAugmentation";
196 type Solution = Vec<bool>;
197 type Value = crate::types::Or;
198
199 crate::problem_parameters![
200 ("max_numeric_magnitude_bits", max_numeric_magnitude_bits),
201 ("num_arcs", num_arcs),
202 ("num_potential_arcs", num_potential_arcs),
203 ("num_vertices", num_vertices),
204 ];
205
206 fn variant() -> Vec<(&'static str, &'static str)> {
207 crate::variant_params![W]
208 }
209
210 fn evaluate(
211 &self,
212 config: &Self::Solution,
213 ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
214 if config.len() != self.candidate_arcs.len() {
215 return Err(crate::traits::EvaluationError::InvalidConfiguration(
216 "arc-selection length does not match the candidate arcs".into(),
217 ));
218 }
219 Ok(crate::types::Or(self.evaluate_config(config)?))
220 }
221}
222
223impl<W> crate::solvers::BruteForceProblem for StrongConnectivityAugmentation<W>
224where
225 W: WeightElement + crate::variant::VariantParam,
226{
227 fn dimensions(&self) -> Vec<usize> {
228 vec![2; self.candidate_arcs.len()]
229 }
230}
231
232crate::declare_variants! {
233 default StrongConnectivityAugmentation<i64> => "2^num_potential_arcs",
234}
235
236crate::register_brute_force! {
237 StrongConnectivityAugmentation<i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
238}
239
240#[derive(Deserialize)]
241struct StrongConnectivityAugmentationData<W: WeightElement> {
242 graph: DirectedGraph,
243 candidate_arcs: Vec<(usize, usize, W)>,
244 bound: W::Sum,
245}
246
247impl<'de, W> Deserialize<'de> for StrongConnectivityAugmentation<W>
248where
249 W: WeightElement + Deserialize<'de>,
250 W::Sum: Deserialize<'de>,
251{
252 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
253 where
254 D: Deserializer<'de>,
255 {
256 let data = StrongConnectivityAugmentationData::<W>::deserialize(deserializer)?;
257 Self::try_new(data.graph, data.candidate_arcs, data.bound).map_err(serde::de::Error::custom)
258 }
259}
260
261#[cfg(feature = "example-db")]
262pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
263 vec![crate::example_db::specs::ModelExampleSpec {
264 id: "strong_connectivity_augmentation",
265 instance: Box::new(StrongConnectivityAugmentation::new(
269 DirectedGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]),
270 vec![
271 (4, 0, 10), (4, 3, 3), (4, 2, 3), (4, 1, 3), (3, 0, 7), (3, 1, 3), (2, 0, 7), (2, 1, 3), (1, 0, 5), ],
281 8,
282 )),
283 optimal_config: serde_json::json!(vec![
284 false, false, false, true, false, false, false, false, true
285 ]),
286 optimal_value: serde_json::json!(true),
287 }]
288}
289
290#[cfg(test)]
291#[path = "../../unit_tests/models/graph/strong_connectivity_augmentation.rs"]
292mod tests;