Back-end Plug-ins: Executor Examples

Mitiq uses executor functions to abstract different backends. Executors always accept a quantum program, sometimes accept other arguments, and always return an expectation value as a float. If your quantum programming interface of choice can be used to make a Python function with this type, then it can be used with mitiq.

These example executors as especially flexible as they accept an arbitrary observable. You can instead hardcode your choice of observable in any way you like. All that matters from mitiq’s perspective is that your executor accepts a quantum program and returns a float.

Cirq Executors

This section includes noisy and noiseless simulator executor examples using cirq.

Cirq: Wavefunction Simulation

This executor can be used for noiseless simulation. Note that this executor can be wrapped using partial function application to be used in mitiq.

import numpy as np
from cirq import Circuit

def wvf_sim(circ: Circuit, obs: np.ndarray) -> float:
    """Simulates noiseless wavefunction evolution and returns the
    expectation value of some observable.

    Args:
        circ: The input Cirq circuit.
        obs: The observable to measure as a NumPy array.

    Returns:
        The expectation value of obs as a float.
    """
    final_wvf = circ.final_wavefunction()
    return np.real(final_wvf.conj().T @ obs @ final_wvf)

Cirq: Wavefunction Simulation with Sampling

We can add in functionality that takes into account some finite number of samples (aka shots). Here we will use cirq’s PauliString methods to construct our observable. You can read more about these methods in the cirq documentation here.

def wvf_sampling_sim(circ: Circuit, obs: cirq.PauliString, shots: int) -> float:
    """Simulates noiseless wavefunction evolution and returns the
    expectation value of a PauliString observable.

    Args:
        circ: The input Cirq circuit.
        obs: The observable to measure as a cirq.PauliString.
        shots: The number of measurements.

    Returns:
        The expectation value of obs as a float.
    """

    # Do the sampling
    psum = cirq.PauliSumCollector(circ, obs, samples_per_term=shots)
    psum.collect(sampler=cirq.Simulator())

    # Return the expectation value
    return psum.estimated_energy()

Cirq: Density-matrix Simulation with Depolarizing Noise

This executor can be used for noisy depolarizing simulation.

import numpy as np
from cirq import Circuit, depolarize
from cirq import DensityMatrixSimulator

SIMULATOR = DensityMatrixSimulator()

def noisy_sim(circ: Circuit, obs: np.ndarray, noise: float) -> float:
    """Simulates a circuit with depolarizing noise at level noise.

    Args:
        circ: The input Cirq circuit.
        obs: The observable to measure as a NumPy array.
        noise: The depolarizing noise as a float, i.e. 0.001 is 0.1% noise.

    Returns:
        The expectation value of obs as a float.
    """
    circuit = circ.with_noise(depolarize(p=noise))
    rho = SIMULATOR.simulate(circuit).final_density_matrix
    expectation = np.real(np.trace(rho @ obs))
    return expectation

Other noise models can be used by substituting the depolarize channel with any other channel available in cirq, for example cirq.amplitude_damp. More details can be found in the cirq noise documentation

Cirq: Density-matrix Simulation with Depolarizing Noise and Sampling

You can also include both noise models and finite sampling in your executor.

import numpy as np
from cirq import Circuit, depolarize
from cirq import DensityMatrixSimulator

SIMULATOR = DensityMatrixSimulator()

def noisy_sample_sim(circ: Circuit, obs: cirq.PauliString, noise: float, shots: int) -> float:
    """Simulates a circuit with depolarizing noise at level noise.

    Args:
        circ: The input Cirq circuit.
        obs: The observable to measure as a NumPy array.
        noise: The depolarizing noise strength as a float, i.e. 0.001 is 0.1%.
        shots: The number of measurements.

    Returns:
        The expectation value of obs as a float.
    """
    # add the noise
    noisy = circ.with_noise(depolarize(p=noise))

    # Do the sampling
    psum = cirq.PauliSumCollector(noisy, obs, samples_per_term=shots)
    psum.collect(sampler=cirq.DensityMatrixSimulator())

    # Return the expectation value
    return psum.estimated_energy()

Qiskit Executors

This section includes noisy and noiseless simulator executor examples using qiskit.

Qiskit: Wavefunction Simulation

This executor can be used for noiseless simulation. Note that this executor can be wrapped using partial function application to be used in mitiq.

import numpy as np
import qiskit
from qiskit import QuantumCircuit

wvf_simulator = qiskit.Aer.get_backend('statevector_simulator')

def qs_wvf_sim(circ: QuantumCircuit, obs: np.ndarray) -> float:
    """Simulates noiseless wavefunction evolution and returns the
    expectation value of some observable.

    Args:
        circ: The input Qiskit circuit.
        obs: The observable to measure as a NumPy array.

    Returns:
        The expectation value of obs as a float.
    """
    result = qiskit.execute(circ, wvf_simulator).result()
    final_wvf = result.get_statevector()
    return np.real(final_wvf.conj().T @ obs @ final_wvf)

