diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index ef762e4b1d..09750a5557 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -556,6 +556,9 @@ * Added ``CZ`` support to ``to-ppr`` pass. [(#3009)](https://github.com/PennyLaneAI/catalyst/pull/3009) +* ``to_ppr`` now directly lowers PennyLane's discrete ``PPR`` operator to ``pbc.ppr``. + [(#3185)](https://github.com/PennyLaneAI/catalyst/pull/3185) +

Breaking changes 💔

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