-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathmaxkcut_binary_powertwo.py
More file actions
263 lines (229 loc) · 9.91 KB
/
maxkcut_binary_powertwo.py
File metadata and controls
263 lines (229 loc) · 9.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
import networkx as nx
import numpy as np
import itertools
from qiskit import QuantumCircuit, QuantumRegister, AncillaRegister
from qiskit.circuit import Parameter
from qiskit.circuit.library import PhaseGate
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.quantum_info import SparsePauliOp, Pauli
from .graph_problem import GraphProblem
class MaxKCutBinaryPowerOfTwo(GraphProblem):
"""
Max k-CUT binary power of two graph problem.
Subclass of the `GraphProblem` class. This class uses binary encoding for the problem when k is a power of two,
or when k has been rounded up to the nearest power of two.
Attributes:
G (nx.Graph): The input graph on which the Max k-Cut problem is defined.
k_cuts (int): The number of partitions (colors) to cut the graph into (must be a power of two).
method (str): The method used for circuit construction ("PauliBasis" or "Diffusion").
fix_one_node (bool): If True, fixes the last node to a specific color, reducing the number of variables.
Methods:
is_power_of_two(k): Checks if the given integer k is a power of two.
validate_parameters(k, method): Validates the input parameters for k and method.
construct_colors(): Constructs the mapping from binary strings to color classes based on k.
create_edge_circuit(theta): Creates the parameterized quantum circuit for an edge, according to the chosen method.
create_edge_circuit_fixed_node(theta): Creates the parameterized quantum circuit for an edge when one node is fixed.
getPauliOperator(k_cuts, color_encoding): Returns the Pauli operators for the cost Hamiltonian for the given k and encoding.
"""
def __init__(
self,
G: nx.Graph,
k_cuts: int,
method: str = "Diffusion",
fix_one_node: bool = False, # this fixes the last node to color 1, i.e., one qubit gets removed
) -> None:
"""
Args:
G (nx.Graph): The input graph on which the Max k-Cut problem is defined.
k_cuts (int): The number of partitions (colors) to cut the graph into (must be a power of two).
method (str): The method used for circuit construction ("PauliBasis" or "Diffusion").
fix_one_node (bool): If True, fixes the last node to a specific color, reducing the number of variables.
Raises:
ValueError: If k_cuts is not a power of two, is less than 2, greater than 8, or if method is not valid.
"""
MaxKCutBinaryPowerOfTwo.validate_parameters(k_cuts, method)
self.k_cuts = k_cuts
self.method = method
N_qubits_per_node = int(np.ceil(np.log2(self.k_cuts)))
super().__init__(G, N_qubits_per_node, fix_one_node)
if self.method == "PauliBasis":
self.op, self.ophalf = self.getPauliOperator(self.k_cuts, "all")
self.construct_colors()
@staticmethod
def is_power_of_two(k) -> bool:
"""
Checks if the given integer k is a power of two.
Args:
k (int): The integer to check.
Returns:
bool: True if k is a power of two, False otherwise.
"""
if k > 0 and (k & (k - 1)) == 0:
return True
return False
@staticmethod
def validate_parameters(k, method) -> None:
"""
Validates the input parameters for k and method.
Args:
k (int): Number of partitions (colors).
method (str): Circuit construction method ("PauliBasis" or "Diffusion").
Raises:
ValueError: If k is not a power of two.
ValueError: If k is less than 2 or greater than 8.
ValueError: If method is not valid.
"""
### 1) k_cuts must be a power of 2
if not MaxKCutBinaryPowerOfTwo.is_power_of_two(k):
raise ValueError("k_cuts must be a power of two")
### 2) k_cuts needs to be between 2 and 8
if (k < 2) or (k > 8):
raise ValueError(
"k_cuts must be 2 or more, and is not implemented for k_cuts > 8"
)
### 3) method
valid_methods = ["PauliBasis", "Diffusion"]
if method not in valid_methods:
raise ValueError("method must be in " + str(valid_methods))
def construct_colors(self):
"""
Constructs the mapping from binary strings to color classes based on k.
Raises:
ValueError: If k_cuts is not supported.
"""
if self.k_cuts == 2:
self.colors = {"color1": ["0"], "color2": ["1"]}
elif self.k_cuts == 4:
self.colors = {
"color1": ["00"],
"color2": ["01"],
"color3": ["10"],
"color4": ["11"],
}
elif self.k_cuts == 8:
self.colors = {
"color1": ["000"],
"color2": ["001"],
"color3": ["010"],
"color4": ["011"],
"color5": ["100"],
"color6": ["101"],
"color7": ["110"],
"color8": ["111"],
}
# Create a dictionary to map each index to its corresponding set
self.bitstring_to_color = {}
for key, indices in self.colors.items():
for index in indices:
self.bitstring_to_color[index] = key
def create_edge_circuit(self, theta):
"""
Creates the parameterized quantum circuit for an edge, according to the chosen method.
Args:
theta (float): The phase parameter.
Returns:
qc (QuantumCircuit): The constructed quantum circuit for the edge.
"""
qc = QuantumCircuit(2 * self.N_qubits_per_node)
if self.method == "PauliBasis":
qc.append(PauliEvolutionGate(self.op, time=theta), qc.qubits)
else:
for k in range(self.N_qubits_per_node):
qc.cx(k, self.N_qubits_per_node + k)
qc.x(self.N_qubits_per_node + k)
# C^{n-1}Phase
if self.N_qubits_per_node == 1:
phase_gate = PhaseGate(-theta)
else:
phase_gate = PhaseGate(-theta).control(self.N_qubits_per_node - 1)
qc.append(
phase_gate,
[
self.N_qubits_per_node - 1 + ind
for ind in range(1, self.N_qubits_per_node + 1)
],
)
for k in reversed(range(self.N_qubits_per_node)):
qc.x(self.N_qubits_per_node + k)
qc.cx(k, self.N_qubits_per_node + k)
return qc
def create_edge_circuit_fixed_node(self, theta):
"""
Creates the parameterized quantum circuit for an edge when one node is fixed.
Args:
theta (float): The phase parameter.
Returns:
qc (QuantumCircuit): The constructed quantum circuit for the edge with a fixed node.
"""
qc = QuantumCircuit(self.N_qubits_per_node)
if self.method == "PauliBasis":
qc.append(PauliEvolutionGate(self.ophalf, time=-theta), qc.qubits)
else:
qc.x(qc.qubits)
# C^{n-1}Phase
if self.N_qubits_per_node == 1:
phase_gate = PhaseGate(-theta)
else:
phase_gate = PhaseGate(-theta).control(self.N_qubits_per_node - 1)
qc.append(phase_gate, qc.qubits)
qc.x(qc.qubits)
return qc
def getPauliOperator(self, k_cuts, color_encoding):
"""
Returns the Pauli operators for the cost Hamiltonian for the given k and encoding.
Args:
k_cuts (int): Number of partitions (colors).
color_encoding (str): The encoding scheme for colors.
Returns:
op (SparsePauliOp): The full Pauli operator for the cost Hamiltonian.
ophalf (SparsePauliOp): The half Pauli operator for the fixed-node case.
"""
# flip Pauli strings, because of qiskit's little endian encoding
if k_cuts == 2:
P = [
[2 / (2**1), Pauli("ZZ")],
]
Phalf = [
[2 / (2**1), Pauli("Z")],
]
elif k_cuts == 4:
P = [
[-8 / (2**4), Pauli("IIII"[::-1])],
[+8 / (2**4), Pauli("IZIZ"[::-1])],
[+8 / (2**4), Pauli("ZIZI"[::-1])],
[+8 / (2**4), Pauli("ZZZZ"[::-1])],
]
Phalf = [
[-8 / (2**4), Pauli("II"[::-1])],
[+8 / (2**4), Pauli("IZ"[::-1])],
[+8 / (2**4), Pauli("ZI"[::-1])],
[+8 / (2**4), Pauli("ZZ"[::-1])],
]
else:
P = [
[-48 / (2**6), Pauli("IIIIII"[::-1])],
[+16 / (2**6), Pauli("IIZIIZ"[::-1])],
[+16 / (2**6), Pauli("IZIIZI"[::-1])],
[+16 / (2**6), Pauli("IZZIZZ"[::-1])],
[+16 / (2**6), Pauli("ZIIZII"[::-1])],
[+16 / (2**6), Pauli("ZIZZIZ"[::-1])],
[+16 / (2**6), Pauli("ZZIZZI"[::-1])],
[+16 / (2**6), Pauli("ZZZZZZ"[::-1])],
]
Phalf = [
[-48 / (2**6), Pauli("III"[::-1])],
[+16 / (2**6), Pauli("IIZ"[::-1])],
[+16 / (2**6), Pauli("IZI"[::-1])],
[+16 / (2**6), Pauli("IZZ"[::-1])],
[+16 / (2**6), Pauli("ZII"[::-1])],
[+16 / (2**6), Pauli("ZIZ"[::-1])],
[+16 / (2**6), Pauli("ZZI"[::-1])],
[+16 / (2**6), Pauli("ZZZ"[::-1])],
]
# devide coefficients by 2, since:
# "The evolution gates are related to the Pauli rotation gates by a factor of 2"
op = SparsePauliOp([item[1] for item in P], coeffs=[item[0] / 2 for item in P])
ophalf = SparsePauliOp(
[item[1] for item in Phalf], coeffs=[item[0] / 2 for item in Phalf]
)
return op, ophalf