Source code for loom_repetition_code.applicator.split

"""
Copyright 2024 Entropica Labs Pte Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

"""

# pylint: disable=duplicate-code
from loom.eka import ChannelType, Circuit, PauliOperator, Stabilizer
from loom.eka.operations import Split
from loom.eka.utilities import Direction, Orientation
from loom.interpreter import InterpretationStep
from loom.interpreter.utilities import Cbit

from ..code_factory import RepetitionCode


# pylint: disable=too-many-locals
[docs] def split_consistency_check( interpretation_step: InterpretationStep, operation: Split ) -> RepetitionCode: """ Check that the split operation can be performed on the given chain. Parameters ---------- interpretation_step : InterpretationStep InterpretationStep containing the block to split. operation : Split Split operation to perform. Returns ------- RepetitionCode Block to split. Raises ------ ValueError If the split position is larger than the length of the chain. If the split position is at the edge of the chain. If one of the chains has a length of one. """ # Extract Block block = interpretation_step.get_block(operation.input_block_name) # Check valid Block type if not isinstance(block, RepetitionCode): raise ValueError( f"This split operation is not supported for {type(block)} blocks." ) relative_split_position = operation.split_position left_boundary = block.boundary_qubits(Direction.LEFT)[0] right_boundary = block.boundary_qubits(Direction.RIGHT)[0] split_position = left_boundary + relative_split_position # Check that the split position is under correct parameters if split_position > right_boundary or split_position < left_boundary: raise ValueError(f"Split position {split_position} is outside the chain.") if split_position in [left_boundary, right_boundary]: raise ValueError("Split position cannot be at the edge of the chain.") if split_position in [left_boundary + 1, right_boundary - 1]: raise ValueError( "Split has to partition chain in units of at least two qubits." ) # Check that the split operation can be performed if operation.orientation != Orientation.VERTICAL: raise ValueError("Only vertical splits are supported.") return block
[docs] def find_qubit_to_measure( block: RepetitionCode, split_position: int ) -> tuple[int, int]: """Find the qubit to measure in the split operation. Parameters ---------- block : RepetitionCode Block to split. split_position : int Position where the chain will be split. Returns ------- tuple[int, int] Qubit to be measured in the split operation. """ boundary_qubit_position = block.boundary_qubits(Direction.LEFT)[0] qubit_to_measure = (boundary_qubit_position + split_position, 0) return qubit_to_measure
[docs] def create_split_circuit( interpretation_step: InterpretationStep, check_type: str, qubit_to_measure: tuple[int, int], circuit_name: str, ) -> tuple[Circuit, Cbit]: """Create the circuit for the split operation. Parameters ---------- interpretation_step : InterpretationStep Interpretation step containing the chain to split. check_type : str Type of stabilizer to measure. qubits_to_measure : tuple[int, int] Qubit to be measured in the split operation. circuit_name : str Name of the circuit for the split operation. Returns ------- tuple[Circuit, Cbit] Circuit for the split operation and the classical bit used in the circuit. """ # Create classical channels for all data qubit measurements cbit = interpretation_step.get_new_cbit_MUT(f"c_{qubit_to_measure}") cbit_channel = interpretation_step.get_channel_MUT( f"{cbit[0]}_{cbit[1]}", channel_type=ChannelType.CLASSICAL ) # Create a measurement circuit for every measured data qubit measure_circuit_seq = [ Circuit( "Measurement", channels=[ interpretation_step.get_channel_MUT(qubit_to_measure), cbit_channel, ], ) ] # If needed, apply a basis change if check_type == "X": # If phaseflip code qubits can already be measured split_circuit_list = measure_circuit_seq else: # If bitflip code qubits need to be Hadamard transformed before measurement basis_change_circuit_seq = [ Circuit( "H", channels=[interpretation_step.get_channel_MUT(qubit_to_measure)] ) ] # Add basis change before measurement split_circuit_list = basis_change_circuit_seq + measure_circuit_seq # Build circuit split_circuit = Circuit( name=circuit_name, circuit=split_circuit_list, ) return split_circuit, cbit
[docs] def find_new_stabilizers( block: RepetitionCode, qubit_to_measure: tuple[int, int], ) -> tuple[list[Stabilizer], list[Stabilizer]]: """Split the initial Block stabilizers into two lists of stabilizers, one for each Block. Parameters ---------- block : RepetitionCode Initial block to be split qubits_to_measure : tuple[int,int] Qubit to be measured in the split operation. Returns ------- tuple[list[Stabilizer], list[Stabilizer]] A tuple of two lists of stabilizers, one for each block. """ # Find stabilizers which are completely removed stabs_to_remove = [ stab for stab in block.stabilizers if any(q == qubit_to_measure for q in stab.data_qubits) ] # Create two sets of stabilizers, one for each new block new_stabilizers = [ stab for stab in block.stabilizers if stab not in stabs_to_remove ] data_qubits_block_1 = set( q for q in block.data_qubits if q[0] < qubit_to_measure[0] ) data_qubits_block_2 = set( q for q in block.data_qubits if q[0] > qubit_to_measure[0] ) new_stabs_block_1 = [ stab for stab in new_stabilizers if set(stab.data_qubits).issubset(data_qubits_block_1) ] new_stabs_block_2 = [ stab for stab in new_stabilizers if set(stab.data_qubits).issubset(data_qubits_block_2) ] # Compute total set of remaining stabilizers remaining_stabs = ( set(new_stabilizers) - set(new_stabs_block_1) - set(new_stabs_block_2) ) # Consistency check that all new stabilizers are assigned to a block if len(remaining_stabs) != 0: raise ValueError( f"Stabilizers {remaining_stabs} are not assigned to any block." ) return ( new_stabs_block_1, new_stabs_block_2, )
[docs] def get_logical_operator_and_updates( int_step: InterpretationStep, block: RepetitionCode, check_type: str, qubit_to_measure: tuple[int, int], cbit: Cbit, ) -> tuple[ list[list[PauliOperator]], list[list[PauliOperator]], list[dict[str, tuple[str, ...]]], list[dict[str, tuple[str, ...]]], list[dict[str, tuple[Cbit, ...]]], list[dict[str, tuple[Cbit, ...]]], ]: """Finds the logical operators for the new blocks after the split operation. The logical spanning the chain will be split in two, the logical at the left end remains there for the first chain and a new one is created for the second chain, on its left end. Parameters ---------- int_step : InterpretationStep Interpretation step containing the block to split. block : RepetitionCode Block to split. check_type : str Type of stabilizer to measure. qubit_to_measure : tuple[int,int] Qubit to be measured in the split operation. cbit : Cbit Classical bit used to measure the qubit that splits the chain. Returns ------- tuple[ list[list[PauliOperator]],list[list[PauliOperator]], list[dict[str, tuple[str, ...]]], list[dict[str, tuple[str, ...]]], list[dict[str, tuple[Cbit, ...]]], list[dict[str, tuple[Cbit, ...]]], ] Tuple containing the new logical operators for the blocks, the evolution, and the updates. """ other_check_type = "X" if check_type == "Z" else "Z" # block 1 is the left one qubits_block_1 = [q for q in block.data_qubits if q[0] < qubit_to_measure[0]] left_boundary_qubit_1 = min(qubits_block_1, key=lambda x: x[0]) # block 2 is the right one qubits_block_2 = [q for q in block.data_qubits if q[0] > qubit_to_measure[0]] left_boundary_qubit_2 = min(qubits_block_2, key=lambda x: x[0]) # Extract old long and short operators old_short_log, old_long_log = sorted( (block.logical_x_operators[0], block.logical_z_operators[0]), key=lambda x: len(x.data_qubits), ) # Define new logical operators new_long_log_1 = PauliOperator( pauli=other_check_type * len(qubits_block_1), data_qubits=qubits_block_1 ) long_log_evolution_1 = {new_long_log_1.uuid: (old_long_log.uuid,)} long_log_updates_1 = { new_long_log_1.uuid: (cbit,) } # Give the correction to the first operator new_long_log_2 = PauliOperator( pauli=other_check_type * len(qubits_block_2), data_qubits=qubits_block_2 ) long_log_evolution_2 = {new_long_log_2.uuid: (old_long_log.uuid,)} long_log_updates_2 = {} # Short logicals get placed in the left boundary in the new blocks new_short_log_1, required_stabs_1 = block.get_shifted_equivalent_logical_operator( left_boundary_qubit_1 ) id_required_stabs_1 = [stab.uuid for stab in required_stabs_1] new_short_log_2, required_stabs_2 = block.get_shifted_equivalent_logical_operator( left_boundary_qubit_2 ) id_required_stabs_2 = [stab.uuid for stab in required_stabs_2] # If logical was already in the default position, no evolution if id_required_stabs_1 == []: new_short_log_1 = old_short_log short_log_evolution_1 = {} short_log_update_1 = {} # If logical in left Block was shifted, add evolution else: short_log_evolution_1 = { new_short_log_1.uuid: tuple([old_short_log.uuid] + id_required_stabs_1) } short_log_cbits_1 = int_step.retrieve_cbits_from_stabilizers( required_stabs_1, block ) short_log_update_1 = {new_short_log_1.uuid: short_log_cbits_1} # If logical was already in the default position, no evolution if id_required_stabs_2 == []: new_short_log_2 = old_short_log short_log_evolution_2 = {} short_log_update_2 = {} # If logical in right Block was shifted, add evolution else: short_log_evolution_2 = { new_short_log_2.uuid: tuple([old_short_log.uuid] + id_required_stabs_2) } short_log_cbits_2 = int_step.retrieve_cbits_from_stabilizers( required_stabs_2, block ) short_log_update_2 = {new_short_log_2.uuid: short_log_cbits_2} # New logicals new_logs_1 = [[new_long_log_1], [new_short_log_1]] new_logs_2 = [[new_long_log_2], [new_short_log_2]] # New operator evolution and updates # Find cbits for the logical updates log_updates_1 = [long_log_updates_1, short_log_update_1] log_evolution_1 = [long_log_evolution_1, short_log_evolution_1] log_updates_2 = [long_log_updates_2, short_log_update_2] log_evolution_2 = [long_log_evolution_2, short_log_evolution_2] return ( new_logs_1, new_logs_2, log_evolution_1, log_evolution_2, log_updates_1, log_updates_2, )
[docs] def split( interpretation_step: InterpretationStep, operation: Split, same_timeslice: bool, debug_mode: bool, ) -> InterpretationStep: """ Applicator to split a Repetition Code chain using the Split description. Since the Repetition code is a 1D chain, the split operation is always vertical. The algorithm is the following: - A.) DATA QUBITS - A.1) Find data qubit to be measured - B.) - CIRCUIT - B.1) Create a measurement circuit for the measured data qubit - B.2) Append the measurement circuits to the InterpretationStep circuit - C.) - STABILIZERS - C.1) Create two sets of stabilizers, one for each new block, after the split - D.) LOGICAL OPERATORS - D.1) Find logical operators which are not partially measured - D.2) Find logical operators which have to be split - D.3) Create new logical operators and final to initial mapping - D.4) Update `logical_x/z_evolution` - D.5) Update `logical_x/z_updates` - E.) NEW BLOCK AND NEW INTERPRETATION STEP - E.1) Create the new blocks - E.2) Update the block history Parameters ---------- interpretation_step : InterpretationStep Interpretation step containing the block to split. operation : Split Split operation description. same_timeslice : bool Flag indicating whether the operation is part of the same timestep as the previous operation. debug_mode : bool Flag indicating whether the interpretation should be done in debug mode. Activating debug mode will enable commutation validation for Block. Returns ------- InterpretationStep Interpretation step after the split operation. """ # Check that the operation can be performed block = split_consistency_check(interpretation_step, operation) # Extract split position split_position = operation.split_position # Extract check type check_type = block.check_type # A) - DATA QUBITS # A.1) Find data qubit to be measured qubit_to_measure = find_qubit_to_measure(block, split_position) # B) - CIRCUIT # B.1) Create a measurement circuit for the measured data qubit circuit_name = f"split {block.unique_label} at {split_position}" split_circuit, cbit = create_split_circuit( interpretation_step, check_type, qubit_to_measure, circuit_name ) # B.2) Append the measurement circuits to the InterpretationStep circuit interpretation_step.append_circuit_MUT(split_circuit, same_timeslice) # C) - STABILIZERS # C.1) Create two sets of stabilizers, one for each new block, after the split ( new_stabs_block_1, new_stabs_block_2, ) = find_new_stabilizers(block, qubit_to_measure) # D) LOGICAL OPERATORS # D.1) Extract logical operators and updates ( new_logs_1, new_logs_2, log_evolution_1, log_evolution_2, log_updates_1, log_updates_2, ) = get_logical_operator_and_updates( interpretation_step, block, check_type, qubit_to_measure, cbit ) # D.2) Update the logical operator history # Evolution and updates are returned in the order [long_log,short_log] # Depending on the check_type we invert the order of the evolution and # updates output, to match the order of variable assignment below ordering = 1 if check_type == "Z" else -1 x_is_long_operator = check_type == "Z" new_log_x_ops_1, new_log_z_ops_1 = new_logs_1[::ordering] new_log_x_ops_2, new_log_z_ops_2 = new_logs_2[::ordering] logical_x_evolution_1, logical_z_evolution_1 = log_evolution_1[::ordering] logical_x_evolution_2, logical_z_evolution_2 = log_evolution_2[::ordering] logical_x_updates_1, logical_z_updates_1 = log_updates_1[::ordering] logical_x_updates_2, logical_z_updates_2 = log_updates_2[::ordering] interpretation_step.logical_x_evolution.update( logical_x_evolution_1 | logical_x_evolution_2 ) # Update the logical X operator updates: # The first block's operator always inherits updates, only the second block's short # operator also inherits updates for op_id, cbits in logical_x_updates_1.items(): interpretation_step.update_logical_operator_updates_MUT("X", op_id, cbits, True) for op_id, cbits in logical_x_updates_2.items(): interpretation_step.update_logical_operator_updates_MUT( "X", op_id, cbits, not x_is_long_operator ) interpretation_step.logical_z_evolution.update( logical_z_evolution_1 | logical_z_evolution_2 ) # Update the logical Z operator updates: # The first block's operator always inherits updates, only the second block's short # operator also inherits updates for op_id, cbits in logical_z_updates_1.items(): interpretation_step.update_logical_operator_updates_MUT("Z", op_id, cbits, True) for op_id, cbits in logical_z_updates_2.items(): interpretation_step.update_logical_operator_updates_MUT( "Z", op_id, cbits, x_is_long_operator ) # E) NEW BLOCK AND NEW INTERPRETATION STEP # E.1) Create the new blocks new_block_1 = RepetitionCode( stabilizers=new_stabs_block_1, logical_x_operators=new_log_x_ops_1, logical_z_operators=new_log_z_ops_1, unique_label=operation.output_blocks_name[0], skip_validation=not debug_mode, ) new_block_2 = RepetitionCode( stabilizers=new_stabs_block_2, logical_x_operators=new_log_x_ops_2, logical_z_operators=new_log_z_ops_2, unique_label=operation.output_blocks_name[1], skip_validation=not debug_mode, ) # E.2) Update the block history interpretation_step.update_block_history_and_evolution_MUT( new_blocks=(new_block_1, new_block_2), old_blocks=(block,) ) return interpretation_step