Skip to content

Implement a decomposition of PPRs into PPMs #1664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dd110d1
Add the `qec.prepare` operation for initializing the logical qubits.
sengthai Apr 16, 2025
907957d
- Introduced `SelectPPMeasurementOp` for conditional Pauli product me…
sengthai Apr 22, 2025
9de681b
Add convenience builder
sengthai Apr 22, 2025
a37ab39
Implement new pass that includes `auto-corrected` and `inject-magic-s…
sengthai Apr 23, 2025
dc78cfb
Added test case
sengthai Apr 23, 2025
57e1276
Refactor decomposition method
sengthai Apr 24, 2025
6aa10bf
Fixed memory buffer caused by using `ArrayRef`
sengthai Apr 24, 2025
0994d63
Added verifier to
sengthai Apr 24, 2025
3bd3292
Supported decomposition of
sengthai Apr 27, 2025
a26289b
Merge branch 'main' into ppr-to-ppm
sengthai Apr 27, 2025
44c7f90
Supported pass in frontend
sengthai Apr 28, 2025
d2c2e26
Add tests
sengthai Apr 29, 2025
2fb6a99
Separate decomposition of pi/4 and pi/8 PPR into two MLIR passes
sengthai Apr 29, 2025
d23eddd
codefactor
sengthai Apr 29, 2025
9b84432
Updated test in
sengthai Apr 30, 2025
914843a
Formated
sengthai Apr 30, 2025
a5a55bd
Added test case
sengthai Apr 30, 2025
055681c
Apply suggestions from code review
sengthai May 7, 2025
f663816
Update docstring
sengthai May 7, 2025
6191c9c
- Modified `state-prep` to `avoid-y-measure`
sengthai May 8, 2025
1b917e6
Update frontend/catalyst/passes/builtin_passes.py
sengthai May 8, 2025
a511f45
Added more info on
sengthai May 8, 2025
e3874c2
Removed function call in pytest
sengthai May 8, 2025
7521430
Update docstring for `ppr_to_ppm` function to clarify decomposition m…
sengthai May 8, 2025
3beb2f3
- Added `qec.fabricate` to QEC dialect
sengthai May 8, 2025
fb28104
Supported `qec.fabricate` in `decompose_non_clifford_ppr`
sengthai May 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

<h3>New features since last release</h3>

* A new compilation pass called :func:`~.passes.ppr_to_ppm` has been added to Catalyst to decompose Pauli product rotations (PPRs) into
Pauli product measurements (PPMs). Non-Clifford (pi/8) rotations require the consumption of a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Pauli product measurements (PPMs). Non-Clifford (pi/8) rotations require the consumption of a
Pauli product measurements (PPMs). Non-Clifford rotations, :math:`\exp(i\sigma_{x, y, y} \tfrac{\pi}{8})`, require the consumption of a

Copy link
Contributor

@dime10 dime10 May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x, y, z?

The expression is also missing an indication that the exponential argument is a product.

Copy link
Contributor

@dime10 dime10 May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that these rendered forms add that much, unless we say we have to define the term PPR mathematically whenever we use it. It's also harder to read unrendered.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should do something to clarify what the "pi/8" means, especially because, in PL, we take the convention of the factor of 1/2 being in the exponent already! I found that to be a pesky blocker when trying to roughly implement some things in the paper in PL.

Copy link
Contributor

@dime10 dime10 May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about defining PPR once when it is first mentioned? Then you can say Non-Clifford (Θ=pi/8) rotations assuming theta was the variable used in the definition

magic state, while Clifford rotations will not. The methods are implemented as described in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
magic state, while Clifford rotations will not. The methods are implemented as described in
magic state, while Clifford rotations, :math:`\exp(i\sigma_{x, y, y} \tfrac{\pi}{4})`, will not. The methods are implemented as described in

[arXiv:1808.02892](https://arxiv.org/abs/1808.02892v3).
[(#1664)](https://github.com/PennyLaneAI/catalyst/pull/1664)

The new compilation pass can be accessed in the frontend from the :mod:`~.passes` module:
* :func:`~.passes.ppr_to_ppm`: Decomposes all PPRs into PPMs (except Pauli rotations).

Or via the Catalyst CLI as two separate passes:
* `decompose_clifford_ppr`: Decompose pi/4 rotations into PPMs.
* `decompose_non_clifford_ppr`: Decompose pi/8 rotations into PPMs using a magic state.


<h3>Improvements 🛠</h3>

* The behaviour of measurement processes executed on `null.qubit` with QJIT is now more in line with
Expand Down
2 changes: 2 additions & 0 deletions frontend/catalyst/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
ions_decomposition,
merge_ppr_ppm,
merge_rotations,
ppr_to_ppm,
to_ppr,
)
from catalyst.passes.pass_api import Pass, PassPlugin, apply_pass, apply_pass_plugin
Expand All @@ -54,4 +55,5 @@
"apply_pass",
"apply_pass_plugin",
"merge_ppr_ppm",
"ppr_to_ppm",
)
80 changes: 80 additions & 0 deletions frontend/catalyst/passes/builtin_passes.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,83 @@ def circuit():

"""
return PassPipelineWrapper(qnode, "merge_ppr_ppm")


def ppr_to_ppm(qnode=None, decompose_method="auto-corrected", avoid_y_measure=False):
R"""Specify that the MLIR compiler passes for decomposing Pauli Product rotations (PPR)
into Pauli Pauli measurements (PPM) will be applied.

This pass is used to decompose both non-Clifford and Clifford PPRs into PPMs. The non-Clifford
PPRs (pi/8) are decomposed first, and then Clifford PPRs (pi/4) are decomposed. Non-Clifford
decomposition can be performed in one of two ways: "clifford-corrected" or "auto-corrected",
Comment on lines +509 to +510
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See changelog entry suggestion. I'd do the same here!

by default the latter is used. Both methods are based on
`A Game of Surface Codes <https://arxiv.org/abs/1808.02892>`, figures 7 and 17(b) respectively.

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 "auto-corrected" and "clifford-corrected". Defaults to "auto-corrected".
"auto-corrected" uses an additional measurement for correction.
"clifford-corrected" uses a Clifford rotation for correction.
avoid_y_measure (bool): Rather than performing a Pauli-Y measurement for Clifford rotations
(sometimes more costly), a :math:`Y` state (:math:`Y\vert 0 \rangle`) is used instead
(requires :math:`Y` state preparation). Defaults to ``False``.

