# Executors#

Error mitigation methods can involve running many circuits. The `mitiq.Executor`

class is a tool for efficiently running many circuits and storing the results.

```
from mitiq import Executor, Observable, PauliString, QPROGRAM, QuantumResult
```

## The input function#

To instantiate an `Executor`

, provide a function which either:

Inputs a

`mitiq.QPROGRAM`

and outputs a`mitiq.QuantumResult`

.Inputs a sequence of

`mitiq.QPROGRAM`

s and outputs a sequence of`mitiq.QuantumResult`

s.

**The function must be annotated to tell Mitiq which type of QuantumResult it returns. Functions with no annotations are assumed to return floats.**

A `QPROGRAM`

is “something which a quantum computer inputs” and a `QuantumResult`

is “something which a quantum computer outputs.” The latter is canonically a bitstring for real quantum hardware, but can be other objects for testing, e.g. a density matrix.

```
print(QPROGRAM)
```

```
typing.Union[cirq.circuits.circuit.Circuit, pyquil.quil.Program, qiskit.circuit.quantumcircuit.QuantumCircuit, braket.circuits.circuit.Circuit, pennylane.tape.tape.QuantumTape, qibo.models.circuit.Circuit]
```

```
print(QuantumResult)
```

```
typing.Union[float, mitiq.typing.MeasurementResult, numpy.ndarray]
```

## Creating an `Executor`

#

The function `mitiq_cirq.compute_density_matrix`

inputs a Cirq circuit and returns a density matrix as an `np.ndarray`

.

```
import inspect
from mitiq.interface import mitiq_cirq
print(inspect.getfullargspec(mitiq_cirq.compute_density_matrix).annotations["return"])
```

```
numpy.ndarray[typing.Any, numpy.dtype[numpy.complex64]]
```

We can instantiate an `Executor`

with it as follows.

```
executor = Executor(mitiq_cirq.compute_density_matrix)
```

## Running circuits#

When first created, the executor hasn’t been called yet and has no executed circuits and no computed results in memory.

```
print("Calls to executor:", executor.calls_to_executor)
print("\nExecuted circuits:\n", *executor.executed_circuits, sep="\n")
print("\nQuantum results:\n", *executor.quantum_results, sep="\n")
```

```
Calls to executor: 0
Executed circuits:
Quantum results:
```

To run a circuit of sequence of circuits, use the `Executor.evaluate`

method.

```
import cirq
q = cirq.LineQubit(0)
circuit = cirq.Circuit(cirq.H.on(q))
obs = Observable(PauliString("Z"))
results = executor.evaluate(circuit, obs)
print("Results:", results)
```

```
Results: [0.010000020265579224]
```

The `executor`

has now been called and has results in memory. Note that `mitiq_cirq.compute_density_matrix`

simulates the circuit with noise by default, so the resulting state (density matrix) is noisy.

```
print("Calls to executor:", executor.calls_to_executor)
print("\nExecuted circuits:\n", *executor.executed_circuits, sep="\n")
print("\nQuantum results:\n", *executor.quantum_results, sep="\n")
```

```
Calls to executor: 1
Executed circuits:
0: ───H───
Quantum results:
[[0.505 +0.j 0.49749368+0.j]
[0.49749368+0.j 0.49499997+0.j]]
```

The interface for running a sequence of circuits is the same.

```
circuits = [cirq.Circuit(pauli.on(q)) for pauli in (cirq.X, cirq.Y, cirq.Z)]
results = executor.evaluate(circuits, obs)
print("Results:", results)
```

```
Results: [-0.9800000190734863, -0.9800000190734863, 1.0]
```

In addition to the results of running these circuits we have the full history.

```
print("Calls to executor:", executor.calls_to_executor)
print("\nExecuted circuits:\n", *executor.executed_circuits, sep="\n")
print("\nQuantum results:\n", *executor.quantum_results, sep="\n")
```

```
Calls to executor: 4
Executed circuits:
0: ───H───
0: ───X───
0: ───Y───
0: ───Z───
Quantum results:
[[0.505 +0.j 0.49749368+0.j]
[0.49749368+0.j 0.49499997+0.j]]
[[0.01+0.j 0. +0.j]
[0. +0.j 0.99+0.j]]
[[0.01+0.j 0. +0.j]
[0. +0.j 0.99+0.j]]
[[1.+0.j 0.+0.j]
[0.+0.j 0.+0.j]]
```

### Batched execution#

Notice in the above output that the executor has been called once for each circuit it has executed. This is because `mitiq_cirq.compute_density_matrix`

inputs one circuit and outputs one `QuantumResult`

.

Several quantum computing services allow running a sequence, or “batch,” of circuits at once. This is important for error mitigation when running many circuits to speed up the computation.

To define a batched executor, annotate it with `Sequence[T]`

, `List[T]`

, `Tuple[T]`

, or `Iterable[T]`

where `T`

is a `QuantumResult`

. Here is an example:

```
from typing import List
import numpy as np
def batch_compute_density_matrix(circuits: List[cirq.Circuit]) -> List[np.ndarray]:
return [mitiq_cirq.compute_density_matrix(circuit) for circuit in circuits]
batched_executor = Executor(batch_compute_density_matrix, max_batch_size=10)
```

