{"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}