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:

  1. Inputs a mitiq.QPROGRAM and outputs a mitiq.QuantumResult.

  2. Inputs a sequence of mitiq.QPROGRAMs and outputs a sequence of mitiq.QuantumResults.

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]
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.GridQubit(0, 0)
circuit = cirq.Circuit(cirq.H.on(q))

obs = Observable(PauliString("Z"))

results = executor.evaluate(circuit, obs)
print("Results:", results)
before: [array([[0.505     +0.j, 0.49749368+0.j],
       [0.49749368+0.j, 0.49499997+0.j]], dtype=complex64)]
after:  [array([[0.505     +0.j, 0.49749368+0.j],
       [0.49749368+0.j, 0.49499997+0.j]], dtype=complex64)]
Results: [(0.010000020265579224+0j)]

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, 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)
before: [array([[0.01+0.j, 0.  +0.j],
       [0.  +0.j, 0.99+0.j]], dtype=complex64), array([[0.01+0.j, 0.  +0.j],
       [0.  +0.j, 0.99+0.j]], dtype=complex64), array([[1.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j]], dtype=complex64)]
after:  [array([[0.01+0.j, 0.  +0.j],
       [0.  +0.j, 0.99+0.j]], dtype=complex64), array([[0.01+0.j, 0.  +0.j],
       [0.  +0.j, 0.99+0.j]], dtype=complex64), array([[1.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j]], dtype=complex64)]
Results: [(-0.980000008828938+0j), (-0.980000008828938+0j), (1+0j)]

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, 0): ───H───
(0, 0): ───X───
(0, 0): ───Y───
(0, 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")
before: [array([[0.01+0.j, 0.  +0.j],
       [0.  +0.j, 0.99+0.j]], dtype=complex64), array([[0.01+0.j, 0.  +0.j],
       [0.  +0.j, 0.99+0.j]], dtype=complex64), array([[1.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j]], dtype=complex64)]
after:  [array([[0.01+0.j, 0.  +0.j],
       [0.  +0.j, 0.99+0.j]], dtype=complex64), array([[0.01+0.j, 0.  +0.j],
       [0.  +0.j, 0.99+0.j]], dtype=complex64), array([[1.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j]], dtype=complex64)]
Results: [(-0.980000008828938+0j), (-0.980000008828938+0j), (1+0j)]

Calls to executor: 1

Executed circuits:

(0, 0): ───X───
(0, 0): ───Y───
(0, 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 Executors 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}")
before: [array([[0.992667  +0.j, 0.01470262+0.j],
       [0.01470265+0.j, 0.00733284+0.j]], dtype=complex64), array([[0.98565817+0.j, 0.02875519+0.j],
       [0.0287551 +0.j, 0.01434148+0.j]], dtype=complex64), array([[0.97895914+0.j, 0.0421859 +0.j],
       [0.04218622+0.j, 0.02104016+0.j]], dtype=complex64)]
after:  [array([[0.992667  +0.j, 0.01470262+0.j],
       [0.01470265+0.j, 0.00733284+0.j]], dtype=complex64), array([[0.98565817+0.j, 0.02875519+0.j],
       [0.0287551 +0.j, 0.01434148+0.j]], dtype=complex64), array([[0.97895914+0.j, 0.0421859 +0.j],
       [0.04218622+0.j, 0.02104016+0.j]], dtype=complex64)]
ZNE value: 0.999971+0j
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, 0): ───H───H───H───H───H───H───
(0, 0): ───H───H───H───H───H───H───H───H───H───H───H───H───
(0, 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.98565817+0.j 0.02875519+0.j]
 [0.0287551 +0.j 0.01434148+0.j]]
[[0.97895914+0.j 0.0421859 +0.j]
 [0.04218622+0.j 0.02104016+0.j]]