You can check if Mitiq detected the ability to batch as follows.

```
batched_executor.can_batch
```

```
True
```

Now when running a batch of circuits, the executor will be called as few times as possible.

```
circuits = [cirq.Circuit(pauli.on(q)) for pauli in (cirq.X, cirq.Y, cirq.Z)]
results = batched_executor.evaluate(circuits, obs)
print("Results:", results)
print("\nCalls to executor:", batched_executor.calls_to_executor)
print("\nExecuted circuits:\n", *batched_executor.executed_circuits, sep="\n")
print("\nQuantum results:\n", *batched_executor.quantum_results, sep="\n")
```

```
Results: [-0.9800000190734863, -0.9800000190734863, 1.0]
Calls to executor: 1
Executed circuits:
0: ───X───
0: ───Y───
0: ───Z───
Quantum results:
[[0.01+0.j 0. +0.j]
[0. +0.j 0.99+0.j]]
[[0.01+0.j 0. +0.j]
[0. +0.j 0.99+0.j]]
[[1.+0.j 0.+0.j]
[0.+0.j 0.+0.j]]
```

## Using `Executor`

s in error mitigation techniques#

You can provide a function or an `Executor`

to the `executor`

argument of error mitigation techniques, but **providing an Executor is strongly recommended** for seeing the history of results.

```
from mitiq import zne
```

```
batched_executor = Executor(batch_compute_density_matrix, max_batch_size=10)
zne_value = zne.execute_with_zne(
cirq.Circuit(cirq.H.on(q) for _ in range(6)),
executor=batched_executor,
observable=obs
)
print(f"ZNE value: {zne_value :g}")
```

```
ZNE value: 0.999972
```

```
print("Calls to executor:", batched_executor.calls_to_executor)
print("\nExecuted circuits:\n", *batched_executor.executed_circuits, sep="\n")
print("\nQuantum results:\n", *batched_executor.quantum_results, sep="\n")
```

```
Calls to executor: 1
Executed circuits:
0: ───H───H───H───H───H───H───
0: ───H───H───H───H───H───H───H───H───H───H───H───H───
0: ───H───H───H───H───H───H───H───H───H───H───H───H───H───H───H───H───H───H───
Quantum results:
[[0.992667 +0.j 0.01470262+0.j]
[0.01470265+0.j 0.00733284+0.j]]
[[0.9856578 +0.j 0.02875494+0.j]
[0.02875516+0.j 0.01434145+0.j]]
[[0.97895914+0.j 0.0421859 +0.j]
[0.04218622+0.j 0.02104016+0.j]]
```

## Defining an `Executor`

that returns measurement outcomes (bitstrings)#

In the previous examples we have shown executors that return the density matrix of the final state. This is possible only for classical simulations.
The typical result of a real quantum computation is instead a list of bitstrings corresponding to the (“0” or “1”) outcomes obtained when measuring each qubit in the computational basis.
In Mitiq this type of quantum backend is captured by an `Executor`

that returns a `MeasurementResult`

object.

For example, here is an example of a Cirq executor function that returns raw measurement outcomes:

```
from mitiq import MeasurementResult
def noisy_sampler(circuit, noise_level=0.1, shots=1000) -> MeasurementResult:
circuit_to_run = circuit.with_noise(cirq.depolarize(noise_level))
simulator = cirq.DensityMatrixSimulator()
result = simulator.run(circuit_to_run, repetitions=shots)
bitstrings = np.column_stack(list(result.measurements.values()))
qubit_indices = tuple(
int(q[2:-1]) # Extract index from "q(index)" string
for k in result.measurements.keys()
for q in k.split(",")
)
return MeasurementResult(bitstrings, qubit_indices)
```

```
# Circuit with measurements to test the noisy_sampler function
circuit_with_measurements = circuit.copy()
circuit_with_measurements.append(cirq.measure(*circuit.all_qubits()))
print("Circuit to execute:", circuit_with_measurements)
noisy_sampler(circuit_with_measurements)
```

```
Circuit to execute: 0: ───H───M───
```

```
MeasurementResult: {'nqubits': 1, 'qubit_indices': (0,), 'shots': 1000, 'counts': {'0': 492, '1': 508}}
```

The rest of the Mitiq workflow is the same as in the case of a density matrix executor. For example:

```
executor = Executor(noisy_sampler)
obs = Observable(PauliString("X"))
results = executor.evaluate(circuit, obs)
print("Results:", results)
print("Calls to executor:", executor.calls_to_executor)
print("\nExecuted circuits:\n", *executor.executed_circuits, sep="\n")
print("\nQuantum results:\n", *executor.quantum_results, sep="\n")
```

```
Results: [(0.772+0j)]
Calls to executor: 1
Executed circuits:
0: ───H───Y^-0.5───M───
Quantum results:
MeasurementResult: {'nqubits': 1, 'qubit_indices': (0,), 'shots': 1000, 'counts': {'0': 886, '1': 114}}
```