jupytext | kernelspec | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
(guide/executors/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
+++
To instantiate an Executor
, provide a function which either:
- Inputs a
mitiq.QPROGRAM
and outputs amitiq.QuantumResult
. - Inputs a sequence of
mitiq.QPROGRAM
s and outputs a sequence ofmitiq.QuantumResult
s.
The function must be annotated 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.
print(QPROGRAM)
print(QuantumResult)
+++
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"])
We can instantiate an Executor
with it as follows.
executor = Executor(mitiq_cirq.compute_density_matrix)
+++
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")
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)
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")
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)
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")
+++
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
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")
+++
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}")
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")
+++
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 {class}.Executor
that returns a {class}.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)
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")