diff --git a/examples/Quantinuum_variational_experiment_with_batching.ipynb b/examples/Quantinuum_variational_experiment_with_batching.ipynb
index 84c4e53c..a3f16334 100644
--- a/examples/Quantinuum_variational_experiment_with_batching.ipynb
+++ b/examples/Quantinuum_variational_experiment_with_batching.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Quantinuum Variational Experiment on H-Series with tket"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Hybrid Quantum-Classical variational quantum algorithms consist of optimising a trial parametric wavefunction, $| \\psi (\\vec{\\theta}) \\rangle$, to estimate the lowest eigenvalue (or expectation value) of a Hamiltonian, $\\hat{H}$. This could be an Electronic Structure Hamiltonian or a Hamiltonian defining a QUBO (quadratic unconstrained binary optimisation) or MAXCUT problem. The optimal parameters of the wavefunction, $(\\vec{\\theta})$ are an estimation of the lowest eigenvector of the Hamiltonian."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Further details can be found in the following articles:
\n", "* [A variational eigenvalue solver on a quantum processor](https://arxiv.org/abs/1304.3061)
\n", "* [Towards Practical Quantum Variational Algorithms](https://arxiv.org/abs/1507.08969)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For the problem today, we will evaluate the ground-state energy (lowest eigenvalue) of a di-Hydrodgen molecule. A Hamiltonian is defined over two-qubits ([PhysRevX.6.031007](https://journals.aps.org/prx/abstract/10.1103/PhysRevX.6.031007)). A state-preparation (or Ansatz) circuit, a sequence of single-qubit and two-qubit gates, is used to generate a trial wavefunction. The wavefunction parameters are rotations on the circuit."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The hardware-efficient state-preparation method is used for today's problem ([nature23879](https://www.nature.com/articles/nature23879)). The variational experiment optimises the parameters on this circuit, over multiple iterations, in order to minimise the expectation value of the Hamiltonian, $\\langle \\psi (\\vec{\\theta}) | \\hat{H} | \\psi (\\vec{\\theta}) \\rangle$."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Workflow and Tools"]}, {"cell_type": "markdown", "metadata": {}, "source": ["`pytket` is used to synthesise a state-preparation circuit, prepare measurement circuits with `pytket-quantinuum` being used to submit (retrieve) jobs in a batch to (from) the H-Series service. The variational experiment requires the following as inputs:
\n", "* a symbolic state-preparation circuit.
\n", "* an Hamiltonian defining the problem to be solved."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The state-preparation, described above, consists of fixed-angle single-qubit and two-qubit gates in addition to variable-angle single-qubit gates. In pytket, variable-angle single-qubit gates can have two types of parameters:
\n", "* numerical parameters (`float`);
\n", "* symbolic parameters (`sympy.symbol`)."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Numerical parameters are native python `float`s. Symbolic parameters require the use of the symbolic library, `sympy`, which is also a dependency of `pytket`. Throughout the variational experiment, symbolic parameters on the state-preparation circuit are replaced with additional numerical parameters."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The variational procedure consists of $n$ iterations until a specific criterion is satisfied. A batch session will run over these $n$ iterations.
\n", "Inactivity for over 1 minutes will lead to the batch session ending, given how the batch feature works for H-Series devices."]}, {"cell_type": "markdown", "metadata": {}, "source": ["During the variational experiment, each iteration updates the numerical values in the parameter set, as described above. Subsequently, these are substituted into a new copy of the original symbolic state-preparation circuit. A set of sub-circuits, each containing measurement information defined by the input Hamiltonian, are appended to the numerical state-preparation circuit, leading to a set of measurement circuits. Finally, these circuits are submitted to H-Series."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Specifically, each iteration consists of:
\n", "* classical pre-processing to define measurement circuits;
\n", "* batch submission to H-Series;
\n", "* retrieval of measurement results;
\n", "* classical post-processing to evaluate the cost function;"]}, {"cell_type": "markdown", "metadata": {}, "source": ["determining whether to stop or continue the variational procedure."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `SciPy` minimiser is used to control the optimisation of the cost function. The minimised value of the cost function and the optimal parameters can be retrieved at the end of the variational experiment."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The observable is a sum of Pauli-strings (tensor product over `m` qubits of Pauli-$\\hat{X}$, Pauli-$\\hat{Y}$, Pauli-$\\hat{Z}$ & Pauli-$\\hat{I}$) multiplied by numerical coefficients.
\n", "* a set of initial numerical parameters to substitute into the symbolic state-preparation circuit. For example, this can be a set of random numerical floating-point numbers.
\n", "* `pytket.backends.Backend` object to interface with the H-Series quantum computing service.
\n", "* Number of shots to simulate each circuit with to generate a distribution of measurements.
\n", "* Maximum batch cost to limit the credit cost of the variational experiment."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## QuantinuumBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QuantinuumBackend` is used to submit and retreive all circuits required for the variational experiment. This backend is included in the `pytket-quantinuum` extension. With this backend, the end-user can access H-series hardware, emulators, syntax checkers. The Quantinuum user portal lists all devices and emulators the end-user can access."]}, {"cell_type": "markdown", "metadata": {}, "source": ["In the code cell below, the instance of QuantinuumBackend uses the H-Series emulator, `H1-1E`. The H1 syntax checker's target is `H1-1SC` and the quantum device's target is `H1-1`. The H-Series emulators are a useful utility to test and cost the performance of an algorithm before any hardware session."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QuantinuumBackend` instance requires the user to be authenticated before any jobs can be submitted. The `login` method will allow authentication."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.quantinuum import QuantinuumBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["quantinuum_backend = QuantinuumBackend(device_name=\"H1-1E\")\n", "quantinuum_backend.login()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Contents"]}, {"cell_type": "markdown", "metadata": {}, "source": ["1. [Synthesise Symbolic State-Preparation Circuit](#state-prep)
\n", "2. [Hamiltonian Definition & Analysis](#hamiltonian)
\n", "3. [Computing Expectation Values](#expval)
\n", "4. [Variational Procedure with Batches](#variational)
\n", "
\n", "## 1. Synthesise Symbolic State-Preparation Circuit
\n", "We first prepare a two-qubit circuit consisting of fixed-angle two-qubit `CX` gates (`pytket.circuit.OpType.CX`) and variable-angle single-qubit `Ry` gates (`pytket.circuit.OpType.Rz`). This state-preparation technique is known as the Hardware-Efficient Ansatz (HEA) ([nature23879](https://www.nature.com/articles/nature23879)), instead of the usual chemistry state-preparation method, Unitary Coupled Cluster (UCC) ([arxiv.1701.02691](https://arxiv.org/abs/1701.02691)).
\n", "
\n", "The hardware-efficient state-preparation method requires alternating layers of fixed-angle two-qubit gates and variable-angle single-qubit gates. Ultimately, this leads to fewer two-qubit gates, but requires greater variational parameters, compared to UCC. The optimal parameters for HEA are governed by the noise profile of the device. The HEA circuit used in this example consists of one-layer (4-parameters) and only uses `Ry` gates."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import Circuit\n", "from sympy import Symbol"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["symbols = [Symbol(f\"p{i}\") for i in range(4)]\n", "symbolic_circuit = Circuit(2)\n", "symbolic_circuit.X(0)\n", "symbolic_circuit.Ry(symbols[0], 0).Ry(symbols[1], 1)\n", "symbolic_circuit.CX(0, 1)\n", "symbolic_circuit.Ry(symbols[2], 0).Ry(symbols[3], 0)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The symbolic state-preparation circuit can be visualised using the `pytket.circuit.display` submodule."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(symbolic_circuit)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## 2. Hamiltonian Definition and Analysis "]}, {"cell_type": "markdown", "metadata": {}, "source": ["A problem hamiltonian is defined using the [`pytket.utils.operator.QubitPauliOperator`](https://tket.quantinuum.com/api-docs/utils.html#pytket.utils.QubitPauliOperator) class. Each `QubitPauliOperator` consists of complex coefficients and tensor products of Pauli-operations. The tensor products are referred to as Pauli-strings. This particular Hamiltonian consists of 5 terms operating on qubits `q[0]` and `q[1]`. The problem Hamiltonian, $\\hat{H}$, is defined as:"]}, {"cell_type": "markdown", "metadata": {}, "source": ["\\begin{align} \\hat{H} &= g_0 \\hat{I}_{q[0]} \\otimes \\hat{I}_{q[1]} + g_1 \\hat{Z}_{q[0]} \\otimes \\hat{I}_{q[1]} + g_2 \\hat{I}_{q[0]} \\otimes \\hat{Z}_{q[1]} \\\\ &+ g_3 \\hat{Z}_{q[0]} \\otimes \\hat{Z}_{q[1]} + g_4 \\hat{X}_{q[0]} \\otimes \\hat{X}_{q[1]} + g_5 \\hat{Y}_{q[0]} \\otimes \\hat{Y}_{q[1]} \\\\ \\end{align}"]}, {"cell_type": "markdown", "metadata": {}, "source": ["where $g_0, g_1, g_2$, $g_3$, $g_4$ and $g_5$ are real numercial coefficients."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QubitPauliOperator` is a dictionary mapping [`pytket.pauli.QubitPauliString`](https://tket.quantinuum.com/api-docs/pauli.html#pytket.pauli.QubitPauliString) to a complex coefficient. These coefficients are sympified (converted from python `complex` types to sympy `complex` types)."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QubitPauliString` is a map from `pytket.circuit.Qubit` to `pytket.pauli.Pauli`."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The coefficients in the Hamiltonian are obtained from [PhysRevX.6.031007](https://journals.aps.org/prx/abstract/10.1103/PhysRevX.6.031007)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils.operators import QubitPauliOperator\n", "from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.circuit import Qubit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["coeffs = [-0.4804, 0.3435, -0.4347, 0.5716, 0.0910, 0.0910]\n", "term0 = {\n", " QubitPauliString(\n", " {\n", " Qubit(0): Pauli.I,\n", " Qubit(1): Pauli.I,\n", " }\n", " ): coeffs[0]\n", "}\n", "term1 = {QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.I}): coeffs[1]}\n", "term2 = {QubitPauliString({Qubit(0): Pauli.I, Qubit(1): Pauli.Z}): coeffs[2]}\n", "term3 = {QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Z}): coeffs[3]}\n", "term4 = {QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.X}): coeffs[4]}\n", "term5 = {QubitPauliString({Qubit(0): Pauli.Y, Qubit(1): Pauli.Y}): coeffs[5]}\n", "term_sum = {}\n", "term_sum.update(term0)\n", "term_sum.update(term1)\n", "term_sum.update(term2)\n", "term_sum.update(term3)\n", "term_sum.update(term4)\n", "term_sum.update(term5)\n", "hamiltonian = QubitPauliOperator(term_sum)\n", "print(hamiltonian)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To measure $\\hat{H}$ on hardware, naively 5 measurement circuits are required. The Identity term does not need to measured, since its expectation value always equals 1.
\n", "With pytket, $\\hat{H}$ only requires simulating 2 measurement circuit, thanks to measurement reduction. The four terms $\\hat{X}_{q[0]} \\otimes \\hat{X}_{q[1]}$, $\\hat{Y}_{q[0]} \\otimes \\hat{Y}_{q[1]}$, $\\hat{Z}_{q[0]} \\otimes \\hat{Z}_{q[1]}$, $\\hat{Z}_{q[0]} \\otimes \\hat{Z}_{q[1]}$ and $\\hat{I}_{q[0]} \\otimes \\hat{Z}_{q[1]}$, form a commuting set and can be measured with two circuits instead of three. This partitioning can be performed automatically using the [`measurement_reduction`](https://tket.quantinuum.com/api-docs/partition.html#pytket.partition.measurement_reduction) function available in [`pytket.partition`](https://tket.quantinuum.com/api-docs/partition.html#module-pytket.partition) submodule."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The measurement operations for the two commuting set,
\n", "* $\\left\\{ \\hat{X}_{q[0]} \\otimes \\hat{X}_{q[1]}, \\hat{Y}_{q[0]} \\otimes \\hat{Y}_{q[1]} \\right\\}$,
\n", "* $\\left\\{ \\hat{Z}_{q[0]} \\otimes \\hat{Z}_{q[1]}, \\hat{Z}_{q[0]} \\otimes \\hat{I}_{q[1]}, \\hat{I}_{q[0]} \\otimes \\hat{Z}_{q[1]}\\right\\}$"]}, {"cell_type": "markdown", "metadata": {}, "source": ["include additional two-qubit gate resources."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.partition import (\n", " measurement_reduction,\n", " PauliPartitionStrat,\n", ")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["strat = PauliPartitionStrat.CommutingSets\n", "pauli_strings = [term for term in hamiltonian._dict.keys()]\n", "measurement_setup = measurement_reduction(pauli_strings, strat)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["A measurement subcircuit contains the necessary operations to measure the terms in a commuting set. The subcircuit is appended to the numerical state-preparation circuit. Combining the numerical state-preparation circuit and the measurement subcircuits results in a set of measurement circuits required to solve the problem. The [`MeasurementSetup`](https://tket.quantinuum.com/api-docs/partition.html#pytket.partition.MeasurementSetup) instance contains all the necessary sub-circuits to measure $\\hat{H}$. The next code cell lists and visualises all measurement subcircuits."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for measurement_subcircuit in measurement_setup.measurement_circs:\n", " render_circuit_jupyter(measurement_subcircuit)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Once the quantum computation has been completed, the measurement results can be mapped back to the Pauli-operations and coefficients in the Hamiltonian. This enables calculation of the expectation value for the Hamiltonian. The results attribute in the [`pytket.partition.MeasurementSetup`](https://tket.quantinuum.com/api-docs/partition.html#pytket.partition.MeasurementSetup) lists:"]}, {"cell_type": "markdown", "metadata": {}, "source": ["* all the Pauli-strings that have been measured;
\n", "* information to process the quantum computed measurement result in order"]}, {"cell_type": "markdown", "metadata": {}, "source": ["to estimate the expectation value of each Pauli-strings."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for i, (term, bitmap_list) in enumerate(measurement_setup.results.items()):\n", " print(f\"{term}\\t{bitmap_list}\\n\")\n", "# ## 3. Computing Expectation Values "]}, {"cell_type": "markdown", "metadata": {}, "source": ["Once the Hamiltonian has been partitioned into commuting sets, measurement circuits need to be constructed. These measurement circuits are submitted to hardware or emulators for simulation. Once the simulation is complete, a result is available to request, and can be retrieved using `pytket`. These results are the outcomes
\n", "of the measurement circuit simulation. Each result is a distribution of outcomes, specifically, the probability of observing specific bitstring. This distribution is post-processed to compute the expectation value of the Hamiltonian, a necessity to evaluate the cost function in a Hybrid Quantum-Classical variational procedure."]}, {"cell_type": "markdown", "metadata": {}, "source": ["### 3.1 Computing Expectation Values for Pauli-Strings"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The Hamiltonian we are interested in consists of Pauli-strings. The expectation value of the Pauli-string is in the interval $[-1, 1]$."]}, {"cell_type": "markdown", "metadata": {}, "source": ["In the code cell below, a function is provided that calculates the expectation value of Pauli-string from a measured distribution. The [`MeasurementBitmap`](https://tket.quantinuum.com/api-docs/partition.html#pytket.partition.MeasurementBitMap) is used to extract the necessary data from the measured distribution. The resulting distribution can be summed over to estimate the expectation value of one Pauli-string."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from typing import Dict, Tuple\n", "from pytket.partition import MeasurementBitMap"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def compute_expectation_paulistring(\n", " distribution: Dict[Tuple[int, ...], float], bitmap: MeasurementBitMap\n", ") -> float:\n", " value = 0\n", " for bitstring, probability in distribution.items():\n", " value += probability * (sum(bitstring[i] for i in bitmap.bits) % 2)\n", " return ((-1) ** bitmap.invert) * (-2 * value + 1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In the example below, the function `compute_expectation_paulistring` is called to calculate the expectation for the $\\hat{Z} \\otimes \\hat{Z}$. First the `QubitPauliString` is initialised, and that is used to extract the relevant data from the MeasurementSetup object defined in section 2. This data is used for postprocessing."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.circuit import Qubit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["distribution = {(0, 0): 0.45, (1, 1): 0.3, (0, 1): 0.1, (1, 0): 0.15}\n", "zz = QubitPauliString([Qubit(0), Qubit(1)], [Pauli.Z, Pauli.Z])\n", "bitmap_list = measurement_setup.results.get(zz)\n", "for bitmap in bitmap_list:\n", " ev = compute_expectation_paulistring(distribution, bitmap)\n", " print(ev)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### 3.2 Computing Expectation Values for sums of Pauli-strings multiplied by coefficients
\n", "In this step, we will submit circuits to the H-Series emulator (`H1-1E`). This circuit will produce a result. The result can be retrieved with the `ResultHandle` object. First, the symbolic circuit is converted into a numerical circuit. The symbols in the circuit are substituted for numerical parameters."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["symbol_map = {sym: 0.1 for sym in symbolic_circuit.free_symbols()}\n", "numerical_circuit = symbolic_circuit.copy()\n", "numerical_circuit.symbol_substitution(symbol_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### 3.3 Using QuantinuumBackend
\n", "The Quantinuum backend was initialised at the start of the notebook to use the H1-1E emulator. This backend will now be used to calculate the expectation value.
\n", "The measurement operations from the `MeasurementSetup` object are appended to the numerical circuit. Once this step is complete, the circuit is ready for submission if tket optimisation in the H-Series stack is selected."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circuit_list = []\n", "for mc in measurement_setup.measurement_circs:\n", " c = numerical_circuit.copy()\n", " c.append(mc)\n", " circuit_list += [c]\n", "compiled_circuit_list = quantinuum_backend.get_compiled_circuits(\n", " circuit_list, optimisation_level=2\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Before submitting to the emulator, the total cost of running the set of circuits can be checked beforehand."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cost_list = []"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots = 500\n", "for comp_circ in compiled_circuit_list:\n", " cost = quantinuum_backend.cost(comp_circ, n_shots=n_shots, syntax_checker=\"H1-1SC\")\n", " cost_list.append(cost)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Cost of experiment in HQCs:\", sum(cost_list))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now we run the circuits."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["handles = quantinuum_backend.process_circuits(\n", " compiled_circuit_list, n_shots=10, options={\"tket-opt-level\": None}\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The status of the jobs can be checked with `ciruit_status` method. This method requires the `ResultHandle` to be passed as input. In this example, the job has completed and the results are reported as being ready to request."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for h in handles:\n", " circuit_status = quantinuum_backend.circuit_status(h)\n", " print(circuit_status)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The expectation value of the operator can be evaluated with the function `compute_expectation_value` in the next code cell. This function requires a list of `BackendResult` objects, a `MeasurementSetup` instance, and the `QubitPauliOperator` instance for the expectation value computation. It is assumed the `MeasurementSetup` instance contains the measurement info of all the Pauli-strings in the `QubitPauliOperator` instance. Otherwise the `compute_expectation_value` function will return zero."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from typing import List\n", "from pytket.utils.operators import QubitPauliOperator\n", "from pytket.partition import MeasurementSetup\n", "from pytket.backends.backendresult import BackendResult"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def compute_expectation_value(\n", " results: List[BackendResult],\n", " measurement_setup: MeasurementSetup,\n", " operator: QubitPauliOperator,\n", ") -> float:\n", " energy = 0\n", " for pauli_string, bitmaps in measurement_setup.results.items():\n", " string_coeff = operator.get(pauli_string, 0.0)\n", " if string_coeff > 0:\n", " for bm in bitmaps:\n", " index = bm.circ_index\n", " distribution = results[index].get_distribution()\n", " value = compute_expectation_paulistring(distribution, bm)\n", " energy += complex(value * string_coeff).real\n", " return energy"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The results of the previously submitted circuits can be retrieved with the `get_results` method on `QuantinuumBackend`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["results = quantinuum_backend.get_results(handles)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Finally, the expectation value, $\\langle{\\psi (\\vec{\\theta}_r)} | \\hat{H} | { \\psi (\\vec{\\theta}_r)} \\rangle$, of the `QubitPauliOperator` instance, $\\hat{H}$, is calculated with respect to $| { \\psi (\\vec{\\theta}_r)} \\rangle$. The state, $| \\psi \\rangle$, is prepared with the state-preparation circuit, and $\\vec{\\theta}_r$ is a random parameter set."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["expectation_value = compute_expectation_value(results, measurement_setup, hamiltonian)\n", "print(f\"Expectation Value: {expectation_value}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## 4. Variational Procedure "]}, {"cell_type": "markdown", "metadata": {}, "source": ["A hybrid quantum-classical variational procedure consists of multiple iterations, controlled by a classical parameter optimiser. The parameters are gate-angles on quantum circuits submitted to H-Series for simulation. In [step 3](#expval), a procedure is showcased to calculate the expectation value of a Hamiltonian with respect to a quantum state. It is shown how to use the measurement reduction and Pauli-string partitioning facility in `pytket` to reduce measurement resources for the experiments. For the variational procedure demonstrated below, the cost function calculates the expectation of an input Hamiltonian. The aim is to find the optimal parameters that minimise this expectation value."]}, {"cell_type": "markdown", "metadata": {}, "source": ["### 4.1. Objective function"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `Objective` class defined in the code cell performs the following utilities:
\n", "* Measurement Reduction;
\n", "* Creation of a Batch session to use across the variational experiment;
\n", "* Submission and retrieval of quantum circuits using `QuantinuumBackend`;
\n", "* Expectation Value evaluation."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `Objective` class requires the following inputs:
\n", "* Input symbolic state-preparation circuit;
\n", "* A `QubitPauliOperator` instance of the Hamiltonian characterising the use-case of interest;
\n", "* The backend to use. `QuantinuumBackend` is used to access H-Series service. The backend needs to be instantiated and the user needs to login within previous code cell.
\n", "* Number of shots to perform per circuit. H-Series devices have an upper limit of 10000 shots per job.
\n", "* Maximum consumable HQC credit before the batch is terminated.
\n", "* Number of iterations before the variational experiment is terminated."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `Objective` class instance can be passed as a callable to `scipy.optimize.minimize`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from typing import Callable\n", "from numpy import ndarray\n", "from numpy.random import random_sample\n", "from pytket.extensions.quantinuum import QuantinuumBackend\n", "from pytket.partition import PauliPartitionStrat\n", "from pytket.backends.resulthandle import ResultHandle"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["class Objective:\n", " def __init__(\n", " self,\n", " symbolic_circuit: Circuit,\n", " problem_hamiltonian: QubitPauliOperator,\n", " quantinuum_backend: QuantinuumBackend,\n", " n_shots_per_circuit: int,\n", " max_batch_cost: float = 300,\n", " n_iterations: int = 10,\n", " ) -> None:\n", " r\"\"\"Returns the objective function needed for a variational\n", " procedure on H-Series.\n", " Args:\n", " symbolic_circuit\n", " (pytket.circuit.Circuit): State-preparation\n", " circuit with symbolic parameters\n", " problem_hamiltonian (pytket.utils.operators.QubitPauliOperator):\n", " QubitPauliOperator instance defining the Hamiltonian of the\n", " problem.\n", " quantinuum_backend (pytket.extensions.quantinuum.QuantinuumBackend): Backend\n", " instance to use for the simulation. This will be\n", " QuantinuumBackend from the pytket.extensions.quantinuum\n", " package to run experiments on H-Series devices and emulators.\n", " n_shots_per_circuit (int): Number of shots per circuit.\n", " max_batch_cost (float): Maximum cost of all jobs in batch. If\n", " exceeded the batch will terminate.\n", " n_iterations (int): Total number of iterations before ending\n", " the batch session.\n", " Returns:\n", " Callable[[ndarray], float]\n", " \"\"\"\n", " terms = [term for term in problem_hamiltonian._dict.keys()]\n", " self._symbolic_circuit: Circuit = symbolic_circuit\n", " self._symbols: List[Symbol] = symbolic_circuit.free_symbols()\n", " self._hamiltonian: QubitPauliOperator = problem_hamiltonian\n", " self._backend: QuantinuumBackend = quantinuum_backend\n", " self._nshots: int = n_shots_per_circuit\n", " self._max_batch_cost: float = max_batch_cost\n", " self._measurement_setup: MeasurementSetup = measurement_reduction(\n", " terms, strat=PauliPartitionStrat.CommutingSets\n", " )\n", " self._iteration_number: int = 0\n", " self._niters: int = n_iterations\n", " def __call__(self, parameter: ndarray) -> float:\n", " value = self._objective_function(parameter, self._iteration_number)\n", " self._iteration_number += 1\n", " if self._iteration_number >= self._niters:\n", " self._iteration_number = 0\n", " return value\n", " def circuit_cost(self, syntax_checker: str = \"H1-1SC\") -> float:\n", " n = len(self._symbolic_circuit.free_symbols())\n", " random_parameters = random_sample(n)\n", " return sum(\n", " [\n", " self._backend.cost(c, self._nshots, syntax_checker=syntax_checker)\n", " for c in self._build_circuits(random_parameters)\n", " ]\n", " )\n", " def _objective_function(\n", " self,\n", " parameters: ndarray,\n", " iteration_number: int,\n", " ) -> float:\n", " r\"\"\"Substitutes input parameters into the\n", " symbolic state-preparation circuit, and\n", " calculates the expectation value.\n", " Args:\n", " parameters (ndarray): A list of numpy.ndarray\n", " Returns:\n", " float\n", " \"\"\"\n", " assert len(parameters) == len(self._symbolic_circuit.free_symbols())\n", " circuit_list = self._build_circuits(parameters)\n", " if iteration_number == 0:\n", " self._startjob = quantinuum_backend.start_batch(\n", " self._max_batch_cost, circuit_list[0], self._nshots\n", " )\n", " handles = [self._startjob] + self._submit_batch(circuit_list[1:])\n", " else:\n", " handles = self._submit_batch(circuit_list)\n", " results = self._backend.get_results(handles)\n", " expval = compute_expectation_value(\n", " results, self._measurement_setup, self._hamiltonian\n", " )\n", " return expval\n", " def _build_circuits(self, parameters: ndarray) -> List[Circuit]:\n", " circuit = self._symbolic_circuit.copy()\n", " symbol_dict = {s: p for s, p in zip(self._symbols, parameters)}\n", " circuit.symbol_substitution(symbol_dict)\n", " circuit_list = []\n", " for mc in self._measurement_setup.measurement_circs:\n", " c = circuit.copy()\n", " c.append(mc)\n", " circuit_list.append(c)\n", " cc_list = self._backend.get_compiled_circuits(\n", " circuit_list, optimisation_level=2\n", " )\n", " return cc_list\n", " def _submit_batch(\n", " self,\n", " circuits: List[Circuit],\n", " ) -> List[ResultHandle]:\n", " r\"\"\"Submit a list of circuits with N shots each\n", " to the H-Series batch.\n", " Args:\n", " circuits (List[Circuit]): A list of circuits\n", " to submit to the batch on H-Series.\n", " first job (ResultHandle): The result handle for the\n", " first job submitted in the batch.\n", " Returns:\n", " List[ResultHandle]\n", " \"\"\"\n", " return [\n", " self._backend.add_to_batch(\n", " self._startjob, c, self._nshots, options={\"tket-opt-level\": None}\n", " )\n", " for c in circuits\n", " ]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `Objective` class is initialised with the essential data needed to perform the variational experiment. The object contains all the necessary information to compute the value of the objective function."]}, {"cell_type": "markdown", "metadata": {}, "source": ["A convenience method `circuit_cost` can be used to estimate the total number of HQCs required to estimate the objective function. The variational loop will be multiples of this value (number of function calls across the variational procedure multiplied by the HQC cost of evaluating the objective function)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots_per_circuit = 500\n", "n_iterations = 10\n", "max_batch_cost = 500\n", "objective = Objective(\n", " symbolic_circuit,\n", " hamiltonian,\n", " quantinuum_backend,\n", " n_shots_per_circuit,\n", " max_batch_cost=max_batch_cost,\n", " n_iterations=n_iterations,\n", ")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["objective.circuit_cost(\"H1-1SC\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### 4.2. Execute the Objective Function
\n", "The SciPy minimiser is used to optimise the value of the objective function. Initial parameters are pseudo-random. Passing the `Objective` instance into `scipy.optimize.minimize` will start the variational experiment."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The first iteration creates a batch session, and all subsequent circuit submission are added to this batch. If additional circuits are not submitted within 1 minute, the batch session will terminate."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Remember that the status of the batch can be checked at any time on the Quantinuum User Portal."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from scipy.optimize import minimize\n", "from numpy.random import random_sample"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["method = \"COBYLA\"\n", "initial_parameters = random_sample(len(symbolic_circuit.free_symbols()))\n", "result = minimize(\n", " objective,\n", " initial_parameters,\n", " method=method,\n", " options={\"disp\": True, \"maxiter\": objective._niters},\n", " tol=1e-2,\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The minimal value of the objective function can be retrieved with the `fun` attribute."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["result.fun"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The optimal parameters can be retreived with the `x` attribute."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["result.x"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The Symbols can be mapped to the optimal parameter by iterating through both lists:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["optimal_parameters = {s.name: p for s, p in zip(objective._symbols, result.x)}\n", "print(optimal_parameters)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["These symbols can be saved to an output file for further use if necessary using json. See the example below."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import json"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["json_io = open(\"parameters.json\", \"w\")\n", "json.dump(optimal_parameters, json_io)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["
© 2024 by Quantinuum. All Rights Reserved.
"]}], "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": ["\n", "
"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Variational Experiment on Quantinuum H-Series with Batching"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Hybrid Quantum-Classical variational quantum algorithms consist of optimising a trial parametric wavefunction, $| \\psi (\\vec{\\theta}) \\rangle$, to estimate the lowest eigenvalue (or expectation value) of a Hamiltonian, $\\hat{H}$. This could be an Electronic Structure Hamiltonian or a Hamiltonian defining a QUBO (quadratic unconstrained binary optimisation) or MAXCUT problem. The optimal parameters of the wavefunction, $(\\vec{\\theta})$ are an estimation of the lowest eigenvector of the Hamiltonian."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Further details can be found in the following articles:
\n", "* [A variational eigenvalue solver on a quantum processor](https://arxiv.org/abs/1304.3061)
\n", "* [Towards Practical Quantum Variational Algorithms](https://arxiv.org/abs/1507.08969)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For the problem today, we will evaluate the ground-state energy (lowest eigenvalue) of a di-Hydrodgen molecule. A Hamiltonian is defined over two-qubits ([PhysRevX.6.031007](https://journals.aps.org/prx/abstract/10.1103/PhysRevX.6.031007)). A state-preparation (or Ansatz) circuit, a sequence of single-qubit and two-qubit gates, is used to generate a trial wavefunction. The wavefunction parameters are rotations on the circuit."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The hardware-efficient state-preparation method is used for today's problem ([nature23879](https://www.nature.com/articles/nature23879)). The variational experiment optimises the parameters on this circuit, over multiple iterations, in order to minimise the expectation value of the Hamiltonian, $\\langle \\psi (\\vec{\\theta}) | \\hat{H} | \\psi (\\vec{\\theta}) \\rangle$."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Workflow and Tools"]}, {"cell_type": "markdown", "metadata": {}, "source": ["`pytket` is used to synthesise a state-preparation circuit, prepare measurement circuits with `pytket-quantinuum` being used to submit (retrieve) jobs in a batch to (from) the H-Series service. The variational experiment requires the following as inputs:
\n", "* a symbolic state-preparation circuit.
\n", "* an Hamiltonian defining the problem to be solved."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The state-preparation, described above, consists of fixed-angle single-qubit and two-qubit gates in addition to variable-angle single-qubit gates. In pytket, variable-angle single-qubit gates can have two types of parameters:
\n", "* numerical parameters (`float`);
\n", "* symbolic parameters (`sympy.symbol`)."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Numerical parameters are native python `float`s. Symbolic parameters require the use of the symbolic library, `sympy`, which is also a dependency of `pytket`. Throughout the variational experiment, symbolic parameters on the state-preparation circuit are replaced with additional numerical parameters."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The variational procedure consists of $n$ iterations until a specific criterion is satisfied. A batch session will run over these $n$ iterations.
\n", "Inactivity for over 1 minutes will lead to the batch session ending, given how the batch feature works for H-Series devices."]}, {"cell_type": "markdown", "metadata": {}, "source": ["During the variational experiment, each iteration updates the numerical values in the parameter set, as described above. Subsequently, these are substituted into a new copy of the original symbolic state-preparation circuit. A set of sub-circuits, each containing measurement information defined by the input Hamiltonian, are appended to the numerical state-preparation circuit, leading to a set of measurement circuits. Finally, these circuits are submitted to H-Series."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Specifically, each iteration consists of:
\n", "* classical pre-processing to define measurement circuits;
\n", "* batch submission to H-Series;
\n", "* retrieval of measurement results;
\n", "* classical post-processing to evaluate the cost function;"]}, {"cell_type": "markdown", "metadata": {}, "source": ["determining whether to stop or continue the variational procedure."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `SciPy` minimiser is used to control the optimisation of the cost function. The minimised value of the cost function and the optimal parameters can be retrieved at the end of the variational experiment."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The observable is a sum of Pauli-strings (tensor product over `m` qubits of Pauli-$\\hat{X}$, Pauli-$\\hat{Y}$, Pauli-$\\hat{Z}$ & Pauli-$\\hat{I}$) multiplied by numerical coefficients.
\n", "* a set of initial numerical parameters to substitute into the symbolic state-preparation circuit. For example, this can be a set of random numerical floating-point numbers.
\n", "* `pytket.backends.Backend` object to interface with the H-Series quantum computing service.
\n", "* Number of shots to simulate each circuit with to generate a distribution of measurements.
\n", "* Maximum batch cost to limit the credit cost of the variational experiment."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## QuantinuumBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QuantinuumBackend` is used to submit and retreive all circuits required for the variational experiment. This backend is included in the `pytket-quantinuum` extension. With this backend, the end-user can access H-series hardware, emulators, syntax checkers. The Quantinuum user portal lists all devices and emulators the end-user can access."]}, {"cell_type": "markdown", "metadata": {}, "source": ["In the code cell below, the instance of QuantinuumBackend uses the H-Series emulator, `H1-1E`. The H1 syntax checker's target is `H1-1SC` and the quantum device's target is `H1-1`. The H-Series emulators are a useful utility to test and cost the performance of an algorithm before any hardware session."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QuantinuumBackend` instance requires the user to be authenticated before any jobs can be submitted. The `login` method will allow authentication."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.quantinuum import QuantinuumBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["quantinuum_backend = QuantinuumBackend(device_name=\"H1-1E\")\n", "quantinuum_backend.login()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Contents"]}, {"cell_type": "markdown", "metadata": {}, "source": ["1. [Synthesise Symbolic State-Preparation Circuit](#state-prep)
\n", "2. [Hamiltonian Definition & Analysis](#hamiltonian)
\n", "3. [Computing Expectation Values](#expval)
\n", "4. [Variational Procedure with Batches](#variational)
\n", "
\n", "## 1. Synthesise Symbolic State-Preparation Circuit
\n", "The code-cell below synthesises a two-qubit circuit consisting of arbitrary-angle two-qubit `ZZPhase` gates (`pytket.circuit.OpType.ZZPhase`) and fixed-angle single-qubit `X` gate (`pytket.circuit.OpType.X`). This state-preparation technique is inspired by the Hardware-Efficient Ansatz (HEA) ([nature23879](https://www.nature.com/articles/nature23879)), instead of the usual chemistry state-preparation method, Unitary Coupled Cluster (UCC) ([arxiv.1701.02691](https://arxiv.org/abs/1701.02691)).
\n", "
\n", "The hardware-efficient state-preparation method requires alternating layers of two-qubit gates and single-qubit gates. Ultimately, this leads to fewer two-qubit gates, but requires a greater number of variational parameters, compared to UCC. The optimal parameters for HEA are governed by the noise profile of the device. The HEA circuit used in this example consists of one-layer `ZZPhase` gates."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import Circuit\n", "from sympy import Symbol"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["symbols = [Symbol(f\"p{0}\")]\n", "symbolic_circuit = Circuit(2)\n", "symbolic_circuit.X(0)\n", "symbolic_circuit.ZZPhase(symbols[0], 0, 1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The symbolic state-preparation circuit can be visualised using the `pytket.circuit.display` submodule."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(symbolic_circuit)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## 2. Hamiltonian Definition and Analysis "]}, {"cell_type": "markdown", "metadata": {}, "source": ["A problem hamiltonian is defined using the [`pytket.utils.operator.QubitPauliOperator`](https://tket.quantinuum.com/api-docs/utils.html#pytket.utils.QubitPauliOperator) class. Each `QubitPauliOperator` consists of complex coefficients and tensor products of Pauli-operations. The tensor products are referred to as Pauli-strings. This particular Hamiltonian consists of 5 terms operating on qubits `q[0]` and `q[1]`. The problem Hamiltonian, $\\hat{H}$, is defined as:"]}, {"cell_type": "markdown", "metadata": {}, "source": ["\\begin{align} \\hat{H} &= g_0 \\hat{I}_{q[0]} \\otimes \\hat{I}_{q[1]} + g_1 \\hat{Z}_{q[0]} \\otimes \\hat{I}_{q[1]} + g_2 \\hat{I}_{q[0]} \\otimes \\hat{Z}_{q[1]} \\\\ &+ g_3 \\hat{Z}_{q[0]} \\otimes \\hat{Z}_{q[1]} + g_4 \\hat{X}_{q[0]} \\otimes \\hat{X}_{q[1]} + g_5 \\hat{Y}_{q[0]} \\otimes \\hat{Y}_{q[1]} \\\\ \\end{align}"]}, {"cell_type": "markdown", "metadata": {}, "source": ["where $g_0, g_1, g_2$, $g_3$, $g_4$ and $g_5$ are real numercial coefficients."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QubitPauliOperator` is a dictionary mapping [`pytket.pauli.QubitPauliString`](https://tket.quantinuum.com/api-docs/pauli.html#pytket.pauli.QubitPauliString) to a complex coefficient. These coefficients are sympified (converted from python `complex` types to sympy `complex` types)."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QubitPauliString` is a map from `pytket.circuit.Qubit` to `pytket.pauli.Pauli`."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The coefficients in the Hamiltonian are obtained from [PhysRevX.6.031007](https://journals.aps.org/prx/abstract/10.1103/PhysRevX.6.031007)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils.operators import QubitPauliOperator\n", "from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.circuit import Qubit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["coeffs = [-0.4804, 0.3435, -0.4347, 0.5716, 0.0910, 0.0910]\n", "term0 = {\n", " QubitPauliString(\n", " {\n", " Qubit(0): Pauli.I,\n", " Qubit(1): Pauli.I,\n", " }\n", " ): coeffs[0]\n", "}\n", "term1 = {QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.I}): coeffs[1]}\n", "term2 = {QubitPauliString({Qubit(0): Pauli.I, Qubit(1): Pauli.Z}): coeffs[2]}\n", "term3 = {QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Z}): coeffs[3]}\n", "term4 = {QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.X}): coeffs[4]}\n", "term5 = {QubitPauliString({Qubit(0): Pauli.Y, Qubit(1): Pauli.Y}): coeffs[5]}\n", "term_sum = {}\n", "term_sum.update(term0)\n", "term_sum.update(term1)\n", "term_sum.update(term2)\n", "term_sum.update(term3)\n", "term_sum.update(term4)\n", "term_sum.update(term5)\n", "hamiltonian = QubitPauliOperator(term_sum)\n", "print(hamiltonian)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This Hamiltonian can be converted into a `numpy.ndarray` instance, and the lowest eigenvalue can be obtained using `numpy.linalg.eig`. This value is used as a benchmark for the VQE result. The ground-state energy is measured in units of Hartrees (Ha)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from scipy.linalg import eig"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["sm = hamiltonian.to_sparse_matrix().toarray()\n", "ground_state_energy = eig(sm)[0].real[0]\n", "print(f\"{ground_state_energy} Ha\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To measure $\\hat{H}$ on hardware, naively 5 measurement circuits are required. The Identity term does not need to measured, since its expectation value always equals 1.
\n", "With pytket, $\\hat{H}$ only requires simulating 2 measurement circuit, thanks to measurement reduction. The four terms $\\hat{X}_{q[0]} \\otimes \\hat{X}_{q[1]}$, $\\hat{Y}_{q[0]} \\otimes \\hat{Y}_{q[1]}$, $\\hat{Z}_{q[0]} \\otimes \\hat{Z}_{q[1]}$, $\\hat{Z}_{q[0]} \\otimes \\hat{Z}_{q[1]}$ and $\\hat{I}_{q[0]} \\otimes \\hat{Z}_{q[1]}$, form a commuting set and can be measured with two circuits instead of three. This partitioning can be performed automatically using the [`measurement_reduction`](https://tket.quantinuum.com/api-docs/partition.html#pytket.partition.measurement_reduction) function available in [`pytket.partition`](https://tket.quantinuum.com/api-docs/partition.html#module-pytket.partition) submodule."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The measurement operations for the two commuting set,
\n", "* $\\left\\{ \\hat{X}_{q[0]} \\otimes \\hat{X}_{q[1]}, \\hat{Y}_{q[0]} \\otimes \\hat{Y}_{q[1]} \\right\\}$,
\n", "* $\\left\\{ \\hat{Z}_{q[0]} \\otimes \\hat{Z}_{q[1]}, \\hat{Z}_{q[0]} \\otimes \\hat{I}_{q[1]}, \\hat{I}_{q[0]} \\otimes \\hat{Z}_{q[1]}\\right\\}$"]}, {"cell_type": "markdown", "metadata": {}, "source": ["include additional two-qubit gate resources."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.partition import (\n", " measurement_reduction,\n", " PauliPartitionStrat,\n", ")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["strat = PauliPartitionStrat.CommutingSets\n", "pauli_strings = [term for term in hamiltonian._dict.keys()]\n", "measurement_setup = measurement_reduction(pauli_strings, strat)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["A measurement subcircuit contains the necessary operations to measure the terms in a commuting set. The subcircuit is appended to the numerical state-preparation circuit. Combining the numerical state-preparation circuit and the measurement subcircuits results in a set of measurement circuits required to solve the problem. The [`MeasurementSetup`](https://tket.quantinuum.com/api-docs/partition.html#pytket.partition.MeasurementSetup) instance contains all the necessary sub-circuits to measure $\\hat{H}$. The next code cell lists and visualises all measurement subcircuits."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for measurement_subcircuit in measurement_setup.measurement_circs:\n", " render_circuit_jupyter(measurement_subcircuit)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Once the quantum computation has been completed, the measurement results can be mapped back to the Pauli-operations and coefficients in the Hamiltonian. This enables calculation of the expectation value for the Hamiltonian. The results attribute in the [`pytket.partition.MeasurementSetup`](https://tket.quantinuum.com/api-docs/partition.html#pytket.partition.MeasurementSetup) lists:"]}, {"cell_type": "markdown", "metadata": {}, "source": ["* all the Pauli-strings that have been measured;
\n", "* information to process the quantum computed measurement result in order"]}, {"cell_type": "markdown", "metadata": {}, "source": ["to estimate the expectation value of each Pauli-strings."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for i, (term, bitmap_list) in enumerate(measurement_setup.results.items()):\n", " print(f\"{term}\\t{bitmap_list}\\n\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## 3. Computing Expectation Values "]}, {"cell_type": "markdown", "metadata": {}, "source": ["Once the Hamiltonian has been partitioned into commuting sets, measurement circuits need to be constructed. These measurement circuits are submitted to hardware or emulators for simulation. Once the simulation is complete, a result is available to request, and can be retrieved using `pytket`. These results are the outcomes
\n", "of the measurement circuit simulation. Each result is a distribution of outcomes, specifically, the probability of observing specific bitstring. This distribution is post-processed to compute the expectation value of the Hamiltonian, a necessity to evaluate the cost function in a Hybrid Quantum-Classical variational procedure."]}, {"cell_type": "markdown", "metadata": {}, "source": ["### 3.1 Computing Expectation Values for Pauli-Strings"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The Hamiltonian we are interested in consists of Pauli-strings. The expectation value of the Pauli-string is in the interval $[-1, 1]$."]}, {"cell_type": "markdown", "metadata": {}, "source": ["In the code cell below, a function is provided that calculates the expectation value of Pauli-string from a measured distribution. The [`MeasurementBitmap`](https://tket.quantinuum.com/api-docs/partition.html#pytket.partition.MeasurementBitMap) is used to extract the necessary data from the measured distribution. The resulting distribution can be summed over to estimate the expectation value of one Pauli-string."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from typing import Dict, Tuple\n", "from pytket.partition import MeasurementBitMap"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def compute_expectation_paulistring(\n", " distribution: Dict[Tuple[int, ...], float], bitmap: MeasurementBitMap\n", ") -> float:\n", " value = 0\n", " for bitstring, probability in distribution.items():\n", " value += probability * (sum(bitstring[i] for i in bitmap.bits) % 2)\n", " return ((-1) ** bitmap.invert) * (-2 * value + 1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In the example below, the function `compute_expectation_paulistring` is called to calculate the expectation for the $\\hat{Z} \\otimes \\hat{Z}$. First the `QubitPauliString` is initialised, and that is used to extract the relevant data from the MeasurementSetup object defined in section 2. This data is used for postprocessing."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.circuit import Qubit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["distribution = {(0, 0): 0.45, (1, 1): 0.3, (0, 1): 0.1, (1, 0): 0.15}\n", "zz = QubitPauliString([Qubit(0), Qubit(1)], [Pauli.Z, Pauli.Z])\n", "bitmap_list = measurement_setup.results.get(zz)\n", "for bitmap in bitmap_list:\n", " ev = compute_expectation_paulistring(distribution, bitmap)\n", " print(ev)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### 3.2 Computing Expectation Values for sums of Pauli-strings multiplied by coefficients
\n", "In this step, we will submit circuits to the H-Series emulator (`H1-1E`). This circuit will produce a result. The result can be retrieved with the `ResultHandle` object. First, the symbolic circuit is converted into a numerical circuit. The symbols in the circuit are substituted for numerical parameters."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["symbol_map = {sym: 0.1 for sym in symbolic_circuit.free_symbols()}\n", "numerical_circuit = symbolic_circuit.copy()\n", "numerical_circuit.symbol_substitution(symbol_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### 3.3 Using QuantinuumBackend
\n", "The Quantinuum backend was initialised at the start of the notebook to use the H1-1E emulator. This backend will now be used to calculate the expectation value.
\n", "The measurement operations from the `MeasurementSetup` object are appended to the numerical circuit. Once this step is complete, the circuit is ready for submission if tket optimisation in the H-Series stack is selected."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circuit_list = []\n", "for mc in measurement_setup.measurement_circs:\n", " c = numerical_circuit.copy()\n", " c.append(mc)\n", " circuit_list += [c]\n", "compiled_circuit_list = quantinuum_backend.get_compiled_circuits(\n", " circuit_list, optimisation_level=2\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Before submitting to the emulator, the total cost of running the set of circuits can be checked beforehand."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cost_list = []"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots = 500\n", "for comp_circ in compiled_circuit_list:\n", " cost = quantinuum_backend.cost(comp_circ, n_shots=n_shots, syntax_checker=\"H1-1SC\")\n", " cost_list.append(cost)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Cost of experiment in HQCs:\", sum(cost_list))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now we run the circuits."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["handles = quantinuum_backend.process_circuits(\n", " compiled_circuit_list, n_shots=10, options={\"tket-opt-level\": None}\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The status of the jobs can be checked with `ciruit_status` method. This method requires the `ResultHandle` to be passed as input. In this example, the job has completed and the results are reported as being ready to request."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for h in handles:\n", " circuit_status = quantinuum_backend.circuit_status(h)\n", " print(circuit_status)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The expectation value of the operator can be evaluated with the function `compute_expectation_value` in the next code cell. This function requires a list of `BackendResult` objects, a `MeasurementSetup` instance, and the `QubitPauliOperator` instance for the expectation value computation. It is assumed the `MeasurementSetup` instance contains the measurement info of all the Pauli-strings in the `QubitPauliOperator` instance. Otherwise the `compute_expectation_value` function will return zero."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from typing import List\n", "from pytket.utils.operators import QubitPauliOperator\n", "from pytket.partition import MeasurementSetup\n", "from pytket.backends.backendresult import BackendResult"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from sympy import Abs"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def compute_expectation_value(\n", " results: List[BackendResult],\n", " measurement_setup: MeasurementSetup,\n", " operator: QubitPauliOperator,\n", ") -> float:\n", " energy = 0\n", " for pauli_string, bitmaps in measurement_setup.results.items():\n", " string_coeff = operator.get(pauli_string, 0.0)\n", " if Abs(string_coeff) > 0:\n", " value = 0\n", " for bm in bitmaps:\n", " index = bm.circ_index\n", " distribution = results[index].get_distribution()\n", " value += compute_expectation_paulistring(distribution, bm)\n", " energy += value * string_coeff / len(bitmaps)\n", " return energy"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The results of the previously submitted circuits can be retrieved with the `get_results` method on `QuantinuumBackend`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["results = quantinuum_backend.get_results(handles)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Finally, the expectation value, $\\langle{\\psi (\\vec{\\theta}_r)} | \\hat{H} | { \\psi (\\vec{\\theta}_r)} \\rangle$, of the `QubitPauliOperator` instance, $\\hat{H}$, is calculated with respect to $| { \\psi (\\vec{\\theta}_r)} \\rangle$. The state, $| \\psi \\rangle$, is prepared with the state-preparation circuit, and $\\vec{\\theta}_r$ is a random parameter set."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["expectation_value = compute_expectation_value(results, measurement_setup, hamiltonian)\n", "print(f\"Expectation Value: {expectation_value}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## 4. Variational Procedure "]}, {"cell_type": "markdown", "metadata": {}, "source": ["A hybrid quantum-classical variational procedure consists of multiple iterations, controlled by a classical parameter optimiser. The parameters are gate-angles on quantum circuits submitted to H-Series for simulation. In [step 3](#expval), a procedure is showcased to calculate the expectation value of a Hamiltonian with respect to a quantum state. It is shown how to use the measurement reduction and Pauli-string partitioning facility in `pytket` to reduce measurement resources for the experiments. For the variational procedure demonstrated below, the cost function calculates the expectation of an input Hamiltonian. The aim is to find the optimal parameters that minimise this expectation value."]}, {"cell_type": "markdown", "metadata": {}, "source": ["### 4.1. Objective function"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `Objective` class defined in the code cell performs the following utilities:
\n", "* Measurement Reduction;
\n", "* Creation of a Batch session to use across the variational experiment;
\n", "* Submission and retrieval of quantum circuits using `QuantinuumBackend`;
\n", "* Expectation Value evaluation."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `Objective` class requires the following inputs:
\n", "* Input symbolic state-preparation circuit;
\n", "* A `QubitPauliOperator` instance of the Hamiltonian characterising the use-case of interest;
\n", "* The backend to use. `QuantinuumBackend` is used to access H-Series service. The backend needs to be instantiated and the user needs to login within previous code cell.
\n", "* Number of shots to perform per circuit. H-Series devices have an upper limit of 10000 shots per job.
\n", "* Maximum consumable HQC credit before the batch is terminated.
\n", "* Number of iterations before the variational experiment is terminated."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `Objective` class instance can be passed as a callable to `scipy.optimize.minimize`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from typing import Callable\n", "from numpy import ndarray\n", "from numpy.random import random_sample\n", "from pytket.extensions.quantinuum import QuantinuumBackend\n", "from pytket.partition import PauliPartitionStrat\n", "from pytket.backends.resulthandle import ResultHandle"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["class Objective:\n", " def __init__(\n", " self,\n", " symbolic_circuit: Circuit,\n", " problem_hamiltonian: QubitPauliOperator,\n", " quantinuum_backend: QuantinuumBackend,\n", " n_shots_per_circuit: int,\n", " max_batch_cost: float = 300,\n", " n_iterations: int = 10,\n", " ) -> None:\n", " r\"\"\"Returns the objective function needed for a variational\n", " procedure on H-Series.\n", " Args:\n", " symbolic_circuit\n", " (pytket.circuit.Circuit): State-preparation\n", " circuit with symbolic parameters\n", " problem_hamiltonian (pytket.utils.operators.QubitPauliOperator):\n", " QubitPauliOperator instance defining the Hamiltonian of the\n", " problem.\n", " quantinuum_backend (pytket.extensions.quantinuum.QuantinuumBackend): Backend\n", " instance to use for the simulation. This will be\n", " QuantinuumBackend from the pytket.extensions.quantinuum\n", " package to run experiments on H-Series devices and emulators.\n", " n_shots_per_circuit (int): Number of shots per circuit.\n", " max_batch_cost (float): Maximum cost of all jobs in batch. If\n", " exceeded the batch will terminate.\n", " n_iterations (int): Total number of iterations before ending\n", " the batch session.\n", " Returns:\n", " Callable[[ndarray], float]\n", " \"\"\"\n", " terms = [term for term in problem_hamiltonian._dict.keys()]\n", " self._symbolic_circuit: Circuit = symbolic_circuit\n", " self._symbols: List[Symbol] = symbolic_circuit.free_symbols()\n", " self._hamiltonian: QubitPauliOperator = problem_hamiltonian\n", " self._backend: QuantinuumBackend = quantinuum_backend\n", " self._nshots: int = n_shots_per_circuit\n", " self._max_batch_cost: float = max_batch_cost\n", " self._measurement_setup: MeasurementSetup = measurement_reduction(\n", " terms, strat=PauliPartitionStrat.CommutingSets\n", " )\n", " self._iteration_number: int = 0\n", " self._niters: int = n_iterations\n", " def __call__(self, parameter: ndarray) -> float:\n", " value = self._objective_function(parameter, self._iteration_number)\n", " self._iteration_number += 1\n", " if self._iteration_number >= self._niters:\n", " self._iteration_number = 0\n", " return value\n", " def circuit_cost(self, syntax_checker: str = \"H1-1SC\") -> float:\n", " n = len(self._symbolic_circuit.free_symbols())\n", " random_parameters = random_sample(n)\n", " return sum(\n", " [\n", " self._backend.cost(c, self._nshots, syntax_checker=syntax_checker)\n", " for c in self._build_circuits(random_parameters)\n", " ]\n", " )\n", " def _objective_function(\n", " self,\n", " parameters: ndarray,\n", " iteration_number: int,\n", " ) -> float:\n", " r\"\"\"Substitutes input parameters into the\n", " symbolic state-preparation circuit, and\n", " calculates the expectation value.\n", " Args:\n", " parameters (ndarray): A list of numpy.ndarray\n", " Returns:\n", " float\n", " \"\"\"\n", " assert len(parameters) == len(self._symbolic_circuit.free_symbols())\n", " circuit_list = self._build_circuits(parameters)\n", " if not isinstance(self._backend, QuantinuumBackend):\n", " raise RuntimeError(\n", " \"Batching is not supported for any backend other than QuantinuumBackend.\"\n", " )\n", " if iteration_number == 0:\n", " self._startjob = self._backend.start_batch(\n", " self._max_batch_cost,\n", " circuit_list[0],\n", " self._nshots,\n", " noisy_simulation=False,\n", " options={\"tket-opt-level\": None},\n", " )\n", " handles = [self._startjob] + self._submit_batch(circuit_list[1:])\n", " else:\n", " handles = self._submit_batch(circuit_list)\n", " results = self._backend.get_results(handles)\n", " expval = compute_expectation_value(\n", " results, self._measurement_setup, self._hamiltonian\n", " )\n", " return expval\n", " def _build_circuits(self, parameters: ndarray) -> List[Circuit]:\n", " circuit = self._symbolic_circuit.copy()\n", " symbol_dict = {s: p for s, p in zip(self._symbols, parameters)}\n", " circuit.symbol_substitution(symbol_dict)\n", " circuit_list = []\n", " for mc in self._measurement_setup.measurement_circs:\n", " c = circuit.copy()\n", " c.append(mc)\n", " circuit_list.append(c)\n", " cc_list = self._backend.get_compiled_circuits(\n", " circuit_list, optimisation_level=2\n", " )\n", " return cc_list\n", " def _submit_batch(\n", " self,\n", " circuits: List[Circuit],\n", " ) -> List[ResultHandle]:\n", " r\"\"\"Submit a list of circuits with N shots each\n", " to the H-Series batch.\n", " Args:\n", " circuits (List[Circuit]): A list of circuits\n", " to submit to the batch on H-Series.\n", " first job (ResultHandle): The result handle for the\n", " first job submitted in the batch.\n", " Returns:\n", " List[ResultHandle]\n", " \"\"\"\n", " return [\n", " self._backend.add_to_batch(\n", " self._startjob,\n", " c,\n", " self._nshots,\n", " options={\"tket-opt-level\": None},\n", " noisy_simulation=False,\n", " )\n", " for c in circuits\n", " ]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `Objective` class is initialised with the essential data needed to perform the variational experiment. The object contains all the necessary information to compute the value of the objective function."]}, {"cell_type": "markdown", "metadata": {}, "source": ["A convenience method `circuit_cost` can be used to estimate the total number of HQCs required to estimate the objective function. The variational loop will be multiples of this value (number of function calls across the variational procedure multiplied by the HQC cost of evaluating the objective function)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots_per_circuit = 500\n", "n_iterations = 10\n", "max_batch_cost = 500\n", "objective = Objective(\n", " symbolic_circuit,\n", " hamiltonian,\n", " quantinuum_backend,\n", " n_shots_per_circuit,\n", " max_batch_cost=max_batch_cost,\n", " n_iterations=n_iterations,\n", ")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["objective.circuit_cost(\"H1-1SC\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### 4.2. Execute the Objective Function
\n", "The SciPy minimiser is used to optimise the value of the objective function. Initial parameters are pseudo-random. Passing the `Objective` instance into `scipy.optimize.minimize` will start the variational experiment."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The first iteration creates a batch session, and all subsequent circuit submission are added to this batch. If additional circuits are not submitted within 1 minute, the batch session will terminate."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Remember that the status of the batch can be checked at any time on the Quantinuum User Portal."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from scipy.optimize import minimize\n", "from numpy.random import random_sample"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["method = \"COBYLA\"\n", "initial_parameters = random_sample(len(symbolic_circuit.free_symbols()))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["result = minimize(\n", " objective,\n", " initial_parameters,\n", " method=method,\n", " options={\"disp\": True, \"maxiter\": objective._niters},\n", " tol=1e-2,\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The minimal value of the objective function can be retrieved with the `fun` attribute."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(f\"VQE Energy:\\t{result.fun} Ha\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The ground-state energy estimated with VQE can be compared with the value obtained from the `numpy.linalg` computation. The absolute error and the relative error is calculated here."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import numpy as np"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["abs_err = lambda experiment, benchmark: np.absolute(experiment - benchmark)\n", "rel_err = (\n", " lambda experiment, benchmark: abs_err(experiment, benchmark)\n", " / np.absolute(benchmark)\n", " * 100\n", ")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ae = abs_err(result.fun, ground_state_energy)\n", "re = rel_err(result.fun, ground_state_energy)\n", "print(f\"Absolute error:\\t{ae} Ha\\nRelative error:\\t{re}%\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The optimal parameters can be retreived with the `x` attribute."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["result.x"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The Symbols can be mapped to the optimal parameter by iterating through both lists:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["optimal_parameters = {s.name: p for s, p in zip(objective._symbols, result.x)}\n", "print(optimal_parameters)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["These symbols can be saved to an output file for further use if necessary using json. See the example below."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import json"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["json_io = open(\"parameters.json\", \"w\")\n", "json.dump(optimal_parameters, json_io)"]}, {"cell_type": "markdown", "metadata": {}, "source": [" © 2023 by Quantinuum. All Rights Reserved.
"]}], "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/python/Quantinuum_variational_experiment_with_batching.py b/examples/python/Quantinuum_variational_experiment_with_batching.py
index d06be48c..e3af47f7 100644
--- a/examples/python/Quantinuum_variational_experiment_with_batching.py
+++ b/examples/python/Quantinuum_variational_experiment_with_batching.py
@@ -1,4 +1,7 @@
-# # Quantinuum Variational Experiment on H-Series with tket
+#
+#
+
+# # Variational Experiment on Quantinuum H-Series with Batching
# Hybrid Quantum-Classical variational quantum algorithms consist of optimising a trial parametric wavefunction, $| \psi (\vec{\theta}) \rangle$, to estimate the lowest eigenvalue (or expectation value) of a Hamiltonian, $\hat{H}$. This could be an Electronic Structure Hamiltonian or a Hamiltonian defining a QUBO (quadratic unconstrained binary optimisation) or MAXCUT problem. The optimal parameters of the wavefunction, $(\vec{\theta})$ are an estimation of the lowest eigenvector of the Hamiltonian.
@@ -64,19 +67,17 @@
# 4. [Variational Procedure with Batches](#variational)
#
# ## 1. Synthesise Symbolic State-Preparation Circuit
-# We first prepare a two-qubit circuit consisting of fixed-angle two-qubit `CX` gates (`pytket.circuit.OpType.CX`) and variable-angle single-qubit `Ry` gates (`pytket.circuit.OpType.Rz`). This state-preparation technique is known as the Hardware-Efficient Ansatz (HEA) ([nature23879](https://www.nature.com/articles/nature23879)), instead of the usual chemistry state-preparation method, Unitary Coupled Cluster (UCC) ([arxiv.1701.02691](https://arxiv.org/abs/1701.02691)).
+# The code-cell below synthesises a two-qubit circuit consisting of arbitrary-angle two-qubit `ZZPhase` gates (`pytket.circuit.OpType.ZZPhase`) and fixed-angle single-qubit `X` gate (`pytket.circuit.OpType.X`). This state-preparation technique is inspired by the Hardware-Efficient Ansatz (HEA) ([nature23879](https://www.nature.com/articles/nature23879)), instead of the usual chemistry state-preparation method, Unitary Coupled Cluster (UCC) ([arxiv.1701.02691](https://arxiv.org/abs/1701.02691)).
#
-# The hardware-efficient state-preparation method requires alternating layers of fixed-angle two-qubit gates and variable-angle single-qubit gates. Ultimately, this leads to fewer two-qubit gates, but requires greater variational parameters, compared to UCC. The optimal parameters for HEA are governed by the noise profile of the device. The HEA circuit used in this example consists of one-layer (4-parameters) and only uses `Ry` gates.
+# The hardware-efficient state-preparation method requires alternating layers of two-qubit gates and single-qubit gates. Ultimately, this leads to fewer two-qubit gates, but requires a greater number of variational parameters, compared to UCC. The optimal parameters for HEA are governed by the noise profile of the device. The HEA circuit used in this example consists of one-layer `ZZPhase` gates.
from pytket.circuit import Circuit
from sympy import Symbol
-symbols = [Symbol(f"p{i}") for i in range(4)]
+symbols = [Symbol(f"p{0}")]
symbolic_circuit = Circuit(2)
symbolic_circuit.X(0)
-symbolic_circuit.Ry(symbols[0], 0).Ry(symbols[1], 1)
-symbolic_circuit.CX(0, 1)
-symbolic_circuit.Ry(symbols[2], 0).Ry(symbols[3], 0)
+symbolic_circuit.ZZPhase(symbols[0], 0, 1)
# The symbolic state-preparation circuit can be visualised using the `pytket.circuit.display` submodule.
@@ -126,6 +127,14 @@
hamiltonian = QubitPauliOperator(term_sum)
print(hamiltonian)
+# This Hamiltonian can be converted into a `numpy.ndarray` instance, and the lowest eigenvalue can be obtained using `numpy.linalg.eig`. This value is used as a benchmark for the VQE result. The ground-state energy is measured in units of Hartrees (Ha).
+
+from scipy.linalg import eig
+
+sm = hamiltonian.to_sparse_matrix().toarray()
+ground_state_energy = eig(sm)[0].real[0]
+print(f"{ground_state_energy} Ha")
+
# To measure $\hat{H}$ on hardware, naively 5 measurement circuits are required. The Identity term does not need to measured, since its expectation value always equals 1.
# With pytket, $\hat{H}$ only requires simulating 2 measurement circuit, thanks to measurement reduction. The four terms $\hat{X}_{q[0]} \otimes \hat{X}_{q[1]}$, $\hat{Y}_{q[0]} \otimes \hat{Y}_{q[1]}$, $\hat{Z}_{q[0]} \otimes \hat{Z}_{q[1]}$, $\hat{Z}_{q[0]} \otimes \hat{Z}_{q[1]}$ and $\hat{I}_{q[0]} \otimes \hat{Z}_{q[1]}$, form a commuting set and can be measured with two circuits instead of three. This partitioning can be performed automatically using the [`measurement_reduction`](https://tket.quantinuum.com/api-docs/partition.html#pytket.partition.measurement_reduction) function available in [`pytket.partition`](https://tket.quantinuum.com/api-docs/partition.html#module-pytket.partition) submodule.
@@ -160,6 +169,7 @@
for i, (term, bitmap_list) in enumerate(measurement_setup.results.items()):
print(f"{term}\t{bitmap_list}\n")
+
# ## 3. Computing Expectation Values
# Once the Hamiltonian has been partitioned into commuting sets, measurement circuits need to be constructed. These measurement circuits are submitted to hardware or emulators for simulation. Once the simulation is complete, a result is available to request, and can be retrieved using `pytket`. These results are the outcomes
@@ -246,6 +256,8 @@ def compute_expectation_paulistring(
from pytket.partition import MeasurementSetup
from pytket.backends.backendresult import BackendResult
+from sympy import Abs
+
def compute_expectation_value(
results: List[BackendResult],
@@ -255,12 +267,13 @@ def compute_expectation_value(
energy = 0
for pauli_string, bitmaps in measurement_setup.results.items():
string_coeff = operator.get(pauli_string, 0.0)
- if string_coeff > 0:
+ if Abs(string_coeff) > 0:
+ value = 0
for bm in bitmaps:
index = bm.circ_index
distribution = results[index].get_distribution()
- value = compute_expectation_paulistring(distribution, bm)
- energy += complex(value * string_coeff).real
+ value += compute_expectation_paulistring(distribution, bm)
+ energy += value * string_coeff / len(bitmaps)
return energy
@@ -379,9 +392,17 @@ def _objective_function(
"""
assert len(parameters) == len(self._symbolic_circuit.free_symbols())
circuit_list = self._build_circuits(parameters)
+ if not isinstance(self._backend, QuantinuumBackend):
+ raise RuntimeError(
+ "Batching is not supported for any backend other than QuantinuumBackend."
+ )
if iteration_number == 0:
- self._startjob = quantinuum_backend.start_batch(
- self._max_batch_cost, circuit_list[0], self._nshots
+ self._startjob = self._backend.start_batch(
+ self._max_batch_cost,
+ circuit_list[0],
+ self._nshots,
+ noisy_simulation=False,
+ options={"tket-opt-level": None},
)
handles = [self._startjob] + self._submit_batch(circuit_list[1:])
else:
@@ -422,7 +443,11 @@ def _submit_batch(
"""
return [
self._backend.add_to_batch(
- self._startjob, c, self._nshots, options={"tket-opt-level": None}
+ self._startjob,
+ c,
+ self._nshots,
+ options={"tket-opt-level": None},
+ noisy_simulation=False,
)
for c in circuits
]
@@ -458,6 +483,7 @@ def _submit_batch(
method = "COBYLA"
initial_parameters = random_sample(len(symbolic_circuit.free_symbols()))
+
result = minimize(
objective,
initial_parameters,
@@ -468,7 +494,22 @@ def _submit_batch(
# The minimal value of the objective function can be retrieved with the `fun` attribute.
-result.fun
+print(f"VQE Energy:\t{result.fun} Ha")
+
+# The ground-state energy estimated with VQE can be compared with the value obtained from the `numpy.linalg` computation. The absolute error and the relative error is calculated here.
+
+import numpy as np
+
+abs_err = lambda experiment, benchmark: np.absolute(experiment - benchmark)
+rel_err = (
+ lambda experiment, benchmark: abs_err(experiment, benchmark)
+ / np.absolute(benchmark)
+ * 100
+)
+
+ae = abs_err(result.fun, ground_state_energy)
+re = rel_err(result.fun, ground_state_energy)
+print(f"Absolute error:\t{ae} Ha\nRelative error:\t{re}%")
# The optimal parameters can be retreived with the `x` attribute.
@@ -486,4 +527,4 @@ def _submit_batch(
json_io = open("parameters.json", "w")
json.dump(optimal_parameters, json_io)
-# © 2024 by Quantinuum. All Rights Reserved.
+# © 2023 by Quantinuum. All Rights Reserved.
diff --git a/tests/integration/backend_test.py b/tests/integration/backend_test.py
index 20026aec..177a6c60 100644
--- a/tests/integration/backend_test.py
+++ b/tests/integration/backend_test.py
@@ -53,7 +53,7 @@
from pytket.extensions.quantinuum.backends.quantinuum import (
GetResultFailed,
_GATE_SET,
- NoSyntaxChecker,
+ # NoSyntaxChecker,
)
from pytket.extensions.quantinuum.backends.api_wrappers import (
QuantinuumAPIError,