Qiskit: Wavefunction Simulation with Sampling

The above executor can be modified to still perform exact wavefunction simulation, but to also include finite sampling of measurements. Note that this executor can be wrapped using partial function application to be used in mitiq.

Note that this executor implementation measures arbitrary observables by using a change of basis into the computational basis. More information about the math behind how this example is available here.

import copy

QISKIT_SIMULATOR = qiskit.Aer.get_backend("qasm_simulator")

def qs_wvf_sampling_sim(circ: QuantumCircuit, obs: np.ndarray, shots: int) -> float:
    """Simulates the evolution of the circuit and returns
    the expectation value of the observable.

    Args:
        circ: The input Qiskit circuit.
        obs: The observable to measure as a NumPy array.
        shots: The number of measurements.

    Returns:
        The expectation value of obs as a float.

    """
    if len(circ.clbits) > 0:
        raise ValueError("This executor only works on programs with no classical bits.")

    circ = copy.deepcopy(circ)
    # we need to modify the circuit to measure obs in its eigenbasis
    # we do this by appending a unitary operation
    eigvals, U = np.linalg.eigh(obs) # obtains a U s.t. obs = U diag(eigvals) U^dag
    circ.unitary(np.linalg.inv(U), qubits=range(circ.n_qubits))

    circ.measure_all()

    # execution of the experiment
    job = qiskit.execute(
        circ,
        backend=QISKIT_SIMULATOR,
        # we want all gates to be actually applied,
        # so we skip any circuit optimization
        optimization_level=0,
        shots=shots
    )
    results = job.result()
    counts = results.get_counts()
    expectation = 0
    # classical bits are included in bitstrings with a space
    # this is what breaks if you have them
    for bitstring, count in counts.items():
        expectation += eigvals[int(bitstring, 2)] * count / shots
    return expectation

Qiskit: Density-matrix Simulation with Depolarizing Noise

TODO

Qiskit: Density-matrix Simulation with Depolarizing Noise and Sampling

This executor can be used for noisy depolarizing simulation.

import qiskit
from qiskit import QuantumCircuit
import numpy as np
import copy

# Noise simulation packages
from qiskit.providers.aer.noise import NoiseModel
from qiskit.providers.aer.noise.errors.standard_errors import depolarizing_error

QISKIT_SIMULATOR = qiskit.Aer.get_backend("qasm_simulator")

def qs_noisy_sampling_sim(circ: QuantumCircuit, obs: np.ndarray, noise: float, shots: int) -> float:
    """Simulates the evolution of the noisy circuit and returns
    the expectation value of the observable.

    Args:
        circ: The input Cirq circuit.
        obs: The observable to measure as a NumPy array.
        noise: The depolarizing noise strength as a float, i.e. 0.001 is 0.1%.
        shots: The number of measurements.

    Returns:
        The expectation value of obs as a float.
    """
    if len(circ.clbits) > 0:
        raise ValueError("This executor only works on programs with no classical bits.")

    circ = copy.deepcopy(circ)
    # we need to modify the circuit to measure obs in its eigenbasis
    # we do this by appending a unitary operation
    eigvals, U = np.linalg.eigh(obs) # obtains a U s.t. obs = U diag(eigvals) U^dag
    circ.unitary(np.linalg.inv(U), qubits=range(circ.n_qubits))

    circ.measure_all()

    # initialize a qiskit noise model
    noise_model = NoiseModel()

    # we assume the same depolarizing error for each
    # gate of the standard IBM basis
    noise_model.add_all_qubit_quantum_error(depolarizing_error(noise, 1), ["u1", "u2", "u3"])
    noise_model.add_all_qubit_quantum_error(depolarizing_error(noise, 2), ["cx"])

    # execution of the experiment
    job = qiskit.execute(
        circ,
        backend=QISKIT_SIMULATOR,
        backend_options={'method':'density_matrix'},
        noise_model=noise_model,
        # we want all gates to be actually applied,
        # so we skip any circuit optimization
        basis_gates=noise_model.basis_gates,
        optimization_level=0,
        shots=shots,
    )
    results = job.result()
    counts = results.get_counts()
    expectation = 0
    # classical bits are included in bitstrings with a space
    # this is what breaks if you have them
    for bitstring, count in counts.items():
        expectation += eigvals[int(bitstring, 2)] * count / shots
    return expectation

Other noise models can be defined using any functionality available in qiskit. More details can be found in the qiskit simulator documentation

Qiskit: Hardware

An example of an executor that runs on IBMQ hardware is given here.