From 7d9ebd1f6bd40facb2f70629749973953eac921f Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:36:39 +0100 Subject: [PATCH 01/13] update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index f159a181..9ea21dd4 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,6 @@ result .venv docs/pyproject.toml docs/poetry.lock +.jupyter_cache/ +jupyter_execute/ From 2fc1277708030af4eb579713e0cdec835a615fe4 Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Thu, 24 Oct 2024 18:14:06 +0100 Subject: [PATCH 02/13] move examples folder into docs folder --- {examples => docs/examples}/README.md | 0 {examples => docs/examples}/check-examples | 0 {examples => docs/examples}/ci-tested-notebooks.txt | 0 docs/examples/general_state_tutorial.ipynb | 1 + {examples => docs/examples}/images/mps.png | Bin {examples => docs/examples}/mpi/README.md | 0 .../examples}/mpi/mpi_overlap_bcast_circ.py | 0 .../examples}/mpi/mpi_overlap_bcast_mps.py | 0 .../examples}/mpi/mpi_overlap_bcast_net.py | 0 docs/examples/mps_tutorial.ipynb | 1 + .../examples}/python/general_state_tutorial.py | 0 {examples => docs/examples}/python/mps_tutorial.py | 0 {examples => docs/examples}/python/ttn_tutorial.py | 0 docs/examples/ttn_tutorial.ipynb | 1 + examples/general_state_tutorial.ipynb | 1 - examples/mps_tutorial.ipynb | 1 - examples/ttn_tutorial.ipynb | 1 - 17 files changed, 3 insertions(+), 3 deletions(-) rename {examples => docs/examples}/README.md (100%) rename {examples => docs/examples}/check-examples (100%) rename {examples => docs/examples}/ci-tested-notebooks.txt (100%) create mode 100644 docs/examples/general_state_tutorial.ipynb rename {examples => docs/examples}/images/mps.png (100%) rename {examples => docs/examples}/mpi/README.md (100%) rename {examples => docs/examples}/mpi/mpi_overlap_bcast_circ.py (100%) rename {examples => docs/examples}/mpi/mpi_overlap_bcast_mps.py (100%) rename {examples => docs/examples}/mpi/mpi_overlap_bcast_net.py (100%) create mode 100644 docs/examples/mps_tutorial.ipynb rename {examples => docs/examples}/python/general_state_tutorial.py (100%) rename {examples => docs/examples}/python/mps_tutorial.py (100%) rename {examples => docs/examples}/python/ttn_tutorial.py (100%) create mode 100644 docs/examples/ttn_tutorial.ipynb delete mode 100644 examples/general_state_tutorial.ipynb delete mode 100644 examples/mps_tutorial.ipynb delete mode 100644 examples/ttn_tutorial.ipynb diff --git a/examples/README.md b/docs/examples/README.md similarity index 100% rename from examples/README.md rename to docs/examples/README.md diff --git a/examples/check-examples b/docs/examples/check-examples similarity index 100% rename from examples/check-examples rename to docs/examples/check-examples diff --git a/examples/ci-tested-notebooks.txt b/docs/examples/ci-tested-notebooks.txt similarity index 100% rename from examples/ci-tested-notebooks.txt rename to docs/examples/ci-tested-notebooks.txt diff --git a/docs/examples/general_state_tutorial.ipynb b/docs/examples/general_state_tutorial.ipynb new file mode 100644 index 00000000..a4d0e137 --- /dev/null +++ b/docs/examples/general_state_tutorial.ipynb @@ -0,0 +1 @@ +{"cells":[{"cell_type":"markdown","metadata":{},"source":["# `GeneralState` Tutorial"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import numpy as np\n","from sympy import Symbol\n","from scipy.stats import unitary_group # type: ignore\n","from pytket.circuit import Circuit, OpType, Unitary2qBox, Qubit, Bit\n","from pytket.passes import DecomposeBoxes\n","from pytket.utils import QubitPauliOperator\n","from pytket._tket.pauli import Pauli, QubitPauliString\n","from pytket.circuit.display import render_circuit_jupyter"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.cutensornet.general_state import (\n"," GeneralState,\n"," GeneralBraOpKet,\n",")\n","from pytket.extensions.cutensornet.backends import CuTensorNetShotsBackend"]},{"cell_type":"markdown","metadata":{},"source":["## Introduction
\n","This notebook is a guide on how to use the features provided in the `general_state` submodule of pytket-cutensornet. This submodule is a thin wrapper of CuTensorNet's `NetworkState`, allowing users to convert pytket circuits into tensor networks and use CuTensorNet's contraction path optimisation algorithm.
\n","All simulations realised with this submodule are *exact*. Once the pytket circuit has been converted to a tensor network, the computation has two steps:
\n"," 1. *Contraction path optimisation*. Attempts to find an order of contracting pairs of tensors in which the the total number of FLOPs is minimised. No operation on the tensor network occurs at this point. Runs on CPU.
\n"," 2. *Tensor network contraction*. Uses the ordering of contractions found in the previous step evaluate the tensor network. Runs on GPU.
\n","
\n","**Reference**: The original contraction path optimisation algorithm that NVIDIA implemented on CuTensorNet: https://arxiv.org/abs/2002.01935"]},{"cell_type":"markdown","metadata":{},"source":["## `GeneralState`
\n","The class `GeneralState` is used to convert a circuit into a tensor network and query information from the final state. Let's walk through a simple example."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["my_circ = Circuit(5)\n","my_circ.CX(3, 4)\n","my_circ.H(2)\n","my_circ.CZ(0, 1)\n","my_circ.ZZPhase(0.1, 4, 3)\n","my_circ.TK2(0.3, 0.5, 0.7, 2, 1)\n","my_circ.Ry(0.2, 0)\n","my_circ.measure_all()"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(my_circ)"]},{"cell_type":"markdown","metadata":{},"source":["The first step is to convert our pytket circuit into a tensor network. This is straightforward:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["tn_state = GeneralState(my_circ)"]},{"cell_type":"markdown","metadata":{},"source":["The variable `tn_state` now holds a tensor network representation of `my_circ`.
\n","**Note**: Circuits must not have mid-circuit measurements or classical logic. The measurements at the end of the circuit are stripped and only considered when calling `tn_state.sample(n_shots)`.
\n","We can now query information from the state. For instance, let's calculate the probability of in the qubits 0 and 3 agreeing in their outcome."]},{"cell_type":"markdown","metadata":{},"source":["First, let's generate `|x>` computational basis states where `q[0]` and `q[3]` agree on their values. We can do this with some bitwise operators and list comprehension.
\n","**Note**: Remember that pytket uses \"increasing lexicographic order\" (ILO) for qubits, so `q[0]` is the most significant bit."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["selected_states = [\n"," x\n"," for x in range(2**my_circ.n_qubits)\n"," if ( # Iterate over all possible states\n"," x & int(\"10000\", 2) == 0\n"," and x & int(\"00010\", 2) == 0 # both qubits are 0 or...\n"," or x & int(\"10000\", 2) != 0\n"," and x & int(\"00010\", 2) != 0 # both qubits are 1\n"," )\n","]"]},{"cell_type":"markdown","metadata":{},"source":["We can now query the amplitude of all of these states and calculate the probability by summing their squared absolute values."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["amplitudes = []\n","for x in selected_states:\n"," amplitudes.append(tn_state.get_amplitude(x))\n","probability = sum(abs(a) ** 2 for a in amplitudes)\n","print(f\"Probability: {probability}\")"]},{"cell_type":"markdown","metadata":{},"source":["Of course, calculating probabilities by considering the amplitudes of all relevant states is not efficient in general, since we may need to calculate a number of amplitudes that scales exponentially with the number of qubits. An alternative is to use expectation values. In particular, all of the states in `selected_states` are +1 eigenvectors of the `ZIIZI` observable and, hence, we can calculate the probability `p` by solving the equation ` = (+1)p + (-1)(1-p)` using the fact that `ZIIZI` only has +1 and -1 eigenvalues."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["string_ZIIZI = QubitPauliString(\n"," my_circ.qubits, [Pauli.Z, Pauli.I, Pauli.I, Pauli.Z, Pauli.I]\n",")\n","observable = QubitPauliOperator({string_ZIIZI: 1.0})\n","expectation_val = tn_state.expectation_value(observable).real\n","exp_probability = (expectation_val + 1) / 2\n","assert np.isclose(probability, exp_probability, atol=0.0001)\n","print(f\"Probability: {exp_probability}\")"]},{"cell_type":"markdown","metadata":{},"source":["Alternatively, we can estimate the probability by sampling."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_shots = 100000\n","outcomes = tn_state.sample(n_shots)\n","hit_count = 0\n","for bit_tuple, count in outcomes.get_counts().items():\n"," if bit_tuple[0] == bit_tuple[3]:\n"," hit_count += count\n","samp_probability = hit_count / n_shots\n","assert np.isclose(probability, samp_probability, atol=0.01)\n","print(f\"Probability: {samp_probability}\")"]},{"cell_type":"markdown","metadata":{},"source":["When we finish doing computations with the `tn_state` we must destroy it to free GPU memory."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["tn_state.destroy()"]},{"cell_type":"markdown","metadata":{},"source":["To avoid forgetting this final step, we recommend users call `GeneralState` (and `GeneralBraOpKet`) as context managers:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with GeneralState(my_circ) as my_state:\n"," expectation_val = my_state.expectation_value(observable)\n","print(expectation_val)"]},{"cell_type":"markdown","metadata":{},"source":["Using this syntax, `my_state` is automatically destroyed when the code exists the `with ...` block."]},{"cell_type":"markdown","metadata":{},"source":["## Parameterised circuits
\n","Circuits that only differ on the parameters of their gates have the same tensor network topology and, hence, we may use the same contraction path for all of them."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["a, b, c = Symbol(\"a\"), Symbol(\"b\"), Symbol(\"c\")\n","param_circ1 = Circuit(5)\n","param_circ1.Ry(a, 3).Ry(0.27, 4).CX(4, 3).Ry(b, 2).Ry(0.21, 3)\n","param_circ1.Ry(0.12, 0).Ry(a, 1)\n","param_circ1.add_gate(OpType.CnX, [0, 1, 4]).add_gate(OpType.CnX, [4, 1, 3])\n","param_circ1.X(0).X(1).add_gate(OpType.CnY, [0, 1, 2]).add_gate(OpType.CnY, [0, 4, 3]).X(\n"," 0\n",").X(1)\n","param_circ1.Ry(-b, 0).Ry(-c, 1)\n","render_circuit_jupyter(param_circ1)"]},{"cell_type":"markdown","metadata":{},"source":["We can pass a parameterised circuit to `GeneralState`. The value of the parameters is provided when calling methods of `GeneralState`. The contraction path is automatically reused on different calls to the same method."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_circs = 5\n","with GeneralState(param_circ1) as param_state:\n"," for i in range(n_circs):\n"," symbol_map = {s: np.random.random() for s in [a, b, c]}\n"," exp_val = param_state.expectation_value(observable, symbol_map=symbol_map)\n"," print(f\"Expectation value for circuit {i}: {exp_val.real}\")"]},{"cell_type":"markdown","metadata":{},"source":["## `GeneralBraOpKet`
\n","\n","The `GeneralBraOpKet` can be used to calculate any number that can be represented as the result of some `` where `|bra>` and `|ket>` are the final states of pytket circuits, and `op` is a `QubitPauliOperator`. The circuits for `|bra>` and `|ket>` need not be the same."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["x, y, z = Symbol(\"x\"), Symbol(\"y\"), Symbol(\"z\")\n","param_circ2 = Circuit(5)\n","param_circ2.H(0)\n","param_circ2.S(1)\n","param_circ2.Rz(x * z, 2)\n","param_circ2.Ry(y + x, 3)\n","param_circ2.TK1(x, y, z, 4)\n","param_circ2.TK2(z - y, z - x, (x + y) * z, 1, 3)\n","symbol_map = {a: 2.1, b: 1.3, c: 0.7, x: 3.0, y: 1.6, z: -8.3}"]},{"cell_type":"markdown","metadata":{},"source":["We can calculate inner products by providing no `op`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with GeneralBraOpKet(bra=param_circ2, ket=param_circ1) as braket:\n"," inner_prod = braket.contract(symbol_map=symbol_map)\n","with GeneralBraOpKet(bra=param_circ1, ket=param_circ2) as braket:\n"," inner_prod_conj = braket.contract(symbol_map=symbol_map)\n","assert np.isclose(np.conj(inner_prod), inner_prod_conj)\n","print(f\" = {inner_prod}\")\n","print(f\" = {inner_prod_conj}\")"]},{"cell_type":"markdown","metadata":{},"source":["And we are not constrained to Hermitian operators:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["string_XZIXX = QubitPauliString(\n"," param_circ2.qubits, [Pauli.X, Pauli.Z, Pauli.I, Pauli.X, Pauli.X]\n",")\n","string_IZZYX = QubitPauliString(\n"," param_circ2.qubits, [Pauli.I, Pauli.Z, Pauli.Z, Pauli.Y, Pauli.X]\n",")\n","string_ZIZXY = QubitPauliString(\n"," param_circ2.qubits, [Pauli.Z, Pauli.I, Pauli.Z, Pauli.X, Pauli.Y]\n",")\n","operator = QubitPauliOperator(\n"," {string_XZIXX: -1.38j, string_IZZYX: 2.36, string_ZIZXY: 0.42j + 0.3}\n",")\n","with GeneralBraOpKet(bra=param_circ2, ket=param_circ1) as braket:\n"," value = braket.contract(operator, symbol_map=symbol_map)\n","print(value)"]},{"cell_type":"markdown","metadata":{},"source":["## Backends
\n","We provide a pytket `Backend` to obtain shots using `GeneralState`."]},{"cell_type":"markdown","metadata":{},"source":["Let's consider a more challenging circuit"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def random_circuit(n_qubits: int, n_layers: int) -> Circuit:\n"," \"\"\"Random quantum volume circuit.\"\"\"\n"," c = Circuit(n_qubits, n_qubits)\n"," for _ in range(n_layers):\n"," qubits = np.random.permutation([i for i in range(n_qubits)])\n"," qubit_pairs = [[qubits[i], qubits[i + 1]] for i in range(0, n_qubits - 1, 2)]\n"," for pair in qubit_pairs:\n"," # Generate random 4x4 unitary matrix.\n"," SU4 = unitary_group.rvs(4) # random unitary in SU4\n"," SU4 = SU4 / (np.linalg.det(SU4) ** 0.25)\n"," SU4 = np.matrix(SU4)\n"," c.add_unitary2qbox(Unitary2qBox(SU4), *pair)\n"," DecomposeBoxes().apply(c)\n"," return c"]},{"cell_type":"markdown","metadata":{},"source":["Let's measure only three of the qubits.
\n","**Note**: The complexity of this simulation increases exponentially with the number of qubits measured. Other factors leading to intractability are circuit depth and qubit connectivity."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_shots = 1000\n","quantum_vol_circ = random_circuit(n_qubits=40, n_layers=5)\n","quantum_vol_circ.Measure(Qubit(0), Bit(0))\n","quantum_vol_circ.Measure(Qubit(1), Bit(1))\n","quantum_vol_circ.Measure(Qubit(2), Bit(2))"]},{"cell_type":"markdown","metadata":{},"source":["The `CuTensorNetShotsBackend` is used in the same way as any other pytket `Backend`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["backend = CuTensorNetShotsBackend()\n","compiled_circ = backend.get_compiled_circuit(quantum_vol_circ)\n","results = backend.run_circuit(compiled_circ, n_shots=n_shots)\n","print(results.get_counts())"]}],"metadata":{"kernelspec":{"display_name":".venv","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.1"}},"nbformat":4,"nbformat_minor":2} diff --git a/examples/images/mps.png b/docs/examples/images/mps.png similarity index 100% rename from examples/images/mps.png rename to docs/examples/images/mps.png diff --git a/examples/mpi/README.md b/docs/examples/mpi/README.md similarity index 100% rename from examples/mpi/README.md rename to docs/examples/mpi/README.md diff --git a/examples/mpi/mpi_overlap_bcast_circ.py b/docs/examples/mpi/mpi_overlap_bcast_circ.py similarity index 100% rename from examples/mpi/mpi_overlap_bcast_circ.py rename to docs/examples/mpi/mpi_overlap_bcast_circ.py diff --git a/examples/mpi/mpi_overlap_bcast_mps.py b/docs/examples/mpi/mpi_overlap_bcast_mps.py similarity index 100% rename from examples/mpi/mpi_overlap_bcast_mps.py rename to docs/examples/mpi/mpi_overlap_bcast_mps.py diff --git a/examples/mpi/mpi_overlap_bcast_net.py b/docs/examples/mpi/mpi_overlap_bcast_net.py similarity index 100% rename from examples/mpi/mpi_overlap_bcast_net.py rename to docs/examples/mpi/mpi_overlap_bcast_net.py diff --git a/docs/examples/mps_tutorial.ipynb b/docs/examples/mps_tutorial.ipynb new file mode 100644 index 00000000..21c30098 --- /dev/null +++ b/docs/examples/mps_tutorial.ipynb @@ -0,0 +1 @@ +{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Matrix Product State (MPS) Tutorial"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import numpy as np\n","from time import time\n","import matplotlib.pyplot as plt\n","from pytket import Circuit, OpType\n","from pytket.circuit.display import render_circuit_jupyter"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.cutensornet.structured_state import (\n"," CuTensorNetHandle,\n"," Config,\n"," SimulationAlgorithm,\n"," simulate,\n"," prepare_circuit_mps,\n",")"]},{"cell_type":"markdown","metadata":{},"source":["## Introduction
\n","This notebook provides examples of the usage of the MPS functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.
\n","A Matrix Product State (MPS) represents a state on `n` qubits as a list of `n` tensors connected in a line as show below:
\n","![MPS](images/mps.png)
\n","Each of these circles corresponds to a tensor. We refer to each leg of a tensor as a *bond* and the number of bonds a tensor has is its *rank*. In code, a tensor is just a multidimensional array:
\n","
\n","```tensor[i][j][k] = v```
\n","
\n","In the case above, we are assigning an entry value `v` of a rank-3 tensor (one `[ ]` coordinate per bond). Each bond allows a different number of values for its indices; for instance `0 <= i < 4` would mean that the first bond of our tensor can take up to four different indices; we refer to this as the *dimension* of the bond. We refer to the bonds connecting different tensors in the MPS as *virtual bonds*; the maximum allowed value for the dimension of virtual bonds is often denoted by the greek letter `chi`. The open bonds are known as *physical bonds* and, in our case, each will correspond to a qubit; hence, they have dimension `2` -- the dimension of the vector space of a single qubit.
\n","In essence, whenever we want to apply a gate to certain qubit we will connect a tensor (matrix) representing the gate to the corresponding physical bond and *contract* the network back to an MPS form (tensor contraction is a generalisation of matrix multiplication to multidimensional arrays). Whenever a two-qubit gate is applied, the entanglement information after contraction will be kept in the degrees of freedom of the virtual bonds. As such, the dimension of the virtual bonds will generally increase exponentially as we apply entangling gates, leading to large memory footprints of the tensors and, consequently, long runtime for tensor contraction. We provide functionalities to limit the growth of the dimension of the virtual bonds, keeping resource consumption in check. Read the *Approximate simulation* section on this notebook to learn more.
\n","**References**: To read more about MPS we recommend the following papers.
\n","* For an introduction to MPS and its canonical form: https://arxiv.org/abs/1901.05824.
\n","* For a description of the `MPSxGate` algorithm we provide: https://arxiv.org/abs/2002.07730.
\n","* For a description of the `MPSxMPO` algorithm we provide: https://arxiv.org/abs/2207.05612.
\n","* For insights on the reationship between truncation error and the error model in a quantum computer: https://arxiv.org/abs/2004.02388"]},{"cell_type":"markdown","metadata":{},"source":["## Basic functionality and exact simulation
\n","Here we show an example of the basic use of our MPS methods. We first generate a simple `pytket` circuit to be simulated."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["my_circ = Circuit(5)\n","my_circ.CX(3, 4)\n","my_circ.H(2)\n","my_circ.CZ(0, 1)\n","my_circ.ZZPhase(0.1, 4, 3)\n","my_circ.TK2(0.3, 0.5, 0.7, 2, 1)\n","my_circ.Ry(0.2, 0)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(my_circ)"]},{"cell_type":"markdown","metadata":{},"source":["For **exact** simulation, simply call the `simulate` function on the circuit and choose a contraction algorithm. To learn more about the contraction algorithms we provide see the *Contraction algorithms* section of this notebook. You will also need to provide a configuration, the default one is provided by `Config()`. Custom settings of `Config` are discussed in the *Approximate simulation* section.
\n","**NOTE**: whenever you wish to generate an `MPS` object or execute calculations on it you must do so within a `with CuTensorNetHandle() as libhandle:` block; this will initialise the cuTensorNetwork library for you, and destroy its handles at the end of the `with` block. You will need to pass the `libhandle` to the `MPS` object via the method that generates it (in the snippet below, `simulate`), or if already initialised, pass it via the `update_libhandle` method.
\n","Due to the nature of Jupyter notebooks, we will be starting most of these cells with a `with CuTensorNetHandle() as libhandle:`. However, in a standard script, all of these cells would be grouped together and a single `with CuTensorNetHandle() as libhandle:` statement would be necessary at the beginning of the script."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," my_mps = simulate(libhandle, my_circ, SimulationAlgorithm.MPSxGate, Config())"]},{"cell_type":"markdown","metadata":{},"source":["Notice that `my_circ` uses a rich gateset -- in fact, every single-qubit and two-qubit gate supported by `pytket` can be used in our MPS approaches. Gates acting on more than two qubits are not currently supported."]},{"cell_type":"markdown","metadata":{},"source":["The output of `simulate` is an `MPS` object encoding the output state of the circuit.
\n","### Obtain an amplitude from an MPS
\n","Let's first see how to get the amplitude of the state `|10100>` from the output of the previous circuit."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["state = int(\"10100\", 2)\n","with CuTensorNetHandle() as libhandle:\n"," my_mps.update_libhandle(libhandle)\n"," amplitude = my_mps.get_amplitude(state)\n","print(amplitude)"]},{"cell_type":"markdown","metadata":{},"source":["Since this is a very small circuit, we can use `pytket`'s state vector simulator capabilities to verify that the state is correct by checking the amplitude of each of the computational states."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["state_vector = my_circ.get_statevector()\n","n_qubits = len(my_circ.qubits)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["correct_amplitude = [False] * (2**n_qubits)\n","with CuTensorNetHandle() as libhandle:\n"," my_mps.update_libhandle(libhandle)\n"," for i in range(2**n_qubits):\n"," correct_amplitude[i] = np.isclose(state_vector[i], my_mps.get_amplitude(i))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(\"Are all amplitudes correct?\")\n","print(all(correct_amplitude))"]},{"cell_type":"markdown","metadata":{},"source":["### Sampling from an MPS
\n","We can also sample from the output state of a circuit by calling `my_mps.sample`, where `my_mps` is the outcome of simulating the circuit."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_samples = 100\n","n_qubits = len(my_circ.qubits)"]},{"cell_type":"markdown","metadata":{},"source":["Initialise the sample counter"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["sample_count = [0 for _ in range(2**n_qubits)]"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," my_mps.update_libhandle(libhandle)\n"," for _ in range(n_samples):\n"," # Draw a sample\n"," qubit_outcomes = my_mps.sample()\n"," # Convert qubit outcomes to bitstring\n"," bitstring = \"\".join(str(qubit_outcomes[q]) for q in my_circ.qubits)\n"," # Convert bitstring to int\n"," outcome = int(bitstring, 2)\n"," # Update the sample dictionary\n"," sample_count[outcome] += 1"]},{"cell_type":"markdown","metadata":{},"source":["Calculate the theoretical number of samples per bitstring"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["expected_count = [n_samples * abs(state_vector[i]) ** 2 for i in range(2**n_qubits)]"]},{"cell_type":"markdown","metadata":{},"source":["Plot a comparison of theory vs sampled"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["plt.scatter(range(2**n_qubits), expected_count, label=\"Theory\")\n","plt.scatter(range(2**n_qubits), sample_count, label=\"Experiment\", marker=\"x\")\n","plt.xlabel(\"Basis states\")\n","plt.ylabel(\"Samples\")\n","plt.legend()\n","plt.show()"]},{"cell_type":"markdown","metadata":{},"source":["We also provide methods to apply mid-circuit measurements via `my_mps.measure(qubits)` and postselection via `my_mps.postselect(qubit_outcomes)`. Their use is similar to that of `my_mps.sample()` shown above.
\n","**Note:** whereas `my_mps.sample()` does *not* change the state of the MPS, `my_mps.measure(qubits)` and `my_mps.postselect(qubit_outcomes)` do change it, projecting the state to the resulting outcome and removing the measured qubits."]},{"cell_type":"markdown","metadata":{},"source":["### Inner products
\n","Using `vdot` you can obtain the inner product of two states in MPS form. This method does not change the internal data of neither of the MPS. Moreover, it can be used on the same `MPS` object for both inputs, yielding the squared norm of the state."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," my_mps.update_libhandle(libhandle)\n"," norm_sq = my_mps.vdot(my_mps)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(\"As expected, the squared norm of a state is 1\")\n","print(np.isclose(norm_sq, 1))"]},{"cell_type":"markdown","metadata":{},"source":["Let's come up with another circuit on the same qubits and apply an inner product between the two `MPS` objects."]},{"cell_type":"markdown","metadata":{},"source":["Generate circuits"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["other_circ = Circuit(5)\n","other_circ.H(3)\n","other_circ.CZ(3, 4)\n","other_circ.XXPhase(0.3, 1, 2)\n","other_circ.Ry(0.7, 3)"]},{"cell_type":"markdown","metadata":{},"source":["Simulate them"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," other_mps = simulate(libhandle, other_circ, SimulationAlgorithm.MPSxGate, Config())"]},{"cell_type":"markdown","metadata":{},"source":["Let's calculate the inner product and check that it agrees with `pytket`'s state vector based computation."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," my_mps.update_libhandle(libhandle)\n"," inner_product = my_mps.vdot(other_mps)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["my_state = my_circ.get_statevector()\n","other_state = other_circ.get_statevector()"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(\"Is the inner product correct?\")\n","print(np.isclose(np.vdot(my_state, other_state), inner_product))"]},{"cell_type":"markdown","metadata":{},"source":["### Mid-circuit measurements and classical control
\n","Mid-circuit measurements and classical control is supported (only in `MPSxGate` as of v0.8.0). For instance, we can implement the teleportation protocol on a pytket circuit and simulate it:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ = Circuit()\n","alice = circ.add_q_register(\"alice\", 2)\n","alice_bits = circ.add_c_register(\"alice_bits\", 2)\n","bob = circ.add_q_register(\"bob\", 1)\n","# Initialise Alice's first qubit in some arbitrary state\n","circ.Rx(0.42, alice[0])\n","orig_state = circ.get_statevector()\n","# Create a Bell pair shared between Alice and Bob\n","circ.H(alice[1]).CX(alice[1], bob[0])\n","# Apply a Bell measurement on Alice's qubits\n","circ.CX(alice[0], alice[1]).H(alice[0])\n","circ.Measure(alice[0], alice_bits[0])\n","circ.Measure(alice[1], alice_bits[1])\n","# Apply conditional corrections on Bob's qubits\n","circ.add_gate(OpType.X, [bob[0]], condition_bits=[alice_bits[1]], condition_value=1)\n","circ.add_gate(OpType.Z, [bob[0]], condition_bits=[alice_bits[0]], condition_value=1)\n","# Reset Alice's qubits\n","circ.add_gate(OpType.Reset, [alice[0]])\n","circ.add_gate(OpType.Reset, [alice[1]])\n","# Display the circuit\n","render_circuit_jupyter(circ)"]},{"cell_type":"markdown","metadata":{},"source":["We can now simulate the circuit and check that the qubit has been successfully teleported."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(\n"," f\"Initial state:\\n {np.round(orig_state[0],2)}|00>|0> + {np.round(orig_state[4],2)}|10>|0>\"\n",")\n","with CuTensorNetHandle() as libhandle:\n"," state = simulate(libhandle, circ, SimulationAlgorithm.MPSxGate, Config())\n"," print(\n"," f\"Teleported state:\\n {np.round(state.get_amplitude(0),2)}|00>|0> + {np.round(state.get_amplitude(1),2)}|00>|1>\"\n"," )\n"," print(f\"Measurement outcomes:\\n {state.get_bits()}\")"]},{"cell_type":"markdown","metadata":{},"source":["### Two-qubit gates acting on non-adjacent qubits
\n","Standard MPS algorithms only support simulation of two-qubit gates acting on neighbour qubits. In our implementation, however, two-qubit gates between arbitrary qubits may be applied, as shown below."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ = Circuit(5)\n","circ.H(1)\n","circ.ZZPhase(0.3, 1, 3)\n","circ.CX(0, 2)\n","circ.Ry(0.8, 4)\n","circ.CZ(3, 4)\n","circ.XXPhase(0.7, 1, 2)\n","circ.TK2(0.1, 0.2, 0.4, 1, 4)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(circ)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," mps = simulate(libhandle, circ, SimulationAlgorithm.MPSxGate, Config())\n"," print(\"Did simulation succeed?\")\n"," print(mps.is_valid())"]},{"cell_type":"markdown","metadata":{},"source":["**Note**: Even though two-qubit gates on non-adjacent qubits are simulable, the overhead on these is considerably larger than simulating gates on adjacent qubits. As a rule of thumb if the two qubits are `n` positions apart, the overhead is upper bounded by the cost of simulating `n-1` additional SWAP gates to move the leftmost qubit near the rightmost. In reality, the implementation we use is more nuanced than just applying SWAP gates, and the qubits don't actually change position.
\n","When circuits are shallow, using our approach to simulate long-distance two-qubit gates is advantageous. In the case of deep circuits with many long-distance gates, it is sometimes beneficial to use TKET routing on the circuit, explicitly adding SWAP gates so that all two-qubit gates act on nearest neighbour qubits. Users may do this by calling `prepare_circuit_mps`, which is a wrapper of the corresponding TKET routing pass."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["prepared_circ, qubit_map = prepare_circuit_mps(circ)\n","render_circuit_jupyter(prepared_circ)"]},{"cell_type":"markdown","metadata":{},"source":["The circuit can now be simulated as usual."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," mps = simulate(libhandle, prepared_circ, SimulationAlgorithm.MPSxGate, Config())\n"," print(\"Did simulation succeed?\")\n"," print(mps.is_valid())"]},{"cell_type":"markdown","metadata":{},"source":["Notice that the qubits in the `prepared_circ` were renamed when applying `prepare_circuit_mps`. Implicit SWAPs may have been added to the circuit, meaning that the logical qubit held at the ``node[i]`` qubit at the beginning of the circuit may differ from the one it holds at the end; this information is captured by the `qubit_map` output. We recommend applying ``apply_qubit_relabelling`` on the MPS after simulation, relabelling the qubits according to these implicit SWAPs."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(qubit_map)\n","mps.apply_qubit_relabelling(qubit_map)"]},{"cell_type":"markdown","metadata":{},"source":["## Approximate simulation
\n","We provide two policies for approximate simulation; these are supported by both of our current MPS contraction algorithms:
\n","* Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes.
\n","* Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate.
\n","Values for `chi` and `truncation_fidelity` can be set via `Config`. To showcase approximate simulation, let's define a circuit where exact MPS contraction starts struggling."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def random_line_circuit(n_qubits: int, layers: int) -> Circuit:\n"," \"\"\"Random circuit with line connectivity.\"\"\"\n"," c = Circuit(n_qubits)\n"," for i in range(layers):\n"," # Layer of TK1 gates\n"," for q in range(n_qubits):\n"," c.TK1(np.random.rand(), np.random.rand(), np.random.rand(), q)\n","\n"," # Layer of CX gates\n"," offset = np.mod(i, 2) # Even layers connect (q0,q1), odd (q1,q2)\n"," qubit_pairs = [\n"," [c.qubits[i], c.qubits[i + 1]] for i in range(offset, n_qubits - 1, 2)\n"," ]\n"," # Direction of each CX gate is random\n"," for pair in qubit_pairs:\n"," np.random.shuffle(pair)\n"," for pair in qubit_pairs:\n"," c.CX(pair[0], pair[1])\n"," return c"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circuit = random_line_circuit(n_qubits=20, layers=20)"]},{"cell_type":"markdown","metadata":{},"source":["For exact contraction, `chi` must be allowed to be up to `2**(n_qubits // 2)`, meaning that if we set `n_qubits = 20` it would require `chi = 1024`; already too much for this particular circuit to be simulated in a gaming laptop using the current implementation. Instead, let's bound `chi` to a maximum of `16`. Doing so results in faster runtime, at the expense of losing output state fidelity."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(chi=16)\n"," bound_chi_mps = simulate(libhandle, circuit, SimulationAlgorithm.MPSxGate, config)\n","end = time()\n","print(\"Time taken by approximate contraction with bound chi:\")\n","print(f\"{round(end-start,2)} seconds\")\n","print(\"\\nLower bound of the fidelity:\")\n","print(round(bound_chi_mps.fidelity, 4))"]},{"cell_type":"markdown","metadata":{},"source":["Alternatively, we can fix `truncation_fidelity` and let `chi` increase as necessary to satisfy it."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(truncation_fidelity=0.999)\n"," fixed_fidelity_mps = simulate(\n"," libhandle, circuit, SimulationAlgorithm.MPSxGate, config\n"," )\n","end = time()\n","print(\"Time taken by approximate contraction with fixed truncation fidelity:\")\n","print(f\"{round(end-start,2)} seconds\")\n","print(\"\\nLower bound of the fidelity:\")\n","print(round(fixed_fidelity_mps.fidelity, 4))"]},{"cell_type":"markdown","metadata":{},"source":["## Contraction algorithms"]},{"cell_type":"markdown","metadata":{},"source":["We currently offer two MPS-based simulation algorithms:
\n","* **MPSxGate**: Apply gates one by one to the MPS, canonicalising the MPS and truncating when necessary. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2002.07730.
\n","* **MPSxMPO**: Maintain two MPS copies of the state as it evolves, one updated eagerly using the **MPSxGate** method and the other updated in batches of up to `k` layers of two-qubit gates. Whenever the second MPS is updated, both copies are synchronised and an optimisation algorithm is applied to increase the fidelity of the state. This algorithm is often referred to as DMRG-like simulation. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2207.05612.
\n","The `MPSxGate` algorithm is the one we have been using for all of the examples above. In comparison, the `MPSxMPO` algorithm provides the user with two new parameters to tune:
\n","* **k**: The maximum number of layers the MPO is allowed to have before being contracted. Increasing this might increase fidelity, but it will also increase resource requirements exponentially. Default value is `4`.
\n","* **optim_delta**: Stopping criteria for the optimisation when contracting the `k` layers of MPO. Stops when the increase of fidelity between iterations is smaller than `optim_delta`. Default value is `1e-5`.
\n","Both `k` and `optim_delta` can be set via `Config`. Below we compare `MPSxGate` versus `MPSxMPO` with default parameters and `MPSxMPO` with more resource-hungry parameters. The circuit used is the same as in the previous section."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(chi=16)\n"," fixed_fidelity_mps = simulate(\n"," libhandle, circuit, SimulationAlgorithm.MPSxGate, config\n"," )\n","end = time()\n","print(\"MPSxGate\")\n","print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n","print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(chi=16)\n"," fixed_fidelity_mps = simulate(\n"," libhandle, circuit, SimulationAlgorithm.MPSxMPO, config\n"," )\n","end = time()\n","print(\"MPSxMPO, default parameters\")\n","print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n","print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(k=8, optim_delta=1e-15, chi=16)\n"," fixed_fidelity_mps = simulate(\n"," libhandle, circuit, SimulationAlgorithm.MPSxMPO, config\n"," )\n","end = time()\n","print(\"MPSxMPO, custom parameters\")\n","print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n","print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]},{"cell_type":"markdown","metadata":{},"source":["**Note**: `MPSxMPO` also admits truncation policy in terms of `truncation_fidelity` instead of `chi`."]},{"cell_type":"markdown","metadata":{},"source":["## Using the logger"]},{"cell_type":"markdown","metadata":{},"source":["You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent):
\n","- `logging.INFO` will print information about progress percent, memory currently occupied by the MPS and current fidelity. Additionally, some high level information of the current stage of the simulation is provided, such as when `MPSxMPO` is applying optimisation sweeps.
\n","- `logging.DEBUG` provides all of the messages from the loglevel above plus detailed information of the current operation being carried out and the values of important variables.
\n","**Note**: Due to technical issues with the `logging` module and Jupyter notebooks we need to reload the `logging` module. When working with python scripts and command line, just doing `import logging` is enough."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from importlib import reload # Not needed in Python 2\n","import logging"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["reload(logging)"]},{"cell_type":"markdown","metadata":{},"source":["An example of the use of `logging.INFO` is provided below."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," config = Config(truncation_fidelity=0.999, loglevel=logging.INFO)\n"," simulate(libhandle, circuit, SimulationAlgorithm.MPSxMPO, config)"]}],"metadata":{"kernelspec":{"display_name":".venv","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.1"}},"nbformat":4,"nbformat_minor":2} diff --git a/examples/python/general_state_tutorial.py b/docs/examples/python/general_state_tutorial.py similarity index 100% rename from examples/python/general_state_tutorial.py rename to docs/examples/python/general_state_tutorial.py diff --git a/examples/python/mps_tutorial.py b/docs/examples/python/mps_tutorial.py similarity index 100% rename from examples/python/mps_tutorial.py rename to docs/examples/python/mps_tutorial.py diff --git a/examples/python/ttn_tutorial.py b/docs/examples/python/ttn_tutorial.py similarity index 100% rename from examples/python/ttn_tutorial.py rename to docs/examples/python/ttn_tutorial.py diff --git a/docs/examples/ttn_tutorial.ipynb b/docs/examples/ttn_tutorial.ipynb new file mode 100644 index 00000000..6e6fdb20 --- /dev/null +++ b/docs/examples/ttn_tutorial.ipynb @@ -0,0 +1 @@ +{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Tree Tensor Network Tutotial"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import numpy as np\n","from time import time\n","import matplotlib.pyplot as plt\n","import networkx as nx\n","from pytket import Circuit\n","from pytket.circuit.display import render_circuit_jupyter"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.cutensornet.structured_state import (\n"," CuTensorNetHandle,\n"," Config,\n"," SimulationAlgorithm,\n"," simulate,\n",")"]},{"cell_type":"markdown","metadata":{},"source":["## Introduction
\n","This notebook provides examples of the usage of the TTN functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.
\n","Some good references to learn about Tree Tensor Network state simulation:
\n","- For an introduction into TTN based simulation of quantum circuits: https://arxiv.org/abs/2206.01000
\n","- For an introduction on some of the optimisation concerns that are relevant to TTN: https://arxiv.org/abs/2209.03196
\n","The implementation in pytket-cutensornet differs from previously published literature. I am still experimenting with the algorithm. I intend to write up a document detailing the approach, once I reach a stable version.
\n","The main advantage of TTN over MPS is that it can be used to efficiently simulate circuits with richer qubit connectivity. This does **not** mean that TTN has an easy time simulating all-to-all connectivity, but it is far more flexible than MPS. TTN's strength is in simulating circuit where certain subsets of qubits interact densely with each other, and there is not that many gates acting on qubits in different subsets."]},{"cell_type":"markdown","metadata":{},"source":["## How to use
\n","The interface for TTN matches that of MPS. As such, you should be able to run any code that uses `SimulationAlgorithm.MPSxGate` by replacing it with `SimulationAlgorithm.TTNxGate`. Calling `prepare_circuit_mps` is no longer necessary, since `TTNxGate` can apply gates between non-neighbouring qubits.
\n","**NOTE**: If you are new to pytket-cutensornet, it is highly recommended to start reading the `mps_tutorial.ipynb` notebook instead. More details about the use of the library are discussed there (for instance, why and when to call `CuTensorNetHandle()`)."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def random_graph_circuit(n_qubits: int, edge_prob: float, layers: int) -> Circuit:\n"," \"\"\"Random circuit with qubit connectivity determined by a random graph.\"\"\"\n"," c = Circuit(n_qubits)\n"," for i in range(layers):\n"," # Layer of TK1 gates\n"," for q in range(n_qubits):\n"," c.TK1(np.random.rand(), np.random.rand(), np.random.rand(), q)\n","\n"," # Layer of CX gates\n"," graph = nx.erdos_renyi_graph(n_qubits, edge_prob, directed=True)\n"," qubit_pairs = list(graph.edges)\n"," for pair in qubit_pairs:\n"," c.CX(pair[0], pair[1])\n"," return c"]},{"cell_type":"markdown","metadata":{},"source":["For **exact** simulation, you can call `simulate` directly, providing the default `Config()`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["simple_circ = random_graph_circuit(n_qubits=10, edge_prob=0.1, layers=1)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," my_ttn = simulate(libhandle, simple_circ, SimulationAlgorithm.TTNxGate, Config())"]},{"cell_type":"markdown","metadata":{},"source":["## Obtain an amplitude from a TTN
\n","Let's first see how to get the amplitude of the state `|10100>` from the output of the previous circuit."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["state = int(\"10100\", 2)\n","with CuTensorNetHandle() as libhandle:\n"," my_ttn.update_libhandle(libhandle)\n"," amplitude = my_ttn.get_amplitude(state)\n","print(amplitude)"]},{"cell_type":"markdown","metadata":{},"source":["Since this is a very small circuit, we can use `pytket`'s state vector simulator capabilities to verify that the state is correct by checking the amplitude of each of the computational states."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["state_vector = simple_circ.get_statevector()\n","n_qubits = len(simple_circ.qubits)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["correct_amplitude = [False] * (2**n_qubits)\n","with CuTensorNetHandle() as libhandle:\n"," my_ttn.update_libhandle(libhandle)\n"," for i in range(2**n_qubits):\n"," correct_amplitude[i] = np.isclose(state_vector[i], my_ttn.get_amplitude(i))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(\"Are all amplitudes correct?\")\n","print(all(correct_amplitude))"]},{"cell_type":"markdown","metadata":{},"source":["## Sampling from a TTN
\n","Sampling and measurement from a TTN state is not currently supported. This will be added in an upcoming release."]},{"cell_type":"markdown","metadata":{},"source":["## Approximate simulation
\n","We provide two policies for approximate simulation:
\n","* Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes.
\n","* Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate.
\n","Values for `chi` and `truncation_fidelity` can be set via `Config`. To showcase approximate simulation, let's define a circuit where exact TTN contraction would not be enough."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circuit = random_graph_circuit(n_qubits=30, edge_prob=0.1, layers=1)"]},{"cell_type":"markdown","metadata":{},"source":["We can simulate it using bounded `chi` as follows:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(chi=64, float_precision=np.float32)\n"," bound_chi_ttn = simulate(libhandle, circuit, SimulationAlgorithm.TTNxGate, config)\n","end = time()\n","print(\"Time taken by approximate contraction with bounded chi:\")\n","print(f\"{round(end-start,2)} seconds\")\n","print(\"\\nLower bound of the fidelity:\")\n","print(round(bound_chi_ttn.fidelity, 4))"]},{"cell_type":"markdown","metadata":{},"source":["Alternatively, we can fix `truncation_fidelity` and let the bond dimension increase as necessary to satisfy it."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(truncation_fidelity=0.99, float_precision=np.float32)\n"," fixed_fidelity_ttn = simulate(\n"," libhandle, circuit, SimulationAlgorithm.TTNxGate, config\n"," )\n","end = time()\n","print(\"Time taken by approximate contraction with fixed truncation fidelity:\")\n","print(f\"{round(end-start,2)} seconds\")\n","print(\"\\nLower bound of the fidelity:\")\n","print(round(fixed_fidelity_ttn.fidelity, 4))"]},{"cell_type":"markdown","metadata":{},"source":["## Contraction algorithms"]},{"cell_type":"markdown","metadata":{},"source":["We currently offer only one TTN-based simulation algorithm.
\n","* **TTNxGate**: Apply gates one by one to the TTN, canonicalising the TTN and truncating when necessary."]},{"cell_type":"markdown","metadata":{},"source":["## Using the logger"]},{"cell_type":"markdown","metadata":{},"source":["You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent):
\n","- `logging.INFO` will print information about progress percent, memory currently occupied by the TTN and current fidelity. Additionally, some high level information of the current stage of the simulation is provided.
\n","- `logging.DEBUG` provides all of the messages from the loglevel above plus detailed information of the current operation being carried out and the values of important variables.
\n","**Note**: Due to technical issues with the `logging` module and Jupyter notebooks we need to reload the `logging` module. When working with python scripts and command line, just doing `import logging` is enough."]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.6.4"}},"nbformat":4,"nbformat_minor":2} diff --git a/examples/general_state_tutorial.ipynb b/examples/general_state_tutorial.ipynb deleted file mode 100644 index 9a041f99..00000000 --- a/examples/general_state_tutorial.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"cells": [{"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np\n", "from sympy import Symbol\n", "from scipy.stats import unitary_group # type: ignore\n", "from pytket.circuit import Circuit, OpType, Unitary2qBox, Qubit, Bit\n", "from pytket.passes import DecomposeBoxes\n", "from pytket.utils import QubitPauliOperator\n", "from pytket._tket.pauli import Pauli, QubitPauliString\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.cutensornet.general_state import (\n", " GeneralState,\n", " GeneralBraOpKet,\n", ")\n", "from pytket.extensions.cutensornet.backends import CuTensorNetShotsBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Introduction
\n", "This notebook is a guide on how to use the features provided in the `general_state` submodule of pytket-cutensornet. This submodule is a thin wrapper of CuTensorNet's `NetworkState`, allowing users to convert pytket circuits into tensor networks and use CuTensorNet's contraction path optimisation algorithm.
\n", "All simulations realised with this submodule are *exact*. Once the pytket circuit has been converted to a tensor network, the computation has two steps:
\n", " 1. *Contraction path optimisation*. Attempts to find an order of contracting pairs of tensors in which the the total number of FLOPs is minimised. No operation on the tensor network occurs at this point. Runs on CPU.
\n", " 2. *Tensor network contraction*. Uses the ordering of contractions found in the previous step evaluate the tensor network. Runs on GPU.
\n", "
\n", "**Reference**: The original contraction path optimisation algorithm that NVIDIA implemented on CuTensorNet: https://arxiv.org/abs/2002.01935"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# `GeneralState`
\n", "The class `GeneralState` is used to convert a circuit into a tensor network and query information from the final state. Let's walk through a simple example."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_circ = Circuit(5)\n", "my_circ.CX(3, 4)\n", "my_circ.H(2)\n", "my_circ.CZ(0, 1)\n", "my_circ.ZZPhase(0.1, 4, 3)\n", "my_circ.TK2(0.3, 0.5, 0.7, 2, 1)\n", "my_circ.Ry(0.2, 0)\n", "my_circ.measure_all()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(my_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The first step is to convert our pytket circuit into a tensor network. This is straightforward:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tn_state = GeneralState(my_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The variable `tn_state` now holds a tensor network representation of `my_circ`.
\n", "**Note**: Circuits must not have mid-circuit measurements or classical logic. The measurements at the end of the circuit are stripped and only considered when calling `tn_state.sample(n_shots)`.
\n", "We can now query information from the state. For instance, let's calculate the probability of in the qubits 0 and 3 agreeing in their outcome."]}, {"cell_type": "markdown", "metadata": {}, "source": ["First, let's generate `|x>` computational basis states where `q[0]` and `q[3]` agree on their values. We can do this with some bitwise operators and list comprehension.
\n", "**Note**: Remember that pytket uses \"increasing lexicographic order\" (ILO) for qubits, so `q[0]` is the most significant bit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["selected_states = [\n", " x\n", " for x in range(2**my_circ.n_qubits)\n", " if ( # Iterate over all possible states\n", " x & int(\"10000\", 2) == 0\n", " and x & int(\"00010\", 2) == 0 # both qubits are 0 or...\n", " or x & int(\"10000\", 2) != 0\n", " and x & int(\"00010\", 2) != 0 # both qubits are 1\n", " )\n", "]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now query the amplitude of all of these states and calculate the probability by summing their squared absolute values."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["amplitudes = []\n", "for x in selected_states:\n", " amplitudes.append(tn_state.get_amplitude(x))\n", "probability = sum(abs(a) ** 2 for a in amplitudes)\n", "print(f\"Probability: {probability}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Of course, calculating probabilities by considering the amplitudes of all relevant states is not efficient in general, since we may need to calculate a number of amplitudes that scales exponentially with the number of qubits. An alternative is to use expectation values. In particular, all of the states in `selected_states` are +1 eigenvectors of the `ZIIZI` observable and, hence, we can calculate the probability `p` by solving the equation ` = (+1)p + (-1)(1-p)` using the fact that `ZIIZI` only has +1 and -1 eigenvalues."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["string_ZIIZI = QubitPauliString(\n", " my_circ.qubits, [Pauli.Z, Pauli.I, Pauli.I, Pauli.Z, Pauli.I]\n", ")\n", "observable = QubitPauliOperator({string_ZIIZI: 1.0})\n", "expectation_val = tn_state.expectation_value(observable).real\n", "exp_probability = (expectation_val + 1) / 2\n", "assert np.isclose(probability, exp_probability, atol=0.0001)\n", "print(f\"Probability: {exp_probability}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can estimate the probability by sampling."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots = 100000\n", "outcomes = tn_state.sample(n_shots)\n", "hit_count = 0\n", "for bit_tuple, count in outcomes.get_counts().items():\n", " if bit_tuple[0] == bit_tuple[3]:\n", " hit_count += count\n", "samp_probability = hit_count / n_shots\n", "assert np.isclose(probability, samp_probability, atol=0.01)\n", "print(f\"Probability: {samp_probability}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["When we finish doing computations with the `tn_state` we must destroy it to free GPU memory."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tn_state.destroy()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To avoid forgetting this final step, we recommend users call `GeneralState` (and `GeneralBraOpKet`) as context managers:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with GeneralState(my_circ) as my_state:\n", " expectation_val = my_state.expectation_value(observable)\n", "print(expectation_val)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Using this syntax, `my_state` is automatically destroyed when the code exists the `with ...` block."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Parameterised circuits
\n", "Circuits that only differ on the parameters of their gates have the same tensor network topology and, hence, we may use the same contraction path for all of them."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["a, b, c = Symbol(\"a\"), Symbol(\"b\"), Symbol(\"c\")\n", "param_circ1 = Circuit(5)\n", "param_circ1.Ry(a, 3).Ry(0.27, 4).CX(4, 3).Ry(b, 2).Ry(0.21, 3)\n", "param_circ1.Ry(0.12, 0).Ry(a, 1)\n", "param_circ1.add_gate(OpType.CnX, [0, 1, 4]).add_gate(OpType.CnX, [4, 1, 3])\n", "param_circ1.X(0).X(1).add_gate(OpType.CnY, [0, 1, 2]).add_gate(OpType.CnY, [0, 4, 3]).X(\n", " 0\n", ").X(1)\n", "param_circ1.Ry(-b, 0).Ry(-c, 1)\n", "render_circuit_jupyter(param_circ1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can pass a parameterised circuit to `GeneralState`. The value of the parameters is provided when calling methods of `GeneralState`. The contraction path is automatically reused on different calls to the same method."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_circs = 5\n", "with GeneralState(param_circ1) as param_state:\n", " for i in range(n_circs):\n", " symbol_map = {s: np.random.random() for s in [a, b, c]}\n", " exp_val = param_state.expectation_value(observable, symbol_map=symbol_map)\n", " print(f\"Expectation value for circuit {i}: {exp_val.real}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# `GeneralBraOpKet`
\n", "The `GeneralBraOpKet` can be used to calculate any number that can be represented as the result of some `` where `|bra>` and `|ket>` are the final states of pytket circuits, and `op` is a `QubitPauliOperator`. The circuits for `|bra>` and `|ket>` need not be the same."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["x, y, z = Symbol(\"x\"), Symbol(\"y\"), Symbol(\"z\")\n", "param_circ2 = Circuit(5)\n", "param_circ2.H(0)\n", "param_circ2.S(1)\n", "param_circ2.Rz(x * z, 2)\n", "param_circ2.Ry(y + x, 3)\n", "param_circ2.TK1(x, y, z, 4)\n", "param_circ2.TK2(z - y, z - x, (x + y) * z, 1, 3)\n", "symbol_map = {a: 2.1, b: 1.3, c: 0.7, x: 3.0, y: 1.6, z: -8.3}"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can calculate inner products by providing no `op`:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with GeneralBraOpKet(bra=param_circ2, ket=param_circ1) as braket:\n", " inner_prod = braket.contract(symbol_map=symbol_map)\n", "with GeneralBraOpKet(bra=param_circ1, ket=param_circ2) as braket:\n", " inner_prod_conj = braket.contract(symbol_map=symbol_map)\n", "assert np.isclose(np.conj(inner_prod), inner_prod_conj)\n", "print(f\" = {inner_prod}\")\n", "print(f\" = {inner_prod_conj}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["And we are not constrained to Hermitian operators:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["string_XZIXX = QubitPauliString(\n", " param_circ2.qubits, [Pauli.X, Pauli.Z, Pauli.I, Pauli.X, Pauli.X]\n", ")\n", "string_IZZYX = QubitPauliString(\n", " param_circ2.qubits, [Pauli.I, Pauli.Z, Pauli.Z, Pauli.Y, Pauli.X]\n", ")\n", "string_ZIZXY = QubitPauliString(\n", " param_circ2.qubits, [Pauli.Z, Pauli.I, Pauli.Z, Pauli.X, Pauli.Y]\n", ")\n", "operator = QubitPauliOperator(\n", " {string_XZIXX: -1.38j, string_IZZYX: 2.36, string_ZIZXY: 0.42j + 0.3}\n", ")\n", "with GeneralBraOpKet(bra=param_circ2, ket=param_circ1) as braket:\n", " value = braket.contract(operator, symbol_map=symbol_map)\n", "print(value)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Backends
\n", "We provide a pytket `Backend` to obtain shots using `GeneralState`."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's consider a more challenging circuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def random_circuit(n_qubits: int, n_layers: int) -> Circuit:\n", " \"\"\"Random quantum volume circuit.\"\"\"\n", " c = Circuit(n_qubits, n_qubits)\n", " for _ in range(n_layers):\n", " qubits = np.random.permutation([i for i in range(n_qubits)])\n", " qubit_pairs = [[qubits[i], qubits[i + 1]] for i in range(0, n_qubits - 1, 2)]\n", " for pair in qubit_pairs:\n", " # Generate random 4x4 unitary matrix.\n", " SU4 = unitary_group.rvs(4) # random unitary in SU4\n", " SU4 = SU4 / (np.linalg.det(SU4) ** 0.25)\n", " SU4 = np.matrix(SU4)\n", " c.add_unitary2qbox(Unitary2qBox(SU4), *pair)\n", " DecomposeBoxes().apply(c)\n", " return c"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's measure only three of the qubits.
\n", "**Note**: The complexity of this simulation increases exponentially with the number of qubits measured. Other factors leading to intractability are circuit depth and qubit connectivity."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots = 1000\n", "quantum_vol_circ = random_circuit(n_qubits=40, n_layers=5)\n", "quantum_vol_circ.Measure(Qubit(0), Bit(0))\n", "quantum_vol_circ.Measure(Qubit(1), Bit(1))\n", "quantum_vol_circ.Measure(Qubit(2), Bit(2))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `CuTensorNetShotsBackend` is used in the same way as any other pytket `Backend`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = CuTensorNetShotsBackend()\n", "compiled_circ = backend.get_compiled_circuit(quantum_vol_circ)\n", "results = backend.run_circuit(compiled_circ, n_shots=n_shots)\n", "print(results.get_counts())"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4"}}, "nbformat": 4, "nbformat_minor": 2} \ No newline at end of file diff --git a/examples/mps_tutorial.ipynb b/examples/mps_tutorial.ipynb deleted file mode 100644 index 1735b1f9..00000000 --- a/examples/mps_tutorial.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"cells": [{"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np\n", "from time import time\n", "import matplotlib.pyplot as plt\n", "from pytket import Circuit, OpType\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.cutensornet.structured_state import (\n", " CuTensorNetHandle,\n", " Config,\n", " SimulationAlgorithm,\n", " simulate,\n", " prepare_circuit_mps,\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Introduction
\n", "This notebook provides examples of the usage of the MPS functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.
\n", "A Matrix Product State (MPS) represents a state on `n` qubits as a list of `n` tensors connected in a line as show below:
\n", "![MPS](images/mps.png)
\n", "Each of these circles corresponds to a tensor. We refer to each leg of a tensor as a *bond* and the number of bonds a tensor has is its *rank*. In code, a tensor is just a multidimensional array:
\n", "
\n", "```tensor[i][j][k] = v```
\n", "
\n", "In the case above, we are assigning an entry value `v` of a rank-3 tensor (one `[ ]` coordinate per bond). Each bond allows a different number of values for its indices; for instance `0 <= i < 4` would mean that the first bond of our tensor can take up to four different indices; we refer to this as the *dimension* of the bond. We refer to the bonds connecting different tensors in the MPS as *virtual bonds*; the maximum allowed value for the dimension of virtual bonds is often denoted by the greek letter `chi`. The open bonds are known as *physical bonds* and, in our case, each will correspond to a qubit; hence, they have dimension `2` -- the dimension of the vector space of a single qubit.
\n", "In essence, whenever we want to apply a gate to certain qubit we will connect a tensor (matrix) representing the gate to the corresponding physical bond and *contract* the network back to an MPS form (tensor contraction is a generalisation of matrix multiplication to multidimensional arrays). Whenever a two-qubit gate is applied, the entanglement information after contraction will be kept in the degrees of freedom of the virtual bonds. As such, the dimension of the virtual bonds will generally increase exponentially as we apply entangling gates, leading to large memory footprints of the tensors and, consequently, long runtime for tensor contraction. We provide functionalities to limit the growth of the dimension of the virtual bonds, keeping resource consumption in check. Read the *Approximate simulation* section on this notebook to learn more.
\n", "**References**: To read more about MPS we recommend the following papers.
\n", "* For an introduction to MPS and its canonical form: https://arxiv.org/abs/1901.05824.
\n", "* For a description of the `MPSxGate` algorithm we provide: https://arxiv.org/abs/2002.07730.
\n", "* For a description of the `MPSxMPO` algorithm we provide: https://arxiv.org/abs/2207.05612.
\n", "* For insights on the reationship between truncation error and the error model in a quantum computer: https://arxiv.org/abs/2004.02388"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Basic functionality and exact simulation
\n", "Here we show an example of the basic use of our MPS methods. We first generate a simple `pytket` circuit to be simulated."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_circ = Circuit(5)\n", "my_circ.CX(3, 4)\n", "my_circ.H(2)\n", "my_circ.CZ(0, 1)\n", "my_circ.ZZPhase(0.1, 4, 3)\n", "my_circ.TK2(0.3, 0.5, 0.7, 2, 1)\n", "my_circ.Ry(0.2, 0)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(my_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For **exact** simulation, simply call the `simulate` function on the circuit and choose a contraction algorithm. To learn more about the contraction algorithms we provide see the *Contraction algorithms* section of this notebook. You will also need to provide a configuration, the default one is provided by `Config()`. Custom settings of `Config` are discussed in the *Approximate simulation* section.
\n", "**NOTE**: whenever you wish to generate an `MPS` object or execute calculations on it you must do so within a `with CuTensorNetHandle() as libhandle:` block; this will initialise the cuTensorNetwork library for you, and destroy its handles at the end of the `with` block. You will need to pass the `libhandle` to the `MPS` object via the method that generates it (in the snippet below, `simulate`), or if already initialised, pass it via the `update_libhandle` method.
\n", "Due to the nature of Jupyter notebooks, we will be starting most of these cells with a `with CuTensorNetHandle() as libhandle:`. However, in a standard script, all of these cells would be grouped together and a single `with CuTensorNetHandle() as libhandle:` statement would be necessary at the beginning of the script."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps = simulate(libhandle, my_circ, SimulationAlgorithm.MPSxGate, Config())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Notice that `my_circ` uses a rich gateset -- in fact, every single-qubit and two-qubit gate supported by `pytket` can be used in our MPS approaches. Gates acting on more than two qubits are not currently supported."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The output of `simulate` is an `MPS` object encoding the output state of the circuit.
\n", "### Obtain an amplitude from an MPS
\n", "Let's first see how to get the amplitude of the state `|10100>` from the output of the previous circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state = int(\"10100\", 2)\n", "with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " amplitude = my_mps.get_amplitude(state)\n", "print(amplitude)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Since this is a very small circuit, we can use `pytket`'s state vector simulator capabilities to verify that the state is correct by checking the amplitude of each of the computational states."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state_vector = my_circ.get_statevector()\n", "n_qubits = len(my_circ.qubits)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["correct_amplitude = [False] * (2**n_qubits)\n", "with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " for i in range(2**n_qubits):\n", " correct_amplitude[i] = np.isclose(state_vector[i], my_mps.get_amplitude(i))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Are all amplitudes correct?\")\n", "print(all(correct_amplitude))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Sampling from an MPS
\n", "We can also sample from the output state of a circuit by calling `my_mps.sample`, where `my_mps` is the outcome of simulating the circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_samples = 100\n", "n_qubits = len(my_circ.qubits)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Initialise the sample counter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["sample_count = [0 for _ in range(2**n_qubits)]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " for _ in range(n_samples):\n", " # Draw a sample\n", " qubit_outcomes = my_mps.sample()\n", " # Convert qubit outcomes to bitstring\n", " bitstring = \"\".join(str(qubit_outcomes[q]) for q in my_circ.qubits)\n", " # Convert bitstring to int\n", " outcome = int(bitstring, 2)\n", " # Update the sample dictionary\n", " sample_count[outcome] += 1"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Calculate the theoretical number of samples per bitstring"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["expected_count = [n_samples * abs(state_vector[i]) ** 2 for i in range(2**n_qubits)]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Plot a comparison of theory vs sampled"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["plt.scatter(range(2**n_qubits), expected_count, label=\"Theory\")\n", "plt.scatter(range(2**n_qubits), sample_count, label=\"Experiment\", marker=\"x\")\n", "plt.xlabel(\"Basis states\")\n", "plt.ylabel(\"Samples\")\n", "plt.legend()\n", "plt.show()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We also provide methods to apply mid-circuit measurements via `my_mps.measure(qubits)` and postselection via `my_mps.postselect(qubit_outcomes)`. Their use is similar to that of `my_mps.sample()` shown above.
\n", "**Note:** whereas `my_mps.sample()` does *not* change the state of the MPS, `my_mps.measure(qubits)` and `my_mps.postselect(qubit_outcomes)` do change it, projecting the state to the resulting outcome and removing the measured qubits."]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Inner products
\n", "Using `vdot` you can obtain the inner product of two states in MPS form. This method does not change the internal data of neither of the MPS. Moreover, it can be used on the same `MPS` object for both inputs, yielding the squared norm of the state."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " norm_sq = my_mps.vdot(my_mps)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"As expected, the squared norm of a state is 1\")\n", "print(np.isclose(norm_sq, 1))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's come up with another circuit on the same qubits and apply an inner product between the two `MPS` objects."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Generate circuits"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["other_circ = Circuit(5)\n", "other_circ.H(3)\n", "other_circ.CZ(3, 4)\n", "other_circ.XXPhase(0.3, 1, 2)\n", "other_circ.Ry(0.7, 3)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Simulate them"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " other_mps = simulate(libhandle, other_circ, SimulationAlgorithm.MPSxGate, Config())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's calculate the inner product and check that it agrees with `pytket`'s state vector based computation."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " inner_product = my_mps.vdot(other_mps)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_state = my_circ.get_statevector()\n", "other_state = other_circ.get_statevector()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Is the inner product correct?\")\n", "print(np.isclose(np.vdot(my_state, other_state), inner_product))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Mid-circuit measurements and classical control
\n", "Mid-circuit measurements and classical control is supported (only in `MPSxGate` as of v0.8.0). For instance, we can implement the teleportation protocol on a pytket circuit and simulate it:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit()\n", "alice = circ.add_q_register(\"alice\", 2)\n", "alice_bits = circ.add_c_register(\"alice_bits\", 2)\n", "bob = circ.add_q_register(\"bob\", 1)\n", "# Initialise Alice's first qubit in some arbitrary state\n", "circ.Rx(0.42, alice[0])\n", "orig_state = circ.get_statevector()\n", "# Create a Bell pair shared between Alice and Bob\n", "circ.H(alice[1]).CX(alice[1], bob[0])\n", "# Apply a Bell measurement on Alice's qubits\n", "circ.CX(alice[0], alice[1]).H(alice[0])\n", "circ.Measure(alice[0], alice_bits[0])\n", "circ.Measure(alice[1], alice_bits[1])\n", "# Apply conditional corrections on Bob's qubits\n", "circ.add_gate(OpType.X, [bob[0]], condition_bits=[alice_bits[1]], condition_value=1)\n", "circ.add_gate(OpType.Z, [bob[0]], condition_bits=[alice_bits[0]], condition_value=1)\n", "# Reset Alice's qubits\n", "circ.add_gate(OpType.Reset, [alice[0]])\n", "circ.add_gate(OpType.Reset, [alice[1]])\n", "# Display the circuit\n", "render_circuit_jupyter(circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now simulate the circuit and check that the qubit has been successfully teleported."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\n", " f\"Initial state:\\n {np.round(orig_state[0],2)}|00>|0> + {np.round(orig_state[4],2)}|10>|0>\"\n", ")\n", "with CuTensorNetHandle() as libhandle:\n", " state = simulate(libhandle, circ, SimulationAlgorithm.MPSxGate, Config())\n", " print(\n", " f\"Teleported state:\\n {np.round(state.get_amplitude(0),2)}|00>|0> + {np.round(state.get_amplitude(1),2)}|00>|1>\"\n", " )\n", " print(f\"Measurement outcomes:\\n {state.get_bits()}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Two-qubit gates acting on non-adjacent qubits
\n", "Standard MPS algorithms only support simulation of two-qubit gates acting on neighbour qubits. In our implementation, however, two-qubit gates between arbitrary qubits may be applied, as shown below."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit(5)\n", "circ.H(1)\n", "circ.ZZPhase(0.3, 1, 3)\n", "circ.CX(0, 2)\n", "circ.Ry(0.8, 4)\n", "circ.CZ(3, 4)\n", "circ.XXPhase(0.7, 1, 2)\n", "circ.TK2(0.1, 0.2, 0.4, 1, 4)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " mps = simulate(libhandle, circ, SimulationAlgorithm.MPSxGate, Config())\n", " print(\"Did simulation succeed?\")\n", " print(mps.is_valid())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["**Note**: Even though two-qubit gates on non-adjacent qubits are simulable, the overhead on these is considerably larger than simulating gates on adjacent qubits. As a rule of thumb if the two qubits are `n` positions apart, the overhead is upper bounded by the cost of simulating `n-1` additional SWAP gates to move the leftmost qubit near the rightmost. In reality, the implementation we use is more nuanced than just applying SWAP gates, and the qubits don't actually change position.
\n", "When circuits are shallow, using our approach to simulate long-distance two-qubit gates is advantageous. In the case of deep circuits with many long-distance gates, it is sometimes beneficial to use TKET routing on the circuit, explicitly adding SWAP gates so that all two-qubit gates act on nearest neighbour qubits. Users may do this by calling `prepare_circuit_mps`, which is a wrapper of the corresponding TKET routing pass."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["prepared_circ, qubit_map = prepare_circuit_mps(circ)\n", "render_circuit_jupyter(prepared_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The circuit can now be simulated as usual."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " mps = simulate(libhandle, prepared_circ, SimulationAlgorithm.MPSxGate, Config())\n", " print(\"Did simulation succeed?\")\n", " print(mps.is_valid())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Notice that the qubits in the `prepared_circ` were renamed when applying `prepare_circuit_mps`. Implicit SWAPs may have been added to the circuit, meaning that the logical qubit held at the ``node[i]`` qubit at the beginning of the circuit may differ from the one it holds at the end; this information is captured by the `qubit_map` output. We recommend applying ``apply_qubit_relabelling`` on the MPS after simulation, relabelling the qubits according to these implicit SWAPs."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(qubit_map)\n", "mps.apply_qubit_relabelling(qubit_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Approximate simulation
\n", "We provide two policies for approximate simulation; these are supported by both of our current MPS contraction algorithms:
\n", "* Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes.
\n", "* Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate.
\n", "Values for `chi` and `truncation_fidelity` can be set via `Config`. To showcase approximate simulation, let's define a circuit where exact MPS contraction starts struggling."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def random_line_circuit(n_qubits: int, layers: int) -> Circuit:\n", " \"\"\"Random circuit with line connectivity.\"\"\"\n", " c = Circuit(n_qubits)\n", " for i in range(layers):\n", " # Layer of TK1 gates\n", " for q in range(n_qubits):\n", " c.TK1(np.random.rand(), np.random.rand(), np.random.rand(), q)\n\n", " # Layer of CX gates\n", " offset = np.mod(i, 2) # Even layers connect (q0,q1), odd (q1,q2)\n", " qubit_pairs = [\n", " [c.qubits[i], c.qubits[i + 1]] for i in range(offset, n_qubits - 1, 2)\n", " ]\n", " # Direction of each CX gate is random\n", " for pair in qubit_pairs:\n", " np.random.shuffle(pair)\n", " for pair in qubit_pairs:\n", " c.CX(pair[0], pair[1])\n", " return c"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circuit = random_line_circuit(n_qubits=20, layers=20)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For exact contraction, `chi` must be allowed to be up to `2**(n_qubits // 2)`, meaning that if we set `n_qubits = 20` it would require `chi = 1024`; already too much for this particular circuit to be simulated in a gaming laptop using the current implementation. Instead, let's bound `chi` to a maximum of `16`. Doing so results in faster runtime, at the expense of losing output state fidelity."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=16)\n", " bound_chi_mps = simulate(libhandle, circuit, SimulationAlgorithm.MPSxGate, config)\n", "end = time()\n", "print(\"Time taken by approximate contraction with bound chi:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(bound_chi_mps.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can fix `truncation_fidelity` and let `chi` increase as necessary to satisfy it."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(truncation_fidelity=0.999)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxGate, config\n", " )\n", "end = time()\n", "print(\"Time taken by approximate contraction with fixed truncation fidelity:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(fixed_fidelity_mps.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Contraction algorithms"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We currently offer two MPS-based simulation algorithms:
\n", "* **MPSxGate**: Apply gates one by one to the MPS, canonicalising the MPS and truncating when necessary. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2002.07730.
\n", "* **MPSxMPO**: Maintain two MPS copies of the state as it evolves, one updated eagerly using the **MPSxGate** method and the other updated in batches of up to `k` layers of two-qubit gates. Whenever the second MPS is updated, both copies are synchronised and an optimisation algorithm is applied to increase the fidelity of the state. This algorithm is often referred to as DMRG-like simulation. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2207.05612.
\n", "The `MPSxGate` algorithm is the one we have been using for all of the examples above. In comparison, the `MPSxMPO` algorithm provides the user with two new parameters to tune:
\n", "* **k**: The maximum number of layers the MPO is allowed to have before being contracted. Increasing this might increase fidelity, but it will also increase resource requirements exponentially. Default value is `4`.
\n", "* **optim_delta**: Stopping criteria for the optimisation when contracting the `k` layers of MPO. Stops when the increase of fidelity between iterations is smaller than `optim_delta`. Default value is `1e-5`.
\n", "Both `k` and `optim_delta` can be set via `Config`. Below we compare `MPSxGate` versus `MPSxMPO` with default parameters and `MPSxMPO` with more resource-hungry parameters. The circuit used is the same as in the previous section."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=16)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxGate, config\n", " )\n", "end = time()\n", "print(\"MPSxGate\")\n", "print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n", "print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=16)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxMPO, config\n", " )\n", "end = time()\n", "print(\"MPSxMPO, default parameters\")\n", "print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n", "print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(k=8, optim_delta=1e-15, chi=16)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxMPO, config\n", " )\n", "end = time()\n", "print(\"MPSxMPO, custom parameters\")\n", "print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n", "print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["**Note**: `MPSxMPO` also admits truncation policy in terms of `truncation_fidelity` instead of `chi`."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Using the logger"]}, {"cell_type": "markdown", "metadata": {}, "source": ["You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent):
\n", "- `logging.INFO` will print information about progress percent, memory currently occupied by the MPS and current fidelity. Additionally, some high level information of the current stage of the simulation is provided, such as when `MPSxMPO` is applying optimisation sweeps.
\n", "- `logging.DEBUG` provides all of the messages from the loglevel above plus detailed information of the current operation being carried out and the values of important variables.
\n", "**Note**: Due to technical issues with the `logging` module and Jupyter notebooks we need to reload the `logging` module. When working with python scripts and command line, just doing `import logging` is enough."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from importlib import reload # Not needed in Python 2\n", "import logging"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["reload(logging)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["An example of the use of `logging.INFO` is provided below."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " config = Config(truncation_fidelity=0.999, loglevel=logging.INFO)\n", " simulate(libhandle, circuit, SimulationAlgorithm.MPSxMPO, config)"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4"}}, "nbformat": 4, "nbformat_minor": 2} \ No newline at end of file diff --git a/examples/ttn_tutorial.ipynb b/examples/ttn_tutorial.ipynb deleted file mode 100644 index d00ef37b..00000000 --- a/examples/ttn_tutorial.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"cells": [{"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np\n", "from time import time\n", "import matplotlib.pyplot as plt\n", "import networkx as nx\n", "from pytket import Circuit\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.cutensornet.structured_state import (\n", " CuTensorNetHandle,\n", " Config,\n", " SimulationAlgorithm,\n", " simulate,\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Introduction
\n", "This notebook provides examples of the usage of the TTN functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.
\n", "Some good references to learn about Tree Tensor Network state simulation:
\n", "- For an introduction into TTN based simulation of quantum circuits: https://arxiv.org/abs/2206.01000
\n", "- For an introduction on some of the optimisation concerns that are relevant to TTN: https://arxiv.org/abs/2209.03196
\n", "The implementation in pytket-cutensornet differs from previously published literature. I am still experimenting with the algorithm. I intend to write up a document detailing the approach, once I reach a stable version.
\n", "The main advantage of TTN over MPS is that it can be used to efficiently simulate circuits with richer qubit connectivity. This does **not** mean that TTN has an easy time simulating all-to-all connectivity, but it is far more flexible than MPS. TTN's strength is in simulating circuit where certain subsets of qubits interact densely with each other, and there is not that many gates acting on qubits in different subsets."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# How to use
\n", "The interface for TTN matches that of MPS. As such, you should be able to run any code that uses `SimulationAlgorithm.MPSxGate` by replacing it with `SimulationAlgorithm.TTNxGate`. Calling `prepare_circuit_mps` is no longer necessary, since `TTNxGate` can apply gates between non-neighbouring qubits.
\n", "**NOTE**: If you are new to pytket-cutensornet, it is highly recommended to start reading the `mps_tutorial.ipynb` notebook instead. More details about the use of the library are discussed there (for instance, why and when to call `CuTensorNetHandle()`)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def random_graph_circuit(n_qubits: int, edge_prob: float, layers: int) -> Circuit:\n", " \"\"\"Random circuit with qubit connectivity determined by a random graph.\"\"\"\n", " c = Circuit(n_qubits)\n", " for i in range(layers):\n", " # Layer of TK1 gates\n", " for q in range(n_qubits):\n", " c.TK1(np.random.rand(), np.random.rand(), np.random.rand(), q)\n\n", " # Layer of CX gates\n", " graph = nx.erdos_renyi_graph(n_qubits, edge_prob, directed=True)\n", " qubit_pairs = list(graph.edges)\n", " for pair in qubit_pairs:\n", " c.CX(pair[0], pair[1])\n", " return c"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For **exact** simulation, you can call `simulate` directly, providing the default `Config()`:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["simple_circ = random_graph_circuit(n_qubits=10, edge_prob=0.1, layers=1)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_ttn = simulate(libhandle, simple_circ, SimulationAlgorithm.TTNxGate, Config())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Obtain an amplitude from a TTN
\n", "Let's first see how to get the amplitude of the state `|10100>` from the output of the previous circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state = int(\"10100\", 2)\n", "with CuTensorNetHandle() as libhandle:\n", " my_ttn.update_libhandle(libhandle)\n", " amplitude = my_ttn.get_amplitude(state)\n", "print(amplitude)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Since this is a very small circuit, we can use `pytket`'s state vector simulator capabilities to verify that the state is correct by checking the amplitude of each of the computational states."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state_vector = simple_circ.get_statevector()\n", "n_qubits = len(simple_circ.qubits)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["correct_amplitude = [False] * (2**n_qubits)\n", "with CuTensorNetHandle() as libhandle:\n", " my_ttn.update_libhandle(libhandle)\n", " for i in range(2**n_qubits):\n", " correct_amplitude[i] = np.isclose(state_vector[i], my_ttn.get_amplitude(i))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Are all amplitudes correct?\")\n", "print(all(correct_amplitude))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Sampling from a TTN
\n", "Sampling and measurement from a TTN state is not currently supported. This will be added in an upcoming release."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Approximate simulation
\n", "We provide two policies for approximate simulation:
\n", "* Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes.
\n", "* Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate.
\n", "Values for `chi` and `truncation_fidelity` can be set via `Config`. To showcase approximate simulation, let's define a circuit where exact TTN contraction would not be enough."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circuit = random_graph_circuit(n_qubits=30, edge_prob=0.1, layers=1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can simulate it using bounded `chi` as follows:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=64, float_precision=np.float32)\n", " bound_chi_ttn = simulate(libhandle, circuit, SimulationAlgorithm.TTNxGate, config)\n", "end = time()\n", "print(\"Time taken by approximate contraction with bounded chi:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(bound_chi_ttn.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can fix `truncation_fidelity` and let the bond dimension increase as necessary to satisfy it."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(truncation_fidelity=0.99, float_precision=np.float32)\n", " fixed_fidelity_ttn = simulate(\n", " libhandle, circuit, SimulationAlgorithm.TTNxGate, config\n", " )\n", "end = time()\n", "print(\"Time taken by approximate contraction with fixed truncation fidelity:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(fixed_fidelity_ttn.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Contraction algorithms"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We currently offer only one TTN-based simulation algorithm.
\n", "* **TTNxGate**: Apply gates one by one to the TTN, canonicalising the TTN and truncating when necessary."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Using the logger"]}, {"cell_type": "markdown", "metadata": {}, "source": ["You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent):
\n", "- `logging.INFO` will print information about progress percent, memory currently occupied by the TTN and current fidelity. Additionally, some high level information of the current stage of the simulation is provided.
\n", "- `logging.DEBUG` provides all of the messages from the loglevel above plus detailed information of the current operation being carried out and the values of important variables.
\n", "**Note**: Due to technical issues with the `logging` module and Jupyter notebooks we need to reload the `logging` module. When working with python scripts and command line, just doing `import logging` is enough."]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4"}}, "nbformat": 4, "nbformat_minor": 2} \ No newline at end of file From 3b0abd9cee78a35a3e0eb9c347cb9b3b6c8f01c6 Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Thu, 24 Oct 2024 18:14:33 +0100 Subject: [PATCH 03/13] add notebooks to table of contents --- docs/index.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index f1c9fab3..f42616ae 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,6 +32,13 @@ This will include the necessary dependencies such as CUDA toolkit. Then, to inst api.rst changelog.rst +.. toctree:: + :caption: Example Notebooks + + examples/general_state_tutorial.ipynb + examples/mps_tutorial.ipynb + examples/ttn_tutorial.ipynb + .. toctree:: :caption: Useful links From 86966e5f97ea3024e749a602252cb581d553e671 Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:54:09 +0100 Subject: [PATCH 04/13] attempt to fix check-examples workflow --- .github/workflows/check-examples.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-examples.yml b/.github/workflows/check-examples.yml index 08240a8a..a83371cf 100644 --- a/.github/workflows/check-examples.yml +++ b/.github/workflows/check-examples.yml @@ -22,7 +22,7 @@ jobs: base: ${{ github.ref }} filters: | examples: - - 'examples/**' + - 'docs/examples/**' - '.github/**' check: @@ -47,5 +47,5 @@ jobs: python -m pip install p2j - name: test example notebooks run: | - cd examples + cd docs/examples ./check-examples From a338305298f25f459cfd95224903fc5809bb365c Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:57:03 +0100 Subject: [PATCH 05/13] fix cd command --- .github/workflows/check-examples.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-examples.yml b/.github/workflows/check-examples.yml index a83371cf..3d6a8306 100644 --- a/.github/workflows/check-examples.yml +++ b/.github/workflows/check-examples.yml @@ -43,7 +43,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install . - cd examples + cd docs/examples python -m pip install p2j - name: test example notebooks run: | From eadb24a4463ddd1dd7a786a3405add235f8c3dd0 Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:33:57 +0100 Subject: [PATCH 06/13] Revert "move examples folder into docs folder" This reverts commit 2fc1277708030af4eb579713e0cdec835a615fe4. --- docs/examples/general_state_tutorial.ipynb | 1 - docs/examples/mps_tutorial.ipynb | 1 - docs/examples/ttn_tutorial.ipynb | 1 - {docs/examples => examples}/README.md | 0 {docs/examples => examples}/check-examples | 0 {docs/examples => examples}/ci-tested-notebooks.txt | 0 examples/general_state_tutorial.ipynb | 1 + {docs/examples => examples}/images/mps.png | Bin {docs/examples => examples}/mpi/README.md | 0 .../mpi/mpi_overlap_bcast_circ.py | 0 .../mpi/mpi_overlap_bcast_mps.py | 0 .../mpi/mpi_overlap_bcast_net.py | 0 examples/mps_tutorial.ipynb | 1 + .../python/general_state_tutorial.py | 0 {docs/examples => examples}/python/mps_tutorial.py | 0 {docs/examples => examples}/python/ttn_tutorial.py | 0 examples/ttn_tutorial.ipynb | 1 + 17 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 docs/examples/general_state_tutorial.ipynb delete mode 100644 docs/examples/mps_tutorial.ipynb delete mode 100644 docs/examples/ttn_tutorial.ipynb rename {docs/examples => examples}/README.md (100%) rename {docs/examples => examples}/check-examples (100%) rename {docs/examples => examples}/ci-tested-notebooks.txt (100%) create mode 100644 examples/general_state_tutorial.ipynb rename {docs/examples => examples}/images/mps.png (100%) rename {docs/examples => examples}/mpi/README.md (100%) rename {docs/examples => examples}/mpi/mpi_overlap_bcast_circ.py (100%) rename {docs/examples => examples}/mpi/mpi_overlap_bcast_mps.py (100%) rename {docs/examples => examples}/mpi/mpi_overlap_bcast_net.py (100%) create mode 100644 examples/mps_tutorial.ipynb rename {docs/examples => examples}/python/general_state_tutorial.py (100%) rename {docs/examples => examples}/python/mps_tutorial.py (100%) rename {docs/examples => examples}/python/ttn_tutorial.py (100%) create mode 100644 examples/ttn_tutorial.ipynb diff --git a/docs/examples/general_state_tutorial.ipynb b/docs/examples/general_state_tutorial.ipynb deleted file mode 100644 index a4d0e137..00000000 --- a/docs/examples/general_state_tutorial.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"cells":[{"cell_type":"markdown","metadata":{},"source":["# `GeneralState` Tutorial"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import numpy as np\n","from sympy import Symbol\n","from scipy.stats import unitary_group # type: ignore\n","from pytket.circuit import Circuit, OpType, Unitary2qBox, Qubit, Bit\n","from pytket.passes import DecomposeBoxes\n","from pytket.utils import QubitPauliOperator\n","from pytket._tket.pauli import Pauli, QubitPauliString\n","from pytket.circuit.display import render_circuit_jupyter"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.cutensornet.general_state import (\n"," GeneralState,\n"," GeneralBraOpKet,\n",")\n","from pytket.extensions.cutensornet.backends import CuTensorNetShotsBackend"]},{"cell_type":"markdown","metadata":{},"source":["## Introduction
\n","This notebook is a guide on how to use the features provided in the `general_state` submodule of pytket-cutensornet. This submodule is a thin wrapper of CuTensorNet's `NetworkState`, allowing users to convert pytket circuits into tensor networks and use CuTensorNet's contraction path optimisation algorithm.
\n","All simulations realised with this submodule are *exact*. Once the pytket circuit has been converted to a tensor network, the computation has two steps:
\n"," 1. *Contraction path optimisation*. Attempts to find an order of contracting pairs of tensors in which the the total number of FLOPs is minimised. No operation on the tensor network occurs at this point. Runs on CPU.
\n"," 2. *Tensor network contraction*. Uses the ordering of contractions found in the previous step evaluate the tensor network. Runs on GPU.
\n","
\n","**Reference**: The original contraction path optimisation algorithm that NVIDIA implemented on CuTensorNet: https://arxiv.org/abs/2002.01935"]},{"cell_type":"markdown","metadata":{},"source":["## `GeneralState`
\n","The class `GeneralState` is used to convert a circuit into a tensor network and query information from the final state. Let's walk through a simple example."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["my_circ = Circuit(5)\n","my_circ.CX(3, 4)\n","my_circ.H(2)\n","my_circ.CZ(0, 1)\n","my_circ.ZZPhase(0.1, 4, 3)\n","my_circ.TK2(0.3, 0.5, 0.7, 2, 1)\n","my_circ.Ry(0.2, 0)\n","my_circ.measure_all()"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(my_circ)"]},{"cell_type":"markdown","metadata":{},"source":["The first step is to convert our pytket circuit into a tensor network. This is straightforward:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["tn_state = GeneralState(my_circ)"]},{"cell_type":"markdown","metadata":{},"source":["The variable `tn_state` now holds a tensor network representation of `my_circ`.
\n","**Note**: Circuits must not have mid-circuit measurements or classical logic. The measurements at the end of the circuit are stripped and only considered when calling `tn_state.sample(n_shots)`.
\n","We can now query information from the state. For instance, let's calculate the probability of in the qubits 0 and 3 agreeing in their outcome."]},{"cell_type":"markdown","metadata":{},"source":["First, let's generate `|x>` computational basis states where `q[0]` and `q[3]` agree on their values. We can do this with some bitwise operators and list comprehension.
\n","**Note**: Remember that pytket uses \"increasing lexicographic order\" (ILO) for qubits, so `q[0]` is the most significant bit."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["selected_states = [\n"," x\n"," for x in range(2**my_circ.n_qubits)\n"," if ( # Iterate over all possible states\n"," x & int(\"10000\", 2) == 0\n"," and x & int(\"00010\", 2) == 0 # both qubits are 0 or...\n"," or x & int(\"10000\", 2) != 0\n"," and x & int(\"00010\", 2) != 0 # both qubits are 1\n"," )\n","]"]},{"cell_type":"markdown","metadata":{},"source":["We can now query the amplitude of all of these states and calculate the probability by summing their squared absolute values."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["amplitudes = []\n","for x in selected_states:\n"," amplitudes.append(tn_state.get_amplitude(x))\n","probability = sum(abs(a) ** 2 for a in amplitudes)\n","print(f\"Probability: {probability}\")"]},{"cell_type":"markdown","metadata":{},"source":["Of course, calculating probabilities by considering the amplitudes of all relevant states is not efficient in general, since we may need to calculate a number of amplitudes that scales exponentially with the number of qubits. An alternative is to use expectation values. In particular, all of the states in `selected_states` are +1 eigenvectors of the `ZIIZI` observable and, hence, we can calculate the probability `p` by solving the equation ` = (+1)p + (-1)(1-p)` using the fact that `ZIIZI` only has +1 and -1 eigenvalues."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["string_ZIIZI = QubitPauliString(\n"," my_circ.qubits, [Pauli.Z, Pauli.I, Pauli.I, Pauli.Z, Pauli.I]\n",")\n","observable = QubitPauliOperator({string_ZIIZI: 1.0})\n","expectation_val = tn_state.expectation_value(observable).real\n","exp_probability = (expectation_val + 1) / 2\n","assert np.isclose(probability, exp_probability, atol=0.0001)\n","print(f\"Probability: {exp_probability}\")"]},{"cell_type":"markdown","metadata":{},"source":["Alternatively, we can estimate the probability by sampling."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_shots = 100000\n","outcomes = tn_state.sample(n_shots)\n","hit_count = 0\n","for bit_tuple, count in outcomes.get_counts().items():\n"," if bit_tuple[0] == bit_tuple[3]:\n"," hit_count += count\n","samp_probability = hit_count / n_shots\n","assert np.isclose(probability, samp_probability, atol=0.01)\n","print(f\"Probability: {samp_probability}\")"]},{"cell_type":"markdown","metadata":{},"source":["When we finish doing computations with the `tn_state` we must destroy it to free GPU memory."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["tn_state.destroy()"]},{"cell_type":"markdown","metadata":{},"source":["To avoid forgetting this final step, we recommend users call `GeneralState` (and `GeneralBraOpKet`) as context managers:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with GeneralState(my_circ) as my_state:\n"," expectation_val = my_state.expectation_value(observable)\n","print(expectation_val)"]},{"cell_type":"markdown","metadata":{},"source":["Using this syntax, `my_state` is automatically destroyed when the code exists the `with ...` block."]},{"cell_type":"markdown","metadata":{},"source":["## Parameterised circuits
\n","Circuits that only differ on the parameters of their gates have the same tensor network topology and, hence, we may use the same contraction path for all of them."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["a, b, c = Symbol(\"a\"), Symbol(\"b\"), Symbol(\"c\")\n","param_circ1 = Circuit(5)\n","param_circ1.Ry(a, 3).Ry(0.27, 4).CX(4, 3).Ry(b, 2).Ry(0.21, 3)\n","param_circ1.Ry(0.12, 0).Ry(a, 1)\n","param_circ1.add_gate(OpType.CnX, [0, 1, 4]).add_gate(OpType.CnX, [4, 1, 3])\n","param_circ1.X(0).X(1).add_gate(OpType.CnY, [0, 1, 2]).add_gate(OpType.CnY, [0, 4, 3]).X(\n"," 0\n",").X(1)\n","param_circ1.Ry(-b, 0).Ry(-c, 1)\n","render_circuit_jupyter(param_circ1)"]},{"cell_type":"markdown","metadata":{},"source":["We can pass a parameterised circuit to `GeneralState`. The value of the parameters is provided when calling methods of `GeneralState`. The contraction path is automatically reused on different calls to the same method."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_circs = 5\n","with GeneralState(param_circ1) as param_state:\n"," for i in range(n_circs):\n"," symbol_map = {s: np.random.random() for s in [a, b, c]}\n"," exp_val = param_state.expectation_value(observable, symbol_map=symbol_map)\n"," print(f\"Expectation value for circuit {i}: {exp_val.real}\")"]},{"cell_type":"markdown","metadata":{},"source":["## `GeneralBraOpKet`
\n","\n","The `GeneralBraOpKet` can be used to calculate any number that can be represented as the result of some `` where `|bra>` and `|ket>` are the final states of pytket circuits, and `op` is a `QubitPauliOperator`. The circuits for `|bra>` and `|ket>` need not be the same."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["x, y, z = Symbol(\"x\"), Symbol(\"y\"), Symbol(\"z\")\n","param_circ2 = Circuit(5)\n","param_circ2.H(0)\n","param_circ2.S(1)\n","param_circ2.Rz(x * z, 2)\n","param_circ2.Ry(y + x, 3)\n","param_circ2.TK1(x, y, z, 4)\n","param_circ2.TK2(z - y, z - x, (x + y) * z, 1, 3)\n","symbol_map = {a: 2.1, b: 1.3, c: 0.7, x: 3.0, y: 1.6, z: -8.3}"]},{"cell_type":"markdown","metadata":{},"source":["We can calculate inner products by providing no `op`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with GeneralBraOpKet(bra=param_circ2, ket=param_circ1) as braket:\n"," inner_prod = braket.contract(symbol_map=symbol_map)\n","with GeneralBraOpKet(bra=param_circ1, ket=param_circ2) as braket:\n"," inner_prod_conj = braket.contract(symbol_map=symbol_map)\n","assert np.isclose(np.conj(inner_prod), inner_prod_conj)\n","print(f\" = {inner_prod}\")\n","print(f\" = {inner_prod_conj}\")"]},{"cell_type":"markdown","metadata":{},"source":["And we are not constrained to Hermitian operators:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["string_XZIXX = QubitPauliString(\n"," param_circ2.qubits, [Pauli.X, Pauli.Z, Pauli.I, Pauli.X, Pauli.X]\n",")\n","string_IZZYX = QubitPauliString(\n"," param_circ2.qubits, [Pauli.I, Pauli.Z, Pauli.Z, Pauli.Y, Pauli.X]\n",")\n","string_ZIZXY = QubitPauliString(\n"," param_circ2.qubits, [Pauli.Z, Pauli.I, Pauli.Z, Pauli.X, Pauli.Y]\n",")\n","operator = QubitPauliOperator(\n"," {string_XZIXX: -1.38j, string_IZZYX: 2.36, string_ZIZXY: 0.42j + 0.3}\n",")\n","with GeneralBraOpKet(bra=param_circ2, ket=param_circ1) as braket:\n"," value = braket.contract(operator, symbol_map=symbol_map)\n","print(value)"]},{"cell_type":"markdown","metadata":{},"source":["## Backends
\n","We provide a pytket `Backend` to obtain shots using `GeneralState`."]},{"cell_type":"markdown","metadata":{},"source":["Let's consider a more challenging circuit"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def random_circuit(n_qubits: int, n_layers: int) -> Circuit:\n"," \"\"\"Random quantum volume circuit.\"\"\"\n"," c = Circuit(n_qubits, n_qubits)\n"," for _ in range(n_layers):\n"," qubits = np.random.permutation([i for i in range(n_qubits)])\n"," qubit_pairs = [[qubits[i], qubits[i + 1]] for i in range(0, n_qubits - 1, 2)]\n"," for pair in qubit_pairs:\n"," # Generate random 4x4 unitary matrix.\n"," SU4 = unitary_group.rvs(4) # random unitary in SU4\n"," SU4 = SU4 / (np.linalg.det(SU4) ** 0.25)\n"," SU4 = np.matrix(SU4)\n"," c.add_unitary2qbox(Unitary2qBox(SU4), *pair)\n"," DecomposeBoxes().apply(c)\n"," return c"]},{"cell_type":"markdown","metadata":{},"source":["Let's measure only three of the qubits.
\n","**Note**: The complexity of this simulation increases exponentially with the number of qubits measured. Other factors leading to intractability are circuit depth and qubit connectivity."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_shots = 1000\n","quantum_vol_circ = random_circuit(n_qubits=40, n_layers=5)\n","quantum_vol_circ.Measure(Qubit(0), Bit(0))\n","quantum_vol_circ.Measure(Qubit(1), Bit(1))\n","quantum_vol_circ.Measure(Qubit(2), Bit(2))"]},{"cell_type":"markdown","metadata":{},"source":["The `CuTensorNetShotsBackend` is used in the same way as any other pytket `Backend`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["backend = CuTensorNetShotsBackend()\n","compiled_circ = backend.get_compiled_circuit(quantum_vol_circ)\n","results = backend.run_circuit(compiled_circ, n_shots=n_shots)\n","print(results.get_counts())"]}],"metadata":{"kernelspec":{"display_name":".venv","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.1"}},"nbformat":4,"nbformat_minor":2} diff --git a/docs/examples/mps_tutorial.ipynb b/docs/examples/mps_tutorial.ipynb deleted file mode 100644 index 21c30098..00000000 --- a/docs/examples/mps_tutorial.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Matrix Product State (MPS) Tutorial"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import numpy as np\n","from time import time\n","import matplotlib.pyplot as plt\n","from pytket import Circuit, OpType\n","from pytket.circuit.display import render_circuit_jupyter"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.cutensornet.structured_state import (\n"," CuTensorNetHandle,\n"," Config,\n"," SimulationAlgorithm,\n"," simulate,\n"," prepare_circuit_mps,\n",")"]},{"cell_type":"markdown","metadata":{},"source":["## Introduction
\n","This notebook provides examples of the usage of the MPS functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.
\n","A Matrix Product State (MPS) represents a state on `n` qubits as a list of `n` tensors connected in a line as show below:
\n","![MPS](images/mps.png)
\n","Each of these circles corresponds to a tensor. We refer to each leg of a tensor as a *bond* and the number of bonds a tensor has is its *rank*. In code, a tensor is just a multidimensional array:
\n","
\n","```tensor[i][j][k] = v```
\n","
\n","In the case above, we are assigning an entry value `v` of a rank-3 tensor (one `[ ]` coordinate per bond). Each bond allows a different number of values for its indices; for instance `0 <= i < 4` would mean that the first bond of our tensor can take up to four different indices; we refer to this as the *dimension* of the bond. We refer to the bonds connecting different tensors in the MPS as *virtual bonds*; the maximum allowed value for the dimension of virtual bonds is often denoted by the greek letter `chi`. The open bonds are known as *physical bonds* and, in our case, each will correspond to a qubit; hence, they have dimension `2` -- the dimension of the vector space of a single qubit.
\n","In essence, whenever we want to apply a gate to certain qubit we will connect a tensor (matrix) representing the gate to the corresponding physical bond and *contract* the network back to an MPS form (tensor contraction is a generalisation of matrix multiplication to multidimensional arrays). Whenever a two-qubit gate is applied, the entanglement information after contraction will be kept in the degrees of freedom of the virtual bonds. As such, the dimension of the virtual bonds will generally increase exponentially as we apply entangling gates, leading to large memory footprints of the tensors and, consequently, long runtime for tensor contraction. We provide functionalities to limit the growth of the dimension of the virtual bonds, keeping resource consumption in check. Read the *Approximate simulation* section on this notebook to learn more.
\n","**References**: To read more about MPS we recommend the following papers.
\n","* For an introduction to MPS and its canonical form: https://arxiv.org/abs/1901.05824.
\n","* For a description of the `MPSxGate` algorithm we provide: https://arxiv.org/abs/2002.07730.
\n","* For a description of the `MPSxMPO` algorithm we provide: https://arxiv.org/abs/2207.05612.
\n","* For insights on the reationship between truncation error and the error model in a quantum computer: https://arxiv.org/abs/2004.02388"]},{"cell_type":"markdown","metadata":{},"source":["## Basic functionality and exact simulation
\n","Here we show an example of the basic use of our MPS methods. We first generate a simple `pytket` circuit to be simulated."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["my_circ = Circuit(5)\n","my_circ.CX(3, 4)\n","my_circ.H(2)\n","my_circ.CZ(0, 1)\n","my_circ.ZZPhase(0.1, 4, 3)\n","my_circ.TK2(0.3, 0.5, 0.7, 2, 1)\n","my_circ.Ry(0.2, 0)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(my_circ)"]},{"cell_type":"markdown","metadata":{},"source":["For **exact** simulation, simply call the `simulate` function on the circuit and choose a contraction algorithm. To learn more about the contraction algorithms we provide see the *Contraction algorithms* section of this notebook. You will also need to provide a configuration, the default one is provided by `Config()`. Custom settings of `Config` are discussed in the *Approximate simulation* section.
\n","**NOTE**: whenever you wish to generate an `MPS` object or execute calculations on it you must do so within a `with CuTensorNetHandle() as libhandle:` block; this will initialise the cuTensorNetwork library for you, and destroy its handles at the end of the `with` block. You will need to pass the `libhandle` to the `MPS` object via the method that generates it (in the snippet below, `simulate`), or if already initialised, pass it via the `update_libhandle` method.
\n","Due to the nature of Jupyter notebooks, we will be starting most of these cells with a `with CuTensorNetHandle() as libhandle:`. However, in a standard script, all of these cells would be grouped together and a single `with CuTensorNetHandle() as libhandle:` statement would be necessary at the beginning of the script."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," my_mps = simulate(libhandle, my_circ, SimulationAlgorithm.MPSxGate, Config())"]},{"cell_type":"markdown","metadata":{},"source":["Notice that `my_circ` uses a rich gateset -- in fact, every single-qubit and two-qubit gate supported by `pytket` can be used in our MPS approaches. Gates acting on more than two qubits are not currently supported."]},{"cell_type":"markdown","metadata":{},"source":["The output of `simulate` is an `MPS` object encoding the output state of the circuit.
\n","### Obtain an amplitude from an MPS
\n","Let's first see how to get the amplitude of the state `|10100>` from the output of the previous circuit."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["state = int(\"10100\", 2)\n","with CuTensorNetHandle() as libhandle:\n"," my_mps.update_libhandle(libhandle)\n"," amplitude = my_mps.get_amplitude(state)\n","print(amplitude)"]},{"cell_type":"markdown","metadata":{},"source":["Since this is a very small circuit, we can use `pytket`'s state vector simulator capabilities to verify that the state is correct by checking the amplitude of each of the computational states."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["state_vector = my_circ.get_statevector()\n","n_qubits = len(my_circ.qubits)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["correct_amplitude = [False] * (2**n_qubits)\n","with CuTensorNetHandle() as libhandle:\n"," my_mps.update_libhandle(libhandle)\n"," for i in range(2**n_qubits):\n"," correct_amplitude[i] = np.isclose(state_vector[i], my_mps.get_amplitude(i))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(\"Are all amplitudes correct?\")\n","print(all(correct_amplitude))"]},{"cell_type":"markdown","metadata":{},"source":["### Sampling from an MPS
\n","We can also sample from the output state of a circuit by calling `my_mps.sample`, where `my_mps` is the outcome of simulating the circuit."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_samples = 100\n","n_qubits = len(my_circ.qubits)"]},{"cell_type":"markdown","metadata":{},"source":["Initialise the sample counter"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["sample_count = [0 for _ in range(2**n_qubits)]"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," my_mps.update_libhandle(libhandle)\n"," for _ in range(n_samples):\n"," # Draw a sample\n"," qubit_outcomes = my_mps.sample()\n"," # Convert qubit outcomes to bitstring\n"," bitstring = \"\".join(str(qubit_outcomes[q]) for q in my_circ.qubits)\n"," # Convert bitstring to int\n"," outcome = int(bitstring, 2)\n"," # Update the sample dictionary\n"," sample_count[outcome] += 1"]},{"cell_type":"markdown","metadata":{},"source":["Calculate the theoretical number of samples per bitstring"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["expected_count = [n_samples * abs(state_vector[i]) ** 2 for i in range(2**n_qubits)]"]},{"cell_type":"markdown","metadata":{},"source":["Plot a comparison of theory vs sampled"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["plt.scatter(range(2**n_qubits), expected_count, label=\"Theory\")\n","plt.scatter(range(2**n_qubits), sample_count, label=\"Experiment\", marker=\"x\")\n","plt.xlabel(\"Basis states\")\n","plt.ylabel(\"Samples\")\n","plt.legend()\n","plt.show()"]},{"cell_type":"markdown","metadata":{},"source":["We also provide methods to apply mid-circuit measurements via `my_mps.measure(qubits)` and postselection via `my_mps.postselect(qubit_outcomes)`. Their use is similar to that of `my_mps.sample()` shown above.
\n","**Note:** whereas `my_mps.sample()` does *not* change the state of the MPS, `my_mps.measure(qubits)` and `my_mps.postselect(qubit_outcomes)` do change it, projecting the state to the resulting outcome and removing the measured qubits."]},{"cell_type":"markdown","metadata":{},"source":["### Inner products
\n","Using `vdot` you can obtain the inner product of two states in MPS form. This method does not change the internal data of neither of the MPS. Moreover, it can be used on the same `MPS` object for both inputs, yielding the squared norm of the state."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," my_mps.update_libhandle(libhandle)\n"," norm_sq = my_mps.vdot(my_mps)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(\"As expected, the squared norm of a state is 1\")\n","print(np.isclose(norm_sq, 1))"]},{"cell_type":"markdown","metadata":{},"source":["Let's come up with another circuit on the same qubits and apply an inner product between the two `MPS` objects."]},{"cell_type":"markdown","metadata":{},"source":["Generate circuits"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["other_circ = Circuit(5)\n","other_circ.H(3)\n","other_circ.CZ(3, 4)\n","other_circ.XXPhase(0.3, 1, 2)\n","other_circ.Ry(0.7, 3)"]},{"cell_type":"markdown","metadata":{},"source":["Simulate them"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," other_mps = simulate(libhandle, other_circ, SimulationAlgorithm.MPSxGate, Config())"]},{"cell_type":"markdown","metadata":{},"source":["Let's calculate the inner product and check that it agrees with `pytket`'s state vector based computation."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," my_mps.update_libhandle(libhandle)\n"," inner_product = my_mps.vdot(other_mps)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["my_state = my_circ.get_statevector()\n","other_state = other_circ.get_statevector()"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(\"Is the inner product correct?\")\n","print(np.isclose(np.vdot(my_state, other_state), inner_product))"]},{"cell_type":"markdown","metadata":{},"source":["### Mid-circuit measurements and classical control
\n","Mid-circuit measurements and classical control is supported (only in `MPSxGate` as of v0.8.0). For instance, we can implement the teleportation protocol on a pytket circuit and simulate it:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ = Circuit()\n","alice = circ.add_q_register(\"alice\", 2)\n","alice_bits = circ.add_c_register(\"alice_bits\", 2)\n","bob = circ.add_q_register(\"bob\", 1)\n","# Initialise Alice's first qubit in some arbitrary state\n","circ.Rx(0.42, alice[0])\n","orig_state = circ.get_statevector()\n","# Create a Bell pair shared between Alice and Bob\n","circ.H(alice[1]).CX(alice[1], bob[0])\n","# Apply a Bell measurement on Alice's qubits\n","circ.CX(alice[0], alice[1]).H(alice[0])\n","circ.Measure(alice[0], alice_bits[0])\n","circ.Measure(alice[1], alice_bits[1])\n","# Apply conditional corrections on Bob's qubits\n","circ.add_gate(OpType.X, [bob[0]], condition_bits=[alice_bits[1]], condition_value=1)\n","circ.add_gate(OpType.Z, [bob[0]], condition_bits=[alice_bits[0]], condition_value=1)\n","# Reset Alice's qubits\n","circ.add_gate(OpType.Reset, [alice[0]])\n","circ.add_gate(OpType.Reset, [alice[1]])\n","# Display the circuit\n","render_circuit_jupyter(circ)"]},{"cell_type":"markdown","metadata":{},"source":["We can now simulate the circuit and check that the qubit has been successfully teleported."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(\n"," f\"Initial state:\\n {np.round(orig_state[0],2)}|00>|0> + {np.round(orig_state[4],2)}|10>|0>\"\n",")\n","with CuTensorNetHandle() as libhandle:\n"," state = simulate(libhandle, circ, SimulationAlgorithm.MPSxGate, Config())\n"," print(\n"," f\"Teleported state:\\n {np.round(state.get_amplitude(0),2)}|00>|0> + {np.round(state.get_amplitude(1),2)}|00>|1>\"\n"," )\n"," print(f\"Measurement outcomes:\\n {state.get_bits()}\")"]},{"cell_type":"markdown","metadata":{},"source":["### Two-qubit gates acting on non-adjacent qubits
\n","Standard MPS algorithms only support simulation of two-qubit gates acting on neighbour qubits. In our implementation, however, two-qubit gates between arbitrary qubits may be applied, as shown below."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ = Circuit(5)\n","circ.H(1)\n","circ.ZZPhase(0.3, 1, 3)\n","circ.CX(0, 2)\n","circ.Ry(0.8, 4)\n","circ.CZ(3, 4)\n","circ.XXPhase(0.7, 1, 2)\n","circ.TK2(0.1, 0.2, 0.4, 1, 4)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(circ)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," mps = simulate(libhandle, circ, SimulationAlgorithm.MPSxGate, Config())\n"," print(\"Did simulation succeed?\")\n"," print(mps.is_valid())"]},{"cell_type":"markdown","metadata":{},"source":["**Note**: Even though two-qubit gates on non-adjacent qubits are simulable, the overhead on these is considerably larger than simulating gates on adjacent qubits. As a rule of thumb if the two qubits are `n` positions apart, the overhead is upper bounded by the cost of simulating `n-1` additional SWAP gates to move the leftmost qubit near the rightmost. In reality, the implementation we use is more nuanced than just applying SWAP gates, and the qubits don't actually change position.
\n","When circuits are shallow, using our approach to simulate long-distance two-qubit gates is advantageous. In the case of deep circuits with many long-distance gates, it is sometimes beneficial to use TKET routing on the circuit, explicitly adding SWAP gates so that all two-qubit gates act on nearest neighbour qubits. Users may do this by calling `prepare_circuit_mps`, which is a wrapper of the corresponding TKET routing pass."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["prepared_circ, qubit_map = prepare_circuit_mps(circ)\n","render_circuit_jupyter(prepared_circ)"]},{"cell_type":"markdown","metadata":{},"source":["The circuit can now be simulated as usual."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," mps = simulate(libhandle, prepared_circ, SimulationAlgorithm.MPSxGate, Config())\n"," print(\"Did simulation succeed?\")\n"," print(mps.is_valid())"]},{"cell_type":"markdown","metadata":{},"source":["Notice that the qubits in the `prepared_circ` were renamed when applying `prepare_circuit_mps`. Implicit SWAPs may have been added to the circuit, meaning that the logical qubit held at the ``node[i]`` qubit at the beginning of the circuit may differ from the one it holds at the end; this information is captured by the `qubit_map` output. We recommend applying ``apply_qubit_relabelling`` on the MPS after simulation, relabelling the qubits according to these implicit SWAPs."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(qubit_map)\n","mps.apply_qubit_relabelling(qubit_map)"]},{"cell_type":"markdown","metadata":{},"source":["## Approximate simulation
\n","We provide two policies for approximate simulation; these are supported by both of our current MPS contraction algorithms:
\n","* Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes.
\n","* Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate.
\n","Values for `chi` and `truncation_fidelity` can be set via `Config`. To showcase approximate simulation, let's define a circuit where exact MPS contraction starts struggling."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def random_line_circuit(n_qubits: int, layers: int) -> Circuit:\n"," \"\"\"Random circuit with line connectivity.\"\"\"\n"," c = Circuit(n_qubits)\n"," for i in range(layers):\n"," # Layer of TK1 gates\n"," for q in range(n_qubits):\n"," c.TK1(np.random.rand(), np.random.rand(), np.random.rand(), q)\n","\n"," # Layer of CX gates\n"," offset = np.mod(i, 2) # Even layers connect (q0,q1), odd (q1,q2)\n"," qubit_pairs = [\n"," [c.qubits[i], c.qubits[i + 1]] for i in range(offset, n_qubits - 1, 2)\n"," ]\n"," # Direction of each CX gate is random\n"," for pair in qubit_pairs:\n"," np.random.shuffle(pair)\n"," for pair in qubit_pairs:\n"," c.CX(pair[0], pair[1])\n"," return c"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circuit = random_line_circuit(n_qubits=20, layers=20)"]},{"cell_type":"markdown","metadata":{},"source":["For exact contraction, `chi` must be allowed to be up to `2**(n_qubits // 2)`, meaning that if we set `n_qubits = 20` it would require `chi = 1024`; already too much for this particular circuit to be simulated in a gaming laptop using the current implementation. Instead, let's bound `chi` to a maximum of `16`. Doing so results in faster runtime, at the expense of losing output state fidelity."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(chi=16)\n"," bound_chi_mps = simulate(libhandle, circuit, SimulationAlgorithm.MPSxGate, config)\n","end = time()\n","print(\"Time taken by approximate contraction with bound chi:\")\n","print(f\"{round(end-start,2)} seconds\")\n","print(\"\\nLower bound of the fidelity:\")\n","print(round(bound_chi_mps.fidelity, 4))"]},{"cell_type":"markdown","metadata":{},"source":["Alternatively, we can fix `truncation_fidelity` and let `chi` increase as necessary to satisfy it."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(truncation_fidelity=0.999)\n"," fixed_fidelity_mps = simulate(\n"," libhandle, circuit, SimulationAlgorithm.MPSxGate, config\n"," )\n","end = time()\n","print(\"Time taken by approximate contraction with fixed truncation fidelity:\")\n","print(f\"{round(end-start,2)} seconds\")\n","print(\"\\nLower bound of the fidelity:\")\n","print(round(fixed_fidelity_mps.fidelity, 4))"]},{"cell_type":"markdown","metadata":{},"source":["## Contraction algorithms"]},{"cell_type":"markdown","metadata":{},"source":["We currently offer two MPS-based simulation algorithms:
\n","* **MPSxGate**: Apply gates one by one to the MPS, canonicalising the MPS and truncating when necessary. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2002.07730.
\n","* **MPSxMPO**: Maintain two MPS copies of the state as it evolves, one updated eagerly using the **MPSxGate** method and the other updated in batches of up to `k` layers of two-qubit gates. Whenever the second MPS is updated, both copies are synchronised and an optimisation algorithm is applied to increase the fidelity of the state. This algorithm is often referred to as DMRG-like simulation. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2207.05612.
\n","The `MPSxGate` algorithm is the one we have been using for all of the examples above. In comparison, the `MPSxMPO` algorithm provides the user with two new parameters to tune:
\n","* **k**: The maximum number of layers the MPO is allowed to have before being contracted. Increasing this might increase fidelity, but it will also increase resource requirements exponentially. Default value is `4`.
\n","* **optim_delta**: Stopping criteria for the optimisation when contracting the `k` layers of MPO. Stops when the increase of fidelity between iterations is smaller than `optim_delta`. Default value is `1e-5`.
\n","Both `k` and `optim_delta` can be set via `Config`. Below we compare `MPSxGate` versus `MPSxMPO` with default parameters and `MPSxMPO` with more resource-hungry parameters. The circuit used is the same as in the previous section."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(chi=16)\n"," fixed_fidelity_mps = simulate(\n"," libhandle, circuit, SimulationAlgorithm.MPSxGate, config\n"," )\n","end = time()\n","print(\"MPSxGate\")\n","print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n","print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(chi=16)\n"," fixed_fidelity_mps = simulate(\n"," libhandle, circuit, SimulationAlgorithm.MPSxMPO, config\n"," )\n","end = time()\n","print(\"MPSxMPO, default parameters\")\n","print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n","print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(k=8, optim_delta=1e-15, chi=16)\n"," fixed_fidelity_mps = simulate(\n"," libhandle, circuit, SimulationAlgorithm.MPSxMPO, config\n"," )\n","end = time()\n","print(\"MPSxMPO, custom parameters\")\n","print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n","print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]},{"cell_type":"markdown","metadata":{},"source":["**Note**: `MPSxMPO` also admits truncation policy in terms of `truncation_fidelity` instead of `chi`."]},{"cell_type":"markdown","metadata":{},"source":["## Using the logger"]},{"cell_type":"markdown","metadata":{},"source":["You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent):
\n","- `logging.INFO` will print information about progress percent, memory currently occupied by the MPS and current fidelity. Additionally, some high level information of the current stage of the simulation is provided, such as when `MPSxMPO` is applying optimisation sweeps.
\n","- `logging.DEBUG` provides all of the messages from the loglevel above plus detailed information of the current operation being carried out and the values of important variables.
\n","**Note**: Due to technical issues with the `logging` module and Jupyter notebooks we need to reload the `logging` module. When working with python scripts and command line, just doing `import logging` is enough."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from importlib import reload # Not needed in Python 2\n","import logging"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["reload(logging)"]},{"cell_type":"markdown","metadata":{},"source":["An example of the use of `logging.INFO` is provided below."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," config = Config(truncation_fidelity=0.999, loglevel=logging.INFO)\n"," simulate(libhandle, circuit, SimulationAlgorithm.MPSxMPO, config)"]}],"metadata":{"kernelspec":{"display_name":".venv","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.1"}},"nbformat":4,"nbformat_minor":2} diff --git a/docs/examples/ttn_tutorial.ipynb b/docs/examples/ttn_tutorial.ipynb deleted file mode 100644 index 6e6fdb20..00000000 --- a/docs/examples/ttn_tutorial.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Tree Tensor Network Tutotial"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import numpy as np\n","from time import time\n","import matplotlib.pyplot as plt\n","import networkx as nx\n","from pytket import Circuit\n","from pytket.circuit.display import render_circuit_jupyter"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.cutensornet.structured_state import (\n"," CuTensorNetHandle,\n"," Config,\n"," SimulationAlgorithm,\n"," simulate,\n",")"]},{"cell_type":"markdown","metadata":{},"source":["## Introduction
\n","This notebook provides examples of the usage of the TTN functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.
\n","Some good references to learn about Tree Tensor Network state simulation:
\n","- For an introduction into TTN based simulation of quantum circuits: https://arxiv.org/abs/2206.01000
\n","- For an introduction on some of the optimisation concerns that are relevant to TTN: https://arxiv.org/abs/2209.03196
\n","The implementation in pytket-cutensornet differs from previously published literature. I am still experimenting with the algorithm. I intend to write up a document detailing the approach, once I reach a stable version.
\n","The main advantage of TTN over MPS is that it can be used to efficiently simulate circuits with richer qubit connectivity. This does **not** mean that TTN has an easy time simulating all-to-all connectivity, but it is far more flexible than MPS. TTN's strength is in simulating circuit where certain subsets of qubits interact densely with each other, and there is not that many gates acting on qubits in different subsets."]},{"cell_type":"markdown","metadata":{},"source":["## How to use
\n","The interface for TTN matches that of MPS. As such, you should be able to run any code that uses `SimulationAlgorithm.MPSxGate` by replacing it with `SimulationAlgorithm.TTNxGate`. Calling `prepare_circuit_mps` is no longer necessary, since `TTNxGate` can apply gates between non-neighbouring qubits.
\n","**NOTE**: If you are new to pytket-cutensornet, it is highly recommended to start reading the `mps_tutorial.ipynb` notebook instead. More details about the use of the library are discussed there (for instance, why and when to call `CuTensorNetHandle()`)."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def random_graph_circuit(n_qubits: int, edge_prob: float, layers: int) -> Circuit:\n"," \"\"\"Random circuit with qubit connectivity determined by a random graph.\"\"\"\n"," c = Circuit(n_qubits)\n"," for i in range(layers):\n"," # Layer of TK1 gates\n"," for q in range(n_qubits):\n"," c.TK1(np.random.rand(), np.random.rand(), np.random.rand(), q)\n","\n"," # Layer of CX gates\n"," graph = nx.erdos_renyi_graph(n_qubits, edge_prob, directed=True)\n"," qubit_pairs = list(graph.edges)\n"," for pair in qubit_pairs:\n"," c.CX(pair[0], pair[1])\n"," return c"]},{"cell_type":"markdown","metadata":{},"source":["For **exact** simulation, you can call `simulate` directly, providing the default `Config()`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["simple_circ = random_graph_circuit(n_qubits=10, edge_prob=0.1, layers=1)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with CuTensorNetHandle() as libhandle:\n"," my_ttn = simulate(libhandle, simple_circ, SimulationAlgorithm.TTNxGate, Config())"]},{"cell_type":"markdown","metadata":{},"source":["## Obtain an amplitude from a TTN
\n","Let's first see how to get the amplitude of the state `|10100>` from the output of the previous circuit."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["state = int(\"10100\", 2)\n","with CuTensorNetHandle() as libhandle:\n"," my_ttn.update_libhandle(libhandle)\n"," amplitude = my_ttn.get_amplitude(state)\n","print(amplitude)"]},{"cell_type":"markdown","metadata":{},"source":["Since this is a very small circuit, we can use `pytket`'s state vector simulator capabilities to verify that the state is correct by checking the amplitude of each of the computational states."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["state_vector = simple_circ.get_statevector()\n","n_qubits = len(simple_circ.qubits)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["correct_amplitude = [False] * (2**n_qubits)\n","with CuTensorNetHandle() as libhandle:\n"," my_ttn.update_libhandle(libhandle)\n"," for i in range(2**n_qubits):\n"," correct_amplitude[i] = np.isclose(state_vector[i], my_ttn.get_amplitude(i))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(\"Are all amplitudes correct?\")\n","print(all(correct_amplitude))"]},{"cell_type":"markdown","metadata":{},"source":["## Sampling from a TTN
\n","Sampling and measurement from a TTN state is not currently supported. This will be added in an upcoming release."]},{"cell_type":"markdown","metadata":{},"source":["## Approximate simulation
\n","We provide two policies for approximate simulation:
\n","* Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes.
\n","* Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate.
\n","Values for `chi` and `truncation_fidelity` can be set via `Config`. To showcase approximate simulation, let's define a circuit where exact TTN contraction would not be enough."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circuit = random_graph_circuit(n_qubits=30, edge_prob=0.1, layers=1)"]},{"cell_type":"markdown","metadata":{},"source":["We can simulate it using bounded `chi` as follows:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(chi=64, float_precision=np.float32)\n"," bound_chi_ttn = simulate(libhandle, circuit, SimulationAlgorithm.TTNxGate, config)\n","end = time()\n","print(\"Time taken by approximate contraction with bounded chi:\")\n","print(f\"{round(end-start,2)} seconds\")\n","print(\"\\nLower bound of the fidelity:\")\n","print(round(bound_chi_ttn.fidelity, 4))"]},{"cell_type":"markdown","metadata":{},"source":["Alternatively, we can fix `truncation_fidelity` and let the bond dimension increase as necessary to satisfy it."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["start = time()\n","with CuTensorNetHandle() as libhandle:\n"," config = Config(truncation_fidelity=0.99, float_precision=np.float32)\n"," fixed_fidelity_ttn = simulate(\n"," libhandle, circuit, SimulationAlgorithm.TTNxGate, config\n"," )\n","end = time()\n","print(\"Time taken by approximate contraction with fixed truncation fidelity:\")\n","print(f\"{round(end-start,2)} seconds\")\n","print(\"\\nLower bound of the fidelity:\")\n","print(round(fixed_fidelity_ttn.fidelity, 4))"]},{"cell_type":"markdown","metadata":{},"source":["## Contraction algorithms"]},{"cell_type":"markdown","metadata":{},"source":["We currently offer only one TTN-based simulation algorithm.
\n","* **TTNxGate**: Apply gates one by one to the TTN, canonicalising the TTN and truncating when necessary."]},{"cell_type":"markdown","metadata":{},"source":["## Using the logger"]},{"cell_type":"markdown","metadata":{},"source":["You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent):
\n","- `logging.INFO` will print information about progress percent, memory currently occupied by the TTN and current fidelity. Additionally, some high level information of the current stage of the simulation is provided.
\n","- `logging.DEBUG` provides all of the messages from the loglevel above plus detailed information of the current operation being carried out and the values of important variables.
\n","**Note**: Due to technical issues with the `logging` module and Jupyter notebooks we need to reload the `logging` module. When working with python scripts and command line, just doing `import logging` is enough."]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.6.4"}},"nbformat":4,"nbformat_minor":2} diff --git a/docs/examples/README.md b/examples/README.md similarity index 100% rename from docs/examples/README.md rename to examples/README.md diff --git a/docs/examples/check-examples b/examples/check-examples similarity index 100% rename from docs/examples/check-examples rename to examples/check-examples diff --git a/docs/examples/ci-tested-notebooks.txt b/examples/ci-tested-notebooks.txt similarity index 100% rename from docs/examples/ci-tested-notebooks.txt rename to examples/ci-tested-notebooks.txt diff --git a/examples/general_state_tutorial.ipynb b/examples/general_state_tutorial.ipynb new file mode 100644 index 00000000..9a041f99 --- /dev/null +++ b/examples/general_state_tutorial.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np\n", "from sympy import Symbol\n", "from scipy.stats import unitary_group # type: ignore\n", "from pytket.circuit import Circuit, OpType, Unitary2qBox, Qubit, Bit\n", "from pytket.passes import DecomposeBoxes\n", "from pytket.utils import QubitPauliOperator\n", "from pytket._tket.pauli import Pauli, QubitPauliString\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.cutensornet.general_state import (\n", " GeneralState,\n", " GeneralBraOpKet,\n", ")\n", "from pytket.extensions.cutensornet.backends import CuTensorNetShotsBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Introduction
\n", "This notebook is a guide on how to use the features provided in the `general_state` submodule of pytket-cutensornet. This submodule is a thin wrapper of CuTensorNet's `NetworkState`, allowing users to convert pytket circuits into tensor networks and use CuTensorNet's contraction path optimisation algorithm.
\n", "All simulations realised with this submodule are *exact*. Once the pytket circuit has been converted to a tensor network, the computation has two steps:
\n", " 1. *Contraction path optimisation*. Attempts to find an order of contracting pairs of tensors in which the the total number of FLOPs is minimised. No operation on the tensor network occurs at this point. Runs on CPU.
\n", " 2. *Tensor network contraction*. Uses the ordering of contractions found in the previous step evaluate the tensor network. Runs on GPU.
\n", "
\n", "**Reference**: The original contraction path optimisation algorithm that NVIDIA implemented on CuTensorNet: https://arxiv.org/abs/2002.01935"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# `GeneralState`
\n", "The class `GeneralState` is used to convert a circuit into a tensor network and query information from the final state. Let's walk through a simple example."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_circ = Circuit(5)\n", "my_circ.CX(3, 4)\n", "my_circ.H(2)\n", "my_circ.CZ(0, 1)\n", "my_circ.ZZPhase(0.1, 4, 3)\n", "my_circ.TK2(0.3, 0.5, 0.7, 2, 1)\n", "my_circ.Ry(0.2, 0)\n", "my_circ.measure_all()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(my_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The first step is to convert our pytket circuit into a tensor network. This is straightforward:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tn_state = GeneralState(my_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The variable `tn_state` now holds a tensor network representation of `my_circ`.
\n", "**Note**: Circuits must not have mid-circuit measurements or classical logic. The measurements at the end of the circuit are stripped and only considered when calling `tn_state.sample(n_shots)`.
\n", "We can now query information from the state. For instance, let's calculate the probability of in the qubits 0 and 3 agreeing in their outcome."]}, {"cell_type": "markdown", "metadata": {}, "source": ["First, let's generate `|x>` computational basis states where `q[0]` and `q[3]` agree on their values. We can do this with some bitwise operators and list comprehension.
\n", "**Note**: Remember that pytket uses \"increasing lexicographic order\" (ILO) for qubits, so `q[0]` is the most significant bit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["selected_states = [\n", " x\n", " for x in range(2**my_circ.n_qubits)\n", " if ( # Iterate over all possible states\n", " x & int(\"10000\", 2) == 0\n", " and x & int(\"00010\", 2) == 0 # both qubits are 0 or...\n", " or x & int(\"10000\", 2) != 0\n", " and x & int(\"00010\", 2) != 0 # both qubits are 1\n", " )\n", "]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now query the amplitude of all of these states and calculate the probability by summing their squared absolute values."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["amplitudes = []\n", "for x in selected_states:\n", " amplitudes.append(tn_state.get_amplitude(x))\n", "probability = sum(abs(a) ** 2 for a in amplitudes)\n", "print(f\"Probability: {probability}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Of course, calculating probabilities by considering the amplitudes of all relevant states is not efficient in general, since we may need to calculate a number of amplitudes that scales exponentially with the number of qubits. An alternative is to use expectation values. In particular, all of the states in `selected_states` are +1 eigenvectors of the `ZIIZI` observable and, hence, we can calculate the probability `p` by solving the equation ` = (+1)p + (-1)(1-p)` using the fact that `ZIIZI` only has +1 and -1 eigenvalues."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["string_ZIIZI = QubitPauliString(\n", " my_circ.qubits, [Pauli.Z, Pauli.I, Pauli.I, Pauli.Z, Pauli.I]\n", ")\n", "observable = QubitPauliOperator({string_ZIIZI: 1.0})\n", "expectation_val = tn_state.expectation_value(observable).real\n", "exp_probability = (expectation_val + 1) / 2\n", "assert np.isclose(probability, exp_probability, atol=0.0001)\n", "print(f\"Probability: {exp_probability}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can estimate the probability by sampling."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots = 100000\n", "outcomes = tn_state.sample(n_shots)\n", "hit_count = 0\n", "for bit_tuple, count in outcomes.get_counts().items():\n", " if bit_tuple[0] == bit_tuple[3]:\n", " hit_count += count\n", "samp_probability = hit_count / n_shots\n", "assert np.isclose(probability, samp_probability, atol=0.01)\n", "print(f\"Probability: {samp_probability}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["When we finish doing computations with the `tn_state` we must destroy it to free GPU memory."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tn_state.destroy()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To avoid forgetting this final step, we recommend users call `GeneralState` (and `GeneralBraOpKet`) as context managers:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with GeneralState(my_circ) as my_state:\n", " expectation_val = my_state.expectation_value(observable)\n", "print(expectation_val)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Using this syntax, `my_state` is automatically destroyed when the code exists the `with ...` block."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Parameterised circuits
\n", "Circuits that only differ on the parameters of their gates have the same tensor network topology and, hence, we may use the same contraction path for all of them."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["a, b, c = Symbol(\"a\"), Symbol(\"b\"), Symbol(\"c\")\n", "param_circ1 = Circuit(5)\n", "param_circ1.Ry(a, 3).Ry(0.27, 4).CX(4, 3).Ry(b, 2).Ry(0.21, 3)\n", "param_circ1.Ry(0.12, 0).Ry(a, 1)\n", "param_circ1.add_gate(OpType.CnX, [0, 1, 4]).add_gate(OpType.CnX, [4, 1, 3])\n", "param_circ1.X(0).X(1).add_gate(OpType.CnY, [0, 1, 2]).add_gate(OpType.CnY, [0, 4, 3]).X(\n", " 0\n", ").X(1)\n", "param_circ1.Ry(-b, 0).Ry(-c, 1)\n", "render_circuit_jupyter(param_circ1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can pass a parameterised circuit to `GeneralState`. The value of the parameters is provided when calling methods of `GeneralState`. The contraction path is automatically reused on different calls to the same method."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_circs = 5\n", "with GeneralState(param_circ1) as param_state:\n", " for i in range(n_circs):\n", " symbol_map = {s: np.random.random() for s in [a, b, c]}\n", " exp_val = param_state.expectation_value(observable, symbol_map=symbol_map)\n", " print(f\"Expectation value for circuit {i}: {exp_val.real}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# `GeneralBraOpKet`
\n", "The `GeneralBraOpKet` can be used to calculate any number that can be represented as the result of some `` where `|bra>` and `|ket>` are the final states of pytket circuits, and `op` is a `QubitPauliOperator`. The circuits for `|bra>` and `|ket>` need not be the same."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["x, y, z = Symbol(\"x\"), Symbol(\"y\"), Symbol(\"z\")\n", "param_circ2 = Circuit(5)\n", "param_circ2.H(0)\n", "param_circ2.S(1)\n", "param_circ2.Rz(x * z, 2)\n", "param_circ2.Ry(y + x, 3)\n", "param_circ2.TK1(x, y, z, 4)\n", "param_circ2.TK2(z - y, z - x, (x + y) * z, 1, 3)\n", "symbol_map = {a: 2.1, b: 1.3, c: 0.7, x: 3.0, y: 1.6, z: -8.3}"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can calculate inner products by providing no `op`:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with GeneralBraOpKet(bra=param_circ2, ket=param_circ1) as braket:\n", " inner_prod = braket.contract(symbol_map=symbol_map)\n", "with GeneralBraOpKet(bra=param_circ1, ket=param_circ2) as braket:\n", " inner_prod_conj = braket.contract(symbol_map=symbol_map)\n", "assert np.isclose(np.conj(inner_prod), inner_prod_conj)\n", "print(f\" = {inner_prod}\")\n", "print(f\" = {inner_prod_conj}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["And we are not constrained to Hermitian operators:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["string_XZIXX = QubitPauliString(\n", " param_circ2.qubits, [Pauli.X, Pauli.Z, Pauli.I, Pauli.X, Pauli.X]\n", ")\n", "string_IZZYX = QubitPauliString(\n", " param_circ2.qubits, [Pauli.I, Pauli.Z, Pauli.Z, Pauli.Y, Pauli.X]\n", ")\n", "string_ZIZXY = QubitPauliString(\n", " param_circ2.qubits, [Pauli.Z, Pauli.I, Pauli.Z, Pauli.X, Pauli.Y]\n", ")\n", "operator = QubitPauliOperator(\n", " {string_XZIXX: -1.38j, string_IZZYX: 2.36, string_ZIZXY: 0.42j + 0.3}\n", ")\n", "with GeneralBraOpKet(bra=param_circ2, ket=param_circ1) as braket:\n", " value = braket.contract(operator, symbol_map=symbol_map)\n", "print(value)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Backends
\n", "We provide a pytket `Backend` to obtain shots using `GeneralState`."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's consider a more challenging circuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def random_circuit(n_qubits: int, n_layers: int) -> Circuit:\n", " \"\"\"Random quantum volume circuit.\"\"\"\n", " c = Circuit(n_qubits, n_qubits)\n", " for _ in range(n_layers):\n", " qubits = np.random.permutation([i for i in range(n_qubits)])\n", " qubit_pairs = [[qubits[i], qubits[i + 1]] for i in range(0, n_qubits - 1, 2)]\n", " for pair in qubit_pairs:\n", " # Generate random 4x4 unitary matrix.\n", " SU4 = unitary_group.rvs(4) # random unitary in SU4\n", " SU4 = SU4 / (np.linalg.det(SU4) ** 0.25)\n", " SU4 = np.matrix(SU4)\n", " c.add_unitary2qbox(Unitary2qBox(SU4), *pair)\n", " DecomposeBoxes().apply(c)\n", " return c"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's measure only three of the qubits.
\n", "**Note**: The complexity of this simulation increases exponentially with the number of qubits measured. Other factors leading to intractability are circuit depth and qubit connectivity."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots = 1000\n", "quantum_vol_circ = random_circuit(n_qubits=40, n_layers=5)\n", "quantum_vol_circ.Measure(Qubit(0), Bit(0))\n", "quantum_vol_circ.Measure(Qubit(1), Bit(1))\n", "quantum_vol_circ.Measure(Qubit(2), Bit(2))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `CuTensorNetShotsBackend` is used in the same way as any other pytket `Backend`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = CuTensorNetShotsBackend()\n", "compiled_circ = backend.get_compiled_circuit(quantum_vol_circ)\n", "results = backend.run_circuit(compiled_circ, n_shots=n_shots)\n", "print(results.get_counts())"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4"}}, "nbformat": 4, "nbformat_minor": 2} \ No newline at end of file diff --git a/docs/examples/images/mps.png b/examples/images/mps.png similarity index 100% rename from docs/examples/images/mps.png rename to examples/images/mps.png diff --git a/docs/examples/mpi/README.md b/examples/mpi/README.md similarity index 100% rename from docs/examples/mpi/README.md rename to examples/mpi/README.md diff --git a/docs/examples/mpi/mpi_overlap_bcast_circ.py b/examples/mpi/mpi_overlap_bcast_circ.py similarity index 100% rename from docs/examples/mpi/mpi_overlap_bcast_circ.py rename to examples/mpi/mpi_overlap_bcast_circ.py diff --git a/docs/examples/mpi/mpi_overlap_bcast_mps.py b/examples/mpi/mpi_overlap_bcast_mps.py similarity index 100% rename from docs/examples/mpi/mpi_overlap_bcast_mps.py rename to examples/mpi/mpi_overlap_bcast_mps.py diff --git a/docs/examples/mpi/mpi_overlap_bcast_net.py b/examples/mpi/mpi_overlap_bcast_net.py similarity index 100% rename from docs/examples/mpi/mpi_overlap_bcast_net.py rename to examples/mpi/mpi_overlap_bcast_net.py diff --git a/examples/mps_tutorial.ipynb b/examples/mps_tutorial.ipynb new file mode 100644 index 00000000..1735b1f9 --- /dev/null +++ b/examples/mps_tutorial.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np\n", "from time import time\n", "import matplotlib.pyplot as plt\n", "from pytket import Circuit, OpType\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.cutensornet.structured_state import (\n", " CuTensorNetHandle,\n", " Config,\n", " SimulationAlgorithm,\n", " simulate,\n", " prepare_circuit_mps,\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Introduction
\n", "This notebook provides examples of the usage of the MPS functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.
\n", "A Matrix Product State (MPS) represents a state on `n` qubits as a list of `n` tensors connected in a line as show below:
\n", "![MPS](images/mps.png)
\n", "Each of these circles corresponds to a tensor. We refer to each leg of a tensor as a *bond* and the number of bonds a tensor has is its *rank*. In code, a tensor is just a multidimensional array:
\n", "
\n", "```tensor[i][j][k] = v```
\n", "
\n", "In the case above, we are assigning an entry value `v` of a rank-3 tensor (one `[ ]` coordinate per bond). Each bond allows a different number of values for its indices; for instance `0 <= i < 4` would mean that the first bond of our tensor can take up to four different indices; we refer to this as the *dimension* of the bond. We refer to the bonds connecting different tensors in the MPS as *virtual bonds*; the maximum allowed value for the dimension of virtual bonds is often denoted by the greek letter `chi`. The open bonds are known as *physical bonds* and, in our case, each will correspond to a qubit; hence, they have dimension `2` -- the dimension of the vector space of a single qubit.
\n", "In essence, whenever we want to apply a gate to certain qubit we will connect a tensor (matrix) representing the gate to the corresponding physical bond and *contract* the network back to an MPS form (tensor contraction is a generalisation of matrix multiplication to multidimensional arrays). Whenever a two-qubit gate is applied, the entanglement information after contraction will be kept in the degrees of freedom of the virtual bonds. As such, the dimension of the virtual bonds will generally increase exponentially as we apply entangling gates, leading to large memory footprints of the tensors and, consequently, long runtime for tensor contraction. We provide functionalities to limit the growth of the dimension of the virtual bonds, keeping resource consumption in check. Read the *Approximate simulation* section on this notebook to learn more.
\n", "**References**: To read more about MPS we recommend the following papers.
\n", "* For an introduction to MPS and its canonical form: https://arxiv.org/abs/1901.05824.
\n", "* For a description of the `MPSxGate` algorithm we provide: https://arxiv.org/abs/2002.07730.
\n", "* For a description of the `MPSxMPO` algorithm we provide: https://arxiv.org/abs/2207.05612.
\n", "* For insights on the reationship between truncation error and the error model in a quantum computer: https://arxiv.org/abs/2004.02388"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Basic functionality and exact simulation
\n", "Here we show an example of the basic use of our MPS methods. We first generate a simple `pytket` circuit to be simulated."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_circ = Circuit(5)\n", "my_circ.CX(3, 4)\n", "my_circ.H(2)\n", "my_circ.CZ(0, 1)\n", "my_circ.ZZPhase(0.1, 4, 3)\n", "my_circ.TK2(0.3, 0.5, 0.7, 2, 1)\n", "my_circ.Ry(0.2, 0)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(my_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For **exact** simulation, simply call the `simulate` function on the circuit and choose a contraction algorithm. To learn more about the contraction algorithms we provide see the *Contraction algorithms* section of this notebook. You will also need to provide a configuration, the default one is provided by `Config()`. Custom settings of `Config` are discussed in the *Approximate simulation* section.
\n", "**NOTE**: whenever you wish to generate an `MPS` object or execute calculations on it you must do so within a `with CuTensorNetHandle() as libhandle:` block; this will initialise the cuTensorNetwork library for you, and destroy its handles at the end of the `with` block. You will need to pass the `libhandle` to the `MPS` object via the method that generates it (in the snippet below, `simulate`), or if already initialised, pass it via the `update_libhandle` method.
\n", "Due to the nature of Jupyter notebooks, we will be starting most of these cells with a `with CuTensorNetHandle() as libhandle:`. However, in a standard script, all of these cells would be grouped together and a single `with CuTensorNetHandle() as libhandle:` statement would be necessary at the beginning of the script."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps = simulate(libhandle, my_circ, SimulationAlgorithm.MPSxGate, Config())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Notice that `my_circ` uses a rich gateset -- in fact, every single-qubit and two-qubit gate supported by `pytket` can be used in our MPS approaches. Gates acting on more than two qubits are not currently supported."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The output of `simulate` is an `MPS` object encoding the output state of the circuit.
\n", "### Obtain an amplitude from an MPS
\n", "Let's first see how to get the amplitude of the state `|10100>` from the output of the previous circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state = int(\"10100\", 2)\n", "with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " amplitude = my_mps.get_amplitude(state)\n", "print(amplitude)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Since this is a very small circuit, we can use `pytket`'s state vector simulator capabilities to verify that the state is correct by checking the amplitude of each of the computational states."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state_vector = my_circ.get_statevector()\n", "n_qubits = len(my_circ.qubits)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["correct_amplitude = [False] * (2**n_qubits)\n", "with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " for i in range(2**n_qubits):\n", " correct_amplitude[i] = np.isclose(state_vector[i], my_mps.get_amplitude(i))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Are all amplitudes correct?\")\n", "print(all(correct_amplitude))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Sampling from an MPS
\n", "We can also sample from the output state of a circuit by calling `my_mps.sample`, where `my_mps` is the outcome of simulating the circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_samples = 100\n", "n_qubits = len(my_circ.qubits)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Initialise the sample counter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["sample_count = [0 for _ in range(2**n_qubits)]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " for _ in range(n_samples):\n", " # Draw a sample\n", " qubit_outcomes = my_mps.sample()\n", " # Convert qubit outcomes to bitstring\n", " bitstring = \"\".join(str(qubit_outcomes[q]) for q in my_circ.qubits)\n", " # Convert bitstring to int\n", " outcome = int(bitstring, 2)\n", " # Update the sample dictionary\n", " sample_count[outcome] += 1"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Calculate the theoretical number of samples per bitstring"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["expected_count = [n_samples * abs(state_vector[i]) ** 2 for i in range(2**n_qubits)]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Plot a comparison of theory vs sampled"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["plt.scatter(range(2**n_qubits), expected_count, label=\"Theory\")\n", "plt.scatter(range(2**n_qubits), sample_count, label=\"Experiment\", marker=\"x\")\n", "plt.xlabel(\"Basis states\")\n", "plt.ylabel(\"Samples\")\n", "plt.legend()\n", "plt.show()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We also provide methods to apply mid-circuit measurements via `my_mps.measure(qubits)` and postselection via `my_mps.postselect(qubit_outcomes)`. Their use is similar to that of `my_mps.sample()` shown above.
\n", "**Note:** whereas `my_mps.sample()` does *not* change the state of the MPS, `my_mps.measure(qubits)` and `my_mps.postselect(qubit_outcomes)` do change it, projecting the state to the resulting outcome and removing the measured qubits."]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Inner products
\n", "Using `vdot` you can obtain the inner product of two states in MPS form. This method does not change the internal data of neither of the MPS. Moreover, it can be used on the same `MPS` object for both inputs, yielding the squared norm of the state."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " norm_sq = my_mps.vdot(my_mps)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"As expected, the squared norm of a state is 1\")\n", "print(np.isclose(norm_sq, 1))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's come up with another circuit on the same qubits and apply an inner product between the two `MPS` objects."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Generate circuits"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["other_circ = Circuit(5)\n", "other_circ.H(3)\n", "other_circ.CZ(3, 4)\n", "other_circ.XXPhase(0.3, 1, 2)\n", "other_circ.Ry(0.7, 3)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Simulate them"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " other_mps = simulate(libhandle, other_circ, SimulationAlgorithm.MPSxGate, Config())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's calculate the inner product and check that it agrees with `pytket`'s state vector based computation."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " inner_product = my_mps.vdot(other_mps)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_state = my_circ.get_statevector()\n", "other_state = other_circ.get_statevector()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Is the inner product correct?\")\n", "print(np.isclose(np.vdot(my_state, other_state), inner_product))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Mid-circuit measurements and classical control
\n", "Mid-circuit measurements and classical control is supported (only in `MPSxGate` as of v0.8.0). For instance, we can implement the teleportation protocol on a pytket circuit and simulate it:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit()\n", "alice = circ.add_q_register(\"alice\", 2)\n", "alice_bits = circ.add_c_register(\"alice_bits\", 2)\n", "bob = circ.add_q_register(\"bob\", 1)\n", "# Initialise Alice's first qubit in some arbitrary state\n", "circ.Rx(0.42, alice[0])\n", "orig_state = circ.get_statevector()\n", "# Create a Bell pair shared between Alice and Bob\n", "circ.H(alice[1]).CX(alice[1], bob[0])\n", "# Apply a Bell measurement on Alice's qubits\n", "circ.CX(alice[0], alice[1]).H(alice[0])\n", "circ.Measure(alice[0], alice_bits[0])\n", "circ.Measure(alice[1], alice_bits[1])\n", "# Apply conditional corrections on Bob's qubits\n", "circ.add_gate(OpType.X, [bob[0]], condition_bits=[alice_bits[1]], condition_value=1)\n", "circ.add_gate(OpType.Z, [bob[0]], condition_bits=[alice_bits[0]], condition_value=1)\n", "# Reset Alice's qubits\n", "circ.add_gate(OpType.Reset, [alice[0]])\n", "circ.add_gate(OpType.Reset, [alice[1]])\n", "# Display the circuit\n", "render_circuit_jupyter(circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now simulate the circuit and check that the qubit has been successfully teleported."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\n", " f\"Initial state:\\n {np.round(orig_state[0],2)}|00>|0> + {np.round(orig_state[4],2)}|10>|0>\"\n", ")\n", "with CuTensorNetHandle() as libhandle:\n", " state = simulate(libhandle, circ, SimulationAlgorithm.MPSxGate, Config())\n", " print(\n", " f\"Teleported state:\\n {np.round(state.get_amplitude(0),2)}|00>|0> + {np.round(state.get_amplitude(1),2)}|00>|1>\"\n", " )\n", " print(f\"Measurement outcomes:\\n {state.get_bits()}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Two-qubit gates acting on non-adjacent qubits
\n", "Standard MPS algorithms only support simulation of two-qubit gates acting on neighbour qubits. In our implementation, however, two-qubit gates between arbitrary qubits may be applied, as shown below."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit(5)\n", "circ.H(1)\n", "circ.ZZPhase(0.3, 1, 3)\n", "circ.CX(0, 2)\n", "circ.Ry(0.8, 4)\n", "circ.CZ(3, 4)\n", "circ.XXPhase(0.7, 1, 2)\n", "circ.TK2(0.1, 0.2, 0.4, 1, 4)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " mps = simulate(libhandle, circ, SimulationAlgorithm.MPSxGate, Config())\n", " print(\"Did simulation succeed?\")\n", " print(mps.is_valid())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["**Note**: Even though two-qubit gates on non-adjacent qubits are simulable, the overhead on these is considerably larger than simulating gates on adjacent qubits. As a rule of thumb if the two qubits are `n` positions apart, the overhead is upper bounded by the cost of simulating `n-1` additional SWAP gates to move the leftmost qubit near the rightmost. In reality, the implementation we use is more nuanced than just applying SWAP gates, and the qubits don't actually change position.
\n", "When circuits are shallow, using our approach to simulate long-distance two-qubit gates is advantageous. In the case of deep circuits with many long-distance gates, it is sometimes beneficial to use TKET routing on the circuit, explicitly adding SWAP gates so that all two-qubit gates act on nearest neighbour qubits. Users may do this by calling `prepare_circuit_mps`, which is a wrapper of the corresponding TKET routing pass."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["prepared_circ, qubit_map = prepare_circuit_mps(circ)\n", "render_circuit_jupyter(prepared_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The circuit can now be simulated as usual."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " mps = simulate(libhandle, prepared_circ, SimulationAlgorithm.MPSxGate, Config())\n", " print(\"Did simulation succeed?\")\n", " print(mps.is_valid())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Notice that the qubits in the `prepared_circ` were renamed when applying `prepare_circuit_mps`. Implicit SWAPs may have been added to the circuit, meaning that the logical qubit held at the ``node[i]`` qubit at the beginning of the circuit may differ from the one it holds at the end; this information is captured by the `qubit_map` output. We recommend applying ``apply_qubit_relabelling`` on the MPS after simulation, relabelling the qubits according to these implicit SWAPs."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(qubit_map)\n", "mps.apply_qubit_relabelling(qubit_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Approximate simulation
\n", "We provide two policies for approximate simulation; these are supported by both of our current MPS contraction algorithms:
\n", "* Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes.
\n", "* Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate.
\n", "Values for `chi` and `truncation_fidelity` can be set via `Config`. To showcase approximate simulation, let's define a circuit where exact MPS contraction starts struggling."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def random_line_circuit(n_qubits: int, layers: int) -> Circuit:\n", " \"\"\"Random circuit with line connectivity.\"\"\"\n", " c = Circuit(n_qubits)\n", " for i in range(layers):\n", " # Layer of TK1 gates\n", " for q in range(n_qubits):\n", " c.TK1(np.random.rand(), np.random.rand(), np.random.rand(), q)\n\n", " # Layer of CX gates\n", " offset = np.mod(i, 2) # Even layers connect (q0,q1), odd (q1,q2)\n", " qubit_pairs = [\n", " [c.qubits[i], c.qubits[i + 1]] for i in range(offset, n_qubits - 1, 2)\n", " ]\n", " # Direction of each CX gate is random\n", " for pair in qubit_pairs:\n", " np.random.shuffle(pair)\n", " for pair in qubit_pairs:\n", " c.CX(pair[0], pair[1])\n", " return c"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circuit = random_line_circuit(n_qubits=20, layers=20)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For exact contraction, `chi` must be allowed to be up to `2**(n_qubits // 2)`, meaning that if we set `n_qubits = 20` it would require `chi = 1024`; already too much for this particular circuit to be simulated in a gaming laptop using the current implementation. Instead, let's bound `chi` to a maximum of `16`. Doing so results in faster runtime, at the expense of losing output state fidelity."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=16)\n", " bound_chi_mps = simulate(libhandle, circuit, SimulationAlgorithm.MPSxGate, config)\n", "end = time()\n", "print(\"Time taken by approximate contraction with bound chi:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(bound_chi_mps.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can fix `truncation_fidelity` and let `chi` increase as necessary to satisfy it."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(truncation_fidelity=0.999)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxGate, config\n", " )\n", "end = time()\n", "print(\"Time taken by approximate contraction with fixed truncation fidelity:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(fixed_fidelity_mps.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Contraction algorithms"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We currently offer two MPS-based simulation algorithms:
\n", "* **MPSxGate**: Apply gates one by one to the MPS, canonicalising the MPS and truncating when necessary. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2002.07730.
\n", "* **MPSxMPO**: Maintain two MPS copies of the state as it evolves, one updated eagerly using the **MPSxGate** method and the other updated in batches of up to `k` layers of two-qubit gates. Whenever the second MPS is updated, both copies are synchronised and an optimisation algorithm is applied to increase the fidelity of the state. This algorithm is often referred to as DMRG-like simulation. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2207.05612.
\n", "The `MPSxGate` algorithm is the one we have been using for all of the examples above. In comparison, the `MPSxMPO` algorithm provides the user with two new parameters to tune:
\n", "* **k**: The maximum number of layers the MPO is allowed to have before being contracted. Increasing this might increase fidelity, but it will also increase resource requirements exponentially. Default value is `4`.
\n", "* **optim_delta**: Stopping criteria for the optimisation when contracting the `k` layers of MPO. Stops when the increase of fidelity between iterations is smaller than `optim_delta`. Default value is `1e-5`.
\n", "Both `k` and `optim_delta` can be set via `Config`. Below we compare `MPSxGate` versus `MPSxMPO` with default parameters and `MPSxMPO` with more resource-hungry parameters. The circuit used is the same as in the previous section."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=16)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxGate, config\n", " )\n", "end = time()\n", "print(\"MPSxGate\")\n", "print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n", "print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=16)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxMPO, config\n", " )\n", "end = time()\n", "print(\"MPSxMPO, default parameters\")\n", "print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n", "print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(k=8, optim_delta=1e-15, chi=16)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxMPO, config\n", " )\n", "end = time()\n", "print(\"MPSxMPO, custom parameters\")\n", "print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n", "print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["**Note**: `MPSxMPO` also admits truncation policy in terms of `truncation_fidelity` instead of `chi`."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Using the logger"]}, {"cell_type": "markdown", "metadata": {}, "source": ["You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent):
\n", "- `logging.INFO` will print information about progress percent, memory currently occupied by the MPS and current fidelity. Additionally, some high level information of the current stage of the simulation is provided, such as when `MPSxMPO` is applying optimisation sweeps.
\n", "- `logging.DEBUG` provides all of the messages from the loglevel above plus detailed information of the current operation being carried out and the values of important variables.
\n", "**Note**: Due to technical issues with the `logging` module and Jupyter notebooks we need to reload the `logging` module. When working with python scripts and command line, just doing `import logging` is enough."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from importlib import reload # Not needed in Python 2\n", "import logging"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["reload(logging)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["An example of the use of `logging.INFO` is provided below."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " config = Config(truncation_fidelity=0.999, loglevel=logging.INFO)\n", " simulate(libhandle, circuit, SimulationAlgorithm.MPSxMPO, config)"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4"}}, "nbformat": 4, "nbformat_minor": 2} \ No newline at end of file diff --git a/docs/examples/python/general_state_tutorial.py b/examples/python/general_state_tutorial.py similarity index 100% rename from docs/examples/python/general_state_tutorial.py rename to examples/python/general_state_tutorial.py diff --git a/docs/examples/python/mps_tutorial.py b/examples/python/mps_tutorial.py similarity index 100% rename from docs/examples/python/mps_tutorial.py rename to examples/python/mps_tutorial.py diff --git a/docs/examples/python/ttn_tutorial.py b/examples/python/ttn_tutorial.py similarity index 100% rename from docs/examples/python/ttn_tutorial.py rename to examples/python/ttn_tutorial.py diff --git a/examples/ttn_tutorial.ipynb b/examples/ttn_tutorial.ipynb new file mode 100644 index 00000000..d00ef37b --- /dev/null +++ b/examples/ttn_tutorial.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np\n", "from time import time\n", "import matplotlib.pyplot as plt\n", "import networkx as nx\n", "from pytket import Circuit\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.cutensornet.structured_state import (\n", " CuTensorNetHandle,\n", " Config,\n", " SimulationAlgorithm,\n", " simulate,\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Introduction
\n", "This notebook provides examples of the usage of the TTN functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.
\n", "Some good references to learn about Tree Tensor Network state simulation:
\n", "- For an introduction into TTN based simulation of quantum circuits: https://arxiv.org/abs/2206.01000
\n", "- For an introduction on some of the optimisation concerns that are relevant to TTN: https://arxiv.org/abs/2209.03196
\n", "The implementation in pytket-cutensornet differs from previously published literature. I am still experimenting with the algorithm. I intend to write up a document detailing the approach, once I reach a stable version.
\n", "The main advantage of TTN over MPS is that it can be used to efficiently simulate circuits with richer qubit connectivity. This does **not** mean that TTN has an easy time simulating all-to-all connectivity, but it is far more flexible than MPS. TTN's strength is in simulating circuit where certain subsets of qubits interact densely with each other, and there is not that many gates acting on qubits in different subsets."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# How to use
\n", "The interface for TTN matches that of MPS. As such, you should be able to run any code that uses `SimulationAlgorithm.MPSxGate` by replacing it with `SimulationAlgorithm.TTNxGate`. Calling `prepare_circuit_mps` is no longer necessary, since `TTNxGate` can apply gates between non-neighbouring qubits.
\n", "**NOTE**: If you are new to pytket-cutensornet, it is highly recommended to start reading the `mps_tutorial.ipynb` notebook instead. More details about the use of the library are discussed there (for instance, why and when to call `CuTensorNetHandle()`)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def random_graph_circuit(n_qubits: int, edge_prob: float, layers: int) -> Circuit:\n", " \"\"\"Random circuit with qubit connectivity determined by a random graph.\"\"\"\n", " c = Circuit(n_qubits)\n", " for i in range(layers):\n", " # Layer of TK1 gates\n", " for q in range(n_qubits):\n", " c.TK1(np.random.rand(), np.random.rand(), np.random.rand(), q)\n\n", " # Layer of CX gates\n", " graph = nx.erdos_renyi_graph(n_qubits, edge_prob, directed=True)\n", " qubit_pairs = list(graph.edges)\n", " for pair in qubit_pairs:\n", " c.CX(pair[0], pair[1])\n", " return c"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For **exact** simulation, you can call `simulate` directly, providing the default `Config()`:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["simple_circ = random_graph_circuit(n_qubits=10, edge_prob=0.1, layers=1)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_ttn = simulate(libhandle, simple_circ, SimulationAlgorithm.TTNxGate, Config())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Obtain an amplitude from a TTN
\n", "Let's first see how to get the amplitude of the state `|10100>` from the output of the previous circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state = int(\"10100\", 2)\n", "with CuTensorNetHandle() as libhandle:\n", " my_ttn.update_libhandle(libhandle)\n", " amplitude = my_ttn.get_amplitude(state)\n", "print(amplitude)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Since this is a very small circuit, we can use `pytket`'s state vector simulator capabilities to verify that the state is correct by checking the amplitude of each of the computational states."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state_vector = simple_circ.get_statevector()\n", "n_qubits = len(simple_circ.qubits)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["correct_amplitude = [False] * (2**n_qubits)\n", "with CuTensorNetHandle() as libhandle:\n", " my_ttn.update_libhandle(libhandle)\n", " for i in range(2**n_qubits):\n", " correct_amplitude[i] = np.isclose(state_vector[i], my_ttn.get_amplitude(i))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Are all amplitudes correct?\")\n", "print(all(correct_amplitude))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Sampling from a TTN
\n", "Sampling and measurement from a TTN state is not currently supported. This will be added in an upcoming release."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Approximate simulation
\n", "We provide two policies for approximate simulation:
\n", "* Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes.
\n", "* Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate.
\n", "Values for `chi` and `truncation_fidelity` can be set via `Config`. To showcase approximate simulation, let's define a circuit where exact TTN contraction would not be enough."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circuit = random_graph_circuit(n_qubits=30, edge_prob=0.1, layers=1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can simulate it using bounded `chi` as follows:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=64, float_precision=np.float32)\n", " bound_chi_ttn = simulate(libhandle, circuit, SimulationAlgorithm.TTNxGate, config)\n", "end = time()\n", "print(\"Time taken by approximate contraction with bounded chi:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(bound_chi_ttn.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can fix `truncation_fidelity` and let the bond dimension increase as necessary to satisfy it."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(truncation_fidelity=0.99, float_precision=np.float32)\n", " fixed_fidelity_ttn = simulate(\n", " libhandle, circuit, SimulationAlgorithm.TTNxGate, config\n", " )\n", "end = time()\n", "print(\"Time taken by approximate contraction with fixed truncation fidelity:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(fixed_fidelity_ttn.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Contraction algorithms"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We currently offer only one TTN-based simulation algorithm.
\n", "* **TTNxGate**: Apply gates one by one to the TTN, canonicalising the TTN and truncating when necessary."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Using the logger"]}, {"cell_type": "markdown", "metadata": {}, "source": ["You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent):
\n", "- `logging.INFO` will print information about progress percent, memory currently occupied by the TTN and current fidelity. Additionally, some high level information of the current stage of the simulation is provided.
\n", "- `logging.DEBUG` provides all of the messages from the loglevel above plus detailed information of the current operation being carried out and the values of important variables.
\n", "**Note**: Due to technical issues with the `logging` module and Jupyter notebooks we need to reload the `logging` module. When working with python scripts and command line, just doing `import logging` is enough."]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4"}}, "nbformat": 4, "nbformat_minor": 2} \ No newline at end of file From e1f85c052c87e17d046cc8c06a487ab72ef0cb5a Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:35:27 +0100 Subject: [PATCH 07/13] move examples folder inside docs directory --- {examples => docs/examples}/README.md | 0 {examples => docs/examples}/check-examples | 0 {examples => docs/examples}/ci-tested-notebooks.txt | 0 .../examples}/general_state_tutorial.ipynb | 0 {examples => docs/examples}/images/mps.png | Bin {examples => docs/examples}/mpi/README.md | 0 .../examples}/mpi/mpi_overlap_bcast_circ.py | 0 .../examples}/mpi/mpi_overlap_bcast_mps.py | 0 .../examples}/mpi/mpi_overlap_bcast_net.py | 0 {examples => docs/examples}/mps_tutorial.ipynb | 0 .../examples}/python/general_state_tutorial.py | 0 {examples => docs/examples}/python/mps_tutorial.py | 0 {examples => docs/examples}/python/ttn_tutorial.py | 0 {examples => docs/examples}/ttn_tutorial.ipynb | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename {examples => docs/examples}/README.md (100%) rename {examples => docs/examples}/check-examples (100%) rename {examples => docs/examples}/ci-tested-notebooks.txt (100%) rename {examples => docs/examples}/general_state_tutorial.ipynb (100%) rename {examples => docs/examples}/images/mps.png (100%) rename {examples => docs/examples}/mpi/README.md (100%) rename {examples => docs/examples}/mpi/mpi_overlap_bcast_circ.py (100%) rename {examples => docs/examples}/mpi/mpi_overlap_bcast_mps.py (100%) rename {examples => docs/examples}/mpi/mpi_overlap_bcast_net.py (100%) rename {examples => docs/examples}/mps_tutorial.ipynb (100%) rename {examples => docs/examples}/python/general_state_tutorial.py (100%) rename {examples => docs/examples}/python/mps_tutorial.py (100%) rename {examples => docs/examples}/python/ttn_tutorial.py (100%) rename {examples => docs/examples}/ttn_tutorial.ipynb (100%) diff --git a/examples/README.md b/docs/examples/README.md similarity index 100% rename from examples/README.md rename to docs/examples/README.md diff --git a/examples/check-examples b/docs/examples/check-examples similarity index 100% rename from examples/check-examples rename to docs/examples/check-examples diff --git a/examples/ci-tested-notebooks.txt b/docs/examples/ci-tested-notebooks.txt similarity index 100% rename from examples/ci-tested-notebooks.txt rename to docs/examples/ci-tested-notebooks.txt diff --git a/examples/general_state_tutorial.ipynb b/docs/examples/general_state_tutorial.ipynb similarity index 100% rename from examples/general_state_tutorial.ipynb rename to docs/examples/general_state_tutorial.ipynb diff --git a/examples/images/mps.png b/docs/examples/images/mps.png similarity index 100% rename from examples/images/mps.png rename to docs/examples/images/mps.png diff --git a/examples/mpi/README.md b/docs/examples/mpi/README.md similarity index 100% rename from examples/mpi/README.md rename to docs/examples/mpi/README.md diff --git a/examples/mpi/mpi_overlap_bcast_circ.py b/docs/examples/mpi/mpi_overlap_bcast_circ.py similarity index 100% rename from examples/mpi/mpi_overlap_bcast_circ.py rename to docs/examples/mpi/mpi_overlap_bcast_circ.py diff --git a/examples/mpi/mpi_overlap_bcast_mps.py b/docs/examples/mpi/mpi_overlap_bcast_mps.py similarity index 100% rename from examples/mpi/mpi_overlap_bcast_mps.py rename to docs/examples/mpi/mpi_overlap_bcast_mps.py diff --git a/examples/mpi/mpi_overlap_bcast_net.py b/docs/examples/mpi/mpi_overlap_bcast_net.py similarity index 100% rename from examples/mpi/mpi_overlap_bcast_net.py rename to docs/examples/mpi/mpi_overlap_bcast_net.py diff --git a/examples/mps_tutorial.ipynb b/docs/examples/mps_tutorial.ipynb similarity index 100% rename from examples/mps_tutorial.ipynb rename to docs/examples/mps_tutorial.ipynb diff --git a/examples/python/general_state_tutorial.py b/docs/examples/python/general_state_tutorial.py similarity index 100% rename from examples/python/general_state_tutorial.py rename to docs/examples/python/general_state_tutorial.py diff --git a/examples/python/mps_tutorial.py b/docs/examples/python/mps_tutorial.py similarity index 100% rename from examples/python/mps_tutorial.py rename to docs/examples/python/mps_tutorial.py diff --git a/examples/python/ttn_tutorial.py b/docs/examples/python/ttn_tutorial.py similarity index 100% rename from examples/python/ttn_tutorial.py rename to docs/examples/python/ttn_tutorial.py diff --git a/examples/ttn_tutorial.ipynb b/docs/examples/ttn_tutorial.ipynb similarity index 100% rename from examples/ttn_tutorial.ipynb rename to docs/examples/ttn_tutorial.ipynb From 9a258358a0b1429b7ead3403d6cb5273abad4e2d Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:46:26 +0100 Subject: [PATCH 08/13] fix headings for GeneralState --- docs/examples/python/general_state_tutorial.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/examples/python/general_state_tutorial.py b/docs/examples/python/general_state_tutorial.py index bef9b47d..ff673806 100644 --- a/docs/examples/python/general_state_tutorial.py +++ b/docs/examples/python/general_state_tutorial.py @@ -1,3 +1,5 @@ +# # `GeneralState` Tutorial + import numpy as np from sympy import Symbol from scipy.stats import unitary_group # type: ignore @@ -13,7 +15,7 @@ ) from pytket.extensions.cutensornet.backends import CuTensorNetShotsBackend -# # Introduction +# ## Introduction # This notebook is a guide on how to use the features provided in the `general_state` submodule of pytket-cutensornet. This submodule is a thin wrapper of CuTensorNet's `NetworkState`, allowing users to convert pytket circuits into tensor networks and use CuTensorNet's contraction path optimisation algorithm. # All simulations realised with this submodule are *exact*. Once the pytket circuit has been converted to a tensor network, the computation has two steps: # 1. *Contraction path optimisation*. Attempts to find an order of contracting pairs of tensors in which the the total number of FLOPs is minimised. No operation on the tensor network occurs at this point. Runs on CPU. @@ -93,7 +95,7 @@ # Using this syntax, `my_state` is automatically destroyed when the code exists the `with ...` block. -# # Parameterised circuits +# ## Parameterised circuits # Circuits that only differ on the parameters of their gates have the same tensor network topology and, hence, we may use the same contraction path for all of them. a, b, c = Symbol("a"), Symbol("b"), Symbol("c") param_circ1 = Circuit(5) @@ -115,7 +117,7 @@ print(f"Expectation value for circuit {i}: {exp_val.real}") -# # `GeneralBraOpKet` +# ## `GeneralBraOpKet` # The `GeneralBraOpKet` can be used to calculate any number that can be represented as the result of some `` where `|bra>` and `|ket>` are the final states of pytket circuits, and `op` is a `QubitPauliOperator`. The circuits for `|bra>` and `|ket>` need not be the same. x, y, z = Symbol("x"), Symbol("y"), Symbol("z") param_circ2 = Circuit(5) @@ -153,7 +155,7 @@ value = braket.contract(operator, symbol_map=symbol_map) print(value) -# # Backends +# ## Backends # We provide a pytket `Backend` to obtain shots using `GeneralState`. From 54673651e275507e7547db72e82fa49c9c6ac106 Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:47:47 +0100 Subject: [PATCH 09/13] fix mps headings --- docs/examples/python/mps_tutorial.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/examples/python/mps_tutorial.py b/docs/examples/python/mps_tutorial.py index 21a1daca..e172b38b 100644 --- a/docs/examples/python/mps_tutorial.py +++ b/docs/examples/python/mps_tutorial.py @@ -1,3 +1,5 @@ +# # Matrix Product State (MPS) Tutorial + import numpy as np from time import time import matplotlib.pyplot as plt @@ -12,7 +14,7 @@ prepare_circuit_mps, ) -# # Introduction +# ## Introduction # This notebook provides examples of the usage of the MPS functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html. # A Matrix Product State (MPS) represents a state on `n` qubits as a list of `n` tensors connected in a line as show below: # ![MPS](images/mps.png) @@ -28,7 +30,7 @@ # * For a description of the `MPSxMPO` algorithm we provide: https://arxiv.org/abs/2207.05612. # * For insights on the reationship between truncation error and the error model in a quantum computer: https://arxiv.org/abs/2004.02388 -# # Basic functionality and exact simulation +# ## Basic functionality and exact simulation # Here we show an example of the basic use of our MPS methods. We first generate a simple `pytket` circuit to be simulated. my_circ = Circuit(5) @@ -219,7 +221,7 @@ print(qubit_map) mps.apply_qubit_relabelling(qubit_map) -# # Approximate simulation +# ## Approximate simulation # We provide two policies for approximate simulation; these are supported by both of our current MPS contraction algorithms: # * Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes. # * Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate. @@ -278,7 +280,7 @@ def random_line_circuit(n_qubits: int, layers: int) -> Circuit: print("\nLower bound of the fidelity:") print(round(fixed_fidelity_mps.fidelity, 4)) -# # Contraction algorithms +# ## Contraction algorithms # We currently offer two MPS-based simulation algorithms: # * **MPSxGate**: Apply gates one by one to the MPS, canonicalising the MPS and truncating when necessary. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2002.07730. @@ -323,7 +325,7 @@ def random_line_circuit(n_qubits: int, layers: int) -> Circuit: # **Note**: `MPSxMPO` also admits truncation policy in terms of `truncation_fidelity` instead of `chi`. -# # Using the logger +# ## Using the logger # You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent): # - `logging.INFO` will print information about progress percent, memory currently occupied by the MPS and current fidelity. Additionally, some high level information of the current stage of the simulation is provided, such as when `MPSxMPO` is applying optimisation sweeps. From 50f93a98bbb343daa00dfc741ff033c3336dd4a1 Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:49:24 +0100 Subject: [PATCH 10/13] fix ttn headings --- docs/examples/python/ttn_tutorial.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/examples/python/ttn_tutorial.py b/docs/examples/python/ttn_tutorial.py index daa2325e..8c9d6141 100644 --- a/docs/examples/python/ttn_tutorial.py +++ b/docs/examples/python/ttn_tutorial.py @@ -1,3 +1,5 @@ +# # Tree Tensor Network (TTN) Tutorial + import numpy as np from time import time import matplotlib.pyplot as plt @@ -12,7 +14,7 @@ simulate, ) -# # Introduction +# ## Introduction # This notebook provides examples of the usage of the TTN functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html. # Some good references to learn about Tree Tensor Network state simulation: # - For an introduction into TTN based simulation of quantum circuits: https://arxiv.org/abs/2206.01000 @@ -20,7 +22,7 @@ # The implementation in pytket-cutensornet differs from previously published literature. I am still experimenting with the algorithm. I intend to write up a document detailing the approach, once I reach a stable version. # The main advantage of TTN over MPS is that it can be used to efficiently simulate circuits with richer qubit connectivity. This does **not** mean that TTN has an easy time simulating all-to-all connectivity, but it is far more flexible than MPS. TTN's strength is in simulating circuit where certain subsets of qubits interact densely with each other, and there is not that many gates acting on qubits in different subsets. -# # How to use +# ## How to use # The interface for TTN matches that of MPS. As such, you should be able to run any code that uses `SimulationAlgorithm.MPSxGate` by replacing it with `SimulationAlgorithm.TTNxGate`. Calling `prepare_circuit_mps` is no longer necessary, since `TTNxGate` can apply gates between non-neighbouring qubits. # **NOTE**: If you are new to pytket-cutensornet, it is highly recommended to start reading the `mps_tutorial.ipynb` notebook instead. More details about the use of the library are discussed there (for instance, why and when to call `CuTensorNetHandle()`). @@ -76,7 +78,7 @@ def random_graph_circuit(n_qubits: int, edge_prob: float, layers: int) -> Circui # ## Sampling from a TTN # Sampling and measurement from a TTN state is not currently supported. This will be added in an upcoming release. -# # Approximate simulation +# ## Approximate simulation # We provide two policies for approximate simulation: # * Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes. # * Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate. @@ -110,12 +112,12 @@ def random_graph_circuit(n_qubits: int, edge_prob: float, layers: int) -> Circui print("\nLower bound of the fidelity:") print(round(fixed_fidelity_ttn.fidelity, 4)) -# # Contraction algorithms +# ## Contraction algorithms # We currently offer only one TTN-based simulation algorithm. # * **TTNxGate**: Apply gates one by one to the TTN, canonicalising the TTN and truncating when necessary. -# # Using the logger +# ## Using the logger # You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent): # - `logging.INFO` will print information about progress percent, memory currently occupied by the TTN and current fidelity. Additionally, some high level information of the current stage of the simulation is provided. From 4464392c73bfc1d8411bd8a6ff87baf6eae729f9 Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:57:50 +0100 Subject: [PATCH 11/13] fix one last heading in GS notebook --- docs/examples/python/general_state_tutorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/python/general_state_tutorial.py b/docs/examples/python/general_state_tutorial.py index ff673806..dec45bb5 100644 --- a/docs/examples/python/general_state_tutorial.py +++ b/docs/examples/python/general_state_tutorial.py @@ -23,7 +23,7 @@ # # **Reference**: The original contraction path optimisation algorithm that NVIDIA implemented on CuTensorNet: https://arxiv.org/abs/2002.01935 -# # `GeneralState` +# ## `GeneralState` # The class `GeneralState` is used to convert a circuit into a tensor network and query information from the final state. Let's walk through a simple example. my_circ = Circuit(5) From 6e397a988e247d252bebcbc331b6dc41a5839cec Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:03:31 +0100 Subject: [PATCH 12/13] add notebooks with fixed headings --- docs/examples/general_state_tutorial.ipynb | 2 +- docs/examples/mps_tutorial.ipynb | 2 +- docs/examples/ttn_tutorial.ipynb | 2 +- docs/pytket-docs-theming | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples/general_state_tutorial.ipynb b/docs/examples/general_state_tutorial.ipynb index 9a041f99..66e0b252 100644 --- a/docs/examples/general_state_tutorial.ipynb +++ b/docs/examples/general_state_tutorial.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np\n", "from sympy import Symbol\n", "from scipy.stats import unitary_group # type: ignore\n", "from pytket.circuit import Circuit, OpType, Unitary2qBox, Qubit, Bit\n", "from pytket.passes import DecomposeBoxes\n", "from pytket.utils import QubitPauliOperator\n", "from pytket._tket.pauli import Pauli, QubitPauliString\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.cutensornet.general_state import (\n", " GeneralState,\n", " GeneralBraOpKet,\n", ")\n", "from pytket.extensions.cutensornet.backends import CuTensorNetShotsBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Introduction
\n", "This notebook is a guide on how to use the features provided in the `general_state` submodule of pytket-cutensornet. This submodule is a thin wrapper of CuTensorNet's `NetworkState`, allowing users to convert pytket circuits into tensor networks and use CuTensorNet's contraction path optimisation algorithm.
\n", "All simulations realised with this submodule are *exact*. Once the pytket circuit has been converted to a tensor network, the computation has two steps:
\n", " 1. *Contraction path optimisation*. Attempts to find an order of contracting pairs of tensors in which the the total number of FLOPs is minimised. No operation on the tensor network occurs at this point. Runs on CPU.
\n", " 2. *Tensor network contraction*. Uses the ordering of contractions found in the previous step evaluate the tensor network. Runs on GPU.
\n", "
\n", "**Reference**: The original contraction path optimisation algorithm that NVIDIA implemented on CuTensorNet: https://arxiv.org/abs/2002.01935"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# `GeneralState`
\n", "The class `GeneralState` is used to convert a circuit into a tensor network and query information from the final state. Let's walk through a simple example."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_circ = Circuit(5)\n", "my_circ.CX(3, 4)\n", "my_circ.H(2)\n", "my_circ.CZ(0, 1)\n", "my_circ.ZZPhase(0.1, 4, 3)\n", "my_circ.TK2(0.3, 0.5, 0.7, 2, 1)\n", "my_circ.Ry(0.2, 0)\n", "my_circ.measure_all()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(my_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The first step is to convert our pytket circuit into a tensor network. This is straightforward:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tn_state = GeneralState(my_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The variable `tn_state` now holds a tensor network representation of `my_circ`.
\n", "**Note**: Circuits must not have mid-circuit measurements or classical logic. The measurements at the end of the circuit are stripped and only considered when calling `tn_state.sample(n_shots)`.
\n", "We can now query information from the state. For instance, let's calculate the probability of in the qubits 0 and 3 agreeing in their outcome."]}, {"cell_type": "markdown", "metadata": {}, "source": ["First, let's generate `|x>` computational basis states where `q[0]` and `q[3]` agree on their values. We can do this with some bitwise operators and list comprehension.
\n", "**Note**: Remember that pytket uses \"increasing lexicographic order\" (ILO) for qubits, so `q[0]` is the most significant bit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["selected_states = [\n", " x\n", " for x in range(2**my_circ.n_qubits)\n", " if ( # Iterate over all possible states\n", " x & int(\"10000\", 2) == 0\n", " and x & int(\"00010\", 2) == 0 # both qubits are 0 or...\n", " or x & int(\"10000\", 2) != 0\n", " and x & int(\"00010\", 2) != 0 # both qubits are 1\n", " )\n", "]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now query the amplitude of all of these states and calculate the probability by summing their squared absolute values."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["amplitudes = []\n", "for x in selected_states:\n", " amplitudes.append(tn_state.get_amplitude(x))\n", "probability = sum(abs(a) ** 2 for a in amplitudes)\n", "print(f\"Probability: {probability}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Of course, calculating probabilities by considering the amplitudes of all relevant states is not efficient in general, since we may need to calculate a number of amplitudes that scales exponentially with the number of qubits. An alternative is to use expectation values. In particular, all of the states in `selected_states` are +1 eigenvectors of the `ZIIZI` observable and, hence, we can calculate the probability `p` by solving the equation ` = (+1)p + (-1)(1-p)` using the fact that `ZIIZI` only has +1 and -1 eigenvalues."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["string_ZIIZI = QubitPauliString(\n", " my_circ.qubits, [Pauli.Z, Pauli.I, Pauli.I, Pauli.Z, Pauli.I]\n", ")\n", "observable = QubitPauliOperator({string_ZIIZI: 1.0})\n", "expectation_val = tn_state.expectation_value(observable).real\n", "exp_probability = (expectation_val + 1) / 2\n", "assert np.isclose(probability, exp_probability, atol=0.0001)\n", "print(f\"Probability: {exp_probability}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can estimate the probability by sampling."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots = 100000\n", "outcomes = tn_state.sample(n_shots)\n", "hit_count = 0\n", "for bit_tuple, count in outcomes.get_counts().items():\n", " if bit_tuple[0] == bit_tuple[3]:\n", " hit_count += count\n", "samp_probability = hit_count / n_shots\n", "assert np.isclose(probability, samp_probability, atol=0.01)\n", "print(f\"Probability: {samp_probability}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["When we finish doing computations with the `tn_state` we must destroy it to free GPU memory."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tn_state.destroy()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To avoid forgetting this final step, we recommend users call `GeneralState` (and `GeneralBraOpKet`) as context managers:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with GeneralState(my_circ) as my_state:\n", " expectation_val = my_state.expectation_value(observable)\n", "print(expectation_val)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Using this syntax, `my_state` is automatically destroyed when the code exists the `with ...` block."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Parameterised circuits
\n", "Circuits that only differ on the parameters of their gates have the same tensor network topology and, hence, we may use the same contraction path for all of them."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["a, b, c = Symbol(\"a\"), Symbol(\"b\"), Symbol(\"c\")\n", "param_circ1 = Circuit(5)\n", "param_circ1.Ry(a, 3).Ry(0.27, 4).CX(4, 3).Ry(b, 2).Ry(0.21, 3)\n", "param_circ1.Ry(0.12, 0).Ry(a, 1)\n", "param_circ1.add_gate(OpType.CnX, [0, 1, 4]).add_gate(OpType.CnX, [4, 1, 3])\n", "param_circ1.X(0).X(1).add_gate(OpType.CnY, [0, 1, 2]).add_gate(OpType.CnY, [0, 4, 3]).X(\n", " 0\n", ").X(1)\n", "param_circ1.Ry(-b, 0).Ry(-c, 1)\n", "render_circuit_jupyter(param_circ1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can pass a parameterised circuit to `GeneralState`. The value of the parameters is provided when calling methods of `GeneralState`. The contraction path is automatically reused on different calls to the same method."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_circs = 5\n", "with GeneralState(param_circ1) as param_state:\n", " for i in range(n_circs):\n", " symbol_map = {s: np.random.random() for s in [a, b, c]}\n", " exp_val = param_state.expectation_value(observable, symbol_map=symbol_map)\n", " print(f\"Expectation value for circuit {i}: {exp_val.real}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# `GeneralBraOpKet`
\n", "The `GeneralBraOpKet` can be used to calculate any number that can be represented as the result of some `` where `|bra>` and `|ket>` are the final states of pytket circuits, and `op` is a `QubitPauliOperator`. The circuits for `|bra>` and `|ket>` need not be the same."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["x, y, z = Symbol(\"x\"), Symbol(\"y\"), Symbol(\"z\")\n", "param_circ2 = Circuit(5)\n", "param_circ2.H(0)\n", "param_circ2.S(1)\n", "param_circ2.Rz(x * z, 2)\n", "param_circ2.Ry(y + x, 3)\n", "param_circ2.TK1(x, y, z, 4)\n", "param_circ2.TK2(z - y, z - x, (x + y) * z, 1, 3)\n", "symbol_map = {a: 2.1, b: 1.3, c: 0.7, x: 3.0, y: 1.6, z: -8.3}"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can calculate inner products by providing no `op`:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with GeneralBraOpKet(bra=param_circ2, ket=param_circ1) as braket:\n", " inner_prod = braket.contract(symbol_map=symbol_map)\n", "with GeneralBraOpKet(bra=param_circ1, ket=param_circ2) as braket:\n", " inner_prod_conj = braket.contract(symbol_map=symbol_map)\n", "assert np.isclose(np.conj(inner_prod), inner_prod_conj)\n", "print(f\" = {inner_prod}\")\n", "print(f\" = {inner_prod_conj}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["And we are not constrained to Hermitian operators:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["string_XZIXX = QubitPauliString(\n", " param_circ2.qubits, [Pauli.X, Pauli.Z, Pauli.I, Pauli.X, Pauli.X]\n", ")\n", "string_IZZYX = QubitPauliString(\n", " param_circ2.qubits, [Pauli.I, Pauli.Z, Pauli.Z, Pauli.Y, Pauli.X]\n", ")\n", "string_ZIZXY = QubitPauliString(\n", " param_circ2.qubits, [Pauli.Z, Pauli.I, Pauli.Z, Pauli.X, Pauli.Y]\n", ")\n", "operator = QubitPauliOperator(\n", " {string_XZIXX: -1.38j, string_IZZYX: 2.36, string_ZIZXY: 0.42j + 0.3}\n", ")\n", "with GeneralBraOpKet(bra=param_circ2, ket=param_circ1) as braket:\n", " value = braket.contract(operator, symbol_map=symbol_map)\n", "print(value)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Backends
\n", "We provide a pytket `Backend` to obtain shots using `GeneralState`."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's consider a more challenging circuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def random_circuit(n_qubits: int, n_layers: int) -> Circuit:\n", " \"\"\"Random quantum volume circuit.\"\"\"\n", " c = Circuit(n_qubits, n_qubits)\n", " for _ in range(n_layers):\n", " qubits = np.random.permutation([i for i in range(n_qubits)])\n", " qubit_pairs = [[qubits[i], qubits[i + 1]] for i in range(0, n_qubits - 1, 2)]\n", " for pair in qubit_pairs:\n", " # Generate random 4x4 unitary matrix.\n", " SU4 = unitary_group.rvs(4) # random unitary in SU4\n", " SU4 = SU4 / (np.linalg.det(SU4) ** 0.25)\n", " SU4 = np.matrix(SU4)\n", " c.add_unitary2qbox(Unitary2qBox(SU4), *pair)\n", " DecomposeBoxes().apply(c)\n", " return c"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's measure only three of the qubits.
\n", "**Note**: The complexity of this simulation increases exponentially with the number of qubits measured. Other factors leading to intractability are circuit depth and qubit connectivity."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots = 1000\n", "quantum_vol_circ = random_circuit(n_qubits=40, n_layers=5)\n", "quantum_vol_circ.Measure(Qubit(0), Bit(0))\n", "quantum_vol_circ.Measure(Qubit(1), Bit(1))\n", "quantum_vol_circ.Measure(Qubit(2), Bit(2))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `CuTensorNetShotsBackend` is used in the same way as any other pytket `Backend`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = CuTensorNetShotsBackend()\n", "compiled_circ = backend.get_compiled_circuit(quantum_vol_circ)\n", "results = backend.run_circuit(compiled_circ, n_shots=n_shots)\n", "print(results.get_counts())"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4"}}, "nbformat": 4, "nbformat_minor": 2} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# `GeneralState` Tutorial"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np\n", "from sympy import Symbol\n", "from scipy.stats import unitary_group # type: ignore\n", "from pytket.circuit import Circuit, OpType, Unitary2qBox, Qubit, Bit\n", "from pytket.passes import DecomposeBoxes\n", "from pytket.utils import QubitPauliOperator\n", "from pytket._tket.pauli import Pauli, QubitPauliString\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.cutensornet.general_state import (\n", " GeneralState,\n", " GeneralBraOpKet,\n", ")\n", "from pytket.extensions.cutensornet.backends import CuTensorNetShotsBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Introduction
\n", "This notebook is a guide on how to use the features provided in the `general_state` submodule of pytket-cutensornet. This submodule is a thin wrapper of CuTensorNet's `NetworkState`, allowing users to convert pytket circuits into tensor networks and use CuTensorNet's contraction path optimisation algorithm.
\n", "All simulations realised with this submodule are *exact*. Once the pytket circuit has been converted to a tensor network, the computation has two steps:
\n", " 1. *Contraction path optimisation*. Attempts to find an order of contracting pairs of tensors in which the the total number of FLOPs is minimised. No operation on the tensor network occurs at this point. Runs on CPU.
\n", " 2. *Tensor network contraction*. Uses the ordering of contractions found in the previous step evaluate the tensor network. Runs on GPU.
\n", "
\n", "**Reference**: The original contraction path optimisation algorithm that NVIDIA implemented on CuTensorNet: https://arxiv.org/abs/2002.01935"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `GeneralState`
\n", "The class `GeneralState` is used to convert a circuit into a tensor network and query information from the final state. Let's walk through a simple example."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_circ = Circuit(5)\n", "my_circ.CX(3, 4)\n", "my_circ.H(2)\n", "my_circ.CZ(0, 1)\n", "my_circ.ZZPhase(0.1, 4, 3)\n", "my_circ.TK2(0.3, 0.5, 0.7, 2, 1)\n", "my_circ.Ry(0.2, 0)\n", "my_circ.measure_all()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(my_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The first step is to convert our pytket circuit into a tensor network. This is straightforward:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tn_state = GeneralState(my_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The variable `tn_state` now holds a tensor network representation of `my_circ`.
\n", "**Note**: Circuits must not have mid-circuit measurements or classical logic. The measurements at the end of the circuit are stripped and only considered when calling `tn_state.sample(n_shots)`.
\n", "We can now query information from the state. For instance, let's calculate the probability of in the qubits 0 and 3 agreeing in their outcome."]}, {"cell_type": "markdown", "metadata": {}, "source": ["First, let's generate `|x>` computational basis states where `q[0]` and `q[3]` agree on their values. We can do this with some bitwise operators and list comprehension.
\n", "**Note**: Remember that pytket uses \"increasing lexicographic order\" (ILO) for qubits, so `q[0]` is the most significant bit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["selected_states = [\n", " x\n", " for x in range(2**my_circ.n_qubits)\n", " if ( # Iterate over all possible states\n", " x & int(\"10000\", 2) == 0\n", " and x & int(\"00010\", 2) == 0 # both qubits are 0 or...\n", " or x & int(\"10000\", 2) != 0\n", " and x & int(\"00010\", 2) != 0 # both qubits are 1\n", " )\n", "]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now query the amplitude of all of these states and calculate the probability by summing their squared absolute values."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["amplitudes = []\n", "for x in selected_states:\n", " amplitudes.append(tn_state.get_amplitude(x))\n", "probability = sum(abs(a) ** 2 for a in amplitudes)\n", "print(f\"Probability: {probability}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Of course, calculating probabilities by considering the amplitudes of all relevant states is not efficient in general, since we may need to calculate a number of amplitudes that scales exponentially with the number of qubits. An alternative is to use expectation values. In particular, all of the states in `selected_states` are +1 eigenvectors of the `ZIIZI` observable and, hence, we can calculate the probability `p` by solving the equation ` = (+1)p + (-1)(1-p)` using the fact that `ZIIZI` only has +1 and -1 eigenvalues."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["string_ZIIZI = QubitPauliString(\n", " my_circ.qubits, [Pauli.Z, Pauli.I, Pauli.I, Pauli.Z, Pauli.I]\n", ")\n", "observable = QubitPauliOperator({string_ZIIZI: 1.0})\n", "expectation_val = tn_state.expectation_value(observable).real\n", "exp_probability = (expectation_val + 1) / 2\n", "assert np.isclose(probability, exp_probability, atol=0.0001)\n", "print(f\"Probability: {exp_probability}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can estimate the probability by sampling."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots = 100000\n", "outcomes = tn_state.sample(n_shots)\n", "hit_count = 0\n", "for bit_tuple, count in outcomes.get_counts().items():\n", " if bit_tuple[0] == bit_tuple[3]:\n", " hit_count += count\n", "samp_probability = hit_count / n_shots\n", "assert np.isclose(probability, samp_probability, atol=0.01)\n", "print(f\"Probability: {samp_probability}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["When we finish doing computations with the `tn_state` we must destroy it to free GPU memory."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tn_state.destroy()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To avoid forgetting this final step, we recommend users call `GeneralState` (and `GeneralBraOpKet`) as context managers:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with GeneralState(my_circ) as my_state:\n", " expectation_val = my_state.expectation_value(observable)\n", "print(expectation_val)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Using this syntax, `my_state` is automatically destroyed when the code exists the `with ...` block."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Parameterised circuits
\n", "Circuits that only differ on the parameters of their gates have the same tensor network topology and, hence, we may use the same contraction path for all of them."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["a, b, c = Symbol(\"a\"), Symbol(\"b\"), Symbol(\"c\")\n", "param_circ1 = Circuit(5)\n", "param_circ1.Ry(a, 3).Ry(0.27, 4).CX(4, 3).Ry(b, 2).Ry(0.21, 3)\n", "param_circ1.Ry(0.12, 0).Ry(a, 1)\n", "param_circ1.add_gate(OpType.CnX, [0, 1, 4]).add_gate(OpType.CnX, [4, 1, 3])\n", "param_circ1.X(0).X(1).add_gate(OpType.CnY, [0, 1, 2]).add_gate(OpType.CnY, [0, 4, 3]).X(\n", " 0\n", ").X(1)\n", "param_circ1.Ry(-b, 0).Ry(-c, 1)\n", "render_circuit_jupyter(param_circ1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can pass a parameterised circuit to `GeneralState`. The value of the parameters is provided when calling methods of `GeneralState`. The contraction path is automatically reused on different calls to the same method."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_circs = 5\n", "with GeneralState(param_circ1) as param_state:\n", " for i in range(n_circs):\n", " symbol_map = {s: np.random.random() for s in [a, b, c]}\n", " exp_val = param_state.expectation_value(observable, symbol_map=symbol_map)\n", " print(f\"Expectation value for circuit {i}: {exp_val.real}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `GeneralBraOpKet`
\n", "The `GeneralBraOpKet` can be used to calculate any number that can be represented as the result of some `` where `|bra>` and `|ket>` are the final states of pytket circuits, and `op` is a `QubitPauliOperator`. The circuits for `|bra>` and `|ket>` need not be the same."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["x, y, z = Symbol(\"x\"), Symbol(\"y\"), Symbol(\"z\")\n", "param_circ2 = Circuit(5)\n", "param_circ2.H(0)\n", "param_circ2.S(1)\n", "param_circ2.Rz(x * z, 2)\n", "param_circ2.Ry(y + x, 3)\n", "param_circ2.TK1(x, y, z, 4)\n", "param_circ2.TK2(z - y, z - x, (x + y) * z, 1, 3)\n", "symbol_map = {a: 2.1, b: 1.3, c: 0.7, x: 3.0, y: 1.6, z: -8.3}"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can calculate inner products by providing no `op`:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with GeneralBraOpKet(bra=param_circ2, ket=param_circ1) as braket:\n", " inner_prod = braket.contract(symbol_map=symbol_map)\n", "with GeneralBraOpKet(bra=param_circ1, ket=param_circ2) as braket:\n", " inner_prod_conj = braket.contract(symbol_map=symbol_map)\n", "assert np.isclose(np.conj(inner_prod), inner_prod_conj)\n", "print(f\" = {inner_prod}\")\n", "print(f\" = {inner_prod_conj}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["And we are not constrained to Hermitian operators:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["string_XZIXX = QubitPauliString(\n", " param_circ2.qubits, [Pauli.X, Pauli.Z, Pauli.I, Pauli.X, Pauli.X]\n", ")\n", "string_IZZYX = QubitPauliString(\n", " param_circ2.qubits, [Pauli.I, Pauli.Z, Pauli.Z, Pauli.Y, Pauli.X]\n", ")\n", "string_ZIZXY = QubitPauliString(\n", " param_circ2.qubits, [Pauli.Z, Pauli.I, Pauli.Z, Pauli.X, Pauli.Y]\n", ")\n", "operator = QubitPauliOperator(\n", " {string_XZIXX: -1.38j, string_IZZYX: 2.36, string_ZIZXY: 0.42j + 0.3}\n", ")\n", "with GeneralBraOpKet(bra=param_circ2, ket=param_circ1) as braket:\n", " value = braket.contract(operator, symbol_map=symbol_map)\n", "print(value)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Backends
\n", "We provide a pytket `Backend` to obtain shots using `GeneralState`."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's consider a more challenging circuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def random_circuit(n_qubits: int, n_layers: int) -> Circuit:\n", " \"\"\"Random quantum volume circuit.\"\"\"\n", " c = Circuit(n_qubits, n_qubits)\n", " for _ in range(n_layers):\n", " qubits = np.random.permutation([i for i in range(n_qubits)])\n", " qubit_pairs = [[qubits[i], qubits[i + 1]] for i in range(0, n_qubits - 1, 2)]\n", " for pair in qubit_pairs:\n", " # Generate random 4x4 unitary matrix.\n", " SU4 = unitary_group.rvs(4) # random unitary in SU4\n", " SU4 = SU4 / (np.linalg.det(SU4) ** 0.25)\n", " SU4 = np.matrix(SU4)\n", " c.add_unitary2qbox(Unitary2qBox(SU4), *pair)\n", " DecomposeBoxes().apply(c)\n", " return c"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's measure only three of the qubits.
\n", "**Note**: The complexity of this simulation increases exponentially with the number of qubits measured. Other factors leading to intractability are circuit depth and qubit connectivity."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots = 1000\n", "quantum_vol_circ = random_circuit(n_qubits=40, n_layers=5)\n", "quantum_vol_circ.Measure(Qubit(0), Bit(0))\n", "quantum_vol_circ.Measure(Qubit(1), Bit(1))\n", "quantum_vol_circ.Measure(Qubit(2), Bit(2))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `CuTensorNetShotsBackend` is used in the same way as any other pytket `Backend`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = CuTensorNetShotsBackend()\n", "compiled_circ = backend.get_compiled_circuit(quantum_vol_circ)\n", "results = backend.run_circuit(compiled_circ, n_shots=n_shots)\n", "print(results.get_counts())"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4"}}, "nbformat": 4, "nbformat_minor": 2} \ No newline at end of file diff --git a/docs/examples/mps_tutorial.ipynb b/docs/examples/mps_tutorial.ipynb index 1735b1f9..649a9f76 100644 --- a/docs/examples/mps_tutorial.ipynb +++ b/docs/examples/mps_tutorial.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np\n", "from time import time\n", "import matplotlib.pyplot as plt\n", "from pytket import Circuit, OpType\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.cutensornet.structured_state import (\n", " CuTensorNetHandle,\n", " Config,\n", " SimulationAlgorithm,\n", " simulate,\n", " prepare_circuit_mps,\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Introduction
\n", "This notebook provides examples of the usage of the MPS functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.
\n", "A Matrix Product State (MPS) represents a state on `n` qubits as a list of `n` tensors connected in a line as show below:
\n", "![MPS](images/mps.png)
\n", "Each of these circles corresponds to a tensor. We refer to each leg of a tensor as a *bond* and the number of bonds a tensor has is its *rank*. In code, a tensor is just a multidimensional array:
\n", "
\n", "```tensor[i][j][k] = v```
\n", "
\n", "In the case above, we are assigning an entry value `v` of a rank-3 tensor (one `[ ]` coordinate per bond). Each bond allows a different number of values for its indices; for instance `0 <= i < 4` would mean that the first bond of our tensor can take up to four different indices; we refer to this as the *dimension* of the bond. We refer to the bonds connecting different tensors in the MPS as *virtual bonds*; the maximum allowed value for the dimension of virtual bonds is often denoted by the greek letter `chi`. The open bonds are known as *physical bonds* and, in our case, each will correspond to a qubit; hence, they have dimension `2` -- the dimension of the vector space of a single qubit.
\n", "In essence, whenever we want to apply a gate to certain qubit we will connect a tensor (matrix) representing the gate to the corresponding physical bond and *contract* the network back to an MPS form (tensor contraction is a generalisation of matrix multiplication to multidimensional arrays). Whenever a two-qubit gate is applied, the entanglement information after contraction will be kept in the degrees of freedom of the virtual bonds. As such, the dimension of the virtual bonds will generally increase exponentially as we apply entangling gates, leading to large memory footprints of the tensors and, consequently, long runtime for tensor contraction. We provide functionalities to limit the growth of the dimension of the virtual bonds, keeping resource consumption in check. Read the *Approximate simulation* section on this notebook to learn more.
\n", "**References**: To read more about MPS we recommend the following papers.
\n", "* For an introduction to MPS and its canonical form: https://arxiv.org/abs/1901.05824.
\n", "* For a description of the `MPSxGate` algorithm we provide: https://arxiv.org/abs/2002.07730.
\n", "* For a description of the `MPSxMPO` algorithm we provide: https://arxiv.org/abs/2207.05612.
\n", "* For insights on the reationship between truncation error and the error model in a quantum computer: https://arxiv.org/abs/2004.02388"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Basic functionality and exact simulation
\n", "Here we show an example of the basic use of our MPS methods. We first generate a simple `pytket` circuit to be simulated."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_circ = Circuit(5)\n", "my_circ.CX(3, 4)\n", "my_circ.H(2)\n", "my_circ.CZ(0, 1)\n", "my_circ.ZZPhase(0.1, 4, 3)\n", "my_circ.TK2(0.3, 0.5, 0.7, 2, 1)\n", "my_circ.Ry(0.2, 0)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(my_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For **exact** simulation, simply call the `simulate` function on the circuit and choose a contraction algorithm. To learn more about the contraction algorithms we provide see the *Contraction algorithms* section of this notebook. You will also need to provide a configuration, the default one is provided by `Config()`. Custom settings of `Config` are discussed in the *Approximate simulation* section.
\n", "**NOTE**: whenever you wish to generate an `MPS` object or execute calculations on it you must do so within a `with CuTensorNetHandle() as libhandle:` block; this will initialise the cuTensorNetwork library for you, and destroy its handles at the end of the `with` block. You will need to pass the `libhandle` to the `MPS` object via the method that generates it (in the snippet below, `simulate`), or if already initialised, pass it via the `update_libhandle` method.
\n", "Due to the nature of Jupyter notebooks, we will be starting most of these cells with a `with CuTensorNetHandle() as libhandle:`. However, in a standard script, all of these cells would be grouped together and a single `with CuTensorNetHandle() as libhandle:` statement would be necessary at the beginning of the script."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps = simulate(libhandle, my_circ, SimulationAlgorithm.MPSxGate, Config())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Notice that `my_circ` uses a rich gateset -- in fact, every single-qubit and two-qubit gate supported by `pytket` can be used in our MPS approaches. Gates acting on more than two qubits are not currently supported."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The output of `simulate` is an `MPS` object encoding the output state of the circuit.
\n", "### Obtain an amplitude from an MPS
\n", "Let's first see how to get the amplitude of the state `|10100>` from the output of the previous circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state = int(\"10100\", 2)\n", "with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " amplitude = my_mps.get_amplitude(state)\n", "print(amplitude)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Since this is a very small circuit, we can use `pytket`'s state vector simulator capabilities to verify that the state is correct by checking the amplitude of each of the computational states."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state_vector = my_circ.get_statevector()\n", "n_qubits = len(my_circ.qubits)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["correct_amplitude = [False] * (2**n_qubits)\n", "with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " for i in range(2**n_qubits):\n", " correct_amplitude[i] = np.isclose(state_vector[i], my_mps.get_amplitude(i))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Are all amplitudes correct?\")\n", "print(all(correct_amplitude))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Sampling from an MPS
\n", "We can also sample from the output state of a circuit by calling `my_mps.sample`, where `my_mps` is the outcome of simulating the circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_samples = 100\n", "n_qubits = len(my_circ.qubits)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Initialise the sample counter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["sample_count = [0 for _ in range(2**n_qubits)]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " for _ in range(n_samples):\n", " # Draw a sample\n", " qubit_outcomes = my_mps.sample()\n", " # Convert qubit outcomes to bitstring\n", " bitstring = \"\".join(str(qubit_outcomes[q]) for q in my_circ.qubits)\n", " # Convert bitstring to int\n", " outcome = int(bitstring, 2)\n", " # Update the sample dictionary\n", " sample_count[outcome] += 1"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Calculate the theoretical number of samples per bitstring"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["expected_count = [n_samples * abs(state_vector[i]) ** 2 for i in range(2**n_qubits)]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Plot a comparison of theory vs sampled"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["plt.scatter(range(2**n_qubits), expected_count, label=\"Theory\")\n", "plt.scatter(range(2**n_qubits), sample_count, label=\"Experiment\", marker=\"x\")\n", "plt.xlabel(\"Basis states\")\n", "plt.ylabel(\"Samples\")\n", "plt.legend()\n", "plt.show()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We also provide methods to apply mid-circuit measurements via `my_mps.measure(qubits)` and postselection via `my_mps.postselect(qubit_outcomes)`. Their use is similar to that of `my_mps.sample()` shown above.
\n", "**Note:** whereas `my_mps.sample()` does *not* change the state of the MPS, `my_mps.measure(qubits)` and `my_mps.postselect(qubit_outcomes)` do change it, projecting the state to the resulting outcome and removing the measured qubits."]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Inner products
\n", "Using `vdot` you can obtain the inner product of two states in MPS form. This method does not change the internal data of neither of the MPS. Moreover, it can be used on the same `MPS` object for both inputs, yielding the squared norm of the state."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " norm_sq = my_mps.vdot(my_mps)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"As expected, the squared norm of a state is 1\")\n", "print(np.isclose(norm_sq, 1))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's come up with another circuit on the same qubits and apply an inner product between the two `MPS` objects."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Generate circuits"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["other_circ = Circuit(5)\n", "other_circ.H(3)\n", "other_circ.CZ(3, 4)\n", "other_circ.XXPhase(0.3, 1, 2)\n", "other_circ.Ry(0.7, 3)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Simulate them"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " other_mps = simulate(libhandle, other_circ, SimulationAlgorithm.MPSxGate, Config())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's calculate the inner product and check that it agrees with `pytket`'s state vector based computation."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " inner_product = my_mps.vdot(other_mps)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_state = my_circ.get_statevector()\n", "other_state = other_circ.get_statevector()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Is the inner product correct?\")\n", "print(np.isclose(np.vdot(my_state, other_state), inner_product))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Mid-circuit measurements and classical control
\n", "Mid-circuit measurements and classical control is supported (only in `MPSxGate` as of v0.8.0). For instance, we can implement the teleportation protocol on a pytket circuit and simulate it:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit()\n", "alice = circ.add_q_register(\"alice\", 2)\n", "alice_bits = circ.add_c_register(\"alice_bits\", 2)\n", "bob = circ.add_q_register(\"bob\", 1)\n", "# Initialise Alice's first qubit in some arbitrary state\n", "circ.Rx(0.42, alice[0])\n", "orig_state = circ.get_statevector()\n", "# Create a Bell pair shared between Alice and Bob\n", "circ.H(alice[1]).CX(alice[1], bob[0])\n", "# Apply a Bell measurement on Alice's qubits\n", "circ.CX(alice[0], alice[1]).H(alice[0])\n", "circ.Measure(alice[0], alice_bits[0])\n", "circ.Measure(alice[1], alice_bits[1])\n", "# Apply conditional corrections on Bob's qubits\n", "circ.add_gate(OpType.X, [bob[0]], condition_bits=[alice_bits[1]], condition_value=1)\n", "circ.add_gate(OpType.Z, [bob[0]], condition_bits=[alice_bits[0]], condition_value=1)\n", "# Reset Alice's qubits\n", "circ.add_gate(OpType.Reset, [alice[0]])\n", "circ.add_gate(OpType.Reset, [alice[1]])\n", "# Display the circuit\n", "render_circuit_jupyter(circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now simulate the circuit and check that the qubit has been successfully teleported."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\n", " f\"Initial state:\\n {np.round(orig_state[0],2)}|00>|0> + {np.round(orig_state[4],2)}|10>|0>\"\n", ")\n", "with CuTensorNetHandle() as libhandle:\n", " state = simulate(libhandle, circ, SimulationAlgorithm.MPSxGate, Config())\n", " print(\n", " f\"Teleported state:\\n {np.round(state.get_amplitude(0),2)}|00>|0> + {np.round(state.get_amplitude(1),2)}|00>|1>\"\n", " )\n", " print(f\"Measurement outcomes:\\n {state.get_bits()}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Two-qubit gates acting on non-adjacent qubits
\n", "Standard MPS algorithms only support simulation of two-qubit gates acting on neighbour qubits. In our implementation, however, two-qubit gates between arbitrary qubits may be applied, as shown below."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit(5)\n", "circ.H(1)\n", "circ.ZZPhase(0.3, 1, 3)\n", "circ.CX(0, 2)\n", "circ.Ry(0.8, 4)\n", "circ.CZ(3, 4)\n", "circ.XXPhase(0.7, 1, 2)\n", "circ.TK2(0.1, 0.2, 0.4, 1, 4)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " mps = simulate(libhandle, circ, SimulationAlgorithm.MPSxGate, Config())\n", " print(\"Did simulation succeed?\")\n", " print(mps.is_valid())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["**Note**: Even though two-qubit gates on non-adjacent qubits are simulable, the overhead on these is considerably larger than simulating gates on adjacent qubits. As a rule of thumb if the two qubits are `n` positions apart, the overhead is upper bounded by the cost of simulating `n-1` additional SWAP gates to move the leftmost qubit near the rightmost. In reality, the implementation we use is more nuanced than just applying SWAP gates, and the qubits don't actually change position.
\n", "When circuits are shallow, using our approach to simulate long-distance two-qubit gates is advantageous. In the case of deep circuits with many long-distance gates, it is sometimes beneficial to use TKET routing on the circuit, explicitly adding SWAP gates so that all two-qubit gates act on nearest neighbour qubits. Users may do this by calling `prepare_circuit_mps`, which is a wrapper of the corresponding TKET routing pass."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["prepared_circ, qubit_map = prepare_circuit_mps(circ)\n", "render_circuit_jupyter(prepared_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The circuit can now be simulated as usual."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " mps = simulate(libhandle, prepared_circ, SimulationAlgorithm.MPSxGate, Config())\n", " print(\"Did simulation succeed?\")\n", " print(mps.is_valid())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Notice that the qubits in the `prepared_circ` were renamed when applying `prepare_circuit_mps`. Implicit SWAPs may have been added to the circuit, meaning that the logical qubit held at the ``node[i]`` qubit at the beginning of the circuit may differ from the one it holds at the end; this information is captured by the `qubit_map` output. We recommend applying ``apply_qubit_relabelling`` on the MPS after simulation, relabelling the qubits according to these implicit SWAPs."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(qubit_map)\n", "mps.apply_qubit_relabelling(qubit_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Approximate simulation
\n", "We provide two policies for approximate simulation; these are supported by both of our current MPS contraction algorithms:
\n", "* Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes.
\n", "* Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate.
\n", "Values for `chi` and `truncation_fidelity` can be set via `Config`. To showcase approximate simulation, let's define a circuit where exact MPS contraction starts struggling."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def random_line_circuit(n_qubits: int, layers: int) -> Circuit:\n", " \"\"\"Random circuit with line connectivity.\"\"\"\n", " c = Circuit(n_qubits)\n", " for i in range(layers):\n", " # Layer of TK1 gates\n", " for q in range(n_qubits):\n", " c.TK1(np.random.rand(), np.random.rand(), np.random.rand(), q)\n\n", " # Layer of CX gates\n", " offset = np.mod(i, 2) # Even layers connect (q0,q1), odd (q1,q2)\n", " qubit_pairs = [\n", " [c.qubits[i], c.qubits[i + 1]] for i in range(offset, n_qubits - 1, 2)\n", " ]\n", " # Direction of each CX gate is random\n", " for pair in qubit_pairs:\n", " np.random.shuffle(pair)\n", " for pair in qubit_pairs:\n", " c.CX(pair[0], pair[1])\n", " return c"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circuit = random_line_circuit(n_qubits=20, layers=20)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For exact contraction, `chi` must be allowed to be up to `2**(n_qubits // 2)`, meaning that if we set `n_qubits = 20` it would require `chi = 1024`; already too much for this particular circuit to be simulated in a gaming laptop using the current implementation. Instead, let's bound `chi` to a maximum of `16`. Doing so results in faster runtime, at the expense of losing output state fidelity."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=16)\n", " bound_chi_mps = simulate(libhandle, circuit, SimulationAlgorithm.MPSxGate, config)\n", "end = time()\n", "print(\"Time taken by approximate contraction with bound chi:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(bound_chi_mps.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can fix `truncation_fidelity` and let `chi` increase as necessary to satisfy it."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(truncation_fidelity=0.999)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxGate, config\n", " )\n", "end = time()\n", "print(\"Time taken by approximate contraction with fixed truncation fidelity:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(fixed_fidelity_mps.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Contraction algorithms"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We currently offer two MPS-based simulation algorithms:
\n", "* **MPSxGate**: Apply gates one by one to the MPS, canonicalising the MPS and truncating when necessary. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2002.07730.
\n", "* **MPSxMPO**: Maintain two MPS copies of the state as it evolves, one updated eagerly using the **MPSxGate** method and the other updated in batches of up to `k` layers of two-qubit gates. Whenever the second MPS is updated, both copies are synchronised and an optimisation algorithm is applied to increase the fidelity of the state. This algorithm is often referred to as DMRG-like simulation. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2207.05612.
\n", "The `MPSxGate` algorithm is the one we have been using for all of the examples above. In comparison, the `MPSxMPO` algorithm provides the user with two new parameters to tune:
\n", "* **k**: The maximum number of layers the MPO is allowed to have before being contracted. Increasing this might increase fidelity, but it will also increase resource requirements exponentially. Default value is `4`.
\n", "* **optim_delta**: Stopping criteria for the optimisation when contracting the `k` layers of MPO. Stops when the increase of fidelity between iterations is smaller than `optim_delta`. Default value is `1e-5`.
\n", "Both `k` and `optim_delta` can be set via `Config`. Below we compare `MPSxGate` versus `MPSxMPO` with default parameters and `MPSxMPO` with more resource-hungry parameters. The circuit used is the same as in the previous section."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=16)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxGate, config\n", " )\n", "end = time()\n", "print(\"MPSxGate\")\n", "print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n", "print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=16)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxMPO, config\n", " )\n", "end = time()\n", "print(\"MPSxMPO, default parameters\")\n", "print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n", "print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(k=8, optim_delta=1e-15, chi=16)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxMPO, config\n", " )\n", "end = time()\n", "print(\"MPSxMPO, custom parameters\")\n", "print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n", "print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["**Note**: `MPSxMPO` also admits truncation policy in terms of `truncation_fidelity` instead of `chi`."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Using the logger"]}, {"cell_type": "markdown", "metadata": {}, "source": ["You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent):
\n", "- `logging.INFO` will print information about progress percent, memory currently occupied by the MPS and current fidelity. Additionally, some high level information of the current stage of the simulation is provided, such as when `MPSxMPO` is applying optimisation sweeps.
\n", "- `logging.DEBUG` provides all of the messages from the loglevel above plus detailed information of the current operation being carried out and the values of important variables.
\n", "**Note**: Due to technical issues with the `logging` module and Jupyter notebooks we need to reload the `logging` module. When working with python scripts and command line, just doing `import logging` is enough."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from importlib import reload # Not needed in Python 2\n", "import logging"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["reload(logging)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["An example of the use of `logging.INFO` is provided below."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " config = Config(truncation_fidelity=0.999, loglevel=logging.INFO)\n", " simulate(libhandle, circuit, SimulationAlgorithm.MPSxMPO, config)"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4"}}, "nbformat": 4, "nbformat_minor": 2} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Matrix Product State (MPS) Tutorial"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np\n", "from time import time\n", "import matplotlib.pyplot as plt\n", "from pytket import Circuit, OpType\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.cutensornet.structured_state import (\n", " CuTensorNetHandle,\n", " Config,\n", " SimulationAlgorithm,\n", " simulate,\n", " prepare_circuit_mps,\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Introduction
\n", "This notebook provides examples of the usage of the MPS functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.
\n", "A Matrix Product State (MPS) represents a state on `n` qubits as a list of `n` tensors connected in a line as show below:
\n", "![MPS](images/mps.png)
\n", "Each of these circles corresponds to a tensor. We refer to each leg of a tensor as a *bond* and the number of bonds a tensor has is its *rank*. In code, a tensor is just a multidimensional array:
\n", "
\n", "```tensor[i][j][k] = v```
\n", "
\n", "In the case above, we are assigning an entry value `v` of a rank-3 tensor (one `[ ]` coordinate per bond). Each bond allows a different number of values for its indices; for instance `0 <= i < 4` would mean that the first bond of our tensor can take up to four different indices; we refer to this as the *dimension* of the bond. We refer to the bonds connecting different tensors in the MPS as *virtual bonds*; the maximum allowed value for the dimension of virtual bonds is often denoted by the greek letter `chi`. The open bonds are known as *physical bonds* and, in our case, each will correspond to a qubit; hence, they have dimension `2` -- the dimension of the vector space of a single qubit.
\n", "In essence, whenever we want to apply a gate to certain qubit we will connect a tensor (matrix) representing the gate to the corresponding physical bond and *contract* the network back to an MPS form (tensor contraction is a generalisation of matrix multiplication to multidimensional arrays). Whenever a two-qubit gate is applied, the entanglement information after contraction will be kept in the degrees of freedom of the virtual bonds. As such, the dimension of the virtual bonds will generally increase exponentially as we apply entangling gates, leading to large memory footprints of the tensors and, consequently, long runtime for tensor contraction. We provide functionalities to limit the growth of the dimension of the virtual bonds, keeping resource consumption in check. Read the *Approximate simulation* section on this notebook to learn more.
\n", "**References**: To read more about MPS we recommend the following papers.
\n", "* For an introduction to MPS and its canonical form: https://arxiv.org/abs/1901.05824.
\n", "* For a description of the `MPSxGate` algorithm we provide: https://arxiv.org/abs/2002.07730.
\n", "* For a description of the `MPSxMPO` algorithm we provide: https://arxiv.org/abs/2207.05612.
\n", "* For insights on the reationship between truncation error and the error model in a quantum computer: https://arxiv.org/abs/2004.02388"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Basic functionality and exact simulation
\n", "Here we show an example of the basic use of our MPS methods. We first generate a simple `pytket` circuit to be simulated."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_circ = Circuit(5)\n", "my_circ.CX(3, 4)\n", "my_circ.H(2)\n", "my_circ.CZ(0, 1)\n", "my_circ.ZZPhase(0.1, 4, 3)\n", "my_circ.TK2(0.3, 0.5, 0.7, 2, 1)\n", "my_circ.Ry(0.2, 0)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(my_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For **exact** simulation, simply call the `simulate` function on the circuit and choose a contraction algorithm. To learn more about the contraction algorithms we provide see the *Contraction algorithms* section of this notebook. You will also need to provide a configuration, the default one is provided by `Config()`. Custom settings of `Config` are discussed in the *Approximate simulation* section.
\n", "**NOTE**: whenever you wish to generate an `MPS` object or execute calculations on it you must do so within a `with CuTensorNetHandle() as libhandle:` block; this will initialise the cuTensorNetwork library for you, and destroy its handles at the end of the `with` block. You will need to pass the `libhandle` to the `MPS` object via the method that generates it (in the snippet below, `simulate`), or if already initialised, pass it via the `update_libhandle` method.
\n", "Due to the nature of Jupyter notebooks, we will be starting most of these cells with a `with CuTensorNetHandle() as libhandle:`. However, in a standard script, all of these cells would be grouped together and a single `with CuTensorNetHandle() as libhandle:` statement would be necessary at the beginning of the script."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps = simulate(libhandle, my_circ, SimulationAlgorithm.MPSxGate, Config())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Notice that `my_circ` uses a rich gateset -- in fact, every single-qubit and two-qubit gate supported by `pytket` can be used in our MPS approaches. Gates acting on more than two qubits are not currently supported."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The output of `simulate` is an `MPS` object encoding the output state of the circuit.
\n", "### Obtain an amplitude from an MPS
\n", "Let's first see how to get the amplitude of the state `|10100>` from the output of the previous circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state = int(\"10100\", 2)\n", "with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " amplitude = my_mps.get_amplitude(state)\n", "print(amplitude)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Since this is a very small circuit, we can use `pytket`'s state vector simulator capabilities to verify that the state is correct by checking the amplitude of each of the computational states."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state_vector = my_circ.get_statevector()\n", "n_qubits = len(my_circ.qubits)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["correct_amplitude = [False] * (2**n_qubits)\n", "with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " for i in range(2**n_qubits):\n", " correct_amplitude[i] = np.isclose(state_vector[i], my_mps.get_amplitude(i))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Are all amplitudes correct?\")\n", "print(all(correct_amplitude))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Sampling from an MPS
\n", "We can also sample from the output state of a circuit by calling `my_mps.sample`, where `my_mps` is the outcome of simulating the circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_samples = 100\n", "n_qubits = len(my_circ.qubits)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Initialise the sample counter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["sample_count = [0 for _ in range(2**n_qubits)]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " for _ in range(n_samples):\n", " # Draw a sample\n", " qubit_outcomes = my_mps.sample()\n", " # Convert qubit outcomes to bitstring\n", " bitstring = \"\".join(str(qubit_outcomes[q]) for q in my_circ.qubits)\n", " # Convert bitstring to int\n", " outcome = int(bitstring, 2)\n", " # Update the sample dictionary\n", " sample_count[outcome] += 1"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Calculate the theoretical number of samples per bitstring"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["expected_count = [n_samples * abs(state_vector[i]) ** 2 for i in range(2**n_qubits)]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Plot a comparison of theory vs sampled"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["plt.scatter(range(2**n_qubits), expected_count, label=\"Theory\")\n", "plt.scatter(range(2**n_qubits), sample_count, label=\"Experiment\", marker=\"x\")\n", "plt.xlabel(\"Basis states\")\n", "plt.ylabel(\"Samples\")\n", "plt.legend()\n", "plt.show()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We also provide methods to apply mid-circuit measurements via `my_mps.measure(qubits)` and postselection via `my_mps.postselect(qubit_outcomes)`. Their use is similar to that of `my_mps.sample()` shown above.
\n", "**Note:** whereas `my_mps.sample()` does *not* change the state of the MPS, `my_mps.measure(qubits)` and `my_mps.postselect(qubit_outcomes)` do change it, projecting the state to the resulting outcome and removing the measured qubits."]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Inner products
\n", "Using `vdot` you can obtain the inner product of two states in MPS form. This method does not change the internal data of neither of the MPS. Moreover, it can be used on the same `MPS` object for both inputs, yielding the squared norm of the state."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " norm_sq = my_mps.vdot(my_mps)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"As expected, the squared norm of a state is 1\")\n", "print(np.isclose(norm_sq, 1))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's come up with another circuit on the same qubits and apply an inner product between the two `MPS` objects."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Generate circuits"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["other_circ = Circuit(5)\n", "other_circ.H(3)\n", "other_circ.CZ(3, 4)\n", "other_circ.XXPhase(0.3, 1, 2)\n", "other_circ.Ry(0.7, 3)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Simulate them"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " other_mps = simulate(libhandle, other_circ, SimulationAlgorithm.MPSxGate, Config())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's calculate the inner product and check that it agrees with `pytket`'s state vector based computation."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_mps.update_libhandle(libhandle)\n", " inner_product = my_mps.vdot(other_mps)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_state = my_circ.get_statevector()\n", "other_state = other_circ.get_statevector()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Is the inner product correct?\")\n", "print(np.isclose(np.vdot(my_state, other_state), inner_product))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Mid-circuit measurements and classical control
\n", "Mid-circuit measurements and classical control is supported (only in `MPSxGate` as of v0.8.0). For instance, we can implement the teleportation protocol on a pytket circuit and simulate it:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit()\n", "alice = circ.add_q_register(\"alice\", 2)\n", "alice_bits = circ.add_c_register(\"alice_bits\", 2)\n", "bob = circ.add_q_register(\"bob\", 1)\n", "# Initialise Alice's first qubit in some arbitrary state\n", "circ.Rx(0.42, alice[0])\n", "orig_state = circ.get_statevector()\n", "# Create a Bell pair shared between Alice and Bob\n", "circ.H(alice[1]).CX(alice[1], bob[0])\n", "# Apply a Bell measurement on Alice's qubits\n", "circ.CX(alice[0], alice[1]).H(alice[0])\n", "circ.Measure(alice[0], alice_bits[0])\n", "circ.Measure(alice[1], alice_bits[1])\n", "# Apply conditional corrections on Bob's qubits\n", "circ.add_gate(OpType.X, [bob[0]], condition_bits=[alice_bits[1]], condition_value=1)\n", "circ.add_gate(OpType.Z, [bob[0]], condition_bits=[alice_bits[0]], condition_value=1)\n", "# Reset Alice's qubits\n", "circ.add_gate(OpType.Reset, [alice[0]])\n", "circ.add_gate(OpType.Reset, [alice[1]])\n", "# Display the circuit\n", "render_circuit_jupyter(circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now simulate the circuit and check that the qubit has been successfully teleported."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\n", " f\"Initial state:\\n {np.round(orig_state[0],2)}|00>|0> + {np.round(orig_state[4],2)}|10>|0>\"\n", ")\n", "with CuTensorNetHandle() as libhandle:\n", " state = simulate(libhandle, circ, SimulationAlgorithm.MPSxGate, Config())\n", " print(\n", " f\"Teleported state:\\n {np.round(state.get_amplitude(0),2)}|00>|0> + {np.round(state.get_amplitude(1),2)}|00>|1>\"\n", " )\n", " print(f\"Measurement outcomes:\\n {state.get_bits()}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Two-qubit gates acting on non-adjacent qubits
\n", "Standard MPS algorithms only support simulation of two-qubit gates acting on neighbour qubits. In our implementation, however, two-qubit gates between arbitrary qubits may be applied, as shown below."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit(5)\n", "circ.H(1)\n", "circ.ZZPhase(0.3, 1, 3)\n", "circ.CX(0, 2)\n", "circ.Ry(0.8, 4)\n", "circ.CZ(3, 4)\n", "circ.XXPhase(0.7, 1, 2)\n", "circ.TK2(0.1, 0.2, 0.4, 1, 4)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " mps = simulate(libhandle, circ, SimulationAlgorithm.MPSxGate, Config())\n", " print(\"Did simulation succeed?\")\n", " print(mps.is_valid())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["**Note**: Even though two-qubit gates on non-adjacent qubits are simulable, the overhead on these is considerably larger than simulating gates on adjacent qubits. As a rule of thumb if the two qubits are `n` positions apart, the overhead is upper bounded by the cost of simulating `n-1` additional SWAP gates to move the leftmost qubit near the rightmost. In reality, the implementation we use is more nuanced than just applying SWAP gates, and the qubits don't actually change position.
\n", "When circuits are shallow, using our approach to simulate long-distance two-qubit gates is advantageous. In the case of deep circuits with many long-distance gates, it is sometimes beneficial to use TKET routing on the circuit, explicitly adding SWAP gates so that all two-qubit gates act on nearest neighbour qubits. Users may do this by calling `prepare_circuit_mps`, which is a wrapper of the corresponding TKET routing pass."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["prepared_circ, qubit_map = prepare_circuit_mps(circ)\n", "render_circuit_jupyter(prepared_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The circuit can now be simulated as usual."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " mps = simulate(libhandle, prepared_circ, SimulationAlgorithm.MPSxGate, Config())\n", " print(\"Did simulation succeed?\")\n", " print(mps.is_valid())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Notice that the qubits in the `prepared_circ` were renamed when applying `prepare_circuit_mps`. Implicit SWAPs may have been added to the circuit, meaning that the logical qubit held at the ``node[i]`` qubit at the beginning of the circuit may differ from the one it holds at the end; this information is captured by the `qubit_map` output. We recommend applying ``apply_qubit_relabelling`` on the MPS after simulation, relabelling the qubits according to these implicit SWAPs."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(qubit_map)\n", "mps.apply_qubit_relabelling(qubit_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Approximate simulation
\n", "We provide two policies for approximate simulation; these are supported by both of our current MPS contraction algorithms:
\n", "* Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes.
\n", "* Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate.
\n", "Values for `chi` and `truncation_fidelity` can be set via `Config`. To showcase approximate simulation, let's define a circuit where exact MPS contraction starts struggling."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def random_line_circuit(n_qubits: int, layers: int) -> Circuit:\n", " \"\"\"Random circuit with line connectivity.\"\"\"\n", " c = Circuit(n_qubits)\n", " for i in range(layers):\n", " # Layer of TK1 gates\n", " for q in range(n_qubits):\n", " c.TK1(np.random.rand(), np.random.rand(), np.random.rand(), q)\n\n", " # Layer of CX gates\n", " offset = np.mod(i, 2) # Even layers connect (q0,q1), odd (q1,q2)\n", " qubit_pairs = [\n", " [c.qubits[i], c.qubits[i + 1]] for i in range(offset, n_qubits - 1, 2)\n", " ]\n", " # Direction of each CX gate is random\n", " for pair in qubit_pairs:\n", " np.random.shuffle(pair)\n", " for pair in qubit_pairs:\n", " c.CX(pair[0], pair[1])\n", " return c"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circuit = random_line_circuit(n_qubits=20, layers=20)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For exact contraction, `chi` must be allowed to be up to `2**(n_qubits // 2)`, meaning that if we set `n_qubits = 20` it would require `chi = 1024`; already too much for this particular circuit to be simulated in a gaming laptop using the current implementation. Instead, let's bound `chi` to a maximum of `16`. Doing so results in faster runtime, at the expense of losing output state fidelity."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=16)\n", " bound_chi_mps = simulate(libhandle, circuit, SimulationAlgorithm.MPSxGate, config)\n", "end = time()\n", "print(\"Time taken by approximate contraction with bound chi:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(bound_chi_mps.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can fix `truncation_fidelity` and let `chi` increase as necessary to satisfy it."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(truncation_fidelity=0.999)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxGate, config\n", " )\n", "end = time()\n", "print(\"Time taken by approximate contraction with fixed truncation fidelity:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(fixed_fidelity_mps.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Contraction algorithms"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We currently offer two MPS-based simulation algorithms:
\n", "* **MPSxGate**: Apply gates one by one to the MPS, canonicalising the MPS and truncating when necessary. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2002.07730.
\n", "* **MPSxMPO**: Maintain two MPS copies of the state as it evolves, one updated eagerly using the **MPSxGate** method and the other updated in batches of up to `k` layers of two-qubit gates. Whenever the second MPS is updated, both copies are synchronised and an optimisation algorithm is applied to increase the fidelity of the state. This algorithm is often referred to as DMRG-like simulation. In particular, we implemented the algorithm from the following paper: https://arxiv.org/abs/2207.05612.
\n", "The `MPSxGate` algorithm is the one we have been using for all of the examples above. In comparison, the `MPSxMPO` algorithm provides the user with two new parameters to tune:
\n", "* **k**: The maximum number of layers the MPO is allowed to have before being contracted. Increasing this might increase fidelity, but it will also increase resource requirements exponentially. Default value is `4`.
\n", "* **optim_delta**: Stopping criteria for the optimisation when contracting the `k` layers of MPO. Stops when the increase of fidelity between iterations is smaller than `optim_delta`. Default value is `1e-5`.
\n", "Both `k` and `optim_delta` can be set via `Config`. Below we compare `MPSxGate` versus `MPSxMPO` with default parameters and `MPSxMPO` with more resource-hungry parameters. The circuit used is the same as in the previous section."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=16)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxGate, config\n", " )\n", "end = time()\n", "print(\"MPSxGate\")\n", "print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n", "print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=16)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxMPO, config\n", " )\n", "end = time()\n", "print(\"MPSxMPO, default parameters\")\n", "print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n", "print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(k=8, optim_delta=1e-15, chi=16)\n", " fixed_fidelity_mps = simulate(\n", " libhandle, circuit, SimulationAlgorithm.MPSxMPO, config\n", " )\n", "end = time()\n", "print(\"MPSxMPO, custom parameters\")\n", "print(f\"\\tTime taken: {round(end-start,2)} seconds\")\n", "print(f\"\\tLower bound of the fidelity: {round(fixed_fidelity_mps.fidelity, 4)}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["**Note**: `MPSxMPO` also admits truncation policy in terms of `truncation_fidelity` instead of `chi`."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Using the logger"]}, {"cell_type": "markdown", "metadata": {}, "source": ["You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent):
\n", "- `logging.INFO` will print information about progress percent, memory currently occupied by the MPS and current fidelity. Additionally, some high level information of the current stage of the simulation is provided, such as when `MPSxMPO` is applying optimisation sweeps.
\n", "- `logging.DEBUG` provides all of the messages from the loglevel above plus detailed information of the current operation being carried out and the values of important variables.
\n", "**Note**: Due to technical issues with the `logging` module and Jupyter notebooks we need to reload the `logging` module. When working with python scripts and command line, just doing `import logging` is enough."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from importlib import reload # Not needed in Python 2\n", "import logging"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["reload(logging)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["An example of the use of `logging.INFO` is provided below."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " config = Config(truncation_fidelity=0.999, loglevel=logging.INFO)\n", " simulate(libhandle, circuit, SimulationAlgorithm.MPSxMPO, config)"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4"}}, "nbformat": 4, "nbformat_minor": 2} \ No newline at end of file diff --git a/docs/examples/ttn_tutorial.ipynb b/docs/examples/ttn_tutorial.ipynb index d00ef37b..e0f5304a 100644 --- a/docs/examples/ttn_tutorial.ipynb +++ b/docs/examples/ttn_tutorial.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np\n", "from time import time\n", "import matplotlib.pyplot as plt\n", "import networkx as nx\n", "from pytket import Circuit\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.cutensornet.structured_state import (\n", " CuTensorNetHandle,\n", " Config,\n", " SimulationAlgorithm,\n", " simulate,\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Introduction
\n", "This notebook provides examples of the usage of the TTN functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.
\n", "Some good references to learn about Tree Tensor Network state simulation:
\n", "- For an introduction into TTN based simulation of quantum circuits: https://arxiv.org/abs/2206.01000
\n", "- For an introduction on some of the optimisation concerns that are relevant to TTN: https://arxiv.org/abs/2209.03196
\n", "The implementation in pytket-cutensornet differs from previously published literature. I am still experimenting with the algorithm. I intend to write up a document detailing the approach, once I reach a stable version.
\n", "The main advantage of TTN over MPS is that it can be used to efficiently simulate circuits with richer qubit connectivity. This does **not** mean that TTN has an easy time simulating all-to-all connectivity, but it is far more flexible than MPS. TTN's strength is in simulating circuit where certain subsets of qubits interact densely with each other, and there is not that many gates acting on qubits in different subsets."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# How to use
\n", "The interface for TTN matches that of MPS. As such, you should be able to run any code that uses `SimulationAlgorithm.MPSxGate` by replacing it with `SimulationAlgorithm.TTNxGate`. Calling `prepare_circuit_mps` is no longer necessary, since `TTNxGate` can apply gates between non-neighbouring qubits.
\n", "**NOTE**: If you are new to pytket-cutensornet, it is highly recommended to start reading the `mps_tutorial.ipynb` notebook instead. More details about the use of the library are discussed there (for instance, why and when to call `CuTensorNetHandle()`)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def random_graph_circuit(n_qubits: int, edge_prob: float, layers: int) -> Circuit:\n", " \"\"\"Random circuit with qubit connectivity determined by a random graph.\"\"\"\n", " c = Circuit(n_qubits)\n", " for i in range(layers):\n", " # Layer of TK1 gates\n", " for q in range(n_qubits):\n", " c.TK1(np.random.rand(), np.random.rand(), np.random.rand(), q)\n\n", " # Layer of CX gates\n", " graph = nx.erdos_renyi_graph(n_qubits, edge_prob, directed=True)\n", " qubit_pairs = list(graph.edges)\n", " for pair in qubit_pairs:\n", " c.CX(pair[0], pair[1])\n", " return c"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For **exact** simulation, you can call `simulate` directly, providing the default `Config()`:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["simple_circ = random_graph_circuit(n_qubits=10, edge_prob=0.1, layers=1)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_ttn = simulate(libhandle, simple_circ, SimulationAlgorithm.TTNxGate, Config())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Obtain an amplitude from a TTN
\n", "Let's first see how to get the amplitude of the state `|10100>` from the output of the previous circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state = int(\"10100\", 2)\n", "with CuTensorNetHandle() as libhandle:\n", " my_ttn.update_libhandle(libhandle)\n", " amplitude = my_ttn.get_amplitude(state)\n", "print(amplitude)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Since this is a very small circuit, we can use `pytket`'s state vector simulator capabilities to verify that the state is correct by checking the amplitude of each of the computational states."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state_vector = simple_circ.get_statevector()\n", "n_qubits = len(simple_circ.qubits)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["correct_amplitude = [False] * (2**n_qubits)\n", "with CuTensorNetHandle() as libhandle:\n", " my_ttn.update_libhandle(libhandle)\n", " for i in range(2**n_qubits):\n", " correct_amplitude[i] = np.isclose(state_vector[i], my_ttn.get_amplitude(i))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Are all amplitudes correct?\")\n", "print(all(correct_amplitude))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Sampling from a TTN
\n", "Sampling and measurement from a TTN state is not currently supported. This will be added in an upcoming release."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Approximate simulation
\n", "We provide two policies for approximate simulation:
\n", "* Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes.
\n", "* Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate.
\n", "Values for `chi` and `truncation_fidelity` can be set via `Config`. To showcase approximate simulation, let's define a circuit where exact TTN contraction would not be enough."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circuit = random_graph_circuit(n_qubits=30, edge_prob=0.1, layers=1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can simulate it using bounded `chi` as follows:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=64, float_precision=np.float32)\n", " bound_chi_ttn = simulate(libhandle, circuit, SimulationAlgorithm.TTNxGate, config)\n", "end = time()\n", "print(\"Time taken by approximate contraction with bounded chi:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(bound_chi_ttn.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can fix `truncation_fidelity` and let the bond dimension increase as necessary to satisfy it."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(truncation_fidelity=0.99, float_precision=np.float32)\n", " fixed_fidelity_ttn = simulate(\n", " libhandle, circuit, SimulationAlgorithm.TTNxGate, config\n", " )\n", "end = time()\n", "print(\"Time taken by approximate contraction with fixed truncation fidelity:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(fixed_fidelity_ttn.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Contraction algorithms"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We currently offer only one TTN-based simulation algorithm.
\n", "* **TTNxGate**: Apply gates one by one to the TTN, canonicalising the TTN and truncating when necessary."]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Using the logger"]}, {"cell_type": "markdown", "metadata": {}, "source": ["You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent):
\n", "- `logging.INFO` will print information about progress percent, memory currently occupied by the TTN and current fidelity. Additionally, some high level information of the current stage of the simulation is provided.
\n", "- `logging.DEBUG` provides all of the messages from the loglevel above plus detailed information of the current operation being carried out and the values of important variables.
\n", "**Note**: Due to technical issues with the `logging` module and Jupyter notebooks we need to reload the `logging` module. When working with python scripts and command line, just doing `import logging` is enough."]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4"}}, "nbformat": 4, "nbformat_minor": 2} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Tree Tensor Network (TTN) Tutorial"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np\n", "from time import time\n", "import matplotlib.pyplot as plt\n", "import networkx as nx\n", "from pytket import Circuit\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.cutensornet.structured_state import (\n", " CuTensorNetHandle,\n", " Config,\n", " SimulationAlgorithm,\n", " simulate,\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Introduction
\n", "This notebook provides examples of the usage of the TTN functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.
\n", "Some good references to learn about Tree Tensor Network state simulation:
\n", "- For an introduction into TTN based simulation of quantum circuits: https://arxiv.org/abs/2206.01000
\n", "- For an introduction on some of the optimisation concerns that are relevant to TTN: https://arxiv.org/abs/2209.03196
\n", "The implementation in pytket-cutensornet differs from previously published literature. I am still experimenting with the algorithm. I intend to write up a document detailing the approach, once I reach a stable version.
\n", "The main advantage of TTN over MPS is that it can be used to efficiently simulate circuits with richer qubit connectivity. This does **not** mean that TTN has an easy time simulating all-to-all connectivity, but it is far more flexible than MPS. TTN's strength is in simulating circuit where certain subsets of qubits interact densely with each other, and there is not that many gates acting on qubits in different subsets."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## How to use
\n", "The interface for TTN matches that of MPS. As such, you should be able to run any code that uses `SimulationAlgorithm.MPSxGate` by replacing it with `SimulationAlgorithm.TTNxGate`. Calling `prepare_circuit_mps` is no longer necessary, since `TTNxGate` can apply gates between non-neighbouring qubits.
\n", "**NOTE**: If you are new to pytket-cutensornet, it is highly recommended to start reading the `mps_tutorial.ipynb` notebook instead. More details about the use of the library are discussed there (for instance, why and when to call `CuTensorNetHandle()`)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def random_graph_circuit(n_qubits: int, edge_prob: float, layers: int) -> Circuit:\n", " \"\"\"Random circuit with qubit connectivity determined by a random graph.\"\"\"\n", " c = Circuit(n_qubits)\n", " for i in range(layers):\n", " # Layer of TK1 gates\n", " for q in range(n_qubits):\n", " c.TK1(np.random.rand(), np.random.rand(), np.random.rand(), q)\n\n", " # Layer of CX gates\n", " graph = nx.erdos_renyi_graph(n_qubits, edge_prob, directed=True)\n", " qubit_pairs = list(graph.edges)\n", " for pair in qubit_pairs:\n", " c.CX(pair[0], pair[1])\n", " return c"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For **exact** simulation, you can call `simulate` directly, providing the default `Config()`:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["simple_circ = random_graph_circuit(n_qubits=10, edge_prob=0.1, layers=1)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["with CuTensorNetHandle() as libhandle:\n", " my_ttn = simulate(libhandle, simple_circ, SimulationAlgorithm.TTNxGate, Config())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Obtain an amplitude from a TTN
\n", "Let's first see how to get the amplitude of the state `|10100>` from the output of the previous circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state = int(\"10100\", 2)\n", "with CuTensorNetHandle() as libhandle:\n", " my_ttn.update_libhandle(libhandle)\n", " amplitude = my_ttn.get_amplitude(state)\n", "print(amplitude)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Since this is a very small circuit, we can use `pytket`'s state vector simulator capabilities to verify that the state is correct by checking the amplitude of each of the computational states."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state_vector = simple_circ.get_statevector()\n", "n_qubits = len(simple_circ.qubits)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["correct_amplitude = [False] * (2**n_qubits)\n", "with CuTensorNetHandle() as libhandle:\n", " my_ttn.update_libhandle(libhandle)\n", " for i in range(2**n_qubits):\n", " correct_amplitude[i] = np.isclose(state_vector[i], my_ttn.get_amplitude(i))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Are all amplitudes correct?\")\n", "print(all(correct_amplitude))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Sampling from a TTN
\n", "Sampling and measurement from a TTN state is not currently supported. This will be added in an upcoming release."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Approximate simulation
\n", "We provide two policies for approximate simulation:
\n", "* Bound the maximum value of the virtual bond dimension `chi`. If a bond dimension would increase past that point, we *truncate* (i.e. discard) the degrees of freedom that contribute the least to the state description. We can keep track of a lower bound of the error that this truncation causes.
\n", "* Provide a value for acceptable two-qubit gate fidelity `truncation_fidelity`. After each two-qubit gate we truncate the dimension of virtual bonds as much as we can while guaranteeing the target gate fidelity. The more fidelity you require, the longer it will take to simulate. **Note**: this is *not* the final fidelity of the output state, but the fidelity per gate.
\n", "Values for `chi` and `truncation_fidelity` can be set via `Config`. To showcase approximate simulation, let's define a circuit where exact TTN contraction would not be enough."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circuit = random_graph_circuit(n_qubits=30, edge_prob=0.1, layers=1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can simulate it using bounded `chi` as follows:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(chi=64, float_precision=np.float32)\n", " bound_chi_ttn = simulate(libhandle, circuit, SimulationAlgorithm.TTNxGate, config)\n", "end = time()\n", "print(\"Time taken by approximate contraction with bounded chi:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(bound_chi_ttn.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can fix `truncation_fidelity` and let the bond dimension increase as necessary to satisfy it."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["start = time()\n", "with CuTensorNetHandle() as libhandle:\n", " config = Config(truncation_fidelity=0.99, float_precision=np.float32)\n", " fixed_fidelity_ttn = simulate(\n", " libhandle, circuit, SimulationAlgorithm.TTNxGate, config\n", " )\n", "end = time()\n", "print(\"Time taken by approximate contraction with fixed truncation fidelity:\")\n", "print(f\"{round(end-start,2)} seconds\")\n", "print(\"\\nLower bound of the fidelity:\")\n", "print(round(fixed_fidelity_ttn.fidelity, 4))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Contraction algorithms"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We currently offer only one TTN-based simulation algorithm.
\n", "* **TTNxGate**: Apply gates one by one to the TTN, canonicalising the TTN and truncating when necessary."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Using the logger"]}, {"cell_type": "markdown", "metadata": {}, "source": ["You can request a verbose log to be produced during simulation, by assigning the `loglevel` argument when creating a `Config` instance. Currently, two log levels are supported (other than default, which is silent):
\n", "- `logging.INFO` will print information about progress percent, memory currently occupied by the TTN and current fidelity. Additionally, some high level information of the current stage of the simulation is provided.
\n", "- `logging.DEBUG` provides all of the messages from the loglevel above plus detailed information of the current operation being carried out and the values of important variables.
\n", "**Note**: Due to technical issues with the `logging` module and Jupyter notebooks we need to reload the `logging` module. When working with python scripts and command line, just doing `import logging` is enough."]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4"}}, "nbformat": 4, "nbformat_minor": 2} \ No newline at end of file diff --git a/docs/pytket-docs-theming b/docs/pytket-docs-theming index cfbe34c4..45cc4e49 160000 --- a/docs/pytket-docs-theming +++ b/docs/pytket-docs-theming @@ -1 +1 @@ -Subproject commit cfbe34c48f88c56085b8ef65f640d0108b8a9fa6 +Subproject commit 45cc4e49f473905984b99077e8739fe18e69595e From bb7aedd122f3ea2f6394351a28b41d6ed3ef86e7 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Fri, 25 Oct 2024 12:09:00 +0000 Subject: [PATCH 13/13] Changed nix taarget directory to est of notebooks --- nix-support/pytket-cutensornet.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix-support/pytket-cutensornet.nix b/nix-support/pytket-cutensornet.nix index 6ef4191e..d7cfd8b8 100644 --- a/nix-support/pytket-cutensornet.nix +++ b/nix-support/pytket-cutensornet.nix @@ -22,7 +22,7 @@ in { cp ${../README.md} $out/README.md; # required for setup's long description cp ${../pytest.ini} $out/pytest.ini; cp ${../_metadata.py} $out/_metadata.py; - + # on nix versions of scipy and ipython, stubs are missing. # adjust mypy.ini to ignore these errors. ( @@ -40,7 +40,7 @@ EOF ''; }; propagatedBuildInputs = [ super.pytket super.pycuquantum ]; - + doCheck = true; checkInputs = [ super.mypy' ]; checkPhase = '' @@ -83,8 +83,8 @@ EOF WSL_PATH="/usr/lib/wsl/lib"; LD_LIBRARY_PATH="$NIXGL_PATH:$WSL_PATH:$LD_LIBRARY_PATH"; export LD_LIBRARY_PATH; - - example_dir=${../examples}; + + example_dir=${../docs/examples}; set -e; for name in `cat ''${example_dir}/ci-tested-notebooks.txt`; do