{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# The observable splitter plugin\n", "\n", "In the QLM API, it is possible to submit jobs that contain a quantum circuit and some observable to sample on the output quantum state. \n", "\n", "> my_job = circuit.to_job(observable=my_obs)\n", "\n", "These jobs are atomic computation tasks from the API point of view. In some cases, however, it can happen that some QPU does not natively supports observable evaluation.\n", "\n", "The `ObservableSplitter` plugin is here to fill this gap and allow any stack containing a \"sampling only\" QPU to be able to evaluate observables. The nice thing is that the algorithmic mechanics behind computing an observable using solely computational basis samples is entirely handled by the plugin, transparently for the user.\n", "\n", "## Brief overview\n", "\n", "Lets see how the plugin works:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# To write circuits\n", "from qat.lang.AQASM import *\n", "# To define an observable\n", "from qat.core import Observable, Term\n", "# our Plugin\n", "from qat.plugins import ObservableSplitter\n", "# and a QPU\n", "from qat.qpus import get_default_qpu" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Our circuit:\n", "prog = Program()\n", "qbits = prog.qalloc(2)\n", "prog.apply(H, qbits[0])\n", "prog.apply(CNOT, qbits)\n", "bell = prog.to_circ()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Our observable: it counts the parity of the quantum state\n", "obs = Observable(2, pauli_terms=[Term(-0.5, \"ZZ\", [0, 1])],\n", " constant_coeff=0.5)\n", "print(\"Observable:\\n\", obs)\n", "my_job = bell.to_job(observable=obs)\n", "# We can always use our default qpu to directly run this job:\n", "result = get_default_qpu().submit(my_job)\n", "print(\"Result:\", result.value)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is however not realistic. If our QPU were to be a proper quantum device, or maybe just another simulator, it might not be able to handle observable sampling natively.\n", "For this purpose, we can use the `ObservableSplitter` plugin, like so:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "stack = ObservableSplitter() | get_default_qpu()\n", "print(\"Result:\", stack.submit(my_job).value)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Sampling strategies\n", "\n", "The plugin comes with two distinct algorithms to sample some observable:\n", "* By default, the plugin will generate one new sampling job per term in the observable. This is what we call \"naive\" splitting. This method is interesting in the case where measuring many qubits at the end of computation implies a degradation of the quality of the results. In that case, one might want to limit the number of sampled qubits.\n", "* Another method is also available that group terms of the observable into groups of (trivially) commutating terms. The Plugin then generates a new sampling job per group of trivially commutating terms. This method generates less jobs than the previous one and should be privileged when simulating quantum circuits. The groups of commutating terms are found using a greedy graph coloring algorithms. We call this method \"coloring\"." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# This observable has 4 terms that can be grouped into 2 groups of commutating terms.\n", "obs = Observable(3, pauli_terms=[Term(1., \"ZZZ\", [0,1,2]),\n", " Term(1., \"X\", [0]), Term(1., \"X\", [1]), Term(1., \"X\", [2])])\n", "print(obs)\n", "# We will use a dummy circuit:\n", "prog = Program()\n", "qbits = prog.qalloc(3)\n", "circuit = prog.to_circ()\n", "job = circuit.to_job(observable=obs)\n", "from qat.core import Batch\n", "batch = Batch(jobs=[job])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plugin_naive = ObservableSplitter(splitting_method=\"naive\")\n", "naive_batch = plugin_naive.compile(batch, None)\n", "print(\"We need to sample\", len(naive_batch.jobs), \"circuits\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plugin_naive = ObservableSplitter(splitting_method=\"coloring\")\n", "coloring_batch = plugin_naive.compile(batch, None)\n", "print(\"We need to sample\", len(coloring_batch.jobs), \"circuits\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## [Advanced] Custom basis change\n", "\n", "In order to generate the sampling jobs, the Plugin needs to inject basis change instructions at the end of the initial circuit.\n", "\n", "For instance, if one need to sample a $X$ operator, the plugin will append a $H$ gate at the end of the circuit, and sample the corresponding qubit in the computational basis ($Z$).\n", "\n", "However, some hardware might not support $H$ gates. Luckily, the `ObservableSplitter` allow us to provide any subcircuit performing the appropriate basis change. Lets have a look at its constructor:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "help(ObservableSplitter)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The constructor requires 2 functions : `x_basis_change` and `y_basis_change`.\n", "\n", "This functions take as parameter the index of the qubit to rotate and the total number of qubits, and should return a QRoutine of arity equal to the number of qubits (this is just to encompass the most general sceneari).\n", "\n", "For instance, if our hardware does not supports Hadamard gates, one can imagine performing a sequence of $R_x(\\pi/2)Rz(\\pi/2)Rx(\\pi/2)$:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "def my_x_basis_change(index, nbqbits):\n", " rout = QRoutine()\n", " wires = rout.new_wires(nbqbits)\n", " rout.apply(RX(np.pi/2), wires[index])\n", " rout.apply(RZ(np.pi/2), wires[index])\n", " rout.apply(RX(np.pi/2), wires[index])\n", " return rout\n", "\n", "plugin_custom = ObservableSplitter(splitting_method=\"coloring\", x_basis_change=my_x_basis_change)\n", "plugin = ObservableSplitter(splitting_method=\"coloring\")\n", "default_batch = plugin.compile(batch, None)\n", "custom_batch = plugin_custom.compile(batch, None)\n", "\n", "print(\"Default plugin:\")\n", "for ind, job in enumerate(default_batch.jobs):\n", " print(\"Circuit\", ind)\n", " for op in job.circuit.iterate_simple():\n", " print(op)\n", " \n", "print(\"Custom plugin:\")\n", "for ind, job in enumerate(custom_batch.jobs):\n", " print(\"Circuit\", ind)\n", " for op in job.circuit.iterate_simple():\n", " print(op)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "authors": [ "Simon Martiel" ], "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.8" } }, "nbformat": 4, "nbformat_minor": 2 }