Source code for pytket.extensions.qiskit.backends.aer

# Copyright Quantinuum
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import itertools
import json
import warnings
from collections import defaultdict
from collections.abc import Sequence
from dataclasses import dataclass
from logging import warning
from typing import TYPE_CHECKING, Any, Optional, cast

import numpy as np
from qiskit_aer import Aer  # type: ignore
from qiskit_aer.noise import NoiseModel  # type: ignore

from pytket.architecture import Architecture, FullyConnected
from pytket.backends import Backend, CircuitNotRunError, CircuitStatus, ResultHandle
from pytket.backends.backendinfo import BackendInfo
from pytket.backends.backendresult import BackendResult
from pytket.backends.resulthandle import _ResultIdTuple
from pytket.circuit import Circuit, Node, OpType, Qubit
from pytket.passes import (
    AutoRebase,
    BasePass,
    CliffordSimp,
    CustomPass,
    DecomposeBoxes,
    FullPeepholeOptimise,
    GreedyPauliSimp,
    RemoveBarriers,
    SequencePass,
    SynthesiseTket,
)
from pytket.pauli import Pauli, QubitPauliString
from pytket.predicates import (
    ConnectivityPredicate,
    DefaultRegisterPredicate,
    GateSetPredicate,
    MaxNQubitsPredicate,
    NoBarriersPredicate,
    NoClassicalControlPredicate,
    NoFastFeedforwardPredicate,
    NoSymbolsPredicate,
    Predicate,
)
from pytket.utils import prepare_circuit
from pytket.utils.operators import QubitPauliOperator
from pytket.utils.results import KwargTypes
from qiskit import transpile  # type: ignore
from qiskit.quantum_info.operators import Pauli as qk_Pauli  # type: ignore
from qiskit.quantum_info.operators.symplectic.sparse_pauli_op import (  # type: ignore
    SparsePauliOp,
)

from .._metadata import __extension_version__
from ..qiskit_convert import (
    _gate_str_2_optype,
    tk_to_qiskit,
)
from ..result_convert import qiskit_result_to_backendresult
from .crosstalk_model import (
    CrosstalkParams,
    NoisyCircuitBuilder,
)
from .ibm_utils import _STATUS_MAP, _batch_circuits, _gen_lightsabre_transformation

if TYPE_CHECKING:
    from qiskit_aer import AerJob
    from qiskit_aer.backends.aerbackend import (  # type: ignore
        AerBackend as QiskitAerBackend,
    )


def _default_q_index(q: Qubit) -> int:
    if q.reg_name != "q" or len(q.index) != 1:
        raise ValueError("Non-default qubit register")
    return int(q.index[0])


def _tket_gate_set_from_qiskit_backend(
    qiskit_aer_backend: "QiskitAerBackend",
) -> set[OpType]:
    config = qiskit_aer_backend.configuration()
    gate_set = {
        _gate_str_2_optype[gate_str]
        for gate_str in config.basis_gates
        if gate_str in _gate_str_2_optype
    }

    gate_set.add(OpType.Barrier)

    if "unitary" in config.basis_gates:
        gate_set.add(OpType.Unitary1qBox)
        gate_set.add(OpType.Unitary2qBox)
        gate_set.add(OpType.Unitary3qBox)

    gate_set.add(OpType.Reset)
    gate_set.add(OpType.Measure)
    gate_set.add(OpType.Conditional)

    # special case mapping TK1 to U
    gate_set.add(OpType.TK1)
    return gate_set


def qiskit_aer_backend(backend_name: str) -> "QiskitAerBackend":
    """Find a qiskit backend with the given name.

    If more than one backend with the given name is available, emit a warning
    and return the first one in the list returned by `Aer.backends()`.
    """
    candidates = [b for b in Aer.backends() if b.name == backend_name]
    n_candidates = len(candidates)
    if n_candidates == 0:
        raise ValueError(f"No backend with name '{backend_name}' is available.")
    if n_candidates > 1:
        warnings.warn(
            f"More than one backend with name '{backend_name}' \
is available. Picking one."
        )
    return candidates[0]


