Source code for pennylane.transforms.decompositions.clifford_t_transform

# Copyright 2018-2023 Xanadu Quantum Technologies Inc.

# 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

#     http://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.
"""Transform function for the Clifford+T decomposition."""

import math
import warnings
from itertools import product
from typing import Sequence, Callable

import pennylane as qml
from pennylane.ops import Adjoint
from pennylane.queuing import QueuingManager
from pennylane.transforms.core import transform
from pennylane.tape import QuantumTape
from pennylane.transforms.optimization import (
    cancel_inverses,
    commute_controlled,
    merge_rotations,
    remove_barrier,
)
from pennylane.transforms.optimization.optimization_utils import find_next_gate, _fuse_global_phases
from pennylane.ops.op_math.decompositions.solovay_kitaev import sk_decomposition

# Single qubits Clifford+T gates in PL
_CLIFFORD_T_ONE_GATES = [
    qml.Identity,
    qml.X,
    qml.Y,
    qml.Z,
    qml.Hadamard,
    qml.S,
    qml.SX,
    qml.T,
]

# Two qubits Clifford+T gates in PL
_CLIFFORD_T_TWO_GATES = [
    qml.CNOT,
    qml.CY,
    qml.CZ,
    qml.SWAP,
    qml.ISWAP,
]

# Single-parameter gates in PL
# note: _simplify_param makes use of their periodic nature,
# any additions to this, should be reflected there as well.
_PARAMETER_GATES = (qml.RX, qml.RY, qml.RZ, qml.Rot, qml.PhaseShift)

# Clifford+T gate set
_CLIFFORD_T_GATES = tuple(_CLIFFORD_T_ONE_GATES + _CLIFFORD_T_TWO_GATES) + (qml.GlobalPhase,)

# Gates to be skipped during decomposition
_SKIP_OP_TYPES = (qml.Barrier, qml.Snapshot, qml.WireCut)


def _check_clifford_op(op, use_decomposition=False):
    r"""Checks if an operator is Clifford or not.

    For a given unitary operator :math:`U` acting on :math:`N` qubits, this method checks that the
    transformation :math:`UPU^{\dagger}` maps the Pauli tensor products :math:`P = {I, X, Y, Z}^{\otimes N}`
    to Pauli tensor products using the decomposition of the matrix for :math:`U` in the Pauli basis.

    Args:
        op (~pennylane.operation.Operation): the operator that needs to be tested
        use_decomposition (bool): if ``True``, use operator's decomposition to compute the matrix, in case
            it doesn't define a ``compute_matrix`` method. Default is ``False``.

    Returns:
        Bool that represents whether the provided operator is Clifford or not.
    """

    # Check if matrix can be calculated for the operator
    if (not op.has_matrix and not use_decomposition) or (
        use_decomposition and not op.expand().wires
    ):
        return False

    # Compute the LCUs for the operator in Pauli basis
    pauli_terms = qml.pauli_decompose(qml.matrix(op), wire_order=op.wires, check_hermitian=False)
    pauli_terms_adj = qml.Hamiltonian(qml.math.conj(pauli_terms.coeffs), pauli_terms.ops)

    # Build Pauli tensor products P in the Hamiltonian form
    def pauli_group(x):
        return [qml.Identity(x), qml.X(x), qml.Y(x), qml.Z(x)]

    # Build PauliSentence and Hamiltonians representations for set P
    pauli_sens = [
        qml.pauli.pauli_sentence(qml.prod(*pauli))
        for pauli in product(*(pauli_group(idx) for idx in op.wires))
    ]
    pauli_hams = (pauli_sen.hamiltonian(wire_order=op.wires) for pauli_sen in pauli_sens)

    # Perform U@P@U^\dagger and check if the result exists in set P
    for pauli_prod in product([pauli_terms], pauli_hams, [pauli_terms_adj]):
        # hopefully op_math.prod scales better than matrix multiplication, i.e., O((2^N)^3)
        upu = qml.pauli.pauli_sentence(qml.prod(*pauli_prod))
        upu.simplify()
        # Pauli sum always lie outside set P
        if len(upu) != 1:
            return False

    return True


