From cb615f8f502b12bf5fd8a7ebd0c27067db28ae2a Mon Sep 17 00:00:00 2001 From: Jake Zaia Date: Wed, 7 May 2025 14:30:44 +0000 Subject: [PATCH 1/7] Revert resource tracking to once again print to stdout instead of writing to a file --- pennylane/devices/null_qubit.py | 14 +++++++----- tests/devices/test_null_qubit.py | 37 +++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/pennylane/devices/null_qubit.py b/pennylane/devices/null_qubit.py index 285f6f8d36e..1bb731f344b 100644 --- a/pennylane/devices/null_qubit.py +++ b/pennylane/devices/null_qubit.py @@ -51,6 +51,8 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) +RESOURCE_PRINT_DELIMETER = "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=" + @singledispatch def zero_measurement( @@ -132,7 +134,7 @@ def _interface(config: ExecutionConfig): return config.interface.get_like() if config.gradient_method == "backprop" else "numpy" -def _simulate_resource_use(circuit, resources_fname="__pennylane_resources_data.json"): +def _simulate_resource_use(circuit): num_wires = len(circuit.wires) gate_types = defaultdict(int) @@ -165,15 +167,17 @@ def _simulate_resource_use(circuit, resources_fname="__pennylane_resources_data. gate_types[name] += 1 # NOTE: For now, this information is being printed to match the behavior of catalyst resource tracking. # In the future it may be better to return this information in a more structured way. - with open(resources_fname, "w") as f: - json.dump( + print(RESOURCE_PRINT_DELIMETER) + print( + json.dumps( { "num_wires": num_wires, "num_gates": sum(gate_types.values()), "gate_types": gate_types, - }, - f, + } ) + ) + print(RESOURCE_PRINT_DELIMETER) @simulator_tracking diff --git a/tests/devices/test_null_qubit.py b/tests/devices/test_null_qubit.py index e0570e569ba..8a431b7f7e4 100644 --- a/tests/devices/test_null_qubit.py +++ b/tests/devices/test_null_qubit.py @@ -63,7 +63,7 @@ def test_debugger_attribute(): assert dev._debugger is None -def test_resource_tracking_attribute(): +def test_resource_tracking_attribute(capsys): """Test NullQubit track_resources attribute""" # pylint: disable=protected-access assert NullQubit()._track_resources is False @@ -101,18 +101,7 @@ def small_circ(params): qnode(inputs) - # Check that resource tracking doesn't interfere with backprop - assert qml.grad(qnode)(inputs) == 0 - - RESOURCES_FNAME = "__pennylane_resources_data.json" - assert os.path.exists(RESOURCES_FNAME) - - with open(RESOURCES_FNAME, "r") as f: - stats = f.read() - - os.remove(RESOURCES_FNAME) - - assert stats == json.dumps( + expected = json.dumps( { "num_wires": 3, "num_gates": 9, @@ -129,6 +118,28 @@ def small_circ(params): } ) + captured = capsys.readouterr() + captured = captured.out.splitlines() + + RESOURCE_PRINT_DELIMETER = "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=" + assert len(captured) == 3 + assert captured[0] == RESOURCE_PRINT_DELIMETER + assert captured[-1] == RESOURCE_PRINT_DELIMETER + + assert captured[1] == expected + + # Check that resource tracking doesn't interfere with backprop + assert qml.grad(qnode)(inputs) == 0 + + captured = capsys.readouterr() + captured = captured.out.splitlines() + + # Running grad prints resource information again + assert len(captured) == 3 + assert captured[0] == RESOURCE_PRINT_DELIMETER + assert captured[-1] == RESOURCE_PRINT_DELIMETER + assert captured[1] == expected + @pytest.mark.parametrize("shots", (None, 10)) def test_supports_operator_without_decomp(shots): From 25a0357e8b68317edc827201e641dae85687c34c Mon Sep 17 00:00:00 2001 From: Jake Zaia Date: Wed, 7 May 2025 14:39:25 +0000 Subject: [PATCH 2/7] Update changelog --- doc/releases/changelog-dev.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index e25705b80c7..3d2479ebeea 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -241,6 +241,7 @@ Here's a list of deprecations made this release. For a more detailed breakdown o * `null.qubit` can now support an optional `track_resources` argument which allows it to record which gates are executed. [(#7226)](https://github.com/PennyLaneAI/pennylane/pull/7226) [(#7372)](https://github.com/PennyLaneAI/pennylane/pull/7372) + [(#7392)](https://github.com/PennyLaneAI/pennylane/pull/7392) * A new internal module, `qml.concurrency`, is added to support internal use of multiprocess and multithreaded execution of workloads. This also migrates the use of `concurrent.futures` in `default.qubit` to this new design. [(#7303)](https://github.com/PennyLaneAI/pennylane/pull/7303) From 822e74045734050a257017ec9aafba756cc54d13 Mon Sep 17 00:00:00 2001 From: Jake Zaia Date: Wed, 7 May 2025 14:52:57 +0000 Subject: [PATCH 3/7] Remove unused import --- tests/devices/test_null_qubit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/devices/test_null_qubit.py b/tests/devices/test_null_qubit.py index 8a431b7f7e4..4531229ff3a 100644 --- a/tests/devices/test_null_qubit.py +++ b/tests/devices/test_null_qubit.py @@ -14,7 +14,6 @@ """Tests for null.qubit.""" import json -import os from collections import defaultdict as dd import numpy as np From 7c6bc8df8f29919856cc8c4ae89a6cde660b8e5a Mon Sep 17 00:00:00 2001 From: Jake Zaia Date: Wed, 7 May 2025 22:08:56 +0000 Subject: [PATCH 4/7] Clean up resource delimiter use --- pennylane/devices/null_qubit.py | 7 ++++--- tests/devices/test_null_qubit.py | 9 ++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pennylane/devices/null_qubit.py b/pennylane/devices/null_qubit.py index 1bb731f344b..95b4370202e 100644 --- a/pennylane/devices/null_qubit.py +++ b/pennylane/devices/null_qubit.py @@ -51,7 +51,8 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) -RESOURCE_PRINT_DELIMETER = "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=" +# The delimiter to print before and after each resource JSON to make parsing easier +RESOURCE_PRINT_DELIMITER = "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=" @singledispatch @@ -167,7 +168,7 @@ def _simulate_resource_use(circuit): gate_types[name] += 1 # NOTE: For now, this information is being printed to match the behavior of catalyst resource tracking. # In the future it may be better to return this information in a more structured way. - print(RESOURCE_PRINT_DELIMETER) + print(RESOURCE_PRINT_DELIMITER) print( json.dumps( { @@ -177,7 +178,7 @@ def _simulate_resource_use(circuit): } ) ) - print(RESOURCE_PRINT_DELIMETER) + print(RESOURCE_PRINT_DELIMITER) @simulator_tracking diff --git a/tests/devices/test_null_qubit.py b/tests/devices/test_null_qubit.py index 4531229ff3a..fadf12fb341 100644 --- a/tests/devices/test_null_qubit.py +++ b/tests/devices/test_null_qubit.py @@ -120,10 +120,9 @@ def small_circ(params): captured = capsys.readouterr() captured = captured.out.splitlines() - RESOURCE_PRINT_DELIMETER = "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=" assert len(captured) == 3 - assert captured[0] == RESOURCE_PRINT_DELIMETER - assert captured[-1] == RESOURCE_PRINT_DELIMETER + assert captured[0] == qml.devices.null_qubit.RESOURCE_PRINT_DELIMITER + assert captured[-1] == qml.devices.null_qubit.RESOURCE_PRINT_DELIMITER assert captured[1] == expected @@ -135,8 +134,8 @@ def small_circ(params): # Running grad prints resource information again assert len(captured) == 3 - assert captured[0] == RESOURCE_PRINT_DELIMETER - assert captured[-1] == RESOURCE_PRINT_DELIMETER + assert captured[0] == qml.devices.null_qubit.RESOURCE_PRINT_DELIMITER + assert captured[-1] == qml.devices.null_qubit.RESOURCE_PRINT_DELIMITER assert captured[1] == expected From a083a3debefac08da4d25ede75f74a890c4f58ae Mon Sep 17 00:00:00 2001 From: Jake Zaia Date: Thu, 8 May 2025 14:50:17 +0000 Subject: [PATCH 5/7] Implement more flexible constructor for null.qubit resource tracking --- pennylane/devices/null_qubit.py | 48 +++++++++++++++++++------------- tests/devices/test_null_qubit.py | 45 +++++++++++++----------------- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/pennylane/devices/null_qubit.py b/pennylane/devices/null_qubit.py index 95b4370202e..12cbba1ba86 100644 --- a/pennylane/devices/null_qubit.py +++ b/pennylane/devices/null_qubit.py @@ -20,6 +20,7 @@ import inspect import json import logging +import sys from collections import defaultdict from dataclasses import replace from functools import lru_cache, singledispatch @@ -51,9 +52,6 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) -# The delimiter to print before and after each resource JSON to make parsing easier -RESOURCE_PRINT_DELIMITER = "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=" - @singledispatch def zero_measurement( @@ -135,7 +133,7 @@ def _interface(config: ExecutionConfig): return config.interface.get_like() if config.gradient_method == "backprop" else "numpy" -def _simulate_resource_use(circuit): +def _simulate_resource_use(circuit, outfile): num_wires = len(circuit.wires) gate_types = defaultdict(int) @@ -166,19 +164,16 @@ def _simulate_resource_use(circuit): name = f"{controls if controls > 1 else ''}C({name})" gate_types[name] += 1 - # NOTE: For now, this information is being printed to match the behavior of catalyst resource tracking. + # NOTE: For now, this information is being printed to match the behaviour of catalyst resource tracking. # In the future it may be better to return this information in a more structured way. - print(RESOURCE_PRINT_DELIMITER) - print( - json.dumps( - { - "num_wires": num_wires, - "num_gates": sum(gate_types.values()), - "gate_types": gate_types, - } - ) + json.dump( + { + "num_wires": num_wires, + "num_gates": sum(gate_types.values()), + "gate_types": gate_types, + }, + outfile, ) - print(RESOURCE_PRINT_DELIMITER) @simulator_tracking @@ -193,7 +188,7 @@ class NullQubit(Device): (``['aux_wire', 'q1', 'q2']``). Default ``None`` if not specified. shots (int, Sequence[int], Sequence[Union[int, Sequence[int]]]): The default number of shots to use in executions involving this device. - track_resources (bool): If ``True``, track the number of resources used by the device. This argument is experimental and subject to change. + track_resources (bool | str | None): If truthy, track the number of resources used by the device. If a str is provided, use it as a filename in which to write the results. Else print to stdout. This argument is experimental and subject to change. **Example:** .. code-block:: python @@ -277,20 +272,33 @@ def name(self): """The name of the device.""" return "null.qubit" - def __init__(self, wires=None, shots=None, track_resources=False) -> None: + def __init__(self, wires=None, shots=None, track_resources=None) -> None: super().__init__(wires=wires, shots=shots) self._debugger = None - self._track_resources = track_resources + if track_resources is True: + self._track_resources = sys.stdout + else: + self._track_resources = track_resources # this is required by Catalyst to toggle the tracker at runtime - self.device_kwargs = {"track_resources": track_resources} + self.device_kwargs = { + "track_resources": bool(track_resources), + "track_resources_fname": track_resources if type(track_resources) is str else None, + "track_resources_stdout": track_resources is True, + } def _simulate(self, circuit, interface): num_device_wires = len(self.wires) if self.wires else len(circuit.wires) results = [] if self._track_resources: - _simulate_resource_use(circuit) + if type(self._track_resources) is str: + # if a string is passed, we assume it is a file name + with open(self._track_resources, "w") as f: + _simulate_resource_use(circuit, f) + else: + # NOTE: This will work even if `_track_resources` was originally passed a file-like object + _simulate_resource_use(circuit, self._track_resources) for s in circuit.shots or [None]: r = tuple( diff --git a/tests/devices/test_null_qubit.py b/tests/devices/test_null_qubit.py index fadf12fb341..4aa0a1d8589 100644 --- a/tests/devices/test_null_qubit.py +++ b/tests/devices/test_null_qubit.py @@ -14,6 +14,8 @@ """Tests for null.qubit.""" import json +import os +import sys from collections import defaultdict as dd import numpy as np @@ -62,13 +64,15 @@ def test_debugger_attribute(): assert dev._debugger is None -def test_resource_tracking_attribute(capsys): +def test_resource_tracking_attribute(): """Test NullQubit track_resources attribute""" # pylint: disable=protected-access - assert NullQubit()._track_resources is False - assert NullQubit(track_resources=True)._track_resources is True + RESOURCES_FNAME = "__pennylane_resources_data.json" + assert NullQubit()._track_resources is None + assert NullQubit(track_resources=True)._track_resources == sys.stdout + assert NullQubit(track_resources=RESOURCES_FNAME)._track_resources == RESOURCES_FNAME - dev = NullQubit(track_resources=True) + dev = NullQubit(track_resources=RESOURCES_FNAME) def small_circ(params): qml.X(0) @@ -100,7 +104,17 @@ def small_circ(params): qnode(inputs) - expected = json.dumps( + # Check that resource tracking doesn't interfere with backprop + assert qml.grad(qnode)(inputs) == 0 + + assert os.path.exists(RESOURCES_FNAME) + + with open(RESOURCES_FNAME, "r") as f: + stats = f.read() + + os.remove(RESOURCES_FNAME) + + assert stats == json.dumps( { "num_wires": 3, "num_gates": 9, @@ -117,27 +131,6 @@ def small_circ(params): } ) - captured = capsys.readouterr() - captured = captured.out.splitlines() - - assert len(captured) == 3 - assert captured[0] == qml.devices.null_qubit.RESOURCE_PRINT_DELIMITER - assert captured[-1] == qml.devices.null_qubit.RESOURCE_PRINT_DELIMITER - - assert captured[1] == expected - - # Check that resource tracking doesn't interfere with backprop - assert qml.grad(qnode)(inputs) == 0 - - captured = capsys.readouterr() - captured = captured.out.splitlines() - - # Running grad prints resource information again - assert len(captured) == 3 - assert captured[0] == qml.devices.null_qubit.RESOURCE_PRINT_DELIMITER - assert captured[-1] == qml.devices.null_qubit.RESOURCE_PRINT_DELIMITER - assert captured[1] == expected - @pytest.mark.parametrize("shots", (None, 10)) def test_supports_operator_without_decomp(shots): From 947e081dddddf60124332d6bb276183ac02de66a Mon Sep 17 00:00:00 2001 From: Jake Zaia Date: Thu, 8 May 2025 15:36:12 +0000 Subject: [PATCH 6/7] Fix pylint errors --- pennylane/devices/null_qubit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane/devices/null_qubit.py b/pennylane/devices/null_qubit.py index 12cbba1ba86..1bd778cfe4d 100644 --- a/pennylane/devices/null_qubit.py +++ b/pennylane/devices/null_qubit.py @@ -283,7 +283,7 @@ def __init__(self, wires=None, shots=None, track_resources=None) -> None: # this is required by Catalyst to toggle the tracker at runtime self.device_kwargs = { "track_resources": bool(track_resources), - "track_resources_fname": track_resources if type(track_resources) is str else None, + "track_resources_fname": track_resources if isinstance(track_resources, str) else None, "track_resources_stdout": track_resources is True, } @@ -292,7 +292,7 @@ def _simulate(self, circuit, interface): results = [] if self._track_resources: - if type(self._track_resources) is str: + if isinstance(self._track_resources, str) : # if a string is passed, we assume it is a file name with open(self._track_resources, "w") as f: _simulate_resource_use(circuit, f) From 480fd93c3806d000ef23413f09d46aa3d4098d55 Mon Sep 17 00:00:00 2001 From: Jake Zaia Date: Thu, 8 May 2025 15:50:58 +0000 Subject: [PATCH 7/7] Clean up unit tests for better code coverage --- pennylane/devices/null_qubit.py | 4 +- tests/devices/test_null_qubit.py | 99 ++++++++++++++++++-------------- 2 files changed, 58 insertions(+), 45 deletions(-) diff --git a/pennylane/devices/null_qubit.py b/pennylane/devices/null_qubit.py index 1bd778cfe4d..b25f4274def 100644 --- a/pennylane/devices/null_qubit.py +++ b/pennylane/devices/null_qubit.py @@ -292,9 +292,9 @@ def _simulate(self, circuit, interface): results = [] if self._track_resources: - if isinstance(self._track_resources, str) : + if isinstance(self._track_resources, str): # if a string is passed, we assume it is a file name - with open(self._track_resources, "w") as f: + with open(self._track_resources, "w", encoding="utf-8") as f: _simulate_resource_use(circuit, f) else: # NOTE: This will work even if `_track_resources` was originally passed a file-like object diff --git a/tests/devices/test_null_qubit.py b/tests/devices/test_null_qubit.py index 4aa0a1d8589..1d0a57d0521 100644 --- a/tests/devices/test_null_qubit.py +++ b/tests/devices/test_null_qubit.py @@ -13,6 +13,7 @@ # limitations under the License. """Tests for null.qubit.""" +import io import json import os import sys @@ -72,64 +73,76 @@ def test_resource_tracking_attribute(): assert NullQubit(track_resources=True)._track_resources == sys.stdout assert NullQubit(track_resources=RESOURCES_FNAME)._track_resources == RESOURCES_FNAME - dev = NullQubit(track_resources=RESOURCES_FNAME) - - def small_circ(params): - qml.X(0) - qml.H(0) + for pre_opened in (False, True): + if pre_opened: + in_arg = io.StringIO() + else: + in_arg = RESOURCES_FNAME - qml.Barrier() + dev = NullQubit(track_resources=in_arg) - # Add a more complex operation to check that the innermost operation is counted - op = qml.T(0) - op = qml.adjoint(op) - op = qml.ctrl(op, control=1, control_values=[1]) + def small_circ(params): + qml.X(0) + qml.H(0) - qml.ctrl(qml.S(0), control=[1, 2], control_values=[1, 1]) + qml.Barrier() - qml.CNOT([0, 1]) - qml.Barrier() + # Add a more complex operation to check that the innermost operation is counted + op = qml.T(0) + op = qml.adjoint(op) + op = qml.ctrl(op, control=1, control_values=[1]) - qml.ctrl(qml.IsingXX(0, [0, 1]), control=2, control_values=[1]) - qml.adjoint(qml.S(0)) + qml.ctrl(qml.S(0), control=[1, 2], control_values=[1, 1]) - qml.RX(params[0], wires=0) - qml.RX(params[0] * 2, wires=1) + qml.CNOT([0, 1]) + qml.Barrier() - return qml.expval(qml.PauliZ(0)) + qml.ctrl(qml.IsingXX(0, [0, 1]), control=2, control_values=[1]) + qml.adjoint(qml.S(0)) - qnode = qml.QNode(small_circ, dev, diff_method="backprop") + qml.RX(params[0], wires=0) + qml.RX(params[0] * 2, wires=1) - inputs = qml.numpy.array([0.5]) + return qml.expval(qml.PauliZ(0)) - qnode(inputs) + qnode = qml.QNode(small_circ, dev, diff_method="backprop") - # Check that resource tracking doesn't interfere with backprop - assert qml.grad(qnode)(inputs) == 0 + inputs = qml.numpy.array([0.5]) - assert os.path.exists(RESOURCES_FNAME) + qnode(inputs) - with open(RESOURCES_FNAME, "r") as f: - stats = f.read() + if pre_opened: + stats = in_arg.getvalue() + else: + assert os.path.exists(RESOURCES_FNAME) + + with open(RESOURCES_FNAME, "r", encoding="utf-8") as f: + stats = f.read() + + os.remove(RESOURCES_FNAME) + + assert stats == json.dumps( + { + "num_wires": 3, + "num_gates": 9, + "gate_types": { + "PauliX": 1, + "Hadamard": 1, + "C(Adj(T))": 1, + "2C(S)": 1, + "CNOT": 1, + "C(IsingXX)": 1, + "Adj(S)": 1, + "RX": 2, + }, + } + ) - os.remove(RESOURCES_FNAME) + # Check that resource tracking doesn't interfere with backprop + assert qml.grad(qnode)(inputs) == 0 - assert stats == json.dumps( - { - "num_wires": 3, - "num_gates": 9, - "gate_types": { - "PauliX": 1, - "Hadamard": 1, - "C(Adj(T))": 1, - "2C(S)": 1, - "CNOT": 1, - "C(IsingXX)": 1, - "Adj(S)": 1, - "RX": 2, - }, - } - ) + if pre_opened: + in_arg.close() @pytest.mark.parametrize("shots", (None, 10))