{ "cells": [ { "cell_type": "markdown", "id": "98d4d396", "metadata": {}, "source": [ "# Custom VQE Program for Qiskit Runtime\n", "\n", "```{post} 2021-10-07\n", ":tags: Custom\n", ":category: Runtime\n", "```" ] }, { "cell_type": "markdown", "id": "e9e6d69c", "metadata": {}, "source": [ "Here we will demonstrate how to create, upload, and use a custom Program for Qiskit Runtime. As the utility of the Runtime execution engine lies in its ability to execute many quantum circuits with low latencies, this tutorial will show how to create your own Variational Quantum Eigensolver (VQE) program from scratch." ] }, { "cell_type": "markdown", "id": "96b86d47", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "- You must have Qiskit 0.30+ installed.\n", "- You must have an IBM Quantum account with the ability to upload a runtime program. You have this ability if you belong to more than one provider." ] }, { "cell_type": "markdown", "id": "46817643", "metadata": {}, "source": [ "## Current limitations\n", "\n", "The runtime execution engine currently has the following limitations that must be kept in mind:\n", "\n", "- The Docker images used by the runtime include only Qiskit and its dependencies, with few exceptions. One exception is the inclusion of the `mthree` measurement mitigation package.\n", "\n", "\n", "- For security reasons, the runtime cannot make internet calls outside of the environment.\n", "\n", "\n", "- Your runtime program name must not contain an underscore`_`, otherwise it will cause an error when you try to execute it.\n", "\n", "As Qiskit Runtime matures, these limitations will be removed." ] }, { "cell_type": "markdown", "id": "98e4b3ac", "metadata": {}, "source": [ "## Simple VQE\n", "\n", "VQE is an hybrid quantum-classical optimization procedure that finds the lowest eigenstate and eigenenergy of a linear system defined by a given Hamiltonian of Pauli operators. For example, consider the following two-qubit Hamiltonian:\n", "\n", "$$\n", "H = A X_{1}\\otimes X_{0} + A Y_{1}\\otimes Y_{0} + A Z_{1}\\otimes Z_{0},\n", "$$\n", "\n", "where $A$ is numerical coefficient and the subscripts label the qubits on which the operators act. The zero index being farthest right is the ordering used in Qiskit. The Pauli operators tell us which measurement basis to to use when measuring each of the qubits.\n", "\n", "We want to find the ground state (lowest energy state) of this Hamiltonian, and the associated eigenvector. To do this we must start at a given initial state and iteratively vary the parameters that define this state using a classical optimizer, such that the computed energies of subsequent steps are nominally lower than those previously. The parameterized state of the system is defined by an ansatz quantum circuit that should have non-zero support in the direction of the ground state. Because in general we do not know the solution, the choice of ansatz circuit can be highly problem-specific with a form dictated by additional information. For further information about variational algorithms, we point the reader to [Nature Reviews Physics volume 3, 625 (2021)](https://doi.org/10.1038/s42254-021-00348-9).\n", "\n", "\n", "Thus we need at least the following inputs to create our VQE quantum program:\n", "\n", "1. A representation of the Hamiltonian that specifies the problem.\n", "\n", "\n", "2. A choice of parameterized ansatz circuit, and the ability to pass configuration options, if any.\n", "\n", "\n", "However, the following are also beneficial inputs that users might want to have:\n", "\n", "3. Add the ability to pass an initial state.\n", "\n", "\n", "4. Vary the number of shots that are taken.\n", "\n", "\n", "5. Ability to select which classical optimizer is used, and set configuraton values, if any. \n", "\n", "\n", "6. Ability to turn on and off measurement mitigation.\n" ] }, { "cell_type": "markdown", "id": "d15ad0da", "metadata": {}, "source": [ "## Specifying the form of the input values\n", "\n", "All inputs to runtime programs must be serializable objects. That is to say, whatever you pass into a runtime program must be able to be converted to JSON format. It is thus beneficial to keep inputs limited to basic data types and structures unless you have experience with custom object serialization, or they are common Qiskit types such as ``QuantumCircuit`` etc that the built-in `RuntimeEncoder` can handle. Fortunately, the VQE program described above can be made out of simple Python components.\n", "\n", "First, it is possible to represent any Hamiltonian using a list of values with each containing the numerical coefficeint for each term and the string representation for the Pauli operators. For the above example, the ground state energy with $A=1$ is $-3$ and we can write it as:" ] }, { "cell_type": "code", "execution_count": 1, "id": "78b7a519", "metadata": {}, "outputs": [], "source": [ "H = [(1, 'XX'), (1, 'YY'), (1, 'ZZ')]" ] }, { "cell_type": "markdown", "id": "fca6d7c5", "metadata": {}, "source": [ "Next we have to provide the ability to specify the parameterized Ansatz circuit. Here we will take advange of the fact that many ansatz circuits are pre-defined in the Qiskit Circuit Library. Examples can be found in the [N-local circuits section](https://qiskit.org/documentation/apidoc/circuit_library.html#n-local-circuits).\n", "\n", "We would like the user to be able to select between ansatz options such as: `NLocal`, `TwoLocal`, and `EfficientSU2`. We could have the user pass the whole ansatz circuit to the program; however, in order to reduce the size of the upload we will pass the ansatz by name. In the runtime program, we can take this name and get the class that it corresponds to from the library using, for example, " ] }, { "cell_type": "code", "execution_count": 2, "id": "421f2f51", "metadata": {}, "outputs": [], "source": [ "import qiskit.circuit.library.n_local as lib_local\n", "\n", "ansatz = getattr(lib_local, 'EfficientSU2')" ] }, { "cell_type": "markdown", "id": "48c7ebae", "metadata": {}, "source": [ "For the ansatz configuration, we will pass a simple `dict` of values." ] }, { "cell_type": "markdown", "id": "a592ac05", "metadata": {}, "source": [ "### Optionals \n", "\n", "- If we want to add the ability to pass an initial state, then we will need to add the ability to pass a 1D list/ NumPy array. Because the number of parameters depends on the ansatz and its configuration, the user would have to know what ansatz they are doing ahead of time.\n", "\n", "\n", "- Selecting a number of shots requires simply passing an integer value.\n", "\n", "\n", "- Here we will allow selecting a classical optimizer by name from those in SciPy, and a `dict` of configuration parameters. Note that for execution on an actual system, the noise inherent in today's quantum systems makes having a stochastic optimizer crucial to success. SciPy does not have such a choice, and the one built into Qiskit is wrapped in such a manner as to make it difficult to use elsewhere. As such, here we will use an SPSA optimizer written to match the style of those in SciPy. This function is given in [Appendix A](#Appendix-A)." ] }, { "cell_type": "markdown", "id": "8c5964c1", "metadata": {}, "source": [ "- Finally, for measurement error mitigation we can simply pass a boolean (True/False) value." ] }, { "cell_type": "markdown", "id": "00df4e79", "metadata": {}, "source": [ "## Main program\n", "\n", "We are now in a position to start building our main program. However, before doing so we point out that it makes the code cleaner to make a separate fuction that takes strings of Pauli operators that define our Hamiltonian and convert them to a list of circuits with single-qubit gates that change the measurement basis for each qubit, if needed. This function is given in [Appendix B](#Appendix-B)." ] }, { "cell_type": "markdown", "id": "3ddbadd8", "metadata": {}, "source": [ "### Required signature\n", "\n", "Every runtime program is defined via the `main` function, and must have the following input signature:\n", "\n", "```\n", "main(backend, user_message, *args, **kwargs)\n", "```\n", "\n", "where `backend` is the backend that the program is to be executed on, and `user_message` is the class by which interim (and possibly final) results are communicated back to the user. After these two items, we add our program-specific arguments and keyword arguments." ] }, { "cell_type": "markdown", "id": "28ba84cc", "metadata": {}, "source": [ "### The main VQE program\n", "\n", "Here is the main program for our sample VQE. What each element of the function does is written in the comments before the element appears." ] }, { "cell_type": "code", "execution_count": 3, "id": "96bb7811", "metadata": {}, "outputs": [], "source": [ "# Grab functions and modules from dependencies\n", "import numpy as np\n", "import scipy.optimize as opt\n", "from scipy.optimize import OptimizeResult\n", "import mthree\n", "\n", "# Grab functions and modules from Qiskit needed\n", "from qiskit import QuantumCircuit, transpile\n", "import qiskit.circuit.library.n_local as lib_local\n", "\n", "# The entrypoint for our Runtime Program\n", "def main(backend, user_messenger,\n", " hamiltonian,\n", " ansatz='EfficientSU2',\n", " ansatz_config={},\n", " x0=None,\n", " optimizer='SPSA',\n", " optimizer_config={'maxiter': 100},\n", " shots = 8192,\n", " use_measurement_mitigation=False\n", " ):\n", " \n", " \"\"\"\n", " The main sample VQE program.\n", " \n", " Parameters:\n", " backend (ProgramBackend): Qiskit backend instance.\n", " user_messenger (UserMessenger): Used to communicate with the\n", " program user.\n", " hamiltonian (list): Hamiltonian whose ground state we want to find.\n", " ansatz (str): Optional, name of ansatz quantum circuit to use,\n", " default='EfficientSU2'\n", " ansatz_config (dict): Optional, configuration parameters for the\n", " ansatz circuit.\n", " x0 (array_like): Optional, initial vector of parameters.\n", " optimizer (str): Optional, string specifying classical optimizer,\n", " default='SPSA'.\n", " optimizer_config (dict): Optional, configuration parameters for the\n", " optimizer.\n", " shots (int): Optional, number of shots to take per circuit.\n", " use_measurement_mitigation (bool): Optional, use measurement mitigation,\n", " default=False.\n", " \n", " Returns:\n", " OptimizeResult: The result in SciPy optimization format. \n", " \"\"\"\n", " \n", " # Split the Hamiltonian into two arrays, one for coefficients, the other for\n", " # operator strings\n", " coeffs = np.array([item[0] for item in hamiltonian], dtype=complex)\n", " op_strings = [item[1] for item in hamiltonian]\n", " # The number of qubits needed is given by the number of elements in the strings\n", " # the defiune the Hamiltonian. Here we grab this data from the first element.\n", " num_qubits = len(op_strings[0])\n", " \n", " # We grab the requested ansatz circuit class from the Qiskit circuit library\n", " # n_local module and configure it using the number of qubits and options\n", " # passed in the ansatz_config.\n", " ansatz_instance = getattr(lib_local, ansatz)\n", " ansatz_circuit = ansatz_instance(num_qubits, **ansatz_config)\n", " \n", " # Here we use our convenence function from Appendix B to get measurement circuits\n", " # with the correct single-qubit rotation gates.\n", " meas_circs = opstr_to_meas_circ(op_strings)\n", " \n", " # When computing the expectation value for the energy, we need to know if we\n", " # evaluate a Z measurement or and identity measurement. Here we take and X and Y\n", " # operator in the strings and convert it to a Z since we added the rotations\n", " # with the meas_circs.\n", " meas_strings = [string.replace('X', 'Z').replace('Y', 'Z') for string in op_strings]\n", " \n", " # Take the ansatz circuits, add the single-qubit measurement basis rotations from\n", " # meas_circs, and finally append the measurements themselves.\n", " full_circs = [ansatz_circuit.compose(mcirc).measure_all(inplace=False) for mcirc in meas_circs]\n", " \n", " # Get the number of parameters in the ansatz circuit.\n", " num_params = ansatz_circuit.num_parameters\n", " \n", " # Use a given initial state, if any, or do random initial state.\n", " if x0:\n", " x0 = np.asarray(x0, dtype=float)\n", " if x0.shape[0] != num_params:\n", " raise ValueError('Number of params in x0 ({}) does not match number \\\n", " of ansatz parameters ({})'. format(x0.shape[0],\n", " num_params))\n", " else:\n", " x0 = 2*np.pi*np.random.rand(num_params)\n", " \n", " # Because we are in general targeting a real quantum system, our circuits must be transpiled\n", " # to match the system topology and, hopefully, optimize them.\n", " # Here we will set the transpiler to the most optimal settings where 'sabre' layout and\n", " # routing are used, along with full O3 optimization.\n", "\n", " # This works around a bug in Qiskit where Sabre routing fails for simulators (Issue #7098)\n", " trans_dict = {}\n", " if not backend.configuration().simulator:\n", " trans_dict = {'layout_method': 'sabre', 'routing_method': 'sabre'}\n", " trans_circs = transpile(full_circs, backend, optimization_level=3, **trans_dict)\n", " \n", " # If using measurement mitigation we need to find out which physical qubits our transpiled\n", " # circuits actually measure, construct a mitigation object targeting our backend, and\n", " # finally calibrate our mitgation by running calibration circuits on the backend.\n", " if use_measurement_mitigation:\n", " maps = mthree.utils.final_measurement_mapping(trans_circs)\n", " mit = mthree.M3Mitigation(backend)\n", " mit.cals_from_system(maps)\n", " \n", " # Here we define a callback function that will stream the optimizer parameter vector\n", " # back to the user after each iteration. This uses the `user_messenger` object.\n", " # Here we convert to a list so that the return is user readable locally, but\n", " # this is not required.\n", " def callback(xk):\n", " user_messenger.publish(list(xk))\n", " \n", " # This is the primary VQE function executed by the optimizer. This function takes the \n", " # parameter vector as input and returns the energy evaluated using an ansatz circuit\n", " # bound with those parameters.\n", " def vqe_func(params):\n", " # Attach (bind) parameters in params vector to the transpiled circuits.\n", " bound_circs = [circ.bind_parameters(params) for circ in trans_circs]\n", " \n", " # Submit the job and get the resultant counts back\n", " counts = backend.run(bound_circs, shots=shots).result().get_counts()\n", " \n", " # If using measurement mitigation apply the correction and\n", " # compute expectation values from the resultant quasiprobabilities\n", " # using the measurement strings.\n", " if use_measurement_mitigation:\n", " quasi_collection = mit.apply_correction(counts, maps)\n", " expvals = quasi_collection.expval(meas_strings)\n", " # If not doing any mitigation just compute expectation values\n", " # from the raw counts using the measurement strings.\n", " # Since Qiskit does not have such functionality we use the convenence\n", " # function from the mthree mitigation module.\n", " else:\n", " expvals = mthree.utils.expval(counts, meas_strings)\n", " \n", " # The energy is computed by simply taking the product of the coefficients\n", " # and the computed expectation values and summing them. Here we also\n", " # take just the real part as the coefficients can possibly be complex,\n", " # but the energy (eigenvalue) of a Hamiltonian is always real.\n", " energy = np.sum(coeffs*expvals).real\n", " return energy\n", " \n", " # Here is where we actually perform the computation. We begin by seeing what\n", " # optimization routine the user has requested, eg. SPSA verses SciPy ones,\n", " # and dispatch to the correct optimizer. The selected optimizer starts at\n", " # x0 and calls 'vqe_func' everytime the optimizer needs to evaluate the cost\n", " # function. The result is returned as a SciPy OptimizerResult object.\n", " # Additionally, after every iteration, we use the 'callback' function to\n", " # publish the interm results back to the user. This is important to do\n", " # so that if the Program terminates unexpectedly, the user can start where they\n", " # left off.\n", " \n", " # Since SPSA is not in SciPy need if statement\n", " if optimizer == 'SPSA':\n", " res = fmin_spsa(vqe_func, x0, args=(), **optimizer_config,\n", " callback=callback)\n", " # All other SciPy optimizers here\n", " else:\n", " res = opt.minimize(vqe_func, x0, method=optimizer,\n", " options=optimizer_config, callback=callback)\n", " # Return result. OptimizeResult is a subclass of dict.\n", " return res" ] }, { "cell_type": "markdown", "id": "8ab3432a", "metadata": {}, "source": [ "## Local testing\n", "\n", "
© Copyright IBM 2017, 2021.
This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.