From 0cb17ace207b18139b4ee999fe377a0a792d1014 Mon Sep 17 00:00:00 2001 From: Dennis Wayo <117969019+DennisWayo@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:18:56 +0500 Subject: [PATCH] Add paper 05 syndrome workflow code --- .../paper_05/01_build_syndrome_circuits.sh | 21 + .../paper_05/02_run_local_simulation.sh | 25 + .../paper_05/03_fetch_ibm_runtime_results.sh | 37 ++ .../paper_05/03_submit_ibm_runtime.sh | 38 ++ .../paper_runs/paper_05/04_ingest_results.sh | 35 ++ .../paper_05/05_decode_live_syndromes.sh | 25 + .../paper_05/06_analyze_and_plot.sh | 26 ++ .../11_build_qldpc_syndrome_circuits.sh | 20 + .../paper_05/12_run_qldpc_local_simulation.sh | 24 + .../13_fetch_qldpc_ibm_runtime_results.sh | 37 ++ .../paper_05/13_submit_qldpc_ibm_runtime.sh | 37 ++ .../paper_05/14_ingest_qldpc_results.sh | 35 ++ .../paper_05/15_decode_qldpc_syndromes.sh | 21 + .../paper_runs/paper_05/16_analyze_qldpc.sh | 27 ++ .../21_build_surface_syndrome_circuits.sh | 21 + .../22_run_surface_local_simulation.sh | 25 + .../23_fetch_surface_ibm_runtime_results.sh | 37 ++ .../paper_05/23_submit_surface_ibm_runtime.sh | 38 ++ .../paper_05/24_ingest_surface_results.sh | 35 ++ .../paper_05/25_decode_surface_syndromes.sh | 21 + .../paper_runs/paper_05/26_analyze_surface.sh | 27 ++ .../paper_05/31_build_gkp_digitized_model.sh | 23 + .../paper_05/32_run_gkp_digitized_sampler.sh | 33 ++ .../paper_05/33_ingest_gkp_results.sh | 25 + .../paper_05/34_decode_gkp_syndromes.sh | 21 + .../paper_runs/paper_05/35_analyze_gkp.sh | 27 ++ .../paper_05/36_render_gkp_figures.sh | 21 + .../37_render_supplemental_figures.sh | 22 + examples/paper_runs/paper_05/Makefile | 67 +++ examples/paper_runs/paper_05/README.md | 182 ++++++++ examples/paper_runs/paper_05/common.sh | 33 ++ .../paper_05/ibm_credentials.example.json | 5 + examples/paper_runs/paper_05/run_all.sh | 50 ++ .../paper_05/scripts/analyze_gkp_digitized.py | 305 ++++++++++++ .../paper_05/scripts/analyze_live_css_ldpc.py | 355 ++++++++++++++ .../scripts/analyze_live_repetition.py | 338 ++++++++++++++ .../paper_05/scripts/analyze_live_surface.py | 328 +++++++++++++ .../scripts/build_css_ldpc_syndrome.py | 101 ++++ .../scripts/build_gkp_digitized_model.py | 77 +++ .../scripts/build_repetition_syndrome.py | 105 +++++ .../scripts/build_surface_syndrome.py | 103 ++++ .../paper_05/scripts/css_ldpc_syndrome.py | 181 ++++++++ .../scripts/decode_css_ldpc_syndromes.py | 157 +++++++ .../scripts/decode_gkp_digitized_syndromes.py | 172 +++++++ .../scripts/decode_repetition_syndromes.py | 166 +++++++ .../scripts/decode_surface_syndromes.py | 162 +++++++ .../scripts/fetch_ibm_css_ldpc_results.py | 84 ++++ .../scripts/fetch_ibm_repetition_results.py | 83 ++++ .../scripts/fetch_ibm_surface_results.py | 85 ++++ .../scripts/gkp_digitized_syndrome.py | 161 +++++++ .../scripts/ingest_css_ldpc_results.py | 177 +++++++ .../scripts/ingest_gkp_digitized_results.py | 197 ++++++++ .../scripts/ingest_repetition_results.py | 182 ++++++++ .../scripts/ingest_surface_results.py | 180 +++++++ .../scripts/paper05_decoder_policies.py | 420 +++++++++++++++++ .../paper_05/scripts/paper05_plot_style.py | 180 +++++++ .../scripts/render_gkp_digitized_figures.py | 438 ++++++++++++++++++ .../scripts/render_supplemental_figures.py | 402 ++++++++++++++++ .../paper_05/scripts/repetition_syndrome.py | 165 +++++++ .../scripts/run_local_css_ldpc_sampler.py | 116 +++++ .../run_local_gkp_digitized_sampler.py | 278 +++++++++++ .../scripts/run_local_repetition_sampler.py | 117 +++++ .../scripts/run_local_surface_sampler.py | 118 +++++ .../scripts/submit_ibm_css_ldpc_sampler.py | 148 ++++++ .../scripts/submit_ibm_repetition_sampler.py | 266 +++++++++++ .../scripts/submit_ibm_surface_sampler.py | 155 +++++++ .../paper_05/scripts/surface_syndrome.py | 294 ++++++++++++ 67 files changed, 7917 insertions(+) create mode 100755 examples/paper_runs/paper_05/01_build_syndrome_circuits.sh create mode 100755 examples/paper_runs/paper_05/02_run_local_simulation.sh create mode 100755 examples/paper_runs/paper_05/03_fetch_ibm_runtime_results.sh create mode 100755 examples/paper_runs/paper_05/03_submit_ibm_runtime.sh create mode 100755 examples/paper_runs/paper_05/04_ingest_results.sh create mode 100755 examples/paper_runs/paper_05/05_decode_live_syndromes.sh create mode 100755 examples/paper_runs/paper_05/06_analyze_and_plot.sh create mode 100755 examples/paper_runs/paper_05/11_build_qldpc_syndrome_circuits.sh create mode 100755 examples/paper_runs/paper_05/12_run_qldpc_local_simulation.sh create mode 100755 examples/paper_runs/paper_05/13_fetch_qldpc_ibm_runtime_results.sh create mode 100755 examples/paper_runs/paper_05/13_submit_qldpc_ibm_runtime.sh create mode 100755 examples/paper_runs/paper_05/14_ingest_qldpc_results.sh create mode 100755 examples/paper_runs/paper_05/15_decode_qldpc_syndromes.sh create mode 100755 examples/paper_runs/paper_05/16_analyze_qldpc.sh create mode 100755 examples/paper_runs/paper_05/21_build_surface_syndrome_circuits.sh create mode 100755 examples/paper_runs/paper_05/22_run_surface_local_simulation.sh create mode 100755 examples/paper_runs/paper_05/23_fetch_surface_ibm_runtime_results.sh create mode 100755 examples/paper_runs/paper_05/23_submit_surface_ibm_runtime.sh create mode 100755 examples/paper_runs/paper_05/24_ingest_surface_results.sh create mode 100755 examples/paper_runs/paper_05/25_decode_surface_syndromes.sh create mode 100755 examples/paper_runs/paper_05/26_analyze_surface.sh create mode 100755 examples/paper_runs/paper_05/31_build_gkp_digitized_model.sh create mode 100755 examples/paper_runs/paper_05/32_run_gkp_digitized_sampler.sh create mode 100755 examples/paper_runs/paper_05/33_ingest_gkp_results.sh create mode 100755 examples/paper_runs/paper_05/34_decode_gkp_syndromes.sh create mode 100755 examples/paper_runs/paper_05/35_analyze_gkp.sh create mode 100755 examples/paper_runs/paper_05/36_render_gkp_figures.sh create mode 100755 examples/paper_runs/paper_05/37_render_supplemental_figures.sh create mode 100644 examples/paper_runs/paper_05/Makefile create mode 100644 examples/paper_runs/paper_05/README.md create mode 100755 examples/paper_runs/paper_05/common.sh create mode 100644 examples/paper_runs/paper_05/ibm_credentials.example.json create mode 100755 examples/paper_runs/paper_05/run_all.sh create mode 100644 examples/paper_runs/paper_05/scripts/analyze_gkp_digitized.py create mode 100644 examples/paper_runs/paper_05/scripts/analyze_live_css_ldpc.py create mode 100755 examples/paper_runs/paper_05/scripts/analyze_live_repetition.py create mode 100644 examples/paper_runs/paper_05/scripts/analyze_live_surface.py create mode 100644 examples/paper_runs/paper_05/scripts/build_css_ldpc_syndrome.py create mode 100644 examples/paper_runs/paper_05/scripts/build_gkp_digitized_model.py create mode 100755 examples/paper_runs/paper_05/scripts/build_repetition_syndrome.py create mode 100644 examples/paper_runs/paper_05/scripts/build_surface_syndrome.py create mode 100644 examples/paper_runs/paper_05/scripts/css_ldpc_syndrome.py create mode 100644 examples/paper_runs/paper_05/scripts/decode_css_ldpc_syndromes.py create mode 100644 examples/paper_runs/paper_05/scripts/decode_gkp_digitized_syndromes.py create mode 100755 examples/paper_runs/paper_05/scripts/decode_repetition_syndromes.py create mode 100644 examples/paper_runs/paper_05/scripts/decode_surface_syndromes.py create mode 100644 examples/paper_runs/paper_05/scripts/fetch_ibm_css_ldpc_results.py create mode 100755 examples/paper_runs/paper_05/scripts/fetch_ibm_repetition_results.py create mode 100644 examples/paper_runs/paper_05/scripts/fetch_ibm_surface_results.py create mode 100644 examples/paper_runs/paper_05/scripts/gkp_digitized_syndrome.py create mode 100644 examples/paper_runs/paper_05/scripts/ingest_css_ldpc_results.py create mode 100644 examples/paper_runs/paper_05/scripts/ingest_gkp_digitized_results.py create mode 100755 examples/paper_runs/paper_05/scripts/ingest_repetition_results.py create mode 100644 examples/paper_runs/paper_05/scripts/ingest_surface_results.py create mode 100644 examples/paper_runs/paper_05/scripts/paper05_decoder_policies.py create mode 100644 examples/paper_runs/paper_05/scripts/paper05_plot_style.py create mode 100644 examples/paper_runs/paper_05/scripts/render_gkp_digitized_figures.py create mode 100644 examples/paper_runs/paper_05/scripts/render_supplemental_figures.py create mode 100755 examples/paper_runs/paper_05/scripts/repetition_syndrome.py create mode 100644 examples/paper_runs/paper_05/scripts/run_local_css_ldpc_sampler.py create mode 100644 examples/paper_runs/paper_05/scripts/run_local_gkp_digitized_sampler.py create mode 100755 examples/paper_runs/paper_05/scripts/run_local_repetition_sampler.py create mode 100644 examples/paper_runs/paper_05/scripts/run_local_surface_sampler.py create mode 100644 examples/paper_runs/paper_05/scripts/submit_ibm_css_ldpc_sampler.py create mode 100755 examples/paper_runs/paper_05/scripts/submit_ibm_repetition_sampler.py create mode 100644 examples/paper_runs/paper_05/scripts/submit_ibm_surface_sampler.py create mode 100644 examples/paper_runs/paper_05/scripts/surface_syndrome.py diff --git a/examples/paper_runs/paper_05/01_build_syndrome_circuits.sh b/examples/paper_runs/paper_05/01_build_syndrome_circuits.sh new file mode 100755 index 0000000..3b478ef --- /dev/null +++ b/examples/paper_runs/paper_05/01_build_syndrome_circuits.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "01_build_syndrome_circuits")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/build_repetition_syndrome.py" \ + --out-dir "${OUT_DIR}" \ + --n-data "${LIDMAS_P5_N_DATA:-5}" \ + --targets "${LIDMAS_P5_TARGETS:-all}" + +echo "paper_05 step 01 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/02_run_local_simulation.sh b/examples/paper_runs/paper_05/02_run_local_simulation.sh new file mode 100755 index 0000000..f1d01a5 --- /dev/null +++ b/examples/paper_runs/paper_05/02_run_local_simulation.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "02_local_simulation")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/run_local_repetition_sampler.py" \ + --out-dir "${OUT_DIR}" \ + --n-data "${LIDMAS_P5_N_DATA:-5}" \ + --targets "${LIDMAS_P5_TARGETS:-all}" \ + --shots "${LIDMAS_P5_SHOTS:-256}" \ + --measurement-error-rate "${LIDMAS_P5_LOCAL_MEAS_ERROR:-0.02}" \ + --background-data-error-rate "${LIDMAS_P5_LOCAL_DATA_ERROR:-0.0}" \ + --seed "${LIDMAS_P5_SEED:-20260705}" + +echo "paper_05 step 02 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/03_fetch_ibm_runtime_results.sh b/examples/paper_runs/paper_05/03_fetch_ibm_runtime_results.sh new file mode 100755 index 0000000..779fb97 --- /dev/null +++ b/examples/paper_runs/paper_05/03_fetch_ibm_runtime_results.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "03_ibm_runtime")" +SUBMISSION_JSON="${OUT_DIR}/ibm_runtime_submission.json" +RESULT_JSON="${OUT_DIR}/ibm_repetition_results.json" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi +if [ ! -f "${SUBMISSION_JSON}" ]; then + echo "Error: ${SUBMISSION_JSON} not found. Submit the IBM job first." >&2 + exit 1 +fi + +fetch_args=() +if [ "${LIDMAS_P5_IBM_STATUS_ONLY:-0}" = "1" ]; then + fetch_args+=(--status-only) +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/fetch_ibm_repetition_results.py" \ + --submission-json "${SUBMISSION_JSON}" \ + --out-json "${RESULT_JSON}" \ + --result-timeout "${LIDMAS_P5_IBM_RESULT_TIMEOUT:-300}" \ + ${fetch_args[@]+"${fetch_args[@]}"} + +if [ "${LIDMAS_P5_IBM_STATUS_ONLY:-0}" = "1" ]; then + echo "paper_05 IBM status check complete." +else + echo "paper_05 IBM result fetch complete: ${RESULT_JSON}" +fi diff --git a/examples/paper_runs/paper_05/03_submit_ibm_runtime.sh b/examples/paper_runs/paper_05/03_submit_ibm_runtime.sh new file mode 100755 index 0000000..1e1365a --- /dev/null +++ b/examples/paper_runs/paper_05/03_submit_ibm_runtime.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "03_ibm_runtime")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +backend_args=() +if [ -n "${LIDMAS_P5_IBM_BACKEND:-}" ]; then + backend_args+=(--backend "${LIDMAS_P5_IBM_BACKEND}") +fi +if [ -n "${IBM_QUANTUM_INSTANCE:-}" ]; then + backend_args+=(--instance "${IBM_QUANTUM_INSTANCE}") +fi +if [ "${LIDMAS_P5_IBM_WAIT:-1}" = "0" ]; then + backend_args+=(--no-wait) +fi +if [ -n "${LIDMAS_P5_IBM_RESULT_TIMEOUT:-}" ]; then + backend_args+=(--result-timeout "${LIDMAS_P5_IBM_RESULT_TIMEOUT}") +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/submit_ibm_repetition_sampler.py" \ + --out-dir "${OUT_DIR}" \ + --n-data "${LIDMAS_P5_N_DATA:-5}" \ + --targets "${LIDMAS_P5_TARGETS:-all}" \ + --shots "${LIDMAS_P5_IBM_SHOTS:-${LIDMAS_P5_SHOTS:-256}}" \ + --optimization-level "${LIDMAS_P5_OPTIMIZATION_LEVEL:-1}" \ + ${backend_args[@]+"${backend_args[@]}"} + +echo "paper_05 step 03 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/04_ingest_results.sh b/examples/paper_runs/paper_05/04_ingest_results.sh new file mode 100755 index 0000000..7368063 --- /dev/null +++ b/examples/paper_runs/paper_05/04_ingest_results.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "04_ingest_results")" +LOCAL_JSON="$(paper_results_dir "02_local_simulation")/local_repetition_results.json" +IBM_JSON="$(paper_results_dir "03_ibm_runtime")/ibm_repetition_results.json" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +raw_args=() +if [ -f "${LOCAL_JSON}" ]; then + raw_args+=(--raw-json "${LOCAL_JSON}") +fi +if [ -f "${IBM_JSON}" ]; then + raw_args+=(--raw-json "${IBM_JSON}") +fi + +if [ "${#raw_args[@]}" -eq 0 ]; then + echo "Error: no raw paper_05 result JSON files found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/ingest_repetition_results.py" \ + --out-dir "${OUT_DIR}" \ + "${raw_args[@]}" + +echo "paper_05 step 04 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/05_decode_live_syndromes.sh b/examples/paper_runs/paper_05/05_decode_live_syndromes.sh new file mode 100755 index 0000000..cbdb76e --- /dev/null +++ b/examples/paper_runs/paper_05/05_decode_live_syndromes.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +IN_DIR="$(paper_results_dir "04_ingest_results")" +OUT_DIR="$(paper_results_dir "05_decode_live_syndromes")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +if ! ls "${IN_DIR}"/decoder_requests_*.ndjson >/dev/null 2>&1; then + "${SCRIPT_DIR}/04_ingest_results.sh" +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/decode_repetition_syndromes.py" \ + --in-dir "${IN_DIR}" \ + --out-dir "${OUT_DIR}" + +echo "paper_05 step 05 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/06_analyze_and_plot.sh b/examples/paper_runs/paper_05/06_analyze_and_plot.sh new file mode 100755 index 0000000..114fe18 --- /dev/null +++ b/examples/paper_runs/paper_05/06_analyze_and_plot.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +IN_DIR="$(paper_results_dir "05_decode_live_syndromes")" +OUT_DIR="$(paper_results_dir "06_analysis")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +if [ ! -f "${IN_DIR}/decoded_shots.csv" ]; then + "${SCRIPT_DIR}/05_decode_live_syndromes.sh" +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/analyze_live_repetition.py" \ + --decoded-csv "${IN_DIR}/decoded_shots.csv" \ + --out-dir "${OUT_DIR}" \ + --manuscript-dir "${OUT_DIR}/manuscript_figures" + +echo "paper_05 step 06 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/11_build_qldpc_syndrome_circuits.sh b/examples/paper_runs/paper_05/11_build_qldpc_syndrome_circuits.sh new file mode 100755 index 0000000..63bee11 --- /dev/null +++ b/examples/paper_runs/paper_05/11_build_qldpc_syndrome_circuits.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "11_build_qldpc_syndrome_circuits")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/build_css_ldpc_syndrome.py" \ + --out-dir "${OUT_DIR}" \ + --targets "${LIDMAS_P5_QLDPC_TARGETS:-all}" + +echo "paper_05 qLDPC step 11 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/12_run_qldpc_local_simulation.sh b/examples/paper_runs/paper_05/12_run_qldpc_local_simulation.sh new file mode 100755 index 0000000..5da87ab --- /dev/null +++ b/examples/paper_runs/paper_05/12_run_qldpc_local_simulation.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "12_qldpc_local_simulation")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/run_local_css_ldpc_sampler.py" \ + --out-dir "${OUT_DIR}" \ + --targets "${LIDMAS_P5_QLDPC_TARGETS:-all}" \ + --shots "${LIDMAS_P5_QLDPC_SHOTS:-${LIDMAS_P5_SHOTS:-256}}" \ + --measurement-error-rate "${LIDMAS_P5_QLDPC_LOCAL_MEAS_ERROR:-0.02}" \ + --background-data-error-rate "${LIDMAS_P5_QLDPC_LOCAL_DATA_ERROR:-0.0}" \ + --seed "${LIDMAS_P5_QLDPC_SEED:-20260705}" + +echo "paper_05 qLDPC step 12 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/13_fetch_qldpc_ibm_runtime_results.sh b/examples/paper_runs/paper_05/13_fetch_qldpc_ibm_runtime_results.sh new file mode 100755 index 0000000..8e3ec6c --- /dev/null +++ b/examples/paper_runs/paper_05/13_fetch_qldpc_ibm_runtime_results.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "13_qldpc_ibm_runtime")" +SUBMISSION_JSON="${OUT_DIR}/ibm_css_ldpc_submission.json" +RESULT_JSON="${OUT_DIR}/ibm_css_ldpc_results.json" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi +if [ ! -f "${SUBMISSION_JSON}" ]; then + echo "Error: ${SUBMISSION_JSON} not found. Submit the IBM qLDPC job first." >&2 + exit 1 +fi + +fetch_args=() +if [ "${LIDMAS_P5_QLDPC_IBM_STATUS_ONLY:-${LIDMAS_P5_IBM_STATUS_ONLY:-0}}" = "1" ]; then + fetch_args+=(--status-only) +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/fetch_ibm_css_ldpc_results.py" \ + --submission-json "${SUBMISSION_JSON}" \ + --out-json "${RESULT_JSON}" \ + --result-timeout "${LIDMAS_P5_QLDPC_IBM_RESULT_TIMEOUT:-${LIDMAS_P5_IBM_RESULT_TIMEOUT:-300}}" \ + ${fetch_args[@]+"${fetch_args[@]}"} + +if [ "${LIDMAS_P5_QLDPC_IBM_STATUS_ONLY:-${LIDMAS_P5_IBM_STATUS_ONLY:-0}}" = "1" ]; then + echo "paper_05 qLDPC IBM status check complete." +else + echo "paper_05 qLDPC IBM result fetch complete: ${RESULT_JSON}" +fi diff --git a/examples/paper_runs/paper_05/13_submit_qldpc_ibm_runtime.sh b/examples/paper_runs/paper_05/13_submit_qldpc_ibm_runtime.sh new file mode 100755 index 0000000..d1aff81 --- /dev/null +++ b/examples/paper_runs/paper_05/13_submit_qldpc_ibm_runtime.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "13_qldpc_ibm_runtime")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +backend_args=() +if [ -n "${LIDMAS_P5_QLDPC_IBM_BACKEND:-${LIDMAS_P5_IBM_BACKEND:-}}" ]; then + backend_args+=(--backend "${LIDMAS_P5_QLDPC_IBM_BACKEND:-${LIDMAS_P5_IBM_BACKEND:-}}") +fi +if [ -n "${IBM_QUANTUM_INSTANCE:-}" ]; then + backend_args+=(--instance "${IBM_QUANTUM_INSTANCE}") +fi +if [ "${LIDMAS_P5_QLDPC_IBM_WAIT:-${LIDMAS_P5_IBM_WAIT:-1}}" = "0" ]; then + backend_args+=(--no-wait) +fi +if [ -n "${LIDMAS_P5_QLDPC_IBM_RESULT_TIMEOUT:-${LIDMAS_P5_IBM_RESULT_TIMEOUT:-}}" ]; then + backend_args+=(--result-timeout "${LIDMAS_P5_QLDPC_IBM_RESULT_TIMEOUT:-${LIDMAS_P5_IBM_RESULT_TIMEOUT:-}}") +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/submit_ibm_css_ldpc_sampler.py" \ + --out-dir "${OUT_DIR}" \ + --targets "${LIDMAS_P5_QLDPC_TARGETS:-all}" \ + --shots "${LIDMAS_P5_QLDPC_IBM_SHOTS:-${LIDMAS_P5_QLDPC_SHOTS:-${LIDMAS_P5_SHOTS:-256}}}" \ + --optimization-level "${LIDMAS_P5_QLDPC_OPTIMIZATION_LEVEL:-${LIDMAS_P5_OPTIMIZATION_LEVEL:-1}}" \ + ${backend_args[@]+"${backend_args[@]}"} + +echo "paper_05 qLDPC step 13 submit complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/14_ingest_qldpc_results.sh b/examples/paper_runs/paper_05/14_ingest_qldpc_results.sh new file mode 100755 index 0000000..5dcbaf0 --- /dev/null +++ b/examples/paper_runs/paper_05/14_ingest_qldpc_results.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "14_ingest_qldpc_results")" +LOCAL_JSON="$(paper_results_dir "12_qldpc_local_simulation")/local_css_ldpc_results.json" +IBM_JSON="$(paper_results_dir "13_qldpc_ibm_runtime")/ibm_css_ldpc_results.json" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +raw_args=() +if [ -f "${LOCAL_JSON}" ]; then + raw_args+=(--raw-json "${LOCAL_JSON}") +fi +if [ -f "${IBM_JSON}" ]; then + raw_args+=(--raw-json "${IBM_JSON}") +fi + +if [ "${#raw_args[@]}" -eq 0 ]; then + echo "Error: no raw paper_05 qLDPC result JSON files found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/ingest_css_ldpc_results.py" \ + --out-dir "${OUT_DIR}" \ + "${raw_args[@]}" + +echo "paper_05 qLDPC step 14 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/15_decode_qldpc_syndromes.sh b/examples/paper_runs/paper_05/15_decode_qldpc_syndromes.sh new file mode 100755 index 0000000..ec64dbd --- /dev/null +++ b/examples/paper_runs/paper_05/15_decode_qldpc_syndromes.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +IN_DIR="$(paper_results_dir "14_ingest_qldpc_results")" +OUT_DIR="$(paper_results_dir "15_decode_qldpc_syndromes")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/decode_css_ldpc_syndromes.py" \ + --in-dir "${IN_DIR}" \ + --out-dir "${OUT_DIR}" + +echo "paper_05 qLDPC step 15 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/16_analyze_qldpc.sh b/examples/paper_runs/paper_05/16_analyze_qldpc.sh new file mode 100755 index 0000000..b5c7bfa --- /dev/null +++ b/examples/paper_runs/paper_05/16_analyze_qldpc.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +IN_CSV="$(paper_results_dir "15_decode_qldpc_syndromes")/decoded_shots.csv" +OUT_DIR="$(paper_results_dir "16_qldpc_analysis")" +MANUSCRIPT_DIR="${OUT_DIR}/manuscript_figures" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi +if [ ! -f "${IN_CSV}" ]; then + echo "Error: ${IN_CSV} not found. Run qLDPC decode first." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/analyze_live_css_ldpc.py" \ + --decoded-csv "${IN_CSV}" \ + --out-dir "${OUT_DIR}" \ + --manuscript-dir "${MANUSCRIPT_DIR}" + +echo "paper_05 qLDPC step 16 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/21_build_surface_syndrome_circuits.sh b/examples/paper_runs/paper_05/21_build_surface_syndrome_circuits.sh new file mode 100755 index 0000000..136b866 --- /dev/null +++ b/examples/paper_runs/paper_05/21_build_surface_syndrome_circuits.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "21_build_surface_syndrome_circuits")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/build_surface_syndrome.py" \ + --out-dir "${OUT_DIR}" \ + --distance "${LIDMAS_P5_SURFACE_DISTANCE:-5}" \ + --targets "${LIDMAS_P5_SURFACE_TARGETS:-representative}" + +echo "paper_05 surface step 21 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/22_run_surface_local_simulation.sh b/examples/paper_runs/paper_05/22_run_surface_local_simulation.sh new file mode 100755 index 0000000..68f5b1e --- /dev/null +++ b/examples/paper_runs/paper_05/22_run_surface_local_simulation.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "22_surface_local_simulation")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/run_local_surface_sampler.py" \ + --out-dir "${OUT_DIR}" \ + --distance "${LIDMAS_P5_SURFACE_DISTANCE:-5}" \ + --targets "${LIDMAS_P5_SURFACE_TARGETS:-representative}" \ + --shots "${LIDMAS_P5_SURFACE_SHOTS:-${LIDMAS_P5_SHOTS:-256}}" \ + --measurement-error-rate "${LIDMAS_P5_SURFACE_LOCAL_MEAS_ERROR:-0.02}" \ + --background-data-error-rate "${LIDMAS_P5_SURFACE_LOCAL_DATA_ERROR:-0.0}" \ + --seed "${LIDMAS_P5_SURFACE_SEED:-20260705}" + +echo "paper_05 surface step 22 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/23_fetch_surface_ibm_runtime_results.sh b/examples/paper_runs/paper_05/23_fetch_surface_ibm_runtime_results.sh new file mode 100755 index 0000000..3e2e703 --- /dev/null +++ b/examples/paper_runs/paper_05/23_fetch_surface_ibm_runtime_results.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "23_surface_ibm_runtime")" +SUBMISSION_JSON="${OUT_DIR}/ibm_surface_submission.json" +RESULT_JSON="${OUT_DIR}/ibm_surface_results.json" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi +if [ ! -f "${SUBMISSION_JSON}" ]; then + echo "Error: ${SUBMISSION_JSON} not found. Submit the IBM surface-code job first." >&2 + exit 1 +fi + +fetch_args=() +if [ "${LIDMAS_P5_SURFACE_IBM_STATUS_ONLY:-${LIDMAS_P5_IBM_STATUS_ONLY:-0}}" = "1" ]; then + fetch_args+=(--status-only) +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/fetch_ibm_surface_results.py" \ + --submission-json "${SUBMISSION_JSON}" \ + --out-json "${RESULT_JSON}" \ + --result-timeout "${LIDMAS_P5_SURFACE_IBM_RESULT_TIMEOUT:-${LIDMAS_P5_IBM_RESULT_TIMEOUT:-300}}" \ + ${fetch_args[@]+"${fetch_args[@]}"} + +if [ "${LIDMAS_P5_SURFACE_IBM_STATUS_ONLY:-${LIDMAS_P5_IBM_STATUS_ONLY:-0}}" = "1" ]; then + echo "paper_05 surface IBM status check complete." +else + echo "paper_05 surface IBM result fetch complete: ${RESULT_JSON}" +fi diff --git a/examples/paper_runs/paper_05/23_submit_surface_ibm_runtime.sh b/examples/paper_runs/paper_05/23_submit_surface_ibm_runtime.sh new file mode 100755 index 0000000..640551c --- /dev/null +++ b/examples/paper_runs/paper_05/23_submit_surface_ibm_runtime.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "23_surface_ibm_runtime")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +backend_args=() +if [ -n "${LIDMAS_P5_SURFACE_IBM_BACKEND:-${LIDMAS_P5_IBM_BACKEND:-}}" ]; then + backend_args+=(--backend "${LIDMAS_P5_SURFACE_IBM_BACKEND:-${LIDMAS_P5_IBM_BACKEND:-}}") +fi +if [ -n "${IBM_QUANTUM_INSTANCE:-}" ]; then + backend_args+=(--instance "${IBM_QUANTUM_INSTANCE}") +fi +if [ "${LIDMAS_P5_SURFACE_IBM_WAIT:-${LIDMAS_P5_IBM_WAIT:-1}}" = "0" ]; then + backend_args+=(--no-wait) +fi +if [ -n "${LIDMAS_P5_SURFACE_IBM_RESULT_TIMEOUT:-${LIDMAS_P5_IBM_RESULT_TIMEOUT:-}}" ]; then + backend_args+=(--result-timeout "${LIDMAS_P5_SURFACE_IBM_RESULT_TIMEOUT:-${LIDMAS_P5_IBM_RESULT_TIMEOUT:-}}") +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/submit_ibm_surface_sampler.py" \ + --out-dir "${OUT_DIR}" \ + --distance "${LIDMAS_P5_SURFACE_DISTANCE:-5}" \ + --targets "${LIDMAS_P5_SURFACE_TARGETS:-representative}" \ + --shots "${LIDMAS_P5_SURFACE_IBM_SHOTS:-${LIDMAS_P5_SURFACE_SHOTS:-${LIDMAS_P5_SHOTS:-256}}}" \ + --optimization-level "${LIDMAS_P5_SURFACE_OPTIMIZATION_LEVEL:-${LIDMAS_P5_OPTIMIZATION_LEVEL:-1}}" \ + ${backend_args[@]+"${backend_args[@]}"} + +echo "paper_05 surface step 23 submit complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/24_ingest_surface_results.sh b/examples/paper_runs/paper_05/24_ingest_surface_results.sh new file mode 100755 index 0000000..97999a0 --- /dev/null +++ b/examples/paper_runs/paper_05/24_ingest_surface_results.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "24_ingest_surface_results")" +LOCAL_JSON="$(paper_results_dir "22_surface_local_simulation")/local_surface_results.json" +IBM_JSON="$(paper_results_dir "23_surface_ibm_runtime")/ibm_surface_results.json" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +raw_args=() +if [ -f "${LOCAL_JSON}" ]; then + raw_args+=(--raw-json "${LOCAL_JSON}") +fi +if [ -f "${IBM_JSON}" ]; then + raw_args+=(--raw-json "${IBM_JSON}") +fi + +if [ "${#raw_args[@]}" -eq 0 ]; then + echo "Error: no raw paper_05 surface result JSON files found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/ingest_surface_results.py" \ + --out-dir "${OUT_DIR}" \ + "${raw_args[@]}" + +echo "paper_05 surface step 24 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/25_decode_surface_syndromes.sh b/examples/paper_runs/paper_05/25_decode_surface_syndromes.sh new file mode 100755 index 0000000..ddf11c2 --- /dev/null +++ b/examples/paper_runs/paper_05/25_decode_surface_syndromes.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +IN_DIR="$(paper_results_dir "24_ingest_surface_results")" +OUT_DIR="$(paper_results_dir "25_decode_surface_syndromes")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/decode_surface_syndromes.py" \ + --in-dir "${IN_DIR}" \ + --out-dir "${OUT_DIR}" + +echo "paper_05 surface step 25 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/26_analyze_surface.sh b/examples/paper_runs/paper_05/26_analyze_surface.sh new file mode 100755 index 0000000..6b18b2d --- /dev/null +++ b/examples/paper_runs/paper_05/26_analyze_surface.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +IN_CSV="$(paper_results_dir "25_decode_surface_syndromes")/decoded_shots.csv" +OUT_DIR="$(paper_results_dir "26_surface_analysis")" +MANUSCRIPT_DIR="${OUT_DIR}/manuscript_figures" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi +if [ ! -f "${IN_CSV}" ]; then + echo "Error: ${IN_CSV} not found. Run surface decode first." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/analyze_live_surface.py" \ + --decoded-csv "${IN_CSV}" \ + --out-dir "${OUT_DIR}" \ + --manuscript-dir "${MANUSCRIPT_DIR}" + +echo "paper_05 surface step 26 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/31_build_gkp_digitized_model.sh b/examples/paper_runs/paper_05/31_build_gkp_digitized_model.sh new file mode 100755 index 0000000..2d07b07 --- /dev/null +++ b/examples/paper_runs/paper_05/31_build_gkp_digitized_model.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "31_build_gkp_digitized_model")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/build_gkp_digitized_model.py" \ + --out-dir "${OUT_DIR}" \ + --distance "${LIDMAS_P5_GKP_DISTANCE:-5}" \ + --targets "${LIDMAS_P5_GKP_TARGETS:-representative}" \ + --decision-width-scale "${LIDMAS_P5_GKP_DECISION_WIDTH_SCALE:-0.25}" \ + --injected-shift-scale "${LIDMAS_P5_GKP_INJECTED_SHIFT_SCALE:-0.56}" + +echo "paper_05 digitized-GKP step 31 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/32_run_gkp_digitized_sampler.sh b/examples/paper_runs/paper_05/32_run_gkp_digitized_sampler.sh new file mode 100755 index 0000000..72349f8 --- /dev/null +++ b/examples/paper_runs/paper_05/32_run_gkp_digitized_sampler.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "32_gkp_digitized_sampler")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/run_local_gkp_digitized_sampler.py" \ + --out-dir "${OUT_DIR}" \ + --distance "${LIDMAS_P5_GKP_DISTANCE:-5}" \ + --targets "${LIDMAS_P5_GKP_TARGETS:-representative}" \ + --shots "${LIDMAS_P5_GKP_SHOTS:-${LIDMAS_P5_SHOTS:-4096}}" \ + --rounds "${LIDMAS_P5_GKP_ROUNDS:-3}" \ + --sigma-shift-scale "${LIDMAS_P5_GKP_SIGMA_SHIFT_SCALE:-0.015}" \ + --measurement-error-rate "${LIDMAS_P5_GKP_MEAS_ERROR:-0.01}" \ + --jump-prob "${LIDMAS_P5_GKP_JUMP_PROB:-0.001}" \ + --jump-scale "${LIDMAS_P5_GKP_JUMP_SCALE:-0.5}" \ + --decision-width-scale "${LIDMAS_P5_GKP_DECISION_WIDTH_SCALE:-0.25}" \ + --injected-shift-scale "${LIDMAS_P5_GKP_INJECTED_SHIFT_SCALE:-0.56}" \ + --seed "${LIDMAS_P5_GKP_SEED:-20260706}" \ + --pennylane-mode "${LIDMAS_P5_GKP_PENNYLANE_MODE:-required}" \ + --pennylane-squeeze-r "${LIDMAS_P5_GKP_PENNYLANE_SQUEEZE_R:-2.0}" \ + --pennylane-noise-scale "${LIDMAS_P5_GKP_PENNYLANE_NOISE_SCALE:-1.0}" + +echo "paper_05 digitized-GKP step 32 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/33_ingest_gkp_results.sh b/examples/paper_runs/paper_05/33_ingest_gkp_results.sh new file mode 100755 index 0000000..8d26473 --- /dev/null +++ b/examples/paper_runs/paper_05/33_ingest_gkp_results.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "33_ingest_gkp_results")" +LOCAL_JSON="$(paper_results_dir "32_gkp_digitized_sampler")/local_gkp_digitized_results.json" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi +if [ ! -f "${LOCAL_JSON}" ]; then + echo "Error: ${LOCAL_JSON} not found. Run digitized-GKP sampler first." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/ingest_gkp_digitized_results.py" \ + --out-dir "${OUT_DIR}" \ + --raw-json "${LOCAL_JSON}" + +echo "paper_05 digitized-GKP step 33 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/34_decode_gkp_syndromes.sh b/examples/paper_runs/paper_05/34_decode_gkp_syndromes.sh new file mode 100755 index 0000000..3912d27 --- /dev/null +++ b/examples/paper_runs/paper_05/34_decode_gkp_syndromes.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +IN_DIR="$(paper_results_dir "33_ingest_gkp_results")" +OUT_DIR="$(paper_results_dir "34_decode_gkp_syndromes")" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/decode_gkp_digitized_syndromes.py" \ + --in-dir "${IN_DIR}" \ + --out-dir "${OUT_DIR}" + +echo "paper_05 digitized-GKP step 34 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/35_analyze_gkp.sh b/examples/paper_runs/paper_05/35_analyze_gkp.sh new file mode 100755 index 0000000..9efd06b --- /dev/null +++ b/examples/paper_runs/paper_05/35_analyze_gkp.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +IN_CSV="$(paper_results_dir "34_decode_gkp_syndromes")/decoded_shots.csv" +OUT_DIR="$(paper_results_dir "35_gkp_analysis")" +MANUSCRIPT_DIR="${OUT_DIR}/manuscript_figures" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi +if [ ! -f "${IN_CSV}" ]; then + echo "Error: ${IN_CSV} not found. Run digitized-GKP decode first." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/analyze_gkp_digitized.py" \ + --decoded-csv "${IN_CSV}" \ + --out-dir "${OUT_DIR}" \ + --manuscript-dir "${MANUSCRIPT_DIR}" + +echo "paper_05 digitized-GKP step 35 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/36_render_gkp_figures.sh b/examples/paper_runs/paper_05/36_render_gkp_figures.sh new file mode 100755 index 0000000..ee4a64e --- /dev/null +++ b/examples/paper_runs/paper_05/36_render_gkp_figures.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "36_gkp_figures")" +MANUSCRIPT_DIR="$(paper_results_dir "35_gkp_analysis")/manuscript_figures" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/render_gkp_digitized_figures.py" \ + --out-dir "${OUT_DIR}" \ + --manuscript-dir "${MANUSCRIPT_DIR}" + +echo "paper_05 digitized-GKP step 36 complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/37_render_supplemental_figures.sh b/examples/paper_runs/paper_05/37_render_supplemental_figures.sh new file mode 100755 index 0000000..6846fab --- /dev/null +++ b/examples/paper_runs/paper_05/37_render_supplemental_figures.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +OUT_DIR="$(paper_results_dir "37_supplemental_figures")" +MANUSCRIPT_DIR="${OUT_DIR}/manuscript_figures" +PY_BIN="$(paper_python_bin)" +paper_prepare_plot_env + +if [ -z "${PY_BIN}" ]; then + echo "Error: python3 not found." >&2 + exit 1 +fi + +"${PY_BIN}" "${SCRIPT_DIR}/scripts/render_supplemental_figures.py" \ + --paper-dir "${SCRIPT_DIR}" \ + --out-dir "${OUT_DIR}" \ + --manuscript-dir "${MANUSCRIPT_DIR}" + +echo "paper_05 supplemental figures complete: ${OUT_DIR}" diff --git a/examples/paper_runs/paper_05/Makefile b/examples/paper_runs/paper_05/Makefile new file mode 100644 index 0000000..f1907ae --- /dev/null +++ b/examples/paper_runs/paper_05/Makefile @@ -0,0 +1,67 @@ +SHELL := /bin/bash +.EXPORT_ALL_VARIABLES: + +.PHONY: \ + repetition-local repetition-submit repetition-status repetition-finalize \ + css-local css-submit css-status css-finalize \ + surface-local surface-submit surface-status surface-finalize \ + gkp supplemental + +repetition-local: + ./01_build_syndrome_circuits.sh + ./02_run_local_simulation.sh + +repetition-submit: + ./03_submit_ibm_runtime.sh + +repetition-status: + LIDMAS_P5_IBM_STATUS_ONLY=1 ./03_fetch_ibm_runtime_results.sh + +repetition-finalize: + ./03_fetch_ibm_runtime_results.sh + ./04_ingest_results.sh + ./05_decode_live_syndromes.sh + ./06_analyze_and_plot.sh + +css-local: + ./11_build_qldpc_syndrome_circuits.sh + ./12_run_qldpc_local_simulation.sh + +css-submit: + ./13_submit_qldpc_ibm_runtime.sh + +css-status: + LIDMAS_P5_QLDPC_IBM_STATUS_ONLY=1 ./13_fetch_qldpc_ibm_runtime_results.sh + +css-finalize: + ./13_fetch_qldpc_ibm_runtime_results.sh + ./14_ingest_qldpc_results.sh + ./15_decode_qldpc_syndromes.sh + ./16_analyze_qldpc.sh + +surface-local: + ./21_build_surface_syndrome_circuits.sh + ./22_run_surface_local_simulation.sh + +surface-submit: + ./23_submit_surface_ibm_runtime.sh + +surface-status: + LIDMAS_P5_SURFACE_IBM_STATUS_ONLY=1 ./23_fetch_surface_ibm_runtime_results.sh + +surface-finalize: + ./23_fetch_surface_ibm_runtime_results.sh + ./24_ingest_surface_results.sh + ./25_decode_surface_syndromes.sh + ./26_analyze_surface.sh + +gkp: + ./31_build_gkp_digitized_model.sh + ./32_run_gkp_digitized_sampler.sh + ./33_ingest_gkp_results.sh + ./34_decode_gkp_syndromes.sh + ./35_analyze_gkp.sh + ./36_render_gkp_figures.sh + +supplemental: + ./37_render_supplemental_figures.sh diff --git a/examples/paper_runs/paper_05/README.md b/examples/paper_runs/paper_05/README.md new file mode 100644 index 0000000..8c16f0e --- /dev/null +++ b/examples/paper_runs/paper_05/README.md @@ -0,0 +1,182 @@ +# Paper 05: Hardware-in-the-loop and digitized-GKP syndrome extraction + +This workflow demonstrates syndrome extraction and decoder replay for four +branches: + +- repetition-code circuits; +- compact CSS-LDPC/qLDPC-style Steane Z-check circuits; +- distance-5 surface-code Z-check circuits; +- a PennyLane-backed digitized-GKP companion branch. + +The first three branches can run locally and, when credentials are configured, +on IBM Quantum hardware. The digitized-GKP branch is off-hardware by design: +IBM gate-model backends do not provide oscillator-mode GKP state preparation or +quadrature measurement. The default branch uses PennyLane `default.gaussian` as +a finite-squeezed Gaussian-CV readout proxy before modular binning into outer +Z-check bits. It tests the LiDMaS+ request interface for GKP-derived digitized +syndromes without claiming physical GKP execution. + +Each decode stage replays every extracted syndrome stream through three +policies: MWPM/minimum-weight, UF erasure peeling, and hard-decision BP/min-sum. +The manuscript plots default to the MWPM baseline; the UF and BP rows are +written into the generated decoder response files and `decoded_shots.csv`. + +Run the local workflow: + +```bash +./examples/paper_runs/paper_05/run_all.sh +``` + +This runs all local branches. It skips IBM Runtime submission unless +`LIDMAS_P5_HARDWARE=1` is set. + +Run the IBM Runtime path after configuring an IBM Quantum account: + +```bash +LIDMAS_P5_HARDWARE=1 \ +LIDMAS_P5_IBM_BACKEND=ibm_brisbane \ +IBM_QUANTUM_INSTANCE=your/hub/group/project \ +./examples/paper_runs/paper_05/run_all.sh +``` + +For a manuscript-scale hardware demonstration, use all single-data-qubit +injections and at least 4096 shots per circuit. Submit without waiting, then +fetch and analyze after IBM marks the job complete: + +```bash +LIDMAS_P5_TARGETS=all \ +LIDMAS_P5_SHOTS=4096 \ +./examples/paper_runs/paper_05/01_build_syndrome_circuits.sh + +LIDMAS_P5_TARGETS=all \ +LIDMAS_P5_SHOTS=4096 \ +./examples/paper_runs/paper_05/02_run_local_simulation.sh + +LIDMAS_P5_TARGETS=all \ +LIDMAS_P5_IBM_SHOTS=4096 \ +LIDMAS_P5_IBM_WAIT=0 \ +./examples/paper_runs/paper_05/03_submit_ibm_runtime.sh +``` + +Check the queued job without fetching results: + +```bash +LIDMAS_P5_IBM_STATUS_ONLY=1 \ +./examples/paper_runs/paper_05/03_fetch_ibm_runtime_results.sh +``` + +Once the status is complete, run: + +```bash +./examples/paper_runs/paper_05/03_fetch_ibm_runtime_results.sh +./examples/paper_runs/paper_05/04_ingest_results.sh +./examples/paper_runs/paper_05/05_decode_live_syndromes.sh +./examples/paper_runs/paper_05/06_analyze_and_plot.sh +``` + +Run the compact qLDPC-style CSS-LDPC path: + +```bash +LIDMAS_P5_QLDPC_TARGETS=all \ +LIDMAS_P5_QLDPC_SHOTS=4096 \ +./examples/paper_runs/paper_05/11_build_qldpc_syndrome_circuits.sh + +LIDMAS_P5_QLDPC_TARGETS=all \ +LIDMAS_P5_QLDPC_SHOTS=4096 \ +./examples/paper_runs/paper_05/12_run_qldpc_local_simulation.sh +``` + +The qLDPC path uses the Steane CSS parity-check matrix as a compact +LDPC-style hardware surrogate. It measures the Z-check half for clean and +single-X injected data-qubit circuits. This is suitable for live syndrome +extraction and correction-localization tests, but it should be described as a +small CSS-LDPC demonstration rather than a large asymptotic qLDPC memory. + +Submit and fetch the matching IBM Runtime job: + +```bash +LIDMAS_P5_QLDPC_TARGETS=all \ +LIDMAS_P5_QLDPC_IBM_SHOTS=4096 \ +LIDMAS_P5_QLDPC_IBM_WAIT=0 \ +./examples/paper_runs/paper_05/13_submit_qldpc_ibm_runtime.sh + +LIDMAS_P5_QLDPC_IBM_STATUS_ONLY=1 \ +./examples/paper_runs/paper_05/13_fetch_qldpc_ibm_runtime_results.sh + +./examples/paper_runs/paper_05/13_fetch_qldpc_ibm_runtime_results.sh +./examples/paper_runs/paper_05/14_ingest_qldpc_results.sh +./examples/paper_runs/paper_05/15_decode_qldpc_syndromes.sh +./examples/paper_runs/paper_05/16_analyze_qldpc.sh +``` + +Run the distance-5 surface-code Z-check path: + +```bash +LIDMAS_P5_SURFACE_DISTANCE=5 \ +LIDMAS_P5_SURFACE_TARGETS=representative \ +./examples/paper_runs/paper_05/21_build_surface_syndrome_circuits.sh + +LIDMAS_P5_SURFACE_DISTANCE=5 \ +LIDMAS_P5_SURFACE_TARGETS=representative \ +LIDMAS_P5_SURFACE_SHOTS=4096 \ +./examples/paper_runs/paper_05/22_run_surface_local_simulation.sh +``` + +The surface path measures only the Z-check half for injected-X correction. The +default representative target set avoids running all 40 single-data-qubit +injections while still using a distance-5, 56-active-qubit circuit. + +Submit and fetch the matching IBM Runtime job: + +```bash +LIDMAS_P5_SURFACE_DISTANCE=5 \ +LIDMAS_P5_SURFACE_TARGETS=representative \ +LIDMAS_P5_SURFACE_IBM_SHOTS=4096 \ +LIDMAS_P5_SURFACE_IBM_WAIT=0 \ +./examples/paper_runs/paper_05/23_submit_surface_ibm_runtime.sh + +LIDMAS_P5_SURFACE_IBM_STATUS_ONLY=1 \ +./examples/paper_runs/paper_05/23_fetch_surface_ibm_runtime_results.sh + +./examples/paper_runs/paper_05/23_fetch_surface_ibm_runtime_results.sh +./examples/paper_runs/paper_05/24_ingest_surface_results.sh +./examples/paper_runs/paper_05/25_decode_surface_syndromes.sh +./examples/paper_runs/paper_05/26_analyze_surface.sh +``` + +Run the PennyLane-backed digitized-GKP companion branch: + +```bash +LIDMAS_P5_GKP_DISTANCE=5 \ +LIDMAS_P5_GKP_TARGETS=representative \ +./examples/paper_runs/paper_05/31_build_gkp_digitized_model.sh + +LIDMAS_P5_GKP_DISTANCE=5 \ +LIDMAS_P5_GKP_TARGETS=representative \ +LIDMAS_P5_GKP_SHOTS=4096 \ +LIDMAS_P5_GKP_ROUNDS=3 \ +LIDMAS_P5_GKP_PENNYLANE_MODE=required \ +./examples/paper_runs/paper_05/32_run_gkp_digitized_sampler.sh + +./examples/paper_runs/paper_05/33_ingest_gkp_results.sh +./examples/paper_runs/paper_05/34_decode_gkp_syndromes.sh +./examples/paper_runs/paper_05/35_analyze_gkp.sh +./examples/paper_runs/paper_05/36_render_gkp_figures.sh +``` + +The digitized-GKP summary table is +`results/35_gkp_analysis/table_gkp_digitized_syndrome_summary.csv`. Its +manuscript figures are written under +`results/35_gkp_analysis/manuscript_figures`. + +The scripts do not write credentials. Use a saved Qiskit Runtime account or +environment-provided credentials. If credentials are missing, step 03 exits with a +message explaining what to set. + +For convenience, you can also create an ignored local file: + +```text +examples/paper_runs/paper_05/ibm_credentials.local.json +``` + +Use the shape in `ibm_credentials.example.json`. The local file is ignored by git. diff --git a/examples/paper_runs/paper_05/common.sh b/examples/paper_runs/paper_05/common.sh new file mode 100755 index 0000000..9d0dd2b --- /dev/null +++ b/examples/paper_runs/paper_05/common.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" + +source "${REPO_ROOT}/examples/common.sh" + +paper_results_dir() { + local name="$1" + local base="${LIDMAS_P5_RESULTS_BASE:-${REPO_ROOT}/examples/paper_runs/paper_05/results}" + local dir="${base}/${name}" + mkdir -p "${dir}" + echo "${dir}" +} + +paper_python_bin() { + examples_python_bin "${REPO_ROOT}" +} + +paper_prepare_plot_env() { + local cache_root="${REPO_ROOT}/.cache" + local home_root="${cache_root}/home" + local xdg_cache="${home_root}/.cache" + local mpl_cache="${cache_root}/matplotlib" + + mkdir -p "${xdg_cache}/fontconfig" "${home_root}/.matplotlib" "${mpl_cache}" + + export HOME="${home_root}" + export XDG_CACHE_HOME="${xdg_cache}" + export MPLCONFIGDIR="${mpl_cache}" + export MPLBACKEND="Agg" +} diff --git a/examples/paper_runs/paper_05/ibm_credentials.example.json b/examples/paper_runs/paper_05/ibm_credentials.example.json new file mode 100644 index 0000000..eb1e7d3 --- /dev/null +++ b/examples/paper_runs/paper_05/ibm_credentials.example.json @@ -0,0 +1,5 @@ +{ + "token": "paste-token-here", + "instance": "hub/group/project", + "backend": "ibm_brisbane" +} diff --git a/examples/paper_runs/paper_05/run_all.sh b/examples/paper_runs/paper_05/run_all.sh new file mode 100755 index 0000000..15f2e2b --- /dev/null +++ b/examples/paper_runs/paper_05/run_all.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Repetition code. +"${SCRIPT_DIR}/01_build_syndrome_circuits.sh" +"${SCRIPT_DIR}/02_run_local_simulation.sh" +if [ "${LIDMAS_P5_HARDWARE:-0}" = "1" ]; then + "${SCRIPT_DIR}/03_submit_ibm_runtime.sh" +else + echo "paper_05 repetition: skipping IBM Runtime submission (set LIDMAS_P5_HARDWARE=1 to enable)." +fi +"${SCRIPT_DIR}/04_ingest_results.sh" +"${SCRIPT_DIR}/05_decode_live_syndromes.sh" +"${SCRIPT_DIR}/06_analyze_and_plot.sh" + +# Compact CSS-LDPC/qLDPC-style code. +"${SCRIPT_DIR}/11_build_qldpc_syndrome_circuits.sh" +"${SCRIPT_DIR}/12_run_qldpc_local_simulation.sh" +if [ "${LIDMAS_P5_HARDWARE:-0}" = "1" ]; then + "${SCRIPT_DIR}/13_submit_qldpc_ibm_runtime.sh" +else + echo "paper_05 CSS-LDPC: skipping IBM Runtime submission (set LIDMAS_P5_HARDWARE=1 to enable)." +fi +"${SCRIPT_DIR}/14_ingest_qldpc_results.sh" +"${SCRIPT_DIR}/15_decode_qldpc_syndromes.sh" +"${SCRIPT_DIR}/16_analyze_qldpc.sh" + +# Distance-5 surface-code Z-check branch. +"${SCRIPT_DIR}/21_build_surface_syndrome_circuits.sh" +"${SCRIPT_DIR}/22_run_surface_local_simulation.sh" +if [ "${LIDMAS_P5_HARDWARE:-0}" = "1" ]; then + "${SCRIPT_DIR}/23_submit_surface_ibm_runtime.sh" +else + echo "paper_05 surface: skipping IBM Runtime submission (set LIDMAS_P5_HARDWARE=1 to enable)." +fi +"${SCRIPT_DIR}/24_ingest_surface_results.sh" +"${SCRIPT_DIR}/25_decode_surface_syndromes.sh" +"${SCRIPT_DIR}/26_analyze_surface.sh" + +# PennyLane-backed digitized-GKP companion branch. This branch has no IBM submission step. +"${SCRIPT_DIR}/31_build_gkp_digitized_model.sh" +"${SCRIPT_DIR}/32_run_gkp_digitized_sampler.sh" +"${SCRIPT_DIR}/33_ingest_gkp_results.sh" +"${SCRIPT_DIR}/34_decode_gkp_syndromes.sh" +"${SCRIPT_DIR}/35_analyze_gkp.sh" +"${SCRIPT_DIR}/36_render_gkp_figures.sh" + +echo "paper_05 complete." diff --git a/examples/paper_runs/paper_05/scripts/analyze_gkp_digitized.py b/examples/paper_runs/paper_05/scripts/analyze_gkp_digitized.py new file mode 100644 index 0000000..2c92a02 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/analyze_gkp_digitized.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +"""Analyze paper_05 digitized-GKP syndrome correction results.""" + +from __future__ import annotations + +import argparse +import collections +import csv +import math +import shutil +from pathlib import Path +from typing import Any + +import numpy as np + +from paper05_plot_style import ( + CONTAINS_COLOR, + EXACT_COLOR, + GKP_COLOR, + HEATMAP_CMAP, + apply_journal_style, + compact_source_label, + half_panel_size, + horizontal_heatmap_size, + save_journal_figure, + short_dataset_label, + style_bar_axis, + style_heatmap_axis, + style_rate_axis, +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--decoded-csv", required=True) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--manuscript-dir") + parser.add_argument("--decoder", default="mwpm") + return parser.parse_args() + + +def read_csv(path: Path) -> list[dict[str, str]]: + with path.open("r", encoding="utf-8", newline="") as f: + return list(csv.DictReader(f)) + + +def select_decoder_rows(rows: list[dict[str, str]], decoder: str) -> list[dict[str, str]]: + if not rows or "decoder" not in rows[0]: + return rows + selected = [row for row in rows if row.get("decoder", "mwpm") == decoder] + if not selected: + raise SystemExit(f"no decoded rows found for decoder={decoder!r}") + return selected + + +def f(value: Any) -> float: + try: + return float(value) + except (TypeError, ValueError): + return float("nan") + + +def write_csv(path: Path, rows: list[dict[str, Any]], fields: list[str]) -> None: + with path.open("w", encoding="utf-8", newline="") as fobj: + writer = csv.DictWriter(fobj, fieldnames=fields) + writer.writeheader() + for row in rows: + writer.writerow({field: row.get(field, "") for field in fields}) + + +def wilson_ci(successes: int, total: int, z: float = 1.96) -> tuple[float, float]: + if total <= 0: + return (float("nan"), float("nan")) + phat = successes / total + denom = 1.0 + (z * z / total) + center = (phat + (z * z) / (2.0 * total)) / denom + radius = (z / denom) * math.sqrt((phat * (1.0 - phat) / total) + ((z * z) / (4.0 * total * total))) + return (max(0.0, center - radius), min(1.0, center + radius)) + + +def save_fig(fig: Any, prefix: Path, manuscript_dir: Path | None) -> None: + save_journal_figure(fig, prefix, manuscript_dir) + + +def dataset_label(dataset: str) -> str: + return short_dataset_label(dataset) + + +def circuit_label(circuit_id: str) -> str: + if circuit_id == "clean": + return "clean" + if circuit_id.startswith("q_shift_data_"): + return f"q{circuit_id.removeprefix('q_shift_data_')}" + return circuit_id.replace("_", " ") + + +def circuit_sort_key(circuit_id: str) -> tuple[int, int | str]: + if circuit_id == "clean": + return (0, 0) + if circuit_id.startswith("q_shift_data_"): + try: + return (1, int(circuit_id.removeprefix("q_shift_data_"))) + except ValueError: + pass + return (2, circuit_id) + + +def summarize(rows: list[dict[str, str]]) -> list[dict[str, Any]]: + groups: dict[tuple[str, str], list[dict[str, str]]] = collections.defaultdict(list) + for row in rows: + groups[(row["dataset"], row["circuit_id"])].append(row) + + out: list[dict[str, Any]] = [] + for (dataset, circuit_id), group in sorted(groups.items(), key=lambda item: (item[0][0], circuit_sort_key(item[0][1]))): + injected_values = {r.get("injected_q", "") for r in group if r.get("injected_q", "") != ""} + injected = sorted(injected_values)[0] if injected_values else "" + syndrome_weights = np.asarray([f(r["syndrome_weight"]) for r in group], dtype=float) + correction_weights = np.asarray([f(r["correction_weight"]) for r in group], dtype=float) + exact_values = np.asarray([f(r["exact_intended_match"]) for r in group], dtype=float) + contain_values = np.asarray([f(r["contains_intended_target"]) for r in group], dtype=float) + exact_count = int(np.sum(exact_values)) + contain_count = int(np.sum(contain_values)) + exact_low, exact_high = wilson_ci(exact_count, len(group)) + contain_low, contain_high = wilson_ci(contain_count, len(group)) + syndromes = collections.Counter(r["measured_syndrome"] for r in group) + corrections = collections.Counter(r["correction_indices"] for r in group) + out.append( + { + "dataset": dataset, + "source": group[0].get("source", ""), + "backend": group[0].get("backend", ""), + "circuit_id": circuit_id, + "injected_q": injected, + "shots": len(group), + "mean_syndrome_weight": f"{float(np.mean(syndrome_weights)):.6f}", + "nonempty_syndrome_rate": f"{float(np.mean(syndrome_weights > 0)):.6f}", + "mean_correction_weight": f"{float(np.mean(correction_weights)):.6f}", + "exact_intended_match_count": exact_count, + "exact_intended_match_rate": f"{float(np.mean(exact_values)):.6f}", + "exact_intended_match_ci95_low": f"{exact_low:.6f}", + "exact_intended_match_ci95_high": f"{exact_high:.6f}", + "contains_intended_target_count": contain_count, + "contains_intended_target_rate": f"{float(np.mean(contain_values)):.6f}", + "contains_intended_target_ci95_low": f"{contain_low:.6f}", + "contains_intended_target_ci95_high": f"{contain_high:.6f}", + "most_common_syndrome": syndromes.most_common(1)[0][0] if syndromes else "", + "most_common_syndrome_count": syndromes.most_common(1)[0][1] if syndromes else 0, + "most_common_correction": corrections.most_common(1)[0][0] if corrections else "", + "most_common_correction_count": corrections.most_common(1)[0][1] if corrections else 0, + } + ) + return out + + +def plot_syndrome_heatmap(rows: list[dict[str, str]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + groups: dict[tuple[str, str], list[str]] = collections.defaultdict(list) + max_checks = 0 + for row in rows: + syndrome = row["measured_syndrome"] + key = (row["dataset"], row["circuit_id"]) + groups[key].append(syndrome) + max_checks = max(max_checks, len(syndrome)) + ordered = sorted(groups, key=lambda key: (key[0], circuit_sort_key(key[1]))) + labels = [compact_source_label(dataset, circuit) for dataset, circuit in ordered] + mat = np.zeros((len(labels), max_checks), dtype=float) + for ridx, key in enumerate(ordered): + syndromes = groups[key] + for cidx in range(max_checks): + vals = [int(s[cidx]) for s in syndromes if cidx < len(s)] + mat[ridx, cidx] = float(np.mean(vals)) if vals else 0.0 + + fig, ax = plt.subplots(figsize=horizontal_heatmap_size(len(labels), max_checks), constrained_layout=True) + im = ax.imshow(mat.T, aspect="auto", cmap=HEATMAP_CMAP, vmin=0.0, vmax=1.0, interpolation="nearest") + ax.set_xticks(np.arange(len(labels))) + ax.set_xticklabels(labels, rotation=45, ha="right", rotation_mode="anchor") + ax.set_yticks(np.arange(max_checks)) + ax.set_yticklabels([f"Z{i}" for i in range(max_checks)]) + ax.set_xlabel("stream (PL=PennyLane)") + ax.set_ylabel("outer Z-check bit") + for idx in range(1, len(ordered)): + if ordered[idx][0] != ordered[idx - 1][0]: + ax.axvline(idx - 0.5, color="white", linewidth=1.1) + style_heatmap_axis(ax) + cbar = fig.colorbar(im, ax=ax, fraction=0.045, pad=0.02) + cbar.set_label("activation rate") + save_fig(fig, out_dir / "figure_gkp_digitized_syndrome_heatmap", manuscript_dir) + plt.close(fig) + + +def plot_correction_match(summary_rows: list[dict[str, Any]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + inj_rows = [r for r in summary_rows if str(r.get("injected_q", "")) != ""] + if not inj_rows: + return + targets = sorted({int(str(r["injected_q"])) for r in inj_rows}) + row_by_target = {int(str(r["injected_q"])): r for r in inj_rows} + x = np.arange(len(targets)) + + fig, ax = plt.subplots(figsize=half_panel_size("rate"), constrained_layout=True) + for field, low_field, high_field, label, color, marker in [ + ("exact_intended_match_rate", "exact_intended_match_ci95_low", "exact_intended_match_ci95_high", "exact", "#2563EB", "o"), + ( + "contains_intended_target_rate", + "contains_intended_target_ci95_low", + "contains_intended_target_ci95_high", + "contains", + "#059669", + "s", + ), + ]: + values = np.asarray([f(row_by_target[target][field]) for target in targets], dtype=float) + lows = np.asarray([f(row_by_target[target][low_field]) for target in targets], dtype=float) + highs = np.asarray([f(row_by_target[target][high_field]) for target in targets], dtype=float) + ax.errorbar( + x, + values, + yerr=np.vstack([values - lows, highs - values]), + color=EXACT_COLOR if label == "exact" else CONTAINS_COLOR, + marker=marker, + linewidth=1.45, + markersize=4.2, + capsize=2.6, + capthick=0.8, + label=label, + ) + + ax.set_ylabel("localization rate") + ax.set_xticks(x) + ax.set_xticklabels([f"q{target}" for target in targets]) + style_rate_axis(ax, ymin=0.25, ymax=0.75) + ax.legend(frameon=False, loc="upper right", handlelength=1.4) + save_fig(fig, out_dir / "figure_gkp_digitized_correction_localization", manuscript_dir) + plt.close(fig) + + +def plot_correction_volume(summary_rows: list[dict[str, Any]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + circuit_ids = sorted({str(r["circuit_id"]) for r in summary_rows}, key=circuit_sort_key) + row_by_circuit = {str(r["circuit_id"]): r for r in summary_rows} + x = np.arange(len(circuit_ids)) + values = np.asarray([f(row_by_circuit[circuit]["mean_correction_weight"]) for circuit in circuit_ids], dtype=float) + + fig, ax = plt.subplots(figsize=half_panel_size("bar"), constrained_layout=True) + ax.bar(x, values, width=0.62, color=GKP_COLOR) + ax.set_ylabel("mean correction weight") + ax.set_xticks(x) + ax.set_xticklabels([circuit_label(circuit) for circuit in circuit_ids]) + style_bar_axis(ax) + save_fig(fig, out_dir / "figure_gkp_digitized_correction_volume", manuscript_dir) + plt.close(fig) + + +def main() -> int: + args = parse_args() + decoded_csv = Path(args.decoded_csv) + out_dir = Path(args.out_dir) + manuscript_dir = Path(args.manuscript_dir) if args.manuscript_dir else None + out_dir.mkdir(parents=True, exist_ok=True) + if manuscript_dir is not None: + manuscript_dir.mkdir(parents=True, exist_ok=True) + + rows = select_decoder_rows(read_csv(decoded_csv), args.decoder) + summary_rows = summarize(rows) + fields = [ + "dataset", + "source", + "backend", + "circuit_id", + "injected_q", + "shots", + "mean_syndrome_weight", + "nonempty_syndrome_rate", + "mean_correction_weight", + "exact_intended_match_count", + "exact_intended_match_rate", + "exact_intended_match_ci95_low", + "exact_intended_match_ci95_high", + "contains_intended_target_count", + "contains_intended_target_rate", + "contains_intended_target_ci95_low", + "contains_intended_target_ci95_high", + "most_common_syndrome", + "most_common_syndrome_count", + "most_common_correction", + "most_common_correction_count", + ] + write_csv(out_dir / "table_gkp_digitized_syndrome_summary.csv", summary_rows, fields) + + import matplotlib # type: ignore + + matplotlib.use("Agg", force=True) + apply_journal_style() + plot_syndrome_heatmap(rows, out_dir, manuscript_dir) + plot_correction_match(summary_rows, out_dir, manuscript_dir) + plot_correction_volume(summary_rows, out_dir, manuscript_dir) + print(f"Wrote paper_05 digitized-GKP analysis to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/analyze_live_css_ldpc.py b/examples/paper_runs/paper_05/scripts/analyze_live_css_ldpc.py new file mode 100644 index 0000000..967e684 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/analyze_live_css_ldpc.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +"""Analyze paper_05 live CSS-LDPC syndrome correction results.""" + +from __future__ import annotations + +import argparse +import collections +import csv +import math +import shutil +from pathlib import Path +from typing import Any + +import numpy as np + +from paper05_plot_style import ( + HEATMAP_CMAP, + apply_journal_style, + compact_source_label, + half_panel_size, + horizontal_heatmap_size, + metric_color, + save_journal_figure, + short_dataset_label, + source_linestyle, + source_marker, + style_bar_axis, + style_heatmap_axis, + style_rate_axis, +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--decoded-csv", required=True) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--manuscript-dir") + parser.add_argument("--decoder", default="mwpm") + return parser.parse_args() + + +def read_csv(path: Path) -> list[dict[str, str]]: + with path.open("r", encoding="utf-8", newline="") as f: + return list(csv.DictReader(f)) + + +def select_decoder_rows(rows: list[dict[str, str]], decoder: str) -> list[dict[str, str]]: + if not rows or "decoder" not in rows[0]: + return rows + selected = [row for row in rows if row.get("decoder", "mwpm") == decoder] + if not selected: + raise SystemExit(f"no decoded rows found for decoder={decoder!r}") + return selected + + +def f(value: Any) -> float: + try: + return float(value) + except (TypeError, ValueError): + return float("nan") + + +def write_csv(path: Path, rows: list[dict[str, Any]], fields: list[str]) -> None: + with path.open("w", encoding="utf-8", newline="") as fobj: + writer = csv.DictWriter(fobj, fieldnames=fields) + writer.writeheader() + for row in rows: + writer.writerow({field: row.get(field, "") for field in fields}) + + +def wilson_ci(successes: int, total: int, z: float = 1.96) -> tuple[float, float]: + if total <= 0: + return (float("nan"), float("nan")) + phat = successes / total + denom = 1.0 + (z * z / total) + center = (phat + (z * z) / (2.0 * total)) / denom + radius = (z / denom) * math.sqrt((phat * (1.0 - phat) / total) + ((z * z) / (4.0 * total * total))) + return (max(0.0, center - radius), min(1.0, center + radius)) + + +def save_fig(fig: Any, prefix: Path, manuscript_dir: Path | None) -> None: + save_journal_figure(fig, prefix, manuscript_dir) + + +def dataset_label(dataset: str, backend: str = "") -> str: + return short_dataset_label(dataset, backend) + + +def circuit_label(circuit_id: str) -> str: + if circuit_id == "clean": + return "clean" + if circuit_id.startswith("x_data_"): + return f"X{circuit_id.removeprefix('x_data_')}" + return circuit_id.replace("_", " ") + + +def circuit_sort_key(circuit_id: str) -> tuple[int, int | str]: + if circuit_id == "clean": + return (0, 0) + if circuit_id.startswith("x_data_"): + try: + return (1, int(circuit_id.removeprefix("x_data_"))) + except ValueError: + pass + return (2, circuit_id) + + +def summarize(rows: list[dict[str, str]]) -> list[dict[str, Any]]: + groups: dict[tuple[str, str], list[dict[str, str]]] = collections.defaultdict(list) + for row in rows: + groups[(row["dataset"], row["circuit_id"])].append(row) + + out: list[dict[str, Any]] = [] + for (dataset, circuit_id), group in sorted(groups.items()): + injected_values = {r.get("injected_x", "") for r in group if r.get("injected_x", "") != ""} + injected = sorted(injected_values)[0] if injected_values else "" + syndrome_weights = np.asarray([f(r["syndrome_weight"]) for r in group], dtype=float) + correction_weights = np.asarray([f(r["correction_weight"]) for r in group], dtype=float) + exact_values = np.asarray([f(r["exact_intended_match"]) for r in group], dtype=float) + contain_values = np.asarray([f(r["contains_intended_target"]) for r in group], dtype=float) + exact_count = int(np.sum(exact_values)) + contain_count = int(np.sum(contain_values)) + exact_low, exact_high = wilson_ci(exact_count, len(group)) + contain_low, contain_high = wilson_ci(contain_count, len(group)) + syndromes = collections.Counter(r["measured_syndrome"] for r in group) + corrections = collections.Counter(r["correction_indices"] for r in group) + out.append( + { + "dataset": dataset, + "source": group[0].get("source", ""), + "backend": group[0].get("backend", ""), + "circuit_id": circuit_id, + "injected_x": injected, + "shots": len(group), + "mean_syndrome_weight": f"{float(np.mean(syndrome_weights)):.6f}", + "nonempty_syndrome_rate": f"{float(np.mean(syndrome_weights > 0)):.6f}", + "mean_correction_weight": f"{float(np.mean(correction_weights)):.6f}", + "exact_intended_match_count": exact_count, + "exact_intended_match_rate": f"{float(np.mean(exact_values)):.6f}", + "exact_intended_match_ci95_low": f"{exact_low:.6f}", + "exact_intended_match_ci95_high": f"{exact_high:.6f}", + "contains_intended_target_count": contain_count, + "contains_intended_target_rate": f"{float(np.mean(contain_values)):.6f}", + "contains_intended_target_ci95_low": f"{contain_low:.6f}", + "contains_intended_target_ci95_high": f"{contain_high:.6f}", + "most_common_syndrome": syndromes.most_common(1)[0][0] if syndromes else "", + "most_common_syndrome_count": syndromes.most_common(1)[0][1] if syndromes else 0, + "most_common_correction": corrections.most_common(1)[0][0] if corrections else "", + "most_common_correction_count": corrections.most_common(1)[0][1] if corrections else 0, + } + ) + return out + + +def plot_syndrome_heatmap(rows: list[dict[str, str]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + groups: dict[tuple[str, str], list[str]] = collections.defaultdict(list) + meta: dict[tuple[str, str], str] = {} + max_checks = 0 + for row in rows: + syndrome = row["measured_syndrome"] + key = (row["dataset"], row["circuit_id"]) + groups[key].append(syndrome) + meta[key] = row.get("backend", "") + max_checks = max(max_checks, len(syndrome)) + ordered = sorted(groups, key=lambda key: (0 if key[0].startswith("ibm_") else 1, circuit_sort_key(key[1]))) + labels = [ + compact_source_label(dataset, circuit, meta.get((dataset, circuit), "")) + for dataset, circuit in ordered + ] + mat = np.zeros((len(labels), max_checks), dtype=float) + for ridx, key in enumerate(ordered): + syndromes = groups[key] + for cidx in range(max_checks): + vals = [int(s[cidx]) for s in syndromes if cidx < len(s)] + mat[ridx, cidx] = float(np.mean(vals)) if vals else 0.0 + + fig, ax = plt.subplots(figsize=horizontal_heatmap_size(len(labels), max_checks), constrained_layout=True) + im = ax.imshow(mat.T, aspect="auto", cmap=HEATMAP_CMAP, vmin=0.0, vmax=1.0, interpolation="nearest") + ax.set_xticks(np.arange(len(labels))) + ax.set_xticklabels(labels, rotation=45, ha="right", rotation_mode="anchor") + ax.set_yticks(np.arange(max_checks)) + ax.set_yticklabels([f"Z{i}" for i in range(max_checks)]) + ax.set_xlabel("stream (I=IBM, L=local)") + ax.set_ylabel("Z-check bit") + for idx in range(1, len(ordered)): + if ordered[idx][0] != ordered[idx - 1][0]: + ax.axvline(idx - 0.5, color="white", linewidth=1.1) + style_heatmap_axis(ax) + cbar = fig.colorbar(im, ax=ax, fraction=0.045, pad=0.02) + cbar.set_label("activation rate") + save_fig(fig, out_dir / "figure_qldpc_syndrome_heatmap", manuscript_dir) + plt.close(fig) + + +def plot_correction_match(summary_rows: list[dict[str, Any]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + inj_rows = [r for r in summary_rows if str(r.get("injected_x", "")) != ""] + if not inj_rows: + return + targets = sorted({int(str(r["injected_x"])) for r in inj_rows}) + dataset_keys = sorted({str(r["dataset"]) for r in inj_rows}, key=lambda d: (0 if d.startswith("ibm_") else 1, d)) + row_by_key = {(str(r["dataset"]), int(str(r["injected_x"]))): r for r in inj_rows} + meta = {str(r["dataset"]): str(r.get("backend", "")) for r in inj_rows} + x = np.arange(len(targets)) + + fig, ax = plt.subplots(figsize=half_panel_size("rate"), constrained_layout=True) + contains_same_as_exact = all( + str(row.get("exact_intended_match_rate", "")) == str(row.get("contains_intended_target_rate", "")) + for row in inj_rows + ) + if contains_same_as_exact: + metric_styles = [ + ( + "exact_intended_match_rate", + "exact_intended_match_ci95_low", + "exact_intended_match_ci95_high", + "localization", + "#2563EB", + "o", + ), + ] + else: + metric_styles = [ + ( + "exact_intended_match_rate", + "exact_intended_match_ci95_low", + "exact_intended_match_ci95_high", + "exact", + "#2563EB", + "o", + ), + ( + "contains_intended_target_rate", + "contains_intended_target_ci95_low", + "contains_intended_target_ci95_high", + "contains", + "#059669", + "s", + ), + ] + for didx, dataset in enumerate(dataset_keys): + linestyle = "-" if didx == 0 else "--" + for field, low_field, high_field, metric_name, color, marker in metric_styles: + values = np.asarray([f(row_by_key[(dataset, target)][field]) for target in targets], dtype=float) + lows = np.asarray([f(row_by_key[(dataset, target)][low_field]) for target in targets], dtype=float) + highs = np.asarray([f(row_by_key[(dataset, target)][high_field]) for target in targets], dtype=float) + ax.errorbar( + x, + values, + yerr=np.vstack([values - lows, highs - values]), + color=metric_color(metric_name), + linestyle=source_linestyle(dataset), + marker=source_marker(dataset, marker), + linewidth=1.45, + markersize=4.2, + capsize=2.6, + capthick=0.8, + label=f"{dataset_label(dataset, meta.get(dataset, ''))} {metric_name}", + ) + + ax.set_ylabel("localization rate") + ax.set_xticks(x) + ax.set_xticklabels([f"X{target}" for target in targets]) + style_rate_axis(ax, ymin=0.75) + ax.legend(frameon=False, loc="upper center", bbox_to_anchor=(0.5, -0.18), ncol=2, handlelength=1.4, columnspacing=0.8) + save_fig(fig, out_dir / "figure_qldpc_correction_localization", manuscript_dir) + plt.close(fig) + + +def plot_correction_volume(summary_rows: list[dict[str, Any]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + circuit_ids = sorted({str(r["circuit_id"]) for r in summary_rows}, key=circuit_sort_key) + dataset_keys = sorted({str(r["dataset"]) for r in summary_rows}, key=lambda d: (0 if d.startswith("ibm_") else 1, d)) + row_by_key = {(str(r["dataset"]), str(r["circuit_id"])): r for r in summary_rows} + meta = {str(r["dataset"]): str(r.get("backend", "")) for r in summary_rows} + x = np.arange(len(circuit_ids)) + width = min(0.36, 0.75 / max(1, len(dataset_keys))) + colors = ["#2563EB", "#7C3AED", "#F59E0B", "#059669"] + + fig, ax = plt.subplots(figsize=half_panel_size("bar"), constrained_layout=True) + for didx, dataset in enumerate(dataset_keys): + offset = (didx - (len(dataset_keys) - 1) / 2) * width + values = np.asarray([f(row_by_key[(dataset, circuit)]["mean_correction_weight"]) for circuit in circuit_ids], dtype=float) + ax.bar( + x + offset, + values, + width=width, + color=colors[didx % len(colors)], + label=dataset_label(dataset, meta.get(dataset, "")), + ) + ax.set_ylabel("mean correction weight") + ax.set_xticks(x) + ax.set_xticklabels([circuit_label(circuit) for circuit in circuit_ids]) + style_bar_axis(ax) + ax.legend(frameon=False, loc="upper center", bbox_to_anchor=(0.5, -0.14), ncol=len(dataset_keys)) + save_fig(fig, out_dir / "figure_qldpc_correction_volume", manuscript_dir) + plt.close(fig) + + +def main() -> int: + args = parse_args() + decoded_csv = Path(args.decoded_csv) + out_dir = Path(args.out_dir) + manuscript_dir = Path(args.manuscript_dir) if args.manuscript_dir else None + out_dir.mkdir(parents=True, exist_ok=True) + if manuscript_dir is not None: + manuscript_dir.mkdir(parents=True, exist_ok=True) + + rows = select_decoder_rows(read_csv(decoded_csv), args.decoder) + summary_rows = summarize(rows) + write_csv( + out_dir / "table_qldpc_syndrome_summary.csv", + summary_rows, + [ + "dataset", + "source", + "backend", + "circuit_id", + "injected_x", + "shots", + "mean_syndrome_weight", + "nonempty_syndrome_rate", + "mean_correction_weight", + "exact_intended_match_count", + "exact_intended_match_rate", + "exact_intended_match_ci95_low", + "exact_intended_match_ci95_high", + "contains_intended_target_count", + "contains_intended_target_rate", + "contains_intended_target_ci95_low", + "contains_intended_target_ci95_high", + "most_common_syndrome", + "most_common_syndrome_count", + "most_common_correction", + "most_common_correction_count", + ], + ) + + import matplotlib # type: ignore + + matplotlib.use("Agg", force=True) + apply_journal_style() + plot_syndrome_heatmap(rows, out_dir, manuscript_dir) + plot_correction_match(summary_rows, out_dir, manuscript_dir) + plot_correction_volume(summary_rows, out_dir, manuscript_dir) + print(f"Wrote paper_05 CSS-LDPC analysis to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/analyze_live_repetition.py b/examples/paper_runs/paper_05/scripts/analyze_live_repetition.py new file mode 100755 index 0000000..29baa77 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/analyze_live_repetition.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +"""Analyze paper_05 live repetition-code syndrome correction results.""" + +from __future__ import annotations + +import argparse +import collections +import csv +import math +import shutil +from pathlib import Path +from typing import Any + +import numpy as np + +from paper05_plot_style import ( + CONTAINS_COLOR, + EXACT_COLOR, + HEATMAP_CMAP, + apply_journal_style, + compact_source_label, + half_panel_size, + horizontal_heatmap_size, + metric_color, + save_journal_figure, + short_dataset_label, + source_linestyle, + source_marker, + style_bar_axis, + style_heatmap_axis, + style_rate_axis, +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--decoded-csv", required=True) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--manuscript-dir") + parser.add_argument("--decoder", default="mwpm") + return parser.parse_args() + + +def read_csv(path: Path) -> list[dict[str, str]]: + with path.open("r", encoding="utf-8", newline="") as f: + return list(csv.DictReader(f)) + + +def select_decoder_rows(rows: list[dict[str, str]], decoder: str) -> list[dict[str, str]]: + if not rows or "decoder" not in rows[0]: + return rows + selected = [row for row in rows if row.get("decoder", "mwpm") == decoder] + if not selected: + raise SystemExit(f"no decoded rows found for decoder={decoder!r}") + return selected + + +def f(value: Any) -> float: + try: + return float(value) + except (TypeError, ValueError): + return float("nan") + + +def write_csv(path: Path, rows: list[dict[str, Any]], fields: list[str]) -> None: + with path.open("w", encoding="utf-8", newline="") as fobj: + writer = csv.DictWriter(fobj, fieldnames=fields) + writer.writeheader() + for row in rows: + writer.writerow({field: row.get(field, "") for field in fields}) + + +def wilson_ci(successes: int, total: int, z: float = 1.96) -> tuple[float, float]: + if total <= 0: + return (float("nan"), float("nan")) + phat = successes / total + denom = 1.0 + (z * z / total) + center = (phat + (z * z) / (2.0 * total)) / denom + radius = (z / denom) * math.sqrt((phat * (1.0 - phat) / total) + ((z * z) / (4.0 * total * total))) + return (max(0.0, center - radius), min(1.0, center + radius)) + + +def save_fig(fig: Any, prefix: Path, manuscript_dir: Path | None) -> None: + save_journal_figure(fig, prefix, manuscript_dir) + + +def dataset_label(dataset: str, backend: str = "") -> str: + return short_dataset_label(dataset, backend) + + +def circuit_label(circuit_id: str) -> str: + if circuit_id == "clean": + return "clean" + if circuit_id.startswith("x_data_"): + return f"X{circuit_id.removeprefix('x_data_')}" + return circuit_id.replace("_", " ") + + +def circuit_sort_key(circuit_id: str) -> tuple[int, int | str]: + if circuit_id == "clean": + return (0, 0) + if circuit_id.startswith("x_data_"): + try: + return (1, int(circuit_id.removeprefix("x_data_"))) + except ValueError: + pass + return (2, circuit_id) + + +def summarize(rows: list[dict[str, str]]) -> list[dict[str, Any]]: + groups: dict[tuple[str, str], list[dict[str, str]]] = collections.defaultdict(list) + for row in rows: + groups[(row["dataset"], row["circuit_id"])].append(row) + + out: list[dict[str, Any]] = [] + for (dataset, circuit_id), group in sorted(groups.items()): + injected_values = {r.get("injected_x", "") for r in group if r.get("injected_x", "") != ""} + injected = sorted(injected_values)[0] if injected_values else "" + syndrome_weights = np.asarray([f(r["syndrome_weight"]) for r in group], dtype=float) + correction_weights = np.asarray([f(r["correction_weight"]) for r in group], dtype=float) + exact_values = np.asarray([f(r["exact_intended_match"]) for r in group], dtype=float) + contain_values = np.asarray([f(r["contains_intended_target"]) for r in group], dtype=float) + exact_count = int(np.sum(exact_values)) + contain_count = int(np.sum(contain_values)) + exact_low, exact_high = wilson_ci(exact_count, len(group)) + contain_low, contain_high = wilson_ci(contain_count, len(group)) + syndromes = collections.Counter(r["measured_syndrome"] for r in group) + corrections = collections.Counter(r["correction_indices"] for r in group) + out.append( + { + "dataset": dataset, + "source": group[0].get("source", ""), + "backend": group[0].get("backend", ""), + "circuit_id": circuit_id, + "injected_x": injected, + "shots": len(group), + "mean_syndrome_weight": f"{float(np.mean(syndrome_weights)):.6f}", + "nonempty_syndrome_rate": f"{float(np.mean(syndrome_weights > 0)):.6f}", + "mean_correction_weight": f"{float(np.mean(correction_weights)):.6f}", + "exact_intended_match_count": exact_count, + "exact_intended_match_rate": f"{float(np.mean(exact_values)):.6f}", + "exact_intended_match_ci95_low": f"{exact_low:.6f}", + "exact_intended_match_ci95_high": f"{exact_high:.6f}", + "contains_intended_target_count": contain_count, + "contains_intended_target_rate": f"{float(np.mean(contain_values)):.6f}", + "contains_intended_target_ci95_low": f"{contain_low:.6f}", + "contains_intended_target_ci95_high": f"{contain_high:.6f}", + "most_common_syndrome": syndromes.most_common(1)[0][0] if syndromes else "", + "most_common_syndrome_count": syndromes.most_common(1)[0][1] if syndromes else 0, + "most_common_correction": corrections.most_common(1)[0][0] if corrections else "", + "most_common_correction_count": corrections.most_common(1)[0][1] if corrections else 0, + } + ) + return out + + +def plot_syndrome_heatmap(rows: list[dict[str, str]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + groups: dict[tuple[str, str], list[str]] = collections.defaultdict(list) + meta: dict[tuple[str, str], str] = {} + max_checks = 0 + for row in rows: + syndrome = row["measured_syndrome"] + key = (row["dataset"], row["circuit_id"]) + groups[key].append(syndrome) + meta[key] = row.get("backend", "") + max_checks = max(max_checks, len(syndrome)) + ordered = sorted(groups, key=lambda key: (0 if key[0].startswith("ibm_") else 1, circuit_sort_key(key[1]))) + labels = [ + compact_source_label(dataset, circuit, meta.get((dataset, circuit), "")) + for dataset, circuit in ordered + ] + mat = np.zeros((len(labels), max_checks), dtype=float) + for ridx, key in enumerate(ordered): + syndromes = groups[key] + for cidx in range(max_checks): + vals = [int(s[cidx]) for s in syndromes if cidx < len(s)] + mat[ridx, cidx] = float(np.mean(vals)) if vals else 0.0 + + fig, ax = plt.subplots(figsize=horizontal_heatmap_size(len(labels), max_checks), constrained_layout=True) + im = ax.imshow(mat.T, aspect="auto", cmap=HEATMAP_CMAP, vmin=0.0, vmax=1.0, interpolation="nearest") + ax.set_xticks(np.arange(len(labels))) + ax.set_xticklabels(labels, rotation=45, ha="right", rotation_mode="anchor") + ax.set_yticks(np.arange(max_checks)) + ax.set_yticklabels([f"S{i}" for i in range(max_checks)]) + ax.set_xlabel("stream (I=IBM, L=local)") + ax.set_ylabel("syndrome check") + for idx in range(1, len(ordered)): + if ordered[idx][0] != ordered[idx - 1][0]: + ax.axvline(idx - 0.5, color="white", linewidth=1.1) + style_heatmap_axis(ax) + cbar = fig.colorbar(im, ax=ax, fraction=0.045, pad=0.02) + cbar.set_label("activation rate") + save_fig(fig, out_dir / "figure_live_syndrome_heatmap", manuscript_dir) + plt.close(fig) + + +def plot_correction_match(summary_rows: list[dict[str, Any]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + inj_rows = [r for r in summary_rows if str(r.get("injected_x", "")) != ""] + if not inj_rows: + return + + targets = sorted({int(str(r["injected_x"])) for r in inj_rows}) + dataset_keys = sorted({str(r["dataset"]) for r in inj_rows}, key=lambda d: (0 if d.startswith("ibm_") else 1, d)) + row_by_key = {(str(r["dataset"]), int(str(r["injected_x"]))): r for r in inj_rows} + meta = {str(r["dataset"]): str(r.get("backend", "")) for r in inj_rows} + x = np.arange(len(targets)) + + fig, ax = plt.subplots(figsize=half_panel_size("rate"), constrained_layout=True) + metric_styles = [ + ("exact_intended_match_rate", "exact_intended_match_ci95_low", "exact_intended_match_ci95_high", "exact", "#2563EB", "o"), + ( + "contains_intended_target_rate", + "contains_intended_target_ci95_low", + "contains_intended_target_ci95_high", + "contains", + "#059669", + "s", + ), + ] + for didx, dataset in enumerate(dataset_keys): + linestyle = "-" if didx == 0 else "--" + for field, low_field, high_field, metric_name, color, marker in metric_styles: + values = np.asarray([f(row_by_key[(dataset, target)][field]) for target in targets], dtype=float) + lows = np.asarray([f(row_by_key[(dataset, target)][low_field]) for target in targets], dtype=float) + highs = np.asarray([f(row_by_key[(dataset, target)][high_field]) for target in targets], dtype=float) + label = f"{dataset_label(dataset, meta.get(dataset, ''))} {metric_name}" + ax.errorbar( + x, + values, + yerr=np.vstack([values - lows, highs - values]), + color=metric_color(metric_name), + linestyle=source_linestyle(dataset), + marker=source_marker(dataset, marker), + linewidth=1.45, + markersize=4.2, + capsize=2.6, + capthick=0.8, + label=label, + ) + + ax.set_ylabel("localization rate") + ax.set_xticks(x) + ax.set_xticklabels([f"X{target}" for target in targets]) + style_rate_axis(ax, ymin=0.75) + ax.legend(frameon=False, loc="upper center", bbox_to_anchor=(0.5, -0.18), ncol=2, handlelength=1.4, columnspacing=0.8) + save_fig(fig, out_dir / "figure_correction_localization", manuscript_dir) + plt.close(fig) + + +def plot_correction_volume(summary_rows: list[dict[str, Any]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + circuit_ids = sorted({str(r["circuit_id"]) for r in summary_rows}, key=circuit_sort_key) + dataset_keys = sorted({str(r["dataset"]) for r in summary_rows}, key=lambda d: (0 if d.startswith("ibm_") else 1, d)) + row_by_key = {(str(r["dataset"]), str(r["circuit_id"])): r for r in summary_rows} + meta = {str(r["dataset"]): str(r.get("backend", "")) for r in summary_rows} + x = np.arange(len(circuit_ids)) + width = min(0.36, 0.75 / max(1, len(dataset_keys))) + colors = ["#2563EB", "#7C3AED", "#F59E0B", "#059669"] + + fig, ax = plt.subplots(figsize=half_panel_size("bar"), constrained_layout=True) + for didx, dataset in enumerate(dataset_keys): + offset = (didx - (len(dataset_keys) - 1) / 2) * width + values = np.asarray([f(row_by_key[(dataset, circuit)]["mean_correction_weight"]) for circuit in circuit_ids], dtype=float) + ax.bar( + x + offset, + values, + width=width, + color=colors[didx % len(colors)], + label=dataset_label(dataset, meta.get(dataset, "")), + ) + ax.set_ylabel("mean correction weight") + ax.set_xticks(x) + ax.set_xticklabels([circuit_label(circuit) for circuit in circuit_ids]) + style_bar_axis(ax) + ax.legend(frameon=False, loc="upper left", ncol=len(dataset_keys)) + save_fig(fig, out_dir / "figure_correction_volume", manuscript_dir) + plt.close(fig) + + +def main() -> int: + args = parse_args() + decoded_csv = Path(args.decoded_csv) + out_dir = Path(args.out_dir) + manuscript_dir = Path(args.manuscript_dir) if args.manuscript_dir else None + out_dir.mkdir(parents=True, exist_ok=True) + if manuscript_dir is not None: + manuscript_dir.mkdir(parents=True, exist_ok=True) + + rows = select_decoder_rows(read_csv(decoded_csv), args.decoder) + summary_rows = summarize(rows) + write_csv( + out_dir / "table_live_syndrome_summary.csv", + summary_rows, + [ + "dataset", + "source", + "backend", + "circuit_id", + "injected_x", + "shots", + "mean_syndrome_weight", + "nonempty_syndrome_rate", + "mean_correction_weight", + "exact_intended_match_count", + "exact_intended_match_rate", + "exact_intended_match_ci95_low", + "exact_intended_match_ci95_high", + "contains_intended_target_count", + "contains_intended_target_rate", + "contains_intended_target_ci95_low", + "contains_intended_target_ci95_high", + "most_common_syndrome", + "most_common_syndrome_count", + "most_common_correction", + "most_common_correction_count", + ], + ) + + import matplotlib # type: ignore + + matplotlib.use("Agg", force=True) + import matplotlib.pyplot as plt # noqa: F401 # type: ignore + + apply_journal_style() + plot_syndrome_heatmap(rows, out_dir, manuscript_dir) + plot_correction_match(summary_rows, out_dir, manuscript_dir) + plot_correction_volume(summary_rows, out_dir, manuscript_dir) + print(f"Wrote paper_05 analysis to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/analyze_live_surface.py b/examples/paper_runs/paper_05/scripts/analyze_live_surface.py new file mode 100644 index 0000000..63a4e35 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/analyze_live_surface.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 +"""Analyze paper_05 live surface-code syndrome correction results.""" + +from __future__ import annotations + +import argparse +import collections +import csv +import math +import shutil +from pathlib import Path +from typing import Any + +import numpy as np + +from paper05_plot_style import ( + HEATMAP_CMAP, + apply_journal_style, + compact_source_label, + half_panel_size, + horizontal_heatmap_size, + metric_color, + save_journal_figure, + short_dataset_label, + source_linestyle, + source_marker, + style_bar_axis, + style_heatmap_axis, + style_rate_axis, +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--decoded-csv", required=True) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--manuscript-dir") + parser.add_argument("--decoder", default="mwpm") + return parser.parse_args() + + +def read_csv(path: Path) -> list[dict[str, str]]: + with path.open("r", encoding="utf-8", newline="") as f: + return list(csv.DictReader(f)) + + +def select_decoder_rows(rows: list[dict[str, str]], decoder: str) -> list[dict[str, str]]: + if not rows or "decoder" not in rows[0]: + return rows + selected = [row for row in rows if row.get("decoder", "mwpm") == decoder] + if not selected: + raise SystemExit(f"no decoded rows found for decoder={decoder!r}") + return selected + + +def f(value: Any) -> float: + try: + return float(value) + except (TypeError, ValueError): + return float("nan") + + +def write_csv(path: Path, rows: list[dict[str, Any]], fields: list[str]) -> None: + with path.open("w", encoding="utf-8", newline="") as fobj: + writer = csv.DictWriter(fobj, fieldnames=fields) + writer.writeheader() + for row in rows: + writer.writerow({field: row.get(field, "") for field in fields}) + + +def wilson_ci(successes: int, total: int, z: float = 1.96) -> tuple[float, float]: + if total <= 0: + return (float("nan"), float("nan")) + phat = successes / total + denom = 1.0 + (z * z / total) + center = (phat + (z * z) / (2.0 * total)) / denom + radius = (z / denom) * math.sqrt((phat * (1.0 - phat) / total) + ((z * z) / (4.0 * total * total))) + return (max(0.0, center - radius), min(1.0, center + radius)) + + +def save_fig(fig: Any, prefix: Path, manuscript_dir: Path | None) -> None: + save_journal_figure(fig, prefix, manuscript_dir) + + +def dataset_label(dataset: str, backend: str = "") -> str: + return short_dataset_label(dataset, backend) + + +def circuit_label(circuit_id: str) -> str: + if circuit_id == "clean": + return "clean" + if circuit_id.startswith("x_data_"): + return f"X{circuit_id.removeprefix('x_data_')}" + return circuit_id.replace("_", " ") + + +def circuit_sort_key(circuit_id: str) -> tuple[int, int | str]: + if circuit_id == "clean": + return (0, 0) + if circuit_id.startswith("x_data_"): + try: + return (1, int(circuit_id.removeprefix("x_data_"))) + except ValueError: + pass + return (2, circuit_id) + + +def summarize(rows: list[dict[str, str]]) -> list[dict[str, Any]]: + groups: dict[tuple[str, str], list[dict[str, str]]] = collections.defaultdict(list) + for row in rows: + groups[(row["dataset"], row["circuit_id"])].append(row) + + out: list[dict[str, Any]] = [] + for (dataset, circuit_id), group in sorted(groups.items()): + injected_values = {r.get("injected_x", "") for r in group if r.get("injected_x", "") != ""} + injected = sorted(injected_values)[0] if injected_values else "" + syndrome_weights = np.asarray([f(r["syndrome_weight"]) for r in group], dtype=float) + correction_weights = np.asarray([f(r["correction_weight"]) for r in group], dtype=float) + exact_values = np.asarray([f(r["exact_intended_match"]) for r in group], dtype=float) + contain_values = np.asarray([f(r["contains_intended_target"]) for r in group], dtype=float) + exact_count = int(np.sum(exact_values)) + contain_count = int(np.sum(contain_values)) + exact_low, exact_high = wilson_ci(exact_count, len(group)) + contain_low, contain_high = wilson_ci(contain_count, len(group)) + syndromes = collections.Counter(r["measured_syndrome"] for r in group) + corrections = collections.Counter(r["correction_indices"] for r in group) + out.append( + { + "dataset": dataset, + "source": group[0].get("source", ""), + "backend": group[0].get("backend", ""), + "circuit_id": circuit_id, + "injected_x": injected, + "shots": len(group), + "mean_syndrome_weight": f"{float(np.mean(syndrome_weights)):.6f}", + "nonempty_syndrome_rate": f"{float(np.mean(syndrome_weights > 0)):.6f}", + "mean_correction_weight": f"{float(np.mean(correction_weights)):.6f}", + "exact_intended_match_count": exact_count, + "exact_intended_match_rate": f"{float(np.mean(exact_values)):.6f}", + "exact_intended_match_ci95_low": f"{exact_low:.6f}", + "exact_intended_match_ci95_high": f"{exact_high:.6f}", + "contains_intended_target_count": contain_count, + "contains_intended_target_rate": f"{float(np.mean(contain_values)):.6f}", + "contains_intended_target_ci95_low": f"{contain_low:.6f}", + "contains_intended_target_ci95_high": f"{contain_high:.6f}", + "most_common_syndrome": syndromes.most_common(1)[0][0] if syndromes else "", + "most_common_syndrome_count": syndromes.most_common(1)[0][1] if syndromes else 0, + "most_common_correction": corrections.most_common(1)[0][0] if corrections else "", + "most_common_correction_count": corrections.most_common(1)[0][1] if corrections else 0, + } + ) + return out + + +def plot_syndrome_heatmap(rows: list[dict[str, str]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + groups: dict[tuple[str, str], list[str]] = collections.defaultdict(list) + meta: dict[tuple[str, str], str] = {} + max_checks = 0 + for row in rows: + syndrome = row["measured_syndrome"] + key = (row["dataset"], row["circuit_id"]) + groups[key].append(syndrome) + meta[key] = row.get("backend", "") + max_checks = max(max_checks, len(syndrome)) + ordered = sorted(groups, key=lambda key: (0 if key[0].startswith("ibm_") else 1, circuit_sort_key(key[1]))) + labels = [ + compact_source_label(dataset, circuit, meta.get((dataset, circuit), "")) + for dataset, circuit in ordered + ] + mat = np.zeros((len(labels), max_checks), dtype=float) + for ridx, key in enumerate(ordered): + syndromes = groups[key] + for cidx in range(max_checks): + vals = [int(s[cidx]) for s in syndromes if cidx < len(s)] + mat[ridx, cidx] = float(np.mean(vals)) if vals else 0.0 + + fig, ax = plt.subplots(figsize=horizontal_heatmap_size(len(labels), max_checks), constrained_layout=True) + im = ax.imshow(mat.T, aspect="auto", cmap=HEATMAP_CMAP, vmin=0.0, vmax=1.0, interpolation="nearest") + ax.set_xticks(np.arange(len(labels))) + ax.set_xticklabels(labels, rotation=45, ha="right", rotation_mode="anchor") + ax.set_yticks(np.arange(max_checks)) + ax.set_yticklabels([f"Z{i}" for i in range(max_checks)]) + ax.set_xlabel("stream (I=IBM, L=local)") + ax.set_ylabel("Z-check bit") + for idx in range(1, len(ordered)): + if ordered[idx][0] != ordered[idx - 1][0]: + ax.axvline(idx - 0.5, color="white", linewidth=1.1) + style_heatmap_axis(ax) + cbar = fig.colorbar(im, ax=ax, fraction=0.045, pad=0.02) + cbar.set_label("activation rate") + save_fig(fig, out_dir / "figure_surface_syndrome_heatmap", manuscript_dir) + plt.close(fig) + + +def plot_correction_match(summary_rows: list[dict[str, Any]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + inj_rows = [r for r in summary_rows if str(r.get("injected_x", "")) != ""] + if not inj_rows: + return + targets = sorted({int(str(r["injected_x"])) for r in inj_rows}) + dataset_keys = sorted({str(r["dataset"]) for r in inj_rows}, key=lambda d: (0 if d.startswith("ibm_") else 1, d)) + row_by_key = {(str(r["dataset"]), int(str(r["injected_x"]))): r for r in inj_rows} + meta = {str(r["dataset"]): str(r.get("backend", "")) for r in inj_rows} + x = np.arange(len(targets)) + metric_styles = [ + ("exact_intended_match_rate", "exact_intended_match_ci95_low", "exact_intended_match_ci95_high", "exact", "#2563EB", "o"), + ( + "contains_intended_target_rate", + "contains_intended_target_ci95_low", + "contains_intended_target_ci95_high", + "contains", + "#059669", + "s", + ), + ] + + fig, ax = plt.subplots(figsize=half_panel_size("rate"), constrained_layout=True) + for didx, dataset in enumerate(dataset_keys): + linestyle = "-" if didx == 0 else "--" + for field, low_field, high_field, metric_name, color, marker in metric_styles: + values = np.asarray([f(row_by_key[(dataset, target)][field]) for target in targets], dtype=float) + lows = np.asarray([f(row_by_key[(dataset, target)][low_field]) for target in targets], dtype=float) + highs = np.asarray([f(row_by_key[(dataset, target)][high_field]) for target in targets], dtype=float) + ax.errorbar( + x, + values, + yerr=np.vstack([values - lows, highs - values]), + color=metric_color(metric_name), + linestyle=source_linestyle(dataset), + marker=source_marker(dataset, marker), + linewidth=1.45, + markersize=4.2, + capsize=2.6, + capthick=0.8, + label=f"{dataset_label(dataset, meta.get(dataset, ''))} {metric_name}", + ) + + ax.set_ylabel("localization rate") + ax.set_xticks(x) + ax.set_xticklabels([f"X{target}" for target in targets]) + style_rate_axis(ax, ymin=0.0) + ax.legend(frameon=False, loc="upper center", bbox_to_anchor=(0.5, -0.18), ncol=2, handlelength=1.4, columnspacing=0.8) + save_fig(fig, out_dir / "figure_surface_correction_localization", manuscript_dir) + plt.close(fig) + + +def plot_correction_volume(summary_rows: list[dict[str, Any]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + circuit_ids = sorted({str(r["circuit_id"]) for r in summary_rows}, key=circuit_sort_key) + dataset_keys = sorted({str(r["dataset"]) for r in summary_rows}, key=lambda d: (0 if d.startswith("ibm_") else 1, d)) + row_by_key = {(str(r["dataset"]), str(r["circuit_id"])): r for r in summary_rows} + meta = {str(r["dataset"]): str(r.get("backend", "")) for r in summary_rows} + x = np.arange(len(circuit_ids)) + width = min(0.36, 0.75 / max(1, len(dataset_keys))) + colors = ["#2563EB", "#7C3AED", "#F59E0B", "#059669"] + fig, ax = plt.subplots(figsize=half_panel_size("bar"), constrained_layout=True) + for didx, dataset in enumerate(dataset_keys): + offset = (didx - (len(dataset_keys) - 1) / 2) * width + values = np.asarray([f(row_by_key[(dataset, circuit)]["mean_correction_weight"]) for circuit in circuit_ids], dtype=float) + ax.bar( + x + offset, + values, + width=width, + color=colors[didx % len(colors)], + label=dataset_label(dataset, meta.get(dataset, "")), + ) + ax.set_ylabel("mean correction weight") + ax.set_xticks(x) + ax.set_xticklabels([circuit_label(circuit) for circuit in circuit_ids]) + style_bar_axis(ax) + ax.legend(frameon=False, loc="upper center", bbox_to_anchor=(0.5, -0.14), ncol=len(dataset_keys)) + save_fig(fig, out_dir / "figure_surface_correction_volume", manuscript_dir) + plt.close(fig) + + +def main() -> int: + args = parse_args() + decoded_csv = Path(args.decoded_csv) + out_dir = Path(args.out_dir) + manuscript_dir = Path(args.manuscript_dir) if args.manuscript_dir else None + out_dir.mkdir(parents=True, exist_ok=True) + if manuscript_dir is not None: + manuscript_dir.mkdir(parents=True, exist_ok=True) + + rows = select_decoder_rows(read_csv(decoded_csv), args.decoder) + summary_rows = summarize(rows) + fields = [ + "dataset", + "source", + "backend", + "circuit_id", + "injected_x", + "shots", + "mean_syndrome_weight", + "nonempty_syndrome_rate", + "mean_correction_weight", + "exact_intended_match_count", + "exact_intended_match_rate", + "exact_intended_match_ci95_low", + "exact_intended_match_ci95_high", + "contains_intended_target_count", + "contains_intended_target_rate", + "contains_intended_target_ci95_low", + "contains_intended_target_ci95_high", + "most_common_syndrome", + "most_common_syndrome_count", + "most_common_correction", + "most_common_correction_count", + ] + write_csv(out_dir / "table_surface_syndrome_summary.csv", summary_rows, fields) + + import matplotlib # type: ignore + + matplotlib.use("Agg", force=True) + apply_journal_style() + plot_syndrome_heatmap(rows, out_dir, manuscript_dir) + plot_correction_match(summary_rows, out_dir, manuscript_dir) + plot_correction_volume(summary_rows, out_dir, manuscript_dir) + print(f"Wrote paper_05 surface-code analysis to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/build_css_ldpc_syndrome.py b/examples/paper_runs/paper_05/scripts/build_css_ldpc_syndrome.py new file mode 100644 index 0000000..cce4820 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/build_css_ldpc_syndrome.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Build paper_05 CSS-LDPC syndrome circuit artifacts.""" + +from __future__ import annotations + +import argparse +import csv +import json +from pathlib import Path + +from css_ldpc_syndrome import build_qiskit_circuit, circuit_metadata, experiment_specs + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--targets", default="all") + return parser.parse_args() + + +def write_qasm(path: Path, circuit: object) -> None: + try: + from qiskit import qasm3 # type: ignore + + text = qasm3.dumps(circuit) + except Exception: + try: + text = circuit.qasm() # type: ignore[attr-defined] + except Exception: + text = "// QASM export unavailable in this Qiskit installation.\n" + path.write_text(text, encoding="utf-8") + + +def main() -> int: + args = parse_args() + out_dir = Path(args.out_dir) + qasm_dir = out_dir / "qasm" + out_dir.mkdir(parents=True, exist_ok=True) + qasm_dir.mkdir(parents=True, exist_ok=True) + + specs = experiment_specs(args.targets) + manifest_rows: list[dict[str, object]] = [] + drawings: list[str] = [] + + for spec in specs: + circuit = build_qiskit_circuit(spec) + meta = circuit_metadata(spec) + qasm_path = qasm_dir / f"{spec.circuit_id}.qasm" + write_qasm(qasm_path, circuit) + drawings.append(f"=== {spec.circuit_id} ===\n{circuit.draw(output='text', fold=120)}\n") + manifest_rows.append( + { + **meta, + "n_qubits_total": circuit.num_qubits, + "n_clbits": circuit.num_clbits, + "depth": circuit.depth(), + "qasm_file": str(qasm_path.relative_to(out_dir)), + } + ) + + with (out_dir / "circuit_manifest.json").open("w", encoding="utf-8") as f: + json.dump( + { + "code_family": "css_ldpc", + "code_name": "steane_z_checks", + "targets": args.targets, + "circuits": manifest_rows, + }, + f, + indent=2, + ) + f.write("\n") + + fields = [ + "circuit_id", + "label", + "code_family", + "code_name", + "n_data", + "n_checks", + "injected_x", + "expected_syndrome", + "check_type", + "n_qubits_total", + "n_clbits", + "depth", + "qasm_file", + ] + with (out_dir / "circuit_manifest.csv").open("w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + for row in manifest_rows: + writer.writerow({field: row.get(field, "") for field in fields}) + + (out_dir / "circuit_drawings.txt").write_text("\n".join(drawings), encoding="utf-8") + print(f"Wrote {len(specs)} CSS-LDPC syndrome circuits to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/build_gkp_digitized_model.py b/examples/paper_runs/paper_05/scripts/build_gkp_digitized_model.py new file mode 100644 index 0000000..04e29d7 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/build_gkp_digitized_model.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Build the paper_05 digitized-GKP model metadata.""" + +from __future__ import annotations + +import argparse +import csv +import json +from pathlib import Path + +from gkp_digitized_syndrome import SQRT_PI, experiment_specs, expected_syndrome +from surface_syndrome import build_surface_geometry + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--distance", type=int, default=5) + parser.add_argument("--targets", default="representative") + parser.add_argument("--decision-width-scale", type=float, default=0.25) + parser.add_argument("--injected-shift-scale", type=float, default=0.56) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + geom = build_surface_geometry(args.distance) + specs = experiment_specs(args.distance, args.targets) + target_rows = [] + for spec in specs: + target_rows.append( + { + "circuit_id": spec.circuit_id, + "label": spec.label, + "injected_q": "" if spec.injected_q is None else spec.injected_q, + "expected_z_syndrome": "".join(str(bit) for bit in expected_syndrome(geom, spec.injected_q)), + } + ) + + payload = { + "schema": "paper05_digitized_gkp_model_v1", + "code_family": "digitized_gkp", + "code_name": f"digitized_gkp_surface_d{args.distance}_z_checks", + "distance": args.distance, + "n_gkp_modes": geom.n_data, + "n_x_checks_outer": geom.n_x, + "n_z_checks_outer": geom.n_z, + "sqrt_pi": SQRT_PI, + "decision_width": args.decision_width_scale * SQRT_PI, + "decision_width_scale": args.decision_width_scale, + "injected_shift": args.injected_shift_scale * SQRT_PI, + "injected_shift_scale": args.injected_shift_scale, + "outer_z_supports": geom.z_supports, + "targets": target_rows, + "interpretation": ( + "PennyLane-backed digitized-GKP companion model. Gaussian-CV q-readout " + "samples and analog q-shifts are binned into Z-check syndrome bits on " + "the outer distance-d surface graph." + ), + } + + with (out_dir / "gkp_digitized_model.json").open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + with (out_dir / "table_gkp_digitized_targets.csv").open("w", encoding="utf-8", newline="") as f: + fields = ["circuit_id", "label", "injected_q", "expected_z_syndrome"] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(target_rows) + print(f"Wrote digitized-GKP model metadata to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/build_repetition_syndrome.py b/examples/paper_runs/paper_05/scripts/build_repetition_syndrome.py new file mode 100755 index 0000000..4b534e8 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/build_repetition_syndrome.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Build paper_05 repetition-code syndrome circuit artifacts.""" + +from __future__ import annotations + +import argparse +import csv +import json +from pathlib import Path + +from repetition_syndrome import build_qiskit_circuit, circuit_metadata, experiment_specs + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--n-data", type=int, default=5) + parser.add_argument("--targets", default="all") + return parser.parse_args() + + +def write_qasm(path: Path, circuit: object) -> None: + try: + from qiskit import qasm3 # type: ignore + + text = qasm3.dumps(circuit) + except Exception: + try: + text = circuit.qasm() # type: ignore[attr-defined] + except Exception: + text = "// QASM export unavailable in this Qiskit installation.\n" + path.write_text(text, encoding="utf-8") + + +def main() -> int: + args = parse_args() + if args.n_data < 3: + raise SystemExit("Error: --n-data must be at least 3.") + + out_dir = Path(args.out_dir) + qasm_dir = out_dir / "qasm" + out_dir.mkdir(parents=True, exist_ok=True) + qasm_dir.mkdir(parents=True, exist_ok=True) + + specs = experiment_specs(args.n_data, args.targets) + manifest_rows: list[dict[str, object]] = [] + drawings: list[str] = [] + + for spec in specs: + circuit = build_qiskit_circuit(args.n_data, spec) + meta = circuit_metadata(args.n_data, spec) + qasm_path = qasm_dir / f"{spec.circuit_id}.qasm" + write_qasm(qasm_path, circuit) + + drawing = circuit.draw(output="text", fold=110) + drawings.append(f"=== {spec.circuit_id} ===\n{drawing}\n") + + row = { + **meta, + "n_qubits_total": circuit.num_qubits, + "n_clbits": circuit.num_clbits, + "depth": circuit.depth(), + "qasm_file": str(qasm_path.relative_to(out_dir)), + } + manifest_rows.append(row) + + with (out_dir / "circuit_manifest.json").open("w", encoding="utf-8") as f: + json.dump( + { + "code_family": "repetition", + "n_data": args.n_data, + "n_checks": args.n_data - 1, + "targets": args.targets, + "circuits": manifest_rows, + }, + f, + indent=2, + ) + f.write("\n") + + fields = [ + "circuit_id", + "label", + "n_data", + "n_checks", + "injected_x", + "expected_syndrome", + "n_qubits_total", + "n_clbits", + "depth", + "qasm_file", + ] + with (out_dir / "circuit_manifest.csv").open("w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + for row in manifest_rows: + writer.writerow({field: row.get(field, "") for field in fields}) + + (out_dir / "circuit_drawings.txt").write_text("\n".join(drawings), encoding="utf-8") + print(f"Wrote {len(specs)} repetition-code syndrome circuits to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/build_surface_syndrome.py b/examples/paper_runs/paper_05/scripts/build_surface_syndrome.py new file mode 100644 index 0000000..ab017bf --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/build_surface_syndrome.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Build paper_05 surface-code Z-check syndrome circuit artifacts.""" + +from __future__ import annotations + +import argparse +import csv +import json +from pathlib import Path + +from surface_syndrome import build_qiskit_circuit, circuit_metadata, experiment_specs + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--distance", type=int, default=5) + parser.add_argument("--targets", default="representative") + return parser.parse_args() + + +def write_qasm(path: Path, circuit: object) -> None: + try: + from qiskit import qasm3 # type: ignore + + text = qasm3.dumps(circuit) + except Exception: + try: + text = circuit.qasm() # type: ignore[attr-defined] + except Exception: + text = "// QASM export unavailable in this Qiskit installation.\n" + path.write_text(text, encoding="utf-8") + + +def main() -> int: + args = parse_args() + out_dir = Path(args.out_dir) + qasm_dir = out_dir / "qasm" + out_dir.mkdir(parents=True, exist_ok=True) + qasm_dir.mkdir(parents=True, exist_ok=True) + specs = experiment_specs(args.distance, args.targets) + manifest_rows: list[dict[str, object]] = [] + drawings: list[str] = [] + + for spec in specs: + circuit = build_qiskit_circuit(args.distance, spec) + meta = circuit_metadata(args.distance, spec) + qasm_path = qasm_dir / f"{spec.circuit_id}.qasm" + write_qasm(qasm_path, circuit) + drawings.append(f"=== {spec.circuit_id} ===\n{circuit.draw(output='text', fold=140)}\n") + manifest_rows.append( + { + **meta, + "n_qubits_total": circuit.num_qubits, + "n_clbits": circuit.num_clbits, + "depth": circuit.depth(), + "qasm_file": str(qasm_path.relative_to(out_dir)), + } + ) + + with (out_dir / "circuit_manifest.json").open("w", encoding="utf-8") as f: + json.dump( + { + "code_family": "surface", + "distance": args.distance, + "targets": args.targets, + "circuits": manifest_rows, + }, + f, + indent=2, + ) + f.write("\n") + + fields = [ + "circuit_id", + "label", + "code_family", + "code_name", + "distance", + "n_data", + "n_x_checks", + "n_checks", + "injected_x", + "expected_syndrome", + "check_type", + "n_qubits_total", + "n_clbits", + "depth", + "qasm_file", + ] + with (out_dir / "circuit_manifest.csv").open("w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + for row in manifest_rows: + writer.writerow({field: row.get(field, "") for field in fields}) + + (out_dir / "circuit_drawings.txt").write_text("\n".join(drawings), encoding="utf-8") + print(f"Wrote {len(specs)} surface-code syndrome circuits to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/css_ldpc_syndrome.py b/examples/paper_runs/paper_05/scripts/css_ldpc_syndrome.py new file mode 100644 index 0000000..8a32b43 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/css_ldpc_syndrome.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Small CSS-LDPC syndrome helpers for paper_05. + +The default matrix is the Steane [[7,1,3]] CSS parity-check matrix. This is a +hardware-safe LDPC-style proxy for live syndrome extraction: it is low-density, +has unique single-X syndromes, and needs only one ancilla per Z check. +""" + +from __future__ import annotations + +import itertools +import re +from dataclasses import dataclass +from typing import Any + + +STEANE_HZ: list[list[int]] = [ + [1, 1, 1, 0, 1, 0, 0], + [1, 1, 0, 1, 0, 1, 0], + [1, 0, 1, 1, 0, 0, 1], +] + + +@dataclass(frozen=True) +class ExperimentSpec: + circuit_id: str + injected_x: int | None + label: str + + +def sanitize_label(value: str) -> str: + clean = re.sub(r"[^A-Za-z0-9_.-]+", "_", value.strip()) + clean = clean.strip("_") + return clean or "dataset" + + +def hz_matrix() -> list[list[int]]: + return [row[:] for row in STEANE_HZ] + + +def n_data() -> int: + return len(STEANE_HZ[0]) + + +def n_checks() -> int: + return len(STEANE_HZ) + + +def parse_targets(targets: str) -> list[int | None]: + value = targets.strip().lower() + if value in {"all", "all_injected"}: + return [None, *range(n_data())] + if value in {"middle", "mid"}: + return [None, n_data() // 2] + if value in {"clean", "none"}: + return [None] + + out: list[int | None] = [None] + for part in targets.split(","): + part = part.strip().lower() + if not part: + continue + if part in {"clean", "none"}: + continue + idx = int(part) + if idx < 0 or idx >= n_data(): + raise ValueError(f"target index {idx} outside [0, {n_data() - 1}]") + out.append(idx) + return out + + +def experiment_specs(targets: str) -> list[ExperimentSpec]: + specs: list[ExperimentSpec] = [] + seen: set[int | None] = set() + for target in parse_targets(targets): + if target in seen: + continue + seen.add(target) + if target is None: + specs.append(ExperimentSpec(circuit_id="clean", injected_x=None, label="clean")) + else: + specs.append(ExperimentSpec(circuit_id=f"x_data_{target}", injected_x=target, label=f"X on data {target}")) + return specs + + +def syndrome_from_data_bits(data_bits: list[int]) -> list[int]: + return [sum((bit & 1) * h for bit, h in zip(data_bits, row)) & 1 for row in STEANE_HZ] + + +def expected_syndrome(injected_x: int | None) -> list[int]: + data_bits = [0] * n_data() + if injected_x is not None: + data_bits[injected_x] = 1 + return syndrome_from_data_bits(data_bits) + + +def cbit_values_to_bitstring(cbits_low_to_high: list[int]) -> str: + return "".join(str(int(v) & 1) for v in reversed(cbits_low_to_high)) + + +def parse_bitstring(bitstring: str) -> tuple[list[int], list[int]]: + compact = bitstring.replace(" ", "").strip() + expected = n_checks() + n_data() + if len(compact) != expected: + raise ValueError(f"bitstring length {len(compact)} does not match expected {expected}: {bitstring!r}") + c_low_to_high = [int(ch) for ch in reversed(compact)] + syndrome = c_low_to_high[: n_checks()] + data = c_low_to_high[n_checks() : n_checks() + n_data()] + return syndrome, data + + +def syndrome_to_events(syndrome: list[int], *, time_ns: int = 1000) -> list[dict[str, Any]]: + return [{"index": idx, "time_ns": time_ns, "type": "Z"} for idx, bit in enumerate(syndrome) if bit & 1] + + +def decode_min_weight(syndrome: list[int]) -> list[int]: + """Return a minimum-Hamming-weight X correction matching the Z-check syndrome.""" + target = [bit & 1 for bit in syndrome] + best: tuple[int, tuple[int, ...]] | None = None + for bits in itertools.product((0, 1), repeat=n_data()): + if syndrome_from_data_bits(list(bits)) != target: + continue + weight = sum(bits) + if best is None or (weight, bits) < best: + best = (weight, bits) + if best is None: + return [] + return [idx for idx, bit in enumerate(best[1]) if bit] + + +def correction_syndrome(correction_indices: list[int]) -> list[int]: + bits = [0] * n_data() + for idx in correction_indices: + if 0 <= idx < n_data(): + bits[idx] ^= 1 + return syndrome_from_data_bits(bits) + + +def build_qiskit_circuit(spec: ExperimentSpec) -> Any: + try: + from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister # type: ignore + except Exception as exc: # pragma: no cover - depends on optional environment + raise SystemExit("Qiskit is required to build CSS-LDPC syndrome circuits.") from exc + + data = QuantumRegister(n_data(), "d") + anc = QuantumRegister(n_checks(), "z") + meas = ClassicalRegister(n_checks() + n_data(), "meas") + qc = QuantumCircuit(data, anc, meas, name=spec.circuit_id) + + if spec.injected_x is not None: + qc.x(data[spec.injected_x]) + qc.barrier(data) + + for check_idx, row in enumerate(STEANE_HZ): + for data_idx, enabled in enumerate(row): + if enabled & 1: + qc.cx(data[data_idx], anc[check_idx]) + + qc.barrier(data, anc) + for idx in range(n_checks()): + qc.measure(anc[idx], meas[idx]) + for idx in range(n_data()): + qc.measure(data[idx], meas[n_checks() + idx]) + return qc + + +def circuit_metadata(spec: ExperimentSpec) -> dict[str, Any]: + return { + "circuit_id": spec.circuit_id, + "label": spec.label, + "code_family": "css_ldpc", + "code_name": "steane_z_checks", + "n_data": n_data(), + "n_checks": n_checks(), + "injected_x": "" if spec.injected_x is None else spec.injected_x, + "expected_syndrome": "".join(str(bit) for bit in expected_syndrome(spec.injected_x)), + "check_matrix": hz_matrix(), + "check_type": "Z", + "classical_bit_order": "low-to-high: syndrome[0..n_checks-1], data[0..n_data-1]", + "bitstring_order": "Qiskit count keys are parsed as high-to-low classical bits.", + } diff --git a/examples/paper_runs/paper_05/scripts/decode_css_ldpc_syndromes.py b/examples/paper_runs/paper_05/scripts/decode_css_ldpc_syndromes.py new file mode 100644 index 0000000..b342050 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/decode_css_ldpc_syndromes.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Decode paper_05 CSS-LDPC syndrome request streams.""" + +from __future__ import annotations + +import argparse +import contextlib +import csv +import json +from pathlib import Path +from typing import Any + +from css_ldpc_syndrome import hz_matrix +from paper05_decoder_policies import decode_policy, parse_decoders, syndrome_from_request + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--in-dir", required=True) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--decoders", default="mwpm,uf,bp") + parser.add_argument("--bp-prior", type=float, default=0.08) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + in_dir = Path(args.in_dir) + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + decoders = parse_decoders(args.decoders) + matrix = hz_matrix() + csv_rows: list[dict[str, Any]] = [] + manifest_rows: list[dict[str, Any]] = [] + + for req_path in sorted(in_dir.glob("decoder_requests_*.ndjson")): + dataset = req_path.stem.replace("decoder_requests_", "", 1) + resp_paths = { + decoder: out_dir / f"decoder_responses_{dataset}_css_ldpc_{decoder}.ndjson" + for decoder in decoders + } + line_counts = {decoder: 0 for decoder in decoders} + with contextlib.ExitStack() as stack: + req_f = stack.enter_context(req_path.open("r", encoding="utf-8")) + resp_files = { + decoder: stack.enter_context(path.open("w", encoding="utf-8")) + for decoder, path in resp_paths.items() + } + for line in req_f: + if not line.strip(): + continue + rec = json.loads(line) + meta = rec.get("metadata", {}) + syndrome = syndrome_from_request(rec) + injected_raw = meta.get("injected_x", "") + injected = "" if injected_raw == "" else int(injected_raw) + for decoder in decoders: + result = decode_policy(decoder, matrix, syndrome, prior_p=args.bp_prior) + correction = list(result.correction) + residual = list(result.residual) + if injected != "": + exact_match: int | str = int(correction == [injected]) + contains_target: int | str = int(injected in correction) + else: + exact_match = int(correction == []) + contains_target = exact_match + + diagnostics = { + **result.diagnostics, + "code_id": rec.get("code_id", ""), + "n_qubits": meta.get("n_data", rec.get("n_qubits", "")), + "syndrome": "".join(str(bit) for bit in syndrome), + "residual_syndrome": "".join(str(bit) for bit in residual), + "correction_weight": str(len(correction)), + } + response = { + "correction": { + "qubit_flips": correction, + "qubit_flips_x": correction, + "qubit_flips_z": [], + "confidence": result.confidence, + "decoder_name": f"css_ldpc_{decoder}", + }, + "diagnostics": diagnostics, + "metadata": meta, + } + resp_files[decoder].write(json.dumps(response, separators=(",", ":")) + "\n") + csv_rows.append( + { + "dataset": dataset, + "decoder": decoder, + "source": meta.get("source", ""), + "backend": meta.get("source_backend", ""), + "job_id": meta.get("job_id", ""), + "circuit_id": meta.get("circuit_id", ""), + "injected_x": injected, + "shot_index": meta.get("shot_index", ""), + "bitstring": meta.get("bitstring", ""), + "measured_syndrome": "".join(str(bit) for bit in syndrome), + "expected_syndrome": meta.get("expected_syndrome", ""), + "syndrome_weight": sum(syndrome), + "correction_indices": " ".join(str(idx) for idx in correction), + "correction_weight": len(correction), + "residual_syndrome": "".join(str(bit) for bit in residual), + "exact_intended_match": exact_match, + "contains_intended_target": contains_target, + } + ) + line_counts[decoder] += 1 + for decoder in decoders: + manifest_rows.append( + { + "dataset": dataset, + "request_file": req_path.name, + "response_file": resp_paths[decoder].name, + "decoder": decoder, + "decoder_name": f"css_ldpc_{decoder}", + "lines": line_counts[decoder], + } + ) + + with (out_dir / "decoded_shots.csv").open("w", encoding="utf-8", newline="") as f: + fields = [ + "dataset", + "decoder", + "source", + "backend", + "job_id", + "circuit_id", + "injected_x", + "shot_index", + "bitstring", + "measured_syndrome", + "expected_syndrome", + "syndrome_weight", + "correction_indices", + "correction_weight", + "residual_syndrome", + "exact_intended_match", + "contains_intended_target", + ] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(csv_rows) + + with (out_dir / "decode_manifest.csv").open("w", encoding="utf-8", newline="") as f: + fields = ["dataset", "request_file", "response_file", "decoder", "decoder_name", "lines"] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(manifest_rows) + + print(f"Decoded {len(csv_rows)} CSS-LDPC syndrome-policy rows into {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/decode_gkp_digitized_syndromes.py b/examples/paper_runs/paper_05/scripts/decode_gkp_digitized_syndromes.py new file mode 100644 index 0000000..145c5e9 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/decode_gkp_digitized_syndromes.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Decode paper_05 digitized-GKP syndrome request streams.""" + +from __future__ import annotations + +import argparse +import contextlib +import csv +import json +from pathlib import Path +from typing import Any + +from paper05_decoder_policies import decode_policy, parse_decoders, supports_to_check_matrix, syndrome_from_request +from surface_syndrome import build_surface_geometry + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--in-dir", required=True) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--decoders", default="mwpm,uf,bp") + parser.add_argument("--bp-prior", type=float, default=0.08) + return parser.parse_args() + + +def request_paths(in_dir: Path) -> list[Path]: + manifest_path = in_dir / "ingest_manifest.csv" + if manifest_path.exists(): + with manifest_path.open("r", encoding="utf-8", newline="") as f: + rows = list(csv.DictReader(f)) + paths = [in_dir / row["request_file"] for row in rows if row.get("request_file")] + return sorted(paths) + return sorted(in_dir.glob("decoder_requests_*.ndjson")) + + +def main() -> int: + args = parse_args() + in_dir = Path(args.in_dir) + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + decoders = parse_decoders(args.decoders) + matrices: dict[int, list[list[int]]] = {} + csv_rows: list[dict[str, Any]] = [] + manifest_rows: list[dict[str, Any]] = [] + + for req_path in request_paths(in_dir): + dataset = req_path.stem.replace("decoder_requests_", "", 1) + resp_paths = { + decoder: out_dir / f"decoder_responses_{dataset}_gkp_surface_{decoder}.ndjson" + for decoder in decoders + } + line_counts = {decoder: 0 for decoder in decoders} + with contextlib.ExitStack() as stack: + req_f = stack.enter_context(req_path.open("r", encoding="utf-8")) + resp_files = { + decoder: stack.enter_context(path.open("w", encoding="utf-8")) + for decoder, path in resp_paths.items() + } + for line in req_f: + if not line.strip(): + continue + rec = json.loads(line) + meta = rec.get("metadata", {}) + distance = int(meta.get("distance", "5")) + if distance not in matrices: + geom = build_surface_geometry(distance) + matrices[distance] = supports_to_check_matrix(geom.n_data, geom.z_supports) + matrix = matrices[distance] + syndrome = syndrome_from_request(rec) + injected_raw = meta.get("injected_q", "") + injected = "" if injected_raw == "" else int(injected_raw) + for decoder in decoders: + result = decode_policy(decoder, matrix, syndrome, prior_p=args.bp_prior) + correction = list(result.correction) + residual = list(result.residual) + if injected != "": + exact_match: int | str = int(correction == [injected]) + contains_target: int | str = int(injected in correction) + else: + exact_match = int(correction == []) + contains_target = exact_match + + diagnostics = { + **result.diagnostics, + "code_id": rec.get("code_id", ""), + "n_qubits": meta.get("n_data", rec.get("n_qubits", "")), + "syndrome": "".join(str(bit) for bit in syndrome), + "residual_syndrome": "".join(str(bit) for bit in residual), + "correction_weight": str(len(correction)), + } + response = { + "correction": { + "qubit_flips": correction, + "qubit_flips_x": correction, + "qubit_flips_z": [], + "confidence": result.confidence, + "decoder_name": f"gkp_surface_{decoder}", + }, + "diagnostics": diagnostics, + "metadata": meta, + } + resp_files[decoder].write(json.dumps(response, separators=(",", ":")) + "\n") + csv_rows.append( + { + "dataset": dataset, + "decoder": decoder, + "source": meta.get("source", ""), + "backend": meta.get("source_backend", ""), + "job_id": meta.get("job_id", ""), + "circuit_id": meta.get("circuit_id", ""), + "injected_q": injected, + "shot_index": meta.get("shot_index", ""), + "bitstring": meta.get("bitstring", ""), + "measured_syndrome": "".join(str(bit) for bit in syndrome), + "expected_syndrome": meta.get("expected_syndrome", ""), + "syndrome_weight": sum(syndrome), + "correction_indices": " ".join(str(idx) for idx in correction), + "correction_weight": len(correction), + "residual_syndrome": "".join(str(bit) for bit in residual), + "exact_intended_match": exact_match, + "contains_intended_target": contains_target, + } + ) + line_counts[decoder] += 1 + for decoder in decoders: + manifest_rows.append( + { + "dataset": dataset, + "request_file": req_path.name, + "response_file": resp_paths[decoder].name, + "decoder": decoder, + "decoder_name": f"gkp_surface_{decoder}", + "lines": line_counts[decoder], + } + ) + + with (out_dir / "decoded_shots.csv").open("w", encoding="utf-8", newline="") as f: + fields = [ + "dataset", + "decoder", + "source", + "backend", + "job_id", + "circuit_id", + "injected_q", + "shot_index", + "bitstring", + "measured_syndrome", + "expected_syndrome", + "syndrome_weight", + "correction_indices", + "correction_weight", + "residual_syndrome", + "exact_intended_match", + "contains_intended_target", + ] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(csv_rows) + + with (out_dir / "decode_manifest.csv").open("w", encoding="utf-8", newline="") as f: + fields = ["dataset", "request_file", "response_file", "decoder", "decoder_name", "lines"] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(manifest_rows) + + print(f"Decoded {len(csv_rows)} digitized-GKP syndrome-policy rows into {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/decode_repetition_syndromes.py b/examples/paper_runs/paper_05/scripts/decode_repetition_syndromes.py new file mode 100755 index 0000000..196c867 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/decode_repetition_syndromes.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Decode paper_05 repetition-code syndrome request streams.""" + +from __future__ import annotations + +import argparse +import contextlib +import csv +import json +from pathlib import Path +from typing import Any + +from paper05_decoder_policies import ( + decode_policy, + parse_decoders, + repetition_check_matrix, + residual_syndrome, + syndrome_from_request, +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--in-dir", required=True) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--decoders", default="mwpm,uf,bp") + parser.add_argument("--bp-prior", type=float, default=0.08) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + in_dir = Path(args.in_dir) + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + decoders = parse_decoders(args.decoders) + + csv_rows: list[dict[str, Any]] = [] + manifest_rows: list[dict[str, Any]] = [] + + for req_path in sorted(in_dir.glob("decoder_requests_*.ndjson")): + dataset = req_path.stem.replace("decoder_requests_", "", 1) + resp_paths = { + decoder: out_dir / f"decoder_responses_{dataset}_repetition_{decoder}.ndjson" + for decoder in decoders + } + line_counts = {decoder: 0 for decoder in decoders} + with contextlib.ExitStack() as stack: + req_f = stack.enter_context(req_path.open("r", encoding="utf-8")) + resp_files = { + decoder: stack.enter_context(path.open("w", encoding="utf-8")) + for decoder, path in resp_paths.items() + } + for line in req_f: + if not line.strip(): + continue + rec = json.loads(line) + meta = rec.get("metadata", {}) + n_data = int(meta.get("n_data", rec.get("n_qubits", 0))) + matrix = repetition_check_matrix(n_data) + syndrome = syndrome_from_request(rec) + injected_raw = meta.get("injected_x", "") + injected = "" if injected_raw == "" else int(injected_raw) + for decoder in decoders: + result = decode_policy(decoder, matrix, syndrome, prior_p=args.bp_prior) + correction = list(result.correction) + residual = list(result.residual) + exact_match: int | str + contains_target: int | str + if injected != "": + exact_match = int(correction == [injected]) + contains_target = int(injected in correction) + else: + exact_match = int(correction == []) + contains_target = exact_match + + diagnostics = { + **result.diagnostics, + "code_id": rec.get("code_id", ""), + "n_qubits": str(n_data), + "syndrome": "".join(str(bit) for bit in syndrome), + "residual_syndrome": "".join(str(bit) for bit in residual), + "correction_weight": str(len(correction)), + } + response = { + "correction": { + "qubit_flips": correction, + "qubit_flips_x": correction, + "qubit_flips_z": [], + "confidence": result.confidence, + "decoder_name": f"repetition_{decoder}", + }, + "diagnostics": diagnostics, + "metadata": meta, + } + resp_files[decoder].write(json.dumps(response, separators=(",", ":")) + "\n") + csv_rows.append( + { + "dataset": dataset, + "decoder": decoder, + "source": meta.get("source", ""), + "backend": meta.get("source_backend", ""), + "job_id": meta.get("job_id", ""), + "circuit_id": meta.get("circuit_id", ""), + "injected_x": injected, + "shot_index": meta.get("shot_index", ""), + "bitstring": meta.get("bitstring", ""), + "measured_syndrome": "".join(str(bit) for bit in syndrome), + "expected_syndrome": meta.get("expected_syndrome", ""), + "syndrome_weight": sum(syndrome), + "correction_indices": " ".join(str(idx) for idx in correction), + "correction_weight": len(correction), + "residual_syndrome": "".join(str(bit) for bit in residual), + "exact_intended_match": exact_match, + "contains_intended_target": contains_target, + } + ) + line_counts[decoder] += 1 + for decoder in decoders: + manifest_rows.append( + { + "dataset": dataset, + "request_file": req_path.name, + "response_file": resp_paths[decoder].name, + "decoder": decoder, + "decoder_name": f"repetition_{decoder}", + "lines": line_counts[decoder], + } + ) + + with (out_dir / "decoded_shots.csv").open("w", encoding="utf-8", newline="") as f: + fields = [ + "dataset", + "decoder", + "source", + "backend", + "job_id", + "circuit_id", + "injected_x", + "shot_index", + "bitstring", + "measured_syndrome", + "expected_syndrome", + "syndrome_weight", + "correction_indices", + "correction_weight", + "residual_syndrome", + "exact_intended_match", + "contains_intended_target", + ] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(csv_rows) + + with (out_dir / "decode_manifest.csv").open("w", encoding="utf-8", newline="") as f: + fields = ["dataset", "request_file", "response_file", "decoder", "decoder_name", "lines"] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(manifest_rows) + + print(f"Decoded {len(csv_rows)} repetition syndrome-policy rows into {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/decode_surface_syndromes.py b/examples/paper_runs/paper_05/scripts/decode_surface_syndromes.py new file mode 100644 index 0000000..ea856da --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/decode_surface_syndromes.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Decode paper_05 surface-code Z-check syndrome request streams.""" + +from __future__ import annotations + +import argparse +import contextlib +import csv +import json +from pathlib import Path +from typing import Any + +from paper05_decoder_policies import decode_policy, parse_decoders, supports_to_check_matrix, syndrome_from_request +from surface_syndrome import build_surface_geometry + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--in-dir", required=True) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--decoders", default="mwpm,uf,bp") + parser.add_argument("--bp-prior", type=float, default=0.08) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + in_dir = Path(args.in_dir) + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + decoders = parse_decoders(args.decoders) + matrices: dict[int, list[list[int]]] = {} + csv_rows: list[dict[str, Any]] = [] + manifest_rows: list[dict[str, Any]] = [] + + for req_path in sorted(in_dir.glob("decoder_requests_*.ndjson")): + dataset = req_path.stem.replace("decoder_requests_", "", 1) + resp_paths = { + decoder: out_dir / f"decoder_responses_{dataset}_surface_{decoder}.ndjson" + for decoder in decoders + } + line_counts = {decoder: 0 for decoder in decoders} + with contextlib.ExitStack() as stack: + req_f = stack.enter_context(req_path.open("r", encoding="utf-8")) + resp_files = { + decoder: stack.enter_context(path.open("w", encoding="utf-8")) + for decoder, path in resp_paths.items() + } + for line in req_f: + if not line.strip(): + continue + rec = json.loads(line) + meta = rec.get("metadata", {}) + distance = int(meta.get("distance", "5")) + if distance not in matrices: + geom = build_surface_geometry(distance) + matrices[distance] = supports_to_check_matrix(geom.n_data, geom.z_supports) + matrix = matrices[distance] + syndrome = syndrome_from_request(rec) + injected_raw = meta.get("injected_x", "") + injected = "" if injected_raw == "" else int(injected_raw) + for decoder in decoders: + result = decode_policy(decoder, matrix, syndrome, prior_p=args.bp_prior) + correction = list(result.correction) + residual = list(result.residual) + if injected != "": + exact_match: int | str = int(correction == [injected]) + contains_target: int | str = int(injected in correction) + else: + exact_match = int(correction == []) + contains_target = exact_match + + diagnostics = { + **result.diagnostics, + "code_id": rec.get("code_id", ""), + "n_qubits": meta.get("n_data", rec.get("n_qubits", "")), + "syndrome": "".join(str(bit) for bit in syndrome), + "residual_syndrome": "".join(str(bit) for bit in residual), + "correction_weight": str(len(correction)), + } + response = { + "correction": { + "qubit_flips": correction, + "qubit_flips_x": correction, + "qubit_flips_z": [], + "confidence": result.confidence, + "decoder_name": f"surface_{decoder}", + }, + "diagnostics": diagnostics, + "metadata": meta, + } + resp_files[decoder].write(json.dumps(response, separators=(",", ":")) + "\n") + csv_rows.append( + { + "dataset": dataset, + "decoder": decoder, + "source": meta.get("source", ""), + "backend": meta.get("source_backend", ""), + "job_id": meta.get("job_id", ""), + "circuit_id": meta.get("circuit_id", ""), + "injected_x": injected, + "shot_index": meta.get("shot_index", ""), + "bitstring": meta.get("bitstring", ""), + "measured_syndrome": "".join(str(bit) for bit in syndrome), + "expected_syndrome": meta.get("expected_syndrome", ""), + "syndrome_weight": sum(syndrome), + "correction_indices": " ".join(str(idx) for idx in correction), + "correction_weight": len(correction), + "residual_syndrome": "".join(str(bit) for bit in residual), + "exact_intended_match": exact_match, + "contains_intended_target": contains_target, + } + ) + line_counts[decoder] += 1 + for decoder in decoders: + manifest_rows.append( + { + "dataset": dataset, + "request_file": req_path.name, + "response_file": resp_paths[decoder].name, + "decoder": decoder, + "decoder_name": f"surface_{decoder}", + "lines": line_counts[decoder], + } + ) + + with (out_dir / "decoded_shots.csv").open("w", encoding="utf-8", newline="") as f: + fields = [ + "dataset", + "decoder", + "source", + "backend", + "job_id", + "circuit_id", + "injected_x", + "shot_index", + "bitstring", + "measured_syndrome", + "expected_syndrome", + "syndrome_weight", + "correction_indices", + "correction_weight", + "residual_syndrome", + "exact_intended_match", + "contains_intended_target", + ] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(csv_rows) + + with (out_dir / "decode_manifest.csv").open("w", encoding="utf-8", newline="") as f: + fields = ["dataset", "request_file", "response_file", "decoder", "decoder_name", "lines"] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(manifest_rows) + + print(f"Decoded {len(csv_rows)} surface-code syndrome-policy rows into {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/fetch_ibm_css_ldpc_results.py b/examples/paper_runs/paper_05/scripts/fetch_ibm_css_ldpc_results.py new file mode 100644 index 0000000..efc2995 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/fetch_ibm_css_ldpc_results.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Fetch a completed paper_05 CSS-LDPC IBM Runtime job.""" + +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +from typing import Any + +from submit_ibm_repetition_sampler import extract_counts, load_credentials, load_service + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--submission-json", required=True) + parser.add_argument("--out-json", required=True) + parser.add_argument("--credentials-file", default="") + parser.add_argument("--result-timeout", type=float, default=300.0) + parser.add_argument("--status-only", action="store_true") + return parser.parse_args() + + +def job_status_value(job: Any) -> str: + value = getattr(job, "status", "") + if callable(value): + value = value() + return str(value) + + +def main() -> int: + args = parse_args() + submission_path = Path(args.submission_json) + with submission_path.open("r", encoding="utf-8") as f: + submission = json.load(f) + + creds = load_credentials(args.credentials_file) + token = ( + os.environ.get("IBM_QUANTUM_TOKEN") + or os.environ.get("QISKIT_IBM_TOKEN") + or creds.get("token", "") + or creds.get("ibm_quantum_token", "") + ) + instance = os.environ.get("IBM_QUANTUM_INSTANCE", "") or creds.get("instance", "") + channel = os.environ.get("IBM_QUANTUM_CHANNEL", "") or creds.get("channel", "") or "ibm_quantum_platform" + service = load_service(instance, token, channel) + + job_id = str(submission["job_id"]) + job = service.job(job_id) + status = job_status_value(job) + print(f"Fetched IBM Runtime CSS-LDPC job {job_id}; status={status}") + if args.status_only: + return 0 + + result = job.result(timeout=args.result_timeout) + experiments: list[dict[str, Any]] = [] + for meta, pub_result in zip(submission.get("experiments", []), result): + experiments.append({**meta, "counts": dict(sorted(extract_counts(pub_result).items()))}) + + payload = { + "schema": "paper05_css_ldpc_results_v1", + "source": "ibm_runtime", + "backend": submission.get("backend", ""), + "job_id": job_id, + "shots": int(submission.get("shots", 0)), + "code_family": "css_ldpc", + "code_name": "steane_z_checks", + "n_data": int(submission.get("n_data", 0)), + "n_checks": int(submission.get("n_checks", 0)), + "optimization_level": submission.get("optimization_level", ""), + "experiments": experiments, + } + out_path = Path(args.out_json) + out_path.parent.mkdir(parents=True, exist_ok=True) + with out_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + print(f"Wrote IBM Runtime CSS-LDPC result payload to {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/fetch_ibm_repetition_results.py b/examples/paper_runs/paper_05/scripts/fetch_ibm_repetition_results.py new file mode 100755 index 0000000..d16ab19 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/fetch_ibm_repetition_results.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Fetch a completed paper_05 IBM Runtime job into the raw-results JSON format.""" + +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +from typing import Any + +from submit_ibm_repetition_sampler import extract_counts, load_credentials, load_service + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--submission-json", required=True) + parser.add_argument("--out-json", required=True) + parser.add_argument("--credentials-file", default="") + parser.add_argument("--result-timeout", type=float, default=300.0) + parser.add_argument("--status-only", action="store_true", help="Report Runtime job status without fetching results.") + return parser.parse_args() + + +def job_status_value(job: Any) -> str: + value = getattr(job, "status", "") + if callable(value): + value = value() + return str(value) + + +def main() -> int: + args = parse_args() + submission_path = Path(args.submission_json) + with submission_path.open("r", encoding="utf-8") as f: + submission = json.load(f) + + creds = load_credentials(args.credentials_file) + token = ( + os.environ.get("IBM_QUANTUM_TOKEN") + or os.environ.get("QISKIT_IBM_TOKEN") + or creds.get("token", "") + or creds.get("ibm_quantum_token", "") + ) + instance = os.environ.get("IBM_QUANTUM_INSTANCE", "") or creds.get("instance", "") + channel = os.environ.get("IBM_QUANTUM_CHANNEL", "") or creds.get("channel", "") or "ibm_quantum_platform" + service = load_service(instance, token, channel) + + job_id = str(submission["job_id"]) + job = service.job(job_id) + status = job_status_value(job) + print(f"Fetched IBM Runtime job {job_id}; status={status}") + if args.status_only: + return 0 + result = job.result(timeout=args.result_timeout) + + experiments: list[dict[str, Any]] = [] + for meta, pub_result in zip(submission.get("experiments", []), result): + experiments.append({**meta, "counts": dict(sorted(extract_counts(pub_result).items()))}) + + payload = { + "schema": "paper05_repetition_results_v1", + "source": "ibm_runtime", + "backend": submission.get("backend", ""), + "job_id": job_id, + "shots": int(submission.get("shots", 0)), + "n_data": int(submission.get("n_data", 0)), + "n_checks": int(submission.get("n_checks", 0)), + "optimization_level": submission.get("optimization_level", ""), + "experiments": experiments, + } + + out_path = Path(args.out_json) + out_path.parent.mkdir(parents=True, exist_ok=True) + with out_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + print(f"Wrote IBM Runtime result payload to {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/fetch_ibm_surface_results.py b/examples/paper_runs/paper_05/scripts/fetch_ibm_surface_results.py new file mode 100644 index 0000000..516a04a --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/fetch_ibm_surface_results.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Fetch a completed paper_05 surface-code IBM Runtime job.""" + +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +from typing import Any + +from submit_ibm_repetition_sampler import extract_counts, load_credentials, load_service + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--submission-json", required=True) + parser.add_argument("--out-json", required=True) + parser.add_argument("--credentials-file", default="") + parser.add_argument("--result-timeout", type=float, default=300.0) + parser.add_argument("--status-only", action="store_true") + return parser.parse_args() + + +def job_status_value(job: Any) -> str: + value = getattr(job, "status", "") + if callable(value): + value = value() + return str(value) + + +def main() -> int: + args = parse_args() + submission_path = Path(args.submission_json) + with submission_path.open("r", encoding="utf-8") as f: + submission = json.load(f) + + creds = load_credentials(args.credentials_file) + token = ( + os.environ.get("IBM_QUANTUM_TOKEN") + or os.environ.get("QISKIT_IBM_TOKEN") + or creds.get("token", "") + or creds.get("ibm_quantum_token", "") + ) + instance = os.environ.get("IBM_QUANTUM_INSTANCE", "") or creds.get("instance", "") + channel = os.environ.get("IBM_QUANTUM_CHANNEL", "") or creds.get("channel", "") or "ibm_quantum_platform" + service = load_service(instance, token, channel) + + job_id = str(submission["job_id"]) + job = service.job(job_id) + status = job_status_value(job) + print(f"Fetched IBM Runtime surface-code job {job_id}; status={status}") + if args.status_only: + return 0 + + result = job.result(timeout=args.result_timeout) + experiments: list[dict[str, Any]] = [] + for meta, pub_result in zip(submission.get("experiments", []), result): + experiments.append({**meta, "counts": dict(sorted(extract_counts(pub_result).items()))}) + + payload = { + "schema": "paper05_surface_results_v1", + "source": "ibm_runtime", + "backend": submission.get("backend", ""), + "job_id": job_id, + "shots": int(submission.get("shots", 0)), + "code_family": "surface", + "code_name": submission.get("code_name", ""), + "distance": int(submission.get("distance", 0)), + "n_data": int(submission.get("n_data", 0)), + "n_checks": int(submission.get("n_checks", 0)), + "optimization_level": submission.get("optimization_level", ""), + "experiments": experiments, + } + out_path = Path(args.out_json) + out_path.parent.mkdir(parents=True, exist_ok=True) + with out_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + print(f"Wrote IBM Runtime surface-code result payload to {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/gkp_digitized_syndrome.py b/examples/paper_runs/paper_05/scripts/gkp_digitized_syndrome.py new file mode 100644 index 0000000..c1944a2 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/gkp_digitized_syndrome.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Digitized-GKP helper functions for paper_05. + +The model used here is an off-hardware digitized companion to the IBM hardware runs. +Each data site is interpreted as a GKP oscillator mode. Small analog q-shifts +are accumulated, digitized through a square-lattice GKP cell, and mapped onto +the Z-check layer of the same outer surface-code incidence graph used by the +surface branch. +""" + +from __future__ import annotations + +import math +import random +import re +from dataclasses import dataclass +from typing import Any + +from surface_syndrome import SurfaceGeometry, build_surface_geometry, expected_syndrome as surface_expected_syndrome + + +SQRT_PI = math.sqrt(math.pi) + + +@dataclass(frozen=True) +class ExperimentSpec: + circuit_id: str + injected_q: int | None + label: str + + +def sanitize_label(value: str) -> str: + clean = re.sub(r"[^A-Za-z0-9_.-]+", "_", value.strip()) + clean = clean.strip("_") + return clean or "dataset" + + +def _representative_targets(geom: SurfaceGeometry) -> list[int]: + if geom.distance == 5: + return [1, 5, 10, 14, 17, 22, 32, 37] + out: list[int] = [] + seen: set[tuple[int, ...]] = set() + for q in range(geom.n_data): + syndrome = tuple(i for i, support in enumerate(geom.z_supports) if q in support) + if syndrome in seen: + continue + seen.add(syndrome) + out.append(q) + if len(out) >= min(8, geom.n_data): + break + return out + + +def parse_targets(targets: str, geom: SurfaceGeometry) -> list[int | None]: + value = targets.strip().lower() + if value in {"representative", "rep", "selected"}: + return [None, *_representative_targets(geom)] + if value in {"all", "all_injected"}: + return [None, *range(geom.n_data)] + if value in {"middle", "mid"}: + return [None, geom.n_data // 2] + if value in {"clean", "none"}: + return [None] + + out: list[int | None] = [None] + for part in targets.split(","): + part = part.strip().lower() + if not part: + continue + if part in {"clean", "none"}: + continue + idx = int(part) + if idx < 0 or idx >= geom.n_data: + raise ValueError(f"target index {idx} outside [0, {geom.n_data - 1}]") + out.append(idx) + return out + + +def experiment_specs(distance: int, targets: str) -> list[ExperimentSpec]: + geom = build_surface_geometry(distance) + specs: list[ExperimentSpec] = [] + seen: set[int | None] = set() + for target in parse_targets(targets, geom): + if target in seen: + continue + seen.add(target) + if target is None: + specs.append(ExperimentSpec(circuit_id="clean", injected_q=None, label="clean")) + else: + specs.append( + ExperimentSpec( + circuit_id=f"q_shift_data_{target}", + injected_q=target, + label=f"q shift on data {target}", + ) + ) + return specs + + +def digitize_periodic(value: float, *, period: float = SQRT_PI, width: float = 0.25 * SQRT_PI, bias: float = 0.0) -> int: + """Return the binary bin for a periodic square-lattice GKP decision cell.""" + if period <= 0.0: + return 0 + shifted = value + bias + wrapped = (shifted + 0.5 * period) % period - 0.5 * period + return int(abs(wrapped) > width) + + +def apply_shift_noise( + q_shift: list[float], + p_shift: list[float], + *, + sigma_shift: float, + jump_prob: float, + jump_scale: float, + rng: random.Random, +) -> None: + for idx in range(len(q_shift)): + q_shift[idx] += rng.gauss(0.0, sigma_shift) + p_shift[idx] += rng.gauss(0.0, sigma_shift) + if rng.random() < jump_prob: + q_shift[idx] += jump_scale if rng.random() < 0.5 else -jump_scale + if rng.random() < jump_prob: + p_shift[idx] += jump_scale if rng.random() < 0.5 else -jump_scale + + +def z_syndrome_from_q_shifts( + geom: SurfaceGeometry, + q_shift: list[float], + *, + decision_width: float = 0.25 * SQRT_PI, + measurement_error_rate: float = 0.0, + rng: random.Random | None = None, +) -> tuple[list[int], list[float]]: + syndrome: list[int] = [] + analog_values: list[float] = [] + for support in geom.z_supports: + scale = math.sqrt(float(len(support))) if support else 1.0 + value = sum(q_shift[q] for q in support) / scale + bit = digitize_periodic(value, width=decision_width) + if rng is not None and rng.random() < measurement_error_rate: + bit ^= 1 + syndrome.append(bit) + analog_values.append(value) + return syndrome, analog_values + + +def expected_syndrome(geom: SurfaceGeometry, injected_q: int | None) -> list[int]: + return surface_expected_syndrome(geom, injected_q) + + +def syndrome_to_events(syndrome: list[int], *, time_ns: int = 1000) -> list[dict[str, Any]]: + return [{"index": idx, "time_ns": time_ns, "type": "Z"} for idx, bit in enumerate(syndrome) if bit & 1] + + +def digitized_data_bits(q_shift: list[float], *, decision_width: float = 0.25 * SQRT_PI) -> list[int]: + return [digitize_periodic(value, width=decision_width) for value in q_shift] + + +def cbit_values_to_bitstring(cbits_low_to_high: list[int]) -> str: + return "".join(str(int(v) & 1) for v in reversed(cbits_low_to_high)) diff --git a/examples/paper_runs/paper_05/scripts/ingest_css_ldpc_results.py b/examples/paper_runs/paper_05/scripts/ingest_css_ldpc_results.py new file mode 100644 index 0000000..0cb7e70 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/ingest_css_ldpc_results.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Convert raw paper_05 CSS-LDPC syndrome results into decoder request records.""" + +from __future__ import annotations + +import argparse +import csv +import json +from pathlib import Path +from typing import Any + +from css_ldpc_syndrome import expected_syndrome, parse_bitstring, sanitize_label, syndrome_to_events + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--raw-json", action="append", required=True) + return parser.parse_args() + + +def _injected_value(value: Any) -> int | None: + if value is None or value == "": + return None + return int(value) + + +def _iter_shots(experiment: dict[str, Any]) -> list[dict[str, Any]]: + if experiment.get("shot_records"): + return list(experiment["shot_records"]) + shots: list[dict[str, Any]] = [] + for bitstring, count in sorted(experiment.get("counts", {}).items()): + for _ in range(int(count)): + shots.append({"bitstring": bitstring}) + return shots + + +def main() -> int: + args = parse_args() + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + manifest_rows: list[dict[str, Any]] = [] + table_rows: list[dict[str, Any]] = [] + + for raw in args.raw_json: + raw_path = Path(raw) + with raw_path.open("r", encoding="utf-8") as f: + payload = json.load(f) + + n_data = int(payload["n_data"]) + n_checks = int(payload["n_checks"]) + source = str(payload.get("source", "unknown")) + backend = str(payload.get("backend", source)) + dataset = sanitize_label(source if source != "ibm_runtime" else f"ibm_{backend}") + req_path = out_dir / f"decoder_requests_{dataset}.ndjson" + truth_path = out_dir / f"truth_{dataset}.ndjson" + + line_count = 0 + with req_path.open("w", encoding="utf-8") as req_f, truth_path.open("w", encoding="utf-8") as truth_f: + for experiment in payload.get("experiments", []): + circuit_id = str(experiment["circuit_id"]) + injected = _injected_value(experiment.get("injected_x")) + exp_syndrome = expected_syndrome(injected) + for shot_index, shot in enumerate(_iter_shots(experiment)): + bitstring = str(shot["bitstring"]) + syndrome, data_bits = parse_bitstring(bitstring) + rec = { + "code_id": "css_ldpc_steane_z_checks", + "round_index": line_count, + "n_qubits": n_data, + "events": syndrome_to_events(syndrome), + "noise": { + "sigma": 0.0, + "gate_error_rate": float(payload.get("background_data_error_rate", 0.0) or 0.0), + "meas_error_rate": float(payload.get("measurement_error_rate", 0.0) or 0.0), + "idle_error_rate": 0.0, + "loss_prob_by_qubit": [], + }, + "metadata": { + "dataset": dataset, + "source_backend": backend, + "source": source, + "job_id": str(payload.get("job_id", "")), + "generator": "paper05_css_ldpc_syndrome", + "code_family": "css_ldpc", + "code_name": "steane_z_checks", + "circuit_id": circuit_id, + "injected_x": "" if injected is None else str(injected), + "n_data": str(n_data), + "n_checks": str(n_checks), + "rounds": "1", + "bitstring": bitstring, + "measured_syndrome": "".join(str(bit) for bit in syndrome), + "measured_data": "".join(str(bit) for bit in data_bits), + "expected_syndrome": "".join(str(bit) for bit in exp_syndrome), + "shot_index": str(shot_index), + }, + } + req_f.write(json.dumps(rec, separators=(",", ":")) + "\n") + truth_f.write( + json.dumps( + { + "code_id": "css_ldpc_steane_z_checks", + "round_index": line_count, + "dataset": dataset, + "circuit_id": circuit_id, + "injected_x": injected, + "expected_syndrome": exp_syndrome, + "measured_data": data_bits, + "logical_observable": "steane_z_check_single_x", + "logical_truth": 0, + }, + separators=(",", ":"), + ) + + "\n" + ) + table_rows.append( + { + "dataset": dataset, + "source": source, + "backend": backend, + "job_id": str(payload.get("job_id", "")), + "circuit_id": circuit_id, + "injected_x": "" if injected is None else injected, + "shot_index": shot_index, + "bitstring": bitstring, + "measured_syndrome": "".join(str(bit) for bit in syndrome), + "measured_data": "".join(str(bit) for bit in data_bits), + "syndrome_weight": sum(syndrome), + "expected_syndrome": "".join(str(bit) for bit in exp_syndrome), + } + ) + line_count += 1 + + manifest_rows.append( + { + "dataset": dataset, + "source": source, + "backend": backend, + "raw_json": str(raw_path), + "request_file": req_path.name, + "truth_file": truth_path.name, + "request_lines": line_count, + } + ) + + with (out_dir / "ingest_manifest.csv").open("w", encoding="utf-8", newline="") as f: + fields = ["dataset", "source", "backend", "raw_json", "request_file", "truth_file", "request_lines"] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(manifest_rows) + + with (out_dir / "table_ingested_syndromes.csv").open("w", encoding="utf-8", newline="") as f: + fields = [ + "dataset", + "source", + "backend", + "job_id", + "circuit_id", + "injected_x", + "shot_index", + "bitstring", + "measured_syndrome", + "measured_data", + "syndrome_weight", + "expected_syndrome", + ] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(table_rows) + + print(f"Wrote {len(manifest_rows)} CSS-LDPC request streams to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/ingest_gkp_digitized_results.py b/examples/paper_runs/paper_05/scripts/ingest_gkp_digitized_results.py new file mode 100644 index 0000000..7379088 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/ingest_gkp_digitized_results.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Convert digitized-GKP sample records into LiDMaS+ decoder requests.""" + +from __future__ import annotations + +import argparse +import csv +import json +from pathlib import Path +from typing import Any + +from gkp_digitized_syndrome import expected_syndrome, sanitize_label, syndrome_to_events +from surface_syndrome import build_surface_geometry + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--raw-json", action="append", required=True) + return parser.parse_args() + + +def _injected_value(value: Any) -> int | None: + if value is None or value == "": + return None + return int(value) + + +def _iter_shots(experiment: dict[str, Any]) -> list[dict[str, Any]]: + if experiment.get("shot_records"): + return list(experiment["shot_records"]) + shots: list[dict[str, Any]] = [] + for bitstring, count in sorted(experiment.get("counts", {}).items()): + for _ in range(int(count)): + shots.append({"bitstring": bitstring, "measured_syndrome": []}) + return shots + + +def main() -> int: + args = parse_args() + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + manifest_rows: list[dict[str, Any]] = [] + table_rows: list[dict[str, Any]] = [] + + for raw in args.raw_json: + raw_path = Path(raw) + with raw_path.open("r", encoding="utf-8") as f: + payload = json.load(f) + + distance = int(payload["distance"]) + geom = build_surface_geometry(distance) + n_data = int(payload["n_data"]) + n_checks = int(payload["n_checks"]) + source = str(payload.get("source", "digitized_gkp_local")) + backend = str(payload.get("backend", source)) + dataset = sanitize_label(source) + req_path = out_dir / f"decoder_requests_{dataset}.ndjson" + truth_path = out_dir / f"truth_{dataset}.ndjson" + line_count = 0 + + with req_path.open("w", encoding="utf-8") as req_f, truth_path.open("w", encoding="utf-8") as truth_f: + for experiment in payload.get("experiments", []): + circuit_id = str(experiment["circuit_id"]) + injected = _injected_value(experiment.get("injected_q")) + exp_syndrome = expected_syndrome(geom, injected) + for shot_index, shot in enumerate(_iter_shots(experiment)): + syndrome = [int(bit) & 1 for bit in shot.get("measured_syndrome", [])] + if len(syndrome) != n_checks: + compact = str(shot.get("bitstring", "")).replace(" ", "") + if len(compact) >= n_checks + n_data: + c_low_to_high = [int(ch) for ch in reversed(compact)] + syndrome = c_low_to_high[:n_checks] + else: + raise ValueError(f"missing measured syndrome for {circuit_id} shot {shot_index}") + data_bits = [int(bit) & 1 for bit in shot.get("digitized_data", [])] + analog_values = shot.get("analog_z_values", []) + rec = { + "code_id": f"digitized_gkp_surface_d{distance}_z_checks", + "round_index": line_count, + "n_qubits": n_data, + "events": syndrome_to_events(syndrome), + "noise": { + "sigma": float(payload.get("sigma_shift", 0.0) or 0.0), + "gate_error_rate": 0.0, + "meas_error_rate": float(payload.get("measurement_error_rate", 0.0) or 0.0), + "idle_error_rate": 0.0, + "loss_prob_by_qubit": [], + }, + "metadata": { + "dataset": dataset, + "source_backend": backend, + "source": source, + "job_id": str(payload.get("job_id", "")), + "generator": "paper05_digitized_gkp", + "code_family": "digitized_gkp", + "code_name": f"digitized_gkp_surface_d{distance}_z_checks", + "circuit_id": circuit_id, + "injected_q": "" if injected is None else str(injected), + "distance": str(distance), + "n_data": str(n_data), + "n_checks": str(n_checks), + "rounds": str(payload.get("rounds", "1")), + "bitstring": str(shot.get("bitstring", "")), + "measured_syndrome": "".join(str(bit) for bit in syndrome), + "digitized_data": "".join(str(bit) for bit in data_bits), + "expected_syndrome": "".join(str(bit) for bit in exp_syndrome), + "shot_index": str(shot_index), + "sigma_shift_scale": str(payload.get("sigma_shift_scale", "")), + "injected_shift_scale": str(payload.get("injected_shift_scale", "")), + "decision_width_scale": str(payload.get("decision_width_scale", "")), + }, + } + req_f.write(json.dumps(rec, separators=(",", ":")) + "\n") + truth_f.write( + json.dumps( + { + "code_id": f"digitized_gkp_surface_d{distance}_z_checks", + "round_index": line_count, + "dataset": dataset, + "circuit_id": circuit_id, + "injected_q": injected, + "expected_syndrome": exp_syndrome, + "digitized_data": data_bits, + "analog_z_values": analog_values, + "logical_observable": "digitized_gkp_q_shift_z_syndrome", + "logical_truth": 0, + }, + separators=(",", ":"), + ) + + "\n" + ) + table_rows.append( + { + "dataset": dataset, + "source": source, + "backend": backend, + "job_id": str(payload.get("job_id", "")), + "circuit_id": circuit_id, + "injected_q": "" if injected is None else injected, + "shot_index": shot_index, + "bitstring": str(shot.get("bitstring", "")), + "measured_syndrome": "".join(str(bit) for bit in syndrome), + "digitized_data": "".join(str(bit) for bit in data_bits), + "syndrome_weight": sum(syndrome), + "expected_syndrome": "".join(str(bit) for bit in exp_syndrome), + "mean_abs_analog_z_value": ( + sum(abs(float(v)) for v in analog_values) / len(analog_values) if analog_values else "" + ), + } + ) + line_count += 1 + + manifest_rows.append( + { + "dataset": dataset, + "source": source, + "backend": backend, + "raw_json": str(raw_path), + "request_file": req_path.name, + "truth_file": truth_path.name, + "request_lines": line_count, + } + ) + + with (out_dir / "ingest_manifest.csv").open("w", encoding="utf-8", newline="") as f: + fields = ["dataset", "source", "backend", "raw_json", "request_file", "truth_file", "request_lines"] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(manifest_rows) + + with (out_dir / "table_ingested_gkp_syndromes.csv").open("w", encoding="utf-8", newline="") as f: + fields = [ + "dataset", + "source", + "backend", + "job_id", + "circuit_id", + "injected_q", + "shot_index", + "bitstring", + "measured_syndrome", + "digitized_data", + "syndrome_weight", + "expected_syndrome", + "mean_abs_analog_z_value", + ] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(table_rows) + + print(f"Wrote {len(manifest_rows)} digitized-GKP request streams to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/ingest_repetition_results.py b/examples/paper_runs/paper_05/scripts/ingest_repetition_results.py new file mode 100755 index 0000000..f44b64a --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/ingest_repetition_results.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Convert paper_05 raw repetition syndrome results into decoder request records.""" + +from __future__ import annotations + +import argparse +import csv +import json +from pathlib import Path +from typing import Any + +from repetition_syndrome import ( + expected_syndrome, + parse_bitstring, + sanitize_label, + syndrome_to_events, +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--raw-json", action="append", required=True) + return parser.parse_args() + + +def _injected_value(value: Any) -> int | None: + if value is None or value == "": + return None + return int(value) + + +def _iter_shots(experiment: dict[str, Any]) -> list[dict[str, Any]]: + if experiment.get("shot_records"): + return list(experiment["shot_records"]) + shots: list[dict[str, Any]] = [] + for bitstring, count in sorted(experiment.get("counts", {}).items()): + for _ in range(int(count)): + shots.append({"bitstring": bitstring}) + return shots + + +def main() -> int: + args = parse_args() + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + + manifest_rows: list[dict[str, Any]] = [] + table_rows: list[dict[str, Any]] = [] + + for raw in args.raw_json: + raw_path = Path(raw) + with raw_path.open("r", encoding="utf-8") as f: + payload = json.load(f) + + n_data = int(payload["n_data"]) + n_checks = int(payload["n_checks"]) + source = str(payload.get("source", "unknown")) + backend = str(payload.get("backend", source)) + dataset = sanitize_label(source if source != "ibm_runtime" else f"ibm_{backend}") + req_path = out_dir / f"decoder_requests_{dataset}.ndjson" + truth_path = out_dir / f"truth_{dataset}.ndjson" + + line_count = 0 + with req_path.open("w", encoding="utf-8") as req_f, truth_path.open("w", encoding="utf-8") as truth_f: + for experiment in payload.get("experiments", []): + circuit_id = str(experiment["circuit_id"]) + injected = _injected_value(experiment.get("injected_x")) + exp_syndrome = expected_syndrome(n_data, injected) + for shot_index, shot in enumerate(_iter_shots(experiment)): + bitstring = str(shot["bitstring"]) + syndrome, data_bits = parse_bitstring(bitstring, n_data) + events = syndrome_to_events(syndrome) + rec = { + "code_id": f"repetition_n{n_data}", + "round_index": line_count, + "n_qubits": n_data, + "events": events, + "noise": { + "sigma": 0.0, + "gate_error_rate": float(payload.get("background_data_error_rate", 0.0) or 0.0), + "meas_error_rate": float(payload.get("measurement_error_rate", 0.0) or 0.0), + "idle_error_rate": 0.0, + "loss_prob_by_qubit": [], + }, + "metadata": { + "dataset": dataset, + "source_backend": backend, + "source": source, + "job_id": str(payload.get("job_id", "")), + "generator": "paper05_repetition_syndrome", + "circuit_id": circuit_id, + "injected_x": "" if injected is None else str(injected), + "n_data": str(n_data), + "n_checks": str(n_checks), + "rounds": "1", + "bitstring": bitstring, + "measured_syndrome": "".join(str(bit) for bit in syndrome), + "measured_data": "".join(str(bit) for bit in data_bits), + "expected_syndrome": "".join(str(bit) for bit in exp_syndrome), + "shot_index": str(shot_index), + }, + } + req_f.write(json.dumps(rec, separators=(",", ":")) + "\n") + truth_f.write( + json.dumps( + { + "code_id": f"repetition_n{n_data}", + "round_index": line_count, + "dataset": dataset, + "circuit_id": circuit_id, + "injected_x": injected, + "expected_syndrome": exp_syndrome, + "measured_data": data_bits, + "logical_observable": "repetition_majority_bit", + "logical_truth": 0, + }, + separators=(",", ":"), + ) + + "\n" + ) + table_rows.append( + { + "dataset": dataset, + "source": source, + "backend": backend, + "job_id": str(payload.get("job_id", "")), + "circuit_id": circuit_id, + "injected_x": "" if injected is None else injected, + "shot_index": shot_index, + "bitstring": bitstring, + "measured_syndrome": "".join(str(bit) for bit in syndrome), + "measured_data": "".join(str(bit) for bit in data_bits), + "syndrome_weight": sum(syndrome), + "expected_syndrome": "".join(str(bit) for bit in exp_syndrome), + } + ) + line_count += 1 + + manifest_rows.append( + { + "dataset": dataset, + "source": source, + "backend": backend, + "raw_json": str(raw_path), + "request_file": req_path.name, + "truth_file": truth_path.name, + "request_lines": line_count, + } + ) + + with (out_dir / "ingest_manifest.csv").open("w", encoding="utf-8", newline="") as f: + fields = ["dataset", "source", "backend", "raw_json", "request_file", "truth_file", "request_lines"] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(manifest_rows) + + with (out_dir / "table_ingested_syndromes.csv").open("w", encoding="utf-8", newline="") as f: + fields = [ + "dataset", + "source", + "backend", + "job_id", + "circuit_id", + "injected_x", + "shot_index", + "bitstring", + "measured_syndrome", + "measured_data", + "syndrome_weight", + "expected_syndrome", + ] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(table_rows) + + print(f"Wrote {len(manifest_rows)} request streams to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/ingest_surface_results.py b/examples/paper_runs/paper_05/scripts/ingest_surface_results.py new file mode 100644 index 0000000..0ad89b4 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/ingest_surface_results.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Convert raw paper_05 surface-code syndrome results into decoder requests.""" + +from __future__ import annotations + +import argparse +import csv +import json +from pathlib import Path +from typing import Any + +from surface_syndrome import build_surface_geometry, expected_syndrome, parse_bitstring, sanitize_label, syndrome_to_events + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--raw-json", action="append", required=True) + return parser.parse_args() + + +def _injected_value(value: Any) -> int | None: + if value is None or value == "": + return None + return int(value) + + +def _iter_shots(experiment: dict[str, Any]) -> list[dict[str, Any]]: + if experiment.get("shot_records"): + return list(experiment["shot_records"]) + shots: list[dict[str, Any]] = [] + for bitstring, count in sorted(experiment.get("counts", {}).items()): + for _ in range(int(count)): + shots.append({"bitstring": bitstring}) + return shots + + +def main() -> int: + args = parse_args() + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + manifest_rows: list[dict[str, Any]] = [] + table_rows: list[dict[str, Any]] = [] + + for raw in args.raw_json: + raw_path = Path(raw) + with raw_path.open("r", encoding="utf-8") as f: + payload = json.load(f) + + distance = int(payload["distance"]) + geom = build_surface_geometry(distance) + n_data = int(payload["n_data"]) + n_checks = int(payload["n_checks"]) + source = str(payload.get("source", "unknown")) + backend = str(payload.get("backend", source)) + dataset = sanitize_label(source if source != "ibm_runtime" else f"ibm_{backend}") + req_path = out_dir / f"decoder_requests_{dataset}.ndjson" + truth_path = out_dir / f"truth_{dataset}.ndjson" + + line_count = 0 + with req_path.open("w", encoding="utf-8") as req_f, truth_path.open("w", encoding="utf-8") as truth_f: + for experiment in payload.get("experiments", []): + circuit_id = str(experiment["circuit_id"]) + injected = _injected_value(experiment.get("injected_x")) + exp_syndrome = expected_syndrome(geom, injected) + for shot_index, shot in enumerate(_iter_shots(experiment)): + bitstring = str(shot["bitstring"]) + syndrome, data_bits = parse_bitstring(bitstring, geom) + rec = { + "code_id": f"surface_d{distance}_z_checks", + "round_index": line_count, + "n_qubits": n_data, + "events": syndrome_to_events(syndrome), + "noise": { + "sigma": 0.0, + "gate_error_rate": float(payload.get("background_data_error_rate", 0.0) or 0.0), + "meas_error_rate": float(payload.get("measurement_error_rate", 0.0) or 0.0), + "idle_error_rate": 0.0, + "loss_prob_by_qubit": [], + }, + "metadata": { + "dataset": dataset, + "source_backend": backend, + "source": source, + "job_id": str(payload.get("job_id", "")), + "generator": "paper05_surface_z_syndrome", + "code_family": "surface", + "code_name": f"surface_d{distance}_z_checks", + "circuit_id": circuit_id, + "injected_x": "" if injected is None else str(injected), + "distance": str(distance), + "n_data": str(n_data), + "n_checks": str(n_checks), + "rounds": "1", + "bitstring": bitstring, + "measured_syndrome": "".join(str(bit) for bit in syndrome), + "measured_data": "".join(str(bit) for bit in data_bits), + "expected_syndrome": "".join(str(bit) for bit in exp_syndrome), + "shot_index": str(shot_index), + }, + } + req_f.write(json.dumps(rec, separators=(",", ":")) + "\n") + truth_f.write( + json.dumps( + { + "code_id": f"surface_d{distance}_z_checks", + "round_index": line_count, + "dataset": dataset, + "circuit_id": circuit_id, + "injected_x": injected, + "expected_syndrome": exp_syndrome, + "measured_data": data_bits, + "logical_observable": "surface_z_check_single_x", + "logical_truth": 0, + }, + separators=(",", ":"), + ) + + "\n" + ) + table_rows.append( + { + "dataset": dataset, + "source": source, + "backend": backend, + "job_id": str(payload.get("job_id", "")), + "circuit_id": circuit_id, + "injected_x": "" if injected is None else injected, + "shot_index": shot_index, + "bitstring": bitstring, + "measured_syndrome": "".join(str(bit) for bit in syndrome), + "measured_data": "".join(str(bit) for bit in data_bits), + "syndrome_weight": sum(syndrome), + "expected_syndrome": "".join(str(bit) for bit in exp_syndrome), + } + ) + line_count += 1 + + manifest_rows.append( + { + "dataset": dataset, + "source": source, + "backend": backend, + "raw_json": str(raw_path), + "request_file": req_path.name, + "truth_file": truth_path.name, + "request_lines": line_count, + } + ) + + with (out_dir / "ingest_manifest.csv").open("w", encoding="utf-8", newline="") as f: + fields = ["dataset", "source", "backend", "raw_json", "request_file", "truth_file", "request_lines"] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(manifest_rows) + + with (out_dir / "table_ingested_syndromes.csv").open("w", encoding="utf-8", newline="") as f: + fields = [ + "dataset", + "source", + "backend", + "job_id", + "circuit_id", + "injected_x", + "shot_index", + "bitstring", + "measured_syndrome", + "measured_data", + "syndrome_weight", + "expected_syndrome", + ] + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(table_rows) + + print(f"Wrote {len(manifest_rows)} surface-code request streams to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/paper05_decoder_policies.py b/examples/paper_runs/paper_05/scripts/paper05_decoder_policies.py new file mode 100644 index 0000000..eeb210c --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/paper05_decoder_policies.py @@ -0,0 +1,420 @@ +#!/usr/bin/env python3 +"""Decoder-policy helpers for paper_05 syndrome streams.""" + +from __future__ import annotations + +import collections +import functools +import math +from dataclasses import dataclass +from typing import Any + + +POLICIES = ("mwpm", "uf", "bp") + + +@dataclass(frozen=True) +class DecodeResult: + policy: str + correction: tuple[int, ...] + residual: tuple[int, ...] + confidence: float + diagnostics: dict[str, str] + + +def parse_decoders(value: str) -> tuple[str, ...]: + decoders: list[str] = [] + for part in value.split(","): + name = part.strip().lower() + if not name: + continue + if name not in POLICIES: + raise ValueError(f"unknown decoder policy {name!r}; expected one of {', '.join(POLICIES)}") + if name not in decoders: + decoders.append(name) + return tuple(decoders or POLICIES) + + +def syndrome_from_request(rec: dict[str, Any]) -> list[int]: + meta = rec.get("metadata", {}) + n_checks = int(meta.get("n_checks", 0)) + if n_checks <= 0: + n_checks = 1 + max( + (int(event.get("index", -1)) for event in rec.get("events", []) if str(event.get("type", "Z")).upper() == "Z"), + default=-1, + ) + syndrome = [0] * n_checks + for event in rec.get("events", []): + if str(event.get("type", "Z")).upper() != "Z": + continue + idx = int(event.get("index", -1)) + if 0 <= idx < n_checks: + syndrome[idx] ^= 1 + return syndrome + + +def repetition_check_matrix(n_data: int) -> list[list[int]]: + matrix: list[list[int]] = [] + for row in range(max(0, n_data - 1)): + bits = [0] * n_data + bits[row] = 1 + bits[row + 1] = 1 + matrix.append(bits) + return matrix + + +def supports_to_check_matrix(n_data: int, supports: list[list[int]]) -> list[list[int]]: + matrix: list[list[int]] = [] + for support in supports: + bits = [0] * n_data + for idx in support: + if 0 <= idx < n_data: + bits[idx] ^= 1 + matrix.append(bits) + return matrix + + +def correction_syndrome(matrix: list[list[int]], correction: list[int] | tuple[int, ...]) -> list[int]: + selected = set(int(idx) for idx in correction) + syndrome: list[int] = [] + for row in matrix: + parity = 0 + for idx, enabled in enumerate(row): + if enabled & 1 and idx in selected: + parity ^= 1 + syndrome.append(parity) + return syndrome + + +def residual_syndrome( + matrix: list[list[int]], + syndrome: list[int] | tuple[int, ...], + correction: list[int] | tuple[int, ...], +) -> list[int]: + produced = correction_syndrome(matrix, correction) + return [((int(a) & 1) ^ (int(b) & 1)) for a, b in zip(syndrome, produced)] + + +def matrix_key(matrix: list[list[int]]) -> tuple[tuple[int, ...], ...]: + return tuple(tuple(int(bit) & 1 for bit in row) for row in matrix) + + +def syndrome_to_int(syndrome: list[int] | tuple[int, ...]) -> int: + value = 0 + for idx, bit in enumerate(syndrome): + if int(bit) & 1: + value |= 1 << idx + return value + + +def int_to_syndrome(value: int, n_checks: int) -> tuple[int, ...]: + return tuple((value >> idx) & 1 for idx in range(n_checks)) + + +def column_masks(key: tuple[tuple[int, ...], ...]) -> tuple[int, ...]: + if not key: + return () + n_data = len(key[0]) + masks: list[int] = [] + for q in range(n_data): + mask = 0 + for check_idx, row in enumerate(key): + if row[q] & 1: + mask |= 1 << check_idx + masks.append(mask) + return tuple(masks) + + +def _extend_tuple(current: tuple[int, ...], q: int) -> tuple[int, ...] | None: + if q in current: + return None + return tuple(sorted((*current, q))) + + +@functools.cache +def _decoder_table(key: tuple[tuple[int, ...], ...]) -> tuple[tuple[int, ...] | None, ...]: + n_checks = len(key) + n_states = 1 << n_checks + masks = column_masks(key) + corrections: list[tuple[int, ...] | None] = [None] * n_states + corrections[0] = () + queue: collections.deque[int] = collections.deque([0]) + + while queue: + state = queue.popleft() + current = corrections[state] + if current is None: + continue + for q, mask in enumerate(masks): + next_state = state ^ mask + candidate = _extend_tuple(current, q) + if candidate is None: + continue + existing = corrections[next_state] + if existing is None or (len(candidate), candidate) < (len(existing), existing): + corrections[next_state] = candidate + queue.append(next_state) + + return tuple(corrections) + + +def decode_min_weight( + matrix: list[list[int]], + syndrome: list[int] | tuple[int, ...], + *, + allowed_columns: tuple[int, ...] | None = None, +) -> tuple[int, ...] | None: + key = matrix_key(matrix) + target = syndrome_to_int(tuple(int(bit) & 1 for bit in syndrome)) + if allowed_columns is None: + table = _decoder_table(key) + if target >= len(table): + return None + return table[target] + + n_checks = len(key) + n_states = 1 << n_checks + masks = column_masks(key) + corrections: list[tuple[int, ...] | None] = [None] * n_states + corrections[0] = () + queue: collections.deque[int] = collections.deque([0]) + allowed = tuple(sorted({idx for idx in allowed_columns if 0 <= idx < len(masks)})) + + while queue: + state = queue.popleft() + current = corrections[state] + if current is None: + continue + for q in allowed: + next_state = state ^ masks[q] + candidate = _extend_tuple(current, q) + if candidate is None: + continue + existing = corrections[next_state] + if existing is None or (len(candidate), candidate) < (len(existing), existing): + corrections[next_state] = candidate + queue.append(next_state) + + return corrections[target] + + +def _decode_mwpm(matrix: list[list[int]], syndrome: list[int]) -> DecodeResult: + correction = decode_min_weight(matrix, syndrome) or () + residual = tuple(residual_syndrome(matrix, syndrome, correction)) + return DecodeResult( + policy="mwpm", + correction=tuple(correction), + residual=residual, + confidence=1.0 if not any(residual) else 0.0, + diagnostics={ + "policy": "exact_minimum_weight_binary", + "fallback": "0", + }, + ) + + +def _incident_maps(matrix: list[list[int]]) -> tuple[list[list[int]], list[list[int]]]: + if not matrix: + return ([], []) + n_data = len(matrix[0]) + check_to_vars: list[list[int]] = [[] for _ in matrix] + var_to_checks: list[list[int]] = [[] for _ in range(n_data)] + for check_idx, row in enumerate(matrix): + for q, enabled in enumerate(row): + if enabled & 1: + check_to_vars[check_idx].append(q) + var_to_checks[q].append(check_idx) + return check_to_vars, var_to_checks + + +def _decode_uf(matrix: list[list[int]], syndrome: list[int]) -> DecodeResult: + target = [int(bit) & 1 for bit in syndrome] + if not any(target): + return DecodeResult( + policy="uf", + correction=(), + residual=tuple(0 for _ in target), + confidence=1.0, + diagnostics={ + "policy": "union_find_erasure_peeling", + "uf_growth_rounds": "0", + "uf_erasure_size": "0", + "uf_greedy_flips": "0", + "fallback": "0", + }, + ) + + check_to_vars, var_to_checks = _incident_maps(matrix) + active_checks = {idx for idx, bit in enumerate(target) if bit} + erasure: set[int] = set() + for check_idx in active_checks: + if 0 <= check_idx < len(check_to_vars): + erasure.update(check_to_vars[check_idx]) + + residual = target[:] + correction_set: set[int] = set() + greedy_flips = 0 + max_steps = max(1, len(erasure)) + + for _ in range(max_steps): + if not any(residual): + break + best_q = -1 + best_gain = 0 + best_unsatisfied = 0 + for q in sorted(erasure): + if q in correction_set: + continue + checks_for_var = var_to_checks[q] + unsatisfied = sum(residual[check_idx] for check_idx in checks_for_var) + satisfied = len(checks_for_var) - unsatisfied + gain = unsatisfied - satisfied + if (gain, unsatisfied, -q) > (best_gain, best_unsatisfied, -best_q): + best_q = q + best_gain = gain + best_unsatisfied = unsatisfied + if best_q < 0 or best_gain <= 0: + break + correction_set.add(best_q) + greedy_flips += 1 + for check_idx in var_to_checks[best_q]: + residual[check_idx] ^= 1 + + fallback = "0" + if any(residual): + closure = decode_min_weight(matrix, residual) or () + correction_set = correction_set.symmetric_difference(closure) + fallback = "1" + + correction = tuple(sorted(correction_set)) + residual_tuple = tuple(residual_syndrome(matrix, target, correction)) + return DecodeResult( + policy="uf", + correction=tuple(correction), + residual=residual_tuple, + confidence=1.0 if not any(residual_tuple) else 0.0, + diagnostics={ + "policy": "union_find_erasure_peeling", + "uf_growth_rounds": "1", + "uf_erasure_size": str(len(erasure)), + "uf_greedy_flips": str(greedy_flips), + "fallback": fallback, + }, + ) + + +def _decode_bp(matrix: list[list[int]], syndrome: list[int], *, prior_p: float = 0.08, max_iter: int = 12) -> DecodeResult: + target = [int(bit) & 1 for bit in syndrome] + if not matrix: + return DecodeResult( + policy="bp", + correction=(), + residual=tuple(target), + confidence=0.0, + diagnostics={"policy": "belief_propagation_hard_decision_min_sum", "bp_converged": "0", "fallback": "0"}, + ) + + n_data = len(matrix[0]) + check_to_vars, var_to_checks = _incident_maps(matrix) + best_bits = [0] * n_data + best_residual = target[:] + best_score = (sum(best_residual), sum(best_bits), tuple(best_bits)) + converged = False + iterations = 0 + bits = [0] * n_data + residual = target[:] + + if not any(residual): + converged = True + + for iterations in range(1, max_iter + 1): + if not any(residual): + converged = True + break + + best_q = -1 + best_gain = 0 + best_unsatisfied = 0 + for q, checks_for_var in enumerate(var_to_checks): + if not checks_for_var: + continue + unsatisfied = sum(residual[check_idx] for check_idx in checks_for_var) + satisfied = len(checks_for_var) - unsatisfied + gain = unsatisfied - satisfied + if (gain, unsatisfied, -q) > (best_gain, best_unsatisfied, -best_q): + best_q = q + best_gain = gain + best_unsatisfied = unsatisfied + + if best_q < 0 or best_gain <= 0: + break + + bits[best_q] ^= 1 + for check_idx in var_to_checks[best_q]: + residual[check_idx] ^= 1 + + score = (sum(residual), sum(bits), tuple(bits)) + if score < best_score: + best_score = score + best_bits = bits[:] + best_residual = residual[:] + if not any(residual): + converged = True + best_bits = bits[:] + best_residual = residual[:] + break + + correction = tuple(idx for idx, bit in enumerate(best_bits) if bit) + closure_weight = 0 + fallback = "0" + if any(best_residual): + closure = decode_min_weight(matrix, best_residual) or () + closure_weight = len(closure) + correction = tuple(sorted(set(correction).symmetric_difference(closure))) + fallback = "1" + + residual = tuple(residual_syndrome(matrix, target, correction)) + return DecodeResult( + policy="bp", + correction=correction, + residual=residual, + confidence=1.0 if converged else (0.8 if not any(residual) else 0.0), + diagnostics={ + "policy": "belief_propagation_hard_decision_min_sum", + "bp_converged": "1" if converged else "0", + "bp_iterations": str(iterations), + "bp_best_residual_weight": str(sum(best_residual)), + "bp_closure_weight": str(closure_weight), + "fallback": fallback, + }, + ) + + +def decode_policy( + policy: str, + matrix: list[list[int]], + syndrome: list[int], + *, + prior_p: float = 0.08, +) -> DecodeResult: + key = matrix_key(matrix) + syndrome_key = tuple(int(bit) & 1 for bit in syndrome) + return _decode_policy_cached(policy, key, syndrome_key, round(float(prior_p), 12)) + + +@functools.cache +def _decode_policy_cached( + policy: str, + key: tuple[tuple[int, ...], ...], + syndrome: tuple[int, ...], + prior_p: float, +) -> DecodeResult: + matrix = [list(row) for row in key] + syndrome_bits = list(syndrome) + if policy == "mwpm": + return _decode_mwpm(matrix, syndrome_bits) + if policy == "uf": + return _decode_uf(matrix, syndrome_bits) + if policy == "bp": + return _decode_bp(matrix, syndrome_bits, prior_p=prior_p) + raise ValueError(f"unknown decoder policy: {policy}") diff --git a/examples/paper_runs/paper_05/scripts/paper05_plot_style.py b/examples/paper_runs/paper_05/scripts/paper05_plot_style.py new file mode 100644 index 0000000..bf709e1 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/paper05_plot_style.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Shared journal-style plotting helpers for paper_05.""" + +from __future__ import annotations + +import shutil +from pathlib import Path +from typing import Any + + +HEATMAP_CMAP = "viridis" +GRID_COLOR = "#D1D5DB" +AXIS_COLOR = "#374151" +TEXT_COLOR = "#111827" +MUTED_TEXT = "#4B5563" +EXACT_COLOR = "#2563EB" +CONTAINS_COLOR = "#059669" +LOCAL_COLOR = "#D97706" +IBM_COLOR = "#334155" +GKP_COLOR = "#7C3AED" + + +def apply_journal_style() -> None: + import matplotlib as mpl # type: ignore + import matplotlib.pyplot as plt # type: ignore + + plt.style.use("ggplot") + mpl.rcParams.update( + { + "figure.dpi": 180, + "savefig.dpi": 600, + "savefig.facecolor": "white", + "savefig.edgecolor": "white", + "figure.facecolor": "white", + "axes.facecolor": "white", + "axes.edgecolor": AXIS_COLOR, + "axes.labelcolor": TEXT_COLOR, + "axes.titlecolor": TEXT_COLOR, + "axes.linewidth": 0.8, + "axes.grid": True, + "axes.axisbelow": True, + "grid.color": GRID_COLOR, + "grid.linewidth": 0.45, + "grid.alpha": 0.75, + "xtick.color": MUTED_TEXT, + "ytick.color": MUTED_TEXT, + "xtick.major.width": 0.7, + "ytick.major.width": 0.7, + "font.family": "DejaVu Sans", + "font.size": 8.2, + "axes.labelsize": 8.4, + "axes.titlesize": 9.0, + "xtick.labelsize": 7.4, + "ytick.labelsize": 7.2, + "legend.fontsize": 7.0, + "legend.title_fontsize": 7.2, + "lines.linewidth": 1.45, + "lines.markersize": 4.2, + "patch.linewidth": 0.5, + "pdf.fonttype": 42, + "ps.fonttype": 42, + "svg.fonttype": "none", + } + ) + + +def half_panel_size(kind: str, n_rows: int = 0) -> tuple[float, float]: + if kind == "heatmap": + return (3.55, max(2.25, min(4.25, 0.205 * max(1, n_rows) + 0.95))) + if kind == "rate": + return (3.55, 2.55) + if kind == "bar": + return (3.55, 2.35) + return (3.55, 2.55) + + +def horizontal_heatmap_size(n_columns: int, n_rows: int) -> tuple[float, float]: + width = max(4.25, min(6.2, 0.23 * max(1, n_columns) + 1.55)) + height = max(1.85, min(3.05, 0.10 * max(1, n_rows) + 1.25)) + return (width, height) + + +def short_dataset_label(dataset: str, backend: str = "") -> str: + if dataset == "local_simulator": + return "local" + if dataset == "digitized_gkp_pennylane": + return "PennyLane" + if dataset == "digitized_gkp_local": + return "local GKP" + name = backend or dataset + if name.startswith("ibm_"): + return "IBM " + name.removeprefix("ibm_") + if dataset.startswith("ibm_ibm_"): + return "IBM " + dataset.removeprefix("ibm_ibm_") + return dataset.replace("_", " ") + + +def compact_source_label(dataset: str, circuit: str, backend: str = "") -> str: + source = "L" if dataset == "local_simulator" else "I" + if dataset.startswith("digitized_gkp"): + source = "PL" + if dataset.startswith("ibm_") or backend.startswith("ibm_"): + source = "I" + if circuit == "clean": + target = "clean" + elif circuit.startswith("x_data_"): + target = "X" + circuit.removeprefix("x_data_") + elif circuit.startswith("q_shift_data_"): + target = "q" + circuit.removeprefix("q_shift_data_") + else: + target = circuit.replace("_", " ") + return f"{source}-{target}" + + +def metric_color(metric_name: str) -> str: + lowered = metric_name.lower() + if "contain" in lowered: + return CONTAINS_COLOR + if "exact" in lowered or "localization" in lowered: + return EXACT_COLOR + return AXIS_COLOR + + +def source_linestyle(dataset: str) -> str: + return "-" if dataset.startswith("ibm_") or dataset.startswith("digitized_gkp") else (0, (4, 2)) + + +def source_marker(dataset: str, fallback: str) -> str: + if dataset == "local_simulator": + return "D" + return fallback + + +def style_heatmap_axis(ax: Any) -> None: + ax.grid(False) + for spine in ax.spines.values(): + spine.set_visible(True) + spine.set_linewidth(0.65) + spine.set_color(AXIS_COLOR) + ax.tick_params(axis="both", length=2.4, width=0.65, color=AXIS_COLOR) + + +def style_rate_axis(ax: Any, *, ymin: float, ymax: float = 1.02) -> None: + ax.set_ylim(ymin, ymax) + ax.set_yticks([tick for tick in (0.0, 0.25, 0.5, 0.75, 1.0) if ymin <= tick <= ymax]) + ax.grid(axis="y") + ax.grid(axis="x", visible=False) + for side in ("top", "right"): + ax.spines[side].set_visible(False) + for side in ("left", "bottom"): + ax.spines[side].set_color(AXIS_COLOR) + ax.spines[side].set_linewidth(0.75) + ax.tick_params(axis="both", length=2.4, width=0.65, color=AXIS_COLOR) + + +def style_bar_axis(ax: Any) -> None: + ax.grid(axis="y") + ax.grid(axis="x", visible=False) + for side in ("top", "right"): + ax.spines[side].set_visible(False) + for side in ("left", "bottom"): + ax.spines[side].set_color(AXIS_COLOR) + ax.spines[side].set_linewidth(0.75) + + +def save_journal_figure(fig: Any, prefix: Path, manuscript_dir: Path | None) -> None: + for ext in (".pdf", ".png", ".svg"): + out = prefix.with_suffix(ext) + kwargs: dict[str, Any] = { + "bbox_inches": "tight", + "facecolor": "white", + "edgecolor": "white", + "pad_inches": 0.04, + } + if ext == ".png": + kwargs["dpi"] = 600 + fig.savefig(out, **kwargs) + if manuscript_dir is not None: + manuscript_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(out, manuscript_dir / out.name) diff --git a/examples/paper_runs/paper_05/scripts/render_gkp_digitized_figures.py b/examples/paper_runs/paper_05/scripts/render_gkp_digitized_figures.py new file mode 100644 index 0000000..de0ce5e --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/render_gkp_digitized_figures.py @@ -0,0 +1,438 @@ +#!/usr/bin/env python3 +"""Render conceptual digitized-GKP encoding figures for paper_05.""" + +from __future__ import annotations + +import argparse +import math +import shutil +from pathlib import Path +from typing import Any + +import numpy as np + +from gkp_digitized_syndrome import SQRT_PI + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--manuscript-dir") + return parser.parse_args() + + +def save_fig(fig: Any, prefix: Path, manuscript_dir: Path | None) -> None: + for ext in (".pdf", ".png", ".svg"): + out = prefix.with_suffix(ext) + fig.savefig(out, bbox_inches="tight") + if manuscript_dir is not None: + manuscript_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(out, manuscript_dir / out.name) + + +def add_panel_label(ax: Any, label: str) -> None: + ax.text( + 0.5, + -0.34, + label, + transform=ax.transAxes, + ha="center", + va="top", + fontsize=8.0, + color="#111827", + clip_on=False, + ) + + +def draw_phase_space(ax: Any) -> None: + from matplotlib.patches import FancyArrowPatch, Rectangle + + xs = np.arange(-2, 3) * SQRT_PI + ys = np.arange(-2, 3) * SQRT_PI + for x in xs: + ax.axvline(x, color="#E5E7EB", linewidth=0.8, zorder=0) + for y in ys: + ax.axhline(y, color="#E5E7EB", linewidth=0.8, zorder=0) + xx, yy = np.meshgrid(xs, ys) + ax.scatter(xx.ravel(), yy.ravel(), s=32, color="#2563EB", edgecolor="white", linewidth=0.6, zorder=2) + cell = 0.25 * SQRT_PI + ax.add_patch( + Rectangle( + (-cell, -cell), + 2 * cell, + 2 * cell, + facecolor="#D1FAE5", + alpha=0.45, + edgecolor="#059669", + linewidth=1.8, + linestyle="-", + ) + ) + ax.add_patch( + FancyArrowPatch( + (0.0, 0.0), + (0.58 * SQRT_PI, 0.0), + arrowstyle="-|>", + mutation_scale=10, + linewidth=1.2, + color="#DC2626", + zorder=3, + ) + ) + ax.text(0.22 * SQRT_PI, 0.18 * SQRT_PI, r"$\Delta q$", color="#DC2626", fontsize=8.4) + ax.set_xlim(-2.25 * SQRT_PI, 2.25 * SQRT_PI) + ax.set_ylim(-2.25 * SQRT_PI, 2.25 * SQRT_PI) + ax.set_aspect("equal") + ax.set_xlabel(r"$q$ quadrature") + ax.set_ylabel(r"$p$ quadrature") + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + + +def draw_digitized_circuit_schematic(fig: Any, out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + from matplotlib.patches import Circle, FancyArrowPatch, FancyBboxPatch, Rectangle + + ax = fig.add_subplot(111) + ax.set_xlim(0.0, 18.0) + ax.set_ylim(0.0, 7.9) + ax.axis("off") + + mode_rows = [ + ("q1", 6.35), + ("q5", 5.80), + ("q10", 5.25), + ("q14", 4.70), + ("q17", 4.15), + ("q22", 3.60), + ("q32", 3.05), + ("q37", 2.50), + ] + check_rows = [("Z0", 1.55), ("Z1...Z14", 1.05), ("Z15", 0.55)] + columns = { + "source": 1.70, + "inject": 2.95, + "noise": 4.15, + "measure": 5.35, + "bin": 6.55, + "checks": 7.85, + "request": 9.65, + "decoder": 11.35, + "out": 12.65, + } + + colors = { + "wire": "#6B7280", + "grid": "#EEF2F7", + "text": "#111827", + "muted": "#64748B", + "source": "#EAF3FF", + "source_edge": "#4EA3F1", + "inject": "#FFF1F2", + "inject_edge": "#E0527D", + "noise": "#F8FAFC", + "noise_edge": "#94A3B8", + "readout": "#ECFDF5", + "readout_edge": "#10B981", + "bin": "#F0FDFA", + "bin_edge": "#14B8A6", + "request": "#F5F3FF", + "request_edge": "#7C3AED", + "out": "#FFF7ED", + "out_edge": "#F59E0B", + "measure_gray": "#D1D5DB", + "measure_gray_edge": "#6B7280", + } + + def stage(x0: float, x1: float, label: str) -> None: + ax.add_patch( + Rectangle( + (x0, 1.95), + x1 - x0, + 4.75, + facecolor="#F8FAFC", + edgecolor="none", + alpha=0.18, + zorder=0, + ) + ) + ax.text((x0 + x1) / 2.0, 6.93, label, ha="center", va="bottom", fontsize=7.1, color=colors["muted"]) + + def rounded( + x: float, + y: float, + w: float, + h: float, + text: str, + *, + face: str, + edge: str, + fs: float = 7.6, + weight: str = "normal", + text_color: str = "#111827", + lw: float = 0.9, + ) -> None: + ax.add_patch( + FancyBboxPatch( + (x - w / 2.0, y - h / 2.0), + w, + h, + boxstyle="round,pad=0.015,rounding_size=0.035", + facecolor=face, + edgecolor=edge, + linewidth=lw, + zorder=4, + ) + ) + ax.text(x, y, text, ha="center", va="center", fontsize=fs, weight=weight, color=text_color, zorder=5) + + def arrow(x0: float, y0: float, x1: float, y1: float, *, color: str = "#64748B", lw: float = 0.9) -> None: + ax.add_patch( + FancyArrowPatch( + (x0, y0), + (x1, y1), + arrowstyle="-|>", + mutation_scale=8.5, + linewidth=lw, + color=color, + shrinkA=1.0, + shrinkB=1.0, + zorder=2, + ) + ) + + def meter(x: float, y: float) -> None: + rounded(x, y, 0.56, 0.30, r"$M_q$", face=colors["readout"], edge=colors["readout_edge"], fs=7.2) + ax.plot([x - 0.17, x, x + 0.17], [y - 0.02, y + 0.09, y - 0.02], color=colors["readout_edge"], linewidth=0.75, zorder=6) + + def measure_symbol(x: float, y: float, scale: float = 1.0) -> None: + rounded( + x, + y, + 0.34 * scale, + 0.28 * scale, + r"$M$", + face=colors["measure_gray"], + edge=colors["measure_gray_edge"], + fs=6.6 * scale, + ) + ax.plot( + [x - 0.11 * scale, x + 0.02 * scale, x + 0.13 * scale], + [y - 0.02 * scale, y + 0.08 * scale, y - 0.02 * scale], + color="#374151", + linewidth=0.65 * scale, + zorder=7, + ) + + for x0, x1, label in [ + (1.18, 2.25, "CV source"), + (2.45, 3.40, r"$q$ injection"), + (3.68, 4.62, "shift noise"), + (4.88, 6.98, "readout and binning"), + (7.24, 8.52, "outer checks"), + (9.02, 12.95, "request and correction"), + ]: + stage(x0, x1, label) + + ax.text(0.40, 6.90, "mode", fontsize=7.2, color=colors["muted"], ha="left") + ax.text(0.40, 1.86, "check", fontsize=7.2, color=colors["muted"], ha="left") + + for label, y in mode_rows: + ax.text(0.62, y, label, ha="right", va="center", fontsize=7.8, color=colors["text"]) + ax.plot([0.78, 7.08], [y, y], color=colors["wire"], linewidth=0.85, zorder=1) + rounded(columns["source"], y, 0.62, 0.28, r"$G_{\rm CV}$", face=colors["source"], edge=colors["source_edge"], fs=7.0) + rounded(columns["inject"], y, 0.58, 0.28, r"$D_q$", face=colors["inject"], edge=colors["inject_edge"], fs=7.3) + rounded(columns["noise"], y, 0.58, 0.28, r"$N_\sigma$", face=colors["noise"], edge=colors["noise_edge"], fs=7.2) + meter(columns["measure"], y) + rounded(columns["bin"], y, 0.50, 0.28, r"$b$", face=colors["bin"], edge=colors["bin_edge"], fs=7.4, weight="bold") + arrow(columns["bin"] + 0.31, y, 7.06, y, color=colors["bin_edge"], lw=0.70) + + ax.plot([7.08, 7.08], [2.25, 6.60], color=colors["bin_edge"], linewidth=1.35, zorder=2) + for label, y in check_rows: + ax.text(0.62, y, label, ha="right", va="center", fontsize=7.5, color=colors["text"]) + ax.plot([0.78, 8.44], [y, y], color="#D1D5DB", linewidth=0.78, zorder=1) + arrow(7.08, 4.35, columns["checks"] - 0.38, y, color=colors["bin_edge"], lw=0.82) + rounded(columns["checks"], y, 0.66, 0.30, r"$Z_j$", face="#FFFFFF", edge=colors["bin_edge"], fs=7.1) + arrow(columns["checks"] + 0.40, y, columns["request"] - 0.85, y, color="#0F766E", lw=0.92) + + ax.add_patch( + FancyBboxPatch( + (7.33, 0.28), + 1.15, + 1.62, + boxstyle="round,pad=0.02,rounding_size=0.06", + facecolor="none", + edgecolor="#99F6E4", + linewidth=0.8, + zorder=1, + ) + ) + + rounded( + columns["request"], + 1.05, + 1.28, + 0.96, + "LiDMaS+\nrequest\nschema", + face=colors["request"], + edge=colors["request_edge"], + fs=7.3, + weight="bold", + ) + rounded( + columns["decoder"], + 1.05, + 1.32, + 0.96, + "minimum-\nweight\ncorrection", + face="#FFFFFF", + edge=colors["request_edge"], + fs=7.2, + ) + rounded( + columns["out"], + 1.05, + 0.82, + 0.74, + r"$C$", + face=colors["out"], + edge=colors["out_edge"], + fs=10.5, + weight="bold", + ) + arrow(columns["request"] + 0.72, 1.05, columns["decoder"] - 0.76, 1.05, color=colors["request_edge"], lw=1.15) + arrow(columns["decoder"] + 0.76, 1.05, columns["out"] - 0.48, 1.05, color=colors["out_edge"], lw=1.0) + + for x, y, c in [(12.24, 1.47, "#0EA5E9"), (12.34, 1.56, "#E0527D"), (12.45, 1.45, "#14B8A6")]: + ax.add_patch(Circle((x, y), 0.035, facecolor=c, edgecolor="white", linewidth=0.3, zorder=7)) + + inset_x0 = 14.0 + ax.plot([inset_x0, inset_x0], [0.85, 6.85], color="#94A3B8", linewidth=0.8, linestyle=(0, (5, 4)), zorder=1) + ax.text( + inset_x0 + 0.12, + 7.00, + "measurement-bit ordering inset", + fontsize=6.8, + color=colors["muted"], + ha="left", + va="bottom", + ) + for idx, number in enumerate([8, 1, 7, 0, 6, 2, 9, 3]): + x = inset_x0 + 0.58 + idx * 0.46 + y = 6.55 - idx * 0.42 + ax.plot([inset_x0, x + 0.34], [y, y], color="#CBD5E1", linewidth=0.65, linestyle=(0, (3, 3)), zorder=0) + measure_symbol(x, y, scale=0.92) + ax.plot([x, x], [y - 0.18, 0.80], color="#6B7280", linewidth=0.65, linestyle=(0, (2, 3)), zorder=1) + arrow(x, 0.80, x, 0.58, color="#6B7280", lw=0.65) + ax.text(x, 0.47, str(number), fontsize=6.2, color="#374151", ha="center", va="top") + + ax.text( + 8.85, + 0.20, + "Binary events preserve the same outer Z-check ordering as the surface-code branch.", + fontsize=7.0, + color=colors["muted"], + ha="center", + ) + + save_fig(fig, out_dir / "figure_gkp_digitized_circuit_schematic", manuscript_dir) + plt.close(fig) + + +def main() -> int: + args = parse_args() + out_dir = Path(args.out_dir) + manuscript_dir = Path(args.manuscript_dir) if args.manuscript_dir else None + out_dir.mkdir(parents=True, exist_ok=True) + if manuscript_dir is not None: + manuscript_dir.mkdir(parents=True, exist_ok=True) + + import matplotlib # type: ignore + + matplotlib.use("Agg", force=True) + import matplotlib.pyplot as plt # type: ignore + from matplotlib.patches import FancyArrowPatch, FancyBboxPatch + + fig, axes = plt.subplots(1, 3, figsize=(7.8, 2.85), gridspec_kw={"width_ratios": [1.12, 1.0, 1.22]}) + fig.subplots_adjust(left=0.08, right=0.98, top=0.93, bottom=0.32, wspace=0.36) + draw_phase_space(axes[0]) + + axes[1].axis("off") + axes[1].set_xlim(0.0, 1.0) + axes[1].set_ylim(0.0, 1.0) + + def flow_box(y: float, text: str, *, face: str, edge: str) -> None: + axes[1].add_patch( + FancyBboxPatch( + (0.10, y - 0.095), + 0.78, + 0.19, + boxstyle="round,pad=0.018,rounding_size=0.025", + facecolor=face, + edgecolor=edge, + linewidth=0.8, + zorder=2, + ) + ) + axes[1].text(0.49, y, text, ha="center", va="center", fontsize=8.0, color="#111827", zorder=3) + + flow_box(0.76, r"$y_j=|S_j|^{-1/2}\sum_{i\in S_j}\Delta q_i$", face="#F8FAFC", edge="#94A3B8") + flow_box(0.49, r"$\tilde y_j=y_j\ {\rm mod}\ \sqrt{\pi}$", face="#EFF6FF", edge="#60A5FA") + flow_box(0.22, r"$b_j=\mathbf{1}(|\tilde y_j|>0.25\sqrt{\pi})$", face="#ECFDF5", edge="#10B981") + for y0, y1 in [(0.655, 0.595), (0.385, 0.325)]: + axes[1].add_patch( + FancyArrowPatch( + (0.49, y0), + (0.49, y1), + transform=axes[1].transAxes, + arrowstyle="-|>", + mutation_scale=11, + linewidth=1.1, + color="#475569", + ) + ) + + ax = axes[2] + ax.axis("off") + ax.set_xlim(0.0, 1.0) + ax.set_ylim(0.0, 1.0) + data_x = np.linspace(0.12, 0.88, 8) + check_x = np.array([0.25, 0.50, 0.75]) + data_y = 0.72 + check_y = 0.34 + edge_sets = { + 0: [0, 1, 3, 4], + 1: [1, 2, 4, 6], + 2: [3, 5, 6, 7], + } + for check_index, data_indices in edge_sets.items(): + for data_index in data_indices: + ax.plot( + [check_x[check_index], data_x[data_index]], + [check_y, data_y], + color="#CBD5E1", + linewidth=0.9, + zorder=1, + ) + ax.scatter(data_x, np.full_like(data_x, data_y), s=46, color="#2563EB", edgecolor="white", linewidth=0.7, zorder=3) + ax.scatter(check_x, np.full_like(check_x, check_y), s=62, marker="s", color="#059669", edgecolor="white", linewidth=0.8, zorder=4) + for x, label in zip(data_x, ["1", "5", "10", "14", "17", "22", "32", "37"]): + ax.text(x, data_y + 0.095, label, ha="center", va="center", fontsize=6.6, color="#374151") + for x, label in zip(check_x, [r"$Z_0$", r"$Z_j$", r"$Z_{15}$"]): + ax.text(x, check_y - 0.12, label, ha="center", va="center", fontsize=7.4, color="#111827") + ax.text(0.50, 0.08, "binary bits inherit the surface-code Z-check order", ha="center", va="center", fontsize=7.3, color="#64748B") + + add_panel_label(axes[0], "(a) GKP lattice") + add_panel_label(axes[1], "(b) Analog-to-binary map") + add_panel_label(axes[2], "(c) Outer checks") + save_fig(fig, out_dir / "figure_gkp_digitized_encoding_schematic", manuscript_dir) + plt.close(fig) + + fig = plt.figure(figsize=(10.8, 5.3)) + draw_digitized_circuit_schematic(fig, out_dir, manuscript_dir) + print(f"Wrote digitized-GKP figures to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/render_supplemental_figures.py b/examples/paper_runs/paper_05/scripts/render_supplemental_figures.py new file mode 100644 index 0000000..d896497 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/render_supplemental_figures.py @@ -0,0 +1,402 @@ +#!/usr/bin/env python3 +"""Render candidate supplemental figures for paper_05.""" + +from __future__ import annotations + +import argparse +import collections +import csv +import json +import math +from pathlib import Path +from typing import Any + +import numpy as np + +from paper05_plot_style import ( + CONTAINS_COLOR, + EXACT_COLOR, + GKP_COLOR, + HEATMAP_CMAP, + IBM_COLOR, + LOCAL_COLOR, + apply_journal_style, + save_journal_figure, + style_bar_axis, + style_heatmap_axis, + style_rate_axis, +) + + +DECODER_ORDER = ["mwpm", "uf", "bp"] +DECODER_LABELS = {"mwpm": "MWPM", "uf": "UF", "bp": "BP"} + + +def add_panel_label(ax: Any, label: str, *, y: float = -0.23) -> None: + ax.text( + 0.5, + y, + label, + transform=ax.transAxes, + ha="center", + va="top", + fontsize=8.1, + color="#111827", + clip_on=False, + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--paper-dir", default=".") + parser.add_argument("--out-dir", required=True) + parser.add_argument("--manuscript-dir") + return parser.parse_args() + + +def read_csv_rows(path: Path) -> list[dict[str, str]]: + with path.open("r", encoding="utf-8", newline="") as fobj: + return list(csv.DictReader(fobj)) + + +def f(value: Any) -> float: + try: + return float(value) + except (TypeError, ValueError): + return float("nan") + + +def parse_indices(value: str) -> list[int]: + if not value.strip(): + return [] + return [int(part) for part in value.split() if part.strip()] + + +def target_sort(value: str) -> int: + return int(value) + + +def select_injected(rows: list[dict[str, str]], *, dataset: str, decoder: str, target_field: str) -> list[dict[str, str]]: + return [ + row + for row in rows + if row.get("dataset") == dataset and row.get("decoder") == decoder and row.get(target_field, "") != "" + ] + + +def correction_inclusion_matrix( + rows: list[dict[str, str]], *, dataset: str, decoder: str, target_field: str, n_data: int +) -> tuple[list[int], np.ndarray]: + selected = select_injected(rows, dataset=dataset, decoder=decoder, target_field=target_field) + targets = sorted({target_sort(row[target_field]) for row in selected}) + grouped: dict[int, list[dict[str, str]]] = collections.defaultdict(list) + for row in selected: + grouped[target_sort(row[target_field])].append(row) + + matrix = np.zeros((len(targets), n_data), dtype=float) + for ridx, target in enumerate(targets): + group = grouped[target] + if not group: + continue + for row in group: + for index in parse_indices(row.get("correction_indices", "")): + if 0 <= index < n_data: + matrix[ridx, index] += 1.0 + matrix[ridx, :] /= float(len(group)) + return targets, matrix + + +def plot_correction_confusion( + surface_rows: list[dict[str, str]], gkp_rows: list[dict[str, str]], out_dir: Path, manuscript_dir: Path | None +) -> None: + import matplotlib.pyplot as plt # type: ignore + from matplotlib.patches import Rectangle # type: ignore + + surface_targets, surface_mat = correction_inclusion_matrix( + surface_rows, dataset="ibm_ibm_fez", decoder="mwpm", target_field="injected_x", n_data=40 + ) + gkp_targets, gkp_mat = correction_inclusion_matrix( + gkp_rows, dataset="digitized_gkp_pennylane", decoder="mwpm", target_field="injected_q", n_data=40 + ) + + fig, axes = plt.subplots(2, 1, figsize=(6.45, 4.9), constrained_layout=True, sharex=True) + panels = [ + (axes[0], surface_mat, surface_targets, "Surface IBM MWPM", "X"), + (axes[1], gkp_mat, gkp_targets, "Digitized-GKP MWPM", "q"), + ] + im = None + for ax, matrix, targets, title, prefix in panels: + im = ax.imshow(matrix, aspect="auto", cmap=HEATMAP_CMAP, vmin=0.0, vmax=0.7, interpolation="nearest") + ax.set_yticks(np.arange(len(targets))) + ax.set_yticklabels([f"{prefix}{target}" for target in targets]) + ax.set_ylabel("intended target") + for ridx, target in enumerate(targets): + ax.add_patch(Rectangle((target - 0.5, ridx - 0.5), 1.0, 1.0, fill=False, edgecolor="white", linewidth=1.1)) + style_heatmap_axis(ax) + add_panel_label(axes[0], "(a) Surface IBM MWPM", y=-0.14) + add_panel_label(axes[1], "(b) Digitized-GKP MWPM", y=-0.18) + axes[-1].set_xlabel("decoded correction data index") + ticks = list(range(0, 40, 5)) + [39] + axes[-1].set_xticks(ticks) + axes[-1].set_xticklabels([str(tick) for tick in ticks]) + if im is not None: + cbar = fig.colorbar(im, ax=axes, fraction=0.025, pad=0.02) + cbar.set_label("inclusion rate") + save_journal_figure(fig, out_dir / "figure_correction_confusion_surface_gkp", manuscript_dir) + plt.close(fig) + + +def plot_surface_weight_distribution(rows: list[dict[str, str]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + selected = [ + row + for row in rows + if row.get("decoder") == "mwpm" and row.get("injected_x", "") != "" and row.get("dataset") in {"ibm_ibm_fez", "local_simulator"} + ] + weights_by_dataset: dict[str, list[int]] = collections.defaultdict(list) + for row in selected: + weights_by_dataset[row["dataset"]].append(int(f(row.get("correction_weight", "0")))) + max_weight = max(max(values) for values in weights_by_dataset.values() if values) + x = np.arange(max_weight + 1) + width = 0.38 + + fig, ax = plt.subplots(figsize=(3.75, 2.65), constrained_layout=True) + for offset, (dataset, color, label) in zip( + [-width / 2.0, width / 2.0], + [("ibm_ibm_fez", EXACT_COLOR, "IBM fez"), ("local_simulator", GKP_COLOR, "local")], + ): + values = weights_by_dataset[dataset] + counts = collections.Counter(values) + rates = np.asarray([counts.get(int(weight), 0) / max(1, len(values)) for weight in x], dtype=float) + ax.bar(x + offset, rates, width=width, color=color, label=label) + mean_value = float(np.mean(values)) + ax.axvline(mean_value, color=color, linestyle=(0, (3, 2)), linewidth=1.0) + ax.set_xlabel("MWPM correction weight") + ax.set_ylabel("shot fraction") + ax.set_xticks(x) + style_bar_axis(ax) + ax.legend(frameon=False, loc="upper right") + save_journal_figure(fig, out_dir / "figure_surface_correction_weight_distribution", manuscript_dir) + plt.close(fig) + + +def aggregate_policy_metrics(rows: list[dict[str, str]], *, dataset: str, target_field: str) -> dict[str, tuple[float, float, float]]: + out: dict[str, tuple[float, float, float]] = {} + for decoder in DECODER_ORDER: + selected = select_injected(rows, dataset=dataset, decoder=decoder, target_field=target_field) + if not selected: + out[decoder] = (float("nan"), float("nan"), float("nan")) + continue + exact = float(np.mean([f(row.get("exact_intended_match", "0")) for row in selected])) + contains = float(np.mean([f(row.get("contains_intended_target", "0")) for row in selected])) + weight = float(np.mean([f(row.get("correction_weight", "0")) for row in selected])) + out[decoder] = (exact, contains, weight) + return out + + +def plot_decoder_policy_comparison( + rep_rows: list[dict[str, str]], + qldpc_rows: list[dict[str, str]], + surface_rows: list[dict[str, str]], + gkp_rows: list[dict[str, str]], + out_dir: Path, + manuscript_dir: Path | None, +) -> None: + import matplotlib.pyplot as plt # type: ignore + + studies = [ + ("Rep.", aggregate_policy_metrics(rep_rows, dataset="ibm_ibm_fez", target_field="injected_x")), + ("Steane", aggregate_policy_metrics(qldpc_rows, dataset="ibm_ibm_fez", target_field="injected_x")), + ("Surface", aggregate_policy_metrics(surface_rows, dataset="ibm_ibm_fez", target_field="injected_x")), + ("GKP", aggregate_policy_metrics(gkp_rows, dataset="digitized_gkp_pennylane", target_field="injected_q")), + ] + metric_specs = [ + ("exact", 0, "exact localization", (0.0, 1.02)), + ("contains", 1, "target-containing", (0.0, 1.02)), + ("weight", 2, "mean correction weight", None), + ] + colors = {"mwpm": EXACT_COLOR, "uf": CONTAINS_COLOR, "bp": LOCAL_COLOR} + x = np.arange(len(studies)) + width = 0.23 + + fig, axes = plt.subplots(1, 3, figsize=(6.9, 2.45), constrained_layout=False) + fig.subplots_adjust(left=0.065, right=0.985, top=0.82, bottom=0.28, wspace=0.55) + for panel_index, (ax, (_name, metric_index, ylabel, ylim)) in enumerate(zip(axes, metric_specs)): + for didx, decoder in enumerate(DECODER_ORDER): + values = [metrics[decoder][metric_index] for _, metrics in studies] + ax.bar(x + (didx - 1) * width, values, width=width, color=colors[decoder], label=DECODER_LABELS[decoder]) + ax.set_xticks(x) + ax.set_xticklabels([name for name, _metrics in studies], rotation=25, ha="right") + ax.set_ylabel(ylabel) + style_bar_axis(ax) + if ylim is not None: + ax.set_ylim(*ylim) + add_panel_label(ax, f"({chr(ord('a') + panel_index)}) {ylabel}", y=-0.34) + handles, labels = axes[0].get_legend_handles_labels() + fig.legend(handles, labels, frameon=False, loc="upper center", bbox_to_anchor=(0.5, 0.995), ncol=3) + save_journal_figure(fig, out_dir / "figure_decoder_policy_comparison", manuscript_dir) + plt.close(fig) + + +def wrap_gkp_value(value: float, period: float) -> float: + return (value + 0.5 * period) % period - 0.5 * period + + +def plot_gkp_binning(paper_dir: Path, out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + data = json.loads((paper_dir / "results/32_gkp_digitized_sampler/local_gkp_digitized_results.json").read_text(encoding="utf-8")) + period = math.sqrt(math.pi) + decision_width = float(data["decision_width"]) + clean_values: list[float] = [] + injected_support_values: list[float] = [] + injected_background_values: list[float] = [] + for experiment in data["experiments"]: + expected = experiment["expected_syndrome"] + for record in experiment["shot_records"]: + wrapped = [wrap_gkp_value(float(value), period) for value in record["analog_z_values"]] + if experiment["injected_q"] is None: + clean_values.extend(wrapped) + else: + for idx, value in enumerate(wrapped): + if int(expected[idx]): + injected_support_values.append(value) + else: + injected_background_values.append(value) + + bins = np.linspace(-0.5 * period, 0.5 * period, 81) + fig, ax = plt.subplots(figsize=(4.15, 2.75), constrained_layout=True) + ax.hist(clean_values, bins=bins, density=True, histtype="stepfilled", alpha=0.32, color=IBM_COLOR, label="clean checks") + ax.hist( + injected_background_values, + bins=bins, + density=True, + histtype="step", + linewidth=1.35, + color=LOCAL_COLOR, + label="injected background checks", + ) + ax.hist( + injected_support_values, + bins=bins, + density=True, + histtype="step", + linewidth=1.55, + color=GKP_COLOR, + label="injected target-support checks", + ) + for sign in (-1.0, 1.0): + ax.axvline(sign * decision_width, color="#111827", linestyle=(0, (3, 2)), linewidth=1.0) + ax.axvspan(-decision_width, decision_width, color="#F3F4F6", alpha=0.45, zorder=-1) + ax.set_xlabel(r"wrapped check coordinate") + ax.set_ylabel("density") + style_bar_axis(ax) + ax.legend(frameon=False, loc="upper left") + save_journal_figure(fig, out_dir / "figure_gkp_wrapped_quadrature_binning", manuscript_dir) + plt.close(fig) + + +def activation_rates(rows: list[dict[str, str]], *, dataset: str, circuit_id: str) -> np.ndarray: + selected = [row for row in rows if row.get("decoder") == "mwpm" and row.get("dataset") == dataset and row.get("circuit_id") == circuit_id] + if not selected: + return np.asarray([], dtype=float) + n_checks = len(selected[0]["measured_syndrome"]) + matrix = np.zeros((len(selected), n_checks), dtype=float) + for ridx, row in enumerate(selected): + matrix[ridx, :] = [int(bit) for bit in row["measured_syndrome"]] + return np.mean(matrix, axis=0) + + +def stream_summaries(rows: list[dict[str, str]], *, dataset: str) -> tuple[list[str], np.ndarray, np.ndarray]: + selected = [row for row in rows if row.get("decoder") == "mwpm" and row.get("dataset") == dataset] + grouped: dict[str, list[dict[str, str]]] = collections.defaultdict(list) + for row in selected: + grouped[row["circuit_id"]].append(row) + + def key(circuit_id: str) -> tuple[int, int]: + if circuit_id == "clean": + return (0, 0) + return (1, int(circuit_id.removeprefix("x_data_"))) + + labels: list[str] = [] + syndrome_weight: list[float] = [] + correction_weight: list[float] = [] + for circuit_id in sorted(grouped, key=key): + group = grouped[circuit_id] + labels.append("clean" if circuit_id == "clean" else "X" + circuit_id.removeprefix("x_data_")) + syndrome_weight.append(float(np.mean([f(row["syndrome_weight"]) for row in group]))) + correction_weight.append(float(np.mean([f(row["correction_weight"]) for row in group]))) + return labels, np.asarray(syndrome_weight), np.asarray(correction_weight) + + +def plot_surface_empirical_noise_overlay(rows: list[dict[str, str]], out_dir: Path, manuscript_dir: Path | None) -> None: + import matplotlib.pyplot as plt # type: ignore + + ibm_clean = activation_rates(rows, dataset="ibm_ibm_fez", circuit_id="clean") + local_clean = activation_rates(rows, dataset="local_simulator", circuit_id="clean") + labels, ibm_syndrome_weight, ibm_correction_weight = stream_summaries(rows, dataset="ibm_ibm_fez") + local_labels, local_syndrome_weight, local_correction_weight = stream_summaries(rows, dataset="local_simulator") + + fig, axes = plt.subplots(2, 1, figsize=(5.2, 4.55), constrained_layout=False) + fig.subplots_adjust(left=0.13, right=0.97, top=0.97, bottom=0.18, hspace=0.58) + x_checks = np.arange(len(ibm_clean)) + axes[0].bar(x_checks - 0.18, ibm_clean, width=0.36, color=EXACT_COLOR, label="IBM clean") + axes[0].bar(x_checks + 0.18, local_clean, width=0.36, color=GKP_COLOR, label="local clean") + axes[0].set_xticks(x_checks) + axes[0].set_xticklabels([f"Z{i}" for i in x_checks]) + axes[0].set_ylabel("activation rate") + style_bar_axis(axes[0]) + axes[0].legend(frameon=False, loc="upper right") + + axes[1].scatter(local_syndrome_weight, local_correction_weight, color=GKP_COLOR, marker="D", label="local") + axes[1].scatter(ibm_syndrome_weight, ibm_correction_weight, color=EXACT_COLOR, marker="o", label="IBM") + labels_to_mark = {"clean", "X5", "X10", "X17", "X37"} + for label, sx, cy in zip(labels, ibm_syndrome_weight, ibm_correction_weight): + if label in labels_to_mark: + axes[1].annotate(label, (sx, cy), xytext=(4, 3), textcoords="offset points", fontsize=6.8, color="#374151") + for label, sx, cy in zip(local_labels, local_syndrome_weight, local_correction_weight): + if label == "clean": + axes[1].annotate("local clean", (sx, cy), xytext=(3, 2), textcoords="offset points", fontsize=6.8, color="#374151") + axes[1].set_xlabel("mean syndrome weight") + axes[1].set_ylabel("mean correction weight") + add_panel_label(axes[0], "(a) Clean background activation", y=-0.18) + axes[1].xaxis.set_label_coords(0.5, -0.12) + add_panel_label(axes[1], "(b) Syndrome vs correction burden", y=-0.34) + axes[1].set_ylim(0.0, max(float(np.max(ibm_correction_weight)) + 0.4, 4.0)) + style_bar_axis(axes[1]) + axes[1].legend(frameon=False, loc="upper left") + save_journal_figure(fig, out_dir / "figure_surface_empirical_noise_overlay", manuscript_dir) + plt.close(fig) + + +def main() -> int: + args = parse_args() + paper_dir = Path(args.paper_dir).resolve() + out_dir = Path(args.out_dir) + manuscript_dir = Path(args.manuscript_dir) if args.manuscript_dir else None + out_dir.mkdir(parents=True, exist_ok=True) + if manuscript_dir is not None: + manuscript_dir.mkdir(parents=True, exist_ok=True) + + import matplotlib # type: ignore + + matplotlib.use("Agg", force=True) + apply_journal_style() + + rep_rows = read_csv_rows(paper_dir / "results/05_decode_live_syndromes/decoded_shots.csv") + qldpc_rows = read_csv_rows(paper_dir / "results/15_decode_qldpc_syndromes/decoded_shots.csv") + surface_rows = read_csv_rows(paper_dir / "results/25_decode_surface_syndromes/decoded_shots.csv") + gkp_rows = read_csv_rows(paper_dir / "results/34_decode_gkp_syndromes/decoded_shots.csv") + + plot_correction_confusion(surface_rows, gkp_rows, out_dir, manuscript_dir) + plot_surface_weight_distribution(surface_rows, out_dir, manuscript_dir) + plot_decoder_policy_comparison(rep_rows, qldpc_rows, surface_rows, gkp_rows, out_dir, manuscript_dir) + plot_gkp_binning(paper_dir, out_dir, manuscript_dir) + plot_surface_empirical_noise_overlay(surface_rows, out_dir, manuscript_dir) + print(f"Wrote paper_05 supplemental figures to {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/repetition_syndrome.py b/examples/paper_runs/paper_05/scripts/repetition_syndrome.py new file mode 100755 index 0000000..e982074 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/repetition_syndrome.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Shared repetition-code helpers for paper_05.""" + +from __future__ import annotations + +import itertools +import re +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class ExperimentSpec: + circuit_id: str + injected_x: int | None + label: str + + +def sanitize_label(value: str) -> str: + clean = re.sub(r"[^A-Za-z0-9_.-]+", "_", value.strip()) + clean = clean.strip("_") + return clean or "dataset" + + +def parse_targets(targets: str, n_data: int) -> list[int | None]: + value = targets.strip().lower() + if value in {"all", "all_injected"}: + return [None, *range(n_data)] + if value in {"middle", "mid"}: + return [None, n_data // 2] + if value in {"clean", "none"}: + return [None] + + out: list[int | None] = [None] + for part in targets.split(","): + part = part.strip().lower() + if not part: + continue + if part in {"clean", "none"}: + continue + idx = int(part) + if idx < 0 or idx >= n_data: + raise ValueError(f"target index {idx} outside [0, {n_data - 1}]") + out.append(idx) + return out + + +def experiment_specs(n_data: int, targets: str) -> list[ExperimentSpec]: + specs: list[ExperimentSpec] = [] + seen: set[int | None] = set() + for target in parse_targets(targets, n_data): + if target in seen: + continue + seen.add(target) + if target is None: + specs.append(ExperimentSpec(circuit_id="clean", injected_x=None, label="clean")) + else: + specs.append(ExperimentSpec(circuit_id=f"x_data_{target}", injected_x=target, label=f"X on data {target}")) + return specs + + +def expected_syndrome(n_data: int, injected_x: int | None) -> list[int]: + syndrome = [0] * (n_data - 1) + if injected_x is None: + return syndrome + if injected_x > 0: + syndrome[injected_x - 1] ^= 1 + if injected_x < n_data - 1: + syndrome[injected_x] ^= 1 + return syndrome + + +def syndrome_from_data_bits(data_bits: list[int]) -> list[int]: + return [(data_bits[i] ^ data_bits[i + 1]) & 1 for i in range(len(data_bits) - 1)] + + +def cbit_values_to_bitstring(cbits_low_to_high: list[int]) -> str: + return "".join(str(int(v) & 1) for v in reversed(cbits_low_to_high)) + + +def parse_bitstring(bitstring: str, n_data: int) -> tuple[list[int], list[int]]: + compact = bitstring.replace(" ", "").strip() + n_checks = n_data - 1 + expected = n_checks + n_data + if len(compact) != expected: + raise ValueError(f"bitstring length {len(compact)} does not match expected {expected}: {bitstring!r}") + c_low_to_high = [int(ch) for ch in reversed(compact)] + syndrome = c_low_to_high[:n_checks] + data = c_low_to_high[n_checks : n_checks + n_data] + return syndrome, data + + +def syndrome_to_events(syndrome: list[int], *, time_ns: int = 1000) -> list[dict[str, Any]]: + return [ + {"index": idx, "time_ns": time_ns, "type": "Z"} + for idx, bit in enumerate(syndrome) + if bit & 1 + ] + + +def decode_min_weight(syndrome: list[int], n_data: int) -> list[int]: + """Return minimum-Hamming-weight data-bit correction matching a repetition syndrome.""" + target = [bit & 1 for bit in syndrome] + best: tuple[int, tuple[int, ...]] | None = None + for bits in itertools.product((0, 1), repeat=n_data): + if syndrome_from_data_bits(list(bits)) != target: + continue + weight = sum(bits) + if best is None or (weight, bits) < best: + best = (weight, bits) + if best is None: + return [] + return [idx for idx, bit in enumerate(best[1]) if bit] + + +def correction_syndrome(correction_indices: list[int], n_data: int) -> list[int]: + bits = [0] * n_data + for idx in correction_indices: + if 0 <= idx < n_data: + bits[idx] ^= 1 + return syndrome_from_data_bits(bits) + + +def build_qiskit_circuit(n_data: int, spec: ExperimentSpec) -> Any: + try: + from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister # type: ignore + except Exception as exc: # pragma: no cover - exercised only when qiskit is absent + raise SystemExit( + "Qiskit is required to build the paper_05 circuit artifacts. " + "Install qiskit or run inside the project .venv." + ) from exc + + n_checks = n_data - 1 + data = QuantumRegister(n_data, "d") + anc = QuantumRegister(n_checks, "a") + meas = ClassicalRegister(n_checks + n_data, "meas") + qc = QuantumCircuit(data, anc, meas, name=spec.circuit_id) + + if spec.injected_x is not None: + qc.x(data[spec.injected_x]) + qc.barrier(data) + + for idx in range(n_checks): + qc.cx(data[idx], anc[idx]) + qc.cx(data[idx + 1], anc[idx]) + + qc.barrier(data, anc) + for idx in range(n_checks): + qc.measure(anc[idx], meas[idx]) + for idx in range(n_data): + qc.measure(data[idx], meas[n_checks + idx]) + return qc + + +def circuit_metadata(n_data: int, spec: ExperimentSpec) -> dict[str, Any]: + return { + "circuit_id": spec.circuit_id, + "label": spec.label, + "n_data": n_data, + "n_checks": n_data - 1, + "injected_x": "" if spec.injected_x is None else spec.injected_x, + "expected_syndrome": "".join(str(bit) for bit in expected_syndrome(n_data, spec.injected_x)), + "classical_bit_order": "low-to-high: syndrome[0..n_checks-1], data[0..n_data-1]", + "bitstring_order": "Qiskit count keys are parsed as high-to-low classical bits.", + } diff --git a/examples/paper_runs/paper_05/scripts/run_local_css_ldpc_sampler.py b/examples/paper_runs/paper_05/scripts/run_local_css_ldpc_sampler.py new file mode 100644 index 0000000..c71b2f4 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/run_local_css_ldpc_sampler.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Generate local paper_05 CSS-LDPC syndrome measurements.""" + +from __future__ import annotations + +import argparse +import collections +import json +import random +from pathlib import Path +from typing import Any + +from css_ldpc_syndrome import ( + cbit_values_to_bitstring, + experiment_specs, + expected_syndrome, + n_checks, + n_data, + syndrome_from_data_bits, +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--targets", default="all") + parser.add_argument("--shots", type=int, default=256) + parser.add_argument("--measurement-error-rate", type=float, default=0.02) + parser.add_argument("--background-data-error-rate", type=float, default=0.0) + parser.add_argument("--seed", type=int, default=20260705) + return parser.parse_args() + + +def maybe_flip(bit: int, p: float, rng: random.Random) -> int: + return bit ^ int(rng.random() < p) + + +def main() -> int: + args = parse_args() + if args.shots <= 0: + raise SystemExit("Error: --shots must be positive.") + if not 0.0 <= args.measurement_error_rate <= 1.0: + raise SystemExit("Error: --measurement-error-rate must be in [0, 1].") + if not 0.0 <= args.background_data_error_rate <= 1.0: + raise SystemExit("Error: --background-data-error-rate must be in [0, 1].") + + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + rng = random.Random(args.seed) + experiments: list[dict[str, Any]] = [] + + for spec in experiment_specs(args.targets): + counts: collections.Counter[str] = collections.Counter() + shot_records: list[dict[str, Any]] = [] + for shot in range(args.shots): + data_bits = [0] * n_data() + if spec.injected_x is not None: + data_bits[spec.injected_x] ^= 1 + background_flips: list[int] = [] + for idx in range(n_data()): + if rng.random() < args.background_data_error_rate: + data_bits[idx] ^= 1 + background_flips.append(idx) + + ideal_syndrome = syndrome_from_data_bits(data_bits) + measured_syndrome = [maybe_flip(bit, args.measurement_error_rate, rng) for bit in ideal_syndrome] + measured_data = [maybe_flip(bit, args.measurement_error_rate, rng) for bit in data_bits] + bitstring = cbit_values_to_bitstring([*measured_syndrome, *measured_data]) + counts[bitstring] += 1 + shot_records.append( + { + "shot_index": shot, + "bitstring": bitstring, + "measured_syndrome": measured_syndrome, + "measured_data": measured_data, + "ideal_syndrome": ideal_syndrome, + "background_flips": background_flips, + } + ) + + experiments.append( + { + "circuit_id": spec.circuit_id, + "label": spec.label, + "injected_x": spec.injected_x, + "expected_syndrome": expected_syndrome(spec.injected_x), + "counts": dict(sorted(counts.items())), + "shot_records": shot_records, + } + ) + + payload = { + "schema": "paper05_css_ldpc_results_v1", + "source": "local_simulator", + "backend": "local_css_ldpc_sampler", + "job_id": f"local-css-ldpc-{args.seed}", + "code_family": "css_ldpc", + "code_name": "steane_z_checks", + "shots": args.shots, + "n_data": n_data(), + "n_checks": n_checks(), + "measurement_error_rate": args.measurement_error_rate, + "background_data_error_rate": args.background_data_error_rate, + "seed": args.seed, + "experiments": experiments, + } + out_path = out_dir / "local_css_ldpc_results.json" + with out_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + print(f"Wrote local CSS-LDPC results to {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/run_local_gkp_digitized_sampler.py b/examples/paper_runs/paper_05/scripts/run_local_gkp_digitized_sampler.py new file mode 100644 index 0000000..1685d47 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/run_local_gkp_digitized_sampler.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +"""Sample PennyLane-backed digitized-GKP syndrome records for paper_05.""" + +from __future__ import annotations + +import argparse +import collections +import json +import math +import sys +import random +from pathlib import Path +from typing import Any + +from gkp_digitized_syndrome import ( + SQRT_PI, + apply_shift_noise, + cbit_values_to_bitstring, + digitized_data_bits, + experiment_specs, + expected_syndrome, + z_syndrome_from_q_shifts, +) +from surface_syndrome import build_surface_geometry + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--distance", type=int, default=5) + parser.add_argument("--targets", default="representative") + parser.add_argument("--shots", type=int, default=4096) + parser.add_argument("--rounds", type=int, default=3) + parser.add_argument("--sigma-shift-scale", type=float, default=0.015) + parser.add_argument("--measurement-error-rate", type=float, default=0.01) + parser.add_argument("--jump-prob", type=float, default=0.001) + parser.add_argument("--jump-scale", type=float, default=0.5) + parser.add_argument("--decision-width-scale", type=float, default=0.25) + parser.add_argument("--injected-shift-scale", type=float, default=0.56) + parser.add_argument("--seed", type=int, default=20260706) + parser.add_argument( + "--pennylane-mode", + choices=("required", "auto", "disabled"), + default="required", + help="Use PennyLane default.gaussian for finite-squeezed q-readout noise.", + ) + parser.add_argument( + "--pennylane-squeeze-r", + type=float, + default=2.0, + help="Single-mode squeezing parameter used by the PennyLane Gaussian readout proxy.", + ) + parser.add_argument( + "--pennylane-noise-scale", + type=float, + default=1.0, + help="Scale applied to PennyLane QuadX samples before adding them to q-shifts.", + ) + return parser.parse_args() + + +def _validate_probability(name: str, value: float) -> None: + if not 0.0 <= value <= 1.0: + raise SystemExit(f"Error: --{name} must be in [0, 1].") + + +def _load_pennylane(mode: str) -> tuple[Any | None, str]: + if mode == "disabled": + return None, "" + try: + import numpy as np # type: ignore + import pennylane as qml # type: ignore + except Exception as exc: + if mode == "required": + raise SystemExit( + "Error: PennyLane is required for this sampler. Install with: pip install pennylane" + ) from exc + print( + "Warning: PennyLane unavailable; falling back to deterministic local digitization.", + file=sys.stderr, + ) + return None, "" + return (qml, np), str(getattr(qml, "__version__", "unknown")) + + +def _build_quadx_noise_sampler( + *, + qml_np: Any | None, + shots: int, + squeeze_r: float, + noise_scale: float, + seed: int, +) -> tuple[Any, dict[str, Any]]: + if qml_np is None: + def local_noise() -> list[float]: + return [0.0] * shots + + return local_noise, { + "enabled": False, + "device": "", + "squeeze_r": "", + "noise_scale": "", + } + + qml, np = qml_np + np.random.seed(seed + 7919) + dev = qml.device("default.gaussian", wires=1) + + @qml.set_shots(shots=shots) + @qml.qnode(dev) + def sample_zero_mean_quadx(): + qml.SqueezedState(squeeze_r, 0.0, wires=0) + return qml.sample(qml.QuadX(0)) + + def pennylane_noise() -> list[float]: + raw = sample_zero_mean_quadx() + return [float(value) * noise_scale for value in raw] + + return pennylane_noise, { + "enabled": True, + "device": "default.gaussian", + "squeeze_r": squeeze_r, + "noise_scale": noise_scale, + } + + +def main() -> int: + args = parse_args() + if args.shots <= 0: + raise SystemExit("Error: --shots must be positive.") + if args.rounds <= 0: + raise SystemExit("Error: --rounds must be positive.") + _validate_probability("measurement-error-rate", args.measurement_error_rate) + _validate_probability("jump-prob", args.jump_prob) + if not math.isfinite(args.pennylane_squeeze_r): + raise SystemExit("Error: --pennylane-squeeze-r must be finite.") + if not math.isfinite(args.pennylane_noise_scale) or args.pennylane_noise_scale < 0.0: + raise SystemExit("Error: --pennylane-noise-scale must be finite and non-negative.") + + geom = build_surface_geometry(args.distance) + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + rng = random.Random(args.seed) + qml_np, pennylane_version = _load_pennylane(args.pennylane_mode) + quadx_noise, pennylane_meta = _build_quadx_noise_sampler( + qml_np=qml_np, + shots=args.shots, + squeeze_r=args.pennylane_squeeze_r, + noise_scale=args.pennylane_noise_scale, + seed=args.seed, + ) + sigma_shift = args.sigma_shift_scale * SQRT_PI + jump_scale = args.jump_scale * SQRT_PI + injected_shift = args.injected_shift_scale * SQRT_PI + decision_width = args.decision_width_scale * SQRT_PI + experiments: list[dict[str, Any]] = [] + + for spec in experiment_specs(args.distance, args.targets): + counts: collections.Counter[str] = collections.Counter() + shot_records: list[dict[str, Any]] = [] + q_shift_by_shot = [[0.0] * geom.n_data for _ in range(args.shots)] + p_shift_by_shot = [[0.0] * geom.n_data for _ in range(args.shots)] + for q_shift in q_shift_by_shot: + if spec.injected_q is not None: + q_shift[spec.injected_q] += injected_shift + + round_syndromes_by_shot: list[list[str]] = [[] for _ in range(args.shots)] + final_syndromes: list[list[int]] = [[0] * geom.n_z for _ in range(args.shots)] + final_analog_values: list[list[float]] = [[0.0] * geom.n_z for _ in range(args.shots)] + final_data_bits: list[list[int]] = [[0] * geom.n_data for _ in range(args.shots)] + + for round_index in range(args.rounds): + readout_noise_by_mode = [quadx_noise() for _ in range(geom.n_data)] + for shot in range(args.shots): + q_shift = q_shift_by_shot[shot] + p_shift = p_shift_by_shot[shot] + apply_shift_noise( + q_shift, + p_shift, + sigma_shift=sigma_shift, + jump_prob=args.jump_prob, + jump_scale=jump_scale, + rng=rng, + ) + q_readout = [q_shift[idx] + readout_noise_by_mode[idx][shot] for idx in range(geom.n_data)] + measured_syndrome, analog_values = z_syndrome_from_q_shifts( + geom, + q_readout, + decision_width=decision_width, + measurement_error_rate=args.measurement_error_rate, + rng=rng, + ) + round_syndromes_by_shot[shot].append("".join(str(bit) for bit in measured_syndrome)) + if round_index == args.rounds - 1: + final_syndromes[shot] = measured_syndrome + final_analog_values[shot] = analog_values + final_data_bits[shot] = digitized_data_bits(q_readout, decision_width=decision_width) + + for shot in range(args.shots): + bitstring = cbit_values_to_bitstring([*final_syndromes[shot], *final_data_bits[shot]]) + counts[bitstring] += 1 + shot_records.append( + { + "shot_index": shot, + "bitstring": bitstring, + "measured_syndrome": final_syndromes[shot], + "digitized_data": final_data_bits[shot], + "analog_z_values": [round(value, 8) for value in final_analog_values[shot]], + "round_syndromes": round_syndromes_by_shot[shot], + } + ) + + experiments.append( + { + "circuit_id": spec.circuit_id, + "label": spec.label, + "injected_q": spec.injected_q, + "expected_syndrome": expected_syndrome(geom, spec.injected_q), + "counts": dict(sorted(counts.items())), + "shot_records": shot_records, + } + ) + + payload = { + "schema": "paper05_digitized_gkp_results_v1", + "source": "digitized_gkp_pennylane" if pennylane_meta["enabled"] else "digitized_gkp_local", + "backend": ( + "pennylane_default_gaussian_digitized_gkp" + if pennylane_meta["enabled"] + else "local_digitized_gkp_sampler" + ), + "job_id": ( + f"pennylane-gkp-d{args.distance}-{args.seed}" + if pennylane_meta["enabled"] + else f"local-gkp-d{args.distance}-{args.seed}" + ), + "code_family": "digitized_gkp", + "code_name": f"digitized_gkp_surface_d{args.distance}_z_checks", + "distance": args.distance, + "shots": args.shots, + "rounds": args.rounds, + "n_data": geom.n_data, + "n_checks": geom.n_z, + "sigma_shift": sigma_shift, + "sigma_shift_scale": args.sigma_shift_scale, + "measurement_error_rate": args.measurement_error_rate, + "jump_prob": args.jump_prob, + "jump_scale": jump_scale, + "jump_scale_pi": args.jump_scale, + "decision_width": decision_width, + "decision_width_scale": args.decision_width_scale, + "injected_shift": injected_shift, + "injected_shift_scale": args.injected_shift_scale, + "seed": args.seed, + "pennylane_enabled": bool(pennylane_meta["enabled"]), + "pennylane_version": pennylane_version, + "pennylane_device": pennylane_meta["device"], + "pennylane_squeeze_r": pennylane_meta["squeeze_r"], + "pennylane_noise_scale": pennylane_meta["noise_scale"], + "interpretation": ( + "PennyLane default.gaussian finite-squeezed q-readout proxy with classical " + "GKP displacement noise and modular outer-code binning." + if pennylane_meta["enabled"] + else "Local deterministic digitized-GKP fallback without PennyLane quadrature readout." + ), + "experiments": experiments, + } + out_path = out_dir / "local_gkp_digitized_results.json" + with out_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + source_label = "PennyLane-backed" if pennylane_meta["enabled"] else "local" + print(f"Wrote {source_label} digitized-GKP results to {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/run_local_repetition_sampler.py b/examples/paper_runs/paper_05/scripts/run_local_repetition_sampler.py new file mode 100755 index 0000000..188f065 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/run_local_repetition_sampler.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Generate local paper_05 repetition-code syndrome measurements.""" + +from __future__ import annotations + +import argparse +import collections +import json +import random +from pathlib import Path +from typing import Any + +from repetition_syndrome import ( + cbit_values_to_bitstring, + experiment_specs, + expected_syndrome, + syndrome_from_data_bits, +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--n-data", type=int, default=5) + parser.add_argument("--targets", default="all") + parser.add_argument("--shots", type=int, default=256) + parser.add_argument("--measurement-error-rate", type=float, default=0.02) + parser.add_argument("--background-data-error-rate", type=float, default=0.0) + parser.add_argument("--seed", type=int, default=20260705) + return parser.parse_args() + + +def maybe_flip(bit: int, p: float, rng: random.Random) -> int: + return bit ^ int(rng.random() < p) + + +def main() -> int: + args = parse_args() + if args.n_data < 3: + raise SystemExit("Error: --n-data must be at least 3.") + if args.shots <= 0: + raise SystemExit("Error: --shots must be positive.") + if not 0.0 <= args.measurement_error_rate <= 1.0: + raise SystemExit("Error: --measurement-error-rate must be in [0, 1].") + if not 0.0 <= args.background_data_error_rate <= 1.0: + raise SystemExit("Error: --background-data-error-rate must be in [0, 1].") + + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + rng = random.Random(args.seed) + + experiments: list[dict[str, Any]] = [] + n_checks = args.n_data - 1 + + for spec in experiment_specs(args.n_data, args.targets): + counts: collections.Counter[str] = collections.Counter() + shot_records: list[dict[str, Any]] = [] + for shot in range(args.shots): + data_bits = [0] * args.n_data + if spec.injected_x is not None: + data_bits[spec.injected_x] ^= 1 + background_flips: list[int] = [] + for idx in range(args.n_data): + if rng.random() < args.background_data_error_rate: + data_bits[idx] ^= 1 + background_flips.append(idx) + + ideal_syndrome = syndrome_from_data_bits(data_bits) + measured_syndrome = [maybe_flip(bit, args.measurement_error_rate, rng) for bit in ideal_syndrome] + measured_data = [maybe_flip(bit, args.measurement_error_rate, rng) for bit in data_bits] + bitstring = cbit_values_to_bitstring([*measured_syndrome, *measured_data]) + counts[bitstring] += 1 + shot_records.append( + { + "shot_index": shot, + "bitstring": bitstring, + "measured_syndrome": measured_syndrome, + "measured_data": measured_data, + "ideal_syndrome": ideal_syndrome, + "background_flips": background_flips, + } + ) + + experiments.append( + { + "circuit_id": spec.circuit_id, + "label": spec.label, + "injected_x": spec.injected_x, + "expected_syndrome": expected_syndrome(args.n_data, spec.injected_x), + "counts": dict(sorted(counts.items())), + "shot_records": shot_records, + } + ) + + payload = { + "schema": "paper05_repetition_results_v1", + "source": "local_simulator", + "backend": "local_repetition_sampler", + "job_id": f"local-{args.seed}", + "shots": args.shots, + "n_data": args.n_data, + "n_checks": n_checks, + "measurement_error_rate": args.measurement_error_rate, + "background_data_error_rate": args.background_data_error_rate, + "seed": args.seed, + "experiments": experiments, + } + out_path = out_dir / "local_repetition_results.json" + with out_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + print(f"Wrote local repetition results to {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/run_local_surface_sampler.py b/examples/paper_runs/paper_05/scripts/run_local_surface_sampler.py new file mode 100644 index 0000000..60db9e5 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/run_local_surface_sampler.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Generate local paper_05 surface-code Z-check syndrome measurements.""" + +from __future__ import annotations + +import argparse +import collections +import json +import random +from pathlib import Path +from typing import Any + +from surface_syndrome import ( + build_surface_geometry, + cbit_values_to_bitstring, + experiment_specs, + expected_syndrome, + syndrome_from_data_bits, +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--distance", type=int, default=5) + parser.add_argument("--targets", default="representative") + parser.add_argument("--shots", type=int, default=256) + parser.add_argument("--measurement-error-rate", type=float, default=0.02) + parser.add_argument("--background-data-error-rate", type=float, default=0.0) + parser.add_argument("--seed", type=int, default=20260705) + return parser.parse_args() + + +def maybe_flip(bit: int, p: float, rng: random.Random) -> int: + return bit ^ int(rng.random() < p) + + +def main() -> int: + args = parse_args() + if args.shots <= 0: + raise SystemExit("Error: --shots must be positive.") + if not 0.0 <= args.measurement_error_rate <= 1.0: + raise SystemExit("Error: --measurement-error-rate must be in [0, 1].") + if not 0.0 <= args.background_data_error_rate <= 1.0: + raise SystemExit("Error: --background-data-error-rate must be in [0, 1].") + + geom = build_surface_geometry(args.distance) + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + rng = random.Random(args.seed) + experiments: list[dict[str, Any]] = [] + + for spec in experiment_specs(args.distance, args.targets): + counts: collections.Counter[str] = collections.Counter() + shot_records: list[dict[str, Any]] = [] + for shot in range(args.shots): + data_bits = [0] * geom.n_data + if spec.injected_x is not None: + data_bits[spec.injected_x] ^= 1 + background_flips: list[int] = [] + for idx in range(geom.n_data): + if rng.random() < args.background_data_error_rate: + data_bits[idx] ^= 1 + background_flips.append(idx) + + ideal_syndrome = syndrome_from_data_bits(geom, data_bits) + measured_syndrome = [maybe_flip(bit, args.measurement_error_rate, rng) for bit in ideal_syndrome] + measured_data = [maybe_flip(bit, args.measurement_error_rate, rng) for bit in data_bits] + bitstring = cbit_values_to_bitstring([*measured_syndrome, *measured_data]) + counts[bitstring] += 1 + shot_records.append( + { + "shot_index": shot, + "bitstring": bitstring, + "measured_syndrome": measured_syndrome, + "measured_data": measured_data, + "ideal_syndrome": ideal_syndrome, + "background_flips": background_flips, + } + ) + + experiments.append( + { + "circuit_id": spec.circuit_id, + "label": spec.label, + "injected_x": spec.injected_x, + "expected_syndrome": expected_syndrome(geom, spec.injected_x), + "counts": dict(sorted(counts.items())), + "shot_records": shot_records, + } + ) + + payload = { + "schema": "paper05_surface_results_v1", + "source": "local_simulator", + "backend": "local_surface_z_sampler", + "job_id": f"local-surface-d{args.distance}-{args.seed}", + "code_family": "surface", + "code_name": f"surface_d{args.distance}_z_checks", + "distance": args.distance, + "shots": args.shots, + "n_data": geom.n_data, + "n_checks": geom.n_z, + "measurement_error_rate": args.measurement_error_rate, + "background_data_error_rate": args.background_data_error_rate, + "seed": args.seed, + "experiments": experiments, + } + out_path = out_dir / "local_surface_results.json" + with out_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + print(f"Wrote local surface-code results to {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/submit_ibm_css_ldpc_sampler.py b/examples/paper_runs/paper_05/scripts/submit_ibm_css_ldpc_sampler.py new file mode 100644 index 0000000..29e5112 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/submit_ibm_css_ldpc_sampler.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Submit paper_05 CSS-LDPC syndrome circuits to IBM Runtime Sampler.""" + +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +from typing import Any + +from css_ldpc_syndrome import build_qiskit_circuit, circuit_metadata, experiment_specs, n_checks, n_data +from submit_ibm_repetition_sampler import ( + choose_backend, + extract_counts, + job_id_value, + load_credentials, + load_service, + sampler_class, + transpile_for_backend, +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--targets", default="all") + parser.add_argument("--shots", type=int, default=256) + parser.add_argument("--backend", default="", help="IBM backend name. If omitted, least-busy hardware is selected.") + parser.add_argument("--instance", default="", help="Optional IBM Quantum instance/hub/group/project.") + parser.add_argument("--credentials-file", default="") + parser.add_argument("--no-wait", action="store_true") + parser.add_argument("--result-timeout", type=float, default=900.0) + parser.add_argument("--optimization-level", type=int, default=1) + return parser.parse_args() + + +def write_submission( + path: Path, + *, + backend_name: str, + job_id: str, + shots: int, + optimization_level: int, + status: str, + experiments: list[dict[str, Any]], +) -> None: + payload = { + "schema": "paper05_css_ldpc_ibm_submission_v1", + "source": "ibm_runtime", + "backend": backend_name, + "job_id": job_id, + "shots": shots, + "code_family": "css_ldpc", + "code_name": "steane_z_checks", + "n_data": n_data(), + "n_checks": n_checks(), + "optimization_level": optimization_level, + "status": status, + "experiments": experiments, + } + with path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + + +def main() -> int: + args = parse_args() + if args.shots <= 0: + raise SystemExit("Error: --shots must be positive.") + + creds = load_credentials(args.credentials_file) + token = ( + os.environ.get("IBM_QUANTUM_TOKEN") + or os.environ.get("QISKIT_IBM_TOKEN") + or creds.get("token", "") + or creds.get("ibm_quantum_token", "") + ) + instance = args.instance or os.environ.get("IBM_QUANTUM_INSTANCE", "") or creds.get("instance", "") + channel = os.environ.get("IBM_QUANTUM_CHANNEL", "") or creds.get("channel", "") or "ibm_quantum_platform" + backend_name_arg = args.backend or creds.get("backend", "") + + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + specs = experiment_specs(args.targets) + circuits = [build_qiskit_circuit(spec) for spec in specs] + experiment_metadata = [circuit_metadata(spec) for spec in specs] + + service = load_service(instance, token, channel) + backend = choose_backend(service, backend_name_arg, min_qubits=n_data() + n_checks()) + backend_name = getattr(backend, "name", None) + if callable(backend_name): + backend_name = backend_name() + backend_name = str(backend_name or backend) + + isa_circuits = transpile_for_backend(circuits, backend, args.optimization_level) + Sampler = sampler_class() + sampler = Sampler(backend) + job = sampler.run(isa_circuits, shots=args.shots) + job_id = job_id_value(job) + submission_path = out_dir / "ibm_css_ldpc_submission.json" + write_submission( + submission_path, + backend_name=backend_name, + job_id=job_id, + shots=args.shots, + optimization_level=args.optimization_level, + status="submitted", + experiments=experiment_metadata, + ) + print(f"Submitted IBM Runtime CSS-LDPC job {job_id} on {backend_name}; wrote {submission_path}") + + if args.no_wait: + print("Not waiting for results because --no-wait was set.") + return 0 + + try: + result = job.result(timeout=args.result_timeout) + except TypeError: + result = job.result() + + experiments: list[dict[str, Any]] = [] + for spec, pub_result in zip(specs, result): + experiments.append({**circuit_metadata(spec), "counts": dict(sorted(extract_counts(pub_result).items()))}) + + payload = { + "schema": "paper05_css_ldpc_results_v1", + "source": "ibm_runtime", + "backend": backend_name, + "job_id": job_id, + "shots": args.shots, + "code_family": "css_ldpc", + "code_name": "steane_z_checks", + "n_data": n_data(), + "n_checks": n_checks(), + "optimization_level": args.optimization_level, + "experiments": experiments, + } + out_path = out_dir / "ibm_css_ldpc_results.json" + with out_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + print(f"Wrote IBM Runtime CSS-LDPC results to {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/submit_ibm_repetition_sampler.py b/examples/paper_runs/paper_05/scripts/submit_ibm_repetition_sampler.py new file mode 100755 index 0000000..8630966 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/submit_ibm_repetition_sampler.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""Submit paper_05 repetition-code syndrome circuits to IBM Runtime Sampler.""" + +from __future__ import annotations + +import argparse +import collections +import json +import os +from pathlib import Path +from typing import Any + +from repetition_syndrome import build_qiskit_circuit, circuit_metadata, experiment_specs + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--n-data", type=int, default=5) + parser.add_argument("--targets", default="all") + parser.add_argument("--shots", type=int, default=256) + parser.add_argument("--backend", default="", help="IBM backend name. If omitted, least-busy hardware is selected.") + parser.add_argument("--instance", default="", help="Optional IBM Quantum instance/hub/group/project.") + parser.add_argument( + "--credentials-file", + default="", + help="Optional local JSON file with token, instance, and backend. Defaults to paper_05/ibm_credentials.local.json if present.", + ) + parser.add_argument( + "--no-wait", + action="store_true", + help="Submit the Runtime job and write job metadata without waiting for results.", + ) + parser.add_argument( + "--result-timeout", + type=float, + default=900.0, + help="Maximum seconds to wait for Runtime results when waiting is enabled.", + ) + parser.add_argument("--optimization-level", type=int, default=1) + return parser.parse_args() + + +def default_credentials_path() -> Path: + return Path(__file__).resolve().parents[1] / "ibm_credentials.local.json" + + +def load_credentials(path_arg: str) -> dict[str, str]: + path = Path(path_arg).expanduser() if path_arg else default_credentials_path() + if not path.exists(): + return {} + with path.open("r", encoding="utf-8") as f: + raw = json.load(f) + return {str(k): str(v) for k, v in raw.items() if v is not None and str(v).strip()} + + +def load_service(instance: str, token: str, channel: str) -> Any: + try: + from qiskit_ibm_runtime import QiskitRuntimeService # type: ignore + except Exception as exc: + raise SystemExit( + "qiskit-ibm-runtime is required for hardware submission. " + "Install it in the active Python environment." + ) from exc + + kwargs: dict[str, Any] = {} + if instance: + kwargs["instance"] = instance + if token: + kwargs["channel"] = channel + kwargs["token"] = token + try: + return QiskitRuntimeService(**kwargs) + except Exception as exc: + raise SystemExit( + "Could not initialize QiskitRuntimeService. Configure credentials with a saved " + "Qiskit IBM Runtime account, set IBM_QUANTUM_TOKEN, or create " + "examples/paper_runs/paper_05/ibm_credentials.local.json. If your account " + "requires an instance, include instance=hub/group/project." + ) from exc + + +def choose_backend(service: Any, backend_name: str, min_qubits: int) -> Any: + if backend_name: + return service.backend(backend_name) + try: + return service.least_busy(operational=True, simulator=False, min_num_qubits=min_qubits) + except TypeError: + candidates = [ + backend + for backend in service.backends(simulator=False, operational=True) + if getattr(backend, "num_qubits", 0) >= min_qubits + ] + if not candidates: + raise SystemExit(f"No operational IBM hardware backend with at least {min_qubits} qubits was found.") + return sorted(candidates, key=lambda b: getattr(getattr(b, "status", lambda: None)(), "pending_jobs", 10**9))[0] + + +def transpile_for_backend(circuits: list[Any], backend: Any, optimization_level: int) -> list[Any]: + try: + from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager # type: ignore + + pm = generate_preset_pass_manager(backend=backend, optimization_level=optimization_level) + return list(pm.run(circuits)) + except Exception: + from qiskit import transpile # type: ignore + + return list(transpile(circuits, backend=backend, optimization_level=optimization_level)) + + +def sampler_class() -> Any: + try: + from qiskit_ibm_runtime import SamplerV2 as Sampler # type: ignore + + return Sampler + except Exception: + from qiskit_ibm_runtime import Sampler # type: ignore + + return Sampler + + +def extract_counts(pub_result: Any) -> dict[str, int]: + data = getattr(pub_result, "data", pub_result) + bit_array = getattr(data, "meas", None) + if bit_array is None: + for name in dir(data): + if name.startswith("_"): + continue + candidate = getattr(data, name) + if hasattr(candidate, "get_counts") or hasattr(candidate, "get_bitstrings"): + bit_array = candidate + break + if bit_array is None: + raise RuntimeError("Could not locate a measured classical register in Sampler result.") + + if hasattr(bit_array, "get_counts"): + counts = bit_array.get_counts() + return {str(k): int(v) for k, v in counts.items()} + if hasattr(bit_array, "get_bitstrings"): + return dict(collections.Counter(str(b) for b in bit_array.get_bitstrings())) + raise RuntimeError("Sampler result object does not expose counts or bitstrings.") + + +def job_id_value(job: Any) -> str: + value = getattr(job, "job_id", "") + if callable(value): + value = value() + return str(value) + + +def write_submission( + path: Path, + *, + backend_name: str, + job_id: str, + shots: int, + n_data: int, + optimization_level: int, + status: str, + experiments: list[dict[str, Any]], +) -> None: + payload = { + "schema": "paper05_ibm_submission_v1", + "source": "ibm_runtime", + "backend": backend_name, + "job_id": job_id, + "shots": shots, + "n_data": n_data, + "n_checks": n_data - 1, + "optimization_level": optimization_level, + "status": status, + "experiments": experiments, + } + with path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + + +def main() -> int: + args = parse_args() + if args.n_data < 3: + raise SystemExit("Error: --n-data must be at least 3.") + if args.shots <= 0: + raise SystemExit("Error: --shots must be positive.") + + creds = load_credentials(args.credentials_file) + token = ( + os.environ.get("IBM_QUANTUM_TOKEN") + or os.environ.get("QISKIT_IBM_TOKEN") + or creds.get("token", "") + or creds.get("ibm_quantum_token", "") + ) + instance = args.instance or os.environ.get("IBM_QUANTUM_INSTANCE", "") or creds.get("instance", "") + channel = os.environ.get("IBM_QUANTUM_CHANNEL", "") or creds.get("channel", "") or "ibm_quantum_platform" + backend_name_arg = args.backend or creds.get("backend", "") + + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + + specs = experiment_specs(args.n_data, args.targets) + circuits = [build_qiskit_circuit(args.n_data, spec) for spec in specs] + experiment_metadata = [circuit_metadata(args.n_data, spec) for spec in specs] + service = load_service(instance, token, channel) + backend = choose_backend(service, backend_name_arg, min_qubits=(2 * args.n_data - 1)) + backend_name = getattr(backend, "name", None) + if callable(backend_name): + backend_name = backend_name() + backend_name = str(backend_name or backend) + + isa_circuits = transpile_for_backend(circuits, backend, args.optimization_level) + Sampler = sampler_class() + sampler = Sampler(backend) + job = sampler.run(isa_circuits, shots=args.shots) + job_id = job_id_value(job) + submission_path = out_dir / "ibm_runtime_submission.json" + write_submission( + submission_path, + backend_name=backend_name, + job_id=job_id, + shots=args.shots, + n_data=args.n_data, + optimization_level=args.optimization_level, + status="submitted", + experiments=experiment_metadata, + ) + print(f"Submitted IBM Runtime job {job_id} on {backend_name}; wrote {submission_path}") + + if args.no_wait: + print("Not waiting for results because --no-wait was set.") + return 0 + + try: + result = job.result(timeout=args.result_timeout) + except TypeError: + result = job.result() + + experiments: list[dict[str, Any]] = [] + for spec, pub_result in zip(specs, result): + experiments.append( + { + **circuit_metadata(args.n_data, spec), + "counts": dict(sorted(extract_counts(pub_result).items())), + } + ) + + payload = { + "schema": "paper05_repetition_results_v1", + "source": "ibm_runtime", + "backend": backend_name, + "job_id": job_id, + "shots": args.shots, + "n_data": args.n_data, + "n_checks": args.n_data - 1, + "optimization_level": args.optimization_level, + "experiments": experiments, + } + out_path = out_dir / "ibm_repetition_results.json" + with out_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + print(f"Wrote IBM Runtime repetition results to {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/submit_ibm_surface_sampler.py b/examples/paper_runs/paper_05/scripts/submit_ibm_surface_sampler.py new file mode 100644 index 0000000..3a31eed --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/submit_ibm_surface_sampler.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Submit paper_05 surface-code Z-check circuits to IBM Runtime Sampler.""" + +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +from typing import Any + +from submit_ibm_repetition_sampler import ( + choose_backend, + extract_counts, + job_id_value, + load_credentials, + load_service, + sampler_class, + transpile_for_backend, +) +from surface_syndrome import build_qiskit_circuit, build_surface_geometry, circuit_metadata, experiment_specs + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--distance", type=int, default=5) + parser.add_argument("--targets", default="representative") + parser.add_argument("--shots", type=int, default=256) + parser.add_argument("--backend", default="") + parser.add_argument("--instance", default="") + parser.add_argument("--credentials-file", default="") + parser.add_argument("--no-wait", action="store_true") + parser.add_argument("--result-timeout", type=float, default=900.0) + parser.add_argument("--optimization-level", type=int, default=1) + return parser.parse_args() + + +def write_submission( + path: Path, + *, + backend_name: str, + job_id: str, + shots: int, + distance: int, + optimization_level: int, + status: str, + experiments: list[dict[str, Any]], +) -> None: + geom = build_surface_geometry(distance) + payload = { + "schema": "paper05_surface_ibm_submission_v1", + "source": "ibm_runtime", + "backend": backend_name, + "job_id": job_id, + "shots": shots, + "code_family": "surface", + "code_name": f"surface_d{distance}_z_checks", + "distance": distance, + "n_data": geom.n_data, + "n_checks": geom.n_z, + "optimization_level": optimization_level, + "status": status, + "experiments": experiments, + } + with path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + + +def main() -> int: + args = parse_args() + if args.shots <= 0: + raise SystemExit("Error: --shots must be positive.") + + geom = build_surface_geometry(args.distance) + creds = load_credentials(args.credentials_file) + token = ( + os.environ.get("IBM_QUANTUM_TOKEN") + or os.environ.get("QISKIT_IBM_TOKEN") + or creds.get("token", "") + or creds.get("ibm_quantum_token", "") + ) + instance = args.instance or os.environ.get("IBM_QUANTUM_INSTANCE", "") or creds.get("instance", "") + channel = os.environ.get("IBM_QUANTUM_CHANNEL", "") or creds.get("channel", "") or "ibm_quantum_platform" + backend_name_arg = args.backend or creds.get("backend", "") + + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + specs = experiment_specs(args.distance, args.targets) + circuits = [build_qiskit_circuit(args.distance, spec) for spec in specs] + experiment_metadata = [circuit_metadata(args.distance, spec) for spec in specs] + + service = load_service(instance, token, channel) + backend = choose_backend(service, backend_name_arg, min_qubits=geom.n_data + geom.n_z) + backend_name = getattr(backend, "name", None) + if callable(backend_name): + backend_name = backend_name() + backend_name = str(backend_name or backend) + + isa_circuits = transpile_for_backend(circuits, backend, args.optimization_level) + Sampler = sampler_class() + sampler = Sampler(backend) + job = sampler.run(isa_circuits, shots=args.shots) + job_id = job_id_value(job) + submission_path = out_dir / "ibm_surface_submission.json" + write_submission( + submission_path, + backend_name=backend_name, + job_id=job_id, + shots=args.shots, + distance=args.distance, + optimization_level=args.optimization_level, + status="submitted", + experiments=experiment_metadata, + ) + print(f"Submitted IBM Runtime surface-code job {job_id} on {backend_name}; wrote {submission_path}") + + if args.no_wait: + print("Not waiting for results because --no-wait was set.") + return 0 + + try: + result = job.result(timeout=args.result_timeout) + except TypeError: + result = job.result() + + experiments: list[dict[str, Any]] = [] + for spec, pub_result in zip(specs, result): + experiments.append({**circuit_metadata(args.distance, spec), "counts": dict(sorted(extract_counts(pub_result).items()))}) + + payload = { + "schema": "paper05_surface_results_v1", + "source": "ibm_runtime", + "backend": backend_name, + "job_id": job_id, + "shots": args.shots, + "code_family": "surface", + "code_name": f"surface_d{args.distance}_z_checks", + "distance": args.distance, + "n_data": geom.n_data, + "n_checks": geom.n_z, + "optimization_level": args.optimization_level, + "experiments": experiments, + } + out_path = out_dir / "ibm_surface_results.json" + with out_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + f.write("\n") + print(f"Wrote IBM Runtime surface-code results to {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/paper_runs/paper_05/scripts/surface_syndrome.py b/examples/paper_runs/paper_05/scripts/surface_syndrome.py new file mode 100644 index 0000000..98c5146 --- /dev/null +++ b/examples/paper_runs/paper_05/scripts/surface_syndrome.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +"""Distance-d surface-code Z-check syndrome helpers for paper_05.""" + +from __future__ import annotations + +import collections +import functools +import re +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class SurfaceGeometry: + distance: int + n_data: int + n_x: int + n_z: int + x_supports: list[list[int]] + z_supports: list[list[int]] + + +@dataclass(frozen=True) +class ExperimentSpec: + circuit_id: str + injected_x: int | None + label: str + + +def sanitize_label(value: str) -> str: + clean = re.sub(r"[^A-Za-z0-9_.-]+", "_", value.strip()) + clean = clean.strip("_") + return clean or "dataset" + + +def build_surface_geometry(distance: int) -> SurfaceGeometry: + if distance < 3 or (distance % 2) == 0: + raise ValueError("surface-code distance must be odd and >= 3") + + d = distance + n_data = 2 * d * (d - 1) + n_x = d * d + n_z = (d - 1) * (d - 1) + + def h_index(x: int, y: int) -> int: + return y * (d - 1) + x + + def v_index(x: int, y: int) -> int: + h_count = d * (d - 1) + return h_count + y * d + x + + x_supports: list[list[int]] = [] + for y in range(d): + for x in range(d): + support: list[int] = [] + if x > 0: + support.append(h_index(x - 1, y)) + if x < d - 1: + support.append(h_index(x, y)) + if y > 0: + support.append(v_index(x, y - 1)) + if y < d - 1: + support.append(v_index(x, y)) + x_supports.append(support) + + z_supports: list[list[int]] = [] + for y in range(d - 1): + for x in range(d - 1): + z_supports.append( + [ + h_index(x, y), + h_index(x, y + 1), + v_index(x, y), + v_index(x + 1, y), + ] + ) + + return SurfaceGeometry( + distance=d, + n_data=n_data, + n_x=n_x, + n_z=n_z, + x_supports=x_supports, + z_supports=z_supports, + ) + + +def _representative_targets(geom: SurfaceGeometry) -> list[int]: + if geom.distance == 5: + return [1, 5, 10, 14, 17, 22, 32, 37] + out: list[int] = [] + seen: set[tuple[int, ...]] = set() + for q in range(geom.n_data): + syndrome = tuple(i for i, support in enumerate(geom.z_supports) if q in support) + if syndrome in seen: + continue + seen.add(syndrome) + out.append(q) + if len(out) >= min(8, geom.n_data): + break + return out + + +def parse_targets(targets: str, geom: SurfaceGeometry) -> list[int | None]: + value = targets.strip().lower() + if value in {"representative", "rep", "selected"}: + return [None, *_representative_targets(geom)] + if value in {"all", "all_injected"}: + return [None, *range(geom.n_data)] + if value in {"all_unique", "unique"}: + out: list[int | None] = [None] + seen: set[tuple[int, ...]] = set() + for q in range(geom.n_data): + syndrome = tuple(i for i, support in enumerate(geom.z_supports) if q in support) + if syndrome in seen: + continue + seen.add(syndrome) + out.append(q) + return out + if value in {"middle", "mid"}: + return [None, geom.n_data // 2] + if value in {"clean", "none"}: + return [None] + + out = [None] + for part in targets.split(","): + part = part.strip().lower() + if not part: + continue + if part in {"clean", "none"}: + continue + idx = int(part) + if idx < 0 or idx >= geom.n_data: + raise ValueError(f"target index {idx} outside [0, {geom.n_data - 1}]") + out.append(idx) + return out + + +def experiment_specs(distance: int, targets: str) -> list[ExperimentSpec]: + geom = build_surface_geometry(distance) + specs: list[ExperimentSpec] = [] + seen: set[int | None] = set() + for target in parse_targets(targets, geom): + if target in seen: + continue + seen.add(target) + if target is None: + specs.append(ExperimentSpec(circuit_id="clean", injected_x=None, label="clean")) + else: + specs.append(ExperimentSpec(circuit_id=f"x_data_{target}", injected_x=target, label=f"X on data {target}")) + return specs + + +def syndrome_from_data_bits(geom: SurfaceGeometry, data_bits: list[int]) -> list[int]: + syndrome: list[int] = [] + for support in geom.z_supports: + parity = 0 + for q in support: + parity ^= data_bits[q] & 1 + syndrome.append(parity) + return syndrome + + +def expected_syndrome(geom: SurfaceGeometry, injected_x: int | None) -> list[int]: + data_bits = [0] * geom.n_data + if injected_x is not None: + data_bits[injected_x] = 1 + return syndrome_from_data_bits(geom, data_bits) + + +def cbit_values_to_bitstring(cbits_low_to_high: list[int]) -> str: + return "".join(str(int(v) & 1) for v in reversed(cbits_low_to_high)) + + +def parse_bitstring(bitstring: str, geom: SurfaceGeometry) -> tuple[list[int], list[int]]: + compact = bitstring.replace(" ", "").strip() + expected = geom.n_z + geom.n_data + if len(compact) != expected: + raise ValueError(f"bitstring length {len(compact)} does not match expected {expected}: {bitstring!r}") + c_low_to_high = [int(ch) for ch in reversed(compact)] + syndrome = c_low_to_high[: geom.n_z] + data = c_low_to_high[geom.n_z : geom.n_z + geom.n_data] + return syndrome, data + + +def syndrome_to_events(syndrome: list[int], *, time_ns: int = 1000) -> list[dict[str, Any]]: + return [{"index": idx, "time_ns": time_ns, "type": "Z"} for idx, bit in enumerate(syndrome) if bit & 1] + + +def syndrome_to_int(syndrome: list[int]) -> int: + value = 0 + for idx, bit in enumerate(syndrome): + if bit & 1: + value |= 1 << idx + return value + + +def int_to_syndrome(value: int, n_checks: int) -> list[int]: + return [(value >> idx) & 1 for idx in range(n_checks)] + + +def _column_masks(geom: SurfaceGeometry) -> list[int]: + masks: list[int] = [] + for q in range(geom.n_data): + bits = [0] * geom.n_z + for idx, support in enumerate(geom.z_supports): + if q in support: + bits[idx] = 1 + masks.append(syndrome_to_int(bits)) + return masks + + +@functools.cache +def _decoder_table(distance: int) -> tuple[tuple[int, ...], ...]: + geom = build_surface_geometry(distance) + n_states = 1 << geom.n_z + columns = _column_masks(geom) + corrections: list[tuple[int, ...] | None] = [None] * n_states + corrections[0] = () + queue: collections.deque[int] = collections.deque([0]) + + while queue: + state = queue.popleft() + current = corrections[state] + if current is None: + continue + for q, mask in enumerate(columns): + next_state = state ^ mask + candidate = tuple(sorted((*current, q))) + if corrections[next_state] is None or (len(candidate), candidate) < (len(corrections[next_state]), corrections[next_state]): + corrections[next_state] = candidate + queue.append(next_state) + + return tuple(c if c is not None else () for c in corrections) + + +def decode_min_weight(geom: SurfaceGeometry, syndrome: list[int]) -> list[int]: + table = _decoder_table(geom.distance) + return list(table[syndrome_to_int(syndrome)]) + + +def correction_syndrome(geom: SurfaceGeometry, correction_indices: list[int]) -> list[int]: + bits = [0] * geom.n_data + for idx in correction_indices: + if 0 <= idx < geom.n_data: + bits[idx] ^= 1 + return syndrome_from_data_bits(geom, bits) + + +def build_qiskit_circuit(distance: int, spec: ExperimentSpec) -> Any: + try: + from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister # type: ignore + except Exception as exc: # pragma: no cover - depends on optional environment + raise SystemExit("Qiskit is required to build surface-code syndrome circuits.") from exc + + geom = build_surface_geometry(distance) + data = QuantumRegister(geom.n_data, "d") + anc = QuantumRegister(geom.n_z, "z") + meas = ClassicalRegister(geom.n_z + geom.n_data, "meas") + qc = QuantumCircuit(data, anc, meas, name=spec.circuit_id) + + if spec.injected_x is not None: + qc.x(data[spec.injected_x]) + qc.barrier(data) + + for check_idx, support in enumerate(geom.z_supports): + for data_idx in support: + qc.cx(data[data_idx], anc[check_idx]) + + qc.barrier(data, anc) + for idx in range(geom.n_z): + qc.measure(anc[idx], meas[idx]) + for idx in range(geom.n_data): + qc.measure(data[idx], meas[geom.n_z + idx]) + return qc + + +def circuit_metadata(distance: int, spec: ExperimentSpec) -> dict[str, Any]: + geom = build_surface_geometry(distance) + return { + "circuit_id": spec.circuit_id, + "label": spec.label, + "code_family": "surface", + "code_name": f"surface_d{distance}_z_checks", + "distance": distance, + "n_data": geom.n_data, + "n_x_checks": geom.n_x, + "n_checks": geom.n_z, + "injected_x": "" if spec.injected_x is None else spec.injected_x, + "expected_syndrome": "".join(str(bit) for bit in expected_syndrome(geom, spec.injected_x)), + "check_type": "Z", + "classical_bit_order": "low-to-high: z_syndrome[0..n_z-1], data[0..n_data-1]", + "bitstring_order": "Qiskit count keys are parsed as high-to-low classical bits.", + }