Skip to main content

problemreductions/rules/
travelingsalesman_qubo.rs

1//! Reduction from TravelingSalesman to QUBO.
2//!
3//! Uses the standard position-based QUBO encoding for TSP:
4//! - Binary variables x_{v,p} = 1 iff vertex v is at position p in the tour
5//! - H_A: each vertex appears exactly once (row constraint)
6//! - H_B: each position has exactly one vertex (column constraint)
7//! - H_C: objective encoding edge costs between consecutive positions
8
9use 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/// Result of reducing TravelingSalesman to QUBO.
17#[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    /// Decode position encoding back to edge-based configuration.
37    ///
38    /// The QUBO solution uses n^2 binary variables x_{v,p} (vertex v at position p).
39    /// We extract the tour order, then map consecutive pairs to edge indices.
40    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            // Build edge-based config: for each consecutive pair in the tour, mark the edge
80            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        // The offset is nonnegative; below the feasibility bound, the sum is
116        // less than A + n * shift <= A, so addition cannot overflow in either direction.
117        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        // The source represents a connected degree-two edge set. With fewer
150        // than three vertices this means one loop or two parallel edges.
151        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        // A tour on at least three vertices uses no loops and at most one
186        // edge per endpoint pair. Retain the cheapest parallel edge.
187        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        // Every permutation tour uses n edges. Shifting each cost therefore
225        // adds a constant. All costs are now nonnegative even off-premise.
226        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        // A >= |shift| makes this offset positive. Check its transport once.
232        let objective_offset = i64::try_from(omitted_constant + n as i128 * i128::from(shift))
233            .map_err(|_| overflow("computing the tour objective offset"))?;
234        // A valid tour's energy is its shifted cost minus 2nA, and every
235        // partial sum stays within [-2nA, shifted cost]; -2nA must fit i64.
236        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        // Build n^2 x n^2 upper-triangular QUBO matrix
241        let mut matrix = vec![vec![0i64; dim]; dim];
242
243        // Helper: add value to upper-triangular position
244        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        // H_A: each vertex visited exactly once (row constraint)
253        // For each vertex v: (sum_p x_{v,p} - 1)^2
254        // = sum_p x_{v,p}^2 - 2*sum_p x_{v,p} + 1
255        // = -sum_p x_{v,p} + 2*sum_{p1<p2} x_{v,p1}*x_{v,p2} + const
256        for v in 0..n {
257            for p in 0..n {
258                // Diagonal: -A (from expanding (sum - 1)^2, the -2*x + x^2 = -x for binary)
259                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                    // Cross terms: 2*A * x_{v,p1} * x_{v,p2}
269                    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        // H_B: each position has exactly one vertex (column constraint)
280        // For each position p: (sum_v x_{v,p} - 1)^2
281        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        // H_C: distance objective
303        // For each pair (u, v), add cost for x_{u,p} * x_{v,p_next} and x_{v,p} * x_{u,p_next}
304        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                    // The bound calculation already checked this subtraction.
308                    edges[index].2 - shift
309                });
310                for p in 0..n {
311                    let p_next = (p + 1) % n;
312                    // x_{u,p} * x_{v,p_next}
313                    add_upper(u * n + p, v * n + p_next, cost)?;
314                    // x_{v,p} * x_{u,p_next}
315                    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;