{
"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
}