Source code for oitg.circuits.clifford

r"""Brute-force Clifford group helpers.

The Gottesmann—Knill theorem states that circuits consisting of gates from the
Clifford group :math:`\mathcal{C}_n = \operatorname{Aut}(\mathrm{Pauli}_n)` can be
efficiently simulated clasically, typically using some implementation of the stabiliser
formalism.

This module is emphatically *not* about that. Instead, it contains utilities for
directly manipulating elements of a group of unitaries as gate strings, without making
any use of its structure to make the calculations easier. This is only really practical
for the single- and two-qubit Clifford groups :math:`\mathcal{C}_1` and
:math:`\mathcal{C}_2`.

TODO (DPN): Instead of explicitly listing more than one Clifford group decomposition,
this should just directly integrate my decomposition search code and transparently use
:mod:`oitg.cache` for caching. In the meantime, just ask if you want to use a different
gate set.
"""

from enum import Enum, unique
from itertools import product
from typing import Callable, Dict, List
import numpy as np
from .gate import Gate, GateGenerator, GateSequence, remap_operands
from .to_matrix import gate_sequence_matrix


[docs] class GateGroup: r"""A group (in the mathematical sense) of gates, where each element is represented by its index in some canonical order. :param num_qubits: The number of qubits the gates operate on. :param gate_sequences: The :class:`.GateSequence`\ s corresponding to each group element. :param matrices: The unitary matrices describing the action of each group element. :param inverse_idxs: The index of the inverse element for each group element, given as a map of canonical matrix key (see :meth:`to_canonical_matrix_key`) to element index. """ def __init__(self, num_qubits: int, gate_sequences: List[GateSequence], matrices: List[np.ndarray], inverse_idxs: Dict[bytearray, int]): self.num_qubits = num_qubits # Exhaustive list of gate sequences for all elements and their unitary matrices. self._gate_sequences = gate_sequences self._matrices = matrices assert len(self._matrices) == len(self._gate_sequences) # Maps canonical matrix (in `bytes` form) to the index of the inverse element. self._inverse_idxs = inverse_idxs assert len(self._inverse_idxs) == len(self._gate_sequences)
[docs] def num_elements(self) -> int: """Return the total number of elements in the group.""" return len(self._gate_sequences)
[docs] def gates_for_idx(self, idx: int) -> GateSequence: """Return the gate sequence for the given element index.""" return self._gate_sequences[idx]
[docs] def matrix_for_idx(self, idx: int) -> np.ndarray: """Return the unitary matrix for the given element index.""" return self._matrices[idx]
[docs] def find_inverse_idx(self, matrix: np.ndarray) -> int: """Look up the index of the element that is the inverse of the given unitary matrix.""" return self._inverse_idxs[to_canonical_matrix_key(matrix)]
[docs] def pauli_idxs(self) -> List[int]: """Return the indices corresponding to the (generalised) Pauli group (which is always a subgroup of the Clifford group). """ # The n-qubit Pauli group elements are tensor products of single-qubit Pauli # group elements. By convention, they are all kept at the beginning of the # index lists. SQ_PAULI_IDXS = [0, 2, 4, 6] return [ sum(v * 24**i for (i, v) in enumerate(a)) for a in product(SQ_PAULI_IDXS, repeat=self.num_qubits) ]
[docs] def to_canonical_matrix(gate_matrix: np.ndarray) -> np.ndarray: """Convert the given gate matrix to a canonical form, which is exactly the same no matter the global phase or rounding errors. Note that this rounds to 4 decimal places for robustness, which is plenty for Clifford group calculations, but might not be sufficient for other applications. """ # Divide by the phase of the first non-small element in the first row to normalise # out the global phase. This is pretty arbitrary (we just need a phase convention # that avoids dividing by zero), and rather silly looking, but works. u = gate_matrix.copy() i = 0 while np.abs(u[0, i]) < 1e-6: i += 1 phase = u[0, i] / np.abs(u[0, i]) return np.array(np.around(u / phase, decimals=4) + 0.0, dtype='complex64')
[docs] def to_canonical_matrix_key(gate_matrix: np.ndarray) -> bytearray: """Return the canonical matrix corresponding to `gate_matrix` in a form suitable for use as a dictionary key.""" # TODO: Could hash here directly for performance. key = to_canonical_matrix(gate_matrix).tobytes() return key
CliffordImpl = Callable[[int], GateGenerator]
[docs] def make_clifford_group(num_qubits: int, implementation: CliffordImpl) -> GateGroup: """Construct a :class:`GateGroup` instance which enumerates all elements of the ``num_qubit``-qubit Clifford group, with elements decomposed according to the given ``implementation``. """ if num_qubits == 1: num_elements = 24 elif num_qubits == 2: num_elements = 11520 else: raise ValueError("Unsupported number of qubits: {}".format(num_qubits)) gate_sequences = [] matrices = [] inverse_idxs = {} for idx in range(num_elements): gates = list(implementation(idx)) gate_sequences.append(gates) u_gate = gate_sequence_matrix(gates, num_qubits) matrices.append(u_gate) inverse_idxs[to_canonical_matrix_key(np.linalg.inv(u_gate))] = idx return GateGroup(num_qubits, gate_sequences, matrices, inverse_idxs)
# Indices of various well-known single-qubit Clifford group elements. # TODO: Look up by unitary for dry-ness. C1_HADAMARD_IDX = 9 C1_Y_PI_BY_2_IDX = 11 C1_X_PI_BY_2_IDX = 23 _clifford_1q_xypm_implementations = [ [], [0, 3, 2], [0, 0, 1, 1], [0, 1, 2], [0, 0], [0, 3, 0], [1, 1], [0, 1, 0], [1, 0], [0, 0, 3], [1, 2], [1], [3, 2], [0, 0, 1], [3, 0], [3], [2, 3], [2], [2, 1], [2, 1, 1], [0, 3], [0, 1, 1], [0, 1], [0], ]
[docs] def get_clifford_1q_xypm_implementation(idx: int) -> GateGenerator: """Return an implementation of the 1-qubit Clifford group element with the given index as ±π/2 rotations about the x and y axes. :return: A minimal-length gate string implementing the given Clifford. """ for e in _clifford_1q_xypm_implementations[idx]: if e == 0: yield Gate("rx", (np.pi / 2, ), (0, )) elif e == 1: yield Gate("ry", (np.pi / 2, ), (0, )) elif e == 2: yield Gate("rx", (-np.pi / 2, ), (0, )) elif e == 3: yield Gate("ry", (-np.pi / 2, ), (0, )) else: assert False
_clifford_1q_xzpm2_implementations = [ [], [1], [5], [3], [4], [1, 4], [4, 5], [3, 4], [0, 1], [0, 1, 0], [2, 3], [0, 1, 2], [2, 1], [0, 3, 0], [0, 3], [0, 3, 2], [3, 2], [2], [1, 2], [0, 5], [1, 0], [2, 5], [3, 0], [0], ]
[docs] def get_clifford_1q_xzpm2_implementation(idx: int) -> GateGenerator: """Return an implementation of the 1-qubit Clifford group element with the given index as ±π/2 and π rotations about the x and z axes. :return: A minimal-length gate string implementing the given Clifford. """ for e in _clifford_1q_xzpm2_implementations[idx]: if e == 0: yield Gate("rx", (np.pi / 2, ), (0, )) elif e == 1: yield Gate("rz", (np.pi / 2, ), (0, )) elif e == 2: yield Gate("rx", (-np.pi / 2, ), (0, )) elif e == 3: yield Gate("rz", (-np.pi / 2, ), (0, )) elif e == 4: yield Gate("rx", (np.pi, ), (0, )) elif e == 5: yield Gate("rz", (np.pi, ), (0, )) else: assert False
[docs] @unique class EntanglingGate(Enum): """Specifies the equivalence class (up to local single-qubit Cliffords) of a non-trivial two-qubit Clifford operation. """ #: cz_like = 0 #: iswap_like = 1 #: swap_like = 2
[docs] def get_clifford_2q_implementation( idx: int, clifford_1q_impl: CliffordImpl, entangling_gates_impl: Callable[[EntanglingGate, CliffordImpl], GateGenerator] ) -> GateGenerator: """Generate an implementation of the 2-qubit Clifford group element with the given index. :param idx: The element index. :param clifford_1q_impl: The single-qubit Clifford gate implementation to use (acting on qubit 0). :param entangling_gates_impl: The entangling gate implementation to use. """ # TODO: This implementation correctly enumerates the two-qubit Clifford group for # any valid entangling_gates_impl, but the element indices aren't stable across # different choices of entangling gates. def local_idx_to_gates(i): assert 0 <= i < 24**2 c0, c1 = divmod(i, 24) yield from clifford_1q_impl(c0) yield from remap_operands(clifford_1q_impl(c1), {0: 1}) axis_permute_clifford_1q_elems = [0, 12, 22] def perm_idx_to_gates(i): assert 0 <= i < 3**2 p0, p1 = divmod(i, 3) yield from clifford_1q_impl(axis_permute_clifford_1q_elems[p0]) yield from remap_operands(clifford_1q_impl(axis_permute_clifford_1q_elems[p1]), {0: 1}) i = idx NUM_LOCAL = 24**2 if i < NUM_LOCAL: # Local operation class. yield from local_idx_to_gates(i) return i -= NUM_LOCAL NUM_CZ_LIKE = 24**2 * 3**2 if i < NUM_CZ_LIKE: # CZ-like class. local_idx, perm_idx = divmod(i, 3**2) yield from local_idx_to_gates(local_idx) yield from entangling_gates_impl(EntanglingGate.cz_like, clifford_1q_impl) yield from perm_idx_to_gates(perm_idx) return i -= NUM_CZ_LIKE NUM_ISWAP_LIKE = 24**2 * 3**2 if i < NUM_ISWAP_LIKE: # iSWAP-like class. local_idx, perm_idx = divmod(i, 3**2) yield from local_idx_to_gates(local_idx) yield from entangling_gates_impl(EntanglingGate.iswap_like, clifford_1q_impl) yield from perm_idx_to_gates(perm_idx) return i -= NUM_ISWAP_LIKE NUM_SWAP_LIKE = 24**2 if i < NUM_SWAP_LIKE: # Local operation class. yield from local_idx_to_gates(i) yield from entangling_gates_impl(EntanglingGate.swap_like, clifford_1q_impl) return raise ValueError("Invalid 2-qubit Clifford gate index: {}".format(idx))
[docs] def get_cz_entangling_gate_implementation( kind: EntanglingGate, clifford_1q_impl_0: CliffordImpl) -> GateGenerator: """Generate an implementation of the given entangling gate category using CZ gates and the given single-qubit gate implementation. """ def clifford_1q_impl_1(idx): return remap_operands(clifford_1q_impl_0(idx), {0: 1}) if kind == EntanglingGate.cz_like: yield Gate("cz", (), (0, 1)) elif kind == EntanglingGate.iswap_like: yield Gate("cz", (), (0, 1)) yield from clifford_1q_impl_0(C1_X_PI_BY_2_IDX) yield from clifford_1q_impl_1(C1_X_PI_BY_2_IDX) yield Gate("cz", (), (0, 1)) elif kind == EntanglingGate.swap_like: yield Gate("cz", (), (0, 1)) yield from clifford_1q_impl_0(C1_X_PI_BY_2_IDX) yield from clifford_1q_impl_1(C1_X_PI_BY_2_IDX) yield Gate("cz", (), (0, 1)) yield from clifford_1q_impl_0(C1_X_PI_BY_2_IDX) yield from clifford_1q_impl_1(C1_X_PI_BY_2_IDX) yield Gate("cz", (), (0, 1)) else: assert False
[docs] def get_zzw_entangling_gate_implementation( kind: EntanglingGate, clifford_1q_impl_0: CliffordImpl) -> GateGenerator: """Generate an implementation of the given entangling gate category using symmetric ZZ (wobble) gates and the given single-qubit gate implementation. """ def clifford_1q_impl_1(idx): return remap_operands(clifford_1q_impl_0(idx), {0: 1}) if kind == EntanglingGate.cz_like: yield Gate("zzw", (), (0, 1)) elif kind == EntanglingGate.iswap_like: yield Gate("zzw", (), (0, 1)) yield from clifford_1q_impl_0(C1_X_PI_BY_2_IDX) yield from clifford_1q_impl_1(C1_X_PI_BY_2_IDX) yield Gate("zzw", (), (0, 1)) elif kind == EntanglingGate.swap_like: yield Gate("zzw", (), (0, 1)) yield from clifford_1q_impl_0(C1_X_PI_BY_2_IDX) yield from clifford_1q_impl_1(C1_X_PI_BY_2_IDX) yield Gate("zzw", (), (0, 1)) yield from clifford_1q_impl_0(22) yield from clifford_1q_impl_1(22) yield Gate("zzw", (), (0, 1)) else: assert False
[docs] def get_clifford_2q_xypm_cz_implementation(idx: int) -> GateGenerator: """Generate an implementation of the given 2-qubit Clifford group element using CZ gates and local single-qubit ±π/2 rotations about the x and y axes. """ return get_clifford_2q_implementation(idx, get_clifford_1q_xypm_implementation, get_cz_entangling_gate_implementation)
[docs] def get_clifford_2q_xzpm2_cz_implementation(idx: int) -> GateGenerator: """Generate an implementation of the given 2-qubit Clifford group element using CZ gates and local single-qubit ±π/2 and π rotations about the x and z axes. """ return get_clifford_2q_implementation(idx, get_clifford_1q_xzpm2_implementation, get_cz_entangling_gate_implementation)
[docs] def get_clifford_2q_xzpm2_zzw_implementation(idx: int) -> GateGenerator: """Generate an implementation of the given 2-qubit Clifford group element using a symmetric ZZ entangling gate implemented as a spin-echo wobble gate and local single-qubit ±π/2 and π rotations about the x and z axes. """ return get_clifford_2q_implementation(idx, get_clifford_1q_xzpm2_implementation, get_zzw_entangling_gate_implementation)