Executing quantum circuits
The workflow of a quantum computer, henceforth called a “Quantum Processing Unit” (QPU), is the following: its state (register of qubits) is initialized, a series of quantum gates (a quantum circuit \(\mathcal{C}\)) is operated on the register, and a measurement is made on the final state \(|\psi\rangle\) of the QPU.
QLM provides classical emulators of QPUs that allow to simulate the execution of this workflow.
In this page, you will learn how to use the various objects required to describe this workflow and to run a QLM QPU.
Describing a quantum job
The computational jobs that are fed to a QPU primarily consist of two main components:
the quantum circuit to be executed on the qubit register. You have learned how to create such a circuit here.
the final measurement to be carried out on the final state of the register, i.e on the wavefunction \(|\psi\rangle\) that has been prepared by the quantum circuit.
Two types of final measurements are commonly used in quantum algorithms:
“Sample”: Measuring the projection of the wavefunction on the computational basis (formally speaking, this means measuring qubits “along the Z axis”). For instance, if the final state is \(|\psi\rangle=(|00\rangle+|11\rangle)/\sqrt{2}\), measuring “in the computational basis” will yield the bitstring “00” in 50% (\(1/\sqrt{2}^2\)) of cases, and “11” the rest of the time. Because of the statistical nature of the measurement, many repeated measurements on the computational basis, yielding many different bitstrings or “samples”, are necessary to get an accurate estimate of the frequencies of each computational basis state (“00”: 50%, “01”: 0%, “10”: 0%, “11”: 50% in our case). We will call “nbshots” the number of such repetitions (with a classical emulator, one can in general emulate an infinite number of shots since one has access to the exact frequencies). Of course, one may decide to measure only a subset of qubits.
“Observable”: Measuring the value of a quantum mechanical observable \(O\) in the final state. On average, such a value is given by the formula \(\langle O \rangle = \langle \psi | O |\psi \rangle\). While this average value can directly be computed using a classical computer, on an actual QPU, only a limited set of observables can be measured. In practice, most QPUs provide only “Z-axis” measurements, i.e, as described above, the possibility to measure the projection of the wavefunction on computational basis states. In order to perform the estimation of the average value of an observable, one thus needs to convert the computation of \(\langle O \rangle\) into a series of Z axis measurements (with possible modifications of the circuit), estimate bitstrings frequencies as described above (with a given number of shots or repetitions), and combine those subresults back into an estimate of \(\langle O \rangle\).
Let us emphasize the fact that while the first final processing type is native to most actual QPUs, the same does not necessarily holds for the “OBSERVABLE” processing type. In the absence of a native support, the ObservableSplitter plugin of the QLM can enhance the QPU with an observable-processing capacity.
In QLM, quantum jobs describing these tasks are implemented by a qat.core.Job
object, that contains
the circuit \(\mathcal{C}\) to be executed
the type of the final processing (
SAMPLE
orOBS
), with the necessary parameters (which qubits to be measured or which observable to evaluate, respectively)the number of allowed repetitions (shots)
Note that by default, the processing type is SAMPLE
, with all qubits being measured, and an infinite number of shots (i.e no statistical uncertainty).
A full example on the Bell state circuit
Here, we exemplify the execution of a simple Bell circuit with the different modes described above.
SAMPLE mode
Infinite number of shots, all qubits
We take the same example as the one we examined in the Getting Started page for an illustration of this case:
from qat.lang.AQASM import Program, H, CNOT
from qat.qpus import get_default_qpu
# Create a circuit
qprog = Program()
qbits = qprog.qalloc(2)
H(qbits[0])
CNOT(qbits[0], qbits[1])
circuit = qprog.to_circ()
# Create a job
job = circuit.to_job() # no parameters, equivalent to "nbshots=0"
# Instantiate a QPU (simulator)
qpu = get_default_qpu()
# Execute
result = qpu.submit(job)
for sample in result:
print("State %s: probability %s, amplitude %s" % (sample.state, sample.probability, sample.amplitude))
State |00>: probability 0.4999999999999999, amplitude (0.7071067811865475+0j)
State |11>: probability 0.4999999999999999, amplitude (0.7071067811865475+0j)
You can notice that a Job
is created from a circuit using the circuit’s to_job()
method.
This job is then fed to the QPU’s submit
method, which returns a result
object.
In SAMPLE mode, this result can be iterated on, yielding the various “samples”, with each sample
corresponding to a given bitstring (state
) and its frequency of appearance (probability
) upon
conducting a Z measurement over the final state \(|\psi\rangle\). States with zero probability
are not listed (one can set a threshold amp_threshold
to filter out states below a certain probability amplitude).
Here, we also print the probability amplitude (amplitude
) corresponding to each computational basis state.
Let us stress that this piece of information is in general not available from an actual QPU, but merely
from some classical simulators.
Finite number of shots, all qubits
Let us switch to a finite nbshots
:
from qat.lang.AQASM import Program, H, CNOT
from qat.qpus import get_default_qpu
# Create a circuit
qprog = Program()
qbits = qprog.qalloc(2)
H(qbits[0])
CNOT(qbits[0], qbits[1])
circuit = qprog.to_circ()
# Create a job
job = circuit.to_job(nbshots=100)
# Execute
result = get_default_qpu().submit(job)
for sample in result:
print("State %s: probability %s +/- %s" % (sample.state, sample.probability, sample.err))
State |00>: probability 0.49 +/- 0.05024183937956914
State |11>: probability 0.51 +/- 0.05024183937956914
Notice how the estimated probability of the states differs from the ideal one due to “shot noise”. The err
field of the sample
object contains the standard error of the mean on the frequency of appearance.
It decreases as \(1/\sqrt{n_\mathrm{shots}}\).
Infinite number of shots, only one qubit
Here, we decide to measure only the second qubit:
from qat.lang.AQASM import Program, H, CNOT
from qat.qpus import get_default_qpu
# Create a circuit
qprog = Program()
qbits = qprog.qalloc(2)
H(qbits[0])
CNOT(qbits[0], qbits[1])
circuit = qprog.to_circ()
# Create a job
job = circuit.to_job(nbshots=0, qubits=[1])
# Execute
result = get_default_qpu().submit(job)
for sample in result:
print("State %s: probability %s +/- %s" % (sample.state, sample.probability, sample.err))
State |0>: probability 0.4999999999999999 +/- None
State |1>: probability 0.4999999999999999 +/- None
As expected, the probability of measuring “0” on the second qubit is the same as the
probability of measuring “1”. Here, err
is None
because we took an infinite number
of shots.
OBSERVABLE mode
Let us now turn to the second type of final processing, namely the “OBSERVABLE” mode. Our goal is to compute the average value of the following observable:
for the Bell state \(|\psi\rangle=(|00\rangle+|11\rangle)/\sqrt{2}\). A straightforward computation yields \(\langle O \rangle = 0\). Let us now perform this computation with QLM.
Infinite number of shots
Let us start with a computation devoid of any shot noise. It directly yields the expectation value \(\langle O \rangle\):
from qat.lang.AQASM import Program, H, CNOT
from qat.qpus import get_default_qpu
# Create a circuit
qprog = Program()
qbits = qprog.qalloc(2)
H(qbits[0])
CNOT(qbits[0], qbits[1])
circuit = qprog.to_circ()
# Create an observable
from qat.core import Observable, Term
obs = Observable(2, pauli_terms=[Term(1, "XZ", [0, 1])])
# Create a job
job = circuit.to_job(observable=obs)
# Execute
result = get_default_qpu().submit(job)
print("<O> = ", result.value)
<O> = 0.0
Finite number of shots
We now look at the effect of shot noise, i.e the fact that in practice one can only repeat the measurement a finite number of times:
from qat.lang.AQASM import Program, H, CNOT
from qat.qpus import get_default_qpu
# Create a circuit
qprog = Program()
qbits = qprog.qalloc(2)
H(qbits[0])
CNOT(qbits[0], qbits[1])
circuit = qprog.to_circ()
# Create an observable
from qat.core import Observable, Term
obs = Observable(2, pauli_terms=[Term(1, "XZ", [0, 1])])
# Create a job
for nbshots in [100, 1000, 10000]:
job = circuit.to_job(observable=obs, nbshots=nbshots)
result = get_default_qpu().submit(job)
print(f"<O> ({nbshots} shots) = {result.value} +/- {result.error}")
Traceback (most recent call last):
File "<stdin>", line 18, in <module>
File "qpu.py", line 198, in qat.core.qpu.qpu.CommonQPU.submit
File "qpu.py", line 201, in qat.core.qpu.qpu.CommonQPU.submit
File "qpu.py", line 124, in qat.core.qpu.qpu.CommonQPU._submit_batch
File "qpu.py", line 240, in qat.clinalg.qpu.CLinalg.submit_job
File "qpu.py", line 246, in qat.clinalg.qpu.CLinalg.submit_job
File "qpu.py", line 278, in qat.clinalg.qpu.CLinalg._calculate_expectation_value
qat.comm.exceptions.ttypes.QPUException: QPUException(code=2, modulename='qat.clinalg', message='Observable with nbshots > 0 not implemented. Use ObservableSplitter plugin.', file=None, line=None)
Further information
You will find a complete documentation of the objects mentioned above in the source code documentation:
job:
qat.core.Job
. See also:qat.core.Circuit.to_job()
.observable:
qat.core.Observable
andqat.core.Term
.results and samples:
qat.core.Result
andqat.core.wrappers.result.Sample
.