class _AerBaseBackend(Backend):
    """Common base class for all Aer simulator backends"""

    _qiskit_backend: "QiskitAerBackend"
    _backend_info: BackendInfo
    _memory: bool
    _required_predicates: list[Predicate]
    _noise_model: Optional[NoiseModel] = None
    _has_arch: bool = False
    _needs_transpile: bool = False

    # Map from (job ID, circuit index) to (number of qubits, postprocessing circuit),
    # i.e. from the first two components of the ResultHandle to the last two.
    _circuit_data: dict[
        tuple[int | float | complex | str | bool | bytes, int], tuple[int, str]
    ] = {}

    @property
    def required_predicates(self) -> list[Predicate]:
        return self._required_predicates

    @property
    def _result_id_type(self) -> _ResultIdTuple:
        return (str, int, int, str)

    @property
    def backend_info(self) -> BackendInfo:
        return self._backend_info

    def rebase_pass(self) -> BasePass:
        return AutoRebase(
            self._backend_info.gate_set,
        )

    def _arch_dependent_default_compilation_pass(
        self,
        arch: Architecture,
        optimisation_level: int = 2,
        timeout: int = 300,
    ) -> BasePass:
        assert optimisation_level in range(4)
        arch_specific_passes = [
            AutoRebase({OpType.CX, OpType.TK1}),
            CustomPass(_gen_lightsabre_transformation(arch), label="lightsabrepass"),
        ]
        if optimisation_level == 0:
            return SequencePass(
                [
                    DecomposeBoxes(),
                    self.rebase_pass(),
                    *arch_specific_passes,
                    self.rebase_pass(),
                ],
            )
        if optimisation_level == 1:
            return SequencePass(
                [
                    DecomposeBoxes(),
                    SynthesiseTket(),
                    *arch_specific_passes,
                    SynthesiseTket(),
                ],
            )
        if optimisation_level == 2:
            return SequencePass(
                [
                    DecomposeBoxes(),
                    FullPeepholeOptimise(),
                    *arch_specific_passes,
                    CliffordSimp(False),
                    SynthesiseTket(),
                ],
            )
        return SequencePass(
            [
                DecomposeBoxes(),
                RemoveBarriers(),
                AutoRebase(
                    {
                        OpType.Z,
                        OpType.X,
                        OpType.Y,
                        OpType.S,
                        OpType.Sdg,
                        OpType.V,
                        OpType.Vdg,
                        OpType.H,
                        OpType.CX,
                        OpType.CY,
                        OpType.CZ,
                        OpType.SWAP,
                        OpType.Rz,
                        OpType.Rx,
                        OpType.Ry,
                        OpType.T,
                        OpType.Tdg,
                        OpType.ZZMax,
                        OpType.ZZPhase,
                        OpType.XXPhase,
                        OpType.YYPhase,
                        OpType.PhasedX,
                    }
                ),
                GreedyPauliSimp(thread_timeout=timeout, only_reduce=True, trials=10),
                *arch_specific_passes,
                self.rebase_pass(),
                SynthesiseTket(),
            ],
        )

    def _arch_independent_default_compilation_pass(
        self,
        optimisation_level: int = 2,
        timeout: int = 300,
    ) -> BasePass:
        assert optimisation_level in range(4)
        if optimisation_level == 0:
            return SequencePass([DecomposeBoxes(), self.rebase_pass()])
        if optimisation_level == 1:
            return SequencePass([DecomposeBoxes(), SynthesiseTket()])
        if optimisation_level == 2:
            return SequencePass([DecomposeBoxes(), FullPeepholeOptimise()])
        return SequencePass(
            [
                DecomposeBoxes(),
                RemoveBarriers(),
                AutoRebase(
                    {
                        OpType.Z,
                        OpType.X,
                        OpType.Y,
                        OpType.S,
                        OpType.Sdg,
                        OpType.V,
                        OpType.Vdg,
                        OpType.H,
                        OpType.CX,
                        OpType.CY,
                        OpType.CZ,
                        OpType.SWAP,
                        OpType.Rz,
                        OpType.Rx,
                        OpType.Ry,
                        OpType.T,
                        OpType.Tdg,
                        OpType.ZZMax,
                        OpType.ZZPhase,
                        OpType.XXPhase,
                        OpType.YYPhase,
                        OpType.PhasedX,
                    }
                ),
                GreedyPauliSimp(thread_timeout=timeout, only_reduce=True, trials=10),
                self.rebase_pass(),
                SynthesiseTket(),
            ],
        )

    def default_compilation_pass(
        self,
        optimisation_level: int = 2,
        timeout: int = 300,
    ) -> BasePass:
        """
        See documentation for :py:meth:`IBMQBackend.default_compilation_pass`.
        """
        arch = self._backend_info.architecture
        if self._has_arch and arch.coupling:  # type: ignore
            return self._arch_dependent_default_compilation_pass(
                arch,  # type: ignore
                optimisation_level,
                timeout,
            )
        return self._arch_independent_default_compilation_pass(
            optimisation_level, timeout
        )

    def get_compiled_circuit(
        self, circuit: Circuit, optimisation_level: int = 2, timeout: int = 300
    ) -> Circuit:
        """
        Return a single circuit compiled with :py:meth:`default_compilation_pass`.

        :param optimisation_level: Allows values of 0, 1, 2 or 3, with higher values
            prompting more computationally heavy optimising compilation that
            can lead to reduced gate count in circuits.
        :type optimisation_level: int, optional
        :param timeout: Only valid for optimisation level 3, gives a maximum time
            for running a single thread of the pass `GreedyPauliSimp`. Increase for
            optimising larger circuits.
        :type timeout: int, optional

        :return: An optimised quantum circuit
        :rtype: Circuit
        """
        return_circuit = circuit.copy()
        if optimisation_level == 3 and circuit.n_gates_of_type(OpType.Barrier) > 0:
            warnings.warn(
                "Barrier operations in this circuit will be removed when using "
                "optimisation level 3."
            )
        self.default_compilation_pass(optimisation_level, timeout).apply(return_circuit)
        return return_circuit

    def get_compiled_circuits(
        self,
        circuits: Sequence[Circuit],
        optimisation_level: int = 2,
        timeout: int = 300,
    ) -> list[Circuit]:
        """Compile a sequence of circuits with :py:meth:`default_compilation_pass`
        and return the list of compiled circuits (does not act in place).

        As well as applying a degree of optimisation (controlled by the
        `optimisation_level` parameter), this method tries to ensure that the circuits
        can be run on the backend (i.e. successfully passed to
        :py:meth:`process_circuits`), for example by rebasing to the supported gate set,
        or routing to match the connectivity of the device. However, this is not always
        possible, for example if the circuit contains classical operations that are not
        supported by the backend. You may use :py:meth:`valid_circuit` to check whether
        the circuit meets the backend's requirements after compilation. This validity
        check is included in :py:meth:`process_circuits` by default, before any circuits
        are submitted to the backend.

        If the validity check fails, you can obtain more information about the failure
        by iterating through the predicates in the `required_predicates` property of the
        backend, and running the :py:meth:`verify` method on each in turn with your
        circuit.

        :param circuits: The circuits to compile.
        :type circuit: Sequence[Circuit]
        :param optimisation_level: The level of optimisation to perform during
            compilation. See :py:meth:`default_compilation_pass` for a description of
            the different levels (0, 1, 2 or 3). Defaults to 2.
        :type optimisation_level: int, optional
        :param timeout: Only valid for optimisation level 3, gives a maximum time
            for running a single thread of the pass `GreedyPauliSimp`. Increase for
            optimising larger circuits.
        :type timeout: int, optional
        :return: Compiled circuits.
        :rtype: List[Circuit]
        """
        return [
            self.get_compiled_circuit(c, optimisation_level, timeout) for c in circuits
        ]

    def process_circuits(
        self,
        circuits: Sequence[Circuit],
        n_shots: None | int | Sequence[Optional[int]] = None,
        valid_check: bool = True,
        **kwargs: KwargTypes,
    ) -> list[ResultHandle]:
        """
        See :py:meth:`pytket.backends.Backend.process_circuits`.
        Supported kwargs: `seed`, `postprocess`.
        """
        postprocess = kwargs.get("postprocess", False)

        circuits = list(circuits)
        n_shots_list = Backend._get_n_shots_as_list(
            n_shots,
            len(circuits),
            optional=True,
        )

        if valid_check:
            self._check_all_circuits(circuits)

        if hasattr(self, "_crosstalk_params") and self._crosstalk_params is not None:
            noisy_circuits = []
            for c in circuits:
                noisy_circ_builder = NoisyCircuitBuilder(c, self._crosstalk_params)
                noisy_circ_builder.build()
                noisy_circuits.append(noisy_circ_builder.get_circuit())
            circuits = noisy_circuits

        handle_list: list[Optional[ResultHandle]] = [None] * len(circuits)
        seed = kwargs.get("seed")
        circuit_batches, batch_order = _batch_circuits(circuits, n_shots_list)

        replace_implicit_swaps = self.supports_state or self.supports_unitary

        for (n_shots, batch), indices in zip(circuit_batches, batch_order):
            qcs, ppcirc_strs, tkc_qubits_count = [], [], []
            for tkc in batch:
                if postprocess:
                    c0, ppcirc = prepare_circuit(tkc, allow_classical=False)
                    ppcirc_rep = ppcirc.to_dict()
                else:
                    c0, ppcirc_rep = tkc, None

                qc = tk_to_qiskit(c0, replace_implicit_swaps, perm_warning=False)

                if self.supports_state:
                    qc.save_state()

                elif self.supports_density_matrix:
                    qc.save_density_matrix()

                elif self.supports_unitary:
                    qc.save_unitary()

                qcs.append(qc)
                tkc_qubits_count.append(c0.n_qubits)
                ppcirc_strs.append(json.dumps(ppcirc_rep))

            if self._needs_transpile:
                qcs = transpile(qcs, self._qiskit_backend)

            job = self._qiskit_backend.run(
                qcs,
                shots=n_shots,
                memory=self._memory,
                seed_simulator=seed,
                noise_model=self._noise_model,
            )
            if type(seed) is int:
                seed += 1
            jobid = job.job_id()
            for i, ind in enumerate(indices):
                handle = ResultHandle(jobid, i, tkc_qubits_count[i], ppcirc_strs[i])
                handle_list[ind] = handle
                self._circuit_data[(jobid, i)] = (tkc_qubits_count[i], ppcirc_strs[i])
                self._cache[handle] = {"job": job}
        return cast("list[ResultHandle]", handle_list)

    def cancel(self, handle: ResultHandle) -> None:
        job: AerJob = self._cache[handle]["job"]
        cancelled = job.cancel()
        if not cancelled:
            warning(f"Unable to cancel job {cast('str', handle[0])}")

    def circuit_status(self, handle: ResultHandle) -> CircuitStatus:
        self._check_handle_type(handle)
        job: AerJob = self._cache[handle]["job"]
        ibmstatus = job.status()
        return CircuitStatus(_STATUS_MAP[ibmstatus], ibmstatus.value)

    def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResult:
        try:
            return super().get_result(handle)
        except CircuitNotRunError:
            jobid = handle[0]
            try:
                job: AerJob = self._cache[handle]["job"]
            except KeyError:
                raise CircuitNotRunError(handle)

            res = job.result()
            backresults = qiskit_result_to_backendresult(
                res,
                include_shots=self._supports_shots,
                include_counts=self._supports_counts,
                include_state=self._supports_state,
                include_unitary=self._supports_unitary,
                include_density_matrix=self._supports_density_matrix,
            )
            for circ_index, backres in enumerate(backresults):
                qubit_n, ppc = self._circuit_data[(jobid, circ_index)]
                self._cache[ResultHandle(jobid, circ_index, qubit_n, ppc)][
                    "result"
                ] = backres

            return cast("BackendResult", self._cache[handle]["result"])

    def _snapshot_expectation_value(
        self,
        circuit: Circuit,
        hamiltonian: SparsePauliOp | qk_Pauli,
        valid_check: bool = True,
    ) -> complex:
        if valid_check:
            self._check_all_circuits([circuit], nomeasure_warn=False)

        circ_qbs = circuit.qubits
        q_indices = (_default_q_index(q) for q in circ_qbs)
        if not all(q_ind == i for q_ind, i in zip(q_indices, range(len(circ_qbs)))):
            raise ValueError(
                "Circuit must act on default register Qubits, contiguously from 0"
                + f" onwards. Circuit qubits were: {circ_qbs}"
            )
        qc = tk_to_qiskit(circuit)
        qc.save_expectation_value(hamiltonian, qc.qubits, "snap")
        job = self._qiskit_backend.run(qc)
        return cast(
            "complex",
            job.result().data(qc)["snap"],
        )

    def get_pauli_expectation_value(
        self,
        state_circuit: Circuit,
        pauli: QubitPauliString,
        valid_check: bool = True,
    ) -> complex:
        """Calculates the expectation value of the given circuit using the built-in Aer
        snapshot functionality
        Requires a simple circuit with default register qubits.

        :param state_circuit: Circuit that generates the desired state
            :math:`\\left|\\psi\\right>`.
        :param pauli: Pauli operator
        :param valid_check: Explicitly check that the circuit satisfies all required
            predicates to run on the backend. Defaults to True
        :return: :math:`\\left<\\psi | P | \\psi \\right>`
        """
        if self._noise_model:
            raise RuntimeError(
                "Snapshot based expectation value not supported with noise model. "
                "Use shots."
            )
        if not self._supports_expectation:
            raise NotImplementedError("Cannot get expectation value from this backend")

        operator = qk_Pauli(_sparse_to_zx_tup(pauli, state_circuit.n_qubits))
        return self._snapshot_expectation_value(state_circuit, operator, valid_check)

    def get_operator_expectation_value(
        self,
        state_circuit: Circuit,
        operator: QubitPauliOperator,
        valid_check: bool = True,
    ) -> complex:
        """Calculates the expectation value of the given circuit with respect to the
        operator using the built-in Aer snapshot functionality
        Requires a simple circuit with default register qubits.

        :param state_circuit: Circuit that generates the desired state
            :math:`\\left|\\psi\\right>`.
        :param operator: Operator :math:`H`.
        :param valid_check: Explicitly check that the circuit satisfies all required
            predicates to run on the backend. Defaults to True
        :return: :math:`\\left<\\psi | H | \\psi \\right>`
        """
        if self._noise_model:
            raise RuntimeError(
                "Snapshot based expectation value not supported with noise model. "
                "Use shots."
            )
        if not self._supports_expectation:
            raise NotImplementedError("Cannot get expectation value from this backend")

        sparse_op = _qubitpauliop_to_sparsepauliop(operator, state_circuit.n_qubits)
        return self._snapshot_expectation_value(state_circuit, sparse_op, valid_check)


