Observables#

The mitiq.Observable class is a way to represent an observable as a linear combination of Pauli strings. This class can be used to compute expectation values which are mitigated by techniques in Mitiq.

from mitiq import Executor, Observable, PauliString

Creating observables#

To create an observable, specify the mitiq.PauliStrings that form the observable.

pauli1 = PauliString("ZZ", coeff=-1.21)
pauli2 = PauliString("X", support=(1,))
pauli3 = PauliString("ZX", coeff=3.2)

obs = Observable(pauli1, pauli2, pauli3)
print(obs)
(-1.21+0j)*Z(q(0))*Z(q(1)) + X(q(1)) + (3.2+0j)*Z(q(0))*X(q(1))

Basic properties and operations#

See the Observables support and number of qubits as follows:

print(f"Observable acts (non-trivially) on {obs.nqubits} qubit(s) indexed {obs.qubit_indices}.")
Observable acts (non-trivially) on 2 qubit(s) indexed [0, 1].

The PauliStrings of an observable are split into groups which can be measured simultaneously via single-qubit basis rotations and measurements.

print(f"Observable has {obs.nterms} `PauliString`(s) partitioned into {obs.ngroups} group(s).", end="\n\n")

for i, group in enumerate(obs.groups, start=1):
    print(f"Group {i}:", group)
Observable has 3 `PauliString`(s) partitioned into 2 group(s).

Group 1: X(q(1)) + (3.2+0j)*Z(q(0))*X(q(1))
Group 2: (-1.21+0j)*Z(q(0))*Z(q(1))

You can (re-)partition the groups by calling Observable.partition.

obs.partition(seed=0)

Partitioning methods are generally randomized algorithms. For deterministic behavior, supply a seed. You can specify the groups manually as follows.

from mitiq.observable.pauli import PauliStringCollection

group1 = PauliStringCollection(pauli1)
group2 = PauliStringCollection(pauli2, pauli3)

obs = Observable.from_pauli_string_collections(group1, group2)

To see the (potentially very large) matrix representation of the observable:

obs.matrix()
array([[-1.21+0.j,  4.2 +0.j,  0.  +0.j,  0.  +0.j],
       [ 4.2 +0.j,  1.21+0.j,  0.  +0.j,  0.  +0.j],
       [ 0.  +0.j,  0.  +0.j,  1.21+0.j, -2.2 +0.j],
       [ 0.  +0.j,  0.  +0.j, -2.2 +0.j, -1.21+0.j]], dtype=complex64)

You can explicitly specify the qubits to include in the matrix as follows.

obs.matrix(qubit_indices=[0, 1, 2])
array([[-1.21+0.j,  0.  +0.j,  4.2 +0.j,  0.  +0.j,  0.  +0.j,  0.  +0.j,
         0.  +0.j,  0.  +0.j],
       [ 0.  +0.j, -1.21+0.j,  0.  +0.j,  4.2 +0.j,  0.  +0.j,  0.  +0.j,
         0.  +0.j,  0.  +0.j],
       [ 4.2 +0.j,  0.  +0.j,  1.21+0.j,  0.  +0.j,  0.  +0.j,  0.  +0.j,
         0.  +0.j,  0.  +0.j],
       [ 0.  +0.j,  4.2 +0.j,  0.  +0.j,  1.21+0.j,  0.  +0.j,  0.  +0.j,
         0.  +0.j,  0.  +0.j],
       [ 0.  +0.j,  0.  +0.j,  0.  +0.j,  0.  +0.j,  1.21+0.j,  0.  +0.j,
        -2.2 +0.j,  0.  +0.j],
       [ 0.  +0.j,  0.  +0.j,  0.  +0.j,  0.  +0.j,  0.  +0.j,  1.21+0.j,
         0.  +0.j, -2.2 +0.j],
       [ 0.  +0.j,  0.  +0.j,  0.  +0.j,  0.  +0.j, -2.2 +0.j,  0.  +0.j,
        -1.21+0.j,  0.  +0.j],
       [ 0.  +0.j,  0.  +0.j,  0.  +0.j,  0.  +0.j,  0.  +0.j, -2.2 +0.j,
         0.  +0.j, -1.21+0.j]], dtype=complex64)

Identity matrices are inserted on qubits outside of the observable’s support.

Computing expectation values#

The main purpose of observables in Mitiq is computing expectation values (to perform error mitigation on). To do so, specify a state-preparation circuit as any mitiq.QPROGRAM. Here will we use Cirq.

import cirq
from mitiq.interface import mitiq_cirq
circuit = cirq.testing.random_circuit(
    cirq.LineQubit.range(obs.nqubits), n_moments=5, op_density=1, random_state=2
)
circuit
0: ───Y───T───Y───×───────
                  │
1: ───────────────×───S───

The Observable.measure_in method returns a set of circuits with single-qubit measurements to run on hardware for computing the expectation value.

circuits = obs.measure_in(circuit)

for c in circuits:
    print(c, end="\n\n")
0: ───Y───T───Y───×───────M───
                  │       │
1: ───────────────×───S───M───

0: ───Y───T───Y───×────────────────M───
                  │                │
1: ───────────────×───S───Y^-0.5───M───

To compute the expectation value, use the Observable.expectation method which an executor.

obs.expectation(circuit, execute=mitiq_cirq.sample_bitstrings)
(-1.16129150390625+0j)

Using observables in error mitigation techniques#

In error mitigation techniques, you can provide an observable to specify the expectation value to mitigate.

Warning

As note in the executor documentation, the executor must be annotated with the appropriate type hinting for the return type. Additionally, when specifying an Observable, you must ensure that the return type of the executor function is MeasurementResultLike or DensityMatrixLike.

from mitiq import zne
executor = Executor(mitiq_cirq.compute_density_matrix)

zne_value = zne.execute_with_zne(circuit, executor, obs)
print(f"ZNE value: {zne_value :g}")
ZNE value: -1.14351

If you do not provide an observable, the executor must compute and return the expectation value to mitigate.