def check_clifford_t(op, use_decomposition=False):
    r"""Checks whether the gate is in the standard Clifford+T basis.

    For a given unitary operator :math:`U` acting on :math:`N` qubits, which is not a T-gate,
    this method checks that the transformation :math:`UPU^{\dagger}` maps the Pauli tensor products
    :math:`P = {I, X, Y, Z}^{\otimes N}` to Pauli tensor products using the decomposition of the
    matrix for :math:`U` in the Pauli basis.

    Args:
        op (~pennylane.operation.Operation): the operator that needs to be checked
        use_decomposition (bool): if ``True``, use operator's decomposition to compute the matrix, in case
            it doesn't define a ``compute_matrix`` method. Default is ``False``.

    Returns:
        Bool that represents whether the provided operator is Clifford+T or not.
    """
    # Get the base operation for an adjointed operation
    if isinstance(op, Adjoint):
        base = op.base
    else:
        base = None

    # Save time and check from the pre-computed list
    if isinstance(op, _CLIFFORD_T_GATES) or isinstance(base, _CLIFFORD_T_GATES):
        return True

    # Save time and check from the parameter of rotation gates
    if isinstance(op, _PARAMETER_GATES) or isinstance(base, _PARAMETER_GATES):
        theta = op.data[0]
        return (
            False
            if qml.math.is_abstract(theta)
            else qml.math.allclose(qml.math.mod(theta, math.pi), 0.0)
        )

    return _check_clifford_op(op, use_decomposition=use_decomposition)


def _simplify_param(theta, gate):
    r"""Check if the parameter allows simplification for the rotation gate.

    For the cases where theta is an integer multiple of π: (a) returns a global phase
    when even, and (b) returns combination of provided gate with global phase when odd.
    In rest of the other cases it would return None.
    """
    if qml.math.is_abstract(theta):  # pragma: no cover
        return None

    if qml.math.allclose(theta, 0.0, atol=1e-6):
        return [qml.GlobalPhase(0.0)]

    rem_, mod_ = qml.math.divide(theta, math.pi), qml.math.mod(theta, math.pi)
    if qml.math.allclose(mod_, 0.0, atol=1e-6):
        ops = [qml.GlobalPhase(theta / 2)]
        if qml.math.allclose(qml.math.mod(rem_, 2), 1.0, atol=1e-6):
            ops.append(gate)
        return ops

    return None


# pylint: disable= too-many-branches,
def _rot_decompose(op):
    r"""Decomposes a rotation operation: :class:`~.Rot`, :class:`~.RX`, :class:`~.RY`, :class:`~.RZ`,
    :class:`~.PhaseShift` into a basis composed of :class:`~.RZ`, :class:`~.S`, and :class:`~.Hadamard`.
    """
    d_ops = []
    # Extend for Rot operation with Rz.Ry.Rz decompositions
    if isinstance(op, qml.Rot):
        (phi, theta, omega), wires = op.parameters, op.wires
        for dec in [qml.RZ(phi, wires), qml.RY(theta, wires), qml.RZ(omega, wires)]:
            d_ops.extend(_rot_decompose(dec))
        return d_ops

    (theta,), wires = op.data, op.wires
    if isinstance(op, qml.ops.Adjoint):  # pylint: disable=no-member
        ops_ = _rot_decompose(op.base.adjoint())
    elif isinstance(op, qml.RX):
        ops_ = _simplify_param(theta, qml.X(wires))
        if ops_ is None:  # Use Rx = H @ Rz @ H
            ops_ = [qml.Hadamard(wires), qml.RZ(theta, wires), qml.Hadamard(wires)]
    elif isinstance(op, qml.RY):
        ops_ = _simplify_param(theta, qml.Y(wires))
        if ops_ is None:  # Use Ry = S @ H @ Rz @ H @ S.adjoint()
            ops_ = [
                qml.S(wires),
                qml.Hadamard(wires),
                qml.RZ(theta, wires),
                qml.Hadamard(wires),
                qml.adjoint(qml.S(wires)),
            ][::-1]
    elif isinstance(op, qml.RZ):
        ops_ = _simplify_param(theta, qml.Z(wires))
        if ops_ is None:
            ops_ = [qml.RZ(theta, wires)]
    elif isinstance(op, qml.PhaseShift):
        ops_ = _simplify_param(theta, qml.Z(wires))
        if ops_ is None:
            ops_ = [qml.RZ(theta, wires=wires), qml.GlobalPhase(-theta / 2)]
        else:
            ops_.append(qml.GlobalPhase(-theta / 2))

    else:
        raise ValueError(
            f"Operation {op} is not a supported Pauli rotation: qml.RX, qml.RY, qml.RZ, qml.Rot and qml.PhaseShift."
        )

    d_ops.extend(ops_)
    return d_ops


def _one_qubit_decompose(op):
    r"""Decomposition for single qubit operations using combination of :class:`~.RZ`, :class:`~.S`, and
    :class:`~.Hadamard`."""

    with warnings.catch_warnings():
        warnings.simplefilter("ignore", RuntimeWarning)
        sd_ops = qml.ops.one_qubit_decomposition(
            qml.matrix(op), op.wires, "ZXZ", return_global_phase=True
        )
    # Get the global phase
    gphase_op = sd_ops.pop()

    # Decompose the rotation gates
    d_ops = []
    for sd_op in sd_ops:
        d_ops.extend(_rot_decompose(sd_op) if sd_op.num_params else [sd_op])

    return d_ops, gphase_op


def _two_qubit_decompose(op):
    r"""Decomposition for two qubit operations using combination of :class:`~.RZ`, :class:`~.S`,
    :class:`~.Hadamard`, and :class:`~.CNOT`."""

    with warnings.catch_warnings():
        warnings.simplefilter("ignore", RuntimeWarning)
        td_ops = qml.ops.two_qubit_decomposition(qml.matrix(op), op.wires)

    d_ops = []
    for td_op in td_ops:
        d_ops.extend(
            _rot_decompose(td_op) if td_op.num_params and td_op.num_wires == 1 else [td_op]
        )

    return d_ops


def _merge_param_gates(operations, merge_ops=None):
    """Merge the provided parameterized gates on the same wires that are adjacent to each other"""

    copied_ops = operations.copy()
    merged_ops, number_ops = [], 0

    while len(copied_ops) > 0:
        curr_gate = copied_ops.pop(0)

        # If gate is not to be merged, let it go
        if merge_ops is not None and curr_gate.name not in merge_ops:
            merged_ops.append(curr_gate)
            continue

        # If gate is in the merge_ops, update counter
        if curr_gate.name in merge_ops:
            number_ops += 1

        # Find the next gate that acts on the same wires
        next_gate_idx = find_next_gate(curr_gate.wires, copied_ops)
        if next_gate_idx is None:
            merged_ops.append(curr_gate)
            continue

        # Initialize the current angle and iterate until end of merge
        curr_params = curr_gate.parameters
        curr_intrfc = qml.math.get_deep_interface(curr_gate.parameters)
        cumulative_angles = qml.math.array(curr_params, dtype=float, like=curr_intrfc)
        next_gate = copied_ops[next_gate_idx]
        while curr_gate.name == next_gate.name and curr_gate.wires == next_gate.wires:
            cumulative_angles += qml.math.array(next_gate.parameters, like=curr_intrfc)
            # Check if the subsequent gate exists in the vicinity
            copied_ops.pop(next_gate_idx)
            next_gate_idx = find_next_gate(curr_gate.wires, copied_ops)
            if next_gate_idx is None:
                break
            next_gate = copied_ops[next_gate_idx]

        # Replace the current gate, add it to merged list and pop it from orginal one
        merged_ops.append(curr_gate.__class__(*cumulative_angles, wires=curr_gate.wires))

    return merged_ops, number_ops


# pylint: disable= too-many-nested-blocks, too-many-branches, too-many-statements, unnecessary-lambda-assignment
[docs]@transform def clifford_t_decomposition( tape: QuantumTape, epsilon=1e-4, max_expansion=6, method="sk", **method_kwargs, ) -> (Sequence[QuantumTape], Callable): r"""Decomposes a circuit into the Clifford+T basis. This method first decomposes the gate operations to a basis comprised of Clifford, :class:`~.T`, :class:`~.RZ` and :class:`~.GlobalPhase` operations (and their adjoints). The Clifford gates include the following PennyLane operations: - Single qubit gates - :class:`~.Identity`, :class:`~.PauliX`, :class:`~.PauliY`, :class:`~.PauliZ`, :class:`~.SX`, :class:`~.S`, and :class:`~.Hadamard`. - Two qubit gates - :class:`~.CNOT`, :class:`~.CY`, :class:`~.CZ`, :class:`~.SWAP`, and :class:`~.ISWAP`. Then, the leftover single qubit :class:`~.RZ` operations are approximated in the Clifford+T basis with :math:`\epsilon > 0` error. By default, we use the Solovay-Kitaev algorithm described in `Dawson and Nielsen (2005) <https://arxiv.org/abs/quant-ph/0505030>`_ for this. Args: tape (QNode or QuantumTape or Callable): The quantum circuit to be decomposed. epsilon (float): The maximum permissible operator norm error of the complete circuit decomposition. Defaults to ``0.0001``. max_expansion (int): The depth to be used for tape expansion before manual decomposition to Clifford+T basis is applied. method (str): Method to be used for Clifford+T decomposition. Default value is ``"sk"`` for Solovay-Kitaev. **method_kwargs: Keyword argument to pass options for the ``method`` used for decompositions. Returns: qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in the :func:`qml.transform <pennylane.transform>`. **Keyword Arguments** - Solovay-Kitaev decomposition -- **max_depth** (int), **basis_set** (list[str]), **basis_length** (int) -- arguments for the ``"sk"`` method, where the decomposition is performed using the :func:`~.sk_decomposition` method. Raises: ValueError: If a gate operation does not have a decomposition when required. NotImplementedError: If chosen decomposition ``method`` is not supported. .. seealso:: :func:`~.sk_decomposition` for Solovay-Kitaev decomposition. **Example** .. code-block:: python3 @qml.qnode(qml.device("default.qubit")) def circuit(x, y): qml.RX(x, 0) qml.CNOT([0, 1]) qml.RY(y, 0) return qml.expval(qml.Z(0)) x, y = 1.1, 2.2 decomposed_circuit = qml.transforms.clifford_t_decomposition(circuit) result = circuit(x, y) approx = decomposed_circuit(x, y) >>> qml.math.allclose(result, approx, atol=1e-4) True """ with QueuingManager.stop_recording(): # Build the basis set and the pipeline for intial compilation pass basis_set = [op.__name__ for op in _PARAMETER_GATES + _CLIFFORD_T_GATES] pipelines = [remove_barrier, commute_controlled, cancel_inverses, merge_rotations] # Compile the tape according to depth provided by the user and expand it [compiled_tape], _ = qml.compile( tape, pipelines, basis_set=basis_set, expand_depth=max_expansion ) # Now iterate over the expanded tape operations decomp_ops, gphase_ops = [], [] for op in compiled_tape.operations: # Check whether operation is to be skipped if isinstance(op, _SKIP_OP_TYPES): decomp_ops.append(op) # Check whether the operation is a global phase elif isinstance(op, qml.GlobalPhase): gphase_ops.append(op) # Check whether the operation is a Clifford or a T-gate elif op.name in basis_set and check_clifford_t(op): if op.num_params: decomp_ops.extend(_rot_decompose(op)) else: decomp_ops.append(op) # Decompose and then iteratively go deeper via DFS else: # Single qubit unitary decomposition with ZXZ rotations if op.num_wires == 1: if op.name in basis_set: d_ops = _rot_decompose(op) else: d_ops, g_op = _one_qubit_decompose(op) gphase_ops.append(g_op) decomp_ops.extend(d_ops) # Two qubit unitary decomposition with SU(4) rotations elif op.num_wires == 2: d_ops = _two_qubit_decompose(op) decomp_ops.extend(d_ops) # For special multi-qubit gates and ones constructed from matrix else: try: # Attempt decomposing the operation md_ops = op.decomposition() idx = 0 # might not be fast but at least is not recursive while idx < len(md_ops): md_op = md_ops[idx] if md_op.name not in basis_set or not check_clifford_t(md_op): # For the gates acting on one qubit if len(md_op.wires) == 1: if md_op.name in basis_set: # For known recipe d_ops = _rot_decompose(md_op) else: # Resort to decomposing manually d_ops, g_op = _one_qubit_decompose(md_op) gphase_ops.append(g_op) # For the gates acting on two qubits elif len(md_op.wires) == 2: # Resort to decomposing manually d_ops = _two_qubit_decompose(md_op) # Final resort (should not enter in an ideal situtation) else: d_ops = md_op.decomposition() # Expand the list and iterate over del md_ops[idx] md_ops[idx:idx] = d_ops idx += 1 decomp_ops.extend(md_ops) # If we don't know how to decompose the operation except Exception as exc: raise ValueError( f"Cannot unroll {op} into the Clifford+T basis as no rule exists for its decomposition" ) from exc # Merge RZ rotations together merged_ops, number_ops = _merge_param_gates(decomp_ops, merge_ops=["RZ"]) # Squeeze global phases into a single global phase new_operations = _fuse_global_phases(merged_ops + gphase_ops) # Compute the per-gate epsilon value epsilon /= number_ops or 1 # Every decomposition implementation should have the following shape: # def decompose_fn(op: Operator, epsilon: float, **method_kwargs) -> List[Operator] # note: the last operator in the decomposition must be a GlobalPhase # Build the approximation set for Solovay-Kitaev decomposition if method == "sk": decompose_fn = sk_decomposition else: raise NotImplementedError( f"Currently we only support Solovay-Kitaev ('sk') decompostion, got {method}" ) decomp_ops = [] phase = new_operations.pop().data[0] for op in new_operations: if isinstance(op, qml.RZ): clifford_ops = decompose_fn(op, epsilon, **method_kwargs) phase += qml.math.convert_like(clifford_ops.pop().data[0], phase) decomp_ops.extend(clifford_ops) else: decomp_ops.append(op) # check if phase is non-zero for non jax-jit cases if qml.math.is_abstract(phase) or not qml.math.allclose(phase, 0.0): decomp_ops.append(qml.GlobalPhase(phase)) # Construct a new tape with the expanded set of operations new_tape = type(tape)(decomp_ops, compiled_tape.measurements, shots=tape.shots) # Perform a final attempt of simplification before return [new_tape], _ = cancel_inverses(new_tape) def null_postprocessing(results): """A postprocesing function returned by a transform that only converts the batch of results into a result for a single ``QuantumTape``. """ return results[0] return [new_tape], null_postprocessing