def __init__(self, data, coeffs=None): """Initialize an operator object. Args: data (Paulilist, SparsePauliOp, PauliTable): Pauli list of terms. coeffs (np.ndarray): complex coefficients for Pauli terms. Raises: QiskitError: If the input data or coeffs are invalid. """ if isinstance(data, SparsePauliOp): pauli_list = data._pauli_list coeffs = data._coeffs else: pauli_list = PauliList(data) if coeffs is None: coeffs = np.ones(pauli_list.size, dtype=complex) # Initialize PauliList self._pauli_list = PauliList.from_symplectic(pauli_list.z, pauli_list.x) # Initialize Coeffs self._coeffs = np.asarray((-1j) ** pauli_list.phase * coeffs, dtype=complex) if self._coeffs.shape != (self._pauli_list.size,): raise QiskitError( "coeff vector is incorrect shape for number" " of Paulis {} != {}".format(self._coeffs.shape, self._pauli_list.size) ) # Initialize LinearOp super().__init__(num_qubits=self._pauli_list.num_qubits)
def pauli_basis(num_qubits, weight=False, pauli_list=False): """Return the ordered PauliTable or PauliList for the n-qubit Pauli basis. Args: num_qubits (int): number of qubits weight (bool): if True optionally return the basis sorted by Pauli weight rather than lexicographic order (Default: False) pauli_list (bool): if True, the return type becomes PauliList, otherwise PauliTable. Returns: PauliTable, PauliList: the Paulis for the basis """ if pauli_list: pauli_1q = PauliList(["I", "X", "Y", "Z"]) else: warnings.warn( "The pauli_basis function with PauliTable output is deprecated as of Qiskit Terra " "0.19.0 and will be removed no sooner than 3 months after the releasedate. " "Use PauliList by pauli_list=True instead.", DeprecationWarning, stacklevel=2, ) pauli_1q = PauliTable( np.array( [[False, False], [True, False], [True, True], [False, True]], dtype=bool)) if num_qubits == 1: return pauli_1q pauli = pauli_1q for _ in range(num_qubits - 1): pauli = pauli_1q.tensor(pauli) if weight: return pauli.sort(weight=True) return pauli
def pauli_basis(num_qubits, weight=False, pauli_list=False): """Return the ordered PauliTable or PauliList for the n-qubit Pauli basis. Args: num_qubits (int): number of qubits weight (bool): if True optionally return the basis sorted by Pauli weight rather than lexicographic order (Default: False) pauli_list (bool): if True, the return type becomes PauliList, otherwise PauliTable. Returns: PauliTable, PauliList: the Paulis for the basis """ if pauli_list: pauli_1q = PauliList(["I", "X", "Y", "Z"]) else: warnings.warn( "The return type of 'pauli_basis' will change from PauliTable to PauliList in a " "future release of Qiskit Terra. Returning PauliTable is deprecated as of " "Qiskit Terra 0.19, and will be removed in a future release. To immediately switch " "to the new behaviour, pass the keyword argument 'pauli_list=True'.", FutureWarning, stacklevel=2, ) pauli_1q = PauliTable( np.array( [[False, False], [True, False], [True, True], [False, True]], dtype=bool)) if num_qubits == 1: return pauli_1q pauli = pauli_1q for _ in range(num_qubits - 1): pauli = pauli_1q.tensor(pauli) if weight: return pauli.sort(weight=True) return pauli
def compose(self, other, qargs=None, front=False): if qargs is None: qargs = getattr(other, "qargs", None) if not isinstance(other, SparsePauliOp): other = SparsePauliOp(other) # Validate composition dimensions and qargs match self._op_shape.compose(other._op_shape, qargs, front) if qargs is not None: x1, z1 = self.paulis.x[:, qargs], self.paulis.z[:, qargs] else: x1, z1 = self.paulis.x, self.paulis.z x2, z2 = other.paulis.x, other.paulis.z num_qubits = other.num_qubits # This method is the outer version of `BasePauli.compose`. # `x1` and `z1` have shape `(self.size, num_qubits)`. # `x2` and `z2` have shape `(other.size, num_qubits)`. # `x1[:, no.newaxis]` results in shape `(self.size, 1, num_qubits)`. # `ar = ufunc(x1[:, np.newaxis], x2)` will be in shape `(self.size, other.size, num_qubits)`. # So, `ar.reshape((-1, num_qubits))` will be in shape `(self.size * other.size, num_qubits)`. # Ref: https://numpy.org/doc/stable/user/theory.broadcasting.html phase = np.add.outer(self.paulis._phase, other.paulis._phase).reshape(-1) if front: q = np.logical_and(x1[:, np.newaxis], z2).reshape((-1, num_qubits)) else: q = np.logical_and(z1[:, np.newaxis], x2).reshape((-1, num_qubits)) # `np.mod` will be applied to `phase` in `SparsePauliOp.__init__` phase = phase + 2 * q.sum(axis=1, dtype=np.uint8) x3 = np.logical_xor(x1[:, np.newaxis], x2).reshape((-1, num_qubits)) z3 = np.logical_xor(z1[:, np.newaxis], z2).reshape((-1, num_qubits)) if qargs is None: pauli_list = PauliList(BasePauli(z3, x3, phase)) else: x4 = np.repeat(self.paulis.x, other.size, axis=0) z4 = np.repeat(self.paulis.z, other.size, axis=0) x4[:, qargs] = x3 z4[:, qargs] = z3 pauli_list = PauliList(BasePauli(z4, x4, phase)) # note: the following is a faster code equivalent to # `coeffs = np.kron(self.coeffs, other.coeffs)` # since `self.coeffs` and `other.coeffs` are both 1d arrays. coeffs = np.multiply.outer(self.coeffs, other.coeffs).ravel() return SparsePauliOp(pauli_list, coeffs, copy=False)
def sum(ops): """Sum of SparsePauliOps. This is a specialized version of the builtin ``sum`` function for SparsePauliOp with smaller overhead. Args: ops (list[SparsePauliOp]): a list of SparsePauliOps. Returns: SparsePauliOp: the SparsePauliOp representing the sum of the input list. Raises: QiskitError: if the input list is empty. QiskitError: if the input list includes an object that is not SparsePauliOp. QiskitError: if the numbers of qubits of the objects in the input list do not match. """ if len(ops) == 0: raise QiskitError("Input list is empty") if not all(isinstance(op, SparsePauliOp) for op in ops): raise QiskitError( "Input list includes an object that is not SparsePauliOp") for other in ops[1:]: ops[0]._op_shape._validate_add(other._op_shape) z = np.vstack([op.paulis.z for op in ops]) x = np.vstack([op.paulis.x for op in ops]) phase = np.hstack([op.paulis._phase for op in ops]) pauli_list = PauliList(BasePauli(z, x, phase)) coeffs = np.hstack([op.coeffs for op in ops]) return SparsePauliOp(pauli_list, coeffs, ignore_pauli_phase=True, copy=False)
def compose(self, other, qargs=None, front=False): if qargs is None: qargs = getattr(other, "qargs", None) if not isinstance(other, SparsePauliOp): other = SparsePauliOp(other) # Validate composition dimensions and qargs match self._op_shape.compose(other._op_shape, qargs, front) if qargs is not None: x1, z1 = self.paulis.x[:, qargs], self.paulis.z[:, qargs] else: x1, z1 = self.paulis.x, self.paulis.z x2, z2 = other.paulis.x, other.paulis.z num_qubits = other.num_qubits # This method is the outer version of `BasePauli.compose`. # `x1` and `z1` have shape `(self.size, num_qubits)`. # `x2` and `z2` have shape `(other.size, num_qubits)`. # `x1[:, no.newaxis]` results in shape `(self.size, 1, num_qubits)`. # `ar = ufunc(x1[:, np.newaxis], x2)` will be in shape `(self.size, other.size, num_qubits)`. # So, `ar.reshape((-1, num_qubits))` will be in shape `(self.size * other.size, num_qubits)`. # Ref: https://numpy.org/doc/stable/user/theory.broadcasting.html phase = np.add.outer(self.paulis._phase, other.paulis._phase).reshape(-1) if front: q = np.logical_and(x1[:, np.newaxis], z2).reshape((-1, num_qubits)) else: q = np.logical_and(z1[:, np.newaxis], x2).reshape((-1, num_qubits)) phase = np.mod(phase + 2 * np.sum(q, axis=1), 4) x3 = np.logical_xor(x1[:, np.newaxis], x2).reshape((-1, num_qubits)) z3 = np.logical_xor(z1[:, np.newaxis], z2).reshape((-1, num_qubits)) if qargs is None: pauli_list = PauliList(BasePauli(z3, x3, phase)) else: x4 = np.repeat(self.paulis.x, other.size, axis=0) z4 = np.repeat(self.paulis.z, other.size, axis=0) x4[:, qargs] = x3 z4[:, qargs] = z3 pauli_list = PauliList(BasePauli(z4, x4, phase)) coeffs = np.kron(self.coeffs, other.coeffs) return SparsePauliOp(pauli_list, coeffs)
def from_sparse_list(obj, num_qubits): """Construct from a list of local Pauli strings and coefficients. Each list element is a 3-tuple of a local Pauli string, indices where to apply it, and a coefficient. For example, the 5-qubit Hamiltonian .. math:: H = Z_1 X_4 + 2 Y_0 Y_3 can be constructed as .. code-block:: python # via triples and local Paulis with indices op = SparsePauliOp.from_sparse_list([("ZX", [1, 4], 1), ("YY", [0, 3], 2)], num_qubits=5) # equals the following construction from "dense" Paulis op = SparsePauliOp.from_list([("XIIZI", 1), ("IYIIY", 2)]) Args: obj (Iterable[Tuple[str, List[int], complex]]): The list 3-tuples specifying the Paulis. num_qubits (int): The number of qubits of the operator. Returns: SparsePauliOp: The SparsePauliOp representation of the Pauli terms. Raises: QiskitError: If the list of Paulis is empty. QiskitError: If the number of qubits is incompatible with the indices of the Pauli terms. """ obj = list(obj) # To convert zip or other iterable size = len(obj) # number of Pauli terms if size == 0: raise QiskitError("Input Pauli list is empty.") coeffs = np.zeros(size, dtype=complex) labels = np.zeros(size, dtype=f"<U{num_qubits}") for i, (paulis, indices, coeff) in enumerate(obj): # construct the full label based off the non-trivial Paulis and indices label = ["I"] * num_qubits for pauli, index in zip(paulis, indices): if index >= num_qubits: raise QiskitError( f"The number of qubits ({num_qubits}) is smaller than a required index {index}." ) label[~index] = pauli labels[i] = "".join(label) coeffs[i] = coeff paulis = PauliList(labels) return SparsePauliOp(paulis, coeffs, copy=False)
def simplify(self, atol=None, rtol=None): """Simplify PauliList by combining duplicates and removing zeros. Args: atol (float): Optional. Absolute tolerance for checking if coefficients are zero (Default: 1e-8). rtol (float): Optional. relative tolerance for checking if coefficients are zero (Default: 1e-5). Returns: SparsePauliOp: the simplified SparsePauliOp operator. """ # Get default atol and rtol if atol is None: atol = self.atol if rtol is None: rtol = self.rtol # Filter non-zero coefficients non_zero = np.logical_not( np.isclose(self.coeffs, 0, atol=atol, rtol=rtol)) paulis_x = self.paulis.x[non_zero] paulis_z = self.paulis.z[non_zero] nz_coeffs = self.coeffs[non_zero] # Pack bool vectors into np.uint8 vectors by np.packbits array = np.packbits(paulis_x, axis=1) * 256 + np.packbits(paulis_z, axis=1) indexes, inverses = unordered_unique(array) if np.all(non_zero) and indexes.shape[0] == array.shape[0]: # No zero operator or duplicate operator return self.copy() coeffs = np.zeros(indexes.shape[0], dtype=complex) np.add.at(coeffs, inverses, nz_coeffs) # Delete zero coefficient rows is_zero = np.isclose(coeffs, 0, atol=atol, rtol=rtol) # Check edge case that we deleted all Paulis # In this case we return an identity Pauli with a zero coefficient if np.all(is_zero): x = np.zeros((1, self.num_qubits), dtype=bool) z = np.zeros((1, self.num_qubits), dtype=bool) coeffs = np.array([0j], dtype=complex) else: non_zero = np.logical_not(is_zero) non_zero_indexes = indexes[non_zero] x = paulis_x[non_zero_indexes] z = paulis_z[non_zero_indexes] coeffs = coeffs[non_zero] return SparsePauliOp(PauliList.from_symplectic(z, x), coeffs, ignore_pauli_phase=True, copy=False)
def _evolve_clifford(self, other, qargs=None, frame="h"): """Heisenberg picture evolution of a Pauli by a Clifford.""" if qargs is None: idx = slice(None) num_act = self.num_qubits else: idx = list(qargs) num_act = len(idx) # Set return to I on qargs ret = self.copy() ret._x[:, idx] = False ret._z[:, idx] = False # pylint: disable=cyclic-import from qiskit.quantum_info.operators.symplectic.pauli import Pauli from qiskit.quantum_info.operators.symplectic.pauli_list import PauliList # Get action of Pauli's from Clifford if frame == "s": adj = other.copy() else: adj = other.adjoint() pauli_list = [] for z in self._z[:, idx]: pauli = Pauli("I" * num_act) for row in adj.stabilizer[z]: pauli.compose(Pauli((row.Z[0], row.X[0], 2 * row.phase[0])), inplace=True) pauli_list.append(pauli) ret.dot(PauliList(pauli_list), qargs=qargs, inplace=True) pauli_list = [] for x in self._x[:, idx]: pauli = Pauli("I" * num_act) for row in adj.destabilizer[x]: pauli.compose(Pauli((row.Z[0], row.X[0], 2 * row.phase[0])), inplace=True) pauli_list.append(pauli) ret.dot(PauliList(pauli_list), qargs=qargs, inplace=True) return ret
def from_list(obj): """Construct from a list [(pauli_str, coeffs)]""" obj = list(obj) # To convert zip or other iterable num_qubits = len(obj[0][0]) size = len(obj) coeffs = np.zeros(size, dtype=complex) labels = np.zeros(size, dtype=f"<U{num_qubits}") for i, item in enumerate(obj): labels[i] = item[0] coeffs[i] = item[1] paulis = PauliList(labels) return SparsePauliOp(paulis, coeffs)
def compose(self, other, qargs=None, front=False): if qargs is None: qargs = getattr(other, "qargs", None) if not isinstance(other, SparsePauliOp): other = SparsePauliOp(other) # Validate composition dimensions and qargs match self._op_shape.compose(other._op_shape, qargs, front) x1 = np.reshape( np.stack(other.size * [self.paulis.x], axis=1), (self.size * other.size, self.num_qubits), ) z1 = np.reshape( np.stack(other.size * [self.paulis.z], axis=1), (self.size * other.size, self.num_qubits), ) p1 = np.reshape( np.stack(other.size * [self.paulis.phase], axis=1), self.size * other.size, ) paulis1 = PauliList.from_symplectic(z1, x1, p1) x2 = np.reshape( np.stack(self.size * [other.paulis.x]), (self.size * other.size, other.num_qubits) ) z2 = np.reshape( np.stack(self.size * [other.paulis.z]), (self.size * other.size, other.num_qubits) ) p2 = np.reshape( np.stack(self.size * [other.paulis.phase]), self.size * other.size, ) paulis2 = PauliList.from_symplectic(z2, x2, p2) pauli_list = paulis1.compose(paulis2, qargs, front) coeffs = np.kron(self.coeffs, other.coeffs) return SparsePauliOp(pauli_list, coeffs)
def _multiply(self, other): if not isinstance(other, Number): raise QiskitError("other is not a number") if other == 0: # Check edge case that we deleted all Paulis # In this case we return an identity Pauli with a zero coefficient paulis = PauliList.from_symplectic( np.zeros((1, self.num_qubits), dtype=bool), np.zeros((1, self.num_qubits), dtype=bool), ) coeffs = np.array([0j]) return SparsePauliOp(paulis, coeffs) # Otherwise we just update the phases return SparsePauliOp(self.paulis, other * self.coeffs)
def simplify(self, atol=None, rtol=None): """Simplify PauliList by combining duplicates and removing zeros. Args: atol (float): Optional. Absolute tolerance for checking if coefficients are zero (Default: 1e-8). rtol (float): Optional. relative tolerance for checking if coefficients are zero (Default: 1e-5). Returns: SparsePauliOp: the simplified SparsePauliOp operator. """ # Get default atol and rtol if atol is None: atol = self.atol if rtol is None: rtol = self.rtol array = np.column_stack((self.paulis.x, self.paulis.z)) flatten_paulis, indexes = np.unique(array, return_inverse=True, axis=0) coeffs = np.zeros(self.size, dtype=complex) for i, val in zip(indexes, self.coeffs): coeffs[i] += val # Delete zero coefficient rows # TODO: Add atol/rtol for zero comparison non_zero = [ i for i in range(coeffs.size) if not np.isclose(coeffs[i], 0, atol=atol, rtol=rtol) ] # Check edge case that we deleted all Paulis # In this case we return an identity Pauli with a zero coefficient if len(non_zero) == 0: x = np.zeros((1, self.num_qubits), dtype=bool) z = np.zeros((1, self.num_qubits), dtype=bool) coeffs = np.array([0j], dtype=complex) else: x, z = ( flatten_paulis[non_zero] .reshape((len(non_zero), 2, self.num_qubits)) .transpose(1, 0, 2) ) coeffs = coeffs[non_zero] return SparsePauliOp(PauliList.from_symplectic(z, x), coeffs)
def from_list(obj): """Construct from a list of Pauli strings and coefficients. For example, the 5-qubit Hamiltonian .. math:: H = Z_1 X_4 + 2 Y_0 Y_3 can be constructed as .. code-block:: python # via tuples and the full Pauli string op = SparsePauliOp.from_list([("XIIZI", 1), ("IYIIY", 2)]) Args: obj (Iterable[Tuple[str, complex]]): The list of 2-tuples specifying the Pauli terms. Returns: SparsePauliOp: The SparsePauliOp representation of the Pauli terms. Raises: QiskitError: If the list of Paulis is empty. """ obj = list(obj) # To convert zip or other iterable size = len(obj) # number of Pauli terms if size == 0: raise QiskitError("Input Pauli list is empty.") # determine the number of qubits num_qubits = len(obj[0][0]) coeffs = np.zeros(size, dtype=complex) labels = np.zeros(size, dtype=f"<U{num_qubits}") for i, item in enumerate(obj): labels[i] = item[0] coeffs[i] = item[1] paulis = PauliList(labels) return SparsePauliOp(paulis, coeffs, copy=False)
def chop(self, tol=1e-14): """Set real and imaginary parts of the coefficients to 0 if ``< tol`` in magnitude. For example, the operator representing ``1+1e-17j X + 1e-17 Y`` with a tolerance larger than ``1e-17`` will be reduced to ``1 X`` whereas :meth:`.SparsePauliOp.simplify` would return ``1+1e-17j X``. If a both the real and imaginary part of a coefficient is 0 after chopping, the corresponding Pauli is removed from the operator. Args: tol (float): The absolute tolerance to check whether a real or imaginary part should be set to 0. Returns: SparsePauliOp: This operator with chopped coefficients. """ realpart_nonzero = np.abs(self.coeffs.real) > tol imagpart_nonzero = np.abs(self.coeffs.imag) > tol remaining_indices = np.logical_or(realpart_nonzero, imagpart_nonzero) remaining_real = realpart_nonzero[remaining_indices] remaining_imag = imagpart_nonzero[remaining_indices] if len(remaining_real) == 0: # if no Paulis are left x = np.zeros((1, self.num_qubits), dtype=bool) z = np.zeros((1, self.num_qubits), dtype=bool) coeffs = np.array([0j], dtype=complex) else: coeffs = np.zeros(np.count_nonzero(remaining_indices), dtype=complex) coeffs.real[remaining_real] = self.coeffs.real[realpart_nonzero] coeffs.imag[remaining_imag] = self.coeffs.imag[imagpart_nonzero] x = self.paulis.x[remaining_indices] z = self.paulis.z[remaining_indices] return SparsePauliOp(PauliList.from_symplectic(z, x), coeffs, ignore_pauli_phase=True, copy=False)
def _evolve_clifford(self, other, qargs=None, frame="h"): """Heisenberg picture evolution of a Pauli by a Clifford.""" if frame == "s": adj = other else: adj = other.adjoint() if qargs is None: qargs_ = slice(None) else: qargs_ = list(qargs) # pylint: disable=cyclic-import from qiskit.quantum_info.operators.symplectic.pauli_list import PauliList num_paulis = self._x.shape[0] ret = self.copy() ret._x[:, qargs_] = False ret._z[:, qargs_] = False idx = np.concatenate((self._x[:, qargs_], self._z[:, qargs_]), axis=1) for idx_, row in zip( idx.T, PauliList.from_symplectic(z=adj.table.Z, x=adj.table.X, phase=2 * adj.table.phase), ): # most of the logic below is to properly index if self is a PauliList (2D), # while not trying to index if the object is just a Pauli (1D). if idx_.any(): if np.sum(idx_) == num_paulis: ret.compose(row, qargs=qargs, inplace=True) else: ret[idx_] = ret[idx_].compose(row, qargs=qargs) return ret
def table(self, value): if not isinstance(value, PauliTable): value = PauliTable(value) self._pauli_list = PauliList(value)
def __init__(self, data, coeffs=None, *, ignore_pauli_phase=False, copy=True): """Initialize an operator object. Args: data (PauliList or SparsePauliOp or PauliTable or Pauli or list or str): Pauli list of terms. A list of Pauli strings or a Pauli string is also allowed. coeffs (np.ndarray): complex coefficients for Pauli terms. .. note:: If ``data`` is a :obj:`~SparsePauliOp` and ``coeffs`` is not ``None``, the value of the ``SparsePauliOp.coeffs`` will be ignored, and only the passed keyword argument ``coeffs`` will be used. ignore_pauli_phase (bool): if true, any ``phase`` component of a given :obj:`~PauliList` will be assumed to be zero. This is more efficient in cases where a :obj:`~PauliList` has been constructed purely for this object, and it is already known that the phases in the ZX-convention are zero. It only makes sense to pass this option when giving :obj:`~PauliList` data. (Default: False) copy (bool): copy the input data if True, otherwise assign it directly, if possible. (Default: True) Raises: QiskitError: If the input data or coeffs are invalid. """ if ignore_pauli_phase and not isinstance(data, PauliList): raise QiskitError( "ignore_pauli_list=True is only valid with PauliList data") if isinstance(data, SparsePauliOp): if coeffs is None: coeffs = data.coeffs data = data._pauli_list # `SparsePauliOp._pauli_list` is already compatible with the internal ZX-phase # convention. See `BasePauli._from_array` for the internal ZX-phase convention. ignore_pauli_phase = True pauli_list = PauliList( data.copy() if copy and hasattr(data, "copy") else data) if coeffs is None: coeffs = np.ones(pauli_list.size, dtype=complex) else: coeffs = np.array(coeffs, copy=copy, dtype=complex) if ignore_pauli_phase: # Fast path used in copy operations, where the phase of the PauliList is already known # to be zero. This path only works if the input data is compatible with the internal # ZX-phase convention. self._pauli_list = pauli_list self._coeffs = coeffs else: # move the phase of `pauli_list` to `self._coeffs` phase = pauli_list._phase count_y = pauli_list._count_y() self._coeffs = np.asarray((-1j)**(phase - count_y) * coeffs, dtype=complex) pauli_list._phase = np.mod(count_y, 4) self._pauli_list = pauli_list if self._coeffs.shape != (self._pauli_list.size, ): raise QiskitError("coeff vector is incorrect shape for number" " of Paulis {} != {}".format( self._coeffs.shape, self._pauli_list.size)) # Initialize LinearOp super().__init__(num_qubits=self._pauli_list.num_qubits)
def paulis(self, value): if not isinstance(value, PauliList): value = PauliList(value) self._pauli_list = value