From 6c4bced9af8cdc6f097a4781dda75c76ba33b16e Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:53:54 -0500 Subject: [PATCH 1/3] Revert RUNTIME_OPERATIONS in the frontend --- doc/releases/changelog-dev.md | 6 --- frontend/catalyst/device/qjit_device.py | 54 ++++++++++++++++++----- frontend/test/pytest/test_preprocess.py | 8 ++-- frontend/test/pytest/test_verification.py | 22 ++++----- 4 files changed, 58 insertions(+), 32 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 0e27eecf96..b0fea6dd2a 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -5,12 +5,6 @@ * Added ``catalyst.switch``, a qjit compatible, index-switch style control flow decorator. [(#2171)](https://github.com/PennyLaneAI/catalyst/pull/2171) -* Catalyst can now compile circuits that are directly expressed in terms of Pauli product rotation - (PPR) and Pauli product measurement (PPM) operations: :class:`~.PauliRot` and - :func:`~.pauli_measure`, respectively. This support enables research and development - spurred from `A Game of Surface Codes (arXiv1808.02892) `_. - [(#2145)](https://github.com/PennyLaneAI/catalyst/pull/2145) - :class:`~.PauliRot` and :func:`~.pauli_measure` can be manipulated with Catalyst's existing passes for PPR-PPM compilation, which includes :func:`catalyst.passes.to_ppr`, :func:`catalyst.passes.commute_ppr`, :func:`catalyst.passes.merge_ppr_ppm`, diff --git a/frontend/catalyst/device/qjit_device.py b/frontend/catalyst/device/qjit_device.py index 848a638d48..9c245aeea3 100644 --- a/frontend/catalyst/device/qjit_device.py +++ b/frontend/catalyst/device/qjit_device.py @@ -56,6 +56,44 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) +RUNTIME_OPERATIONS = [ + "CNOT", + "ControlledPhaseShift", + "CRot", + "CRX", + "CRY", + "CRZ", + "CSWAP", + "CY", + "CZ", + "Hadamard", + "Identity", + "IsingXX", + "IsingXY", + "IsingYY", + "IsingZZ", + "SingleExcitation", + "DoubleExcitation", + "ISWAP", + "MultiRZ", + "PauliX", + "PauliY", + "PauliZ", + "PCPhase", + "PhaseShift", + "PSWAP", + "QubitUnitary", + "Rot", + "RX", + "RY", + "RZ", + "S", + "SWAP", + "T", + "Toffoli", + "GlobalPhase", +] + RUNTIME_OBSERVABLES = [ "Identity", "PauliX", @@ -71,9 +109,11 @@ RUNTIME_MPS = ["ExpectationMP", "SampleMP", "VarianceMP", "CountsMP", "StateMP", "ProbabilityMP"] -# A list of custom operations supported by the Catalyst compiler. -# This is useful especially for testing a device with custom operations. -CUSTOM_OPERATIONS = {} +# The runtime interface does not care about specific gate properties, so set them all to True. +RUNTIME_OPERATIONS = { + op: OperatorProperties(invertible=True, controllable=True, differentiable=True) + for op in RUNTIME_OPERATIONS +} RUNTIME_OBSERVABLES = { obs: OperatorProperties(invertible=True, controllable=True, differentiable=True) @@ -159,14 +199,6 @@ def extract_backend_info(device: qml.devices.QubitDevice) -> BackendInfo: return BackendInfo(dname, device_name, device_lpath, device_kwargs) -def union_operations( - a: Dict[str, OperatorProperties], b: Dict[str, OperatorProperties] -) -> Dict[str, OperatorProperties]: - """Union of two sets of operator properties""" - return {**a, **b} - # return {k: a[k] & b[k] for k in (a.keys() & b.keys())} - - def intersect_operations( a: Dict[str, OperatorProperties], b: Dict[str, OperatorProperties] ) -> Dict[str, OperatorProperties]: diff --git a/frontend/test/pytest/test_preprocess.py b/frontend/test/pytest/test_preprocess.py index 916e3e7c97..a918ec09d4 100644 --- a/frontend/test/pytest/test_preprocess.py +++ b/frontend/test/pytest/test_preprocess.py @@ -118,14 +118,14 @@ def test_decompose_integration(self): @qml.qnode(dev) def circuit(theta: float): - qml.OrbitalRotation(theta, wires=[0, 1, 2, 3]) + qml.SingleExcitationPlus(theta, wires=[0, 1]) return qml.state() mlir = qjit(circuit, target="mlir").mlir - assert "SingleExcitation" in mlir assert "Hadamard" in mlir - assert "RX" in mlir - assert "OrbitalRotation" not in mlir + assert "CNOT" in mlir + assert "RY" in mlir + assert "SingleExcitationPlus" not in mlir def test_decompose_ops_to_unitary(self): """Test the decompose ops to unitary transform.""" diff --git a/frontend/test/pytest/test_verification.py b/frontend/test/pytest/test_verification.py index b88cf26642..e50d5c10ce 100644 --- a/frontend/test/pytest/test_verification.py +++ b/frontend/test/pytest/test_verification.py @@ -37,7 +37,7 @@ from catalyst.api_extensions import HybridAdjoint, HybridCtrl from catalyst.compiler import get_lib_path from catalyst.device import get_device_capabilities -from catalyst.device.qjit_device import CUSTOM_OPERATIONS, get_qjit_device_capabilities +from catalyst.device.qjit_device import RUNTIME_OPERATIONS, get_qjit_device_capabilities from catalyst.device.verification import validate_measurements # pylint: disable = unused-argument, unnecessary-lambda-assignment, unnecessary-lambda @@ -290,7 +290,7 @@ def test_non_controllable_gate_hybridctrl(self): # Note: The HybridCtrl operator is not currently supported with the QJIT device, but the # verification structure is in place, so we test the verification of its nested operators by # adding HybridCtrl to the list of native gates for the custom base device and by patching - # the list of CUSTOM_OPERATIONS for the QJIT device to include HybridCtrl for this test. + # the list of RUNTIME_OPERATIONS for the QJIT device to include HybridCtrl for this test. @qml.qnode( get_custom_device( @@ -302,12 +302,12 @@ def f(x: float): assert isinstance(op, HybridCtrl), f"op expected to be HybridCtrl but got {type(op)}" return qml.expval(qml.PauliX(0)) - runtime_ops_with_qctrl = deepcopy(CUSTOM_OPERATIONS) + runtime_ops_with_qctrl = deepcopy(RUNTIME_OPERATIONS) runtime_ops_with_qctrl["HybridCtrl"] = OperatorProperties( invertible=True, controllable=True, differentiable=True ) - with patch("catalyst.device.qjit_device.CUSTOM_OPERATIONS", runtime_ops_with_qctrl): + with patch("catalyst.device.qjit_device.RUNTIME_OPERATIONS", runtime_ops_with_qctrl): with pytest.raises(CompileError, match="PauliZ is not controllable"): qjit(f)(1.2) @@ -321,7 +321,7 @@ def test_hybridctrl_raises_error(self): """Test that a HybridCtrl operator is rejected by the verification.""" # TODO: If you are deleting this test because HybridCtrl support has been added, consider - # updating the tests that patch CUSTOM_OPERATIONS to inclue HybridCtrl accordingly + # updating the tests that patch RUNTIME_OPERATIONS to inclue HybridCtrl accordingly @qml.qnode(get_custom_device(non_controllable_gates={"PauliZ"}, wires=4)) def f(x: float): @@ -391,7 +391,7 @@ def test_hybrid_ctrl_containing_adjoint(self, adjoint_type, unsupported_gate_att # Note: The HybridCtrl operator is not currently supported with the QJIT device, but the # verification structure is in place, so we test the verification of its nested operators by # adding HybridCtrl to the list of native gates for the custom base device and by patching - # the list of CUSTOM_OPERATIONS for the QJIT device to include HybridCtrl for this test. + # the list of RUNTIME_OPERATIONS for the QJIT device to include HybridCtrl for this test. def _ops(x, wires): if adjoint_type == HybridAdjoint: @@ -410,12 +410,12 @@ def f(x: float): assert isinstance(base, adjoint_type), f"expected {adjoint_type} but got {type(op)}" return qml.expval(qml.PauliX(0)) - runtime_ops_with_qctrl = deepcopy(CUSTOM_OPERATIONS) + runtime_ops_with_qctrl = deepcopy(RUNTIME_OPERATIONS) runtime_ops_with_qctrl["HybridCtrl"] = OperatorProperties( invertible=True, controllable=True, differentiable=True ) - with patch("catalyst.device.qjit_device.CUSTOM_OPERATIONS", runtime_ops_with_qctrl): + with patch("catalyst.device.qjit_device.RUNTIME_OPERATIONS", runtime_ops_with_qctrl): with pytest.raises(CompileError, match=f"PauliZ is not {unsupported_gate_attribute}"): qjit(f)(1.2) @@ -434,7 +434,7 @@ def test_hybrid_adjoint_containing_hybrid_ctrl(self, ctrl_type, unsupported_gate # Note: The HybridCtrl operator is not currently supported with the QJIT device, but the # verification structure is in place, so we test the verification of its nested operators by # adding HybridCtrl to the list of native gates for the custom base device and by patching - # the list of CUSTOM_OPERATIONS for the QJIT device to include HybridCtrl for this test. + # the list of RUNTIME_OPERATIONS for the QJIT device to include HybridCtrl for this test. def _ops(x, wires): if ctrl_type == HybridCtrl: @@ -453,12 +453,12 @@ def f(x: float): assert isinstance(base, ctrl_type), f"expected {ctrl_type} but got {type(op)}" return qml.expval(qml.PauliX(0)) - runtime_ops_with_qctrl = deepcopy(CUSTOM_OPERATIONS) + runtime_ops_with_qctrl = deepcopy(RUNTIME_OPERATIONS) runtime_ops_with_qctrl["HybridCtrl"] = OperatorProperties( invertible=True, controllable=True, differentiable=True ) - with patch("catalyst.device.qjit_device.CUSTOM_OPERATIONS", runtime_ops_with_qctrl): + with patch("catalyst.device.qjit_device.RUNTIME_OPERATIONS", runtime_ops_with_qctrl): with pytest.raises(CompileError, match=f"PauliZ is not {unsupported_gate_attribute}"): qjit(f)(1.2) From 983fc369f0ed92e1e54d0642ceaefbd468e67a18 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:59:11 -0500 Subject: [PATCH 2/3] Up --- frontend/catalyst/device/qjit_device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/catalyst/device/qjit_device.py b/frontend/catalyst/device/qjit_device.py index 9c245aeea3..b4f92bf022 100644 --- a/frontend/catalyst/device/qjit_device.py +++ b/frontend/catalyst/device/qjit_device.py @@ -223,8 +223,8 @@ def get_qjit_device_capabilities(target_capabilities: DeviceCapabilities) -> Dev qjit_capabilities = deepcopy(target_capabilities) # Intersection of gates and observables supported by the device and by Catalyst runtime. - qjit_capabilities.operations = union_operations( - target_capabilities.operations, CUSTOM_OPERATIONS + qjit_capabilities.operations = intersect_operations( + target_capabilities.operations, RUNTIME_OPERATIONS ) qjit_capabilities.observables = intersect_operations( target_capabilities.observables, RUNTIME_OBSERVABLES From a4ba7f659d8db5a5980326d11d15c8f1f7597e4b Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Mon, 1 Dec 2025 11:27:35 -0500 Subject: [PATCH 3/3] Update changelog --- doc/releases/changelog-dev.md | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 741f88944e..177698fdd2 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -5,13 +5,19 @@ * Added ``catalyst.switch``, a qjit compatible, index-switch style control flow decorator. [(#2171)](https://github.com/PennyLaneAI/catalyst/pull/2171) +* Catalyst can now compile circuits that are directly expressed in terms of Pauli product rotation + (PPR) and Pauli product measurement (PPM) operations: :class:`~.PauliRot` and + :func:`~.pauli_measure`, respectively. This support enables research and development + spurred from `A Game of Surface Codes (arXiv1808.02892) `_. + [(#2145)](https://github.com/PennyLaneAI/catalyst/pull/2145) + :class:`~.PauliRot` and :func:`~.pauli_measure` can be manipulated with Catalyst's existing passes - for PPR-PPM compilation, which includes :func:`catalyst.passes.to_ppr`, - :func:`catalyst.passes.commute_ppr`, :func:`catalyst.passes.merge_ppr_ppm`, - :func:`catalyst.passes.ppr_to_ppm`, :func:`catalyst.passes.reduce_t_depth`, and - :func:`catalyst.passes.ppm_compilation`. For clear and inspectable results, use ``target="mlir"`` - in the ``qjit`` decorator, ensure that PennyLane's program capture is enabled, - :func:`pennylane.capture.enable`, and call the Catalyst passes from the PennyLane frontend (e.g., + for PPR-PPM compilation, which includes :func:`catalyst.passes.to_ppr`, + :func:`catalyst.passes.commute_ppr`, :func:`catalyst.passes.merge_ppr_ppm`, + :func:`catalyst.passes.ppr_to_ppm`, :func:`catalyst.passes.reduce_t_depth`, and + :func:`catalyst.passes.ppm_compilation`. For clear and inspectable results, use ``target="mlir"`` + in the ``qjit`` decorator, ensure that PennyLane's program capture is enabled, + :func:`pennylane.capture.enable`, and call the Catalyst passes from the PennyLane frontend (e.g., ``qml.transforms.ppr_to_ppm`` instead of from ``catalyst.passes.``). ```python @@ -48,14 +54,14 @@ ```pycon >>> print(qml.specs(circuit, level="all")()['resources']) { - 'No transforms': ..., + 'No transforms': ..., 'Before MLIR Passes (MLIR-0)': ..., 'ppm-compilation (MLIR-1)': Resources( - num_wires=6, - num_gates=14, - gate_types=defaultdict(, {'PPM-w3': 2, 'PPM-w2': 4, 'PPM-w1': 4, 'PPR-pi/2-w1': 4}), - gate_sizes=defaultdict(, {3: 2, 2: 4, 1: 8}), - depth=None, + num_wires=6, + num_gates=14, + gate_types=defaultdict(, {'PPM-w3': 2, 'PPM-w2': 4, 'PPM-w1': 4, 'PPR-pi/2-w1': 4}), + gate_sizes=defaultdict(, {3: 2, 2: 4, 1: 8}), + depth=None, shots=Shots(total_shots=None, shot_vector=()) ) } @@ -113,7 +119,7 @@ * Dynamically allocated wires can now be passed into control flow and subroutines. [(#2130)](https://github.com/PennyLaneAI/catalyst/pull/2130) -* Catalyst now supports arbitrary angle Pauli product rotations in the QEC dialect. +* Catalyst now supports arbitrary angle Pauli product rotations in the QEC dialect. This will allow :class:`qml.PauliRot` with arbitrary angles to be lowered to QEC dialect. This is implemented as a new `qec.ppr.arbitrary` operation, which takes a Pauli product and an arbitrary angle (as a double) as input.