Skip to content

Commit 75ae209

Browse files
authored
Merge pull request #77 from mit-han-lab/feat-qaoa
[major] add param shift version of qaoa
2 parents 9f9a071 + 33d926c commit 75ae209

File tree

3 files changed

+269
-14
lines changed

3 files changed

+269
-14
lines changed

examples/qaoa/max_cut_backprop.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def mixer(self, beta):
4343
tqf.rx(
4444
self.q_device,
4545
wires=wire,
46-
params=2 * beta.unsqueeze(0),
46+
params=beta.unsqueeze(0),
4747
static=self.static_mode,
4848
parent_graph=self.graph,
4949
)
@@ -116,8 +116,8 @@ def forward(self, measure_all=False):
116116
expVal = 0
117117
for edge in self.input_graph:
118118
pauli_string = self.edge_to_PauliString(edge)
119-
expVal -= 0.5 * (
120-
1 - expval_joint_analytical(self.q_device, observable=pauli_string)
119+
expVal += 0.5 * (
120+
expval_joint_analytical(self.q_device, observable=pauli_string)
121121
)
122122
return expVal
123123
else:

examples/qaoa/max_cut_backprop_new.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def mixer(self, qdev, beta):
4040
for wire in range(self.n_wires):
4141
qdev.rx(
4242
wires=wire,
43-
params=2 * beta.unsqueeze(0),
43+
params=beta.unsqueeze(0),
4444
) # type: ignore
4545

4646
def entangler(self, qdev, gamma):
@@ -74,6 +74,7 @@ def circuit(self, qdev):
7474
"""
7575
execute the quantum circuit
7676
"""
77+
# print(self.betas, self.gammas)
7778
for wire in range(self.n_wires):
7879
qdev.h(
7980
wires=wire,
@@ -92,15 +93,17 @@ def forward(self, measure_all=False):
9293
qdev = tq.QuantumDevice(n_wires=self.n_wires, device=self.betas.device)
9394

9495
self.circuit(qdev)
95-
print(tq.measure(qdev, n_shots=1024))
96+
# print(tq.measure(qdev, n_shots=1024))
9697
# compute the expectation value
98+
# print(qdev.get_states_1d())
9799
if measure_all is False:
98100
expVal = 0
99101
for edge in self.input_graph:
100102
pauli_string = self.edge_to_PauliString(edge)
101-
expVal -= 0.5 * (
102-
1 - expval_joint_analytical(qdev, observable=pauli_string)
103-
)
103+
expv = expval_joint_analytical(qdev, observable=pauli_string)
104+
expVal += 0.5 * expv
105+
# print(pauli_string, expv)
106+
# print(expVal)
104107
return expVal
105108
else:
106109
return tq.measure(qdev, n_shots=1024, draw_id=0)
@@ -143,16 +146,16 @@ def main():
143146
n_wires = 4
144147
n_layers = 3
145148
model = MAXCUT(n_wires=n_wires, input_graph=input_graph, n_layers=n_layers)
146-
model.to("cuda")
149+
# model.to("cuda")
147150
# model.to(torch.device("cuda"))
148-
circ = tq2qiskit(tq.QuantumDevice(n_wires=4), model)
149-
print(circ)
151+
# circ = tq2qiskit(tq.QuantumDevice(n_wires=4), model)
152+
# print(circ)
150153
# print("The circuit is", circ.draw(output="mpl"))
151154
# circ.draw(output="mpl")
152155
# use backprop
153156
backprop_optimize(model, n_steps=300, lr=0.01)
154157
# use parameter shift rule
155-
# param_shift_optimize(model, n_steps=10, step_size=0.1)
158+
# param_shift_optimize(model, n_steps=500, step_size=100000)
156159

157160
"""
158161
Notes:
@@ -161,7 +164,7 @@ def main():
161164
"""
162165

163166
if __name__ == "__main__":
164-
import pdb
165-
pdb.set_trace()
167+
# import pdb
168+
# pdb.set_trace()
166169

167170
main()
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import torch
2+
import torchquantum as tq
3+
import torchquantum.functional as tqf
4+
5+
import random
6+
import numpy as np
7+
8+
from torchquantum.functional import mat_dict
9+
10+
from torchquantum.plugins import tq2qiskit, qiskit2tq
11+
from torchquantum.measurement import expval_joint_analytical
12+
13+
seed = 0
14+
random.seed(seed)
15+
np.random.seed(seed)
16+
torch.manual_seed(seed)
17+
18+
class MAXCUT(tq.QuantumModule):
19+
"""computes the optimal cut for a given graph.
20+
outputs: the most probable bitstring decides the set {0 or 1} each
21+
node belongs to.
22+
"""
23+
24+
def __init__(self, n_wires, input_graph, n_layers):
25+
super().__init__()
26+
27+
self.n_wires = n_wires
28+
29+
self.input_graph = input_graph # list of edges
30+
self.n_layers = n_layers
31+
self.n_edges = len(input_graph)
32+
33+
self.betas = torch.nn.Parameter(0.01 * torch.rand(self.n_layers))
34+
self.gammas = torch.nn.Parameter(0.01 * torch.rand(self.n_layers))
35+
36+
self.reset_shift_param()
37+
38+
def mixer(self, qdev, beta, layer_id):
39+
"""
40+
Apply the single rotation and entangling layer of the QAOA ansatz.
41+
mixer = exp(-i * beta * sigma_x)
42+
"""
43+
44+
for wire in range(self.n_wires):
45+
if self.shift_param_name == 'beta' and self.shift_wire == wire and layer_id == self.shift_layer:
46+
degree = self.shift_degree
47+
else:
48+
degree = 0
49+
qdev.rx(
50+
wires=wire,
51+
params=(beta.unsqueeze(0) + degree),
52+
) # type: ignore
53+
54+
def entangler(self, qdev, gamma, layer_id):
55+
"""
56+
Apply the single rotation and entangling layer of the QAOA ansatz.
57+
entangler = exp(-i * gamma * (1 - sigma_z * sigma_z)/2)
58+
"""
59+
for edge_id, edge in enumerate(self.input_graph):
60+
if self.shift_param_name == 'gamma' and edge_id == self.shift_edge_id and layer_id == self.shift_layer:
61+
degree = self.shift_degree
62+
else:
63+
degree = 0
64+
qdev.cx(
65+
[edge[0], edge[1]],
66+
) # type: ignore
67+
qdev.rz(
68+
wires=edge[1],
69+
params=(gamma.unsqueeze(0) + degree),
70+
) # type: ignore
71+
qdev.cx(
72+
[edge[0], edge[1]],
73+
) # type: ignore
74+
75+
def set_shift_param(self, layer, wire, param_name, degree, edge_id):
76+
"""
77+
set the shift parameter for the parameter shift rule
78+
"""
79+
self.shift_layer = layer
80+
self.shift_wire = wire
81+
self.shift_param_name = param_name
82+
self.shift_degree = degree
83+
self.shift_edge_id = edge_id
84+
85+
def reset_shift_param(self):
86+
"""
87+
reset the shift parameter
88+
"""
89+
self.shift_layer = None
90+
self.shift_wire = None
91+
self.shift_param_name = None
92+
self.shift_degree = None
93+
self.shift_edge_id = None
94+
95+
def edge_to_PauliString(self, edge):
96+
# construct pauli string
97+
pauli_string = ""
98+
for wire in range(self.n_wires):
99+
if wire in edge:
100+
pauli_string += "Z"
101+
else:
102+
pauli_string += "I"
103+
return pauli_string
104+
105+
def circuit(self, qdev):
106+
"""
107+
execute the quantum circuit
108+
"""
109+
# print(self.betas, self.gammas)
110+
for wire in range(self.n_wires):
111+
qdev.h(
112+
wires=wire,
113+
) # type: ignore
114+
115+
for i in range(self.n_layers):
116+
self.mixer(qdev, self.betas[i], i)
117+
self.entangler(qdev, self.gammas[i], i)
118+
119+
def forward(self, measure_all=False):
120+
"""
121+
Apply the QAOA ansatz and only measure the edge qubit on z-basis.
122+
Args:
123+
if edge is None
124+
"""
125+
qdev = tq.QuantumDevice(n_wires=self.n_wires, device=self.betas.device)
126+
127+
self.circuit(qdev)
128+
# print(tq.measure(qdev, n_shots=1024))
129+
# compute the expectation value
130+
# print(qdev.get_states_1d())
131+
if measure_all is False:
132+
expVal = 0
133+
for edge in self.input_graph:
134+
pauli_string = self.edge_to_PauliString(edge)
135+
expv = expval_joint_analytical(qdev, observable=pauli_string)
136+
expVal += 0.5 * expv
137+
# print(pauli_string, expv)
138+
# print(expVal)
139+
return expVal
140+
else:
141+
return tq.measure(qdev, n_shots=1024, draw_id=0)
142+
143+
def main():
144+
# create a input_graph
145+
input_graph = [(0, 1), (0, 3), (1, 2), (2, 3)]
146+
n_wires = 4
147+
n_layers = 3
148+
model = MAXCUT(n_wires=n_wires, input_graph=input_graph, n_layers=n_layers)
149+
# model.to("cuda")
150+
# model.to(torch.device("cuda"))
151+
# circ = tq2qiskit(tq.QuantumDevice(n_wires=4), model)
152+
# print(circ)
153+
# print("The circuit is", circ.draw(output="mpl"))
154+
# circ.draw(output="mpl")
155+
# use backprop
156+
# backprop_optimize(model, n_steps=300, lr=0.01)
157+
# use parameter shift rule
158+
param_shift_optimize(model, n_steps=500, step_size=0.01)
159+
160+
def shift_and_run(model, use_qiskit=False):
161+
# flatten the parameters into 1D array
162+
163+
grad_betas = []
164+
grad_gammas = []
165+
n_layers = model.n_layers
166+
n_wires = model.n_wires
167+
n_edges = model.n_edges
168+
169+
for i in range(n_layers):
170+
grad_gamma = 0
171+
for k in range(n_edges):
172+
model.set_shift_param(i, None, 'gamma', np.pi * 0.5, k)
173+
out1 = model(use_qiskit)
174+
model.reset_shift_param()
175+
176+
model.set_shift_param(i, None, 'gamma', -np.pi * 0.5, k)
177+
out2 = model(use_qiskit)
178+
model.reset_shift_param()
179+
180+
grad_gamma += 0.5 * (out1 - out2).squeeze().item()
181+
grad_gammas.append(grad_gamma)
182+
183+
grad_beta = 0
184+
for j in range(n_wires):
185+
model.set_shift_param(i, j, 'beta', np.pi * 0.5, None)
186+
out1 = model(use_qiskit)
187+
model.reset_shift_param()
188+
189+
model.set_shift_param(i, j, 'beta', -np.pi * 0.5, None)
190+
out2 = model(use_qiskit)
191+
model.reset_shift_param()
192+
193+
grad_beta += 0.5 * (out1 - out2).squeeze().item()
194+
grad_betas.append(grad_beta)
195+
196+
return model(use_qiskit), [grad_betas, grad_gammas]
197+
198+
199+
def param_shift_optimize(model, n_steps=10, step_size=0.1):
200+
"""finds the optimal cut where parameter shift rule is used to compute the gradient"""
201+
# optimize the parameters and return the optimal values
202+
# print(
203+
# "The initial parameters are betas = {} and gammas = {}".format(
204+
# *model.parameters()
205+
# )
206+
# )
207+
n_layers = model.n_layers
208+
for step in range(n_steps):
209+
with torch.no_grad():
210+
loss, grad_list = shift_and_run(model)
211+
# param_list = list(model.parameters())
212+
# print(
213+
# "The initial parameters are betas = {} and gammas = {}".format(
214+
# *model.parameters()
215+
# )
216+
# )
217+
# param_list = torch.cat([param.flatten() for param in param_list])
218+
219+
# print("The shape of the params", len(param_list), param_list[0].shape, param_list)
220+
# print("")
221+
# print("The shape of the grad_list = {}, 0th elem shape = {}, grad_list = {}".format(len(grad_list), grad_list[0].shape, grad_list))
222+
# print(grad_list, loss, model.betas, model.gammas)
223+
print(loss)
224+
with torch.no_grad():
225+
for i in range(n_layers):
226+
model.betas[i].copy_(model.betas[i] - step_size * grad_list[0][i])
227+
model.gammas[i].copy_(model.gammas[i] - step_size * grad_list[1][i])
228+
229+
# for param, grad in zip(param_list, grad_list):
230+
# modify the parameters and ensure that there are no multiple views
231+
# param.copy_(param - step_size * grad)
232+
# if step % 5 == 0:
233+
# print("Step: {}, Cost Objective: {}".format(step, loss.item()))
234+
235+
# print(
236+
# "The updated parameters are betas = {} and gammas = {}".format(
237+
# *model.parameters()
238+
# )
239+
# )
240+
return model(measure_all=True)
241+
242+
"""
243+
Notes:
244+
1. input_graph = [(0, 1), (3, 0), (1, 2), (2, 3)], mixer 1st & entangler 2nd, n_layers >= 2, answer is correct.
245+
246+
"""
247+
248+
if __name__ == "__main__":
249+
# import pdb
250+
# pdb.set_trace()
251+
252+
main()

0 commit comments

Comments
 (0)