From 034af98fd05b57fb46babd00d05819a3321030ce Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Fri, 26 Jan 2024 16:18:40 +0000 Subject: [PATCH 01/76] Moved modules dealing with exact TN circuit representation to s separate folder. --- pytket/__init__.py | 0 pytket/extensions/__init__.py | 0 pytket/extensions/cutensornet/__init__.py | 14 ++++++----- .../backends/cutensornet_backend.py | 2 +- .../extensions/cutensornet/exact/__init__.py | 25 +++++++++++++++++++ .../{ => exact}/tensor_network_convert.py | 0 .../cutensornet/{ => exact}/utils.py | 0 tests/test_cutensornet_postselect.py | 2 +- tests/test_mps.py | 2 +- tests/test_utils.py | 2 +- 10 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 pytket/__init__.py create mode 100644 pytket/extensions/__init__.py create mode 100644 pytket/extensions/cutensornet/exact/__init__.py rename pytket/extensions/cutensornet/{ => exact}/tensor_network_convert.py (100%) rename pytket/extensions/cutensornet/{ => exact}/utils.py (100%) diff --git a/pytket/__init__.py b/pytket/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pytket/extensions/__init__.py b/pytket/extensions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pytket/extensions/cutensornet/__init__.py b/pytket/extensions/cutensornet/__init__.py index 5d12f155..3544a07f 100644 --- a/pytket/extensions/cutensornet/__init__.py +++ b/pytket/extensions/cutensornet/__init__.py @@ -13,12 +13,9 @@ # limitations under the License. """Module for conversion from tket primitives to cuQuantum primitives.""" -# _metadata.py is copied to the folder after installation. -from ._metadata import __extension_version__, __extension_name__ # type: ignore +from pytket.extensions.cutensornet.exact.utils import circuit_statevector_postselect -from .backends import CuTensorNetBackend - -from .tensor_network_convert import ( +from pytket.extensions.cutensornet.exact.tensor_network_convert import ( TensorNetwork, PauliOperatorTensorNetwork, ExpectationValueTensorNetwork, @@ -26,4 +23,9 @@ measure_qubits_state, ) -from .utils import circuit_statevector_postselect +from .backends import CuTensorNetBackend + +# _metadata.py is copied to the folder after installation. +from ._metadata import __extension_version__, __extension_name__ # type: ignore + + diff --git a/pytket/extensions/cutensornet/backends/cutensornet_backend.py b/pytket/extensions/cutensornet/backends/cutensornet_backend.py index b6e13757..5aa3fed2 100644 --- a/pytket/extensions/cutensornet/backends/cutensornet_backend.py +++ b/pytket/extensions/cutensornet/backends/cutensornet_backend.py @@ -30,7 +30,7 @@ from pytket.backends.backend import KwargTypes, Backend, BackendResult from pytket.backends.backendinfo import BackendInfo from pytket.backends.resulthandle import _ResultIdTuple -from pytket.extensions.cutensornet.tensor_network_convert import ( +from pytket.extensions.cutensornet.exact import ( TensorNetwork, ExpectationValueTensorNetwork, tk_to_tensor_network, diff --git a/pytket/extensions/cutensornet/exact/__init__.py b/pytket/extensions/cutensornet/exact/__init__.py new file mode 100644 index 00000000..2daea10d --- /dev/null +++ b/pytket/extensions/cutensornet/exact/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2019-2024 Quantinuum +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Module for conversion from tket primitives to cuQuantum primitives.""" + +from pytket.extensions.cutensornet.exact.utils import circuit_statevector_postselect + +from .tensor_network_convert import ( + TensorNetwork, + PauliOperatorTensorNetwork, + ExpectationValueTensorNetwork, + tk_to_tensor_network, + measure_qubits_state, +) + diff --git a/pytket/extensions/cutensornet/tensor_network_convert.py b/pytket/extensions/cutensornet/exact/tensor_network_convert.py similarity index 100% rename from pytket/extensions/cutensornet/tensor_network_convert.py rename to pytket/extensions/cutensornet/exact/tensor_network_convert.py diff --git a/pytket/extensions/cutensornet/utils.py b/pytket/extensions/cutensornet/exact/utils.py similarity index 100% rename from pytket/extensions/cutensornet/utils.py rename to pytket/extensions/cutensornet/exact/utils.py diff --git a/tests/test_cutensornet_postselect.py b/tests/test_cutensornet_postselect.py index faa379ff..50c85683 100644 --- a/tests/test_cutensornet_postselect.py +++ b/tests/test_cutensornet_postselect.py @@ -9,7 +9,7 @@ TensorNetwork, measure_qubits_state, ) -from pytket.extensions.cutensornet.utils import circuit_statevector_postselect +from pytket.extensions.cutensornet.exact.utils import circuit_statevector_postselect @pytest.mark.parametrize( diff --git a/tests/test_mps.py b/tests/test_mps.py index ab2c5b0d..e33f4f9d 100644 --- a/tests/test_mps.py +++ b/tests/test_mps.py @@ -18,7 +18,7 @@ prepare_circuit, ContractionAlg, ) -from pytket.extensions.cutensornet.utils import circuit_statevector_postselect +from pytket.extensions.cutensornet.exact.utils import circuit_statevector_postselect def test_libhandle_manager() -> None: diff --git a/tests/test_utils.py b/tests/test_utils.py index 48453371..cad498c3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,5 @@ import numpy -from pytket.extensions.cutensornet.utils import circuit_statevector_postselect +from pytket.extensions.cutensornet.exact.utils import circuit_statevector_postselect from pytket import Circuit, Qubit # type: ignore From 0c05f1fcf25abe302309d8905c4a986ad6fe5b10 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Fri, 26 Jan 2024 17:48:54 +0000 Subject: [PATCH 02/76] Started drafting `TensorNetworkState` constructor (converter). --- .../cutensornet/exact/tensor_network_state.py | 63 +++++++++++++++++++ pytket/extensions/cutensornet/mps/mps.py | 1 + 2 files changed, 64 insertions(+) create mode 100644 pytket/extensions/cutensornet/exact/tensor_network_state.py diff --git a/pytket/extensions/cutensornet/exact/tensor_network_state.py b/pytket/extensions/cutensornet/exact/tensor_network_state.py new file mode 100644 index 00000000..a097dc5e --- /dev/null +++ b/pytket/extensions/cutensornet/exact/tensor_network_state.py @@ -0,0 +1,63 @@ +import logging +import warnings +try: + import cupy as cp # type: ignore +except ImportError: + warnings.warn("local settings failed to import cupy", ImportWarning) +from pytket.circuit import Circuit # type: ignore +from pytket.extensions.cutensornet.general import set_logger +from pytket.extensions.cutensornet.mps import CuTensorNetHandle +try: + import cuquantum as cq # type: ignore + from cuquantum import cutensornet as cutn # type: ignore +except ImportError: + warnings.warn("local settings failed to import cuquantum", ImportWarning) + + +class TensorNetworkState: + """Handles cuTensorNet tensor network state object.""" + + def __init__( + self, + circuit: Circuit, + libhandle: CuTensorNetHandle, + loglevel: int = logging.INFO + ) -> None: + """Constructs a tensor network state representation from a pytket circuit. + + Note: + Circuit should not contain boxes - only explicit gates with specific unitary + matrix representation available in pytket. + + Args: + circuit: A pytket circuit to be converted to a tensor network. + libhandle: cuTensorNet handle. + loglevel: Internal logger output level. + """ + self._logger = set_logger("TensorNetwork", loglevel) + self._circuit = circuit + + device_props = cp.cuda.runtime.getDeviceProperties(libhandle.device_id) + self._logger.debug("===== device info ======") + self._logger.debug("GPU-name:", device_props["name"].decode()) + self._logger.debug("GPU-clock:", device_props["clockRate"]) + self._logger.debug("GPU-memoryClock:", device_props["memoryClockRate"]) + self._logger.debug("GPU-nSM:", device_props["multiProcessorCount"]) + self._logger.debug("GPU-major:", device_props["major"]) + self._logger.debug("GPU-minor:", device_props["minor"]) + self._logger.debug("========================") + + num_qubits = circuit.n_qubits + dim = 2 # We are always dealing with qubits, not qudits + qubits_dims = (dim,) * num_qubits # qubit size + self._logger.debug(f"Converting a quantum circuit with {num_qubits} qubits.") + data_type = cq.cudaDataType.CUDA_C_64F # for now let that be hard-coded + + # Is this necessary? + free_mem = libhandle.dev.mem_info[0] + # use half of the totol free size + scratch_size = free_mem // 2 + scratch_space = cp.cuda.alloc(scratch_size) + + for com in circuit.get_commands(): + pass \ No newline at end of file diff --git a/pytket/extensions/cutensornet/mps/mps.py b/pytket/extensions/cutensornet/mps/mps.py index 6f072241..9056df79 100644 --- a/pytket/extensions/cutensornet/mps/mps.py +++ b/pytket/extensions/cutensornet/mps/mps.py @@ -75,6 +75,7 @@ def __init__(self, device_id: Optional[int] = None): cp.cuda.Device(device_id).use() dev = cp.cuda.Device() + self.dev = dev self.device_id = int(dev) def __enter__(self) -> CuTensorNetHandle: From 00c5b211c872bca65047a726034f2570f9833d76 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Tue, 30 Jan 2024 16:38:00 +0000 Subject: [PATCH 03/76] Finished drafting `TensorNetworkState` constructor (converter). --- pytket/extensions/cutensornet/__init__.py | 2 - .../extensions/cutensornet/exact/__init__.py | 1 - .../cutensornet/exact/tensor_network_state.py | 50 +++++++++++++++---- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/pytket/extensions/cutensornet/__init__.py b/pytket/extensions/cutensornet/__init__.py index 3544a07f..efb27b5a 100644 --- a/pytket/extensions/cutensornet/__init__.py +++ b/pytket/extensions/cutensornet/__init__.py @@ -27,5 +27,3 @@ # _metadata.py is copied to the folder after installation. from ._metadata import __extension_version__, __extension_name__ # type: ignore - - diff --git a/pytket/extensions/cutensornet/exact/__init__.py b/pytket/extensions/cutensornet/exact/__init__.py index 2daea10d..12ff4828 100644 --- a/pytket/extensions/cutensornet/exact/__init__.py +++ b/pytket/extensions/cutensornet/exact/__init__.py @@ -22,4 +22,3 @@ tk_to_tensor_network, measure_qubits_state, ) - diff --git a/pytket/extensions/cutensornet/exact/tensor_network_state.py b/pytket/extensions/cutensornet/exact/tensor_network_state.py index a097dc5e..e4d95e13 100644 --- a/pytket/extensions/cutensornet/exact/tensor_network_state.py +++ b/pytket/extensions/cutensornet/exact/tensor_network_state.py @@ -1,5 +1,7 @@ import logging import warnings +from typing import Optional + try: import cupy as cp # type: ignore except ImportError: @@ -7,6 +9,7 @@ from pytket.circuit import Circuit # type: ignore from pytket.extensions.cutensornet.general import set_logger from pytket.extensions.cutensornet.mps import CuTensorNetHandle + try: import cuquantum as cq # type: ignore from cuquantum import cutensornet as cutn # type: ignore @@ -18,10 +21,10 @@ class TensorNetworkState: """Handles cuTensorNet tensor network state object.""" def __init__( - self, - circuit: Circuit, - libhandle: CuTensorNetHandle, - loglevel: int = logging.INFO + self, + circuit: Circuit, + libhandle: Optional[CuTensorNetHandle] = None, + loglevel: int = logging.INFO, ) -> None: """Constructs a tensor network state representation from a pytket circuit. @@ -34,6 +37,8 @@ def __init__( libhandle: cuTensorNet handle. loglevel: Internal logger output level. """ + if libhandle is None: + libhandle = CuTensorNetHandle() self._logger = set_logger("TensorNetwork", loglevel) self._circuit = circuit @@ -53,11 +58,36 @@ def __init__( self._logger.debug(f"Converting a quantum circuit with {num_qubits} qubits.") data_type = cq.cudaDataType.CUDA_C_64F # for now let that be hard-coded - # Is this necessary? - free_mem = libhandle.dev.mem_info[0] - # use half of the totol free size - scratch_size = free_mem // 2 - scratch_space = cp.cuda.alloc(scratch_size) + # This is only required (if at all?) when doing evaluation + # free_mem = libhandle.dev.mem_info[0] + # use half of the total free size + # scratch_size = free_mem // 2 + # scratch_space = cp.cuda.alloc(scratch_size) + self._state = cutn.create_state( + libhandle.handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type + ) + mutable_gates_map = {} for com in circuit.get_commands(): - pass \ No newline at end of file + gate_unitary = ( + com.op.get_unitary() + .astype("complex128") + .reshape([2] * (2 * com.op.n_qubits), order="F") + ) # TODO: why column-major order? + gate_strides = 0 # Always 0? + gate_qubit_indices = [self._circuit.qubits.index(qb) for qb in com.qubits] + gate_n_qubits = len(gate_qubit_indices) + gate_qubit_indices_reversed = tuple(reversed(gate_qubit_indices)) + gate_id = cutn.state_apply_tensor( + libhandle.handle, + self._state, + gate_n_qubits, + gate_qubit_indices_reversed, + gate_unitary.data.ptr, + gate_strides, + 1, + 0, + 1, + ) + if com.opgroup is not None: + mutable_gates_map[com.opgroup] = gate_id From acdf9587829e1355c7ba35a037998e3c9b645559 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Wed, 31 Jan 2024 11:40:31 +0000 Subject: [PATCH 04/76] Subtle changes to CuTensorNetHandle (untested). --- pytket/extensions/cutensornet/mps/mps.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pytket/extensions/cutensornet/mps/mps.py b/pytket/extensions/cutensornet/mps/mps.py index 9056df79..6f04c414 100644 --- a/pytket/extensions/cutensornet/mps/mps.py +++ b/pytket/extensions/cutensornet/mps/mps.py @@ -14,6 +14,7 @@ from __future__ import annotations # type: ignore import warnings import logging +from logging import Logger from typing import Any, Optional, Union from enum import Enum @@ -72,11 +73,11 @@ def __init__(self, device_id: Optional[int] = None): self._is_destroyed = False # Make sure CuPy uses the specified device - cp.cuda.Device(device_id).use() + dev = cp.cuda.Device(device_id) + dev.use() - dev = cp.cuda.Device() self.dev = dev - self.device_id = int(dev) + self.device_id = dev.id def __enter__(self) -> CuTensorNetHandle: return self @@ -85,6 +86,18 @@ def __exit__(self, exc_type: Any, exc_value: Any, exc_tb: Any) -> None: cutn.destroy(self.handle) self._is_destroyed = True + def print_device_properties(self, logger: Logger): + """Prints local GPU properties.""" + device_props = cp.cuda.runtime.getDeviceProperties(self.dev.id) + logger.debug("===== device info ======") + logger.debug("GPU-name:", device_props["name"].decode()) + logger.debug("GPU-clock:", device_props["clockRate"]) + logger.debug("GPU-memoryClock:", device_props["memoryClockRate"]) + logger.debug("GPU-nSM:", device_props["multiProcessorCount"]) + logger.debug("GPU-major:", device_props["major"]) + logger.debug("GPU-minor:", device_props["minor"]) + logger.debug("========================") + class ConfigMPS: """Configuration class for simulation using MPS.""" From 657740a9637f2cf68593d7d9ded03191e0ae2591 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Fri, 2 Feb 2024 15:50:42 +0000 Subject: [PATCH 05/76] Some refurbishment. Renamed `TensorNetworkState` into `GeneralState`. Added `GeneralState.update_gates()` method. --- .../cutensornet/exact/tensor_network_state.py | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/pytket/extensions/cutensornet/exact/tensor_network_state.py b/pytket/extensions/cutensornet/exact/tensor_network_state.py index e4d95e13..68313845 100644 --- a/pytket/extensions/cutensornet/exact/tensor_network_state.py +++ b/pytket/extensions/cutensornet/exact/tensor_network_state.py @@ -1,6 +1,6 @@ import logging +import math import warnings -from typing import Optional try: import cupy as cp # type: ignore @@ -17,13 +17,13 @@ warnings.warn("local settings failed to import cuquantum", ImportWarning) -class TensorNetworkState: +class GeneralState: """Handles cuTensorNet tensor network state object.""" def __init__( self, circuit: Circuit, - libhandle: Optional[CuTensorNetHandle] = None, + libhandle: CuTensorNetHandle, loglevel: int = logging.INFO, ) -> None: """Constructs a tensor network state representation from a pytket circuit. @@ -37,20 +37,11 @@ def __init__( libhandle: cuTensorNet handle. loglevel: Internal logger output level. """ - if libhandle is None: - libhandle = CuTensorNetHandle() - self._logger = set_logger("TensorNetwork", loglevel) + self._logger = set_logger("GeneralState", loglevel) self._circuit = circuit + self._handle = libhandle.handle - device_props = cp.cuda.runtime.getDeviceProperties(libhandle.device_id) - self._logger.debug("===== device info ======") - self._logger.debug("GPU-name:", device_props["name"].decode()) - self._logger.debug("GPU-clock:", device_props["clockRate"]) - self._logger.debug("GPU-memoryClock:", device_props["memoryClockRate"]) - self._logger.debug("GPU-nSM:", device_props["multiProcessorCount"]) - self._logger.debug("GPU-major:", device_props["major"]) - self._logger.debug("GPU-minor:", device_props["minor"]) - self._logger.debug("========================") + libhandle.print_device_properties(self._logger) num_qubits = circuit.n_qubits dim = 2 # We are always dealing with qubits, not qudits @@ -65,11 +56,11 @@ def __init__( # scratch_space = cp.cuda.alloc(scratch_size) self._state = cutn.create_state( - libhandle.handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type + self._handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type ) - mutable_gates_map = {} + self._mutable_gates_map = {} for com in circuit.get_commands(): - gate_unitary = ( + gate_tensor = ( com.op.get_unitary() .astype("complex128") .reshape([2] * (2 * com.op.n_qubits), order="F") @@ -79,15 +70,36 @@ def __init__( gate_n_qubits = len(gate_qubit_indices) gate_qubit_indices_reversed = tuple(reversed(gate_qubit_indices)) gate_id = cutn.state_apply_tensor( - libhandle.handle, + self._handle, self._state, gate_n_qubits, gate_qubit_indices_reversed, - gate_unitary.data.ptr, + gate_tensor.data.ptr, gate_strides, 1, 0, 1, ) if com.opgroup is not None: - mutable_gates_map[com.opgroup] = gate_id + self._mutable_gates_map[com.opgroup] = gate_id + + def update_gates(self, gates_update_map: dict) -> None: + """Updates gate unitaries in the tensor network state. + + Args: + gates_update_map: Map from gate (Command) opgroup name to a corresponding + gate unitary. + """ + for gate_label, unitary in gates_update_map.items(): + gate_id = self._mutable_gates_map[gate_label] + gate_n_qubits = math.log2(unitary.shape[0]) + if not gate_n_qubits.is_integer(): + raise ValueError( + f"Gate {gate_label} unitary's number of rows is not a power of two." + ) + gate_tensor = unitary.astype("complex128").reshape( + [2] * (2 * int(gate_n_qubits)), order="F" + ) + cutn.state_update_tensor( + self._handle, self._state, gate_id, gate_tensor.data.ptr, 0, 1 + ) From 94d701900892b5474bf78f5d0b68f29a814fdb6a Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Fri, 2 Feb 2024 17:02:44 +0000 Subject: [PATCH 06/76] Refactoring folder name: exact -> general_state --- pytket/extensions/cutensornet/__init__.py | 6 ++++-- .../extensions/cutensornet/backends/cutensornet_backend.py | 2 +- .../cutensornet/{exact => general_state}/__init__.py | 2 +- .../{exact => general_state}/tensor_network_convert.py | 0 .../{exact => general_state}/tensor_network_state.py | 2 +- .../cutensornet/{exact => general_state}/utils.py | 0 tests/test_cutensornet_postselect.py | 4 +++- tests/test_tensor_network_convert.py | 2 +- tests/test_utils.py | 4 +++- 9 files changed, 14 insertions(+), 8 deletions(-) rename pytket/extensions/cutensornet/{exact => general_state}/__init__.py (90%) rename pytket/extensions/cutensornet/{exact => general_state}/tensor_network_convert.py (100%) rename pytket/extensions/cutensornet/{exact => general_state}/tensor_network_state.py (98%) rename pytket/extensions/cutensornet/{exact => general_state}/utils.py (100%) diff --git a/pytket/extensions/cutensornet/__init__.py b/pytket/extensions/cutensornet/__init__.py index efb27b5a..d32b9d3e 100644 --- a/pytket/extensions/cutensornet/__init__.py +++ b/pytket/extensions/cutensornet/__init__.py @@ -13,9 +13,11 @@ # limitations under the License. """Module for conversion from tket primitives to cuQuantum primitives.""" -from pytket.extensions.cutensornet.exact.utils import circuit_statevector_postselect +from pytket.extensions.cutensornet.general_state.utils import ( + circuit_statevector_postselect, +) -from pytket.extensions.cutensornet.exact.tensor_network_convert import ( +from pytket.extensions.cutensornet.general_state.tensor_network_convert import ( TensorNetwork, PauliOperatorTensorNetwork, ExpectationValueTensorNetwork, diff --git a/pytket/extensions/cutensornet/backends/cutensornet_backend.py b/pytket/extensions/cutensornet/backends/cutensornet_backend.py index 5aa3fed2..5c1af286 100644 --- a/pytket/extensions/cutensornet/backends/cutensornet_backend.py +++ b/pytket/extensions/cutensornet/backends/cutensornet_backend.py @@ -30,7 +30,7 @@ from pytket.backends.backend import KwargTypes, Backend, BackendResult from pytket.backends.backendinfo import BackendInfo from pytket.backends.resulthandle import _ResultIdTuple -from pytket.extensions.cutensornet.exact import ( +from pytket.extensions.cutensornet.general_state import ( TensorNetwork, ExpectationValueTensorNetwork, tk_to_tensor_network, diff --git a/pytket/extensions/cutensornet/exact/__init__.py b/pytket/extensions/cutensornet/general_state/__init__.py similarity index 90% rename from pytket/extensions/cutensornet/exact/__init__.py rename to pytket/extensions/cutensornet/general_state/__init__.py index 12ff4828..453a9de7 100644 --- a/pytket/extensions/cutensornet/exact/__init__.py +++ b/pytket/extensions/cutensornet/general_state/__init__.py @@ -13,7 +13,7 @@ # limitations under the License. """Module for conversion from tket primitives to cuQuantum primitives.""" -from pytket.extensions.cutensornet.exact.utils import circuit_statevector_postselect +from .utils import circuit_statevector_postselect from .tensor_network_convert import ( TensorNetwork, diff --git a/pytket/extensions/cutensornet/exact/tensor_network_convert.py b/pytket/extensions/cutensornet/general_state/tensor_network_convert.py similarity index 100% rename from pytket/extensions/cutensornet/exact/tensor_network_convert.py rename to pytket/extensions/cutensornet/general_state/tensor_network_convert.py diff --git a/pytket/extensions/cutensornet/exact/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py similarity index 98% rename from pytket/extensions/cutensornet/exact/tensor_network_state.py rename to pytket/extensions/cutensornet/general_state/tensor_network_state.py index 68313845..1813de27 100644 --- a/pytket/extensions/cutensornet/exact/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -8,7 +8,7 @@ warnings.warn("local settings failed to import cupy", ImportWarning) from pytket.circuit import Circuit # type: ignore from pytket.extensions.cutensornet.general import set_logger -from pytket.extensions.cutensornet.mps import CuTensorNetHandle +from pytket.extensions.cutensornet.structured_state import CuTensorNetHandle try: import cuquantum as cq # type: ignore diff --git a/pytket/extensions/cutensornet/exact/utils.py b/pytket/extensions/cutensornet/general_state/utils.py similarity index 100% rename from pytket/extensions/cutensornet/exact/utils.py rename to pytket/extensions/cutensornet/general_state/utils.py diff --git a/tests/test_cutensornet_postselect.py b/tests/test_cutensornet_postselect.py index 50c85683..14e2e2c3 100644 --- a/tests/test_cutensornet_postselect.py +++ b/tests/test_cutensornet_postselect.py @@ -9,7 +9,9 @@ TensorNetwork, measure_qubits_state, ) -from pytket.extensions.cutensornet.exact.utils import circuit_statevector_postselect +from pytket.extensions.cutensornet.general_state.utils import ( + circuit_statevector_postselect, +) @pytest.mark.parametrize( diff --git a/tests/test_tensor_network_convert.py b/tests/test_tensor_network_convert.py index f6d09a03..ef7a279e 100644 --- a/tests/test_tensor_network_convert.py +++ b/tests/test_tensor_network_convert.py @@ -15,7 +15,7 @@ warnings.warn("local settings failed to import cutensornet", ImportWarning) from pytket.circuit import Circuit -from pytket.extensions.cutensornet.tensor_network_convert import ( # type: ignore +from pytket.extensions.cutensornet.general_state.tensor_network_convert import ( # type: ignore tk_to_tensor_network, TensorNetwork, ) diff --git a/tests/test_utils.py b/tests/test_utils.py index cad498c3..a4aad95f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,7 @@ import numpy -from pytket.extensions.cutensornet.exact.utils import circuit_statevector_postselect +from pytket.extensions.cutensornet.general_state.utils import ( + circuit_statevector_postselect, +) from pytket import Circuit, Qubit # type: ignore From 3516303a0cc12a1449d721a16a31d5ca34ba641b Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Fri, 2 Feb 2024 17:36:04 +0000 Subject: [PATCH 07/76] Started drafting `GeneralOperator`. --- .../general_state/tensor_network_state.py | 38 +++++++++++++++++++ .../cutensornet/structured_state/general.py | 7 +++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 1813de27..001fe253 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -7,6 +7,7 @@ except ImportError: warnings.warn("local settings failed to import cupy", ImportWarning) from pytket.circuit import Circuit # type: ignore +from pytket.pauli import QubitPauliString # type: ignore from pytket.extensions.cutensornet.general import set_logger from pytket.extensions.cutensornet.structured_state import CuTensorNetHandle @@ -103,3 +104,40 @@ def update_gates(self, gates_update_map: dict) -> None: cutn.state_update_tensor( self._handle, self._state, gate_id, gate_tensor.data.ptr, 0, 1 ) + + def destroy(self): + """Destroys tensor network state.""" + cutn.destroy_state(self._state) + + +class GeneralOperator: + """Handles tensor network operator.""" + + def __init__( + self, + operator: list[tuple[float, QubitPauliString]], + num_qubits: int, + libhandle: CuTensorNetHandle, + loglevel: int = logging.INFO, + ) -> None: + """Constructs a tensor network operator. + + From a list of Pauli strings and corresponding coefficients. + + Args: + operator: List of tuples, containing a Paulistring and a corresponding + numeric coefficient. + num_qubits: Number of qubits in a circuit for which operator is to be defined. + libhandle: cuTensorNet handle. + loglevel: Internal logger output level. + """ + self._logger = set_logger("GeneralOperator", loglevel) + self._handle = libhandle.handle + qubits_dims = (2,) * num_qubits + data_type = cq.cudaDataType.CUDA_C_64F # TODO: implement a config class? + self._operator = cutn.create_network_operator( + self._handle, num_qubits, qubits_dims, data_type + ) + + def append_pauli_string(self, pauli_string: QubitPauliString, coeff: float) -> None: + pass diff --git a/pytket/extensions/cutensornet/structured_state/general.py b/pytket/extensions/cutensornet/structured_state/general.py index bb2af0a0..928d6f66 100644 --- a/pytket/extensions/cutensornet/structured_state/general.py +++ b/pytket/extensions/cutensornet/structured_state/general.py @@ -69,8 +69,7 @@ def __enter__(self) -> CuTensorNetHandle: return self def __exit__(self, exc_type: Any, exc_value: Any, exc_tb: Any) -> None: - cutn.destroy(self.handle) - self._is_destroyed = True + self.destroy() def print_device_properties(self, logger: Logger): """Prints local GPU properties.""" @@ -84,6 +83,10 @@ def print_device_properties(self, logger: Logger): logger.debug("GPU-minor:", device_props["minor"]) logger.debug("========================") + def destroy(self): + cutn.destroy(self.handle) + self._is_destroyed = True + class Config: """Configuration class for simulation using ``StructuredState``.""" From 6695940862fd1e268c69c0d8590de068a4e9c3a8 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Mon, 5 Feb 2024 14:23:17 +0000 Subject: [PATCH 08/76] Finished drafting `GeneralOperator` class. --- .../general_state/tensor_network_state.py | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 001fe253..776350d7 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -6,6 +6,7 @@ import cupy as cp # type: ignore except ImportError: warnings.warn("local settings failed to import cupy", ImportWarning) +from numpy.typing import NDArray from pytket.circuit import Circuit # type: ignore from pytket.pauli import QubitPauliString # type: ignore from pytket.extensions.cutensornet.general import set_logger @@ -61,10 +62,9 @@ def __init__( ) self._mutable_gates_map = {} for com in circuit.get_commands(): - gate_tensor = ( - com.op.get_unitary() - .astype("complex128") - .reshape([2] * (2 * com.op.n_qubits), order="F") + gate_unitary = com.op.get_unitary().astype("complex128", copy=False) + gate_tensor = cp.asarray(gate_unitary, dtype="complex128").reshape( + [2] * (2 * com.op.n_qubits), order="F" ) # TODO: why column-major order? gate_strides = 0 # Always 0? gate_qubit_indices = [self._circuit.qubits.index(qb) for qb in com.qubits] @@ -84,7 +84,7 @@ def __init__( if com.opgroup is not None: self._mutable_gates_map[com.opgroup] = gate_id - def update_gates(self, gates_update_map: dict) -> None: + def update_gates(self, gates_update_map: dict[str, NDArray]) -> None: """Updates gate unitaries in the tensor network state. Args: @@ -98,14 +98,14 @@ def update_gates(self, gates_update_map: dict) -> None: raise ValueError( f"Gate {gate_label} unitary's number of rows is not a power of two." ) - gate_tensor = unitary.astype("complex128").reshape( + gate_tensor = cp.asarray(unitary, dtype="complex128").reshape( [2] * (2 * int(gate_n_qubits)), order="F" ) cutn.state_update_tensor( self._handle, self._state, gate_id, gate_tensor.data.ptr, 0, 1 ) - def destroy(self): + def destroy(self) -> None: """Destroys tensor network state.""" cutn.destroy_state(self._state) @@ -113,9 +113,16 @@ def destroy(self): class GeneralOperator: """Handles tensor network operator.""" + PAULI = { + "X": cp.array([[0, 1], [1, 0]], dtype="complex128", order="F"), + "Y": cp.array([[0, -1j], [1j, 0]], dtype="complex128", order="F"), + "Z": cp.array([[1, 0], [0, -1]], dtype="complex128", order="F"), + "I": cp.array([[1, 0], [0, 1]], dtype="complex128", order="F"), + } + def __init__( self, - operator: list[tuple[float, QubitPauliString]], + operator: list[tuple[complex, QubitPauliString]], num_qubits: int, libhandle: CuTensorNetHandle, loglevel: int = logging.INFO, @@ -125,9 +132,10 @@ def __init__( From a list of Pauli strings and corresponding coefficients. Args: - operator: List of tuples, containing a Paulistring and a corresponding + operator: List of tuples, containing a Pauli string and a corresponding numeric coefficient. - num_qubits: Number of qubits in a circuit for which operator is to be defined. + num_qubits: Number of qubits in a circuit for which operator is to be + defined. libhandle: cuTensorNet handle. loglevel: Internal logger output level. """ @@ -138,6 +146,35 @@ def __init__( self._operator = cutn.create_network_operator( self._handle, num_qubits, qubits_dims, data_type ) + for coeff, pauli_string in operator: + self.append_pauli_string(pauli_string, coeff) + + def append_pauli_string( + self, pauli_string: QubitPauliString, coeff: complex + ) -> None: + """Appends a Pauli string to a tensor network operator. + + Args: + pauli_string: A Pauli string. + coeff: Numeric coefficient. + """ + num_pauli = len(pauli_string.map) + num_modes = (1,) * num_pauli + state_modes = tuple((qubit.index[0],) for qubit in pauli_string.map.keys()) + gate_data = tuple( + self.PAULI[pauli.name].data.ptr for pauli in pauli_string.map.values() + ) + cutn.network_operator_append_product( + self._handle, + self._operator, + coeff, + num_pauli, + num_modes, + state_modes, + 0, + gate_data, + ) - def append_pauli_string(self, pauli_string: QubitPauliString, coeff: float) -> None: - pass + def destroy(self) -> None: + """Destroys tensor network operator.""" + cutn.destroy_network_operator() From 2df1abb1201b017dac1e0214e150fadada20b82e Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Mon, 5 Feb 2024 17:38:54 +0000 Subject: [PATCH 09/76] Started drafting `GeneralExpectationValue` class. --- .../general_state/tensor_network_state.py | 109 +++++++++++++++++- 1 file changed, 104 insertions(+), 5 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 776350d7..d3cbec97 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -6,6 +6,7 @@ import cupy as cp # type: ignore except ImportError: warnings.warn("local settings failed to import cupy", ImportWarning) +import numpy as np from numpy.typing import NDArray from pytket.circuit import Circuit # type: ignore from pytket.pauli import QubitPauliString # type: ignore @@ -52,10 +53,6 @@ def __init__( data_type = cq.cudaDataType.CUDA_C_64F # for now let that be hard-coded # This is only required (if at all?) when doing evaluation - # free_mem = libhandle.dev.mem_info[0] - # use half of the total free size - # scratch_size = free_mem // 2 - # scratch_space = cp.cuda.alloc(scratch_size) self._state = cutn.create_state( self._handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type @@ -90,6 +87,9 @@ def update_gates(self, gates_update_map: dict[str, NDArray]) -> None: Args: gates_update_map: Map from gate (Command) opgroup name to a corresponding gate unitary. + + Raises: + ValueError: If a gate's unitary is of a wrong size. """ for gate_label, unitary in gates_update_map.items(): gate_id = self._mutable_gates_map[gate_label] @@ -146,6 +146,7 @@ def __init__( self._operator = cutn.create_network_operator( self._handle, num_qubits, qubits_dims, data_type ) + self._logger.debug("Adding operator terms:") for coeff, pauli_string in operator: self.append_pauli_string(pauli_string, coeff) @@ -158,6 +159,7 @@ def append_pauli_string( pauli_string: A Pauli string. coeff: Numeric coefficient. """ + self._logger.debug(f" {coeff}, {pauli_string}") num_pauli = len(pauli_string.map) num_modes = (1,) * num_pauli state_modes = tuple((qubit.index[0],) for qubit in pauli_string.map.keys()) @@ -177,4 +179,101 @@ def append_pauli_string( def destroy(self) -> None: """Destroys tensor network operator.""" - cutn.destroy_network_operator() + cutn.destroy_network_operator(self._operator) + + +class GeneralExpectationValue: + """Handles a general tensor network operator expectation value.""" + + def __init__( + self, + state: GeneralState, + operator: GeneralOperator, + libhandle: CuTensorNetHandle, + loglevel: int = logging.INFO, + num_hyper_samples: int = 8, + scratch_fraction: float = 0.5, + ) -> None: + """Initialises expectation value object and corresponding work space. + + Notes: + State and Operator must have the same handle as ExpectationValue. + State (and Operator?) need to exist during the whole lifetime of + ExpectationValue. + + Args: + state: General tensor network state. + operator: General tensor network operator. + libhandle: cuTensorNet handle. + loglevel: Internal logger output level. + num_hyper_samples: Number of hyper samples to use at contraction. + scratch_fraction: Fraction of free memory on GPU to allocate as scratch + space. + + Raises: + MemoryError: If there is insufficient workspace size on a GPU device. + """ + self._handle = libhandle.handle + self._logger = set_logger("GeneralExpectationValue", loglevel) + + self._expectation = cutn.create_expectation(self._handle, state, operator) + + # Configure expectation value contraction. + # TODO: factor into a separate method, if order-independent with workspace + # allocation + num_hyper_samples_dtype = cutn.expectation_get_attribute_dtype( + cutn.ExpectationAttribute.OPT_NUM_HYPER_SAMPLES + ) + num_hyper_samples = np.asarray(num_hyper_samples, dtype=num_hyper_samples_dtype) + cutn.expectation_configure( + self._handle, + self._expectation, + cutn.ExpectationAttribute.OPT_NUM_HYPER_SAMPLES, + num_hyper_samples.ctypes.data, + num_hyper_samples.dtype.itemsize, + ) + + # Set a workspace. One may consider doing this somewhere else outside of the + # class, but it seems to really only needed for expectation value. + # TODO: need to figure out if this needs to be done explicitly at all + stream = cp.cuda.Stream() # In current cuTN release it is unused (could be 0x0) + free_mem = libhandle.dev.mem_info[0] + scratch_size = int(scratch_fraction * free_mem) + self._scratch_space = cp.cuda.alloc(scratch_size) + self._logger.debug(f"Allocated {scratch_size} bytes of scratch memory on GPU") + self._work_desc = cutn.create_workspace_descriptor(self._handle) + cutn.expectation_prepare( + self._handle, self._expectation, scratch_size, self._work_desc, stream.ptr + ) + workspace_size_d = cutn.workspace_get_memory_size( + self._handle, + self._work_desc, + cutn.WorksizePref.RECOMMENDED, + cutn.Memspace.DEVICE, + cutn.WorkspaceKind.SCRATCH, + ) + + if workspace_size_d <= scratch_size: + cutn.workspace_set_memory( + self._handle, + self._work_desc, + cutn.Memspace.DEVICE, + cutn.WorkspaceKind.SCRATCH, + self._scratch_space.ptr, + workspace_size_d, + ) + self._logger.debug( + f"Set {workspace_size_d} bytes of workspace memory out of the allocated" + f" scratch space." + ) + else: + self.destroy() + raise MemoryError( + f"Insufficient workspace size on the GPU device {self._handle.dev.id}" + ) + + def destroy(self) -> None: + """Destroys tensor network expectation value and workspace descriptor.""" + cutn.destroy_workspace_descriptor(self._work_desc) + cutn.destroy_expectation(self._expectation) + del self._scratch_space # TODO is this the correct way? From c67074948bd130a4ff79a34bf482d84b65c31eaf Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Wed, 7 Feb 2024 12:41:30 +0000 Subject: [PATCH 10/76] Finished drafting `GeneralExpectationValue` class. --- .../general_state/tensor_network_state.py | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index d3cbec97..c43c4b57 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -234,16 +234,22 @@ def __init__( ) # Set a workspace. One may consider doing this somewhere else outside of the - # class, but it seems to really only needed for expectation value. + # class, but it seems to be really only needed for expectation value. # TODO: need to figure out if this needs to be done explicitly at all - stream = cp.cuda.Stream() # In current cuTN release it is unused (could be 0x0) + self._stream = ( + cp.cuda.Stream() + ) # In current cuTN release it is unused (could be 0x0) free_mem = libhandle.dev.mem_info[0] scratch_size = int(scratch_fraction * free_mem) self._scratch_space = cp.cuda.alloc(scratch_size) self._logger.debug(f"Allocated {scratch_size} bytes of scratch memory on GPU") self._work_desc = cutn.create_workspace_descriptor(self._handle) cutn.expectation_prepare( - self._handle, self._expectation, scratch_size, self._work_desc, stream.ptr + self._handle, + self._expectation, + scratch_size, + self._work_desc, + self._stream.ptr, ) workspace_size_d = cutn.workspace_get_memory_size( self._handle, @@ -272,6 +278,21 @@ def __init__( f"Insufficient workspace size on the GPU device {self._handle.dev.id}" ) + def compute(self) -> tuple[complex, complex]: + """Computes expectation value.""" + expectation_value = np.empty(1, dtype="complex128") + state_norm = np.empty(1, dtype="complex128") + cutn.expectation_compute( + self._handle, + self._expectation, + self._work_desc, + expectation_value.ctypes.data, + state_norm.ctypes.data, + self._stream.ptr, + ) + self._stream.synchronize() + return expectation_value.item(), state_norm.item() + def destroy(self) -> None: """Destroys tensor network expectation value and workspace descriptor.""" cutn.destroy_workspace_descriptor(self._work_desc) From bce7c9519baf885d69a5f5253225c56ec0dd8e59 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Wed, 7 Feb 2024 18:25:26 +0000 Subject: [PATCH 11/76] Added `.configure()`, `.prepare()` and drafted `compute()` in `GeneralState`. --- .../general_state/tensor_network_state.py | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index c43c4b57..5a28d61e 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -1,5 +1,6 @@ import logging import math +from typing import Optional import warnings try: @@ -52,7 +53,10 @@ def __init__( self._logger.debug(f"Converting a quantum circuit with {num_qubits} qubits.") data_type = cq.cudaDataType.CUDA_C_64F # for now let that be hard-coded - # This is only required (if at all?) when doing evaluation + # These are only required when doing preparation and evaluation. + self._stream = None + self._scratch_space = None + self._work_desc = None self._state = cutn.create_state( self._handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type @@ -105,6 +109,91 @@ def update_gates(self, gates_update_map: dict[str, NDArray]) -> None: self._handle, self._state, gate_id, gate_tensor.data.ptr, 0, 1 ) + def configure(self, attributes: Optional[dict] = None) -> None: + """Configures tensor network state for future contraction. + + Args: + attributes: A dict of cuTensorNet State attributes and their values. + """ + if attributes is None: + attributes = {"OPT_NUM_HYPER_SAMPLES": 8} + attribute_values = [val for val in attributes.values()] + attributes = [getattr(cutn.StateAttribute, attr) for attr in attributes.keys()] + for attr, val in zip(attributes, attribute_values): + attr_dtype = cutn.state_get_attribute_dtype(attr) + attr_arr = np.asarray(val, dtype=attr_dtype) + cutn.state_configure( + self._handle, + self._state, + attr_dtype, + attr_arr.ctypes.data, + attr_arr.dtype.itemsize, + ) + + def prepare(self, scratch_fraction: float = 0.5) -> None: + """Prepare tensor network state for future contraction. + + Allocates workspace memory necessary for contraction. + + Args: + scratch_fraction: Fraction of free memory on GPU to allocate as scratch + space. + """ + self._stream = ( + cp.cuda.Stream() + ) # In current cuTN release it is unused (could be 0x0) + free_mem = self._handle.dev.mem_info[0] + scratch_size = int(scratch_fraction * free_mem) + self._scratch_space = cp.cuda.alloc(scratch_size) + self._logger.debug(f"Allocated {scratch_size} bytes of scratch memory on GPU") + self._work_desc = cutn.create_workspace_descriptor(self._handle) + cutn.state_prepare( + self._handle, + self._state, + scratch_size, + self._work_desc, + self._stream.ptr, + ) + workspace_size_d = cutn.workspace_get_memory_size( + self._handle, + self._work_desc, + cutn.WorksizePref.RECOMMENDED, + cutn.Memspace.DEVICE, + cutn.WorkspaceKind.SCRATCH, + ) + + if workspace_size_d <= scratch_size: + cutn.workspace_set_memory( + self._handle, + self._work_desc, + cutn.Memspace.DEVICE, + cutn.WorkspaceKind.SCRATCH, + self._scratch_space.ptr, + workspace_size_d, + ) + self._logger.debug( + f"Set {workspace_size_d} bytes of workspace memory out of the allocated" + f" scratch space." + ) + else: + cutn.destroy_workspace_descriptor(self._work_desc) + del self._scratch_space # TODO: is it OK to do so? + + def compute(self) -> tuple: + """Evaluates state vector.""" + state_vector = cp.asarray(pow(self._circuit.n_qubits, 2), dtype="complex128") + cutn.state_compute( + self._handle, + self._state, + self._work_desc, + 0, + 0, + state_vector, + self._stream.ptr, + ) + self._stream.synchronize() + return state_vector + def destroy(self) -> None: """Destroys tensor network state.""" cutn.destroy_state(self._state) From 8a6dccfd090ac29422582d879c78ccbe1554f21a Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Sun, 18 Feb 2024 12:11:20 +0000 Subject: [PATCH 12/76] Imports additions/refactor. --- pytket/extensions/cutensornet/__init__.py | 12 ------------ .../extensions/cutensornet/general_state/__init__.py | 2 ++ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/pytket/extensions/cutensornet/__init__.py b/pytket/extensions/cutensornet/__init__.py index d32b9d3e..a3ed1b65 100644 --- a/pytket/extensions/cutensornet/__init__.py +++ b/pytket/extensions/cutensornet/__init__.py @@ -13,18 +13,6 @@ # limitations under the License. """Module for conversion from tket primitives to cuQuantum primitives.""" -from pytket.extensions.cutensornet.general_state.utils import ( - circuit_statevector_postselect, -) - -from pytket.extensions.cutensornet.general_state.tensor_network_convert import ( - TensorNetwork, - PauliOperatorTensorNetwork, - ExpectationValueTensorNetwork, - tk_to_tensor_network, - measure_qubits_state, -) - from .backends import CuTensorNetBackend # _metadata.py is copied to the folder after installation. diff --git a/pytket/extensions/cutensornet/general_state/__init__.py b/pytket/extensions/cutensornet/general_state/__init__.py index 453a9de7..20d19c79 100644 --- a/pytket/extensions/cutensornet/general_state/__init__.py +++ b/pytket/extensions/cutensornet/general_state/__init__.py @@ -22,3 +22,5 @@ tk_to_tensor_network, measure_qubits_state, ) + +from .tensor_network_state import GeneralState, GeneralOperator, GeneralExpectationValue From b1d72955e9d6cb010ba38d6237f06058f5109eb7 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Sun, 18 Feb 2024 14:23:34 +0000 Subject: [PATCH 13/76] Fix in `cutn` attributes. --- .../cutensornet/general_state/tensor_network_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 5a28d61e..6704765d 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -116,7 +116,7 @@ def configure(self, attributes: Optional[dict] = None) -> None: attributes: A dict of cuTensorNet State attributes and their values. """ if attributes is None: - attributes = {"OPT_NUM_HYPER_SAMPLES": 8} + attributes = {"NUM_HYPER_SAMPLES": 8} attribute_values = [val for val in attributes.values()] attributes = [getattr(cutn.StateAttribute, attr) for attr in attributes.keys()] for attr, val in zip(attributes, attribute_values): From c196e4d5cf5754b96a56763a8ef35217f43612d9 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Sun, 18 Feb 2024 14:45:21 +0000 Subject: [PATCH 14/76] Fix a bug in `GeneralState.configure()`. --- .../cutensornet/general_state/tensor_network_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 6704765d..852bd5db 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -125,7 +125,7 @@ def configure(self, attributes: Optional[dict] = None) -> None: cutn.state_configure( self._handle, self._state, - attr_dtype, + attr, attr_arr.ctypes.data, attr_arr.dtype.itemsize, ) From 0faf65a60f76ac9510b64f50768dac4318477e52 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Sun, 18 Feb 2024 14:50:03 +0000 Subject: [PATCH 15/76] Handle device aliasing bug fix. --- .../cutensornet/general_state/tensor_network_state.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 852bd5db..a156935b 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -44,6 +44,7 @@ def __init__( self._logger = set_logger("GeneralState", loglevel) self._circuit = circuit self._handle = libhandle.handle + self._dev = libhandle.dev libhandle.print_device_properties(self._logger) @@ -142,7 +143,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> None: self._stream = ( cp.cuda.Stream() ) # In current cuTN release it is unused (could be 0x0) - free_mem = self._handle.dev.mem_info[0] + free_mem = self._dev.mem_info[0] scratch_size = int(scratch_fraction * free_mem) self._scratch_space = cp.cuda.alloc(scratch_size) self._logger.debug(f"Allocated {scratch_size} bytes of scratch memory on GPU") @@ -303,6 +304,7 @@ def __init__( MemoryError: If there is insufficient workspace size on a GPU device. """ self._handle = libhandle.handle + self._dev = libhandle.dev self._logger = set_logger("GeneralExpectationValue", loglevel) self._expectation = cutn.create_expectation(self._handle, state, operator) @@ -328,7 +330,7 @@ def __init__( self._stream = ( cp.cuda.Stream() ) # In current cuTN release it is unused (could be 0x0) - free_mem = libhandle.dev.mem_info[0] + free_mem = self._dev.mem_info[0] scratch_size = int(scratch_fraction * free_mem) self._scratch_space = cp.cuda.alloc(scratch_size) self._logger.debug(f"Allocated {scratch_size} bytes of scratch memory on GPU") @@ -364,7 +366,7 @@ def __init__( else: self.destroy() raise MemoryError( - f"Insufficient workspace size on the GPU device {self._handle.dev.id}" + f"Insufficient workspace size on the GPU device {self._dev.id}" ) def compute(self) -> tuple[complex, complex]: From e4defcd87022ef26d2a5075875f5e823168c4939 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Tue, 20 Feb 2024 09:45:52 +0000 Subject: [PATCH 16/76] Corrected `cutn.state_compute()` arguments list. --- .../cutensornet/general_state/tensor_network_state.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index a156935b..7c81deee 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -187,8 +187,6 @@ def compute(self) -> tuple: self._handle, self._state, self._work_desc, - 0, - 0, state_vector, self._stream.ptr, ) From 4cdb622bbe277eb46ef9f53918fd07deb5bd1816 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Fri, 23 Feb 2024 13:30:07 +0000 Subject: [PATCH 17/76] Fixed `cutn.state_compute()`. --- .../general_state/tensor_network_state.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 7c81deee..53eb87ef 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -1,6 +1,6 @@ import logging import math -from typing import Optional +from typing import Union, Optional import warnings try: @@ -180,18 +180,31 @@ def prepare(self, scratch_fraction: float = 0.5) -> None: cutn.destroy_workspace_descriptor(self._work_desc) del self._scratch_space # TODO: is it OK to do so? - def compute(self) -> tuple: - """Evaluates state vector.""" - state_vector = cp.asarray(pow(self._circuit.n_qubits, 2), dtype="complex128") + def compute(self, on_host: bool = True) -> Union[cp.ndarray, np.ndarray]: + """Evaluates state vector. + + Args: + on_host: If :code:`True`, converts cupy :code:`ndarray` to numpy + :code:`ndarray`, copying it to host device (CPU). + + Returns: + Either a :code:`cupy.ndarray` on a GPU, or a :code:`numpy.ndarray` on a + host device (CPU). + """ + state_vector = cp.empty( + (2,) * self._circuit.n_qubits, dtype="complex128", order="F" + ) cutn.state_compute( self._handle, self._state, self._work_desc, - state_vector, + (state_vector.data.ptr,), self._stream.ptr, ) self._stream.synchronize() - return state_vector + if on_host: + return cp.asnumpy(state_vector.flatten()) + return state_vector.flatten() def destroy(self) -> None: """Destroys tensor network state.""" From 9e64915199dcc8ffeda5a3c211e601a549b936df Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Fri, 23 Feb 2024 16:45:44 +0000 Subject: [PATCH 18/76] Factored our configure and prepare steps for `GeneralExpectationValue`. --- .../general_state/tensor_network_state.py | 67 ++++++++++++------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 53eb87ef..5ad74c2e 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -67,7 +67,7 @@ def __init__( gate_unitary = com.op.get_unitary().astype("complex128", copy=False) gate_tensor = cp.asarray(gate_unitary, dtype="complex128").reshape( [2] * (2 * com.op.n_qubits), order="F" - ) # TODO: why column-major order? + ) gate_strides = 0 # Always 0? gate_qubit_indices = [self._circuit.qubits.index(qb) for qb in com.qubits] gate_n_qubits = len(gate_qubit_indices) @@ -177,8 +177,10 @@ def prepare(self, scratch_fraction: float = 0.5) -> None: f" scratch space." ) else: - cutn.destroy_workspace_descriptor(self._work_desc) - del self._scratch_space # TODO: is it OK to do so? + self.destroy() + raise MemoryError( + f"Insufficient workspace size on the GPU device {self._dev.id}" + ) def compute(self, on_host: bool = True) -> Union[cp.ndarray, np.ndarray]: """Evaluates state vector. @@ -208,7 +210,10 @@ def compute(self, on_host: bool = True) -> Union[cp.ndarray, np.ndarray]: def destroy(self) -> None: """Destroys tensor network state.""" + if self._work_desc is not None: + cutn.destroy_workspace_descriptor(self._work_desc) cutn.destroy_state(self._state) + del self._scratch_space class GeneralOperator: @@ -292,8 +297,6 @@ def __init__( operator: GeneralOperator, libhandle: CuTensorNetHandle, loglevel: int = logging.INFO, - num_hyper_samples: int = 8, - scratch_fraction: float = 0.5, ) -> None: """Initialises expectation value object and corresponding work space. @@ -307,9 +310,6 @@ def __init__( operator: General tensor network operator. libhandle: cuTensorNet handle. loglevel: Internal logger output level. - num_hyper_samples: Number of hyper samples to use at contraction. - scratch_fraction: Fraction of free memory on GPU to allocate as scratch - space. Raises: MemoryError: If there is insufficient workspace size on a GPU device. @@ -320,23 +320,42 @@ def __init__( self._expectation = cutn.create_expectation(self._handle, state, operator) - # Configure expectation value contraction. - # TODO: factor into a separate method, if order-independent with workspace - # allocation - num_hyper_samples_dtype = cutn.expectation_get_attribute_dtype( - cutn.ExpectationAttribute.OPT_NUM_HYPER_SAMPLES - ) - num_hyper_samples = np.asarray(num_hyper_samples, dtype=num_hyper_samples_dtype) - cutn.expectation_configure( - self._handle, - self._expectation, - cutn.ExpectationAttribute.OPT_NUM_HYPER_SAMPLES, - num_hyper_samples.ctypes.data, - num_hyper_samples.dtype.itemsize, - ) + def configure(self, attributes: Optional[dict] = None) -> None: + """Configures expectation value for future contraction. - # Set a workspace. One may consider doing this somewhere else outside of the - # class, but it seems to be really only needed for expectation value. + Args: + attributes: A map of cuTensorNet :code:`ExpectationAttribute` objects to + their values. + + Note: + Currently :code:`ExpectationAttribute` has only one attribute. + """ + if attributes is None: + attributes = {"OPT_NUM_HYPER_SAMPLES": 8} + attribute_values = [val for val in attributes.values()] + attributes = [ + getattr(cutn.ExpectationAttribute, attr) for attr in attributes.keys() + ] + for attr, val in zip(attributes, attribute_values): + attr_dtype = cutn.expectation_get_attribute_dtype(attr) + attr_arr = np.asarray(val, dtype=attr_dtype) + cutn.expectation_configure( + self._handle, + self._expectation, + attr, + attr_arr.ctypes.data, + attr_arr.dtype.itemsize, + ) + + def prepare(self, scratch_fraction: float = 0.5) -> None: + """Prepare tensor network state for future contraction. + + Allocates workspace memory necessary for contraction. + + Args: + scratch_fraction: Fraction of free memory on GPU to allocate as scratch + space. + """ # TODO: need to figure out if this needs to be done explicitly at all self._stream = ( cp.cuda.Stream() From c5df6dfdbcc0320b850f6ae90ff4095019632237 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Thu, 29 Feb 2024 17:36:07 +0000 Subject: [PATCH 19/76] Fixes to `GeneralState` after debugging. --- .../general_state/tensor_network_state.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 5ad74c2e..e30d7946 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -63,21 +63,24 @@ def __init__( self._handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type ) self._mutable_gates_map = {} + self._gate_tensors = [] for com in circuit.get_commands(): gate_unitary = com.op.get_unitary().astype("complex128", copy=False) - gate_tensor = cp.asarray(gate_unitary, dtype="complex128").reshape( - [2] * (2 * com.op.n_qubits), order="F" + self._gate_tensors.append( + cp.asarray(gate_unitary, dtype="complex128").reshape( + [2] * (2 * com.op.n_qubits), order="F" + ) ) gate_strides = 0 # Always 0? - gate_qubit_indices = [self._circuit.qubits.index(qb) for qb in com.qubits] - gate_n_qubits = len(gate_qubit_indices) - gate_qubit_indices_reversed = tuple(reversed(gate_qubit_indices)) + gate_qubit_indices = tuple( + self._circuit.qubits.index(qb) for qb in com.qubits + ) gate_id = cutn.state_apply_tensor( self._handle, self._state, - gate_n_qubits, - gate_qubit_indices_reversed, - gate_tensor.data.ptr, + com.op.n_qubits, + gate_qubit_indices, + self._gate_tensors[-1].data.ptr, gate_strides, 1, 0, @@ -107,7 +110,7 @@ def update_gates(self, gates_update_map: dict[str, NDArray]) -> None: [2] * (2 * int(gate_n_qubits)), order="F" ) cutn.state_update_tensor( - self._handle, self._state, gate_id, gate_tensor.data.ptr, 0, 1 + self._handle, self._state, gate_id, gate_tensor.data.ptr, 1 ) def configure(self, attributes: Optional[dict] = None) -> None: From 5cd6936b743f16c1e8b9908e0330a881d9e72d0e Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Thu, 29 Feb 2024 17:59:41 +0000 Subject: [PATCH 20/76] Fixes to `GeneralOperator` after debugging. --- .../general_state/tensor_network_state.py | 58 ++++++++----------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index e30d7946..674453a4 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -222,13 +222,6 @@ def destroy(self) -> None: class GeneralOperator: """Handles tensor network operator.""" - PAULI = { - "X": cp.array([[0, 1], [1, 0]], dtype="complex128", order="F"), - "Y": cp.array([[0, -1j], [1j, 0]], dtype="complex128", order="F"), - "Z": cp.array([[1, 0], [0, -1]], dtype="complex128", order="F"), - "I": cp.array([[1, 0], [0, 1]], dtype="complex128", order="F"), - } - def __init__( self, operator: list[tuple[complex, QubitPauliString]], @@ -248,6 +241,12 @@ def __init__( libhandle: cuTensorNet handle. loglevel: Internal logger output level. """ + self._pauli = { + "X": cp.asarray([[0, 1], [1, 0]]).astype("complex128", order="F"), + "Y": cp.asarray([[0, -1j], [1j, 0]]).astype("complex128", order="F"), + "Z": cp.asarray([[1, 0], [0, -1]]).astype("complex128", order="F"), + "I": cp.asarray([[1, 0], [0, 1]]).astype("complex128", order="F"), + } self._logger = set_logger("GeneralOperator", loglevel) self._handle = libhandle.handle qubits_dims = (2,) * num_qubits @@ -257,34 +256,23 @@ def __init__( ) self._logger.debug("Adding operator terms:") for coeff, pauli_string in operator: - self.append_pauli_string(pauli_string, coeff) - - def append_pauli_string( - self, pauli_string: QubitPauliString, coeff: complex - ) -> None: - """Appends a Pauli string to a tensor network operator. - - Args: - pauli_string: A Pauli string. - coeff: Numeric coefficient. - """ - self._logger.debug(f" {coeff}, {pauli_string}") - num_pauli = len(pauli_string.map) - num_modes = (1,) * num_pauli - state_modes = tuple((qubit.index[0],) for qubit in pauli_string.map.keys()) - gate_data = tuple( - self.PAULI[pauli.name].data.ptr for pauli in pauli_string.map.values() - ) - cutn.network_operator_append_product( - self._handle, - self._operator, - coeff, - num_pauli, - num_modes, - state_modes, - 0, - gate_data, - ) + self._logger.debug(f" {coeff}, {pauli_string}") + num_pauli = len(pauli_string.map) + num_modes = (1,) * num_pauli + state_modes = tuple((qubit.index[0],) for qubit in pauli_string.map.keys()) + gate_data = tuple( + self._pauli[pauli.name].data.ptr for pauli in pauli_string.map.values() + ) + cutn.network_operator_append_product( + self._handle, + self._operator, + coeff, + num_pauli, + num_modes, + state_modes, + 0, + gate_data, + ) def destroy(self) -> None: """Destroys tensor network operator.""" From fb343057d2a999ae1eff977b3546499feb606ba2 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Thu, 29 Feb 2024 18:11:42 +0000 Subject: [PATCH 21/76] Fixes to `GeneralExpectationValue` after debugging. --- .../cutensornet/general_state/tensor_network_state.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 674453a4..3a480319 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -309,7 +309,13 @@ def __init__( self._dev = libhandle.dev self._logger = set_logger("GeneralExpectationValue", loglevel) - self._expectation = cutn.create_expectation(self._handle, state, operator) + self._stream = None + self._scratch_space = None + self._work_desc = None + + self._expectation = cutn.create_expectation( + self._handle, state._state, operator._operator + ) def configure(self, attributes: Optional[dict] = None) -> None: """Configures expectation value for future contraction. @@ -407,6 +413,7 @@ def compute(self) -> tuple[complex, complex]: def destroy(self) -> None: """Destroys tensor network expectation value and workspace descriptor.""" - cutn.destroy_workspace_descriptor(self._work_desc) + if self._work_desc is not None: + cutn.destroy_workspace_descriptor(self._work_desc) cutn.destroy_expectation(self._expectation) del self._scratch_space # TODO is this the correct way? From 1eb091f73bc5a5f8358d6c287bd20a0829ba5c0e Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Fri, 1 Mar 2024 11:35:34 +0000 Subject: [PATCH 22/76] Added `state` and `operator` properties to corresponding classes. --- .../cutensornet/general_state/tensor_network_state.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 3a480319..bbc5e2a3 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -89,6 +89,11 @@ def __init__( if com.opgroup is not None: self._mutable_gates_map[com.opgroup] = gate_id + @property + def state(self) -> int: + """Returns tensor network state handle as Python :code:`int`.""" + return self._state + def update_gates(self, gates_update_map: dict[str, NDArray]) -> None: """Updates gate unitaries in the tensor network state. @@ -274,6 +279,11 @@ def __init__( gate_data, ) + @property + def operator(self) -> int: + """Returns tensor network operator handle as Python :code:`int`.""" + return self._operator + def destroy(self) -> None: """Destroys tensor network operator.""" cutn.destroy_network_operator(self._operator) From 71aff80a76a9743c72c8ff66f3101340929a6458 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Fri, 1 Mar 2024 13:49:58 +0000 Subject: [PATCH 23/76] Enabled chaining of some methods. --- .../general_state/tensor_network_state.py | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index bbc5e2a3..04de05f9 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -1,3 +1,4 @@ +from __future__ import annotations import logging import math from typing import Union, Optional @@ -118,11 +119,14 @@ def update_gates(self, gates_update_map: dict[str, NDArray]) -> None: self._handle, self._state, gate_id, gate_tensor.data.ptr, 1 ) - def configure(self, attributes: Optional[dict] = None) -> None: + def configure(self, attributes: Optional[dict] = None) -> GeneralState: """Configures tensor network state for future contraction. Args: attributes: A dict of cuTensorNet State attributes and their values. + + Returns: + Self (to allow for chaining with other methods). """ if attributes is None: attributes = {"NUM_HYPER_SAMPLES": 8} @@ -138,15 +142,22 @@ def configure(self, attributes: Optional[dict] = None) -> None: attr_arr.ctypes.data, attr_arr.dtype.itemsize, ) + return self - def prepare(self, scratch_fraction: float = 0.5) -> None: + def prepare(self, scratch_fraction: float = 0.5) -> GeneralState: """Prepare tensor network state for future contraction. Allocates workspace memory necessary for contraction. + Raises: + MemoryError: If there is insufficient workspace on GPU. + Args: scratch_fraction: Fraction of free memory on GPU to allocate as scratch space. + + Returns: + Self (to allow for chaining with other methods). """ self._stream = ( cp.cuda.Stream() @@ -184,6 +195,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> None: f"Set {workspace_size_d} bytes of workspace memory out of the allocated" f" scratch space." ) + return self else: self.destroy() raise MemoryError( @@ -199,7 +211,7 @@ def compute(self, on_host: bool = True) -> Union[cp.ndarray, np.ndarray]: Returns: Either a :code:`cupy.ndarray` on a GPU, or a :code:`numpy.ndarray` on a - host device (CPU). + host device (CPU). Arrays are returned in a 1D shape. """ state_vector = cp.empty( (2,) * self._circuit.n_qubits, dtype="complex128", order="F" @@ -327,7 +339,7 @@ def __init__( self._handle, state._state, operator._operator ) - def configure(self, attributes: Optional[dict] = None) -> None: + def configure(self, attributes: Optional[dict] = None) -> GeneralExpectationValue: """Configures expectation value for future contraction. Args: @@ -336,6 +348,9 @@ def configure(self, attributes: Optional[dict] = None) -> None: Note: Currently :code:`ExpectationAttribute` has only one attribute. + + Returns: + Self (to allow for chaining with other methods). """ if attributes is None: attributes = {"OPT_NUM_HYPER_SAMPLES": 8} @@ -353,15 +368,22 @@ def configure(self, attributes: Optional[dict] = None) -> None: attr_arr.ctypes.data, attr_arr.dtype.itemsize, ) + return self - def prepare(self, scratch_fraction: float = 0.5) -> None: + def prepare(self, scratch_fraction: float = 0.5) -> GeneralExpectationValue: """Prepare tensor network state for future contraction. Allocates workspace memory necessary for contraction. + Raises: + MemoryError: If there is insufficient space on the GPU device. + Args: scratch_fraction: Fraction of free memory on GPU to allocate as scratch space. + + Returns: + Self (to allow for chaining with other methods). """ # TODO: need to figure out if this needs to be done explicitly at all self._stream = ( @@ -400,6 +422,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> None: f"Set {workspace_size_d} bytes of workspace memory out of the allocated" f" scratch space." ) + return self else: self.destroy() raise MemoryError( From 627d475874c3836a9bac0377ef580220095f31fe Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Mon, 4 Mar 2024 14:30:00 +0000 Subject: [PATCH 24/76] `GeneralOperator` now accepts `QubitPauliOperator` as parameter. --- .../general_state/tensor_network_state.py | 18 +++++++++++------- tests/test_tensor_network_convert.py | 4 ++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 04de05f9..e25c9d57 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -9,11 +9,12 @@ except ImportError: warnings.warn("local settings failed to import cupy", ImportWarning) import numpy as np +from sympy import Expr # type: ignore from numpy.typing import NDArray from pytket.circuit import Circuit # type: ignore -from pytket.pauli import QubitPauliString # type: ignore from pytket.extensions.cutensornet.general import set_logger from pytket.extensions.cutensornet.structured_state import CuTensorNetHandle +from pytket.utils.operators import QubitPauliOperator try: import cuquantum as cq # type: ignore @@ -241,7 +242,7 @@ class GeneralOperator: def __init__( self, - operator: list[tuple[complex, QubitPauliString]], + operator: QubitPauliOperator, num_qubits: int, libhandle: CuTensorNetHandle, loglevel: int = logging.INFO, @@ -251,8 +252,7 @@ def __init__( From a list of Pauli strings and corresponding coefficients. Args: - operator: List of tuples, containing a Pauli string and a corresponding - numeric coefficient. + operator: The Pauli operator. num_qubits: Number of qubits in a circuit for which operator is to be defined. libhandle: cuTensorNet handle. @@ -272,8 +272,12 @@ def __init__( self._handle, num_qubits, qubits_dims, data_type ) self._logger.debug("Adding operator terms:") - for coeff, pauli_string in operator: - self._logger.debug(f" {coeff}, {pauli_string}") + for pauli_string, coeff in operator._dict.items(): + if isinstance(coeff, Expr): + numeric_coeff = complex(coeff.evalf()) # type: ignore + else: + numeric_coeff = complex(coeff) # type: ignore + self._logger.debug(f" {numeric_coeff}, {pauli_string}") num_pauli = len(pauli_string.map) num_modes = (1,) * num_pauli state_modes = tuple((qubit.index[0],) for qubit in pauli_string.map.keys()) @@ -283,7 +287,7 @@ def __init__( cutn.network_operator_append_product( self._handle, self._operator, - coeff, + numeric_coeff, num_pauli, num_modes, state_modes, diff --git a/tests/test_tensor_network_convert.py b/tests/test_tensor_network_convert.py index ef7a279e..73a66939 100644 --- a/tests/test_tensor_network_convert.py +++ b/tests/test_tensor_network_convert.py @@ -21,7 +21,7 @@ ) -def state_contract(tn: List[Union[NDArray, List]], nqubit: int) -> NDArray: +def state_contract(tn: List[Union[NDArray, List]]) -> NDArray: """Calls cuQuantum contract function to contract an input state tensor network.""" state_tn = tn.copy() state: NDArray = cq.contract(*state_tn).flatten() @@ -58,7 +58,7 @@ def circuit_overlap_contract(circuit_ket: Circuit) -> float: ) def test_convert_statevec_overlap(circuit: Circuit) -> None: tn = tk_to_tensor_network(circuit) - result_cu = state_contract(tn, circuit.n_qubits).flatten().round(10) + result_cu = state_contract(tn).flatten().round(10) state_vector = np.array([circuit.get_statevector()]) assert np.allclose(result_cu, state_vector) ovl = circuit_overlap_contract(circuit) From 106b87da565e40a9ed6189b090b9484ec670896f Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Mon, 4 Mar 2024 15:24:06 +0000 Subject: [PATCH 25/76] Added statevector test. --- tests/test_general_state_operator_expval.py | 53 +++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/test_general_state_operator_expval.py diff --git a/tests/test_general_state_operator_expval.py b/tests/test_general_state_operator_expval.py new file mode 100644 index 00000000..a748381a --- /dev/null +++ b/tests/test_general_state_operator_expval.py @@ -0,0 +1,53 @@ +from typing import List, Union +import warnings +import numpy as np +from numpy.typing import NDArray +import pytest +from pytket.circuit import ToffoliBox, Qubit # type: ignore +from pytket.passes import DecomposeBoxes, CnXPairwiseDecomposition # type: ignore +from pytket.transform import Transform # type: ignore + +try: + import cuquantum as cq # type: ignore +except ImportError: + warnings.warn("local settings failed to import cutensornet", ImportWarning) +from pytket.circuit import Circuit + +from pytket.extensions.cutensornet.general_state import ( # type: ignore + GeneralState, + GeneralOperator, + GeneralExpectationValue, +) +from pytket.extensions.cutensornet.structured_state import CuTensorNetHandle + + +@pytest.mark.parametrize( + "circuit", + [ + pytest.lazy_fixture("q2_x0"), # type: ignore + pytest.lazy_fixture("q2_x1"), # type: ignore + pytest.lazy_fixture("q2_v0"), # type: ignore + pytest.lazy_fixture("q2_x0cx01"), # type: ignore + pytest.lazy_fixture("q2_x1cx10x1"), # type: ignore + pytest.lazy_fixture("q2_x0cx01cx10"), # type: ignore + pytest.lazy_fixture("q2_v0cx01cx10"), # type: ignore + pytest.lazy_fixture("q2_hadamard_test"), # type: ignore + pytest.lazy_fixture("q2_lcu1"), # type: ignore + pytest.lazy_fixture("q2_lcu2"), # type: ignore + pytest.lazy_fixture("q2_lcu3"), # type: ignore + pytest.lazy_fixture("q3_v0cx02"), # type: ignore + pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + pytest.lazy_fixture("q4_lcu1"), # type: ignore + pytest.lazy_fixture("q4_multicontrols"), # type: ignore + ], +) +def test_convert_statevec(circuit: Circuit) -> None: + sv = None + with CuTensorNetHandle() as libhandle: + state = GeneralState(circuit, libhandle) + sv = state.configure().prepare().compute() + state.destroy() + sv_pytket = np.array([circuit.get_statevector()]) + assert np.allclose(sv.round(10), sv_pytket.round(10)) + # ovl = circuit_overlap_contract(circuit) + # assert ovl == pytest.approx(1.0) From 3d12261298434ee1eb92da105c2ac0d77a51a674 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Wed, 6 Mar 2024 09:04:52 -0800 Subject: [PATCH 26/76] Added gate unitary transpose to account for the way cuTN stores tensors --- .../cutensornet/general_state/tensor_network_state.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index e25c9d57..91933402 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -68,10 +68,12 @@ def __init__( self._gate_tensors = [] for com in circuit.get_commands(): gate_unitary = com.op.get_unitary().astype("complex128", copy=False) + # Transpose is needed because of the way cuTN stores tensors. + # See https://github.com/NVIDIA/cuQuantum/discussions/124#discussioncomment-8683146 for details. self._gate_tensors.append( - cp.asarray(gate_unitary, dtype="complex128").reshape( - [2] * (2 * com.op.n_qubits), order="F" - ) + cp.asarray(gate_unitary) + .T.astype(dtype="complex128", order="F") + .reshape([2] * (2 * com.op.n_qubits), order="F") ) gate_strides = 0 # Always 0? gate_qubit_indices = tuple( @@ -258,9 +260,10 @@ def __init__( libhandle: cuTensorNet handle. loglevel: Internal logger output level. """ + # Mind the transpose for Y (same argument as in GeneralState) self._pauli = { "X": cp.asarray([[0, 1], [1, 0]]).astype("complex128", order="F"), - "Y": cp.asarray([[0, -1j], [1j, 0]]).astype("complex128", order="F"), + "Y": cp.asarray([[0, -1j], [1j, 0]]).T.astype("complex128", order="F"), "Z": cp.asarray([[1, 0], [0, -1]]).astype("complex128", order="F"), "I": cp.asarray([[1, 0], [0, 1]]).astype("complex128", order="F"), } From cf30aa3acb62503f80cff230fdf5d0ca3739de22 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Thu, 7 Mar 2024 16:19:42 +0000 Subject: [PATCH 27/76] Added overlap test. --- tests/test_general_state_operator_expval.py | 23 +++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/test_general_state_operator_expval.py b/tests/test_general_state_operator_expval.py index a748381a..ac56daf4 100644 --- a/tests/test_general_state_operator_expval.py +++ b/tests/test_general_state_operator_expval.py @@ -6,6 +6,8 @@ from pytket.circuit import ToffoliBox, Qubit # type: ignore from pytket.passes import DecomposeBoxes, CnXPairwiseDecomposition # type: ignore from pytket.transform import Transform # type: ignore +from pytket.pauli import QubitPauliString, Pauli # type: ignore +from pytket.utils.operators import QubitPauliOperator # type: ignore try: import cuquantum as cq # type: ignore @@ -41,13 +43,26 @@ pytest.lazy_fixture("q4_multicontrols"), # type: ignore ], ) -def test_convert_statevec(circuit: Circuit) -> None: - sv = None +def test_convert_statevec_ovl(circuit: Circuit) -> None: with CuTensorNetHandle() as libhandle: state = GeneralState(circuit, libhandle) sv = state.configure().prepare().compute() state.destroy() sv_pytket = np.array([circuit.get_statevector()]) assert np.allclose(sv.round(10), sv_pytket.round(10)) - # ovl = circuit_overlap_contract(circuit) - # assert ovl == pytest.approx(1.0) + + op = QubitPauliOperator( + { + QubitPauliString({Qubit(0): Pauli.I, Qubit(1): Pauli.I}): 1.0, + } + ) + with CuTensorNetHandle() as libhandle: + state = GeneralState(circuit, libhandle) + oper = GeneralOperator(op, 2, libhandle) + ev = GeneralExpectationValue(state, oper, libhandle) + ovl, state_norm = ev.configure().prepare().compute() + ev.destroy() + oper.destroy() + state.destroy() + assert ovl == pytest.approx(1.0) + assert state_norm == pytest.approx(1.0) From 3d477adcb70fd211a7e8e0cb4d9783f5edaa7c6e Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Thu, 7 Mar 2024 18:02:06 +0000 Subject: [PATCH 28/76] Added a toffoli box with implicit swaps test and corresponding fix to `GeneralState`. --- .../general_state/tensor_network_state.py | 5 ++- tests/test_general_state_operator_expval.py | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 91933402..ed9a5cb2 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -38,13 +38,16 @@ def __init__( Circuit should not contain boxes - only explicit gates with specific unitary matrix representation available in pytket. + Note: + If present, implicit wire swaps are replaced with explicit SWAP gates. + Args: circuit: A pytket circuit to be converted to a tensor network. libhandle: cuTensorNet handle. loglevel: Internal logger output level. """ self._logger = set_logger("GeneralState", loglevel) - self._circuit = circuit + self._circuit = circuit.replace_implicit_wire_swaps() self._handle = libhandle.handle self._dev = libhandle.dev diff --git a/tests/test_general_state_operator_expval.py b/tests/test_general_state_operator_expval.py index ac56daf4..74ab2d00 100644 --- a/tests/test_general_state_operator_expval.py +++ b/tests/test_general_state_operator_expval.py @@ -1,5 +1,6 @@ from typing import List, Union import warnings +import cmath import numpy as np from numpy.typing import NDArray import pytest @@ -66,3 +67,39 @@ def test_convert_statevec_ovl(circuit: Circuit) -> None: state.destroy() assert ovl == pytest.approx(1.0) assert state_norm == pytest.approx(1.0) + + +def test_toffoli_box_with_implicit_swaps() -> None: + # Using specific permutation here + perm = { + (False, False): (True, True), + (False, True): (False, False), + (True, False): (True, False), + (True, True): (False, True), + } + + # Create a circuit with more qubits and multiple applications of the permutation + # above + ket_circ = Circuit(3) + + # Create the circuit + ket_circ.add_toffolibox(ToffoliBox(perm), [Qubit(0), Qubit(1)]) # type: ignore + ket_circ.add_toffolibox(ToffoliBox(perm), [Qubit(1), Qubit(2)]) # type: ignore + + DecomposeBoxes().apply(ket_circ) + CnXPairwiseDecomposition().apply(ket_circ) + Transform.OptimiseCliffords().apply(ket_circ) + + # Convert and contract + with CuTensorNetHandle() as libhandle: + state = GeneralState(ket_circ, libhandle) + ket_net_vector = state.configure().prepare().compute() + state.destroy() + + # Apply phase + ket_net_vector = ket_net_vector * cmath.exp(1j * cmath.pi * ket_circ.phase) + + # Compare to pytket statevector + ket_pytket_vector = ket_circ.get_statevector() + + assert np.allclose(ket_net_vector, ket_pytket_vector) From 5a0f418da37877f34506fe3cc32d708df5179d40 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Mon, 11 Mar 2024 13:04:39 +0000 Subject: [PATCH 29/76] Added `test_generalised_toffoli_box`. --- tests/test_general_state_operator_expval.py | 79 ++++++++++++++++++--- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/tests/test_general_state_operator_expval.py b/tests/test_general_state_operator_expval.py index 74ab2d00..69d26ea3 100644 --- a/tests/test_general_state_operator_expval.py +++ b/tests/test_general_state_operator_expval.py @@ -1,21 +1,13 @@ -from typing import List, Union -import warnings import cmath +import random import numpy as np -from numpy.typing import NDArray import pytest from pytket.circuit import ToffoliBox, Qubit # type: ignore from pytket.passes import DecomposeBoxes, CnXPairwiseDecomposition # type: ignore from pytket.transform import Transform # type: ignore from pytket.pauli import QubitPauliString, Pauli # type: ignore from pytket.utils.operators import QubitPauliOperator # type: ignore - -try: - import cuquantum as cq # type: ignore -except ImportError: - warnings.warn("local settings failed to import cutensornet", ImportWarning) from pytket.circuit import Circuit - from pytket.extensions.cutensornet.general_state import ( # type: ignore GeneralState, GeneralOperator, @@ -103,3 +95,72 @@ def test_toffoli_box_with_implicit_swaps() -> None: ket_pytket_vector = ket_circ.get_statevector() assert np.allclose(ket_net_vector, ket_pytket_vector) + + +@pytest.mark.parametrize("n_qubits", [4, 5, 6]) +def test_generalised_toffoli_box(n_qubits: int) -> None: + def to_bool_tuple(n_qubits: int, x: int) -> tuple: + bool_list = [] + for i in reversed(range(n_qubits)): + bool_list.append((x >> i) % 2 == 1) + return tuple(bool_list) + + random.seed(1) + + # Generate a random permutation + cycle = list(range(2**n_qubits)) + random.shuffle(cycle) + + perm = dict() + for orig, dest in enumerate(cycle): + perm[to_bool_tuple(n_qubits, orig)] = to_bool_tuple(n_qubits, dest) + + # Create a circuit implementing the permutation above + ket_circ = ToffoliBox(perm).get_circuit() # type: ignore + + DecomposeBoxes().apply(ket_circ) + CnXPairwiseDecomposition().apply(ket_circ) + Transform.OptimiseCliffords().apply(ket_circ) + + # The ideal outcome on ket 0 input + output = perm[(False,) * n_qubits] + # A trivial circuit generating this state + bra_circ = Circuit() + for q in ket_circ.qubits: + bra_circ.add_qubit(q) + for i, bit in enumerate(output): + if bit: + bra_circ.X(i) + + with CuTensorNetHandle() as libhandle: + state = GeneralState(ket_circ, libhandle) + ket_net_vector = state.configure().prepare().compute() + state.destroy() + ket_net_vector = ket_net_vector * cmath.exp(1j * cmath.pi * ket_circ.phase) + ket_pytket_vector = ket_circ.get_statevector() + assert np.allclose(ket_net_vector, ket_pytket_vector) + + with CuTensorNetHandle() as libhandle: + state = GeneralState(bra_circ, libhandle) + bra_net_vector = state.configure().prepare().compute() + state.destroy() + bra_net_vector = bra_net_vector * cmath.exp(1j * cmath.pi * bra_circ.phase) + bra_pytket_vector = bra_circ.get_statevector() + assert np.allclose(bra_net_vector, bra_pytket_vector) + + op = QubitPauliOperator( + { + QubitPauliString({Qubit(i): Pauli.I for i in range(n_qubits)}): 1.0, + } + ) + + with CuTensorNetHandle() as libhandle: + state = GeneralState(ket_circ, libhandle) + oper = GeneralOperator(op, 2, libhandle) + ev = GeneralExpectationValue(state, oper, libhandle) + ovl, state_norm = ev.configure().prepare().compute() + ev.destroy() + oper.destroy() + state.destroy() + assert ovl == pytest.approx(1.0) + assert state_norm == pytest.approx(1.0) From f6fce3b60fd41c2032a00536efbe8881424e6e4d Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Mon, 11 Mar 2024 06:25:19 -0700 Subject: [PATCH 30/76] A couple of bug fixes. --- .../cutensornet/general_state/tensor_network_state.py | 3 ++- tests/test_general_state_operator_expval.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index ed9a5cb2..7e94ae7d 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -47,7 +47,8 @@ def __init__( loglevel: Internal logger output level. """ self._logger = set_logger("GeneralState", loglevel) - self._circuit = circuit.replace_implicit_wire_swaps() + self._circuit = circuit + self._circuit.replace_implicit_wire_swaps() self._handle = libhandle.handle self._dev = libhandle.dev diff --git a/tests/test_general_state_operator_expval.py b/tests/test_general_state_operator_expval.py index 69d26ea3..ea88a56a 100644 --- a/tests/test_general_state_operator_expval.py +++ b/tests/test_general_state_operator_expval.py @@ -156,7 +156,7 @@ def to_bool_tuple(n_qubits: int, x: int) -> tuple: with CuTensorNetHandle() as libhandle: state = GeneralState(ket_circ, libhandle) - oper = GeneralOperator(op, 2, libhandle) + oper = GeneralOperator(op, n_qubits, libhandle) ev = GeneralExpectationValue(state, oper, libhandle) ovl, state_norm = ev.configure().prepare().compute() ev.destroy() From be22f4c109a559ce52277745bd1fcc9bc0322c92 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Mon, 11 Mar 2024 14:51:35 +0000 Subject: [PATCH 31/76] Updated public API docs. --- docs/api.rst | 2 +- docs/modules/fullTN.rst | 5 ----- docs/modules/general_state.rst | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) delete mode 100644 docs/modules/fullTN.rst create mode 100644 docs/modules/general_state.rst diff --git a/docs/api.rst b/docs/api.rst index f669c940..48ed0e2c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2,5 +2,5 @@ API documentation ----------------- .. toctree:: - modules/fullTN.rst + modules/general_state.rst modules/structured_state.rst diff --git a/docs/modules/fullTN.rst b/docs/modules/fullTN.rst deleted file mode 100644 index 48f4ee13..00000000 --- a/docs/modules/fullTN.rst +++ /dev/null @@ -1,5 +0,0 @@ -Full tensor network contraction -=============================== - -.. automodule:: pytket.extensions.cutensornet - :members: TensorNetwork, PauliOperatorTensorNetwork, ExpectationValueTensorNetwork, measure_qubits_state, tk_to_tensor_network, CuTensorNetBackend \ No newline at end of file diff --git a/docs/modules/general_state.rst b/docs/modules/general_state.rst new file mode 100644 index 00000000..284f3f43 --- /dev/null +++ b/docs/modules/general_state.rst @@ -0,0 +1,32 @@ +Full tensor network (general state) contraction +=============================================== + +.. automodule:: pytket.extensions.cutensornet.general_state + +cuQuantum `contract` API interface +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pytket.extensions.cutensornet.general_state.TensorNetwork + +.. autoclass:: pytket.extensions.cutensornet.general_state.PauliOperatorTensorNetwork + +.. autoclass:: pytket.extensions.cutensornet.general_state.ExpectationValueTensorNetwork + +.. autofunction:: pytket.extensions.cutensornet.general_state.measre_qubits_state + +.. autofunction:: pytket.extensions.cutensornet.general_state.tk_to_tensor_network + +cuQuantum `high-level` API interface +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pytket.extensions.cutensornet.general_state.GeneralState + +.. autoclass:: pytket.extensions.cutensornet.general_state.GeneralOperator + +.. autoclass:: pytket.extensions.cutensornet.general_state.GeneralExpectationValue + +Pytket backend +~~~~~~~~~~~~~~ + +.. automodule:: pytket.extensions.cutensornet + :members: CuTensorNetBackend From 4d72fa11e96602793d32f81af5b188df45046271 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Mon, 11 Mar 2024 15:11:48 +0000 Subject: [PATCH 32/76] Addressed some of mypy issues. --- .../general_state/tensor_network_state.py | 23 +++++++++++-------- tests/test_structured_state.py | 4 +++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 7e94ae7d..838132ab 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -1,11 +1,12 @@ from __future__ import annotations import logging import math -from typing import Union, Optional +from typing import Union, Optional, Any import warnings try: import cupy as cp # type: ignore + from cp.cuda import Stream, MemoryPointer except ImportError: warnings.warn("local settings failed to import cupy", ImportWarning) import numpy as np @@ -61,8 +62,8 @@ def __init__( data_type = cq.cudaDataType.CUDA_C_64F # for now let that be hard-coded # These are only required when doing preparation and evaluation. - self._stream = None - self._scratch_space = None + self._stream: Optional[Stream] = None + self._scratch_space: Optional[MemoryPointer] = None self._work_desc = None self._state = cutn.create_state( @@ -98,7 +99,7 @@ def __init__( self._mutable_gates_map[com.opgroup] = gate_id @property - def state(self) -> int: + def state(self) -> Any: """Returns tensor network state handle as Python :code:`int`.""" return self._state @@ -138,8 +139,10 @@ def configure(self, attributes: Optional[dict] = None) -> GeneralState: if attributes is None: attributes = {"NUM_HYPER_SAMPLES": 8} attribute_values = [val for val in attributes.values()] - attributes = [getattr(cutn.StateAttribute, attr) for attr in attributes.keys()] - for attr, val in zip(attributes, attribute_values): + attribute_lst = [ + getattr(cutn.StateAttribute, attr) for attr in attributes.keys() + ] + for attr, val in zip(attribute_lst, attribute_values): attr_dtype = cutn.state_get_attribute_dtype(attr) attr_arr = np.asarray(val, dtype=attr_dtype) cutn.state_configure( @@ -342,8 +345,8 @@ def __init__( self._dev = libhandle.dev self._logger = set_logger("GeneralExpectationValue", loglevel) - self._stream = None - self._scratch_space = None + self._stream: Optional[Stream] = None + self._scratch_space: Optional[MemoryPointer] = None self._work_desc = None self._expectation = cutn.create_expectation( @@ -366,10 +369,10 @@ def configure(self, attributes: Optional[dict] = None) -> GeneralExpectationValu if attributes is None: attributes = {"OPT_NUM_HYPER_SAMPLES": 8} attribute_values = [val for val in attributes.values()] - attributes = [ + attribute_lst = [ getattr(cutn.ExpectationAttribute, attr) for attr in attributes.keys() ] - for attr, val in zip(attributes, attribute_values): + for attr, val in zip(attribute_lst, attribute_values): attr_dtype = cutn.expectation_get_attribute_dtype(attr) attr_arr = np.asarray(val, dtype=attr_dtype) cutn.expectation_configure( diff --git a/tests/test_structured_state.py b/tests/test_structured_state.py index d08ae50a..7e12639d 100644 --- a/tests/test_structured_state.py +++ b/tests/test_structured_state.py @@ -21,7 +21,9 @@ SimulationAlgorithm, ) from pytket.extensions.cutensornet.structured_state.ttn import RootPath -from pytket.extensions.cutensornet.utils import circuit_statevector_postselect +from pytket.extensions.cutensornet.general_state.utils import ( + circuit_statevector_postselect, +) def test_libhandle_manager() -> None: From 61efd3b522f79ef9db39c202b127b119521f9f71 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Mon, 11 Mar 2024 15:21:39 +0000 Subject: [PATCH 33/76] Some more types fixes (some are backwards). --- docs/modules/general_state.rst | 2 +- .../general_state/tensor_network_state.py | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/modules/general_state.rst b/docs/modules/general_state.rst index 284f3f43..b4b9500b 100644 --- a/docs/modules/general_state.rst +++ b/docs/modules/general_state.rst @@ -12,7 +12,7 @@ cuQuantum `contract` API interface .. autoclass:: pytket.extensions.cutensornet.general_state.ExpectationValueTensorNetwork -.. autofunction:: pytket.extensions.cutensornet.general_state.measre_qubits_state +.. autofunction:: pytket.extensions.cutensornet.general_state.measure_qubits_state .. autofunction:: pytket.extensions.cutensornet.general_state.tk_to_tensor_network diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 838132ab..9da79667 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -6,7 +6,6 @@ try: import cupy as cp # type: ignore - from cp.cuda import Stream, MemoryPointer except ImportError: warnings.warn("local settings failed to import cupy", ImportWarning) import numpy as np @@ -62,8 +61,8 @@ def __init__( data_type = cq.cudaDataType.CUDA_C_64F # for now let that be hard-coded # These are only required when doing preparation and evaluation. - self._stream: Optional[Stream] = None - self._scratch_space: Optional[MemoryPointer] = None + self._stream = None + self._scratch_space = None self._work_desc = None self._state = cutn.create_state( @@ -74,7 +73,8 @@ def __init__( for com in circuit.get_commands(): gate_unitary = com.op.get_unitary().astype("complex128", copy=False) # Transpose is needed because of the way cuTN stores tensors. - # See https://github.com/NVIDIA/cuQuantum/discussions/124#discussioncomment-8683146 for details. + # See https://github.com/NVIDIA/cuQuantum/discussions/124 + # #discussioncomment-8683146 for details. self._gate_tensors.append( cp.asarray(gate_unitary) .T.astype(dtype="complex128", order="F") @@ -226,21 +226,21 @@ def compute(self, on_host: bool = True) -> Union[cp.ndarray, np.ndarray]: state_vector = cp.empty( (2,) * self._circuit.n_qubits, dtype="complex128", order="F" ) - cutn.state_compute( + cutn.state_compute( # type: ignore self._handle, self._state, self._work_desc, (state_vector.data.ptr,), self._stream.ptr, ) - self._stream.synchronize() + self._stream.synchronize() # type: ignore if on_host: return cp.asnumpy(state_vector.flatten()) return state_vector.flatten() def destroy(self) -> None: """Destroys tensor network state.""" - if self._work_desc is not None: + if self._work_desc is not None: # type: ignore cutn.destroy_workspace_descriptor(self._work_desc) cutn.destroy_state(self._state) del self._scratch_space @@ -306,7 +306,7 @@ def __init__( ) @property - def operator(self) -> int: + def operator(self) -> Any: """Returns tensor network operator handle as Python :code:`int`.""" return self._operator @@ -345,8 +345,8 @@ def __init__( self._dev = libhandle.dev self._logger = set_logger("GeneralExpectationValue", loglevel) - self._stream: Optional[Stream] = None - self._scratch_space: Optional[MemoryPointer] = None + self._stream = None + self._scratch_space = None self._work_desc = None self._expectation = cutn.create_expectation( @@ -447,7 +447,7 @@ def compute(self) -> tuple[complex, complex]: """Computes expectation value.""" expectation_value = np.empty(1, dtype="complex128") state_norm = np.empty(1, dtype="complex128") - cutn.expectation_compute( + cutn.expectation_compute( # type: ignore self._handle, self._expectation, self._work_desc, @@ -455,12 +455,12 @@ def compute(self) -> tuple[complex, complex]: state_norm.ctypes.data, self._stream.ptr, ) - self._stream.synchronize() + self._stream.synchronize() # type: ignore return expectation_value.item(), state_norm.item() def destroy(self) -> None: """Destroys tensor network expectation value and workspace descriptor.""" - if self._work_desc is not None: + if self._work_desc is not None: # type: ignore cutn.destroy_workspace_descriptor(self._work_desc) cutn.destroy_expectation(self._expectation) del self._scratch_space # TODO is this the correct way? From 93e86012b9545f606c33fb206e100141c22b8cba Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Mon, 11 Mar 2024 15:48:08 +0000 Subject: [PATCH 34/76] More mypy tweaks. --- .../general_state/tensor_network_state.py | 26 +++++++++---------- .../cutensornet/structured_state/general.py | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 9da79667..7b431472 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -14,7 +14,7 @@ from pytket.circuit import Circuit # type: ignore from pytket.extensions.cutensornet.general import set_logger from pytket.extensions.cutensornet.structured_state import CuTensorNetHandle -from pytket.utils.operators import QubitPauliOperator +from pytket.utils.operators import QubitPauliOperator # type: ignore try: import cuquantum as cq # type: ignore @@ -182,7 +182,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralState: self._state, scratch_size, self._work_desc, - self._stream.ptr, + self._stream.ptr, # type: ignore ) workspace_size_d = cutn.workspace_get_memory_size( self._handle, @@ -198,7 +198,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralState: self._work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, - self._scratch_space.ptr, + self._scratch_space.ptr, # type: ignore workspace_size_d, ) self._logger.debug( @@ -226,12 +226,12 @@ def compute(self, on_host: bool = True) -> Union[cp.ndarray, np.ndarray]: state_vector = cp.empty( (2,) * self._circuit.n_qubits, dtype="complex128", order="F" ) - cutn.state_compute( # type: ignore + cutn.state_compute( self._handle, self._state, self._work_desc, (state_vector.data.ptr,), - self._stream.ptr, + self._stream.ptr, # type: ignore ) self._stream.synchronize() # type: ignore if on_host: @@ -240,8 +240,8 @@ def compute(self, on_host: bool = True) -> Union[cp.ndarray, np.ndarray]: def destroy(self) -> None: """Destroys tensor network state.""" - if self._work_desc is not None: # type: ignore - cutn.destroy_workspace_descriptor(self._work_desc) + if self._work_desc is not None: + cutn.destroy_workspace_descriptor(self._work_desc) # type: ignore cutn.destroy_state(self._state) del self._scratch_space @@ -413,7 +413,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralExpectationValue: self._expectation, scratch_size, self._work_desc, - self._stream.ptr, + self._stream.ptr, # type: ignore ) workspace_size_d = cutn.workspace_get_memory_size( self._handle, @@ -429,7 +429,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralExpectationValue: self._work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, - self._scratch_space.ptr, + self._scratch_space.ptr, # type: ignore workspace_size_d, ) self._logger.debug( @@ -447,20 +447,20 @@ def compute(self) -> tuple[complex, complex]: """Computes expectation value.""" expectation_value = np.empty(1, dtype="complex128") state_norm = np.empty(1, dtype="complex128") - cutn.expectation_compute( # type: ignore + cutn.expectation_compute( self._handle, self._expectation, self._work_desc, expectation_value.ctypes.data, state_norm.ctypes.data, - self._stream.ptr, + self._stream.ptr, # type: ignore ) self._stream.synchronize() # type: ignore return expectation_value.item(), state_norm.item() def destroy(self) -> None: """Destroys tensor network expectation value and workspace descriptor.""" - if self._work_desc is not None: # type: ignore - cutn.destroy_workspace_descriptor(self._work_desc) + if self._work_desc is not None: + cutn.destroy_workspace_descriptor(self._work_desc) # type: ignore cutn.destroy_expectation(self._expectation) del self._scratch_space # TODO is this the correct way? diff --git a/pytket/extensions/cutensornet/structured_state/general.py b/pytket/extensions/cutensornet/structured_state/general.py index 928d6f66..497c2817 100644 --- a/pytket/extensions/cutensornet/structured_state/general.py +++ b/pytket/extensions/cutensornet/structured_state/general.py @@ -71,7 +71,7 @@ def __enter__(self) -> CuTensorNetHandle: def __exit__(self, exc_type: Any, exc_value: Any, exc_tb: Any) -> None: self.destroy() - def print_device_properties(self, logger: Logger): + def print_device_properties(self, logger: Logger) -> None: """Prints local GPU properties.""" device_props = cp.cuda.runtime.getDeviceProperties(self.dev.id) logger.debug("===== device info ======") @@ -83,7 +83,7 @@ def print_device_properties(self, logger: Logger): logger.debug("GPU-minor:", device_props["minor"]) logger.debug("========================") - def destroy(self): + def destroy(self) -> None: cutn.destroy(self.handle) self._is_destroyed = True From 4b60441376b1fd4664e4549119366c0c604c1501 Mon Sep 17 00:00:00 2001 From: Iakov Polyak Date: Mon, 11 Mar 2024 15:58:05 +0000 Subject: [PATCH 35/76] Attempt to silence pytket mypy complains. --- tests/test_general_state_operator_expval.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_general_state_operator_expval.py b/tests/test_general_state_operator_expval.py index ea88a56a..ce288cbd 100644 --- a/tests/test_general_state_operator_expval.py +++ b/tests/test_general_state_operator_expval.py @@ -7,7 +7,7 @@ from pytket.transform import Transform # type: ignore from pytket.pauli import QubitPauliString, Pauli # type: ignore from pytket.utils.operators import QubitPauliOperator # type: ignore -from pytket.circuit import Circuit +from pytket.circuit import Circuit # type: ignore from pytket.extensions.cutensornet.general_state import ( # type: ignore GeneralState, GeneralOperator, From 74a103726647c62244d52437340b92e140c93c80 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Wed, 29 May 2024 12:53:05 +0100 Subject: [PATCH 36/76] Removed duplicated definition of destroy --- pytket/extensions/cutensornet/structured_state/general.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pytket/extensions/cutensornet/structured_state/general.py b/pytket/extensions/cutensornet/structured_state/general.py index ab4fc87f..beecd696 100644 --- a/pytket/extensions/cutensornet/structured_state/general.py +++ b/pytket/extensions/cutensornet/structured_state/general.py @@ -93,10 +93,6 @@ def print_device_properties(self, logger: Logger) -> None: logger.debug("GPU-minor:", device_props["minor"]) logger.debug("========================") - def destroy(self) -> None: - cutn.destroy(self.handle) - self._is_destroyed = True - class Config: """Configuration class for simulation using ``StructuredState``.""" From 61995ec090a46b82d4833c8ab3371b0d80ac3253 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 4 Jun 2024 02:12:49 -0700 Subject: [PATCH 37/76] Fixed broken import --- tests/test_cutensornet_postselect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cutensornet_postselect.py b/tests/test_cutensornet_postselect.py index 14e2e2c3..99ef38c6 100644 --- a/tests/test_cutensornet_postselect.py +++ b/tests/test_cutensornet_postselect.py @@ -5,7 +5,7 @@ from pytket.pauli import Pauli, QubitPauliString # type: ignore from pytket.utils import QubitPauliOperator from pytket.extensions.cutensornet.backends import CuTensorNetBackend -from pytket.extensions.cutensornet.tensor_network_convert import ( # type: ignore +from pytket.extensions.cutensornet.general_state.tensor_network_convert import ( # type: ignore TensorNetwork, measure_qubits_state, ) From 7c46299d46e4116b350a4eaf7f97dc4c82b6696e Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 4 Jun 2024 11:23:24 +0100 Subject: [PATCH 38/76] Applying changes from comments --- .../general_state/tensor_network_state.py | 132 ++++++++---------- 1 file changed, 57 insertions(+), 75 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 7b431472..fce27164 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -1,6 +1,5 @@ from __future__ import annotations import logging -import math from typing import Union, Optional, Any import warnings @@ -68,65 +67,31 @@ def __init__( self._state = cutn.create_state( self._handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type ) - self._mutable_gates_map = {} self._gate_tensors = [] for com in circuit.get_commands(): - gate_unitary = com.op.get_unitary().astype("complex128", copy=False) - # Transpose is needed because of the way cuTN stores tensors. - # See https://github.com/NVIDIA/cuQuantum/discussions/124 - # #discussioncomment-8683146 for details. - self._gate_tensors.append( - cp.asarray(gate_unitary) - .T.astype(dtype="complex128", order="F") - .reshape([2] * (2 * com.op.n_qubits), order="F") - ) - gate_strides = 0 # Always 0? + gate_unitary = com.op.get_unitary() + self._gate_tensors.append(_formatted_tensor(gate_unitary, com.op.n_qubits)) gate_qubit_indices = tuple( self._circuit.qubits.index(qb) for qb in com.qubits ) - gate_id = cutn.state_apply_tensor( - self._handle, - self._state, - com.op.n_qubits, - gate_qubit_indices, - self._gate_tensors[-1].data.ptr, - gate_strides, - 1, - 0, - 1, + + cutn.state_apply_tensor_operator( + handle=self._handle, + tensor_network_state=self._state, + num_state_modes=com.op.n_qubits, + state_modes=gate_qubit_indices, + tensor_data=self._gate_tensors[-1].data.ptr, + tensor_mode_strides=0, + immutable=1, + adjoint=0, + unitary=1, ) - if com.opgroup is not None: - self._mutable_gates_map[com.opgroup] = gate_id @property def state(self) -> Any: """Returns tensor network state handle as Python :code:`int`.""" return self._state - def update_gates(self, gates_update_map: dict[str, NDArray]) -> None: - """Updates gate unitaries in the tensor network state. - - Args: - gates_update_map: Map from gate (Command) opgroup name to a corresponding - gate unitary. - - Raises: - ValueError: If a gate's unitary is of a wrong size. - """ - for gate_label, unitary in gates_update_map.items(): - gate_id = self._mutable_gates_map[gate_label] - gate_n_qubits = math.log2(unitary.shape[0]) - if not gate_n_qubits.is_integer(): - raise ValueError( - f"Gate {gate_label} unitary's number of rows is not a power of two." - ) - gate_tensor = cp.asarray(unitary, dtype="complex128").reshape( - [2] * (2 * int(gate_n_qubits)), order="F" - ) - cutn.state_update_tensor( - self._handle, self._state, gate_id, gate_tensor.data.ptr, 1 - ) - def configure(self, attributes: Optional[dict] = None) -> GeneralState: """Configures tensor network state for future contraction. @@ -137,12 +102,14 @@ def configure(self, attributes: Optional[dict] = None) -> GeneralState: Self (to allow for chaining with other methods). """ if attributes is None: - attributes = {"NUM_HYPER_SAMPLES": 8} - attribute_values = [val for val in attributes.values()] - attribute_lst = [ - getattr(cutn.StateAttribute, attr) for attr in attributes.keys() + attributes = dict() + if "NUM_HYPER_SAMPLES" not in attributes: + attributes["NUM_HYPER_SAMPLES"] = 8 + attribute_pairs = [ + getattr(cutn.StateAttribute, v) for k, v in attributes.items() ] - for attr, val in zip(attribute_lst, attribute_values): + + for attr, val in attribute_pairs: attr_dtype = cutn.state_get_attribute_dtype(attr) attr_arr = np.asarray(val, dtype=attr_dtype) cutn.state_configure( @@ -269,15 +236,15 @@ def __init__( """ # Mind the transpose for Y (same argument as in GeneralState) self._pauli = { - "X": cp.asarray([[0, 1], [1, 0]]).astype("complex128", order="F"), - "Y": cp.asarray([[0, -1j], [1j, 0]]).T.astype("complex128", order="F"), - "Z": cp.asarray([[1, 0], [0, -1]]).astype("complex128", order="F"), - "I": cp.asarray([[1, 0], [0, 1]]).astype("complex128", order="F"), + "X": _formatted_tensor(np.asarray([[0, 1], [1, 0]]), 1), + "Y": _formatted_tensor(np.asarray([[0, -1j], [1j, 0]]), 1), + "Z": _formatted_tensor(np.asarray([[1, 0], [0, -1]]), 1), + "I": _formatted_tensor(np.asarray([[1, 0], [0, 1]]), 1), } self._logger = set_logger("GeneralOperator", loglevel) self._handle = libhandle.handle qubits_dims = (2,) * num_qubits - data_type = cq.cudaDataType.CUDA_C_64F # TODO: implement a config class? + data_type = cq.cudaDataType.CUDA_C_64F self._operator = cutn.create_network_operator( self._handle, num_qubits, qubits_dims, data_type ) @@ -294,15 +261,16 @@ def __init__( gate_data = tuple( self._pauli[pauli.name].data.ptr for pauli in pauli_string.map.values() ) + cutn.network_operator_append_product( - self._handle, - self._operator, - numeric_coeff, - num_pauli, - num_modes, - state_modes, - 0, - gate_data, + handle=self._handle, + tensor_network_operator=self._operator, + coefficient=numeric_coeff, + num_tensors=num_pauli, + num_state_modes=num_modes, + state_modes=state_modes, + tensor_mode_strides=0, + tensor_data=gate_data, ) @property @@ -329,7 +297,7 @@ def __init__( Notes: State and Operator must have the same handle as ExpectationValue. - State (and Operator?) need to exist during the whole lifetime of + State and Operator need to exist during the whole lifetime of ExpectationValue. Args: @@ -350,7 +318,7 @@ def __init__( self._work_desc = None self._expectation = cutn.create_expectation( - self._handle, state._state, operator._operator + self._handle, state.state, operator.operator ) def configure(self, attributes: Optional[dict] = None) -> GeneralExpectationValue: @@ -367,12 +335,14 @@ def configure(self, attributes: Optional[dict] = None) -> GeneralExpectationValu Self (to allow for chaining with other methods). """ if attributes is None: - attributes = {"OPT_NUM_HYPER_SAMPLES": 8} - attribute_values = [val for val in attributes.values()] - attribute_lst = [ - getattr(cutn.ExpectationAttribute, attr) for attr in attributes.keys() + attributes = dict() + if "OPT_NUM_HYPER_SAMPLES" not in attributes: + attributes["OPT_NUM_HYPER_SAMPLES"] = 8 + attribute_pairs = [ + getattr(cutn.ExpectationAttribute, v) for k, v in attributes.items() ] - for attr, val in zip(attribute_lst, attribute_values): + + for attr, val in attribute_pairs: attr_dtype = cutn.expectation_get_attribute_dtype(attr) attr_arr = np.asarray(val, dtype=attr_dtype) cutn.expectation_configure( @@ -394,7 +364,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralExpectationValue: Args: scratch_fraction: Fraction of free memory on GPU to allocate as scratch - space. + space. Defaults to 0.5. Returns: Self (to allow for chaining with other methods). @@ -463,4 +433,16 @@ def destroy(self) -> None: if self._work_desc is not None: cutn.destroy_workspace_descriptor(self._work_desc) # type: ignore cutn.destroy_expectation(self._expectation) - del self._scratch_space # TODO is this the correct way? + del self._scratch_space + + +def _formatted_tensor(matrix: NDArray, n_qubits: int) -> cp.ndarray: + """Convert a matrix to the tensor format accepted by NVIDIA's API.""" + + # Transpose is needed because of the way cuTN stores tensors. + # See https://github.com/NVIDIA/cuQuantum/discussions/124 + # #discussioncomment-8683146 for details. + cupy_matrix = cp.asarray(matrix).T.astype(dtype="complex128", order="F") + # We also need to reshape since a matrix only has 2 bonds, but for an + # n-qubit gate we want 2^n bonds for input and another 2^n for output + return cupy_matrix.reshape([2] * (2 * n_qubits), order="F") From b2d65a4f366c78dff7e7159a4a92f11b8d9aa30f Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 4 Jun 2024 03:32:52 -0700 Subject: [PATCH 39/76] Fixing slip-up --- .../cutensornet/general_state/tensor_network_state.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index fce27164..f395bd7e 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -106,7 +106,7 @@ def configure(self, attributes: Optional[dict] = None) -> GeneralState: if "NUM_HYPER_SAMPLES" not in attributes: attributes["NUM_HYPER_SAMPLES"] = 8 attribute_pairs = [ - getattr(cutn.StateAttribute, v) for k, v in attributes.items() + (getattr(cutn.StateAttribute, k), v) for k, v in attributes.items() ] for attr, val in attribute_pairs: @@ -339,7 +339,7 @@ def configure(self, attributes: Optional[dict] = None) -> GeneralExpectationValu if "OPT_NUM_HYPER_SAMPLES" not in attributes: attributes["OPT_NUM_HYPER_SAMPLES"] = 8 attribute_pairs = [ - getattr(cutn.ExpectationAttribute, v) for k, v in attributes.items() + (getattr(cutn.ExpectationAttribute, k), v) for k, v in attributes.items() ] for attr, val in attribute_pairs: From 53162e484e5c7f580940c585c9debdefc745107b Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 4 Jun 2024 11:56:13 +0100 Subject: [PATCH 40/76] Removing type ignores from imports --- .../general_state/tensor_network_state.py | 4 ++-- tests/test_general_state_operator_expval.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index f395bd7e..15ae27b7 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -10,10 +10,10 @@ import numpy as np from sympy import Expr # type: ignore from numpy.typing import NDArray -from pytket.circuit import Circuit # type: ignore +from pytket.circuit import Circuit from pytket.extensions.cutensornet.general import set_logger from pytket.extensions.cutensornet.structured_state import CuTensorNetHandle -from pytket.utils.operators import QubitPauliOperator # type: ignore +from pytket.utils.operators import QubitPauliOperator try: import cuquantum as cq # type: ignore diff --git a/tests/test_general_state_operator_expval.py b/tests/test_general_state_operator_expval.py index ce288cbd..9c320b48 100644 --- a/tests/test_general_state_operator_expval.py +++ b/tests/test_general_state_operator_expval.py @@ -2,13 +2,13 @@ import random import numpy as np import pytest -from pytket.circuit import ToffoliBox, Qubit # type: ignore -from pytket.passes import DecomposeBoxes, CnXPairwiseDecomposition # type: ignore -from pytket.transform import Transform # type: ignore -from pytket.pauli import QubitPauliString, Pauli # type: ignore -from pytket.utils.operators import QubitPauliOperator # type: ignore -from pytket.circuit import Circuit # type: ignore -from pytket.extensions.cutensornet.general_state import ( # type: ignore +from pytket.circuit import ToffoliBox, Qubit +from pytket.passes import DecomposeBoxes, CnXPairwiseDecomposition +from pytket.transform import Transform +from pytket.pauli import QubitPauliString, Pauli +from pytket.utils.operators import QubitPauliOperator +from pytket.circuit import Circuit +from pytket.extensions.cutensornet.general_state import ( GeneralState, GeneralOperator, GeneralExpectationValue, From c6b1c723fa8faeedbb9d958b58e2e68adba22b27 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 4 Jun 2024 12:05:02 +0100 Subject: [PATCH 41/76] Removing more type ignores from imports --- .../cutensornet/general_state/tensor_network_convert.py | 6 +++--- pytket/extensions/cutensornet/general_state/utils.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_convert.py b/pytket/extensions/cutensornet/general_state/tensor_network_convert.py index 23a715a1..c901db6a 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_convert.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_convert.py @@ -22,10 +22,10 @@ from networkx.classes.reportviews import OutMultiEdgeView, OutMultiEdgeDataView # type: ignore import numpy as np from numpy.typing import NDArray -from pytket import Qubit # type: ignore +from pytket import Qubit from pytket.utils import Graph -from pytket.pauli import QubitPauliString # type: ignore -from pytket.circuit import Circuit, Qubit # type: ignore +from pytket.pauli import QubitPauliString +from pytket.circuit import Circuit, Qubit from pytket.utils import permute_rows_cols_in_unitary from pytket.extensions.cutensornet.general import set_logger diff --git a/pytket/extensions/cutensornet/general_state/utils.py b/pytket/extensions/cutensornet/general_state/utils.py index 3069191f..33aefc59 100644 --- a/pytket/extensions/cutensornet/general_state/utils.py +++ b/pytket/extensions/cutensornet/general_state/utils.py @@ -1,6 +1,6 @@ from numpy.typing import NDArray from pytket.backends.backendresult import BackendResult -from pytket.circuit import Qubit, Circuit # type: ignore +from pytket.circuit import Qubit, Circuit def _reorder_qlist( From 3954fecce8ff75fa91d21cda3595f2ccc96252da Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 4 Jun 2024 12:13:39 +0100 Subject: [PATCH 42/76] Removed invalid import --- .../cutensornet/general_state/tensor_network_convert.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_convert.py b/pytket/extensions/cutensornet/general_state/tensor_network_convert.py index c901db6a..c4ab04a4 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_convert.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_convert.py @@ -22,7 +22,6 @@ from networkx.classes.reportviews import OutMultiEdgeView, OutMultiEdgeDataView # type: ignore import numpy as np from numpy.typing import NDArray -from pytket import Qubit from pytket.utils import Graph from pytket.pauli import QubitPauliString from pytket.circuit import Circuit, Qubit From 26f848a6fa8d11b7554d15e09dac43750b6ff6d4 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 4 Jun 2024 12:37:14 +0100 Subject: [PATCH 43/76] Moving CuTensorNetHandle to the general.py at the root of the module --- pytket/extensions/cutensornet/__init__.py | 1 + pytket/extensions/cutensornet/general.py | 67 +++++++++++++++++++ .../general_state/tensor_network_state.py | 3 +- .../cutensornet/structured_state/__init__.py | 4 +- .../cutensornet/structured_state/general.py | 60 +---------------- .../cutensornet/structured_state/mps.py | 4 +- .../cutensornet/structured_state/mps_mpo.py | 3 +- .../structured_state/simulation.py | 4 +- .../cutensornet/structured_state/ttn.py | 4 +- 9 files changed, 81 insertions(+), 69 deletions(-) diff --git a/pytket/extensions/cutensornet/__init__.py b/pytket/extensions/cutensornet/__init__.py index a3ed1b65..ba720477 100644 --- a/pytket/extensions/cutensornet/__init__.py +++ b/pytket/extensions/cutensornet/__init__.py @@ -14,6 +14,7 @@ """Module for conversion from tket primitives to cuQuantum primitives.""" from .backends import CuTensorNetBackend +from .general import CuTensorNetHandle # _metadata.py is copied to the folder after installation. from ._metadata import __extension_version__, __extension_name__ # type: ignore diff --git a/pytket/extensions/cutensornet/general.py b/pytket/extensions/cutensornet/general.py index f39ed406..7bac904a 100644 --- a/pytket/extensions/cutensornet/general.py +++ b/pytket/extensions/cutensornet/general.py @@ -11,9 +11,76 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations # type: ignore +import warnings import logging from logging import Logger +from typing import Any, Optional + +try: + import cupy as cp # type: ignore +except ImportError: + warnings.warn("local settings failed to import cupy", ImportWarning) +try: + import cuquantum.cutensornet as cutn # type: ignore +except ImportError: + warnings.warn("local settings failed to import cutensornet", ImportWarning) + + +class CuTensorNetHandle: + """Initialise the cuTensorNet library with automatic workspace memory + management. + + Note: + Always use as ``with CuTensorNetHandle() as libhandle:`` so that cuTensorNet + handles are automatically destroyed at the end of execution. + + Attributes: + handle (int): The cuTensorNet library handle created by this initialisation. + device_id (int): The ID of the device (GPU) where cuTensorNet is initialised. + If not provided, defaults to ``cp.cuda.Device()``. + """ + + def __init__(self, device_id: Optional[int] = None): + self._is_destroyed = False + + # Make sure CuPy uses the specified device + dev = cp.cuda.Device(device_id) + dev.use() + + self.dev = dev + self.device_id = dev.id + + self.handle = cutn.create() + + def destroy(self) -> None: + """Destroys the memory handle, releasing memory. + + Only call this method if you are initialising a ``CuTensorNetHandle`` outside + a ``with CuTensorNetHandle() as libhandle`` statement. + """ + cutn.destroy(self.handle) + self._is_destroyed = True + + def __enter__(self) -> CuTensorNetHandle: + return self + + def __exit__(self, exc_type: Any, exc_value: Any, exc_tb: Any) -> None: + self.destroy() + + def print_device_properties(self, logger: Logger) -> None: + """Prints local GPU properties.""" + device_props = cp.cuda.runtime.getDeviceProperties(self.dev.id) + logger.debug("===== device info ======") + logger.debug("GPU-name:", device_props["name"].decode()) + logger.debug("GPU-clock:", device_props["clockRate"]) + logger.debug("GPU-memoryClock:", device_props["memoryClockRate"]) + logger.debug("GPU-nSM:", device_props["multiProcessorCount"]) + logger.debug("GPU-major:", device_props["major"]) + logger.debug("GPU-minor:", device_props["minor"]) + logger.debug("========================") + def set_logger( logger_name: str, diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 15ae27b7..b8d1f3b7 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -11,8 +11,7 @@ from sympy import Expr # type: ignore from numpy.typing import NDArray from pytket.circuit import Circuit -from pytket.extensions.cutensornet.general import set_logger -from pytket.extensions.cutensornet.structured_state import CuTensorNetHandle +from pytket.extensions.cutensornet.general import CuTensorNetHandle, set_logger from pytket.utils.operators import QubitPauliOperator try: diff --git a/pytket/extensions/cutensornet/structured_state/__init__.py b/pytket/extensions/cutensornet/structured_state/__init__.py index be9dd9f5..3daffd99 100644 --- a/pytket/extensions/cutensornet/structured_state/__init__.py +++ b/pytket/extensions/cutensornet/structured_state/__init__.py @@ -19,7 +19,9 @@ https://github.com/CQCL/pytket-cutensornet. """ -from .general import CuTensorNetHandle, Config, StructuredState +from pytket.extensions.cutensornet import CuTensorNetHandle + +from .general import Config, StructuredState from .simulation import SimulationAlgorithm, simulate, prepare_circuit_mps from .mps import DirMPS, MPS diff --git a/pytket/extensions/cutensornet/structured_state/general.py b/pytket/extensions/cutensornet/structured_state/general.py index beecd696..a458e723 100644 --- a/pytket/extensions/cutensornet/structured_state/general.py +++ b/pytket/extensions/cutensornet/structured_state/general.py @@ -15,7 +15,6 @@ from abc import ABC, abstractmethod import warnings import logging -from logging import Logger from typing import Any, Optional, Type import numpy as np # type: ignore @@ -27,11 +26,8 @@ import cupy as cp # type: ignore except ImportError: warnings.warn("local settings failed to import cupy", ImportWarning) -try: - import cuquantum.cutensornet as cutn # type: ignore -except ImportError: - warnings.warn("local settings failed to import cutensornet", ImportWarning) +from pytket.extensions.cutensornet import CuTensorNetHandle # An alias for the CuPy type used for tensors try: @@ -40,60 +36,6 @@ Tensor = Any -class CuTensorNetHandle: - """Initialise the cuTensorNet library with automatic workspace memory - management. - - Note: - Always use as ``with CuTensorNetHandle() as libhandle:`` so that cuTensorNet - handles are automatically destroyed at the end of execution. - - Attributes: - handle (int): The cuTensorNet library handle created by this initialisation. - device_id (int): The ID of the device (GPU) where cuTensorNet is initialised. - If not provided, defaults to ``cp.cuda.Device()``. - """ - - def __init__(self, device_id: Optional[int] = None): - self._is_destroyed = False - - # Make sure CuPy uses the specified device - dev = cp.cuda.Device(device_id) - dev.use() - - self.dev = dev - self.device_id = dev.id - - self.handle = cutn.create() - - def destroy(self) -> None: - """Destroys the memory handle, releasing memory. - - Only call this method if you are initialising a ``CuTensorNetHandle`` outside - a ``with CuTensorNetHandle() as libhandle`` statement. - """ - cutn.destroy(self.handle) - self._is_destroyed = True - - def __enter__(self) -> CuTensorNetHandle: - return self - - def __exit__(self, exc_type: Any, exc_value: Any, exc_tb: Any) -> None: - self.destroy() - - def print_device_properties(self, logger: Logger) -> None: - """Prints local GPU properties.""" - device_props = cp.cuda.runtime.getDeviceProperties(self.dev.id) - logger.debug("===== device info ======") - logger.debug("GPU-name:", device_props["name"].decode()) - logger.debug("GPU-clock:", device_props["clockRate"]) - logger.debug("GPU-memoryClock:", device_props["memoryClockRate"]) - logger.debug("GPU-nSM:", device_props["multiProcessorCount"]) - logger.debug("GPU-major:", device_props["major"]) - logger.debug("GPU-minor:", device_props["minor"]) - logger.debug("========================") - - class Config: """Configuration class for simulation using ``StructuredState``.""" diff --git a/pytket/extensions/cutensornet/structured_state/mps.py b/pytket/extensions/cutensornet/structured_state/mps.py index dda12f34..c87aae5c 100644 --- a/pytket/extensions/cutensornet/structured_state/mps.py +++ b/pytket/extensions/cutensornet/structured_state/mps.py @@ -32,9 +32,9 @@ from pytket.circuit import Command, Op, OpType, Qubit from pytket.pauli import Pauli, QubitPauliString -from pytket.extensions.cutensornet.general import set_logger +from pytket.extensions.cutensornet.general import CuTensorNetHandle, set_logger -from .general import CuTensorNetHandle, Config, StructuredState, Tensor +from .general import Config, StructuredState, Tensor class DirMPS(Enum): diff --git a/pytket/extensions/cutensornet/structured_state/mps_mpo.py b/pytket/extensions/cutensornet/structured_state/mps_mpo.py index ad64b64a..ea29c6d8 100644 --- a/pytket/extensions/cutensornet/structured_state/mps_mpo.py +++ b/pytket/extensions/cutensornet/structured_state/mps_mpo.py @@ -29,7 +29,8 @@ warnings.warn("local settings failed to import cutensornet", ImportWarning) from pytket.circuit import Qubit -from .general import CuTensorNetHandle, Tensor, Config +from pytket.extensions.cutensornet import CuTensorNetHandle +from .general import Tensor, Config from .mps import ( DirMPS, MPS, diff --git a/pytket/extensions/cutensornet/structured_state/simulation.py b/pytket/extensions/cutensornet/structured_state/simulation.py index 0d68880e..5aaef3e6 100644 --- a/pytket/extensions/cutensornet/structured_state/simulation.py +++ b/pytket/extensions/cutensornet/structured_state/simulation.py @@ -32,8 +32,8 @@ from pytket.passes import DefaultMappingPass from pytket.predicates import CompilationUnit -from pytket.extensions.cutensornet.general import set_logger -from .general import CuTensorNetHandle, Config, StructuredState +from pytket.extensions.cutensornet.general import CuTensorNetHandle, set_logger +from .general import Config, StructuredState from .mps_gate import MPSxGate from .mps_mpo import MPSxMPO from .ttn_gate import TTNxGate diff --git a/pytket/extensions/cutensornet/structured_state/ttn.py b/pytket/extensions/cutensornet/structured_state/ttn.py index 59460190..9b36c6fa 100644 --- a/pytket/extensions/cutensornet/structured_state/ttn.py +++ b/pytket/extensions/cutensornet/structured_state/ttn.py @@ -33,9 +33,9 @@ from pytket.circuit import Command, Qubit from pytket.pauli import QubitPauliString -from pytket.extensions.cutensornet.general import set_logger +from pytket.extensions.cutensornet.general import CuTensorNetHandle, set_logger -from .general import CuTensorNetHandle, Config, StructuredState, Tensor +from .general import Config, StructuredState, Tensor class DirTTN(IntEnum): From 600568dbc2410a1f11e844ba15e7139d15ac88f8 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 4 Jun 2024 16:03:54 +0100 Subject: [PATCH 44/76] Fixing mypy complaints: removing top level init files --- pytket/__init__.py | 0 pytket/extensions/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pytket/__init__.py delete mode 100644 pytket/extensions/__init__.py diff --git a/pytket/__init__.py b/pytket/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pytket/extensions/__init__.py b/pytket/extensions/__init__.py deleted file mode 100644 index e69de29b..00000000 From 7fc94cc95d6ac85b1b97f2ffea99f32988bc03ee Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 4 Jun 2024 16:51:06 +0100 Subject: [PATCH 45/76] Made use of library handler safer --- pytket/extensions/cutensornet/general.py | 13 ++++- .../general_state/tensor_network_state.py | 50 +++++++++---------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/pytket/extensions/cutensornet/general.py b/pytket/extensions/cutensornet/general.py index 7bac904a..3f33882e 100644 --- a/pytket/extensions/cutensornet/general.py +++ b/pytket/extensions/cutensornet/general.py @@ -52,7 +52,16 @@ def __init__(self, device_id: Optional[int] = None): self.dev = dev self.device_id = dev.id - self.handle = cutn.create() + self._handle = cutn.create() + + @property + def handle(self) -> Any: + if self._is_destroyed: + raise RuntimeError( + "The cuTensorNet library handle is out of scope.", + "See the documentation of CuTensorNetHandle.", + ) + return self._handle def destroy(self) -> None: """Destroys the memory handle, releasing memory. @@ -60,7 +69,7 @@ def destroy(self) -> None: Only call this method if you are initialising a ``CuTensorNetHandle`` outside a ``with CuTensorNetHandle() as libhandle`` statement. """ - cutn.destroy(self.handle) + cutn.destroy(self._handle) self._is_destroyed = True def __enter__(self) -> CuTensorNetHandle: diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index b8d1f3b7..66b0b9f1 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -47,8 +47,7 @@ def __init__( self._logger = set_logger("GeneralState", loglevel) self._circuit = circuit self._circuit.replace_implicit_wire_swaps() - self._handle = libhandle.handle - self._dev = libhandle.dev + self._lib = libhandle libhandle.print_device_properties(self._logger) @@ -64,7 +63,7 @@ def __init__( self._work_desc = None self._state = cutn.create_state( - self._handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type + self._lib.handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type ) self._gate_tensors = [] for com in circuit.get_commands(): @@ -75,7 +74,7 @@ def __init__( ) cutn.state_apply_tensor_operator( - handle=self._handle, + handle=self._lib.handle, tensor_network_state=self._state, num_state_modes=com.op.n_qubits, state_modes=gate_qubit_indices, @@ -112,7 +111,7 @@ def configure(self, attributes: Optional[dict] = None) -> GeneralState: attr_dtype = cutn.state_get_attribute_dtype(attr) attr_arr = np.asarray(val, dtype=attr_dtype) cutn.state_configure( - self._handle, + self._lib.handle, self._state, attr, attr_arr.ctypes.data, @@ -138,20 +137,20 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralState: self._stream = ( cp.cuda.Stream() ) # In current cuTN release it is unused (could be 0x0) - free_mem = self._dev.mem_info[0] + free_mem = self._lib.dev.mem_info[0] scratch_size = int(scratch_fraction * free_mem) self._scratch_space = cp.cuda.alloc(scratch_size) self._logger.debug(f"Allocated {scratch_size} bytes of scratch memory on GPU") - self._work_desc = cutn.create_workspace_descriptor(self._handle) + self._work_desc = cutn.create_workspace_descriptor(self._lib.handle) cutn.state_prepare( - self._handle, + self._lib.handle, self._state, scratch_size, self._work_desc, self._stream.ptr, # type: ignore ) workspace_size_d = cutn.workspace_get_memory_size( - self._handle, + self._lib.handle, self._work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, @@ -160,7 +159,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralState: if workspace_size_d <= scratch_size: cutn.workspace_set_memory( - self._handle, + self._lib.handle, self._work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, @@ -175,7 +174,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralState: else: self.destroy() raise MemoryError( - f"Insufficient workspace size on the GPU device {self._dev.id}" + f"Insufficient workspace size on the GPU device {self._lib.dev.id}" ) def compute(self, on_host: bool = True) -> Union[cp.ndarray, np.ndarray]: @@ -193,7 +192,7 @@ def compute(self, on_host: bool = True) -> Union[cp.ndarray, np.ndarray]: (2,) * self._circuit.n_qubits, dtype="complex128", order="F" ) cutn.state_compute( - self._handle, + self._lib.handle, self._state, self._work_desc, (state_vector.data.ptr,), @@ -241,11 +240,11 @@ def __init__( "I": _formatted_tensor(np.asarray([[1, 0], [0, 1]]), 1), } self._logger = set_logger("GeneralOperator", loglevel) - self._handle = libhandle.handle + self._lib = libhandle qubits_dims = (2,) * num_qubits data_type = cq.cudaDataType.CUDA_C_64F self._operator = cutn.create_network_operator( - self._handle, num_qubits, qubits_dims, data_type + self._lib.handle, num_qubits, qubits_dims, data_type ) self._logger.debug("Adding operator terms:") for pauli_string, coeff in operator._dict.items(): @@ -262,7 +261,7 @@ def __init__( ) cutn.network_operator_append_product( - handle=self._handle, + handle=self._lib.handle, tensor_network_operator=self._operator, coefficient=numeric_coeff, num_tensors=num_pauli, @@ -308,8 +307,7 @@ def __init__( Raises: MemoryError: If there is insufficient workspace size on a GPU device. """ - self._handle = libhandle.handle - self._dev = libhandle.dev + self._lib = libhandle self._logger = set_logger("GeneralExpectationValue", loglevel) self._stream = None @@ -317,7 +315,7 @@ def __init__( self._work_desc = None self._expectation = cutn.create_expectation( - self._handle, state.state, operator.operator + self._lib.handle, state.state, operator.operator ) def configure(self, attributes: Optional[dict] = None) -> GeneralExpectationValue: @@ -345,7 +343,7 @@ def configure(self, attributes: Optional[dict] = None) -> GeneralExpectationValu attr_dtype = cutn.expectation_get_attribute_dtype(attr) attr_arr = np.asarray(val, dtype=attr_dtype) cutn.expectation_configure( - self._handle, + self._lib.handle, self._expectation, attr, attr_arr.ctypes.data, @@ -372,20 +370,20 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralExpectationValue: self._stream = ( cp.cuda.Stream() ) # In current cuTN release it is unused (could be 0x0) - free_mem = self._dev.mem_info[0] + free_mem = self._lib.dev.mem_info[0] scratch_size = int(scratch_fraction * free_mem) self._scratch_space = cp.cuda.alloc(scratch_size) self._logger.debug(f"Allocated {scratch_size} bytes of scratch memory on GPU") - self._work_desc = cutn.create_workspace_descriptor(self._handle) + self._work_desc = cutn.create_workspace_descriptor(self._lib.handle) cutn.expectation_prepare( - self._handle, + self._lib.handle, self._expectation, scratch_size, self._work_desc, self._stream.ptr, # type: ignore ) workspace_size_d = cutn.workspace_get_memory_size( - self._handle, + self._lib.handle, self._work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, @@ -394,7 +392,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralExpectationValue: if workspace_size_d <= scratch_size: cutn.workspace_set_memory( - self._handle, + self._lib.handle, self._work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, @@ -409,7 +407,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralExpectationValue: else: self.destroy() raise MemoryError( - f"Insufficient workspace size on the GPU device {self._dev.id}" + f"Insufficient workspace size on the GPU device {self._lib.dev.id}" ) def compute(self) -> tuple[complex, complex]: @@ -417,7 +415,7 @@ def compute(self) -> tuple[complex, complex]: expectation_value = np.empty(1, dtype="complex128") state_norm = np.empty(1, dtype="complex128") cutn.expectation_compute( - self._handle, + self._lib.handle, self._expectation, self._work_desc, expectation_value.ctypes.data, From f749bfdb436f265e3f1132172c423d43fade9bb9 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 4 Jun 2024 17:06:14 +0100 Subject: [PATCH 46/76] Renamed test file --- ...est_general_state_operator_expval.py => test_general_state.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_general_state_operator_expval.py => test_general_state.py} (100%) diff --git a/tests/test_general_state_operator_expval.py b/tests/test_general_state.py similarity index 100% rename from tests/test_general_state_operator_expval.py rename to tests/test_general_state.py From 074192c8dee24ede59e461aa14c43e5b54c31f35 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 4 Jun 2024 17:13:21 +0100 Subject: [PATCH 47/76] Made changes on test --- tests/test_general_state.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/tests/test_general_state.py b/tests/test_general_state.py index 9c320b48..80b98e5a 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -42,13 +42,16 @@ def test_convert_statevec_ovl(circuit: Circuit) -> None: sv = state.configure().prepare().compute() state.destroy() sv_pytket = np.array([circuit.get_statevector()]) - assert np.allclose(sv.round(10), sv_pytket.round(10)) + assert np.allclose(sv, sv_pytket, atol=1e-10) op = QubitPauliOperator( { QubitPauliString({Qubit(0): Pauli.I, Qubit(1): Pauli.I}): 1.0, } ) + + # Use an alternative calculation of the overlap as the expectation value + # of the identity operator: = with CuTensorNetHandle() as libhandle: state = GeneralState(circuit, libhandle) oper = GeneralOperator(op, 2, libhandle) @@ -124,13 +127,6 @@ def to_bool_tuple(n_qubits: int, x: int) -> tuple: # The ideal outcome on ket 0 input output = perm[(False,) * n_qubits] - # A trivial circuit generating this state - bra_circ = Circuit() - for q in ket_circ.qubits: - bra_circ.add_qubit(q) - for i, bit in enumerate(output): - if bit: - bra_circ.X(i) with CuTensorNetHandle() as libhandle: state = GeneralState(ket_circ, libhandle) @@ -140,14 +136,8 @@ def to_bool_tuple(n_qubits: int, x: int) -> tuple: ket_pytket_vector = ket_circ.get_statevector() assert np.allclose(ket_net_vector, ket_pytket_vector) - with CuTensorNetHandle() as libhandle: - state = GeneralState(bra_circ, libhandle) - bra_net_vector = state.configure().prepare().compute() - state.destroy() - bra_net_vector = bra_net_vector * cmath.exp(1j * cmath.pi * bra_circ.phase) - bra_pytket_vector = bra_circ.get_statevector() - assert np.allclose(bra_net_vector, bra_pytket_vector) - + # Use an alternative calculation of the overlap as the expectation value + # of the identity operator: = op = QubitPauliOperator( { QubitPauliString({Qubit(i): Pauli.I for i in range(n_qubits)}): 1.0, From ee64c55fedbd4df02b2154e309e95f8e0c4ba23e Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 4 Jun 2024 17:36:50 +0100 Subject: [PATCH 48/76] Removing dead code from example --- tests/test_general_state.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_general_state.py b/tests/test_general_state.py index 80b98e5a..2f79d2f5 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -125,9 +125,6 @@ def to_bool_tuple(n_qubits: int, x: int) -> tuple: CnXPairwiseDecomposition().apply(ket_circ) Transform.OptimiseCliffords().apply(ket_circ) - # The ideal outcome on ket 0 input - output = perm[(False,) * n_qubits] - with CuTensorNetHandle() as libhandle: state = GeneralState(ket_circ, libhandle) ket_net_vector = state.configure().prepare().compute() From 1c6f5c9e7a68689c1a4ac67c2c2c160e64416d5f Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Wed, 5 Jun 2024 13:52:30 +0100 Subject: [PATCH 49/76] Removed outdated comment --- .../extensions/cutensornet/general_state/tensor_network_state.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 66b0b9f1..8e6e6a8c 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -232,7 +232,6 @@ def __init__( libhandle: cuTensorNet handle. loglevel: Internal logger output level. """ - # Mind the transpose for Y (same argument as in GeneralState) self._pauli = { "X": _formatted_tensor(np.asarray([[0, 1], [1, 0]]), 1), "Y": _formatted_tensor(np.asarray([[0, -1j], [1j, 0]]), 1), From 2b12bceb0ff35343565a8c46ced32d859973dc7b Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Thu, 6 Jun 2024 13:33:58 +0100 Subject: [PATCH 50/76] Refactor of GeneralState so that it adheres to the upcoming format of NVIDIA's API --- .../cutensornet/general_state/__init__.py | 2 +- .../general_state/tensor_network_state.py | 369 ++++++++---------- tests/test_general_state.py | 26 +- 3 files changed, 173 insertions(+), 224 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/__init__.py b/pytket/extensions/cutensornet/general_state/__init__.py index 20d19c79..d72f2110 100644 --- a/pytket/extensions/cutensornet/general_state/__init__.py +++ b/pytket/extensions/cutensornet/general_state/__init__.py @@ -23,4 +23,4 @@ measure_qubits_state, ) -from .tensor_network_state import GeneralState, GeneralOperator, GeneralExpectationValue +from .tensor_network_state import GeneralState diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 8e6e6a8c..84f36af6 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -1,6 +1,6 @@ from __future__ import annotations import logging -from typing import Union, Optional, Any +from typing import Union, Optional import warnings try: @@ -22,7 +22,7 @@ class GeneralState: - """Handles cuTensorNet tensor network state object.""" + """Wraps a cuTensorNet TN object for exact simulations via path optimisation""" def __init__( self, @@ -32,6 +32,10 @@ def __init__( ) -> None: """Constructs a tensor network state representation from a pytket circuit. + Note: + The tensor network is *not* contracted until the appropriate methods + from this class are called. + Note: Circuit should not contain boxes - only explicit gates with specific unitary matrix representation available in pytket. @@ -46,6 +50,9 @@ def __init__( """ self._logger = set_logger("GeneralState", loglevel) self._circuit = circuit + # TODO: This is not strictly necessary; implicit SWAPs could be resolved by + # qubit relabelling, but it's only worth doing so if there are clear signs + # of inefficiency due to this. self._circuit.replace_implicit_wire_swaps() self._lib = libhandle @@ -66,6 +73,11 @@ def __init__( self._lib.handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type ) self._gate_tensors = [] + + # Append all gates to the TN + # TODO: we should add a check to verify that the commands are unitaries + # (e.g. don't accept measurements). Potentially, measurements at the end of + # the circuit can be ignored at the user's request. for com in circuit.get_commands(): gate_unitary = com.op.get_unitary() self._gate_tensors.append(_formatted_tensor(gate_unitary, com.op.n_qubits)) @@ -85,20 +97,30 @@ def __init__( unitary=1, ) - @property - def state(self) -> Any: - """Returns tensor network state handle as Python :code:`int`.""" - return self._state - - def configure(self, attributes: Optional[dict] = None) -> GeneralState: - """Configures tensor network state for future contraction. + def get_statevector( + self, + attributes: Optional[dict] = None, + scratch_fraction: float = 0.5, + on_host: bool = True, + ) -> Union[cp.ndarray, np.ndarray]: + """Contracts the circuit and returns the final statevector. Args: attributes: A dict of cuTensorNet State attributes and their values. - + scratch_fraction: Fraction of free memory on GPU to allocate as scratch + space. Defaults to `0.5`. + on_host: If :code:`True`, converts cupy :code:`ndarray` to numpy + :code:`ndarray`, copying it to host device (CPU). + Raises: + MemoryError: If there is insufficient workspace on GPU. Returns: - Self (to allow for chaining with other methods). + Either a :code:`cupy.ndarray` on a GPU, or a :code:`numpy.ndarray` on a + host device (CPU). Arrays are returned in a 1D shape. """ + + #################################### + # Configure the TN for contraction # + #################################### if attributes is None: attributes = dict() if "NUM_HYPER_SAMPLES" not in attributes: @@ -117,23 +139,10 @@ def configure(self, attributes: Optional[dict] = None) -> GeneralState: attr_arr.ctypes.data, attr_arr.dtype.itemsize, ) - return self - - def prepare(self, scratch_fraction: float = 0.5) -> GeneralState: - """Prepare tensor network state for future contraction. - - Allocates workspace memory necessary for contraction. - - Raises: - MemoryError: If there is insufficient workspace on GPU. - Args: - scratch_fraction: Fraction of free memory on GPU to allocate as scratch - space. - - Returns: - Self (to allow for chaining with other methods). - """ + ###################################### + # Allocate workspace for contraction # + ###################################### self._stream = ( cp.cuda.Stream() ) # In current cuTN release it is unused (could be 0x0) @@ -142,6 +151,7 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralState: self._scratch_space = cp.cuda.alloc(scratch_size) self._logger.debug(f"Allocated {scratch_size} bytes of scratch memory on GPU") self._work_desc = cutn.create_workspace_descriptor(self._lib.handle) + cutn.state_prepare( self._lib.handle, self._state, @@ -167,27 +177,19 @@ def prepare(self, scratch_fraction: float = 0.5) -> GeneralState: workspace_size_d, ) self._logger.debug( - f"Set {workspace_size_d} bytes of workspace memory out of the allocated" - f" scratch space." + f"Set {workspace_size_d} bytes of workspace memory out of the" + f" allocated scratch space." ) - return self + else: self.destroy() raise MemoryError( f"Insufficient workspace size on the GPU device {self._lib.dev.id}" ) - def compute(self, on_host: bool = True) -> Union[cp.ndarray, np.ndarray]: - """Evaluates state vector. - - Args: - on_host: If :code:`True`, converts cupy :code:`ndarray` to numpy - :code:`ndarray`, copying it to host device (CPU). - - Returns: - Either a :code:`cupy.ndarray` on a GPU, or a :code:`numpy.ndarray` on a - host device (CPU). Arrays are returned in a 1D shape. - """ + ################### + # Contract the TN # + ################### state_vector = cp.empty( (2,) * self._circuit.n_qubits, dtype="complex128", order="F" ) @@ -203,48 +205,43 @@ def compute(self, on_host: bool = True) -> Union[cp.ndarray, np.ndarray]: return cp.asnumpy(state_vector.flatten()) return state_vector.flatten() - def destroy(self) -> None: - """Destroys tensor network state.""" - if self._work_desc is not None: - cutn.destroy_workspace_descriptor(self._work_desc) # type: ignore - cutn.destroy_state(self._state) - del self._scratch_space - - -class GeneralOperator: - """Handles tensor network operator.""" - - def __init__( + def expectation_value( self, operator: QubitPauliOperator, - num_qubits: int, - libhandle: CuTensorNetHandle, - loglevel: int = logging.INFO, - ) -> None: - """Constructs a tensor network operator. - - From a list of Pauli strings and corresponding coefficients. + attributes: Optional[dict] = None, + scratch_fraction: float = 0.5, + ) -> float: + """Calculates the expectation value of the given operator. Args: - operator: The Pauli operator. - num_qubits: Number of qubits in a circuit for which operator is to be - defined. - libhandle: cuTensorNet handle. - loglevel: Internal logger output level. + operator: The operator whose expectation value is to be measured. + attributes: A dict of cuTensorNet Expectation attributes and their values. + scratch_fraction: Fraction of free memory on GPU to allocate as scratch + space. Defaults to `0.5`. + + Raises: + ValueError: If the operator acts on qubits not present in the circuit. + + Returns: + The expectation value. """ - self._pauli = { + + ############################################ + # Generate the cuTensorNet operator object # + ############################################ + pauli_tensors = { "X": _formatted_tensor(np.asarray([[0, 1], [1, 0]]), 1), "Y": _formatted_tensor(np.asarray([[0, -1j], [1j, 0]]), 1), "Z": _formatted_tensor(np.asarray([[1, 0], [0, -1]]), 1), - "I": _formatted_tensor(np.asarray([[1, 0], [0, 1]]), 1), } - self._logger = set_logger("GeneralOperator", loglevel) - self._lib = libhandle + num_qubits = self._circuit.n_qubits qubits_dims = (2,) * num_qubits data_type = cq.cudaDataType.CUDA_C_64F - self._operator = cutn.create_network_operator( + + operator = cutn.create_network_operator( self._lib.handle, num_qubits, qubits_dims, data_type ) + self._logger.debug("Adding operator terms:") for pauli_string, coeff in operator._dict.items(): if isinstance(coeff, Expr): @@ -252,16 +249,34 @@ def __init__( else: numeric_coeff = complex(coeff) # type: ignore self._logger.debug(f" {numeric_coeff}, {pauli_string}") - num_pauli = len(pauli_string.map) + + # Raise an error if the operator acts on qubits that are not in the circuit + if any(q not in self._circuit.qubits for q in pauli_string.map.keys()): + raise ValueError( + f"The operator is acting on qubits {pauli_string.map.keys()}, " + "but some of these are not present in the circuit, whose set of " + f"qubits is: {self._circuit.qubits}." + ) + + # Obtain the tensors corresponding to this operator + qubit_pauli_map = { + q: pauli_tensors[pauli.name] + for q, pauli in pauli_string.map.items() + if pauli.name != "I" # Identity operators can be omitted + } + + num_pauli = len(qubit_pauli_map) num_modes = (1,) * num_pauli - state_modes = tuple((qubit.index[0],) for qubit in pauli_string.map.keys()) + state_modes = tuple( + self._circuit.qubits.index(qb) for qb in qubit_pauli_map.keys() + ) gate_data = tuple( - self._pauli[pauli.name].data.ptr for pauli in pauli_string.map.values() + pauli_tensors[pauli.name].data.ptr for pauli in qubit_pauli_map.values() ) cutn.network_operator_append_product( handle=self._lib.handle, - tensor_network_operator=self._operator, + tensor_network_operator=operator, coefficient=numeric_coeff, num_tensors=num_pauli, num_state_modes=num_modes, @@ -270,66 +285,11 @@ def __init__( tensor_data=gate_data, ) - @property - def operator(self) -> Any: - """Returns tensor network operator handle as Python :code:`int`.""" - return self._operator - - def destroy(self) -> None: - """Destroys tensor network operator.""" - cutn.destroy_network_operator(self._operator) - - -class GeneralExpectationValue: - """Handles a general tensor network operator expectation value.""" - - def __init__( - self, - state: GeneralState, - operator: GeneralOperator, - libhandle: CuTensorNetHandle, - loglevel: int = logging.INFO, - ) -> None: - """Initialises expectation value object and corresponding work space. - - Notes: - State and Operator must have the same handle as ExpectationValue. - State and Operator need to exist during the whole lifetime of - ExpectationValue. - - Args: - state: General tensor network state. - operator: General tensor network operator. - libhandle: cuTensorNet handle. - loglevel: Internal logger output level. - - Raises: - MemoryError: If there is insufficient workspace size on a GPU device. - """ - self._lib = libhandle - self._logger = set_logger("GeneralExpectationValue", loglevel) - - self._stream = None - self._scratch_space = None - self._work_desc = None + ###################################################### + # Configure the cuTensorNet expectation value object # + ###################################################### + expectation = cutn.create_expectation(self._lib.handle, self._state, operator) - self._expectation = cutn.create_expectation( - self._lib.handle, state.state, operator.operator - ) - - def configure(self, attributes: Optional[dict] = None) -> GeneralExpectationValue: - """Configures expectation value for future contraction. - - Args: - attributes: A map of cuTensorNet :code:`ExpectationAttribute` objects to - their values. - - Note: - Currently :code:`ExpectationAttribute` has only one attribute. - - Returns: - Self (to allow for chaining with other methods). - """ if attributes is None: attributes = dict() if "OPT_NUM_HYPER_SAMPLES" not in attributes: @@ -343,92 +303,95 @@ def configure(self, attributes: Optional[dict] = None) -> GeneralExpectationValu attr_arr = np.asarray(val, dtype=attr_dtype) cutn.expectation_configure( self._lib.handle, - self._expectation, + expectation, attr, attr_arr.ctypes.data, attr_arr.dtype.itemsize, ) - return self - def prepare(self, scratch_fraction: float = 0.5) -> GeneralExpectationValue: - """Prepare tensor network state for future contraction. + try: + ###################################### + # Allocate workspace for contraction # + ###################################### + self._stream = cp.cuda.Stream() + free_mem = self._lib.dev.mem_info[0] + scratch_size = int(scratch_fraction * free_mem) + self._scratch_space = cp.cuda.alloc(scratch_size) - Allocates workspace memory necessary for contraction. - - Raises: - MemoryError: If there is insufficient space on the GPU device. - - Args: - scratch_fraction: Fraction of free memory on GPU to allocate as scratch - space. Defaults to 0.5. - - Returns: - Self (to allow for chaining with other methods). - """ - # TODO: need to figure out if this needs to be done explicitly at all - self._stream = ( - cp.cuda.Stream() - ) # In current cuTN release it is unused (could be 0x0) - free_mem = self._lib.dev.mem_info[0] - scratch_size = int(scratch_fraction * free_mem) - self._scratch_space = cp.cuda.alloc(scratch_size) - self._logger.debug(f"Allocated {scratch_size} bytes of scratch memory on GPU") - self._work_desc = cutn.create_workspace_descriptor(self._lib.handle) - cutn.expectation_prepare( - self._lib.handle, - self._expectation, - scratch_size, - self._work_desc, - self._stream.ptr, # type: ignore - ) - workspace_size_d = cutn.workspace_get_memory_size( - self._lib.handle, - self._work_desc, - cutn.WorksizePref.RECOMMENDED, - cutn.Memspace.DEVICE, - cutn.WorkspaceKind.SCRATCH, - ) - - if workspace_size_d <= scratch_size: - cutn.workspace_set_memory( + self._logger.debug( + f"Allocated {scratch_size} bytes of scratch memory on GPU" + ) + self._work_desc = cutn.create_workspace_descriptor(self._lib.handle) + cutn.expectation_prepare( self._lib.handle, + expectation, + scratch_size, self._work_desc, + self._stream.ptr, # type: ignore + ) + workspace_size_d = cutn.workspace_get_memory_size( + self._lib.handle, + self._work_desc, + cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, - self._scratch_space.ptr, # type: ignore - workspace_size_d, - ) - self._logger.debug( - f"Set {workspace_size_d} bytes of workspace memory out of the allocated" - f" scratch space." - ) - return self - else: - self.destroy() - raise MemoryError( - f"Insufficient workspace size on the GPU device {self._lib.dev.id}" ) - def compute(self) -> tuple[complex, complex]: - """Computes expectation value.""" - expectation_value = np.empty(1, dtype="complex128") - state_norm = np.empty(1, dtype="complex128") - cutn.expectation_compute( - self._lib.handle, - self._expectation, - self._work_desc, - expectation_value.ctypes.data, - state_norm.ctypes.data, - self._stream.ptr, # type: ignore - ) - self._stream.synchronize() # type: ignore - return expectation_value.item(), state_norm.item() + if workspace_size_d <= scratch_size: + cutn.workspace_set_memory( + self._lib.handle, + self._work_desc, + cutn.Memspace.DEVICE, + cutn.WorkspaceKind.SCRATCH, + self._scratch_space.ptr, # type: ignore + workspace_size_d, + ) + self._logger.debug( + f"Set {workspace_size_d} bytes of workspace memory out of the" + f" allocated scratch space." + ) + else: + raise MemoryError( + f"Insufficient workspace size on the GPU device {self._lib.dev.id}" + ) + + ################################# + # Compute the expectation value # + ################################# + expectation_value = np.empty(1, dtype="complex128") + state_norm = np.empty(1, dtype="complex128") + cutn.expectation_compute( + self._lib.handle, + expectation, + self._work_desc, + expectation_value.ctypes.data, + state_norm.ctypes.data, + self._stream.ptr, # type: ignore + ) + self._stream.synchronize() # type: ignore + + # Note: we can also return `state_norm.item()`, but this should be 1 since + # we are always running unitary circuits + assert np.isclose(state_norm.item(), 1.0) + # The expectation value is a real number + assert np.isclose(expectation_value.item().imag, 0.0) + return expectation_value.item().real # type: ignore + + finally: + ##################################################### + # Destroy the Operator and ExpectationValue objects # + ##################################################### + if self._work_desc is not None: + cutn.destroy_workspace_descriptor(self._work_desc) # type: ignore + cutn.destroy_network_operator(operator) + cutn.destroy_expectation(expectation) + del self._scratch_space def destroy(self) -> None: - """Destroys tensor network expectation value and workspace descriptor.""" + """Destroys tensor network state.""" if self._work_desc is not None: cutn.destroy_workspace_descriptor(self._work_desc) # type: ignore - cutn.destroy_expectation(self._expectation) + cutn.destroy_state(self._state) del self._scratch_space diff --git a/tests/test_general_state.py b/tests/test_general_state.py index 2f79d2f5..28966190 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -8,11 +8,7 @@ from pytket.pauli import QubitPauliString, Pauli from pytket.utils.operators import QubitPauliOperator from pytket.circuit import Circuit -from pytket.extensions.cutensornet.general_state import ( - GeneralState, - GeneralOperator, - GeneralExpectationValue, -) +from pytket.extensions.cutensornet.general_state import GeneralState from pytket.extensions.cutensornet.structured_state import CuTensorNetHandle @@ -39,7 +35,7 @@ def test_convert_statevec_ovl(circuit: Circuit) -> None: with CuTensorNetHandle() as libhandle: state = GeneralState(circuit, libhandle) - sv = state.configure().prepare().compute() + sv = state.get_statevector() state.destroy() sv_pytket = np.array([circuit.get_statevector()]) assert np.allclose(sv, sv_pytket, atol=1e-10) @@ -54,14 +50,9 @@ def test_convert_statevec_ovl(circuit: Circuit) -> None: # of the identity operator: = with CuTensorNetHandle() as libhandle: state = GeneralState(circuit, libhandle) - oper = GeneralOperator(op, 2, libhandle) - ev = GeneralExpectationValue(state, oper, libhandle) - ovl, state_norm = ev.configure().prepare().compute() - ev.destroy() - oper.destroy() + ovl = state.expectation_value(op) state.destroy() assert ovl == pytest.approx(1.0) - assert state_norm == pytest.approx(1.0) def test_toffoli_box_with_implicit_swaps() -> None: @@ -88,7 +79,7 @@ def test_toffoli_box_with_implicit_swaps() -> None: # Convert and contract with CuTensorNetHandle() as libhandle: state = GeneralState(ket_circ, libhandle) - ket_net_vector = state.configure().prepare().compute() + ket_net_vector = state.get_statevector() state.destroy() # Apply phase @@ -127,7 +118,7 @@ def to_bool_tuple(n_qubits: int, x: int) -> tuple: with CuTensorNetHandle() as libhandle: state = GeneralState(ket_circ, libhandle) - ket_net_vector = state.configure().prepare().compute() + ket_net_vector = state.get_statevector() state.destroy() ket_net_vector = ket_net_vector * cmath.exp(1j * cmath.pi * ket_circ.phase) ket_pytket_vector = ket_circ.get_statevector() @@ -143,11 +134,6 @@ def to_bool_tuple(n_qubits: int, x: int) -> tuple: with CuTensorNetHandle() as libhandle: state = GeneralState(ket_circ, libhandle) - oper = GeneralOperator(op, n_qubits, libhandle) - ev = GeneralExpectationValue(state, oper, libhandle) - ovl, state_norm = ev.configure().prepare().compute() - ev.destroy() - oper.destroy() + ovl = state.expectation_value(op) state.destroy() assert ovl == pytest.approx(1.0) - assert state_norm == pytest.approx(1.0) From 0e9526b01c0c1ab242f296ef4e558d4cd1ee13fe Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Thu, 6 Jun 2024 07:13:33 -0700 Subject: [PATCH 51/76] Fixed small bugs --- .../general_state/tensor_network_state.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 84f36af6..a9daef66 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -233,12 +233,13 @@ def expectation_value( "X": _formatted_tensor(np.asarray([[0, 1], [1, 0]]), 1), "Y": _formatted_tensor(np.asarray([[0, -1j], [1j, 0]]), 1), "Z": _formatted_tensor(np.asarray([[1, 0], [0, -1]]), 1), + "I": _formatted_tensor(np.asarray([[1, 0], [0, 1]]), 1), } num_qubits = self._circuit.n_qubits qubits_dims = (2,) * num_qubits data_type = cq.cudaDataType.CUDA_C_64F - operator = cutn.create_network_operator( + tn_operator = cutn.create_network_operator( self._lib.handle, num_qubits, qubits_dims, data_type ) @@ -262,21 +263,20 @@ def expectation_value( qubit_pauli_map = { q: pauli_tensors[pauli.name] for q, pauli in pauli_string.map.items() - if pauli.name != "I" # Identity operators can be omitted } num_pauli = len(qubit_pauli_map) num_modes = (1,) * num_pauli state_modes = tuple( - self._circuit.qubits.index(qb) for qb in qubit_pauli_map.keys() + (self._circuit.qubits.index(qb),) for qb in qubit_pauli_map.keys() ) gate_data = tuple( - pauli_tensors[pauli.name].data.ptr for pauli in qubit_pauli_map.values() + tensor.data.ptr for tensor in qubit_pauli_map.values() ) cutn.network_operator_append_product( handle=self._lib.handle, - tensor_network_operator=operator, + tensor_network_operator=tn_operator, coefficient=numeric_coeff, num_tensors=num_pauli, num_state_modes=num_modes, @@ -288,7 +288,7 @@ def expectation_value( ###################################################### # Configure the cuTensorNet expectation value object # ###################################################### - expectation = cutn.create_expectation(self._lib.handle, self._state, operator) + expectation = cutn.create_expectation(self._lib.handle, self._state, tn_operator) if attributes is None: attributes = dict() @@ -383,8 +383,8 @@ def expectation_value( ##################################################### if self._work_desc is not None: cutn.destroy_workspace_descriptor(self._work_desc) # type: ignore - cutn.destroy_network_operator(operator) cutn.destroy_expectation(expectation) + cutn.destroy_network_operator(tn_operator) del self._scratch_space def destroy(self) -> None: From 1ab3fe691ebef51e9f4b366d0835331c23605299 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Thu, 6 Jun 2024 15:35:01 +0100 Subject: [PATCH 52/76] Removed _scratch_space and _work_desc attributes from GeneralState, because they are only used locally in get_statevector and expectation_value and they only need to survive for the duration of said function calls. --- .../general_state/tensor_network_state.py | 162 +++++++++--------- tests/test_general_state.py | 5 - 2 files changed, 78 insertions(+), 89 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index a9daef66..78f5a96b 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -64,11 +64,6 @@ def __init__( self._logger.debug(f"Converting a quantum circuit with {num_qubits} qubits.") data_type = cq.cudaDataType.CUDA_C_64F # for now let that be hard-coded - # These are only required when doing preparation and evaluation. - self._stream = None - self._scratch_space = None - self._work_desc = None - self._state = cutn.create_state( self._lib.handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type ) @@ -140,70 +135,74 @@ def get_statevector( attr_arr.dtype.itemsize, ) - ###################################### - # Allocate workspace for contraction # - ###################################### - self._stream = ( - cp.cuda.Stream() - ) # In current cuTN release it is unused (could be 0x0) - free_mem = self._lib.dev.mem_info[0] - scratch_size = int(scratch_fraction * free_mem) - self._scratch_space = cp.cuda.alloc(scratch_size) - self._logger.debug(f"Allocated {scratch_size} bytes of scratch memory on GPU") - self._work_desc = cutn.create_workspace_descriptor(self._lib.handle) - - cutn.state_prepare( - self._lib.handle, - self._state, - scratch_size, - self._work_desc, - self._stream.ptr, # type: ignore - ) - workspace_size_d = cutn.workspace_get_memory_size( - self._lib.handle, - self._work_desc, - cutn.WorksizePref.RECOMMENDED, - cutn.Memspace.DEVICE, - cutn.WorkspaceKind.SCRATCH, - ) + try: + ###################################### + # Allocate workspace for contraction # + ###################################### + stream = cp.cuda.Stream() + free_mem = self._lib.dev.mem_info[0] + scratch_size = int(scratch_fraction * free_mem) + scratch_space = cp.cuda.alloc(scratch_size) + self._logger.debug( + f"Allocated {scratch_size} bytes of scratch memory on GPU" + ) + work_desc = cutn.create_workspace_descriptor(self._lib.handle) - if workspace_size_d <= scratch_size: - cutn.workspace_set_memory( + cutn.state_prepare( self._lib.handle, - self._work_desc, + self._state, + scratch_size, + work_desc, + stream.ptr, + ) + workspace_size_d = cutn.workspace_get_memory_size( + self._lib.handle, + work_desc, + cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, - self._scratch_space.ptr, # type: ignore - workspace_size_d, - ) - self._logger.debug( - f"Set {workspace_size_d} bytes of workspace memory out of the" - f" allocated scratch space." ) - else: - self.destroy() - raise MemoryError( - f"Insufficient workspace size on the GPU device {self._lib.dev.id}" + if workspace_size_d <= scratch_size: + cutn.workspace_set_memory( + self._lib.handle, + work_desc, + cutn.Memspace.DEVICE, + cutn.WorkspaceKind.SCRATCH, + scratch_space.ptr, + workspace_size_d, + ) + self._logger.debug( + f"Set {workspace_size_d} bytes of workspace memory out of the" + f" allocated scratch space." + ) + + else: + raise MemoryError( + f"Insufficient workspace size on the GPU device {self._lib.dev.id}" + ) + + ################### + # Contract the TN # + ################### + state_vector = cp.empty( + (2,) * self._circuit.n_qubits, dtype="complex128", order="F" + ) + cutn.state_compute( + self._lib.handle, + self._state, + work_desc, + (state_vector.data.ptr,), + stream.ptr, ) + stream.synchronize() + if on_host: + return cp.asnumpy(state_vector.flatten()) + return state_vector.flatten() - ################### - # Contract the TN # - ################### - state_vector = cp.empty( - (2,) * self._circuit.n_qubits, dtype="complex128", order="F" - ) - cutn.state_compute( - self._lib.handle, - self._state, - self._work_desc, - (state_vector.data.ptr,), - self._stream.ptr, # type: ignore - ) - self._stream.synchronize() # type: ignore - if on_host: - return cp.asnumpy(state_vector.flatten()) - return state_vector.flatten() + finally: + cutn.destroy_workspace_descriptor(work_desc) # type: ignore + del scratch_space def expectation_value( self, @@ -261,8 +260,7 @@ def expectation_value( # Obtain the tensors corresponding to this operator qubit_pauli_map = { - q: pauli_tensors[pauli.name] - for q, pauli in pauli_string.map.items() + q: pauli_tensors[pauli.name] for q, pauli in pauli_string.map.items() } num_pauli = len(qubit_pauli_map) @@ -270,9 +268,7 @@ def expectation_value( state_modes = tuple( (self._circuit.qubits.index(qb),) for qb in qubit_pauli_map.keys() ) - gate_data = tuple( - tensor.data.ptr for tensor in qubit_pauli_map.values() - ) + gate_data = tuple(tensor.data.ptr for tensor in qubit_pauli_map.values()) cutn.network_operator_append_product( handle=self._lib.handle, @@ -288,7 +284,9 @@ def expectation_value( ###################################################### # Configure the cuTensorNet expectation value object # ###################################################### - expectation = cutn.create_expectation(self._lib.handle, self._state, tn_operator) + expectation = cutn.create_expectation( + self._lib.handle, self._state, tn_operator + ) if attributes is None: attributes = dict() @@ -313,25 +311,25 @@ def expectation_value( ###################################### # Allocate workspace for contraction # ###################################### - self._stream = cp.cuda.Stream() + stream = cp.cuda.Stream() free_mem = self._lib.dev.mem_info[0] scratch_size = int(scratch_fraction * free_mem) - self._scratch_space = cp.cuda.alloc(scratch_size) + scratch_space = cp.cuda.alloc(scratch_size) self._logger.debug( f"Allocated {scratch_size} bytes of scratch memory on GPU" ) - self._work_desc = cutn.create_workspace_descriptor(self._lib.handle) + work_desc = cutn.create_workspace_descriptor(self._lib.handle) cutn.expectation_prepare( self._lib.handle, expectation, scratch_size, - self._work_desc, - self._stream.ptr, # type: ignore + work_desc, + stream.ptr, ) workspace_size_d = cutn.workspace_get_memory_size( self._lib.handle, - self._work_desc, + work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, @@ -340,10 +338,10 @@ def expectation_value( if workspace_size_d <= scratch_size: cutn.workspace_set_memory( self._lib.handle, - self._work_desc, + work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, - self._scratch_space.ptr, # type: ignore + scratch_space.ptr, workspace_size_d, ) self._logger.debug( @@ -363,12 +361,12 @@ def expectation_value( cutn.expectation_compute( self._lib.handle, expectation, - self._work_desc, + work_desc, expectation_value.ctypes.data, state_norm.ctypes.data, - self._stream.ptr, # type: ignore + stream.ptr, ) - self._stream.synchronize() # type: ignore + stream.synchronize() # Note: we can also return `state_norm.item()`, but this should be 1 since # we are always running unitary circuits @@ -381,18 +379,14 @@ def expectation_value( ##################################################### # Destroy the Operator and ExpectationValue objects # ##################################################### - if self._work_desc is not None: - cutn.destroy_workspace_descriptor(self._work_desc) # type: ignore + cutn.destroy_workspace_descriptor(work_desc) # type: ignore cutn.destroy_expectation(expectation) cutn.destroy_network_operator(tn_operator) - del self._scratch_space + del scratch_space def destroy(self) -> None: """Destroys tensor network state.""" - if self._work_desc is not None: - cutn.destroy_workspace_descriptor(self._work_desc) # type: ignore cutn.destroy_state(self._state) - del self._scratch_space def _formatted_tensor(matrix: NDArray, n_qubits: int) -> cp.ndarray: diff --git a/tests/test_general_state.py b/tests/test_general_state.py index 28966190..8b332e74 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -36,7 +36,6 @@ def test_convert_statevec_ovl(circuit: Circuit) -> None: with CuTensorNetHandle() as libhandle: state = GeneralState(circuit, libhandle) sv = state.get_statevector() - state.destroy() sv_pytket = np.array([circuit.get_statevector()]) assert np.allclose(sv, sv_pytket, atol=1e-10) @@ -51,7 +50,6 @@ def test_convert_statevec_ovl(circuit: Circuit) -> None: with CuTensorNetHandle() as libhandle: state = GeneralState(circuit, libhandle) ovl = state.expectation_value(op) - state.destroy() assert ovl == pytest.approx(1.0) @@ -80,7 +78,6 @@ def test_toffoli_box_with_implicit_swaps() -> None: with CuTensorNetHandle() as libhandle: state = GeneralState(ket_circ, libhandle) ket_net_vector = state.get_statevector() - state.destroy() # Apply phase ket_net_vector = ket_net_vector * cmath.exp(1j * cmath.pi * ket_circ.phase) @@ -119,7 +116,6 @@ def to_bool_tuple(n_qubits: int, x: int) -> tuple: with CuTensorNetHandle() as libhandle: state = GeneralState(ket_circ, libhandle) ket_net_vector = state.get_statevector() - state.destroy() ket_net_vector = ket_net_vector * cmath.exp(1j * cmath.pi * ket_circ.phase) ket_pytket_vector = ket_circ.get_statevector() assert np.allclose(ket_net_vector, ket_pytket_vector) @@ -135,5 +131,4 @@ def to_bool_tuple(n_qubits: int, x: int) -> tuple: with CuTensorNetHandle() as libhandle: state = GeneralState(ket_circ, libhandle) ovl = state.expectation_value(op) - state.destroy() assert ovl == pytest.approx(1.0) From 301c9f0f4867d5fe5593bf9a906c5d68edd9000c Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Thu, 6 Jun 2024 15:39:14 +0100 Subject: [PATCH 53/76] Updated tests so that libhandle is reused and state.destroy() is called at the very end --- tests/test_general_state.py | 51 ++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/tests/test_general_state.py b/tests/test_general_state.py index 8b332e74..d2e75b7c 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -36,21 +36,23 @@ def test_convert_statevec_ovl(circuit: Circuit) -> None: with CuTensorNetHandle() as libhandle: state = GeneralState(circuit, libhandle) sv = state.get_statevector() - sv_pytket = np.array([circuit.get_statevector()]) - assert np.allclose(sv, sv_pytket, atol=1e-10) - op = QubitPauliOperator( - { - QubitPauliString({Qubit(0): Pauli.I, Qubit(1): Pauli.I}): 1.0, - } - ) + sv_pytket = np.array([circuit.get_statevector()]) + assert np.allclose(sv, sv_pytket, atol=1e-10) - # Use an alternative calculation of the overlap as the expectation value - # of the identity operator: = - with CuTensorNetHandle() as libhandle: + op = QubitPauliOperator( + { + QubitPauliString({Qubit(0): Pauli.I, Qubit(1): Pauli.I}): 1.0, + } + ) + + # Use an alternative calculation of the overlap as the expectation value + # of the identity operator: = state = GeneralState(circuit, libhandle) ovl = state.expectation_value(op) - assert ovl == pytest.approx(1.0) + assert ovl == pytest.approx(1.0) + + state.destroy() def test_toffoli_box_with_implicit_swaps() -> None: @@ -78,6 +80,7 @@ def test_toffoli_box_with_implicit_swaps() -> None: with CuTensorNetHandle() as libhandle: state = GeneralState(ket_circ, libhandle) ket_net_vector = state.get_statevector() + state.destroy() # Apply phase ket_net_vector = ket_net_vector * cmath.exp(1j * cmath.pi * ket_circ.phase) @@ -116,19 +119,21 @@ def to_bool_tuple(n_qubits: int, x: int) -> tuple: with CuTensorNetHandle() as libhandle: state = GeneralState(ket_circ, libhandle) ket_net_vector = state.get_statevector() - ket_net_vector = ket_net_vector * cmath.exp(1j * cmath.pi * ket_circ.phase) - ket_pytket_vector = ket_circ.get_statevector() - assert np.allclose(ket_net_vector, ket_pytket_vector) - # Use an alternative calculation of the overlap as the expectation value - # of the identity operator: = - op = QubitPauliOperator( - { - QubitPauliString({Qubit(i): Pauli.I for i in range(n_qubits)}): 1.0, - } - ) + ket_net_vector = ket_net_vector * cmath.exp(1j * cmath.pi * ket_circ.phase) + ket_pytket_vector = ket_circ.get_statevector() + assert np.allclose(ket_net_vector, ket_pytket_vector) + + # Use an alternative calculation of the overlap as the expectation value + # of the identity operator: = + op = QubitPauliOperator( + { + QubitPauliString({Qubit(i): Pauli.I for i in range(n_qubits)}): 1.0, + } + ) - with CuTensorNetHandle() as libhandle: state = GeneralState(ket_circ, libhandle) ovl = state.expectation_value(op) - assert ovl == pytest.approx(1.0) + assert ovl == pytest.approx(1.0) + + state.destroy() From b8b713cb33edc1f8ab30b71d0e4694af32c04a03 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 7 Jun 2024 04:53:29 -0700 Subject: [PATCH 54/76] Applying overall phase on state vector. Copying circuit when passed to GeneralState. --- .../cutensornet/general_state/tensor_network_state.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 78f5a96b..bf529b72 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -49,7 +49,7 @@ def __init__( loglevel: Internal logger output level. """ self._logger = set_logger("GeneralState", loglevel) - self._circuit = circuit + self._circuit = circuit.copy() # TODO: This is not strictly necessary; implicit SWAPs could be resolved by # qubit relabelling, but it's only worth doing so if there are clear signs # of inefficiency due to this. @@ -196,9 +196,12 @@ def get_statevector( stream.ptr, ) stream.synchronize() + sv = state_vector.flatten() if on_host: - return cp.asnumpy(state_vector.flatten()) - return state_vector.flatten() + sv = cp.asnumpy(sv) + # Apply the phase from the circuit + sv *= np.exp(1j * np.pi * self._circuit.phase) + return sv finally: cutn.destroy_workspace_descriptor(work_desc) # type: ignore From 9867c46a207611eec91b7e91c46231454af80275 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 7 Jun 2024 04:57:29 -0700 Subject: [PATCH 55/76] Fixing bug due to not using local copy of circuit --- .../cutensornet/general_state/tensor_network_state.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index bf529b72..816bd726 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -58,7 +58,7 @@ def __init__( libhandle.print_device_properties(self._logger) - num_qubits = circuit.n_qubits + num_qubits = self._circuit.n_qubits dim = 2 # We are always dealing with qubits, not qudits qubits_dims = (dim,) * num_qubits # qubit size self._logger.debug(f"Converting a quantum circuit with {num_qubits} qubits.") @@ -73,7 +73,7 @@ def __init__( # TODO: we should add a check to verify that the commands are unitaries # (e.g. don't accept measurements). Potentially, measurements at the end of # the circuit can be ignored at the user's request. - for com in circuit.get_commands(): + for com in self._circuit.get_commands(): gate_unitary = com.op.get_unitary() self._gate_tensors.append(_formatted_tensor(gate_unitary, com.op.n_qubits)) gate_qubit_indices = tuple( From d2b6e253af1f3ec9e1355359dd6941d0f7fb1f11 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 7 Jun 2024 04:59:06 -0700 Subject: [PATCH 56/76] Adding more tests and fixing some issues --- tests/test_general_state.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/test_general_state.py b/tests/test_general_state.py index d2e75b7c..4ef6c46f 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -15,6 +15,8 @@ @pytest.mark.parametrize( "circuit", [ + pytest.lazy_fixture("q5_empty"), # type: ignore + pytest.lazy_fixture("q8_empty"), # type: ignore pytest.lazy_fixture("q2_x0"), # type: ignore pytest.lazy_fixture("q2_x1"), # type: ignore pytest.lazy_fixture("q2_v0"), # type: ignore @@ -28,8 +30,14 @@ pytest.lazy_fixture("q2_lcu3"), # type: ignore pytest.lazy_fixture("q3_v0cx02"), # type: ignore pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + pytest.lazy_fixture("q3_toffoli_box_with_implicit_swaps"), # type: ignore pytest.lazy_fixture("q4_lcu1"), # type: ignore pytest.lazy_fixture("q4_multicontrols"), # type: ignore + pytest.lazy_fixture("q4_with_creates"), # type: ignore + pytest.lazy_fixture("q5_h0s1rz2ry3tk4tk13"), # type: ignore + pytest.lazy_fixture("q5_line_circ_30_layers"), # type: ignore + pytest.lazy_fixture("q6_qvol"), # type: ignore + pytest.lazy_fixture("q8_x0h2v5z6"), # type: ignore ], ) def test_convert_statevec_ovl(circuit: Circuit) -> None: @@ -37,16 +45,16 @@ def test_convert_statevec_ovl(circuit: Circuit) -> None: state = GeneralState(circuit, libhandle) sv = state.get_statevector() - sv_pytket = np.array([circuit.get_statevector()]) + sv_pytket = circuit.get_statevector() assert np.allclose(sv, sv_pytket, atol=1e-10) op = QubitPauliOperator( { - QubitPauliString({Qubit(0): Pauli.I, Qubit(1): Pauli.I}): 1.0, + QubitPauliString({q: Pauli.I for q in circuit.qubits}): 1.0, } ) - # Use an alternative calculation of the overlap as the expectation value + # Calculate the inner product as the expectation value # of the identity operator: = state = GeneralState(circuit, libhandle) ovl = state.expectation_value(op) @@ -82,9 +90,6 @@ def test_toffoli_box_with_implicit_swaps() -> None: ket_net_vector = state.get_statevector() state.destroy() - # Apply phase - ket_net_vector = ket_net_vector * cmath.exp(1j * cmath.pi * ket_circ.phase) - # Compare to pytket statevector ket_pytket_vector = ket_circ.get_statevector() @@ -120,15 +125,14 @@ def to_bool_tuple(n_qubits: int, x: int) -> tuple: state = GeneralState(ket_circ, libhandle) ket_net_vector = state.get_statevector() - ket_net_vector = ket_net_vector * cmath.exp(1j * cmath.pi * ket_circ.phase) ket_pytket_vector = ket_circ.get_statevector() assert np.allclose(ket_net_vector, ket_pytket_vector) - # Use an alternative calculation of the overlap as the expectation value + # Calculate the inner product as the expectation value # of the identity operator: = op = QubitPauliOperator( { - QubitPauliString({Qubit(i): Pauli.I for i in range(n_qubits)}): 1.0, + QubitPauliString({q: Pauli.I for q in ket_circ.qubits}): 1.0, } ) From 4aeb976d92ce88fcb79dfa8b4939d6f177dceb8b Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 7 Jun 2024 06:25:44 -0700 Subject: [PATCH 57/76] Added a test for expectation values. Changed the name of some tests --- tests/test_general_state.py | 72 ++++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/tests/test_general_state.py b/tests/test_general_state.py index 4ef6c46f..76c9db76 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -7,7 +7,7 @@ from pytket.transform import Transform from pytket.pauli import QubitPauliString, Pauli from pytket.utils.operators import QubitPauliOperator -from pytket.circuit import Circuit +from pytket.circuit import Circuit, OpType from pytket.extensions.cutensornet.general_state import GeneralState from pytket.extensions.cutensornet.structured_state import CuTensorNetHandle @@ -40,7 +40,7 @@ pytest.lazy_fixture("q8_x0h2v5z6"), # type: ignore ], ) -def test_convert_statevec_ovl(circuit: Circuit) -> None: +def test_get_statevec(circuit: Circuit) -> None: with CuTensorNetHandle() as libhandle: state = GeneralState(circuit, libhandle) sv = state.get_statevector() @@ -63,7 +63,7 @@ def test_convert_statevec_ovl(circuit: Circuit) -> None: state.destroy() -def test_toffoli_box_with_implicit_swaps() -> None: +def test_sv_toffoli_box_with_implicit_swaps() -> None: # Using specific permutation here perm = { (False, False): (True, True), @@ -97,7 +97,7 @@ def test_toffoli_box_with_implicit_swaps() -> None: @pytest.mark.parametrize("n_qubits", [4, 5, 6]) -def test_generalised_toffoli_box(n_qubits: int) -> None: +def test_sv_generalised_toffoli_box(n_qubits: int) -> None: def to_bool_tuple(n_qubits: int, x: int) -> tuple: bool_list = [] for i in reversed(range(n_qubits)): @@ -141,3 +141,67 @@ def to_bool_tuple(n_qubits: int, x: int) -> tuple: assert ovl == pytest.approx(1.0) state.destroy() + + +@pytest.mark.parametrize( + "circuit", + [ + pytest.lazy_fixture("q5_empty"), # type: ignore + pytest.lazy_fixture("q8_empty"), # type: ignore + pytest.lazy_fixture("q2_x0"), # type: ignore + pytest.lazy_fixture("q2_x1"), # type: ignore + pytest.lazy_fixture("q2_v0"), # type: ignore + pytest.lazy_fixture("q2_x0cx01"), # type: ignore + pytest.lazy_fixture("q2_x1cx10x1"), # type: ignore + pytest.lazy_fixture("q2_x0cx01cx10"), # type: ignore + pytest.lazy_fixture("q2_v0cx01cx10"), # type: ignore + pytest.lazy_fixture("q2_hadamard_test"), # type: ignore + pytest.lazy_fixture("q2_lcu1"), # type: ignore + pytest.lazy_fixture("q2_lcu2"), # type: ignore + pytest.lazy_fixture("q2_lcu3"), # type: ignore + pytest.lazy_fixture("q3_v0cx02"), # type: ignore + pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + pytest.lazy_fixture("q3_toffoli_box_with_implicit_swaps"), # type: ignore + pytest.lazy_fixture("q4_lcu1"), # type: ignore + pytest.lazy_fixture("q4_multicontrols"), # type: ignore + pytest.lazy_fixture("q4_with_creates"), # type: ignore + pytest.lazy_fixture("q5_h0s1rz2ry3tk4tk13"), # type: ignore + pytest.lazy_fixture("q5_line_circ_30_layers"), # type: ignore + pytest.lazy_fixture("q6_qvol"), # type: ignore + pytest.lazy_fixture("q8_x0h2v5z6"), # type: ignore + ], +) +@pytest.mark.parametrize( + "observable", + [ + QubitPauliOperator( + { + QubitPauliString({Qubit(0): Pauli.I, Qubit(1): Pauli.X}): 1.0, + } + ), + QubitPauliOperator( + { + QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.Y}): 3.5, + } + ), + QubitPauliOperator( + { + QubitPauliString({Qubit(0): Pauli.Z}): 0.25, + QubitPauliString({Qubit(1): Pauli.Y}): 0.33, + QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.X}): 0.42, + } + ), + ], +) +def test_expectation_value(circuit: Circuit, observable: QubitPauliOperator) -> None: + # Note: not all qubits are acted on by the observable. The remaining qubits are + # interpreted to have I (identity) operators on them both by pytket and cutensornet. + exp_val_tket = observable.state_expectation(circuit.get_statevector()) + + # Calculate using GeneralState + with CuTensorNetHandle() as libhandle: + state = GeneralState(circuit, libhandle) + exp_val = state.expectation_value(observable) + + assert np.isclose(exp_val, exp_val_tket) + state.destroy() From 44f7d6b73145b04b5812ab8f206b40f7cc405a6c Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 7 Jun 2024 06:31:37 -0700 Subject: [PATCH 58/76] Allowing complex expectation value --- .../cutensornet/general_state/tensor_network_state.py | 7 +++---- tests/test_general_state.py | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 816bd726..29600228 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -212,7 +212,7 @@ def expectation_value( operator: QubitPauliOperator, attributes: Optional[dict] = None, scratch_fraction: float = 0.5, - ) -> float: + ) -> complex: """Calculates the expectation value of the given operator. Args: @@ -374,9 +374,8 @@ def expectation_value( # Note: we can also return `state_norm.item()`, but this should be 1 since # we are always running unitary circuits assert np.isclose(state_norm.item(), 1.0) - # The expectation value is a real number - assert np.isclose(expectation_value.item().imag, 0.0) - return expectation_value.item().real # type: ignore + + return expectation_value.item() # type: ignore finally: ##################################################### diff --git a/tests/test_general_state.py b/tests/test_general_state.py index 76c9db76..74cb251b 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -181,14 +181,14 @@ def to_bool_tuple(n_qubits: int, x: int) -> tuple: ), QubitPauliOperator( { - QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.Y}): 3.5, + QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.Y}): 3.5 + 0.3j, } ), QubitPauliOperator( { QubitPauliString({Qubit(0): Pauli.Z}): 0.25, - QubitPauliString({Qubit(1): Pauli.Y}): 0.33, - QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.X}): 0.42, + QubitPauliString({Qubit(1): Pauli.Y}): 0.33j, + QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.X}): 0.42 + 0.1j, } ), ], From bd8d5127300e744250b9f077d3ab634d34015ea8 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 7 Jun 2024 15:14:41 +0100 Subject: [PATCH 59/76] Updated docs --- docs/api.rst | 5 ++ docs/modules/general_state.rst | 19 ++++---- docs/modules/structured_state.rst | 4 -- .../cutensornet/general_state/__init__.py | 2 +- .../general_state/tensor_network_state.py | 46 +++++++++++-------- .../structured_state/simulation.py | 2 +- 6 files changed, 43 insertions(+), 35 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 48ed0e2c..1e91e2c8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,6 +1,11 @@ API documentation ----------------- +.. autoclass:: pytket.extensions.cutensornet.CuTensorNetHandle + + .. automethod:: destroy + + .. toctree:: modules/general_state.rst modules/structured_state.rst diff --git a/docs/modules/general_state.rst b/docs/modules/general_state.rst index b4b9500b..4ee45838 100644 --- a/docs/modules/general_state.rst +++ b/docs/modules/general_state.rst @@ -1,8 +1,15 @@ -Full tensor network (general state) contraction -=============================================== +General state (exact) simulation +================================ .. automodule:: pytket.extensions.cutensornet.general_state +.. autoclass:: pytket.extensions.cutensornet.general_state.GeneralState() + + .. automethod:: __init__ + .. automethod:: get_statevector + .. automethod:: expectation_value + .. automethod:: destroy + cuQuantum `contract` API interface ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -16,14 +23,6 @@ cuQuantum `contract` API interface .. autofunction:: pytket.extensions.cutensornet.general_state.tk_to_tensor_network -cuQuantum `high-level` API interface -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: pytket.extensions.cutensornet.general_state.GeneralState - -.. autoclass:: pytket.extensions.cutensornet.general_state.GeneralOperator - -.. autoclass:: pytket.extensions.cutensornet.general_state.GeneralExpectationValue Pytket backend ~~~~~~~~~~~~~~ diff --git a/docs/modules/structured_state.rst b/docs/modules/structured_state.rst index f360d196..e6b658d7 100644 --- a/docs/modules/structured_state.rst +++ b/docs/modules/structured_state.rst @@ -16,10 +16,6 @@ Simulation .. automethod:: __init__ -.. autoclass:: pytket.extensions.cutensornet.structured_state.CuTensorNetHandle - - .. automethod:: destroy - Classes ~~~~~~~ diff --git a/pytket/extensions/cutensornet/general_state/__init__.py b/pytket/extensions/cutensornet/general_state/__init__.py index d72f2110..ea7a0775 100644 --- a/pytket/extensions/cutensornet/general_state/__init__.py +++ b/pytket/extensions/cutensornet/general_state/__init__.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Module for conversion from tket primitives to cuQuantum primitives.""" +"""Module for simulating circuits with no predetermined tensor network structure.""" from .utils import circuit_statevector_postselect diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 29600228..cd94b965 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -22,7 +22,7 @@ class GeneralState: - """Wraps a cuTensorNet TN object for exact simulations via path optimisation""" + """Wrapper of cuTensorNet object for exact simulations via path optimisation.""" def __init__( self, @@ -30,22 +30,22 @@ def __init__( libhandle: CuTensorNetHandle, loglevel: int = logging.INFO, ) -> None: - """Constructs a tensor network state representation from a pytket circuit. + """Constructs a tensor network representating a pytket circuit. - Note: - The tensor network is *not* contracted until the appropriate methods - from this class are called. + The resulting object stores the *uncontracted* tensor network. Note: - Circuit should not contain boxes - only explicit gates with specific unitary - matrix representation available in pytket. + A ``libhandle`` is created via a ``with CuTensorNetHandle() as libhandle:`` + statement. The device where the ``GeneralState`` is stored will match the + one specified by the library handle. Note: - If present, implicit wire swaps are replaced with explicit SWAP gates. + The ``circuit`` must not contain any ``CircBox`` or non-unitary command. + Internally, implicit wire swaps are replaced with explicit SWAP gates. Args: circuit: A pytket circuit to be converted to a tensor network. - libhandle: cuTensorNet handle. + libhandle: An instance of a ``CuTensorNetHandle``. loglevel: Internal logger output level. """ self._logger = set_logger("GeneralState", loglevel) @@ -101,15 +101,16 @@ def get_statevector( """Contracts the circuit and returns the final statevector. Args: - attributes: A dict of cuTensorNet State attributes and their values. - scratch_fraction: Fraction of free memory on GPU to allocate as scratch - space. Defaults to `0.5`. - on_host: If :code:`True`, converts cupy :code:`ndarray` to numpy - :code:`ndarray`, copying it to host device (CPU). + attributes: Optional. A dict of cuTensorNet `StateAttribute` keys and + their values. + scratch_fraction: Optional. Fraction of free memory on GPU to allocate as + scratch space. + on_host: Optional. If ``True``, converts cupy ``ndarray`` to numpy + ``ndarray``, copying it to host device (CPU). Raises: MemoryError: If there is insufficient workspace on GPU. Returns: - Either a :code:`cupy.ndarray` on a GPU, or a :code:`numpy.ndarray` on a + Either a ``cupy.ndarray`` on a GPU, or a ``numpy.ndarray`` on a host device (CPU). Arrays are returned in a 1D shape. """ @@ -217,9 +218,10 @@ def expectation_value( Args: operator: The operator whose expectation value is to be measured. - attributes: A dict of cuTensorNet Expectation attributes and their values. - scratch_fraction: Fraction of free memory on GPU to allocate as scratch - space. Defaults to `0.5`. + attributes: Optional. A dict of cuTensorNet `ExpectationAttribute` keys + and their values. + scratch_fraction: Optional. Fraction of free memory on GPU to allocate as + scratch space. Raises: ValueError: If the operator acts on qubits not present in the circuit. @@ -387,7 +389,13 @@ def expectation_value( del scratch_space def destroy(self) -> None: - """Destroys tensor network state.""" + """Destroy the tensor network and free up GPU memory. + + Note: + Users are required to call `destroy()` when done using a + `GeneralState` object. GPU memory deallocation is not + guaranteed otherwise. + """ cutn.destroy_state(self._state) diff --git a/pytket/extensions/cutensornet/structured_state/simulation.py b/pytket/extensions/cutensornet/structured_state/simulation.py index 5aaef3e6..88cc9002 100644 --- a/pytket/extensions/cutensornet/structured_state/simulation.py +++ b/pytket/extensions/cutensornet/structured_state/simulation.py @@ -60,7 +60,7 @@ def simulate( """Simulates the circuit and returns the ``StructuredState`` of the final state. Note: - A ``libhandle`` should be created via a ``with CuTensorNet() as libhandle:`` + A ``libhandle`` is created via a ``with CuTensorNetHandle() as libhandle:`` statement. The device where the ``StructuredState`` is stored will match the one specified by the library handle. From 608df86e3b3554c4e41a57bd4996cfd9322b9225 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 7 Jun 2024 15:19:31 +0100 Subject: [PATCH 60/76] Fixing a complaint from pylint --- tests/test_general_state.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_general_state.py b/tests/test_general_state.py index 74cb251b..097f8c05 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -1,4 +1,3 @@ -import cmath import random import numpy as np import pytest @@ -7,7 +6,7 @@ from pytket.transform import Transform from pytket.pauli import QubitPauliString, Pauli from pytket.utils.operators import QubitPauliOperator -from pytket.circuit import Circuit, OpType +from pytket.circuit import Circuit from pytket.extensions.cutensornet.general_state import GeneralState from pytket.extensions.cutensornet.structured_state import CuTensorNetHandle From dfcdcf3c3866389dec33ba53c9a72de8833cf52b Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 7 Jun 2024 15:23:52 +0100 Subject: [PATCH 61/76] Updated changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 11cc6d38..a53343c6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Unreleased ---------- +* New API: ``GeneralState`` for exact simulation of circuits via contraction-path optimisation. Currently supports ``get_statevector()`` and ``expectation_value()``. * New feature: ``add_qubit`` to add fresh qubits at specified positions in an ``MPS``. * New feature: added an option to ``measure`` to toggle destructive measurement on/off. Currently only supported for ``MPS``. * New feature: a seed can now be provided to ``Config`` objects, providing reproducibility across ``StructuredState`` simulations. From 704c9dc0701a70f653cfabfa0eff0d02314eb1ea Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 7 Jun 2024 07:27:36 -0700 Subject: [PATCH 62/76] Removed default choice of NUM_HYPER_SAMPLES --- .../cutensornet/general_state/tensor_network_state.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index cd94b965..0e3a6468 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -119,8 +119,6 @@ def get_statevector( #################################### if attributes is None: attributes = dict() - if "NUM_HYPER_SAMPLES" not in attributes: - attributes["NUM_HYPER_SAMPLES"] = 8 attribute_pairs = [ (getattr(cutn.StateAttribute, k), v) for k, v in attributes.items() ] @@ -295,8 +293,6 @@ def expectation_value( if attributes is None: attributes = dict() - if "OPT_NUM_HYPER_SAMPLES" not in attributes: - attributes["OPT_NUM_HYPER_SAMPLES"] = 8 attribute_pairs = [ (getattr(cutn.ExpectationAttribute, k), v) for k, v in attributes.items() ] From 5ef76f5b1e6a1e96b4112966ab3e7f672a55c2bc Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 7 Jun 2024 15:51:21 +0100 Subject: [PATCH 63/76] Adding copyright notice to new files --- .../general_state/tensor_network_state.py | 14 ++++++++++++++ .../extensions/cutensornet/general_state/utils.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 0e3a6468..fa9a6059 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -1,3 +1,17 @@ +# Copyright 2019-2024 Quantinuum +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from __future__ import annotations import logging from typing import Union, Optional diff --git a/pytket/extensions/cutensornet/general_state/utils.py b/pytket/extensions/cutensornet/general_state/utils.py index 33aefc59..d0b77927 100644 --- a/pytket/extensions/cutensornet/general_state/utils.py +++ b/pytket/extensions/cutensornet/general_state/utils.py @@ -1,3 +1,17 @@ +# Copyright 2019-2024 Quantinuum +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from numpy.typing import NDArray from pytket.backends.backendresult import BackendResult from pytket.circuit import Qubit, Circuit From 7186c632585481e0569cd5393d7e74658c975ddb Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 7 Jun 2024 15:59:17 +0100 Subject: [PATCH 64/76] Adding error message if circuit contains non-unitary gates --- .../cutensornet/general_state/tensor_network_state.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index fa9a6059..d4dc5570 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -84,11 +84,14 @@ def __init__( self._gate_tensors = [] # Append all gates to the TN - # TODO: we should add a check to verify that the commands are unitaries - # (e.g. don't accept measurements). Potentially, measurements at the end of - # the circuit can be ignored at the user's request. for com in self._circuit.get_commands(): - gate_unitary = com.op.get_unitary() + try: + gate_unitary = com.op.get_unitary() + except: + raise ValueError( + "All commands in the circuit must be unitary gates. The circuit " + f"contains {com}; no unitary matrix could be retrived for it." + ) self._gate_tensors.append(_formatted_tensor(gate_unitary, com.op.n_qubits)) gate_qubit_indices = tuple( self._circuit.qubits.index(qb) for qb in com.qubits From 25aa6e786f7933b51d9af44fc056d02586c74cee Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Mon, 10 Jun 2024 16:43:44 +0100 Subject: [PATCH 65/76] Boilerplate code for sampler. Implementation not yet ready, but pushing to do some debugging on remote machine first. --- .../general_state/tensor_network_state.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index d4dc5570..1f42f240 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -26,6 +26,7 @@ from numpy.typing import NDArray from pytket.circuit import Circuit from pytket.extensions.cutensornet.general import CuTensorNetHandle, set_logger +from pytket.utils import OutcomeArray from pytket.utils.operators import QubitPauliOperator try: @@ -401,6 +402,115 @@ def expectation_value( cutn.destroy_network_operator(tn_operator) del scratch_space + + def sample(self, n_shots: int) -> OutcomeArray: + """Obtain samples from the circuit. + """ + + num_qubits = self._circuit.n_qubits + qubits_dims = (2,) * num_qubits + data_type = cq.cudaDataType.CUDA_C_64F + + ############################################ + # Configure the cuTensorNet sampler object # + ############################################ + sampler = cutn.create_sampler( + handle=self._lib.handle, + tensor_network_state=self._state, + num_modes_to_sample=num_qubits, # TODO: may be smaller if not all qubits are measured + modes_to_sample=0, # TODO: if not all qubits are measured, these need to be specified + ) + + if attributes is None: + attributes = dict() + attribute_pairs = [ + (getattr(cutn.SamplerAttribute, k), v) for k, v in attributes.items() + ] + + for attr, val in attribute_pairs: + attr_dtype = cutn.sampler_get_attribute_dtype(attr) + attr_arr = np.asarray(val, dtype=attr_dtype) + cutn.sampler_configure( + self._lib.handle, + sampler, + attr, + attr_arr.ctypes.data, + attr_arr.dtype.itemsize, + ) + + try: + ###################################### + # Allocate workspace for contraction # + ###################################### + stream = cp.cuda.Stream() + free_mem = self._lib.dev.mem_info[0] + scratch_size = int(scratch_fraction * free_mem) + scratch_space = cp.cuda.alloc(scratch_size) + + self._logger.debug( + f"Allocated {scratch_size} bytes of scratch memory on GPU" + ) + work_desc = cutn.create_workspace_descriptor(self._lib.handle) + cutn.sampler_prepare( + self._lib.handle, + sampler, + scratch_size, + work_desc, + stream.ptr, + ) + workspace_size_d = cutn.workspace_get_memory_size( + self._lib.handle, + work_desc, + cutn.WorksizePref.RECOMMENDED, + cutn.Memspace.DEVICE, + cutn.WorkspaceKind.SCRATCH, + ) + + if workspace_size_d <= scratch_size: + cutn.workspace_set_memory( + self._lib.handle, + work_desc, + cutn.Memspace.DEVICE, + cutn.WorkspaceKind.SCRATCH, + scratch_space.ptr, + workspace_size_d, + ) + self._logger.debug( + f"Set {workspace_size_d} bytes of workspace memory out of the" + f" allocated scratch space." + ) + else: + raise MemoryError( + f"Insufficient workspace size on the GPU device {self._lib.dev.id}" + ) + + ########################### + # Sample from the circuit # + ########################### + samples = np.empty((num_qubits, n_shots), dtype='int64', order='F') + cutn.sampler_sample( + self._lib.handle, + sampler, + n_shots, + work_desc, + samples.ctypes.data, + stream.ptr + ) + stream.synchronize() + + # TODO: Convert the data in `samples` to an OutcomeArray + return None + + finally: + ##################################################### + # Destroy the Operator and ExpectationValue objects # + ##################################################### + cutn.destroy_workspace_descriptor(work_desc) # type: ignore + cutn.destroy_sampler(sampler) + del scratch_space + + + def destroy(self) -> None: """Destroy the tensor network and free up GPU memory. From 4f56437b5879691a064c27ae4a0a404c93207781 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Mon, 10 Jun 2024 08:54:31 -0700 Subject: [PATCH 66/76] Adding minimal test for sampler --- tests/test_general_state.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_general_state.py b/tests/test_general_state.py index 097f8c05..77906d66 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -204,3 +204,40 @@ def test_expectation_value(circuit: Circuit, observable: QubitPauliOperator) -> assert np.isclose(exp_val, exp_val_tket) state.destroy() + + +@pytest.mark.parametrize( + "circuit", + [ + pytest.lazy_fixture("q5_empty"), # type: ignore + pytest.lazy_fixture("q8_empty"), # type: ignore + pytest.lazy_fixture("q2_x0"), # type: ignore + pytest.lazy_fixture("q2_x1"), # type: ignore + pytest.lazy_fixture("q2_v0"), # type: ignore + pytest.lazy_fixture("q2_x0cx01"), # type: ignore + pytest.lazy_fixture("q2_x1cx10x1"), # type: ignore + pytest.lazy_fixture("q2_x0cx01cx10"), # type: ignore + pytest.lazy_fixture("q2_v0cx01cx10"), # type: ignore + pytest.lazy_fixture("q2_hadamard_test"), # type: ignore + pytest.lazy_fixture("q2_lcu1"), # type: ignore + pytest.lazy_fixture("q2_lcu2"), # type: ignore + pytest.lazy_fixture("q2_lcu3"), # type: ignore + pytest.lazy_fixture("q3_v0cx02"), # type: ignore + pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + pytest.lazy_fixture("q3_toffoli_box_with_implicit_swaps"), # type: ignore + pytest.lazy_fixture("q4_lcu1"), # type: ignore + pytest.lazy_fixture("q4_multicontrols"), # type: ignore + pytest.lazy_fixture("q4_with_creates"), # type: ignore + pytest.lazy_fixture("q5_h0s1rz2ry3tk4tk13"), # type: ignore + pytest.lazy_fixture("q5_line_circ_30_layers"), # type: ignore + pytest.lazy_fixture("q6_qvol"), # type: ignore + pytest.lazy_fixture("q8_x0h2v5z6"), # type: ignore + ], +) +def test_sampler(circuit: Circuit) -> None: + + with CuTensorNetHandle() as libhandle: + state = GeneralState(circuit, libhandle) + shots = state.sample(100) + + state.destroy() From 307ab5343be39ecd2c88d63389c29b6dd97d13a5 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Mon, 10 Jun 2024 08:54:48 -0700 Subject: [PATCH 67/76] Solving minor bugs --- .../general_state/tensor_network_state.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 1f42f240..c6293428 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -402,14 +402,15 @@ def expectation_value( cutn.destroy_network_operator(tn_operator) del scratch_space - - def sample(self, n_shots: int) -> OutcomeArray: - """Obtain samples from the circuit. - """ + def sample( + self, + n_shots: int, + attributes: Optional[dict] = None, + scratch_fraction: float = 0.5, + ) -> OutcomeArray: + """Obtain samples from the circuit.""" num_qubits = self._circuit.n_qubits - qubits_dims = (2,) * num_qubits - data_type = cq.cudaDataType.CUDA_C_64F ############################################ # Configure the cuTensorNet sampler object # @@ -417,7 +418,7 @@ def sample(self, n_shots: int) -> OutcomeArray: sampler = cutn.create_sampler( handle=self._lib.handle, tensor_network_state=self._state, - num_modes_to_sample=num_qubits, # TODO: may be smaller if not all qubits are measured + num_modes_to_sample=num_qubits, # TODO: may be smaller if not all qubits are measured modes_to_sample=0, # TODO: if not all qubits are measured, these need to be specified ) @@ -487,14 +488,14 @@ def sample(self, n_shots: int) -> OutcomeArray: ########################### # Sample from the circuit # ########################### - samples = np.empty((num_qubits, n_shots), dtype='int64', order='F') + samples = np.empty((num_qubits, n_shots), dtype="int64", order="F") cutn.sampler_sample( self._lib.handle, sampler, n_shots, work_desc, samples.ctypes.data, - stream.ptr + stream.ptr, ) stream.synchronize() @@ -509,8 +510,6 @@ def sample(self, n_shots: int) -> OutcomeArray: cutn.destroy_sampler(sampler) del scratch_space - - def destroy(self) -> None: """Destroy the tensor network and free up GPU memory. From 1b00c0b8578ac39847e1630a55a13e3085adab16 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Mon, 10 Jun 2024 17:06:21 +0100 Subject: [PATCH 68/76] Now returning an OutcomeArray --- .../cutensornet/general_state/tensor_network_state.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index c6293428..819e9048 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -499,8 +499,10 @@ def sample( ) stream.synchronize() - # TODO: Convert the data in `samples` to an OutcomeArray - return None + # Convert the data in `samples` to an `OutcomeArray` + # `samples` is a 2D numpy array `samples[SampleId][QubitId]`, which is + # the transpose of what `OutcomeArray.from_readouts` expects + return OutcomeArray.from_readouts(samples.T) finally: ##################################################### From a62594cb7cb5a6a0d99a3f950cf2a61198f8de3d Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Mon, 10 Jun 2024 09:28:51 -0700 Subject: [PATCH 69/76] Testing sampler against theoretical probabilities --- tests/test_general_state.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_general_state.py b/tests/test_general_state.py index 77906d66..e35c2d7c 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -236,8 +236,18 @@ def test_expectation_value(circuit: Circuit, observable: QubitPauliOperator) -> ) def test_sampler(circuit: Circuit) -> None: + n_shots = 100000 + + # Get the statevector so that we can calculate theoretical probabilities + sv_pytket = circuit.get_statevector() + with CuTensorNetHandle() as libhandle: state = GeneralState(circuit, libhandle) - shots = state.sample(100) + shots = state.sample(n_shots) + + for out, count in shots.counts().items(): + outcome = out.to_intlist()[0] # Unpack from singleton OutcomeArray + prob = abs(sv_pytket[outcome])**2 # Theoretical probability + assert np.isclose(count / n_shots, prob, atol=0.01) state.destroy() From 924ab67d6be0b2a1217dfbe2ab3740398159ec5e Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Thu, 13 Jun 2024 11:43:04 +0100 Subject: [PATCH 70/76] Support for end-of-circuit measurements and sampling on subsets of qubits --- .../general_state/tensor_network_state.py | 69 +++++++++++++++---- tests/test_general_state.py | 19 +++-- 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 819e9048..9e836c6c 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -14,7 +14,7 @@ from __future__ import annotations import logging -from typing import Union, Optional +from typing import Union, Optional, Tuple, Dict import warnings try: @@ -24,10 +24,11 @@ import numpy as np from sympy import Expr # type: ignore from numpy.typing import NDArray -from pytket.circuit import Circuit +from pytket.circuit import Circuit, Qubit, Bit, OpType from pytket.extensions.cutensornet.general import CuTensorNetHandle, set_logger from pytket.utils import OutcomeArray from pytket.utils.operators import QubitPauliOperator +from pytket.backends.backendresult import BackendResult try: import cuquantum as cq # type: ignore @@ -64,14 +65,15 @@ def __init__( loglevel: Internal logger output level. """ self._logger = set_logger("GeneralState", loglevel) - self._circuit = circuit.copy() + self._lib = libhandle + libhandle.print_device_properties(self._logger) # TODO: This is not strictly necessary; implicit SWAPs could be resolved by # qubit relabelling, but it's only worth doing so if there are clear signs # of inefficiency due to this. - self._circuit.replace_implicit_wire_swaps() - self._lib = libhandle - - libhandle.print_device_properties(self._logger) + circ = circuit.copy() + circ.replace_implicit_wire_swaps() + # Remove end-of-circuit measurements and keep track of them separately + self._circuit, self._measurements = _extract_measurements(circ) num_qubits = self._circuit.n_qubits dim = 2 # We are always dealing with qubits, not qudits @@ -407,19 +409,25 @@ def sample( n_shots: int, attributes: Optional[dict] = None, scratch_fraction: float = 0.5, - ) -> OutcomeArray: + ) -> BackendResult: """Obtain samples from the circuit.""" - num_qubits = self._circuit.n_qubits + num_measurements = len(self._measurements) + # We will need both a list of the qubits and a list of the classical bits + # and it is essential that the elements in the same index of either list + # match according to the self._measurements map. We guarantee this here. + qbit_list, cbit_list = zip(*self._measurements.items()) + measured_modes = tuple(self._circuit.qubits.index(qb) for qb in qbit_list) ############################################ # Configure the cuTensorNet sampler object # ############################################ + sampler = cutn.create_sampler( handle=self._lib.handle, tensor_network_state=self._state, - num_modes_to_sample=num_qubits, # TODO: may be smaller if not all qubits are measured - modes_to_sample=0, # TODO: if not all qubits are measured, these need to be specified + num_modes_to_sample=num_measurements, + modes_to_sample=measured_modes, ) if attributes is None: @@ -488,7 +496,7 @@ def sample( ########################### # Sample from the circuit # ########################### - samples = np.empty((num_qubits, n_shots), dtype="int64", order="F") + samples = np.empty((num_measurements, n_shots), dtype="int64", order="F") cutn.sampler_sample( self._lib.handle, sampler, @@ -502,7 +510,14 @@ def sample( # Convert the data in `samples` to an `OutcomeArray` # `samples` is a 2D numpy array `samples[SampleId][QubitId]`, which is # the transpose of what `OutcomeArray.from_readouts` expects - return OutcomeArray.from_readouts(samples.T) + shots = OutcomeArray.from_readouts(samples.T) + # We need to specify which bits correspond to which columns in the shots + # table. Since cuTensorNet promises that the ordering of outcomes is + # determined by the ordering we provided as `measured_modes`, which in + # turn corresponds to the ordering of qubits in `qbit_list`, the fact that + # `cbit_list` has the appropriate order in relation to `self._measurements` + # determines this defines the ordering of classical bits we intend. + return BackendResult(c_bits=cbit_list, shots=shots) finally: ##################################################### @@ -533,3 +548,31 @@ def _formatted_tensor(matrix: NDArray, n_qubits: int) -> cp.ndarray: # We also need to reshape since a matrix only has 2 bonds, but for an # n-qubit gate we want 2^n bonds for input and another 2^n for output return cupy_matrix.reshape([2] * (2 * n_qubits), order="F") + + +def _extract_measurements(circ: Circuit) -> Tuple[Circuit, Dict[Qubit, Bit]]: + """Convert a pytket Circuit to a MyCircuit object and a measurement map. + + Only supports end-of-circuit measurements, which are removed from the returned + circuit and added to the dictionary. + """ + pure_circ = Circuit() + for q in circ.qubits: + pure_circ.add_qubit(q) + measure_map = dict() + # Track measured Qubits/used Bits to identify mid-circuit measurement + measured_units = set() + + for command in circ: + for u in command.args: + if u in measured_units: + raise ValueError("Circuit contains a mid-circuit measurement") + + if command.op.type == OpType.Measure: + measure_map[command.args[0]] = command.args[1] + measured_units.add(command.args[0]) + measured_units.add(command.args[1]) + else: + pure_circ.add_gate(command.op, command.args) + + return pure_circ, measure_map # type: ignore diff --git a/tests/test_general_state.py b/tests/test_general_state.py index e35c2d7c..2ee5818c 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -1,12 +1,11 @@ import random import numpy as np import pytest -from pytket.circuit import ToffoliBox, Qubit +from pytket.circuit import Circuit, ToffoliBox, Qubit, Bit from pytket.passes import DecomposeBoxes, CnXPairwiseDecomposition from pytket.transform import Transform from pytket.pauli import QubitPauliString, Pauli from pytket.utils.operators import QubitPauliOperator -from pytket.circuit import Circuit from pytket.extensions.cutensornet.general_state import GeneralState from pytket.extensions.cutensornet.structured_state import CuTensorNetHandle @@ -241,13 +240,21 @@ def test_sampler(circuit: Circuit) -> None: # Get the statevector so that we can calculate theoretical probabilities sv_pytket = circuit.get_statevector() + # Add measurements to qubits + for i, q in enumerate(circuit.qubits): + circuit.add_bit(Bit(i)) + circuit.Measure(q, Bit(i)) + + # Sample using our library with CuTensorNetHandle() as libhandle: state = GeneralState(circuit, libhandle) - shots = state.sample(n_shots) + results = state.sample(n_shots) - for out, count in shots.counts().items(): - outcome = out.to_intlist()[0] # Unpack from singleton OutcomeArray - prob = abs(sv_pytket[outcome])**2 # Theoretical probability + # Verify distribution matches theoretical probabilities + for bit_tuple, count in results.get_counts().items(): + # Convert bitstring (Tuple[int,...]) to integer base 10 + outcome = sum(bit << i for i, bit in enumerate(reversed(bit_tuple))) + prob = abs(sv_pytket[outcome]) ** 2 # Theoretical probability assert np.isclose(count / n_shots, prob, atol=0.01) state.destroy() From ca73177b8594c6d0077683a8975f75051f2a52af Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Thu, 13 Jun 2024 05:44:27 -0700 Subject: [PATCH 71/76] Now managing implicit swaps without adding SWAP gates. --- .../general_state/tensor_network_state.py | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 9e836c6c..6979f94d 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -46,8 +46,9 @@ def __init__( libhandle: CuTensorNetHandle, loglevel: int = logging.INFO, ) -> None: - """Constructs a tensor network representating a pytket circuit. + """Constructs a tensor network for the output state of a pytket circuit. + The qubits are assumed to be initialised in the ``|0>`` state. The resulting object stores the *uncontracted* tensor network. Note: @@ -57,7 +58,6 @@ def __init__( Note: The ``circuit`` must not contain any ``CircBox`` or non-unitary command. - Internally, implicit wire swaps are replaced with explicit SWAP gates. Args: circuit: A pytket circuit to be converted to a tensor network. @@ -67,13 +67,12 @@ def __init__( self._logger = set_logger("GeneralState", loglevel) self._lib = libhandle libhandle.print_device_properties(self._logger) - # TODO: This is not strictly necessary; implicit SWAPs could be resolved by - # qubit relabelling, but it's only worth doing so if there are clear signs - # of inefficiency due to this. - circ = circuit.copy() - circ.replace_implicit_wire_swaps() + # Remove end-of-circuit measurements and keep track of them separately - self._circuit, self._measurements = _extract_measurements(circ) + # It also resolves implicit sawps + self._circuit, self._measurements = _remove_meas_and_implicit_swaps(circuit) + # + self._qubit_idx_map = {q: i for i, q in enumerate(sorted(self._circuit.qubits))} num_qubits = self._circuit.n_qubits dim = 2 # We are always dealing with qubits, not qudits @@ -96,9 +95,7 @@ def __init__( f"contains {com}; no unitary matrix could be retrived for it." ) self._gate_tensors.append(_formatted_tensor(gate_unitary, com.op.n_qubits)) - gate_qubit_indices = tuple( - self._circuit.qubits.index(qb) for qb in com.qubits - ) + gate_qubit_indices = tuple(self._qubit_idx_map[qb] for qb in com.qubits) cutn.state_apply_tensor_operator( handle=self._lib.handle, @@ -289,7 +286,7 @@ def expectation_value( num_pauli = len(qubit_pauli_map) num_modes = (1,) * num_pauli state_modes = tuple( - (self._circuit.qubits.index(qb),) for qb in qubit_pauli_map.keys() + (self._qubit_idx_map[qb],) for qb in qubit_pauli_map.keys() ) gate_data = tuple(tensor.data.ptr for tensor in qubit_pauli_map.values()) @@ -417,7 +414,7 @@ def sample( # and it is essential that the elements in the same index of either list # match according to the self._measurements map. We guarantee this here. qbit_list, cbit_list = zip(*self._measurements.items()) - measured_modes = tuple(self._circuit.qubits.index(qb) for qb in qbit_list) + measured_modes = tuple(self._qubit_idx_map[qb] for qb in qbit_list) ############################################ # Configure the cuTensorNet sampler object # @@ -550,8 +547,9 @@ def _formatted_tensor(matrix: NDArray, n_qubits: int) -> cp.ndarray: return cupy_matrix.reshape([2] * (2 * n_qubits), order="F") -def _extract_measurements(circ: Circuit) -> Tuple[Circuit, Dict[Qubit, Bit]]: - """Convert a pytket Circuit to a MyCircuit object and a measurement map. +def _remove_meas_and_implicit_swaps(circ: Circuit) -> Tuple[Circuit, Dict[Qubit, Bit]]: + """Convert a pytket Circuit to an equivalent circuit with no measurements or + implicit swaps. The measurements are returned as a map between qubits and bits. Only supports end-of-circuit measurements, which are removed from the returned circuit and added to the dictionary. @@ -559,20 +557,26 @@ def _extract_measurements(circ: Circuit) -> Tuple[Circuit, Dict[Qubit, Bit]]: pure_circ = Circuit() for q in circ.qubits: pure_circ.add_qubit(q) + q_perm = circ.implicit_qubit_permutation() + measure_map = dict() - # Track measured Qubits/used Bits to identify mid-circuit measurement - measured_units = set() + # Track measured Qubits to identify mid-circuit measurement + measured_qubits = set() for command in circ: - for u in command.args: - if u in measured_units: + cmd_qubits = [q_perm[q] for q in command.qubits] + + for q in cmd_qubits: + if q in measured_qubits: raise ValueError("Circuit contains a mid-circuit measurement") if command.op.type == OpType.Measure: - measure_map[command.args[0]] = command.args[1] - measured_units.add(command.args[0]) - measured_units.add(command.args[1]) + measure_map[cmd_qubits[0]] = command.bits[0] + measured_qubits.add(cmd_qubits[0]) else: - pure_circ.add_gate(command.op, command.args) + if command.bits: + raise ValueError("Circuit contains an operation on a bit") + pure_circ.add_gate(command.op, cmd_qubits) + pure_circ.add_phase(circ.phase) return pure_circ, measure_map # type: ignore From 7fbd67bb109006c2dfa13a17d45a52975c3522ef Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Thu, 13 Jun 2024 06:31:57 -0700 Subject: [PATCH 72/76] Added tests for sampler on subset of qubits --- tests/test_general_state.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/test_general_state.py b/tests/test_general_state.py index 2ee5818c..f3aadf5b 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -233,7 +233,8 @@ def test_expectation_value(circuit: Circuit, observable: QubitPauliOperator) -> pytest.lazy_fixture("q8_x0h2v5z6"), # type: ignore ], ) -def test_sampler(circuit: Circuit) -> None: +@pytest.mark.parametrize("measure_all", [True, False]) # Measure all or a subset +def test_sampler(circuit: Circuit, measure_all: bool) -> None: n_shots = 100000 @@ -241,9 +242,15 @@ def test_sampler(circuit: Circuit) -> None: sv_pytket = circuit.get_statevector() # Add measurements to qubits + if measure_all: + num_measured = circuit.n_qubits + else: + num_measured = circuit.n_qubits // 2 + for i, q in enumerate(circuit.qubits): - circuit.add_bit(Bit(i)) - circuit.Measure(q, Bit(i)) + if i < num_measured: # Skip the least significant qubits + circuit.add_bit(Bit(i)) + circuit.Measure(q, Bit(i)) # Sample using our library with CuTensorNetHandle() as libhandle: @@ -254,7 +261,19 @@ def test_sampler(circuit: Circuit) -> None: for bit_tuple, count in results.get_counts().items(): # Convert bitstring (Tuple[int,...]) to integer base 10 outcome = sum(bit << i for i, bit in enumerate(reversed(bit_tuple))) - prob = abs(sv_pytket[outcome]) ** 2 # Theoretical probability + + # Calculate the theoretical probabilities + if measure_all: + prob = abs(sv_pytket[outcome]) ** 2 + else: + # Obtain all compatible basis states (bitstring encoded as int) + non_measured = circuit.n_qubits - num_measured + compatible = [ + (outcome << non_measured) + offset for offset in range(2**non_measured) + ] + # The probability is the sum of that of all compatible basis states + prob = sum(abs(sv_pytket[v]) ** 2 for v in compatible) + assert np.isclose(count / n_shots, prob, atol=0.01) state.destroy() From 8aefd5e80fae6905ed0f57fa336c6da27f4e8d82 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Thu, 13 Jun 2024 15:49:39 +0100 Subject: [PATCH 73/76] Updated docs --- docs/changelog.rst | 2 +- docs/modules/general_state.rst | 1 + .../general_state/tensor_network_state.py | 14 +++++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a53343c6..57a3dbb6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ Changelog Unreleased ---------- -* New API: ``GeneralState`` for exact simulation of circuits via contraction-path optimisation. Currently supports ``get_statevector()`` and ``expectation_value()``. +* New API: ``GeneralState`` for exact simulation of circuits via contraction-path optimisation. Currently supports ``get_statevector()``, ``expectation_value()`` and ``sample()``. * New feature: ``add_qubit`` to add fresh qubits at specified positions in an ``MPS``. * New feature: added an option to ``measure`` to toggle destructive measurement on/off. Currently only supported for ``MPS``. * New feature: a seed can now be provided to ``Config`` objects, providing reproducibility across ``StructuredState`` simulations. diff --git a/docs/modules/general_state.rst b/docs/modules/general_state.rst index 4ee45838..c2a892c8 100644 --- a/docs/modules/general_state.rst +++ b/docs/modules/general_state.rst @@ -8,6 +8,7 @@ General state (exact) simulation .. automethod:: __init__ .. automethod:: get_statevector .. automethod:: expectation_value + .. automethod:: sample .. automethod:: destroy cuQuantum `contract` API interface diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 6979f94d..03e565a0 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -407,7 +407,19 @@ def sample( attributes: Optional[dict] = None, scratch_fraction: float = 0.5, ) -> BackendResult: - """Obtain samples from the circuit.""" + """Obtains samples from the measurements at the end of the circuit. + + Args: + n_shots: The number of samples to obtain. + attributes: Optional. A dict of cuTensorNet `SamplerAttribute` keys and + their values. + scratch_fraction: Optional. Fraction of free memory on GPU to allocate as + scratch space. + Raises: + MemoryError: If there is insufficient workspace on GPU. + Returns: + A pytket ``BackendResult`` with the data from the shots. + """ num_measurements = len(self._measurements) # We will need both a list of the qubits and a list of the classical bits From 44746a743e066025c350ab6c0e2f19e9fb5c14b8 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 14 Jun 2024 10:11:49 +0100 Subject: [PATCH 74/76] Added a comment --- .../cutensornet/general_state/tensor_network_state.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 03e565a0..2855d5fa 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -71,7 +71,9 @@ def __init__( # Remove end-of-circuit measurements and keep track of them separately # It also resolves implicit sawps self._circuit, self._measurements = _remove_meas_and_implicit_swaps(circuit) - # + # Identify each qubit with the index of the bond that represents it in the + # tensor network stored in this GeneralState. Qubits are sorted in increasing + # lexicographical order, which is the TKET standard. self._qubit_idx_map = {q: i for i, q in enumerate(sorted(self._circuit.qubits))} num_qubits = self._circuit.n_qubits From 528e0beba9f950ccfd4171b843620f5ab7861bcc Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Wed, 26 Jun 2024 15:57:45 +0100 Subject: [PATCH 75/76] black in CI is now happy --- tests/test_general_state.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_general_state.py b/tests/test_general_state.py index f3aadf5b..d8cebb62 100644 --- a/tests/test_general_state.py +++ b/tests/test_general_state.py @@ -269,7 +269,8 @@ def test_sampler(circuit: Circuit, measure_all: bool) -> None: # Obtain all compatible basis states (bitstring encoded as int) non_measured = circuit.n_qubits - num_measured compatible = [ - (outcome << non_measured) + offset for offset in range(2**non_measured) + (outcome << non_measured) + offset + for offset in range(2**non_measured) ] # The probability is the sum of that of all compatible basis states prob = sum(abs(sv_pytket[v]) ** 2 for v in compatible) From 5e8e6657bdaf7c847c46296a455819c4b564cc42 Mon Sep 17 00:00:00 2001 From: Pablo Andres-Martinez <104848389+PabloAndresCQ@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:54:22 +0100 Subject: [PATCH 76/76] Apply changes to comments --- .../cutensornet/general_state/tensor_network_state.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 2855d5fa..bfe3561e 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -69,7 +69,7 @@ def __init__( libhandle.print_device_properties(self._logger) # Remove end-of-circuit measurements and keep track of them separately - # It also resolves implicit sawps + # It also resolves implicit swaps self._circuit, self._measurements = _remove_meas_and_implicit_swaps(circuit) # Identify each qubit with the index of the bond that represents it in the # tensor network stored in this GeneralState. Qubits are sorted in increasing @@ -532,7 +532,7 @@ def sample( finally: ##################################################### - # Destroy the Operator and ExpectationValue objects # + # Destroy the Sampler object # ##################################################### cutn.destroy_workspace_descriptor(work_desc) # type: ignore cutn.destroy_sampler(sampler)