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

# This code is part of Qiskit.
# (C) Copyright IBM 2017, 2020
# 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
# 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.
N-qubit Pauli Operator Class

from __future__ import annotations

import re
import warnings
from typing import Literal, TYPE_CHECKING

import numpy as np

from qiskit.circuit import Instruction, QuantumCircuit
from qiskit.circuit.barrier import Barrier
from qiskit.circuit.delay import Delay
from qiskit.circuit.library.generalized_gates import PauliGate
from qiskit.circuit.library.standard_gates import IGate, XGate, YGate, ZGate
from qiskit.exceptions import QiskitError
from qiskit.quantum_info.operators.mixins import generate_apidocs
from qiskit.quantum_info.operators.scalar_op import ScalarOp
from qiskit.quantum_info.operators.symplectic.base_pauli import BasePauli, _count_y

    from qiskit.quantum_info.operators.symplectic.clifford import Clifford
    from qiskit.quantum_info.operators.symplectic.pauli_list import PauliList

[docs]class Pauli(BasePauli): r"""N-qubit Pauli operator. This class represents an operator :math:`P` from the full :math:`n`-qubit *Pauli* group .. math:: P = (-i)^{q} P_{n-1} \otimes ... \otimes P_{0} where :math:`q\in \mathbb{Z}_4` and :math:`P_i \in \{I, X, Y, Z\}` are single-qubit Pauli matrices: .. math:: I = \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}, X = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}, Y = \begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix}, Z = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix}. **Initialization** A Pauli object can be initialized in several ways: ``Pauli(obj)`` where ``obj`` is a Pauli string, ``Pauli`` or :class:`~qiskit.quantum_info.ScalarOp` operator, or a Pauli gate or :class:`~qiskit.QuantumCircuit` containing only Pauli gates. ``Pauli((z, x, phase))`` where ``z`` and ``x`` are boolean ``numpy.ndarrays`` and ``phase`` is an integer in ``[0, 1, 2, 3]``. ``Pauli((z, x))`` equivalent to ``Pauli((z, x, 0))`` with trivial phase. **String representation** An :math:`n`-qubit Pauli may be represented by a string consisting of :math:`n` characters from ``['I', 'X', 'Y', 'Z']``, and optionally phase coefficient in :math:`['', '-i', '-', 'i']`. For example: ``XYZ`` or ``'-iZIZ'``. In the string representation qubit-0 corresponds to the right-most Pauli character, and qubit-:math:`(n-1)` to the left-most Pauli character. For example ``'XYZ'`` represents :math:`X\otimes Y \otimes Z` with ``'Z'`` on qubit-0, ``'Y'`` on qubit-1, and ``'X'`` on qubit-3. The string representation can be converted to a ``Pauli`` using the class initialization (``Pauli('-iXYZ')``). A ``Pauli`` object can be converted back to the string representation using the :meth:`to_label` method or ``str(pauli)``. .. note:: Using ``str`` to convert a ``Pauli`` to a string will truncate the returned string for large numbers of qubits while :meth:`to_label` will return the full string with no truncation. The default truncation length is 50 characters. The default value can be changed by setting the class `__truncate__` attribute to an integer value. If set to ``0`` no truncation will be performed. **Array Representation** The internal data structure of an :math:`n`-qubit Pauli is two length-:math:`n` boolean vectors :math:`z \in \mathbb{Z}_2^N`, :math:`x \in \mathbb{Z}_2^N`, and an integer :math:`q \in \mathbb{Z}_4` defining the Pauli operator .. math:: P = (-i)^{q + z\cdot x} Z^z \cdot X^x. The :math:`k`th qubit corresponds to the :math:`k`th entry in the :math:`z` and :math:`x` arrays .. math:: P &= P_{n-1} \otimes ... \otimes P_{0} \\ P_k &= (-i)^{z[k] * x[k]} Z^{z[k]}\cdot X^{x[k]} where ``z[k] = P.z[k]``, ``x[k] = P.x[k]`` respectively. The :math:`z` and :math:`x` arrays can be accessed and updated using the :attr:`z` and :attr:`x` properties respectively. The phase integer :math:`q` can be accessed and updated using the :attr:`phase` property. **Matrix Operator Representation** Pauli's can be converted to :math:`(2^n, 2^n)` :class:`~qiskit.quantum_info.Operator` using the :meth:`to_operator` method, or to a dense or sparse complex matrix using the :meth:`to_matrix` method. **Data Access** The individual qubit Paulis can be accessed and updated using the ``[]`` operator which accepts integer, lists, or slices for selecting subsets of Paulis. Note that selecting subsets of Pauli's will discard the phase of the current Pauli. For example .. code-block:: python p = Pauli('-iXYZ') print('P[0] =', repr(P[0])) print('P[1] =', repr(P[1])) print('P[2] =', repr(P[2])) print('P[:] =', repr(P[:])) print('P[::-1] =, repr(P[::-1])) """ # Set the max Pauli string size before truncation __truncate__ = 50 _VALID_LABEL_PATTERN = re.compile(r"(?P<coeff>[+-]?1?[ij]?)(?P<pauli>[IXYZ]*)") _CANONICAL_PHASE_LABEL = {"": 0, "-i": 1, "-": 2, "i": 3} def __init__( self, data: str | tuple | Pauli | ScalarOp | QuantumCircuit | None = None, x=None, *, z=None, label=None, ): """Initialize the Pauli. When using the symplectic array input data both z and x arguments must be provided, however the first (z) argument can be used alone for string label, Pauli operator, or ScalarOp input data. Args: data (str or tuple or Pauli or ScalarOp): input data for Pauli. If input is a tuple it must be of the form ``(z, x)`` or (z, x, phase)`` where ``z`` and ``x`` are boolean Numpy arrays, and phase is an integer from Z_4. If input is a string, it must be a concatenation of a phase and a Pauli string (e.g. 'XYZ', '-iZIZ') where a phase string is a combination of at most three characters from ['+', '-', ''], ['1', ''], and ['i', 'j', ''] in this order, e.g. '', '-1j' while a Pauli string is 1 or more characters of 'I', 'X', 'Y' or 'Z', e.g. 'Z', 'XIYY'. Raises: QiskitError: if input array is invalid shape. """ if isinstance(data, BasePauli): base_z, base_x, base_phase = data._z, data._x, data._phase elif isinstance(data, tuple): if len(data) not in [2, 3]: raise QiskitError( "Invalid input tuple for Pauli, input tuple must be `(z, x, phase)` or `(z, x)`" ) base_z, base_x, base_phase = self._from_array(*data) elif isinstance(data, str): base_z, base_x, base_phase = self._from_label(data) elif isinstance(data, ScalarOp): base_z, base_x, base_phase = self._from_scalar_op(data) elif isinstance(data, (QuantumCircuit, Instruction)): base_z, base_x, base_phase = self._from_circuit(data) elif x is not None: if z is None: # Using old Pauli initialization with positional args instead of kwargs z = data warnings.warn( "Passing 'z' and 'x' arrays separately to 'Pauli' is deprecated as of" " Qiskit Terra 0.17 and will be removed in version 0.23 or later." " Use a tuple instead, such as 'Pauli((z, x[, phase]))'.", DeprecationWarning, stacklevel=2, ) base_z, base_x, base_phase = self._from_array(z, x) elif label is not None: warnings.warn( "The 'label' keyword argument of 'Pauli' is deprecated as of" " Qiskit Terra 0.17 and will be removed in version 0.23 or later." " Pass the label positionally instead, such as 'Pauli(\"XYZ\")'.", DeprecationWarning, stacklevel=2, ) base_z, base_x, base_phase = self._from_label(label) else: raise QiskitError("Invalid input data for Pauli.") # Initialize BasePauli if base_z.shape[0] != 1: raise QiskitError("Input is not a single Pauli") super().__init__(base_z, base_x, base_phase) @property def name(self): """Unique string identifier for operation type.""" return "pauli" @property def num_clbits(self): """Number of classical bits.""" return 0 def __repr__(self): """Display representation.""" return f"Pauli('{self.__str__()}')" def __str__(self): """Print representation.""" if self.__truncate__ and self.num_qubits > self.__truncate__: front = self[-self.__truncate__ :].to_label() return front + "..." return self.to_label() def __array__(self, dtype=None): if dtype: return np.asarray(self.to_matrix(), dtype=dtype) return self.to_matrix()
[docs] @classmethod def set_truncation(cls, val: int): """Set the max number of Pauli characters to display before truncation/ Args: val (int): the number of characters. .. note:: Truncation will be disabled if the truncation value is set to 0. """ cls.__truncate__ = int(val)
def __eq__(self, other): """Test if two Paulis are equal.""" if not isinstance(other, BasePauli): return False return self._eq(other)
[docs] def equiv(self, other: Pauli) -> bool: """Return True if Pauli's are equivalent up to group phase. Args: other (Pauli): an operator object. Returns: bool: True if the Pauli's are equivalent up to group phase. """ if not isinstance(other, Pauli): try: other = Pauli(other) except QiskitError: return False return np.all(self._z == other._z) and np.all(self._x == other._x)
@property def settings(self) -> dict: """Return settings.""" return {"data": self.to_label()} # --------------------------------------------------------------------- # Direct array access # --------------------------------------------------------------------- @property def phase(self): """Return the group phase exponent for the Pauli.""" # Convert internal ZX-phase convention of BasePauli to group phase return np.mod(self._phase - self._count_y(dtype=self._phase.dtype), 4)[0] @phase.setter def phase(self, value): # Convert group phase convention to internal ZX-phase convention self._phase[:] = np.mod(value + self._count_y(dtype=self._phase.dtype), 4) @property def x(self): """The x vector for the Pauli.""" return self._x[0] @x.setter def x(self, val): self._x[0, :] = val @property def z(self): """The z vector for the Pauli.""" return self._z[0] @z.setter def z(self, val): self._z[0, :] = val # --------------------------------------------------------------------- # Pauli Array methods # --------------------------------------------------------------------- def __len__(self): """Return the number of qubits in the Pauli.""" return self.num_qubits def __getitem__(self, qubits): """Return the unsigned Pauli group Pauli for subset of qubits.""" # Set group phase to 0 so returned Pauli is always +1 coeff if isinstance(qubits, (int, np.integer)): qubits = [qubits] return Pauli((self.z[qubits], self.x[qubits])) def __setitem__(self, qubits, value): """Update the Pauli for a subset of qubits.""" if not isinstance(value, Pauli): value = Pauli(value) self._z[0, qubits] = value.z self._x[0, qubits] = value.x # Add extra phase from new Pauli to current self._phase = self._phase + value._phase
[docs] def delete(self, qubits: int | list) -> Pauli: """Return a Pauli with qubits deleted. Args: qubits (int or list): qubits to delete from Pauli. Returns: Pauli: the resulting Pauli with the specified qubits removed. Raises: QiskitError: if ind is out of bounds for the array size or number of qubits. """ if isinstance(qubits, (int, np.integer)): qubits = [qubits] if max(qubits) > self.num_qubits - 1: raise QiskitError( "Qubit index is larger than the number of qubits " "({}>{}).".format(max(qubits), self.num_qubits - 1) ) if len(qubits) == self.num_qubits: raise QiskitError("Cannot delete all qubits of Pauli") z = np.delete(self._z, qubits, axis=1) x = np.delete(self._x, qubits, axis=1) return Pauli((z, x, self.phase))
[docs] def insert(self, qubits: int | list, value: Pauli) -> Pauli: """Insert a Pauli at specific qubit value. Args: qubits (int or list): qubits index to insert at. value (Pauli): value to insert. Returns: Pauli: the resulting Pauli with the entries inserted. Raises: QiskitError: if the insertion qubits are invalid. """ if not isinstance(value, Pauli): value = Pauli(value) # Initialize empty operator ret_qubits = self.num_qubits + value.num_qubits ret = Pauli((np.zeros(ret_qubits, dtype=bool), np.zeros(ret_qubits, dtype=bool))) if isinstance(qubits, (int, np.integer)): if value.num_qubits == 1: qubits = [qubits] else: qubits = list(range(qubits, qubits + value.num_qubits)) if len(qubits) != value.num_qubits: raise QiskitError( "Number of indices does not match number of qubits for " "the inserted Pauli ({}!={})".format(len(qubits), value.num_qubits) ) if max(qubits) > ret.num_qubits - 1: raise QiskitError( "Index is too larger for combined Pauli number of qubits " "({}>{}).".format(max(qubits), ret.num_qubits - 1) ) # Qubit positions for original op self_qubits = [i for i in range(ret.num_qubits) if i not in qubits] ret[self_qubits] = self ret[qubits] = value return ret
# --------------------------------------------------------------------- # Representation conversions # --------------------------------------------------------------------- def __hash__(self): """Make hashable based on string representation.""" return hash(self.to_label())
[docs] def to_label(self) -> str: """Convert a Pauli to a string label. .. note:: The difference between `to_label` and :meth:`__str__` is that the later will truncate the output for large numbers of qubits. Returns: str: the Pauli string label. """ return self._to_label(self.z, self.x, self._phase[0])
[docs] def to_matrix(self, sparse: bool = False) -> np.ndarray: r"""Convert to a Numpy array or sparse CSR matrix. Args: sparse (bool): if True return sparse CSR matrices, otherwise return dense Numpy arrays (default: False). Returns: array: The Pauli matrix. """ return self._to_matrix(self.z, self.x, self._phase[0], sparse=sparse)
[docs] def to_instruction(self): """Convert to Pauli circuit instruction.""" from math import pi pauli, phase = self._to_label( self.z, self.x, self._phase[0], full_group=False, return_phase=True ) if len(pauli) == 1: gate = {"I": IGate(), "X": XGate(), "Y": YGate(), "Z": ZGate()}[pauli] else: gate = PauliGate(pauli) if not phase: return gate # Add global phase circuit = QuantumCircuit(self.num_qubits, name=str(self)) circuit.global_phase = -phase * pi / 2 circuit.append(gate, range(self.num_qubits)) return circuit.to_instruction()
# --------------------------------------------------------------------- # BaseOperator methods # ---------------------------------------------------------------------
[docs] def compose( self, other: Pauli, qargs: list | None = None, front: bool = False, inplace: bool = False ) -> Pauli: """Return the operator composition with another Pauli. Args: other (Pauli): a Pauli object. qargs (list or None): Optional, qubits to apply dot product on (default: None). front (bool): If True compose using right operator multiplication, instead of left multiplication [default: False]. inplace (bool): If True update in-place (default: False). Returns: Pauli: The composed Pauli. Raises: QiskitError: if other cannot be converted to an operator, or has incompatible dimensions for specified subsystems. .. note:: Composition (``&``) by default is defined as `left` matrix multiplication for matrix operators, while :meth:`dot` is defined as `right` matrix multiplication. That is that ``A & B == A.compose(B)`` is equivalent to ```` when ``A`` and ``B`` are of the same type. Setting the ``front=True`` kwarg changes this to `right` matrix multiplication and is equivalent to the :meth:`dot` method `` == A.compose(B, front=True)``. """ if qargs is None: qargs = getattr(other, "qargs", None) if not isinstance(other, Pauli): other = Pauli(other) return Pauli(super().compose(other, qargs=qargs, front=front, inplace=inplace))
[docs] def dot(self, other: Pauli, qargs: list | None = None, inplace: bool = False) -> Pauli: """Return the right multiplied operator self * other. Args: other (Pauli): an operator object. qargs (list or None): Optional, qubits to apply dot product on (default: None). inplace (bool): If True update in-place (default: False). Returns: Pauli: The operator self * other. """ return self.compose(other, qargs=qargs, front=True, inplace=inplace)
[docs] def tensor(self, other: Pauli) -> Pauli: if not isinstance(other, Pauli): other = Pauli(other) return Pauli(super().tensor(other))
[docs] def expand(self, other: Pauli) -> Pauli: if not isinstance(other, Pauli): other = Pauli(other) return Pauli(super().expand(other))
def _multiply(self, other): return Pauli(super()._multiply(other))
[docs] def conjugate(self): return Pauli(super().conjugate())
[docs] def transpose(self): return Pauli(super().transpose())
[docs] def adjoint(self): return Pauli(super().adjoint())
[docs] def inverse(self): """Return the inverse of the Pauli.""" return Pauli(super().adjoint())
# --------------------------------------------------------------------- # Utility methods # ---------------------------------------------------------------------
[docs] def commutes(self, other: Pauli | PauliList, qargs: list | None = None) -> bool: """Return True if the Pauli commutes with other. Args: other (Pauli or PauliList): another Pauli operator. qargs (list): qubits to apply dot product on (default: None). Returns: bool: True if Pauli's commute, False if they anti-commute. """ if qargs is None: qargs = getattr(other, "qargs", None) if not isinstance(other, BasePauli): other = Pauli(other) ret = super().commutes(other, qargs=qargs) if len(ret) == 1: return ret[0] return ret
[docs] def anticommutes(self, other: Pauli, qargs: list | None = None) -> bool: """Return True if other Pauli anticommutes with self. Args: other (Pauli): another Pauli operator. qargs (list): qubits to apply dot product on (default: None). Returns: bool: True if Pauli's anticommute, False if they commute. """ return np.logical_not(self.commutes(other, qargs=qargs))
[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: Pauli: 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. """ if qargs is None: qargs = getattr(other, "qargs", None) # pylint: disable=cyclic-import from qiskit.quantum_info.operators.symplectic.clifford import Clifford if not isinstance(other, (Pauli, Instruction, QuantumCircuit, Clifford)): # Convert to a Pauli other = Pauli(other) return Pauli(super().evolve(other, qargs=qargs, frame=frame))
# --------------------------------------------------------------------- # Initialization helper functions # --------------------------------------------------------------------- @staticmethod def _from_label(label): """Return the symplectic representation of Pauli string. Args: label (str): the Pauli string label. Returns: BasePauli: the BasePauli corresponding to the label. Raises: QiskitError: if Pauli string is not valid. """ match_ = Pauli._VALID_LABEL_PATTERN.fullmatch(label) if match_ is None: raise QiskitError(f'Pauli string label "{label}" is not valid.') phase = Pauli._CANONICAL_PHASE_LABEL[ (match_["coeff"] or "").replace("1", "").replace("+", "").replace("j", "i") ] # Convert to Symplectic representation pauli_bytes = np.frombuffer(match_["pauli"].encode("ascii"), dtype=np.uint8)[::-1] ys = pauli_bytes == ord("Y") base_x = np.logical_or(pauli_bytes == ord("X"), ys).reshape(1, -1) base_z = np.logical_or(pauli_bytes == ord("Z"), ys).reshape(1, -1) base_phase = np.array([(phase + np.count_nonzero(ys)) % 4], dtype=int) return base_z, base_x, base_phase @classmethod def _from_scalar_op(cls, op): """Convert a ScalarOp to BasePauli data.""" if op.num_qubits is None: raise QiskitError(f"{op} is not an N-qubit identity") base_z = np.zeros((1, op.num_qubits), dtype=bool) base_x = np.zeros((1, op.num_qubits), dtype=bool) base_phase = np.mod( cls._phase_from_complex(op.coeff) + _count_y(base_x, base_z), 4, dtype=int ) return base_z, base_x, base_phase @classmethod def _from_pauli_instruction(cls, instr): """Convert a Pauli instruction to BasePauli data.""" if isinstance(instr, PauliGate): return cls._from_label(instr.params[0]) if isinstance(instr, IGate): return np.array([[False]]), np.array([[False]]), np.array([0]) if isinstance(instr, XGate): return np.array([[False]]), np.array([[True]]), np.array([0]) if isinstance(instr, YGate): return np.array([[True]]), np.array([[True]]), np.array([1]) if isinstance(instr, ZGate): return np.array([[True]]), np.array([[False]]), np.array([0]) raise QiskitError("Invalid Pauli instruction.") @classmethod def _from_circuit(cls, instr): """Convert a Pauli circuit to BasePauli data.""" # Try and convert single instruction if isinstance(instr, (PauliGate, IGate, XGate, YGate, ZGate)): return cls._from_pauli_instruction(instr) if isinstance(instr, Instruction): # Convert other instructions to circuit definition if instr.definition is None: raise QiskitError(f"Cannot apply Instruction: {}") # Convert to circuit instr = instr.definition # Initialize identity Pauli ret = Pauli( BasePauli( np.zeros((1, instr.num_qubits), dtype=bool), np.zeros((1, instr.num_qubits), dtype=bool), np.zeros(1, dtype=int), ) ) # Add circuit global phase if specified if instr.global_phase: ret.phase = cls._phase_from_complex(np.exp(1j * float(instr.global_phase))) # Recursively apply instructions for inner in if inner.clbits: raise QiskitError( f"Cannot apply instruction with classical bits: {}" ) if not isinstance(inner.operation, (Barrier, Delay)): next_instr = BasePauli(*cls._from_circuit(inner.operation)) if next_instr is not None: qargs = [instr.find_bit(tup).index for tup in inner.qubits] ret = ret.compose(next_instr, qargs=qargs) return ret._z, ret._x, ret._phase
# Update docstrings for API docs generate_apidocs(Pauli)