1use crate::models::algebraic::QUBO;
10use crate::models::graph::TravelingSalesman;
11use crate::reduction;
12use crate::rules::traits::{ReduceTo, ReductionResult};
13use crate::topology::SimpleGraph;
14use std::collections::HashMap;
15
16#[derive(Debug, Clone)]
18pub struct ReductionTravelingSalesmanToQUBO {
19 target: QUBO<i64>,
20 num_vertices: usize,
21 num_edges: usize,
22 edge_index: HashMap<(usize, usize), usize>,
23 objective_offset: i64,
24 feasible_energy_upper: i128,
25 small_optimum: Option<(Vec<bool>, i64)>,
26}
27
28impl ReductionResult for ReductionTravelingSalesmanToQUBO {
29 type Source = TravelingSalesman<SimpleGraph, i64>;
30 type Target = QUBO<i64>;
31
32 fn target_problem(&self) -> &Self::Target {
33 &self.target
34 }
35
36 fn extract_solution(
41 &self,
42 target_solution: &<Self::Target as crate::traits::Problem>::Solution,
43 ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
44 crate::rules::traits::validate_target_witness(
45 self.target_problem(),
46 target_solution,
47 |value| {
48 crate::rules::AggregateReductionResult::extract_value(self, value)
49 .0
50 .is_some()
51 },
52 "target energy does not encode a feasible tour",
53 )?;
54 if self.num_vertices < 3 {
55 return Ok(self
56 .small_optimum
57 .as_ref()
58 .expect("value mapping established a small tour")
59 .0
60 .clone());
61 }
62
63 Ok({
64 let n = self.num_vertices;
65
66 let tour: Vec<usize> = (0..n)
67 .map(|position| {
68 let mut selected =
69 (0..n).filter(|&vertex| target_solution[vertex * n + position]);
70 match (selected.next(), selected.next()) {
71 (Some(vertex), None) => Ok(vertex),
72 _ => Err(crate::rules::ExtractionError::invalid(format!(
73 "tour position {position} does not select exactly one vertex"
74 ))),
75 }
76 })
77 .collect::<crate::rules::ExtractionResult<_>>()?;
78
79 let mut config = vec![false; self.num_edges];
81 for p in 0..n {
82 let u = tour[p];
83 let v = tour[(p + 1) % n];
84 let key = (u.min(v), u.max(v));
85 let &edge = self.edge_index.get(&key).ok_or_else(|| {
86 crate::rules::ExtractionError::invalid(format!(
87 "target tour uses absent source edge ({u}, {v})"
88 ))
89 })?;
90 config[edge] = true;
91 }
92
93 config
94 })
95 }
96}
97
98#[crate::aggregate_reduction]
99impl crate::rules::AggregateReductionResult for ReductionTravelingSalesmanToQUBO {
100 type Source = TravelingSalesman<SimpleGraph, i64>;
101 type Target = QUBO<i64>;
102
103 fn target_problem(&self) -> &Self::Target {
104 &self.target
105 }
106
107 fn extract_value(&self, value: crate::types::Min<i64>) -> crate::types::Min<i64> {
108 if self.num_vertices < 3 {
109 return crate::types::Min(
110 value
111 .0
112 .and(self.small_optimum.as_ref().map(|(_, cost)| *cost)),
113 );
114 }
115 crate::types::Min(
118 value
119 .0
120 .filter(|&energy| i128::from(energy) < self.feasible_energy_upper)
121 .map(|energy| energy + self.objective_offset),
122 )
123 }
124}
125
126#[reduction(transform = {
127 exact {
128 num_vars = "num_vertices^2",
129 },
130 upper_bound {
131 num_quadratic_terms = "(num_vertices^2) * ((num_vertices^2) - 1) / 2",
132 },
133})]
134impl ReduceTo<QUBO<i64>> for TravelingSalesman<SimpleGraph, i64> {
135 type Result = ReductionTravelingSalesmanToQUBO;
136
137 fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
138 let n = self.num_vertices();
139 let edges = self.edges();
140
141 let overflow = |operation| {
142 crate::rules::ReductionError::integer_overflow::<Self, QUBO<i64>>(operation)
143 };
144 let num_edges = edges.len();
145 let dim = n
146 .checked_mul(n)
147 .ok_or_else(|| overflow("computing the number of QUBO variables"))?;
148
149 if n < 3 {
152 let mut candidates: Vec<usize> = edges
153 .iter()
154 .enumerate()
155 .filter(|&(_, &(u, v, _))| (n == 1 && u == v) || (n == 2 && u != v))
156 .map(|(index, _)| index)
157 .collect();
158
159 let small_optimum = if n > 0 && candidates.len() >= n {
160 candidates.select_nth_unstable_by_key(n - 1, |&index| (edges[index].2, index));
161 let mut solution = vec![false; num_edges];
162 let mut cost = 0i64;
163 for &index in &candidates[..n] {
164 solution[index] = true;
165 cost = cost
166 .checked_add(edges[index].2)
167 .ok_or_else(|| overflow("summing a small tour cost"))?;
168 }
169 Some((solution, cost))
170 } else {
171 None
172 };
173 return Ok(ReductionTravelingSalesmanToQUBO {
174 target: QUBO::from_matrix(vec![vec![0; dim]; dim])
175 .map_err(<Self as ReduceTo<QUBO<i64>>>::target_construction)?,
176 num_vertices: n,
177 num_edges,
178 edge_index: HashMap::new(),
179 objective_offset: 0,
180 feasible_energy_upper: 0,
181 small_optimum,
182 });
183 }
184
185 let mut edge_index: HashMap<(usize, usize), usize> = HashMap::new();
188 for (index, &(u, v, weight)) in edges.iter().enumerate() {
189 if u == v {
190 continue;
191 }
192 let key = (u.min(v), u.max(v));
193 edge_index
194 .entry(key)
195 .and_modify(|previous| {
196 if weight < edges[*previous].2 {
197 *previous = index;
198 }
199 })
200 .or_insert(index);
201 }
202 let shift = edge_index
203 .values()
204 .map(|&index| edges[index].2)
205 .fold(0, i64::min);
206 let mut shifted_sum = 0i64;
207 let mut absolute_sum = 0i64;
208 for &index in edge_index.values() {
209 let weight = edges[index].2;
210 absolute_sum = absolute_sum
211 .checked_add(
212 weight
213 .checked_abs()
214 .ok_or_else(|| overflow("taking the absolute value of a tour weight"))?,
215 )
216 .ok_or_else(|| overflow("summing absolute tour weights"))?;
217 let shifted = weight
218 .checked_sub(shift)
219 .ok_or_else(|| overflow("shifting a tour weight"))?;
220 shifted_sum = shifted_sum
221 .checked_add(shifted)
222 .ok_or_else(|| overflow("summing shifted tour weights"))?;
223 }
224 let a = shifted_sum
227 .max(absolute_sum)
228 .checked_add(1)
229 .ok_or_else(|| overflow("computing the tour penalty"))?;
230 let omitted_constant = 2 * n as i128 * i128::from(a);
231 let objective_offset = i64::try_from(omitted_constant + n as i128 * i128::from(shift))
233 .map_err(|_| overflow("computing the tour objective offset"))?;
234 i64::try_from(omitted_constant)
237 .map_err(|_| overflow("computing the tour penalty constant"))?;
238 let feasible_energy_upper = i128::from(a) - omitted_constant;
239
240 let mut matrix = vec![vec![0i64; dim]; dim];
242
243 let mut add_upper = |i: usize, j: usize, val: i64| {
245 let (lo, hi) = if i <= j { (i, j) } else { (j, i) };
246 matrix[lo][hi] = matrix[lo][hi]
247 .checked_add(val)
248 .ok_or_else(|| overflow("adding a tour QUBO coefficient"))?;
249 Ok::<(), crate::rules::ReductionError>(())
250 };
251
252 for v in 0..n {
257 for p in 0..n {
258 add_upper(
260 v * n + p,
261 v * n + p,
262 a.checked_neg()
263 .ok_or_else(|| overflow("negating the tour penalty"))?,
264 )?;
265 }
266 for p1 in 0..n {
267 for p2 in (p1 + 1)..n {
268 add_upper(
270 v * n + p1,
271 v * n + p2,
272 a.checked_mul(2)
273 .ok_or_else(|| overflow("doubling the tour penalty"))?,
274 )?;
275 }
276 }
277 }
278
279 for p in 0..n {
282 for v in 0..n {
283 add_upper(
284 v * n + p,
285 v * n + p,
286 a.checked_neg()
287 .ok_or_else(|| overflow("negating the tour penalty"))?,
288 )?;
289 }
290 for v1 in 0..n {
291 for v2 in (v1 + 1)..n {
292 add_upper(
293 v1 * n + p,
294 v2 * n + p,
295 a.checked_mul(2)
296 .ok_or_else(|| overflow("doubling the tour penalty"))?,
297 )?;
298 }
299 }
300 }
301
302 for u in 0..n {
305 for v in (u + 1)..n {
306 let cost = edge_index.get(&(u, v)).map_or(a, |&index| {
307 edges[index].2 - shift
309 });
310 for p in 0..n {
311 let p_next = (p + 1) % n;
312 add_upper(u * n + p, v * n + p_next, cost)?;
314 add_upper(v * n + p, u * n + p_next, cost)?;
316 }
317 }
318 }
319
320 let target = QUBO::from_matrix(matrix)
321 .map_err(<Self as ReduceTo<QUBO<i64>>>::target_construction)?;
322
323 Ok(ReductionTravelingSalesmanToQUBO {
324 target,
325 num_vertices: n,
326 num_edges,
327 edge_index,
328 objective_offset,
329 feasible_energy_upper,
330 small_optimum: None,
331 })
332 }
333}
334
335#[cfg(feature = "example-db")]
336pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
337 use crate::export::SolutionPair;
338 use crate::models::algebraic::QUBO;
339
340 vec![crate::example_db::specs::RuleExampleSpec {
341 id: "travelingsalesman_to_qubo",
342 build: || {
343 let source = TravelingSalesman::new(
344 SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]),
345 vec![1, 2, 3],
346 );
347 crate::example_db::specs::rule_example_with_witness::<_, QUBO<i64>>(
348 source,
349 SolutionPair {
350 source_config: serde_json::json!(vec![true, true, true]),
351 target_config: serde_json::json!(vec![
352 false, false, true, true, false, false, false, true, false
353 ]),
354 },
355 )
356 },
357 }]
358}
359
360#[cfg(test)]
361#[path = "../unit_tests/rules/travelingsalesman_qubo.rs"]
362mod tests;