# Source code for cirq.experiments.qubit_characterizations

```
# 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.
import itertools
from typing import Any, Iterator, List, NamedTuple, Optional, Sequence, Tuple
import numpy as np
import sympy
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D # type: ignore # pylint: disable=unused-import
from cirq import circuits, devices, ops, protocols, study, work
Cliffords = NamedTuple('Cliffords', [('c1_in_xy', List[List[ops.Gate]]),
('c1_in_xz', List[List[ops.Gate]]),
('s1', List[List[ops.Gate]]),
('s1_x', List[List[ops.Gate]]),
('s1_y', List[List[ops.Gate]])])
[docs]class RabiResult:
"""Results from a Rabi oscillation experiment."""
[docs] def __init__(self, rabi_angles: Sequence[float],
excited_state_probabilities: Sequence[float]):
"""
Args:
rabi_angles: The rotation angles of the qubit around the x-axis
of the Bloch sphere.
excited_state_probabilities: The corresponding probabilities that
the qubit is in the excited state.
"""
self._rabi_angles = rabi_angles
self._excited_state_probs = excited_state_probabilities
@property
def data(self) -> Sequence[Tuple[float, float]]:
"""Returns a sequence of tuple pairs with the first item being a Rabi
angle and the second item being the corresponding excited state
probability.
"""
return [
(angle, prob)
for angle, prob in zip(self._rabi_angles, self._excited_state_probs)
]
[docs] def plot(self, ax: Optional[plt.Axes] = None,
**plot_kwargs: Any) -> plt.Axes:
"""Plots excited state probability vs the Rabi angle (angle of rotation
around the x-axis).
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))
ax.set_ylim([0, 1])
ax.plot(self._rabi_angles, self._excited_state_probs, 'ro-',
**plot_kwargs)
ax.set_xlabel(r"Rabi Angle (Radian)")
ax.set_ylabel('Excited State Probability')
if show_plot:
fig.show()
return ax
[docs]class RandomizedBenchMarkResult:
"""Results from a randomized benchmarking experiment."""
[docs] def __init__(self, num_cliffords: Sequence[int],
ground_state_probabilities: Sequence[float]):
"""
Args:
num_cliffords: The different numbers of Cliffords in the RB
study.
ground_state_probabilities: The corresponding average ground state
probabilities.
"""
self._num_cfds_seq = num_cliffords
self._gnd_state_probs = ground_state_probabilities
@property
def data(self) -> Sequence[Tuple[int, float]]:
"""Returns a sequence of tuple pairs with the first item being a
number of Cliffords and the second item being the corresponding average
ground state probability.
"""
return [(num, prob)
for num, prob in zip(self._num_cfds_seq, self._gnd_state_probs)]
[docs] def plot(self, ax: Optional[plt.Axes] = None,
**plot_kwargs: Any) -> plt.Axes:
"""Plots the average ground state probability vs the number of
Cliffords in the RB study.
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))
ax.set_ylim([0, 1])
ax.plot(self._num_cfds_seq, self._gnd_state_probs, 'ro-', **plot_kwargs)
ax.set_xlabel(r"Number of Cliffords")
ax.set_ylabel('Ground State Probability')
if show_plot:
fig.show()
return ax
[docs]class TomographyResult:
"""Results from a state tomography experiment."""
[docs] def __init__(self, density_matrix: np.ndarray):
"""
Args:
density_matrix: The density matrix obtained from tomography.
"""
self._density_matrix = density_matrix
@property
def data(self) -> np.ndarray:
"""Returns an n^2 by n^2 complex matrix representing the density
matrix of the n-qubit system.
"""
return self._density_matrix
[docs] def plot(self, axes: Optional[List[plt.Axes]] = None,
**plot_kwargs: Any) -> List[plt.Axes]:
"""Plots the real and imaginary parts of the density matrix as two
3D bar plots.
Args:
axes: a list of 2 `plt.Axes` instances. Note that they must be in
3d projections. If not given, a new figure is created with 2
axes and the plotted figure is shown.
plot_kwargs: the optional kwargs passed to bar3d.
Returns:
the list of `plt.Axes` being plotted on.
Raises:
ValueError if axes is a list with length != 2.
"""
show_plot = axes is None
if axes is None:
fig, axes = plt.subplots(1,
2,
figsize=(12.0, 5.0),
subplot_kw={'projection': '3d'})
elif len(axes) != 2:
raise ValueError('A TomographyResult needs 2 axes to plot.')
mat = self._density_matrix
a, _ = mat.shape
num_qubits = int(np.log2(a))
state_labels = [[0, 1]] * num_qubits
kets = []
for label in itertools.product(*state_labels):
kets.append('|' + str(list(label))[1:-1] + '>')
mat_re = np.real(mat)
mat_im = np.imag(mat)
_matrix_bar_plot(mat_re,
r'Real($\rho$)',
axes[0],
kets,
'Density Matrix (Real Part)',
ylim=(-1, 1),
**plot_kwargs)
_matrix_bar_plot(mat_im,
r'Imaginary($\rho$)',
axes[1],
kets,
'Density Matrix (Imaginary Part)',
ylim=(-1, 1),
**plot_kwargs)
if show_plot:
fig.show()
return axes
[docs]def rabi_oscillations(sampler: work.Sampler,
qubit: devices.GridQubit,
max_angle: float = 2 * np.pi,
*,
repetitions: int = 1000,
num_points: int = 200) -> RabiResult:
"""Runs a Rabi oscillation experiment.
Rotates a qubit around the x-axis of the Bloch sphere by a sequence of Rabi
angles evenly spaced between 0 and max_angle. For each rotation, repeat
the circuit a number of times and measure the average probability of the
qubit being in the |1> state.
Args:
sampler: The quantum engine or simulator to run the circuits.
qubit: The qubit under test.
max_angle: The final Rabi angle in radians.
repetitions: The number of repetitions of the circuit for each Rabi
angle.
num_points: The number of Rabi angles.
Returns:
A RabiResult object that stores and plots the result.
"""
theta = sympy.Symbol('theta')
circuit = circuits.Circuit(ops.X(qubit)**theta)
circuit.append(ops.measure(qubit, key='z'))
sweep = study.Linspace(key='theta',
start=0.0,
stop=max_angle / np.pi,
length=num_points)
results = sampler.run_sweep(circuit, params=sweep, repetitions=repetitions)
angles = np.linspace(0.0, max_angle, num_points)
excited_state_probs = np.zeros(num_points)
for i in range(num_points):
excited_state_probs[i] = np.mean(results[i].measurements['z'])
return RabiResult(angles, excited_state_probs)
[docs]def single_qubit_randomized_benchmarking(
sampler: work.Sampler,
qubit: devices.GridQubit,
use_xy_basis: bool = True,
*,
num_clifford_range: Sequence[int] = range(10, 100, 10),
num_circuits: int = 20,
repetitions: int = 1000) -> RandomizedBenchMarkResult:
"""Clifford-based randomized benchmarking (RB) of a single qubit.
A total of num_circuits random circuits are generated, each of which
contains a fixed number of single-qubit Clifford gates plus one
additional Clifford that inverts the whole sequence and a measurement in
the z-basis. Each circuit is repeated a number of times and the average
|0> state population is determined from the measurement outcomes of all
of the circuits.
The above process is done for different circuit lengths specified by the
integers in num_clifford_range. For example, an integer 10 means the
random circuits will contain 10 Clifford gates each plus one inverting
Clifford. The user may use the result to extract an average gate fidelity,
by analyzing the change in the average |0> state population at different
circuit lengths. For actual experiments, one should choose
num_clifford_range such that a clear exponential decay is observed in the
results.
See Barends et al., Nature 508, 500 for details.
Args:
sampler: The quantum engine or simulator to run the circuits.
qubit: The qubit under test.
use_xy_basis: Determines if the Clifford gates are built with x and y
rotations (True) or x and z rotations (False).
num_clifford_range: The different numbers of Cliffords in the RB study.
num_circuits: The number of random circuits generated for each
number of Cliffords.
repetitions: The number of repetitions of each circuit.
Returns:
A RandomizedBenchMarkResult object that stores and plots the result.
"""
cliffords = _single_qubit_cliffords()
c1 = cliffords.c1_in_xy if use_xy_basis else cliffords.c1_in_xz
cfd_mats = np.array([_gate_seq_to_mats(gates) for gates in c1])
gnd_probs = []
for num_cfds in num_clifford_range:
excited_probs_l = []
for _ in range(num_circuits):
circuit = _random_single_q_clifford(qubit, num_cfds, c1, cfd_mats)
circuit.append(ops.measure(qubit, key='z'))
results = sampler.run(circuit, repetitions=repetitions)
excited_probs_l.append(np.mean(results.measurements['z']))
gnd_probs.append(1.0 - np.mean(excited_probs_l))
return RandomizedBenchMarkResult(num_clifford_range, gnd_probs)
[docs]def two_qubit_randomized_benchmarking(
sampler: work.Sampler,
first_qubit: devices.GridQubit,
second_qubit: devices.GridQubit,
*,
num_clifford_range: Sequence[int] = range(5, 50, 5),
num_circuits: int = 20,
repetitions: int = 1000) -> RandomizedBenchMarkResult:
"""Clifford-based randomized benchmarking (RB) of two qubits.
A total of num_circuits random circuits are generated, each of which
contains a fixed number of two-qubit Clifford gates plus one additional
Clifford that inverts the whole sequence and a measurement in the
z-basis. Each circuit is repeated a number of times and the average
|00> state population is determined from the measurement outcomes of all
of the circuits.
The above process is done for different circuit lengths specified by the
integers in num_clifford_range. For example, an integer 10 means the
random circuits will contain 10 Clifford gates each plus one inverting
Clifford. The user may use the result to extract an average gate fidelity,
by analyzing the change in the average |00> state population at different
circuit lengths. For actual experiments, one should choose
num_clifford_range such that a clear exponential decay is observed in the
results.
The two-qubit Cliffords here are decomposed into CZ gates plus single-qubit
x and y rotations. See Barends et al., Nature 508, 500 for details.
Args:
sampler: The quantum engine or simulator to run the circuits.
first_qubit: The first qubit under test.
second_qubit: The second qubit under test.
num_clifford_range: The different numbers of Cliffords in the RB study.
num_circuits: The number of random circuits generated for each
number of Cliffords.
repetitions: The number of repetitions of each circuit.
Returns:
A RandomizedBenchMarkResult object that stores and plots the result.
"""
cliffords = _single_qubit_cliffords()
cfd_matrices = _two_qubit_clifford_matrices(first_qubit, second_qubit,
cliffords)
gnd_probs = []
for num_cfds in num_clifford_range:
gnd_probs_l = []
for _ in range(num_circuits):
circuit = _random_two_q_clifford(first_qubit, second_qubit,
num_cfds, cfd_matrices, cliffords)
circuit.append(ops.measure(first_qubit, second_qubit, key='z'))
results = sampler.run(circuit, repetitions=repetitions)
gnds = [(not r[0] and not r[1]) for r in results.measurements['z']]
gnd_probs_l.append(np.mean(gnds))
gnd_probs.append(float(np.mean(gnd_probs_l)))
return RandomizedBenchMarkResult(num_clifford_range, gnd_probs)
[docs]def single_qubit_state_tomography(sampler: work.Sampler,
qubit: devices.GridQubit,
circuit: circuits.Circuit,
repetitions: int = 1000) -> TomographyResult:
"""Single-qubit state tomography.
The density matrix of the output state of a circuit is measured by first
doing projective measurements in the z-basis, which determine the
diagonal elements of the matrix. A X/2 or Y/2 rotation is then added before
the z-basis measurement, which determines the imaginary and real parts of
the off-diagonal matrix elements, respectively.
See Vandersypen and Chuang, Rev. Mod. Phys. 76, 1037 for details.
Args:
sampler: The quantum engine or simulator to run the circuits.
qubit: The qubit under test.
circuit: The circuit to execute on the qubit before tomography.
repetitions: The number of measurements for each basis rotation.
Returns:
A TomographyResult object that stores and plots the density matrix.
"""
circuit_z = circuit + circuits.Circuit(ops.measure(qubit, key='z'))
results = sampler.run(circuit_z, repetitions=repetitions)
rho_11 = np.mean(results.measurements['z'])
rho_00 = 1.0 - rho_11
circuit_x = circuits.Circuit(circuit,
ops.X(qubit)**0.5, ops.measure(qubit, key='z'))
results = sampler.run(circuit_x, repetitions=repetitions)
rho_01_im = np.mean(results.measurements['z']) - 0.5
circuit_y = circuits.Circuit(circuit,
ops.Y(qubit)**-0.5, ops.measure(qubit,
key='z'))
results = sampler.run(circuit_y, repetitions=repetitions)
rho_01_re = 0.5 - np.mean(results.measurements['z'])
rho_01 = rho_01_re + 1j * rho_01_im
rho_10 = np.conj(rho_01)
rho = np.array([[rho_00, rho_01], [rho_10, rho_11]])
return TomographyResult(rho)
[docs]def two_qubit_state_tomography(sampler: work.Sampler,
first_qubit: devices.GridQubit,
second_qubit: devices.GridQubit,
circuit: circuits.Circuit,
repetitions: int = 1000) -> TomographyResult:
r"""Two-qubit state tomography.
To measure the density matrix of the output state of a two-qubit circuit,
different combinations of I, X/2 and Y/2 operations are applied to the
two qubits before measurements in the z-basis to determine the state
probabilities P_00, P_01, P_10.
The density matrix rho is decomposed into an operator-sum representation
\sum_{i, j} c_ij * sigma_i \bigotimes sigma_j, where i, j = 0, 1, 2,
3 and sigma_0 = I, sigma_1 = sigma_x, sigma_2 = sigma_y, sigma_3 =
sigma_z are the single-qubit Identity and Pauli matrices.
Based on the measured probabilities probs and the transformations of the
measurement operator by different basis rotations, one can build an
overdetermined set of linear equations.
As an example, if the identity operation (I) is applied to both qubits,
the measurement operators are (I +/- sigma_z) \bigotimes (I +/- sigma_z).
The state probabilities P_00, P_01, P_10 thus obtained contribute to the
following linear equations (setting c_00 = 1):
c_03 + c_30 + c_33 = 4*P_00 - 1
-c_03 + c_30 - c_33 = 4*P_01 - 1
c_03 - c_30 - c_33 = 4*P_10 - 1
And if a Y/2 rotation is applied to the first qubit and a X/2 rotation
is applied to the second qubit before measurement, the measurement
operators are (I -/+ sigma_x) \bigotimes (I +/- sigma_y). The probabilities
obtained instead contribute to the following linear equations:
c_02 - c_10 - c_12 = 4*P_00 - 1
-c_02 - c_10 + c_12 = 4*P_01 - 1
c_02 + c_10 + c_12 = 4*P_10 - 1
Note that this set of equations has the same form as the first set under
the transformation c_03 <-> c_02, c_30 <-> -c_10 and c_33 <-> -c_12.
Since there are 9 possible combinations of rotations (each producing 3
independent probabilities) and a total of 15 unknown coefficients c_ij,
one can cast all the measurement results into a overdetermined set of
linear equations numpy.dot(mat, c) = probs. Here c is of length 15 and
contains all the c_ij's (except c_00 which is set to 1), and mat is a 27
by 15 matrix having three non-zero elements in each row that are either
1 or -1.
The least-square solution to the above set of linear equations is then
used to construct the density matrix rho.
See Vandersypen and Chuang, Rev. Mod. Phys. 76, 1037 for details and
Steffen et al, Science 313, 1423 for a related experiment.
Args:
sampler: The quantum engine or simulator to run the circuits.
first_qubit: The first qubit under test.
second_qubit: The second qubit under test.
circuit: The circuit to execute on the qubits before tomography.
repetitions: The number of measurements for each basis rotation.
Returns:
A TomographyResult object that stores and plots the density matrix.
"""
# The size of the system of linear equations to be solved.
num_rows = 27
num_cols = 15
def _measurement(two_qubit_circuit: circuits.Circuit) -> np.ndarray:
two_qubit_circuit.append(ops.measure(first_qubit, second_qubit,
key='z'))
results = sampler.run(two_qubit_circuit, repetitions=repetitions)
results_hist = results.histogram(key='z')
prob_list = [results_hist[0], results_hist[1], results_hist[2]]
return np.asarray(prob_list) / repetitions
sigma_0 = np.eye(2) * 0.5
sigma_1 = np.array([[0.0, 1.0], [1.0, 0.0]]) * 0.5
sigma_2 = np.array([[0.0, -1.0j], [1.0j, 0.0]]) * 0.5
sigma_3 = np.array([[1.0, 0.0], [0.0, -1.0]]) * 0.5
sigmas = [sigma_0, sigma_1, sigma_2, sigma_3]
# Stores all 27 measured probabilities (P_00, P_01, P_10 after 9
# different basis rotations).
probs = np.array([])
rots = [ops.X**0, ops.X**0.5, ops.Y**0.5]
# Represents the coefficients in front of the c_ij's (-1, 0 or 1) in the
# system of 27 linear equations.
mat = np.zeros((num_rows, num_cols))
# Represents the relative signs between the linear equations for P_00,
# P_01, and P_10.
s = np.array([[1.0, 1.0, 1.0], [-1.0, 1.0, -1.0], [1.0, -1.0, -1.0]])
for i, rot_1 in enumerate(rots):
for j, rot_2 in enumerate(rots):
m_idx, indices, signs = _indices_after_basis_rot(i, j)
mat[m_idx:(m_idx + 3), indices] = s * np.tile(signs, (3, 1))
test_circuit = circuit + circuits.Circuit(rot_1(first_qubit))
test_circuit.append(rot_2(second_qubit))
probs = np.concatenate((probs, _measurement(test_circuit)))
c, _, _, _ = np.linalg.lstsq(mat, 4.0 * probs - 1.0, rcond=-1)
c = np.concatenate(([1.0], c))
c = c.reshape(4, 4)
rho = np.zeros((4, 4))
for i in range(4):
for j in range(4):
rho = rho + c[i, j] * np.kron(sigmas[i], sigmas[j])
return TomographyResult(rho)
def _indices_after_basis_rot(i: int, j: int
) -> Tuple[int, Sequence[int], Sequence[int]]:
mat_idx = 3 * (3 * i + j)
q_0_i = 3 - i
q_1_j = 3 - j
indices = [q_1_j - 1, 4 * q_0_i - 1, 4 * q_0_i + q_1_j - 1]
signs = [(-1)**(j == 2), (-1)**(i == 2), (-1)**((i == 2) + (j == 2))]
return mat_idx, indices, signs
def _two_qubit_clifford_matrices(q_0: devices.GridQubit, q_1: devices.GridQubit,
cliffords: Cliffords) -> np.ndarray:
mats = []
# Total number of different gates in the two-qubit Clifford group.
clifford_group_size = 11520
starters = []
for idx_0 in range(24):
subset = []
for idx_1 in range(24):
circuit = circuits.Circuit(
_two_qubit_clifford_starters(q_0, q_1, idx_0, idx_1, cliffords))
subset.append(protocols.unitary(circuit))
starters.append(subset)
mixers = []
# Add the identity for the case where there is no mixer.
mixers.append(np.eye(4))
for idx_2 in range(1, 20):
circuit = circuits.Circuit(
_two_qubit_clifford_mixers(q_0, q_1, idx_2, cliffords))
mixers.append(protocols.unitary(circuit))
for i in range(clifford_group_size):
idx_0, idx_1, idx_2 = _split_two_q_clifford_idx(i)
mats.append(np.matmul(mixers[idx_2], starters[idx_0][idx_1]))
return np.array(mats)
def _random_single_q_clifford(qubit: devices.GridQubit, num_cfds: int,
cfds: Sequence[Sequence[ops.Gate]],
cfd_matrices: np.ndarray) -> circuits.Circuit:
clifford_group_size = 24
gate_ids = list(np.random.choice(clifford_group_size, num_cfds))
gate_sequence = [] # type: List[ops.Gate]
for gate_id in gate_ids:
gate_sequence.extend(cfds[gate_id])
idx = _find_inv_matrix(_gate_seq_to_mats(gate_sequence), cfd_matrices)
gate_sequence.extend(cfds[idx])
circuit = circuits.Circuit(gate(qubit) for gate in gate_sequence)
return circuit
def _random_two_q_clifford(q_0: devices.GridQubit, q_1: devices.GridQubit,
num_cfds: int, cfd_matrices: np.ndarray,
cliffords: Cliffords) -> circuits.Circuit:
clifford_group_size = 11520
idx_list = list(np.random.choice(clifford_group_size, num_cfds))
circuit = circuits.Circuit()
for idx in idx_list:
circuit.append(_two_qubit_clifford(q_0, q_1, idx, cliffords))
inv_idx = _find_inv_matrix(protocols.unitary(circuit), cfd_matrices)
circuit.append(_two_qubit_clifford(q_0, q_1, inv_idx, cliffords))
return circuit
def _find_inv_matrix(mat: np.ndarray, mat_sequence: np.ndarray) -> int:
mat_prod = np.einsum('ij,...jk->...ik', mat, mat_sequence)
diag_sums = list(np.absolute(np.einsum('...ii->...', mat_prod)))
idx = diag_sums.index(max(diag_sums))
return idx
def _matrix_bar_plot(mat: np.ndarray,
z_label: str,
ax: plt.Axes,
kets: Sequence[str] = None,
title: str = None,
ylim: Tuple[int, int] = (-1, 1),
**bar3d_kwargs: Any) -> None:
num_rows, num_cols = mat.shape
indices = np.meshgrid(range(num_cols), range(num_rows))
x_indices = np.array(indices[1]).flatten()
y_indices = np.array(indices[0]).flatten()
z_indices = np.zeros(mat.size)
dx = np.ones(mat.size) * 0.3
dy = np.ones(mat.size) * 0.3
dz = mat.flatten()
ax.bar3d(x_indices,
y_indices,
z_indices,
dx,
dy,
dz,
color='#ff0080',
alpha=1.0,
**bar3d_kwargs)
ax.set_zlabel(z_label)
ax.set_zlim3d(ylim[0], ylim[1])
if kets is not None:
ax.set_xticks(np.arange(num_cols) + 0.15)
ax.set_yticks(np.arange(num_rows) + 0.15)
ax.set_xticklabels(kets)
ax.set_yticklabels(kets)
if title is not None:
ax.set_title(title)
def _gate_seq_to_mats(gate_seq: Sequence[ops.Gate]) -> np.ndarray:
mat_rep = protocols.unitary(gate_seq[0])
for gate in gate_seq[1:]:
mat_rep = np.dot(protocols.unitary(gate), mat_rep)
return mat_rep
def _two_qubit_clifford(q_0: devices.GridQubit, q_1: devices.GridQubit,
idx: int,
cliffords: Cliffords) -> Iterator[ops.OP_TREE]:
"""Generates a two-qubit Clifford gate.
An integer (idx) from 0 to 11519 is used to generate a two-qubit Clifford
gate which is constructed with single-qubit X and Y rotations and CZ gates.
The decomposition of the Cliffords follow those described in the appendix
of Barends et al., Nature 508, 500.
The integer idx is first decomposed into idx_0 (which ranges from 0 to
23), idx_1 (ranging from 0 to 23) and idx_2 (ranging from 0 to 19). idx_0
and idx_1 determine the two single-qubit rotations which happen at the
beginning of all two-qubit Clifford gates. idx_2 determines the
subsequent gates in the following:
a) If idx_2 = 0, do nothing so the Clifford is just two single-qubit
Cliffords (total of 24*24 = 576 possibilities).
b) If idx_2 = 1, perform a CZ, followed by -Y/2 on q_0 and Y/2 on q_1,
followed by another CZ, followed by Y/2 on q_0 and -Y/2 on q_1, followed
by one more CZ and finally a Y/2 on q_1. The Clifford is then a member of
the SWAP-like class (total of 24*24 = 576 possibilities).
c) If 2 <= idx_2 <= 10, perform a CZ followed by a member of the S_1
group on q_0 and a member of the S_1^(Y/2) group on q_1. The Clifford is
a member of the CNOT-like class (a total of 3*3*24*24 = 5184 possibilities).
d) If 11 <= idx_2 <= 19, perform a CZ, followed by Y/2 on q_0 and -X/2 on
q_1, followed by another CZ, and finally a member of the S_1^(Y/2) group on
q_0 and a member of the S_1^(X/2) group on q_1. The Clifford is a member
of the iSWAP-like class (a total of 3*3*24*24 = 5184 possibilities).
Through the above process, all 11520 members of the two-qubit Clifford
group may be generated.
Args:
q_0: The first qubit under test.
q_1: The second qubit under test.
idx: An integer from 0 to 11519.
cliffords: A NamedTuple that contains single-qubit Cliffords from the
C1, S1, S_1^(X/2) and S_1^(Y/2) groups.
"""
idx_0, idx_1, idx_2 = _split_two_q_clifford_idx(idx)
yield _two_qubit_clifford_starters(q_0, q_1, idx_0, idx_1, cliffords)
yield _two_qubit_clifford_mixers(q_0, q_1, idx_2, cliffords)
def _split_two_q_clifford_idx(idx: int):
"""Decompose the index for two-qubit Cliffords."""
idx_0 = int(idx / 480)
idx_1 = int((idx % 480) * 0.05)
idx_2 = idx - idx_0 * 480 - idx_1 * 20
return (idx_0, idx_1, idx_2)
def _two_qubit_clifford_starters(q_0: devices.GridQubit, q_1: devices.GridQubit,
idx_0: int, idx_1: int,
cliffords: Cliffords) -> Iterator[ops.OP_TREE]:
"""Fulfills part (a) for two-qubit Cliffords."""
c1 = cliffords.c1_in_xy
yield _single_qubit_gates(c1[idx_0], q_0)
yield _single_qubit_gates(c1[idx_1], q_1)
def _two_qubit_clifford_mixers(q_0: devices.GridQubit, q_1: devices.GridQubit,
idx_2: int,
cliffords: Cliffords) -> Iterator[ops.OP_TREE]:
"""Fulfills parts (b-d) for two-qubit Cliffords."""
s1 = cliffords.s1
s1_x = cliffords.s1_x
s1_y = cliffords.s1_y
if idx_2 == 1:
yield ops.CZ(q_0, q_1)
yield ops.Y(q_0)**-0.5
yield ops.Y(q_1)**0.5
yield ops.CZ(q_0, q_1)
yield ops.Y(q_0)**0.5
yield ops.Y(q_1)**-0.5
yield ops.CZ(q_0, q_1)
yield ops.Y(q_1)**0.5
elif 2 <= idx_2 <= 10:
yield ops.CZ(q_0, q_1)
idx_3 = int((idx_2 - 2) / 3)
idx_4 = (idx_2 - 2) % 3
yield _single_qubit_gates(s1[idx_3], q_0)
yield _single_qubit_gates(s1_y[idx_4], q_1)
elif idx_2 >= 11:
yield ops.CZ(q_0, q_1)
yield ops.Y(q_0)**0.5
yield ops.X(q_1)**-0.5
yield ops.CZ(q_0, q_1)
idx_3 = int((idx_2 - 11) / 3)
idx_4 = (idx_2 - 11) % 3
yield _single_qubit_gates(s1_y[idx_3], q_0)
yield _single_qubit_gates(s1_x[idx_4], q_1)
def _single_qubit_gates(gate_seq: Sequence[ops.Gate],
qubit: devices.GridQubit) -> Iterator[ops.OP_TREE]:
for gate in gate_seq:
yield gate(qubit)
def _single_qubit_cliffords() -> Cliffords:
c1_in_xy = [] # type: List[List[ops.Gate]]
c1_in_xz = [] # type: List[List[ops.Gate]]
for phi_0, phi_1 in itertools.product([1.0, 0.5, -0.5], [0.0, 0.5, -0.5]):
c1_in_xy.append([ops.X**phi_0, ops.Y**phi_1])
c1_in_xy.append([ops.Y**phi_0, ops.X**phi_1])
c1_in_xz.append([ops.X**phi_0, ops.Z**phi_1])
c1_in_xz.append([ops.Z**phi_0, ops.X**phi_1])
c1_in_xy.append([ops.X**0.0])
c1_in_xy.append([ops.Y, ops.X])
phi_xy = [[-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], [0.5, 0.5, 0.5],
[-0.5, 0.5, -0.5]]
for phi in phi_xy:
c1_in_xy.append([ops.X**phi[0], ops.Y**phi[1], ops.X**phi[2]])
phi_xz = [[0.5, 0.5, -0.5], [0.5, -0.5, -0.5], [-0.5, -0.5, -0.5],
[-0.5, 0.5, -0.5]]
for phi in phi_xz:
c1_in_xz.append([ops.X**phi[0], ops.Z**phi[1], ops.X**phi[2]])
s1 = [[ops.X**0.0], [ops.Y**0.5, ops.X**0.5],
[ops.X**-0.5, ops.Y**-0.5]] # type: List[List[ops.Gate]]
s1_x = [[ops.X**0.5], [ops.X**0.5, ops.Y**0.5, ops.X**0.5],
[ops.Y**-0.5]] # type: List[List[ops.Gate]]
s1_y = [[ops.Y**0.5], [ops.X**-0.5, ops.Y**-0.5, ops.X**0.5],
[ops.Y, ops.X**0.5]] # type: List[List[ops.Gate]]
return Cliffords(c1_in_xy, c1_in_xz, s1, s1_x, s1_y)
```