diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index ef762e4b1d..09750a5557 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -556,6 +556,9 @@
* Added ``CZ`` support to ``to-ppr`` pass.
[(#3009)](https://github.com/PennyLaneAI/catalyst/pull/3009)
+* ``to_ppr`` now directly lowers PennyLane's discrete ``PPR`` operator to ``pbc.ppr``.
+ [(#3185)](https://github.com/PennyLaneAI/catalyst/pull/3185)
+
Breaking changes 💔
* Removes :func:`~.passes.ppm_specs` and the ``--ppm-specs`` MLIR pass. Use :func:`~.specs` and
diff --git a/frontend/catalyst/passes/builtin_passes.py b/frontend/catalyst/passes/builtin_passes.py
index a43d754140..e9ba3f5bdb 100644
--- a/frontend/catalyst/passes/builtin_passes.py
+++ b/frontend/catalyst/passes/builtin_passes.py
@@ -945,6 +945,7 @@ def to_ppr_setup_inputs():
``qp.IsingZZ``,
``qp.MultiRZ``,
``qp.PauliRot``,
+ ``qp.PPR``,
and adjoint versions thereof, as well as
``qp.measure`` and
``qp.pauli_measure``.
@@ -965,6 +966,9 @@ def to_ppr_setup_inputs():
For better compatibility with other PennyLane functionality, ensure that PennyLane program
capture is enabled with ``@qjit(capture=True)``.
+ Note that the angle convention of ``qp.PauliRot`` differs from Catalyst's angle convention
+ for PPRs by a factor of two, whereas ``qp.PPR`` follows Catalyst's convention.
+
**Example**
The ``to_ppr`` compilation pass can be applied as a decorator on a QNode:
@@ -1060,7 +1064,6 @@ def commute_ppr_setup_inputs(max_pauli_size=0):
.. code-block:: python
import pennylane as qp
- import jax.numpy as jnp
@qp.qjit(capture=True)
@qp.transforms.commute_ppr(max_pauli_size=2)
@@ -1069,17 +1072,17 @@ def commute_ppr_setup_inputs(max_pauli_size=0):
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)
+ qp.PPR(4, pauli_word="Z", wires=0)
+ qp.PPR(4, pauli_word="X", wires=0)
+ qp.PPR(4, 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)
+ qp.PPR(4, pauli_word="ZX", wires=[0, 1])
+ qp.PPR(-4, pauli_word="Z", wires=0)
+ qp.PPR(-4, pauli_word="X", wires=1)
# equivalent to a T gate
- qp.PauliRot(jnp.pi / 4, pauli_word="Z", wires=0)
+ qp.PPR(8, pauli_word="Z", wires=0)
return qp.expval(qp.Z(0))
@@ -1101,9 +1104,9 @@ def circuit():
- 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,
+ In the example above, the Clifford PPRs (:class:`~.PPR` instances with an angle denominator
+ of :math:`\pm 4`) will be commuted past the non-Clifford PPR (:class:`~.PPR`
+ instances with an angle denominator :math:`\pm 8`). In the above output,
``PPR-theta-w`` denotes the type of PPR present in the circuit, where ``theta`` is the PPR
angle (:math:`\theta`) and ``w`` denotes the PPR weight (the number of qubits it acts on,
or the length of the Pauli word).
@@ -1165,15 +1168,14 @@ def merge_ppr_ppm_setup_inputs(max_pauli_size=0):
.. 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)
+ qp.PPR(4, pauli_word="Z", wires=0)
+ qp.PPR(4, pauli_word="X", wires=1)
ppm = qp.pauli_measure(pauli_word="ZX", wires=[0, 1])
@@ -1265,7 +1267,6 @@ def ppr_to_ppm_setup_inputs(decompose_method="pauli-corrected", avoid_y_measure=
import pennylane as qp
from functools import partial
- import jax.numpy as jnp
@qp.qjit(capture=True)
@qp.transforms.ppr_to_ppm
@@ -1273,17 +1274,17 @@ def ppr_to_ppm_setup_inputs(decompose_method="pauli-corrected", avoid_y_measure=
@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)
+ qp.PPR(4, pauli_word="Z", wires=0)
+ qp.PPR(4, pauli_word="X", wires=0)
+ qp.PPR(4, 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])
+ qp.PPR(4, pauli_word="ZX", wires=[0, 1])
+ qp.PPR(-4, pauli_word="Z", wires=[0])
+ qp.PPR(-4, pauli_word="X", wires=[1])
# equivalent to a T gate
- qp.PauliRot(jnp.pi / 4, pauli_word="Z", wires=0)
+ qp.PPR(8, pauli_word="Z", wires=0)
return qp.expval(qp.Z(0))
@@ -1487,18 +1488,17 @@ def reduce_t_depth_setup_inputs():
.. 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])
+ qp.PPR(8, pauli_word="Z", wires=1)
+ qp.PPR(-8, pauli_word="XYZ", wires=[0, 2, 3])
+ qp.PPR(-4, pauli_word="XYZY", wires=[0, 1, 2, 3])
+ qp.PPR(8, pauli_word="XZX", wires=[0, 1, 3])
+ qp.PPR(-8, pauli_word="XZY", wires=[0, 1, 2])
return qp.expval(qp.Z(0))
diff --git a/frontend/test/pytest/test_pauli_rot_and_measure.py b/frontend/test/pytest/test_pauli_rot_and_measure.py
index 4bef13757d..c1ae9934c6 100644
--- a/frontend/test/pytest/test_pauli_rot_and_measure.py
+++ b/frontend/test/pytest/test_pauli_rot_and_measure.py
@@ -78,6 +78,44 @@ def f():
assert "pbc.ppr" in optimized_ir
+def test_ppr_operator_capture():
+ """Test that PPR remains a generic operator before applying to_ppr."""
+ pipe = [("pipe", ["quantum-compilation-stage"])]
+
+ @qjit(pipelines=pipe, target="mlir", capture=True)
+ def test_ppr_operator_capture_workflow():
+
+ @qp.qnode(qp.device("null.qubit", wires=2))
+ def f():
+ qp.PPR(4, "XY", wires=[0, 1])
+
+ return f()
+
+ optimized_ir = test_ppr_operator_capture_workflow.mlir_opt
+ assert 'quantum.operator "PPR"' in optimized_ir
+ assert "pbc.ppr" not in optimized_ir
+
+
+def test_ppr_operator_to_ppr():
+ """Test that to_ppr converts a PPR operator to pbc.ppr."""
+ pipe = [("pipe", ["quantum-compilation-stage"])]
+
+ @qjit(pipelines=pipe, target="mlir", capture=True)
+ @to_ppr
+ def test_ppr_operator_to_ppr_workflow():
+
+ @qp.qnode(qp.device("null.qubit", wires=2))
+ def f():
+ qp.PPR(4, "XY", wires=[0, 1])
+
+ return f()
+
+ optimized_ir = test_ppr_operator_to_ppr_workflow.mlir_opt
+ assert 'pbc.ppr ["X", "Y"](4)' in optimized_ir
+ assert 'quantum.operator "PPR"' not in optimized_ir
+ assert "quantum.paulirot" not in optimized_ir
+
+
def test_pauli_rot_with_arbitrary_angle_to_ppr():
"""Test that Pauli rotation for arbitrary angle."""
pipe = [("pipe", ["quantum-compilation-stage"])]
diff --git a/mlir/lib/PBC/Transforms/ToPPR.cpp b/mlir/lib/PBC/Transforms/ToPPR.cpp
index 02f4a20a1f..597960bf84 100644
--- a/mlir/lib/PBC/Transforms/ToPPR.cpp
+++ b/mlir/lib/PBC/Transforms/ToPPR.cpp
@@ -411,6 +411,31 @@ LogicalResult convertPauliRotGate(PauliRotOp op, ConversionPatternRewriter &rewr
op.getAdjoint(), rewriter);
}
+LogicalResult convertPPROperator(OperatorOp op, ConversionPatternRewriter &rewriter) {
+ assert(op.getAllParams().empty() && "PPR operator does not support dynamic parameters");
+
+ DictionaryAttr staticData = op.getStaticData();
+ auto pauliWordAttr = staticData.getAs("pauli_word");
+ StringRef pauliWord = pauliWordAttr.getValue();
+ SmallVector pauliCharacters;
+ pauliCharacters.reserve(pauliWord.size());
+ for (char pauli : pauliWord) {
+ pauliCharacters.push_back(rewriter.getStringAttr(StringRef(&pauli, 1)));
+ }
+ ArrayAttr pauliProduct = rewriter.getArrayAttr(pauliCharacters);
+
+ auto denominatorAttr = staticData.getAs("angle_denominator");
+ int8_t rotationKind = static_cast(denominatorAttr.getInt());
+ if (op.getAdjoint()) {
+ rotationKind = -rotationKind;
+ }
+
+ auto pprOp =
+ PPRotationOp::create(rewriter, op.getLoc(), pauliProduct, rotationKind, op.getInQubits());
+ rewriter.replaceOp(op, pprOp.getOutQubits());
+ return success();
+}
+
//===----------------------------------------------------------------------===//
// PBC Lowering Patterns
//===----------------------------------------------------------------------===//
@@ -420,7 +445,7 @@ struct PBCGateLowering : public OpInterfaceConversionPattern {
LogicalResult matchAndRewrite(QuantumOperation operation, ArrayRef operands,
ConversionPatternRewriter &rewriter) const final {
- StringRef supportedGates = "Supported gates: H, S, T, X, Y, Z, S†, T†, I, CNOT, CZ, "
+ StringRef supportedGates = "Supported gates: H, S, T, X, Y, Z, S†, T†, I, CNOT, CZ, PPR,"
"RX, RY, RZ, IsingXX, IsingYY, IsingZZ, MultiRZ, and PauliRot.";
Operation *op = operation.getOperation();
@@ -469,6 +494,10 @@ struct PBCGateLowering : public OpInterfaceConversionPattern {
return convertMultiRZGate(originOp, rewriter);
} else if (auto originOp = dyn_cast(op)) {
return convertPauliRotGate(originOp, rewriter);
+ } else if (auto originOp = dyn_cast(op)) {
+ if (originOp.getOpName() == "PPR") {
+ return convertPPROperator(originOp, rewriter);
+ }
}
return op->emitError("Unsupported operation for PBC conversion. " + supportedGates);
diff --git a/mlir/test/PBC/ToPPRTest.mlir b/mlir/test/PBC/ToPPRTest.mlir
index c1d0f5a48b..5383e7f691 100644
--- a/mlir/test/PBC/ToPPRTest.mlir
+++ b/mlir/test/PBC/ToPPRTest.mlir
@@ -293,3 +293,33 @@ func.func private @"some_decomp_rule"(%arg0: tensor<1xf64>, %arg1: tensor<1xi64>
%3 = quantum.insert %arg2[%extracted], %out_qubits : !quantum.reg, !quantum.bit
return %3 : !quantum.reg
}
+
+// -----
+
+// CHECK-LABEL: func.func @test_ppr_operator_to_ppr
+func.func @test_ppr_operator_to_ppr(%q0 : !quantum.bit, %q1 : !quantum.bit) {
+ // CHECK-NOT: quantum.operator
+ // CHECK: [[out:%.+]]:2 = pbc.ppr ["X", "Y"](4) [[q0:%.+]], [[q1:%.+]]
+ %0:2 = quantum.operator "PPR"() qubits(%q0, %q1)
+ static_data = {angle_denominator = 4 : i64, pauli_word = "XY"}
+ // CHECK-NOT: quantum.operator
+ // CHECK: return
+ func.return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_negative_and_adjoint_ppr_operator
+func.func @test_negative_and_adjoint_ppr_operator(%q0 : !quantum.bit) {
+ // CHECK-NOT: quantum.operator
+ // CHECK: [[q0_0:%.+]] = pbc.ppr ["Z"](-2) [[q0:%.+]]
+ %0 = quantum.operator "PPR"() qubits(%q0)
+ static_data = {angle_denominator = -2 : i64, pauli_word = "Z"}
+ // The second PPR consumes the first PPR's output, not the original input qubit.
+ // CHECK: pbc.ppr ["X"](-8) [[q0_0]]
+ %1 = quantum.operator "PPR"() adj qubits(%0)
+ static_data = {angle_denominator = 8 : i64, pauli_word = "X"}
+ // CHECK-NOT: quantum.operator
+ // CHECK: return
+ func.return
+}