Manipulating and measuring observables¶

This notebook introduces the Observable class that allows to describe, manipulate and sample observables over quantum states produced by circuits.

Defining a new observable¶

We will take as example a simple observable that counts the number of ones in a quantum state over 5 qubits.

This observable can be written as:

$$ O = \Sigma_i (1 - \sigma_z^i)/2 $$

An observable is initialized with the number of qubits it acts on:

In [1]:
from qat.core import Observable, Term
nbqbits = 5
one_count = Observable(nbqbits)

New Pauli terms can be added to the observable.

First, we need to write our observable $O$ as a sum of weighted Pauli operators:

$$ O = N/2 - \Sigma_i \frac{1}{2}\sigma_z^i $$

In [2]:
# The sigma Z terms:
for i in range(nbqbits):
    one_count.add_term(Term(-0.5, "Z", [i]))
# And the constant term:
one_count.constant_coeff += nbqbits/2

We can print our observable to check if it is correct

In [3]:
print(one_count)
2.5 * I^5 +
-0.5 * (Z|[0]) +
-0.5 * (Z|[1]) +
-0.5 * (Z|[2]) +
-0.5 * (Z|[3]) +
-0.5 * (Z|[4])

An equivalent alternative to set up the same observable would be the following one-liner:

In [4]:
one_count2 = Observable(nbqbits,
                        pauli_terms=[Term(-0.5, "Z", [i]) for i in range(nbqbits)],
                        constant_coeff=nbqbits/2)
print(one_count2)
2.5 * I^5 +
-0.5 * (Z|[0]) +
-0.5 * (Z|[1]) +
-0.5 * (Z|[2]) +
-0.5 * (Z|[3]) +
-0.5 * (Z|[4])

Sampling an observable over the final state of a circuit¶

Let us build a simple circuit and approximate the expectation of our observable $\hat{O}$ over its final state $|\psi\rangle$, $\langle \psi | \hat{O} |\psi\rangle$:

In [5]:
from qat.lang.AQASM import Program, X, CNOT, RX

prog_2_ones = Program()
qbits = prog_2_ones.qalloc(nbqbits)
prog_2_ones.apply(X, qbits[0])
prog_2_ones.apply(CNOT, qbits[0], qbits[2])
circ_2_ones = prog_2_ones.to_circ()

from qat.qpus import LinAlg
qpu = LinAlg()
job = circ_2_ones.to_job("OBS", observable=one_count, nbshots=30)
print("Number of ones:", qpu.submit(job).value)
Number of ones: 2.0

Now with a less obvious circuit:

In [6]:
prog = Program()
qbits = prog.qalloc(5)
for i, qb in enumerate(qbits):
    prog.apply(RX(0.324 * i), qb)
circ = prog.to_circ()
job = circ.to_job("OBS", observable=one_count, nbshots=30)
print("Number of ones:", qpu.submit(job).value)
Number of ones: 0.7

Of course, we can reduce the deviation of this result by increasing the number of samples:

In [7]:
job = circ.to_job("OBS", observable=one_count, nbshots=1000)
print("Number of ones:", qpu.submit(job).value)
Number of ones: 0.696

Or even compute the exact value of the observable using an "infinite" number of shots

In [8]:
job = circ.to_job("OBS", observable=one_count)
print("Exact number of ones:", qpu.submit(job).value)
Exact number of ones: 0.7098691594779745

Operations on observables¶

Addition¶

In [9]:
obs_sum = one_count + one_count2
print(obs_sum)
5.0 * I^5 +
-1.0 * (Z|[0]) +
-1.0 * (Z|[1]) +
-1.0 * (Z|[2]) +
-1.0 * (Z|[3]) +
-1.0 * (Z|[4])

Multiplication by a scalar¶

In [10]:
obs_mult = one_count * 4.5
print(obs_mult)
11.25 * I^5 +
-2.25 * (Z|[0]) +
-2.25 * (Z|[1]) +
-2.25 * (Z|[2]) +
-2.25 * (Z|[3]) +
-2.25 * (Z|[4])

Multiplication by another observable¶

In [11]:
obs_mult2 = one_count * Observable(5, pauli_terms=[Term(1, "Y", [0])])

print(obs_mult2)
2.5 * (Y|[0]) +
0.5j * (X|[0]) +
-0.5 * (YZ|[0, 1]) +
-0.5 * (YZ|[0, 2]) +
-0.5 * (YZ|[0, 3]) +
-0.5 * (YZ|[0, 4])
In [ ]: