Source code for catalyst.passes.builtin_passes

# Copyright 2024 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.

"""This module exposes built-in Catalyst MLIR passes to the frontend."""

import copy
import functools
import json

from catalyst.compiler import _options_to_cli_flags, _quantum_opt
from catalyst.passes.pass_api import PassPipelineWrapper
from catalyst.utils.exceptions import CompileError

# pylint: disable=line-too-long, too-many-lines


## API ##
[docs] def cancel_inverses(qnode): """ Specify that the ``-removed-chained-self-inverse`` MLIR compiler pass for cancelling two neighbouring self-inverse gates should be applied to the decorated QNode during :func:`~.qjit` compilation. The full list of supported gates are as follows: One-bit Gates: :class:`qml.Hadamard <pennylane.Hadamard>`, :class:`qml.PauliX <pennylane.PauliX>`, :class:`qml.PauliY <pennylane.PauliY>`, :class:`qml.PauliZ <pennylane.PauliZ>` Two-bit Gates: :class:`qml.CNOT <pennylane.CNOT>`, :class:`qml.CY <pennylane.CY>`, :class:`qml.CZ <pennylane.CZ>`, :class:`qml.SWAP <pennylane.SWAP>` Three-bit Gates: - :class:`qml.Toffoli <pennylane.Toffoli>` .. note:: Unlike PennyLane :doc:`circuit transformations <introduction/compiling_circuits>`, the QNode itself will not be changed or transformed by applying these decorators. As a result, circuit inspection tools such as :func:`~.draw` will continue to display the circuit as written in Python. To instead view the optimized circuit, the MLIR must be viewed after the ``"QuantumCompilationPass"`` stage via the :func:`~.get_compilation_stage` function. Args: fn (QNode): the QNode to apply the cancel inverses compiler pass to Returns: ~.QNode: **Example** .. code-block:: python from catalyst.debug import get_compilation_stage from catalyst.passes import cancel_inverses dev = qml.device("lightning.qubit", wires=1) @qjit(keep_intermediate=True) @cancel_inverses @qml.qnode(dev) def circuit(x: float): qml.RX(x, wires=0) qml.Hadamard(wires=0) qml.Hadamard(wires=0) return qml.expval(qml.PauliZ(0)) >>> circuit(0.54) Array(0.85770868, dtype=float64) Note that the QNode will be unchanged in Python, and will continue to include self-inverse gates when inspected with Python (for example, with :func:`~.draw`). To instead view the optimized circuit, the MLIR must be viewed after the ``"QuantumCompilationPass"`` stage: >>> print(get_compilation_stage(circuit, stage="QuantumCompilationPass")) module @circuit { func.func public @jit_circuit(%arg0: tensor<f64>) -> tensor<f64> attributes {llvm.emit_c_interface} { %0 = call @circuit(%arg0) : (tensor<f64>) -> tensor<f64> return %0 : tensor<f64> } func.func private @circuit(%arg0: tensor<f64>) -> tensor<f64> attributes {diff_method = "parameter-shift", llvm.linkage = #llvm.linkage<internal>, qnode} { quantum.device["catalyst/utils/../lib/librtd_lightning.dylib", "LightningSimulator", "{'shots': 0, 'mcmc': False, 'num_burnin': 0, 'kernel_name': None}"] %0 = quantum.alloc( 1) : !quantum.reg %1 = quantum.extract %0[ 0] : !quantum.reg -> !quantum.bit %extracted = tensor.extract %arg0[] : tensor<f64> %out_qubits = quantum.custom "RX"(%extracted) %1 : !quantum.bit %2 = quantum.namedobs %out_qubits[ PauliZ] : !quantum.obs %3 = quantum.expval %2 : f64 %from_elements = tensor.from_elements %3 : tensor<f64> %4 = quantum.insert %0[ 0], %out_qubits : !quantum.reg, !quantum.bit quantum.dealloc %4 : !quantum.reg quantum.device_release return %from_elements : tensor<f64> } func.func @setup() { quantum.init return } func.func @teardown() { quantum.finalize return } } It can be seen that both Hadamards have been cancelled, and the measurement directly follows the ``RX`` gate: .. code-block:: mlir %out_qubits = quantum.custom "RX"(%extracted) %1 : !quantum.bit %2 = quantum.namedobs %out_qubits[ PauliZ] : !quantum.obs %3 = quantum.expval %2 : f64 """ return PassPipelineWrapper(qnode, "remove-chained-self-inverse")
[docs] def disentangle_cnot(qnode): """ Specify that the ``-disentangle-CNOT`` MLIR compiler pass for simplifying CNOT gates should be applied to the decorated QNode during :func:`~.qjit` compilation. Args: fn (QNode): the QNode to apply the disentangle CNOT compiler pass to Returns: ~.QNode: **Example** .. code-block:: python import pennylane as qml from catalyst import qjit from catalyst.debug import get_compilation_stage from catalyst.passes import disentangle_cnot dev = qml.device("lightning.qubit", wires=2) @qjit(keep_intermediate=True) @disentangle_cnot @qml.qnode(dev) def circuit(): # first qubit in |1> qml.X(0) # second qubit in |0> # current state : |10> qml.CNOT([0,1]) # state after CNOT : |11> return qml.state() >>> circuit() [0.+0.j 0.+0.j 0.+0.j 1.+0.j] Note that the QNode will be unchanged in Python, and will continue to include keep CNOT gates gates when inspected with Python (for example, with :func:`~.draw`). To instead view the optimized circuit, the MLIR must be viewed after the ``"QuantumCompilationPass"`` stage: >>> print(get_compilation_stage(circuit, stage="QuantumCompilationPass")) .. code-block:: mlir module @circuit { func.func public @jit_circuit() -> tensor<4xcomplex<f64>> attributes {llvm.emit_c_interface} { %0 = call @circuit_0() : () -> tensor<4xcomplex<f64>> return %0 : tensor<4xcomplex<f64>> } func.func public @circuit_0() -> tensor<4xcomplex<f64>> attributes {diff_method = "parameter-shift", llvm.linkage = #llvm.linkage<internal>, qnode} { %c0_i64 = arith.constant 0 : i64 quantum.device["catalyst/utils/../lib/librtd_lightning.dylib", "LightningSimulator", "{'shots': 0, 'mcmc': False, 'num_burnin': 0, 'kernel_name': None}"] %0 = quantum.alloc( 2) : !quantum.reg %1 = quantum.extract %0[ 0] : !quantum.reg -> !quantum.bit %out_qubits = quantum.custom "PauliX"() %1 : !quantum.bit %2 = quantum.extract %0[ 1] : !quantum.reg -> !quantum.bit %out_qubits_0 = quantum.custom "PauliX"() %2 : !quantum.bit %3 = quantum.insert %0[ 0], %out_qubits : !quantum.reg, !quantum.bit %4 = quantum.insert %3[ 1], %out_qubits_0 : !quantum.reg, !quantum.bit %5 = quantum.compbasis qreg %4 : !quantum.obs %6 = quantum.state %5 : tensor<4xcomplex<f64>> quantum.dealloc %4 : !quantum.reg quantum.device_release return %6 : tensor<4xcomplex<f64>> } func.func @setup() { quantum.init return } func.func @teardown() { quantum.finalize return } } It can be seen that the CNOT(0,1) has been replaced with X(1) .. code-block:: mlir %2 = quantum.extract %0[ 1] : !quantum.reg -> !quantum.bit %out_qubits_0 = quantum.custom "PauliX"() %2 : !quantum.bit """ return PassPipelineWrapper(qnode, "disentangle-CNOT")
[docs] def disentangle_swap(qnode): """ Specify that the ``-disentangle-SWAP`` MLIR compiler pass for simplifying SWAP gates should be applied to the decorated QNode during :func:`~.qjit` compilation. Args: fn (QNode): the QNode to apply the disentangle SWAP compiler pass to Returns: ~.QNode: **Example** .. code-block:: python import pennylane as qml from pennylane import numpy as np from catalyst import qjit from catalyst.debug import get_compilation_stage from catalyst.passes import disentangle_swap dev = qml.device("lightning.qubit", wires=2) @qjit(keep_intermediate=True) @disentangle_swap @qml.qnode(dev) def circuit(): # first qubit in |1> qml.X(0) # second qubit in non-basis qml.RX(np.pi/4,1) qml.SWAP([0,1]) return qml.state() >>> circuit() [0.+0.j 0.92387953+0.j 0.+0.j 0.-0.38268343j] Note that the QNode will be unchanged in Python, and will continue to include keep SWAP gates gates when inspected with Python (for example, with :func:`~.draw`). To instead view the optimized circuit, the MLIR must be viewed after the ``"QuantumCompilationPass"`` stage: >>> print(get_compilation_stage(circuit, stage="QuantumCompilationPass")) .. code-block:: mlir module @circuit { func.func public @jit_circuit() -> tensor<4xcomplex<f64>> attributes {llvm.emit_c_interface} { %0 = call @circuit_0() : () -> tensor<4xcomplex<f64>> return %0 : tensor<4xcomplex<f64>> } func.func public @circuit_0() -> tensor<4xcomplex<f64>> attributes {diff_method = "parameter-shift", llvm.linkage = #llvm.linkage<internal>, qnode} { %c0_i64 = arith.constant 0 : i64 %cst = arith.constant 0.78539816339744828 : f64 quantum.device["catalyst/utils/../lib/librtd_lightning.dylib", "LightningSimulator", "{'shots': 0, 'mcmc': False, 'num_burnin': 0, 'kernel_name': None}"] %0 = quantum.alloc( 2) : !quantum.reg %1 = quantum.extract %0[ 0] : !quantum.reg -> !quantum.bit %out_qubits = quantum.custom "PauliX"() %1 : !quantum.bit5 %2 = quantum.extract %0[ 1] : !quantum.reg -> !quantum.bit %out_qubits_0 = quantum.custom "RX"(%cst) %2 : !quantum.bit %out_qubits_1 = quantum.custom "PauliX"() %out_qubits_0 : !quantum.bit %out_qubits_2:2 = quantum.custom "CNOT"() %out_qubits_1, %out_qubits : !quantum.bit, !quantum.bit %out_qubits_3:2 = quantum.custom "CNOT"() %out_qubits_2#1, %out_qubits_2#0 : !quantum.bit, !quantum.bit %3 = quantum.insert %0[ 0], %out_qubits_3#0 : !quantum.reg, !quantum.bit %4 = quantum.insert %3[ 1], %out_qubits_3#1 : !quantum.reg, !quantum.bit %5 = quantum.compbasis qreg %4 : !quantum.obs %6 = quantum.state %5 : tensor<4xcomplex<f64>> quantum.dealloc %4 : !quantum.reg quantum.device_release return %6 : tensor<4xcomplex<f64>> } func.func @setup() { quantum.init return } func.func @teardown() { quantum.finalize return } } It can be seen that the SWAP(0,1) has been replaced with the folliowing .. code-block:: mlir %0 = quantum.alloc( 2) : !quantum.reg %1 = quantum.extract %0[ 0] : !quantum.reg -> !quantum.bit %out_qubits = quantum.custom "PauliX"() %1 : !quantum.bit5 %2 = quantum.extract %0[ 1] : !quantum.reg -> !quantum.bit %out_qubits_0 = quantum.custom "RX"(%cst) %2 : !quantum.bit %out_qubits_1 = quantum.custom "PauliX"() %out_qubits_0 : !quantum.bit %out_qubits_2:2 = quantum.custom "CNOT"() %out_qubits_1, %out_qubits : !quantum.bit, !quantum.bit %out_qubits_3:2 = quantum.custom "CNOT"() %out_qubits_2#1, %out_qubits_2#0 : !quantum.bit, !quantum.bit """ return PassPipelineWrapper(qnode, "disentangle-SWAP")
[docs] def merge_rotations(qnode): """ Specify that the ``-merge-rotations`` MLIR compiler pass for merging roations (peephole) will be applied. The full list of supported gates are as follows: :class:`qml.RX <pennylane.RX>`, :class:`qml.CRX <pennylane.CRX>`, :class:`qml.RY <pennylane.RY>`, :class:`qml.CRY <pennylane.CRY>`, :class:`qml.RZ <pennylane.RZ>`, :class:`qml.CRZ <pennylane.CRZ>`, :class:`qml.PhaseShift <pennylane.PhaseShift>`, :class:`qml.ControlledPhaseShift <pennylane.ControlledPhaseShift>`, :class:`qml.Rot <pennylane.Rot>`, :class:`qml.CRot <pennylane.CRot>`, :class:`qml.MultiRZ <pennylane.MultiRZ>`. .. note:: Unlike PennyLane :doc:`circuit transformations <introduction/compiling_circuits>`, the QNode itself will not be changed or transformed by applying these decorators. As a result, circuit inspection tools such as :func:`~.draw` will continue to display the circuit as written in Python. To instead view the optimized circuit, the MLIR must be viewed after the ``"QuantumCompilationPass"`` stage via the :func:`~.get_compilation_stage` function. Args: fn (QNode): the QNode to apply the cancel inverses compiler pass to Returns: ~.QNode: **Example** In this example the three :class:`qml.RX <pennylane.RX>` will be merged in a single one with the sum of angles as parameter. .. code-block:: python from catalyst.debug import get_compilation_stage from catalyst.passes import merge_rotations dev = qml.device("lightning.qubit", wires=1) @qjit(keep_intermediate=True) @merge_rotations @qml.qnode(dev) def circuit(x: float): qml.RX(x, wires=0) qml.RX(0.1, wires=0) qml.RX(x**2, wires=0) return qml.expval(qml.PauliZ(0)) >>> circuit(0.54) Array(0.5965506257017892, dtype=float64) """ return PassPipelineWrapper(qnode, "merge-rotations")
def decompose_lowering(qnode): """ Specify that the ``-decompose-lowering`` MLIR compiler pass for applying the compiled decomposition rules to the QNode recursively. Args: fn (QNode): the QNode to apply the cancel inverses compiler pass to Returns: ~.QNode: **Example** // TODO: add example here """ return PassPipelineWrapper(qnode, "decompose-lowering") # pragma: no cover
[docs] def ions_decomposition(qnode): # pragma: nocover """ Specify that the ``--ions-decomposition`` MLIR compiler pass should be applied to the decorated QNode during :func:`~.qjit` compilation. This compiler pass decomposes the gates from the set {T, S, PauliZ, Hadamard, PhaseShift, RZ, CNOT} into gates from the set {RX, RY, MS}, where MS is the Mølmer–Sørensen gate, commonly used by trapped-ion quantum devices. .. note:: Unlike PennyLane :doc:`circuit transformations <introduction/compiling_circuits>`, the QNode itself will not be changed or transformed by applying these decorators. As a result, circuit inspection tools such as :func:`~.draw` will continue to display the circuit as written in Python. To instead view the optimized circuit, the MLIR must be viewed after the ``"QuantumCompilationPass"`` stage via the :func:`~.get_compilation_stage` function. Args: fn (QNode): the QNode to apply the ions-decomposition pass to Returns: ~.QNode: **Example** .. code-block:: python import pennylane as qml from pennylane.devices import NullQubit import catalyst from catalyst import qjit from catalyst.debug import get_compilation_stage @qjit(keep_intermediate=True) @catalyst.passes.ions_decomposition @qml.qnode(NullQubit(2)) def circuit(): qml.Hadamard(wires=[0]) qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliY(wires=0)) >>> print(get_compilation_stage(circuit, stage="QuantumCompilationPass")) module @circuit { func.func public @jit_circuit() -> tensor<f64> attributes {llvm.emit_c_interface} { %0 = call @circuit_0() : () -> tensor<f64> return %0 : tensor<f64> } func.func public @circuit_0() -> tensor<f64> attributes {diff_method = "parameter-shift", llvm.linkage = #llvm.linkage<internal>, qnode} { %c0_i64 = arith.constant 0 : i64 %cst = arith.constant 0.000000e+00 : f64 %cst_0 = arith.constant 1.5707963267948966 : f64 %cst_1 = arith.constant 3.1415926535897931 : f64 %cst_2 = arith.constant -1.5707963267948966 : f64 quantum.device shots(%c0_i64) ["catalyst/runtime/build/lib/librtd_null_qubit.so", "NullQubit", "{'shots': 0}"] %0 = quantum.alloc( 2) : !quantum.reg %1 = quantum.extract %0[ 0] : !quantum.reg -> !quantum.bit %out_qubits = quantum.custom "RX"(%cst) %1 : !quantum.bit %out_qubits_3 = quantum.custom "RY"(%cst_0) %out_qubits : !quantum.bit %out_qubits_4 = quantum.custom "RX"(%cst_1) %out_qubits_3 : !quantum.bit %2 = quantum.extract %0[ 1] : !quantum.reg -> !quantum.bit %out_qubits_5 = quantum.custom "RY"(%cst_0) %out_qubits_4 : !quantum.bit %out_qubits_6:2 = quantum.custom "MS"(%cst_0) %out_qubits_5, %2 : !quantum.bit, !quantum.bit %out_qubits_7 = quantum.custom "RX"(%cst_2) %out_qubits_6#0 : !quantum.bit %out_qubits_8 = quantum.custom "RY"(%cst_2) %out_qubits_6#1 : !quantum.bit %out_qubits_9 = quantum.custom "RY"(%cst_2) %out_qubits_7 : !quantum.bit %3 = quantum.namedobs %out_qubits_8[ PauliY] : !quantum.obs %4 = quantum.expval %3 : f64 %from_elements = tensor.from_elements %4 : tensor<f64> %5 = quantum.insert %0[ 0], %out_qubits_8 : !quantum.reg, !quantum.bit %6 = quantum.insert %5[ 1], %out_qubits_9 : !quantum.reg, !quantum.bit quantum.dealloc %6 : !quantum.reg quantum.device_release return %from_elements : tensor<f64> } func.func @setup() { quantum.init return } func.func @teardown() { quantum.finalize return } } You can see that the Hadamard gate has been decomposed to RX(0)RY(pi/2)RX(pi): .. code-block:: mlir %cst = arith.constant 0.000000e+00 : f64 %cst_0 = arith.constant 1.5707963267948966 : f64 %cst_1 = arith.constant 3.1415926535897931 : f64 ... %out_qubits = quantum.custom "RX"(%cst) %1 : !quantum.bit %out_qubits_3 = quantum.custom "RY"(%cst_0) %out_qubits : !quantum.bit %out_qubits_4 = quantum.custom "RX"(%cst_1) %out_qubits_3 : !quantum.bit and that the CNOT gate has been decomposed to its corresponding circuit implementation using the RX, RY and MS gates: .. code-block:: mlir %cst_0 = arith.constant 1.5707963267948966 : f64 %cst_2 = arith.constant -1.5707963267948966 : f64 ... %out_qubits_5 = quantum.custom "RY"(%cst_0) %out_qubits_4 : !quantum.bit %out_qubits_6:2 = quantum.custom "MS"(%cst_0) %out_qubits_5, %2 : !quantum.bit, !quantum.bit %out_qubits_7 = quantum.custom "RX"(%cst_2) %out_qubits_6#0 : !quantum.bit %out_qubits_8 = quantum.custom "RY"(%cst_2) %out_qubits_6#1 : !quantum.bit %out_qubits_9 = quantum.custom "RY"(%cst_2) %out_qubits_7 : !quantum.bit """ return PassPipelineWrapper(qnode, "ions-decomposition")
[docs] def to_ppr(qnode): R""" Specify that the MLIR compiler pass for converting clifford+T gates into Pauli Product Rotation (PPR) gates will be applied. Clifford gates are defined as :math:`\exp({iP\tfrac{\pi}{4}})`, where :math:`P` is a Pauli word. Non-Clifford gates are defined as :math:`\exp({iP\tfrac{\pi}{8}})`. For more information on the PPM compilation pass, check out the `compilation hub <https://pennylane.ai/compilation/pauli-product-measurement>`__. .. note:: The circuits that generated from this pass are currently only not executable in any backend. This pass is only for analysis and potential future execution when a suitable backend is available. The full list of supported gates and operations are ``qml.H``, ``qml.S``, ``qml.T``, ``qml.X``, ``qml.Y``, ``qml.Z``, ``qml.adjoint(qml.S)``, ``qml.adjoint(qml.T)``, ``qml.CNOT``, and ``catalyst.measure`` Args: fn (QNode): QNode to apply the pass to Returns: :class:`QNode <pennylane.QNode>` **Example** In this example the Clifford+T gates will be converted into PPRs. .. code-block:: python import pennylane as qml from catalyst import qjit, measure ppm_passes = [("PPM", ["to-ppr"])] @qjit(pipelines=ppm_passes, keep_intermediate=True, target="mlir") @qml.qnode(qml.device("null.qubit", wires=2)) def circuit(): qml.H(0) qml.CNOT([0, 1]) qml.T(0) return measure(1) print(circuit.mlir_opt) Example MLIR Representation: .. code-block:: mlir . . . %2 = qec.ppr ["Z"](4) %1 : !quantum.bit %3 = qec.ppr ["X"](4) %2 : !quantum.bit %4 = qec.ppr ["Z"](4) %3 : !quantum.bit %c_3 = stablehlo.constant dense<1> : tensor<i64> %extracted_4 = tensor.extract %c_3[] : tensor<i64> %5 = quantum.extract %0[%extracted_4] : !quantum.reg -> !quantum.bit %6:2 = qec.ppr ["Z", "X"](4) %4, %5 : !quantum.bit, !quantum.bit %7 = qec.ppr ["Z"](-4) %6#0 : !quantum.bit %8 = qec.ppr ["X"](-4) %6#1 : !quantum.bit %9 = qec.ppr ["Z"](8) %7 : !quantum.bit %mres, %out_qubits = qec.ppm ["Z"] %8 : !quantum.bit . . . """ return PassPipelineWrapper(qnode, "to-ppr")
[docs] def commute_ppr(qnode=None, *, max_pauli_size=0): R""" Applies a quantum compilation pass that commutes Clifford Pauli product rotation (PPR) gates, :math:`\exp({iP\tfrac{\pi}{4}})`, past non-Clifford PPRs gates, :math:`\exp({iP\tfrac{\pi}{8}})`, where :math:`P` is a Pauli word. .. seealso:: For more information on PPRs, check out the `Compilation Hub <https://pennylane.ai/compilation/pauli-product-measurement>`_. .. note:: The ``commute_ppr`` compilation pass requires that :func:`~.passes.to_ppr` be applied first. Args: fn (QNode): QNode to apply the pass to. max_pauli_size (int): The maximum size of the Pauli strings after commuting. Returns: :class:`QNode <pennylane.QNode>` **Example** The ``commute_ppr`` pass must be used in conjunction with :func:`~.passes.to_ppr` to first convert gates into PPRs. In this example, the Clifford+T gates in the circuit will be converted into PPRs first, then the Clifford PPRs will be commuted past the non-Clifford PPR. .. code-block:: python import pennylane as qml from catalyst import qjit, measure ppm_passes = [("PPM", ["to-ppr", "commute-ppr"])] @qjit(pipelines=ppm_passes, keep_intermediate=True, target="mlir") @qml.qnode(qml.device("null.qubit", wires=1)) def circuit(): qml.H(0) qml.T(0) return measure(0) print(circuit.mlir_opt) Example MLIR Representation: .. code-block:: mlir . . . %2 = qec.ppr ["X"](8) %1 : !quantum.bit %3 = qec.ppr ["Z"](4) %2 : !quantum.bit %4 = qec.ppr ["X"](4) %3 : !quantum.bit %5 = qec.ppr ["Z"](4) %4 : !quantum.bit %mres, %out_qubits = qec.ppm ["Z"] %5 : !quantum.bit . . . If a commutation resulted in a PPR acting on more than ``max_pauli_size`` qubits (here, ``max_pauli_size = 2``), that commutation would be skipped. .. code-block:: python from catalyst.passes import to_ppr, commute_ppr pips = [("pipe", ["enforce-runtime-invariants-pipeline"])] @qjit(pipelines=pips, target="mlir") @to_ppr @commute_ppr(max_pauli_size=2) @qml.qnode(qml.device("lightning.qubit", wires=3)) def circuit(): qml.H(0) qml.CNOT([1, 2]) qml.CNOT([0, 1]) qml.CNOT([0, 2]) for i in range(3): qml.T(i) return measure(0), measure(1), measure(2) print(circuit.mlir_opt) Example MLIR Representation: .. code-block:: mlir . . . %4:2 = qec.ppr ["Z", "X"](4) %2, %3 : !quantum.bit, !quantum.bit . . . %6:2 = qec.ppr ["X", "Y"](-8) %5, %4#1 : !quantum.bit, !quantum.bit . . . """ if qnode is None: return functools.partial(commute_ppr, max_pauli_size=max_pauli_size) commute_ppr_pass = {"commute_ppr": {"max-pauli-size": max_pauli_size}} return PassPipelineWrapper(qnode, commute_ppr_pass)
[docs] def merge_ppr_ppm(qnode=None, *, max_pauli_size=0): R""" Applies a quantum compilation pass that absorbs Clifford Pauli product rotation (PPR) operations, :math:`\exp{iP\tfrac{\pi}{4}}`, into the final Pauli product measurements (PPMs). .. seealso:: For more information on PPRs and PPMs, check out the `Compilation Hub <https://pennylane.ai/compilation/pauli-product-measurement>`_. Args: fn (QNode): QNode to apply the pass to max_pauli_size (int): The maximum size of the Pauli strings after merging. Returns: :class:`QNode <pennylane.QNode>` **Example** In this example, the Clifford+T gates will be converted into PPRs first, then the Clifford PPRs will be commuted past the non-Clifford PPR, and finally the Clifford PPRs will be absorbed into the Pauli Product Measurements. .. code-block:: python import pennylane as qml from catalyst import qjit, measure ppm_passes = [("PPM",["to-ppr", "commute-ppr","merge-ppr-ppm",])] @qjit(pipelines=ppm_passes, keep_intermediate=True, target="mlir") @qml.qnode(qml.device("lightning.qubit", wires=1)) def circuit(): qml.H(0) qml.T(0) return measure(0) print(circuit.mlir_opt) Example MLIR Representation: .. code-block:: mlir . . . %2 = qec.ppr ["X"](8) %1 : !quantum.bit %mres, %out_qubits = qec.ppm ["X"] %2 : !quantum.bit . . . If a merging resulted in a PPM acting on more than `max_pauli_size` qubits (here, `max_pauli_size = 2`), that merging would be skipped. .. code-block:: python from catalyst import measure, qjit from catalyst.passes import to_ppr, merge_ppr_ppm pips = [("pipe", ["enforce-runtime-invariants-pipeline"])] @qjit(pipelines=pips, target="mlir") @to_ppr @merge_ppr_ppm(max_pauli_size=2) @qml.qnode(qml.device("lightning.qubit", wires=3)) def circuit(): qml.CNOT([1, 2]) qml.CNOT([0, 1]) qml.CNOT([0, 2]) return measure(0), measure(1), measure(2) print(circuit.mlir_opt) Example MLIR Representation: .. code-block:: mlir . . . %3:2 = qec.ppr ["Z", "X"](4) %1, %2 : !quantum.bit, !quantum.bit . . . %mres, %out_qubits:2 = qec.ppm ["Y", "Z"](-1) %3#1, %4 : !quantum.bit, !quantum.bit . . . """ if qnode is None: return functools.partial(merge_ppr_ppm, max_pauli_size=max_pauli_size) merge_ppr_ppm_pass = {"merge_ppr_ppm": {"max-pauli-size": max_pauli_size}} return PassPipelineWrapper(qnode, merge_ppr_ppm_pass)
[docs] def ppr_to_ppm(qnode=None, *, decompose_method="pauli-corrected", avoid_y_measure=False): R"""Applies a quantum compilation pass that decomposes Pauli product rotations (PPRs), :math:`\exp(-iP\theta)`, into Pauli product measurements (PPMs). This pass is used to decompose both non-Clifford and Clifford PPRs into PPMs. The non-Clifford PPRs (:math:`\theta = \tfrac{\pi}{8}`) are decomposed first, and then Clifford PPRs (:math:`\theta = \tfrac{\pi}{4}`) are decomposed. Non-Clifford decomposition can be performed in one of three ways: ``"pauli-corrected"`` (default), ``"clifford-corrected"`` or ``"auto-corrected"``. The ``"pauli-corrected"`` method is based on Figure 13 in `arXiv:2211.15465 <https://arxiv.org/pdf/2211.15465>`_. The latter two methods are based on `A Game of Surface Codes <https://arxiv.org/abs/1808.02892>`__ (figures 7 and 17(b), respectively). .. seealso:: For more information on PPRs and PPMs, check out the `Compilation Hub <https://pennylane.ai/compilation/pauli-product-measurement>`_. Args: qnode (QNode): QNode to apply the pass to. decompose_method (str, optional): The method to use for decomposing non-Clifford PPRs. Options are ``"pauli-corrected"``, ``"auto-corrected"``, and ``"clifford-corrected"``. Defaults to ``"pauli-corrected"``. ``"pauli-corrected"`` uses a reactive measurement for correction. ``"auto-corrected"`` uses an additional measurement for correction. ``"clifford-corrected"`` uses a Clifford rotation for correction. avoid_y_measure (bool): Rather than performing a Pauli-Y measurement for Clifford rotations (sometimes more costly), a :math:`Y` state (:math:`Y\vert 0 \rangle`) is used instead (requires :math:`Y` state preparation). This is currently only supported when using the ``"clifford-corrected"`` and ``"pauli-corrected"`` decomposition method. Defaults to ``False``. Returns: :class:`QNode <pennylane.QNode>` **Example** This example shows the sequence of passes that will be applied. The last pass will convert the non-Clifford PPR into Pauli Product Measurements. .. code-block:: python import pennylane as qml from catalyst import qjit, measure from catalyst.passes import to_ppr, commute_ppr, merge_ppr_ppm, ppr_to_ppm pipeline = [("pipe", ["enforce-runtime-invariants-pipeline"])] @qjit(pipelines=pipeline, target="mlir") @to_ppr @commute_ppr @merge_ppr_ppm @ppr_to_ppm(decompose_method="auto-corrected") @qml.qnode(qml.device("null.qubit", wires=2)) def circuit(): qml.H(0) qml.T(0) qml.CNOT([0, 1]) return measure(0), measure(1) print(circuit.mlir_opt) Example MLIR Representation: .. code-block:: mlir . . . %5 = qec.fabricate zero : !quantum.bit %6 = qec.fabricate magic : !quantum.bit %mres, %out_qubits:2 = qec.ppm ["X", "Z"] %1, %6 : !quantum.bit, !quantum.bit %mres_0, %out_qubits_1:2 = qec.ppm ["Z", "Y"](-1) %5, %out_qubits#1 : !quantum.bit, !quantum.bit %mres_2, %out_qubits_3 = qec.ppm ["X"] %out_qubits_1#1 : !quantum.bit %mres_4, %out_qubits_5 = qec.select.ppm(%mres, ["X"], ["Z"]) %out_qubits_1#0 : !quantum.bit %7 = arith.xori %mres_0, %mres_2 : i1 %8 = qec.ppr ["X"](2) %out_qubits#0 cond(%7) : !quantum.bit . . . """ passes = { "decompose_non_clifford_ppr": { "decompose-method": decompose_method, "avoid-y-measure": avoid_y_measure, }, "decompose_clifford_ppr": {"avoid-y-measure": avoid_y_measure}, } if qnode is None: return functools.partial( ppr_to_ppm, decompose_method=decompose_method, avoid_y_measure=avoid_y_measure ) return PassPipelineWrapper(qnode, passes)
[docs] def ppm_compilation( qnode=None, *, decompose_method="pauli-corrected", avoid_y_measure=False, max_pauli_size=0 ): R""" Specify that the MLIR compiler pass for transforming Clifford+T gates into Pauli product measurements (PPMs) will be applied. This pass combines multiple sub-passes: - :func:`~.passes.to_ppr` : Converts gates into Pauli Product Rotations (PPRs) - :func:`~.passes.commute_ppr` : Commutes PPRs past non-Clifford PPRs - :func:`~.passes.merge_ppr_ppm` : Merges PPRs into Pauli Product Measurements (PPMs) - :func:`~.passes.ppr_to_ppm` : Decomposes PPRs into PPMs The ``avoid_y_measure`` and ``decompose_method`` arguments are passed to the :func:`~.passes.ppr_to_ppm` pass. The ``max_pauli_size`` argument is passed to the :func:`~.passes.commute_ppr` and :func:`~.passes.merge_ppr_ppm` passes. .. seealso:: For more information on PPRs and PPMs, check out the `Compilation Hub <https://pennylane.ai/compilation/pauli-product-measurement>`_. Args: qnode (QNode, optional): QNode to apply the pass to. If None, returns a decorator. decompose_method (str, optional): The method to use for decomposing non-Clifford PPRs. Options are ``"pauli-corrected"``, ``"auto-corrected"`` and ``"clifford-corrected"``. Defaults to ``"pauli-corrected"``. ``"pauli-corrected"`` uses a reactive measurement for correction. ``"auto-corrected"`` uses an additional measurement for correction. ``"clifford-corrected"`` uses a Clifford rotation for correction. avoid_y_measure (bool): Rather than performing a Pauli-Y measurement for Clifford rotations (sometimes more costly), a :math:`Y` state (:math:`Y\vert 0 \rangle`) is used instead (requires :math:`Y` state preparation). Defaults to ``False``. max_pauli_size (int): The maximum size of the Pauli strings after commuting or merging. Defaults to 0 (no limit). Returns: :class:`QNode <pennylane.QNode>` **Example** If a merging resulted in a PPM acting on more than ``max_pauli_size`` qubits (here, ``max_pauli_size = 2``), that merging would be skipped. However, when decomposed into PPMs, at least one qubit will be applied, so the final PPMs will act on at least one additional qubit. .. code-block:: python import pennylane as qml from catalyst import qjit, measure from catalyst.passes import ppm_compilation pipeline = [("pipe", ["enforce-runtime-invariants-pipeline"])] method = "clifford-corrected" @qjit(pipelines=pipeline, target="mlir") @ppm_compilation(decompose_method=method, max_pauli_size=2) @qml.qnode(qml.device("null.qubit", wires=2)) def circuit(): qml.CNOT([0, 1]) qml.CNOT([1, 0]) qml.adjoint(qml.T)(0) qml.T(1) return measure(0), measure(1) print(circuit.mlir_opt) Example MLIR Representation: .. code-block:: mlir . . . %m, %out:3 = qec.ppm ["Z", "Z", "Z"] %1, %2, %4 : !quantum.bit, !quantum.bit, !quantum.bit %m_0, %out_1:2 = qec.ppm ["Z", "Y"] %3, %out#2 : !quantum.bit, !quantum.bit %m_2, %out_3 = qec.ppm ["X"] %out_1#1 : !quantum.bit %m_4, %out_5 = qec.select.ppm(%m, ["X"], ["Z"]) %out_1#0 : !quantum.bit %5 = arith.xori %m_0, %m_2 : i1 %6:2 = qec.ppr ["Z", "Z"](2) %out#0, %out#1 cond(%5) : !quantum.bit, !quantum.bit quantum.dealloc_qb %out_5 : !quantum.bit quantum.dealloc_qb %out_3 : !quantum.bit %7 = quantum.alloc_qb : !quantum.bit %8 = qec.fabricate magic_conj : !quantum.bit %m_6, %out_7:2 = qec.ppm ["Z", "Z"] %6#1, %8 : !quantum.bit, !quantum.bit %m_8, %out_9:2 = qec.ppm ["Z", "Y"] %7, %out_7#1 : !quantum.bit, !quantum.bit %m_10, %out_11 = qec.ppm ["X"] %out_9#1 : !quantum.bit %m_12, %out_13 = qec.select.ppm(%m_6, ["X"], ["Z"]) %out_9#0 : !quantum.bit %9 = arith.xori %m_8, %m_10 : i1 %10 = qec.ppr ["Z"](2) %out_7#0 cond(%9) : !quantum.bit quantum.dealloc_qb %out_13 : !quantum.bit quantum.dealloc_qb %out_11 : !quantum.bit %m_14, %out_15:2 = qec.ppm ["Z", "Z"] %6#0, %10 : !quantum.bit, !quantum.bit %from_elements = tensor.from_elements %m_14 : tensor<i1> %m_16, %out_17 = qec.ppm ["Z"] %out_15#1 : !quantum.bit . . . """ passes = { "ppm-compilation": { "decompose-method": decompose_method, "avoid-y-measure": avoid_y_measure, "max-pauli-size": max_pauli_size, } } if qnode is None: return functools.partial( ppm_compilation, decompose_method=decompose_method, avoid_y_measure=avoid_y_measure, max_pauli_size=max_pauli_size, ) return PassPipelineWrapper(qnode, passes)
[docs] def ppm_specs(fn): R""" This function returns following PPM specs in a dictionary: - Pi/4 PPR (count the number of clifford PPRs) - Pi/8 PPR (count the number of non-clifford PPRs) - Pi/2 PPR (count the number of classical PPRs) - Max weight for pi/8 PPRs - Max weight for pi/4 PPRs - Max weight for pi/2 PPRs - Number of logical qubits - Number of PPMs PPM specs are returned after the last PPM compilation pass is run. When there is control flow, this function can count the above statistics inside for loops with a statically known number of iterations. For all other cases, including dynamically sized for loops, and any conditionals and while loops, this pass exits with failure. Args: fn (QJIT): qjit-decorated function for which ``ppm_specs`` need to be printed. Returns: dict: A Python dictionary containing PPM specs of all functions in ``fn``. **Example** .. code-block:: python import pennylane as qml from catalyst import qjit, measure, for_loop from catalyst.passes import ppm_specs, ppm_compilation pipe = [("pipe", ["enforce-runtime-invariants-pipeline"])] device = qml.device("lightning.qubit", wires=2) @qjit(pipelines=pipe, target="mlir") @ppm_compilation @qml.qnode(device) def circuit(): qml.H(0) qml.CNOT([0,1]) @for_loop(0,10,1) def loop(i): qml.T(1) loop() return measure(0), measure(1) ppm_specs = ppm_specs(circuit) print(ppm_specs) Example PPM Specs: .. code-block:: pycon . . . { 'circuit_0': { 'max_weight_pi2': 2, 'logical_qubits': 2, 'num_of_ppm': 44, 'pi2_ppr': 16 }, } . . . """ if fn.mlir_module is not None: # aot mode new_options = copy.copy(fn.compile_options) if new_options.pipelines is None: raise CompileError("No pipeline found") # add ppm-spec pass at the end to existing pipeline _, pass_list = new_options.pipelines[0] # first pipeline runs the user passes # check if ppm-specs is already in the pass list if "ppm-specs" not in pass_list: # pragma: nocover pass_list.append("ppm-specs") new_options = _options_to_cli_flags(new_options) raw_result = _quantum_opt(*new_options, [], stdin=str(fn.mlir_module)) try: return json.loads( raw_result[: raw_result.index("module")] ) # remove MLIR starting with substring "module..." except Exception as e: # pragma: nocover raise CompileError( "Invalid json format encountered in ppm_specs. " f"Expected valid JSON but got {raw_result[: raw_result.index('module')]}" ) from e else: raise NotImplementedError("PPM passes only support AOT (Ahead-Of-Time) compilation mode.")
[docs] def reduce_t_depth(qnode): R""" An MLIR compiler pass that reduces the depth and count of non-Clifford PPRs (e.g., ``T`` gates) by commuting PPRs in adjacent layers and merging compatible ones (a layer comprises a set of PPRs that mutually commute). For more details, see the Figure 6 of `A Game of Surface Codes <https://arXiv:1808.02892v3>`_. The impact can be measured using ``catalyst.passes.ppm_specs`` to compare the circuit depth before and after applying the pass. The ``ppm_specs`` function provides detailed statistics including ``depth_pi8_ppr`` (non-Clifford PPR depth) and ``pi8_ppr`` (number of non-Clifford PPRs), allowing users to quantify the optimization achieved by the pass. Args: qnode (QNode): QNode to apply the pass to. Returns: ~.QNode: Returns decorated QNode. **Example** In the example below, after performing the :func:`catalyst.passes.to_ppr` and :func:`catalyst.passes.merge_ppr_ppm` passes, the circuit contains a depth of four of non-Clifford PPRs. Subsequently applying the ``reduce_t_depth`` pass will move PPRs around via commutation, resulting in a circuit with a smaller PPR depth. Specifically, in the circuit below, the Pauli-:math:`X` PPR (:math:`\exp(iX\tfrac{\pi}{8})`) on qubit Q1 will be moved to the first layer, which results in a depth of three non-Clifford PPRs. .. code-block:: python import pennylane as qml from catalyst import qjit, measure from catalyst.passes import to_ppr, commute_ppr, reduce_t_depth, merge_ppr_ppm pips = [("pipe", ["enforce-runtime-invariants-pipeline"])] @qjit(pipelines=pips, target="mlir") @reduce_t_depth @merge_ppr_ppm @commute_ppr @to_ppr @qml.qnode(qml.device("null.qubit", wires=3)) def circuit(): n = 3 for i in range(n): qml.H(wires=i) qml.S(wires=i) qml.CNOT(wires=[i, (i + 1) % n]) qml.T(wires=i) qml.H(wires=i) qml.T(wires=i) return [measure(wires=i) for i in range(n)] print(circuit.mlir_opt) Example MLIR Representation: .. code-block:: mlir . . . %1 = quantum.extract %0[ 0] : !quantum.reg -> !quantum.bit %2 = quantum.extract %0[ 1] : !quantum.reg -> !quantum.bit // layer 1 %3 = qec.ppr ["X"](8) %1 : !quantum.bit %4 = qec.ppr ["X"](8) %2 : !quantum.bit // layer 2 %5 = quantum.extract %0[ 2] : !quantum.reg -> !quantum.bit %6:2 = qec.ppr ["Y", "X"](8) %3, %4 : !quantum.bit, !quantum.bit %7 = qec.ppr ["X"](8) %5 : !quantum.bit %8:3 = qec.ppr ["X", "Y", "X"](8) %6#0, %6#1, %7:!quantum.bit, !quantum.bit, !quantum.bit // layer 3 %9:3 = qec.ppr ["X", "X", "Y"](8) %8#0, %8#1, %8#2:!quantum.bit, !quantum.bit, !quantum.bit . . . """ return PassPipelineWrapper(qnode, "reduce-t-depth")
[docs] def ppr_to_mbqc(qnode): R""" Specify that the MLIR compiler pass for lowering Pauli Product Rotations (PPR) and Pauli Product Measurements (PPM) to a measurement-based quantum computing (MBQC) style circuit will be applied. This pass replaces QEC operations (``qec.ppr`` and ``qec.ppm``) with a gate-based sequence in the Quantum dialect using universal gates and measurements that supported as MBQC gate set. For details, see the Figure 2 of [Measurement-based Quantum Computation on cluster states](https://arxiv.org/abs/quant-ph/0301052). Conceptually, each Pauli product is handled by: - Mapping its Pauli string to the Z basis via per-qubit conjugations (e.g., ``H`` for ``X``; specialized ``RotXZX`` sequences for ``Y``). - Accumulating parity onto the first qubit with a right-to-left CNOT ladder. - Emitting the kernel operation: - **PPR**: apply an ``RZ`` with an angle derived from the rotation kind. - **PPM**: perform a measurement and return an ``i1`` result. - Uncomputing by reversing the CNOT ladder and the conjugations. - Conjugating the qubits back to the original basis. .. note:: This pass expects PPR/PPM operations to be present. In practice, use it after :func:`~.passes.to_ppr` and/or :func:`~.passes.commute_ppr` and/or :func:`~.passes.merge_ppr_ppm`. Args: fn (QNode): QNode to apply the pass to. Returns: ~.QNode **Example** Convert a simple Clifford+T circuit to PPRs, then lower them to an MBQC-style circuit. Note that this pass should be applied before :func:`~.passes.ppr_to_ppm` since it requires the actual PPR/PPM operations. .. code-block:: python import pennylane as qml from catalyst import qjit, measure from catalyst.passes import to_ppr, ppr_to_mbqc pipeline = [("pipe", ["enforce-runtime-invariants-pipeline"])] @qjit(pipelines=pipeline, keep_intermediate=True, target="mlir") @ppr_to_mbqc @to_ppr @qml.qnode(qml.device("null.qubit", wires=2)) def circuit(): qml.H(0) qml.CNOT([0, 1]) return measure(1) print(circuit.mlir_opt) Example MLIR excerpt (structure only): .. code-block:: mlir ... %cst = arith.constant -1.5707963267948966 : f64 %cst_0 = arith.constant 1.5707963267948966 : f64 %0 = quantum.alloc( 2) : !quantum.reg %1 = quantum.extract %0[ 0] : !quantum.reg -> !quantum.bit %2 = quantum.extract %0[ 1] : !quantum.reg -> !quantum.bit %out_qubits = quantum.custom "RZ"(%cst_0) %1 : !quantum.bit %out_qubits_1 = quantum.custom "H"() %out_qubits : !quantum.bit %out_qubits_2 = quantum.custom "RZ"(%cst_0) %out_qubits_1 : !quantum.bit %out_qubits_3 = quantum.custom "H"() %out_qubits_2 : !quantum.bit %out_qubits_4 = quantum.custom "RZ"(%cst_0) %out_qubits_3 : !quantum.bit %out_qubits_5 = quantum.custom "H"() %2 : !quantum.bit %out_qubits_6:2 = quantum.custom "CNOT"() %out_qubits_5, %out_qubits_4 : !quantum.bit, !quantum.bit %out_qubits_7 = quantum.custom "RZ"(%cst_0) %out_qubits_6#1 : !quantum.bit %out_qubits_8:2 = quantum.custom "CNOT"() %out_qubits_6#0, %out_qubits_7 : !quantum.bit, !quantum.bit %out_qubits_9 = quantum.custom "H"() %out_qubits_8#0 : !quantum.bit %out_qubits_10 = quantum.custom "RZ"(%cst) %out_qubits_8#1 : !quantum.bit %out_qubits_11 = quantum.custom "H"() %out_qubits_9 : !quantum.bit %out_qubits_12 = quantum.custom "RZ"(%cst) %out_qubits_11 : !quantum.bit %out_qubits_13 = quantum.custom "H"() %out_qubits_12 : !quantum.bit %mres, %out_qubit = quantum.measure %out_qubits_13 : i1, !quantum.bit ... """ return PassPipelineWrapper(qnode, "ppr-to-mbqc")