Skip to content

Commit 02dd122

Browse files
authored
Merge branch 'main' into patch-1
2 parents 9682caf + 3162fef commit 02dd122

File tree

5 files changed

+171
-17
lines changed

5 files changed

+171
-17
lines changed

.mergify.yml

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
queue_rules:
22
- name: automerge
3-
conditions:
4-
- check-success=Deprecation_Messages_and_Coverage (3.8)
5-
6-
pull_request_rules:
7-
- name: automatic merge on CI success and review
8-
conditions:
3+
queue_conditions:
94
- check-success=Deprecation_Messages_and_Coverage (3.8)
105
- "#approved-reviews-by>=1"
116
- label=automerge
127
- label!=on hold
13-
actions:
14-
queue:
15-
name: automerge
16-
method: squash
8+
merge_conditions:
9+
- check-success=Deprecation_Messages_and_Coverage (3.8)
10+
merge_method: squash
11+
12+
pull_request_rules:
1713
- name: backport
1814
conditions:
1915
- label=stable backport potential
2016
actions:
2117
backport:
2218
branches:
2319
- stable/0.6
20+
- name: automatic merge on CI success and review
21+
conditions: []
22+
actions:
23+
queue:

qiskit_optimization/applications/tsp.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This code is part of a Qiskit project.
22
#
3-
# (C) Copyright IBM 2018, 2023.
3+
# (C) Copyright IBM 2018, 2024.
44
#
55
# This code is licensed under the Apache License, Version 2.0. You may
66
# obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -46,18 +46,29 @@ def to_quadratic_program(self) -> QuadraticProgram:
4646
mdl = Model(name="TSP")
4747
n = self._graph.number_of_nodes()
4848
x = {(i, k): mdl.binary_var(name=f"x_{i}_{k}") for i in range(n) for k in range(n)}
49+
50+
# Only sum over existing edges in the graph
4951
tsp_func = mdl.sum(
5052
self._graph.edges[i, j]["weight"] * x[(i, k)] * x[(j, (k + 1) % n)]
51-
for i in range(n)
52-
for j in range(n)
53+
for i, j in self._graph.edges
54+
for k in range(n)
55+
)
56+
# Add reverse edges since we have an undirected graph
57+
tsp_func += mdl.sum(
58+
self._graph.edges[i, j]["weight"] * x[(j, k)] * x[(i, (k + 1) % n)]
59+
for i, j in self._graph.edges
5360
for k in range(n)
54-
if i != j
5561
)
62+
5663
mdl.minimize(tsp_func)
5764
for i in range(n):
5865
mdl.add_constraint(mdl.sum(x[(i, k)] for k in range(n)) == 1)
5966
for k in range(n):
6067
mdl.add_constraint(mdl.sum(x[(i, k)] for i in range(n)) == 1)
68+
for i, j in nx.non_edges(self._graph):
69+
for k in range(n):
70+
mdl.add_constraint(x[i, k] + x[j, (k + 1) % n] <= 1)
71+
mdl.add_constraint(x[j, k] + x[i, (k + 1) % n] <= 1)
6172
op = from_docplex_mp(mdl)
6273
return op
6374

