Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.DS_Store
workspace.ipynb
notebooks/logic_ops.ipynb
data/*

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

**Graph QEC** is a Python package under development that provides tools for implementing **Quantum Error Correction Codes (QECC)** by constructing their **Tanner Graphs**, automatically compiling them into **[Stim](https://github.com/quantumlib/Stim)** circuits, and computing error correction thresholds. This package allows researchers and developers to explore quantum error correction techniques, simulate quantum codes under gate error models, and analyze their performance through error thresholds.

Graph-QEC is still being prototyped and developed. It is not stable nor polished.

## Why Graph QEC?

- **Tanner Graph Representation**: Visualize and analyze quantum error correction codes through Tanner graphs, a graphical representation that simplifies the understanding of code structure and error syndromes.
Expand Down
46 changes: 35 additions & 11 deletions examples/rot_vs_unrot_surface_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@


def main(
configs: dict[str, int | float],
configs_rot: dict[str, int | float],
configs_unrot: dict[str, int | float],
errors: np.ndarray,
max_shots: int,
max_errors: int,
logic_check: str,
save_path_rot: str = None,
save_path_unrot: str = None,
existing_files_rot: list[str] = [],
existing_files_unrot: list[str] = [],
) -> None:
r"""
Compare the performance of Rotated and Unrotated Surface Codes.
Expand All @@ -41,24 +46,32 @@ def main(

# Get stats for the Rotated Surface Code with Z logic
rot_th_z = ThresholdLAB(
configurations=configs,
configurations=configs_rot,
code=RotatedSurfaceCode,
error_rates=errors,
decoder="pymatching",
)
rot_th_z.collect_stats(
max_shots=max_shots, max_errors=max_errors, logic_check=logic_check
max_shots=max_shots,
max_errors=max_errors,
logic_check=logic_check,
save_resume_filepath=save_path_rot,
existing_data_filepaths=existing_files_rot,
)

# Get stats for the Unrotated Surface code with Z logic
unrot_th_z = ThresholdLAB(
configurations=configs,
configurations=configs_unrot,
code=UnrotatedSurfaceCode,
error_rates=errors,
decoder="pymatching",
)
unrot_th_z.collect_stats(
max_shots=max_shots, max_errors=max_errors, logic_check=logic_check
max_shots=max_shots,
max_errors=max_errors,
logic_check=logic_check,
save_resume_filepath=save_path_unrot,
existing_data_filepaths=existing_files_unrot,
)

# Find the range of values for the fit
Expand Down Expand Up @@ -173,7 +186,7 @@ def main(
for m, label in zip(["^", "o"], ["Unrotated", "Rotated"])
]

height_per_entry = 0.055
height_per_entry = 0.2
extra_for_title = 0.1
n_lines = len(marker_handles)
y_marker_legend = 1.0 - (n_lines * height_per_entry + extra_for_title)
Expand All @@ -190,7 +203,7 @@ def main(
ax.set_xlim(min_n - 2, max_n + 2)
ax.set_title("")
ax.set_xlabel(r"$\sqrt{\mathrm{Number\ of\ Physical\ Qubits}}$")
ax.set_ylabel("Logical Error Rate per Round")
ax.set_ylabel("Logical Error Rate per d rounds")
ax.grid(which="major", zorder=0)
ax.grid(which="minor", zorder=0)
plt.tight_layout()
Expand Down Expand Up @@ -219,10 +232,11 @@ def fit(
n = np.sqrt(stats.json_metadata["n"])
per_shot = stats.errors / stats.shots
per_round = sinter.shot_error_rate_to_piece_error_rate(
per_shot, pieces=stats.json_metadata["r"]
per_shot, pieces=stats.json_metadata["r"] * stats.json_metadata["d"]
)
xs.append(n)
ys.append(per_round)
# print(f"x={xs[-1]} y={ys[-1]} for {stats.json_metadata}")
log_ys.append(np.log(per_round))
fit = scipy.stats.linregress(xs, log_ys)
return fit, xs, ys
Expand All @@ -233,12 +247,22 @@ def fit(
max_shots = 1_000_000
max_errors = 1000
logic_check = "Z"
configs = [{"distance": d} for d in [9, 11, 13, 17, 20, 23]]
errors = np.linspace(0.001, 0.01, 10)
# configs = [{"distance": d} for d in [9, 11, 13, 17, 20, 23]]
# errors = np.linspace(0.001, 0.01, 10)
configs_rot = [{"distance": d} for d in [7, 11, 13, 17]]
configs_unrot = [{"distance": d} for d in [7, 11, 13, 17]]
errors = np.linspace(0.001, 0.008, 5)
save_path_rot = "rot_vs_unrot_surface_code_rot.csv"
save_path_unrot = "rot_vs_unrot_surface_code_unrot.csv"
main(
configs=configs,
configs_rot=configs_rot,
configs_unrot=configs_unrot,
errors=errors,
max_shots=max_shots,
max_errors=max_errors,
logic_check=logic_check,
save_path_rot=save_path_rot,
save_path_unrot=save_path_unrot,
existing_files_rot=[],
existing_files_unrot=[],
)
3 changes: 3 additions & 0 deletions graphqec/codes/rotated_surface_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ def __init__(
Initialize the Rotated Surface Code instance.
"""

if not distance % 2:
raise ValueError("Distance must be odd.")

self._distance = distance
self._num_data_qubits = self.distance**2
self._num_logical_qubits = 1
Expand Down
76 changes: 37 additions & 39 deletions graphqec/lab/threshold/threshold_lab.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@

import sinter
import matplotlib.pyplot as plt
from stimbposd import SinterDecoder_BPOSD
from beliefmatching import BeliefMatchingSinterDecoder
from stimbposd import sinter_decoders as bposd_sinter_decoder
from mwpf import SinterMWPFDecoder
from beliefmatching import BeliefMatchingSinterDecoder

from graphqec.codes.base_code import BaseCode

__all__ = ["ThresholdLAB"]

__available_decoders__ = {
"pymatching": None,
"fusion_blossom": None,
"bposd": SinterDecoder_BPOSD,
"mwpf": SinterMWPFDecoder,
"beliefmatching": BeliefMatchingSinterDecoder,
}
__available_decoders__ = [
"pymatching",
"fusion_blossom",
"bposd",
"mwpf",
"beliefmatching",
]


class ThresholdLAB:
Expand Down Expand Up @@ -70,7 +70,7 @@ def __init__(
self._code = code
self._error_rates = error_rates

if decoder in __available_decoders__.keys():
if decoder in __available_decoders__:
self._decoder = decoder
else:
ValueError("This decoder is not available.")
Expand Down Expand Up @@ -146,7 +146,7 @@ def generate_sinter_tasks(self, logic_check: str = "Z") -> sinter.TaskGenerator:
"n": int(code.num_physical_qubits),
"k": int(code.num_logical_qubits),
"d": int(code.distance),
"r": num_rounds,
"r": int(num_rounds),
}
yield sinter.Task(circuit=code.memory_circuit, json_metadata=metadata)

Expand All @@ -155,8 +155,9 @@ def collect_stats(
num_workers: int | None = None,
max_shots: int = 10**4,
max_errors: int = 1000,
decoder_params: dict[str, any] | None = None,
logic_check: str = "Z",
save_resume_filepath: str | None = None,
existing_data_filepaths: list[str] = [],
) -> None:
r"""
Collect sampling statistics over ranges of distance and errors.
Expand All @@ -165,36 +166,33 @@ def collect_stats(
:param max_shots: Maximum number of shots.
:param max_errors: Maximum tolerated errors.
:param decoder_params: The optional decoder parameters.
:param logic_check: The logic check type.
:param save_resume_filepath: The path to the file where the statistics will be saved.
:param existing_data_filepaths: The paths to existing data files to resume from.
"""

if num_workers is None:
num_workers = multiprocessing.cpu_count() - 1

if __available_decoders__[self.decoder] is not None:

# if decoder_params is None:
# custom_decoder = {self.decoder: __available_decoders__[self.decoder]()}
# else:
# custom_decoder = {
# self.decoder: __available_decoders__[self.decoder](**decoder_params)
# }

self._samples = sinter.collect(
num_workers=num_workers,
max_shots=max_shots,
max_errors=max_errors,
tasks=self.generate_sinter_tasks(logic_check=logic_check),
decoders=[self.decoder],
custom_decoders=decoder_params,
)
else:
self._samples = sinter.collect(
num_workers=num_workers,
max_shots=max_shots,
max_errors=max_errors,
tasks=self.generate_sinter_tasks(logic_check=logic_check),
decoders=[self.decoder],
)
if self.decoder in ["pymatching", "fusion_blossom"]:
decoder_params = None
elif self.decoder == "bposd":
decoder_params = bposd_sinter_decoder()
elif self.decoder == "mwpf":
decoder_params = {"mwpf": SinterMWPFDecoder(cluster_node_limit=50)}
elif self.decoder == "beliefmatching":
decoder_params = {"beliefmatching": BeliefMatchingSinterDecoder()}

self._samples = sinter.collect(
num_workers=num_workers,
max_shots=max_shots,
max_errors=max_errors,
tasks=self.generate_sinter_tasks(logic_check=logic_check),
decoders=[self.decoder],
custom_decoders=decoder_params,
save_resume_filepath=save_resume_filepath,
existing_data_filepaths=existing_data_filepaths,
)

def plot_stats(
self,
Expand Down Expand Up @@ -263,5 +261,5 @@ def check_is_family(samples: list[sinter.TaskStats]) -> bool:
r"""
Check if the given samples belong to the same family.
"""
first_name = samples[0].json_metadata["name"]
return any(sample.json_metadata["name"] != first_name for sample in samples)
lst = [sample.json_metadata["name"] for sample in samples]
return all(x == lst[0] for x in lst)