{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# VQE workflow using Quantinuum Nexus\n", "\n", "This is a full example of using the features in `qnexus` to run and restart a 'Variational Quantum Eigensolver' workflow.\n", "\n", "\n", "VQE example adapted from https://github.com/CQCL/pytket-quantinuum/blob/develop/examples/Quantinuum_variational_experiment_with_batching.ipynb" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from datetime import datetime\n", "\n", "from pytket import Circuit\n", "from pytket.circuit.display import render_circuit_jupyter\n", "from pytket.utils.operators import QubitPauliOperator\n", "from pytket.partition import measurement_reduction, MeasurementBitMap, MeasurementSetup, PauliPartitionStrat\n", "from pytket.backends.backendresult import BackendResult\n", "from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.circuit import Qubit\n", "\n", "from scipy.optimize import minimize\n", "from numpy import ndarray\n", "from numpy.random import random_sample\n", "from sympy import Symbol\n", "\n", "import qnexus as qnx" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Set up the VQE components" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 1. Synthesise Symbolic State-Preparation Circuit (hardware efficient ansatz)\n", "\n", "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": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "render_circuit_jupyter(symbolic_circuit)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 2. Define Hamiltonian \n", "# coefficients in the Hamiltonian are obtained from PhysRevX.6.031007\n", "\n", "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" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 3 Computing Expectation Values\n", "\n", "# Computing Expectation Values for Pauli-Strings\n", "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": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 3.2 Computing Expectation Values for sums of Pauli-strings multiplied by coefficients\n", "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": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 4. Building our Objective function\n", "\n", "class Objective:\n", " def __init__(\n", " self,\n", " symbolic_circuit: qnx.circuits.CircuitRef,\n", " problem_hamiltonian: QubitPauliOperator,\n", " n_shots_per_circuit: int,\n", " target: qnx.BackendConfig,\n", " iteration_number: int = 0,\n", " n_iterations: int = 10,\n", " ) -> None:\n", " \"\"\"Returns the objective function needed for a variational\n", " procedure.\n", " \"\"\"\n", " terms = [term for term in problem_hamiltonian._dict.keys()]\n", " self._symbolic_circuit: Circuit = symbolic_circuit.download_circuit()\n", " self._hamiltonian: QubitPauliOperator = problem_hamiltonian\n", " self._nshots: int = n_shots_per_circuit\n", " self._measurement_setup: MeasurementSetup = measurement_reduction(\n", " terms, strat=PauliPartitionStrat.CommutingSets\n", " )\n", " self._iteration_number: int = iteration_number\n", " self._niters: int = n_iterations\n", " self._target = target\n", "\n", "\n", " def __call__(self, parameter: ndarray) -> float:\n", " value = self._objective_function(parameter)\n", " self._iteration_number += 1\n", " if self._iteration_number >= self._niters:\n", " self._iteration_number = 0\n", " return value\n", " \n", " def _objective_function(\n", " self,\n", " parameters: ndarray,\n", " ) -> float:\n", " \n", " # Prepare the parameterised state preparation circuit\n", " assert len(parameters) == len(self._symbolic_circuit.free_symbols())\n", " symbol_dict = {s: p for s, p in zip(self._symbolic_circuit.free_symbols(), parameters)}\n", " state_prep_circuit = self._symbolic_circuit.copy()\n", " state_prep_circuit.symbol_substitution(symbol_dict)\n", "\n", " # Label each job with the properties associated with the circuit.\n", " properties = {str(sym): val for sym, val in symbol_dict.items()} | {\"iteration\": self._iteration_number}\n", "\n", " with qnx.context.using_properties(**properties):\n", "\n", " circuit_list = self._build_circuits(state_prep_circuit)\n", "\n", " # Execute circuits with Nexus\n", " results = qnx.execute(\n", " name=f\"execute_job_VQE_{datetime.now()}_{self._iteration_number}\",\n", " circuits=circuit_list,\n", " n_shots=[self._nshots]*len(circuit_list),\n", " backend_config=self._target,\n", " timeout=None,\n", " )\n", "\n", " expval = compute_expectation_value(\n", " results, self._measurement_setup, self._hamiltonian\n", " )\n", " return expval\n", "\n", " def _build_circuits(self, state_prep_circuit: Circuit) -> list[qnx.circuits.CircuitRef]:\n", " # Requires properties to be set in the context\n", " \n", " # Upload the numerical state-prep circuit to Nexus\n", " qnx.circuits.upload(\n", " circuit=state_prep_circuit,\n", " name=f\"state prep circuit {self._iteration_number}\",\n", " )\n", " circuit_list = []\n", " for mc in self._measurement_setup.measurement_circs:\n", " c = state_prep_circuit.copy()\n", " c.append(mc)\n", " # Upload each measurement circuit to Nexus with correct params\n", " measurement_circuit_ref = qnx.circuits.upload(\n", " circuit=c, \n", " name=f\"state prep circuit {self._iteration_number}\",\n", " )\n", " circuit_list.append(measurement_circuit_ref)\n", "\n", " # Compile circuits with Nexus\n", " compiled_circuit_refs = qnx.compile(\n", " name=f\"compile_job_VQE_{datetime.now()}_{self._iteration_number}\",\n", " circuits=circuit_list,\n", " optimisation_level=2,\n", " backend_config=self._target,\n", " timeout=None,\n", " )\n", " \n", " return compiled_circuit_refs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Set up the Nexus Project and run the VQE" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# set up the project\n", "project_ref = qnx.projects.create(\n", " name=f\"VQE_example_{str(datetime.now())}\",\n", " description=\"A VQE done with qnexus\",\n", ")\n", "\n", "# set this in the context\n", "qnx.context.set_active_project(project_ref)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Using Properties for Parameters\n", "\n", "Properties are a way to annotate resources in Nexus with custom attributes.\n", "\n", "As we will be computing properties in a loop, the iteration number is a natural fit for the property." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "qnx.projects.add_property(\n", " name=\"iteration\", \n", " property_type=\"int\", \n", " description=\"The iteration number in my dihydrogen VQE experiment\", \n", ")\n", "\n", "# Set up the properties for the symbolic circuit parameters\n", "for sym in symbolic_circuit.free_symbols():\n", " qnx.projects.add_property(\n", " name=str(sym), \n", " property_type=\"float\",\n", " description=f\"Our VQE {str(sym)} parameter\", \n", " )" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Upload our ansatz circuit\n", "\n", "ansatz_ref = qnx.circuits.upload(\n", " circuit=symbolic_circuit,\n", " name=\"ansatz_circuit\",\n", " description=\"The ansatz state-prep circuit for my dihydrogen VQE\",\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Construct our objective function" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "objective = Objective(\n", " symbolic_circuit = ansatz_ref,\n", " problem_hamiltonian = hamiltonian,\n", " n_shots_per_circuit = 500,\n", " n_iterations= 4,\n", " target = qnx.QuantinuumConfig(device_name=\"H1-1LE\")\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Run the VQE loop" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "initial_parameters = random_sample(len(symbolic_circuit.free_symbols()))\n", "\n", "result = minimize(\n", " objective,\n", " initial_parameters,\n", " method=\"COBYLA\",\n", " options={\"disp\": True, \"maxiter\": objective._niters},\n", " tol=1e-2,\n", ")\n", "\n", "print(result.fun)\n", "print(result.x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Use Nexus to Rescue a VQE workflow\n", "\n", "For instance, lets say that some failure happened on the 2nd iteration (e.g. laptop ran out of battery) and we want to resume ASAP.\n", "\n", "In the above we ran for 4 iterations, lets pretend that we actually wanted to run for 7 and it failed on the 4th one.\n", "\n", "N.B. The SciPy minimizer will have internal state which is not accounted for in this example." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Get the project, fetching the latest one with the name prefix from above\n", "project_matches = qnx.projects.get_all(name_like=\"VQE_example_\", sort_filters=['-created'])\n", "\n", "project_ref = project_matches.list()[0]\n", "\n", "# set this in the context\n", "qnx.context.set_active_project(project_ref)\n", "\n", "project_ref.df()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Get the symbolic circuit\n", "symbolic_circuit_ref = qnx.circuits.get(name_like=\"ansatz_circuit\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "most_recent_circuits = qnx.circuits.get_all(name_like=\"final\", project=project_ref)\n", "\n", "most_recent_circuits.summarize()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "most_recent_circuits_refs = most_recent_circuits.list()\n", "\n", "most_recent_circuits_refs.df()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Get the latest circuit to get the new 'initial_parameters'\n", "latest_circuit: qnx.circuits.CircuitRef = most_recent_circuits_refs[-1]\n", "\n", "latest_circuit_properties = latest_circuit.annotations.properties\n", "\n", "latest_circuit.df()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Get what iteration we were on (from the latest circuit)\n", "\n", "last_iteration_count = latest_circuit_properties.pop(\"iteration\")\n", "\n", "print(last_iteration_count)\n", "\n", "# Retreive the params and check them\n", "new_starting_params = list(latest_circuit_properties.values())\n", "print(new_starting_params)\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Build the Objective and run 'minimize' to continue the experiment\n", "objective = Objective(\n", " symbolic_circuit_ref,\n", " hamiltonian,\n", " n_shots_per_circuit = 500,\n", " iteration_number=last_iteration_count, # resume from 3rd iteration of 7\n", " n_iterations = 7,\n", " target = qnx.QuantinuumConfig(device_name=\"H1-1LE\")\n", ")\n", "\n", "result = minimize(\n", " objective,\n", " new_starting_params,\n", " method=\"COBYLA\",\n", " options={\"disp\": True, \"maxiter\": objective._niters},\n", " tol=1e-2,\n", ")\n", "\n", "print(result.fun)\n", "print(result.x)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.8" } }, "nbformat": 4, "nbformat_minor": 4 }