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:
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 $$
# 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
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:
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$:
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:
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:
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
job = circ.to_job("OBS", observable=one_count)
print("Exact number of ones:", qpu.submit(job).value)
Exact number of ones: 0.7098691594779745
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¶
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¶
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])