# 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 json
import os
from pathlib import Path
from typing import Iterable
import pennylane as qp
from pennylane.decomposition.utils import to_name
from catalyst.compiler import _options_to_cli_flags, _quantum_opt
from catalyst.utils.exceptions import CompileError
from catalyst.utils.runtime_environment import BYTECODE_FILE_PATH, get_lib_path, get_libpython_path
# pylint: disable=line-too-long, too-many-lines
## API ##
def cancel_inverses_setup_inputs():
"""
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:`qp.Hadamard <pennylane.Hadamard>`,
:class:`qp.PauliX <pennylane.PauliX>`,
:class:`qp.PauliY <pennylane.PauliY>`,
:class:`qp.PauliZ <pennylane.PauliZ>`
Two-bit Gates:
:class:`qp.CNOT <pennylane.CNOT>`,
:class:`qp.CY <pennylane.CY>`,
:class:`qp.CZ <pennylane.CZ>`,
:class:`qp.SWAP <pennylane.SWAP>`
Three-bit Gates:
:class:`qp.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
import pennylane as qp
from catalyst import qjit
from catalyst.debug import get_compilation_stage
from catalyst.passes import cancel_inverses
dev = qp.device("lightning.qubit", wires=1)
@qjit(keep_intermediate=True)
@cancel_inverses
@qp.qnode(dev)
def circuit(x: float):
qp.RX(x, wires=0)
qp.Hadamard(wires=0)
qp.Hadamard(wires=0)
return qp.expval(qp.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 (), {}
cancel_inverses = qp.transform(
pass_name="cancel-inverses", setup_inputs=cancel_inverses_setup_inputs
)
def diagonalize_measurements_setup_inputs(
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 qp
from catalyst import qjit
from catalyst.passes import diagonalize_measurements
@qjit
@diagonalize_measurements(supported_base_obs=("PauliX",))
@qp.qnode(qp.device("lightning.qubit", wires=1))
def circuit():
qp.Hadamard(0)
qp.RZ(1.1, 0)
qp.PhaseShift(0.22, 0)
return qp.expval(qp.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 qp
from catalyst import qjit
from catalyst.passes import diagonalize_measurements
@diagonalize_measurements(to_eigvals=True)
@qp.qnode(qp.device("lightning.qubit", wires=1))
def circuit():
qp.Hadamard(0)
qp.PhaseShift(0.22, 0)
return qp.expval(qp.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 qp
from pennylane.exceptions import CompileError
from catalyst import qjit
from catalyst.passes import diagonalize_measurements
@diagonalize_measurements
@qp.qnode(qp.device("lightning.qubit", wires=1))
def circuit():
qp.Hadamard(0)
return qp.expval(qp.Y(0) + qp.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.
"""
return (), {"supported_base_obs": supported_base_obs, "to_eigvals": to_eigvals}
diagonalize_measurements = qp.transform(
pass_name="diagonalize-final-measurements", setup_inputs=diagonalize_measurements_setup_inputs
)
def disentangle_cnot_setup_inputs():
r"""A relaxed peephole optimization for replacing ``CNOT`` gates with single-qubit gates.
The optimizations that this pass performs are found in
`arXiv:2012.07711 <https://arxiv.org/pdf/2012.07711>`_, specifically TABLE I. The patterns
therein represent functional equivalencies to applying a ``CNOT`` gate on certain two-qubit
input states.
.. note::
This transform requires decorating the workflow with :func:`~.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 qp
dev = qp.device("lightning.qubit", wires=2)
@qp.qjit(capture=True)
@qp.transforms.disentangle_cnot
@qp.qnode(dev)
def circuit():
# first qubit in |1>
qp.X(0)
# second qubit in |0>
# current state : |10>
qp.CNOT([0, 1]) # state after CNOT : |11>
return qp.state()
When inspecting the circuit resources, only ``PauliX`` gates are present.
>>> print(qp.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 (), {}
disentangle_cnot = qp.transform(
pass_name="disentangle-cnot", setup_inputs=disentangle_cnot_setup_inputs
)
def disentangle_swap_setup_inputs():
r"""A relaxed peephole optimization for replacing ``SWAP`` gates with single-qubit gates.
The optimizations that this pass performs are found in
`arXiv:2012.07711 <https://arxiv.org/pdf/2012.07711>`_, specifically TABLE VI. The patterns
therein represent functional equivalencies to applying a ``SWAP`` gate on certain two-qubit
input states.
.. note::
This transform requires decorating the workflow with :func:`~.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 qp
dev = qp.device("lightning.qubit", wires=2)
@qp.qjit(keep_intermediate=True)
@qp.transforms.disentangle_swap
@qp.qnode(dev)
def circuit():
# first qubit in |1>
qp.X(0)
# second qubit in non-basis
qp.RX(0.2, 1)
qp.SWAP([0, 1])
return qp.state()
When inspecting the circuit resources, the ``SWAP`` gate is no longer present.
>>> print(qp.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 (), {}
disentangle_swap = qp.transform(
pass_name="disentangle-swap", setup_inputs=disentangle_swap_setup_inputs
)
def merge_rotations_setup_inputs():
r"""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:`qp.RX <pennylane.RX>`,
:class:`qp.CRX <pennylane.CRX>`,
:class:`qp.RY <pennylane.RY>`,
:class:`qp.CRY <pennylane.CRY>`,
:class:`qp.RZ <pennylane.RZ>`,
:class:`qp.CRZ <pennylane.CRZ>`,
:class:`qp.PhaseShift <pennylane.PhaseShift>`,
:class:`qp.ControlledPhaseShift <pennylane.ControlledPhaseShift>`,
:class:`qp.Rot <pennylane.Rot>`,
:class:`qp.CRot <pennylane.CRot>`,
:class:`qp.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 merge rotations compiler pass to
Returns:
:class:`QNode <pennylane.QNode>`
**Example**
In this example the three :class:`qp.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 = qp.device("lightning.qubit", wires=1)
@qjit(keep_intermediate=True)
@merge_rotations
@qp.qnode(dev)
def circuit(x: float):
qp.RX(x, wires=0)
qp.RX(0.1, wires=0)
qp.RX(x**2, wires=0)
return qp.expval(qp.PauliZ(0))
>>> circuit(0.54)
Array(0.5965506257017892, dtype=float64)
"""
return (), {}
merge_rotations = qp.transform(
pass_name="merge-rotations", setup_inputs=merge_rotations_setup_inputs
)
def parity_synth_setup_inputs():
r"""Pass for synthesizing phase polynomials in a circuit.
ParitySynth has been proposed by Vandaele et al. in `arXiv:2104.00934
<https://arxiv.org/abs/2104.00934>`__ as a technique to synthesize
`phase polynomials
<https://pennylane.ai/compilation/phase-polynomial-intermediate-representation>`__
into elementary quantum gates, namely ``CNOT`` and ``RZ``. For this, it synthesizes the
`parity table <https://pennylane.ai/compilation/parity-table>`__ of the phase polynomial,
and defers the remaining `parity matrix <https://pennylane.ai/compilation/parity-matrix>`__
synthesis to `RowCol <https://pennylane.ai/compilation/rowcol-algorithm>`__.
.. note::
This transform requires decorating the workflow with :func:`~.qjit`. Additionally, this pass
requires the ``networkx`` package, which can be installed via
``pip install networkx``.
Args:
fn (QNode): QNode to apply the pass to
Returns:
:class:`QNode <pennylane.QNode>`
This pass walks over the input circuit and aggregates all ``CNOT`` and ``RZ`` operators
into a subcircuit that describes a phase polyonomial. Other gates form the boundaries of
these subcircuits, and whenever one is encountered the phase polynomial of the aggregated
subcircuit is resynthesized with the ParitySynth algorithm. This implies that while this
pass works on circuits containing any operations, it is recommended to maximize the
subcircuits that represent phase polynomials (i.e. consist of ``CNOT`` and ``RZ`` gates) to
enhance the effectiveness of the pass. This might be possible through decomposition or
re-ordering of commuting gates.
Note that higher-level program structures, such as nested functions and control flow, are
synthesized independently. I.e., boundaries of such structures are always treated as boundaries
of phase polynomial subcircuits as well. Similarly, dynamic wires create boundaries around the
operations using them, potentially causing the separation of consecutive phase polynomial
operations into multiple subcircuits.
**Example**
In the following, we apply the pass to a simple quantum circuit that has optimization
potential in terms of commuting gates that can be interchanged to unlock a cancellation of
a self-inverse gate (``CNOT``) with itself. Concretely, the circuit is:
.. code-block:: python
import pennylane as qp
dev = qp.device("lightning.qubit", wires=2)
@qp.qjit(capture=True)
@qp.transforms.parity_synth
@qp.qnode(dev)
def circuit(x: float, y: float, z: float):
qp.CNOT((0, 1))
qp.RZ(x, 1)
qp.CNOT((0, 1))
qp.RX(y, 1)
qp.CNOT((1, 0))
qp.RZ(z, 1)
qp.CNOT((1, 0))
return qp.state()
We can draw the circuit and observe the last ``RZ`` gate to be wrapped in a pair of ``CNOT``
gates that commute with it. Before the pass is applied:
>>> fig, ax = catalyst.draw_graph(circuit, level=0)(0.52, 0.12, 0.2)
>>> fig.savefig('path-to-file.png', dpi=300, bbox_inches='tight')
.. figure:: /_static/parity-synth-example-before.png
:width: 35%
:alt: Example using ``parity_synth``
:align: left
After the pass is applied:
>>> fig, ax = catalyst.draw_graph(circuit, level=1)(0.52, 0.12, 0.2)
>>> fig.savefig('path-to-file.png', dpi=300, bbox_inches='tight')
.. figure:: /_static/parity-synth-example-pass-applied.png
:width: 35%
:alt: Example using ``parity_synth``
:align: left
"""
return (), {}
parity_synth = qp.transform(pass_name="parity-synth", setup_inputs=parity_synth_setup_inputs)
def decompose_lowering_setup_inputs(): # pragma: no cover
"""
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 decompose-lowering compiler pass to
Returns:
:class:`QNode <pennylane.QNode>`
**Example**
// TODO: add example here
"""
return (), {}
decompose_lowering = qp.transform(
pass_name="decompose-lowering", setup_inputs=decompose_lowering_setup_inputs
) # pragma: no cover
def ions_decomposition_setup_inputs(): # 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 qp
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
@qp.qnode(NullQubit(2))
def circuit():
qp.Hadamard(wires=[0])
qp.CNOT(wires=[0, 1])
return qp.expval(qp.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 (), {}
ions_decomposition = qp.transform(
pass_name="ions-decomposition", setup_inputs=ions_decomposition_setup_inputs
)
def combine_global_phases_setup_inputs():
r"""A quantum compilation pass that combines global phase instructions for each region in the
program.
Args:
qnode (QNode): the QNode to apply the compiler pass to
Returns:
:class:`QNode <pennylane.QNode>`
**Example**
Consider the following example:
.. code-block:: python
import pennylane as qp
import catalyst
from catalyst import qjit, for_loop
from catalyst.passes import combine_global_phases
from catalyst.debug import get_compilation_stage
n = 10
dev = qp.device('null.qubit', wires=n)
@qjit(keep_intermediate=True, capture=True)
@combine_global_phases
@qp.qnode(dev)
def circuit(n):
qp.GlobalPhase(0.1, wires = n-1)
qp.X(n-1)
qp.GlobalPhase(0.1, wires = n-2)
qp.H(n-2)
@qp.for_loop(0, n)
def loop(i):
qp.GlobalPhase(0.1967, wires=i)
qp.GlobalPhase(0.7691, wires=i)
loop()
qp.GlobalPhase(0.1, wires=n-3)
qp.GlobalPhase(0.1, wires=0)
return qp.expval(qp.Z(0))
>>> circuit(n)
0.0
The ``GlobalPhase`` operations surrounding control flow operations will be merged, while those
within the control flow are merged together separately (i.e., no formal loop-boundary
optimizations).
Example MLIR Representation:
>>> print(get_compilation_stage(circuit, stage="QuantumCompilationStage"))
.. code-block:: mlir
. . .
%extracted_4 = tensor.extract %3[] : tensor<i64>
%5 = quantum.extract %4[%extracted_4] : !quantum.reg -> !quantum.bit
%out_qubits = quantum.custom "PauliX"() %5 : !quantum.bit
%6 = stablehlo.subtract %arg0, %c_1 : tensor<i64>
%extracted_5 = tensor.extract %3[] : tensor<i64>
%7 = quantum.insert %4[%extracted_5], %out_qubits : !quantum.reg, !quantum.bit
%extracted_6 = tensor.extract %6[] : tensor<i64>
%8 = quantum.extract %7[%extracted_6] : !quantum.reg -> !quantum.bit
%9 = stablehlo.subtract %arg0, %c_1 : tensor<i64>
%extracted_7 = tensor.extract %6[] : tensor<i64>
%10 = quantum.insert %7[%extracted_7], %8 : !quantum.reg, !quantum.bit
%extracted_8 = tensor.extract %9[] : tensor<i64>
%11 = quantum.extract %10[%extracted_8] : !quantum.reg -> !quantum.bit
%out_qubits_9 = quantum.custom "Hadamard"() %11 : !quantum.bit
%extracted_10 = tensor.extract %9[] : tensor<i64>
%12 = quantum.insert %10[%extracted_10], %out_qubits_9 : !quantum.reg, !quantum.bit
%extracted_11 = tensor.extract %arg0[] : tensor<i64>
%13 = arith.index_cast %extracted_11 : i64 to index
%14 = scf.for %arg1 = %c0 to %13 step %c1 iter_args(%arg2 = %12) -> (!quantum.reg) {
%22 = arith.index_cast %arg1 : index to i64
%23 = quantum.extract %arg2[%22] : !quantum.reg -> !quantum.bit
quantum.gphase(%cst_0)
%24 = quantum.insert %arg2[%22], %23 : !quantum.reg, !quantum.bit
scf.yield %24 : !quantum.reg
}
%15 = stablehlo.subtract %arg0, %c : tensor<i64>
%extracted_12 = tensor.extract %15[] : tensor<i64>
%16 = quantum.extract %14[%extracted_12] : !quantum.reg -> !quantum.bit
%extracted_13 = tensor.extract %15[] : tensor<i64>
%17 = quantum.insert %14[%extracted_13], %16 : !quantum.reg, !quantum.bit
%18 = quantum.extract %17[ 0] : !quantum.reg -> !quantum.bit
quantum.gphase(%cst)
%19 = quantum.namedobs %18[ PauliZ] : !quantum.obs
%20 = quantum.expval %19 : f64
. . .
"""
return (), {}
combine_global_phases = qp.transform(
pass_name="combine-global-phases", setup_inputs=combine_global_phases_setup_inputs
)
def gridsynth_setup_inputs(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 qp
from catalyst import qjit
from catalyst.passes import gridsynth
pipe = [("pipe", ["quantum-compilation-stage"])]
@qjit(pipelines=pipe, target="mlir")
@gridsynth
@qp.qnode(qp.device("null.qubit", wires=1))
def circuit():
qp.RZ(x, wires=0)
return qp.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)
. . .
}
"""
return (), {"epsilon": epsilon, "ppr_basis": ppr_basis}
gridsynth = qp.transform(pass_name="gridsynth", setup_inputs=gridsynth_setup_inputs)
def to_ppr_setup_inputs():
r"""A quantum compilation pass that converts Clifford+T gates into Pauli-product rotations (PPR)
and measurements (PPM). Pauli-based operations are also allowed in the input.
.. 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).
Pauli gates are converted to :math:`\exp(-{iP\tfrac{\pi}{2}})`, Clifford gates to
:math:`\exp(-{iP\tfrac{\pi}{4}})`, and non-Clifford gates to
:math:`\exp(-{iP\tfrac{\pi}{8}})`, where :math:`P` is a Pauli word.
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
``qp.H``,
``qp.S``,
``qp.T``,
``qp.X``,
``qp.Y``,
``qp.Z``,
``qp.CNOT``,
``qp.CZ``,
``qp.RX``,
``qp.RY``,
``qp.RZ``,
``qp.IsingXX``,
``qp.IsingYY``,
``qp.IsingZZ``,
``qp.MultiRZ``,
``qp.PauliRot``,
and adjoint versions thereof, as well as
``qp.measure`` and
``qp.pauli_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 qp
@qp.qjit(capture=True)
@qp.transforms.to_ppr
@qp.qnode(qp.device("lightning.qubit", wires=2))
def circuit():
qp.H(0)
qp.CNOT([0, 1])
m = qp.measure(0)
qp.T(0)
return qp.expval(qp.Z(0))
>>> circuit()
Array(-1., dtype=float64)
>>> print(qp.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 (), {}
to_ppr = qp.transform(pass_name="to-ppr", setup_inputs=to_ppr_setup_inputs)
def commute_ppr_setup_inputs(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:`@qp.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 qp
import jax.numpy as jnp
@qp.qjit(capture=True)
@qp.transforms.commute_ppr(max_pauli_size=2)
@qp.transforms.to_ppr
@qp.qnode(qp.device("lightning.qubit", wires=2))
def circuit():
# equivalent to a Hadamard gate
qp.PauliRot(jnp.pi / 2, pauli_word="Z", wires=0)
qp.PauliRot(jnp.pi / 2, pauli_word="X", wires=0)
qp.PauliRot(jnp.pi / 2, pauli_word="Z", wires=0)
# equivalent to a CNOT gate
qp.PauliRot(jnp.pi / 2, pauli_word="ZX", wires=[0, 1])
qp.PauliRot(-jnp.pi / 2, pauli_word="Z", wires=0)
qp.PauliRot(-jnp.pi / 2, pauli_word="X", wires=1)
# equivalent to a T gate
qp.PauliRot(jnp.pi / 4, pauli_word="Z", wires=0)
return qp.expval(qp.Z(0))
>>> circuit()
Array(-1.11022302e-16, dtype=float64)
>>> print(qp.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.
"""
return (), {"max_pauli_size": max_pauli_size}
commute_ppr = qp.transform(pass_name="commute-ppr", setup_inputs=commute_ppr_setup_inputs)
def merge_ppr_ppm_setup_inputs(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:`@qp.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 qp
import jax.numpy as jnp
@qp.qjit(capture=True)
@qp.transforms.merge_ppr_ppm(max_pauli_size=2)
@qp.transforms.to_ppr
@qp.qnode(qp.device("lightning.qubit", wires=2))
def circuit():
qp.PauliRot(jnp.pi / 2, pauli_word="Z", wires=0)
qp.PauliRot(jnp.pi / 2, pauli_word="X", wires=1)
ppm = qp.pauli_measure(pauli_word="ZX", wires=[0, 1])
return qp.probs()
>>> circuit()
Array([0.5, 0.5, 0. , 0. ], dtype=float64)
>>> print(qp.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).
"""
return (), {"max_pauli_size": max_pauli_size}
merge_ppr_ppm = qp.transform(pass_name="merge-ppr-ppm", setup_inputs=merge_ppr_ppm_setup_inputs)
def ppr_to_ppm_setup_inputs(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:`@qp.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 qp
from functools import partial
import jax.numpy as jnp
@qp.qjit(capture=True)
@qp.transforms.ppr_to_ppm
@qp.transforms.to_ppr
@qp.qnode(qp.device("null.qubit", wires=2))
def circuit():
# equivalent to a Hadamard gate
qp.PauliRot(jnp.pi / 2, pauli_word="Z", wires=0)
qp.PauliRot(jnp.pi / 2, pauli_word="X", wires=0)
qp.PauliRot(jnp.pi / 2, pauli_word="Z", wires=0)
# equivalent to a CNOT gate
qp.PauliRot(jnp.pi / 2, pauli_word="ZX", wires=[0, 1])
qp.PauliRot(-jnp.pi / 2, pauli_word="Z", wires=[0])
qp.PauliRot(-jnp.pi / 2, pauli_word="X", wires=[1])
# equivalent to a T gate
qp.PauliRot(jnp.pi / 4, pauli_word="Z", wires=0)
return qp.expval(qp.Z(0))
>>> print(qp.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.
"""
return (), {"decompose_method": decompose_method, "avoid_y_measure": avoid_y_measure}
ppr_to_ppm = qp.transform(pass_name="ppr-to-ppm", setup_inputs=ppr_to_ppm_setup_inputs)
def ppm_compilation_setup_inputs(
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:`@qp.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 qp
@qp.qjit(capture=True)
@qp.transforms.ppm_compilation(decompose_method="clifford-corrected", max_pauli_size=2)
@qp.qnode(qp.device("null.qubit", wires=2))
def circuit():
qp.H(0)
qp.CNOT([0, 1])
qp.T(0)
return qp.expval(qp.Z(0))
>>> print(qp.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.
"""
return (), {
"decompose_method": decompose_method,
"avoid_y_measure": avoid_y_measure,
"max_pauli_size": max_pauli_size,
}
ppm_compilation = qp.transform(
pass_name="ppm-compilation", setup_inputs=ppm_compilation_setup_inputs
)
[docs]
def ppm_specs(fn, only_disjoint_qubit: bool = False):
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
- ``depth``: number of PBC layers
- ``depth_type``: ``0`` if commuting ops on overlapping qubits may share a layer;
``1`` if only ops with disjoint qubit support may share a layer
.. 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.
only_disjoint_qubit (bool): If ``True``, depth is computed using disjoint-qubit layering
(``depth_type`` 1). If ``False`` (default), commuting ops on overlapping qubits may
share a layer (``depth_type`` 0). Passed to the ``ppm-specs`` MLIR pass as
``disjoint-qubit``.
Returns:
dict: A Python dictionary containing PPM specs of all functions in ``fn``.
**Example**
.. code-block:: python
import pennylane as qp
import catalyst
from catalyst.passes import ppm_specs, to_ppr
@qp.qjit(target="mlir")
@to_ppr
@qp.qnode(qp.device("lightning.qubit", wires=2))
def f():
qp.H(0)
qp.S(1)
qp.T(0)
qp.CNOT(wires=[0, 1])
return qp.expval(qp.Z(0))
print(ppm_specs(f))
print(ppm_specs(f, only_disjoint_qubit=True))
Example output:
.. code-block:: pycon
{'f_0': {'depth': 4, 'depth_type': 0, 'logical_qubits': 2, 'max_weight_pi4': 2,
'max_weight_pi8': 1, 'pi4_ppr': 7, 'pi8_ppr': 1}}
{'f_0': {'depth': 7, 'depth_type': 1, 'logical_qubits': 2, 'max_weight_pi4': 2,
'max_weight_pi8': 1, 'pi4_ppr': 7, 'pi8_ppr': 1}}
"""
if fn.mlir_module is not None:
# aot mode: e.g: setting target="mlir", pipeline="..."
new_options = copy.copy(fn.compile_options)
if new_options.pipelines is None:
new_options.pipelines = [
(name, list(passes)) for name, passes in new_options.get_pipelines()
]
# 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
ppm_specs_pass = "ppm-specs{disjoint-qubit=true}" if only_disjoint_qubit else "ppm-specs"
pass_list[:] = [p for p in pass_list if not p.startswith("ppm-specs")] + [ppm_specs_pass]
new_options = _options_to_cli_flags(new_options)
# redirect output to devnull to avoid printing the MLIR
raw_result = _quantum_opt(*new_options, "-o", os.devnull, stdin=str(fn.mlir_module))
try:
return json.loads(raw_result) # validate before returning
except (json.JSONDecodeError, ValueError) as e: # pragma: nocover
raise CompileError(
"Invalid json format encountered in ppm_specs. "
f"Expected valid JSON but got {raw_result}"
) from e
else:
raise NotImplementedError("PPM passes only support AOT (Ahead-Of-Time) compilation mode.")
def reduce_t_depth_setup_inputs():
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:`@qp.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): the 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 qp
import jax.numpy as jnp
@qp.qjit(capture=True)
@qp.transforms.reduce_t_depth
@qp.transforms.to_ppr
@qp.qnode(qp.device("null.qubit", wires=4))
def circuit():
qp.PauliRot(jnp.pi / 4, pauli_word="Z", wires=1)
qp.PauliRot(-jnp.pi / 4, pauli_word="XYZ", wires=[0, 2, 3])
qp.PauliRot(-jnp.pi / 2, pauli_word="XYZY", wires=[0, 1, 2, 3])
qp.PauliRot(jnp.pi / 4, pauli_word="XZX", wires=[0, 1, 3])
qp.PauliRot(-jnp.pi / 4, pauli_word="XZY", wires=[0, 1, 2])
return qp.expval(qp.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 (), {}
reduce_t_depth = qp.transform(pass_name="reduce-t-depth", setup_inputs=reduce_t_depth_setup_inputs)
def ppr_to_mbqc_setup_inputs():
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): the 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 qp
import catalyst
p = [("my_pipe", ["quantum-compilation-stage"])]
@qp.qjit(pipelines=p, target="mlir", keep_intermediate=True)
@catalyst.passes.ppr_to_mbqc
@catalyst.passes.to_ppr
@qp.qnode(qp.device("null.qubit", wires=2))
def circuit():
qp.H(0)
qp.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 (), {}
ppr_to_mbqc = qp.transform(pass_name="ppr-to-mbqc", setup_inputs=ppr_to_mbqc_setup_inputs)
# This pass is already covered via applying by pass
# `qp.transform(pass_name="decompose-arbitrary-ppr")` in Pennylane.
def decompose_arbitrary_ppr_setup_inputs(): # 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:`@qp.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): the 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
(``qp.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 qp
@qp.qjit(capture=True)
@qp.transforms.decompose_arbitrary_ppr
@qp.transforms.to_ppr
@qp.qnode(qp.device("null.qubit", wires=3))
def circuit():
qp.PauliRot(0.1, pauli_word="XY", wires=[0, 1])
return qp.expval(qp.Z(0))
>>> print(qp.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 (), {}
decompose_arbitrary_ppr = qp.transform(
pass_name="decompose-arbitrary-ppr", setup_inputs=decompose_arbitrary_ppr_setup_inputs
)
def graph_decomposition_setup_inputs(
gate_set: Iterable[type | str] | dict[type | str, float],
fixed_decomps: dict | None = None,
alt_decomps: dict | None = None,
bytecode_rules: str | None = None,
_builtin_rule_path: str = BYTECODE_FILE_PATH,
libQPD_path: Path | None = None,
libpython_path: Path | None = None,
): # pylint: disable=unused-argument, too-many-arguments, too-many-positional-arguments
R"""
Specify that the ``-graph-decomposition`` MLIR compiler pass for applying the graph-based
decomposition should be applied to the decorated QNode during :func:`~.qjit` compilation.
The graph-based decomposition pass decomposes gates into a weighted target ``gate_set``
by applying user-provided and built-in decomposition rules. The graph-based framework
allows multiple decomposition rules to be defined for a quantum operation,
and the graph solver will determine the optimal decomposition rules to apply,
minimizing the overall gate count or the cost according to user-specified weights.
.. note::
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 graph decomposition compiler pass to.
gate_set (Iterable[type | str] | dict[type | str, float]): the set of gates that are
permissable after decomposition.
fixed_decomps (dict | None): map operators to specific decomposition rules that will be applied if the
operators need to be decomposed (i.e. when they're not
in the target gate set).
alt_decomps (dict | None): map operators to lists of decomposition rules that the graph system will consider as alternative rules for operators.
Returns:
~.QNode:
**Example**
.. code-block:: python
import pennylane as qp
import pennylane.numpy as np
from catalyst import qjit
from catalyst.jax_primitives import decomposition_rule
from catalyst.passes import cancel_inverses, graph_decomposition, merge_rotations
@decomposition_rule(op_type=qp.PauliX)
def x_to_rx(wire: int):
qp.RX(np.pi, wire)
@decomposition_rule(op_type=qp.PauliY)
def y_to_ry(wire: int):
qp.RY(np.pi, wire)
@decomposition_rule(op_type=qp.Hadamard)
def h_to_rx_ry(wire: int):
qp.RX(np.pi / 2, wire)
qp.RY(np.pi / 2, wire)
@qjit(capture=True)
@graph_decomposition(gate_set={qp.Rot})
@merge_rotations
@graph_decomposition(
gate_set={qp.RX, qp.RY},
fixed_decomps={qp.PauliX: x_to_rx, qp.PauliY: y_to_ry},
alt_decomps={qp.H: [h_to_rx_ry]},
)
@cancel_inverses
@qp.qnode(qp.device("lightning.qubit", wires=2))
def circuit(x: float, y: float):
qp.H(0)
qp.H(0)
qp.RX(x, wires=0)
qp.PauliX(0)
qp.RY(y, wires=0)
qp.PauliY(0)
qp.RY(x + y, wires=0)
# register custom decomposition rules
x_to_rx(int)
y_to_ry(int)
h_to_rx_ry(int)
return qp.state()
>>> qp.specs(circuit, level="device")(1.23, 4.56).resources.gate_types
{'Rot': 2}
"""
if not isinstance(gate_set, dict):
gate_set = {to_name(op): 1.0 for op in gate_set}
else:
gate_set = {to_name(op): float(cost) for op, cost in gate_set.items()}
options: dict[str, dict | tuple | str] = {
"gate_set": gate_set,
"bytecode_rules": _builtin_rule_path,
"libQPD_path": str(
libQPD_path if libQPD_path else get_lib_path("catalyst", "CATALYST_LIB_DIR")
),
"libpython_path": str(libpython_path if libpython_path else get_libpython_path()),
}
if fixed_decomps:
options |= {
"fixed_decomps": {
to_name(op): (rule if isinstance(rule, str) else rule.__name__)
for op, rule in fixed_decomps.items()
}
}
if alt_decomps:
options |= {
"alt_decomps": {
to_name(op): tuple(
(rule if isinstance(rule, str) else rule.__name__) for rule in rules
)
for op, rules in alt_decomps.items()
}
}
return (), options
graph_decomposition = qp.transform(
pass_name="graph-decomposition", setup_inputs=graph_decomposition_setup_inputs
)
__all__ = [
"cancel_inverses",
"combine_global_phases",
"disentangle_cnot",
"disentangle_swap",
"merge_rotations",
"parity_synth",
"decompose_lowering",
"ions_decomposition",
"to_ppr",
"gridsynth",
"commute_ppr",
"merge_ppr_ppm",
"ppr_to_ppm",
"ppm_compilation",
"reduce_t_depth",
"ppr_to_mbqc",
"decompose_arbitrary_ppr",
"graph_decomposition",
"diagonalize_measurements",
]