Source code for mpqp.core.instruction.measurement.pauli_string

"""Represents Pauli strings, which is linear combinations of
:class:`PauliMonomial` which is a combination of :class:`PauliAtom`.
:class:`PauliString` objects can be added, subtracted, and multiplied by
scalars. They also support matrix multiplication with other :class:`PauliString`
objects.
"""

from __future__ import annotations

from copy import deepcopy
from functools import reduce
from numbers import Real
from typing import Any, Optional, Union

import numpy as np
import numpy.typing as npt

FixedReal = Union[Real, float]
from mpqp.tools.generics import Matrix
from mpqp.tools.maths import atol, rtol


[docs]class PauliString: """Represents a Pauli string, a linear combination of Pauli monomials. Args: monomials : List of Pauli monomials. Example: >>> from mpqp.core.instruction.measurement.pauli_string import I, X, Y, Z >>> I @ Z + 2 * Y @ I + X @ Z 1*I@Z + 2*Y@I + 1*X@Z Note: Pauli atoms are named ``I``, ``X``, ``Y``, and ``Z``. If you have conflicts with the gates of the same name, you could: - Rename the Pauli atoms: .. code-block:: python from mpqp.measures import X as Pauli_X, Y as Pauli_Y ps = Pauli_X + Pauli_Y/2 - Import the Pauli atoms directly from the module: .. code-block:: python from mpqp.measures import pauli_string ps = pauli_string.X + pauli_string.Y/2 """ def __init__(self, monomials: Optional[list["PauliStringMonomial"]] = None): self._monomials: list[PauliStringMonomial] = [] if monomials is not None: for mono in monomials: if isinstance(mono, PauliStringAtom): mono = PauliStringMonomial(1, [mono]) self._monomials.append(mono) for mono in self._monomials: if mono.nb_qubits != self.monomials[0].nb_qubits: raise ValueError( f"Non homogeneous sizes for given PauliStrings: {monomials}" ) @property def monomials(self) -> list[PauliStringMonomial]: """Gets the monomials of the PauliString. Returns: The list of monomials in the PauliString. """ return self._monomials @property def nb_qubits(self) -> int: """Gets the number of qubits associated with the PauliString. Returns: The number of qubits associated with the PauliString. """ return 0 if len(self._monomials) == 0 else self._monomials[0].nb_qubits def __str__(self): return " + ".join(map(str, self.round().simplify().sort_monomials()._monomials)) def __repr__(self): return " + ".join(map(str, self._monomials)) def __pos__(self) -> "PauliString": return deepcopy(self) def __neg__(self) -> "PauliString": return -1 * self def __iadd__(self, other: "PauliString") -> "PauliString": for mono in other.monomials: if ( len(self._monomials) != 0 and mono.nb_qubits != self._monomials[0].nb_qubits ): raise ValueError( f"Non homogeneous sizes for given PauliStrings: {(self, other)}" ) self._monomials.extend(deepcopy(other.monomials)) return self def __add__(self, other: "PauliString") -> "PauliString": res = deepcopy(self) res += other return res def __isub__(self, other: "PauliString") -> "PauliString": self += -1 * other return self def __sub__(self, other: "PauliString") -> "PauliString": return self + (-1) * other def __imul__(self, other: FixedReal) -> "PauliString": for i, mono in enumerate(self._monomials): if isinstance(mono, PauliStringAtom): self.monomials[i] = PauliStringMonomial(atoms=[mono]) self.monomials[i] *= other return self def __mul__(self, other: FixedReal) -> "PauliString": res = deepcopy(self) res *= other return res def __rmul__(self, other: FixedReal) -> "PauliString": return self * other def __itruediv__(self, other: FixedReal) -> "PauliString": self *= 1 / other # pyright: ignore[reportOperatorIssue] return self def __truediv__(self, other: FixedReal) -> "PauliString": return self * (1 / other) # pyright: ignore[reportOperatorIssue] def __imatmul__(self, other: "PauliString") -> "PauliString": self._monomials = [ mono for s_mono in self.monomials for mono in (s_mono @ other).monomials ] return self def __matmul__(self, other: "PauliString") -> "PauliString": res = deepcopy(self) res @= other return res def __eq__(self, other: object) -> bool: if not isinstance(other, PauliString): return False return self.to_dict() == other.to_dict()
[docs] def simplify(self, inplace: bool = False) -> PauliString: """Simplifies the PauliString by combining identical terms and removing terms with null coefficients. Args: inplace: Indicates if ``simplify`` should update self. Returns: PauliString: A simplified version of the PauliString. Example: >>> from mpqp.core.instruction.measurement.pauli_string import I, X, Y, Z >>> ps = I@I - 2 *I@I + Z@I - Z@I >>> simplified_ps = ps.simplify() >>> print(simplified_ps) -1*I@I """ res = PauliString() for unique_mono_atoms in {tuple(mono.atoms) for mono in self.monomials}: coef = float( sum( [ mono.coef for mono in self.monomials if mono.atoms == list(unique_mono_atoms) ] ).real ) if coef == int(coef): coef = int(coef) if coef != 0: res.monomials.append(PauliStringMonomial(coef, list(unique_mono_atoms))) if len(res.monomials) == 0: res.monomials.append( PauliStringMonomial(0, [I for _ in range(self.nb_qubits)]) ) if inplace: self._monomials = res.monomials return res
[docs] def round(self, round_off_till: int = 4) -> PauliString: """Rounds the coefficients of the PauliString to a specified number of decimal places. Args: round_off_till : Number of decimal places to round the coefficients to. Defaults to 4. Returns: PauliString: A PauliString with coefficients rounded to the specified number of decimal places. Example: >>> from mpqp.core.instruction.measurement.pauli_string import I, X, Y, Z >>> ps = 0.6875*I@I + 0.1275*I@Z >>> rounded_ps = ps.round(1) >>> print(rounded_ps) 0.7*I@I + 0.1*I@Z """ res = PauliString() for mono in self.monomials: coef = float(np.round(float(mono.coef.real), round_off_till)) if coef == int(coef): coef = int(coef) if coef != 0: res.monomials.append(PauliStringMonomial(coef, mono.atoms)) if len(res.monomials) == 0: res.monomials.append( PauliStringMonomial(0, [I for _ in range(self.nb_qubits)]) ) return res
[docs] def sort_monomials(self) -> PauliString: """Sorts the monomials of the PauliString based on their coefficients. Returns: PauliString: A new PauliString object with monomials sorted based on their coefficients, in descending order. """ sorted_monomials = sorted( self.monomials, key=lambda m: tuple(str(atom) for atom in m.atoms) ) return PauliString(sorted_monomials)
[docs] def to_matrix(self) -> Matrix: """Converts the PauliString to a matrix representation. Returns: Matrix representation of the PauliString. Example: >>> from mpqp.core.instruction.measurement.pauli_string import I, X, Y, Z >>> ps = I + Z >>> matrix_representation = ps.to_matrix() >>> print(matrix_representation) [[2.+0.j 0.+0.j] [0.+0.j 0.+0.j]] """ self = self.simplify() return sum( map(lambda m: m.to_matrix(), self.monomials), start=np.zeros((2**self.nb_qubits, 2**self.nb_qubits), dtype=np.complex64), )
[docs] @classmethod def from_matrix(cls, matrix: Matrix) -> PauliString: """Constructs a PauliString from a matrix. Args: matrix: Matrix from which the PauliString is generated Returns: PauliString: Pauli string decomposition of the matrix in parameter. Raises: ValueError: If the input matrix is not square or its dimensions are not a power of 2. Example: >>> ps = PauliString.from_matrix(np.array([[0, 1], [1, 2]])) >>> print(ps) 1*I + 1*X + -1*Z """ if matrix.shape[0] != matrix.shape[1]: raise ValueError("Input matrix must be square.") num_qubits = int(np.log2(matrix.shape[0])) if 2**num_qubits != matrix.shape[0]: raise ValueError("Matrix dimensions must be a power of 2.") # Return the ordered Pauli basis for the n-qubit Pauli basis. pauli_1q = [PauliStringMonomial(1, [atom]) for atom in [I, X, Y, Z]] basis = pauli_1q for _ in range(num_qubits - 1): basis = [p1 @ p2 for p1 in basis for p2 in pauli_1q] pauli_list = PauliString() for i, mat in enumerate(basis): coeff = (np.trace(mat.to_matrix().dot(matrix)) / (2**num_qubits)).real if not np.isclose(coeff, 0, atol=atol, rtol=rtol): mono = basis[i] * coeff pauli_list += mono if len(pauli_list.monomials) == 0: pauli_list.monomials.append( PauliStringMonomial(0, [I for _ in range(num_qubits)]) ) return pauli_list
[docs] def to_dict(self) -> dict[str, float]: """Converts the PauliString object to a dictionary representation. Returns: Dictionary representation of the PauliString object. Example: >>> from mpqp.core.instruction.measurement.pauli_string import I, X, Y, Z >>> ps = 1 * I@Z + 2 * I@I >>> print(ps.to_dict()) {'II': 2, 'IZ': 1} """ self = self.simplify() dict = {} for mono in self.monomials: atom_str = "" for atom in mono.atoms: atom_str += str(atom) if atom_str not in dict: dict[atom_str] = mono.coef else: dict[atom_str] += mono.coef return {k: dict[k] for k in sorted(dict)}
def __hash__(self): monomials_as_tuples = tuple( tuple((atom.label for atom in mono.atoms) for mono in self.monomials) ) return hash(monomials_as_tuples)
[docs]class PauliStringMonomial(PauliString): """Represents a monomial in a Pauli string, consisting of a coefficient and a list of PauliStringAtom objects. Args: coef: The coefficient of the monomial. atoms: The list of PauliStringAtom objects forming the monomial. """ def __init__( self, coef: Real | float = 1, atoms: Optional[list["PauliStringAtom"]] = None ): self.coef = coef self.atoms = [] if atoms is None else atoms @property def nb_qubits(self) -> int: return len(self.atoms) @property def monomials(self) -> list["PauliStringMonomial"]: return [PauliStringMonomial(self.coef, self.atoms)] def __str__(self): return f"{self.coef}*{'@'.join(map(str,self.atoms))}" def __repr__(self): return str(self)
[docs] def to_matrix(self) -> Matrix: return ( reduce( np.kron, map(lambda a: a.to_matrix(), self.atoms), np.eye(1, dtype=np.complex64).tolist(), ) * self.coef ) # pyright: ignore[reportReturnType]
def __iadd__(self, other: "PauliString"): for mono in other.monomials: if ( len(self.monomials) != 0 and mono.nb_qubits != self.monomials[0].nb_qubits ): raise ValueError( f"Non homogeneous sizes for given PauliStrings: {(self, other)}" ) res = PauliString([self]) res.monomials.extend(deepcopy(other.monomials)) return res def __add__(self, other: "PauliString") -> PauliString: res = deepcopy(self) res += other return res def __imul__(self, other: FixedReal) -> PauliStringMonomial: self.coef *= other return self def __mul__(self, other: FixedReal) -> PauliStringMonomial: res = deepcopy(self) res *= other return res def __itruediv__(self, other: FixedReal) -> PauliStringMonomial: self.coef /= other return self def __truediv__(self, other: FixedReal) -> PauliStringMonomial: res = deepcopy(self) res /= other return res def __imatmul__(self, other: PauliString) -> PauliString: if isinstance(other, PauliStringAtom): self.atoms.append(other) return self elif isinstance(other, PauliStringMonomial): self.coef *= other.coef self.atoms.extend(other.atoms) return self else: res = deepcopy(other) res._monomials = [ mono for s_mono in self.monomials for mono in (other @ s_mono)._monomials ] return res def __matmul__(self, other: PauliString): res = deepcopy(self) res @= other return res
[docs] def simplify(self, inplace: bool = False): return deepcopy(self)
def __eq__(self, other: object) -> bool: if isinstance(other, PauliStringMonomial): for a1, a2 in zip(self.atoms, other.atoms): if a1 != a2: return False return self.coef == other.coef return super().__eq__(other) def __hash__(self): atoms_as_tuples = tuple((atom.label for atom in self.atoms)) return hash(atoms_as_tuples)
[docs]class PauliStringAtom(PauliStringMonomial): """Represents a single Pauli operator acting on a qubit in a Pauli string. Args: label: The label representing the Pauli operator. matrix: The matrix representation of the Pauli operator. Raises: RuntimeError: New atoms cannot be created, you should use the available ones. Note: All the atoms are already initialized. Available atoms are (``I``, ``X``, ``Y``, ``Z``). """ __is_mutable = True def __init__(self, label: str, matrix: npt.NDArray[np.complex64]): if _allow_atom_creation: self.label = label self.matrix = matrix self.__is_mutable = False else: raise RuntimeError( "New atoms cannot be created, just use the given `I`, `X`, `Y` " "and `Z`" ) @property def nb_qubits(self) -> int: return 1 @property def atoms(self): return [self] @property def coef(self): return 1 @property def monomials(self): return [PauliStringMonomial(self.coef, [a for a in self.atoms])] def __setattr__(self, name: str, value: Any): if self.__is_mutable: super().__setattr__(name, value) else: raise AttributeError("This object is immutable") def __str__(self): return self.label def __repr__(self): return str(self) def __truediv__(self, other: FixedReal) -> PauliStringMonomial: return PauliStringMonomial( 1 / other, [self] # pyright: ignore[reportArgumentType] ) def __imul__(self, other: FixedReal) -> PauliStringMonomial: return self * other def __mul__(self, other: FixedReal) -> PauliStringMonomial: return PauliStringMonomial(other, [self]) def __rmul__(self, other: FixedReal) -> PauliStringMonomial: return PauliStringMonomial(other, [self]) def __matmul__(self, other: PauliString) -> PauliString: res = ( PauliStringMonomial(1, [other]) if isinstance(other, PauliStringAtom) else deepcopy(other) ) if isinstance(res, PauliStringMonomial): res.atoms.insert(0, self) else: for i, mono in enumerate(res.monomials): res.monomials[i] = PauliStringMonomial(mono.coef, mono.atoms) res.monomials[i].atoms.insert(0, self) return res
[docs] def to_matrix(self) -> npt.NDArray[np.complex64]: return self.matrix
def __eq__(self, other: object) -> bool: if isinstance(other, PauliStringAtom): return self.label == other.label else: return super().__eq__(other) def __hash__(self): return hash(self.label)
_allow_atom_creation = True I = PauliStringAtom("I", np.eye(2, dtype=np.complex64)) r"""Pauli-I atom representing the identity operator in a Pauli monomial or string. Matrix representation: `\begin{pmatrix}1&0\\0&1\end{pmatrix}` """ X = PauliStringAtom("X", 1 - np.eye(2, dtype=np.complex64)) r"""Pauli-X atom representing the X operator in a Pauli monomial or string. Matrix representation: `\begin{pmatrix}0&1\\1&0\end{pmatrix}` """ Y = PauliStringAtom("Y", np.fliplr(np.diag([1j, -1j]))) r"""Pauli-Y atom representing the Y operator in a Pauli monomial or string. Matrix representation: `\begin{pmatrix}0&-i\\i&0\end{pmatrix}` """ Z = PauliStringAtom("Z", np.diag([1, -1])) r"""Pauli-Z atom representing the Z operator in a Pauli monomial or string. Matrix representation: `\begin{pmatrix}1&0\\0&-1\end{pmatrix}` """ _allow_atom_creation = False