Source code for qiskit_experiments.data_processing.mitigation.correlated_readout_mitigator

# This code is part of Qiskit.
#
# (C) Copyright IBM 2021
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""
Readout mitigator class based on the A-matrix inversion method
"""

import math
from collections.abc import Iterable, Callable
import numpy as np

from qiskit.exceptions import QiskitError

from qiskit.result.distributions.quasi import QuasiDistribution
from qiskit.result.counts import Counts
from .base_readout_mitigator import BaseReadoutMitigator
from .utils import counts_probability_vector, z_diagonal, str2diag


[docs] class CorrelatedReadoutMitigator(BaseReadoutMitigator): """N-qubit readout error mitigator. Mitigates :meth:`expectation_value` and :meth:`quasi_probabilities`. The mitigation_matrix should be calibrated using qiskit experiments. This mitigation method should be used in case the readout errors of the qubits are assumed to be correlated. The mitigation_matrix of *N* qubits is of size :math:`2^N x 2^N` so the mitigation complexity is :math:`O(4^N)`. """ def __init__(self, assignment_matrix: np.ndarray, qubits: Iterable[int] | None = None): """Initialize a CorrelatedReadoutMitigator Args: assignment_matrix: readout error assignment matrix. qubits: Optional, the measured physical qubits for mitigation. Raises: QiskitError: matrix size does not agree with number of qubits """ if np.any(assignment_matrix < 0) or not np.allclose(np.sum(assignment_matrix, axis=0), 1): raise QiskitError("Assignment matrix columns must be valid probability distributions") assignment_matrix = np.asarray(assignment_matrix, dtype=float) matrix_qubits_num = int(math.log2(assignment_matrix.shape[0])) if qubits is None: self._num_qubits = matrix_qubits_num self._qubits = range(self._num_qubits) else: if len(qubits) != matrix_qubits_num: raise QiskitError( f"The number of given qubits ({len(qubits)}) is different than the number of " f"qubits inferred from the matrices ({matrix_qubits_num})" ) self._qubits = qubits self._num_qubits = len(self._qubits) self._qubit_index = dict(zip(self._qubits, range(self._num_qubits))) self._assignment_mat = assignment_matrix self._mitigation_mats = {} @property def settings(self) -> dict: """Return settings.""" return {"assignment_matrix": self._assignment_mat, "qubits": self._qubits}
[docs] def expectation_value( self, data: Counts, diagonal: Callable | dict | str | np.ndarray = None, qubits: Iterable[int] = None, clbits: list[int] | None = None, shots: int | None = None, ) -> tuple[float, float]: r"""Compute the mitigated expectation value of a diagonal observable. This computes the mitigated estimator of :math:`\langle O \rangle = \mbox{Tr}[\rho. O]` of a diagonal observable :math:`O = \sum_{x\in\{0, 1\}^n} O(x)|x\rangle\!\langle x|`. Args: data: Counts object diagonal: Optional, the vector of diagonal values for summing the expectation value. If ``None`` the default value is :math:`[1, -1]^\otimes n`. qubits: Optional, the measured physical qubits the count bitstrings correspond to. If None qubits are assumed to be :math:`[0, ..., n-1]`. clbits: Optional, if not None marginalize counts to the specified bits. shots: the number of shots. Returns: (float, float): the expectation value and an upper bound of the standard deviation. Additional Information: The diagonal observable :math:`O` is input using the ``diagonal`` kwarg as a list or Numpy array :math:`[O(0), ..., O(2^n -1)]`. If no diagonal is specified the diagonal of the Pauli operator :math`O = \mbox{diag}(Z^{\otimes n}) = [1, -1]^{\otimes n}` is used. The ``clbits`` kwarg is used to marginalize the input counts dictionary over the specified bit-values, and the ``qubits`` kwarg is used to specify which physical qubits these bit-values correspond to as ``circuit.measure(qubits, clbits)``. """ if qubits is None: qubits = self._qubits probs_vec, shots = counts_probability_vector( data, qubit_index=self._qubit_index, clbits=clbits, qubits=qubits ) # Get qubit mitigation matrix and mitigate probs mit_mat = self.mitigation_matrix(qubits) # Get operator coeffs if diagonal is None: diagonal = z_diagonal(2**self._num_qubits) elif isinstance(diagonal, str): diagonal = str2diag(diagonal) # Apply transpose of mitigation matrix coeffs = mit_mat.T.dot(diagonal) expval = coeffs.dot(probs_vec) stddev_upper_bound = self.stddev_upper_bound(shots) return (expval, stddev_upper_bound)
[docs] def quasi_probabilities( self, data: Counts, qubits: list[int] | None = None, clbits: list[int] | None = None, shots: int | None = None, ) -> QuasiDistribution: """Compute mitigated quasi probabilities value. Args: data: counts object qubits: qubits the count bitstrings correspond to. clbits: Optional, marginalize counts to just these bits. shots: Optional, the total number of shots, if None shots will be calculated as the sum of all counts. Returns: QuasiDistribution: A dictionary containing pairs of [output, mean] where "output" is the key in the dictionaries, which is the length-N bitstring of a measured standard basis state, and "mean" is the mean of non-zero quasi-probability estimates. """ if qubits is None: qubits = self._qubits probs_vec, calculated_shots = counts_probability_vector( data, qubit_index=self._qubit_index, clbits=clbits, qubits=qubits ) if shots is None: shots = calculated_shots # Get qubit mitigation matrix and mitigate probs mit_mat = self.mitigation_matrix(qubits) # Apply transpose of mitigation matrix probs_vec = mit_mat.dot(probs_vec) probs_dict = {} for index, _ in enumerate(probs_vec): probs_dict[index] = probs_vec[index] quasi_dist = QuasiDistribution( probs_dict, stddev_upper_bound=self.stddev_upper_bound(shots) ) return quasi_dist
[docs] def mitigation_matrix(self, qubits: list[int] = None) -> np.ndarray: r"""Return the readout mitigation matrix for the specified qubits. The mitigation matrix :math:`A^{-1}` is defined as the inverse of the :meth:`assignment_matrix` :math:`A`. Args: qubits: Optional, qubits being measured. Returns: np.ndarray: the measurement error mitigation matrix :math:`A^{-1}`. """ if qubits is None: qubits = self._qubits qubits = tuple(sorted(qubits)) # Check for cached mitigation matrix # if not present compute if qubits not in self._mitigation_mats: marginal_matrix = self.assignment_matrix(qubits) try: mit_mat = np.linalg.inv(marginal_matrix) except np.linalg.LinAlgError: # Use pseudo-inverse if matrix is singular mit_mat = np.linalg.pinv(marginal_matrix) self._mitigation_mats[qubits] = mit_mat return self._mitigation_mats[qubits]
[docs] def assignment_matrix(self, qubits: list[int] = None) -> np.ndarray: r"""Return the readout assignment matrix for specified qubits. The assignment matrix is the stochastic matrix :math:`A` which assigns a noisy readout probability distribution to an ideal input readout distribution: :math:`P(i|j) = \langle i | A | j \rangle`. Args: qubits: Optional, qubits being measured. Returns: np.ndarray: the assignment matrix A. """ if qubits is None: qubits = self._qubits if qubits == self._num_qubits: return self._assignment_mat if isinstance(qubits, int): qubits = [qubits] qubit_indices = [self._qubit_index[qubit] for qubit in qubits] # Compute marginal matrix axis = tuple( self._num_qubits - 1 - i for i in set(range(self._num_qubits)).difference(qubit_indices) ) num_qubits = len(qubits) new_amat = np.zeros(2 * [2**num_qubits], dtype=float) for i, col in enumerate(self._assignment_mat.T[self._keep_indexes(qubit_indices)]): new_amat[i] = ( np.reshape(col, self._num_qubits * [2]).sum(axis=axis).reshape([2**num_qubits]) ) new_amat = new_amat.T return new_amat
@staticmethod def _keep_indexes(qubits): indexes = [0] for i in sorted(qubits): indexes += [idx + (1 << i) for idx in indexes] return indexes def _compute_gamma(self): """Compute gamma for N-qubit mitigation""" mitmat = self.mitigation_matrix(qubits=self._qubits) return np.max(np.sum(np.abs(mitmat), axis=0))
[docs] def stddev_upper_bound(self, shots: int): """Return an upper bound on standard deviation of expval estimator. Args: shots: Number of shots used for expectation value measurement. Returns: float: the standard deviation upper bound. """ gamma = self._compute_gamma() return gamma / math.sqrt(shots)
@property def qubits(self) -> tuple[int]: """The device qubits for this mitigator""" return self._qubits