From 4b5d10f8d261ce977e379ae54c709b958571e3c8 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 3 Sep 2026 16:54:25 +0200 Subject: [PATCH 01/27] docs: design PennyLane PPR lowering Co-authored-by: Cursor --- .../specs/2026-09-03-ppr-lowering-design.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-03-ppr-lowering-design.md diff --git a/docs/superpowers/specs/2026-09-03-ppr-lowering-design.md b/docs/superpowers/specs/2026-09-03-ppr-lowering-design.md new file mode 100644 index 0000000000..314a80ccd9 --- /dev/null +++ b/docs/superpowers/specs/2026-09-03-ppr-lowering-design.md @@ -0,0 +1,57 @@ +# PennyLane PPR Lowering + +## Goal + +Support PennyLane's discrete `PPR` Operator2 in Catalyst's `to_ppr` pass without first +decomposing it through `PauliRot`. + +## Representation and lowering + +PennyLane captures `PPR` as a generic `qref.operator` because both +`angle_denominator` and `pauli_word` are compilable static arguments. Value-semantics conversion +preserves this as `quantum.operator "PPR"` with both values in `static_data`. + +The `to_ppr` conversion will recognize that operator, validate its expected shape, and emit one +native `pbc.ppr` operation. If PennyLane's angle denominator is `k`, the PBC rotation kind is +`2 * k`: PennyLane uses the `PauliRot` angle convention +`exp(-i (pi / k) P / 2)`, while `pbc.ppr` represents `exp(-i pi P / rotation_kind)`. +An adjoint negates the resulting rotation kind. + +The conversion will reject controlled PPRs consistently with the other operations accepted by +`to_ppr`. Invalid or missing static data will produce a compilation diagnostic rather than +silently decomposing or mis-lowering the operation. + +This design does not introduce dedicated QRef or Quantum dialect PPR operations. The generic +operator already preserves all required compile-time data, and the PBC dialect remains the +canonical discrete-PPR representation after `to_ppr`. + +## Decomposition preservation + +Add `PPR` to Catalyst's runtime-supported operation set. This keeps device preprocessing from +decomposing the operator to its PennyLane `PauliRot` decomposition before the registered +`to_ppr` pass runs. + +No decomposition rule will be added for PPR in Catalyst. Existing graph-decomposition behavior +continues to preserve any operator explicitly included in its target gate set. + +## User-facing documentation + +Add `qp.PPR` to the `to_ppr` supported-operations list. The dependency pin remains unchanged; +the implementation targets the API from PennyLane PR #10107. + +## Tests + +1. Add MLIR tests for direct conversion of positive and negative PPR denominators, multi-qubit + Pauli words, and adjoints. +2. Add frontend coverage proving that `qp.PPR` remains a `quantum.operator "PPR"` without + `to_ppr`, and becomes one `pbc.ppr` with `to_ppr`. +3. Add preprocessing coverage proving PPR is in Catalyst's supported operation set and therefore + is not decomposed before lowering. +4. Keep existing PauliRot conversion tests unchanged to guard the separate arbitrary-angle path. + +## Non-goals + +- Executing a raw PennyLane `PPR` without a PBC-lowering pass. +- Supporting controlled PPRs in `to_ppr`. +- Updating Catalyst's PennyLane dependency pin. +- Adding a new QRef or Quantum dialect operation solely for PPR. From 0b3833e68d743fe8c2b8bd85ed2ab767919716de Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 3 Sep 2026 17:00:11 +0200 Subject: [PATCH 02/27] docs: plan PennyLane PPR lowering Co-authored-by: Cursor --- .../plans/2026-09-03-ppr-lowering.md | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-03-ppr-lowering.md diff --git a/docs/superpowers/plans/2026-09-03-ppr-lowering.md b/docs/superpowers/plans/2026-09-03-ppr-lowering.md new file mode 100644 index 0000000000..f09462d47c --- /dev/null +++ b/docs/superpowers/plans/2026-09-03-ppr-lowering.md @@ -0,0 +1,302 @@ +# PennyLane PPR Lowering Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Lower PennyLane's fixed-angle `PPR` Operator2 directly to Catalyst `pbc.ppr` without an intermediate `PauliRot` decomposition. + +**Architecture:** Keep `PPR` in the existing generic `qref.operator`/`quantum.operator` representation until the registered `to-ppr` pass. Add one conversion branch that reads the two static arguments and emits the canonical PBC operation, and mark `PPR` as runtime-supported so PennyLane device preprocessing preserves it. + +**Tech Stack:** Python 3, PennyLane Operator2 capture, Catalyst QRef/Quantum/PBC MLIR dialects, C++17 MLIR conversion patterns, LLVM lit/FileCheck, pytest. + +## Global Constraints + +- PennyLane `PPR(k, P)` uses `exp(-i (pi / k) P / 2)`; Catalyst `pbc.ppr(P)(r)` uses `exp(-i pi P / r)`, so `r = 2 * k`. +- Allowed PennyLane denominators are exactly `-4`, `-2`, `-1`, `1`, `2`, and `4`. +- An adjoint negates the emitted PBC rotation kind. +- Controlled PPR remains unsupported by `to_ppr`. +- Do not add dedicated QRef or Quantum PPR operations. +- Do not update Catalyst's PennyLane dependency pin. + +--- + +### Task 1: Convert generic quantum PPR operators in `to-ppr` + +**Files:** +- Modify: `mlir/test/PBC/ToPPRTest.mlir` +- Modify: `mlir/lib/PBC/Transforms/ToPPR.cpp` + +**Interfaces:** +- Consumes: `quantum.operator "PPR"()` in qubit mode with `static_data = {angle_denominator = , pauli_word = ""}`. +- Produces: one `pbc.ppr` with the same qubits and Pauli word and rotation kind `2 * angle_denominator`, negated when the source operator has `adj`. + +- [ ] **Step 1: Add failing direct-lowering tests** + +Append split-input tests covering positive, negative, multi-qubit, and adjoint cases: + +```mlir +// ----- + +func.func @test_ppr_operator_to_ppr(%q0 : !quantum.bit, %q1 : !quantum.bit) { + %0:2 = quantum.operator "PPR"() qubits(%q0, %q1) + static_data = {angle_denominator = 4 : i64, pauli_word = "XY"} + // CHECK: pbc.ppr ["X", "Y"](8) + func.return +} + +// ----- + +func.func @test_negative_and_adjoint_ppr_operator(%q0 : !quantum.bit) { + %0 = quantum.operator "PPR"() qubits(%q0) + static_data = {angle_denominator = -2 : i64, pauli_word = "Z"} + %1 = quantum.operator "PPR"() adj qubits(%0) + static_data = {angle_denominator = 1 : i64, pauli_word = "X"} + // CHECK: pbc.ppr ["Z"](-4) + // CHECK: pbc.ppr ["X"](-2) + func.return +} +``` + +- [ ] **Step 2: Run the focused MLIR test and verify RED** + +Run: + +```bash +lit mlir/test/PBC/ToPPRTest.mlir -v +``` + +Expected: FAIL because `quantum.operator "PPR"` is explicitly illegal and `PBCGateLowering` reports it as unsupported. + +- [ ] **Step 3: Implement minimal PPR conversion** + +In `mlir/lib/PBC/Transforms/ToPPR.cpp`, add a helper for `OperatorOp` that: + +```cpp +LogicalResult convertPPROperator(OperatorOp op, ConversionPatternRewriter &rewriter) +``` + +The helper must: + +1. Read `angle_denominator` as `IntegerAttr` and `pauli_word` as `StringAttr` from `op.getStaticData()`. +2. Require qubit mode, no dynamic parameters, a non-empty Pauli word, one Pauli character per input qubit, only `X`, `Y`, or `Z`, and an allowed denominator. +3. Build an `ArrayAttr` containing one-character string attributes. +4. Compute `int8_t rotationKind = 2 * denominator`, negating it for `op.getAdjoint()`. +5. Create `PPRotationOp` and replace the generic operator results. + +Extend `PBCGateLowering::matchAndRewrite` with an `OperatorOp` branch that calls the helper only when `getOpName() == "PPR"`. Update the supported-operation diagnostic to include `PPR`. + +- [ ] **Step 4: Add malformed-input diagnostics** + +Add split-input tests with `expected-error` checks for: + +```mlir +quantum.operator "PPR"() qubits(%q) + static_data = {pauli_word = "X"} +``` + +and: + +```mlir +quantum.operator "PPR"() qubits(%q) + static_data = {angle_denominator = 3 : i64, pauli_word = "X"} +``` + +Expected diagnostics must identify the missing `angle_denominator` and the unsupported denominator respectively. + +- [ ] **Step 5: Run the focused MLIR test and verify GREEN** + +Run: + +```bash +lit mlir/test/PBC/ToPPRTest.mlir -v +``` + +Expected: PASS with all `quantum.operator "PPR"` instances converted or diagnosed as expected. + +- [ ] **Step 6: Commit the conversion** + +```bash +git add mlir/lib/PBC/Transforms/ToPPR.cpp mlir/test/PBC/ToPPRTest.mlir +git commit -m "feat: lower PennyLane PPR in to-ppr" +``` + +--- + +### Task 2: Preserve PPR through device preprocessing + +**Files:** +- Modify: `frontend/test/pytest/test_verification.py` +- Modify: `frontend/catalyst/device/qjit_device.py` + +**Interfaces:** +- Consumes: PennyLane target-device capabilities containing an operation named `PPR`. +- Produces: QJIT capabilities that retain `PPR`, allowing PennyLane preprocessing to leave the operation intact for `to-ppr`. + +- [ ] **Step 1: Add a failing capability-intersection test** + +Use the existing `get_custom_device` helper to construct capabilities containing `PPR`: + +```python +def test_ppr_is_supported_by_qjit_capabilities(): + """Test that QJIT preserves a target device's PPR support.""" + dev = get_custom_device(native_gates={"PPR"}, wires=1) + target_capabilities = get_device_capabilities(dev, shots=None) + + qjit_capabilities = get_qjit_device_capabilities(target_capabilities) + + assert "PPR" in qjit_capabilities.operations +``` + +Keep the test independent of `qp.PPR` so it runs against the unchanged PennyLane pin. + +- [ ] **Step 2: Run the focused pytest and verify RED** + +Run the exact new test: + +```bash +pytest frontend/test/pytest/test_verification.py::test_ppr_is_supported_by_qjit_capabilities -v +``` + +Expected: FAIL because `RUNTIME_OPERATIONS` does not contain `PPR`. + +- [ ] **Step 3: Add PPR to runtime-supported operations** + +Add the string: + +```python +"PPR", +``` + +next to `"PauliRot"` in `RUNTIME_OPERATIONS` in `frontend/catalyst/device/qjit_device.py`. + +- [ ] **Step 4: Run the focused pytest and verify GREEN** + +Run: + +```bash +pytest frontend/test/pytest/test_verification.py::test_ppr_is_supported_by_qjit_capabilities -v +``` + +Expected: PASS. + +- [ ] **Step 5: Commit preprocessing preservation** + +```bash +git add frontend/catalyst/device/qjit_device.py frontend/test/pytest/test_verification.py +git commit -m "feat: preserve PPR during device preprocessing" +``` + +--- + +### Task 3: Document and integration-test the frontend behavior + +**Files:** +- Modify: `frontend/catalyst/passes/builtin_passes.py` +- Modify: `frontend/test/pytest/test_pauli_rot_and_measure.py` + +**Interfaces:** +- Consumes: `qp.PPR` when the installed PennyLane version provides PR #10107. +- Produces: frontend evidence that generic Operator2 capture retains PPR and `@to_ppr` emits exactly one `pbc.ppr`. + +- [ ] **Step 1: Add version-compatible integration tests** + +Add tests guarded with: + +```python +@pytest.mark.skipif(not hasattr(qp, "PPR"), reason="PennyLane PPR is not installed") +``` + +The first compiles a raw `qp.PPR(4, "XY", wires=[0, 1])` to the quantum compilation stage without `to_ppr` and asserts: + +```python +assert 'quantum.operator "PPR"' in optimized_ir +assert "pbc.ppr" not in optimized_ir +``` + +The second applies `@to_ppr` and asserts: + +```python +assert 'pbc.ppr ["X", "Y"](8)' in optimized_ir +assert 'quantum.operator "PPR"' not in optimized_ir +assert "quantum.paulirot" not in optimized_ir +``` + +- [ ] **Step 2: Run the focused integration tests** + +Run: + +```bash +pytest frontend/test/pytest/test_pauli_rot_and_measure.py -k "ppr_operator" -v +``` + +Expected with the current pin: SKIPPED with the explicit reason. Expected with PennyLane PR #10107 installed: both tests PASS. The non-skipped MLIR test from Task 1 remains the required proof of conversion behavior. + +- [ ] **Step 3: Update `to_ppr` documentation** + +Add ``qp.PPR`` beside ``qp.PauliRot`` in the supported operation list in `to_ppr_setup_inputs`. + +- [ ] **Step 4: Run focused frontend regression tests** + +Run: + +```bash +pytest frontend/test/pytest/test_pauli_rot_and_measure.py -v +``` + +Expected: existing tests PASS and new tests either PASS with PR #10107 or SKIP with the current pin. + +- [ ] **Step 5: Commit frontend coverage and documentation** + +```bash +git add frontend/catalyst/passes/builtin_passes.py frontend/test/pytest/test_pauli_rot_and_measure.py +git commit -m "test: cover PennyLane PPR frontend lowering" +``` + +--- + +### Task 4: Full verification + +**Files:** +- Verify only; modify scoped files if a regression reveals a defect. + +**Interfaces:** +- Consumes: Tasks 1–3. +- Produces: a regression-tested branch ready for review. + +- [ ] **Step 1: Run PBC MLIR tests** + +```bash +lit mlir/test/PBC -v +``` + +Expected: PASS. + +- [ ] **Step 2: Run relevant frontend tests** + +```bash +pytest frontend/test/pytest/test_pauli_rot_and_measure.py frontend/test/pytest/test_verification.py -v +``` + +Expected: PASS, except the two explicitly version-gated PPR integration tests may SKIP on the unchanged PennyLane pin. + +- [ ] **Step 3: Run formatting and lint checks for changed files** + +Use the repository's configured C++ formatter and Python checks on: + +```text +mlir/lib/PBC/Transforms/ToPPR.cpp +frontend/catalyst/device/qjit_device.py +frontend/catalyst/passes/builtin_passes.py +frontend/test/pytest/test_verification.py +frontend/test/pytest/test_pauli_rot_and_measure.py +``` + +Expected: no formatting or lint errors. + +- [ ] **Step 4: Inspect the final diff** + +```bash +git status --short +git diff origin/main...HEAD --check +``` + +Expected: only the design, plan, implementation, tests, and documentation are present; `git diff --check` exits successfully. From ec5ef4d862fd09f7161306e77671f07c3e683717 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 3 Sep 2026 17:05:29 +0200 Subject: [PATCH 03/27] feat: lower PennyLane PPR in to-ppr Co-authored-by: Cursor --- mlir/lib/PBC/Transforms/ToPPR.cpp | 62 ++++++++++++++++++++++++++++++- mlir/test/PBC/ToPPRTest.mlir | 39 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/mlir/lib/PBC/Transforms/ToPPR.cpp b/mlir/lib/PBC/Transforms/ToPPR.cpp index 02f4a20a1f..06e842ede8 100644 --- a/mlir/lib/PBC/Transforms/ToPPR.cpp +++ b/mlir/lib/PBC/Transforms/ToPPR.cpp @@ -411,6 +411,61 @@ LogicalResult convertPauliRotGate(PauliRotOp op, ConversionPatternRewriter &rewr op.getAdjoint(), rewriter); } +LogicalResult convertPPROperator(OperatorOp op, ConversionPatternRewriter &rewriter) { + if (op.getInQreg() || op.getInQubits().empty()) { + return op.emitOpError("PPR operator requires qubit mode"); + } + if (!op.getAllParams().empty()) { + return op.emitOpError("PPR operator does not support dynamic parameters"); + } + + DictionaryAttr staticData = op.getStaticData(); + auto denominatorAttr = staticData.getAs("angle_denominator"); + if (!denominatorAttr) { + return op.emitOpError( + "PPR operator requires an integer 'angle_denominator' in static_data"); + } + + int64_t denominator = denominatorAttr.getInt(); + if (denominator != 1 && denominator != -1 && denominator != 2 && denominator != -2 && + denominator != 4 && denominator != -4) { + return op.emitOpError("unsupported PPR angle denominator: ") << denominator; + } + + auto pauliWordAttr = staticData.getAs("pauli_word"); + if (!pauliWordAttr) { + return op.emitOpError("PPR operator requires a string 'pauli_word' in static_data"); + } + + StringRef pauliWord = pauliWordAttr.getValue(); + if (pauliWord.empty()) { + return op.emitOpError("PPR operator requires a non-empty Pauli word"); + } + if (pauliWord.size() != op.getInQubits().size()) { + return op.emitOpError("PPR operator requires one Pauli character per input qubit"); + } + + SmallVector pauliCharacters; + pauliCharacters.reserve(pauliWord.size()); + for (char pauli : pauliWord) { + if (pauli != 'X' && pauli != 'Y' && pauli != 'Z') { + return op.emitOpError("PPR operator Pauli word may contain only X, Y, or Z"); + } + pauliCharacters.push_back(rewriter.getStringAttr(StringRef(&pauli, 1))); + } + + ArrayAttr pauliProduct = rewriter.getArrayAttr(pauliCharacters); + int8_t rotationKind = static_cast(2 * denominator); + 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 //===----------------------------------------------------------------------===// @@ -421,7 +476,8 @@ 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, " - "RX, RY, RZ, IsingXX, IsingYY, IsingZZ, MultiRZ, and PauliRot."; + "RX, RY, RZ, IsingXX, IsingYY, IsingZZ, MultiRZ, PauliRot, " + "and PPR."; Operation *op = operation.getOperation(); if (auto gateLikeOp = dyn_cast(op)) { @@ -469,6 +525,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..18a645fb62 100644 --- a/mlir/test/PBC/ToPPRTest.mlir +++ b/mlir/test/PBC/ToPPRTest.mlir @@ -293,3 +293,42 @@ 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 } + +// ----- + +func.func @test_ppr_operator_to_ppr(%q0 : !quantum.bit, %q1 : !quantum.bit) { + %0:2 = quantum.operator "PPR"() qubits(%q0, %q1) + static_data = {angle_denominator = 4 : i64, pauli_word = "XY"} + // CHECK: pbc.ppr ["X", "Y"](8) + func.return +} + +// ----- + +func.func @test_negative_and_adjoint_ppr_operator(%q0 : !quantum.bit) { + %0 = quantum.operator "PPR"() qubits(%q0) + static_data = {angle_denominator = -2 : i64, pauli_word = "Z"} + %1 = quantum.operator "PPR"() adj qubits(%0) + static_data = {angle_denominator = 1 : i64, pauli_word = "X"} + // CHECK: pbc.ppr ["Z"](-4) + // CHECK: pbc.ppr ["X"](-2) + func.return +} + +// ----- + +func.func @test_ppr_operator_missing_angle_denominator(%q : !quantum.bit) { + // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} + %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator requires an integer 'angle_denominator' in static_data}} + static_data = {pauli_word = "X"} + func.return +} + +// ----- + +func.func @test_ppr_operator_unsupported_angle_denominator(%q : !quantum.bit) { + // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} + %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{unsupported PPR angle denominator: 3}} + static_data = {angle_denominator = 3 : i64, pauli_word = "X"} + func.return +} From 702d8ba5009cd2498794e6a6aa2fb4e57510a79e Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 3 Sep 2026 17:12:44 +0200 Subject: [PATCH 04/27] feat: preserve PPR during device preprocessing Co-authored-by: Cursor --- frontend/catalyst/device/qjit_device.py | 1 + frontend/test/pytest/test_verification.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/frontend/catalyst/device/qjit_device.py b/frontend/catalyst/device/qjit_device.py index cc6ef89e53..4732b43ab9 100644 --- a/frontend/catalyst/device/qjit_device.py +++ b/frontend/catalyst/device/qjit_device.py @@ -81,6 +81,7 @@ "ISWAP", "MultiRZ", "PauliRot", + "PPR", "PauliMeasure", "PauliX", "PauliY", diff --git a/frontend/test/pytest/test_verification.py b/frontend/test/pytest/test_verification.py index 615b2cf794..1b429a50a4 100644 --- a/frontend/test/pytest/test_verification.py +++ b/frontend/test/pytest/test_verification.py @@ -131,6 +131,16 @@ def __repr__(self): return "PauliX2" +def test_ppr_is_supported_by_qjit_capabilities(): + """Test that QJIT preserves a target device's PPR support.""" + dev = get_custom_device(native_gates={"PPR"}, wires=1) + target_capabilities = get_device_capabilities(dev, shots=None) + + qjit_capabilities = get_qjit_device_capabilities(target_capabilities) + + assert "PPR" in qjit_capabilities.operations + + @patch("catalyst.device.qjit_device.catalyst_decompose", null_transform) def test_unsupported_ops_raise_an_error(): """Test that an unsupported op raises an error""" From a9d6071871d8a08681cf324623358ba0f1bc630d Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 3 Sep 2026 17:22:21 +0200 Subject: [PATCH 05/27] test: cover PennyLane PPR frontend lowering Co-authored-by: Cursor --- frontend/catalyst/passes/builtin_passes.py | 1 + .../test/pytest/test_pauli_rot_and_measure.py | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/frontend/catalyst/passes/builtin_passes.py b/frontend/catalyst/passes/builtin_passes.py index a43d754140..84ef5ee503 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``. diff --git a/frontend/test/pytest/test_pauli_rot_and_measure.py b/frontend/test/pytest/test_pauli_rot_and_measure.py index 70ad2535e1..d3035d5c7b 100644 --- a/frontend/test/pytest/test_pauli_rot_and_measure.py +++ b/frontend/test/pytest/test_pauli_rot_and_measure.py @@ -78,6 +78,46 @@ def f(): assert "pbc.ppr" in optimized_ir +@pytest.mark.skipif(not hasattr(qp, "PPR"), reason="PennyLane PPR is not installed") +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 + + +@pytest.mark.skipif(not hasattr(qp, "PPR"), reason="PennyLane PPR is not installed") +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"](8)' 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"])] From 64b3482c37246bd9936c608f565a217a965567f9 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 3 Sep 2026 17:56:54 +0200 Subject: [PATCH 06/27] fix: harden to-ppr PPR-operator lowering and address final review findings - Validate that the quantum.operator PPR angle_denominator attribute is a signless integer with bit width >= 2 before calling IntegerAttr::getInt(). A handwritten si64 previously crashed the compiler (assertion failure), and a bare i1 true silently sign-extended to -1. Both now emit the existing integer-angle-denominator diagnostic. - Split the combined qubit-mode diagnostic into a register-mode message and a zero-qubit message, with small dedicated tests for each. - Tighten the positive to-ppr PPR-operator MLIR checks with CHECK-LABEL/ CHECK-NOT and by capturing SSA values so the second PPR is checked to consume the first PPR's output. - Document the to_ppr PPR direct-lowering improvement in the dev changelog, referencing upstream PennyLane PR #10107. - Remove the inert "PPR" entry from RUNTIME_OPERATIONS (no shipped device declares it and the runtime cannot execute raw PPR) and the now-tautological test_ppr_is_supported_by_qjit_capabilities test; the real frontend integration tests already prove PPR survives and lowers without PauliRot. - Remove committed internal workflow artifacts under docs/superpowers/ (this repo uses doc/ for shipped documentation). Co-authored-by: Cursor --- doc/releases/changelog-dev.md | 4 + .../plans/2026-09-03-ppr-lowering.md | 302 ------------------ .../specs/2026-09-03-ppr-lowering-design.md | 57 ---- frontend/catalyst/device/qjit_device.py | 1 - frontend/test/pytest/test_verification.py | 10 - mlir/lib/PBC/Transforms/ToPPR.cpp | 17 +- mlir/test/PBC/ToPPRTest.mlir | 56 +++- 7 files changed, 71 insertions(+), 376 deletions(-) delete mode 100644 docs/superpowers/plans/2026-09-03-ppr-lowering.md delete mode 100644 docs/superpowers/specs/2026-09-03-ppr-lowering-design.md diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index c615256eb9..fd785baf70 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -479,6 +479,10 @@ * 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``, without + first decomposing it through ``PauliRot``. + [(#10107)](https://github.com/PennyLaneAI/pennylane/pull/10107) +