Returns:
~.QNode or callable: Returns decorated QNode if qnode is provided,
otherwise returns a decorator.

**Example**

This example shows the sequence of passes that will be applied. The last pass
will convert the non-Clifford PPR into Pauli Product Measurements.

.. code-block:: python

import pennylane as qml
from catalyst import qjit, measure
from catalyst.passes import to_ppr, commute_ppr, merge_ppr_ppm, ppr_to_ppm

pipeline = [("pipe", ["enforce-runtime-invariants-pipeline"])]

@qjit(pipelines=pipeline, target="mlir")
@to_ppr
@commute_ppr
@merge_ppr_ppm
@ppr_to_ppm
@qml.qnode(qml.device("null.qubit", wires=2))
def circuit():
qml.H(0)
qml.T(0)
qml.CNOT([0, 1])
return measure(0), measure(1)

print(circuit.mlir_opt)

Example MLIR Representation:

.. code-block:: mlir

. . .
%5 = qec.fabricate zero : !quantum.bit
%6 = qec.fabricate magic : !quantum.bit
%mres, %out_qubits:2 = qec.ppm ["X", "Z"] %1, %6 : !quantum.bit, !quantum.bit
%mres_0, %out_qubits_1:2 = qec.ppm ["Z", "Y"] %5, %out_qubits#1 : !quantum.bit, !quantum.bit
%mres_2, %out_qubits_3 = qec.ppm ["X"] %out_qubits_1#1 : !quantum.bit
%mres_4, %out_qubits_5 = qec.select.ppm(%mres, ["X"], ["Z"]) %out_qubits_1#0 : !quantum.bit
%7 = arith.xori %mres_0, %mres_2 : i1
%8 = qec.ppr ["X"](2) %out_qubits#0 cond(%7) : !quantum.bit
. . .

