"""
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, too-many-lines
from __future__ import annotations
from uuid import uuid4
from functools import cached_property
from itertools import combinations
from pydantic.dataclasses import dataclass
from loom.eka import (
Block,
Lattice,
LatticeType,
PauliOperator,
Stabilizer,
SyndromeCircuit,
Circuit,
Channel,
)
from loom.eka.utilities import (
Direction,
Orientation,
DiagonalDirection,
dataclass_config,
)
from loom_rotated_surface_code.utilities import FourBodySchedule
# pylint: disable=duplicate-code
[docs]
@dataclass(config=dataclass_config)
class RotatedSurfaceCode(Block): # pylint: disable=too-many-public-methods
"""
A sub-class of ``Block`` that represents a rotated surface code block.
Contains methods to create a rotated surface code block along with
properties to access the block's size, upper left qubit, stabilizers,
logical operators, and other relevant information.
"""
[docs]
@classmethod
def create(
# pylint: disable=too-many-branches,too-many-statements, too-many-locals
# pylint: disable=too-many-arguments, too-many-positional-arguments
cls,
dx: int,
dz: int,
lattice: Lattice,
unique_label: str | None = None,
position: tuple[int, ...] = (0, 0),
x_boundary: Orientation = Orientation.HORIZONTAL,
weight_2_stab_is_first_row: bool = True,
weight_4_x_schedule: FourBodySchedule | None = None,
logical_x_operator: PauliOperator | None = None,
logical_z_operator: PauliOperator | None = None,
skip_validation: bool = False,
) -> RotatedSurfaceCode:
"""
Create a ``Block`` object for a rotated surface code block. The orientation of
the block (i.e. where which boundaries are) and where the weight-2 stabilizers
are can be controlled with the ``x_boundary`` and ``weight_2_stab_is_first_row``
argument. By default, the top row and the left column are chosen as the logical
operators. Their pauli string and whether they belong to the logical Z or X
operator depends on the orientation of the boundaries.
The coordinates used for data qubits are the following (here as an example for a
d=3 rotated surface code, with ``x_boundary=Orientation.H`` and
``weight_2_stab_is_first_row=True``):
..code-block::
Z
(0,0) --- (1,0) --- (2,0)
| | |
X | Z | X |
| | |
(0,1) --- (1,1) --- (2,1)
| | |
| X | Z | X
| | |
(0,2) --- (1,2) --- (2,2)
Z
Parameters
----------
dx : int
Size of the block in the horizontal direction
dz : int
Size of the block in the vertical direction
lattice : Lattice
Lattice on which the block is defined. The qubit indices depend on the type
of lattice.
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.
position : tuple[int, ...], optional
Position of the top left corner of the block on the lattice, by default
(0, 0)
x_boundary : Orientation, optional
Specifies whether the X boundaries are horizontal (Orientation.HORIZONTAL),
i.e. going from left to right, or vertical (Orientation.V), i.e. going from
top to bottom.
The X boundary is the boundary that exhibits X Pauli charges. In other words
it is the boundary with 2-body Z stabilizers. By default
Orientation.HORIZONTAL.
weight_2_stab_is_first_row : bool, optional
Specifies whether the top most weight-2 stabilizer at the left boundary is
in the first row (if True) or in the second row (if False), by default True
weight_4_x_schedule : FourBodySchedule, optional
Schedule for measuring the XXXX stabilizer, by default None. If None is
provided, the schedule is calculated from the orientation of the X
boundary. E.g. if ``x_boundary`` is Orientation.HORIZONTAL, the schedule is
set to FourBodySchedule.N.
The default scheme is described in https://arxiv.org/abs/1404.3747 III, B.
In the example above, ``weight_4_x_schedule`` is ``FourBodySchedule.N``,
this is equivalent to measure the upper right XXXX stabilizer in the order
(2,0) -> (2,1) -> (1,0) -> (1,1).
logical_x_operator: PauliOperator | None, optional
Logical X operator. If None is provided, by default the top row or the left
column is chosen (depending on the orientation of the block as specified by
the ``x_boundary`` and ``weight_2_stab_is_first_row`` parameter)
logical_z_operator: PauliOperator | None, optional
Logical Z operator. If None is provided, by default the top row or the left
column is chosen (depending on the orientation of the block as specified by
the ``x_boundary`` and ``weight_2_stab_is_first_row`` parameter)
skip_validation : bool, optional
Skip validation of the block object, by default False.
Returns
-------
Block
Block object for a rotated surface code block
"""
# Input validation
if lattice.lattice_type != LatticeType.SQUARE_2D:
raise ValueError(
"The creation of rotated surface code blocks is "
"currently only supported for 2D square lattices. Instead "
f"the lattice is of type {lattice.lattice_type}."
)
if not isinstance(position, tuple) or any(
not isinstance(x, int) for x in position
):
raise ValueError(
f"`position` must be a tuple of integers. Got '{position}' instead."
)
# Check that lattice co-ordinate of the qubit == system dimension
# The unit vector is not included in the lattice co-ordinate.
if len(position) != lattice.n_dimensions:
raise ValueError(
f"`position` has length {len(position)} while length "
f"{lattice.n_dimensions} is required to match the lattice dimension."
)
if unique_label is None:
unique_label = str(uuid4())
if isinstance(x_boundary, Orientation) is False:
x_boundary = Orientation(x_boundary)
# Create stabilizers
top_left_is_xxxx = (
x_boundary == Orientation.HORIZONTAL
) != weight_2_stab_is_first_row
# Create the schedule for stabilizers
if weight_4_x_schedule is None:
weight_4_x_schedule = (
FourBodySchedule.N
if x_boundary == Orientation.HORIZONTAL
else FourBodySchedule.Z
)
elif isinstance(weight_4_x_schedule, str):
weight_4_x_schedule = FourBodySchedule(weight_4_x_schedule)
weight_4_z_schedule = weight_4_x_schedule.opposite_schedule()
# Generate weight-4 stabilizers covering half of the block in a checkerboard
# pattern starting in the top left corner
if top_left_is_xxxx:
weight4_stabs_top_left = cls.generate_weight4_stabs(
dx, dz, "XXXX", weight_4_x_schedule, True
)
else:
weight4_stabs_top_left = cls.generate_weight4_stabs(
dx, dz, "ZZZZ", weight_4_z_schedule, True
)
# Generate the remaining weight-4 stabilizers
if top_left_is_xxxx:
weight4_stabs_others = cls.generate_weight4_stabs(
dx, dz, "ZZZZ", weight_4_z_schedule, False
)
else:
weight4_stabs_others = cls.generate_weight4_stabs(
dx, dz, "XXXX", weight_4_x_schedule, False
)
stab_left_right_is_x = top_left_is_xxxx != weight_2_stab_is_first_row
stab_left_right = "XX" if stab_left_right_is_x else "ZZ"
stab_top_bottom = "ZZ" if stab_left_right_is_x else "XX"
# Left boundary
if dz % 2 == 1:
num_weight2_stabs = (dz - 1) / 2
else:
num_weight2_stabs = dz / 2 - (not weight_2_stab_is_first_row)
stabs_left = cls.generate_weight2_stabs(
pauli=stab_left_right,
initial_position=(0, (not weight_2_stab_is_first_row)),
num_stabs=num_weight2_stabs,
orientation=Orientation.VERTICAL,
is_bottom_or_right=False,
)
# Right boundary
right_first_row = weight_2_stab_is_first_row != (dx % 2)
if dz % 2 == 1:
num_weight2_stabs = (dz - 1) / 2
else:
num_weight2_stabs = dz / 2 - (not right_first_row)
stabs_right = cls.generate_weight2_stabs(
pauli=stab_left_right,
initial_position=(dx - 1, (not right_first_row)),
num_stabs=num_weight2_stabs,
orientation=Orientation.VERTICAL,
is_bottom_or_right=True,
)
# Top boundary
num_weight2_stabs = dx // 2
if dx % 2 == 1:
num_weight2_stabs = (dx - 1) / 2
else:
num_weight2_stabs = dx / 2 - weight_2_stab_is_first_row
stabs_top = cls.generate_weight2_stabs(
pauli=stab_top_bottom,
initial_position=(weight_2_stab_is_first_row, 0),
num_stabs=num_weight2_stabs,
orientation=Orientation.HORIZONTAL,
is_bottom_or_right=False,
)
# Bottom boundary
bottom_first_col = weight_2_stab_is_first_row == (dz % 2)
if dx % 2 == 1:
num_weight2_stabs = (dx - 1) / 2
else:
num_weight2_stabs = dx / 2 - (not bottom_first_col)
stabs_bottom = cls.generate_weight2_stabs(
pauli=stab_top_bottom,
initial_position=((not bottom_first_col), dz - 1),
num_stabs=num_weight2_stabs,
orientation=Orientation.HORIZONTAL,
is_bottom_or_right=True,
)
# Combine all stabilizers
stabilizers = (
weight4_stabs_top_left
+ weight4_stabs_others
+ stabs_left
+ stabs_right
+ stabs_top
+ stabs_bottom
)
# Define syndrome circuits
xxxx_syndrome_circuit = cls.generate_syndrome_circuit(
pauli="XXXX", padding=None, name="XXXX"
)
zzzz_syndrome_circuit = cls.generate_syndrome_circuit(
pauli="ZZZZ", padding=None, name="ZZZZ"
)
# Get left padding and generate left syndrome circuit
left_padding = cls.find_padding(
boundary="left",
schedule=(
weight_4_x_schedule if stab_left_right_is_x else weight_4_z_schedule
),
)
left_syndrome_circuit = cls.generate_syndrome_circuit(
pauli=stab_left_right,
padding=left_padding,
name=f"left-{stab_left_right}",
)
# Get right padding and generate right syndrome circuit
right_padding = cls.find_padding(
boundary="right",
schedule=(
weight_4_x_schedule if stab_left_right_is_x else weight_4_z_schedule
),
)
right_syndrome_circuit = cls.generate_syndrome_circuit(
pauli=stab_left_right,
padding=right_padding,
name=f"right-{stab_left_right}",
)
# Get top padding and generate top syndrome circuit
top_padding = cls.find_padding(
boundary="top",
schedule=(
weight_4_z_schedule if stab_left_right_is_x else weight_4_x_schedule
),
)
top_syndrome_circuit = cls.generate_syndrome_circuit(
pauli=stab_top_bottom,
padding=top_padding,
name=f"top-{stab_top_bottom}",
)
# Get bottom padding and generate bottom syndrome circuit
bottom_padding = cls.find_padding(
boundary="bottom",
schedule=(
weight_4_z_schedule if stab_left_right_is_x else weight_4_x_schedule
),
)
bottom_syndrome_circuit = cls.generate_syndrome_circuit(
pauli=stab_top_bottom,
padding=bottom_padding,
name=f"bottom-{stab_top_bottom}",
)
# Construct the list of all syndrome circuits
syndrome_circuits = [
xxxx_syndrome_circuit,
zzzz_syndrome_circuit,
top_syndrome_circuit,
bottom_syndrome_circuit,
left_syndrome_circuit,
right_syndrome_circuit,
]
# Create stabilizer_to_circuit mapping
stabilizer_to_circuit = (
{
stab.uuid: (
xxxx_syndrome_circuit.uuid
if top_left_is_xxxx
else zzzz_syndrome_circuit.uuid
)
for stab in weight4_stabs_top_left
}
| {
stab.uuid: (
zzzz_syndrome_circuit.uuid
if top_left_is_xxxx
else xxxx_syndrome_circuit.uuid
)
for stab in weight4_stabs_others
}
| {stab.uuid: left_syndrome_circuit.uuid for stab in stabs_left}
| {stab.uuid: right_syndrome_circuit.uuid for stab in stabs_right}
| {stab.uuid: top_syndrome_circuit.uuid for stab in stabs_top}
| {stab.uuid: bottom_syndrome_circuit.uuid for stab in stabs_bottom}
)
# Create logical operators
if logical_x_operator is None:
qubits = (
[(dx_i, 0, 0) for dx_i in range(dx)]
if x_boundary == Orientation.HORIZONTAL
else [(0, dz_i, 0) for dz_i in range(dz)]
)
logical_x_operator = PauliOperator(
pauli="X" * len(qubits), data_qubits=qubits
)
if logical_z_operator is None:
qubits = (
[(0, dz_i, 0) for dz_i in range(dz)]
if x_boundary == Orientation.HORIZONTAL
else [(dx_i, 0, 0) for dx_i in range(dx)]
)
logical_z_operator = PauliOperator(
pauli="Z" * len(qubits), data_qubits=qubits
)
block = cls(
unique_label=unique_label,
stabilizers=stabilizers,
logical_x_operators=[logical_x_operator],
logical_z_operators=[logical_z_operator],
syndrome_circuits=syndrome_circuits,
stabilizer_to_circuit=stabilizer_to_circuit,
skip_validation=skip_validation,
)
if position == (0, 0):
return block
return block.shift(position)
[docs]
@staticmethod
def find_padding(
boundary: Direction, schedule: FourBodySchedule
) -> tuple[int, int]:
"""
Finds the padding indices for the two body stabilizers. Padding indices
are used to indicate empty time steps in the syndrome circuits. This allows
to standardize the size of syndrome circuits for the rotated surface code.
Parameters
----------
boundary : Direction
Type of boundary for the surface code can be LEFT, RIGHT, TOP, or
BOTTOM
schedule : FourBodySchedule
Schedule for measuring the four body stabilizers, we can deduce the two
body schedule from this.
Returns
-------
tuple[int, int]
Padding indices.
"""
match (schedule, boundary):
case (FourBodySchedule.N, Direction.LEFT) | (
FourBodySchedule.Z,
Direction.BOTTOM,
):
return (2, 3)
case (FourBodySchedule.N, Direction.RIGHT) | (
FourBodySchedule.Z,
Direction.TOP,
):
return (0, 1)
case (FourBodySchedule.N, Direction.TOP) | (
FourBodySchedule.Z,
Direction.RIGHT,
):
return (0, 2)
case (FourBodySchedule.N, Direction.BOTTOM) | (
FourBodySchedule.Z,
Direction.LEFT,
):
return (1, 3)
case _:
return ValueError(
f"The boundary {boundary} and schedule {schedule} are"
f" not compatible. Only N and Z schedules are "
"supported."
)
[docs]
@staticmethod
def generate_syndrome_circuit(
pauli: str, padding: tuple[int, ...] | None, name: str
) -> SyndromeCircuit:
"""
Generates a syndrome circuit for a given Pauli string. The syndrome
circuits generated are all the same size, where padding indices indicate
where to add empty time steps.
Parameters
----------
pauli : str
Pauli string associated the stabilizer
padding : tuple[int, int] | None
Padding indices for the two body stabilizers. Padding indices are used
to locate empty spaces in the syndrome circuits.
name : str
Name of the syndrome circuit
Returns
-------
SyndromeCircuit
Syndrome circuit for the Pauli string
"""
weight = len(pauli)
data_channels = [Channel(type="quantum", label=f"d{i}") for i in range(weight)]
cbit_channel = Channel(type="classical", label="c0")
ancilla_channel = Channel(type="quantum", label="a0")
reset = [Circuit("Reset_0", channels=[ancilla_channel])]
hadamard1 = [Circuit("H", channels=[ancilla_channel])]
hadamard2 = [Circuit("H", channels=[ancilla_channel])]
# If there is no padding, there are no extra empty time steps
if padding is None:
entangle_ancilla = [
[Circuit(f"C{p}", channels=[ancilla_channel, data_channels[i]])]
for i, p in enumerate(pauli)
]
# If there is padding, we add empty time steps to the circuit
else:
# We create lists of single items to insert None at the padding indices
padded_pauli = list(pauli)
padded_data_channels = list(data_channels)
for i in padding:
padded_pauli.insert(i, None)
padded_data_channels.insert(i, None)
entangle_ancilla = [
(
[
Circuit(
f"C{p}",
channels=[ancilla_channel, padded_data_channels[i]],
)
]
if p is not None # If not None, we add the empty time step
else []
)
for i, p in enumerate(padded_pauli)
]
measurement = [Circuit("Measurement", channels=[ancilla_channel, cbit_channel])]
circuit_list = [reset, hadamard1] + entangle_ancilla + [hadamard2, measurement]
return SyndromeCircuit(
pauli=pauli,
name=name,
circuit=Circuit(
name=name,
circuit=circuit_list,
channels=data_channels + [ancilla_channel, cbit_channel],
),
)
# Properties
@property
def size(self) -> tuple[int, int]:
"""
Return the size of the block in the horizontal and vertical direction.
"""
size_0 = max(qb[0] for qb in self.data_qubits) - min(
qb[0] for qb in self.data_qubits
)
size_1 = max(qb[1] for qb in self.data_qubits) - min(
qb[1] for qb in self.data_qubits
)
return (size_0 + 1, size_1 + 1)
@property
def upper_left_qubit(self) -> tuple[int, ...]:
"""
Return the qubit with the smallest coordinates in the block.
"""
return min(self.data_qubits, key=lambda x: x[0] + x[1])
@property
def upper_left_4body_stabilizer(self) -> Stabilizer:
"""
Return the 4-body stabilizer associated with the upper left qubit.
"""
return [
stab
for stab in self.stabilizers
if len(stab.data_qubits) == 4 and self.upper_left_qubit in stab.data_qubits
][0]
# NOTE these four properties should be deleted and included as fields at the
# creation of the RotatedSurfaceCode instance
@property
def weight_4_x_schedule(self) -> FourBodySchedule:
"""
Return the schedule for measuring the XXXX stabilizer.
"""
for stab in self.stabilizers:
if stab.pauli == "XXXX":
schedule = (
FourBodySchedule.N
if stab.data_qubits[1][1] > stab.data_qubits[0][1]
else FourBodySchedule.Z
)
return schedule
raise ValueError("XXXX stabilizer not found in the stabilizers.")
@property
def weight_4_z_schedule(self) -> FourBodySchedule:
"""
Return the schedule for measuring the ZZZZ stabilizer.
"""
return (
FourBodySchedule.N
if self.weight_4_x_schedule == FourBodySchedule.Z
else FourBodySchedule.Z
)
@property
def x_boundary(self) -> Orientation:
"""
Return the orientation of the X boundary.
"""
if self.boundary_type(Direction.TOP) == "X":
return Orientation.HORIZONTAL
# if the top boundary is Z, then the X boundary is vertical
return Orientation.VERTICAL
@property
def weight_2_stab_is_first_row(self) -> bool:
"""
Return whether the top most weight-2 stabilizer at the left boundary is
in the first row.
"""
first_qubit = self.upper_left_qubit
second_qubit = tuple(
q if i != 1 else q + 1 for i, q in enumerate(self.upper_left_qubit)
)
return any(
# first_qubit in stab.data_qubits and second_qubit in stab.data_qubits
# and len(stab.data_qubits) == 2
set([first_qubit, second_qubit]) == set(stab.data_qubits)
for stab in self.stabilizers
)
@property
def topological_corners(self) -> tuple[tuple[int, ...], ...]:
"""
Return the coordinates of the topological corners of the block.
"""
return tuple(q for q, p in self.pauli_charges.items() if p == "Y")
@property
def geometric_corners(self) -> tuple[tuple[int, ...], ...]:
"""
Return the coordinates of the geometric corners of the block. The geometric
corners that can be detected are qubits that may be the single most:
- top-left qubit
- top-right qubit
- bottom-left qubit
- bottom-right qubit
- right-qubit
- bottom-qubit
- left-qubit
- top-qubit
"""
def get_sole_max(lambda_expression):
"""
Find and return the qubit with the maximum value of the lambda expression
only if it is the only qubit with that value. If there are multiple qubits
with the same value, return None.
"""
max_value = max(lambda_expression(q) for q in self.data_qubits)
max_qubits = [
q for q in self.data_qubits if lambda_expression(q) == max_value
]
return max_qubits[0] if len(max_qubits) == 1 else None
lambdas_to_maximize = [
lambda x: x[0] + x[1], # bottom right corner
lambda x: x[0] - x[1], # top right corner
lambda x: -x[0] + x[1], # bottom left corner
lambda x: -x[0] - x[1], # top left corner
lambda x: x[0], # right corner
lambda x: x[1], # bottom corner
lambda x: -x[0], # left corner
lambda x: -x[1], # top corner
]
return tuple(
get_sole_max(lambda_expression)
for lambda_expression in lambdas_to_maximize
if get_sole_max(lambda_expression) is not None
)
@property
def all_boundary_stabilizers(self) -> tuple[Stabilizer, ...]:
"""
Return the stabilizers associated with any boundary.
"""
return (
self.boundary_stabilizers(direction=Direction.TOP)
+ self.boundary_stabilizers(direction=Direction.BOTTOM)
+ self.boundary_stabilizers(direction=Direction.LEFT)
+ self.boundary_stabilizers(direction=Direction.RIGHT)
)
@property
def bulk_stabilizers(self) -> tuple[Stabilizer, ...]:
"""
Return the stabilizers not associated with any boundary.
"""
return tuple(
stab
for stab in self.stabilizers
if stab not in self.all_boundary_stabilizers
)
@property
def orientation(self) -> Orientation | None:
"""
Return the orientation of the block. If the block is square, return None.
"""
if self.size[0] > self.size[1]:
return Orientation.HORIZONTAL
if self.size[0] < self.size[1]:
return Orientation.VERTICAL
return None
@property
def is_horizontal(self) -> bool:
"""
Return True if the horizontal size is larger than the vertical size.
"""
return self.orientation == Orientation.HORIZONTAL
@property
def is_vertical(self) -> bool:
"""
Return True if the vertical size is larger than the horizontal size.
"""
return self.orientation == Orientation.VERTICAL
# Static methods
[docs]
@staticmethod
def generate_weight4_stabs(
dx: int,
dz: int,
pauli: str,
schedule: FourBodySchedule,
start_in_top_left_corner: bool,
initial_position: tuple[int, int] = (0, 0),
) -> list[Stabilizer]:
"""
Generate the list of all weight-4 stabilizers of a rotated surface code
block of the given type (= pauli string).
Parameters
----------
dx: int
Distance in the horizontal direction. If dx=3, there will be 2 weight-4
stabilizers created in the horizontal direction.
dz: int
Distance in the vertical direction. If dz=3, there will be 2 weight-4
stabilizers created in the vertical direction.
pauli : str
Pauli string of the stabilizers
start_in_top_left_corner : bool
If True, the first stabilizer will start in the top left corner and then
follow the alternating checkerboard pattern. If False, it will be
exactly the opposite covering of the checkerboard pattern.
schedule : FourBodySchedule
Schedule for measuring the four body stabilizers, see
https://arxiv.org/abs/1404.3747, III, B. for more details.
initial_position : tuple[int, int], optional
Initial position where the chain of weight-4 stabilizers should start,
by default (0, 0)
Returns
-------
list[Stabilizer]
List of weight-4 stabilizers of the specified type for the rotated
surface code
"""
if len(initial_position) > 2:
initial_position = initial_position[:2]
elif len(initial_position) < 2:
raise ValueError(
"Initial position must be a tuple of length >= 2. Got "
f"{initial_position}."
)
x, z = initial_position
is_n_pattern = schedule == FourBodySchedule.N
return [
Stabilizer(
pauli=pauli,
data_qubits=[
(x + dx_i + 1, z + dz_i, 0),
(x + dx_i + is_n_pattern, z + dz_i + is_n_pattern, 0),
(x + dx_i + (not is_n_pattern), z + dz_i + (not is_n_pattern), 0),
(x + dx_i, z + dz_i + 1, 0),
],
ancilla_qubits=[(x + dx_i + 1, z + dz_i + 1, 1)],
)
for dx_i in range(dx - 1)
for dz_i in range(dz - 1)
if (dx_i + dz_i) % 2 != start_in_top_left_corner
]
[docs]
@staticmethod
def generate_weight2_stabs(
pauli: str,
initial_position: tuple[int, int],
num_stabs: int,
orientation: Orientation,
is_bottom_or_right: bool,
) -> list[Stabilizer]:
"""
Generate the list of all weight-2 stabilizers along one of the four
boundaries. Note that the schedule for measuring the weight-4 stabilizers
does not change the order in which we specify the weight-2 stabilizers.
For stabilizers along the right and bottom boundaries, the stabilizers are
generated differently. The stabilizers along the right and bottom boundary have
the coordinates of the ancilla qubits shifted by (1, 0) and (0, 1) respectively.
Parameters
----------
pauli : str
Pauli string of the stabilizers
initial_position : tuple[int, int]
Initial position where the chain of weight-2 stabilizers should start
num_stabs : int
Number of weight-2 stabilizers to generate along this boundary
orientation : Orientation
Orientation of the boundary, either HORIZONTAL or VERTICAL
is_bottom_or_right : bool
If True, the stabilizers are along the bottom or right boundaries, this
means their ancilla qubit is 'outside' of the block geometry. If False, the
stabilizers are along the top or left boundaries.
Returns
-------
list[Stabilizer]
List of weight-2 stabilizers along the specified boundary
"""
is_horizontal = orientation == Orientation.HORIZONTAL
return [
Stabilizer(
pauli=pauli,
data_qubits=[
(
initial_position[0] + is_horizontal * (2 * i + 1),
initial_position[1] + (not is_horizontal) * 2 * i,
0,
),
(
initial_position[0] + is_horizontal * (2 * i),
initial_position[1] + (not is_horizontal) * (2 * i + 1),
0,
),
],
ancilla_qubits=[
(
initial_position[0]
+ 1 * (not is_horizontal) * is_bottom_or_right
+ is_horizontal * (2 * i + 1),
initial_position[1]
+ 1 * is_horizontal * is_bottom_or_right
+ (not is_horizontal) * (2 * i + 1),
1,
)
],
)
for i in range(0, int(num_stabs))
]
# Instance methods
[docs]
def boundary_qubits(self, direction: Direction | str) -> list[tuple[int, ...]]:
"""
Return the data qubits that are part of the specified boundary.
Parameters
----------
direction : Direction | str
Boundary (top, bottom, left, or right) for which the data qubits should be
returned. If a string is provided, it is converted to a Direction enum.
Returns
-------
list[tuple[int, ...]]
Data qubits that are part of the specified boundary
"""
# Input validation: cast direction to Direction enum if it is not already
if not isinstance(direction, Direction):
direction = Direction(direction)
axis = 1 if direction in [Direction.TOP, Direction.BOTTOM] else 0
selector_function = (
max if direction in [Direction.BOTTOM, Direction.RIGHT] else min
)
min_or_max_value = selector_function(qb[axis] for qb in self.data_qubits)
return [qb for qb in self.data_qubits if qb[axis] == min_or_max_value]
[docs]
def boundary_type(self, direction: Direction | str) -> str:
"""
Return the type of the specified boundary, either X or Z. This assumes that
the block is a standard rotated surface code block which is a square block with
Y charges at the four corners and X and Z charges at the boundaries.
Note that there are different conventions about when to call a boundary X or Z.
We call a boundary X type if it exhibits X Pauli charges. In other words, it is
of X type if the stabilizers along the boundary are Z stabilizers.
Parameters
----------
direction : Direction | str
Boundary (top, bottom, left, or right) for which the data qubits should be
returned. If a string is provided, it is converted to a Direction enum.
Returns
-------
str
Type of the boundary, either X or Z
"""
# Input validation: cast direction to Direction if it is not already
if not isinstance(direction, Direction):
direction = Direction(direction)
# Input validation: check that block size is > 2 in the direction of the
# boundary
if direction in [Direction.TOP, Direction.BOTTOM] and self.size[1] <= 2:
raise ValueError(
"The block is too small to have a top or bottom boundary of well "
"defined type."
)
if direction in [Direction.LEFT, Direction.RIGHT] and self.size[0] <= 2:
raise ValueError(
"The block is too small to have a left or right boundary of well "
"defined type."
)
# Get the Pauli charges of the boundary qubits,
# transform them into a set to remove duplicates,
# and remove the "Y" charge to be left with the X and Z charges.
# For the standard rotated surface code block, there should be only one charge
# left.
boundary_pauli_charges = list(
set(self.pauli_charges[qb] for qb in self.boundary_qubits(direction))
- set("Y")
)
if len(boundary_pauli_charges) > 1:
raise RuntimeError(
"Boundary has multiple Pauli charges. This should not happen for the "
"standard rotated surface code block. The boundary pauli charges "
f"(excluding Y charges) are {boundary_pauli_charges}."
)
return boundary_pauli_charges[0]
[docs]
def boundary_stabilizers(self, direction: Direction) -> tuple[Stabilizer, ...]:
"""
Return the stabilizers associated with the given boundary direction.
"""
boundary_qubits = self.boundary_qubits(direction)
return tuple(
stab
for stab in self.stabilizers
if all(q in boundary_qubits for q in stab.data_qubits)
)
[docs]
def get_corner_from_direction(
self, which_corner: DiagonalDirection
) -> tuple[int, int, int]:
"""
Get the coordinates of the qubit at the specified corner of the block.
"""
return (
set(self.boundary_qubits(which_corner.components[0]))
.intersection(self.boundary_qubits(which_corner.components[1]))
.pop()
)
@cached_property
def config_and_pivot_corners(
self,
) -> tuple[int, tuple[tuple[int, int, int], ...]]:
"""
Classify the Block based on the geometry of its topological corners. Return the
config and the topological corners ordered in a specific way that reflects their
geometric arrangement.
Type 1: Rectangular config
Four topological corners coincide with 4 geometric corners
The list of corners is returned in the order:
(top-left, bottom-left, bottom-right, top-right)
Visualized example::
1-------4
| |
| |
| |
2-------3
Type 2: U-config
Block has rectangular shape with size (d, 2d-1) or (2d-1, d)
Three of four topological corners coincide with three geometric corners,
the last topological corner resides on the middle of the long edge whose
ends occupy only one topological corner
The list of corners is returned in the order:
(long_end, middle_edge, angle, short_end)
Visualized example (can be rotated)::
1-------|
| |
| 2
| |
3-------4
Type 3: L-config
Block has rectangular shape with size (d, 2d-1) or (2d-1, d)
Three of four topological corners coincide with three geometric corners,
the last topological corner resides on the middle of the long edge whose
ends occupy two topological corners.
The list of corners is returned in the order as seen below:
(long_end, middle_edge, angle, short_end)
Visualized example (can be rotated)::
1-------|
| |
2 |
| |
3-------4
Type 4: U-config (phase gate)
Block has rectangular shape with size (d, 2d) or (2d, d)
Three of four topological corners coincide with three geometric corners,
the last topological corner resides in the middle of the long edge whose
ends occupy only one topological corner. Since the length of a long edge
is even, there are two qubits near the middle point of the edge.
Only support the case where the qubit is further away from the end
occupied by a topological corner.
The list of corners is returned in the order:
(long_end, middle_edge, angle, short_end)
Visualized example (can be rotated)::
1-------|
| |
| 2
| |
| |
3-------4
Type 0: Other configs
Returns
-------
int:
The configuration type of the block.
tuple[tuple[int, int, int], ...]:
The list of corners in the order specified by the configuration type.
"""
# Find topological corners that are also geometric corners
dx, dz = self.size
is_block_horizontal = dx >= dz
topological_geometric_corners = set(self.topological_corners) & set(
self.geometric_corners
)
if len(topological_geometric_corners) == 4:
# Type 1 (rectangle)
return 1, sorted(topological_geometric_corners)
is_almost_two_squares = dx == 2 * dz - 1 or dz == 2 * dx - 1
is_two_squares = dx == 2 * dz or dz == 2 * dx
is_ul_config = len(topological_geometric_corners) == 3 and is_almost_two_squares
is_uphase_config = len(topological_geometric_corners) == 3 and is_two_squares
if not (is_ul_config or is_uphase_config):
# Type 0
return 0, self.topological_corners
# Find the last topological corner
topological_non_geometric_corner = (
set(self.topological_corners) - set(topological_geometric_corners)
).pop()
# Based on the orientation of the block, deduce the long and short edge indices
long_edge_idx, short_edge_idx = (0, 1) if is_block_horizontal else (1, 0)
# Find two topological-geometric corners that reside on the same long edge
long_edge_geometric_corners = next(
(corner_1, corner_2)
for (corner_1, corner_2) in combinations(topological_geometric_corners, 2)
if corner_1[short_edge_idx] == corner_2[short_edge_idx]
)
# Find the third topological-geometric corner
short_edge_geometric_corner = next(
corner
for corner in topological_geometric_corners
if corner not in long_edge_geometric_corners
)
# Sort the long_edge_geometric_corners so that the first returned corner is
# the long_end.
long_edge_geometric_corners = sorted(
long_edge_geometric_corners,
reverse=topological_non_geometric_corner[long_edge_idx]
> short_edge_geometric_corner[long_edge_idx],
)
# Verify that the distances (along the long edge) between the last topological
# corner and two long_edge_corners differ by no larger than one unit.
# If it's not, the config is not U or L
middle_long_dist = abs(
topological_non_geometric_corner[long_edge_idx]
- long_edge_geometric_corners[0][long_edge_idx]
) # distance from middle_edge corner to long_end corner
middle_angle_dist = abs(
long_edge_geometric_corners[1][long_edge_idx]
- topological_non_geometric_corner[long_edge_idx]
) # distance from angle_corner to middle_edge corner
dist_diff = (
middle_angle_dist - middle_long_dist
) # no abs because only one case is supported
if dist_diff not in [0, 1]:
# Type 0
return 0, self.topological_corners
# If the short edge index of the topological non-geometric corner matches:
# - the short edge geometric corner, then it is a U shape
# - one of the long edge geometric corners, then it is a L shape
if (
topological_non_geometric_corner[short_edge_idx]
== short_edge_geometric_corner[short_edge_idx]
) and dist_diff == 0:
# Type 2 (U shape)
config = 2
elif (
topological_non_geometric_corner[short_edge_idx]
== long_edge_geometric_corners[0][short_edge_idx]
) and dist_diff == 0:
# Type 3 (L shape)
config = 3
elif (
topological_non_geometric_corner[short_edge_idx]
== short_edge_geometric_corner[short_edge_idx]
) and dist_diff == 1:
# Type 4 (U shape phase gate)
config = 4
else:
raise RuntimeError("Something went wrong with the configuration detection.")
# Put all the corners together
corners = (
long_edge_geometric_corners[0], # long_end
topological_non_geometric_corner, # middle_edge
long_edge_geometric_corners[1], # angle
short_edge_geometric_corner, # short_edge
)
return config, corners
[docs]
def get_shifted_equivalent_logical_operator(
self, initial_operator: PauliOperator, new_upleft_qubit: tuple[int, ...]
) -> tuple[PauliOperator, tuple[Stabilizer, ...]]:
"""
Shifts the initial operator to a valid operator in the block that contains
new_upleft_qubit.
NOTE: this function currently assumes that the block is a square rotated surface
code block with a single logical X and Z operator.
Parameters
----------
initial_operator : PauliOperator
Initial logical operator for which the equivalent logical operator in the
block should be found.
new_upleft_qubit : tuple[int, ...]
New top left qubit of the operator in the block.
Returns
-------
tuple[PauliOperator, tuple[Stabilizer, ...]]
Equivalent logical operator in the block and the stabilizers that are
required to go from the initial to the new operator.
Raises
------
NotImplementedError
If the block has more than one logical X or Z operator.
ValueError
If the new upleft qubit is not part of the data qubits of the block.
ValueError
If the initial operator is not one of the logical operators of the block.
ValueError
If the new upleft qubit is not part of the correct boundary.
ValueError
If the shift vector has more than a single non-zero dimension (ill defined
logical operators to begin with).
"""
if len(self.logical_x_operators) != 1 or len(self.logical_z_operators) != 1:
raise NotImplementedError(
"This function currently only supports blocks with a single logical "
"X and Z operator."
)
if new_upleft_qubit not in self.data_qubits:
raise ValueError(
f"The new upleft qubit {new_upleft_qubit} is not part of the data "
"qubits of the block."
)
if initial_operator not in [
self.logical_x_operators[0],
self.logical_z_operators[0],
]:
raise ValueError(
"The initial operator must be one of the logical operators of the "
"block."
)
# We assume all Xs or all Zs pauli operators
# The new upper left qubit needs to be part of the right boundary, Y is valid.
# E.g. Z logical has an X or Y at it upper left most qubit.
if self.pauli_charges[new_upleft_qubit] == initial_operator.pauli[0]:
raise ValueError(
f"The new upleft qubit {new_upleft_qubit} is not part of the correct "
"boundary."
)
initial_upleft_qubit = min(
initial_operator.data_qubits, key=lambda x: x[0] + x[1]
)
shift_vector = tuple(
coord1 - coord2
for coord1, coord2 in zip(
new_upleft_qubit, initial_upleft_qubit, strict=True
)
)
new_logical_operator = PauliOperator(
pauli=initial_operator.pauli,
data_qubits=[
tuple(
coord1 + shift
for coord1, shift in zip(qb, shift_vector, strict=True)
)
for qb in initial_operator.data_qubits
],
)
# Find all stabilizers in the block that are included between the initial and
# new operator
def vector_range(vector: tuple[int, ...]) -> list[tuple[int, ...]]:
"""
Generate vectors from (0, ..., 0) to (0, ..., k, ..., 0) where k is the only
non-zero element in the input vector. The position of k in the tuple may
vary. If k is negative, the range goes from k to 0.
E.g.:
vector_range((0, 5)) -> [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5)]
vector_range((-3, 0, 0)) -> [(-3, 0, 0), (-2, 0, 0), (-1, 0, 0), (0, 0, 0)]
"""
# Find the position and value of the non-zero element in the vector
k_position = next(i for i, x in enumerate(vector) if x != 0)
k = vector[k_position]
# Determine the range based on the sign of k
if k > 0:
range_values = range(k + 1)
else:
range_values = range(k, 1)
return [
tuple(i if j == k_position else 0 for j in range(len(vector)))
for i in range_values
]
# Test that shift vector has at most a single non-zero dimension
n_non_zero_dims = sum(1 for x in shift_vector if x != 0)
match n_non_zero_dims:
# The two qubits have the same coordinates
case 0:
return new_logical_operator, ()
# The two qubits have a single non-zero dimension, they are aligned
case 1:
qubits_in_between = [
tuple(
qubit_coord + shift_coord
for (qubit_coord, shift_coord) in zip(q, v, strict=True)
)
for q in initial_operator.data_qubits
for v in vector_range(shift_vector)
]
# Here we assume all Xs or all Zs stabilizers
stabilizers_in_between = [
stab
for stab in self.stabilizers
if all(q in qubits_in_between for q in stab.data_qubits)
and stab.pauli[0] == initial_operator.pauli[0]
]
return new_logical_operator, tuple(stabilizers_in_between)
# The two qubits have more than a single non-zero dimension,
# they are not aligned.
case _:
raise ValueError(
"The shift vector must have at most a single non-zero dimension."
)
[docs]
def rename(self, name: str) -> RotatedSurfaceCode:
return super().rename(name)
[docs]
def shift(
self, position: tuple[int, ...], new_label: str | None = None
) -> RotatedSurfaceCode:
return super().shift(position, new_label)
def __eq__(self, other) -> bool: # pylint: disable=useless-parent-delegation
return super().__eq__(other)
@cached_property
def stabilizers_labels(self) -> dict[str, dict[str, tuple[int, ...]]]:
return super().stabilizers_labels