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:
Table of contents of this notebook
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 and the Sphinx Documentation
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:
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)
"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).
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.
help(circ.to_job)
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):
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)
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.
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)
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.
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 ?
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)
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 and this notebook.
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)
from qat.core.simutil import wavefunction
qpu = PyLinalg()
wf = wavefunction(circ, qpu)
print(wf) # simple and efficient.