From 823f3225fd6a12126f1cac5206009d9893506bf8 Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Wed, 10 Jun 2026 20:37:33 -0700 Subject: [PATCH] Use Jupytext for tutorial notebook --- .github/dependabot.yaml | 1 + docs/BUILD | 39 ++ docs/sync_tutorial_notebook.py | 143 +++++++ docs/tutorial.ipynb | 17 +- docs/tutorial.py | 671 +++++++++++++++++++++++++++++++++ src/py/BUILD | 1 + src/py/requirements.in | 1 + src/py/requirements_lock.txt | 270 +++++++++++++ 8 files changed, 1139 insertions(+), 4 deletions(-) create mode 100644 docs/BUILD create mode 100644 docs/sync_tutorial_notebook.py create mode 100644 docs/tutorial.py diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index c130667..b58f78a 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -31,6 +31,7 @@ updates: - dependency-name: pytest - dependency-name: sinter - dependency-name: pybind11-stubgen + - dependency-name: jupytext schedule: interval: "monthly" versioning-strategy: "increase-if-necessary" diff --git a/docs/BUILD b/docs/BUILD new file mode 100644 index 0000000..60a1701 --- /dev/null +++ b/docs/BUILD @@ -0,0 +1,39 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_python//python:py_binary.bzl", "py_binary") +load("@rules_python//python:py_test.bzl", "py_test") + +TUTORIAL_FILES = glob([ + "tutorial.ipynb", +]) + glob( + ["tutorial.py"], + allow_empty = True, +) + +py_binary( + name = "sync_tutorial_notebook", + srcs = ["sync_tutorial_notebook.py"], + data = TUTORIAL_FILES, + deps = ["@pypi//jupytext"], +) + +py_test( + name = "tutorial_jupytext_sync_test", + srcs = ["sync_tutorial_notebook.py"], + args = ["--check"], + data = TUTORIAL_FILES, + main = "sync_tutorial_notebook.py", + deps = ["@pypi//jupytext"], +) diff --git a/docs/sync_tutorial_notebook.py b/docs/sync_tutorial_notebook.py new file mode 100644 index 0000000..e0a310e --- /dev/null +++ b/docs/sync_tutorial_notebook.py @@ -0,0 +1,143 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Synchronizes the paired Jupytext tutorial notebook.""" + +import argparse +import difflib +import json +import os +from pathlib import Path +import shutil +import sys +import tempfile + +from jupytext.cli import jupytext + +FORMATS = "ipynb,py:percent" +NOTEBOOK_PATH = Path("docs/tutorial.ipynb") +SOURCE_PATH = Path("docs/tutorial.py") + + +def _run_jupytext(args: list[str], cwd: Path) -> None: + old_cwd = Path.cwd() + try: + os.chdir(cwd) + exit_code = jupytext(args) + except SystemExit as ex: + exit_code = ex.code if isinstance(ex.code, int) else 1 + finally: + os.chdir(old_cwd) + + if exit_code: + raise RuntimeError(f"jupytext failed with exit code {exit_code}: {' '.join(args)}") + + +def _write_notebook_from_source(root: Path) -> None: + if not (root / SOURCE_PATH).exists(): + _run_jupytext( + [ + str(NOTEBOOK_PATH), + "--set-formats", + FORMATS, + "--quiet", + ], + root, + ) + return + + _run_jupytext( + [ + str(SOURCE_PATH), + "--to", + "ipynb", + "--update", + "--output", + str(NOTEBOOK_PATH), + "--quiet", + ], + root, + ) + + +def _canonical_json(path: Path) -> list[str]: + return json.dumps( + json.loads(path.read_text()), + indent=2, + sort_keys=True, + ).splitlines(keepends=True) + + +def _check_pair() -> int: + docs_dir = Path(__file__).resolve().parent + source = docs_dir / SOURCE_PATH.name + notebook = docs_dir / NOTEBOOK_PATH.name + + if not source.exists(): + sys.stderr.write(f"{SOURCE_PATH} is missing; run `bazel run //docs:sync_tutorial_notebook -- --write`.\n") + return 1 + + with tempfile.TemporaryDirectory() as tmp: + tmp_root = Path(tmp) + tmp_docs = tmp_root / "docs" + tmp_docs.mkdir() + shutil.copy2(source, tmp_docs / SOURCE_PATH.name) + shutil.copy2(notebook, tmp_docs / NOTEBOOK_PATH.name) + + _write_notebook_from_source(tmp_root) + + expected = _canonical_json(notebook) + actual = _canonical_json(tmp_docs / NOTEBOOK_PATH.name) + if expected != actual: + sys.stderr.write( + f"{NOTEBOOK_PATH} is not synchronized with {SOURCE_PATH}. " + "Run `bazel run //docs:sync_tutorial_notebook -- --write`.\n" + ) + sys.stderr.writelines( + difflib.unified_diff( + expected, + actual, + fromfile=str(NOTEBOOK_PATH), + tofile="generated tutorial.ipynb", + ) + ) + return 1 + + return 0 + + +def _write_pair() -> int: + workspace = os.environ.get("BUILD_WORKSPACE_DIRECTORY") + if not workspace: + sys.stderr.write("--write must be run with `bazel run //docs:sync_tutorial_notebook -- --write`.\n") + return 1 + + _write_notebook_from_source(Path(workspace)) + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("--check", action="store_true", help="Check that the paired tutorial files are synchronized.") + mode.add_argument("--write", action="store_true", help="Synchronize docs/tutorial.ipynb from docs/tutorial.py.") + args = parser.parse_args() + + if args.write: + return _write_pair() + return _check_pair() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index 77bd3fc..e09f3a9 100644 --- a/docs/tutorial.ipynb +++ b/docs/tutorial.ipynb @@ -553,7 +553,8 @@ " 'num_errors': num_errors,\n", " 'num_shots': len(dets),\n", " 'time_seconds': end_time - start_time,\n", - " }\n" + " }\n", + "\n" ] }, { @@ -1032,7 +1033,8 @@ " bits = body.strip().split()\n", " row = [int(b) for b in bits]\n", " rows.append(row)\n", - " return np.array(rows, dtype=np.uint8)\n" + " return np.array(rows, dtype=np.uint8)\n", + "\n" ] }, { @@ -1403,6 +1405,7 @@ "base_uri": "https://localhost:8080/" }, "id": "ZNKaqvN8dE-X", + "lines_to_next_cell": 2, "outputId": "34ccf8b2-a1fc-4f1c-e1d9-1ef3f99d594e" }, "outputs": [ @@ -1412,12 +1415,14 @@ "text": [ " % Total % Received % Xferd Average Speed Time Time Time Current\n", " Dload Upload Total Spent Left Speed\n", - "\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r100 44154 100 44154 0 0 238k 0 --:--:-- --:--:-- --:--:-- 239k\n" + "\r", + " 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r", + "100 44154 100 44154 0 0 238k 0 --:--:-- --:--:-- --:--:-- 239k\n" ] } ], "source": [ - "!curl 'https://raw.githubusercontent.com/quantumlib/tesseract-decoder/refs/heads/main/testdata/colorcodes/r%3D9%2Cd%3D9%2Cp%3D0.002%2Cnoise%3Dsi1000%2Cc%3Dsuperdense_color_code_X%2Cq%3D121%2Cgates%3Dcz.stim' > d9r9colorcode_p002.stim\n" + "!curl 'https://raw.githubusercontent.com/quantumlib/tesseract-decoder/refs/heads/main/testdata/colorcodes/r%3D9%2Cd%3D9%2Cp%3D0.002%2Cnoise%3Dsi1000%2Cc%3Dsuperdense_color_code_X%2Cq%3D121%2Cgates%3Dcz.stim' > d9r9colorcode_p002.stim" ] }, { @@ -1620,6 +1625,10 @@ "colab": { "provenance": [] }, + "jupytext": { + "formats": "ipynb,py:percent", + "main_language": "python" + }, "kernelspec": { "display_name": "Python 3", "name": "python3" diff --git a/docs/tutorial.py b/docs/tutorial.py new file mode 100644 index 0000000..350d3c4 --- /dev/null +++ b/docs/tutorial.py @@ -0,0 +1,671 @@ +# --- +# jupyter: +# jupytext: +# formats: ipynb,py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.3 +# kernelspec: +# display_name: Python 3 +# name: python3 +# --- + +# %% [markdown] id="KBmkwvhmupn-" +# # Tesseract Tutorial +# +# - We will also, partly, explain how to use features of Stim and PyMatching +# - Stim is a dependency of Tesseract but you can also use other sources of data +# - This is not a comprehensive introduction. + +# %% [markdown] id="jaZcr-NevBSB" +# ## Installation + +# %% id="i6_88o7kKOVJ" +# !pip install --quiet --upgrade stim galois tesseract-decoder pymatching python-sat -U + +# %% [markdown] id="RLXXX3eMT_LR" +# ## Getting a Surface Code Circuit + +# %% id="8zcmVHFFUPq2" +import stim + +d = 11 +p = 0.005 +circuit = stim.Circuit.generated( + code_task="surface_code:rotated_memory_x", + distance=d, + rounds=d, + after_clifford_depolarization=p, + before_round_data_depolarization=p, + before_measure_flip_probability=p, + after_reset_flip_probability=p +) + +# %% [markdown] id="UBMIlXY9U30Y" +# ## Sample + +# %% id="GCkUlTJZU2T_" +sampler = circuit.compile_detector_sampler() + +num_shots = 10000 +detector_outcomes, actual_observables = sampler.sample(shots=num_shots, separate_observables=True) + +# %% [markdown] id="m9x8pivTVCir" +# ## Decode with uncorrelated matching + +# %% colab={"base_uri": "https://localhost:8080/"} id="-5W0AX8nVEyU" outputId="06a66e39-b33b-4a17-a1e1-2b91997b1d40" +import pymatching +import numpy as np + +dem = circuit.detector_error_model(decompose_errors=True) +matching = pymatching.Matching.from_detector_error_model(model=dem) +predicted_observables = matching.decode_batch(shots=detector_outcomes) +num_errors = np.sum(np.any(predicted_observables != actual_observables, axis=1)) + +print(f"Logical error rate: {num_errors}/{num_shots}") + +# %% [markdown] id="Xp7MyK0XVs_6" +# ## Decode with new correlated matching! + +# %% colab={"base_uri": "https://localhost:8080/"} id="vufQ8G5iVx7b" outputId="3b0517a3-e65e-42b7-eb25-fbb068c4a912" +dem = circuit.detector_error_model(decompose_errors=True) +matching_corr = pymatching.Matching.from_detector_error_model( + model=dem, enable_correlations=True + ) +predicted_observables_corr = matching_corr.decode_batch( + shots=detector_outcomes, + enable_correlations=True + ) +num_errors_corr = np.sum(np.any(predicted_observables_corr != actual_observables, axis=1)) + +print(f"Logical error rate: {num_errors_corr}/{num_shots}") + +# %% [markdown] id="a-AMqTUeuqOe" +# ## Getting a Color Code Circuit + +# %% colab={"base_uri": "https://localhost:8080/"} id="W7fU_MYJCRen" outputId="da1b1571-8160-440a-ce1a-f38de9db82e4" +# !curl 'https://raw.githubusercontent.com/quantumlib/tesseract-decoder/refs/heads/main/testdata/colorcodes/r%3D5%2Cd%3D5%2Cp%3D0.001%2Cnoise%3Dsi1000%2Cc%3Dsuperdense_color_code_Z%2Cq%3D37%2Cgates%3Dcz.stim' > d5r5colorcode_p001.stim + +# %% [markdown] id="E-vXEhbaTeQI" +# # Visualizing with Stim + +# %% colab={"base_uri": "https://localhost:8080/", "height": 343} id="2jTOVijwKPXm" outputId="b2b3ecc8-3491-44e7-e200-238dae9b7f36" +import stim + +circuit = stim.Circuit.from_file('d5r5colorcode_p001.stim') +circuit.diagram('timeline-3d') + +# %% [markdown] id="YDnwv2dacTbf" +# # Estimating code distance with Stim + +# %% colab={"base_uri": "https://localhost:8080/"} id="r2MFDDBMvkq3" outputId="6216021d-5506-4304-e4e5-d10ab170fff7" +distance_estimate = len(circuit.search_for_undetectable_logical_errors( + dont_explore_detection_event_sets_with_size_above=6, + dont_explore_edges_with_degree_above=3, + dont_explore_edges_increasing_symptom_degree=False)) +print(f'estimated distance: {distance_estimate}') + +# %% [markdown] id="UuYvEwq9cgYc" +# # Create DEM, detection events and observables with Stim + +# %% [markdown] id="6oCW5jRsXy8-" +# ### Can't decode with pymatching... + +# %% colab={"base_uri": "https://localhost:8080/"} id="x6TQbGZ7b06k" outputId="4767d883-cbc7-4b2c-a42e-e503d0dc6332" +import traceback + +try: + # decompose_errors=True needed for DEM to be matchable + circuit.detector_error_model(decompose_errors=True) +except: + traceback.print_exc() + +# %% [markdown] id="ye5W7BJHX8DJ" +# No need to decompose errors using tesseract: + +# %% id="AVu7idoTYAdM" +dem = circuit.detector_error_model() + +# %% id="vFDn06Xach0_" +num_shots = 1000 +sampler = circuit.compile_detector_sampler() +dets, obs = sampler.sample(num_shots, separate_observables=True) + +# %% [markdown] id="JrX13vNQcrm3" +# # Decoding with Tesseract and ILP decoder + +# %% id="Uds8S04a-z-G" +import tesseract_decoder +import tesseract_decoder.tesseract as tesseract +import numpy as np +import time + +# Helper functions for benchmarking + +def print_results(result): + print("Tesseract Decoder Stats:") + print(f" Number of Errors / num_shots: {results['num_errors']} / {results['num_shots']}") + print(f" Time: {results['time_seconds']:.4f} s") + print() + +def run_tesseract_decoder(decoder, dets, obs): + # Run and time the Tesseract decoder + num_errors = 0 + start_time = time.time() + obs_predicted = decoder.decode_batch(dets) + num_errors = np.sum(np.any(obs_predicted != obs, axis=1)) + end_time = time.time() + + return { + 'num_errors': num_errors, + 'num_shots': len(dets), + 'time_seconds': end_time - start_time, + } + + + +# %% colab={"base_uri": "https://localhost:8080/"} id="D0Tx2eY3ctFw" outputId="f419743f-f692-4490-fecc-6b7a831dc586" +# setup the tesseract decoder configuration +tesseract_config = tesseract.TesseractConfig( + dem=dem, + pqlimit=10000, + no_revisit_dets=True, + # verbose=True, + det_orders=tesseract_decoder.utils.build_det_orders( + dem, num_det_orders=1, + method=tesseract_decoder.utils.DetOrder.DetIndex, + seed=2384753), +) +print(f'Tesseract decoder configurations --> {tesseract_config}\n') + +tesseract_dec = tesseract_config.compile_decoder() + +results = run_tesseract_decoder(tesseract_dec, dets, obs) +print_results(results) + +# %% [markdown] id="INvMKs7zc5T_" +# #Decoding with ILP decoder + +# %% colab={"base_uri": "https://localhost:8080/"} id="9Npo7ibac4x5" outputId="c687e15b-e167-449b-9508-33701a9e1944" +simplex_config = tesseract_decoder.simplex.SimplexConfig( + dem=dem, parallelize=True +) +print(f'ILP decoder configurations --> {simplex_config}') +ilp_dec = simplex_config.compile_decoder() + +start_time = time.time() + +# Run and time ILP decoder -- so slow! +num_shots_to_decode = 10 # Only decoding 10 shots because it's soooo slow +obs_predicted = ilp_dec.decode_batch(dets[0:num_shots_to_decode]) +num_errors = np.sum(np.any(obs_predicted != obs[0:num_shots_to_decode], axis=1)) + +end_time = time.time() +print(f'ILP stats:\n Estimated time for full shots {num_shots/num_shots_to_decode * (end_time - start_time)} s') +print(f" Number of Errors / num_shots: {num_errors} / {num_shots_to_decode}") + +# %% [markdown] id="VQqlMqFRIZ2J" +# # Tesseract Config and impact of heuristic +# You can tune tesseract decoder through the Config that is passed to the decoder with this set of parameters: +# Explanation of configuration arguments: +# +# * `pqlimit` - An integer that sets a limit on the number of nodes in the priority queue. This can be used to constrain the memory usage of the decoder. The default value is `sys.maxsize`, which means the size is effectively unbounded. +# +# + +# %% colab={"base_uri": "https://localhost:8080/"} id="0pExdmuPQuGr" outputId="8e8ff3fd-bde9-4e31-d1e2-4f97c77ce43d" +tesseract_config1 = tesseract.TesseractConfig( + dem=dem, + pqlimit=100, + no_revisit_dets=True, + det_orders=tesseract_decoder.utils.build_det_orders( + dem, num_det_orders=5, + method=tesseract_decoder.utils.DetOrder.DetIndex, + seed=2384753), +) + +print ("Smaller pqlimit") +results = run_tesseract_decoder(tesseract_config1.compile_decoder(), dets, obs) +print_results(results) + + +tesseract_config2 = tesseract.TesseractConfig( + dem=dem, + pqlimit=20000, + no_revisit_dets=True, + det_orders=tesseract_decoder.utils.build_det_orders( + dem, num_det_orders=5, + method=tesseract_decoder.utils.DetOrder.DetIndex, + seed=2384753), +) +print ("Larger pqlimit") +results = run_tesseract_decoder(tesseract_config2.compile_decoder(), dets, obs) +print_results(results) + +# %% [markdown] id="ru-MRctAIq5-" +# #More heurisitcs +# * `det_beam` - This integer value represents the beam search cutoff. It specifies a threshold for the number of "residual detection events" a node can have before it is pruned from the search. A lower `det_beam` value makes the search more aggressive, potentially sacrificing accuracy for speed. The default value `INF_DET_BEAM` means no beam cutoff is applied. +# * `beam_climbing` - A boolean flag that, when set to `True`, enables a heuristic called "beam climbing." This optimization causes the decoder to try different `det_beam` values (up to a maximum) to find a good decoding path. This can improve the decoder's chance of finding the most likely error, even with an initial narrow beam search. +# * Try replacing `DetIndex` with `DetBFS` or `DetCoordinate` -- this enables different heuristics to assign the ordering of detectors for the traversal, leading to different results. +# + +# %% colab={"base_uri": "https://localhost:8080/"} id="cyctTUyzQ-cQ" outputId="8bc31b95-90a0-43d6-bd53-7176ec58b32f" +tesseract_config1 = tesseract.TesseractConfig( + dem=dem, + pqlimit=1000, + det_beam=3, + beam_climbing=True, + det_orders=tesseract_decoder.utils.build_det_orders( + dem, num_det_orders=5, + method=tesseract_decoder.utils.DetOrder.DetIndex, + seed=2384753), +) + +print ("Smaller det_beam") +results = run_tesseract_decoder(tesseract_config1.compile_decoder(), dets, obs) +print_results(results) + +tesseract_config2 = tesseract.TesseractConfig( + dem=dem, + pqlimit=1000, + det_beam=5, + beam_climbing=True, + det_orders=tesseract_decoder.utils.build_det_orders( + dem, num_det_orders=5, + method=tesseract_decoder.utils.DetOrder.DetIndex, + seed=2384753), +) +print ("Larger det_beam") +results = run_tesseract_decoder(tesseract_config2.compile_decoder(), dets, obs) +print_results(results) + +# %% [markdown] id="VJiBCWpUQ9sf" +# #Even More Heuristics +# * `no_revisit_dets` - A boolean flag that, when `True`, activates a heuristic to prevent the decoder from revisiting nodes that have the same set of leftover detection events as a node it has already visited. This can help to reduce search redundancy and improve decoding speed. +# +# * `det_orders` - A list of lists of integers, where each inner list represents an ordering of the detectors. This is used for "ensemble reordering," an optimization that tries different detector orderings to improve the search's convergence. The default is an empty list, meaning a single, fixed ordering is used. +# * `det_penalty` - A floating-point value that adds a cost for each residual detection event. This encourages the decoder to prioritize paths that resolve more detection events, steering the search towards more complete solutions. The default value is `0.0`, meaning no penalty is applied. + +# %% colab={"base_uri": "https://localhost:8080/"} id="0VrW2z8sSXtN" outputId="de264ea5-becb-46f5-f98b-644448fbb773" +tesseract_config1 = tesseract.TesseractConfig( + dem=dem, + pqlimit=1000, + # no_revisit_dets=True, + det_penalty = 10, + # det_orders=tesseract_decoder.utils.build_det_orders( + # dem, num_det_orders=2, + # method=tesseract_decoder.utils.DetOrder.DetIndex, + # seed=2384753), +) + +print ("First version") +results = run_tesseract_decoder(tesseract_config1.compile_decoder(), dets, obs) +print_results(results) + + +tesseract_config2 = tesseract.TesseractConfig( + dem=dem, + pqlimit=1000, + # no_revisit_dets=False, + det_penalty = False, + # det_orders=tesseract_decoder.utils.build_det_orders( + # dem, num_det_orders=2, + # method=tesseract_decoder.utils.DetOrder.DetIndex, + # seed=2384753), +) +print ("Second version") +results = run_tesseract_decoder(tesseract_config2.compile_decoder(), dets, obs) +print_results(results) + +# %% [markdown] id="BoEALeo3OYGp" +# # Decoding Wild Stabilizer Codes under Code Capacity Noise with Tesseract +# +# +# +# * checkout https://www.codetables.de/ for a qubit stabilizer code +# * full table of qubit codes: [here](https://codetables.de/QECC/Tables_color.php?q=4&n0=1&n1=256&k0=0&k1=256) +# * copy the stabilizer matrix for a code, such as: [this one used below](https://codetables.de/QECC/QECC.php?q=4&n=21&k=8) +# +# + +# %% id="pJ1gEKAgPbHO" +import time +import tesseract_decoder +import stim +import numpy as np +import numpy.typing as npt +from galois import GF2 +from typing import List, Tuple + + +def paulis_from_symplectic_matrix(check_matrix: npt.NDArray[np.uint8]) -> List[stim.PauliString]: + n = check_matrix.shape[1] // 2 + paulis = [] + for i in range(check_matrix.shape[0]): + paulis.append( + stim.PauliString.from_numpy( + xs=check_matrix[i, :n].astype(bool), zs=check_matrix[i, n:].astype(bool) + ) + ) + return paulis + +def rank(H): + return np.linalg.matrix_rank(GF2(H)) + +def stabilizer_code_logical_operators( + check_matrix: npt.NDArray[np.uint8]) -> Tuple[npt.NDArray[np.uint8], npt.NDArray[np.uint8]]: + check_matrix = np.array(check_matrix, dtype=np.uint8) + + r = rank(check_matrix) + n = check_matrix.shape[1] // 2 + + stabilisers = paulis_from_symplectic_matrix(check_matrix=check_matrix) + + tableau = stim.Tableau.from_stabilizers( + stabilizers=stabilisers, allow_underconstrained=True, allow_redundant=True + ) + + x2x, x2z, z2x, z2z, x_signs, z_signs = tableau.to_numpy() + + num_logicals = n - r + + Lx = np.zeros((num_logicals, check_matrix.shape[1]), dtype=np.uint8) + Lz = np.zeros((num_logicals, check_matrix.shape[1]), dtype=np.uint8) + + Lx[:, :n] = x2x[r:] + Lx[:, n:] = x2z[r:] + Lz[:, :n] = z2x[r:] + Lz[:, n:] = z2z[r:] + return Lx.astype(np.uint8), Lz.astype(np.uint8) + + +def pauli_to_observable_include_target(pauli: stim.PauliString) -> List[stim.GateTarget]: + obs_pauli_targets = [] + for i in range(len(pauli)): + if pauli[i] != 0: + obs_pauli_targets.append(stim.target_pauli(i, pauli[i])) + return obs_pauli_targets + + +def append_observable_includes_for_paulis(circuit: stim.Circuit, paulis: List[stim.PauliString]) -> None: + for i, obs in enumerate(paulis): + circuit.append( + "OBSERVABLE_INCLUDE", + targets=pauli_to_observable_include_target(pauli=obs), + arg=i + ) + + +def code_capacity_circuit( + stabilizers: npt.NDArray[np.uint8], + x_logicals: npt.NDArray[np.uint8], + z_logicals: npt.NDArray[np.uint8], + p: float +) -> stim.Circuit: + """Generate a code capacity stim circuit for a stabilizer code + + Parameters + ---------- + stabilizers : npt.NDArray[np.uint8] + The stabilizer generators of the code, as a binary symplectic matrix. + The matrix has dimensions (r, 2 * n) where r is the number of stabilizer + generators and n is the number of physical qubits. + `stabilizers[i, j]` is 1 if stabilizer i is X or Y on qubit j and 0 otherwise. + `stabilizers[i, n + j]` is 1 if stabilizer i is Z or Y on qubit j and 0 otherwise. + x_logicals : npt.NDArray[np.uint8] + The X logical operators of the code, as a binary symplectic matrix. + The matrix has dimensions (k, 2 * n) where k is the number of logical qubits + and n is the number of physical qubits. + z_logicals : npt.NDArray[np.uint8] + The Z logical operators of the code, as a binary symplectic matrix. + The matrix has dimensions (k, 2 * n) where k is the number of logical qubits + and n is the number of physical qubits. + p : float + The strength of single-qubit depolarizing noise to use + + Returns + ------- + stim.Circuit + The stim circuit of the code capacity circuit + """ + num_qubits = stabilizers.shape[1] // 2 + num_stabilizers = stabilizers.shape[0] + stabilizer_paulis = paulis_from_symplectic_matrix(stabilizers) + x_logicals_paulis = paulis_from_symplectic_matrix(x_logicals) + z_logicals_paulis = paulis_from_symplectic_matrix(z_logicals) + all_logicals_paulis = x_logicals_paulis + z_logicals_paulis + + circuit = stim.Circuit() + + append_observable_includes_for_paulis( + circuit=circuit, paulis=all_logicals_paulis) + circuit.append("MPP", stabilizer_paulis) + circuit.append("DEPOLARIZE1", targets=list(range(num_qubits)), arg=p) + circuit.append("MPP", stabilizer_paulis) + + for i in range(num_stabilizers): + circuit.append( + "DETECTOR", + targets=[ + stim.target_rec(i - 2 * num_stabilizers), + stim.target_rec(i - num_stabilizers) + ] + ) + + append_observable_includes_for_paulis( + circuit=circuit, paulis=all_logicals_paulis) + return circuit + + +def parse_symplectic_matrix(text: str) -> npt.NDArray[np.uint8]: + rows = [] + for line in text.strip().splitlines(): + line = line.strip() + if not line or line[0] != '[' or line[-1] != ']': + continue # skip malformed lines + body = line[1:-1] + if "|" in body: + left, right = body.split("|") + bits = left.strip().split() + right.strip().split() + else: + bits = body.strip().split() + row = [int(b) for b in bits] + rows.append(row) + return np.array(rows, dtype=np.uint8) + + + +# %% colab={"base_uri": "https://localhost:8080/", "height": 343} id="pH_b3u1rBogl" outputId="8b775afe-a447-4856-f698-ab87ff752dfc" +# Example QEC code: +text = '''[1 0 0 0 0 1 1 0 1 0 1 1 0 0 1 1 1 0 0 0 0|0 0 0 0 0 1 1 1 0 0 1 1 0 0 1 0 1 0 0 0 0] + [0 0 0 0 0 1 1 1 0 0 1 1 0 0 1 1 1 0 0 0 0|1 0 0 0 1 0 0 1 1 0 0 0 0 1 1 0 0 0 0 0 0] + [0 1 0 0 0 1 1 0 1 1 0 1 1 0 1 0 1 0 0 0 0|0 0 0 0 1 1 0 1 0 0 1 0 1 1 0 1 0 0 0 0 0] + [0 0 0 0 0 1 0 1 0 0 1 0 1 0 1 0 0 0 0 0 0|0 1 0 0 0 0 1 1 1 1 1 1 0 0 0 1 1 0 0 0 0] + [0 0 1 0 0 0 0 1 1 1 0 1 1 1 0 1 1 0 0 0 0|0 0 0 0 0 0 0 1 1 0 1 0 0 1 0 0 1 0 0 0 0] + [0 0 0 0 0 0 0 1 1 0 1 0 0 1 0 1 1 0 0 0 0|0 0 1 0 1 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0] + [0 0 0 1 0 1 0 1 0 1 1 0 1 1 0 1 1 0 0 0 0|0 0 0 0 1 1 1 0 0 1 1 0 0 1 1 0 0 0 0 0 0] + [0 0 0 0 0 1 1 0 0 1 1 0 0 0 0 1 0 0 0 0 0|0 0 0 1 0 0 1 1 0 0 0 0 1 1 0 1 1 0 0 0 0] + [0 0 0 0 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0|0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0] + [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0|0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] + [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0|0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] + [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0|0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] + [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1|0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]''' + +H = parse_symplectic_matrix(text) + +LX, LZ = stabilizer_code_logical_operators(check_matrix=H) + +circuit = code_capacity_circuit( + stabilizers=H, + x_logicals=LX, + z_logicals=LZ, + p=0.025 +) + +circuit.diagram('timeline-3d') + +# %% [markdown] id="cK2Mf2fTCAWO" +# ## Computing minimum distance with Stim + SAT Solver + +# %% colab={"base_uri": "https://localhost:8080/"} id="ZdVK4Dq1Bp1B" outputId="84834d1f-6d5a-4dc3-b06e-6898c689c355" +# Note: this maxSAT solver only works for very small codes. +# For larger codes, use the solvers at https://maxsat-evaluations.github.io/2024/ +from pysat.examples.rc2 import RC2 +from pysat.formula import WCNF + +wcnf = WCNF(from_string=circuit.shortest_error_sat_problem()) + +with RC2(wcnf) as rc2: + rc2.compute() + print(f'Distance of code: {rc2.cost}') + +# %% [markdown] id="GQjQkhD4C4rK" +# ## Sample new data for this stabilizer code + +# %% id="7iOIl7vjC3uG" +num_shots = 1000 +dem = circuit.detector_error_model() +sampler = circuit.compile_detector_sampler(seed=23845386) +dets, obs = sampler.sample(num_shots, separate_observables=True) + +# %% [markdown] id="63xjagbBCj8x" +# ## Decode code capacity noise data with ILP and Tesseract + +# %% colab={"base_uri": "https://localhost:8080/"} id="IM7W37cHaKfT" outputId="eb33df9c-30bd-44c5-85f3-ce0919cb8c47" +tesseract_config = tesseract_decoder.tesseract.TesseractConfig( + dem=dem, + pqlimit=1000, + det_beam=10, + det_orders=tesseract_decoder.utils.build_det_orders( + dem, num_det_orders=10, + method=tesseract_decoder.utils.DetOrder.DetIndex, + seed=2384753), + # no_revisit_dets=True, +) + +results = run_tesseract_decoder(tesseract_config.compile_decoder(), dets, obs) +print_results(results) + +# Run and time ILP decoder +ilp_dec = tesseract_decoder.simplex.SimplexConfig( + dem=dem, parallelize=True).compile_decoder() +start_time = time.time() +obs_predicted = ilp_dec.decode_batch(dets) +num_errors_ilp = np.sum(np.any(obs_predicted != obs, axis=1)) +end_time = time.time() +print( + f'ILP: num_errors / num_shots = {num_errors_ilp} / {len(dets)} time {end_time - start_time} s') + +# %% [markdown] id="K0QvSpXQIwgf" +# # Visualize the Tesseract's decoding +# For visualizing tesseract we use the `verbose` flag to get the decoding information. +# ## [Link to visualizer](https://quantumlib.github.io/tesseract-decoder/viz/) +# * `verbose` - A boolean flag that, when `True`, enables verbose logging. This is useful for debugging and understanding the decoder's internal behavior, as it will print information about the search process. + +# %% colab={"base_uri": "https://localhost:8080/"} id="DzWRL1cNjyix" outputId="ef6600a9-1ade-4208-ce50-fc77b6b00db7" +# Remove the existing directory and its contents +# !rm -rf tesseract-decoder +# Clone the repository +# !git clone https://github.com/quantumlib/tesseract-decoder.git + +# %% colab={"base_uri": "https://localhost:8080/"} id="ZNKaqvN8dE-X" outputId="34ccf8b2-a1fc-4f1c-e1d9-1ef3f99d594e" +# !curl 'https://raw.githubusercontent.com/quantumlib/tesseract-decoder/refs/heads/main/testdata/colorcodes/r%3D9%2Cd%3D9%2Cp%3D0.002%2Cnoise%3Dsi1000%2Cc%3Dsuperdense_color_code_X%2Cq%3D121%2Cgates%3Dcz.stim' > d9r9colorcode_p002.stim + + +# %% id="Cdo-oenEdF1-" +import stim + +circuit = stim.Circuit.from_file('d9r9colorcode_p002.stim') + +# %% colab={"base_uri": "https://localhost:8080/"} id="awJYxAOMTc3t" outputId="3ee82ddf-8cb0-4a31-ef95-8d7f3b090c33" +import tesseract_decoder +import tesseract_decoder.tesseract as tesseract +import numpy as np +import time +import contextlib +import io + +num_shots = 100 +dem = circuit.detector_error_model() +dets, obs = circuit.compile_detector_sampler().sample(num_shots, separate_observables=True) + +tesseract_config1 = tesseract.TesseractConfig( + dem=dem, + pqlimit=10000, + verbose=False, + create_visualization=True, + det_orders=tesseract_decoder.utils.build_det_orders( + dem, num_det_orders=2, + method=tesseract_decoder.utils.DetOrder.DetIndex, + seed=2384753), +) + +tesseract_dec = tesseract.TesseractDecoder(tesseract_config1) + +# Run and time the Tesseract decoder +num_errors = 0 +start_time = time.time() +for shot in range(len(dets)): + obs_predicted = tesseract_dec.decode(dets[shot]) + obs_actual = obs[shot] + if np.any(obs_predicted != obs_actual): + num_errors += 1 +end_time = time.time() +print(f'Tesseract: num_errors / num_shots = {num_errors} / {len(dets)} \n time {end_time - start_time} s') + +# Print with the visualizer +tesseract_dec.visualizer.write('/content/tmp.txt') + +# %% id="MuQb8XQlpvU6" +# !cat tmp.txt | grep -E 'Error|Detector|activated_errors|activated_detectors' > logfile.txt + +# %% colab={"base_uri": "https://localhost:8080/"} id="WExtQ3x4j_Md" outputId="7c5f2373-5ecb-450c-ad3f-c9915458d9a6" +# !python tesseract-decoder/viz/to_json.py logfile.txt -o logfile.json + +# %% [markdown] id="HSdTwXBINjkH" +# copy the json file and upload it [here to see the visualizaion](https://quantumlib.github.io/tesseract-decoder/viz/) + +# %% [markdown] id="QehTGJcB7-Ca" +# # Accuracy Comparison between Tesseract and ILP + +# %% colab={"base_uri": "https://localhost:8080/"} id="GOY0hHYx79HC" outputId="a1a35b20-255f-40ab-d71a-202b7ad9edf3" +circuit = stim.Circuit.from_file('d5r5colorcode_p001.stim') +dem = circuit.detector_error_model() + +tesseract_dec = tesseract_decoder.tesseract.TesseractConfig( + dem=dem, + pqlimit=10000, + det_beam=5, + det_orders=tesseract_decoder.utils.build_det_orders( + dem, num_det_orders=10, + method=tesseract_decoder.utils.DetOrder.DetIndex, + seed=2384753), + no_revisit_dets=True, +).compile_decoder() + +ilp_dec = tesseract_decoder.simplex.SimplexConfig( + dem=dem, parallelize=True).compile_decoder() + +num_shots = 1000 +dets, obs = circuit.compile_detector_sampler(seed=237435).sample(num_shots, separate_observables=True) + +num_errors_tesseract = 0 +num_errors_tesseract_no_error_ilp = 0 +start_time = time.time() +for shot in range(len(dets)): + obs_predicted = tesseract_dec.decode(dets[shot]) + obs_actual = obs[shot] + if np.any(obs_predicted != obs_actual): + num_errors_tesseract += 1 + obs_predicted_ilp = ilp_dec.decode(dets[shot]) + if not np.any(obs_predicted_ilp != obs_actual): + num_errors_tesseract_no_error_ilp += 1 + +end_time = time.time() +print(f'Tesseract: num_errors / num_shots = {num_errors_tesseract} / {len(dets)}') +print(f'num_errors_tesseract_no_error_ilp = {num_errors_tesseract_no_error_ilp}') +print(f'time {end_time - start_time} s') + +# %% id="CijALG4TXFaP" diff --git a/src/py/BUILD b/src/py/BUILD index e0bf9d8..7160760 100644 --- a/src/py/BUILD +++ b/src/py/BUILD @@ -139,4 +139,5 @@ compile_pip_requirements( name = "requirements", src = "requirements.in", requirements_txt = "requirements_lock.txt", + timeout = "long", ) diff --git a/src/py/requirements.in b/src/py/requirements.in index 9b52cb5..006da5b 100644 --- a/src/py/requirements.in +++ b/src/py/requirements.in @@ -5,3 +5,4 @@ stim pytest sinter pybind11-stubgen +jupytext diff --git a/src/py/requirements_lock.txt b/src/py/requirements_lock.txt index 46e8950..b0853c3 100644 --- a/src/py/requirements_lock.txt +++ b/src/py/requirements_lock.txt @@ -4,6 +4,12 @@ # # bazel run //src/py:requirements.update # +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 + # via + # jsonschema + # referencing contourpy==1.3.3 \ --hash=sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69 \ --hash=sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc \ @@ -82,6 +88,10 @@ cycler==0.12.1 \ --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c # via matplotlib +fastjsonschema==2.21.2 \ + --hash=sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 \ + --hash=sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de + # via nbformat fonttools==4.59.0 \ --hash=sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de \ --hash=sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0 \ @@ -130,6 +140,22 @@ iniconfig==2.1.0 \ --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \ --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 # via pytest +jsonschema==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + # via nbformat +jsonschema-specifications==2025.9.1 \ + --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ + --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d + # via jsonschema +jupyter-core==5.9.1 \ + --hash=sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508 \ + --hash=sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407 + # via nbformat +jupytext==1.19.3 \ + --hash=sha256:713c3ed4441afe0f31474d28ea2e6b61a268c04c40fd78e5ccfd7f7ac9e9f766 \ + --hash=sha256:acf75492f80895ad8e664fd8db1708b617008dd0e71c341a1abc3d0d07310ed0 + # via -r src/py/requirements.in kiwisolver==1.4.9 \ --hash=sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c \ --hash=sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7 \ @@ -233,6 +259,12 @@ kiwisolver==1.4.9 \ --hash=sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1 \ --hash=sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220 # via matplotlib +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + # via + # jupytext + # mdit-py-plugins matplotlib==3.10.7 \ --hash=sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f \ --hash=sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67 \ @@ -290,6 +322,18 @@ matplotlib==3.10.7 \ --hash=sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8 \ --hash=sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7 # via sinter +mdit-py-plugins==0.6.1 \ + --hash=sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d \ + --hash=sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0 + # via jupytext +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +nbformat==5.10.4 \ + --hash=sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a \ + --hash=sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b + # via jupytext numpy==2.2.6 \ --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \ --hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \ @@ -356,6 +400,7 @@ packaging==26.0 \ --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 # via + # jupytext # matplotlib # pytest pillow==12.0.0 \ @@ -451,6 +496,10 @@ pillow==12.0.0 \ --hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \ --hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1 # via matplotlib +platformdirs==4.10.0 \ + --hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \ + --hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a + # via jupyter-core pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 @@ -475,6 +524,221 @@ python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via matplotlib +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via jupytext +referencing==0.37.0 \ + --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ + --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 + # via + # jsonschema + # jsonschema-specifications +rpds-py==2026.5.1 \ + --hash=sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead \ + --hash=sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a \ + --hash=sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4 \ + --hash=sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256 \ + --hash=sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb \ + --hash=sha256:0a5ae4dbe43c1076983b72616496919872ae7bbe7a1e21cc48336bc3154d130b \ + --hash=sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870 \ + --hash=sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc \ + --hash=sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08 \ + --hash=sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251 \ + --hash=sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473 \ + --hash=sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b \ + --hash=sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a \ + --hash=sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131 \ + --hash=sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9 \ + --hash=sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01 \ + --hash=sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba \ + --hash=sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad \ + --hash=sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db \ + --hash=sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d \ + --hash=sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0 \ + --hash=sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63 \ + --hash=sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee \ + --hash=sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7 \ + --hash=sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b \ + --hash=sha256:3397a5ed7174dc2786bb214030232fc36fe8e5584fec43a9952cc542b1a12036 \ + --hash=sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb \ + --hash=sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16 \ + --hash=sha256:3684a59b158a7683aaeb8e25352e9a9dd2122cec78f2d8530266e4f91b4c7b3f \ + --hash=sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d \ + --hash=sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d \ + --hash=sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5 \ + --hash=sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78 \ + --hash=sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66 \ + --hash=sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972 \ + --hash=sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd \ + --hash=sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89 \ + --hash=sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732 \ + --hash=sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02 \ + --hash=sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef \ + --hash=sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a \ + --hash=sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c \ + --hash=sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723 \ + --hash=sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda \ + --hash=sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7 \ + --hash=sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca \ + --hash=sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02 \ + --hash=sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015 \ + --hash=sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1 \ + --hash=sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed \ + --hash=sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00 \ + --hash=sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a \ + --hash=sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195 \ + --hash=sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a \ + --hash=sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa \ + --hash=sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece \ + --hash=sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df \ + --hash=sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26 \ + --hash=sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa \ + --hash=sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842 \ + --hash=sha256:7bd530e6a530bb3ea892f194fafa455f3516ac25ecf7143fd33c09be62b0470a \ + --hash=sha256:8213afbe8a3a906fb9acb2014423fe3359ee783d0bf90995f70623a3217bfa6c \ + --hash=sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd \ + --hash=sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a \ + --hash=sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf \ + --hash=sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2 \ + --hash=sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f \ + --hash=sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf \ + --hash=sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049 \ + --hash=sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3 \ + --hash=sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964 \ + --hash=sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291 \ + --hash=sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14 \ + --hash=sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc \ + --hash=sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47 \ + --hash=sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5 \ + --hash=sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d \ + --hash=sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb \ + --hash=sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df \ + --hash=sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a \ + --hash=sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc \ + --hash=sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc \ + --hash=sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46 \ + --hash=sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb \ + --hash=sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2 \ + --hash=sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e \ + --hash=sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb \ + --hash=sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec \ + --hash=sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325 \ + --hash=sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600 \ + --hash=sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559 \ + --hash=sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41 \ + --hash=sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644 \ + --hash=sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b \ + --hash=sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162 \ + --hash=sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83 \ + --hash=sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038 \ + --hash=sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6 \ + --hash=sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b \ + --hash=sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3 \ + --hash=sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9 \ + --hash=sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34 \ + --hash=sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6 \ + --hash=sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb \ + --hash=sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa \ + --hash=sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6 \ + --hash=sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d \ + --hash=sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24 \ + --hash=sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838 \ + --hash=sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164 \ + --hash=sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97 \ + --hash=sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4 \ + --hash=sha256:df1d2a1996755b24b9ecee92cb4d36c28f86f464a6a173349c26bab41e94b8c2 \ + --hash=sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55 \ + --hash=sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3 \ + --hash=sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2 \ + --hash=sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358 \ + --hash=sha256:e4abbf391a70be864920858bf360f4fb380577c9a0f732438a1996726e2c195b \ + --hash=sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8 \ + --hash=sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0 \ + --hash=sha256:edf2765d84e42447f112ad877af8fe1db0089aaec5b28e88d6eab45e7fe99cea \ + --hash=sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081 \ + --hash=sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d \ + --hash=sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1 \ + --hash=sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81 \ + --hash=sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3 \ + --hash=sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8 \ + --hash=sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1 \ + --hash=sha256:fe71bca7d547acb17027c7fd1624ff8aae623499c498d3e7011182c4de5c25e0 \ + --hash=sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd + # via + # jsonschema + # referencing scipy==1.16.3 \ --hash=sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2 \ --hash=sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb \ @@ -584,3 +848,9 @@ stim==1.15.0 \ # via # -r src/py/requirements.in # sinter +traitlets==5.15.1 \ + --hash=sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92 \ + --hash=sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722 + # via + # jupyter-core + # nbformat