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 or OBS), 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:

\[O = X_0 \otimes Z_1\]

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: