diff --git a/frontend/catalyst/python_interface/inspection/specs.py b/frontend/catalyst/python_interface/inspection/specs.py index 16a103d45e..8a4581a3ce 100644 --- a/frontend/catalyst/python_interface/inspection/specs.py +++ b/frontend/catalyst/python_interface/inspection/specs.py @@ -19,13 +19,15 @@ import warnings from typing import TYPE_CHECKING, Literal +from pennylane.workflow.qnode import QNode + +from catalyst.jit import QJIT +from catalyst.passes.pass_api import PassPipelineWrapper + from ..compiler import Compiler from .specs_collector import ResourcesResult, specs_collect from .xdsl_conversion import get_mlir_module -if TYPE_CHECKING: - from catalyst.jit import QJIT - class StopCompilation(Exception): """Custom exception to stop compilation early when the desired specs level is reached.""" @@ -37,7 +39,7 @@ def mlir_specs( """Compute the specs used for a circuit at the level of an MLIR pass. Args: - qnode (QNode): The (QJIT'd) qnode to get the specs for + qnode (QJIT): The (QJIT'd) qnode to get the specs for level (int | tuple[int] | list[int] | "all"): The MLIR pass level to get the specs for *args: Positional arguments to pass to the QNode **kwargs: Keyword arguments to pass to the QNode @@ -46,6 +48,18 @@ def mlir_specs( ResourcesResult | dict[str, ResourcesResult]: The resources for the circuit at the specified level """ + + if not isinstance(qnode, QJIT) or ( + not isinstance(qnode.original_function, QNode) + and not ( + isinstance(qnode.original_function, PassPipelineWrapper) + and isinstance(qnode.original_qnode, QNode) + ) + ): + raise ValueError( + "The provided `qnode` argument does not appear to be a valid QJIT compiled QNode." + ) + cache: dict[int, tuple[ResourcesResult, str]] = {} if args or kwargs: diff --git a/frontend/catalyst/python_interface/inspection/xdsl_conversion.py b/frontend/catalyst/python_interface/inspection/xdsl_conversion.py index 28de37febf..998bf24564 100644 --- a/frontend/catalyst/python_interface/inspection/xdsl_conversion.py +++ b/frontend/catalyst/python_interface/inspection/xdsl_conversion.py @@ -15,6 +15,7 @@ from __future__ import annotations +import copy import inspect from collections.abc import Callable from typing import TYPE_CHECKING @@ -64,9 +65,8 @@ def get_mlir_module(qnode: QNode | QJIT, args, kwargs) -> ModuleOp: return qnode.mlir_module if isinstance(qnode, QJIT): - compile_options = qnode.compile_options + compile_options = copy.copy(qnode.compile_options) compile_options.autograph = False # Autograph has already been applied for `user_function` - compile_options.pass_plugins.add(getXDSLPluginAbsolutePath()) jitted_qnode = QJIT(qnode.user_function, compile_options) else: @@ -449,12 +449,10 @@ def xdsl_to_qml_measurement_name(op, obs_op=None) -> str: gate_name = f"{len(op.qubits)} wires" elif op.name == "quantum.hamiltonian": - ops_list = [xdsl_to_qml_measurement_name(term.owner) for term in op.terms] - gate_name = f"Hamiltonian({', '.join(ops_list)})" + gate_name = f"Hamiltonian(num_terms={len(op.terms)})" elif op.name == "quantum.tensor": - ops_list = [xdsl_to_qml_measurement_name(operand.owner) for operand in op.operands] - gate_name = " @ ".join(ops_list) + gate_name = f"Prod(num_terms={len(op.operands)})" elif op.name == "quantum.namedobs": gate_name = op.type.data.value diff --git a/frontend/test/pytest/python_interface/inspection/test_mlir_specs.py b/frontend/test/pytest/python_interface/inspection/test_mlir_specs.py index eb5cc5cbdc..2f4ab1c605 100644 --- a/frontend/test/pytest/python_interface/inspection/test_mlir_specs.py +++ b/frontend/test/pytest/python_interface/inspection/test_mlir_specs.py @@ -262,6 +262,37 @@ def test_basic_passes_multi_level(self, simple_circuit): ): mlir_specs(simple_circuit, level=[0, 3]) + def test_not_qnode(self): + """Test that a malformed QNode raises an error.""" + + def not_a_qnode(): + pass + + with pytest.raises( + ValueError, + match="The provided `qnode` argument does not appear to be a valid QJIT compiled QNode.", + ): + mlir_specs(not_a_qnode, level=0) + + def test_malformed_qnode(self): + """Test that a QNode without measurements can still be collected.""" + + dev = qml.device("lightning.qubit", wires=1) + + @qml.qjit + @qml.qnode(dev) + def circ(x): + qml.X(0) + + res = mlir_specs(circ, 0, 1) + expected = make_static_resources( + operations={"PauliX": {1: 1}}, + measurements={}, + num_allocs=1, + ) + + assert resources_equal(res, expected) + @pytest.mark.parametrize( "pl_ctrl_flow, iters, autograph", [ @@ -539,8 +570,8 @@ def circ(i: int): expected = make_static_resources( operations={}, measurements={ - "expval(Hamiltonian(PauliZ @ PauliZ))": 1, - "expval(Hamiltonian(PauliX @ PauliZ, PauliZ @ Hadamard))": 1, + "expval(Hamiltonian(num_terms=1))": 1, + "expval(Hamiltonian(num_terms=2))": 1, }, num_allocs=2, ) @@ -552,7 +583,7 @@ def test_ppr(self): """Test that PPRs are handled correctly.""" if qml.capture.enabled(): - pytest.xfail("plxpr currently incompatible to_ppr pass") + pytest.xfail("plxpr currently incompatible with to_ppr pass") pipeline = [("pipe", ["enforce-runtime-invariants-pipeline"])] @@ -569,7 +600,7 @@ def circ(): num_allocs=2, ) - res = mlir_specs(circ, level=1, args=(0,)) + res = mlir_specs(circ, level=1) assert resources_equal(res, expected) def test_subroutine(self): diff --git a/frontend/test/pytest/test_specs.py b/frontend/test/pytest/test_specs.py index 2f5930bd70..a97b849cb5 100644 --- a/frontend/test/pytest/test_specs.py +++ b/frontend/test/pytest/test_specs.py @@ -13,87 +13,510 @@ # limitations under the License. """Tests for qml.specs() Catalyst integration""" +from functools import partial + import pennylane as qml import pytest from jax import numpy as jnp +from pennylane.measurements import Shots +from pennylane.resource import CircuitSpecs, SpecsResources +import catalyst from catalyst import qjit # pylint:disable = protected-access,attribute-defined-outside-init +def check_specs_header_same( + actual: CircuitSpecs, expected: CircuitSpecs, skip_level: bool = False +) -> None: + """Check that two specs dictionaries are the same.""" + assert actual["device_name"] == expected["device_name"] + assert actual["num_device_wires"] == expected["num_device_wires"] + if not skip_level: + assert actual["level"] == expected["level"] + assert actual["shots"] == expected["shots"] + + # TODO: Remove this method once feature parity has been reached, and instead use `==` directly -def check_specs_same(specs1, specs2): +def check_specs_resources_same( + actual_res: ( + SpecsResources | list[SpecsResources] | dict[any, SpecsResources | list[SpecsResources]] + ), + expected_res: ( + SpecsResources | list[SpecsResources] | dict[any, SpecsResources | list[SpecsResources]] + ), + skip_measurements: bool = False, +) -> None: + """Helper function to check if 2 resources objects are the same""" + assert type(actual_res) == type(expected_res) + + if isinstance(actual_res, list): + assert len(actual_res) == len(expected_res) + + for r1, r2 in zip(actual_res, expected_res): + check_specs_resources_same(r1, r2, skip_measurements=skip_measurements) + + elif isinstance(actual_res, dict): + assert len(actual_res) == len(expected_res) + + for k in actual_res.keys(): + assert k in expected_res + check_specs_resources_same( + actual_res[k], expected_res[k], skip_measurements=skip_measurements + ) + + elif isinstance(actual_res, SpecsResources): + assert actual_res.gate_types == expected_res.gate_types + assert actual_res.gate_sizes == expected_res.gate_sizes + + # TODO: Measurements are not yet supported in Catalyst device-level specs + if not skip_measurements: + assert actual_res.measurements == expected_res.measurements + + assert actual_res.num_allocs == expected_res.num_allocs + assert actual_res.depth == expected_res.depth + assert actual_res.num_gates == expected_res.num_gates + + else: + raise ValueError("Invalid Type") + + +def check_specs_same(actual: CircuitSpecs, expected: CircuitSpecs, skip_measurements: bool = False): """Check that two specs dictionaries are the same.""" - assert specs1["device_name"] == specs2["device_name"] - assert specs1["num_device_wires"] == specs2["num_device_wires"] - assert specs1["shots"] == specs2["shots"] + check_specs_header_same(actual, expected) + check_specs_resources_same( + actual["resources"], expected["resources"], skip_measurements=skip_measurements + ) + + +class TestDeviceLevelSpecs: + """Test qml.specs() at device level""" + + def test_simple(self): + """Test a simple case of qml.specs() against PennyLane""" + + dev = qml.device("lightning.qubit", wires=1) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(wires=0) + return qml.expval(qml.PauliZ(0)) + + pl_specs = qml.specs(circuit, level="device")() + cat_specs = qml.specs(qjit(circuit), level="device")() + + assert cat_specs["device_name"] == "lightning.qubit" + check_specs_same(cat_specs, pl_specs, skip_measurements=True) + + def test_complex(self): + """Test a complex case of qml.specs() against PennyLane""" + + dev = qml.device("lightning.qubit", wires=4) + U = 1 / jnp.sqrt(2) * jnp.array([[1, 1], [1, -1]], dtype=jnp.complex128) + + @qml.qnode(dev) + def circuit(): + qml.PauliX(0) + qml.adjoint(qml.T)(0) + qml.ctrl(op=qml.S, control=[1], control_values=[1])(0) + qml.ctrl(op=qml.S, control=[1, 2], control_values=[1, 0])(0) + qml.ctrl(op=qml.adjoint(qml.Y), control=[2], control_values=[1])(0) + qml.CNOT([0, 1]) + + qml.QubitUnitary(U, wires=0) + qml.ControlledQubitUnitary(U, control_values=[1], wires=[1, 0]) + qml.adjoint(qml.QubitUnitary(U, wires=0)) + qml.adjoint(qml.ControlledQubitUnitary(U, control_values=[1, 1], wires=[1, 2, 0])) + + return qml.probs() + + pl_specs = qml.specs(circuit, level="device")() + cat_specs = qml.specs(qjit(circuit), level="device")() + + assert cat_specs["device_name"] == "lightning.qubit" + + # Catalyst will handle Adjoint(PauliY) == PauliY + assert "CY" in cat_specs["resources"].gate_types + cat_specs["resources"].gate_types["C(Adjoint(PauliY))"] = cat_specs["resources"].gate_types[ + "CY" + ] + del cat_specs["resources"].gate_types["CY"] + + check_specs_same(cat_specs, pl_specs, skip_measurements=True) + + +class TestPassByPassSpecs: + """Test qml.specs() pass-by-pass specs""" + + @pytest.fixture + def simple_circuit(self): + """Fixture for a circuit.""" + + @qml.qnode(qml.device("lightning.qubit", wires=2)) + def circ(): + qml.RX(1.0, 0) + qml.RX(2.0, 0) + qml.RZ(3.0, 1) + qml.RZ(4.0, 1) + qml.Hadamard(0) + qml.Hadamard(0) + qml.CNOT([0, 1]) + qml.CNOT([0, 1]) + return qml.probs() + + return circ + + @pytest.mark.usefixtures("use_both_frontend") + def test_invalid_levels(self, simple_circuit): + """Test invalid inputs.""" + + no_passes = qjit(simple_circuit) + with pytest.raises( + check=ValueError, + match="The 'level' argument to qml.specs for QJIT'd QNodes must be " + "non-negative, got -1.", + ): + qml.specs(no_passes, level=-1)() + + with pytest.raises(check=ValueError, match="Requested specs levels 2"): + qml.specs(no_passes, level=2)() + + with pytest.raises(check=ValueError, match="Requested specs levels 2, 3"): + qml.specs(no_passes, level=[2, 3])() + + @pytest.mark.usefixtures("use_both_frontend") + def test_basic_passes_multi_level(self, simple_circuit): + """Test that when passes are applied, the circuit resources are updated accordingly.""" + + simple_circuit = qml.transforms.cancel_inverses(simple_circuit) + simple_circuit = qml.transforms.merge_rotations(simple_circuit) + + simple_circuit = qjit(simple_circuit) + + expected = CircuitSpecs( + device_name="lightning.qubit", + num_device_wires=2, + shots=Shots(None), + level=[ + "Before transforms", + "Before MLIR Passes (MLIR-0)", + "cancel-inverses (MLIR-1)", + "merge-rotations (MLIR-2)", + ], + resources={ + "Before transforms": SpecsResources( + gate_types={"RX": 2, "RZ": 2, "Hadamard": 2, "CNOT": 2}, + gate_sizes={1: 6, 2: 2}, + measurements={"probs(all wires)": 1}, + num_allocs=2, + ), + "Before MLIR Passes (MLIR-0)": SpecsResources( + gate_types={"RX": 2, "RZ": 2, "Hadamard": 2, "CNOT": 2}, + gate_sizes={1: 6, 2: 2}, + measurements={"probs(all wires)": 1}, + num_allocs=2, + ), + "cancel-inverses (MLIR-1)": SpecsResources( + gate_types={"RX": 2, "RZ": 2}, + gate_sizes={1: 4}, + measurements={"probs(all wires)": 1}, + num_allocs=2, + ), + "merge-rotations (MLIR-2)": SpecsResources( + gate_types={"RX": 1, "RZ": 1}, + gate_sizes={1: 2}, + measurements={"probs(all wires)": 1}, + num_allocs=2, + ), + }, + ) + + actual = qml.specs(simple_circuit, level="all")() + + check_specs_same(actual, expected) + + # Test resources at each level match individual specs calls + for i, res in enumerate(actual["resources"].values()): + single_level_specs = qml.specs(simple_circuit, level=i)() + check_specs_header_same(actual, single_level_specs, skip_level=True) + check_specs_resources_same(res, single_level_specs["resources"]) + + def test_marker(self, simple_circuit): + """Test that qml.marker can be used appropriately.""" + + simple_circuit = partial(qml.marker, level="m0")(simple_circuit) + simple_circuit = qml.transforms.cancel_inverses(simple_circuit) + simple_circuit = partial(qml.marker, level="m1")(simple_circuit) + simple_circuit = qml.transforms.merge_rotations(simple_circuit) + simple_circuit = partial(qml.marker, level="m2")(simple_circuit) + + simple_circuit = qjit(simple_circuit) + + expected = CircuitSpecs( + device_name="lightning.qubit", + num_device_wires=2, + shots=Shots(None), + level=["m0", "m1", "m2"], + resources={ + "m0": SpecsResources( + gate_types={"RX": 2, "RZ": 2, "Hadamard": 2, "CNOT": 2}, + gate_sizes={1: 6, 2: 2}, + measurements={"probs(all wires)": 1}, + num_allocs=2, + ), + "m1": SpecsResources( + gate_types={"RX": 2, "RZ": 2}, + gate_sizes={1: 4}, + measurements={"probs(all wires)": 1}, + num_allocs=2, + ), + "m2": SpecsResources( + gate_types={"RX": 1, "RZ": 1}, + gate_sizes={1: 2}, + measurements={"probs(all wires)": 1}, + num_allocs=2, + ), + }, + ) + + actual = qml.specs(simple_circuit, level=["m0", "m1", "m2"])() + + check_specs_same(actual, expected) + + def test_mix_transforms_and_passes(self, simple_circuit): + """Test using a mix of compiler passes and plain tape transforms""" + + simple_circuit = qml.transforms.cancel_inverses( + simple_circuit + ) # Has to be applied as a tape transform + simple_circuit = qml.transforms.undo_swaps( + simple_circuit + ) # No actual swaps to undo, but forces normal tape transform + simple_circuit = qml.transforms.merge_rotations( + simple_circuit + ) # Can be applied as an MLIR pass + + simple_circuit = qjit(simple_circuit) + + actual = qml.specs(simple_circuit, level="all")() + expected = CircuitSpecs( + device_name="lightning.qubit", + num_device_wires=2, + shots=Shots(None), + level=[ + "Before transforms", + "cancel_inverses", + "undo_swaps", + "Before MLIR Passes (MLIR-0)", + "merge-rotations (MLIR-1)", + ], + resources={ + "Before transforms": SpecsResources( + gate_types={"RX": 2, "RZ": 2, "Hadamard": 2, "CNOT": 2}, + gate_sizes={1: 6, 2: 2}, + measurements={"probs(all wires)": 1}, + num_allocs=2, + ), + "cancel_inverses": SpecsResources( + gate_types={"RX": 2, "RZ": 2}, + gate_sizes={1: 4}, + measurements={"probs(all wires)": 1}, + num_allocs=2, + ), + "undo_swaps": SpecsResources( + gate_types={"RX": 2, "RZ": 2}, + gate_sizes={1: 4}, + measurements={"probs(all wires)": 1}, + num_allocs=2, + ), + "Before MLIR Passes (MLIR-0)": SpecsResources( + gate_types={"RX": 2, "RZ": 2}, + gate_sizes={1: 4}, + measurements={"probs(all wires)": 1}, + num_allocs=2, + ), + "merge-rotations (MLIR-1)": SpecsResources( + gate_types={"RX": 1, "RZ": 1}, + gate_sizes={1: 2}, + measurements={"probs(all wires)": 1}, + num_allocs=2, + ), + }, + ) + + check_specs_same(actual, expected) + + @pytest.mark.usefixtures("use_both_frontend") + def test_reprs_match(self): + """Test that when no transforms are applied to a typical circuit, the "Before Transform" + and "Before MLIR Passes" representations match.""" + + dev = qml.device("lightning.qubit", wires=7) + + @qml.qnode(dev) + def circuit(): + qml.StatePrep(jnp.array([0, 1]), wires=0) + + qml.Hadamard(wires=0) + qml.CNOT(wires=[0, 1]) + + qml.GlobalPhase(jnp.pi / 4) + qml.MultiRZ(jnp.pi / 2, wires=[1, 2, 3]) + qml.ctrl(qml.T, control=0)(wires=3) + qml.ctrl(op=qml.IsingXX(0.5, wires=[5, 6]), control=range(5), control_values=[1] * 5) + + qml.QubitUnitary(jnp.array([[1, 0], [0, 1j]]), wires=2) + + return ( + qml.expval(qml.PauliZ(0)), + qml.probs(wires=[0, 1]), + qml.probs(), + qml.state(), + ) + + specs_device = qml.specs(circuit, level=0, compute_depth=False)() + specs_all = qml.specs(qjit(circuit), level="all", compute_depth=False)() + + regular_pl = specs_device["resources"] + before_transforms = specs_all["resources"]["Before transforms"] + before_mlir = specs_all["resources"]["Before MLIR Passes (MLIR-0)"] + + check_specs_resources_same(regular_pl, before_transforms) + check_specs_resources_same(before_transforms, before_mlir) + + @pytest.mark.usefixtures("use_both_frontend") + def test_advanced_measurements(self): + """Test that advanced measurements such as LinearCombination are handled correctly.""" + + dev = qml.device("lightning.qubit", wires=7) + + @qml.qnode(dev, shots=10) + def circ(): + coeffs = [0.2, -0.543] + obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)] + ham = qml.ops.LinearCombination(coeffs, obs) - assert specs1["resources"].gate_types == specs2["resources"].gate_types - assert specs1["resources"].gate_sizes == specs2["resources"].gate_sizes + return ( + qml.expval(ham), + qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)), + qml.sample(wires=3), + qml.sample(), + ) - # Measurements are not yet supported in Catalyst device-level specs - # assert specs1["resources"].measurements == specs2["resources"].measurements + # Representations are slightly different from plain PL -- wire counts are missing + info = qml.specs(qjit(circ), level=1, compute_depth=False)() - assert specs1["resources"].num_allocs == specs2["resources"].num_allocs - assert specs1["resources"].depth == specs2["resources"].depth - assert specs1["resources"].num_gates == specs2["resources"].num_gates + assert info.resources.measurements == { + "expval(Hamiltonian(num_terms=2))": 1, + "expval(Prod(num_terms=2))": 1, + "sample(1 wires)": 1, + "sample(all wires)": 1, + } + def test_split_non_commuting(self): + """Test that qml.transforms.split_non_commuting works as expected""" -@pytest.mark.parametrize("level", ["device"]) -def test_simple(level): - """Test a simple case of qml.specs() against PennyLane""" + @qml.transforms.cancel_inverses + @qml.transforms.split_non_commuting + @qml.qnode(qml.device("null.qubit", wires=3)) + def circuit(): + qml.H(0) + qml.X(0) + qml.X(0) + return qml.expval(qml.X(0)), qml.expval(qml.Y(0)), qml.expval(qml.Z(0)) - dev = qml.device("lightning.qubit", wires=1) + actual = qml.specs(qjit(circuit), level=1)() + expected = CircuitSpecs( + device_name="null.qubit", + num_device_wires=3, + shots=Shots(None), + level=1, + resources=[ + SpecsResources( + gate_types={"Hadamard": 1, "PauliX": 2}, + gate_sizes={1: 3}, + measurements={"expval(PauliX)": 1}, + num_allocs=1, + ), + SpecsResources( + gate_types={"Hadamard": 1, "PauliX": 2}, + gate_sizes={1: 3}, + measurements={"expval(PauliY)": 1}, + num_allocs=1, + ), + SpecsResources( + gate_types={"Hadamard": 1, "PauliX": 2}, + gate_sizes={1: 3}, + measurements={"expval(PauliZ)": 1}, + num_allocs=1, + ), + ], + ) - @qml.qnode(dev) - def circuit(): - qml.Hadamard(wires=0) - return qml.expval(qml.PauliZ(0)) + check_specs_same(actual, expected) - pl_specs = qml.specs(circuit, level=level)() - cat_specs = qml.specs(qjit(circuit), level=level)() + @pytest.mark.usefixtures("use_capture") + def test_subroutine(self): + """Test qml.specs when there is a Catalyst subroutine""" + dev = qml.device("lightning.qubit", wires=3) - assert cat_specs["device_name"] == "lightning.qubit" - check_specs_same(pl_specs, cat_specs) + @catalyst.jax_primitives.subroutine + def subroutine(): + qml.Hadamard(wires=0) + @qml.qjit(autograph=True) + @qml.qnode(dev) + def circuit(): -@pytest.mark.parametrize("level", ["device"]) -def test_complex(level): - """Test a complex case of qml.specs() against PennyLane""" + for _ in range(3): + subroutine() - dev = qml.device("lightning.qubit", wires=4) - U = 1 / jnp.sqrt(2) * jnp.array([[1, 1], [1, -1]], dtype=jnp.complex128) + return qml.probs() - @qml.qnode(dev) - def circuit(): - qml.PauliX(0) - qml.adjoint(qml.T)(0) - qml.ctrl(op=qml.S, control=[1], control_values=[1])(0) - qml.ctrl(op=qml.S, control=[1, 2], control_values=[1, 0])(0) - qml.ctrl(op=qml.adjoint(qml.Y), control=[2], control_values=[1])(0) - qml.CNOT([0, 1]) + actual = qml.specs(circuit, level=1)() + expected = CircuitSpecs( + device_name="lightning.qubit", + num_device_wires=3, + shots=Shots(None), + level=1, + resources=SpecsResources( + gate_types={"Hadamard": 3}, + gate_sizes={1: 3}, + measurements={"probs(all wires)": 1}, + num_allocs=3, + ), + ) - qml.QubitUnitary(U, wires=0) - qml.ControlledQubitUnitary(U, control_values=[1], wires=[1, 0]) - qml.adjoint(qml.QubitUnitary(U, wires=0)) - qml.adjoint(qml.ControlledQubitUnitary(U, control_values=[1, 1], wires=[1, 2, 0])) + check_specs_same(actual, expected) - return qml.probs() + def test_ppr(self): + """Test that PPRs are handled correctly.""" - pl_specs = qml.specs(circuit, level=level)() - cat_specs = qml.specs(qjit(circuit), level=level)() + pipeline = [("pipe", ["enforce-runtime-invariants-pipeline"])] - assert cat_specs["device_name"] == "lightning.qubit" + @qml.qjit(pipelines=pipeline, target="mlir") + @catalyst.passes.to_ppr + @qml.qnode(qml.device("null.qubit", wires=2)) + def circ(): + qml.H(0) + qml.T(0) - # Catalyst will handle Adjoint(PauliY) == PauliY - assert "CY" in cat_specs["resources"].gate_types - cat_specs["resources"].gate_types["C(Adjoint(PauliY))"] = cat_specs["resources"].gate_types[ - "CY" - ] - del cat_specs["resources"].gate_types["CY"] + expected = CircuitSpecs( + device_name="null.qubit", + num_device_wires=2, + shots=Shots(None), + level=2, + resources=SpecsResources( + gate_types={"PPR-pi/4": 3, "PPR-pi/8": 1}, + gate_sizes={1: 4}, + measurements={}, + num_allocs=2, + ), + ) - check_specs_same(pl_specs, cat_specs) + actual = qml.specs(circ, level=2)() + check_specs_same(actual, expected) if __name__ == "__main__":