diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 0b8aae779af..d569b5fb8ea 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -279,6 +279,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 58eecf41aea..c3a59e291c8 100644 --- a/pennylane/devices/null_qubit.py +++ b/pennylane/devices/null_qubit.py @@ -20,6 +20,7 @@ import inspect import json import logging +import time from collections import defaultdict from dataclasses import replace from functools import lru_cache, singledispatch @@ -51,6 +52,8 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) +RESOURCES_FNAME_PREFIX = "__pennylane_resources_data_" + @singledispatch def zero_measurement( @@ -132,7 +135,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 +166,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 +190,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): If True, track the number of resources used by the device and save them to a JSON file. This argument is experimental and subject to change. **Example:** .. code-block:: python @@ -285,7 +287,10 @@ def _simulate(self, circuit, interface): results = [] if self._track_resources: - _simulate_resource_use(circuit) + timestamp = int(time.time() * 1e9) # nanoseconds since epoch + resources_fname = f"{RESOURCES_FNAME_PREFIX}{timestamp}.json" + with open(resources_fname, "x", encoding="utf-8") as f: + _simulate_resource_use(circuit, f) 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..c689636dea4 100644 --- a/tests/devices/test_null_qubit.py +++ b/tests/devices/test_null_qubit.py @@ -67,9 +67,8 @@ 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 - dev = NullQubit(track_resources=True) + assert dev._track_resources is True def small_circ(params): qml.X(0) @@ -93,41 +92,59 @@ def small_circ(params): qml.RX(params[0], wires=0) qml.RX(params[0] * 2, wires=1) + qml.QubitUnitary([[1, 0], [0, 1]], wires=0) + qml.ControlledQubitUnitary([[1, 0], [0, 1]], wires=[0, 1, 2], control_values=[1, 1]) + return qml.expval(qml.PauliZ(0)) qnode = qml.QNode(small_circ, dev, diff_method="backprop") inputs = qml.numpy.array([0.5]) + def check_outputs(): + written_files = list( + filter( + lambda fname: fname.startswith(qml.devices.null_qubit.RESOURCES_FNAME_PREFIX), + os.listdir(os.getcwd()), + ) + ) + + assert len(written_files) == 1 + resources_fname = written_files[0] + + 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": 11, + "gate_types": { + "PauliX": 1, + "Hadamard": 1, + "C(Adj(T))": 1, + "2C(S)": 1, + "CNOT": 1, + "C(IsingXX)": 1, + "Adj(S)": 1, + "RX": 2, + "QubitUnitary": 1, + "ControlledQubitUnitary": 1, + }, + } + ) + + # Check ordinary forward computation qnode(inputs) + check_outputs() - # Check that resource tracking doesn't interfere with backprop + # Check backpropagation 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( - { - "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_outputs() @pytest.mark.parametrize("shots", (None, 10))