Breaking changes 💔

* Removes :func:`~.passes.ppm_specs` and the ``--ppm-specs`` MLIR pass. Use :func:`~.specs` and diff --git a/docs/superpowers/plans/2026-09-03-ppr-lowering.md b/docs/superpowers/plans/2026-09-03-ppr-lowering.md deleted file mode 100644 index f09462d47c..0000000000 --- a/docs/superpowers/plans/2026-09-03-ppr-lowering.md +++ /dev/null @@ -1,302 +0,0 @@ -# PennyLane PPR Lowering Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Lower PennyLane's fixed-angle `PPR` Operator2 directly to Catalyst `pbc.ppr` without an intermediate `PauliRot` decomposition. - -**Architecture:** Keep `PPR` in the existing generic `qref.operator`/`quantum.operator` representation until the registered `to-ppr` pass. Add one conversion branch that reads the two static arguments and emits the canonical PBC operation, and mark `PPR` as runtime-supported so PennyLane device preprocessing preserves it. - -**Tech Stack:** Python 3, PennyLane Operator2 capture, Catalyst QRef/Quantum/PBC MLIR dialects, C++17 MLIR conversion patterns, LLVM lit/FileCheck, pytest. - -## Global Constraints - -- PennyLane `PPR(k, P)` uses `exp(-i (pi / k) P / 2)`; Catalyst `pbc.ppr(P)(r)` uses `exp(-i pi P / r)`, so `r = 2 * k`. -- Allowed PennyLane denominators are exactly `-4`, `-2`, `-1`, `1`, `2`, and `4`. -- An adjoint negates the emitted PBC rotation kind. -- Controlled PPR remains unsupported by `to_ppr`. -- Do not add dedicated QRef or Quantum PPR operations. -- Do not update Catalyst's PennyLane dependency pin. - ---- - -### Task 1: Convert generic quantum PPR operators in `to-ppr` - -**Files:** -- Modify: `mlir/test/PBC/ToPPRTest.mlir` -- Modify: `mlir/lib/PBC/Transforms/ToPPR.cpp` - -**Interfaces:** -- Consumes: `quantum.operator "PPR"()` in qubit mode with `static_data = {angle_denominator = , pauli_word = ""}`. -- Produces: one `pbc.ppr` with the same qubits and Pauli word and rotation kind `2 * angle_denominator`, negated when the source operator has `adj`. - -- [ ] **Step 1: Add failing direct-lowering tests** - -Append split-input tests covering positive, negative, multi-qubit, and adjoint cases: - -```mlir -// ----- - -func.func @test_ppr_operator_to_ppr(%q0 : !quantum.bit, %q1 : !quantum.bit) { - %0:2 = quantum.operator "PPR"() qubits(%q0, %q1) - static_data = {angle_denominator = 4 : i64, pauli_word = "XY"} - // CHECK: pbc.ppr ["X", "Y"](8) - func.return -} - -// ----- - -func.func @test_negative_and_adjoint_ppr_operator(%q0 : !quantum.bit) { - %0 = quantum.operator "PPR"() qubits(%q0) - static_data = {angle_denominator = -2 : i64, pauli_word = "Z"} - %1 = quantum.operator "PPR"() adj qubits(%0) - static_data = {angle_denominator = 1 : i64, pauli_word = "X"} - // CHECK: pbc.ppr ["Z"](-4) - // CHECK: pbc.ppr ["X"](-2) - func.return -} -``` - -- [ ] **Step 2: Run the focused MLIR test and verify RED** - -Run: - -```bash -lit mlir/test/PBC/ToPPRTest.mlir -v -``` - -Expected: FAIL because `quantum.operator "PPR"` is explicitly illegal and `PBCGateLowering` reports it as unsupported. - -- [ ] **Step 3: Implement minimal PPR conversion** - -In `mlir/lib/PBC/Transforms/ToPPR.cpp`, add a helper for `OperatorOp` that: - -```cpp -LogicalResult convertPPROperator(OperatorOp op, ConversionPatternRewriter &rewriter) -``` - -The helper must: - -1. Read `angle_denominator` as `IntegerAttr` and `pauli_word` as `StringAttr` from `op.getStaticData()`. -2. Require qubit mode, no dynamic parameters, a non-empty Pauli word, one Pauli character per input qubit, only `X`, `Y`, or `Z`, and an allowed denominator. -3. Build an `ArrayAttr` containing one-character string attributes. -4. Compute `int8_t rotationKind = 2 * denominator`, negating it for `op.getAdjoint()`. -5. Create `PPRotationOp` and replace the generic operator results. - -Extend `PBCGateLowering::matchAndRewrite` with an `OperatorOp` branch that calls the helper only when `getOpName() == "PPR"`. Update the supported-operation diagnostic to include `PPR`. - -- [ ] **Step 4: Add malformed-input diagnostics** - -Add split-input tests with `expected-error` checks for: - -```mlir -quantum.operator "PPR"() qubits(%q) - static_data = {pauli_word = "X"} -``` - -and: - -```mlir -quantum.operator "PPR"() qubits(%q) - static_data = {angle_denominator = 3 : i64, pauli_word = "X"} -``` - -Expected diagnostics must identify the missing `angle_denominator` and the unsupported denominator respectively. - -- [ ] **Step 5: Run the focused MLIR test and verify GREEN** - -Run: - -```bash -lit mlir/test/PBC/ToPPRTest.mlir -v -``` - -Expected: PASS with all `quantum.operator "PPR"` instances converted or diagnosed as expected. - -- [ ] **Step 6: Commit the conversion** - -```bash -git add mlir/lib/PBC/Transforms/ToPPR.cpp mlir/test/PBC/ToPPRTest.mlir -git commit -m "feat: lower PennyLane PPR in to-ppr" -``` - ---- - -### Task 2: Preserve PPR through device preprocessing - -**Files:** -- Modify: `frontend/test/pytest/test_verification.py` -- Modify: `frontend/catalyst/device/qjit_device.py` - -**Interfaces:** -- Consumes: PennyLane target-device capabilities containing an operation named `PPR`. -- Produces: QJIT capabilities that retain `PPR`, allowing PennyLane preprocessing to leave the operation intact for `to-ppr`. - -- [ ] **Step 1: Add a failing capability-intersection test** - -Use the existing `get_custom_device` helper to construct capabilities containing `PPR`: - -```python -def test_ppr_is_supported_by_qjit_capabilities(): - """Test that QJIT preserves a target device's PPR support.""" - dev = get_custom_device(native_gates={"PPR"}, wires=1) - target_capabilities = get_device_capabilities(dev, shots=None) - - qjit_capabilities = get_qjit_device_capabilities(target_capabilities) - - assert "PPR" in qjit_capabilities.operations -``` - -Keep the test independent of `qp.PPR` so it runs against the unchanged PennyLane pin. - -- [ ] **Step 2: Run the focused pytest and verify RED** - -Run the exact new test: - -```bash -pytest frontend/test/pytest/test_verification.py::test_ppr_is_supported_by_qjit_capabilities -v -``` - -Expected: FAIL because `RUNTIME_OPERATIONS` does not contain `PPR`. - -- [ ] **Step 3: Add PPR to runtime-supported operations** - -Add the string: - -```python -"PPR", -``` - -next to `"PauliRot"` in `RUNTIME_OPERATIONS` in `frontend/catalyst/device/qjit_device.py`. - -- [ ] **Step 4: Run the focused pytest and verify GREEN** - -Run: - -```bash -pytest frontend/test/pytest/test_verification.py::test_ppr_is_supported_by_qjit_capabilities -v -``` - -Expected: PASS. - -- [ ] **Step 5: Commit preprocessing preservation** - -```bash -git add frontend/catalyst/device/qjit_device.py frontend/test/pytest/test_verification.py -git commit -m "feat: preserve PPR during device preprocessing" -``` - ---- - -### Task 3: Document and integration-test the frontend behavior - -**Files:** -- Modify: `frontend/catalyst/passes/builtin_passes.py` -- Modify: `frontend/test/pytest/test_pauli_rot_and_measure.py` - -**Interfaces:** -- Consumes: `qp.PPR` when the installed PennyLane version provides PR #10107. -- Produces: frontend evidence that generic Operator2 capture retains PPR and `@to_ppr` emits exactly one `pbc.ppr`. - -- [ ] **Step 1: Add version-compatible integration tests** - -Add tests guarded with: - -```python -@pytest.mark.skipif(not hasattr(qp, "PPR"), reason="PennyLane PPR is not installed") -``` - -The first compiles a raw `qp.PPR(4, "XY", wires=[0, 1])` to the quantum compilation stage without `to_ppr` and asserts: - -```python -assert 'quantum.operator "PPR"' in optimized_ir -assert "pbc.ppr" not in optimized_ir -``` - -The second applies `@to_ppr` and asserts: - -```python -assert 'pbc.ppr ["X", "Y"](8)' in optimized_ir -assert 'quantum.operator "PPR"' not in optimized_ir -assert "quantum.paulirot" not in optimized_ir -``` - -- [ ] **Step 2: Run the focused integration tests** - -Run: - -```bash -pytest frontend/test/pytest/test_pauli_rot_and_measure.py -k "ppr_operator" -v -``` - -Expected with the current pin: SKIPPED with the explicit reason. Expected with PennyLane PR #10107 installed: both tests PASS. The non-skipped MLIR test from Task 1 remains the required proof of conversion behavior. - -- [ ] **Step 3: Update `to_ppr` documentation** - -Add ``qp.PPR`` beside ``qp.PauliRot`` in the supported operation list in `to_ppr_setup_inputs`. - -- [ ] **Step 4: Run focused frontend regression tests** - -Run: - -```bash -pytest frontend/test/pytest/test_pauli_rot_and_measure.py -v -``` - -Expected: existing tests PASS and new tests either PASS with PR #10107 or SKIP with the current pin. - -- [ ] **Step 5: Commit frontend coverage and documentation** - -```bash -git add frontend/catalyst/passes/builtin_passes.py frontend/test/pytest/test_pauli_rot_and_measure.py -git commit -m "test: cover PennyLane PPR frontend lowering" -``` - ---- - -### Task 4: Full verification - -**Files:** -- Verify only; modify scoped files if a regression reveals a defect. - -**Interfaces:** -- Consumes: Tasks 1–3. -- Produces: a regression-tested branch ready for review. - -- [ ] **Step 1: Run PBC MLIR tests** - -```bash -lit mlir/test/PBC -v -``` - -Expected: PASS. - -- [ ] **Step 2: Run relevant frontend tests** - -```bash -pytest frontend/test/pytest/test_pauli_rot_and_measure.py frontend/test/pytest/test_verification.py -v -``` - -Expected: PASS, except the two explicitly version-gated PPR integration tests may SKIP on the unchanged PennyLane pin. - -- [ ] **Step 3: Run formatting and lint checks for changed files** - -Use the repository's configured C++ formatter and Python checks on: - -```text -mlir/lib/PBC/Transforms/ToPPR.cpp -frontend/catalyst/device/qjit_device.py -frontend/catalyst/passes/builtin_passes.py -frontend/test/pytest/test_verification.py -frontend/test/pytest/test_pauli_rot_and_measure.py -``` - -Expected: no formatting or lint errors. - -- [ ] **Step 4: Inspect the final diff** - -```bash -git status --short -git diff origin/main...HEAD --check -``` - -Expected: only the design, plan, implementation, tests, and documentation are present; `git diff --check` exits successfully. diff --git a/docs/superpowers/specs/2026-09-03-ppr-lowering-design.md b/docs/superpowers/specs/2026-09-03-ppr-lowering-design.md deleted file mode 100644 index 314a80ccd9..0000000000 --- a/docs/superpowers/specs/2026-09-03-ppr-lowering-design.md +++ /dev/null @@ -1,57 +0,0 @@ -# PennyLane PPR Lowering - -## Goal - -Support PennyLane's discrete `PPR` Operator2 in Catalyst's `to_ppr` pass without first -decomposing it through `PauliRot`. - -## Representation and lowering - -PennyLane captures `PPR` as a generic `qref.operator` because both -`angle_denominator` and `pauli_word` are compilable static arguments. Value-semantics conversion -preserves this as `quantum.operator "PPR"` with both values in `static_data`. - -The `to_ppr` conversion will recognize that operator, validate its expected shape, and emit one -native `pbc.ppr` operation. If PennyLane's angle denominator is `k`, the PBC rotation kind is -`2 * k`: PennyLane uses the `PauliRot` angle convention -`exp(-i (pi / k) P / 2)`, while `pbc.ppr` represents `exp(-i pi P / rotation_kind)`. -An adjoint negates the resulting rotation kind. - -The conversion will reject controlled PPRs consistently with the other operations accepted by -`to_ppr`. Invalid or missing static data will produce a compilation diagnostic rather than -silently decomposing or mis-lowering the operation. - -This design does not introduce dedicated QRef or Quantum dialect PPR operations. The generic -operator already preserves all required compile-time data, and the PBC dialect remains the -canonical discrete-PPR representation after `to_ppr`. - -## Decomposition preservation - -Add `PPR` to Catalyst's runtime-supported operation set. This keeps device preprocessing from -decomposing the operator to its PennyLane `PauliRot` decomposition before the registered -`to_ppr` pass runs. - -No decomposition rule will be added for PPR in Catalyst. Existing graph-decomposition behavior -continues to preserve any operator explicitly included in its target gate set. - -## User-facing documentation - -Add `qp.PPR` to the `to_ppr` supported-operations list. The dependency pin remains unchanged; -the implementation targets the API from PennyLane PR #10107. - -## Tests - -1. Add MLIR tests for direct conversion of positive and negative PPR denominators, multi-qubit - Pauli words, and adjoints. -2. Add frontend coverage proving that `qp.PPR` remains a `quantum.operator "PPR"` without - `to_ppr`, and becomes one `pbc.ppr` with `to_ppr`. -3. Add preprocessing coverage proving PPR is in Catalyst's supported operation set and therefore - is not decomposed before lowering. -4. Keep existing PauliRot conversion tests unchanged to guard the separate arbitrary-angle path. - -## Non-goals - -- Executing a raw PennyLane `PPR` without a PBC-lowering pass. -- Supporting controlled PPRs in `to_ppr`. -- Updating Catalyst's PennyLane dependency pin. -- Adding a new QRef or Quantum dialect operation solely for PPR. diff --git a/frontend/catalyst/device/qjit_device.py b/frontend/catalyst/device/qjit_device.py index 4732b43ab9..cc6ef89e53 100644 --- a/frontend/catalyst/device/qjit_device.py +++ b/frontend/catalyst/device/qjit_device.py @@ -81,7 +81,6 @@ "ISWAP", "MultiRZ", "PauliRot", - "PPR", "PauliMeasure", "PauliX", "PauliY", diff --git a/frontend/test/pytest/test_verification.py b/frontend/test/pytest/test_verification.py index 1b429a50a4..615b2cf794 100644 --- a/frontend/test/pytest/test_verification.py +++ b/frontend/test/pytest/test_verification.py @@ -131,16 +131,6 @@ def __repr__(self): return "PauliX2" -def test_ppr_is_supported_by_qjit_capabilities(): - """Test that QJIT preserves a target device's PPR support.""" - dev = get_custom_device(native_gates={"PPR"}, wires=1) - target_capabilities = get_device_capabilities(dev, shots=None) - - qjit_capabilities = get_qjit_device_capabilities(target_capabilities) - - assert "PPR" in qjit_capabilities.operations - - @patch("catalyst.device.qjit_device.catalyst_decompose", null_transform) def test_unsupported_ops_raise_an_error(): """Test that an unsupported op raises an error""" diff --git a/mlir/lib/PBC/Transforms/ToPPR.cpp b/mlir/lib/PBC/Transforms/ToPPR.cpp index 06e842ede8..2cce166097 100644 --- a/mlir/lib/PBC/Transforms/ToPPR.cpp +++ b/mlir/lib/PBC/Transforms/ToPPR.cpp @@ -412,8 +412,11 @@ LogicalResult convertPauliRotGate(PauliRotOp op, ConversionPatternRewriter &rewr } LogicalResult convertPPROperator(OperatorOp op, ConversionPatternRewriter &rewriter) { - if (op.getInQreg() || op.getInQubits().empty()) { - return op.emitOpError("PPR operator requires qubit mode"); + if (op.getInQreg()) { + return op.emitOpError("PPR operator requires qubit mode, not register mode"); + } + if (op.getInQubits().empty()) { + return op.emitOpError("PPR operator requires at least one qubit"); } if (!op.getAllParams().empty()) { return op.emitOpError("PPR operator does not support dynamic parameters"); @@ -421,7 +424,15 @@ LogicalResult convertPPROperator(OperatorOp op, ConversionPatternRewriter &rewri DictionaryAttr staticData = op.getStaticData(); auto denominatorAttr = staticData.getAs("angle_denominator"); - if (!denominatorAttr) { + // `getInt()` below asserts that the attribute's type is a signless integer (or index), and + // sign-extends the stored bits to produce the result. Reject anything that isn't a signless + // integer with enough bits to unambiguously represent the denominators we support (at least + // ±1, ±2, ±4): a signed/unsigned type would trip that assertion (crashing the compiler), + // while a signless i1 (e.g. a bare `true`/`false` literal) would silently sign-extend to -1. + bool isValidDenominatorType = denominatorAttr && + denominatorAttr.getType().isSignlessInteger() && + denominatorAttr.getType().getIntOrFloatBitWidth() >= 2; + if (!isValidDenominatorType) { return op.emitOpError( "PPR operator requires an integer 'angle_denominator' in static_data"); } diff --git a/mlir/test/PBC/ToPPRTest.mlir b/mlir/test/PBC/ToPPRTest.mlir index 18a645fb62..471b808ab3 100644 --- a/mlir/test/PBC/ToPPRTest.mlir +++ b/mlir/test/PBC/ToPPRTest.mlir @@ -296,22 +296,50 @@ func.func private @"some_decomp_rule"(%arg0: tensor<1xf64>, %arg1: tensor<1xi64> // ----- +// 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"](8) [[q0:%.+]], [[q1:%.+]] %0:2 = quantum.operator "PPR"() qubits(%q0, %q1) static_data = {angle_denominator = 4 : i64, pauli_word = "XY"} - // CHECK: pbc.ppr ["X", "Y"](8) + // 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"](-4) [[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"](-2) [[q0_0]] %1 = quantum.operator "PPR"() adj qubits(%0) static_data = {angle_denominator = 1 : i64, pauli_word = "X"} - // CHECK: pbc.ppr ["Z"](-4) - // CHECK: pbc.ppr ["X"](-2) + // CHECK-NOT: quantum.operator + // CHECK: return + func.return +} + +// ----- + +func.func @test_ppr_operator_register_mode_unsupported(%r : !quantum.reg, %idx : tensor<1xi64>) { + // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} + %0 = quantum.operator "PPR"() // expected-error @+0 {{PPR operator requires qubit mode, not register mode}} + quregs(%r) indices(%idx : tensor<1xi64>) + static_data = {angle_denominator = 4 : i64, pauli_word = "X"} + func.return +} + +// ----- + +func.func @test_ppr_operator_zero_qubits_unsupported() { + // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} + quantum.operator "PPR"() // expected-error @+0 {{PPR operator requires at least one qubit}} + static_data = {angle_denominator = 4 : i64, pauli_word = ""} func.return } @@ -332,3 +360,25 @@ func.func @test_ppr_operator_unsupported_angle_denominator(%q : !quantum.bit) { static_data = {angle_denominator = 3 : i64, pauli_word = "X"} func.return } + +// ----- + +// A signed integer type (as opposed to signless) must not be accepted: IntegerAttr::getInt() +// asserts on non-signless types, which would otherwise crash the compiler. +func.func @test_ppr_operator_signed_angle_denominator(%q : !quantum.bit) { + // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} + %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator requires an integer 'angle_denominator' in static_data}} + static_data = {angle_denominator = 4 : si64, pauli_word = "X"} + func.return +} + +// ----- + +// A 1-bit signless integer (e.g. a bare `true`/`false` literal) must be rejected rather than +// silently sign-extended to -1 by IntegerAttr::getInt(). +func.func @test_ppr_operator_i1_angle_denominator(%q : !quantum.bit) { + // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} + %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator requires an integer 'angle_denominator' in static_data}} + static_data = {angle_denominator = true, pauli_word = "X"} + func.return +} From ec13245d4395bf108f6dc744a53c3ec030cf33f5 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Fri, 4 Sep 2026 09:13:03 +0200 Subject: [PATCH 07/27] test: require PennyLane PPR support Co-authored-by: Cursor --- frontend/test/pytest/test_pauli_rot_and_measure.py | 2 -- mlir/lib/PBC/Transforms/ToPPR.cpp | 9 ++++----- mlir/test/PBC/ToPPRTest.mlir | 9 +++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/frontend/test/pytest/test_pauli_rot_and_measure.py b/frontend/test/pytest/test_pauli_rot_and_measure.py index d3035d5c7b..94580d0851 100644 --- a/frontend/test/pytest/test_pauli_rot_and_measure.py +++ b/frontend/test/pytest/test_pauli_rot_and_measure.py @@ -78,7 +78,6 @@ def f(): assert "pbc.ppr" in optimized_ir -@pytest.mark.skipif(not hasattr(qp, "PPR"), reason="PennyLane PPR is not installed") def test_ppr_operator_capture(): """Test that PPR remains a generic operator before applying to_ppr.""" pipe = [("pipe", ["quantum-compilation-stage"])] @@ -97,7 +96,6 @@ def f(): assert "pbc.ppr" not in optimized_ir -@pytest.mark.skipif(not hasattr(qp, "PPR"), reason="PennyLane PPR is not installed") def test_ppr_operator_to_ppr(): """Test that to_ppr converts a PPR operator to pbc.ppr.""" pipe = [("pipe", ["quantum-compilation-stage"])] diff --git a/mlir/lib/PBC/Transforms/ToPPR.cpp b/mlir/lib/PBC/Transforms/ToPPR.cpp index 2cce166097..d1bed2472b 100644 --- a/mlir/lib/PBC/Transforms/ToPPR.cpp +++ b/mlir/lib/PBC/Transforms/ToPPR.cpp @@ -424,11 +424,10 @@ LogicalResult convertPPROperator(OperatorOp op, ConversionPatternRewriter &rewri DictionaryAttr staticData = op.getStaticData(); auto denominatorAttr = staticData.getAs("angle_denominator"); - // `getInt()` below asserts that the attribute's type is a signless integer (or index), and - // sign-extends the stored bits to produce the result. Reject anything that isn't a signless - // integer with enough bits to unambiguously represent the denominators we support (at least - // ±1, ±2, ±4): a signed/unsigned type would trip that assertion (crashing the compiler), - // while a signless i1 (e.g. a bare `true`/`false` literal) would silently sign-extend to -1. + // MLIR's ordinary `i64` is a signless integer type: it can hold negative values, but its type + // does not prescribe signed or unsigned arithmetic. `getInt()` supports these ordinary `iN` + // attributes, but asserts on the distinct explicitly signed/unsigned `siN`/`uiN` types. + // Also reject `i1` (e.g. a bare `true`/`false`), which would sign-extend true to -1. bool isValidDenominatorType = denominatorAttr && denominatorAttr.getType().isSignlessInteger() && denominatorAttr.getType().getIntOrFloatBitWidth() >= 2; diff --git a/mlir/test/PBC/ToPPRTest.mlir b/mlir/test/PBC/ToPPRTest.mlir index 471b808ab3..e0fb5d788c 100644 --- a/mlir/test/PBC/ToPPRTest.mlir +++ b/mlir/test/PBC/ToPPRTest.mlir @@ -363,8 +363,9 @@ func.func @test_ppr_operator_unsupported_angle_denominator(%q : !quantum.bit) { // ----- -// A signed integer type (as opposed to signless) must not be accepted: IntegerAttr::getInt() -// asserts on non-signless types, which would otherwise crash the compiler. +// MLIR's ordinary `i64` type is "signless": it can represent negative values, but signedness is +// chosen by the operation interpreting it. Explicitly signed `si64` is a different MLIR type and +// must not be accepted here because IntegerAttr::getInt() only supports signless integer types. func.func @test_ppr_operator_signed_angle_denominator(%q : !quantum.bit) { // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator requires an integer 'angle_denominator' in static_data}} @@ -374,8 +375,8 @@ func.func @test_ppr_operator_signed_angle_denominator(%q : !quantum.bit) { // ----- -// A 1-bit signless integer (e.g. a bare `true`/`false` literal) must be rejected rather than -// silently sign-extended to -1 by IntegerAttr::getInt(). +// A 1-bit `i1` (e.g. a bare `true`/`false` literal) must be rejected rather than silently +// sign-extended to -1 by IntegerAttr::getInt(). func.func @test_ppr_operator_i1_angle_denominator(%q : !quantum.bit) { // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator requires an integer 'angle_denominator' in static_data}} From b87dca7090d6637614f92d873ffd0f9e49228909 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Fri, 4 Sep 2026 09:16:27 +0200 Subject: [PATCH 08/27] tiny comment on angle convention --- frontend/catalyst/passes/builtin_passes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/catalyst/passes/builtin_passes.py b/frontend/catalyst/passes/builtin_passes.py index 84ef5ee503..307c2d71ac 100644 --- a/frontend/catalyst/passes/builtin_passes.py +++ b/frontend/catalyst/passes/builtin_passes.py @@ -966,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`` and ``qp.PPR`` differs from Catalyst's + angle convention for PPRs by a factor of two. + **Example** The ``to_ppr`` compilation pass can be applied as a decorator on a QNode: From 2de9ff9f0b5608b150c33a7f6f1562bc1afceddb Mon Sep 17 00:00:00 2001 From: dwierichs Date: Fri, 4 Sep 2026 09:25:00 +0200 Subject: [PATCH 09/27] remove excessive validation --- doc/releases/changelog-dev.md | 2 +- mlir/lib/PBC/Transforms/ToPPR.cpp | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index fd785baf70..b699082c23 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -481,7 +481,7 @@ * ``to_ppr`` now directly lowers PennyLane's discrete ``PPR`` operator to ``pbc.ppr``, without first decomposing it through ``PauliRot``. - [(#10107)](https://github.com/PennyLaneAI/pennylane/pull/10107) + [(#xxxx)](https://github.com/PennyLaneAI/catalyst/pull/xxxx)

Breaking changes 💔

diff --git a/mlir/lib/PBC/Transforms/ToPPR.cpp b/mlir/lib/PBC/Transforms/ToPPR.cpp index d1bed2472b..6ebccba95b 100644 --- a/mlir/lib/PBC/Transforms/ToPPR.cpp +++ b/mlir/lib/PBC/Transforms/ToPPR.cpp @@ -412,29 +412,12 @@ LogicalResult convertPauliRotGate(PauliRotOp op, ConversionPatternRewriter &rewr } LogicalResult convertPPROperator(OperatorOp op, ConversionPatternRewriter &rewriter) { - if (op.getInQreg()) { - return op.emitOpError("PPR operator requires qubit mode, not register mode"); - } - if (op.getInQubits().empty()) { - return op.emitOpError("PPR operator requires at least one qubit"); - } if (!op.getAllParams().empty()) { return op.emitOpError("PPR operator does not support dynamic parameters"); } DictionaryAttr staticData = op.getStaticData(); auto denominatorAttr = staticData.getAs("angle_denominator"); - // MLIR's ordinary `i64` is a signless integer type: it can hold negative values, but its type - // does not prescribe signed or unsigned arithmetic. `getInt()` supports these ordinary `iN` - // attributes, but asserts on the distinct explicitly signed/unsigned `siN`/`uiN` types. - // Also reject `i1` (e.g. a bare `true`/`false`), which would sign-extend true to -1. - bool isValidDenominatorType = denominatorAttr && - denominatorAttr.getType().isSignlessInteger() && - denominatorAttr.getType().getIntOrFloatBitWidth() >= 2; - if (!isValidDenominatorType) { - return op.emitOpError( - "PPR operator requires an integer 'angle_denominator' in static_data"); - } int64_t denominator = denominatorAttr.getInt(); if (denominator != 1 && denominator != -1 && denominator != 2 && denominator != -2 && From 216ae91dcdf7b37bcb3c641c91fcbeab955ba0c9 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Fri, 4 Sep 2026 09:25:44 +0200 Subject: [PATCH 10/27] test: remove obsolete PPR validation cases Co-authored-by: Cursor --- mlir/test/PBC/ToPPRTest.mlir | 51 ------------------------------------ 1 file changed, 51 deletions(-) diff --git a/mlir/test/PBC/ToPPRTest.mlir b/mlir/test/PBC/ToPPRTest.mlir index e0fb5d788c..ef9fb54270 100644 --- a/mlir/test/PBC/ToPPRTest.mlir +++ b/mlir/test/PBC/ToPPRTest.mlir @@ -326,60 +326,9 @@ func.func @test_negative_and_adjoint_ppr_operator(%q0 : !quantum.bit) { // ----- -func.func @test_ppr_operator_register_mode_unsupported(%r : !quantum.reg, %idx : tensor<1xi64>) { - // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} - %0 = quantum.operator "PPR"() // expected-error @+0 {{PPR operator requires qubit mode, not register mode}} - quregs(%r) indices(%idx : tensor<1xi64>) - static_data = {angle_denominator = 4 : i64, pauli_word = "X"} - func.return -} - -// ----- - -func.func @test_ppr_operator_zero_qubits_unsupported() { - // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} - quantum.operator "PPR"() // expected-error @+0 {{PPR operator requires at least one qubit}} - static_data = {angle_denominator = 4 : i64, pauli_word = ""} - func.return -} - -// ----- - -func.func @test_ppr_operator_missing_angle_denominator(%q : !quantum.bit) { - // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} - %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator requires an integer 'angle_denominator' in static_data}} - static_data = {pauli_word = "X"} - func.return -} - -// ----- - func.func @test_ppr_operator_unsupported_angle_denominator(%q : !quantum.bit) { // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{unsupported PPR angle denominator: 3}} static_data = {angle_denominator = 3 : i64, pauli_word = "X"} func.return } - -// ----- - -// MLIR's ordinary `i64` type is "signless": it can represent negative values, but signedness is -// chosen by the operation interpreting it. Explicitly signed `si64` is a different MLIR type and -// must not be accepted here because IntegerAttr::getInt() only supports signless integer types. -func.func @test_ppr_operator_signed_angle_denominator(%q : !quantum.bit) { - // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} - %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator requires an integer 'angle_denominator' in static_data}} - static_data = {angle_denominator = 4 : si64, pauli_word = "X"} - func.return -} - -// ----- - -// A 1-bit `i1` (e.g. a bare `true`/`false` literal) must be rejected rather than silently -// sign-extended to -1 by IntegerAttr::getInt(). -func.func @test_ppr_operator_i1_angle_denominator(%q : !quantum.bit) { - // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} - %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator requires an integer 'angle_denominator' in static_data}} - static_data = {angle_denominator = true, pauli_word = "X"} - func.return -} From 37fe90cf8a3cd6dcf992422fbe7cf37c33fd6fdc Mon Sep 17 00:00:00 2001 From: dwierichs Date: Fri, 4 Sep 2026 09:43:19 +0200 Subject: [PATCH 11/27] test: cover PPR Pauli word validation Co-authored-by: Cursor --- mlir/test/PBC/ToPPRTest.mlir | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/mlir/test/PBC/ToPPRTest.mlir b/mlir/test/PBC/ToPPRTest.mlir index ef9fb54270..c103510ac0 100644 --- a/mlir/test/PBC/ToPPRTest.mlir +++ b/mlir/test/PBC/ToPPRTest.mlir @@ -332,3 +332,21 @@ func.func @test_ppr_operator_unsupported_angle_denominator(%q : !quantum.bit) { static_data = {angle_denominator = 3 : i64, pauli_word = "X"} func.return } + +// ----- + +func.func @test_ppr_operator_invalid_pauli_character(%q : !quantum.bit) { + // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} + %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator Pauli word may contain only X, Y, or Z}} + static_data = {angle_denominator = 4 : i64, pauli_word = "A"} + func.return +} + +// ----- + +func.func @test_ppr_operator_pauli_qubit_count_mismatch(%q : !quantum.bit) { + // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} + %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator requires one Pauli character per input qubit}} + static_data = {angle_denominator = 4 : i64, pauli_word = "XY"} + func.return +} From a608b1db16aa3d0ac2c6f537ac057b830c8a40d7 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Fri, 4 Sep 2026 09:44:11 +0200 Subject: [PATCH 12/27] better ordering in message. --- mlir/lib/PBC/Transforms/ToPPR.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mlir/lib/PBC/Transforms/ToPPR.cpp b/mlir/lib/PBC/Transforms/ToPPR.cpp index 6ebccba95b..c6145c9a48 100644 --- a/mlir/lib/PBC/Transforms/ToPPR.cpp +++ b/mlir/lib/PBC/Transforms/ToPPR.cpp @@ -468,9 +468,8 @@ 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, " - "RX, RY, RZ, IsingXX, IsingYY, IsingZZ, MultiRZ, PauliRot, " - "and PPR."; + 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(); if (auto gateLikeOp = dyn_cast(op)) { From 0cbce943ed01aa4e17c138aa7dd62b2b48899fe1 Mon Sep 17 00:00:00 2001 From: David Wierichs Date: Fri, 4 Sep 2026 03:45:11 -0400 Subject: [PATCH 13/27] Update doc/releases/changelog-dev.md --- doc/releases/changelog-dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index b699082c23..e8260a840a 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -481,7 +481,7 @@ * ``to_ppr`` now directly lowers PennyLane's discrete ``PPR`` operator to ``pbc.ppr``, without first decomposing it through ``PauliRot``. - [(#xxxx)](https://github.com/PennyLaneAI/catalyst/pull/xxxx) + [(#3185)](https://github.com/PennyLaneAI/catalyst/pull/3185)

Breaking changes 💔

From b0baf11ca1a816b9af581d8e9e2f617fce122b92 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Fri, 4 Sep 2026 21:15:14 +0200 Subject: [PATCH 14/27] test: skip PPR tests before PennyLane support Co-authored-by: Cursor --- frontend/test/pytest/test_pauli_rot_and_measure.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/test/pytest/test_pauli_rot_and_measure.py b/frontend/test/pytest/test_pauli_rot_and_measure.py index 94580d0851..d3035d5c7b 100644 --- a/frontend/test/pytest/test_pauli_rot_and_measure.py +++ b/frontend/test/pytest/test_pauli_rot_and_measure.py @@ -78,6 +78,7 @@ def f(): assert "pbc.ppr" in optimized_ir +@pytest.mark.skipif(not hasattr(qp, "PPR"), reason="PennyLane PPR is not installed") def test_ppr_operator_capture(): """Test that PPR remains a generic operator before applying to_ppr.""" pipe = [("pipe", ["quantum-compilation-stage"])] @@ -96,6 +97,7 @@ def f(): assert "pbc.ppr" not in optimized_ir +@pytest.mark.skipif(not hasattr(qp, "PPR"), reason="PennyLane PPR is not installed") def test_ppr_operator_to_ppr(): """Test that to_ppr converts a PPR operator to pbc.ppr.""" pipe = [("pipe", ["quantum-compilation-stage"])] From 37bbec3eebccf31471fb972f23e001555768e050 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Fri, 4 Sep 2026 21:39:20 +0200 Subject: [PATCH 15/27] Revert "test: skip PPR tests before PennyLane support" This reverts commit b0baf11ca1a816b9af581d8e9e2f617fce122b92. --- frontend/test/pytest/test_pauli_rot_and_measure.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/test/pytest/test_pauli_rot_and_measure.py b/frontend/test/pytest/test_pauli_rot_and_measure.py index d3035d5c7b..94580d0851 100644 --- a/frontend/test/pytest/test_pauli_rot_and_measure.py +++ b/frontend/test/pytest/test_pauli_rot_and_measure.py @@ -78,7 +78,6 @@ def f(): assert "pbc.ppr" in optimized_ir -@pytest.mark.skipif(not hasattr(qp, "PPR"), reason="PennyLane PPR is not installed") def test_ppr_operator_capture(): """Test that PPR remains a generic operator before applying to_ppr.""" pipe = [("pipe", ["quantum-compilation-stage"])] @@ -97,7 +96,6 @@ def f(): assert "pbc.ppr" not in optimized_ir -@pytest.mark.skipif(not hasattr(qp, "PPR"), reason="PennyLane PPR is not installed") def test_ppr_operator_to_ppr(): """Test that to_ppr converts a PPR operator to pbc.ppr.""" pipe = [("pipe", ["quantum-compilation-stage"])] From 11f7488159199c609555f74350a03f30cc114d9a Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 10 Sep 2026 15:56:15 +0200 Subject: [PATCH 16/27] docs: design adjoint PPR lowering Co-authored-by: Cursor --- .../2026-09-10-ppr-to-ppm-adjoint-design.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-10-ppr-to-ppm-adjoint-design.md diff --git a/docs/superpowers/specs/2026-09-10-ppr-to-ppm-adjoint-design.md b/docs/superpowers/specs/2026-09-10-ppr-to-ppm-adjoint-design.md new file mode 100644 index 0000000000..8f8ce9a6ef --- /dev/null +++ b/docs/superpowers/specs/2026-09-10-ppr-to-ppm-adjoint-design.md @@ -0,0 +1,41 @@ +# Resolve Adjoint PPRs Before PPM Lowering + +## Goal + +Ensure `ppr-to-ppm` never lowers a Pauli product rotation while it is still +nested inside `quantum.adjoint`. The pass must first resolve the adjoint, +including reversing operation order and negating each PPR rotation kind, and +only then decompose PPRs into PPMs. + +## Design + +Expose the existing adjoint-lowering rewrite-pattern population function from +the Quantum transforms library. At the start of `PPRToPPMPass::runOnOperation`, +apply those patterns greedily to the module. Reuse is important because the +existing implementation already handles SSA remapping, reversed operation +order, nested control-flow requirements, and PPR angle negation. + +After adjoint lowering succeeds, retain the current two decomposition phases: + +1. Decompose non-Clifford PPRs. +2. Decompose Clifford PPRs. + +If adjoint lowering fails, signal pass failure and do not attempt PPR +decomposition. + +## Build Integration + +Link the PBC transforms library against the Quantum transforms library so +`ppr-to-ppm` can populate the shared adjoint-lowering patterns. + +## Testing + +Extend the `PPRToPPM.mlir` lit test with an adjoint region containing PPRs. +Check that: + +- `quantum.adjoint` is absent after the pass. +- PPRs are processed in reverse order. +- Their rotation kinds are negated before decomposition. +- No PPM remains nested beneath an adjoint operation. + +The test is added and observed failing before production code changes. From ba7f7cdd5bf7dd8f14cfcb1260d16944ae645316 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 10 Sep 2026 16:09:32 +0200 Subject: [PATCH 17/27] docs: plan adjoint PPR lowering Co-authored-by: Cursor --- .../plans/2026-09-10-ppr-to-ppm-adjoint.md | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-10-ppr-to-ppm-adjoint.md diff --git a/docs/superpowers/plans/2026-09-10-ppr-to-ppm-adjoint.md b/docs/superpowers/plans/2026-09-10-ppr-to-ppm-adjoint.md new file mode 100644 index 0000000000..69d7a60d37 --- /dev/null +++ b/docs/superpowers/plans/2026-09-10-ppr-to-ppm-adjoint.md @@ -0,0 +1,180 @@ +# Adjoint PPR-to-PPM Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `ppr-to-ppm` resolve `quantum.adjoint` regions before decomposing their PPR operations into PPM operations. + +**Architecture:** Reuse the existing adjoint-lowering rewrite implementation by exposing its pattern-population function through the Quantum transforms pattern API. `PPRToPPMPass` applies those patterns first, then retains its existing non-Clifford and Clifford decomposition phases. + +**Tech Stack:** C++20, MLIR rewrite patterns and passes, CMake, LLVM lit/FileCheck. + +## Global Constraints + +- Reuse the canonical adjoint lowering rather than duplicating PPR-specific SSA reversal logic. +- A PPR inside an adjoint has its operation order reversed and its rotation kind negated before PPR decomposition. +- Any adjoint-lowering failure fails `ppr-to-ppm` before decomposition begins. +- Preserve the existing non-Clifford-then-Clifford decomposition order. + +--- + +### Task 1: Resolve adjoints before PPR decomposition + +**Files:** +- Modify: `mlir/test/PBC/PPRToPPM.mlir` +- Modify: `mlir/include/Quantum/Transforms/Patterns.h` +- Modify: `mlir/lib/Quantum/Transforms/AdjointLowering/AdjointLowering.cpp` +- Modify: `mlir/lib/PBC/Transforms/ppr_to_ppm.cpp` +- Modify: `mlir/lib/PBC/Transforms/CMakeLists.txt` + +**Interfaces:** +- Consumes: Existing `AdjointSingleOpRewritePattern`, `applyPatternsGreedily`, and PPR decomposition pattern-population functions. +- Produces: `void catalyst::quantum::populateAdjointLoweringPatterns(mlir::RewritePatternSet &patterns)`. + +- [ ] **Step 1: Write the failing regression test** + +Append an input section to `mlir/test/PBC/PPRToPPM.mlir`. Adjoint lowering must +reverse `Z(4)` followed by `X(-4)` into `X(4)` followed by `Z(-4)`. Clifford +decomposition represents those signs with a negated `["X", "Y"]` PPM followed +by a non-negated `["Z", "Y"]` PPM. + +```mlir +// ----- + +func.func @test_ppr_to_ppm_adjoint(%q0 : !quantum.bit) -> !quantum.bit { + %0 = quantum.adjoint(%q0) : !quantum.bit { + ^bb0(%arg0: !quantum.bit): + %1 = pbc.ppr ["Z"](4) %arg0 : !quantum.bit + %2 = pbc.ppr ["X"](-4) %1 : !quantum.bit + quantum.yield %2 : !quantum.bit + } + return %0 : !quantum.bit + + // CHECK-LABEL: @test_ppr_to_ppm_adjoint + // CHECK-NOT: quantum.adjoint + // CHECK: pbc.ppm ["X", "Y"](-) %q0 + // CHECK: pbc.ppm ["Z", "Y"] {{.*}} + // CHECK-NOT: quantum.adjoint + // CHECK: return +} +``` + +- [ ] **Step 2: Run the regression test and verify RED** + +Run: + +```bash +build/bin/quantum-opt --ppr-to-ppm --split-input-file -verify-diagnostics mlir/test/PBC/PPRToPPM.mlir | build/bin/FileCheck mlir/test/PBC/PPRToPPM.mlir --check-prefix=CHECK +``` + +Expected: FAIL in `test_ppr_to_ppm_adjoint` because current output retains `quantum.adjoint` and lowers the positive PPR to a negated PPM inside it. + +- [ ] **Step 3: Expose the shared adjoint-lowering patterns** + +Add this declaration to the `catalyst::quantum` namespace in `mlir/include/Quantum/Transforms/Patterns.h`: + +```cpp +void populateAdjointLoweringPatterns(mlir::RewritePatternSet &patterns); +``` + +In `mlir/lib/Quantum/Transforms/AdjointLowering/AdjointLowering.cpp`, include the public pattern header: + +```cpp +#include "Quantum/Transforms/Patterns.h" +``` + +Define the population function after the anonymous namespace: + +```cpp +namespace catalyst { +namespace quantum { + +void populateAdjointLoweringPatterns(RewritePatternSet &patterns) { + patterns.add(patterns.getContext(), 1); +} + +} // namespace quantum +} // namespace catalyst +``` + +Replace the pass-local `patterns.add(...)` call with: + +```cpp +populateAdjointLoweringPatterns(patterns); +``` + +- [ ] **Step 4: Apply adjoint lowering first in `ppr-to-ppm`** + +Add the public Quantum pattern include to `mlir/lib/PBC/Transforms/ppr_to_ppm.cpp`: + +```cpp +#include "Quantum/Transforms/Patterns.h" +``` + +At the start of `runOnOperation`, before constructing non-Clifford patterns, add: + +```cpp +RewritePatternSet adjoint_patterns(ctx); +quantum::populateAdjointLoweringPatterns(adjoint_patterns); + +if (failed(applyPatternsGreedily(module, std::move(adjoint_patterns)))) { + return signalPassFailure(); +} +``` + +- [ ] **Step 5: Link the shared implementation** + +Add `quantum-transforms` to `LIBS` in `mlir/lib/PBC/Transforms/CMakeLists.txt`: + +```cmake +set(LIBS + ${dialect_libs} + ${conversion_libs} + MLIRPBC + PBCUtils + PBCAnalysis + quantum-transforms +) +``` + +- [ ] **Step 6: Build the affected tools** + +Run: + +```bash +cmake --build build --target quantum-opt FileCheck -j2 +``` + +Expected: Build succeeds without compile or link errors. + +- [ ] **Step 7: Run the focused test and verify GREEN** + +Run: + +```bash +build/bin/quantum-opt --ppr-to-ppm --split-input-file -verify-diagnostics mlir/test/PBC/PPRToPPM.mlir | build/bin/FileCheck mlir/test/PBC/PPRToPPM.mlir --check-prefix=CHECK +``` + +Expected: PASS. The output contains no `quantum.adjoint`; the reversed +`X(-4)` decomposes first as a negated `["X", "Y"]` PPM, and the reversed +`Z(4)` decomposes second as a non-negated `["Z", "Y"]` PPM. + +- [ ] **Step 8: Run adjacent adjoint and PBC tests** + +Run: + +```bash +build/bin/llvm-lit -sv mlir/test/PBC/PPRToPPM.mlir mlir/test/PBC/AdjointTest.mlir mlir/test/Quantum/AdjointTest.mlir +``` + +Expected: All selected tests pass. + +- [ ] **Step 9: Commit the implementation** + +```bash +git add mlir/test/PBC/PPRToPPM.mlir \ + mlir/include/Quantum/Transforms/Patterns.h \ + mlir/lib/Quantum/Transforms/AdjointLowering/AdjointLowering.cpp \ + mlir/lib/PBC/Transforms/ppr_to_ppm.cpp \ + mlir/lib/PBC/Transforms/CMakeLists.txt +git commit -m "fix: resolve adjoint PPRs before PPM lowering" +``` From 6af8a394ef4e8b5da5b6b3228fac567284137273 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Mon, 14 Sep 2026 15:49:28 +0200 Subject: [PATCH 18/27] tiny --- doc/releases/changelog-dev.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 040d1caa4e..1b74229fc0 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -487,8 +487,7 @@ * 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``, without - first decomposing it through ``PauliRot``. +* ``to_ppr`` now directly lowers PennyLane's discrete ``PPR`` operator to ``pbc.ppr``. [(#3185)](https://github.com/PennyLaneAI/catalyst/pull/3185)

Breaking changes 💔

From a8f598b147a9c44fe34e7c92c0f6dffd552f3a53 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Mon, 14 Sep 2026 18:04:20 +0200 Subject: [PATCH 19/27] new angle convention --- frontend/catalyst/passes/builtin_passes.py | 4 ++-- frontend/test/pytest/test_pauli_rot_and_measure.py | 2 +- mlir/lib/PBC/Transforms/ToPPR.cpp | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/catalyst/passes/builtin_passes.py b/frontend/catalyst/passes/builtin_passes.py index 307c2d71ac..d47d768a41 100644 --- a/frontend/catalyst/passes/builtin_passes.py +++ b/frontend/catalyst/passes/builtin_passes.py @@ -966,8 +966,8 @@ 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`` and ``qp.PPR`` differs from Catalyst's - angle convention for PPRs by a factor of two. + 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** diff --git a/frontend/test/pytest/test_pauli_rot_and_measure.py b/frontend/test/pytest/test_pauli_rot_and_measure.py index 8b258e2439..c1ae9934c6 100644 --- a/frontend/test/pytest/test_pauli_rot_and_measure.py +++ b/frontend/test/pytest/test_pauli_rot_and_measure.py @@ -111,7 +111,7 @@ def f(): return f() optimized_ir = test_ppr_operator_to_ppr_workflow.mlir_opt - assert 'pbc.ppr ["X", "Y"](8)' in optimized_ir + assert 'pbc.ppr ["X", "Y"](4)' in optimized_ir assert 'quantum.operator "PPR"' not in optimized_ir assert "quantum.paulirot" not in optimized_ir diff --git a/mlir/lib/PBC/Transforms/ToPPR.cpp b/mlir/lib/PBC/Transforms/ToPPR.cpp index c6145c9a48..b3ad0445b0 100644 --- a/mlir/lib/PBC/Transforms/ToPPR.cpp +++ b/mlir/lib/PBC/Transforms/ToPPR.cpp @@ -420,8 +420,8 @@ LogicalResult convertPPROperator(OperatorOp op, ConversionPatternRewriter &rewri auto denominatorAttr = staticData.getAs("angle_denominator"); int64_t denominator = denominatorAttr.getInt(); - if (denominator != 1 && denominator != -1 && denominator != 2 && denominator != -2 && - denominator != 4 && denominator != -4) { + if (denominator != 2 && denominator != -2 && denominator != 4 && denominator != -4 && + denominator != 8 && denominator != -8) { return op.emitOpError("unsupported PPR angle denominator: ") << denominator; } @@ -441,14 +441,14 @@ LogicalResult convertPPROperator(OperatorOp op, ConversionPatternRewriter &rewri SmallVector pauliCharacters; pauliCharacters.reserve(pauliWord.size()); for (char pauli : pauliWord) { - if (pauli != 'X' && pauli != 'Y' && pauli != 'Z') { - return op.emitOpError("PPR operator Pauli word may contain only X, Y, or Z"); + if (pauli != 'X' && pauli != 'Y' && pauli != 'Z' && pauli != 'I') { + return op.emitOpError("PPR operator Pauli word may contain only X, Y, Z and I"); } pauliCharacters.push_back(rewriter.getStringAttr(StringRef(&pauli, 1))); } ArrayAttr pauliProduct = rewriter.getArrayAttr(pauliCharacters); - int8_t rotationKind = static_cast(2 * denominator); + int8_t rotationKind = static_cast(denominator); if (op.getAdjoint()) { rotationKind = -rotationKind; } From 642684be2531ddb04907df1d44b6c3c0c578142b Mon Sep 17 00:00:00 2001 From: dwierichs Date: Mon, 14 Sep 2026 18:05:21 +0200 Subject: [PATCH 20/27] test as well --- mlir/test/PBC/ToPPRTest.mlir | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mlir/test/PBC/ToPPRTest.mlir b/mlir/test/PBC/ToPPRTest.mlir index c103510ac0..2b44a0985b 100644 --- a/mlir/test/PBC/ToPPRTest.mlir +++ b/mlir/test/PBC/ToPPRTest.mlir @@ -299,7 +299,7 @@ func.func private @"some_decomp_rule"(%arg0: tensor<1xf64>, %arg1: tensor<1xi64> // 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"](8) [[q0:%.+]], [[q1:%.+]] + // 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 @@ -312,13 +312,13 @@ func.func @test_ppr_operator_to_ppr(%q0 : !quantum.bit, %q1 : !quantum.bit) { // 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"](-4) [[q0:%.+]] + // 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"](-2) [[q0_0]] + // CHECK: pbc.ppr ["X"](-8) [[q0_0]] %1 = quantum.operator "PPR"() adj qubits(%0) - static_data = {angle_denominator = 1 : i64, pauli_word = "X"} + static_data = {angle_denominator = 8 : i64, pauli_word = "X"} // CHECK-NOT: quantum.operator // CHECK: return func.return From becba4de3dc5f57b513fd64428c76ea0ec40b794 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 15 Sep 2026 11:44:51 +0200 Subject: [PATCH 21/27] update passes docs --- frontend/catalyst/passes/builtin_passes.py | 52 ++++++++++------------ 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/frontend/catalyst/passes/builtin_passes.py b/frontend/catalyst/passes/builtin_passes.py index d47d768a41..e9ba3f5bdb 100644 --- a/frontend/catalyst/passes/builtin_passes.py +++ b/frontend/catalyst/passes/builtin_passes.py @@ -1064,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) @@ -1073,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)) @@ -1105,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). @@ -1169,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]) @@ -1269,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 @@ -1277,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)) @@ -1491,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)) From 3f6aa57d10d98aad83fd32264a5fe80e8bedb4f4 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 15 Sep 2026 11:47:50 +0200 Subject: [PATCH 22/27] remove helper files --- .../plans/2026-09-10-ppr-to-ppm-adjoint.md | 180 ------------------ .../2026-09-10-ppr-to-ppm-adjoint-design.md | 41 ---- 2 files changed, 221 deletions(-) delete mode 100644 docs/superpowers/plans/2026-09-10-ppr-to-ppm-adjoint.md delete mode 100644 docs/superpowers/specs/2026-09-10-ppr-to-ppm-adjoint-design.md diff --git a/docs/superpowers/plans/2026-09-10-ppr-to-ppm-adjoint.md b/docs/superpowers/plans/2026-09-10-ppr-to-ppm-adjoint.md deleted file mode 100644 index 69d7a60d37..0000000000 --- a/docs/superpowers/plans/2026-09-10-ppr-to-ppm-adjoint.md +++ /dev/null @@ -1,180 +0,0 @@ -# Adjoint PPR-to-PPM Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make `ppr-to-ppm` resolve `quantum.adjoint` regions before decomposing their PPR operations into PPM operations. - -**Architecture:** Reuse the existing adjoint-lowering rewrite implementation by exposing its pattern-population function through the Quantum transforms pattern API. `PPRToPPMPass` applies those patterns first, then retains its existing non-Clifford and Clifford decomposition phases. - -**Tech Stack:** C++20, MLIR rewrite patterns and passes, CMake, LLVM lit/FileCheck. - -## Global Constraints - -- Reuse the canonical adjoint lowering rather than duplicating PPR-specific SSA reversal logic. -- A PPR inside an adjoint has its operation order reversed and its rotation kind negated before PPR decomposition. -- Any adjoint-lowering failure fails `ppr-to-ppm` before decomposition begins. -- Preserve the existing non-Clifford-then-Clifford decomposition order. - ---- - -### Task 1: Resolve adjoints before PPR decomposition - -**Files:** -- Modify: `mlir/test/PBC/PPRToPPM.mlir` -- Modify: `mlir/include/Quantum/Transforms/Patterns.h` -- Modify: `mlir/lib/Quantum/Transforms/AdjointLowering/AdjointLowering.cpp` -- Modify: `mlir/lib/PBC/Transforms/ppr_to_ppm.cpp` -- Modify: `mlir/lib/PBC/Transforms/CMakeLists.txt` - -**Interfaces:** -- Consumes: Existing `AdjointSingleOpRewritePattern`, `applyPatternsGreedily`, and PPR decomposition pattern-population functions. -- Produces: `void catalyst::quantum::populateAdjointLoweringPatterns(mlir::RewritePatternSet &patterns)`. - -- [ ] **Step 1: Write the failing regression test** - -Append an input section to `mlir/test/PBC/PPRToPPM.mlir`. Adjoint lowering must -reverse `Z(4)` followed by `X(-4)` into `X(4)` followed by `Z(-4)`. Clifford -decomposition represents those signs with a negated `["X", "Y"]` PPM followed -by a non-negated `["Z", "Y"]` PPM. - -```mlir -// ----- - -func.func @test_ppr_to_ppm_adjoint(%q0 : !quantum.bit) -> !quantum.bit { - %0 = quantum.adjoint(%q0) : !quantum.bit { - ^bb0(%arg0: !quantum.bit): - %1 = pbc.ppr ["Z"](4) %arg0 : !quantum.bit - %2 = pbc.ppr ["X"](-4) %1 : !quantum.bit - quantum.yield %2 : !quantum.bit - } - return %0 : !quantum.bit - - // CHECK-LABEL: @test_ppr_to_ppm_adjoint - // CHECK-NOT: quantum.adjoint - // CHECK: pbc.ppm ["X", "Y"](-) %q0 - // CHECK: pbc.ppm ["Z", "Y"] {{.*}} - // CHECK-NOT: quantum.adjoint - // CHECK: return -} -``` - -- [ ] **Step 2: Run the regression test and verify RED** - -Run: - -```bash -build/bin/quantum-opt --ppr-to-ppm --split-input-file -verify-diagnostics mlir/test/PBC/PPRToPPM.mlir | build/bin/FileCheck mlir/test/PBC/PPRToPPM.mlir --check-prefix=CHECK -``` - -Expected: FAIL in `test_ppr_to_ppm_adjoint` because current output retains `quantum.adjoint` and lowers the positive PPR to a negated PPM inside it. - -- [ ] **Step 3: Expose the shared adjoint-lowering patterns** - -Add this declaration to the `catalyst::quantum` namespace in `mlir/include/Quantum/Transforms/Patterns.h`: - -```cpp -void populateAdjointLoweringPatterns(mlir::RewritePatternSet &patterns); -``` - -In `mlir/lib/Quantum/Transforms/AdjointLowering/AdjointLowering.cpp`, include the public pattern header: - -```cpp -#include "Quantum/Transforms/Patterns.h" -``` - -Define the population function after the anonymous namespace: - -```cpp -namespace catalyst { -namespace quantum { - -void populateAdjointLoweringPatterns(RewritePatternSet &patterns) { - patterns.add(patterns.getContext(), 1); -} - -} // namespace quantum -} // namespace catalyst -``` - -Replace the pass-local `patterns.add(...)` call with: - -```cpp -populateAdjointLoweringPatterns(patterns); -``` - -- [ ] **Step 4: Apply adjoint lowering first in `ppr-to-ppm`** - -Add the public Quantum pattern include to `mlir/lib/PBC/Transforms/ppr_to_ppm.cpp`: - -```cpp -#include "Quantum/Transforms/Patterns.h" -``` - -At the start of `runOnOperation`, before constructing non-Clifford patterns, add: - -```cpp -RewritePatternSet adjoint_patterns(ctx); -quantum::populateAdjointLoweringPatterns(adjoint_patterns); - -if (failed(applyPatternsGreedily(module, std::move(adjoint_patterns)))) { - return signalPassFailure(); -} -``` - -- [ ] **Step 5: Link the shared implementation** - -Add `quantum-transforms` to `LIBS` in `mlir/lib/PBC/Transforms/CMakeLists.txt`: - -```cmake -set(LIBS - ${dialect_libs} - ${conversion_libs} - MLIRPBC - PBCUtils - PBCAnalysis - quantum-transforms -) -``` - -- [ ] **Step 6: Build the affected tools** - -Run: - -```bash -cmake --build build --target quantum-opt FileCheck -j2 -``` - -Expected: Build succeeds without compile or link errors. - -- [ ] **Step 7: Run the focused test and verify GREEN** - -Run: - -```bash -build/bin/quantum-opt --ppr-to-ppm --split-input-file -verify-diagnostics mlir/test/PBC/PPRToPPM.mlir | build/bin/FileCheck mlir/test/PBC/PPRToPPM.mlir --check-prefix=CHECK -``` - -Expected: PASS. The output contains no `quantum.adjoint`; the reversed -`X(-4)` decomposes first as a negated `["X", "Y"]` PPM, and the reversed -`Z(4)` decomposes second as a non-negated `["Z", "Y"]` PPM. - -- [ ] **Step 8: Run adjacent adjoint and PBC tests** - -Run: - -```bash -build/bin/llvm-lit -sv mlir/test/PBC/PPRToPPM.mlir mlir/test/PBC/AdjointTest.mlir mlir/test/Quantum/AdjointTest.mlir -``` - -Expected: All selected tests pass. - -- [ ] **Step 9: Commit the implementation** - -```bash -git add mlir/test/PBC/PPRToPPM.mlir \ - mlir/include/Quantum/Transforms/Patterns.h \ - mlir/lib/Quantum/Transforms/AdjointLowering/AdjointLowering.cpp \ - mlir/lib/PBC/Transforms/ppr_to_ppm.cpp \ - mlir/lib/PBC/Transforms/CMakeLists.txt -git commit -m "fix: resolve adjoint PPRs before PPM lowering" -``` diff --git a/docs/superpowers/specs/2026-09-10-ppr-to-ppm-adjoint-design.md b/docs/superpowers/specs/2026-09-10-ppr-to-ppm-adjoint-design.md deleted file mode 100644 index 8f8ce9a6ef..0000000000 --- a/docs/superpowers/specs/2026-09-10-ppr-to-ppm-adjoint-design.md +++ /dev/null @@ -1,41 +0,0 @@ -# Resolve Adjoint PPRs Before PPM Lowering - -## Goal - -Ensure `ppr-to-ppm` never lowers a Pauli product rotation while it is still -nested inside `quantum.adjoint`. The pass must first resolve the adjoint, -including reversing operation order and negating each PPR rotation kind, and -only then decompose PPRs into PPMs. - -## Design - -Expose the existing adjoint-lowering rewrite-pattern population function from -the Quantum transforms library. At the start of `PPRToPPMPass::runOnOperation`, -apply those patterns greedily to the module. Reuse is important because the -existing implementation already handles SSA remapping, reversed operation -order, nested control-flow requirements, and PPR angle negation. - -After adjoint lowering succeeds, retain the current two decomposition phases: - -1. Decompose non-Clifford PPRs. -2. Decompose Clifford PPRs. - -If adjoint lowering fails, signal pass failure and do not attempt PPR -decomposition. - -## Build Integration - -Link the PBC transforms library against the Quantum transforms library so -`ppr-to-ppm` can populate the shared adjoint-lowering patterns. - -## Testing - -Extend the `PPRToPPM.mlir` lit test with an adjoint region containing PPRs. -Check that: - -- `quantum.adjoint` is absent after the pass. -- PPRs are processed in reverse order. -- Their rotation kinds are negated before decomposition. -- No PPM remains nested beneath an adjoint operation. - -The test is added and observed failing before production code changes. From 037dd82f93fcbb63ed76b64d071e62fdb732e27f Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 15 Sep 2026 12:08:22 +0200 Subject: [PATCH 23/27] fix error test --- mlir/test/PBC/ToPPRTest.mlir | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/test/PBC/ToPPRTest.mlir b/mlir/test/PBC/ToPPRTest.mlir index 2b44a0985b..773b7cc7a6 100644 --- a/mlir/test/PBC/ToPPRTest.mlir +++ b/mlir/test/PBC/ToPPRTest.mlir @@ -337,7 +337,7 @@ func.func @test_ppr_operator_unsupported_angle_denominator(%q : !quantum.bit) { func.func @test_ppr_operator_invalid_pauli_character(%q : !quantum.bit) { // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} - %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator Pauli word may contain only X, Y, or Z}} + %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator Pauli word may contain only X, Y, Z and I}} static_data = {angle_denominator = 4 : i64, pauli_word = "A"} func.return } From 67d67ad225f8964a3d2135e9286887b541525a99 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Wed, 16 Sep 2026 10:16:40 +0200 Subject: [PATCH 24/27] reduce validation --- mlir/lib/PBC/Transforms/ToPPR.cpp | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/mlir/lib/PBC/Transforms/ToPPR.cpp b/mlir/lib/PBC/Transforms/ToPPR.cpp index b3ad0445b0..37480245c4 100644 --- a/mlir/lib/PBC/Transforms/ToPPR.cpp +++ b/mlir/lib/PBC/Transforms/ToPPR.cpp @@ -417,38 +417,17 @@ LogicalResult convertPPROperator(OperatorOp op, ConversionPatternRewriter &rewri } DictionaryAttr staticData = op.getStaticData(); - auto denominatorAttr = staticData.getAs("angle_denominator"); - - int64_t denominator = denominatorAttr.getInt(); - if (denominator != 2 && denominator != -2 && denominator != 4 && denominator != -4 && - denominator != 8 && denominator != -8) { - return op.emitOpError("unsupported PPR angle denominator: ") << denominator; - } - auto pauliWordAttr = staticData.getAs("pauli_word"); - if (!pauliWordAttr) { - return op.emitOpError("PPR operator requires a string 'pauli_word' in static_data"); - } - StringRef pauliWord = pauliWordAttr.getValue(); - if (pauliWord.empty()) { - return op.emitOpError("PPR operator requires a non-empty Pauli word"); - } - if (pauliWord.size() != op.getInQubits().size()) { - return op.emitOpError("PPR operator requires one Pauli character per input qubit"); - } - SmallVector pauliCharacters; pauliCharacters.reserve(pauliWord.size()); for (char pauli : pauliWord) { - if (pauli != 'X' && pauli != 'Y' && pauli != 'Z' && pauli != 'I') { - return op.emitOpError("PPR operator Pauli word may contain only X, Y, Z and I"); - } pauliCharacters.push_back(rewriter.getStringAttr(StringRef(&pauli, 1))); } - ArrayAttr pauliProduct = rewriter.getArrayAttr(pauliCharacters); - int8_t rotationKind = static_cast(denominator); + + auto denominatorAttr = staticData.getAs("angle_denominator"); + int8_t rotationKind = static_cast(denominatorAttr.getInt()); if (op.getAdjoint()) { rotationKind = -rotationKind; } From d1e8ac73180dc65648ba01a5f34753dbc88593c8 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Wed, 16 Sep 2026 10:17:30 +0200 Subject: [PATCH 25/27] remove validation tests --- mlir/test/PBC/ToPPRTest.mlir | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/mlir/test/PBC/ToPPRTest.mlir b/mlir/test/PBC/ToPPRTest.mlir index 773b7cc7a6..5383e7f691 100644 --- a/mlir/test/PBC/ToPPRTest.mlir +++ b/mlir/test/PBC/ToPPRTest.mlir @@ -323,30 +323,3 @@ func.func @test_negative_and_adjoint_ppr_operator(%q0 : !quantum.bit) { // CHECK: return func.return } - -// ----- - -func.func @test_ppr_operator_unsupported_angle_denominator(%q : !quantum.bit) { - // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} - %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{unsupported PPR angle denominator: 3}} - static_data = {angle_denominator = 3 : i64, pauli_word = "X"} - func.return -} - -// ----- - -func.func @test_ppr_operator_invalid_pauli_character(%q : !quantum.bit) { - // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} - %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator Pauli word may contain only X, Y, Z and I}} - static_data = {angle_denominator = 4 : i64, pauli_word = "A"} - func.return -} - -// ----- - -func.func @test_ppr_operator_pauli_qubit_count_mismatch(%q : !quantum.bit) { - // expected-error @+1 {{failed to legalize operation 'quantum.operator' that was explicitly marked illegal}} - %0 = quantum.operator "PPR"() qubits(%q) // expected-error @+0 {{PPR operator requires one Pauli character per input qubit}} - static_data = {angle_denominator = 4 : i64, pauli_word = "XY"} - func.return -} From 1d6fa1e3dd8a0e0efcf729d86bffa8678aa9bb99 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Wed, 16 Sep 2026 14:50:18 +0200 Subject: [PATCH 26/27] bump PL version --- .dep-versions | 2 +- doc/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.dep-versions b/.dep-versions index 2543c53a1c..4ff9b046c2 100644 --- a/.dep-versions +++ b/.dep-versions @@ -8,7 +8,7 @@ enzyme=v0.0.238 # For a custom PL version, update the package version here and at # 'doc/requirements.txt' -pennylane=0.46.0.dev94 +pennylane=0.46.0.dev100 # For a custom LQ/LK version, update the package version here and at # 'doc/requirements.txt' diff --git a/doc/requirements.txt b/doc/requirements.txt index c6a3225008..ea81e43a9a 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -34,4 +34,4 @@ lxml_html_clean --extra-index-url https://test.pypi.org/simple/ pennylane-lightning-kokkos==0.46.0-dev27 pennylane-lightning==0.46.0-dev27 -pennylane==0.46.0.dev94 +pennylane==0.46.0.dev100 From 59055aa48caf4aea4791f322b01cad2716c5cbb2 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 17 Sep 2026 09:28:08 +0200 Subject: [PATCH 27/27] assert instead of error check --- mlir/lib/PBC/Transforms/ToPPR.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mlir/lib/PBC/Transforms/ToPPR.cpp b/mlir/lib/PBC/Transforms/ToPPR.cpp index 37480245c4..597960bf84 100644 --- a/mlir/lib/PBC/Transforms/ToPPR.cpp +++ b/mlir/lib/PBC/Transforms/ToPPR.cpp @@ -412,9 +412,7 @@ LogicalResult convertPauliRotGate(PauliRotOp op, ConversionPatternRewriter &rewr } LogicalResult convertPPROperator(OperatorOp op, ConversionPatternRewriter &rewriter) { - if (!op.getAllParams().empty()) { - return op.emitOpError("PPR operator does not support dynamic parameters"); - } + assert(op.getAllParams().empty() && "PPR operator does not support dynamic parameters"); DictionaryAttr staticData = op.getStaticData(); auto pauliWordAttr = staticData.getAs("pauli_word");