-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
Copy pathcross_entropy_benchmarking.py
398 lines (329 loc) · 16.6 KB
/
cross_entropy_benchmarking.py
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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# Copyright 2019 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import (Any, Dict, Iterable, List, NamedTuple, Optional, Sequence,
Set, Tuple, Union)
import numpy as np
from matplotlib import pyplot as plt
from cirq import devices, ops, circuits, sim, work
CrossEntropyPair = NamedTuple('CrossEntropyPair', [('num_cycle', int),
('xeb_fidelity', float)])
class CrossEntropyResult:
"""Results from a cross-entropy benchmarking (XEB) experiment."""
def __init__(self, cross_entropy_pairs: Sequence[CrossEntropyPair]):
"""
Args:
cross_entropy_pairs: A sequence of NamedTuples, each of which
contains two fields: num_cycle which returns the circuit
depth as the number of cycles and xeb_fidelity which returns
the XEB fidelity after the given cycle number.
"""
self._data = cross_entropy_pairs
@property
def data(self) -> Sequence[CrossEntropyPair]:
"""Returns a sequence of CrossEntropyPairs.
Each CrossEntropyPair is a NamedTuple that contains a cycle number and
the corresponding XEB fidelity.
"""
return self._data
def plot(self, ax: Optional[plt.Axes] = None,
**plot_kwargs: Any) -> plt.Axes:
"""Plots the average XEB fidelity vs the number of cycles.
Args:
ax: the plt.Axes to plot on. If not given, a new figure is created,
plotted on, and shown.
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.
Returns:
The plt.Axes containing the plot.
"""
show_plot = not ax
if not ax:
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
num_cycles = [d.num_cycle for d in self._data]
fidelities = [d.xeb_fidelity for d in self._data]
ax.set_ylim([0, 1.1])
ax.plot(num_cycles, fidelities, 'ro-', **plot_kwargs)
ax.set_xlabel('Number of Cycles')
ax.set_ylabel('XEB Fidelity')
if show_plot:
fig.show()
return ax
def cross_entropy_benchmarking(
sampler: work.Sampler,
qubits: Sequence[ops.Qid],
*,
benchmark_ops: Sequence[ops.Moment] = None,
num_circuits: int = 20,
repetitions: int = 1000,
cycles: Union[int, Iterable[int]] = range(2, 103, 10),
scrambling_gates_per_cycle: List[List[ops.SingleQubitGate]] = None,
simulator: sim.Simulator = None,
) -> CrossEntropyResult:
r"""Cross-entropy benchmarking (XEB) of multiple qubits.
A total of M random circuits are generated, each of which comprises N
layers where N = max('cycles') or 'cycles' if a single value is specified
for the 'cycles' parameter. Every layer contains randomly generated
single-qubit gates applied to each qubit, followed by a set of
user-defined benchmarking operations (e.g. a set of two-qubit gates).
Each circuit (circuit_m) from the M random circuits is further used to
generate a set of circuits {circuit_mn}, where circuit_mn is built from the
first n cycles of circuit_m. n spans all the values in 'cycles'.
For each fixed value n, the experiment performs the following:
1) Experimentally collect a number of bit-strings for each circuit_mn via
projective measurements in the z-basis.
2) Theoretically compute the expected bit-string probabilities
$P^{th, mn}_|...00>$, $P^{th, mn}_|...01>$, $P^{th, mn}_|...10>$,
$P^{th, mn}_|...11>$ ... at the end of circuit_mn for all m and for all
possible bit-strings in the Hilbert space.
3) Compute an experimental XEB function for each circuit_mn:
$f_{mn}^{meas} = \langle D * P^{th, mn}_q - 1 \rangle$
where D is the number of states in the Hilbert space, $P^{th, mn}_q$ is the
theoretical probability of a bit-string q at the end of circuit_mn, and
$\langle \rangle$ corresponds to the ensemble average over all measured
bit-strings.
Then, take the average of $f_{mn}^{meas}$ over all circuit_mn with fixed
n to obtain:
$f_{n} ^ {meas} = (\sum_m f_{mn}^{meas}) / M$
4) Compute a theoretical XEB function for each circuit_mn:
$f_{mn}^{th} = D \sum_q (P^{th, mn}_q) ** 2 - 1$
where the summation goes over all possible bit-strings q in the Hilbert
space.
Similarly, we then average $f_m^{th}$ over all circuit_mn with fixed n to
obtain:
$f_{n} ^ {th} = (\sum_m f_{mn}^{th}) / M$
5) Calculate the XEB fidelity $\alpha_n$ at fixed n:
$\alpha_n = f_{n} ^ {meas} / f_{n} ^ {th}$
Args:
sampler: The quantum engine or simulator to run the circuits.
qubits: The qubits included in the XEB experiment.
benchmark_ops: A sequence of ops.Moment containing gate operations
between specific qubits which are to be benchmarked for fidelity.
If more than one ops.Moment is specified, the random circuits
will rotate between the ops.Moment's. As an example,
if benchmark_ops = [Moment([ops.CZ(q0, q1), ops.CZ(q2, q3)]),
Moment([ops.CZ(q1, q2)]) where q0, q1, q2 and q3 are instances of
Qid (such as GridQubits), each random circuit will apply CZ gate
between q0 and q1 plus CZ between q2 and q3 for the first cycle,
CZ gate between q1 and q2 for the second cycle, CZ between q0 and
q1 and CZ between q2 and q3 for the third cycle and so on. If
None, the circuits will consist only of single-qubit gates.
num_circuits: The total number of random circuits to be used.
repetitions: The number of measurements for each circuit to estimate
the bit-string probabilities.
cycles: The different numbers of circuit layers in the XEB study.
Could be a single or a collection of values.
scrambling_gates_per_cycle: If None (by default), the single-qubit
gates are chosen from X/2 ($\pi/2$ rotation around the X axis),
Y/2 ($\pi/2$ rotation around the Y axis) and (X + Y)/2 ($\pi/2$
rotation around an axis $\pi/4$ away from the X on the equator of
the Bloch sphere). Otherwise the single-qubit gates for each layer
are chosen from a list of possible choices (each choice is a list
of one or more single-qubit gates).
simulator: A simulator that calculates the bit-string probabilities
of the ideal circuit. By default, this is set to sim.Simulator().
Returns:
A CrossEntropyResult object that stores and plots the result.
"""
simulator = sim.Simulator() if simulator is None else simulator
num_qubits = len(qubits)
if isinstance(cycles, int):
cycle_range = [cycles]
else:
cycle_range = list(cycles)
# These store the measured and simulated bit-string probabilities from
# all trials in two dictionaries. The keys of the dictionaries are the
# numbers of cycles. The values are 2D arrays with each row being the
# probabilities obtained from a single trial.
probs_meas = {
n: np.zeros((num_circuits, 2**num_qubits)) for n in cycle_range
}
probs_exp = {
n: np.zeros((num_circuits, 2**num_qubits)) for n in cycle_range
}
for k in range(num_circuits):
# Generates one random XEB circuit with max(num_cycle_range) cycles.
# Then the first n cycles of the circuit are taken to generate
# shorter circuits with n cycles (n taken from cycles). All of these
# circuits are stored in circuits_k.
circuits_k = _build_xeb_circuits(qubits, cycle_range,
scrambling_gates_per_cycle,
benchmark_ops)
# Run each circuit with the sampler to obtain a collection of
# bit-strings, from which the bit-string probabilities are estimated.
probs_meas_k = _measure_prob_distribution(sampler, repetitions, qubits,
circuits_k)
# Simulate each circuit with the Cirq simulator to obtain the
# wavefunction at the end of each circuit, from which the
# theoretically expected bit-string probabilities are obtained.
probs_exp_k = [] # type: List[np.ndarray]
for circ_k in circuits_k:
res = simulator.simulate(circ_k, qubit_order=qubits)
state_probs = np.abs(np.asarray(res.final_state) # type: ignore
)**2
probs_exp_k.append(state_probs)
for i, num_cycle in enumerate(cycle_range):
probs_exp[num_cycle][k, :] = probs_exp_k[i]
probs_meas[num_cycle][k, :] = probs_meas_k[i]
fidelity_vals = _xeb_fidelities(probs_exp, probs_meas)
xeb_data = [
CrossEntropyPair(c, k) for (c, k) in zip(cycle_range, fidelity_vals)
]
return CrossEntropyResult(xeb_data)
def build_entangling_layers(qubits: Sequence[devices.GridQubit],
two_qubit_gate: ops.TwoQubitGate
) -> List[ops.Moment]:
"""Builds a sequence of gates that entangle all pairs of qubits on a grid.
The qubits are restricted to be physically on a square grid with distinct
row and column indices (not every node of the grid needs to have a
qubit). To entangle all pairs of qubits, a user-specified two-qubit gate
is applied between each and every pair of qubit that are next to each
other. In general, a total of four sets of parallel operations are needed to
perform all possible two-qubit gates. We proceed as follows:
The first layer applies two-qubit gates to qubits (i, j) and (i, j + 1)
where i is any integer and j is an even integer. The second layer
applies two-qubit gates to qubits (i, j) and (i + 1, j) where i is an even
integer and j is any integer. The third layer applies two-qubit gates
to qubits (i, j) and (i, j + 1) where i is any integer and j is an odd
integer. The fourth layer applies two-qubit gates to qubits (i, j) and
(i + 1, j) where i is an odd integer and j is any integer.
After the layers are built as above, any empty layer is ejected.:
Cycle 1: Cycle 2:
q00 ── q01 q02 ── q03 q00 q01 q02 q03
| | | |
q10 ── q11 q12 ── q13 q10 q11 q12 q13
q20 ── q21 q22 ── q23 q20 q21 q22 q23
| | | |
q30 ── q31 q32 ── q33 q30 q31 q32 q33
Cycle 3: Cycle 4:
q00 q01 ── q02 q03 q00 q01 q02 q03
q10 q11 ── q12 q13 q10 q11 q12 q13
| | | |
q20 q21 ── q22 q23 q20 q21 q22 q23
q30 q31 ── q32 q33 q30 q31 q32 q33
Args:
qubits: The grid qubits included in the entangling operations.
two_qubit_gate: The two-qubit gate to be applied between all
neighboring pairs of qubits.
Returns:
A list of ops.Moment, with a maximum length of 4. Each ops.Moment
includes two-qubit gates which can be performed at the same time.
"""
interaction_sequence = _default_interaction_sequence(qubits)
return [
ops.Moment([two_qubit_gate(q_a, q_b)
for (q_a, q_b) in pairs])
for pairs in interaction_sequence
]
def _build_xeb_circuits(
qubits: Sequence[ops.Qid],
cycles: Sequence[int],
single_qubit_gates: List[List[ops.SingleQubitGate]] = None,
benchmark_ops: Sequence[ops.Moment] = None,
) -> List[circuits.Circuit]:
if benchmark_ops is not None:
num_d = len(benchmark_ops)
else:
num_d = 0
max_cycles = max(cycles)
if single_qubit_gates is None:
single_rots = _random_half_rotations(qubits, max_cycles)
else:
single_rots = _random_any_gates(qubits, single_qubit_gates, max_cycles)
all_circuits = [] # type: List[circuits.Circuit]
for num_cycles in cycles:
circuit_exp = circuits.Circuit()
for i in range(num_cycles):
circuit_exp.append(single_rots[i])
if benchmark_ops is not None:
for op_set in benchmark_ops[i % num_d]:
circuit_exp.append(op_set)
all_circuits.append(circuit_exp)
return all_circuits
def _measure_prob_distribution(sampler: work.Sampler, repetitions: int,
qubits: Sequence[ops.Qid],
circuit_list: List[circuits.Circuit]
) -> List[np.ndarray]:
all_probs = [] # type: List[np.ndarray]
num_states = 2**len(qubits)
for circuit in circuit_list:
trial_circuit = circuit.copy()
trial_circuit.append(ops.measure(*qubits, key='z'))
res = sampler.run(trial_circuit, repetitions=repetitions)
res_hist = dict(res.histogram(key='z'))
probs = np.zeros(num_states, dtype=float)
for k, v in res_hist.items():
probs[k] = float(v) / float(repetitions)
all_probs.append(probs)
return all_probs
def _xeb_fidelities(ideal_probs: Dict[int, np.ndarray],
actual_probs: Dict[int, np.ndarray]) -> List[float]:
num_cycles = sorted(list(ideal_probs.keys()))
return [
_compute_fidelity(ideal_probs[n], actual_probs[n]) for n in num_cycles
]
def _compute_fidelity(probs_exp: np.ndarray, probs_meas: np.ndarray) -> float:
_, num_states = probs_exp.shape
pp_cross = probs_exp * probs_meas
pp_exp = probs_exp**2
f_meas = np.mean(num_states * np.sum(pp_cross, axis=1) - 1.0)
f_exp = np.mean(num_states * np.sum(pp_exp, axis=1) - 1.0)
return float(f_meas / f_exp)
def _random_half_rotations(qubits: Sequence[ops.Qid],
num_layers: int) -> List[List[ops.OP_TREE]]:
rot_ops = [
ops.X**0.5, ops.Y**0.5,
ops.PhasedXPowGate(phase_exponent=0.25, exponent=0.5)
]
num_qubits = len(qubits)
rand_nums = np.random.choice(3, (num_qubits, num_layers))
single_q_layers = [] # type: List[List[ops.OP_TREE]]
for i in range(num_layers):
single_q_layers.append(
[rot_ops[rand_nums[j, i]](qubits[j]) for j in range(num_qubits)])
return single_q_layers
def _random_any_gates(qubits: Sequence[ops.Qid],
op_list: List[List[ops.SingleQubitGate]],
num_layers: int) -> List[List[ops.OP_TREE]]:
num_ops = len(op_list)
num_qubits = len(qubits)
rand_nums = np.random.choice(num_ops, (num_qubits, num_layers))
single_q_layers = [] # type: List[List[ops.OP_TREE]]
for i in range(num_layers):
rots_i = [] # type: List[ops.OP_TREE]
for j in range(num_qubits):
rots_i.extend([rot(qubits[j]) for rot in op_list[rand_nums[j, i]]])
single_q_layers.append(rots_i)
return single_q_layers
def _default_interaction_sequence(
qubits: Sequence[devices.GridQubit]
) -> List[Set[Tuple[devices.GridQubit, devices.GridQubit]]]:
qubit_dict = {(qubit.row, qubit.col): qubit for qubit in qubits}
qubit_locs = set(qubit_dict)
num_rows = max([q.row for q in qubits]) + 1
num_cols = max([q.col for q in qubits]) + 1
l_s = [set() for _ in range(4)
] # type: List[Set[Tuple[devices.GridQubit, devices.GridQubit]]]
for i in range(num_rows):
for j in range(num_cols - 1):
if (i, j) in qubit_locs and (i, j + 1) in qubit_locs:
l_s[j % 2 * 2].add((qubit_dict[(i, j)], qubit_dict[(i, j + 1)]))
for i in range(num_rows - 1):
for j in range(num_cols):
if (i, j) in qubit_locs and (i + 1, j) in qubit_locs:
l_s[i % 2 * 2 + 1].add(
(qubit_dict[(i, j)], qubit_dict[(i + 1, j)]))
l_final = [] # type: List[Set[Tuple[devices.GridQubit, devices.GridQubit]]]
for gate_set in l_s:
if len(gate_set) != 0:
l_final.append(gate_set)
return l_final