Source code for circuit_knitting.cutting.qpd.qpd_basis
# This code is a Qiskit project.
# (C) Copyright IBM 2023.
# 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.
"""Class containing the basis in which to decompose an operation."""
from __future__ import annotations
from collections.abc import Sequence
import numpy as np
from qiskit.circuit import Instruction
[docs]
class QPDBasis:
"""Basis in which to decompose an operation.
This class defines a basis in which a quantum operation will be decomposed. The
ideal (noise-free) quantum operation will be decomposed into a quasiprobabilistic
mixture of noisy circuits.
"""
def __init__(
self,
maps: Sequence[tuple[Sequence[Instruction], ...]],
coeffs: Sequence[float],
):
"""
Assign member variables.
Args:
maps: A sequence of tuples describing the noisy operations probabilistically
used to simulate an ideal quantum operation.
coeffs: Coefficients for quasiprobability representation. Each coefficient
can be any real number.
Returns:
None
"""
self._set_maps(maps)
self.coeffs = coeffs # Note: probabilities and kappa calculated through coeffs
@property
def maps(
self,
) -> Sequence[tuple[Sequence[Instruction], ...]]:
"""Get mappings for each qubit in the decomposition."""
return self._maps
def _set_maps(
self,
maps: Sequence[tuple[Sequence[Instruction], ...]],
) -> None:
if len(maps) == 0:
raise ValueError("Number of maps passed to QPDBasis must be nonzero.")
num_qubits = len(maps[0])
if num_qubits > 2:
raise ValueError("QPDBasis supports at most two qubits.")
for i in range(1, len(maps)):
if len(maps[i]) != num_qubits:
raise ValueError(
f"All maps passed to QPDBasis must act on the same number of "
f"qubits. (Index {i} contains a {len(maps[i])}-tuple but should "
f"contain a {num_qubits}-tuple.)"
)
self._maps = maps
@property
def num_qubits(self) -> int:
"""Get number of qubits that this decomposition acts on."""
return len(self._maps[0])
@property
def coeffs(self) -> Sequence[float]:
"""Quasiprobability decomposition coefficients."""
return self._coeffs
@coeffs.setter
def coeffs(self, coeffs: Sequence[float]) -> None:
if len(coeffs) != len(self.maps): # Note: cross-validation
raise ValueError("Coefficients must be same length as maps.")
weights = np.abs(coeffs)
self._kappa = sum(weights)
self._probabilities = weights / self._kappa
self._coeffs = coeffs
@property
def probabilities(self) -> Sequence[float]:
"""Get the probabilities on which the maps will be sampled."""
return self._probabilities
@property
def kappa(self) -> float:
"""
Get the square root of the sampling overhead.
This quantity is the sum of the magnitude of the coefficients.
"""
return self._kappa
@property
def overhead(self) -> float:
"""
Get the sampling overhead.
The sampling overhead is the square of the sum of the magnitude of the coefficients.
"""
return self._kappa**2
@staticmethod
def from_instruction(gate: Instruction, /) -> QPDBasis:
"""
Generate a :class:`.QPDBasis` object, given a supported operation.
This static method is provided for convenience; it simply
calls :func:`~qpd.decompositions.qpdbasis_from_instruction` under the hood.
Args:
gate: The instruction from which to instantiate a decomposition
Returns:
The newly-instantiated :class:`QPDBasis` object
"""
# pylint: disable=cyclic-import
from .decompositions import qpdbasis_from_instruction
return qpdbasis_from_instruction(gate)
def __eq__(self, other):
"""Check equivalence for QPDBasis class."""
if other.__class__ is not self.__class__:
return False
if len(self.maps) != len(other.maps) or len(self.coeffs) != len(other.coeffs):
return False
if self.maps != other.maps:
return False
if self.coeffs != other.coeffs:
return False
return True