---
jupytext:
  text_representation:
    extension: .myst
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.12.0
kernelspec:
  display_name: Python 3 (ipykernel)
  language: python
  name: python3
---

# 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.

```{code-cell} ipython3
from mitiq import Executor, Observable, PauliString, QPROGRAM, QuantumResult
```

## The input function

+++

To instantiate an `Executor`, provide a function which either:

1. Inputs a `mitiq.QPROGRAM` and outputs a `mitiq.QuantumResult`.
2. Inputs a sequence of `mitiq.QPROGRAM`s and outputs a sequence of `mitiq.QuantumResult`s.

**The function must be [annotated](https://www.python.org/dev/peps/pep-3107/) to tell Mitiq which type of `QuantumResult` it returns. Functions with no annotations are assumed to return `float`s.**

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.

```{code-cell} ipython3
print(QPROGRAM)
```

```{code-cell} ipython3
print(QuantumResult)
```

## Creating an `Executor`

+++

The function `mitiq_cirq.compute_density_matrix` inputs a Cirq circuit and returns a density matrix as an `np.ndarray`.

```{code-cell} ipython3
import inspect

from mitiq.interface import mitiq_cirq

print(inspect.getfullargspec(mitiq_cirq.compute_density_matrix).annotations["return"])
```

We can instantiate an `Executor` with it as follows.

```{code-cell} ipython3
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.

```{code-cell} ipython3
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")
```

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

```{code-cell} ipython3
import cirq

q = cirq.GridQubit(0, 0)
circuit = cirq.Circuit(cirq.H.on(q))

obs = Observable(PauliString("Z"))

results = executor.evaluate(circuit, obs)
print("Results:", results)
```

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.

```{code-cell} ipython3
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")
```

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

```{code-cell} ipython3
circuits = [cirq.Circuit(pauli.on(q)) for pauli in (cirq.X, cirq.Y, cirq.Z)]

results = executor.evaluate(circuits, obs)
print("Results:", results)
```

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

```{code-cell} ipython3
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")
```

### 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:

```{code-cell} ipython3
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.

```{code-cell} ipython3
batched_executor.can_batch
```

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

```{code-cell} ipython3
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")
```

## 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.

```{code-cell} ipython3
from mitiq import zne
```

```{code-cell} ipython3
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}")
```

```{code-cell} ipython3
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")
```
