# 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.PauliString`s 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 `Observable`s 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 `PauliString`s 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.1939306640624998+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}")
```
```ZNE value: -1.21395
```

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