@@ -73,7 +84,7 @@ def interpret(
7384
A list of nodes whose indices correspond to its order in a prospective cycle.
7485
"""
7586
x = self._result_to_x(result)
76-
n = int(np.sqrt(len(x)))
87+
n = self._graph.number_of_nodes()
7788
route = [] # type: List[Union[int, List[int]]]
7889
for p__ in range(n):
7990
p_step = []
@@ -141,6 +152,8 @@ def create_random_instance(n: int, low: int = 0, high: int = 100, seed: int = No
141152
def parse_tsplib_format(filename: str) -> "Tsp":
142153
"""Read a graph in TSPLIB format from file and return a Tsp instance.
143154
155+
Only the EUC_2D edge weight format is supported.
156+
144157
Args:
145158
filename: the name of the file.
146159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
features:
3+
- |
4+
Added support for non-complete graphs for the TSP problem.

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
qiskit>=0.44
22
qiskit-algorithms>=0.2.0
33
scipy>=1.9.0,<1.14
4-
numpy>=1.17
4+
numpy>=1.17,<2.2.0
55
docplex>=2.21.207,!=2.24.231
66
setuptools>=40.1.0
77
networkx>=2.6.3

test/applications/test_tsp.py

+138-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This code is part of a Qiskit project.
22
#
3-
# (C) Copyright IBM 2018, 2023.
3+
# (C) Copyright IBM 2018, 2024.
44
#
55
# This code is licensed under the Apache License, Version 2.0. You may
66
# obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -111,5 +111,142 @@ def test_parse_tsplib_format(self):
111111
self.assertEqual(graph.number_of_edges(), 51 * 50 / 2) # fully connected graph
112112

113113

114+
class TestTspCustomGraph(QiskitOptimizationTestCase):
115+
"""Test Tsp class with a custom non-geometric graph"""
116+
117+
def setUp(self):
118+
"""Set up test cases."""
119+
super().setUp()
120+
self.graph = nx.Graph()
121+
self.edges_with_weights = [
122+
(0, 1, 5),
123+
(1, 2, 5),
124+
(1, 3, 15),
125+
(2, 3, 15),
126+
(2, 4, 5),
127+
(3, 4, 5),
128+
(3, 0, 5),
129+
]
130+
131+
self.graph.add_nodes_from(range(5))
132+
for source, target, weight in self.edges_with_weights:
133+
self.graph.add_edge(source, target, weight=weight)
134+
135+
op = QuadraticProgram()
136+
for _ in range(25):
137+
op.binary_var()
138+
139+
result_vector = np.zeros(25)
140+
result_vector[0] = 1
141+
result_vector[6] = 1
142+
result_vector[12] = 1
143+
result_vector[23] = 1
144+
result_vector[19] = 1
145+
146+
self.optimal_path = [0, 1, 2, 4, 3]
147+
self.optimal_edges = [(0, 1), (1, 2), (2, 4), (4, 3), (3, 0)]
148+
self.optimal_cost = 25
149+
150+
self.result = OptimizationResult(
151+
x=result_vector,
152+
fval=self.optimal_cost,
153+
variables=op.variables,
154+
status=OptimizationResultStatus.SUCCESS,
155+
)
156+
157+
def test_to_quadratic_program(self):
158+
"""Test to_quadratic_program with custom graph"""
159+
tsp = Tsp(self.graph)
160+
quadratic_program = tsp.to_quadratic_program()
161+
162+
self.assertEqual(quadratic_program.name, "TSP")
163+
self.assertEqual(quadratic_program.get_num_vars(), 25)
164+
165+
for variable in quadratic_program.variables:
166+
self.assertEqual(variable.vartype, VarType.BINARY)
167+
168+
objective = quadratic_program.objective
169+
self.assertEqual(objective.constant, 0)
170+
self.assertEqual(objective.sense, QuadraticObjective.Sense.MINIMIZE)
171+
172+
# Test objective quadratic terms
173+
quadratic_terms = objective.quadratic.to_dict()
174+
for source, target, weight in self.edges_with_weights:
175+
for position in range(5):
176+
next_position = (position + 1) % 5
177+
key = (
178+
min(source * 5 + position, target * 5 + next_position),
179+
max(source * 5 + position, target * 5 + next_position),
180+
)
181+
self.assertIn(key, quadratic_terms)
182+
self.assertEqual(quadratic_terms[key], weight)
183+
184+
linear_constraints = quadratic_program.linear_constraints
185+
186+
# Test node constraints (each node appears once)
187+
for node in range(5):
188+
self.assertEqual(linear_constraints[node].sense, Constraint.Sense.EQ)
189+
self.assertEqual(linear_constraints[node].rhs, 1)
190+
self.assertEqual(
191+
linear_constraints[node].linear.to_dict(),
192+
{5 * node + pos: 1 for pos in range(5)},
193+
)
194+
195+
# Test position constraints (each position filled once)
196+
for position in range(5):
197+
self.assertEqual(linear_constraints[5 + position].sense, Constraint.Sense.EQ)
198+
self.assertEqual(linear_constraints[5 + position].rhs, 1)
199+
self.assertEqual(
200+
linear_constraints[5 + position].linear.to_dict(),
201+
{5 * node + position: 1 for node in range(5)},
202+
)
203+
204+
# Test non-edge constraints
205+
non_edges = list(nx.non_edges(self.graph))
206+
constraint_idx = 10 # Start after node and position constraints
207+
208+
for i, j in non_edges:
209+
for k in range(5):
210+
next_k = (k + 1) % 5
211+
212+
# Check forward constraint: x[i,k] + x[j,(k+1)%n] <= 1
213+
constraint = linear_constraints[constraint_idx]
214+
self.assertEqual(constraint.sense, Constraint.Sense.LE)
215+
self.assertEqual(constraint.rhs, 1)
216+
linear_dict = constraint.linear.to_dict()
217+
self.assertEqual(len(linear_dict), 2)
218+
self.assertEqual(linear_dict[i * 5 + k], 1)
219+
self.assertEqual(linear_dict[j * 5 + next_k], 1)
220+
constraint_idx += 1
221+
222+
# Check backward constraint: x[j,k] + x[i,(k+1)%n] <= 1
223+
constraint = linear_constraints[constraint_idx]
224+
self.assertEqual(constraint.sense, Constraint.Sense.LE)
225+
self.assertEqual(constraint.rhs, 1)
226+
linear_dict = constraint.linear.to_dict()
227+
self.assertEqual(len(linear_dict), 2)
228+
self.assertEqual(linear_dict[j * 5 + k], 1)
229+
self.assertEqual(linear_dict[i * 5 + next_k], 1)
230+
constraint_idx += 1
231+
232+
# Verify total number of constraints
233+
expected_constraints = (
234+
5 # node constraints
235+
+ 5 # position constraints
236+
+ len(non_edges) * 2 * 5 # non-edge constraints (2 per non-edge per position)
237+
)
238+
self.assertEqual(len(linear_constraints), expected_constraints)
239+
240+
def test_interpret(self):
241+
"""Test interpret with custom graph"""
242+
tsp = Tsp(self.graph)
243+
self.assertEqual(tsp.interpret(self.result), self.optimal_path)
244+
245+
def test_edgelist(self):
246+
"""Test _edgelist with custom graph"""
247+
tsp = Tsp(self.graph)
248+
self.assertEqual(tsp._edgelist(self.result), self.optimal_edges)
249+
250+
114251
if __name__ == "__main__":
115252
unittest.main()

0 commit comments

Comments
 (0)