from collections.abc import Callable
import numpy as np
import numpy.typing as npt
from ..layouts.layout import Layout
from .util import set_idle, set_trans_cnot, set_x, set_z
__all__ = [
"set_x",
"set_z",
"set_idle",
"set_fold_trans_s",
"set_fold_trans_h",
"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 unrotated surface code.
This implementation assumes that the qubits are placed in a square 2D grid,
and the separation between qubits is larger than ``1e-5`` units.
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.1088/1367-2630/17/8/083026
The circuit is shown in https://arxiv.org/pdf/2406.17653
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 != "unrotated_surface_code":
raise ValueError(
"This function is for unrotated surface codes, "
f"but a layout for the code {layout.code} was given."
)
if layout.distance_z != layout.distance_x:
raise ValueError("The transversal S gate requires d_z = d_x.")
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 reflection function
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_diag = layout.get_neighbors(anc_qubit_x, direction=dir_z)[0]
sym_vector = np.array(layout.param("coords", data_qubit_diag)) - np.array(
layout.param("coords", data_qubit)
)
point = np.array(layout.param("coords", data_qubit))
fold_reflection = lambda x: reflection(x, point, sym_vector)
coords_to_label_dict: dict[tuple[float, float], str] = {}
for node, attr in layout.graph.nodes.items():
coords: npt.NDArray[np.float64] = attr["coords"]
coords = np.round(coords, decimals=5) # to avoid numerical issues
coords_to_label_dict[tuple(coords)] = node
# get the CZs from the data qubit positions
cz_gates = {}
data_qubit_coords = layout.get_coords(data_qubits)
for data_qubit, coords in zip(data_qubits, data_qubit_coords):
pair_coords = fold_reflection(coords)
pair_coords = np.round(pair_coords, decimals=5)
data_pair = coords_to_label_dict[tuple(pair_coords)]
cz_gates[data_qubit] = data_pair if data_pair != data_qubit else None
# get S gates from the data qubit positions
s_gates = {q: "I" for q in data_qubits}
for k in range(2 * layout.distance_z - 1):
coords = point + k * sym_vector
coords = np.round(coords, decimals=5)
data_qubit = coords_to_label_dict[tuple(coords)]
s_gates[data_qubit] = "S" if k % 2 == 0 else "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():
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])
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 propagation for s_dag is the same as 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 reflection(
x: npt.NDArray[np.float64],
point: npt.NDArray[np.float64],
line_vector: npt.NDArray[np.float64],
) -> npt.NDArray[np.float64]:
"""Performs a reflection to ``x`` given the vector and point that define
the reflection line.
"""
x = np.array(x)
theta: np.float64 = -np.arctan(line_vector[1] / line_vector[0])
rot_matrix = np.array(
[[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]
)
x_rot = rot_matrix @ x
point = rot_matrix @ point
x_reflected_rot = np.array([x_rot[0], 2 * point[1] - x_rot[1]])
theta = -theta
rot_matrix = np.array(
[[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]
)
x_reflected = rot_matrix @ x_reflected_rot
return x_reflected
[docs]
def set_fold_trans_h(layout: Layout, data_qubit: str) -> None:
"""Adds the required attributes (in place) for the layout to run the transversal H
gate for the unrotated surface code.
This implementation assumes that the qubits are placed in a square 2D grid,
and the separation between qubits is larger than ``1e-5`` units.
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 is shown in https://arxiv.org/pdf/2406.17653
The information about the logical transversal H gate is stored in the layout
as the parameter ``"trans-h_{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 != "unrotated_surface_code":
raise ValueError(
"This function is for unrotated surface codes, "
f"but a layout for the code {layout.code} was given."
)
if layout.distance_z != layout.distance_x:
raise ValueError("The transversal H gate requires d_z = d_x.")
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_h_{layout.logical_qubits[0]}"
# get the reflection function
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_diag = layout.get_neighbors(anc_qubit_x, direction=dir_z)[0]
sym_vector = np.array(layout.param("coords", data_qubit_diag)) - np.array(
layout.param("coords", data_qubit)
)
point = np.array(layout.param("coords", data_qubit))
fold_reflection: Callable[[npt.NDArray[np.float64]], npt.NDArray[np.float64]] = (
lambda x: reflection(x, point, sym_vector)
)
coords_to_label_dict: dict[tuple[float, ...], str] = {}
for node, attr in layout.graph.nodes.items():
coords: npt.NDArray[np.float64] = attr["coords"]
coords = np.round(coords, decimals=5) # to avoid numerical issues
coords_to_label_dict[tuple(coords)] = node
# get the SWAPs from the data qubit positions
swap_gates = {}
data_qubit_coords = layout.get_coords(data_qubits)
for data_qubit, coords in zip(data_qubits, data_qubit_coords):
pair_coords = fold_reflection(coords)
pair_coords = np.round(pair_coords, decimals=5)
data_pair = coords_to_label_dict[tuple(pair_coords)]
swap_gates[data_qubit] = data_pair if data_pair != data_qubit else None
# Store logical gate information to the data qubits
for qubit in data_qubits:
layout.set_param(gate_label, qubit, {"swap": swap_gates[qubit], "local": "H"})
# Compute the new stabilizer generators
anc_to_new_stab = {}
anc_qubit_coords = layout.get_coords(anc_qubits)
for anc_qubit, coords in zip(anc_qubits, anc_qubit_coords):
pair_coords = fold_reflection(coords)
pair_coords = np.round(pair_coords, decimals=5)
anc_pair = coords_to_label_dict[tuple(pair_coords)]
anc_to_new_stab[anc_qubit] = [anc_pair]
# Store new stabilizer generators to the ancilla qubits
# H^\dagger = H
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
[docs]
def set_encoding(layout: Layout) -> None:
"""
Adds the required attributes (in place) for the layout to run the encoding
circuits for the unrotated 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 2 and 9 from:
Higgott, Oscar. "Optimal local unitary encoding circuits for the surface code."
Quantum 5, 517 (2021).
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 != "unrotated_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)
# orient the surface code so that logical X is the horizontal direction
# and logical Z is the vertical direction.
# the unrot surface code is symmetrical along the fold so it does not
# matter which corner is picked to orient it.
top_left_coord = np.array(layout.get_coords([corners[0]])[0])
z_anc = layout.get_neighbors([corners[0]], stab_type="z_type")[0]
z_anc_coord = np.array(layout.get_coords([z_anc])[0])
x_anc = layout.get_neighbors([corners[0]], stab_type="x_type")[0]
x_anc_coord = np.array(layout.get_coords([x_anc])[0])
dir_x = z_anc_coord - top_left_coord
dir_y = x_anc_coord - top_left_coord
# maps from coordinates to qubit labels.
# ancilla qubits are also included for ease in the next double 'for' loop.
coords_to_label_dict: dict[tuple[float, float], str] = {}
for qubit, coords in layout.qubit_coords.items():
coords = np.round(coords, decimals=5) # to avoid numerical issues
coords_to_label_dict[tuple(coords)] = qubit
# get the generalized labels for each qubit.
# note that in the unrot surface code, data qubits are not placed in a square 2D grid.
glabels: dict[str, tuple[int, int]] = {}
for x in range(2 * layout.distance - 1):
for y in range(2 * layout.distance - 1):
coords = top_left_coord + x * dir_x + y * dir_y
coords = np.round(coords, decimals=5) # to avoid numerical issues
qubit = coords_to_label_dict[tuple(coords)]
# (0, 0) = data qubit in the middle of the surface code
glabels[qubit] = (x - layout.distance + 1, y - layout.distance + 1)
# store generalized labels
for data_qubit in layout.data_qubits:
layout.set_param(gate_label, data_qubit, {"label": glabels[data_qubit]})
return