qhyper 0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- QHyper/__init__.py +0 -0
- QHyper/optimizers/__init__.py +17 -0
- QHyper/optimizers/base.py +39 -0
- QHyper/optimizers/basinhopping.py +79 -0
- QHyper/optimizers/cem.py +128 -0
- QHyper/optimizers/qml_gradient_descent.py +72 -0
- QHyper/optimizers/random.py +90 -0
- QHyper/optimizers/scipy_minimizer.py +65 -0
- QHyper/problems/__init__.py +0 -0
- QHyper/problems/base.py +62 -0
- QHyper/problems/community_detection.py +185 -0
- QHyper/problems/knapsack.py +176 -0
- QHyper/problems/maxcut.py +37 -0
- QHyper/problems/tsp.py +190 -0
- QHyper/problems/workflow_scheduling.py +220 -0
- QHyper/solvers/__init__.py +14 -0
- QHyper/solvers/base.py +86 -0
- QHyper/solvers/converter.py +128 -0
- QHyper/solvers/cqm/__init__.py +1 -0
- QHyper/solvers/cqm/cqm.py +66 -0
- QHyper/solvers/dqm/__init__.py +1 -0
- QHyper/solvers/dqm/dqm.py +23 -0
- QHyper/solvers/gurobi/__init__.py +1 -0
- QHyper/solvers/gurobi/gurobi.py +77 -0
- QHyper/solvers/vqa/__init__.py +25 -0
- QHyper/solvers/vqa/base.py +108 -0
- QHyper/solvers/vqa/eval_funcs/__init__.py +0 -0
- QHyper/solvers/vqa/eval_funcs/base.py +40 -0
- QHyper/solvers/vqa/eval_funcs/wfeval.py +32 -0
- QHyper/solvers/vqa/pqc/__init__.py +0 -0
- QHyper/solvers/vqa/pqc/base.py +133 -0
- QHyper/solvers/vqa/pqc/h_qaoa.py +86 -0
- QHyper/solvers/vqa/pqc/mixers.py +12 -0
- QHyper/solvers/vqa/pqc/qaoa.py +130 -0
- QHyper/solvers/vqa/pqc/wf_qaoa.py +80 -0
- QHyper/util.py +4 -0
- qhyper-0.dev1.dist-info/LICENSE.txt +21 -0
- qhyper-0.dev1.dist-info/METADATA +26 -0
- qhyper-0.dev1.dist-info/RECORD +41 -0
- qhyper-0.dev1.dist-info/WHEEL +5 -0
- qhyper-0.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Iterable, Tuple, cast
|
|
3
|
+
|
|
4
|
+
import networkx as nx
|
|
5
|
+
import numpy as np
|
|
6
|
+
import sympy
|
|
7
|
+
from QHyper.hyperparameter_gen.parser import Expression
|
|
8
|
+
from QHyper.problems.base import Problem
|
|
9
|
+
from QHyper.util import VARIABLES
|
|
10
|
+
from sympy.core.expr import Expr
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Network:
|
|
15
|
+
graph: nx.Graph
|
|
16
|
+
resolution: float = field(default=1)
|
|
17
|
+
modularity_matrix: np.ndarray = field(init=False)
|
|
18
|
+
weight: str | None = field(default=None)
|
|
19
|
+
|
|
20
|
+
def __post_init__(self) -> None:
|
|
21
|
+
self.modularity_matrix = self.calculate_modularity_matrix()
|
|
22
|
+
|
|
23
|
+
def calculate_modularity_matrix(self) -> np.ndarray:
|
|
24
|
+
adj_matrix: np.ndarray = nx.to_numpy_array(
|
|
25
|
+
self.graph, weight=self.weight
|
|
26
|
+
)
|
|
27
|
+
degree_matrix: np.ndarray = adj_matrix.sum(axis=1)
|
|
28
|
+
m: int = np.sum(degree_matrix)
|
|
29
|
+
return (
|
|
30
|
+
adj_matrix
|
|
31
|
+
- self.resolution * np.outer(degree_matrix, degree_matrix) / m
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class KarateClubNetwork(Network):
|
|
36
|
+
def __init__(self, resolution: float = 1):
|
|
37
|
+
super().__init__(nx.karate_club_graph(), resolution=resolution)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BrainNetwork(Network):
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
input_data_dir: str,
|
|
44
|
+
input_data_name: str,
|
|
45
|
+
delimiter: str = " ",
|
|
46
|
+
resolution: int = 1,
|
|
47
|
+
):
|
|
48
|
+
adj_matrix = np.genfromtxt(
|
|
49
|
+
f"{input_data_dir}/{input_data_name}.csv", delimiter=delimiter
|
|
50
|
+
)
|
|
51
|
+
super().__init__(
|
|
52
|
+
nx.from_numpy_matrix(adj_matrix), resolution=resolution
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CommunityDetectionProblem(Problem):
|
|
57
|
+
"""
|
|
58
|
+
Problem class instance
|
|
59
|
+
- objective function for network community detection
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
----------
|
|
63
|
+
cases: int
|
|
64
|
+
number of communities into which the graph shall be divided
|
|
65
|
+
(default 2)
|
|
66
|
+
G: networkx graph
|
|
67
|
+
networkx implementation of graph
|
|
68
|
+
B: networkx modularity matrix
|
|
69
|
+
networkx implementation of modularity matrix
|
|
70
|
+
|
|
71
|
+
objective_function : Expression
|
|
72
|
+
objective function in SymPy syntax
|
|
73
|
+
constraints : list[Expression]
|
|
74
|
+
list of problem constraints in SymPy syntax, (default [])
|
|
75
|
+
variables : int
|
|
76
|
+
number of qubits in the circuit, equal to number of nodes
|
|
77
|
+
in the graph
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, network_data: Network, communities: int = 2) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
network_data: Network
|
|
85
|
+
representation of a network with graph and modularity matrix
|
|
86
|
+
communities: int
|
|
87
|
+
number of communities into which the graph shall be divided
|
|
88
|
+
(default 2)
|
|
89
|
+
"""
|
|
90
|
+
self.G: nx.Graph = network_data.graph
|
|
91
|
+
self.B: np.ndarray = network_data.modularity_matrix
|
|
92
|
+
if communities < 1:
|
|
93
|
+
raise Exception(
|
|
94
|
+
"Number of communities must be greater than or equal to 1"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self.cases: int = communities
|
|
98
|
+
self.variables: tuple[
|
|
99
|
+
sympy.Symbol
|
|
100
|
+
] = self._encode_discretes_to_one_hots()
|
|
101
|
+
self._set_objective_function()
|
|
102
|
+
self._set_one_hot_constraints(communities)
|
|
103
|
+
|
|
104
|
+
def _encode_discrete_to_one_hot(
|
|
105
|
+
self, discrete_variable: sympy.Symbol, case_value: int
|
|
106
|
+
) -> sympy.Symbol:
|
|
107
|
+
discrete_id = int(str(discrete_variable)[1:])
|
|
108
|
+
id = discrete_id * self.cases + case_value
|
|
109
|
+
return sympy.symbols(f"s{id}")
|
|
110
|
+
|
|
111
|
+
def _encode_discretes_to_one_hots(self) -> tuple[sympy.Symbol]:
|
|
112
|
+
one_hots: tuple[sympy.Symbol] = sympy.symbols(" ".join([
|
|
113
|
+
str(self._encode_discrete_to_one_hot(var, case_val))
|
|
114
|
+
for var in self._get_discrete_variable_representation()
|
|
115
|
+
for case_val in range(self.cases)
|
|
116
|
+
]))
|
|
117
|
+
return one_hots
|
|
118
|
+
|
|
119
|
+
def iter_variables_cases(self) -> Iterable[Tuple[sympy.Symbol, ...]]:
|
|
120
|
+
"""s -> (s0,s1,s2,...sn-1), (sn,sn+1,sn+2,...s2n-1), ..."""
|
|
121
|
+
return zip(*[iter(self.variables)] * self.cases)
|
|
122
|
+
|
|
123
|
+
def _get_discrete_variable_representation(
|
|
124
|
+
self,
|
|
125
|
+
) -> tuple[sympy.Symbol] | Any:
|
|
126
|
+
return sympy.symbols(
|
|
127
|
+
" ".join([f"x{i}" for i in range(len(self.G.nodes))])
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _set_objective_function(self) -> None:
|
|
131
|
+
equation: dict[VARIABLES, float] = {}
|
|
132
|
+
for i in self.G.nodes:
|
|
133
|
+
for j in range(i + 1, len(self.G.nodes)):
|
|
134
|
+
for case_val in range(self.cases):
|
|
135
|
+
x_i, x_j = sympy.symbols(f"x{i}"), sympy.symbols(f"x{j}")
|
|
136
|
+
s_i = str(self._encode_discrete_to_one_hot(x_i, case_val))
|
|
137
|
+
s_j = str(self._encode_discrete_to_one_hot(x_j, case_val))
|
|
138
|
+
equation[(s_i, s_j)] = self.B[i, j]
|
|
139
|
+
|
|
140
|
+
equation = {key: -1 * val for key, val in equation.items()}
|
|
141
|
+
|
|
142
|
+
self.objective_function = Expression(equation)
|
|
143
|
+
|
|
144
|
+
def _set_one_hot_constraints(self, communities: int) -> None:
|
|
145
|
+
ONE_HOT_CONST = -1
|
|
146
|
+
self.constraints: list[Expression] = []
|
|
147
|
+
# In the case of 1-to-1 mapping between discrete
|
|
148
|
+
# and binary variable values no one-hot constraints
|
|
149
|
+
if communities == ONE_HOT_CONST * -1:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
dummies: Iterable[Tuple[sympy.Symbol, ...]]
|
|
153
|
+
for dummies in self.iter_variables_cases():
|
|
154
|
+
expression: Expr = cast(Expr, 0)
|
|
155
|
+
dummy: sympy.Symbol
|
|
156
|
+
for dummy in dummies:
|
|
157
|
+
expression += dummy
|
|
158
|
+
expression += ONE_HOT_CONST
|
|
159
|
+
self.constraints.append(Expression(expression))
|
|
160
|
+
|
|
161
|
+
def decode_solution(self, solution: dict) -> dict:
|
|
162
|
+
ONE_HOT_VALUE = 1.0
|
|
163
|
+
decoded_solution: dict = {}
|
|
164
|
+
|
|
165
|
+
for variable, value in solution.items():
|
|
166
|
+
id = int(variable[1:])
|
|
167
|
+
if value == ONE_HOT_VALUE:
|
|
168
|
+
case_value = id % self.cases
|
|
169
|
+
variable_id = id // self.cases
|
|
170
|
+
decoded_solution[variable_id] = case_value
|
|
171
|
+
|
|
172
|
+
return self.sort_decoded_solution(decoded_solution)
|
|
173
|
+
|
|
174
|
+
def sort_encoded_solution(self, encoded_solution: dict) -> dict:
|
|
175
|
+
return {
|
|
176
|
+
str(k): encoded_solution[str(k)]
|
|
177
|
+
for k in self.variables
|
|
178
|
+
if str(k) in encoded_solution
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
def sort_decoded_solution(self, decoded_solution: dict) -> dict:
|
|
182
|
+
keyorder = [int(str(v)[1:]) for v in self.variables]
|
|
183
|
+
return {
|
|
184
|
+
k: decoded_solution[k] for k in keyorder if k in decoded_solution
|
|
185
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import sympy
|
|
3
|
+
from collections import namedtuple
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from .base import Problem
|
|
8
|
+
from QHyper.hyperparameter_gen.parser import Expression
|
|
9
|
+
|
|
10
|
+
Item = namedtuple('Item', "weight value")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Knapsack:
|
|
14
|
+
"""Knapsack class
|
|
15
|
+
|
|
16
|
+
Attributes
|
|
17
|
+
----------
|
|
18
|
+
max_weight: int
|
|
19
|
+
maximum weight of an item
|
|
20
|
+
max_item_value: int
|
|
21
|
+
maximum value of an item
|
|
22
|
+
items: list[Item]
|
|
23
|
+
list of items
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
max_weight: int,
|
|
29
|
+
max_item_value: int = 10,
|
|
30
|
+
items_amount: int = 0,
|
|
31
|
+
items: list[tuple[int, int]] = []
|
|
32
|
+
) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
max_weight: int
|
|
37
|
+
maximum weight of an item
|
|
38
|
+
max_item_value: int
|
|
39
|
+
maximum value of an item (default 10)
|
|
40
|
+
items_amount: int
|
|
41
|
+
items amount, used only for random knapsack (default 0)
|
|
42
|
+
items: list[tuple[int, int]]
|
|
43
|
+
set items in knapsack (default [])
|
|
44
|
+
"""
|
|
45
|
+
self.items: list[Item] = []
|
|
46
|
+
self.max_weight: int = max_weight
|
|
47
|
+
self.max_item_value: int = max_item_value
|
|
48
|
+
if items:
|
|
49
|
+
self.set_knapsack(items)
|
|
50
|
+
else:
|
|
51
|
+
self.generate_knapsack(items_amount)
|
|
52
|
+
|
|
53
|
+
def generate_knapsack(self, items_amount: int) -> None:
|
|
54
|
+
for _ in range(items_amount):
|
|
55
|
+
self.items.append(Item(
|
|
56
|
+
random.randint(1, self.max_weight),
|
|
57
|
+
random.randint(1, self.max_item_value)
|
|
58
|
+
))
|
|
59
|
+
|
|
60
|
+
def set_knapsack(self, items: list[tuple[int, int]]) -> None:
|
|
61
|
+
self.items = [Item(weight, value) for weight, value in items]
|
|
62
|
+
|
|
63
|
+
def __len__(self) -> int:
|
|
64
|
+
return len(self.items)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class KnapsackProblem(Problem):
|
|
68
|
+
"""Objective function and constraints for the knapsack problem
|
|
69
|
+
|
|
70
|
+
Attributes
|
|
71
|
+
----------
|
|
72
|
+
objective_function : Expression
|
|
73
|
+
objective function in SymPy syntax wrapped in Expression class
|
|
74
|
+
constraints : list[Expression]
|
|
75
|
+
list of constraints in SymPy syntax wrapped in Expression class
|
|
76
|
+
variables : int
|
|
77
|
+
number of qubits in the circuit, equals to sum of the number
|
|
78
|
+
of items in the knapsack the max weight of a knapsack
|
|
79
|
+
knapsack: Knapsack
|
|
80
|
+
Knapsack instance
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
max_weight: int,
|
|
86
|
+
max_item_value: int = 10,
|
|
87
|
+
items_amount: int = 0,
|
|
88
|
+
items: list[tuple[int, int]] = []
|
|
89
|
+
) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
max_weight: int
|
|
94
|
+
maximum weight of an item
|
|
95
|
+
max_item_value: int
|
|
96
|
+
maximum value of an item (default 10)
|
|
97
|
+
items_amount: int
|
|
98
|
+
items amount, used only for random knapsack (default 0)
|
|
99
|
+
items: list[tuple[int, int]]
|
|
100
|
+
set items in knapsack (default [])
|
|
101
|
+
"""
|
|
102
|
+
self.knapsack = Knapsack(
|
|
103
|
+
max_weight, max_item_value, items_amount, items)
|
|
104
|
+
# self.variables = len(self.knapsack) + self.knapsack.max_weight
|
|
105
|
+
self.variables: tuple[sympy.Symbol] = sympy.symbols(' '.join(
|
|
106
|
+
[f'x{i}' for i
|
|
107
|
+
in range(len(self.knapsack) + self.knapsack.max_weight)]
|
|
108
|
+
))
|
|
109
|
+
self._set_objective_function()
|
|
110
|
+
self._set_constraints()
|
|
111
|
+
|
|
112
|
+
def _set_objective_function(self) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Create the objective functiif items on defined in SymPy syntax
|
|
115
|
+
"""
|
|
116
|
+
# xs = [f"x{i}" for i in range(len(self.knapsack))]
|
|
117
|
+
equation = cast(sympy.Expr, 0)
|
|
118
|
+
for i, x in enumerate(self.variables[:len(self.knapsack)]):
|
|
119
|
+
equation += self.knapsack.items[i].value*x
|
|
120
|
+
equation *= -1
|
|
121
|
+
|
|
122
|
+
self.objective_function: Expression = Expression(equation)
|
|
123
|
+
|
|
124
|
+
def _set_constraints(self) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Create constraints defined in SymPy syntax
|
|
127
|
+
"""
|
|
128
|
+
xs = [self.variables[i] for i in range(len(self.knapsack))]
|
|
129
|
+
ys = [self.variables[i] for i in range(
|
|
130
|
+
len(self.knapsack), len(self.knapsack) + self.knapsack.max_weight)]
|
|
131
|
+
self.constraints: list[Expression] = []
|
|
132
|
+
equation = cast(sympy.Expr, 1)
|
|
133
|
+
for y in ys:
|
|
134
|
+
equation -= y
|
|
135
|
+
# equation = equation
|
|
136
|
+
self.constraints.append(Expression(equation))
|
|
137
|
+
equation = cast(sympy.Expr, 0)
|
|
138
|
+
for i, y in enumerate(ys):
|
|
139
|
+
equation += (i + 1)*y
|
|
140
|
+
for i, x in enumerate(xs):
|
|
141
|
+
equation += -(self.knapsack.items[i].weight)*x
|
|
142
|
+
# equation = equation
|
|
143
|
+
self.constraints.append(Expression(equation))
|
|
144
|
+
|
|
145
|
+
def get_score(self, result: str, penalty: float = 0) -> float:
|
|
146
|
+
"""Returns score of the provided outcome in bits
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
result : str
|
|
151
|
+
outcome as a string of zeros and ones
|
|
152
|
+
penalty : float
|
|
153
|
+
penalty for incorrect results (default 0)
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
float
|
|
158
|
+
Returns negated sum of value of picked items or 0 if knapsack
|
|
159
|
+
isn't correct
|
|
160
|
+
"""
|
|
161
|
+
sum = 0
|
|
162
|
+
weight = 0
|
|
163
|
+
for i, item in enumerate(self.knapsack.items):
|
|
164
|
+
if result[i] == '1':
|
|
165
|
+
sum += item.value
|
|
166
|
+
weight += item.weight
|
|
167
|
+
if weight > self.knapsack.max_weight:
|
|
168
|
+
return penalty
|
|
169
|
+
|
|
170
|
+
for i in range(self.knapsack.max_weight):
|
|
171
|
+
if result[i + len(self.knapsack)] == '1' and i + 1 != weight:
|
|
172
|
+
return penalty
|
|
173
|
+
if weight != 0 and result[weight + len(self.knapsack) - 1] != '1':
|
|
174
|
+
return penalty
|
|
175
|
+
|
|
176
|
+
return -sum
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
import sympy
|
|
4
|
+
from QHyper.hyperparameter_gen.parser import Expression
|
|
5
|
+
|
|
6
|
+
from .base import Problem
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MaxCutProblem(Problem):
|
|
10
|
+
def __init__(self, edges: list[tuple[int, int]]) -> None:
|
|
11
|
+
self.edges = edges
|
|
12
|
+
self.variables = sympy.symbols(
|
|
13
|
+
" ".join(
|
|
14
|
+
[f"x{i}" for i in range(max(v for edge in edges for v in edge))]
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
self._set_objective_function()
|
|
19
|
+
self.constraints = []
|
|
20
|
+
|
|
21
|
+
def _set_objective_function(self) -> None:
|
|
22
|
+
equation = cast(sympy.Expr, 0)
|
|
23
|
+
|
|
24
|
+
for e in self.edges:
|
|
25
|
+
x_i, x_j = self.variables[e[0] - 1], self.variables[e[1] - 1]
|
|
26
|
+
equation -= x_i * (1 - x_j) + x_j * (1 - x_i)
|
|
27
|
+
|
|
28
|
+
self.objective_function: Expression = Expression(equation)
|
|
29
|
+
|
|
30
|
+
def get_score(self, result: str, penalty: float = 0) -> float:
|
|
31
|
+
sum = 0
|
|
32
|
+
|
|
33
|
+
for e in self.edges:
|
|
34
|
+
x_i, x_j = int(result[e[0] - 1]), int(result[e[1] - 1])
|
|
35
|
+
sum += x_i * (1 - x_j) + x_j * (1 - x_i)
|
|
36
|
+
|
|
37
|
+
return sum
|
QHyper/problems/tsp.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import sympy
|
|
5
|
+
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from .base import Problem
|
|
9
|
+
from ..hyperparameter_gen.parser import Expression
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TSP:
|
|
13
|
+
"""Traveling Salesman Problem
|
|
14
|
+
|
|
15
|
+
Attributes
|
|
16
|
+
----------
|
|
17
|
+
number_of_cities : int
|
|
18
|
+
cities count
|
|
19
|
+
coords_range : tuple[int, int]
|
|
20
|
+
range of coordinates of the cities
|
|
21
|
+
cities_coords : list[tuple[float, float]]
|
|
22
|
+
coordinates of the cities
|
|
23
|
+
distance_matrix : list[list[float]]
|
|
24
|
+
matrix of distances between cities
|
|
25
|
+
normalized_distance_matrix : list[list[float]]
|
|
26
|
+
normalized to (0, 1] matrix of distances between cities
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
number_of_cities: int,
|
|
32
|
+
coords_range: tuple[int, int] = (0, 10000)
|
|
33
|
+
) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
number_of_cities : int
|
|
38
|
+
cities count
|
|
39
|
+
coords_range : tuple[float, float]
|
|
40
|
+
range of coordinates of the cities
|
|
41
|
+
"""
|
|
42
|
+
self.number_of_cities: int = number_of_cities
|
|
43
|
+
self.coords_range: tuple[int, int] = coords_range
|
|
44
|
+
self.cities_coords: list[tuple[float, float]] = self.get_cities()
|
|
45
|
+
self.distance_matrix: list[
|
|
46
|
+
list[float]] = self.calculate_distance_matrix()
|
|
47
|
+
self.normalized_distance_matrix: list[
|
|
48
|
+
list[float]] = self.normalize_distance_matrix()
|
|
49
|
+
|
|
50
|
+
def get_cities(self) -> list[tuple[float, float]]:
|
|
51
|
+
cities_coords = np.random.randint(
|
|
52
|
+
self.coords_range[0], self.coords_range[1],
|
|
53
|
+
size=(self.number_of_cities, 2))
|
|
54
|
+
return cast(list[tuple[float, float]], cities_coords)
|
|
55
|
+
|
|
56
|
+
def calculate_distance_between_points(
|
|
57
|
+
self, point_A: tuple[float, float], point_B: tuple[float, float]
|
|
58
|
+
) -> float:
|
|
59
|
+
return cast(
|
|
60
|
+
float,
|
|
61
|
+
np.sqrt((point_A[0] - point_B[0]) ** 2
|
|
62
|
+
+ (point_A[1] - point_B[1]) ** 2)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def calculate_distance_matrix(self) -> list[list[float]]:
|
|
66
|
+
distance_matrix = np.zeros(
|
|
67
|
+
(self.number_of_cities, self.number_of_cities))
|
|
68
|
+
for i in range(self.number_of_cities):
|
|
69
|
+
for j in range(i, self.number_of_cities):
|
|
70
|
+
distance_matrix[i][j] = self.calculate_distance_between_points(
|
|
71
|
+
self.cities_coords[i], self.cities_coords[j])
|
|
72
|
+
distance_matrix[j][i] = distance_matrix[i][j]
|
|
73
|
+
return cast(list[list[float]], distance_matrix)
|
|
74
|
+
|
|
75
|
+
def normalize_distance_matrix(self) -> list[list[float]]:
|
|
76
|
+
return cast(
|
|
77
|
+
list[list[float]],
|
|
78
|
+
np.divide(self.distance_matrix, np.max(self.distance_matrix))
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TSPProblem(Problem):
|
|
83
|
+
"""Class defining objective function and constraints for TSP problem
|
|
84
|
+
|
|
85
|
+
Attributes
|
|
86
|
+
----------
|
|
87
|
+
objective_function : Expression
|
|
88
|
+
objective function in SymPy syntax
|
|
89
|
+
constraints : list[Expression]
|
|
90
|
+
list of constraints in SymPy syntax
|
|
91
|
+
variables : int
|
|
92
|
+
number of qubits in the circuit, equals to number of cities
|
|
93
|
+
to the power of 2
|
|
94
|
+
tsp_instance: TSP
|
|
95
|
+
TSP problem instace
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
number_of_cities: int
|
|
101
|
+
) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
number_of_cities : int
|
|
106
|
+
number of cities
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
self.tsp_instance = TSP(number_of_cities)
|
|
110
|
+
self.variables: tuple[sympy.Symbol] = sympy.symbols(
|
|
111
|
+
' '.join([f'x{i}' for i in range(number_of_cities ** 2)]))
|
|
112
|
+
self._set_objective_function()
|
|
113
|
+
self._set_constraints()
|
|
114
|
+
|
|
115
|
+
def _calc_bit(self, i: int, t: int) -> int:
|
|
116
|
+
return i + t * self.tsp_instance.number_of_cities
|
|
117
|
+
|
|
118
|
+
def _set_objective_function(self) -> None:
|
|
119
|
+
equation = cast(sympy.Expr, 0)
|
|
120
|
+
for i, j in itertools.permutations(
|
|
121
|
+
range(0, self.tsp_instance.number_of_cities), 2
|
|
122
|
+
):
|
|
123
|
+
curr = 0
|
|
124
|
+
for t in range(self.tsp_instance.number_of_cities):
|
|
125
|
+
inc_t = t + 1
|
|
126
|
+
if inc_t == self.tsp_instance.number_of_cities:
|
|
127
|
+
inc_t = 0
|
|
128
|
+
curr += (
|
|
129
|
+
self.variables[self._calc_bit(i, t)]
|
|
130
|
+
* self.variables[self._calc_bit(j, inc_t)]
|
|
131
|
+
)
|
|
132
|
+
equation += (
|
|
133
|
+
self.tsp_instance.normalized_distance_matrix[i][j] * curr
|
|
134
|
+
)
|
|
135
|
+
self.objective_function: Expression = Expression(equation)
|
|
136
|
+
|
|
137
|
+
def _set_constraints(self) -> None:
|
|
138
|
+
self.constraints: list[Expression] = []
|
|
139
|
+
for i in range(self.tsp_instance.number_of_cities):
|
|
140
|
+
equation = cast(sympy.Expr, 1)
|
|
141
|
+
for t in range(self.tsp_instance.number_of_cities):
|
|
142
|
+
equation -= self.variables[self._calc_bit(i, t)]
|
|
143
|
+
self.constraints.append(Expression(equation))
|
|
144
|
+
|
|
145
|
+
for t in range(self.tsp_instance.number_of_cities):
|
|
146
|
+
equation = cast(sympy.Expr, 1)
|
|
147
|
+
for i in range(self.tsp_instance.number_of_cities):
|
|
148
|
+
equation -= self.variables[self._calc_bit(i, t)]
|
|
149
|
+
self.constraints.append(Expression(equation))
|
|
150
|
+
|
|
151
|
+
def _get_distance(self, key: str) -> float:
|
|
152
|
+
results = np.array_split(list(key), self.tsp_instance.number_of_cities)
|
|
153
|
+
dist: float = 0
|
|
154
|
+
tab = []
|
|
155
|
+
for result in results:
|
|
156
|
+
tab.append(list(result).index('1'))
|
|
157
|
+
|
|
158
|
+
for i in range(len(tab)):
|
|
159
|
+
j = i - 1
|
|
160
|
+
dist += (
|
|
161
|
+
self.tsp_instance.normalized_distance_matrix[tab[i]][tab[j]]
|
|
162
|
+
)
|
|
163
|
+
return dist
|
|
164
|
+
|
|
165
|
+
def _valid(self, result: str) -> bool:
|
|
166
|
+
result_bools = np.reshape(
|
|
167
|
+
list(result),
|
|
168
|
+
(-1, self.tsp_instance.number_of_cities)
|
|
169
|
+
).astype(np.bool8)
|
|
170
|
+
return cast(bool, (
|
|
171
|
+
result_bools.sum(0) == 1).all()
|
|
172
|
+
and (result_bools.sum(1) == 1).all()
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def get_score(self, result: str, penalty: float = 0) -> float:
|
|
176
|
+
"""Returns length of the route based on provided outcome in bits.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
result : str
|
|
181
|
+
route as a string of zeros and ones
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
float
|
|
186
|
+
Returns length of the route, or 0 if route wasn't correct
|
|
187
|
+
"""
|
|
188
|
+
if not self._valid(result):
|
|
189
|
+
return penalty # Bigger value that possible distance
|
|
190
|
+
return self._get_distance(result)
|