Skip to content

Commit 63bbdf9

Browse files
hmyuuuclaudeGiggleLiu
authored
Fix #167: Add TravelingSalesman to QUBO reduction (#605)
* Add plan for #167: TravelingSalesman to QUBO reduction Co-authored-by: Claude <noreply@anthropic.com> * Implement TravelingSalesman to QUBO reduction (#167) Position-based encoding (Lucas 2014): n² binary variables x_{v,p} with one-hot row/column constraints and distance objective. Handles non-complete graphs by penalizing non-edge consecutive pairs. Co-authored-by: Claude <noreply@anthropic.com> * chore: remove plan file after implementation Co-authored-by: Claude <noreply@anthropic.com> * style: rustfmt after upstream merge Co-authored-by: Claude <noreply@anthropic.com> * fix: address Copilot review — store num_edges instead of edges Vec, call self.edges() once Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com>
1 parent 3689ff6 commit 63bbdf9

7 files changed

Lines changed: 641 additions & 0 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
{
2+
"source": {
3+
"problem": "TravelingSalesman",
4+
"variant": {
5+
"weight": "i32",
6+
"graph": "SimpleGraph"
7+
},
8+
"instance": {
9+
"num_edges": 3,
10+
"num_vertices": 3
11+
}
12+
},
13+
"target": {
14+
"problem": "QUBO",
15+
"variant": {
16+
"weight": "f64"
17+
},
18+
"instance": {
19+
"matrix": [
20+
[
21+
-14.0,
22+
14.0,
23+
14.0,
24+
14.0,
25+
1.0,
26+
1.0,
27+
14.0,
28+
2.0,
29+
2.0
30+
],
31+
[
32+
0.0,
33+
-14.0,
34+
14.0,
35+
1.0,
36+
14.0,
37+
1.0,
38+
2.0,
39+
14.0,
40+
2.0
41+
],
42+
[
43+
0.0,
44+
0.0,
45+
-14.0,
46+
1.0,
47+
1.0,
48+
14.0,
49+
2.0,
50+
2.0,
51+
14.0
52+
],
53+
[
54+
0.0,
55+
0.0,
56+
0.0,
57+
-14.0,
58+
14.0,
59+
14.0,
60+
14.0,
61+
3.0,
62+
3.0
63+
],
64+
[
65+
0.0,
66+
0.0,
67+
0.0,
68+
0.0,
69+
-14.0,
70+
14.0,
71+
3.0,
72+
14.0,
73+
3.0
74+
],
75+
[
76+
0.0,
77+
0.0,
78+
0.0,
79+
0.0,
80+
0.0,
81+
-14.0,
82+
3.0,
83+
3.0,
84+
14.0
85+
],
86+
[
87+
0.0,
88+
0.0,
89+
0.0,
90+
0.0,
91+
0.0,
92+
0.0,
93+
-14.0,
94+
14.0,
95+
14.0
96+
],
97+
[
98+
0.0,
99+
0.0,
100+
0.0,
101+
0.0,
102+
0.0,
103+
0.0,
104+
0.0,
105+
-14.0,
106+
14.0
107+
],
108+
[
109+
0.0,
110+
0.0,
111+
0.0,
112+
0.0,
113+
0.0,
114+
0.0,
115+
0.0,
116+
0.0,
117+
-14.0
118+
]
119+
],
120+
"num_vars": 9
121+
}
122+
},
123+
"overhead": [
124+
{
125+
"field": "num_vars",
126+
"expr": {
127+
"Pow": [
128+
{
129+
"Var": "num_vertices"
130+
},
131+
{
132+
"Const": 2.0
133+
}
134+
]
135+
},
136+
"formula": "num_vertices^2"
137+
}
138+
]
139+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
{
2+
"solutions": [
3+
{
4+
"source_config": [
5+
1,
6+
1,
7+
1
8+
],
9+
"target_config": [
10+
0,
11+
0,
12+
1,
13+
0,
14+
1,
15+
0,
16+
1,
17+
0,
18+
0
19+
]
20+
},
21+
{
22+
"source_config": [
23+
1,
24+
1,
25+
1
26+
],
27+
"target_config": [
28+
0,
29+
0,
30+
1,
31+
1,
32+
0,
33+
0,
34+
0,
35+
1,
36+
0
37+
]
38+
},
39+
{
40+
"source_config": [
41+
1,
42+
1,
43+
1
44+
],
45+
"target_config": [
46+
0,
47+
1,
48+
0,
49+
0,
50+
0,
51+
1,
52+
1,
53+
0,
54+
0
55+
]
56+
},
57+
{
58+
"source_config": [
59+
1,
60+
1,
61+
1
62+
],
63+
"target_config": [
64+
0,
65+
1,
66+
0,
67+
1,
68+
0,
69+
0,
70+
0,
71+
0,
72+
1
73+
]
74+
},
75+
{
76+
"source_config": [
77+
1,
78+
1,
79+
1
80+
],
81+
"target_config": [
82+
1,
83+
0,
84+
0,
85+
0,
86+
0,
87+
1,
88+
0,
89+
1,
90+
0
91+
]
92+
},
93+
{
94+
"source_config": [
95+
1,
96+
1,
97+
1
98+
],
99+
"target_config": [
100+
1,
101+
0,
102+
0,
103+
0,
104+
1,
105+
0,
106+
0,
107+
0,
108+
1
109+
]
110+
}
111+
]
112+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// # Traveling Salesman to QUBO Reduction (Penalty Method)
2+
//
3+
// ## Mathematical Relationship
4+
// The TSP on a graph G = (V, E) with edge weights is mapped to QUBO using
5+
// position-based encoding. Each vertex v and position k has a binary variable
6+
// x_{v,k}, with penalties enforcing:
7+
//
8+
// 1. Assignment constraint: each vertex appears exactly once in the tour
9+
// 2. Position constraint: each position has exactly one vertex
10+
// 3. Edge constraint: consecutive positions use valid edges
11+
// 4. Objective: total edge weight of the tour
12+
//
13+
// The QUBO has n^2 variables (n vertices x n positions).
14+
//
15+
// ## This Example
16+
// - Instance: K3 complete graph with edge weights [1, 2, 3]
17+
// (w01=1, w02=2, w12=3)
18+
// - Source: TravelingSalesman on 3 vertices, 3 edges
19+
// - QUBO variables: 9 (3^2 = 9, position encoding)
20+
// - Optimal tour cost = 6 (all edges used: 1 + 2 + 3)
21+
//
22+
// ## Outputs
23+
// - `docs/paper/examples/travelingsalesman_to_qubo.json` — reduction structure
24+
// - `docs/paper/examples/travelingsalesman_to_qubo.result.json` — solutions
25+
//
26+
// ## Usage
27+
// ```bash
28+
// cargo run --example reduction_travelingsalesman_to_qubo
29+
// ```
30+
31+
use problemreductions::export::*;
32+
use problemreductions::prelude::*;
33+
use problemreductions::topology::{Graph, SimpleGraph};
34+
35+
pub fn run() {
36+
println!("=== TravelingSalesman -> QUBO Reduction ===\n");
37+
38+
// K3 complete graph with edge weights: w01=1, w02=2, w12=3
39+
let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]);
40+
let tsp = TravelingSalesman::new(graph, vec![1i32, 2, 3]);
41+
42+
// Reduce to QUBO
43+
let reduction = ReduceTo::<QUBO>::reduce_to(&tsp);
44+
let qubo = reduction.target_problem();
45+
46+
println!(
47+
"Source: TravelingSalesman on K3 ({} vertices, {} edges)",
48+
tsp.graph().num_vertices(),
49+
tsp.graph().num_edges()
50+
);
51+
println!(
52+
"Target: QUBO with {} variables (position encoding: 3 vertices x 3 positions)",
53+
qubo.num_variables()
54+
);
55+
println!("Q matrix:");
56+
for row in qubo.matrix() {
57+
let formatted: Vec<String> = row.iter().map(|v| format!("{:8.1}", v)).collect();
58+
println!(" [{}]", formatted.join(", "));
59+
}
60+
61+
// Solve QUBO with brute force
62+
let solver = BruteForce::new();
63+
let qubo_solutions = solver.find_all_best(qubo);
64+
65+
// Extract and verify solutions
66+
println!("\nOptimal QUBO solutions: {}", qubo_solutions.len());
67+
let mut solutions = Vec::new();
68+
for sol in &qubo_solutions {
69+
let extracted = reduction.extract_solution(sol);
70+
let edge_names = ["(0,1)", "(0,2)", "(1,2)"];
71+
let selected: Vec<&str> = extracted
72+
.iter()
73+
.enumerate()
74+
.filter(|(_, &v)| v == 1)
75+
.map(|(i, _)| edge_names[i])
76+
.collect();
77+
println!(" Edges: {}", selected.join(", "));
78+
79+
// Closed-loop verification: check solution is valid in original problem
80+
let metric = tsp.evaluate(&extracted);
81+
assert!(metric.is_valid(), "Tour must be valid in source problem");
82+
println!(" Cost: {:?}", metric);
83+
84+
solutions.push(SolutionPair {
85+
source_config: extracted,
86+
target_config: sol.clone(),
87+
});
88+
}
89+
90+
// Cross-check with brute force on original problem
91+
let bf_solutions = solver.find_all_best(&tsp);
92+
let bf_metric = tsp.evaluate(&bf_solutions[0]);
93+
let qubo_metric = tsp.evaluate(&reduction.extract_solution(&qubo_solutions[0]));
94+
assert_eq!(
95+
bf_metric, qubo_metric,
96+
"QUBO reduction must match brute force optimum"
97+
);
98+
99+
println!(
100+
"\nVerification passed: optimal tour cost matches brute force ({:?})",
101+
bf_metric
102+
);
103+
104+
// Export JSON
105+
let source_variant = variant_to_map(TravelingSalesman::<SimpleGraph, i32>::variant());
106+
let target_variant = variant_to_map(QUBO::<f64>::variant());
107+
let overhead = lookup_overhead(
108+
"TravelingSalesman",
109+
&source_variant,
110+
"QUBO",
111+
&target_variant,
112+
)
113+
.expect("TravelingSalesman -> QUBO overhead not found");
114+
115+
let data = ReductionData {
116+
source: ProblemSide {
117+
problem: TravelingSalesman::<SimpleGraph, i32>::NAME.to_string(),
118+
variant: source_variant,
119+
instance: serde_json::json!({
120+
"num_vertices": tsp.graph().num_vertices(),
121+
"num_edges": tsp.graph().num_edges(),
122+
}),
123+
},
124+
target: ProblemSide {
125+
problem: QUBO::<f64>::NAME.to_string(),
126+
variant: target_variant,
127+
instance: serde_json::json!({
128+
"num_vars": qubo.num_vars(),
129+
"matrix": qubo.matrix(),
130+
}),
131+
},
132+
overhead: overhead_to_json(&overhead),
133+
};
134+
135+
let results = ResultData { solutions };
136+
let name = "travelingsalesman_to_qubo";
137+
write_example(name, &data, &results);
138+
}
139+
140+
fn main() {
141+
run()
142+
}

0 commit comments

Comments
 (0)