diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 60d84c662d2..83eaf5f94de 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -245,6 +245,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) diff --git a/pennylane/devices/null_qubit.py b/pennylane/devices/null_qubit.py index 285f6f8d36e..b25f4274def 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 @@ -132,7 +133,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, outfile): num_wires = len(circuit.wires) gate_types = defaultdict(int) @@ -163,17 +164,16 @@ def _simulate_resource_use(circuit, resources_fname="__pennylane_resources_data. 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. - with open(resources_fname, "w") as f: - json.dump( - { - "num_wires": num_wires, - "num_gates": sum(gate_types.values()), - "gate_types": gate_types, - }, - f, - ) + json.dump( + { + "num_wires": num_wires, + "num_gates": sum(gate_types.values()), + "gate_types": gate_types, + }, + outfile, + ) @simulator_tracking @@ -188,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 @@ -272,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 isinstance(track_resources, 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 isinstance(self._track_resources, str): + # if a string is passed, we assume it is a file name + 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 + _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 e0570e569ba..1d0a57d0521 100644 --- a/tests/devices/test_null_qubit.py +++ b/tests/devices/test_null_qubit.py @@ -13,8 +13,10 @@ # limitations under the License. """Tests for null.qubit.""" +import io import json import os +import sys from collections import defaultdict as dd import numpy as np @@ -66,68 +68,81 @@ def test_debugger_attribute(): 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) + for pre_opened in (False, True): + if pre_opened: + in_arg = io.StringIO() + else: + in_arg = RESOURCES_FNAME - def small_circ(params): - qml.X(0) - qml.H(0) + dev = NullQubit(track_resources=in_arg) - qml.Barrier() + def small_circ(params): + qml.X(0) + qml.H(0) - # 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.Barrier() - qml.ctrl(qml.S(0), control=[1, 2], control_values=[1, 1]) + # 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.CNOT([0, 1]) - qml.Barrier() + qml.ctrl(qml.S(0), control=[1, 2], control_values=[1, 1]) - qml.ctrl(qml.IsingXX(0, [0, 1]), control=2, control_values=[1]) - qml.adjoint(qml.S(0)) + qml.CNOT([0, 1]) + qml.Barrier() - qml.RX(params[0], wires=0) - qml.RX(params[0] * 2, wires=1) + qml.ctrl(qml.IsingXX(0, [0, 1]), control=2, control_values=[1]) + qml.adjoint(qml.S(0)) - return qml.expval(qml.PauliZ(0)) + qml.RX(params[0], wires=0) + qml.RX(params[0] * 2, wires=1) - qnode = qml.QNode(small_circ, dev, diff_method="backprop") + return qml.expval(qml.PauliZ(0)) - inputs = qml.numpy.array([0.5]) + qnode = qml.QNode(small_circ, dev, diff_method="backprop") - qnode(inputs) + inputs = qml.numpy.array([0.5]) - # Check that resource tracking doesn't interfere with backprop - assert qml.grad(qnode)(inputs) == 0 + qnode(inputs) - 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( - { - "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: + 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, + }, + } + ) + + # Check that resource tracking doesn't interfere with backprop + assert qml.grad(qnode)(inputs) == 0 + + if pre_opened: + in_arg.close() @pytest.mark.parametrize("shots", (None, 10))