Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Multi-shot sampling from GeneralState #129

Merged
merged 81 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
034af98
Moved modules dealing with exact TN circuit representation to s separ…
yapolyak Jan 26, 2024
0c05f1f
Started drafting `TensorNetworkState` constructor (converter).
yapolyak Jan 26, 2024
00c5b21
Finished drafting `TensorNetworkState` constructor (converter).
yapolyak Jan 30, 2024
acdf958
Subtle changes to CuTensorNetHandle (untested).
yapolyak Jan 31, 2024
657740a
Some refurbishment. Renamed `TensorNetworkState` into `GeneralState`.…
yapolyak Feb 2, 2024
73a0e20
Merged `develop` in.
yapolyak Feb 2, 2024
94d7019
Refactoring folder name: exact -> general_state
yapolyak Feb 2, 2024
3516303
Started drafting `GeneralOperator`.
yapolyak Feb 2, 2024
6695940
Finished drafting `GeneralOperator` class.
yapolyak Feb 5, 2024
2df1abb
Started drafting `GeneralExpectationValue` class.
yapolyak Feb 5, 2024
c670749
Finished drafting `GeneralExpectationValue` class.
yapolyak Feb 7, 2024
bce7c95
Added `.configure()`, `.prepare()` and drafted `compute()` in `Genera…
yapolyak Feb 7, 2024
8a6dccf
Imports additions/refactor.
yapolyak Feb 18, 2024
b1d7295
Fix in `cutn` attributes.
yapolyak Feb 18, 2024
c196e4d
Fix a bug in `GeneralState.configure()`.
yapolyak Feb 18, 2024
0faf65a
Handle device aliasing bug fix.
yapolyak Feb 18, 2024
e4defcd
Corrected `cutn.state_compute()` arguments list.
yapolyak Feb 20, 2024
4cdb622
Fixed `cutn.state_compute()`.
yapolyak Feb 23, 2024
9e64915
Factored our configure and prepare steps for `GeneralExpectationValue`.
yapolyak Feb 23, 2024
c5df6df
Fixes to `GeneralState` after debugging.
yapolyak Feb 29, 2024
5cd6936
Fixes to `GeneralOperator` after debugging.
yapolyak Feb 29, 2024
fb34305
Fixes to `GeneralExpectationValue` after debugging.
yapolyak Feb 29, 2024
1eb091f
Added `state` and `operator` properties to corresponding classes.
yapolyak Mar 1, 2024
71aff80
Enabled chaining of some methods.
yapolyak Mar 1, 2024
627d475
`GeneralOperator` now accepts `QubitPauliOperator` as parameter.
yapolyak Mar 4, 2024
106b87d
Added statevector test.
yapolyak Mar 4, 2024
3d12261
Added gate unitary transpose to account for the way cuTN stores tensors
yapolyak Mar 6, 2024
cf30aa3
Added overlap test.
yapolyak Mar 7, 2024
3d477ad
Added a toffoli box with implicit swaps test and corresponding fix to…
yapolyak Mar 7, 2024
5a0f418
Added `test_generalised_toffoli_box`.
yapolyak Mar 11, 2024
f6fce3b
A couple of bug fixes.
yapolyak Mar 11, 2024
1f35487
Merge branch 'develop' into feature/exact_state
yapolyak Mar 11, 2024
be22f4c
Updated public API docs.
yapolyak Mar 11, 2024
4d72fa1
Addressed some of mypy issues.
yapolyak Mar 11, 2024
61efd3b
Some more types fixes (some are backwards).
yapolyak Mar 11, 2024
93e8601
More mypy tweaks.
yapolyak Mar 11, 2024
4b60441
Attempt to silence pytket mypy complains.
yapolyak Mar 11, 2024
6f42404
Merge branch 'develop' into feature/exact_state
PabloAndresCQ May 29, 2024
74a1037
Removed duplicated definition of destroy
PabloAndresCQ May 29, 2024
61995ec
Fixed broken import
PabloAndresCQ Jun 4, 2024
7c46299
Applying changes from comments
PabloAndresCQ Jun 4, 2024
b2d65a4
Fixing slip-up
PabloAndresCQ Jun 4, 2024
53162e4
Removing type ignores from imports
PabloAndresCQ Jun 4, 2024
c6b1c72
Removing more type ignores from imports
PabloAndresCQ Jun 4, 2024
3954fec
Removed invalid import
PabloAndresCQ Jun 4, 2024
26f848a
Moving CuTensorNetHandle to the general.py at the root of the module
PabloAndresCQ Jun 4, 2024
600568d
Fixing mypy complaints: removing top level init files
PabloAndresCQ Jun 4, 2024
7fc94cc
Made use of library handler safer
PabloAndresCQ Jun 4, 2024
f749bfd
Renamed test file
PabloAndresCQ Jun 4, 2024
074192c
Made changes on test
PabloAndresCQ Jun 4, 2024
ee64c55
Removing dead code from example
PabloAndresCQ Jun 4, 2024
1c6f5c9
Removed outdated comment
PabloAndresCQ Jun 5, 2024
2b12bce
Refactor of GeneralState so that it adheres to the upcoming format of…
PabloAndresCQ Jun 6, 2024
0e9526b
Fixed small bugs
PabloAndresCQ Jun 6, 2024
1ab3fe6
Removed _scratch_space and _work_desc attributes from GeneralState, b…
PabloAndresCQ Jun 6, 2024
301c9f0
Updated tests so that libhandle is reused and state.destroy() is call…
PabloAndresCQ Jun 6, 2024
b6108f3
Merge pull request #126 from CQCL/refactor/exact_state
PabloAndresCQ Jun 6, 2024
b8b713c
Applying overall phase on state vector. Copying circuit when passed t…
PabloAndresCQ Jun 7, 2024
9867c46
Fixing bug due to not using local copy of circuit
PabloAndresCQ Jun 7, 2024
d2b6e25
Adding more tests and fixing some issues
PabloAndresCQ Jun 7, 2024
4aeb976
Added a test for expectation values. Changed the name of some tests
PabloAndresCQ Jun 7, 2024
44f7d6b
Allowing complex expectation value
PabloAndresCQ Jun 7, 2024
bd8d512
Updated docs
PabloAndresCQ Jun 7, 2024
608df86
Fixing a complaint from pylint
PabloAndresCQ Jun 7, 2024
dfcdcf3
Updated changelog
PabloAndresCQ Jun 7, 2024
704c9dc
Removed default choice of NUM_HYPER_SAMPLES
PabloAndresCQ Jun 7, 2024
5ef76f5
Adding copyright notice to new files
PabloAndresCQ Jun 7, 2024
7186c63
Adding error message if circuit contains non-unitary gates
PabloAndresCQ Jun 7, 2024
25aa6e7
Boilerplate code for sampler. Implementation not yet ready, but pushi…
PabloAndresCQ Jun 10, 2024
4f56437
Adding minimal test for sampler
PabloAndresCQ Jun 10, 2024
307ab53
Solving minor bugs
PabloAndresCQ Jun 10, 2024
1b00c0b
Now returning an OutcomeArray
PabloAndresCQ Jun 10, 2024
a62594c
Testing sampler against theoretical probabilities
PabloAndresCQ Jun 10, 2024
924ab67
Support for end-of-circuit measurements and sampling on subsets of qu…
PabloAndresCQ Jun 13, 2024
ca73177
Now managing implicit swaps without adding SWAP gates.
PabloAndresCQ Jun 13, 2024
7fbd67b
Added tests for sampler on subset of qubits
PabloAndresCQ Jun 13, 2024
8aefd5e
Updated docs
PabloAndresCQ Jun 13, 2024
44746a7
Added a comment
PabloAndresCQ Jun 14, 2024
9affb56
Merge branch 'main' into feature/general_sampler
PabloAndresCQ Jun 26, 2024
528e0be
black in CI is now happy
PabloAndresCQ Jun 26, 2024
5e8e665
Apply changes to comments
PabloAndresCQ Jun 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/modules/general_state.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ General state (exact) simulation
.. automethod:: __init__
.. automethod:: get_statevector
.. automethod:: expectation_value
.. automethod:: sample
.. automethod:: destroy

cuQuantum `contract` API interface
Expand Down
200 changes: 186 additions & 14 deletions pytket/extensions/cutensornet/general_state/tensor_network_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -24,9 +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
Expand All @@ -44,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:
Expand All @@ -55,23 +58,24 @@ 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.
libhandle: An instance of a ``CuTensorNetHandle``.
loglevel: Internal logger output level.
"""
self._logger = set_logger("GeneralState", loglevel)
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.
self._circuit.replace_implicit_wire_swaps()
self._lib = libhandle

libhandle.print_device_properties(self._logger)

# Remove end-of-circuit measurements and keep track of them separately
# 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
# 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
dim = 2 # We are always dealing with qubits, not qudits
qubits_dims = (dim,) * num_qubits # qubit size
Expand All @@ -93,9 +97,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,
Expand Down Expand Up @@ -286,7 +288,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())

Expand Down Expand Up @@ -401,6 +403,141 @@ def expectation_value(
cutn.destroy_network_operator(tn_operator)
del scratch_space

def sample(
self,
n_shots: int,
attributes: Optional[dict] = None,
scratch_fraction: float = 0.5,
) -> BackendResult:
"""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
# 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._qubit_idx_map[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_measurements,
modes_to_sample=measured_modes,
)

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_measurements, n_shots), dtype="int64", order="F")
cutn.sampler_sample(
self._lib.handle,
sampler,
n_shots,
work_desc,
samples.ctypes.data,
stream.ptr,
)
stream.synchronize()

# 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
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:
#####################################################
# Destroy the Sampler object #
#####################################################
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.

Expand All @@ -422,3 +559,38 @@ 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 _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.
"""
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 to identify mid-circuit measurement
measured_qubits = set()

for command in circ:
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[cmd_qubits[0]] = command.bits[0]
measured_qubits.add(cmd_qubits[0])
else:
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
78 changes: 76 additions & 2 deletions tests/test_general_state.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -204,3 +203,78 @@ 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
],
)
@pytest.mark.parametrize("measure_all", [True, False]) # Measure all or a subset
def test_sampler(circuit: Circuit, measure_all: bool) -> None:

n_shots = 100000

# Get the statevector so that we can calculate theoretical probabilities
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):
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:
state = GeneralState(circuit, libhandle)
results = state.sample(n_shots)

# 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)))

# 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()
Loading