"""
passes = {
"decompose_non_clifford_ppr": {"decompose-method": decompose_method},
"decompose_clifford_ppr": {"avoid-y-measure": avoid_y_measure},
}

def decorator(qnode_func):
return PassPipelineWrapper(qnode_func, passes)

if qnode is None:
return decorator
return decorator(qnode)
1 change: 1 addition & 0 deletions frontend/catalyst/passes/pass_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,4 +377,5 @@ def _API_name_to_pass_name():
"to_ppr": "to_ppr",
"commute_ppr": "commute_ppr",
"merge_ppr_ppm": "merge_ppr_ppm",
"ppr_to_ppm": "ppr_to_ppm",
}
161 changes: 156 additions & 5 deletions frontend/test/pytest/test_peephole_optimizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@
import pennylane as qml
import pytest

from catalyst import pipeline, qjit
from catalyst.passes import cancel_inverses, merge_rotations, to_ppr
from catalyst import measure, pipeline, qjit
from catalyst.passes import (
cancel_inverses,
commute_ppr,
merge_ppr_ppm,
merge_rotations,
ppr_to_ppm,
to_ppr,
)

# pylint: disable=missing-function-docstring

Expand Down Expand Up @@ -189,8 +196,6 @@ def test_chained_apply_passes_workflow(x: float):
assert "merge-rotations" in test_chained_apply_passes_workflow.mlir


test_chained_passes()

#
# to_ppr
#
Expand Down Expand Up @@ -221,7 +226,153 @@ def f():
assert "qec.ppr" in optimized_ir


test_convert_clifford_to_ppr()
#
# commute_ppr
#
Comment on lines +229 to +231
Copy link
Contributor

@dime10 dime10 May 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here as well I would avoid these comments, you should use docstrings to document individual tests, and classes to group similar tests together. That's how we organize all our other pytests as well :)

In this case the headers don't really do much anyway since they just repeat the name of the test function.



def test_commute_ppr():
"""
Test commute_ppr
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good docstring will say what you are testing and possibly additional information like what you are expecting (if that isn't obvious).

"""
pipe = [("pipe", ["enforce-runtime-invariants-pipeline"])]

@qjit(pipelines=pipe, target="mlir")
def test_commute_ppr_workflow():

@to_ppr
@commute_ppr
@qml.qnode(qml.device("lightning.qubit", wires=2))
def f():
qml.H(0)
qml.S(1)
qml.T(0)
qml.CNOT([0, 1])
return measure(0), measure(1)

return f()

assert 'transform.apply_registered_pass "commute_ppr"' in test_commute_ppr_workflow.mlir
optimized_ir = test_commute_ppr_workflow.mlir_opt
assert 'transform.apply_registered_pass "commute_ppr"' not in optimized_ir
assert "qec.ppr" in optimized_ir
assert "qec.ppm" in optimized_ir


#
# merge_ppr_ppm
#


def test_merge_ppr_ppm():
"""
Test merge_ppr_ppm
"""
pipe = [("pipe", ["enforce-runtime-invariants-pipeline"])]

@qjit(pipelines=pipe, target="mlir")
def test_merge_ppr_ppm_workflow():

@to_ppr
@merge_ppr_ppm
@qml.qnode(qml.device("lightning.qubit", wires=2))
def f():
qml.H(0)
qml.S(1)
qml.CNOT([0, 1])
return measure(0), measure(1)

return f()

assert 'transform.apply_registered_pass "merge_ppr_ppm"' in test_merge_ppr_ppm_workflow.mlir
optimized_ir = test_merge_ppr_ppm_workflow.mlir_opt
assert 'transform.apply_registered_pass "merge_ppr_ppm"' not in optimized_ir
assert 'qec.ppm ["Z", "X"]' in optimized_ir
assert 'qec.ppm ["X"]' in optimized_ir


#
# ppr_to_ppm
#


def test_ppr_to_ppm():
"""
Test ppr_to_ppm
"""
pipe = [("pipe", ["enforce-runtime-invariants-pipeline"])]

@qjit(pipelines=pipe, target="mlir")
def test_ppr_to_ppm_workflow():

@to_ppr
@ppr_to_ppm
@qml.qnode(qml.device("lightning.qubit", wires=2))
def f():
qml.H(0)
qml.S(1)
qml.T(0)
qml.CNOT([0, 1])
return measure(0), measure(1)

return f()

assert (
'transform.apply_registered_pass "decompose_non_clifford_ppr"'
in test_ppr_to_ppm_workflow.mlir
)
assert (
'transform.apply_registered_pass "decompose_clifford_ppr"' in test_ppr_to_ppm_workflow.mlir
)
optimized_ir = test_ppr_to_ppm_workflow.mlir_opt
assert 'transform.apply_registered_pass "decompose_non_clifford_ppr"' not in optimized_ir
assert 'transform.apply_registered_pass "decompose_clifford_ppr"' not in optimized_ir
assert "qec.fabricate zero" in optimized_ir
assert "qec.fabricate magic" in optimized_ir
assert "qec.select.ppm" in optimized_ir
assert 'qec.ppr ["X"]' in optimized_ir


#
# ppr_to_ppm with clifford-corrected
#


def test_ppr_to_ppm_inject_magic_state():
"""
Test ppr_to_ppm
"""
pipe = [("pipe", ["enforce-runtime-invariants-pipeline"])]

@qjit(pipelines=pipe, target="mlir")
def test_ppr_to_ppm_workflow():

@to_ppr
@ppr_to_ppm(decompose_method="clifford-corrected", avoid_y_measure=True)
@qml.qnode(qml.device("lightning.qubit", wires=2))
def f():
qml.H(0)
qml.S(1)
qml.T(0)
qml.CNOT([0, 1])
return measure(0), measure(1)

return f()

assert (
'transform.apply_registered_pass "decompose_non_clifford_ppr"'
in test_ppr_to_ppm_workflow.mlir
)
assert (
'transform.apply_registered_pass "decompose_clifford_ppr"' in test_ppr_to_ppm_workflow.mlir
)
optimized_ir = test_ppr_to_ppm_workflow.mlir_opt
assert 'transform.apply_registered_pass "decompose_non_clifford_ppr"' not in optimized_ir
assert 'transform.apply_registered_pass "decompose_clifford_ppr"' not in optimized_ir
assert "qec.fabricate magic" in optimized_ir
assert "qec.fabricate plus_i" in optimized_ir
assert 'qec.ppm ["X", "Z"]' in optimized_ir
assert 'qec.ppr ["X"]' in optimized_ir


if __name__ == "__main__":
Expand Down
7 changes: 7 additions & 0 deletions mlir/include/QEC/IR/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@ add_mlir_dialect(QECDialect qec)
add_mlir_interface(QECOpInterfaces)
add_mlir_doc(QECDialect QECDialect QEC/ -gen-dialect-doc -gen-op-doc)
add_mlir_doc(QECOpInterfaces QECOpInterfaces QEC/ -gen-op-interface-docs)

set(LLVM_TARGET_DEFINITIONS QECDialect.td)
mlir_tablegen(QECEnums.h.inc -gen-enum-decls)
mlir_tablegen(QECEnums.cpp.inc -gen-enum-defs)
mlir_tablegen(QECAttributes.h.inc -gen-attrdef-decls)
mlir_tablegen(QECAttributes.cpp.inc -gen-attrdef-defs)
add_public_tablegen_target(MLIRQECEnumsIncGen)
13 changes: 13 additions & 0 deletions mlir/include/QEC/IR/QECDialect.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@
#define GET_TYPEDEF_CLASSES
#include "QEC/IR/QECDialectTypes.h.inc"

//===----------------------------------------------------------------------===//
// QEC enum definitions.
//===----------------------------------------------------------------------===//

#include "QEC/IR/QECEnums.h.inc"

//===----------------------------------------------------------------------===//
// QEC attribute declarations.
//===----------------------------------------------------------------------===//

#define GET_ATTRDEF_CLASSES
#include "QEC/IR/QECAttributes.h.inc"

//===----------------------------------------------------------------------===//
// QEC ops declarations.
//===----------------------------------------------------------------------===//
Expand Down
Loading