#!/usr/bin/env python # coding: utf-8 # # Simulation of quantum circuits on the QLM: introduction

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

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

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

# - 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.

# - 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...).

# # # - **Table of contents of this notebook** # - [Introduction of the process, with default options](#default) # - [Working with a subset of qubits](#subset) # - [Getting a list of measurement results](#emulation) # - [Grouping measurement results by value: aggregate_data](#aggregate) # - [Getting the average value of an observable](#observable) # - [A little hack: directly getting the table of amplitudes as a numpy array](#wavefunction) # # 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).

# # For more details about **how to write observables** see [this notebook](../basic/observables.ipynb) and the [Sphinx Documentation](../../doc/index.html) # # ## Overall process # #

A simulation is started by sending a **job** to a **qpu** (quantum processing unit) via its **submit** method. # # 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.

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

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

# - **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.

# - **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.

# # 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. # In[ ]: help(circ.to_job) # #### We are now going to play around with all these arguments, in order to illustrate the behaviors they lead to. # # # # ## Focusing on a subset of qubits # In[ ]: job = circ.to_job(qubits=[0]) # focusing on the first qubit. result = qpu.submit(job) for state in result: print(state) # 1-qubit states, because we focus on one qubit. # Or, equivalently, but more general (e.g when working with **several registers** of qubits): # In[ ]: job = circ.to_job(qubits=[qbits[0].index]) # focusing on the first qubit. the index contains the # index of the qubit within the entire set of qubits, # composed of all registers. result = qpu.submit(job) for state in result: print(state) # # ## Strict emulation: getting a list of samples from the output probability distribution. # # 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**. # In[ ]: job = circ.to_job(nbshots=10, aggregate_data=False) # see below for aggregate_data. result = qpu.submit(job) for state in result: # 10 results are printed, as required. print(state) # # ## Grouping results by value: aggregate_data option (defaults to True)

# # 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. # In[ ]: job = circ.to_job(nbshots=10) # see below for aggregate_data. result = qpu.submit(job) for state in result: # 10 results are printed, as required. print(state) # **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 ? # In[ ]: job = circ.to_job(nbshots=1000) # see below for aggregate_data. result = qpu.submit(job) for state in result: # 10 results are printed, as required. print(state) # # ## Average value of an observable # # Often, for instance when dealing with hybrid variational algorithms, we are actually interested in the **average value of an observable**, and not measurement values. # # 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). # In[ ]: from qat.core import Observable, Term obs = Observable(2., pauli_terms=[Term(1., "ZZ", [0, 1])]) job = circ.to_job("OBS", observable=obs) result = qpu.submit(job) print("Should be 1, as we are working with a Bell pair: ", result.value) # # ## Getting the tables of amplitude directly, as a numpy array: wavefunction # In[ ]: from qat.core.simutil import wavefunction qpu = PyLinalg() wf = wavefunction(circ, qpu) print(wf) # simple and efficient. # In[ ]: