Source code for qiskit.quantum_info.operators.symplectic.pauli_list

# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2022
#
# 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.
"""
Optimized list of Pauli operators
"""

from __future__ import annotations

from collections import defaultdict
from typing import Literal

import numpy as np
import rustworkx as rx

from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.exceptions import QiskitError
from qiskit.quantum_info.operators.custom_iterator import CustomIterator
from qiskit.quantum_info.operators.mixins import GroupMixin, LinearMixin
from qiskit.quantum_info.operators.symplectic.base_pauli import BasePauli
from qiskit.quantum_info.operators.symplectic.clifford import Clifford
from qiskit.quantum_info.operators.symplectic.pauli import Pauli


[docs]class PauliList(BasePauli, LinearMixin, GroupMixin): r"""List of N-qubit Pauli operators. This class is an efficient representation of a list of :class:`Pauli` operators. It supports 1D numpy array indexing returning a :class:`Pauli` for integer indexes or a :class:`PauliList` for slice or list indices. **Initialization** A PauliList object can be initialized in several ways. ``PauliList(list[str])`` where strings are same representation with :class:`~qiskit.quantum_info.Pauli`. ``PauliList(Pauli) and PauliList(list[Pauli])`` where Pauli is :class:`~qiskit.quantum_info.Pauli`. ``PauliList.from_symplectic(z, x, phase)`` where ``z`` and ``x`` are 2 dimensional boolean ``numpy.ndarrays`` and ``phase`` is an integer in ``[0, 1, 2, 3]``. For example, .. code-block:: import numpy as np from qiskit.quantum_info import Pauli, PauliList # 1. init from list[str] pauli_list = PauliList(["II", "+ZI", "-iYY"]) print("1. ", pauli_list) pauli1 = Pauli("iXI") pauli2 = Pauli("iZZ") # 2. init from Pauli print("2. ", PauliList(pauli1)) # 3. init from list[Pauli] print("3. ", PauliList([pauli1, pauli2])) # 4. init from np.ndarray z = np.array([[True, True], [False, False]]) x = np.array([[False, True], [True, False]]) phase = np.array([0, 1]) pauli_list = PauliList.from_symplectic(z, x, phase) print("4. ", pauli_list) .. parsed-literal:: 1. ['II', 'ZI', '-iYY'] 2. ['iXI'] 3. ['iXI', 'iZZ'] 4. ['YZ', '-iIX'] **Data Access** The individual Paulis can be accessed and updated using the ``[]`` operator which accepts integer, lists, or slices for selecting subsets of PauliList. If integer is given, it returns Pauli not PauliList. .. code-block:: pauli_list = PauliList(["XX", "ZZ", "IZ"]) print("Integer: ", repr(pauli_list[1])) print("List: ", repr(pauli_list[[0, 2]])) print("Slice: ", repr(pauli_list[0:2])) .. parsed-literal:: Integer: Pauli('ZZ') List: PauliList(['XX', 'IZ']) Slice: PauliList(['XX', 'ZZ']) **Iteration** Rows in the Pauli table can be iterated over like a list. Iteration can also be done using the label or matrix representation of each row using the :meth:`label_iter` and :meth:`matrix_iter` methods. """ # Set the max number of qubits * paulis before string truncation __truncate__ = 2000 def __init__(self, data: Pauli | list): """Initialize the PauliList. Args: data (Pauli or list): input data for Paulis. If input is a list each item in the list must be a Pauli object or Pauli str. Raises: QiskitError: if input array is invalid shape. Additional Information: The input array is not copied so multiple Pauli tables can share the same underlying array. """ if isinstance(data, BasePauli): base_z, base_x, base_phase = data._z, data._x, data._phase else: # Conversion as iterable of Paulis base_z, base_x, base_phase = self._from_paulis(data) # Initialize BasePauli super().__init__(base_z, base_x, base_phase) # --------------------------------------------------------------------- # Representation conversions # --------------------------------------------------------------------- @property def settings(self): """Return settings.""" return {"data": self.to_labels()} def __array__(self, dtype=None): """Convert to numpy array""" # pylint: disable=unused-argument shape = (len(self),) + 2 * (2**self.num_qubits,) ret = np.zeros(shape, dtype=complex) for i, mat in enumerate(self.matrix_iter()): ret[i] = mat return ret @staticmethod def _from_paulis(data): """Construct a PauliList from a list of Pauli data. Args: data (iterable): list of Pauli data. Returns: PauliList: the constructed PauliList. Raises: QiskitError: If the input list is empty or contains invalid Pauli strings. """ if not isinstance(data, (list, tuple, set, np.ndarray)): data = [data] num_paulis = len(data) if num_paulis == 0: raise QiskitError("Input Pauli list is empty.") paulis = [] for i in data: if not isinstance(i, Pauli): paulis.append(Pauli(i)) else: paulis.append(i) num_qubits = paulis[0].num_qubits base_z = np.zeros((num_paulis, num_qubits), dtype=bool) base_x = np.zeros((num_paulis, num_qubits), dtype=bool) base_phase = np.zeros(num_paulis, dtype=int) for i, pauli in enumerate(paulis): if pauli.num_qubits != num_qubits: raise ValueError( f"The {i}th Pauli is defined over {pauli.num_qubits} qubits, " f"but num_qubits == {num_qubits} was expected." ) base_z[i] = pauli._z base_x[i] = pauli._x base_phase[i] = pauli._phase.item() return base_z, base_x, base_phase def __repr__(self): """Display representation.""" return self._truncated_str(True) def __str__(self): """Print representation.""" return self._truncated_str(False) def _truncated_str(self, show_class): stop = self._num_paulis if self.__truncate__ and self.num_qubits > 0: max_paulis = self.__truncate__ // self.num_qubits if self._num_paulis > max_paulis: stop = max_paulis labels = [str(self[i]) for i in range(stop)] prefix = "PauliList(" if show_class else "" tail = ")" if show_class else "" if stop != self._num_paulis: suffix = ", ...]" + tail else: suffix = "]" + tail list_str = np.array2string( np.array(labels), threshold=stop + 1, separator=", ", prefix=prefix, suffix=suffix ) return prefix + list_str[:-1] + suffix def __eq__(self, other): """Entrywise comparison of Pauli equality.""" if not isinstance(other, PauliList): other = PauliList(other) if not isinstance(other, BasePauli): return False return self._eq(other)
[docs] def equiv(self, other: PauliList | Pauli) -> np.ndarray: """Entrywise comparison of Pauli equivalence up to global phase. Args: other (PauliList or Pauli): a comparison object. Returns: np.ndarray: An array of ``True`` or ``False`` for entrywise equivalence of the current table. """ if not isinstance(other, PauliList): other = PauliList(other) return np.all(self.z == other.z, axis=1) & np.all(self.x == other.x, axis=1)
# --------------------------------------------------------------------- # Direct array access # --------------------------------------------------------------------- @property def phase(self): """Return the phase exponent of the PauliList.""" # Convert internal ZX-phase convention to group phase convention return np.mod(self._phase - self._count_y(dtype=self._phase.dtype), 4) @phase.setter def phase(self, value): # Convert group phase convetion to internal ZX-phase convention self._phase[:] = np.mod(value + self._count_y(dtype=self._phase.dtype), 4) @property def x(self): """The x array for the symplectic representation.""" return self._x @x.setter def x(self, val): self._x[:] = val @property def z(self): """The z array for the symplectic representation.""" return self._z @z.setter def z(self, val): self._z[:] = val # --------------------------------------------------------------------- # Size Properties # --------------------------------------------------------------------- @property def shape(self): """The full shape of the :meth:`array`""" return self._num_paulis, self.num_qubits @property def size(self): """The number of Pauli rows in the table.""" return self._num_paulis def __len__(self): """Return the number of Pauli rows in the table.""" return self._num_paulis # --------------------------------------------------------------------- # Pauli Array methods # --------------------------------------------------------------------- def __getitem__(self, index): """Return a view of the PauliList.""" # Returns a view of specified rows of the PauliList # This supports all slicing operations the underlying array supports. if isinstance(index, tuple): if len(index) == 1: index = index[0] elif len(index) > 2: raise IndexError(f"Invalid PauliList index {index}") # Row-only indexing if isinstance(index, (int, np.integer)): # Single Pauli return Pauli( BasePauli( self._z[np.newaxis, index], self._x[np.newaxis, index], self._phase[np.newaxis, index], ) ) elif isinstance(index, (slice, list, np.ndarray)): # Sub-Table view return PauliList(BasePauli(self._z[index], self._x[index], self._phase[index])) # Row and Qubit indexing return PauliList((self._z[index], self._x[index], 0)) def __setitem__(self, index, value): """Update PauliList.""" if isinstance(index, tuple): if len(index) == 1: row, qubit = index[0], None elif len(index) > 2: raise IndexError(f"Invalid PauliList index {index}") else: row, qubit = index else: row, qubit = index, None # Modify specified rows of the PauliList if not isinstance(value, PauliList): value = PauliList(value) # It's not valid to set a single item with a sequence, even if the sequence is length 1. phase = value._phase.item() if isinstance(row, (int, np.integer)) else value._phase if qubit is None: self._z[row] = value._z self._x[row] = value._x self._phase[row] = phase else: self._z[row, qubit] = value._z self._x[row, qubit] = value._x self._phase[row] += phase self._phase %= 4
[docs] def delete(self, ind: int | list, qubit: bool = False) -> PauliList: """Return a copy with Pauli rows deleted from table. When deleting qubits the qubit index is the same as the column index of the underlying :attr:`X` and :attr:`Z` arrays. Args: ind (int or list): index(es) to delete. qubit (bool): if ``True`` delete qubit columns, otherwise delete Pauli rows (Default: ``False``). Returns: PauliList: the resulting table with the entries removed. Raises: QiskitError: if ``ind`` is out of bounds for the array size or number of qubits. """ if isinstance(ind, int): ind = [ind] if len(ind) == 0: return PauliList.from_symplectic(self._z, self._x, self.phase) # Row deletion if not qubit: if max(ind) >= len(self): raise QiskitError( "Indices {} are not all less than the size" " of the PauliList ({})".format(ind, len(self)) ) z = np.delete(self._z, ind, axis=0) x = np.delete(self._x, ind, axis=0) phase = np.delete(self._phase, ind) return PauliList(BasePauli(z, x, phase)) # Column (qubit) deletion if max(ind) >= self.num_qubits: raise QiskitError( "Indices {} are not all less than the number of" " qubits in the PauliList ({})".format(ind, self.num_qubits) ) z = np.delete(self._z, ind, axis=1) x = np.delete(self._x, ind, axis=1) # Use self.phase, not self._phase as deleting qubits can change the # ZX phase convention return PauliList.from_symplectic(z, x, self.phase)
[docs] def insert(self, ind: int, value: PauliList, qubit: bool = False) -> PauliList: """Insert Paulis into the table. When inserting qubits the qubit index is the same as the column index of the underlying :attr:`X` and :attr:`Z` arrays. Args: ind (int): index to insert at. value (PauliList): values to insert. qubit (bool): if ``True`` insert qubit columns, otherwise insert Pauli rows (Default: ``False``). Returns: PauliList: the resulting table with the entries inserted. Raises: QiskitError: if the insertion index is invalid. """ if not isinstance(ind, int): raise QiskitError("Insert index must be an integer.") if not isinstance(value, PauliList): value = PauliList(value) # Row insertion size = self._num_paulis if not qubit: if ind > size: raise QiskitError( "Index {} is larger than the number of rows in the" " PauliList ({}).".format(ind, size) ) base_z = np.insert(self._z, ind, value._z, axis=0) base_x = np.insert(self._x, ind, value._x, axis=0) base_phase = np.insert(self._phase, ind, value._phase) return PauliList(BasePauli(base_z, base_x, base_phase)) # Column insertion if ind > self.num_qubits: raise QiskitError( "Index {} is greater than number of qubits" " in the PauliList ({})".format(ind, self.num_qubits) ) if len(value) == 1: # Pad blocks to correct size value_x = np.vstack(size * [value.x]) value_z = np.vstack(size * [value.z]) value_phase = np.vstack(size * [value.phase]) elif len(value) == size: # Blocks are already correct size value_x = value.x value_z = value.z value_phase = value.phase else: # Blocks are incorrect size raise QiskitError( "Input PauliList must have a single row, or" " the same number of rows as the Pauli Table" " ({}).".format(size) ) # Build new array by blocks z = np.hstack([self.z[:, :ind], value_z, self.z[:, ind:]]) x = np.hstack([self.x[:, :ind], value_x, self.x[:, ind:]]) phase = self.phase + value_phase return PauliList.from_symplectic(z, x, phase)
[docs] def argsort(self, weight: bool = False, phase: bool = False) -> np.ndarray: """Return indices for sorting the rows of the table. The default sort method is lexicographic sorting by qubit number. By using the `weight` kwarg the output can additionally be sorted by the number of non-identity terms in the Pauli, where the set of all Paulis of a given weight are still ordered lexicographically. Args: weight (bool): Optionally sort by weight if ``True`` (Default: ``False``). phase (bool): Optionally sort by phase before weight or order (Default: ``False``). Returns: array: the indices for sorting the table. """ # Get order of each Pauli using # I => 0, X => 1, Y => 2, Z => 3 x = self.x z = self.z order = 1 * (x & ~z) + 2 * (x & z) + 3 * (~x & z) phases = self.phase # Optionally get the weight of Pauli # This is the number of non identity terms if weight: weights = np.sum(x | z, axis=1) # To preserve ordering between successive sorts we # are use the 'stable' sort method indices = np.arange(self._num_paulis) # Initial sort by phases sort_inds = phases.argsort(kind="stable") indices = indices[sort_inds] order = order[sort_inds] if phase: phases = phases[sort_inds] if weight: weights = weights[sort_inds] # Sort by order for i in range(self.num_qubits): sort_inds = order[:, i].argsort(kind="stable") order = order[sort_inds] indices = indices[sort_inds] if weight: weights = weights[sort_inds] if phase: phases = phases[sort_inds] # If using weights we implement a sort by total number # of non-identity Paulis if weight: sort_inds = weights.argsort(kind="stable") indices = indices[sort_inds] phases = phases[sort_inds] # If sorting by phase we perform a final sort by the phase value # of each pauli if phase: indices = indices[phases.argsort(kind="stable")] return indices
[docs] def sort(self, weight: bool = False, phase: bool = False) -> PauliList: """Sort the rows of the table. The default sort method is lexicographic sorting by qubit number. By using the `weight` kwarg the output can additionally be sorted by the number of non-identity terms in the Pauli, where the set of all Paulis of a given weight are still ordered lexicographically. **Example** Consider sorting all a random ordering of all 2-qubit Paulis .. code-block:: from numpy.random import shuffle from qiskit.quantum_info.operators import PauliList # 2-qubit labels labels = ['II', 'IX', 'IY', 'IZ', 'XI', 'XX', 'XY', 'XZ', 'YI', 'YX', 'YY', 'YZ', 'ZI', 'ZX', 'ZY', 'ZZ'] # Shuffle Labels shuffle(labels) pt = PauliList(labels) print('Initial Ordering') print(pt) # Lexicographic Ordering srt = pt.sort() print('Lexicographically sorted') print(srt) # Weight Ordering srt = pt.sort(weight=True) print('Weight sorted') print(srt) .. parsed-literal:: Initial Ordering ['YX', 'ZZ', 'XZ', 'YI', 'YZ', 'II', 'XX', 'XI', 'XY', 'YY', 'IX', 'IZ', 'ZY', 'ZI', 'ZX', 'IY'] Lexicographically sorted ['II', 'IX', 'IY', 'IZ', 'XI', 'XX', 'XY', 'XZ', 'YI', 'YX', 'YY', 'YZ', 'ZI', 'ZX', 'ZY', 'ZZ'] Weight sorted ['II', 'IX', 'IY', 'IZ', 'XI', 'YI', 'ZI', 'XX', 'XY', 'XZ', 'YX', 'YY', 'YZ', 'ZX', 'ZY', 'ZZ'] Args: weight (bool): optionally sort by weight if ``True`` (Default: ``False``). phase (bool): Optionally sort by phase before weight or order (Default: ``False``). Returns: PauliList: a sorted copy of the original table. """ return self[self.argsort(weight=weight, phase=phase)]
[docs] def unique(self, return_index: bool = False, return_counts: bool = False) -> PauliList: """Return unique Paulis from the table. **Example** .. code-block:: from qiskit.quantum_info.operators import PauliList pt = PauliList(['X', 'Y', '-X', 'I', 'I', 'Z', 'X', 'iZ']) unique = pt.unique() print(unique) .. parsed-literal:: ['X', 'Y', '-X', 'I', 'Z', 'iZ'] Args: return_index (bool): If ``True``, also return the indices that result in the unique array. (Default: ``False``) return_counts (bool): If ``True``, also return the number of times each unique item appears in the table. Returns: PauliList: unique the table of the unique rows. unique_indices: np.ndarray, optional The indices of the first occurrences of the unique values in the original array. Only provided if ``return_index`` is ``True``. unique_counts: np.array, optional The number of times each of the unique values comes up in the original array. Only provided if ``return_counts`` is ``True``. """ # Check if we need to stack the phase array if np.any(self._phase != self._phase[0]): # Create a single array of Pauli's and phases for calling np.unique on # so that we treat different phased Pauli's as unique array = np.hstack([self._z, self._x, self.phase.reshape((self.phase.shape[0], 1))]) else: # All Pauli's have the same phase so we only need to sort the array array = np.hstack([self._z, self._x]) # Get indexes of unique entries if return_counts: _, index, counts = np.unique(array, return_index=True, return_counts=True, axis=0) else: _, index = np.unique(array, return_index=True, axis=0) # Sort the index so we return unique rows in the original array order sort_inds = index.argsort() index = index[sort_inds] unique = PauliList(BasePauli(self._z[index], self._x[index], self._phase[index])) # Concatinate return tuples ret = (unique,) if return_index: ret += (index,) if return_counts: ret += (counts[sort_inds],) if len(ret) == 1: return ret[0] return ret
# --------------------------------------------------------------------- # BaseOperator methods # ---------------------------------------------------------------------
[docs] def tensor(self, other: PauliList) -> PauliList: """Return the tensor product with each Pauli in the list. Args: other (PauliList): another PauliList. Returns: PauliList: the list of tensor product Paulis. Raises: QiskitError: if other cannot be converted to a PauliList, does not have either 1 or the same number of Paulis as the current list. """ if not isinstance(other, PauliList): other = PauliList(other) return PauliList(super().tensor(other))
[docs] def expand(self, other: PauliList) -> PauliList: """Return the expand product of each Pauli in the list. Args: other (PauliList): another PauliList. Returns: PauliList: the list of tensor product Paulis. Raises: QiskitError: if other cannot be converted to a PauliList, does not have either 1 or the same number of Paulis as the current list. """ if not isinstance(other, PauliList): other = PauliList(other) if len(other) not in [1, len(self)]: raise QiskitError( "Incompatible PauliLists. Other list must " "have either 1 or the same number of Paulis." ) return PauliList(super().expand(other))
[docs] def compose( self, other: PauliList, qargs: None | list = None, front: bool = False, inplace: bool = False, ) -> PauliList: """Return the composition self∘other for each Pauli in the list. Args: other (PauliList): another PauliList. qargs (None or list): qubits to apply dot product on (Default: ``None``). front (bool): If True use `dot` composition method [default: ``False``]. inplace (bool): If ``True`` update in-place (default: ``False``). Returns: PauliList: the list of composed Paulis. Raises: QiskitError: if other cannot be converted to a PauliList, does not have either 1 or the same number of Paulis as the current list, or has the wrong number of qubits for the specified ``qargs``. """ if qargs is None: qargs = getattr(other, "qargs", None) if not isinstance(other, PauliList): other = PauliList(other) if len(other) not in [1, len(self)]: raise QiskitError( "Incompatible PauliLists. Other list must " "have either 1 or the same number of Paulis." ) return PauliList(super().compose(other, qargs=qargs, front=front, inplace=inplace))
[docs] def dot(self, other: PauliList, qargs: None | list = None, inplace: bool = False) -> PauliList: """Return the composition other∘self for each Pauli in the list. Args: other (PauliList): another PauliList. qargs (None or list): qubits to apply dot product on (Default: ``None``). inplace (bool): If True update in-place (default: ``False``). Returns: PauliList: the list of composed Paulis. Raises: QiskitError: if other cannot be converted to a PauliList, does not have either 1 or the same number of Paulis as the current list, or has the wrong number of qubits for the specified ``qargs``. """ return self.compose(other, qargs=qargs, front=True, inplace=inplace)
def _add(self, other, qargs=None): """Append two PauliLists. If ``qargs`` are specified the other operator will be added assuming it is identity on all other subsystems. Args: other (PauliList): another table. qargs (None or list): optional subsystems to add on (Default: ``None``) Returns: PauliList: the concatenated list ``self`` + ``other``. """ if qargs is None: qargs = getattr(other, "qargs", None) if not isinstance(other, PauliList): other = PauliList(other) self._op_shape._validate_add(other._op_shape, qargs) base_phase = np.hstack((self._phase, other._phase)) if qargs is None or (sorted(qargs) == qargs and len(qargs) == self.num_qubits): base_z = np.vstack([self._z, other._z]) base_x = np.vstack([self._x, other._x]) else: # Pad other with identity and then add padded = BasePauli( np.zeros((other.size, self.num_qubits), dtype=bool), np.zeros((other.size, self.num_qubits), dtype=bool), np.zeros(other.size, dtype=int), ) padded = padded.compose(other, qargs=qargs, inplace=True) base_z = np.vstack([self._z, padded._z]) base_x = np.vstack([self._x, padded._x]) return PauliList(BasePauli(base_z, base_x, base_phase)) def _multiply(self, other): """Multiply each Pauli in the list by a phase. Args: other (complex or array): a complex number in [1, -1j, -1, 1j] Returns: PauliList: the list of Paulis other * self. Raises: QiskitError: if the phase is not in the set [1, -1j, -1, 1j]. """ return PauliList(super()._multiply(other))
[docs] def conjugate(self): """Return the conjugate of each Pauli in the list.""" return PauliList(super().conjugate())
[docs] def transpose(self): """Return the transpose of each Pauli in the list.""" return PauliList(super().transpose())
[docs] def adjoint(self): """Return the adjoint of each Pauli in the list.""" return PauliList(super().adjoint())
[docs] def inverse(self): """Return the inverse of each Pauli in the list.""" return PauliList(super().adjoint())
# --------------------------------------------------------------------- # Utility methods # ---------------------------------------------------------------------
[docs] def commutes(self, other: BasePauli, qargs: list | None = None) -> bool: """Return True for each Pauli that commutes with other. Args: other (PauliList): another PauliList operator. qargs (list): qubits to apply dot product on (default: ``None``). Returns: bool: ``True`` if Paulis commute, ``False`` if they anti-commute. """ if qargs is None: qargs = getattr(other, "qargs", None) if not isinstance(other, BasePauli): other = PauliList(other) return super().commutes(other, qargs=qargs)
[docs] def anticommutes(self, other: BasePauli, qargs: list | None = None) -> bool: """Return ``True`` if other Pauli that anticommutes with other. Args: other (PauliList): another PauliList operator. qargs (list): qubits to apply dot product on (default: ``None``). Returns: bool: ``True`` if Paulis anticommute, ``False`` if they commute. """ return np.logical_not(self.commutes(other, qargs=qargs))
[docs] def commutes_with_all(self, other: PauliList) -> np.ndarray: """Return indexes of rows that commute ``other``. If ``other`` is a multi-row Pauli list the returned vector indexes rows of the current PauliList that commute with *all* Paulis in other. If no rows satisfy the condition the returned array will be empty. Args: other (PauliList): a single Pauli or multi-row PauliList. Returns: array: index array of the commuting rows. """ return self._commutes_with_all(other)
[docs] def anticommutes_with_all(self, other: PauliList) -> np.ndarray: """Return indexes of rows that commute other. If ``other`` is a multi-row Pauli list the returned vector indexes rows of the current PauliList that anti-commute with *all* Paulis in other. If no rows satisfy the condition the returned array will be empty. Args: other (PauliList): a single Pauli or multi-row PauliList. Returns: array: index array of the anti-commuting rows. """ return self._commutes_with_all(other, anti=True)
def _commutes_with_all(self, other, anti=False): """Return row indexes that commute with all rows in another PauliList. Args: other (PauliList): a PauliList. anti (bool): if ``True`` return rows that anti-commute, otherwise return rows that commute (Default: ``False``). Returns: array: index array of commuting or anti-commuting row. """ if not isinstance(other, PauliList): other = PauliList(other) comms = self.commutes(other[0]) (inds,) = np.where(comms == int(not anti)) for pauli in other[1:]: comms = self[inds].commutes(pauli) (new_inds,) = np.where(comms == int(not anti)) if new_inds.size == 0: # No commuting rows return new_inds inds = inds[new_inds] return inds
[docs] def evolve( self, other: Pauli | Clifford | QuantumCircuit, qargs: list | None = None, frame: Literal["h", "s"] = "h", ) -> Pauli: r"""Performs either Heisenberg (default) or Schrödinger picture evolution of the Pauli by a Clifford and returns the evolved Pauli. Schrödinger picture evolution can be chosen by passing parameter ``frame='s'``. This option yields a faster calculation. Heisenberg picture evolves the Pauli as :math:`P^\prime = C^\dagger.P.C`. Schrödinger picture evolves the Pauli as :math:`P^\prime = C.P.C^\dagger`. Args: other (Pauli or Clifford or QuantumCircuit): The Clifford operator to evolve by. qargs (list): a list of qubits to apply the Clifford to. frame (string): ``'h'`` for Heisenberg (default) or ``'s'`` for Schrödinger framework. Returns: PauliList: the Pauli :math:`C^\dagger.P.C` (Heisenberg picture) or the Pauli :math:`C.P.C^\dagger` (Schrödinger picture). Raises: QiskitError: if the Clifford number of qubits and qargs don't match. """ from qiskit.circuit import Instruction if qargs is None: qargs = getattr(other, "qargs", None) if not isinstance(other, (BasePauli, Instruction, QuantumCircuit, Clifford)): # Convert to a PauliList other = PauliList(other) return PauliList(super().evolve(other, qargs=qargs, frame=frame))
[docs] def to_labels(self, array: bool = False): r"""Convert a PauliList to a list Pauli string labels. For large PauliLists converting using the ``array=True`` kwarg will be more efficient since it allocates memory for the full Numpy array of labels in advance. .. list-table:: Pauli Representations :header-rows: 1 * - Label - Symplectic - Matrix * - ``"I"`` - :math:`[0, 0]` - :math:`\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}` * - ``"X"`` - :math:`[1, 0]` - :math:`\begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix}` * - ``"Y"`` - :math:`[1, 1]` - :math:`\begin{bmatrix} 0 & -i \\ i & 0 \end{bmatrix}` * - ``"Z"`` - :math:`[0, 1]` - :math:`\begin{bmatrix} 1 & 0 \\ 0 & -1 \end{bmatrix}` Args: array (bool): return a Numpy array if ``True``, otherwise return a list (Default: ``False``). Returns: list or array: The rows of the PauliList in label form. """ if (self.phase == 1).any(): prefix_len = 2 elif (self.phase > 0).any(): prefix_len = 1 else: prefix_len = 0 str_len = self.num_qubits + prefix_len ret = np.zeros(self.size, dtype=f"<U{str_len}") iterator = self.label_iter() for i in range(self.size): ret[i] = next(iterator) if array: return ret return ret.tolist()
[docs] def to_matrix(self, sparse: bool = False, array: bool = False) -> list: r"""Convert to a list or array of Pauli matrices. For large PauliLists converting using the ``array=True`` kwarg will be more efficient since it allocates memory a full rank-3 Numpy array of matrices in advance. .. list-table:: Pauli Representations :header-rows: 1 * - Label - Symplectic - Matrix * - ``"I"`` - :math:`[0, 0]` - :math:`\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}` * - ``"X"`` - :math:`[1, 0]` - :math:`\begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix}` * - ``"Y"`` - :math:`[1, 1]` - :math:`\begin{bmatrix} 0 & -i \\ i & 0 \end{bmatrix}` * - ``"Z"`` - :math:`[0, 1]` - :math:`\begin{bmatrix} 1 & 0 \\ 0 & -1 \end{bmatrix}` Args: sparse (bool): if ``True`` return sparse CSR matrices, otherwise return dense Numpy arrays (Default: ``False``). array (bool): return as rank-3 numpy array if ``True``, otherwise return a list of Numpy arrays (Default: ``False``). Returns: list: A list of dense Pauli matrices if ``array=False` and ``sparse=False`. list: A list of sparse Pauli matrices if ``array=False`` and ``sparse=True``. array: A dense rank-3 array of Pauli matrices if ``array=True``. """ if not array: # We return a list of Numpy array matrices return list(self.matrix_iter(sparse=sparse)) # For efficiency we also allow returning a single rank-3 # array where first index is the Pauli row, and second two # indices are the matrix indices dim = 2**self.num_qubits ret = np.zeros((self.size, dim, dim), dtype=complex) iterator = self.matrix_iter(sparse=sparse) for i in range(self.size): ret[i] = next(iterator) return ret
# --------------------------------------------------------------------- # Custom Iterators # ---------------------------------------------------------------------
[docs] def label_iter(self): """Return a label representation iterator. This is a lazy iterator that converts each row into the string label only as it is used. To convert the entire table to labels use the :meth:`to_labels` method. Returns: LabelIterator: label iterator object for the PauliList. """ class LabelIterator(CustomIterator): """Label representation iteration and item access.""" def __repr__(self): return f"<PauliList_label_iterator at {hex(id(self))}>" def __getitem__(self, key): return self.obj._to_label(self.obj._z[key], self.obj._x[key], self.obj._phase[key]) return LabelIterator(self)
[docs] def matrix_iter(self, sparse: bool = False): """Return a matrix representation iterator. This is a lazy iterator that converts each row into the Pauli matrix representation only as it is used. To convert the entire table to matrices use the :meth:`to_matrix` method. Args: sparse (bool): optionally return sparse CSR matrices if ``True``, otherwise return Numpy array matrices (Default: ``False``) Returns: MatrixIterator: matrix iterator object for the PauliList. """ class MatrixIterator(CustomIterator): """Matrix representation iteration and item access.""" def __repr__(self): return f"<PauliList_matrix_iterator at {hex(id(self))}>" def __getitem__(self, key): return self.obj._to_matrix( self.obj._z[key], self.obj._x[key], self.obj._phase[key], sparse=sparse ) return MatrixIterator(self)
# --------------------------------------------------------------------- # Class methods # ---------------------------------------------------------------------
[docs] @classmethod def from_symplectic( cls, z: np.ndarray, x: np.ndarray, phase: np.ndarray | None = 0 ) -> PauliList: """Construct a PauliList from a symplectic data. Args: z (np.ndarray): 2D boolean Numpy array. x (np.ndarray): 2D boolean Numpy array. phase (np.ndarray or None): Optional, 1D integer array from Z_4. Returns: PauliList: the constructed PauliList. """ base_z, base_x, base_phase = cls._from_array(z, x, phase) return cls(BasePauli(base_z, base_x, base_phase))
def _noncommutation_graph(self, qubit_wise): """Create an edge list representing the non-commutation graph (Pauli Graph). An edge (i, j) is present if i and j are not commutable. Args: qubit_wise (bool): whether the commutation rule is applied to the whole operator, or on a per-qubit basis. Returns: list[tuple[int,int]]: A list of pairs of indices of the PauliList that are not commutable. """ # convert a Pauli operator into int vector where {I: 0, X: 2, Y: 3, Z: 1} mat1 = np.array( [op.z + 2 * op.x for op in self], dtype=np.int8, ) mat2 = mat1[:, None] # This is 0 (false-y) iff one of the operators is the identity and/or both operators are the # same. In other cases, it is non-zero (truth-y). qubit_anticommutation_mat = (mat1 * mat2) * (mat1 - mat2) # 'adjacency_mat[i, j]' is True iff Paulis 'i' and 'j' do not commute in the given strategy. if qubit_wise: adjacency_mat = np.logical_or.reduce(qubit_anticommutation_mat, axis=2) else: # Don't commute if there's an odd number of element-wise anti-commutations. adjacency_mat = np.logical_xor.reduce(qubit_anticommutation_mat, axis=2) # Convert into list where tuple elements are non-commuting operators. We only want to # results from one triangle to avoid symmetric duplications. return list(zip(*np.where(np.triu(adjacency_mat, k=1)))) def _create_graph(self, qubit_wise): """Transform measurement operator grouping problem into graph coloring problem Args: qubit_wise (bool): whether the commutation rule is applied to the whole operator, or on a per-qubit basis. Returns: rustworkx.PyGraph: A class of undirected graphs """ edges = self._noncommutation_graph(qubit_wise) graph = rx.PyGraph() graph.add_nodes_from(range(self.size)) graph.add_edges_from_no_data(edges) return graph
[docs] def group_qubit_wise_commuting(self) -> list[PauliList]: """Partition a PauliList into sets of mutually qubit-wise commuting Pauli strings. Returns: list[PauliList]: List of PauliLists where each PauliList contains commutable Pauli operators. """ return self.group_commuting(qubit_wise=True)
[docs] def group_commuting(self, qubit_wise: bool = False) -> list[PauliList]: """Partition a PauliList into sets of commuting Pauli strings. Args: qubit_wise (bool): whether the commutation rule is applied to the whole operator, or on a per-qubit basis. For example: .. code-block:: python >>> from qiskit.quantum_info import PauliList >>> op = PauliList(["XX", "YY", "IZ", "ZZ"]) >>> op.group_commuting() [PauliList(['XX', 'YY']), PauliList(['IZ', 'ZZ'])] >>> op.group_commuting(qubit_wise=True) [PauliList(['XX']), PauliList(['YY']), PauliList(['IZ', 'ZZ'])] Returns: list[PauliList]: List of PauliLists where each PauliList contains commuting Pauli operators. """ graph = self._create_graph(qubit_wise) # Keys in coloring_dict are nodes, values are colors coloring_dict = rx.graph_greedy_color(graph) groups = defaultdict(list) for idx, color in coloring_dict.items(): groups[color].append(idx) return [self[group] for group in groups.values()]