{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Simulation of quantum circuits on the QLM: introduction

\n", "\n", "This notebook aims at introducing **how simulations are run in the QLM**. It is valid for both noisy and ideal circuit simulators.

\n", "\n", "- When simulating a quantum circuit, one can aim for different kinds of results:

\n", "\n", " - You might want to get the **full amplitude vector** back, e.g for detailed analysis and debugging of your circuit implementation.

\n", " - On the contrary, you may be interested in **strictly emulating** the behavior of a quantum computer, and getting a list of **measurement results** as a result of your computation.

\n", " - Finally, perhaps you are only interested in the average value of an **observable** at the output of your circuit. It might be useful when dealing, for instance, when dealing with hybrid variational approaches (QAOA, VQE...).

\n", " \n", "\n", "- **Table of contents of this notebook**\n", " - [Introduction of the process, with default options](#default)\n", " - [Working with a subset of qubits](#subset)\n", " - [Getting a list of measurement results](#emulation) \n", " - [Grouping measurement results by value: aggregate_data](#aggregate)\n", " - [Getting the average value of an observable](#observable)\n", " - [A little hack: directly getting the table of amplitudes as a numpy array](#wavefunction)\n", " \n", "Within this notebook, we only work with a simple Bell-state-creation quantum circuit, and simulate it with our generic simulator **PyLinalg** (based on linear algebra).

\n", "\n", "For more details about **how to write observables** see [this notebook](../basic/observables.ipynb) and the [Sphinx Documentation](../../doc/index.html)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## Overall process\n", "\n", "

A simulation is started by sending a **job** to a **qpu** (quantum processing unit) via its **submit** method. \n", "\n", "In our case, a qpu is a simulator. The job is created from a quantum circuit. Simulation options are specified at the creation of this job.

\n", "\n", "The following snippet example of the process:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from qat.lang.AQASM import Program, H, CNOT\n", "from qat.qpus import PyLinalg\n", "\n", "# qpu creation\n", "qpu = PyLinalg() \n", "\n", "# program creation and gate applications\n", "my_prog = Program()\n", "qbits = my_prog.qalloc(2)\n", "my_prog.apply(H,qbits[0])\n", "my_prog.apply(CNOT,qbits)\n", "\n", "# converting into a circuit\n", "circ = my_prog.to_circ()\n", "\n", "job = circ.to_job() # specify simulation options. Here: default.\n", "\n", "result = qpu.submit(job)\n", "\n", "for state in result:\n", " print(state)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## What happened here ?\n", "\n", "\"result\" contains **all states with non-zero amplitude**.

\n", "\n", "The job was created with default arguments. Which in particular that the \"nbshots\" argument was equal to 0 (see docstring below).\n", "\n", "- Concerning nbshots, the convention regarding the behavior depending on its value is:

\n", " - **if nbshots = 0** then the qpu returns the **best it can do**. In the case of LinAlg the best it can do is just to return the **full state distribution**. Because states are, by default, filtered with an **amplitude threshold** (see docstring below), the result here consists in the two possible outputs, for a Bell state. It might be the case, for instance with an actual quantum chip, that \"the best the qpu can do\" is to return a list of measurement result, as long as reasonably possible.

\n", " - **if nbshots > 0** then the qpu returns a **list of samples**, obtained by measuring the output probability distribution of the circuit. As we will see later, in that case, the format of the output also depends on the \"aggregate_data\" argument.

\n", " \n", "As you can see, it is also at job creation that one can specify the **subset of qubits** of interest. Apart from the number of qubits in the returned samples, as we will see, the behavior is strictly identical to the default case where all qubits are measured. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "help(circ.to_job)" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "#### We are now going to play around with all these arguments, in order to illustrate the behaviors they lead to.\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## Focusing on a subset of qubits" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "job = circ.to_job(qubits=[0]) # focusing on the first qubit. \n", "\n", "result = qpu.submit(job)\n", "\n", "for state in result:\n", " print(state) # 1-qubit states, because we focus on one qubit." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Or, equivalently, but more general (e.g when working with **several registers** of qubits):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "job = circ.to_job(qubits=[qbits[0].index]) # focusing on the first qubit. the index contains the \n", " # index of the qubit within the entire set of qubits, \n", " # composed of all registers.\n", "result = qpu.submit(job)\n", "\n", "for state in result:\n", " print(state)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## Strict emulation: getting a list of samples from the output probability distribution.\n", "\n", "By \"strict emulation\", we mean that, even though here we are using a classical simulator to process our quantum circuits, the result looks **exactly** like the **raw output** of an **actual quantum computer**." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "job = circ.to_job(nbshots=10, aggregate_data=False) # see below for aggregate_data.\n", "\n", "result = qpu.submit(job)\n", "\n", "for state in result: # 10 results are printed, as required.\n", " print(state) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## Grouping results by value: aggregate_data option (defaults to True)

\n", "\n", "When this option is active (which is the default), all samples containing a given state are \"aggregated\" together. The result object then contains one unique sample per possible output. It comes with an **empirical estimation of the probability of that output**, and an \"error\" associated to that estimation. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "job = circ.to_job(nbshots=10) # see below for aggregate_data.\n", "\n", "result = qpu.submit(job)\n", "\n", "for state in result: # 10 results are printed, as required.\n", " print(state) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Notice how the error decreases when taking a greater number of samples:** TODECIDE: to we remove this or do we implement the error estimation for linalg ?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "job = circ.to_job(nbshots=1000) # see below for aggregate_data.\n", "\n", "result = qpu.submit(job)\n", "\n", "for state in result: # 10 results are printed, as required.\n", " print(state) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## Average value of an observable\n", "\n", "Often, for instance when dealing with hybrid variational algorithms, we are actually interested in the **average value of an observable**, and not measurement values.\n", "\n", "Here, we only give a simple example of the \"ZZ\" observable, which is always equal to 1 for a bell pair. For more advanced used of observables, and details regarding their construction, we refer the reader to [Sphinx Documentation](../../doc/index.html) and [this notebook](../basic/observables.ipynb)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "from qat.core import Observable, Term\n", "\n", "obs = Observable(2., pauli_terms=[Term(1., \"ZZ\", [0, 1])])\n", "job = circ.to_job(\"OBS\", observable=obs)\n", "\n", "result = qpu.submit(job)\n", "\n", "print(\"Should be 1, as we are working with a Bell pair: \", result.value)" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "\n", "## Getting the tables of amplitude directly, as a numpy array: wavefunction" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from qat.core.simutil import wavefunction\n", "\n", "qpu = PyLinalg() \n", "\n", "wf = wavefunction(circ, qpu)\n", "\n", "print(wf) # simple and efficient." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "authors": [ "Bertrand Marchand" ], "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.12.11" } }, "nbformat": 4, "nbformat_minor": 2 }