1"""This module contains the EC class."""
2
3import ast
4from collections import defaultdict
5from typing import Literal
6
7import matplotlib.pyplot as plt
8import networkx as nx
9import numpy as np
10from qiskit.quantum_info import SparsePauliOp
11
12from qlauncher import hampy, models
13from qlauncher.base import Problem
14
15
[docs]
16class EC(Problem):
17 """
18 Class for exact cover problem.
19
20 The exact cover problem is a combinatorial optimization problem that involves finding a subset of a given set
21 of elements such that the subset covers all elements and the number of elements in the subset is minimized.
22 The class contains an instance of the problem, so it can be passed into QLauncher.
23
24 Attributes:
25
26 onehot (str): The one-hot encoding used for the problem.
27 instance (any): The instance of the problem.
28 instance_name (str | None): The name of the instance.
29 instance_path (str): The path to the instance file.
30
31 """
32
33 def __init__(self, instance: list[set[int]], instance_name: str = 'unnamed') -> None:
34 super().__init__(instance=instance, instance_name=instance_name)
35
[docs]
36 @staticmethod
37 def from_preset(instance_name: Literal['micro', 'default'], **kwargs) -> 'EC':
38 match instance_name:
39 case 'micro':
40 instance = [{1, 2}, {1}]
41 case 'default':
42 instance = [{1, 4, 7}, {1, 4}, {4, 5, 7}, {3, 5, 6}, {2, 3, 6, 7}, {2, 7}]
43 case _:
44 raise TypeError
45 return EC(instance=instance, instance_name=instance_name, **kwargs)
46
[docs]
47 @classmethod
48 def from_file(cls, path: str, **kwargs) -> 'EC':
49 with open(path, encoding='utf-8') as file:
50 read_file = file.read()
51 instance = ast.literal_eval(read_file)
52 return EC(instance, **kwargs)
53
[docs]
54 def visualize(self, marked: str | None = None) -> None:
55 G = nx.Graph()
56 size = len(self.instance)
57 ec = [set(map(str, x)) for x in self.instance]
58 names = set.union(*ec)
59 for i in range(len(ec)):
60 G.add_node(i)
61 for i in sorted(names):
62 G.add_node(i)
63 covered = defaultdict(int)
64 for node, edges in enumerate(ec):
65 for goal_node in edges:
66 G.add_edge(node, goal_node)
67
68 edge_colors = []
69 for goal_node in G.edges():
70 node, str_node = goal_node
71 if marked is None:
72 edge_colors.append('black')
73 continue
74 if marked[node] == '1':
75 edge_colors.append('red')
76 covered[str_node] += 1
77 else:
78 edge_colors.append('gray')
79 color_map = []
80 for node in G:
81 if isinstance(node, int):
82 color_map.append('lightblue')
83 elif covered[node] == 0:
84 color_map.append('yellow')
85 elif covered[node] == 1:
86 color_map.append('lightgreen')
87 else:
88 color_map.append('red')
89 pos = nx.bipartite_layout(G, nodes=range(size))
90 nx.draw(G, pos, node_color=color_map, with_labels=True, edge_color=edge_colors)
91 plt.title('Exact Cover Problem Visualization')
92 plt.show()
93
[docs]
94 @staticmethod
95 def generate_ec_instance(n: int, m: int, p: float = 0.5, **kwargs) -> 'EC':
96 graph = nx.bipartite.random_graph(n, m, p)
97 right_nodes = [n for n, d in graph.nodes(data=True) if d['bipartite'] == 0]
98 instance = [set() for _ in right_nodes]
99 for left, right in graph.edges():
100 instance[left].add(right)
101 return EC(instance=instance, **kwargs)
102
[docs]
103 def to_hamiltonian(self, onehot: Literal['exact', 'quadratic'] = 'exact') -> models.Hamiltonian:
104 onehots = []
105 for ele in set().union(*self.instance):
106 ohs = set()
107 for i, subset in enumerate(self.instance):
108 if ele in subset:
109 ohs.add(i)
110 onehots.append(ohs)
111
112 equation = hampy.Equation(len(self.instance))
113
114 for ohs in onehots:
115 if onehot == 'exact':
116 part = ~hampy.one_in_n(list(ohs), len(self.instance))
117 elif onehot == 'quadratic':
118 part = hampy.one_in_n(list(ohs), len(self.instance), quadratic=True)
119 equation += part
120
121 return models.Hamiltonian(
122 equation,
123 mixer_hamiltonian=self.get_mixer_hamiltonian(),
124 )
125
[docs]
126 def get_mixer_hamiltonian(self, amount_of_rings: int | None = None) -> hampy.Equation:
127 """generates mixer hamiltonian"""
128
129 # looking for all rings in a data and creating a list with them
130 ring, x_gate = [], []
131 main_set = []
132 for element_set in self.instance:
133 for elem in element_set:
134 if elem not in main_set:
135 main_set.append(elem)
136 constraints = []
137 for element in main_set:
138 element_set = set()
139 for index, _ in enumerate(self.instance):
140 if element in self.instance[index]:
141 element_set.add(index)
142 if len(element_set) > 0 and element_set not in constraints:
143 constraints.append(element_set)
144
145 ring.append(max(constraints, key=len))
146
147 ring_qubits = set.union(*ring)
148
149 for set_ in constraints:
150 if len(set_.intersection(ring_qubits)) == 0:
151 ring.append(set_)
152 ring_qubits.update(set_)
153
154 if amount_of_rings is not None:
155 max_amount_of_rings, user_rings = len(ring), []
156 if amount_of_rings > max_amount_of_rings:
157 raise ValueError(f'Too many rings. Maximum amount is {max_amount_of_rings}')
158 if amount_of_rings == 0:
159 ring_qubits = []
160 else:
161 current_qubits = ring[0]
162 for index in range(amount_of_rings):
163 user_rings.append(ring[index])
164 current_qubits = current_qubits.union(ring[index])
165 ring_qubits = current_qubits
166 x_gate.extend(id for id, _ in enumerate(self.instance) if id not in ring_qubits)
167
168 # connecting all parts of mixer hamiltonian together
169 mix_ham = hampy.Equation(len(self.instance))
170 for set_ in ring:
171 mix_ham += ring_ham(set_, len(self.instance))
172
173 x_gate_ham = hampy.Equation(len(self.instance))
174 # creating mixer hamiltonians for all qubits that aren't in rings (in other words applying X gate to them)
175 for elem in x_gate:
176 x_gate_ham += SparsePauliOp.from_sparse_list([('X', [elem], 1)], len(self.instance))
177
178 return mix_ham + x_gate_ham
179
[docs]
180 def to_qubo(self) -> models.QUBO:
181 n = len(self.instance)
182 # Calculate length tabs
183 len_routes = []
184 for route in self.instance:
185 len_routes.append(len(route))
186
187 qubo = np.zeros((n, n))
188
189 Jrr_dict = {}
190 indices = np.triu_indices(n, 1)
191 for i1, i2 in zip(indices[0], indices[1], strict=False):
192 Jrr_dict[(i1, i2)] = len(set(self.instance[i1]).intersection(set(self.instance[i2]))) / 2
193
194 hr_dict = {}
195 for i in range(n):
196 i_sum = sum(len(set(r).intersection(set(self.instance[i]))) for r in self.instance)
197
198 hr_dict[i] = (i_sum - len(self.instance[i]) * 2) / 2
199
200 # Space
201 for key, value in Jrr_dict.items():
202 qubo[key[0]][key[1]] = value
203 qubo[key[1]][key[0]] = qubo[key[0]][key[1]]
204
205 for i in hr_dict:
206 qubo[i][i] = -hr_dict[i]
207
208 return models.QUBO(qubo, 0)
209
210
[docs]
211def ring_ham(set_ring: set[int], n: int) -> hampy.Equation:
212 total = hampy.Equation(n)
213 ring = list(set_ring)
214 for index in range(len(ring) - 1):
215 total += SparsePauliOp.from_sparse_list(
216 [
217 ('XX', [ring[index], ring[index + 1]], 1),
218 ('YY', [ring[index], ring[index + 1]], 1),
219 ],
220 n,
221 )
222 total += SparsePauliOp.from_sparse_list(
223 [
224 ('XX', [ring[-1], ring[0]], 1),
225 ('YY', [ring[-1], ring[0]], 1),
226 ],
227 n,
228 )
229 return total