Source code for loom.eka.pauli_operator

"""
Copyright (c) Entropica Labs Pte Ltd 2025.

Use, distribution and reproduction of this program in its source or compiled
form is prohibited without the express written consent of Entropica Labs Pte
Ltd.

"""

from __future__ import annotations
from uuid import uuid4

from pydantic.dataclasses import dataclass
from pydantic import field_validator, Field
import numpy as np

from .utilities.validation_tools import (
    nr_of_qubits_error,
    distinct_error,
    dataclass_params,
    ensure_tuple,
    coordinate_length_error,
    pauli_error,
)
from .utilities.pauli_format_conversion import paulichar_to_xz, paulixz_to_char_npfunc
from .utilities.pauli_binary_vector_rep import SignedPauliOp


[docs] @dataclass(**dataclass_params) class PauliOperator: """ A PauliOperator is defined by a pauli string, and a set of data qubits. Parameters ---------- pauli: str The Pauli string that defines this operator. data_qubits: tuple[tuple[int, ...], ...] Qubits involved in the operator. They are referred to by their coordinates in the lattice. uuid : str Unique identifier of the operator. This is automatically set to a random UUID. """ pauli: str data_qubits: tuple[tuple[int, ...], ...] uuid: str = Field(default_factory=lambda: str(uuid4()), validate_default=True) # Validation functions _validate_pauli = field_validator("pauli")(pauli_error) _validate_qubits_list = field_validator("data_qubits", mode="before")(ensure_tuple) _validate_number_qubits = field_validator("data_qubits")(nr_of_qubits_error) _validate_distinct_qubits = field_validator("data_qubits")(distinct_error) _validate_coordinate_lengths_qubits = field_validator("data_qubits", mode="before")( coordinate_length_error ) # Magic methods def __str__(self) -> str: pauli_ops = [ f"{p}_{idx}" for p, idx in zip(self.pauli, self.data_qubits, strict=True) ] return " ".join(pauli_ops) def __repr__(self) -> str: # use the __str__ method to represent the PauliOperator but add the class name return f"{self.__class__.__name__}({self})" def __eq__(self, other: PauliOperator) -> bool: """ Ignore the uuid in the equality check. """ if not isinstance(other, PauliOperator): return NotImplemented return dict(zip(self.data_qubits, self.pauli, strict=True)) == dict( zip(other.data_qubits, other.pauli, strict=True) ) # Properties @property def weight(self) -> int: """Number of qubits involved in the operator.""" return len(self.data_qubits) @property def pauli_type(self) -> str: """Type of the Pauli operator: 'X', 'Y', or 'Z'.""" unique_paulis = set(self.pauli) if len(unique_paulis) != 1: raise ValueError( "PauliOperator must consist of a single type of Pauli operator " f"to determine its type, got {unique_paulis} instead." ) return unique_paulis.pop() # Methods
[docs] def as_signed_pauli_op( self, all_qubits: tuple[tuple[int, ...], ...] ) -> SignedPauliOp: """ Get the SignedPauliOp representation of the PauliOperator. Parameters ---------- all_qubits: tuple[tuple[int, ...], ...] All qubits coordinates in the system. Returns ------- SignedPauliOp The SignedPauliOp representation of the PauliOperator. Raises ------ ValueError If the number of qubits in the system is less than the number of qubits in the operator. """ all_qubits = tuple(map(tuple, all_qubits)) if len(all_qubits) < len(self.data_qubits): raise ValueError( f"Number of qubits in the operator {len(self.data_qubits)} exceeds the " f"total number of qubits in the system {len(all_qubits)}." ) # Get the x and z values for each qubit in the operator x_values, z_values = tuple( zip(*[paulichar_to_xz(p) for p in self.pauli], strict=True) ) # Get the sign of the operator sign = 0 # Cast the indexed dqubits to a numpy array for indexing all_qubits_map = {q: i for i, q in enumerate(all_qubits)} idx_dqubits = np.array( [all_qubits_map[q] for q in self.data_qubits if q in all_qubits_map] ) # Initialize the operator row op_row = np.zeros(2 * len(all_qubits) + 1, dtype=SignedPauliOp.DTYPE) # Fill the operator row with the x, z values and the sign op_row[idx_dqubits] = x_values op_row[len(all_qubits) + idx_dqubits] = z_values op_row[-1] = sign # Use the op_row to create a SignedPauliOp object that is already validated return SignedPauliOp(op_row, validated=True)
[docs] @staticmethod def from_signed_pauli_op( signed_pauli_op: SignedPauliOp, index_to_qubit_map: dict[int, tuple[int, ...]] ) -> PauliOperator: """ Create a PauliOperator from a SignedPauliOp. Parameters ---------- signed_pauli_op : SignedPauliOp The SignedPauliOp to convert to a PauliOperator. index_to_qubit_map : dict[int, tuple[int, ...]] A dictionary mapping the indices of the SignedPauliOp to the qubit coordinates in the lattice. Returns ------- PauliOperator The PauliOperator representation of the SignedPauliOp. """ # Find the indexed_dqubits indexed_dqubits = np.union1d( np.where(signed_pauli_op.x != 0)[0], np.where(signed_pauli_op.z != 0)[0], ) # Check if all the indexed dqubits have a corresponding qubit in the map missing_indices = set(indexed_dqubits) - set(index_to_qubit_map.keys()) if missing_indices: # Sort and cast them into a sorted list of integers missing_indices = sorted(map(int, missing_indices)) raise ValueError( f"Missing qubit coordinates for indices {missing_indices}." ) # Get the pauli string of the indexed dqubits pauli_str = "".join( paulixz_to_char_npfunc( signed_pauli_op.x[indexed_dqubits], signed_pauli_op.z[indexed_dqubits] ) ) return PauliOperator( pauli=pauli_str, data_qubits=tuple(index_to_qubit_map[i] for i in indexed_dqubits), )
[docs] def commutes_with(self, other_operator: PauliOperator) -> bool: """ Check if the PauliOperator commutes with another PauliOperator. Parameters ---------- other_operator : PauliOperator The other PauliOperator to check commutation with. Returns ------- bool True if the two objects commute, False otherwise. """ if not isinstance(other_operator, PauliOperator): raise ValueError( "Expected PauliOperator object, got " f"{type(other_operator)} instead." ) # Find common qubits common_qubits = set(self.data_qubits).intersection( set(other_operator.data_qubits) ) # Find for each of the common qubits whether their paulis anti-commute. # They anti-commute if their paulis are different. anti_commutation_of_common_qubits = [ self.pauli[self.data_qubits.index(qubit)] # the above is the pauli of the self PauliOperator for the qubit != # the one below is the pauli of the other PauliOperator for the qubit other_operator.pauli[other_operator.data_qubits.index(qubit)] for qubit in common_qubits ] # Return the total commutation of the common qubits return not bool(np.sum(anti_commutation_of_common_qubits) % 2)