from copy import deepcopy
import numpy as np
from ..layouts.layout import Layout
from ..layouts.operations import check_overlap_layouts
from .util import set_idle, set_trans_cnot, set_x, set_z
__all__ = [
"set_x",
"set_z",
"set_idle",
"set_fold_trans_s",
"set_trans_cnot",
"set_encoding",
]
[docs]
def set_fold_trans_s(layout: Layout, data_qubit: str) -> None:
"""Adds the required attributes (in place) for the layout to run the transversal S
gate for the rotated surface code.
This implementation assumes that the qubits are placed in a square 2D grid.
Parameters
----------
layout
The layout in which to add the attributes.
data_qubit
The data qubit in a corner through which the folding of the surface
code runs.
Notes
-----
The circuit implementation follows from https://doi.org/10.22331/q-2024-04-08-1310.
The information about the logical transversal S gate is stored in the layout
as the parameter ``"trans-s_{log_qubit_label}"`` for each of the qubits,
where for the case of data qubits it is the information about which gates
to perform and for the case of the ancilla qubits it corresponds to
how the stabilizers generators are transformed.
"""
if layout.code != "rotated_surface_code":
raise ValueError(
"This function is for rotated surface codes, "
f"but a layout for the code {layout.code} was given."
)
if layout.distance_z != layout.distance_x + 1:
raise ValueError("The transversal S gate requires d_z = d_x + 1.")
if data_qubit not in layout.data_qubits:
raise ValueError(f"{data_qubit} is not a data qubit from the given layout.")
if set(map(len, layout.get_coords(layout.qubits))) != {2}:
raise ValueError("The qubit coordinates must be 2D.")
if len(layout.logical_qubits) != 1:
raise ValueError(
"The given surface code does not have a logical qubit, "
f"it has {len(layout.logical_qubits)}."
)
data_qubits = layout.data_qubits
anc_qubits = layout.anc_qubits
stab_x = layout.get_qubits(role="anc", stab_type="x_type")
stab_z = layout.get_qubits(role="anc", stab_type="z_type")
gate_label = f"log_fold_trans_s_{layout.logical_qubits[0]}"
# get the jump coordinates
neighbors: dict[str, str] = layout.param("neighbors", data_qubit)
dir_x, anc_qubit_x = [(d, q) for d, q in neighbors.items() if q in stab_x][0]
dir_z, anc_qubit_z = [(d, q) for d, q in neighbors.items() if q in stab_z][0]
data_qubit_h = layout.get_neighbors(anc_qubit_x, direction=dir_z)[0]
jump_h = np.array(layout.param("coords", data_qubit_h)) - np.array(
layout.param("coords", data_qubit)
)
jump_v = np.array([jump_h[1], -jump_h[0]]) # perpendicular vector
data_qubit_coords = np.array(layout.param("coords", data_qubit), dtype=float)
# get the CZs from the data qubit positions
coords_to_label_dict = {
tuple(attr["coords"]): node for node, attr in layout.graph.nodes.items()
}
def coords_to_label(
c: tuple[int | float, ...] | np.typing.NDArray[np.float64],
) -> None | str:
c = tuple(c)
if c not in coords_to_label_dict:
return None
else:
return coords_to_label_dict[c]
top_column = deepcopy(data_qubit_coords)
curr_level = 0
cz_gates: dict[str, str] = {}
while True:
if coords_to_label(top_column) is None:
break
coords1 = top_column + curr_level * jump_v
label1 = coords_to_label(coords1)
if label1 is None:
top_column += jump_v + jump_h
curr_level = 0
continue
coords2 = top_column + (curr_level + 1) * jump_h
label2 = coords_to_label(coords2)
cz_gates[label2] = label1
cz_gates[label1] = label2
curr_level += 1
# get S gates from the data qubit positions
s_gates = {q: "I" for q in data_qubits}
s_gates[data_qubit] = "S"
coords = deepcopy(data_qubit_coords) + jump_h
while True:
label = coords_to_label(coords)
if label is None:
break
s_gates[label] = "S"
coords += jump_h + jump_v
label = coords_to_label(coords - jump_h - jump_v)
s_gates[label] = "S_DAG"
# Store logical gate information to the data qubits
for qubit in data_qubits:
layout.set_param(
gate_label, qubit, {"cz": cz_gates[qubit], "local": s_gates[qubit]}
)
# Compute the new stabilizer generators based on the CZs connections
# as 'set' is not hashable, I use tuple(sorted(...))...
anc_to_xstab = {
anc_qubit: tuple(sorted(layout.get_neighbors([anc_qubit])))
for anc_qubit in stab_x
}
zstab_to_anc = {
tuple(sorted(layout.get_neighbors([anc_qubit]))): anc_qubit
for anc_qubit in stab_z
}
anc_to_new_stab = {}
for anc_x, stab in anc_to_xstab.items():
if anc_x == anc_qubit_x:
anc_to_new_stab[anc_x] = [anc_x]
continue
z_stab: set[str] = set()
for d in stab:
if s_gates[d] == "I":
z_stab.symmetric_difference_update([cz_gates[d]])
else:
z_stab.symmetric_difference_update([d, cz_gates[d]])
anc_z = zstab_to_anc[tuple(sorted(z_stab))]
anc_to_new_stab[anc_x] = [anc_x, anc_z]
anc_to_new_stab[anc_z] = [anc_z]
# Store new stabilizer generators to the ancilla qubits
# the stabilizer generator propagation for S_dag is the same for S
for anc_qubit in anc_qubits:
layout.set_param(
gate_label,
anc_qubit,
{
"new_stab_gen": anc_to_new_stab[anc_qubit],
"new_stab_gen_inv": anc_to_new_stab[anc_qubit],
},
)
return
def set_trans_cnot_mid_cycle_css(layout_c: Layout, layout_t: Layout) -> None:
"""Adds the required attributes (in place) for the layout to run the
transversal CNOT gate for the rotated surface code in the mid cycle state,
which corresponds to a CSS unrotated surface code.
Parameters
----------
layout_c
The layout for the control of the CNOT for which to add the attributes.
layout_t
The layout for the target of the CNOT for which to add the attributes.
Notes
-----
This mid-cycle transversal gate is intended for the QEC cycle the uses CNOTs,
thus this transversal gate is implemented using CNOTs.
"""
if (layout_c.code != "rotated_surface_code") or (
layout_t.code != "rotated_surface_code"
):
raise ValueError(
"This function is for rotated surface codes, "
f"but layouts for {layout_t.code} and {layout_c.code} were given."
)
if (layout_c.distance_x != layout_t.distance_x) or (
layout_c.distance_z != layout_t.distance_z
):
raise ValueError("This function requires two surface codes of the same size.")
check_overlap_layouts(layout_c, layout_t)
gate_label = f"log_trans_cnot_mid_cycle_css_{layout_c.logical_qubits[0]}_{layout_t.logical_qubits[0]}"
qubit_coords_c = layout_c.qubit_coords
qubit_coords_t = layout_t.qubit_coords
bottom_left_qubit_c = sorted(
qubit_coords_c.items(), key=lambda x: 999_999_999 * x[1][0] + x[1][1]
)
bottom_left_qubit_t = sorted(
qubit_coords_t.items(), key=lambda x: 999_999_999 * x[1][0] + x[1][1]
)
mapping_t_to_c = {}
mapping_c_to_t = {}
for (qc, _), (qt, _) in zip(bottom_left_qubit_c, bottom_left_qubit_t):
mapping_t_to_c[qt] = qc
mapping_c_to_t[qc] = qt
# Store the logical information for the data qubits
for qubit in layout_c.data_qubits:
layout_c.set_param(gate_label, qubit, {"cnot": mapping_c_to_t[qubit]})
for qubit in layout_t.data_qubits:
layout_t.set_param(gate_label, qubit, {"cnot": mapping_t_to_c[qubit]})
# Compute the new stabilizer generators based on the CNOT connections
anc_to_new_stab = {}
for anc in layout_c.get_qubits(role="anc", stab_type="z_type"):
anc_to_new_stab[anc] = [anc]
for anc in layout_c.get_qubits(role="anc", stab_type="x_type"):
anc_to_new_stab[anc] = [anc, mapping_c_to_t[anc]]
for anc in layout_t.get_qubits(role="anc", stab_type="z_type"):
anc_to_new_stab[anc] = [anc, mapping_t_to_c[anc]]
for anc in layout_t.get_qubits(role="anc", stab_type="x_type"):
anc_to_new_stab[anc] = [anc]
# Store new stabilizer generators to the ancilla qubits
# CNOT^\dagger = CNOT
for anc in layout_c.anc_qubits:
layout_c.set_param(
gate_label,
anc,
{
"new_stab_gen": anc_to_new_stab[anc],
"new_stab_gen_inv": anc_to_new_stab[anc],
"cnot": mapping_c_to_t[anc],
},
)
for anc in layout_t.anc_qubits:
layout_t.set_param(
gate_label,
anc,
{
"new_stab_gen": anc_to_new_stab[anc],
"new_stab_gen_inv": anc_to_new_stab[anc],
"cnot": mapping_t_to_c[anc],
},
)
return
[docs]
def set_encoding(layout: Layout) -> None:
"""
Adds the required attributes (in place) for the layout to run the encoding
circuits for the rotated surface code.
This implementation assumes that the qubits are placed in a square 2D grid,
and that the (spatial) separation between qubits is more than ``1e-6``.
Parameters
----------
layout
The (square) layout in which to add the attributes.
Notes
-----
The implementation follows Figure 1 from:
Claes, Jahan. "Lower-depth local encoding circuits for the surface code."
arXiv preprint arXiv:2509.09779 (2025).
The information about the encoding circuit is stored in the layout
as the parameter ``"encoding_{log_qubit_label}"`` for each of the data qubits.
"""
if layout.code != "rotated_surface_code":
raise ValueError(
"This function is for rotated surface codes, "
f"but a layout for the code {layout.code} was given."
)
if layout.distance_x != layout.distance_z:
raise ValueError(
"This function is for square surface codes, "
f"but d_x={layout.distance_x} and d_z={layout.distance_z} were given."
)
gate_label = f"encoding_{layout.logical_qubits[0]}"
# identify data qubits in the 4 corners of the surface code
corners: list[str] = []
for data_qubit in layout.data_qubits:
if len(layout.get_neighbors([data_qubit])) != 2:
continue
corners.append(data_qubit)
# order the corner qubits following:
# top left, top right, bottom right, bottom left
if layout.distance_x % 2 == 1:
z_corners: list[str] = []
for data_qubit in corners:
z_anc = layout.get_neighbors([data_qubit], stab_type="z_type")[0]
if len(layout.get_neighbors([z_anc])) == 2:
z_corners.append(data_qubit)
x_corners = [q for q in corners if q not in z_corners]
corners = [z_corners[0], x_corners[0], z_corners[1], x_corners[1]]
else:
distance: list[float] = []
coord = np.array(layout.get_coords([corners[0]])[0])
for q in corners:
other_coord = np.array(layout.get_coords([q])[0])
distance.append(((coord - other_coord) ** 2).sum())
corners = [q for _, q in sorted(zip(distance, corners))]
# enforce that the (0, 1) goes along the direction where there is no
# weight-2 stabilizer between (0, 0) and (0, 1)
c1, c2, c3, c4 = corners
ancs = layout.get_neighbors([c1])
anc = ancs[0] if len(layout.get_neighbors([ancs[0]])) == 2 else ancs[1]
c1_neigh = [q for q in layout.get_neighbors([anc]) if q != c1][0]
dir_neigh = np.array(layout.get_coords([c1_neigh])[0]) - coord
dir_c2 = np.array(layout.get_coords([c2])[0]) - coord
if not np.isclose(dir_neigh * dir_c2, 0).all():
# if they are not perpendicular, then change
c2, c3 = c3, c2
corners = [c1, c2, c4, c3]
# get directions for moving horizontally and vertically
top_left_coord = np.array(layout.get_coords([corners[0]])[0])
top_right_coord = np.array(layout.get_coords([corners[1]])[0])
bottom_right_coord = np.array(layout.get_coords([corners[2]])[0])
dir_h = (top_right_coord - top_left_coord) / (layout.distance_x - 1)
dir_v = (bottom_right_coord - top_right_coord) / (layout.distance_x - 1)
# create mapping from coordinates to data qubit labels
coord_to_label: dict[tuple[int, ...], str] = {}
for q, coord in layout.data_coords.items():
_coord = tuple((np.array(coord) * 1e6).astype(int))
coord_to_label[_coord] = q
# extract generalized labels by going around in "outer" rings
max_rings = (layout.distance_x + 1) // 2
directions = (dir_h, dir_v, -dir_h, -dir_v)
glabels: dict[str, tuple[int, int]] = {}
for r in range(max_rings):
curr_coord = top_left_coord + r * (dir_h + dir_v)
_curr_coord = tuple((np.array(curr_coord) * 1e6).astype(int))
curr_label = coord_to_label[_curr_coord]
glabels[curr_label] = (max_rings - 1 - r, 0)
# the most inner ring of odd-distance surface codes contains only
# a single qubit, not a multiple of 4, thus must be treated differently.
if (r == max_rings - 1) and (layout.distance_x % 2 == 1):
continue
for curr_index in range(1, 4 * (layout.distance_x - 1 - 2 * r)):
dir = directions[(curr_index - 1) // (layout.distance_x - 1 - 2 * r)]
curr_coord += dir
_curr_coord = tuple((np.array(curr_coord) * 1e6).astype(int))
curr_label = coord_to_label[_curr_coord]
glabels[curr_label] = (max_rings - 1 - r, curr_index)
# store generalized labels
for data_qubit in layout.data_qubits:
glabel = glabels[data_qubit]
if glabel != (0, 0):
layout.set_param(gate_label, data_qubit, {"label": glabels[data_qubit]})
# in an even distance rotated surface code,
# the CNOTs and resets depend wether the weight-2 stabilizers are Z-type or X-type.
z_anc = layout.get_neighbors([data_qubit], stab_type="z_type")[0]
reversed = len(layout.get_neighbors([z_anc])) != 2
reversed = reversed if layout.distance % 2 == 0 else False
layout.set_param(
gate_label, data_qubit, {"label": glabels[data_qubit], "reversed": reversed}
)
return