@dataclass(frozen=True)
class NoiseModelCharacterisation:
    """Class to hold information from the processing of the noise model"""

    architecture: Architecture
    node_errors: Optional[dict[Node, dict[OpType, float]]] = None
    edge_errors: Optional[dict[tuple[Node, Node], dict[OpType, float]]] = None
    readout_errors: Optional[dict[Node, list[list[float]]]] = None
    averaged_node_errors: Optional[dict[Node, float]] = None
    averaged_edge_errors: Optional[dict[tuple[Node, Node], float]] = None
    averaged_readout_errors: Optional[dict[Node, float]] = None
    generic_q_errors: Optional[dict[str, Any]] = None


def _map_trivial_noise_model_to_none(
    noise_model: Optional[NoiseModel],
) -> Optional[NoiseModel]:
    if noise_model and all(value == [] for value in noise_model.to_dict().values()):
        return None
    return noise_model


def _get_characterisation_of_noise_model(
    noise_model: Optional[NoiseModel], gate_set: set[OpType]
) -> NoiseModelCharacterisation:
    if noise_model is None:
        return NoiseModelCharacterisation(architecture=Architecture([]))
    return _process_noise_model(noise_model, gate_set)


[docs] class AerBackend(_AerBaseBackend): """ Backend for running simulations on the Qiskit Aer QASM simulator. :param noise_model: Noise model to apply during simulation. Defaults to None. :param simulation_method: Simulation method, see https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.AerSimulator.html for available values. Defaults to "automatic". :param crosstalk_params: Apply crosstalk noise simulation to the circuits before execution. `noise_model` will be overwritten if this is given. Default to None. :param n_qubits: The maximum number of qubits supported by the backend. """ _persistent_handles: bool = False _supports_shots: bool = True _supports_counts: bool = True _supports_expectation: bool = True _expectation_allows_nonhermitian: bool = False _memory: bool = True _qiskit_backend_name: str = "aer_simulator" _allowed_special_gates: set[OpType] = { OpType.Measure, OpType.Barrier, OpType.Reset, OpType.RangePredicate, }
[docs] def __init__( self, noise_model: Optional[NoiseModel] = None, simulation_method: str = "automatic", crosstalk_params: Optional[CrosstalkParams] = None, n_qubits: int = 40, ): super().__init__() self._qiskit_backend = qiskit_aer_backend(self._qiskit_backend_name) self._qiskit_backend.set_options(method=simulation_method) gate_set: set[OpType] = _tket_gate_set_from_qiskit_backend( self._qiskit_backend ).union(self._allowed_special_gates) self._crosstalk_params = crosstalk_params if self._crosstalk_params is not None: self._noise_model = self._crosstalk_params.get_noise_model() self._backend_info = BackendInfo( name=type(self).__name__, device_name=self._qiskit_backend_name, version=__extension_version__, architecture=Architecture([]), gate_set=gate_set, ) else: self._noise_model = _map_trivial_noise_model_to_none(noise_model) characterisation = _get_characterisation_of_noise_model( self._noise_model, gate_set ) self._has_arch = bool(characterisation.architecture) and bool( characterisation.architecture.nodes ) self._backend_info = BackendInfo( name=type(self).__name__, device_name=self._qiskit_backend_name, version=__extension_version__, architecture=( characterisation.architecture if self._has_arch else FullyConnected(n_qubits) ), gate_set=gate_set, supports_midcircuit_measurement=True, # is this correct? supports_fast_feedforward=True, all_node_gate_errors=characterisation.node_errors, all_edge_gate_errors=characterisation.edge_errors, all_readout_errors=characterisation.readout_errors, averaged_node_gate_errors=characterisation.averaged_node_errors, averaged_edge_gate_errors=characterisation.averaged_edge_errors, averaged_readout_errors=characterisation.averaged_readout_errors, misc={"characterisation": characterisation.generic_q_errors}, ) self._required_predicates = [ NoSymbolsPredicate(), GateSetPredicate(self._backend_info.gate_set), MaxNQubitsPredicate(n_qubits), ] if self._crosstalk_params is not None: self._required_predicates.extend( [ NoClassicalControlPredicate(), DefaultRegisterPredicate(), NoBarriersPredicate(), ] ) if self._has_arch: # architecture is non-trivial self._required_predicates.append( ConnectivityPredicate(self._backend_info.architecture) # type: ignore )
[docs] class AerStateBackend(_AerBaseBackend): """ Backend for running simulations on the Qiskit Aer Statevector simulator. :param n_qubits: The maximum number of qubits supported by the backend. """ _persistent_handles: bool = False _supports_state: bool = True _supports_expectation: bool = True _expectation_allows_nonhermitian: bool = False _noise_model: Optional[NoiseModel] = None _memory: bool = False _qiskit_backend_name: str = "aer_simulator_statevector"
[docs] def __init__( self, n_qubits: int = 40, ) -> None: super().__init__() self._qiskit_backend = qiskit_aer_backend(self._qiskit_backend_name) self._backend_info = BackendInfo( name=type(self).__name__, device_name=self._qiskit_backend_name, version=__extension_version__, architecture=FullyConnected(n_qubits), gate_set=_tket_gate_set_from_qiskit_backend(self._qiskit_backend), supports_midcircuit_measurement=True, supports_reset=True, supports_fast_feedforward=True, misc={"characterisation": None}, ) self._required_predicates = [ GateSetPredicate(self._backend_info.gate_set), ]
[docs] class AerUnitaryBackend(_AerBaseBackend): """Backend for running simulations on the Qiskit Aer Unitary simulator. :param n_qubits: The maximum number of qubits supported by the backend. """ _persistent_handles: bool = False _supports_unitary: bool = True _memory: bool = False _noise_model: Optional[NoiseModel] = None _needs_transpile: bool = True _qiskit_backend_name: str = "aer_simulator_unitary"
[docs] def __init__(self, n_qubits: int = 40) -> None: super().__init__() self._qiskit_backend = qiskit_aer_backend(self._qiskit_backend_name) self._backend_info = BackendInfo( name=type(self).__name__, device_name=self._qiskit_backend_name, version=__extension_version__, architecture=FullyConnected(n_qubits), gate_set=_tket_gate_set_from_qiskit_backend(self._qiskit_backend), supports_midcircuit_measurement=True, # is this correct? misc={"characterisation": None}, ) self._required_predicates = [ NoClassicalControlPredicate(), NoFastFeedforwardPredicate(), GateSetPredicate(self._backend_info.gate_set), ]
[docs] class AerDensityMatrixBackend(_AerBaseBackend): """ Backend for running simulations on the Qiskit Aer density matrix simulator. :param noise_model: Noise model to apply during simulation. Defaults to None. :param n_qubits: The maximum number of qubits supported by the backend. """ _supports_density_matrix: bool = True _supports_state: bool = False _memory: bool = False _noise_model: Optional[NoiseModel] = None _needs_transpile: bool = True _supports_expectation: bool = True _qiskit_backend_name: str = "aer_simulator_density_matrix" _allowed_special_gates: set[OpType] = { OpType.Measure, OpType.Barrier, OpType.Reset, OpType.RangePredicate, }
[docs] def __init__( self, noise_model: Optional[NoiseModel] = None, n_qubits: int = 40, ) -> None: super().__init__() self._qiskit_backend = qiskit_aer_backend(self._qiskit_backend_name) gate_set: set[OpType] = _tket_gate_set_from_qiskit_backend( self._qiskit_backend ).union(self._allowed_special_gates) self._noise_model = _map_trivial_noise_model_to_none(noise_model) characterisation: NoiseModelCharacterisation = ( _get_characterisation_of_noise_model(self._noise_model, gate_set) ) self._has_arch: bool = bool(characterisation.architecture) and bool( characterisation.architecture.nodes ) self._backend_info = BackendInfo( name=type(self).__name__, device_name=self._qiskit_backend_name, version=__extension_version__, architecture=( FullyConnected(n_qubits) if not self._has_arch else characterisation.architecture ), gate_set=_tket_gate_set_from_qiskit_backend(self._qiskit_backend), supports_midcircuit_measurement=True, supports_reset=True, supports_fast_feedforward=True, all_node_gate_errors=characterisation.node_errors, all_edge_gate_errors=characterisation.edge_errors, all_readout_errors=characterisation.readout_errors, averaged_node_gate_errors=characterisation.averaged_node_errors, averaged_edge_gate_errors=characterisation.averaged_edge_errors, averaged_readout_errors=characterisation.averaged_readout_errors, misc={"characterisation": characterisation.generic_q_errors}, ) self._required_predicates = [ GateSetPredicate(self._backend_info.gate_set), ]
def _process_noise_model( noise_model: NoiseModel, gate_set: set[OpType] ) -> NoiseModelCharacterisation: # obtain approximations for gate errors from noise model by using probability of # "identity" error assert OpType.CX in gate_set # TODO explicitly check for and separate 1 and 2 qubit gates errors = [ e for e in noise_model.to_dict()["errors"] if e["type"] == "qerror" or e["type"] == "roerror" ] node_errors: dict[Node, dict[OpType, float]] = defaultdict(dict) link_errors: dict[tuple[Node, Node], dict[OpType, float]] = defaultdict(dict) readout_errors: dict[Node, list[list[float]]] = {} generic_single_qerrors_dict: dict = defaultdict(list) generic_2q_qerrors_dict: dict = defaultdict(list) qubits_set: set = set() # remember which qubits have explicit link errors qubits_with_link_errors: set = set() coupling_map = [] for error in errors: name = error["operations"] if len(name) > 1: raise RuntimeWarning("Error applies to multiple gates.") if "gate_qubits" not in error: raise RuntimeWarning( "Please define NoiseModel without using the" " add_all_qubit_quantum_error()" " or add_all_qubit_readout_error() method." ) name = name[0] qubits = error["gate_qubits"][0] gate_fid = error["probabilities"][0] if len(qubits) == 1: [q] = qubits optype = _gate_str_2_optype[name] qubits_set.add(q) if error["type"] == "qerror": node_errors[Node(q)].update({optype: float(1 - gate_fid)}) generic_single_qerrors_dict[q].append( [error["instructions"], error["probabilities"]] ) elif error["type"] == "roerror": readout_errors[Node(q)] = cast( "list[list[float]]", error["probabilities"] ) else: raise RuntimeWarning("Error type not 'qerror' or 'roerror'.") elif len(qubits) == 2: # note that if multiple multi-qubit errors are added to the CX gate, # the resulting noise channel is composed and reflected in probabilities [q0, q1] = qubits optype = _gate_str_2_optype[name] link_errors.update() link_errors[(Node(q0), Node(q1))].update({optype: float(1 - gate_fid)}) qubits_with_link_errors.add(q0) qubits_with_link_errors.add(q1) # to simulate a worse reverse direction square the fidelity link_errors[(Node(q1), Node(q0))].update({optype: float(1 - gate_fid**2)}) generic_2q_qerrors_dict[(q0, q1)].append( [error["instructions"], error["probabilities"]] ) coupling_map.append(qubits) # free qubits (ie qubits with no link errors) have full connectivity free_qubits = qubits_set - qubits_with_link_errors for q in free_qubits: for lq in qubits_with_link_errors: coupling_map.append([q, lq]) coupling_map.append([lq, q]) for pair in itertools.permutations(free_qubits, 2): coupling_map.append(pair) generic_q_errors = { "GenericOneQubitQErrors": [ [k, v] for k, v in generic_single_qerrors_dict.items() ], "GenericTwoQubitQErrors": [ [list(k), v] for k, v in generic_2q_qerrors_dict.items() ], } averaged_node_errors: dict[Node, float] = { k: sum(v.values()) / len(v) for k, v in node_errors.items() } averaged_link_errors = {k: sum(v.values()) / len(v) for k, v in link_errors.items()} averaged_readout_errors = { k: (v[0][1] + v[1][0]) / 2.0 for k, v in readout_errors.items() } return NoiseModelCharacterisation( node_errors=dict(node_errors), edge_errors=dict(link_errors), readout_errors=readout_errors, averaged_node_errors=averaged_node_errors, averaged_edge_errors=averaged_link_errors, averaged_readout_errors=averaged_readout_errors, generic_q_errors=generic_q_errors, architecture=Architecture(coupling_map), ) def _sparse_to_zx_tup( pauli: QubitPauliString, n_qubits: int ) -> tuple[np.ndarray, np.ndarray]: x = np.zeros(n_qubits, dtype=np.bool_) z = np.zeros(n_qubits, dtype=np.bool_) for q, p in pauli.map.items(): i = _default_q_index(q) z[i] = p in (Pauli.Z, Pauli.Y) x[i] = p in (Pauli.X, Pauli.Y) return (z, x) def _qubitpauliop_to_sparsepauliop( operator: QubitPauliOperator, n_qubits: int ) -> SparsePauliOp: strings, coeffs = [], [] for term, coeff in operator._dict.items(): termmap = term.map strings.append( "".join( termmap.get(Qubit(i), Pauli.I).name for i in reversed(range(n_qubits)) ) ) coeffs.append(coeff) return SparsePauliOp(strings, coeffs)