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
import pennylane as qml
from catalyst.compiler import _options_to_cli_flags, _quantum_opt
from catalyst.utils.exceptions import CompileError
# pylint: disable=line-too-long, too-many-lines
## API ##
[docs]
def cancel_inverses(qnode):
"""
Specify that the ``-cancel-inverses`` 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 ``"QuantumCompilationStage"`` stage via the
:func:`~.get_compilation_stage` function.
Args:
fn (QNode): the QNode to apply the cancel inverses compiler pass to
Returns:
:class:`QNode <pennylane.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 ``"QuantumCompilationStage"`` stage:
>>> print(get_compilation_stage(circuit, stage="QuantumCompilationStage"))
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 qml.transform(pass_name="cancel-inverses")(qnode)
[docs]
def diagonalize_measurements(
qnode=None,
supported_base_obs: tuple[str, ...] = ("PauliZ", "Identity"),
to_eigvals: bool = False,
):
"""
Specify that the ``diagonalize-final-measurements`` compiler pass
will be applied, which diagonalizes measurements into the standard basis.
Args:
qnode (QNode): The QNode to apply the ``diagonalize_final_measurement`` compiler pass to.
supported_base_obs (tuple[str, ...]): A list of supported base observable names.
Allowed observables are ``PauliX``, ``PauliY``, ``PauliZ``, ``Hadamard`` and ``Identity``.
``PauliZ`` and ``Identity`` are always treated as supported, regardless of input. Defaults to
(``PauliZ``, ``Identity``).
to_eigvals (bool): Whether the diagonalization should create measurements using
eigenvalues and wires rather than observables. Defaults to ``False``.
Returns:
:class:`QNode <pennylane.QNode>`
.. note::
Unlike the PennyLane tape transform, :func:`pennylane.transforms.diagonalize_measurements`,
the QNode itself will not be changed or transformed by applying this decorator.
Unlike the PennyLane tape transform, ``supported_base_obs`` here only accepts a tuple of supported
base observable names, instead of the corresponding classes. The reason is that xDSL does not accept
class types as values of option-elements. For more details, please refer to the `xDSL repo <https://github.com/xdslproject/xdsl/blob/ba190d9ba1612807e7604374afa7eb2c1c3d2047/xdsl/utils/arg_spec.py#L315-L327>`__.
Unlike the PennyLane tape transform, only ``to_eigvals = False`` is supported. Setting ``to_eigvals`` as ``True``
will raise an error.
An error will be raised if non-commuting terms are encountered.
**Example**
The ``diagonalize-final-measurements`` compilation pass can be applied as a decorator on a QNode:
.. code-block:: python
import pennylane as qml
from catalyst import qjit
from catalyst.passes import diagonalize_measurements
@qjit
@diagonalize_measurements(supported_base_obs=("PauliX",))
@qml.qnode(qml.device("lightning.qubit", wires=1))
def circuit():
qml.Hadamard(0)
qml.RZ(1.1, 0)
qml.PhaseShift(0.22, 0)
return qml.expval(qml.Y(0))
expected_substr = 'transform.apply_registered_pass "diagonalize-final-measurements" with options = {"supported-base-obs" = ["PauliX"], "to-eigvals" = false}'
>>> expected_substr in circuit.mlir
True
>>> circuit()
0.9687151001182651
An error is raised if ``to_eigvals=True`` is passed as an option:
.. code-block:: python
import pennylane as qml
from catalyst import qjit
from catalyst.passes import diagonalize_measurements
@diagonalize_measurements(to_eigvals=True)
@qml.qnode(qml.device("lightning.qubit", wires=1))
def circuit():
qml.Hadamard(0)
qml.PhaseShift(0.22, 0)
return qml.expval(qml.Y(0))
error_msg = None
try:
qjit(circuit)
except ValueError as e:
error_msg = str(e)
>>> print(error_msg)
Only to_eigvals = False is supported.
A compile error is raised if non-commuting terms are encountered:
.. code-block:: python
import pennylane as qml
from pennylane.exceptions import CompileError
from catalyst import qjit
from catalyst.passes import diagonalize_measurements
@diagonalize_measurements
@qml.qnode(qml.device("lightning.qubit", wires=1))
def circuit():
qml.Hadamard(0)
return qml.expval(qml.Y(0) + qml.X(0))
error_msg = None
try:
qjit(circuit)
except CompileError as e:
error_msg = str(e)
>>> print(error_msg)
Observables are not qubit-wise commuting. Please apply the `split-non-commuting` pass first.
"""
if qnode is None:
return functools.partial(
diagonalize_measurements, supported_base_obs=supported_base_obs, to_eigvals=to_eigvals
)
return qml.transform(pass_name="diagonalize-final-measurements")(
qnode, supported_base_obs=supported_base_obs, to_eigvals=to_eigvals
)
[docs]
def disentangle_cnot(qnode):
"""A peephole optimization for replacing ``CNOT`` gates with single-qubit gates.
.. note::
This transform requires decorating the workflow with :func:`pennylane.qjit`.
Args:
fn (QNode): the QNode to apply the pass to
Returns:
:class:`QNode <pennylane.QNode>`
**Example**
In the circuit below, the ``CNOT`` gate can be simplified to just a ``PauliX`` gate since the
control qubit is always in the :math:`|1\rangle` state.
.. code-block:: python
import pennylane as qml
dev = qml.device("lightning.qubit", wires=2)
@qml.qjit(capture=True)
@qml.transforms.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()
When inspecting the circuit resources, only ``PauliX`` gates are present.
>>> print(qml.specs(circuit, level=1)())
Device: lightning.qubit
Device wires: 2
Shots: Shots(total=None)
Level: disentangle-cnot
<BLANKLINE>
Wire allocations: 2
Total gates: 2
Gate counts:
- PauliX: 2
Measurements:
- state(all wires): 1
Depth: Not computed
"""
return qml.transform(pass_name="disentangle-cnot")(qnode)
[docs]
def disentangle_swap(qnode):
r"""A peephole optimization for replacing ``SWAP`` gates with simpler gates (``PauliX`` and
``CNOT``).
.. note::
This transform requires decorating the workflow with :func:`pennylane.qjit`.
Args:
fn (QNode): the QNode to apply the pass to.
Returns:
:class:`QNode <pennylane.QNode>`
**Example**
In the circuit below, the ``SWAP`` gate can be simplified to a ``PauliX`` gate and two ``CNOT``
gates.
.. code-block:: python
import pennylane as qml
dev = qml.device("lightning.qubit", wires=2)
@qml.qjit(keep_intermediate=True)
@qml.transforms.disentangle_swap
@qml.qnode(dev)
def circuit():
# first qubit in |1>
qml.X(0)
# second qubit in non-basis
qml.RX(0.2, 1)
qml.SWAP([0, 1])
return qml.state()
When inspecting the circuit resources, the ``SWAP`` gate is no longer present.
>>> print(qml.specs(circuit, level=1)())
Device: lightning.qubit
Device wires: 2
Shots: Shots(total=None)
Level: disentangle-swap
<BLANKLINE>
Wire allocations: 2
Total gates: 5
Gate counts:
- PauliX: 2
- RX: 1
- CNOT: 2
Measurements:
- state(all wires): 1
Depth: Not computed
"""
return qml.transform(pass_name="disentangle-swap")(qnode)
[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 ``"QuantumCompilationStage`` stage via the
:func:`~.get_compilation_stage` function.
Args:
fn (QNode): the QNode to apply the cancel inverses compiler pass to
Returns:
:class:`QNode <pennylane.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 qml.transform(pass_name="merge-rotations")(qnode)
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:
:class:`QNode <pennylane.QNode>`
**Example**
// TODO: add example here
"""
return qml.transform(pass_name="decompose-lowering")(qnode) # 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 ``"QuantumCompilationStage"`` stage via the
:func:`~.get_compilation_stage` function.
Args:
fn (QNode): the QNode to apply the ions-decomposition pass to
Returns:
:class:`QNode <pennylane.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="QuantumCompilationStage"))
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 qml.transform(pass_name="ions-decomposition")(qnode)
[docs]
def gridsynth(qnode=None, *, epsilon=1e-4, ppr_basis=False):
r"""A quantum compilation pass to discretize
single-qubit RZ and PhaseShift gates into the Clifford+T basis or the PPR basis using the Ross-Selinger Gridsynth algorithm.
Reference: https://arxiv.org/abs/1403.2975
.. note::
The actual discretization is only performed during execution time.
Args:
qnode (QNode): the QNode to apply the gridsynth compiler pass to
epsilon (float): The maximum permissible operator norm error per rotation gate. Defaults to ``1e-4``.
ppr_basis (bool): If true, decompose directly to Pauli Product Rotations (PPRs) in PBC dialect. Defaults to ``False``
Returns:
:class:`QNode <pennylane.QNode>`
.. note::
The circuit generated from this pass with ``ppr_basis=True`` are currently only executable on the
``lightning.qubit`` device with program enabled.
**Example**
In this example the RZ gate will be converted into a new function, which
calls the discretization at execution time.
.. code-block:: python
import pennylane as qml
from catalyst import qjit
from catalyst.passes import gridsynth
pipe = [("pipe", ["quantum-compilation-stage"])]
@qjit(pipelines=pipe, target="mlir")
@gridsynth
@qml.qnode(qml.device("null.qubit", wires=1))
def circuit():
qml.RZ(x, wires=0)
return qml.probs()
>>> print(circuit.mlir_opt)
Example MLIR Representation:
.. code-block:: mlir
. . .
func.func private @rs_decomposition_get_phase(f64, f64, i1) -> f64
func.func private @rs_decomposition_get_gates(memref<?xindex>, f64, f64, i1)
func.func private @rs_decomposition_get_size(f64, f64, i1) -> index
func.func private @__catalyst_decompose_RZ_0(%arg0: !quantum.bit, %arg1: f64) -> (!quantum.bit, f64) {
. . .
%2 = scf.for %arg2 = %c0 to %0 step %c1 iter_args(%arg3 = %arg0) -> (!quantum.bit) {
%3 = memref.load %alloc[%arg2] : memref<?xindex>
%4 = scf.index_switch %3 -> !quantum.bit
case 0 {
%out_qubits = quantum.custom "T"() %arg3 : !quantum.bit
scf.yield %out_qubits : !quantum.bit
}
case 1 {
%out_qubits = quantum.custom "Hadamard"() %arg3 : !quantum.bit
%out_qubits_0 = quantum.custom "T"() %out_qubits : !quantum.bit
scf.yield %out_qubits_0 : !quantum.bit
}
case 2 {
%out_qubits = quantum.custom "S"() %arg3 : !quantum.bit
%out_qubits_0 = quantum.custom "Hadamard"() %out_qubits : !quantum.bit
%out_qubits_1 = quantum.custom "T"() %out_qubits_0 : !quantum.bit
scf.yield %out_qubits_1 : !quantum.bit
}
. . .
}
}
func.func public @circuit_0(%arg0: tensor<f64>) -> tensor<f64> attributes {diff_method = "adjoint", llvm.linkage = #llvm.linkage<internal>, qnode} {
. . .
%2:2 = call @__catalyst_decompose_RZ_0(%1, %extracted) : (!quantum.bit, f64) -> (!quantum.bit, f64)
. . .
}
"""
if qnode is None:
return functools.partial(gridsynth, epsilon=epsilon, ppr_basis=ppr_basis)
return qml.transform(pass_name="gridsynth")(qnode, epsilon=epsilon, ppr_basis=ppr_basis)
[docs]
def to_ppr(qnode):
r"""A quantum compilation pass that converts Clifford+T gates into Pauli Product Rotation (PPR)
gates.
.. note::
This transform requires decorating the workflow with :func:`@qjit <~.qjit>`. In
addition, the circuits generated by this pass are currently executable on
``lightning.qubit`` or ``null.qubit`` (for mock-execution).
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 Pauli product measurements and Pauli product rotations, check out the
`compilation hub <https://pennylane.ai/compilation/pauli-based-computation>`__.
The full list of supported gates and operations are
``qml.H``,
``qml.S``,
``qml.T``,
``qml.X``,
``qml.Y``,
``qml.Z``,
``qml.PauliRot``,
``qml.adjoint(qml.PauliRot)``,
``qml.adjoint(qml.S)``,
``qml.adjoint(qml.T)``,
``qml.CNOT``, and
``qml.measure``.
Args:
fn (QNode): the QNode to apply the pass to
Returns:
:class:`QNode <pennylane.QNode>`
.. seealso::
:func:`pennylane.transforms.commute_ppr`, :func:`pennylane.transforms.merge_ppr_ppm`,
:func:`pennylane.transforms.ppr_to_ppm`, :func:`pennylane.transforms.ppm_compilation`,
:func:`pennylane.transforms.reduce_t_depth`, :func:`pennylane.transforms.decompose_arbitrary_ppr`
.. note::
For better compatibility with other PennyLane functionality, ensure that PennyLane program
capture is enabled with ``@qjit(capture=True)``.
**Example**
The ``to_ppr`` compilation pass can be applied as a decorator on a QNode:
.. code-block:: python
import pennylane as qml
@qml.qjit(capture=True)
@qml.transforms.to_ppr
@qml.qnode(qml.device("lightning.qubit", wires=2))
def circuit():
qml.H(0)
qml.CNOT([0, 1])
m = qml.measure(0)
qml.T(0)
return qml.expval(qml.Z(0))
>>> circuit()
Array(-1., dtype=float64)
>>> print(qml.specs(circuit, level=1)())
Device: lightning.qubit
Device wires: 2
Shots: Shots(total=None)
Level: to-ppr
<BLANKLINE>
Wire allocations: 2
Total gates: 11
Gate counts:
- GlobalPhase: 3
- PPR-pi/4-w1: 5
- PPR-pi/4-w2: 1
- PPM-w1: 1
- PPR-pi/8-w1: 1
Measurements:
- expval(PauliZ): 1
Depth: Not computed
In the above output, ``PPR-theta-w<int>`` denotes the type of PPR present in the circuit, where
``theta`` is the PPR angle (:math:`\theta`) and ``w<int>`` denotes the PPR weight (the number of
qubits it acts on, or the length of the Pauli word). ``PPM-w<int>`` follows the same convention.
Note that the mid-circuit measurement (:func:`pennylane.measure`) in the circuit has been
converted to a Pauli product measurement (PPM), as well.
"""
return qml.transform(pass_name="to-ppr")(qnode)
[docs]
def commute_ppr(qnode=None, *, max_pauli_size=0):
r"""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.
.. note::
This transform requires decorating the workflow with :func:`@qml.qjit <pennylane.qjit>`. In
addition, the circuits generated by this pass are currently not executable on any
backend. This pass is only for Pauli-based-computation analysis with the ``null.qubit``
device and potential future execution when a suitable backend is available.
Lastly, the :func:`pennylane.transforms.to_ppr` transform must be applied before
``commute_ppr``.
For more information on PPRs, check out the
`Compilation Hub <https://pennylane.ai/compilation/pauli-product-rotations>`_.
Args:
fn (QNode): QNode to apply the pass to.
max_pauli_size (int):
The maximum size of Pauli strings resulting from commutation. If a commutation results
in a PPR that acts on more than ``max_pauli_size`` qubits, that commutation will not be
performed. Note that the default ``max_pauli_size=0`` indicates no limit.
Returns:
:class:`QNode <pennylane.QNode>`
.. seealso::
:func:`pennylane.transforms.to_ppr`, :func:`pennylane.transforms.merge_ppr_ppm`,
:func:`pennylane.transforms.ppr_to_ppm`, :func:`pennylane.transforms.ppm_compilation`,
:func:`pennylane.transforms.reduce_t_depth`, :func:`pennylane.transforms.decompose_arbitrary_ppr`
.. note::
For better compatibility with other PennyLane functionality, ensure that PennyLane program
capture is enabled with ``@qjit(capture=True)``.
**Example**
The ``commute_ppr`` compilation pass can be applied as a decorator on a QNode:
.. code-block:: python
import pennylane as qml
import jax.numpy as jnp
@qml.qjit(capture=True)
@qml.transforms.commute_ppr(max_pauli_size=2)
@qml.transforms.to_ppr
@qml.qnode(qml.device("lightning.qubit", wires=2))
def circuit():
# equivalent to a Hadamard gate
qml.PauliRot(jnp.pi / 2, pauli_word="Z", wires=0)
qml.PauliRot(jnp.pi / 2, pauli_word="X", wires=0)
qml.PauliRot(jnp.pi / 2, pauli_word="Z", wires=0)
# equivalent to a CNOT gate
qml.PauliRot(jnp.pi / 2, pauli_word="ZX", wires=[0, 1])
qml.PauliRot(-jnp.pi / 2, pauli_word="Z", wires=0)
qml.PauliRot(-jnp.pi / 2, pauli_word="X", wires=1)
# equivalent to a T gate
qml.PauliRot(jnp.pi / 4, pauli_word="Z", wires=0)
return qml.expval(qml.Z(0))
>>> circuit()
Array(-1.11022302e-16, dtype=float64)
>>> print(qml.specs(circuit, level=2)())
Device: lightning.qubit
Device wires: 2
Shots: Shots(total=None)
Level: commute-ppr
<BLANKLINE>
Wire allocations: 2
Total gates: 7
Gate counts:
- PPR-pi/8-w1: 1
- PPR-pi/4-w1: 5
- PPR-pi/4-w2: 1
Measurements:
- expval(PauliZ): 1
Depth: Not computed
In the example above, the Clifford PPRs (:class:`~.PauliRot` instances with an angle of rotation
of :math:`\tfrac{\pi}{2}`) will be commuted past the non-Clifford PPR (:class:`~.PauliRot`
instances with an angle of rotation of :math:`\tfrac{\pi}{4}`). In the above output,
``PPR-theta-w<int>`` denotes the type of PPR present in the circuit, where ``theta`` is the PPR
angle (:math:`\theta`) and ``w<int>`` denotes the PPR weight (the number of qubits it acts on,
or the length of the Pauli word).
Note that 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.
"""
if qnode is None:
return functools.partial(commute_ppr, max_pauli_size=max_pauli_size)
return qml.transform(pass_name="commute-ppr")(qnode, max_pauli_size=max_pauli_size)
[docs]
def merge_ppr_ppm(qnode=None, *, max_pauli_size=0):
r"""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).
.. note::
This transform requires decorating the workflow with :func:`@qml.qjit <pennylane.qjit>`. In
addition, the circuits generated by this pass are currently executable on
``lightning.qubit`` or ``null.qubit`` (for mock-execution).
Secondly, the ``merge_ppr_ppm`` transform does not currently affect terminal measurements.
So, for accurate results, it is recommended to return nothing (i.e., a blank ``return``
statement) from the QNode.
Lastly, the :func:`pennylane.transforms.to_ppr` transform must be applied before
``merge_ppr_ppm``.
For more information on PPRs and PPMs, check out
the `Compilation Hub <https://pennylane.ai/compilation/pauli-based-computation>`_.
Args:
fn (QNode): QNode to apply the pass to
max_pauli_size (int):
The maximum size of Pauli strings resulting from merging. If a merge results in a PPM
that acts on more than ``max_pauli_size`` qubits, that merge will not be performed. The
default value is ``0`` (no limit).
Returns:
:class:`QNode <pennylane.QNode>`
.. seealso::
:func:`pennylane.transforms.to_ppr`, :func:`pennylane.transforms.commute_ppr`,
:func:`pennylane.transforms.ppr_to_ppm`, :func:`pennylane.transforms.ppm_compilation`,
:func:`pennylane.transforms.reduce_t_depth`, :func:`pennylane.transforms.decompose_arbitrary_ppr`
.. note::
For better compatibility with other PennyLane functionality, ensure that PennyLane program
capture is enabled with ``@qjit(capture=True)``.
**Example**
The ``merge_ppr_ppm`` compilation pass can be applied as a decorator on a QNode:
.. code-block:: python
import pennylane as qml
import jax.numpy as jnp
@qml.qjit(capture=True)
@qml.transforms.merge_ppr_ppm(max_pauli_size=2)
@qml.transforms.to_ppr
@qml.qnode(qml.device("lightning.qubit", wires=2))
def circuit():
qml.PauliRot(jnp.pi / 2, pauli_word="Z", wires=0)
qml.PauliRot(jnp.pi / 2, pauli_word="X", wires=1)
ppm = qml.pauli_measure(pauli_word="ZX", wires=[0, 1])
return qml.probs()
>>> circuit()
Array([0.5, 0.5, 0. , 0. ], dtype=float64)
>>> print(qml.specs(circuit, level=2)())
Device: lightning.qubit
Device wires: 2
Shots: Shots(total=None)
Level: merge-ppr-ppm
<BLANKLINE>
Wire allocations: 2
Total gates: 3
Gate counts:
- PPM-w2: 1
- PPR-pi/4-w1: 2
Measurements:
- probs(all wires): 1
Depth: Not computed
If a merging resulted in a PPM acting on more than ``max_pauli_size`` qubits, that merging
operation would be skipped. In the above output, ``PPM-w<int>`` denotes the PPM weight (the
number of qubits it acts on, or the length of the Pauli word).
"""
if qnode is None:
return functools.partial(merge_ppr_ppm, max_pauli_size=max_pauli_size)
return qml.transform(pass_name="merge-ppr-ppm")(qnode, max_pauli_size=max_pauli_size)
[docs]
def ppr_to_ppm(qnode=None, *, decompose_method="pauli-corrected", avoid_y_measure=False):
r"""A quantum compilation pass that decomposes Pauli product rotations (PPRs),
:math:`P(\theta) = \exp(-iP\theta)`, into Pauli product measurements (PPMs).
.. note::
This transform requires decorating the workflow with :func:`@qml.qjit <pennylane.qjit>`. In
addition, the circuits generated by this pass are currently executable on
``lightning.qubit`` or ``null.qubit`` (for mock-execution).
Lastly, the :func:`pennylane.transforms.to_ppr` transform must be applied before
``ppr_to_ppm``.
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, then Clifford PPRs
(:math:`\theta = \tfrac{\pi}{4}`) are decomposed.
For more information on PPRs and PPMs, check out
the `Compilation Hub <https://pennylane.ai/compilation/pauli-based-computation>`_.
Args:
qnode (QNode): QNode to apply the pass to.
decompose_method (str): 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 that is based on Figure
13 in `arXiv:2211.15465 <https://arxiv.org/pdf/2211.15465>`_.
``"auto-corrected"`` uses an additional measurement for correction that is based on
Figure 7 in `A Game of Surface Codes <https://arxiv.org/abs/1808.02892>`__, and
``"clifford-corrected"`` uses a Clifford rotation for correction that is based on
Figure 17(b) in `A Game of Surface Codes <https://arxiv.org/abs/1808.02892>`__.
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 methods. Defaults to
``False``.
Returns:
:class:`QNode <pennylane.QNode>`
.. seealso::
:func:`pennylane.transforms.to_ppr`, :func:`pennylane.transforms.commute_ppr`,
:func:`pennylane.transforms.merge_ppr_ppm`, :func:`pennylane.transforms.ppm_compilation`,
:func:`pennylane.transforms.reduce_t_depth`, :func:`pennylane.transforms.decompose_arbitrary_ppr`
.. note::
For better compatibility with other PennyLane functionality, ensure that PennyLane program
capture is enabled with ``@qjit(capture=True)``.
**Example**
The ``ppr_to_ppm`` compilation pass can be applied as a decorator on a QNode:
.. code-block:: python
import pennylane as qml
from functools import partial
import jax.numpy as jnp
@qml.qjit(capture=True)
@qml.transforms.ppr_to_ppm
@qml.transforms.to_ppr
@qml.qnode(qml.device("null.qubit", wires=2))
def circuit():
# equivalent to a Hadamard gate
qml.PauliRot(jnp.pi / 2, pauli_word="Z", wires=0)
qml.PauliRot(jnp.pi / 2, pauli_word="X", wires=0)
qml.PauliRot(jnp.pi / 2, pauli_word="Z", wires=0)
# equivalent to a CNOT gate
qml.PauliRot(jnp.pi / 2, pauli_word="ZX", wires=[0, 1])
qml.PauliRot(-jnp.pi / 2, pauli_word="Z", wires=[0])
qml.PauliRot(-jnp.pi / 2, pauli_word="X", wires=[1])
# equivalent to a T gate
qml.PauliRot(jnp.pi / 4, pauli_word="Z", wires=0)
return qml.expval(qml.Z(0))
>>> print(qml.specs(circuit, level=2)())
Device: null.qubit
Device wires: 2
Shots: Shots(total=None)
Level: ppr-to-ppm
<BLANKLINE>
Wire allocations: 8
Total gates: 22
Gate counts:
- PPM-w2: 6
- PPM-w1: 7
- PPM-w3: 1
- PPR-pi/2-w1: 6
- PPR-pi/2-w2: 1
- pbc.fabricate: 1
Measurements:
- expval(PauliZ): 1
Depth: Not computed
In the above output, ``PPR-theta-w<int>`` denotes the type of PPR present in the circuit, where
``theta`` is the PPR angle (:math:`\theta`) and ``w<int>`` denotes the PPR weight (the number of
qubits it acts on, or the length of the Pauli word). ``PPM-w<int>`` follows the same convention.
Note that :math:`\theta = \tfrac{\pi}{2}` PPRs correspond to Pauli operators
(:math:`P(\tfrac{\pi}{2}) = \exp(-iP\tfrac{\pi}{2}) = P`). Pauli operators can be commuted to
the end of the circuit and absorbed into terminal measurements.
"""
if qnode is None:
return functools.partial(
ppr_to_ppm, decompose_method=decompose_method, avoid_y_measure=avoid_y_measure
)
return qml.transform(pass_name="ppr-to-ppm")(
qnode, decompose_method=decompose_method, avoid_y_measure=avoid_y_measure
)
[docs]
def ppm_compilation(
qnode=None, *, decompose_method="pauli-corrected", avoid_y_measure=False, max_pauli_size=0
):
r"""A quantum compilation pass that transforms Clifford+T gates into Pauli product measurements
(PPMs).
.. note::
This transform requires decorating the workflow with :func:`@qml.qjit <pennylane.qjit>`. In
addition, the circuits generated by this pass are currently executable on
``lightning.qubit`` or ``null.qubit`` (for mock-execution).
This pass combines multiple sub-passes:
- :func:`pennylane.transforms.to_ppr` : Converts gates into Pauli Product Rotations (PPRs)
- :func:`pennylane.transforms.commute_ppr` : Commutes PPRs past non-Clifford PPRs
- :func:`pennylane.transforms.merge_ppr_ppm` : Merges PPRs into Pauli Product Measurements (PPMs)
- :func:`pennylane.transforms.ppr_to_ppm` : Decomposes PPRs into PPMs
The ``avoid_y_measure`` and ``decompose_method`` arguments are passed to the
:func:`pennylane.transforms.ppr_to_ppm` pass. The ``max_pauli_size`` argument is passed to the
:func:`pennylane.transforms.commute_ppr` and :func:`pennylane.transforms.merge_ppr_ppm` passes.
For more information on PPRs and PPMs, check out
the `Compilation Hub <https://pennylane.ai/compilation/pauli-based-computation>`_.
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 that is based on Figure
13 in `arXiv:2211.15465 <https://arxiv.org/pdf/2211.15465>`_.
``"auto-corrected"`` uses an additional measurement for correction that is based on
Figure 7 in `A Game of Surface Codes <https://arxiv.org/abs/1808.02892>`__, and
``"clifford-corrected"`` uses a Clifford rotation for correction that is based on
Figure 17(b) in `A Game of Surface Codes <https://arxiv.org/abs/1808.02892>`__.
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 methods. 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>`
.. note::
For better compatibility with other PennyLane functionality, ensure that PennyLane program
capture is enabled with ``@qjit(capture=True)``.
**Example**
The ``ppm_compilation`` compilation pass can be applied as a decorator on a QNode:
.. code-block:: python
import pennylane as qml
@qml.qjit(capture=True)
@qml.transforms.ppm_compilation(decompose_method="clifford-corrected", max_pauli_size=2)
@qml.qnode(qml.device("null.qubit", wires=2))
def circuit():
qml.H(0)
qml.CNOT([0, 1])
qml.T(0)
return qml.expval(qml.Z(0))
>>> print(qml.specs(circuit, level=1)())
Device: null.qubit
Device wires: 2
Shots: Shots(total=None)
Level: ppm-compilation
<BLANKLINE>
Wire allocations: 8
Total gates: 25
Gate counts:
- GlobalPhase: 3
- pbc.fabricate: 1
- PPM-w2: 6
- PPM-w1: 7
- PPM-w3: 1
- PPR-pi/2-w1: 6
- PPR-pi/2-w2: 1
Measurements:
- expval(PauliZ): 1
Depth: Not computed
In the above output, ``PPR-theta-w<int>`` denotes the type of PPR present in the circuit, where
``theta`` is the PPR angle (:math:`\theta`) and ``w<int>`` denotes the PPR weight (the number of
qubits it acts on, or the length of the Pauli word). ``PPM-w<int>`` follows the same convention.
Note that :math:`\theta = \tfrac{\pi}{2}` PPRs correspond to Pauli operators
(:math:`P(\tfrac{\pi}{2}) = \exp(-iP\tfrac{\pi}{2}) = P`). Pauli operators can be commuted to
the end of the circuit and absorbed into terminal measurements.
Lastly, if a commutation or merge resulted in a PPR or PPM acting on more than
``max_pauli_size`` qubits (here, ``max_pauli_size = 2``), that commutation or merge would be
skipped.
"""
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 qml.transform(pass_name="ppm-compilation")(
qnode,
decompose_method=decompose_method,
avoid_y_measure=avoid_y_measure,
max_pauli_size=max_pauli_size,
)
[docs]
def ppm_specs(fn):
r"""This function returns following Pauli product rotation (PPR) and Pauli product measurement (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
.. note::
It is recommended to use :func:`pennylane.specs` instead of ``ppm_specs`` to retrieve
resource counts of PPR-PPM workflows.
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
import catalyst
p = [("my_pipe", ["quantum-compilation-stage"])]
device = qml.device("lightning.qubit", wires=2)
@qml.qjit(pipelines=p, target="mlir")
@catalyst.passes.ppm_compilation
@qml.qnode(device)
def circuit():
qml.H(0)
qml.CNOT([0,1])
@catalyst.for_loop(0,10,1)
def loop(i):
qml.T(1)
loop()
return catalyst.measure(0), catalyst.measure(1)
ppm_specs = catalyst.passes.ppm_specs(circuit)
print(ppm_specs)
Example PPM Specs:
.. code-block:: pycon
. . .
{'circuit_0':
{
'depth_pi2_ppr': 7,
'depth_ppm': 15,
'logical_qubits': 2,
'max_weight_pi2': 2,
'num_of_ppm': 24,
'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"""A quantum compilation pass that reduces the depth and count of non-Clifford Pauli product
rotation (PPR, :math:`P(\theta) = \exp(-iP\theta)`) operators (e.g., ``T`` gates) by commuting
PPRs in adjacent layers and merging compatible ones (a layer comprises PPRs that mutually
commute). For more details, see Figure 6 of
`A Game of Surface Codes <https://arXiv:1808.02892v3>`_.
.. note::
This transform requires decorating the workflow with :func:`@qml.qjit <pennylane.qjit>`. In
addition, the circuits generated by this pass are currently executable on
``lightning.qubit`` or ``null.qubit`` (for mock-execution).
Lastly, the :func:`pennylane.transforms.to_ppr` transform must be applied before
``reduce_t_depth``.
Args:
qnode (QNode): QNode to apply the pass to.
Returns:
:class:`QNode <pennylane.QNode>`
.. seealso::
:func:`pennylane.transforms.to_ppr`, :func:`pennylane.transforms.commute_ppr`,
:func:`pennylane.transforms.merge_ppr_ppm`, :func:`pennylane.transforms.ppr_to_ppm`,
:func:`pennylane.transforms.ppm_compilation`, :func:`pennylane.transforms.decompose_arbitrary_ppr`
.. note::
For better compatibility with other PennyLane functionality, ensure that PennyLane program
capture is enabled with ``@qjit(capture=True)``.
**Example**
In the example below, after performing the :func:`pennylane.transforms.to_ppr` and
:func:`pennylane.transforms.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.
Consider the following example:
.. code-block:: python
import pennylane as qml
import jax.numpy as jnp
@qml.qjit(capture=True)
@qml.transforms.reduce_t_depth
@qml.transforms.to_ppr
@qml.qnode(qml.device("null.qubit", wires=4))
def circuit():
qml.PauliRot(jnp.pi / 4, pauli_word="Z", wires=1)
qml.PauliRot(-jnp.pi / 4, pauli_word="XYZ", wires=[0, 2, 3])
qml.PauliRot(-jnp.pi / 2, pauli_word="XYZY", wires=[0, 1, 2, 3])
qml.PauliRot(jnp.pi / 4, pauli_word="XZX", wires=[0, 1, 3])
qml.PauliRot(-jnp.pi / 4, pauli_word="XZY", wires=[0, 1, 2])
return qml.expval(qml.Z(0))
The ``reduce_t_depth`` compilation pass will rearrange the last three PPRs in the above circuit
to reduce the non-Clifford PPR depth. This is best seen with the :func:`catalyst.draw_graph`
function:
>>> import catalyst
>>> num_passes = 2
>>> fig1, _ = catalyst.draw_graph(circuit, level=num_passes-1)() # doctest: +SKIP
>>> fig2, _ = catalyst.draw_graph(circuit, level=num_passes)() # doctest: +SKIP
Without ``reduce_t_depth`` applied:
>>> fig1.savefig('path_to_file1.png', dpi=300, bbox_inches="tight") # doctest: +SKIP
.. figure:: /_static/reduce-t-depth-example1.png
:width: 35%
:alt: Graphical representation of circuit without ``reduce_t_depth``
:align: left
With ``reduce_t_depth`` applied:
>>> fig2.savefig('path_to_file2.png', dpi=300, bbox_inches="tight") # doctest: +SKIP
.. figure:: /_static/reduce-t-depth-example2.png
:width: 35%
:alt: Graphical representation of circuit with ``reduce_t_depth``
:align: left
"""
return qml.transform(pass_name="reduce-t-depth")(qnode)
[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 PBC operations (``pbc.ppr`` and ``pbc.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`.
Args:
fn (QNode): QNode to apply the pass to.
Returns:
:class:`QNode <pennylane.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
import catalyst
p = [("my_pipe", ["quantum-compilation-stage"])]
@qml.qjit(pipelines=p, target="mlir", keep_intermediate=True)
@catalyst.passes.ppr_to_mbqc
@catalyst.passes.to_ppr
@qml.qnode(qml.device("null.qubit", wires=2))
def circuit():
qml.H(0)
qml.CNOT([0, 1])
return
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 qml.transform(pass_name="ppr-to-mbqc")(qnode)
# This pass is already covered via applying by pass
# `qml.transform(pass_name="decompose-arbitrary-ppr")` in Pennylane.
[docs]
def decompose_arbitrary_ppr(qnode): # pragma: nocover
r"""A quantum compilation pass that decomposes arbitrary-angle Pauli product rotations (PPRs) into a
collection of PPRs (with angles of rotation of :math:`\tfrac{\pi}{2}`, :math:`\tfrac{\pi}{4}`,
and :math:`\tfrac{\pi}{8}`), PPMs and a single-qubit arbitrary-angle PPR in the Z basis. For
details, see `Figure 13(d) of arXiv:2211.15465 <https://arxiv.org/abs/2211.15465>`__.
.. note::
This transform requires decorating the workflow with :func:`@qml.qjit <pennylane.qjit>`. In
addition, the circuits generated by this pass are currently executable on
``lightning.qubit`` or ``null.qubit`` (for mock-execution).
Lastly, the :func:`pennylane.transforms.to_ppr` transform must be applied before
``decompose_arbitrary_ppr``.
Args:
qnode (QNode): QNode to apply the pass to.
Returns:
:class:`QNode <pennylane.QNode>`
.. seealso::
:func:`pennylane.transforms.to_ppr`, :func:`pennylane.transforms.commute_ppr`,
:func:`pennylane.transforms.merge_ppr_ppm`, :func:`pennylane.transforms.ppr_to_ppm`,
:func:`pennylane.transforms.ppm_compilation`, :func:`pennylane.transforms.reduce_t_depth`
.. note::
For better compatibility with other PennyLane functionality, ensure that PennyLane program
capture is enabled with ``@qjit(capture=True)``.
**Example**
In the example below, the arbitrary-angle PPR
(``qml.PauliRot(0.1, pauli_word="XY", wires=[0, 1])``), will be decomposed into various other
PPRs and PPMs in accordance with
`Figure 13(d) of arXiv:2211.15465 <https://arxiv.org/abs/2211.15465>`__.
.. code-block:: python
import pennylane as qml
@qml.qjit(capture=True)
@qml.transforms.decompose_arbitrary_ppr
@qml.transforms.to_ppr
@qml.qnode(qml.device("null.qubit", wires=3))
def circuit():
qml.PauliRot(0.1, pauli_word="XY", wires=[0, 1])
return qml.expval(qml.Z(0))
>>> print(qml.specs(circuit, level=2)())
Device: null.qubit
Device wires: 3
Shots: Shots(total=None)
Level: decompose-arbitrary-ppr
<BLANKLINE>
Wire allocations: 3
Total gates: 6
Gate counts:
- pbc.prepare: 1
- PPM-w3: 1
- PPM-w1: 1
- PPR-pi/2-w1: 1
- PPR-pi/2-w2: 1
- PPR-Phi-w1: 1
Measurements:
- expval(PauliZ): 1
Depth: Not computed
In the above output, ``PPR-theta-w<int>`` denotes the type of PPR present in the circuit, where
``theta`` is the PPR angle (:math:`\theta`) and ``w<int>`` denotes the PPR weight (the number of
qubits it acts on, or the length of the Pauli word). ``PPM-w<int>`` follows the same convention.
``PPR-Phi-w<int>`` corresponds to a PPR whose angle of rotation is not :math:`\tfrac{\pi}{2}`,
:math:`\tfrac{\pi}{4}`, or :math:`\tfrac{\pi}{8}`.
"""
return qml.transform(pass_name="decompose-arbitrary-ppr")(qnode)
_modules/catalyst/passes/builtin_passes
Download Python script
Download Notebook
View on GitHub