"""
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 itertools import product
from loom.eka import Block, Circuit, PauliOperator, Stabilizer
from loom.eka.utilities import Orientation
from loom.eka.operations import (
Merge,
MeasureBlockSyndromes,
LogicalMeasurement,
)
from loom.interpreter import InterpretationStep, Cbit, Syndrome
from loom.interpreter.applicator import generate_syndromes, measureblocksyndromes
from ..code_factory import RotatedSurfaceCode
# pylint: disable=too-many-lines
[docs]
def merge_consistency_check( # pylint: disable=too-many-branches
interpretation_step: InterpretationStep,
operation: Merge,
) -> tuple[RotatedSurfaceCode, RotatedSurfaceCode]:
"""
Check if the blocks are compatible for merging. Also re-orders the blocks so
that block1 is the left or top block and block2 is the right or bottom block. This
is required for the rest of the workflow to function properly.
Parameters
----------
interpretation_step : InterpretationStep
InterpretationStep containing the blocks to merge.
operation : Merge
Descriptor of the merge operation to perform.
Returns
-------
tuple[RotatedSurfaceCode, RotatedSurfaceCode]
The blocks to merge, ordered so that block1 is the left or top block and
block2 is the right or bottom block.
Raises
------
ValueError
If the blocks are not compatible for merging. The blocks are compatible if:
(1) They do not overlap (this should already be enforced).
(2) The blocks' upper left corner qubits are aligned (either horizontally or
vertically).
(3) The blocks have the same size in the direction normal to the merge.
(4) The boundaries to be merged are of the same type.
(5) The alternate pattern of stabilizers is preserved. I.e. the blocks have the
same upper left 4-body stabilizer if the distance is even, different stabilizer
if the distance is odd.
(6) There needs to be at least one row/column of data qubits between the two
blocks.
(7) The blocks have a single logical operator.
Also raises a ValueError if the orientation specified in the operation does not
match the default orientation.
"""
name1, name2 = operation.input_blocks_name
block1 = interpretation_step.get_block(name1)
block2 = interpretation_step.get_block(name2)
# 1 - Check that the blocks are of the right type
if not isinstance(block1, RotatedSurfaceCode) or not isinstance(
block2, RotatedSurfaceCode
):
raise TypeError(
f"The merge operation is not supported for "
f"{tuple(set(type(block1), type(block2)))} blocks."
)
context_str = (
# pylint: disable=protected-access
f"Operation {operation.__class__.__name__} on {operation._inputs} failed:\n"
)
# 2 - Check that the blocks do not overlap
if len(set(block1.data_qubits + block2.data_qubits)) < len(
block1.data_qubits
) + len(block2.data_qubits):
raise ValueError(context_str + "The blocks overlap.")
# 3 - Check that the blocks' upper left corners are aligned
# Compute the number of non zero coordinates in the vector that links the upper left
# corners of the two blocks.
link_vector = [
coord2 - coord1
for coord1, coord2 in zip(
block1.upper_left_qubit, block2.upper_left_qubit, strict=True
)
]
blocks_are_aligned = sum(coord != 0 for coord in link_vector) == 1
index_nonzero = next((i for i, coord in enumerate(link_vector) if coord != 0), None)
if not blocks_are_aligned:
raise ValueError(
context_str + "The blocks' upper left corners are not aligned."
)
# Ensure that block1 is left or top and block2 is right or bottom
if link_vector[index_nonzero] < 0:
block1, block2 = block2, block1
# Find the direction of the merge
if index_nonzero == 0:
default_orientation = Orientation.HORIZONTAL
merge_is_horizontal = True
elif index_nonzero == 1:
default_orientation = Orientation.VERTICAL
merge_is_horizontal = False
else:
raise ValueError(
context_str
+ "The blocks corners have same x and y coordinates but differ in another "
"way."
)
orientation = operation.orientation or default_orientation
if orientation != default_orientation:
raise ValueError(
context_str + "The orientation specified in the operation does not "
"match the default orientation."
)
# 4 - Check that the blocks have the same size in the direction normal to the merge
if merge_is_horizontal:
if block1.size[1] != block2.size[1]:
raise ValueError(
context_str
+ "The blocks have different sizes in the vertical direction."
)
else:
if block1.size[0] != block2.size[0]:
raise ValueError(
context_str
+ "The blocks have different sizes in the horizontal direction."
)
# 5 - Check that the boundaries to be merged are of the same type
if merge_is_horizontal:
if block1.boundary_type("right") != block2.boundary_type("left"):
raise ValueError(
context_str + "The boundaries to be merged are of different types."
)
else:
if block1.boundary_type("bottom") != block2.boundary_type("top"):
raise ValueError(
context_str + "The boundaries to be merged are of different types."
)
# 6 - Check that the alternate pattern of stabilizers is preserved
if merge_is_horizontal:
distance = block2.upper_left_qubit[0] - block1.upper_left_qubit[0]
else:
distance = block2.upper_left_qubit[1] - block1.upper_left_qubit[1]
# If distance is even, upper left 4-body stabilizers should be the same type
# If distance is odd, upper left 4-body stabilizers should be different types
if (
distance % 2 == 0
and (
block1.upper_left_4body_stabilizer.pauli
!= block2.upper_left_4body_stabilizer.pauli
)
) or (
distance % 2 != 0
and (
block1.upper_left_4body_stabilizer.pauli
== block2.upper_left_4body_stabilizer.pauli
)
):
raise ValueError(
context_str + "The alternate pattern of stabilizers is not preserved."
)
# 7 - Check that there is at least one row/column of data qubits between the two
# blocks
if merge_is_horizontal:
if block1.upper_left_qubit[0] + block1.size[0] == block2.upper_left_qubit[0]:
raise ValueError(
context_str
+ "There is no column of data qubits between the two blocks."
)
else:
if block1.upper_left_qubit[1] + block1.size[1] == block2.upper_left_qubit[1]:
raise ValueError(
context_str + "There is no row of data qubits between the two blocks."
)
return (block1, block2)
[docs]
def create_merge_circuit(
interpretation_step: InterpretationStep,
blocks: tuple[RotatedSurfaceCode, RotatedSurfaceCode],
operation: Merge,
qubits_to_reset: tuple[tuple[int, ...], ...],
boundary_type: str,
) -> Circuit:
"""Create the circuit for the merge operation.
Parameters
----------
interpretation_step : InterpretationStep
Interpretation step containing the blocks to merge.
blocks : tuple[RotatedSurfaceCode, RotatedSurfaceCode]
Blocks to merge.
operation : Merge
Descriptor of the merge operation to perform.
qubits_to_reset : tuple[tuple[int, ...], ...]
Qubits to reset during the merge operation.
boundary_type : str
Type of boundary to merge.
Returns
-------
Circuit
Circuit for the merge operation.
"""
# B) - CIRCUIT
# B.1) Find which basis the data qubits have to be reset in
# If the boundary type is X the stabilizers are Z type, they need to be reset in |0>
# If the boundary type is Z the stabilizers are X type, they need to be reset in |+>
reset_state = "0" if boundary_type == "X" else "+"
# B.2) Create a reset circuit for every reset data qubit
reset_circuit_seq = [
[
Circuit(
f"Reset_{reset_state}",
channels=[interpretation_step.get_channel_MUT(q)],
)
for q in qubits_to_reset
]
]
merge_circuit = Circuit(
name=(
f"Merge {blocks[0].unique_label} and {blocks[1].unique_label} into "
f"{operation.output_block_name}"
),
circuit=reset_circuit_seq,
)
return merge_circuit
[docs]
def find_merge_stabilizer_to_circuit_mappings(
blocks: tuple[RotatedSurfaceCode, RotatedSurfaceCode],
new_stabs_left: list[Stabilizer],
new_stabs_right: list[Stabilizer],
new_stabs_top: list[Stabilizer],
new_stabs_bottom: list[Stabilizer],
new_bulk_stabilizers: list[Stabilizer],
old_stabs_to_lengthen: list[Stabilizer],
) -> dict[str, str]:
"""Finds the mapping between the stabilizers in the new block and the associated
syndrome circuits.
Parameters
----------
blocks : tuple[RotatedSurfaceCode, RotatedSurfaceCode]
Blocks to merge.
new_stabs_left : list[Stabilizer]
New stabilizers that form a part of the left boundary of the new block. This is
an empty list for horizontal merges.
new_stabs_right : list[Stabilizer]
New stabilizers that form a part of the right boundary of the new block. This is
an empty list for horizontal merges.
new_stabs_top : list[Stabilizer]
New stabilizers that form a part of the top boundary of the new block. This is
an empty list for vertical merges.
new_stabs_bottom : list[Stabilizer]
New stabilizers that form a part of the bottom boundary of the new block. This
is an empty list for vertical merges.
new_bulk_stabilizers : list[Stabilizer]
New stabilizers that form a part of the bulk of the new block.
old_stabs_to_lengthen : list[Stabilizer]
Old stabilizers that are gonna be replaced by new stabilizers.
Returns
-------
dict[str, str]
New mapping of stabilizers to syndrome circuits for the block.
"""
block1, block2 = blocks
# We only use one set of SyndromeCircuits for the new block
# Create the mapping from block2 syndrome circuits to the new syndrome_circuits list
block2_to_block1_synd_circ_map = {
block2_synd_circ.uuid: block1_synd_circ.uuid
for block1_synd_circ in block1.syndrome_circuits
for block2_synd_circ in block2.syndrome_circuits
if block1_synd_circ.name == block2_synd_circ.name
}
# Ensure that the id corresponds to a stabilizer that is not removed
conserved_stabs_id = [
stab.uuid
for stab in block1.stabilizers + block2.stabilizers
if stab not in old_stabs_to_lengthen
]
# Use the stabilizer to circuit mapping of the first block (apart from removed
# stabilizers)
new_stab_to_circuit = {
stab_id: synd_circ_id
for (stab_id, synd_circ_id) in block1.stabilizer_to_circuit.items()
if stab_id in conserved_stabs_id
}
# Create a new mapping to the syndrome circuits for the new block for the
# stabilizers of block2 that are not gonna be replaced
new_stab_to_circuit.update(
{
stab_id: block2_to_block1_synd_circ_map[synd_circ_id]
for (stab_id, synd_circ_id) in block2.stabilizer_to_circuit.items()
if stab_id in conserved_stabs_id
}
)
# Left stabilizers
for stab in new_stabs_left:
new_stab_to_circuit[stab.uuid] = next(
syndrome_circuit.uuid
for syndrome_circuit in block1.syndrome_circuits + block2.syndrome_circuits
if syndrome_circuit.name == f"left-{stab.pauli.lower()}"
)
# Right stabilizers
for stab in new_stabs_right:
new_stab_to_circuit[stab.uuid] = next(
syndrome_circuit.uuid
for syndrome_circuit in block1.syndrome_circuits + block2.syndrome_circuits
if syndrome_circuit.name == f"right-{stab.pauli.lower()}"
)
# Top stabilizers
for stab in new_stabs_top:
new_stab_to_circuit[stab.uuid] = next(
syndrome_circuit.uuid
for syndrome_circuit in block1.syndrome_circuits + block2.syndrome_circuits
if syndrome_circuit.name == f"top-{stab.pauli.lower()}"
)
# Bottom stabilizers
for stab in new_stabs_bottom:
new_stab_to_circuit[stab.uuid] = next(
syndrome_circuit.uuid
for syndrome_circuit in block1.syndrome_circuits + block2.syndrome_circuits
if syndrome_circuit.name == f"bottom-{stab.pauli.lower()}"
)
# Bulk stabilizers
for stab in new_bulk_stabilizers:
new_stab_to_circuit[stab.uuid] = next(
syndrome_circuit.uuid
for syndrome_circuit in block1.syndrome_circuits + block2.syndrome_circuits
if syndrome_circuit.name == stab.pauli.lower()
)
return new_stab_to_circuit
[docs]
def create_merge_2_body_stabilizers(
blocks: tuple[RotatedSurfaceCode, RotatedSurfaceCode],
merge_orientation: Orientation,
merge_distance: int,
filling_upper_left_qubit: tuple[int, int],
pauli: str,
) -> tuple[list[Stabilizer], list[Stabilizer]]:
"""
Create the new 2-body stabilizers located at the merged boundaries.
Parameters
----------
blocks : tuple[RotatedSurfaceCode, RotatedSurfaceCode]
blocks to merge.
merge_orientation : Orientation
Orientation of the merge.
merge_distance : int
Distance between the two blocks in the direction of the merge. A distance m
means that there are m-1 rows/columns of data qubits between the two blocks
to merge.
filling_upper_left_qubit : tuple[int, int]
Upper left qubit of the filling region.
pauli : str
Pauli string of the new stabilizers.
Returns
-------
tuple[list[Stabilizer], list[Stabilizer]]
New left and right boundary stabilizers if the merge is horizontal, new top and
bottom boundary stabilizers if the merge is vertical.
"""
block1, _ = blocks
is_merge_horizontal = merge_orientation == Orientation.HORIZONTAL
# Index in block.size corresponding to the merge direction, 0 for horizontal, 1 for
# vertical
size_index = 0 if is_merge_horizontal else 1
# The two boundaries merged both have a 2 body stabilizer in the first row/column if
# the distance normal to the merge direction is odd, e.g. for a 3x3 block,
# weight_2_stab_is_first_row=True and a vertical merge, the additional left boundary
# will have a stabilizer in the first row but the right boundary does not.
boundaries_have_same_weight_2_stab_is_first = block1.size[not size_index] % 2 == 0
if is_merge_horizontal:
block1_weight_2_stab_is_first_column = not block1.weight_2_stab_is_first_row
# If the size of block1 is even, the filling's weight_2_stab_is_first_column is
# the same as block1's else it's opposite
filling_weight_2_stab_is_first_top_column = (
block1_weight_2_stab_is_first_column ^ ((block1.size[size_index] % 2) == 0)
)
filling_weight_2_stab_is_first_bot_column = (
filling_weight_2_stab_is_first_top_column
if boundaries_have_same_weight_2_stab_is_first
else not filling_weight_2_stab_is_first_top_column
)
n_stabs_top = merge_distance // 2 + (
filling_weight_2_stab_is_first_top_column and (merge_distance % 2)
)
n_stabs_bottom = merge_distance // 2 + (
filling_weight_2_stab_is_first_bot_column and (merge_distance % 2)
)
new_stabs_top = RotatedSurfaceCode.generate_weight2_stabs(
pauli=pauli,
initial_position=(
filling_upper_left_qubit[0]
+ (not filling_weight_2_stab_is_first_top_column),
filling_upper_left_qubit[1],
),
num_stabs=n_stabs_top,
orientation=Orientation.HORIZONTAL,
is_bottom_or_right=False,
)
new_stabs_bottom = RotatedSurfaceCode.generate_weight2_stabs(
pauli=pauli,
initial_position=(
filling_upper_left_qubit[0]
+ (not filling_weight_2_stab_is_first_bot_column),
filling_upper_left_qubit[1] + block1.size[1] - 1,
),
num_stabs=n_stabs_bottom,
orientation=Orientation.HORIZONTAL,
is_bottom_or_right=True,
)
return new_stabs_top, new_stabs_bottom
# else
# If the size of block 1 is even, the filling's weight_2_stab_is_first_row is
# the same as block 1 else it's opposite
filling_weight_2_stab_is_first_left_row = block1.weight_2_stab_is_first_row ^ (
(block1.size[size_index] % 2) == 0
)
filling_weight_2_stab_is_first_right_row = (
filling_weight_2_stab_is_first_left_row
if boundaries_have_same_weight_2_stab_is_first
else not filling_weight_2_stab_is_first_left_row
)
n_stabs_left = merge_distance // 2 + (
filling_weight_2_stab_is_first_left_row and (merge_distance % 2)
)
n_stabs_right = merge_distance // 2 + (
filling_weight_2_stab_is_first_right_row and (merge_distance % 2)
)
new_stabs_left = RotatedSurfaceCode.generate_weight2_stabs(
pauli=pauli,
initial_position=(
filling_upper_left_qubit[0],
filling_upper_left_qubit[1] + (not filling_weight_2_stab_is_first_left_row),
),
num_stabs=n_stabs_left,
orientation=Orientation.VERTICAL,
is_bottom_or_right=False,
)
new_stabs_right = RotatedSurfaceCode.generate_weight2_stabs(
pauli=pauli,
initial_position=(
filling_upper_left_qubit[0] + block1.size[0] - 1,
filling_upper_left_qubit[1]
+ (not filling_weight_2_stab_is_first_right_row),
),
num_stabs=n_stabs_right,
orientation=Orientation.VERTICAL,
is_bottom_or_right=True,
)
return new_stabs_left, new_stabs_right
[docs]
def merge_stabilizers( # pylint: disable=too-many-locals
blocks: tuple[RotatedSurfaceCode, RotatedSurfaceCode],
merge_is_horizontal: bool,
) -> tuple[list[Stabilizer], list[Stabilizer], list[Stabilizer], dict[str, str]]:
"""Merge the two initial blocks stabilizers into a single lists of stabilizers, the
list of old stabilizers that are gonna be replaced and the new stabilizers that will
replace them.
Parameters
----------
blocks : tuple[RotatedSurfaceCode, RotatedSurfaceCode]
Blocks to merge. By construction blocks[0] should be the left or top block and
blocks[1] should be the right or bottom block.
merge_is_horizontal : bool
True if the merge is horizontal, False if the merge is vertical.
Returns
-------
tuple[list[Stabilizer], list[Stabilizer], list[Stabilizer], dict[str, str]]
List of stabilizers forming the new block, list of old stabilizers to be
replaced, list of new stabilizers which will replace them and map from
stabilizer to the syndrome circuits.
"""
# C) - STABILIZERS
# C.1) Create the new 4-body stabilizers located between the two initial blocks
block1, block2 = blocks
weight_4_x_schedule = block1.weight_4_x_schedule
weight_4_z_schedule = block1.weight_4_z_schedule
# Find the qubit located at the upper left corner of the filling
# block1 is always the left or top block and block2 is always the right or bottom
# block
boundary_block_1 = (
block1.boundary_qubits("right")
if merge_is_horizontal
else block1.boundary_qubits("bottom")
)
boundary_block_2 = (
block2.boundary_qubits("left")
if merge_is_horizontal
else block2.boundary_qubits("top")
)
# The "filling" is the set of new 4 body stabilizers and 2-body stabilizers
upper_left_qubit_filling = min(boundary_block_1, key=lambda x: x[0] + x[1])
dx = (
block2.upper_left_qubit[0] - upper_left_qubit_filling[0] + 1
if merge_is_horizontal
else block1.size[0]
)
dz = (
block2.upper_left_qubit[1] - upper_left_qubit_filling[1] + 1
if not merge_is_horizontal
else block1.size[1]
)
adjacent_stab = next(
stab
for stab in block1.stabilizers
if upper_left_qubit_filling in stab.data_qubits and len(stab.pauli) == 4
)
upper_left_4_body_pauli = "XXXX" if adjacent_stab.pauli[0] == "Z" else "ZZZZ"
new_upleft_4_body_stabs = RotatedSurfaceCode.generate_weight4_stabs(
pauli=upper_left_4_body_pauli,
schedule=(
weight_4_x_schedule
if upper_left_4_body_pauli == "XXXX"
else weight_4_z_schedule
),
start_in_top_left_corner=True,
dx=dx,
dz=dz,
initial_position=upper_left_qubit_filling,
)
new_rest_4_body_stabs = RotatedSurfaceCode.generate_weight4_stabs(
pauli="XXXX" if upper_left_4_body_pauli == "ZZZZ" else "ZZZZ",
schedule=(
weight_4_x_schedule
if upper_left_4_body_pauli != "XXXX"
else weight_4_z_schedule
),
start_in_top_left_corner=False,
dx=dx,
dz=dz,
initial_position=upper_left_qubit_filling,
)
new_4_body_stabs = new_upleft_4_body_stabs + new_rest_4_body_stabs
# C.2) Create the new 2-body stabilizers
# new_top_left_is_xxxx = upper_left_4_body_pauli == "XXXX"
stab_left_right_is_x = (
block1.x_boundary == Orientation.HORIZONTAL
) # The logical operators are aligned
stab_left_right = "XX" if stab_left_right_is_x else "ZZ"
stab_top_bottom = "ZZ" if stab_left_right_is_x else "XX"
if merge_is_horizontal:
stabs_top, stabs_bottom = create_merge_2_body_stabilizers(
blocks=blocks,
merge_orientation=Orientation.HORIZONTAL,
merge_distance=dx - 1, # dx is the number of qubits used in the filling
filling_upper_left_qubit=upper_left_qubit_filling,
pauli=stab_top_bottom,
)
stabs_left, stabs_right = [], []
else:
stabs_top, stabs_bottom = [], []
stabs_left, stabs_right = create_merge_2_body_stabilizers(
blocks=blocks,
merge_orientation=Orientation.VERTICAL,
merge_distance=dz - 1, # dz is the number of qubits used in the filling
filling_upper_left_qubit=upper_left_qubit_filling,
pauli=stab_left_right,
)
new_2_body_stabs = stabs_top + stabs_bottom + stabs_left + stabs_right
# C.3) Find the 2-body stabilizers which weight should be increased
old_stabs_to_lengthen = [
stab
for stab in block1.stabilizers + block2.stabilizers
if set(stab.data_qubits).issubset(set(boundary_block_1 + boundary_block_2))
and len(stab.data_qubits) == 2
]
# C.4) Find the associated 4-body stabilizers
# There is a 1-to-1 correspondence between old_stabs_to_lengthen and
# new_stabs_increased_weight
# NOTE: the order in the product matters, we want to match the order
# old_stabs_to_lengthen to new_stabs_increased_weight
new_stabs_increased_weight = [
stab
for (old_stab, stab) in product(old_stabs_to_lengthen, new_4_body_stabs)
if set(old_stab.data_qubits).issubset(set(stab.data_qubits))
and old_stab.pauli in stab.pauli
]
# C.5) Create the new `stabilizer_to_circuit` mapping
new_stab_to_circ = find_merge_stabilizer_to_circuit_mappings(
blocks,
stabs_left,
stabs_right,
stabs_top,
stabs_bottom,
new_4_body_stabs,
old_stabs_to_lengthen,
)
# C.6) Create a single set of stabilizers for the new block
new_stabilizers = new_4_body_stabs + new_2_body_stabs
new_block_stabilizers = [
stab
for stab in block1.stabilizers + block2.stabilizers
if stab not in old_stabs_to_lengthen
] + new_stabilizers
return (
new_block_stabilizers,
old_stabs_to_lengthen,
new_stabs_increased_weight,
new_stab_to_circ,
)
[docs]
def find_data_qubits_between(
qubit1: tuple[int, ...], qubit2: tuple[int, ...]
) -> list[tuple[int, ...]]:
"""
Generate all qubits between two given qubits coordinates (not inclusive).
Parameters
----------
qubit1: tuple[int, ...]:
The first qubit (x1, y1, ...).
qubit2: tuple[int, ...]:
The second qubit (x2, y2, ...).
Returns
-------
list[tuple[int, ...]]
A list of qubits between the two qubits.
"""
x1, y1 = qubit1[:2]
x2, y2 = qubit2[:2]
if x1 == x2:
# Vertical alignment
y_start, y_end = sorted([y1, y2])
qubits_between = [(x1, y, 0) for y in range(y_start + 1, y_end)]
elif y1 == y2:
# Horizontal alignment
x_start, x_end = sorted([x1, x2])
qubits_between = [(x, y1, 0) for x in range(x_start + 1, x_end)]
else:
raise ValueError("The qubits are not aligned horizontally or vertically.")
return qubits_between
[docs]
def merge_logical_operators(
interpretation_step: InterpretationStep,
blocks: tuple[RotatedSurfaceCode, RotatedSurfaceCode],
qubits_to_reset: tuple[tuple[int, ...]],
merge_is_horizontal: bool,
) -> tuple[
InterpretationStep, PauliOperator, tuple[Cbit, ...], PauliOperator, tuple[Cbit, ...]
]:
"""Generate the new logical operators from the two blocks to merge. Note that they
will be pushed to the upper left corner of the new block, similarly to the default
choice of logical operators. This function also retrieves the lates measurements of
the equivalent stabilizers and returns them.
Parameters
----------
interpretation_step : InterpretationStep
Interpretation step containing the blocks to merge.
blocks : tuple[RotatedSurfaceCode, RotatedSurfaceCode]
Initial blocks to merge. By convention, the first block must be the one on the
left or top. The second block must be the one on the right or bottom
respectively.
qubits_to_reset : tuple[tuple[int, ...]]
Qubits to measure between the two blocks.
merge_is_horizontal : bool
Whether the merge is horizontal or vertical.
Returns
-------
tuple[
InterpretationStep,
PauliOperator,
tuple[Cbit, ...],
PauliOperator,
tuple[Cbit, ...]
]
The updated interpretation step, the new logical X operator, the measurements
coming from equivalent X stabilizers, the new logical Z operator and the
measurements coming from equivalent Z stabilizers.
"""
# D) LOGICAL OPERATORS
block1, block2 = blocks
x_log_is_horizontal = block1.x_boundary == Orientation.HORIZONTAL
# The logical operator aligned with the merge will be modified
if x_log_is_horizontal == merge_is_horizontal:
pauli_to_be_merged = "X"
initial_log_ops_to_be_merged = (
block1.logical_x_operators[0],
block2.logical_x_operators[0],
)
log_ops_untouched = (
block1.logical_z_operators[0],
block2.logical_z_operators[0],
)
else:
pauli_to_be_merged = "Z"
initial_log_ops_to_be_merged = (
block1.logical_z_operators[0],
block2.logical_z_operators[0],
)
log_ops_untouched = (
block1.logical_x_operators[0],
block2.logical_x_operators[0],
)
# D.1) Align the 2 initial operators with stabilizer products
aligned_logical_block_1, extra_stabs1 = (
block1.get_shifted_equivalent_logical_operator(
initial_log_ops_to_be_merged[0],
block1.upper_left_qubit,
)
)
aligned_logical_block_2, extra_stabs2 = (
block2.get_shifted_equivalent_logical_operator(
initial_log_ops_to_be_merged[1],
block2.upper_left_qubit,
)
)
# D.2) Retrieve the cbits coming from the latest equivalent stabilizer
# measurements.
cbits_block_1 = interpretation_step.retrieve_cbits_from_stabilizers(
extra_stabs1, block1
)
cbits_block_2 = interpretation_step.retrieve_cbits_from_stabilizers(
extra_stabs2, block2
)
# D.3) Create new logical operators using qubits to measure
# We assume the logical operators are on a line and located between the end of the
# first logical operator and the start of the second one
qubits_in_between = find_data_qubits_between(
max(aligned_logical_block_1.data_qubits, key=lambda x: x[0] + x[1]),
min(aligned_logical_block_2.data_qubits, key=lambda x: x[0] + x[1]),
)
if not set(qubits_in_between).issubset(set(qubits_to_reset)):
raise RuntimeError("The qubits chosen are not included in the reset qubits.")
qubits_new_logical = (
aligned_logical_block_1.data_qubits
+ aligned_logical_block_2.data_qubits
+ tuple(qubits_in_between)
)
new_logical = PauliOperator(
pauli=pauli_to_be_merged * len(qubits_new_logical),
data_qubits=qubits_new_logical,
)
untouched_logical = log_ops_untouched[0]
new_log_x, new_log_z = (
(new_logical, untouched_logical)
if new_logical.pauli[0] == "X"
else (untouched_logical, new_logical)
)
# D.4) Update `logical_x/z_evolution`
if pauli_to_be_merged == "X":
interpretation_step.logical_x_evolution[new_logical.uuid] = (
initial_log_ops_to_be_merged[0].uuid,
initial_log_ops_to_be_merged[1].uuid,
) + tuple(stab.uuid for stab in extra_stabs1 + extra_stabs2)
cbits_x = cbits_block_1 + cbits_block_2
cbits_z = ()
else:
interpretation_step.logical_z_evolution[new_logical.uuid] = (
initial_log_ops_to_be_merged[0].uuid,
initial_log_ops_to_be_merged[1].uuid,
) + tuple(stab.uuid for stab in extra_stabs1 + extra_stabs2)
cbits_x = ()
cbits_z = cbits_block_1 + cbits_block_2
return interpretation_step, new_log_x, cbits_x, new_log_z, cbits_z
[docs]
def create_syndromes( # pylint: disable= too-many-arguments
interpretation_step: InterpretationStep,
qubits_to_reset: tuple[tuple[int, int, int], ...],
reset_type: str,
new_stabilizers: tuple[Stabilizer, ...],
new_stabs_increased_weight: tuple[Stabilizer, ...],
merged_block: Block,
) -> tuple[Syndrome, ...]:
"""Creates the new syndromes for the stabilizers that are reset.
Parameters
----------
interpretation_step : InterpretationStep
Interpretation step to which we add the new syndromes.
qubits_to_reset : tuple[tuple[int, int, int], ...]
Tuple of qubits that are reset during the merge
reset_type:
Pauli type of the reset, this gives information on which stabilizer is reset in
a deterministic state.
new_stabilizers : tuple[Stabilizer, ...]
Stabilizers created by the merge operation.
new_stabs_increased_weight:
Stabilizers that grew from a previous 2-body of the initial blocks.
merged_block : Block
The newly merged block.
Returns
-------
tuple[Syndrome, ...]
Reference syndromes of the reset stabilizers.
"""
reset_stabilizers = tuple(
stab
for stab in new_stabilizers
if (
all(q in qubits_to_reset for q in stab.data_qubits)
and (stab not in new_stabs_increased_weight)
and set(stab.pauli) == {reset_type}
)
)
syndromes = generate_syndromes(
interpretation_step=interpretation_step,
stabilizers=reset_stabilizers,
block=merged_block,
stab_measurements=tuple(() for _ in reset_stabilizers),
)
return syndromes
[docs]
def merge( # pylint: disable=line-too-long, too-many-locals
interpretation_step: InterpretationStep,
operation: Merge,
same_timeslice: bool,
debug_mode: bool,
) -> InterpretationStep:
"""
Merge two blocks specified in the Merge operation.
The algorithm is the following:
- A.) DATA QUBITS
- A.1) Find data qubits to be measured in between the two blocks, they will be \
merged into a single new block
- B.) CIRCUIT
- B.1) Create classical channels for all data qubit measurements
- B.2) Create a measurement circuit for every measured data qubit
- B.3) Append the measurement circuits to the InterpretationStep circuit. \
If needed, apply a basis change
- C.) - STABILIZERS
- C.1) Create the new 4-body stabilizers located between the two initial blocks
- C.2) Create the new 2-body stabilizers located at the merged boundaries
- C.3) Find the 2-body stabilizers which weight should be increased
- C.4) Find the associated 4-body stabilizers
- C.5) Create the new ``stabilizer_to_circuit`` mapping
- C.6) Create a single set of stabilizers for the new block
- C.7) Update ``stabilizer_evolution`` and ``stabilizer_updates`` for the \
stabilizers which have been increased in weight
- D.) LOGICAL OPERATORS
- D.1) Align the 2 initial operators with stabilizer products
- D.2) Retrieve the cbits coming from the latest equivalent stabilizer \
measurements.
- D.3) Create new logical operators using qubits to measure
- 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 block
- E.2) Update the block history
- F.) SYNDROMES
- F.1) Generate syndromes for newly created stabilizers
- G) JOINT OBSERVABLES
- G.1) Obtain the cbits for joint observable and store them in the
interpretation_step
Parameters
----------
interpretation_step : InterpretationStep
Input interpretation step.
operation : Merge
Descriptor of the merge operation to perform.
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
Modified interpretation step after the merge operation.
"""
# Consistency checks
block1, block2 = merge_consistency_check(interpretation_step, operation)
# A) - DATA QUBITS
# Find data qubits to be measured in between the two blocks, they will be
# merged into a single new block
link_vector = [
coord2 - coord1
for coord1, coord2 in zip(
block1.upper_left_qubit, block2.upper_left_qubit, strict=True
)
]
index_nonzero = next((i for i, coord in enumerate(link_vector) if coord != 0))
# Ensure that block1 is left or top and block2 is right or bottom
if link_vector[index_nonzero] < 0:
block1, block2 = block2, block1
merge_is_horizontal = not bool(index_nonzero)
if merge_is_horizontal:
boundary_qubits_block1 = block1.boundary_qubits("right")
_ = block2.boundary_qubits("left")
distance = block2.upper_left_qubit[0] - (
block1.upper_left_qubit[0] + block1.size[0]
)
else:
boundary_qubits_block1 = block1.boundary_qubits("bottom")
_ = block2.boundary_qubits("top")
distance = block2.upper_left_qubit[1] - (
block1.upper_left_qubit[1] + block1.size[1]
)
# Create a square of qubits to reset between the two blocks
qubits_to_reset = [
(
qubit[0] + i * merge_is_horizontal,
qubit[1] + i * (not merge_is_horizontal),
0,
)
for qubit in boundary_qubits_block1
for i in range(1, distance + 1)
]
# B) - CIRCUIT
# B.1) Create classical channels for all data qubit measurements
# B.2) Create a measurement circuit for every measured data qubit
boundary_type = (
block1.boundary_type("right")
if merge_is_horizontal
else block1.boundary_type("bottom")
)
merge_circuit = create_merge_circuit(
interpretation_step,
(block1, block2),
operation,
qubits_to_reset,
boundary_type,
)
# B.3) Append the measurement circuits to the InterpretationStep circuit
# If needed, apply a basis change
interpretation_step.append_circuit_MUT(merge_circuit, same_timeslice)
# C) - STABILIZERS
# C.1) Create the new 4-body stabilizers located between the two initial blocks
# C.2) Create the new 2-body stabilizers located at the merged boundaries
# C.3) Find the 2-body stabilizers which weight should be increased
# C.4) Find the associated 4-body stabilizers
# C.5) Create the new `stabilizer_to_circuit` mapping
# C.6) Create a single set of stabilizers for the new block
(
new_block_stabilizers,
old_stabs_to_lengthen,
new_stabs_increased_weight,
new_stabilizer_to_circuit,
) = merge_stabilizers((block1, block2), merge_is_horizontal)
# C.7) Update `stabilizer_evolution` and `stabilizer_updates` for the
# stabilizers which have been increased in weight
# Stabilizer evolution: Update the stabilizer evolution dictionary
stab_map_weight2_to_weight4 = {
new_stab.uuid: (stab.uuid,)
for new_stab, stab in zip(
new_stabs_increased_weight, old_stabs_to_lengthen, strict=True
)
}
interpretation_step.stabilizer_evolution.update(stab_map_weight2_to_weight4)
# Stabilizer updates: Since we use a reset, there is no update to the stabilizers
# D) LOGICAL OPERATORS
# D.1) Align the 2 initial operators with stabilizer products
# D.2) Retrieve the cbits coming from the latest equivalent stabilizer
# measurements.
# D.3) Create new logical operators using qubits to measure
# D.4) Update `logical_x/z_evolution`
interpretation_step, new_log_x, cbits_x, new_log_z, cbits_z = (
merge_logical_operators(
interpretation_step,
(block1, block2),
qubits_to_reset,
merge_is_horizontal,
)
)
# D.5) Update `logical_x/z_updates`
# Inherit the updates from previous operators (only if the operator has changed),
# adding the Cbits coming from moving logical operators
interpretation_step.update_logical_operator_updates_MUT(
operator_type="X",
logical_operator_id=new_log_x.uuid,
new_updates=cbits_x,
inherit_updates=(
new_log_x.uuid
not in (
block1.logical_x_operators[0].uuid,
block2.logical_x_operators[0].uuid,
)
),
)
interpretation_step.update_logical_operator_updates_MUT(
operator_type="Z",
logical_operator_id=new_log_z.uuid,
new_updates=cbits_z,
inherit_updates=(
new_log_z.uuid
not in (
block1.logical_z_operators[0].uuid,
block2.logical_z_operators[0].uuid,
)
),
)
# E) NEW BLOCK AND NEW INTERPRETATION STEP
# E.1) Create the new block
merged_block = RotatedSurfaceCode(
stabilizers=list(new_block_stabilizers),
logical_x_operators=[new_log_x],
logical_z_operators=[new_log_z],
unique_label=operation.output_block_name,
syndrome_circuits=block1.syndrome_circuits,
stabilizer_to_circuit=new_stabilizer_to_circuit,
skip_validation=not debug_mode,
)
# E.2) Update the block history
# Update only the blocks that are involved in the merge
interpretation_step.update_block_history_and_evolution_MUT(
new_blocks=(merged_block,),
old_blocks=(block1, block2),
)
# F) SYNDROMES
# F.1) Generate syndromes for newly created stabilizers
merge_syndromes = create_syndromes(
interpretation_step=interpretation_step,
qubits_to_reset=qubits_to_reset,
reset_type=("X" if boundary_type == "Z" else "Z"),
new_stabilizers=new_block_stabilizers,
new_stabs_increased_weight=new_stabs_increased_weight,
merged_block=merged_block,
)
interpretation_step.append_syndromes_MUT(merge_syndromes)
# G) JOINT OBSERVABLE
# Measure blocks once to obtain the cbits for the stabilizers in between
interpretation_step = measureblocksyndromes(
interpretation_step,
MeasureBlockSyndromes(merged_block.unique_label, 1),
same_timeslice=False,
debug_mode=debug_mode,
)
new_log_op, old_log_op_2, log_updates = (
(
merged_block.logical_x_operators[0],
block2.logical_x_operators[0],
interpretation_step.logical_x_operator_updates,
)
if boundary_type == "X"
else (
merged_block.logical_z_operators[0],
block2.logical_z_operators[0],
interpretation_step.logical_z_operator_updates,
)
)
# Obtain stabilizers between the logical operators of block1 and block2.
# Note that by default, the new logical operator is equal to that of block1.
_, stabs_between = merged_block.get_shifted_equivalent_logical_operator(
new_log_op, min(old_log_op_2.data_qubits, key=lambda x: x[0] + x[1])
)
# Retrieve cbits associated with `stabs_between` from the latest stabilizer
# measurements.
stabs_between_cbits = interpretation_step.retrieve_cbits_from_stabilizers(
stabs_between, merged_block
)
log_corrections = log_updates.get(new_log_op.uuid, ()) + log_updates.get(
old_log_op_2.uuid, ()
)
joint_observable = stabs_between_cbits + log_corrections
interpretation_step.logical_measurements[
LogicalMeasurement(
blocks=(block1.unique_label, block2.unique_label),
observable=boundary_type * 2,
)
] = joint_observable
return interpretation_step