Source code for loom_rotated_surface_code.applicator.move_block

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

"""

from loom.eka import Stabilizer, Circuit
from loom.eka.utilities import Direction, DiagonalDirection, Orientation
from loom.interpreter import InterpretationStep, Cbit
from loom.interpreter.applicator import generate_syndromes, generate_detectors

from loom_rotated_surface_code.code_factory import RotatedSurfaceCode
from loom_rotated_surface_code.operations import MoveBlock
from .utilities import (
    shift_block_towards_direction,
    find_detailed_schedules,
    direction_to_coord,
    DetailedSchedule,
    update_qubit_coords,
)


# pylint: disable=duplicate-code
[docs] def move_block( interpretation_step: InterpretationStep, operation: MoveBlock, same_timeslice: bool, debug_mode: bool, ) -> InterpretationStep: """ Applicator to move a block in a fault tolerant manner based on the MoveBlock operation. The Block will be moved 1-unit in the specified direction by moving twice diagonally. By default, the diagonal moves are chosen using a secondary direction TOP or LEFT. For example, if the direction is RIGHT, the block will move diagonally upwards to the right in the first syndrome extraction round and diagonally downwards to the right in the second round. The algorithm is as follows: - A.) Valid move check - A.1) Check if the qubits required for the block to be moved are available. - B.) Shift the block - B.1) Shift the block in the specified direction - B.2) Update the block history and evolution - B.3) Update all the evolutions - B.4) Propagate all the updates - C.) Circuit generation - C.1) Find the qubit initializations required for the swap-then-qec operation and create the reset circuit - C.2) Find the stabilizer schedules for the new block and generate the cnot circuit - C.3) Generate the teleportation finalization circuit with necessary updates to stabilizers and logical operators - C.4) Final measurement of the stabilizers and generation of syndromes - C.5) Combine all the circuits into one diagonal move circuit - D.) Append necessary information to the interpretation step - D.1) Append the circuit to the interpretation step - D.2) Update the block history and evolution - D.3) Create and append the new syndromes and detectors - Repeat steps A, B, C for the second diagonal direction. - E.) Final Circuit - E.1) Wrap the generated circuits into a single circuit representing the full \ MoveBlock operation. If the block is moved to the top: The syndrome extraction circuits assign the qubits as follows: 1. The first set of syndrome extraction rounds assigns the qubits as follows:: a x --- x --- x a | a | a | x --- x --- x | a | a | a x --- x --- x a where x are data channels and a are ancilla channels. 2. The second set of syndrome extraction rounds creates a new set of channels BASED on how the channels are first created, in most cases as ancilla qubits of either the teleport circuits or that of the syndrome extraction rounds during the move. As such the new set of qubits are assigned as follows:: a a a x --- x --- x a | a | a | x --- x --- x a | a | a | a x --- x --- x a 2b. Following step:: x x x a --- a --- a x | x | x | x a --- a --- a | x | x | x a --- a --- a a x x x a 2c. Final Expected Configuration:: a x --- x --- x a | a | a | a x --- x --- x a | a | a | a x --- x --- x a a a a x x x a Parameters ---------- interpretation_step: InterpretationStep The current interpretation step. operation: MoveBlock MoveBlock operation description. Returns ------- InterpretationStep Interpretation Step after the MoveBlock operation has been applied. Raises ------ ValueError If the block is not 2D or if the block cannot be moved in the specified direction. """ block = interpretation_step.get_block(operation.input_block_name) # Begin composite operation init_circ_len = len(interpretation_step.intermediate_circuit_sequence) # Find occupied qubits from other blocks in the latest timeslice other_blocks = [ each_block for each_block in interpretation_step.block_history[-1] if each_block != block ] occupied_qubits = [ each_qubit for each_block in other_blocks for each_qubit in each_block.qubits ] # Decompose the direction into 2 diagonal directions decomposed_directions = composite_direction(operation.direction) current_block = block for diag_direction in decomposed_directions: # A) - Valid move check # A.1) Check if the qubits required for the block to be moved are available. check_valid_move(occupied_qubits, current_block.qubits, diag_direction) # B, C, D) - Move the block diagonally via swap-QEC interpretation_step = move_block_diagonally_via_swap_qec( interpretation_step, current_block, diag_direction, debug_mode ) # Prepare for the next iteration current_block = interpretation_step.get_block(operation.input_block_name) # E) Final Circuit # E.1) Wrap the generated circuits into a single circuit representing the full # MoveBlock operation. new_len = len(interpretation_step.intermediate_circuit_sequence) len_op = new_len - init_circ_len circuit_seq = interpretation_step.pop_intermediate_circuit_MUT(len_op) wrapped_circuit_seq = () for timeslice in circuit_seq: timespan = max(composite_circuit.duration for composite_circuit in timeslice) # Create a circuit with empty timeslices and align circuits template_circ = ( tuple(composite_circuit for composite_circuit in timeslice), ) + ((),) * (timespan - 1) wrapped_circuit_seq += template_circ wrapped_circuit = Circuit( name=(f"Move block {block.unique_label} towards {operation.direction.value}"), circuit=wrapped_circuit_seq, ) interpretation_step.append_circuit_MUT(wrapped_circuit, same_timeslice) return interpretation_step
[docs] def move_block_diagonally_via_swap_qec( interpretation_step: InterpretationStep, current_block: RotatedSurfaceCode, diag_direction: Direction, debug_mode: bool, ) -> InterpretationStep: """ Moves the block diagonally in the specified direction using a swap-then-qec procedure. Parameters ---------- interpretation_step: InterpretationStep The current interpretation step. current_block: RotatedSurfaceCode The current block to be moved. diag_direction: Direction The diagonal direction in which the block is to be moved. debug_mode: bool If True, skip validation when creating the new block. Returns ------- tuple[RotatedSurfaceCode, Circuit] The new block after the move and the circuit that performs the move. """ # B) - Shift the block # B.1) Shift the block in the specified direction new_block, stab_ev, logx_ev, logz_ev = shift_block_towards_direction( current_block, diag_direction, debug_mode=debug_mode ) # B.2) Update all the evolutions # Stabilizer evolution interpretation_step.stabilizer_evolution.update(stab_ev) # Logical operator evolution interpretation_step.logical_x_evolution.update(logx_ev) interpretation_step.logical_z_evolution.update(logz_ev) # B.3) Propagate all the updates # Stabilizer updates for new_stab_uuid, old_stab_uuids in stab_ev.items(): propagated_updates = () for old_stab_uuid in old_stab_uuids: propagated_updates += interpretation_step.stabilizer_updates.get( old_stab_uuid, () ) if propagated_updates: interpretation_step.stabilizer_updates[new_stab_uuid] = propagated_updates # Logical operator updates interpretation_step.update_logical_operator_updates_MUT( "X", new_block.logical_x_operators[0].uuid, (), True ) interpretation_step.update_logical_operator_updates_MUT( "Z", new_block.logical_z_operators[0].uuid, (), True ) # C) CIRCUIT GENERATION # C.1) Find the qubit initializations required for the swap-then-qec operation # and create the reset circuit ( anc_qubits_to_init, data_qubits_to_init, teleportation_qubit_pairs, ) = find_swap_then_qec_qubit_initializations( current_block.stabilizers, diag_direction ) reset_basis_circuit = Circuit( # pylint: disable=duplicate-code name=("Initialization of qubits for first swap-then-qec"), circuit=[ [ Circuit( f"reset_{'0' if pauli == 'Z' else '+'}", channels=[interpretation_step.get_channel_MUT(q)], ) for pauli in ["X", "Z"] for q in anc_qubits_to_init[pauli] + data_qubits_to_init[pauli] ] ], ) # C.2) Find the stabilizer schedules for the new block and generate the cnot circuit stab_schedule_dict = find_detailed_schedules(new_block, diag_direction) cnots_circuit = get_swap_qec_cnots( interpretation_step, new_block, diag_direction, stab_schedule_dict, anc_qubits_to_init, data_qubits_to_init, ) # C.3) Generate the teleportation finalization circuit with necessary updates to # stabilizers and logical operators tp_finalization = generate_teleportation_measurement_circuit_with_updates( interpretation_step, new_block, anc_qubits_to_init, teleportation_qubit_pairs, ) # C.4) Final measurement of the stabilizers and generation of syndromes syndrome_meas_circ, stab_measurements = ( generate_syndrome_measurement_circuit_and_cbits( interpretation_step, new_block, ) ) # C.5) Combine all the circuits into one diagonal move circuit circ = Circuit( name=f"move block {new_block.unique_label} towards {diag_direction.value}", circuit=Circuit.construct_padded_circuit_time_sequence( [ (reset_basis_circuit,), (cnots_circuit,), (tp_finalization, syndrome_meas_circ), ] ), ) # D) - APPEND NECESSARY INFORMATION TO THE INTERPRETATION STEP # D.1) Append the circuit to the interpretation step interpretation_step.append_circuit_MUT(circ, same_timeslice=False) # D.2) Update the block history and evolution interpretation_step.update_block_history_and_evolution_MUT( (new_block,), (current_block,) ) # D.3) Create and append the new syndromes and detectors # Create all new syndromes generate_and_append_block_syndromes_and_detectors( interpretation_step=interpretation_step, block=new_block, syndrome_measurement_cbits=stab_measurements, ) return interpretation_step
[docs] def check_valid_move( occupied_qubits: tuple[tuple[int, int, int], ...], moving_qubits: tuple[tuple[int, int, int], ...], direction: Direction | DiagonalDirection, ): """ This function checks if the move operation is valid by ensuring that none of the moving qubits will be moved onto an occupied qubit. If any of the moving qubits will be moved onto an occupied qubit, a ValueError is raised. Parameters ---------- occupied_qubits: tuple[tuple[int, int, int], ...] A tuple of qubit coordinates representing "occupied" qubits. Qubits taking part in the move, cannot be moved onto these set of qubits as they are "occupied". moving_qubits: tuple[tuple[int, int, int], ...] A tuple of qubit coordinates representing qubits that will be moved. The last value in each qubit coordinate tuple represents the sub-lattice index of the qubit. direction: Direction | DiagonalDirection The direction in which the qubits will be moving. Raises ------ ValueError If the move is invalid, i.e., if any of the moving qubits will be moved onto an occupied qubit. """ # Do the diagonal move. for each_qubit in moving_qubits: new_qubit_coords = tuple( q + dir for q, dir in zip( each_qubit, direction_to_coord(direction, each_qubit[-1]), strict=True, ) ) if new_qubit_coords in occupied_qubits: raise ValueError( f"The move operation is invalid. The following qubit, {each_qubit}, is " f"moving to an occupied qubit, {new_qubit_coords}." )
[docs] def composite_direction( direction: Direction, ) -> tuple[DiagonalDirection, DiagonalDirection]: """ For the move operation, the user defines only "right", "left", "top" or "bottom". However, the actual movement of the block is done via 2 diagonal directions. This function returns a tuple of 2 sets of directions based on the user-defined direction. For e.g. If the user specifies "right", the actual movement involves qubits moving to the "top right" then "bottom right". It would return a tuple with a set of directions "right" and "bottom", and a tuple with the directions "top" and "right", representing "bottom right" and "top right" respectively. composite_direction(Direction.RIGHT) -> (DiagonalDirection.TOP_RIGHT, DiagonalDirection.BOTTOM_RIGHT) Parameters ---------- direction: Direction The direction in which the block is to be moved. Returns ------- tuple[DiagonalDirection, DiagonalDirection] A tuple of 2 sets of diagonal directions which together represent the composite direction of movement. """ composite_directions = { Direction.TOP: ( DiagonalDirection.TOP_LEFT, DiagonalDirection.TOP_RIGHT, ), Direction.RIGHT: ( DiagonalDirection.TOP_RIGHT, DiagonalDirection.BOTTOM_RIGHT, ), Direction.LEFT: ( DiagonalDirection.TOP_LEFT, DiagonalDirection.BOTTOM_LEFT, ), Direction.BOTTOM: ( DiagonalDirection.BOTTOM_LEFT, DiagonalDirection.BOTTOM_RIGHT, ), } return composite_directions[direction]
[docs] def find_swap_then_qec_qubit_initializations( # pylint: disable=too-many-locals stabilizers: list[Stabilizer], diag_direction: DiagonalDirection, relocation_diag_direction: DiagonalDirection = None, ) -> tuple[ dict[str, list[tuple[int, ...]]], dict[str, list[tuple[int, ...]]], list[tuple[tuple[int, ...], tuple[int, ...]]], ]: """ Find the qubits to initialize for the swap_then_qec operation in the y_wall_out operation context. This is for a subset of stabilizers of the block that are associated with the boundary qubits. The recipe is as follows for moving towards TOP-RIGHT: - 1) FIND (non-teleporting) ANCILLA QUBIT INITIALIZATIONS: For each stabilizer that is NOT a the TOP-RIGHT boundary stabilizer, find the ancilla qubit on the TOP-RIGHT of the initial ancilla qubit and initialize it in the basis corresponding to the stabilizer. - 2) FIND TELEPORTATION QUBIT PAIRS: For each stabilizer that is NOT a BOTTOM-LEFT boundary stabilizer, check if the ancilla qubit of the stabilizer is already initialized. If not, initialize it in the basis corresponding to the stabilizer and form a teleportation qubit pair with the data qubit on the BOTTOM-LEFT of the ancilla. - 3) FIND DATA QUBIT INITIALIZATIONS: For each stabilizer that is a TOP-RIGHT boundary stabilizer, find the data qubit on the TOP-RIGHT of the ancilla qubit and initialize it in the basis corresponding to the stabilizer. Say we have the following block annotated by its stabilizers:: Z o --- o --- o X | Z | X | o --- o --- o | X | Z | X o --- o --- o Z To perform SWAP-THEN-QEC to move the block diagonally towards the TOP-RIGHT, we need to initialize the qubits in the basis as shown below:: --- z x | z | o --- o --- o | z | x | z o --- o --- o --- x | x | z | x o --- o --- o Of these, the teleportation qubit pairs can be seen with the numberings below:: --- z x | z | o --- o --- o | 1 | x | z 1 --- o --- o --- x | 2 | z | 3 2 --- o --- 3 Parameters ---------- stabilizers: list[Stabilizer] The stabilizers of the block. diag_direction: DiagonalDirection The diagonal direction that the block is moving. relocation_diag_direction: DiagonalDirection The directions to relocate the qubits. If None, the qubits are not relocated. Returns ------- dict[str, list[tuple[int, ...]]] The ancilla qubits to initialize in the X and Z basis. dict[str, list[tuple[int, ...]]] The data qubits to initialize in the X and Z basis. list[tuple[tuple[int, ...], tuple[int, ...]]] The teleportation qubit pairs. The first qubit is the ancilla qubit and the second is the data qubit. """ # Extract the component directions from the diagonal direction vert_direction = diag_direction.direction_along_orientation(Orientation.VERTICAL) hor_direction = diag_direction.direction_along_orientation(Orientation.HORIZONTAL) # Find the data qubits data_qubits = list({q for stab in stabilizers for q in stab.data_qubits}) data_qubit_sublattice_index = set(q[2] for q in data_qubits) if len(data_qubit_sublattice_index) != 1: raise ValueError( "The stabilizers should all belong to the same sublattice of data qubits." ) data_qubit_sublattice_index = data_qubit_sublattice_index.pop() anc_qubit_sublattice_index = 1 - data_qubit_sublattice_index # Find the boundary qubits bound_coords = { Direction.LEFT: min(q[0] for q in data_qubits), Direction.RIGHT: max(q[0] for q in data_qubits), Direction.TOP: min(q[1] for q in data_qubits), Direction.BOTTOM: max(q[1] for q in data_qubits), } # Find the boundary qubits towards the mixed direction and the opposite direction same_direction_boundary_qubits = [ q for q in data_qubits if q[0] == bound_coords[hor_direction] or q[1] == bound_coords[vert_direction] ] opposite_direction_boundary_qubits = [ q for q in data_qubits if q[0] == bound_coords[hor_direction.opposite()] or q[1] == bound_coords[vert_direction.opposite()] ] # Find the stabilizers that are only associated with the boundary qubits mixed_direction_bound_stabs = [ stab for stab in stabilizers if set(stab.data_qubits).issubset(same_direction_boundary_qubits) ] opposite_mixed_direction_bound_stabs = [ stab for stab in stabilizers if set(stab.data_qubits).issubset(opposite_direction_boundary_qubits) ] # Find the mixed direction vector mixed_direction_vector = direction_to_coord(diag_direction) anc_qubits_to_init = {"X": [], "Z": []} # Step 1 # Find the first set of ancilla qubits to initialize for stab in stabilizers: if stab in mixed_direction_bound_stabs: # Skip same boundary stabilizers continue stab_anc = stab.ancilla_qubits[0] stab_type = stab.pauli_type other_anc = tuple( coord1 + coord2 for coord1, coord2 in zip(stab_anc, mixed_direction_vector, strict=True) ) anc_qubits_to_init[stab_type].append(other_anc) # Step 2 # Find the teleportation qubits teleportation_qubit_pairs = [] data_qubit_vector = direction_to_coord( diag_direction.opposite(), anc_qubit_sublattice_index ) for stab in stabilizers: if stab in opposite_mixed_direction_bound_stabs: # Skip opposite boundary stabilizers continue stab_anc = stab.ancilla_qubits[0] pauli = stab.pauli_type # Check if it's a teleportation qubit if stab_anc not in anc_qubits_to_init[pauli]: anc_qubits_to_init[pauli].append(stab_anc) # Find its data qubit pair data_qubit = tuple(map(sum, zip(stab_anc, data_qubit_vector, strict=True))) teleportation_qubit_pairs.append((stab_anc, data_qubit)) # Step 3 # Find the data qubits to initialize data_qubits_to_init = {"X": [], "Z": []} # We need the ancilla to data qubit vector for that anc_to_data_vector = direction_to_coord(diag_direction, anc_qubit_sublattice_index) for stab in mixed_direction_bound_stabs: anc_qubit = stab.ancilla_qubits[0] diag_data_qubit = tuple( coord1 + coord2 for coord1, coord2 in zip(anc_qubit, anc_to_data_vector, strict=True) ) data_qubits_to_init[stab.pauli_type].append(diag_data_qubit) # Finally, relocate the qubits if needed if relocation_diag_direction is not None: anc_relocation_vector = direction_to_coord( relocation_diag_direction, anc_qubit_sublattice_index ) data_relocation_vector = direction_to_coord( relocation_diag_direction, data_qubit_sublattice_index ) def sum_vecs(vec1, vec2): return tuple(map(sum, zip(vec1, vec2, strict=True))) anc_qubits_to_init = { key: [sum_vecs(q, anc_relocation_vector) for q in val] for key, val in anc_qubits_to_init.items() } data_qubits_to_init = { key: [sum_vecs(q, data_relocation_vector) for q in val] for key, val in data_qubits_to_init.items() } teleportation_qubit_pairs = [ tuple( ( sum_vecs(q, anc_relocation_vector) if q[2] == 1 else sum_vecs(q, data_relocation_vector) ) for q in q_pair ) for q_pair in teleportation_qubit_pairs ] return anc_qubits_to_init, data_qubits_to_init, teleportation_qubit_pairs
[docs] def get_swap_qec_cnots( interpretation_step: InterpretationStep, new_block: RotatedSurfaceCode, move_diag_direction: DiagonalDirection, stab_schedule_dict: dict[Stabilizer, DetailedSchedule], anc_qubit_initialization: dict[str, list[tuple[int, ...]]], data_qubit_initialization: dict[str, list[tuple[int, ...]]], ) -> Circuit: """Generates the circuit for the final step of the swap-then-qec operation. This step moves the data qubits to their final positions where the new_block definition is. Parameters ---------- interpretation_step : InterpretationStep The interpretation step. Note that it may be mutated by generating new channels. new_block : RotatedSurfaceCode The new block after the diagonal move. move_diag_direction : DiagonalDirection The diagonal direction in which the block is moving. stab_schedule_dict : dict[Stabilizer, DetailedSchedule] A dictionary mapping each stabilizer to its detailed schedule. anc_qubit_initialization : dict[str, list[tuple[int, ...]]] A dictionary mapping each Pauli type to its list of ancilla qubit initializations. data_qubit_initialization : dict[str, list[tuple[int, ...]]] A dictionary mapping each Pauli type to its list of data qubit initializations. Returns ------- Circuit The generated circuit for the final step of the swap-then-qec operation. """ # Find the vectors to and from the final positions op_move_diag_direction = move_diag_direction.opposite() # Initialize the cnots and find the first layer of them from cnots = [[] for _ in range(4)] for q in new_block.data_qubits: q_prev = update_qubit_coords([q], op_move_diag_direction)[0] if q in anc_qubit_initialization["Z"] + data_qubit_initialization["Z"]: cnot_pair = (q_prev, q) elif q in anc_qubit_initialization["X"] + data_qubit_initialization["X"]: cnot_pair = (q, q_prev) else: raise ValueError( "Data qubit not found in either ancilla or data qubit initialization " "lists." ) cnots[0].append(cnot_pair) # Find the rest of the cnots # pylint: disable=duplicate-code for stab in new_block.stabilizers: if len(stab.data_qubits) == 4: data_qubits = stab_schedule_dict[stab].get_stabilizer_qubits(stab) else: boundary_direction = next( dir for dir in Direction if stab in new_block.boundary_stabilizers(dir) ) data_qubits = stab_schedule_dict[stab].get_stabilizer_qubits( stab, boundary_direction ) for i, data_qubit in enumerate(data_qubits): if data_qubit is None or i == 0: # Skip if the data qubit is: # - None (2-qubit stabilizer) # - the first qubit (already handled due to swap-qec) continue if stab.pauli_type == "Z": # If the stabilizer is Z, cnot from data qubit to ancilla cnot_pair = (data_qubit, stab.ancilla_qubits[0]) elif stab.pauli_type == "X": # If the stabilizer is X, cnot from ancilla to data qubit cnot_pair = (stab.ancilla_qubits[0], data_qubit) else: raise ValueError("Unknown stabilizer type.") cnots[i].append(cnot_pair) # Generate the circuit containing the cnots swap_then_qec_cnots = Circuit( name=( f"Swap-then-QEC CNOTs for moving block {new_block.unique_label} towards " f"{move_diag_direction.value}" ), circuit=[ [ Circuit( "cx", channels=[ interpretation_step.get_channel_MUT(q) for q in qubit_pair ], ) for qubit_pair in cnot_slice ] for cnot_slice in cnots ], ) return swap_then_qec_cnots
[docs] def generate_syndrome_measurement_circuit_and_cbits( interpretation_step: InterpretationStep, block: RotatedSurfaceCode, actual_anc_qubit_relocation_vector: tuple[int, ...] = (0, 0, 0), ) -> tuple[Circuit, list[tuple[Cbit, ...]]]: """ Generate and return the circuit that measures the stabilizers of the block and the list of stabilizer measurements as tuples of Cbits. Parameters ---------- interpretation_step : InterpretationStep The interpretation step. Note that it may be mutated by generating new channels. block : RotatedSurfaceCode The block whose stabilizers are to be measured. actual_anc_qubit_relocation_vector : tuple[int, ...], optional The vector to the actual ancilla qubit, by default (0, 0, 0) Returns ------- Circuit The circuit that performs the measurement of the stabilizers of the block. Note that it's just the final measurement operations and not the full stabilizer measurement circuit. list[tuple[Cbit, ...]] The list of stabilizer measurements as tuples of Cbits. The order of the list corresponds to the order of the stabilizers in block.stabilizers. """ # Initialize the list of circuits stab_measurements = [] meas_circ_seq = [[]] # Find the vector to the actual ancilla qubit for stab in block.stabilizers: # Find the data qubit containing the syndrome actual_anc_qubit = tuple( map( sum, zip( stab.ancilla_qubits[0], actual_anc_qubit_relocation_vector, strict=True, ), ) ) actual_anc_channel = interpretation_step.get_channel_MUT(actual_anc_qubit) cbit = interpretation_step.get_new_cbit_MUT(f"c_{actual_anc_qubit}") stab_measurements.append((cbit,)) cbit_channel = interpretation_step.get_channel_MUT( f"{cbit[0]}_{cbit[1]}", channel_type="classical" ) m_circ = Circuit( f"measure_{stab.pauli_type}", channels=[actual_anc_channel, cbit_channel] ) # Append the circuit to the list meas_circ_seq[0].append(m_circ) # Compile the stabilizer measurement circuit stab_meas_circ = Circuit( "measure stabilizer ancillas", circuit=meas_circ_seq, ) return stab_meas_circ, stab_measurements
[docs] def generate_and_append_block_syndromes_and_detectors( interpretation_step: InterpretationStep, block: RotatedSurfaceCode, syndrome_measurement_cbits: list[tuple[Cbit, ...]], ) -> None: """Generate and append syndromes and detectors to the interpretation step. NOTE: This should probably be made into a method of InterpretationStep. Parameters ---------- interpretation_step : InterpretationStep The interpretation step. Note that it may be mutated by generating new channels. block : RotatedSurfaceCode The block to which the operation will be applied. syndrome_measurement_cbits : list[tuple[Cbit, ...]] The list of syndrome measurement classical bits. It has to correspond to the stabilizers of the block. """ new_syndromes = generate_syndromes( interpretation_step=interpretation_step, stabilizers=block.stabilizers, block=block, stab_measurements=syndrome_measurement_cbits, ) # Generate the new detectors for the new syndromes new_detectors = generate_detectors(interpretation_step, new_syndromes) # Append the syndromes and detectors to the interpretation step interpretation_step.append_syndromes_MUT(new_syndromes) interpretation_step.append_detectors_MUT(new_detectors)
[docs] def generate_teleportation_measurement_circuit_with_updates( interpretation_step: InterpretationStep, new_block: RotatedSurfaceCode, anc_qubits_to_init: dict[str, list[tuple[int, ...]]], teleportation_qubit_pairs: list[tuple[tuple[int, ...], tuple[int, ...]]], ) -> Circuit: """Generate the circuit that finalizes the teleportation operation. This includes the measurement of the data qubits and the updating of the necessary stabilizers based on the measurement outcomes. Parameters ---------- interpretation_step : InterpretationStep The interpretation step. Note that it may be mutated by generating new channels and updating stabilizers and logical operators. new_block : RotatedSurfaceCode The new block after the diagonal move. anc_qubits_to_init : dict[str, list[tuple[int, ...]]] A dictionary mapping each Pauli type to its list of ancilla qubit initializations. teleportation_qubit_pairs : list[tuple[tuple[int, ...], tuple[int, ...]]] A list of tuples, each containing a pair of qubits involved in the teleportation operation. The first qubit in each tuple is the ancilla qubit to be corrected, and the second qubit is the data qubit to be measured. Returns ------- Circuit The circuit that finalizes the teleportation operation """ # pylint: disable=duplicate-code teleportation_circ_seq = [[]] for corrected_qubit, measured_qubit in teleportation_qubit_pairs: # Obtain the necessary channels and cbit cbit = interpretation_step.get_new_cbit_MUT(f"c_{measured_qubit}") cbit_channel = interpretation_step.get_channel_MUT( f"{cbit[0]}_{cbit[1]}", channel_type="classical" ) measured_qubit_channel = interpretation_step.get_channel_MUT(measured_qubit) # Determine the operations based on the initialization basis of the # ancilla qubit if corrected_qubit in anc_qubits_to_init["X"]: measure_op_name = "measure_z" update_pauli = "Z" elif corrected_qubit in anc_qubits_to_init["Z"]: measure_op_name = "measure_x" update_pauli = "X" # pylint: disable=duplicate-code else: raise ValueError("The ancilla qubit was not found in the initialization.") # Define the circuits that measure the data qubit and operate on the ancilla # qubit based on the measurement meas_circ = Circuit( measure_op_name, channels=[measured_qubit_channel, cbit_channel], ) # Append the circuits to the list teleportation_circ_seq[0].append(meas_circ) # Do the same for the logical operator (it's going to be only one operator) logical_ops = ( new_block.logical_z_operators if update_pauli == "Z" else new_block.logical_x_operators ) logical_ops_involved = [ op for op in logical_ops if corrected_qubit in op.data_qubits ] for op in logical_ops_involved: interpretation_step.update_logical_operator_updates_MUT( update_pauli, op.uuid, (cbit,), False ) # Find all appropriate stabilizers stabs_to_update = [ stab for stab in new_block.stabilizers if stab.pauli_type == update_pauli # correct flavor and corrected_qubit in stab.data_qubits # qubit part of stabilizer ] # Append the Cbit on the updates of the stabilizers for stab in stabs_to_update: current_upd = interpretation_step.stabilizer_updates.get(stab.uuid, ()) interpretation_step.stabilizer_updates[stab.uuid] = current_upd + (cbit,) # Compile the teleportation circuit finalization teleportation_circuit_finalization = Circuit( f"teleportation measurements of block {new_block.unique_label}", circuit=teleportation_circ_seq, ) return teleportation_circuit_finalization