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.