Source code for loom.eka.block

"""
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.

"""

# pylint: disable=too-many-lines

from __future__ import annotations
from itertools import combinations, product, chain
from functools import cached_property
from uuid import uuid4

import numpy as np

from pydantic.dataclasses import Field, dataclass
from pydantic import field_validator, model_validator, ValidationInfo
from pydantic_core import ArgsKwargs

from .stabilizer import Stabilizer
from .pauli_operator import PauliOperator
from .syndrome_circuit import SyndromeCircuit
from .utilities.pauli_format_conversion import paulixz_to_char
from .utilities.stab_array import (
    merge_stabarrays,
    reduce_stabarray_with_bookkeeping,
    find_destabarray,
    StabArray,
    invert_bookkeeping_matrix,
)
from .utilities.validation_tools import (
    dataclass_params,
    ensure_tuple,
    empty_list_error,
    retrieve_field,
)


[docs] @dataclass(**dataclass_params) class Block: # pylint: disable=too-many-instance-attributes,too-many-public-methods """ Block describing one or multiple logical qubits. It is defined by a code type, a list of stabilizers, two logical operators per logical qubit and a unique label. The block also contains information about how its syndromes are measured. Parameters ---------- stabilizers : tuple[Stabilizer, ...] The stabilizers that define the block. logical_x_operators : tuple[PauliOperator, ...] The logical X operators associated to the block. logical_z_operators : tuple[PauliOperator, ...] The logical Z operators associated to the block. syndrome_circuits : tuple[SyndromeCircuit, ...] The syndrome circuits with which the stabilizers of this block are measured. NOTE: Block does not have a check for distinct Syndrome Circuits(s) stabilizer_to_circuit : dict[str, str] A dictionary mapping stabilizer uuids to the uuids of the syndrome circuits that measure them. unique_label : str, optional Label for the block. It must be unique among all blocks in the initial Eka. If no label is provided, a unique label is generated automatically using the uuid module. skip_validation : bool, optional Boolean that allows to skip some validation of the Block. Default is False. The validation skipped are the following: - The qubits coordinates have the same dimension. - The stabilizers commute with each other. - The logical operators commute with each other. - The stabilizers commute with the logical operators. - The logical X and Z operators anti-commute at the same index and commute at \ different indices. - The number of qubits and stabilizers in the Block is compatible with the \ number of logical qubits. uuid : str, optional Unique identifier for the block. If no uuid is provided, a unique uuid is generated automatically using the uuid module. Attributes ---------- __version__ : str Version of the Block class implementation. """ stabilizers: tuple[Stabilizer, ...] logical_x_operators: tuple[PauliOperator, ...] logical_z_operators: tuple[PauliOperator, ...] syndrome_circuits: tuple[SyndromeCircuit, ...] = Field( default_factory=tuple, validate_default=True ) stabilizer_to_circuit: dict[str, str] = Field( default_factory=dict, validate_default=True ) unique_label: str = Field(default_factory=lambda: str(uuid4())) skip_validation: bool = Field(default=False, validate_default=True) uuid: str = Field(default_factory=lambda: str(uuid4()), validate_default=True) # version of the Block class implementation __version__: str = "1.0.0" # Model validators with mode="before". # Note that these are executed in the reverse order in which they are defined. # This is where the last "before" validator is executed @model_validator(mode="before") @classmethod def _validate_qubits_included(cls, data: dict): """ Check that all qubits used in the logical operators are included in the set of stabilizers. Parameters ---------- data : dict The data to be validated. """ logical_operator_qubits = set( qubit for operator in data["logical_x_operators"] + data["logical_z_operators"] for qubit in operator.data_qubits ) stabilizers_qubits = set( qubit for stabilizer in data["stabilizers"] for qubit in stabilizer.data_qubits ) qubits_not_in_stabilizers = logical_operator_qubits - stabilizers_qubits if qubits_not_in_stabilizers: raise ValueError( f"Qubits {qubits_not_in_stabilizers} are not included in the" f" stabilizers but are used in the logical operators" ) return data @model_validator(mode="before") @classmethod def _validate_number_of_logical_operators(cls, data: dict): """ Check that the number of logical X operators is equal to the number of logical Z """ if len(data["logical_x_operators"]) != len(data["logical_z_operators"]): raise ValueError( "The number of logical X operators must be equal to the number of " "logical Z operators." ) return data @model_validator(mode="before") @classmethod def _assign_types(cls, data: ArgsKwargs): """ Assign the types of the stabilizers and logical operators and casts ArgsKwargs into a dictionary. This is necessary to ensure that the data is loaded in the correct format when the Block is created from a JSON. It also tests for empty inputs for stabilizers, logical_x_operators and logical_z_operators. """ # Cast ArgsKwargs into a dictionary if not isinstance(data, dict): data = {**dict(data.kwargs), **dict(data.args)} # Check empty arguments list( map( empty_list_error, [ data["stabilizers"], data["logical_x_operators"], data["logical_z_operators"], ], ) ) # Assign types explicitly if isinstance(data["stabilizers"][0], dict): data["stabilizers"] = [Stabilizer(**stab) for stab in data["stabilizers"]] if isinstance(data["logical_x_operators"][0], dict): data["logical_x_operators"] = [ PauliOperator(**{k: v for k, v in op.items() if k != "nr_of_ancillae"}) for op in data["logical_x_operators"] ] if isinstance(data["logical_z_operators"][0], dict): data["logical_z_operators"] = [ PauliOperator(**{k: v for k, v in op.items() if k != "nr_of_ancillae"}) for op in data["logical_z_operators"] ] return data @model_validator(mode="before") @classmethod def _validate_workbench_json_version(cls, data): """ Checks that the major version of the incoming Workbench JSON matches the major version of the Block class. """ if hasattr(data, "kwargs") and data.kwargs is not None: if "__version__" not in data.kwargs: return data if data.kwargs["__version__"][0] != cls.__version__[0]: raise ValueError( """ The major version of the Workbench export is not compatible with the major version of Eka.Block. """ ) return data # Field validators are executed after model_validator with mode="before" @field_validator("stabilizers", mode="before") @classmethod def _validate_distinct_stabilizers(cls, stabilizers: tuple[Stabilizer, ...]): """ Check that stabilizers are distinct. """ for stab1, stab2 in combinations(stabilizers, 2): if stab1 == stab2: raise ValueError("Stabilizers must be distinct.") return stabilizers _validate_stabilizers_list = field_validator("stabilizers", mode="before")( ensure_tuple ) @field_validator("logical_x_operators", mode="before") @classmethod def _validate_distinct_logical_x_operators( cls, logical_x_operators: tuple[PauliOperator, ...] ): """ Check that logical X operators are distinct. """ for log1, log2 in combinations(logical_x_operators, 2): if log1 == log2: raise ValueError("Logical X operators must be distinct.") return logical_x_operators _validate_logical_x_list = field_validator("logical_x_operators", mode="before")( ensure_tuple ) @field_validator("logical_z_operators", mode="before") @classmethod def _validate_distinct_logical_z_operators( cls, logical_z_operators: tuple[PauliOperator, ...] ): """ Check that logical Z operators are distinct. """ for log1, log2 in combinations(logical_z_operators, 2): if log1 == log2: raise ValueError("Logical Z operators must be distinct.") return logical_z_operators _validate_logical_z_list = field_validator("logical_z_operators", mode="before")( ensure_tuple ) @field_validator("syndrome_circuits", mode="after") @classmethod def _create_default_syndrome_circuits( cls, syndrome_circuits: tuple[SyndromeCircuit, ...], info: ValidationInfo ) -> dict[str, str]: """ Check that for every pauli string of the stabilizers there is a corresponding syndrome circuit. If not, create a default syndrome circuit for that pauli string. """ stab_paulis = {stab.pauli for stab in retrieve_field("stabilizers", info)} synd_circ_paulis = {synd_circ.pauli for synd_circ in syndrome_circuits} additional_circuits = [ SyndromeCircuit( name=f"default_{pauli}", pauli=pauli, ) for pauli in stab_paulis - synd_circ_paulis ] return tuple(list(syndrome_circuits) + additional_circuits) @field_validator("stabilizer_to_circuit", mode="after") @classmethod def _check_stabilizer_to_circuit_map_uuids( cls, stabilizer_to_circuit: dict[str, str], info: ValidationInfo ) -> dict[str, str]: """ Check that all uuids in the stabilizer to circuit map are valid. I.e. check that all stabilizer uuids actually appear in the set of stabilizers and that all syndrome circuit uuids actually appear in the list of syndrome circuits. Also check that pauli strings match. """ syndrome_circs_dict = { circ.uuid: circ for circ in retrieve_field("syndrome_circuits", info) } stabilizer_dict = { stab.uuid: stab for stab in retrieve_field("stabilizers", info) } for stab_uuid, syndrome_circ_uuid in stabilizer_to_circuit.items(): if stab_uuid not in stabilizer_dict.keys(): raise ValueError( f"Stabilizer with uuid {stab_uuid} is not present in the stabilizers." ) if syndrome_circ_uuid not in syndrome_circs_dict.keys(): raise ValueError( f"Syndrome circuit with uuid {syndrome_circ_uuid} is not present in " f"the syndrome circuits." ) if ( stabilizer_dict[stab_uuid].pauli != syndrome_circs_dict[syndrome_circ_uuid].pauli ): raise ValueError( f"Stabilizer with uuid {stab_uuid} has a different pauli string " f"{stabilizer_dict[stab_uuid].pauli} than the syndrome circuit " f"with uuid {syndrome_circ_uuid} (pauli string: " f"{syndrome_circs_dict[syndrome_circ_uuid].pauli})." ) return stabilizer_to_circuit @field_validator("stabilizer_to_circuit", mode="after") @classmethod def _associate_stabs_to_syndrome_circuits( cls, stabilizer_to_circuit: dict[str, str], info: ValidationInfo ) -> dict[str, str]: """ For every stabilizer which is not yet associated to a syndrome circuit, associate it with a default syndrome circuit. If there are multiple syndrome circuits for the same pauli string, raise an exception. """ synd_circs = retrieve_field("syndrome_circuits", info) for stab in retrieve_field("stabilizers", info): if stab.uuid not in stabilizer_to_circuit.keys(): matching_circuits = [ circ for circ in synd_circs if circ.pauli == stab.pauli ] if len(matching_circuits) > 1: raise ValueError( "Multiple syndrome circuits for the same stabilizer pauli " f"string {stab.pauli} found. Could not automatically associate " f"stabilizer {stab} with a syndrome circuit. Please do the " "association manually." ) stabilizer_to_circuit[stab.uuid] = matching_circuits[0].uuid return stabilizer_to_circuit # Model validators, mode="after" # NOTE that the order of the validators is important @model_validator(mode="after") def _validate_coordinate_dimension(self): """ Check that all qubits coordinates have the same dimension. """ # Bypass validation if self.skip_validation: return self dimension = len(self.qubits[0]) if any(len(coordinate) != dimension for coordinate in self.qubits): raise ValueError("All qubits coordinates must have the same dimension.") return self @model_validator(mode="after") def _validate_commutation_stabilizers(self): """ Check that stabilizers commute with each other. """ # Bypass validation if self.skip_validation: return self for stab1, stab2 in combinations(self.stabilizers, 2): if not stab1.commutes_with(stab2): raise ValueError( f"Stabilizers must commute with each other:\n" f"{stab1} and\n" f"{stab2} do not commute.\n" ) return self @model_validator(mode="after") def _validate_commutation_logical_operators(self): """ Check that logical operators commute with each other. """ # Bypass validation if self.skip_validation: return self for log1, log2 in combinations(self.logical_x_operators, 2): if not log1.commutes_with(log2): raise ValueError( f"Logical X operators must commute with each other:\n" f"{log1} and\n" f"{log2} do not commute.\n" ) for log1, log2 in combinations(self.logical_z_operators, 2): if not log1.commutes_with(log2): raise ValueError( f"Logical Z operators must commute with each other:\n" f"{log1} and\n" f"{log2} do not commute.\n" ) return self @model_validator(mode="after") def _validate_commutation_stabilizers_logical_operators(self): """ Check that stabilizers commute with logical operators. """ # Bypass validation if self.skip_validation: return self for stab, log_x in product(self.stabilizers, self.logical_x_operators): if not stab.commutes_with(log_x): raise ValueError( f"Stabilizers must commute with logical X operators:\n" f"{repr(stab)} and\n" f"{repr(log_x)} do not commute.\n" ) for stab, log_z in product(self.stabilizers, self.logical_z_operators): if not stab.commutes_with(log_z): raise ValueError( f"Stabilizers must commute with logical Z operators:\n" f"{repr(stab)} and\n" f"{repr(log_z)} do not commute.\n" ) return self @model_validator(mode="after") def _validate_anticommutation_logical_operators_one_to_one(self): """ Check that the logical X and Z operators anti-commute at the same index anticommute and commute at different indices. """ # Bypass validation if self.skip_validation: return self for (i, log_x), (j, log_z) in combinations( chain( enumerate(self.logical_x_operators), enumerate(self.logical_z_operators) ), 2, ): if i == j and log_x.commutes_with(log_z): raise ValueError( f"Logical X and Z operators at the same index must anticommute " f"with each other:\n{log_x} and\n{log_z} do not anticommute.\n" ) if i != j and not log_x.commutes_with(log_z): raise ValueError( f"Logical X and Z operators at different indices must commute with " f"each other:\n{log_x} and\n {log_z} do not commute.\n" ) return self # This is the last "after" validator @model_validator(mode="after") def _validate_dimensional_compatibility(self): """ Check that the number of qubits and stabilizers in the Block is compatible with the number of logical qubits. E.g. if L is the number of logical operators, N the number of data qubits and k the number of independent stabilizers, then L = N - k. """ # Bypass validation if self.skip_validation: return self if self.reduced_stabarray.nstabs + len(self.logical_x_operators) != len( self.data_qubits ): raise ValueError( "The number of qubits and independent stabilizers in the Block is " "not compatible with the number of logical qubits." ) return self # Properties @property def data_qubits(self) -> tuple[tuple[int, ...], ...]: """ Return the set of all data qubits in the block. Returns ------- tuple[tuple[int, ...], ...] : A tuple of coordinates representing the data qubits. """ return tuple( sorted( # sort the qubits so that the order is not dependent on the # order of the stabilizers set( data_qubit for stabilizer in self.stabilizers for data_qubit in stabilizer.data_qubits ) ) ) @property def ancilla_qubits(self) -> tuple[tuple[int, ...], ...]: """ Return the set of all ancilla qubits in the block. Returns ------- tuple[tuple[int, ...], ...] : A tuple of coordinates representing the ancilla qubits. """ return tuple( set( ancilla_qubit for stabilizer in self.stabilizers for ancilla_qubit in stabilizer.ancilla_qubits ) ) @property def qubits(self) -> tuple[tuple[int, ...], ...]: """ Return the set of all qubits in the block. Returns ------- tuple[tuple[int, ...], ...] : A tuple of coordinates representing all qubits in the block. """ return tuple(set(self.data_qubits + self.ancilla_qubits)) @property def n_data_qubits(self) -> int: """ Return the number of data qubits in the block. """ return len(self.data_qubits) @property def n_logical_qubits(self) -> int: """ Return the number of logical qubits in the block. """ return len(self.logical_x_operators) @property def n_irreducible_stabs(self) -> int: """ Return the number of irreducible stabilizers in the embedding, i.e. the minimum number of stabilizers needed to generate the code space. """ return self.reduced_stabarray.nstabs @cached_property def original_stabarray(self) -> StabArray: """ Return the stabilizer array of the block as defined by the stabilizers. """ signed_pauli_ops = [ s.as_signed_pauli_op(self.data_qubits) for s in self.stabilizers ] return StabArray.from_signed_pauli_ops(signed_pauli_ops, validated=False) @cached_property def reduced_stabarray_with_bookkeeping(self) -> tuple[StabArray, np.ndarray]: """ Return the reduced stabilizer array of the embedding with bookkeeping. """ return reduce_stabarray_with_bookkeeping(self.original_stabarray) @property def reduced_stabarray(self) -> StabArray: """ Return the reduced stabilizer array of the embedding. """ return self.reduced_stabarray_with_bookkeeping[0] @property def bookkeeping(self) -> np.ndarray: """ Return the bookkeeping array of the reduced stabilizer array. """ return self.reduced_stabarray_with_bookkeeping[1] @property def reduced_bookkeeping(self) -> np.ndarray: """ Return the reduced bookkeeping array of the reduced stabilizer array. This entails slicing out the last rows of the bookkeeping array that should cancel out and give a zero row. """ return self.bookkeeping[: self.n_irreducible_stabs, :] @cached_property def bookkeeping_inv(self) -> np.ndarray: """ Return the inverted bookkeeping array of the reduced stabilizer array. """ return invert_bookkeeping_matrix(self.reduced_stabarray_with_bookkeeping[1]) @property def reduced_bookkeeping_inv(self) -> np.ndarray: """ Return the inverted reduced bookkeeping array of the reduced stabilizer array. This entails slicing out the last trivial columns of the inverted bookkeeping array that correspond to the zero rows of the reduced StabArray. """ return self.bookkeeping_inv[:, : self.n_irreducible_stabs] @cached_property def x_log_stabarray(self) -> StabArray: """ Return the X stabilizer array of the logical operator set. """ signed_pauli_ops = [ x.as_signed_pauli_op(self.data_qubits) for x in self.logical_x_operators ] return StabArray.from_signed_pauli_ops(signed_pauli_ops, validated=False) @cached_property def z_log_stabarray(self) -> StabArray: """ Return the Z stabilizer array of the logical operator set. """ signed_pauli_ops = [ z.as_signed_pauli_op(self.data_qubits) for z in self.logical_z_operators ] return StabArray.from_signed_pauli_ops(signed_pauli_ops, validated=False) @cached_property def destabarray(self) -> StabArray: """ Return the destabilizer array of the block. The operators in the destabarray anti-commute with exactly one stabilizer of the block each. In particular, the one with a matching index. They also commute with all of the logical operators. Note that the destabilizer is calculated using the reduced stabilizer array and not the original one. """ z_state_stabarray = merge_stabarrays( (self.reduced_stabarray, self.z_log_stabarray) ) full_destabarray = find_destabarray( z_state_stabarray, partial_destabarray=self.x_log_stabarray ) # The last n_logical_qubits rows are the destabilizers of the logical operators # and as such they should be omitted from the full_destabarray. return StabArray.from_signed_pauli_ops( full_destabarray[: -self.n_logical_qubits] ) @cached_property def pauli_charges(self) -> dict[tuple[int, ...], str]: """ Calculate Pauli charges for all data qubits in the given Block. The Pauli charges are calculated from the stabilizers of the Block. For every data qubit, one counts how often the data qubit is included in stabilizers in the X, Y, and Z basis respectively. If it is included in an odd number of X, Y, or Z stabilizers, the data qubit has a Pauli charge of X, Y, or Z respectively. We only report a single Pauli charge where multiple charges are combined into one charge according to the product of Pauli matrices. E.g. if a data qubit has a Pauli charge of both X and Z, the combined Pauli charge is Y since Y=iXZ. E.g. for a d=5 rotated surface code, plotting the Pauli charges on top of the data qubits would look like this (where we omitted plotting data qubits with no Pauli charge): .. code-block:: Y -- X -- X -- X -- Y | | | | | Z -- -- -- -- Z | | | | | Z -- -- -- -- Z | | | | | Z -- -- -- -- Z | | | | | Y -- X -- X -- X -- Y In this example of the rotated surface code, the four logical corners have Pauli charge Y, and there are two boundaries with Pauli charge X and two with Pauli charge Z. The bulk data qubits have no Pauli charge. No Pauli charge is represented by "_" in the output of this function but we omitted plotting these in the example above. Returns ------- dict[tuple[int, ...], str] Dict mapping data qubits to their pauli charges ("_", "X", "Y", or "Z") """ # Initialize the dictionary of pauli charge numbers pauli_charge_numbers = {qb: {"X": 0, "Y": 0, "Z": 0} for qb in self.data_qubits} # Count the number of X, Y, and Z numbers for each data qubit in each stabilizer for stab in self.stabilizers: for i, qb in enumerate(stab.data_qubits): pauli_charge_numbers[qb][stab.pauli[i]] += 1 # Construct the dictionary of pauli charges pauli_charges = { # Get Pauli charge ("_", "X", "Y", or "Z") for each qubit qb: paulixz_to_char( (charge["X"] + charge["Y"]) % 2, # The X charge (charge["Z"] + charge["Y"]) % 2, # The Z charge # Note that Y contributes to both X and Z charges ) for qb, charge in pauli_charge_numbers.items() } return pauli_charges # Magic methods def __eq__(self, other) -> bool: """ Check whether two blocks are equivalent. The order of stabilizers does not matter for the comparison. Also the different fields are checked independently on the uuids of objects. Returns ------- bool True if the two blocks are equivalent, False otherwise """ def check_stabilizer_to_circuit_eq(block1: Block, block2: Block) -> bool: """ Check whether the stabilizer to circuit maps are equivalent. Since the stabilizers and syndrome circuits can have different uuids in the two blocks respectively, the `stabilizer_to_circuit` dict cannot directly be compared. Instead it has to be checked if the dict maps the same elements to each other by comparing them element-wise. """ for stab_uuid, circ_uuid in block1.stabilizer_to_circuit.items(): # Find the stabilizer object in block1 stab1 = [stab for stab in block1.stabilizers if stab.uuid == stab_uuid][ 0 ] # Find the corresponding stabilizer object in block2 using the == method # which ignores the stabilizer uuid. This is done to check that the two # stabilizers are mapped to the same syndrome circuits in both blocks # respectively, irrespective of different uuids stab2 = [stab for stab in block2.stabilizers if stab == stab1][0] # Uuid of the syndrome circuit which stab2 is mapped to circ2_uuid = block2.stabilizer_to_circuit[stab2.uuid] # Find the two syndrome circuit objects circ1 = [ circ for circ in block1.syndrome_circuits if circ.uuid == circ_uuid ][0] circ2 = [ circ for circ in block2.syndrome_circuits if circ.uuid == circ2_uuid ][0] # Compared the two using the __eq__ method which ignores uuids if circ1 != circ2: return False return True if isinstance(other, Block): blocks_not_equal = ( self.unique_label != other.unique_label or set(self.stabilizers) != set(other.stabilizers) or self.logical_x_operators != other.logical_x_operators or self.logical_z_operators != other.logical_z_operators or sorted(self.syndrome_circuits, key=lambda x: x.name) != sorted(other.syndrome_circuits, key=lambda x: x.name) or not check_stabilizer_to_circuit_eq(self, other) ) if blocks_not_equal: return False # Else, No differences found, the blocks are equivalent return True # Else, cannot compare the block for equivalence with another object with is # not a block return NotImplemented # Constructors
[docs] @classmethod def from_blocks(cls, blocks: tuple[Block, ...]) -> Block: """ Combine multiple blocks into a single block. The blocks must not overlap with each other. The output block will be of code type "custom". Parameters ---------- blocks : tuple[Block, ...] The blocks to be combined. Returns ------- Block The combined block. """ # Check that the input is indeed an iterable of blocks if not all(isinstance(block, Block) for block in blocks): raise ValueError("The input argument blocks must only contain blocks.") # Check that the blocks do not overlap cls.blocks_no_overlap(blocks) # Combine the stabilizers and logical operators stabilizers = tuple(stab for block in blocks for stab in block.stabilizers) logical_x_operators = tuple( log_x for block in blocks for log_x in block.logical_x_operators ) logical_z_operators = tuple( log_z for block in blocks for log_z in block.logical_z_operators ) # Get syndrome_circuits without duplicates and create a mapping from old to new # uuids syndrome_circuits_with_duplicates = tuple( circ for block in blocks for circ in block.syndrome_circuits ) syndrome_circuits = tuple(set(syndrome_circuits_with_duplicates)) synd_circ_map_old_to_new_uuid = { circ.uuid: syndrome_circuits[syndrome_circuits.index(circ)].uuid for circ in syndrome_circuits_with_duplicates } # Combine the stabilizer to circuit maps stabilizer_to_circuit = { stab_uuid: synd_circ_map_old_to_new_uuid[circ_uuid] for block in blocks for stab_uuid, circ_uuid in block.stabilizer_to_circuit.items() } return cls( stabilizers=stabilizers, logical_x_operators=logical_x_operators, logical_z_operators=logical_z_operators, syndrome_circuits=syndrome_circuits, stabilizer_to_circuit=stabilizer_to_circuit, unique_label="+".join(block.unique_label for block in blocks), skip_validation=True, )
# Static methods
[docs] @staticmethod def blocks_no_overlap(blocks: tuple[Block, ...]) -> tuple[Block, ...]: """ Check that blocks do no overlap, meaning that no blocks should be defined on the same data or ancilla qubits. NOTE: We do not check that logical operators are not overlapping. In the `Block` class there is a validation checking that logical operators are defined on the same qubits as the stabilizers. """ for block_i, block_j in combinations(blocks, 2): shared_data_qubits = set(block_i.data_qubits) & set(block_j.data_qubits) if len(shared_data_qubits) > 0: raise ValueError( f"Block '{block_i.unique_label}' and block " f"'{block_j.unique_label}' share the data qubits " f"{shared_data_qubits}." ) shared_ancilla_qubits = set(block_i.ancilla_qubits) & set( block_j.ancilla_qubits ) if len(shared_ancilla_qubits) > 0: raise ValueError( f"Block '{block_i.unique_label}' and block " f"'{block_j.unique_label}' share the ancilla qubits " f"{shared_ancilla_qubits}." ) return blocks
# Methods
[docs] def rename(self, name: str) -> Block: """ Return a copy of the Block with the new name. """ return self.__class__( unique_label=name, stabilizers=self.stabilizers, logical_x_operators=self.logical_x_operators, logical_z_operators=self.logical_z_operators, syndrome_circuits=self.syndrome_circuits, stabilizer_to_circuit=self.stabilizer_to_circuit, )
[docs] def shift( self, position: tuple[int, ...], new_label: str | None = None, ) -> Block: """ Return a copy of the Block where all qubit coordinates are shifted by a given position. Parameters ---------- position : tuple[int, ...] Vector by which the block should be shifted new_label : str | None, optional New label for the block. If None, the same label is used. Returns ------- Block A new Block with the shifted qubit coordinates. """ if ( len(position) != len(self.logical_x_operators[0].data_qubits[0]) - 1 ): # Remove unit vector raise ValueError( f"The shift position has a wrong dimension of {len(position)}. " f"Expected {len(self.logical_x_operators[0].data_qubits[0]) - 1} instead." ) if new_label is None: new_label = self.unique_label def shift_coord( coord: tuple[int, ...], shift: tuple[int, ...] ) -> tuple[int, ...]: if coord is None or len(coord) == 0: return coord return [ (coord[i] + shift[i] if i < len(shift) else coord[i]) for i in range(len(coord)) ] # Append the old unit vector new_stabilizers = [ Stabilizer( pauli=stab.pauli, data_qubits=[shift_coord(qb, position) for qb in stab.data_qubits], ancilla_qubits=[ shift_coord(qb, position) for qb in stab.ancilla_qubits ], ) for stab in self.stabilizers ] logical_x_operators = [ PauliOperator( log_x.pauli, data_qubits=[shift_coord(qb, position) for qb in log_x.data_qubits], ) for log_x in self.logical_x_operators ] logical_z_operators = [ PauliOperator( log_z.pauli, data_qubits=[shift_coord(qb, position) for qb in log_z.data_qubits], ) for log_z in self.logical_z_operators ] # Replacing old stabilizer uuids with new ones stabs_map_old_to_new_uuid = { stab.uuid: new_stabilizers[i].uuid for i, stab in enumerate(self.stabilizers) } new_stabilizer_to_circuit = { stabs_map_old_to_new_uuid[old_stab_uuid]: circ_uuid for old_stab_uuid, circ_uuid in self.stabilizer_to_circuit.items() } return self.__class__( unique_label=new_label, stabilizers=new_stabilizers, logical_x_operators=logical_x_operators, logical_z_operators=logical_z_operators, syndrome_circuits=self.syndrome_circuits, stabilizer_to_circuit=new_stabilizer_to_circuit, skip_validation=True, # Skip validation since the block has been validated )
@cached_property def stabilizers_labels(self) -> dict[str, dict[str, tuple[int, ...]]]: """ Builds a dictionary associating stabilizers, via their uuid, with a set of labels defined through a dictionary. Inside Block, this is generically populated with the space coordinates of the stabilizer check, corresponding to the ancilla which measures each stabilizer. This functionality can be leveraged to later provide these labels to Syndromes and Detectors associated with a given Stabilizer. Returns ------- dict[str, dict[str, tuple[int, ...]]] Dictionary associating stabilizer uuids with their labels. """ # Retrieve space coordinates labels = { stab.uuid: {"space_coordinates": stab.ancilla_qubits[0]} for stab in self.stabilizers } return labels
[docs] def get_stabilizer_label(self, stabilizer_uuid: str) -> dict[str, tuple[int, ...]]: """ Get the labels of a stabilizer, specified by its uuid. Parameters ---------- stabilizer_uuid : str uuid of the stabilizer. Returns ------- dict[str, tuple[int, ...]] Labels of the stabilizer. """ return self.stabilizers_labels.get(stabilizer_uuid, {})