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: (3.2+0j)*Z(q(0))*X(q(1)) + 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]])

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]])

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.1546704101562502+0j)

Using observables in error mitigation techniques#

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

Note:

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}")
before: [array([[0.98069096+0.j, 0.        +0.j, 0.        +0.j, 0.        +0.j],
       [0.        +0.j, 0.01930895+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, 0.        +0.j, 0.        +0.j]],
      dtype=complex64), array([[0.9718479 +0.j, 0.        +0.j, 0.        +0.j, 0.        +0.j],
       [0.        +0.j, 0.02815215+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, 0.        +0.j, 0.        +0.j]],
      dtype=complex64), array([[0.9459956 +0.j, 0.        +0.j, 0.        +0.j, 0.        +0.j],
       [0.        +0.j, 0.05400433+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, 0.        +0.j, 0.        +0.j]],
      dtype=complex64)]
after:  [array([[0.98069096+0.j, 0.        +0.j, 0.        +0.j, 0.        +0.j],
       [0.        +0.j, 0.01930895+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, 0.        +0.j, 0.        +0.j]],
      dtype=complex64), array([[0.9718479 +0.j, 0.        +0.j, 0.        +0.j, 0.        +0.j],
       [0.        +0.j, 0.02815215+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, 0.        +0.j, 0.        +0.j]],
      dtype=complex64), array([[0.9459956 +0.j, 0.        +0.j, 0.        +0.j, 0.        +0.j],
       [0.        +0.j, 0.05400433+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, 0.        +0.j, 0.        +0.j]],
      dtype=complex64)]
ZNE value: -1.14351+0j

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