From 99de4ef90244171a921f68bcb927c31b32848413 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sat, 25 Jul 2026 18:06:07 -0700 Subject: [PATCH 01/43] Add validated GARI matrix transformation --- src/py/_tesseract_py_util/BUILD | 31 +- src/py/_tesseract_py_util/gari.py | 442 +++++++++++++++++++++++++ src/py/_tesseract_py_util/gari_test.py | 261 +++++++++++++++ 3 files changed, 733 insertions(+), 1 deletion(-) create mode 100644 src/py/_tesseract_py_util/gari.py create mode 100644 src/py/_tesseract_py_util/gari_test.py diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index 59f2131f..f216f467 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -3,7 +3,13 @@ load("@rules_python//python:py_library.bzl", "py_library") py_library( name = "_tesseract_py_util", - srcs = glob(["*.py"], exclude=["*_test.py"]), + srcs = glob( + ["*.py"], + exclude = [ + "*_test.py", + "gari.py", + ], + ), visibility = ["//:__subpackages__"], deps = [ "@pypi//stim", @@ -11,6 +17,29 @@ py_library( ], ) +py_library( + name = "gari", + srcs = ["gari.py"], + visibility = ["//:__subpackages__"], + deps = [ + "@pypi//numpy", + "@pypi//scipy", + ], +) + +py_test( + name = "gari_test", + srcs = ["gari_test.py"], + imports = ["..", "."], + visibility = ["//:__subpackages__"], + deps = [ + ":gari", + "@pypi//numpy", + "@pypi//pytest", + "@pypi//scipy", + ], +) + py_test( name = "demutil_test", diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py new file mode 100644 index 00000000..3ba9a9ce --- /dev/null +++ b/src/py/_tesseract_py_util/gari.py @@ -0,0 +1,442 @@ +# 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. + +"""Graph augmentation and rewiring for inference (GARI). + +This module implements the matrix construction from A. S. Maan et al., +"Decoding correlated errors in quantum LDPC codes," Nature Communications 17, +3965 (2026), https://doi.org/10.1038/s41467-026-70556-3. + +For source columns ``e_Z``, ``e_X``, and ``e_Y``, the supported CSS check +matrix has the form + +:: + + e_Z e_X e_Y + +------------+------------+------------+ + X syndrome | D_X | 0 | D_X U | + +------------+------------+------------+ + Z syndrome | 0 | D_Z | D_Z V | + +------------+------------+------------+ + +over GF(2). GARI substitutes + +``bar(e)_Z = e_Z XOR U e_Y`` and ``bar(e)_X = e_X XOR V e_Y``. + +Columns are emitted as ``[e_Z, e_X, e_Y, bar(e)_Z, bar(e)_X]`` and rows as +``[physical X, physical Z, virtual Z, virtual X]``: + +:: + + e_Z e_X e_Y bar(e)_Z bar(e)_X + +----+----+----+---------+---------+ + physical X syndrome | 0 | 0 | 0 | D_X | 0 | + physical Z syndrome | 0 | 0 | 0 | 0 | D_Z | + virtual Z constraint | I | 0 | U | I | 0 | + virtual X constraint | 0 | I | V | 0 | I | + +----+----+----+---------+---------+ + +The corresponding decoder syndrome is ``[s_X, s_Z, 0, 0]``. The logical map +stays on the original physical variables: +``[L_eZ, L_eX, L_eY, 0, 0]``. The augmented system is a decoder model with +structural variables; it is not a physical noise model to sample directly. + +Every pure ``e_Z`` and ``e_X`` column receives a barred counterpart, including +columns that are not the projection of any ``e_Y`` column. Such an unmatched +column has an all-zero row in ``U`` or ``V``; its virtual identity constraint +therefore only copies ``e`` to ``bar(e)``. This deliberate redundancy keeps the +five-block structure uniform and the physical top-left blocks zero. +""" + +from __future__ import annotations + +import dataclasses +import numbers +from collections.abc import Sequence + +import numpy as np +import scipy.sparse + + +@dataclasses.dataclass(frozen=True) +class GariTransform: + """A validated GARI augmented check system.""" + + checks: scipy.sparse.csc_matrix + logicals: scipy.sparse.csc_matrix + u: scipy.sparse.csc_matrix + v: scipy.sparse.csc_matrix + e_z_columns: np.ndarray + e_x_columns: np.ndarray + e_y_columns: np.ndarray + source_to_gari_detectors: np.ndarray + physical_x_rows: slice + physical_z_rows: slice + virtual_z_rows: slice + virtual_x_rows: slice + + +def _canonical_binary_csc( + matrix: scipy.sparse.spmatrix, *, name: str +) -> scipy.sparse.csc_matrix: + if not scipy.sparse.issparse(matrix): + raise ValueError(f"{name} must be a sparse matrix.") + if matrix.ndim != 2: + raise ValueError(f"{name} must be two-dimensional.") + + coordinate_matrix = matrix.tocoo(copy=True) + stored_values = np.asarray(coordinate_matrix.data) + if stored_values.size and not np.all(np.isfinite(stored_values)): + raise ValueError(f"{name} must contain only finite binary values.") + is_binary = (stored_values == 0) | (stored_values == 1) + if stored_values.size and not np.all(is_binary): + bad_value = stored_values[np.flatnonzero(~is_binary)[0]] + raise ValueError( + f"{name} must contain only binary values 0 or 1; found " + f"{bad_value!r}." + ) + + result = scipy.sparse.coo_matrix( + ( + stored_values.astype(np.int64), + (coordinate_matrix.row, coordinate_matrix.col), + ), + shape=coordinate_matrix.shape, + dtype=np.int64, + ).tocsc() + result.sum_duplicates() + duplicate_sum_is_binary = (result.data == 0) | (result.data == 1) + if result.data.size and not np.all(duplicate_sum_is_binary): + bad_value = result.data[np.flatnonzero(~duplicate_sum_is_binary)[0]] + raise ValueError( + f"{name} must be canonical after combining duplicate entries; " + f"found stored value {bad_value!r}." + ) + result.eliminate_zeros() + result.sort_indices() + return result.astype(np.uint8) + + +def _validated_detector_indices( + detectors: Sequence[int], *, name: str, detector_count: int +) -> np.ndarray: + try: + values = list(detectors) + except TypeError as ex: + raise ValueError(f"{name} must be a one-dimensional sequence.") from ex + + result: list[int] = [] + seen: dict[int, int] = {} + for position, value in enumerate(values): + if isinstance(value, (bool, np.bool_)) or not isinstance( + value, numbers.Integral + ): + raise ValueError( + f"{name}[{position}] must be an integer detector index; " + f"found {value!r}." + ) + index = int(value) + if index < 0 or index >= detector_count: + raise ValueError( + f"{name}[{position}] = {index} is outside the detector " + f"range [0, {detector_count})." + ) + if index in seen: + raise ValueError( + f"{name} contains detector {index} more than once " + f"(positions {seen[index]} and {position})." + ) + seen[index] = position + result.append(index) + return np.asarray(result, dtype=np.int64) + + +def _column_support( + matrix: scipy.sparse.csc_matrix, column: int +) -> tuple[int, ...]: + start = matrix.indptr[column] + stop = matrix.indptr[column + 1] + return tuple(int(v) for v in matrix.indices[start:stop]) + + +def _projection_lookup( + projections: scipy.sparse.csc_matrix, + source_columns: np.ndarray, + *, + name: str, +) -> dict[tuple[int, ...], tuple[int, int]]: + lookup: dict[tuple[int, ...], tuple[int, int]] = {} + for local_column, source_column in enumerate(source_columns): + support = _column_support(projections, local_column) + if support in lookup: + _, previous_source_column = lookup[support] + raise ValueError( + f"{name} has duplicate columns from source columns " + f"{previous_source_column} and {int(source_column)}." + ) + lookup[support] = (local_column, int(source_column)) + return lookup + + +def _gf2_product( + left: scipy.sparse.csc_matrix, right: scipy.sparse.csc_matrix +) -> scipy.sparse.csc_matrix: + product = (left @ right).tocsc() + product.sum_duplicates() + product.data %= 2 + product.eliminate_zeros() + product.sort_indices() + return product.astype(np.uint8) + + +def _sparse_equal( + left: scipy.sparse.csc_matrix, right: scipy.sparse.csc_matrix +) -> bool: + return left.shape == right.shape and (left != right).nnz == 0 + + +def _readonly_int_array(values: np.ndarray) -> np.ndarray: + result = np.asarray(values, dtype=np.int64).copy() + result.setflags(write=False) + return result + + +def gari_transform( + checks: scipy.sparse.csc_matrix, + logicals: scipy.sparse.csc_matrix, + *, + x_detectors: Sequence[int], + z_detectors: Sequence[int], +) -> GariTransform: + """Constructs the validated GARI augmented system over GF(2). + + ``x_detectors`` and ``z_detectors`` partition the source detector rows. + Their sequence order determines the order within the physical X and + physical Z row blocks, respectively. + + Args: + checks: Binary source detector-by-error matrix. + logicals: Binary source observable-by-error matrix. + x_detectors: Source rows containing X-type checks. + z_detectors: Source rows containing Z-type checks. + + Returns: + The augmented checks, physical logical map, matching matrices, source + column classes, detector mapping, and row block slices. + + Raises: + ValueError: The inputs do not satisfy the supported correlated CSS + structure. + """ + source_checks = _canonical_binary_csc(checks, name="checks") + source_logicals = _canonical_binary_csc(logicals, name="logicals") + if source_checks.shape[1] != source_logicals.shape[1]: + raise ValueError( + "checks and logicals must have the same source column count; " + f"found {source_checks.shape[1]} and {source_logicals.shape[1]}." + ) + + detector_count = source_checks.shape[0] + x_rows = _validated_detector_indices( + x_detectors, name="x_detectors", detector_count=detector_count + ) + z_rows = _validated_detector_indices( + z_detectors, name="z_detectors", detector_count=detector_count + ) + overlap = sorted(set(x_rows.tolist()) & set(z_rows.tolist())) + if overlap: + raise ValueError( + "x_detectors and z_detectors must be disjoint; detectors " + f"{overlap} appear in both." + ) + missing = sorted( + set(range(detector_count)) + - set(x_rows.tolist()) + - set(z_rows.tolist()) + ) + if missing: + raise ValueError( + "x_detectors and z_detectors must form a complete partition; " + f"missing detectors {missing}." + ) + + x_checks = source_checks[x_rows, :].tocsc() + z_checks = source_checks[z_rows, :].tocsc() + x_support_counts = np.diff(x_checks.indptr) + z_support_counts = np.diff(z_checks.indptr) + + e_z_columns = np.flatnonzero( + (x_support_counts > 0) & (z_support_counts == 0) + ) + e_x_columns = np.flatnonzero( + (x_support_counts == 0) & (z_support_counts > 0) + ) + e_y_columns = np.flatnonzero( + (x_support_counts > 0) & (z_support_counts > 0) + ) + detectorless_columns = np.flatnonzero( + (x_support_counts == 0) & (z_support_counts == 0) + ) + if detectorless_columns.size: + source_column = int(detectorless_columns[0]) + logical_support = list(_column_support(source_logicals, source_column)) + raise ValueError( + f"Source column {source_column} is detectorless; logical support " + f"is {logical_support}." + ) + + d_x = x_checks[:, e_z_columns].tocsc() + d_z = z_checks[:, e_x_columns].tocsc() + d_x_prime = x_checks[:, e_y_columns].tocsc() + d_z_prime = z_checks[:, e_y_columns].tocsc() + d_x_lookup = _projection_lookup(d_x, e_z_columns, name="D_X") + d_z_lookup = _projection_lookup(d_z, e_x_columns, name="D_Z") + + u_rows: list[int] = [] + v_rows: list[int] = [] + for local_y_column, source_column_value in enumerate(e_y_columns): + source_column = int(source_column_value) + x_projection = _column_support(d_x_prime, local_y_column) + if x_projection not in d_x_lookup: + raise ValueError( + f"Source column {source_column} has X-side projection " + f"{list(x_projection)}, which does not match a D_X column." + ) + z_projection = _column_support(d_z_prime, local_y_column) + if z_projection not in d_z_lookup: + raise ValueError( + f"Source column {source_column} has Z-side projection " + f"{list(z_projection)}, which does not match a D_Z column." + ) + u_rows.append(d_x_lookup[x_projection][0]) + v_rows.append(d_z_lookup[z_projection][0]) + + y_column_count = len(e_y_columns) + y_indices = np.arange(y_column_count, dtype=np.int64) + u = scipy.sparse.csc_matrix( + ( + np.ones(y_column_count, dtype=np.uint8), + (np.asarray(u_rows, dtype=np.int64), y_indices), + ), + shape=(len(e_z_columns), y_column_count), + dtype=np.uint8, + ) + v = scipy.sparse.csc_matrix( + ( + np.ones(y_column_count, dtype=np.uint8), + (np.asarray(v_rows, dtype=np.int64), y_indices), + ), + shape=(len(e_x_columns), y_column_count), + dtype=np.uint8, + ) + if not np.all(np.diff(u.indptr) == 1): + raise ValueError("Every U column must contain exactly one nonzero.") + if not np.all(np.diff(v.indptr) == 1): + raise ValueError("Every V column must contain exactly one nonzero.") + if not _sparse_equal(_gf2_product(d_x, u), d_x_prime): + raise ValueError("D_X @ U does not equal the e_Y X-side projection.") + if not _sparse_equal(_gf2_product(d_z, v), d_z_prime): + raise ValueError("D_Z @ V does not equal the e_Y Z-side projection.") + + x_row_count = len(x_rows) + z_row_count = len(z_rows) + e_z_count = len(e_z_columns) + e_x_count = len(e_x_columns) + zero = scipy.sparse.csc_matrix + + # Keep a barred variable for every pure column, even when its row in U or V + # is zero. In that case the identity blocks add the redundant constraint + # e = bar(e), preserving the same block form for every supported model. + identity_z = scipy.sparse.identity(e_z_count, dtype=np.uint8, format="csc") + identity_x = scipy.sparse.identity(e_x_count, dtype=np.uint8, format="csc") + augmented_checks = scipy.sparse.bmat( + [ + [ + zero((x_row_count, e_z_count), dtype=np.uint8), + zero((x_row_count, e_x_count), dtype=np.uint8), + zero((x_row_count, y_column_count), dtype=np.uint8), + d_x, + zero((x_row_count, e_x_count), dtype=np.uint8), + ], + [ + zero((z_row_count, e_z_count), dtype=np.uint8), + zero((z_row_count, e_x_count), dtype=np.uint8), + zero((z_row_count, y_column_count), dtype=np.uint8), + zero((z_row_count, e_z_count), dtype=np.uint8), + d_z, + ], + [ + identity_z, + zero((e_z_count, e_x_count), dtype=np.uint8), + u, + identity_z, + zero((e_z_count, e_x_count), dtype=np.uint8), + ], + [ + zero((e_x_count, e_z_count), dtype=np.uint8), + identity_x, + v, + zero((e_x_count, e_z_count), dtype=np.uint8), + identity_x, + ], + ], + format="csc", + ).astype(np.uint8) + + augmented_logicals = scipy.sparse.hstack( + [ + source_logicals[:, e_z_columns], + source_logicals[:, e_x_columns], + source_logicals[:, e_y_columns], + zero((source_logicals.shape[0], e_z_count), dtype=np.uint8), + zero((source_logicals.shape[0], e_x_count), dtype=np.uint8), + ], + format="csc", + ).astype(np.uint8) + + physical_x_rows = slice(0, x_row_count) + physical_z_rows = slice(x_row_count, x_row_count + z_row_count) + virtual_z_rows = slice( + physical_z_rows.stop, physical_z_rows.stop + e_z_count + ) + virtual_x_rows = slice( + virtual_z_rows.stop, virtual_z_rows.stop + e_x_count + ) + + source_to_gari = np.empty(detector_count, dtype=np.int64) + source_to_gari[x_rows] = np.arange(x_row_count, dtype=np.int64) + source_to_gari[z_rows] = x_row_count + np.arange( + z_row_count, dtype=np.int64 + ) + if len(np.unique(source_to_gari)) != detector_count: + raise ValueError("The source-to-GARI detector mapping is not injective.") + if np.any(source_to_gari < 0) or np.any( + source_to_gari >= x_row_count + z_row_count + ): + raise ValueError("The source-to-GARI detector mapping is out of range.") + + return GariTransform( + checks=augmented_checks, + logicals=augmented_logicals, + u=u, + v=v, + e_z_columns=_readonly_int_array(e_z_columns), + e_x_columns=_readonly_int_array(e_x_columns), + e_y_columns=_readonly_int_array(e_y_columns), + source_to_gari_detectors=_readonly_int_array(source_to_gari), + physical_x_rows=physical_x_rows, + physical_z_rows=physical_z_rows, + virtual_z_rows=virtual_z_rows, + virtual_x_rows=virtual_x_rows, + ) diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py new file mode 100644 index 00000000..f60ca70f --- /dev/null +++ b/src/py/_tesseract_py_util/gari_test.py @@ -0,0 +1,261 @@ +# 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. + +import itertools + +import numpy as np +import pytest +import scipy.sparse + +from _tesseract_py_util.gari import GariTransform, gari_transform + + +_X_DETECTORS = [0, 2] +_Z_DETECTORS = [1, 3] + + +def _tiny_source() -> tuple[ + scipy.sparse.csc_matrix, scipy.sparse.csc_matrix +]: + # Columns are e_Z, e_X, e_Y. Rows are interleaved X, Z, X, Z. + checks = scipy.sparse.csc_matrix( + [ + [1, 0, 1], + [0, 1, 1], + [1, 0, 1], + [0, 1, 1], + ], + dtype=np.uint8, + ) + logicals = scipy.sparse.csc_matrix( + [ + [1, 0, 1], + [0, 1, 0], + ], + dtype=np.uint8, + ) + return checks, logicals + + +def _tiny_transform() -> GariTransform: + checks, logicals = _tiny_source() + return gari_transform( + checks, + logicals, + x_detectors=_X_DETECTORS, + z_detectors=_Z_DETECTORS, + ) + + +def _augmented_error( + transform: GariTransform, source_error: np.ndarray +) -> np.ndarray: + e_z = source_error[transform.e_z_columns] + e_x = source_error[transform.e_x_columns] + e_y = source_error[transform.e_y_columns] + bar_e_z = (e_z + transform.u @ e_y) % 2 + bar_e_x = (e_x + transform.v @ e_y) % 2 + return np.concatenate([e_z, e_x, e_y, bar_e_z, bar_e_x]).astype( + np.uint8 + ) + + +def test_exact_tiny_transform(): + transform = _tiny_transform() + + np.testing.assert_array_equal(transform.e_z_columns, [0]) + np.testing.assert_array_equal(transform.e_x_columns, [1]) + np.testing.assert_array_equal(transform.e_y_columns, [2]) + np.testing.assert_array_equal(transform.u.toarray(), [[1]]) + np.testing.assert_array_equal(transform.v.toarray(), [[1]]) + np.testing.assert_array_equal( + transform.checks.toarray(), + [ + [0, 0, 0, 1, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1], + [0, 0, 0, 0, 1], + [1, 0, 1, 1, 0], + [0, 1, 1, 0, 1], + ], + ) + np.testing.assert_array_equal( + transform.logicals.toarray(), + [[1, 0, 1, 0, 0], [0, 1, 0, 0, 0]], + ) + np.testing.assert_array_equal( + transform.source_to_gari_detectors, [0, 2, 1, 3] + ) + assert transform.physical_x_rows == slice(0, 2) + assert transform.physical_z_rows == slice(2, 4) + assert transform.virtual_z_rows == slice(4, 5) + assert transform.virtual_x_rows == slice(5, 6) + + +def test_exhaustive_equivalence_and_virtual_constraints(): + checks, logicals = _tiny_source() + transform = _tiny_transform() + + for bits in itertools.product([0, 1], repeat=checks.shape[1]): + source_error = np.asarray(bits, dtype=np.uint8) + gari_error = _augmented_error(transform, source_error) + source_syndrome = np.asarray(checks @ source_error).reshape(-1) % 2 + expected_syndrome = np.concatenate( + [source_syndrome[[0, 2]], source_syndrome[[1, 3]], [0, 0]] + ) + gari_syndrome = np.asarray(transform.checks @ gari_error).reshape(-1) % 2 + np.testing.assert_array_equal(gari_syndrome, expected_syndrome) + np.testing.assert_array_equal( + np.asarray(transform.logicals @ gari_error).reshape(-1) % 2, + np.asarray(logicals @ source_error).reshape(-1) % 2, + ) + + consistent_count = 0 + for bits in itertools.product([0, 1], repeat=transform.checks.shape[1]): + gari_error = np.asarray(bits, dtype=np.uint8) + syndrome = np.asarray(transform.checks @ gari_error).reshape(-1) % 2 + if np.any(syndrome[transform.virtual_z_rows]) or np.any( + syndrome[transform.virtual_x_rows] + ): + continue + consistent_count += 1 + e_z, e_x, e_y, bar_e_z, bar_e_x = gari_error + assert bar_e_z == (e_z ^ e_y) + assert bar_e_x == (e_x ^ e_y) + assert consistent_count == 8 + + +def test_unmatched_pure_columns_still_receive_barred_variables(): + # The second e_Z and e_X columns are not used by the e_Y projection. + checks = scipy.sparse.csc_matrix( + [ + [1, 0, 0, 0, 1], + [0, 0, 1, 0, 1], + [0, 1, 0, 0, 0], + [0, 0, 0, 1, 0], + ] + ) + transform = gari_transform( + checks, + scipy.sparse.csc_matrix((1, 5)), + x_detectors=_X_DETECTORS, + z_detectors=_Z_DETECTORS, + ) + + np.testing.assert_array_equal(transform.u.toarray(), [[1], [0]]) + np.testing.assert_array_equal(transform.v.toarray(), [[1], [0]]) + # All three original variable blocks remain zero in the physical rows. + assert transform.checks[:4, :5].nnz == 0 + # The unmatched pure variables are copied by their identity constraints. + assert _column_support(transform.checks, 1) == [5] + assert _column_support(transform.checks, 6) == [1, 5] + assert _column_support(transform.checks, 3) == [7] + assert _column_support(transform.checks, 8) == [3, 7] + + +def _column_support(matrix: scipy.sparse.csc_matrix, column: int) -> list[int]: + return matrix[:, column].tocoo().row.tolist() + + +def _assert_rejected( + checks, + logicals, + message, + *, + x_detectors=_X_DETECTORS, + z_detectors=_Z_DETECTORS, +): + with pytest.raises(ValueError, match=message): + gari_transform( + scipy.sparse.csc_matrix(checks), + scipy.sparse.csc_matrix(logicals), + x_detectors=x_detectors, + z_detectors=z_detectors, + ) + + +def test_rejects_unsupported_projection_structure(): + for x_projection, z_projection, message in [ + ([1, 0], [1, 1], "X-side projection"), + ([1, 1], [1, 0], "Z-side projection"), + ]: + _assert_rejected( + [ + [1, 0, x_projection[0]], + [0, 1, z_projection[0]], + [1, 0, x_projection[1]], + [0, 1, z_projection[1]], + ], + scipy.sparse.csc_matrix((1, 3)), + message, + ) + + duplicate_cases = [ + ( + "D_X", + [[1, 1, 0, 1], [0, 0, 1, 1], [1, 1, 0, 1], [0, 0, 1, 1]], + ), + ( + "D_Z", + [[1, 0, 0, 1], [0, 1, 1, 1], [1, 0, 0, 1], [0, 1, 1, 1]], + ), + ] + for side, checks in duplicate_cases: + _assert_rejected( + checks, + scipy.sparse.csc_matrix((1, 4)), + f"{side} has duplicate columns", + ) + + +def test_rejects_invalid_inputs(): + checks, logicals = _tiny_source() + for x_detectors, z_detectors, message in [ + ([0], [1, 3], "complete partition"), + ([0, 2], [1, 2, 3], "disjoint"), + ([0, 0, 2], [1, 3], "more than once"), + ([0, 2], [1, 4], "outside the detector range"), + ]: + _assert_rejected( + checks, + logicals, + message, + x_detectors=x_detectors, + z_detectors=z_detectors, + ) + + detectorless_checks = scipy.sparse.hstack( + [checks, scipy.sparse.csc_matrix((4, 1))], format="csc" + ) + detectorless_logicals = scipy.sparse.hstack( + [logicals, scipy.sparse.csc_matrix([[0], [1]])], format="csc" + ) + _assert_rejected( + detectorless_checks, + detectorless_logicals, + r"column 3.*logical support is \[1\]", + ) + _assert_rejected( + checks, + scipy.sparse.csc_matrix((1, 4)), + "same source column count", + ) + + nonbinary_checks = checks.astype(float) + nonbinary_checks.data[0] = 2 + _assert_rejected(nonbinary_checks, logicals, "binary values") + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__])) From 8b7a68ab91e413451f6a73c8270b85efc3eb2b9f Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sat, 25 Jul 2026 18:30:37 -0700 Subject: [PATCH 02/43] Add canonical Stim adapter for GARI --- src/py/_tesseract_py_util/BUILD | 3 + src/py/_tesseract_py_util/gari.py | 224 +++++++++++++++++++++++++ src/py/_tesseract_py_util/gari_test.py | 71 +++++++- 3 files changed, 297 insertions(+), 1 deletion(-) diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index f216f467..64d297d3 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -22,8 +22,10 @@ py_library( srcs = ["gari.py"], visibility = ["//:__subpackages__"], deps = [ + ":_tesseract_py_util", "@pypi//numpy", "@pypi//scipy", + "@pypi//stim", ], ) @@ -37,6 +39,7 @@ py_test( "@pypi//numpy", "@pypi//pytest", "@pypi//scipy", + "@pypi//stim", ], ) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 3ba9a9ce..c30d358c 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -52,6 +52,12 @@ ``[L_eZ, L_eX, L_eY, 0, 0]``. The augmented system is a decoder model with structural variables; it is not a physical noise model to sample directly. +For certain single-basis CSS memory experiments, the paper instead evaluates +the relevant logical observable on ``bar(e)_X`` or ``bar(e)_Z`` to support its +message-passing convergence and early-stopping strategy. That specialized +logical placement is decoder- and experiment-specific; it is documented here +but is not implemented by this generic transform. + Every pure ``e_Z`` and ``e_X`` column receives a barred counterpart, including columns that are not the projection of any ``e_Y`` column. Such an unmatched column has an all-zero row in ``U`` or ``V``; its virtual identity constraint @@ -67,6 +73,11 @@ import numpy as np import scipy.sparse +import stim + +from _tesseract_py_util.decompose_errors import ( + undecomposed_error_detectors_and_observables, +) @dataclasses.dataclass(frozen=True) @@ -212,6 +223,219 @@ def _readonly_int_array(values: np.ndarray) -> np.ndarray: return result +def dem_to_matrices( + dem: stim.DetectorErrorModel, +) -> tuple[ + scipy.sparse.csc_matrix, scipy.sparse.csc_matrix, np.ndarray +]: + """Extracts canonical binary matrices and probabilities from a Stim DEM. + + The DEM is flattened before extraction. Stim separator targets are treated + as decomposition annotations: all detector and observable targets in an + error instruction are combined by symmetric difference. Repeated targets + therefore cancel over GF(2). Declared detector and observable dimensions + are retained even when their final rows are unused by every error. + + Args: + dem: Source detector error model. + + Returns: + ``(checks, logicals, probabilities)``, with one column and one + probability per flattened error instruction. + + Raises: + ValueError: An error instruction has invalid arguments or targets. + """ + if not isinstance(dem, stim.DetectorErrorModel): + raise ValueError("dem must be a stim.DetectorErrorModel.") + + flattened = dem.flattened() + detector_rows: list[int] = [] + detector_columns: list[int] = [] + logical_rows: list[int] = [] + logical_columns: list[int] = [] + probabilities: list[float] = [] + + for instruction in flattened: + if instruction.type != "error": + continue + arguments = instruction.args_copy() + if len(arguments) != 1: + raise ValueError( + "Each Stim error instruction must contain exactly one " + f"probability; found {len(arguments)} in {instruction}." + ) + probability = float(arguments[0]) + if not np.isfinite(probability) or probability < 0 or probability > 1: + raise ValueError( + f"Stim error probability must be finite and in [0, 1]; " + f"found {probability!r}." + ) + + detectors, observables = undecomposed_error_detectors_and_observables( + instruction + ) + source_column = len(probabilities) + for detector in detectors: + if detector < 0 or detector >= dem.num_detectors: + raise ValueError( + f"Error column {source_column} references detector " + f"{detector}, outside [0, {dem.num_detectors})." + ) + detector_rows.append(detector) + detector_columns.append(source_column) + for observable in observables: + if observable < 0 or observable >= dem.num_observables: + raise ValueError( + f"Error column {source_column} references observable " + f"{observable}, outside [0, {dem.num_observables})." + ) + logical_rows.append(observable) + logical_columns.append(source_column) + probabilities.append(probability) + + source_column_count = len(probabilities) + checks = scipy.sparse.csc_matrix( + ( + np.ones(len(detector_rows), dtype=np.uint8), + (detector_rows, detector_columns), + ), + shape=(dem.num_detectors, source_column_count), + dtype=np.uint8, + ) + logicals = scipy.sparse.csc_matrix( + ( + np.ones(len(logical_rows), dtype=np.uint8), + (logical_rows, logical_columns), + ), + shape=(dem.num_observables, source_column_count), + dtype=np.uint8, + ) + return checks, logicals, np.asarray(probabilities, dtype=np.float64) + + +def _matrices_to_decoder_dem( + checks: scipy.sparse.csc_matrix, + logicals: scipy.sparse.csc_matrix, + probabilities: np.ndarray, +) -> stim.DetectorErrorModel: + """Serializes matrices as an augmented decoder model. + + The result describes structural variables and constraints used for + decoding. It is not a physical noise model and must not be sampled to + generate physical shots. + """ + decoder_checks = _canonical_binary_csc(checks, name="checks") + decoder_logicals = _canonical_binary_csc(logicals, name="logicals") + if decoder_checks.shape[1] != decoder_logicals.shape[1]: + raise ValueError( + "checks and logicals must have the same decoder column count; " + f"found {decoder_checks.shape[1]} and " + f"{decoder_logicals.shape[1]}." + ) + probability_array = np.asarray(probabilities, dtype=np.float64) + if probability_array.ndim != 1: + raise ValueError("probabilities must be one-dimensional.") + if len(probability_array) != decoder_checks.shape[1]: + raise ValueError( + "probabilities must contain one value per decoder column; " + f"found {len(probability_array)} for " + f"{decoder_checks.shape[1]} columns." + ) + if not np.all(np.isfinite(probability_array)): + raise ValueError("probabilities must contain only finite values.") + if np.any(probability_array < 0) or np.any(probability_array > 1): + raise ValueError("probabilities must lie in [0, 1].") + + decoder_dem = stim.DetectorErrorModel() + for column, probability in enumerate(probability_array): + detector_targets = [ + stim.target_relative_detector_id(detector) + for detector in _column_support(decoder_checks, column) + ] + if not detector_targets: + logical_support = list(_column_support(decoder_logicals, column)) + raise ValueError( + f"Decoder column {column} has no detector support; logical " + f"support is {logical_support}." + ) + targets = detector_targets + targets.extend( + stim.target_logical_observable_id(observable) + for observable in _column_support(decoder_logicals, column) + ) + decoder_dem.append( + stim.DemInstruction( + type="error", + args=[float(probability)], + targets=targets, + ) + ) + + # Explicit declarations preserve trailing unused detector and observable + # dimensions when the matrices are serialized and parsed again. + for detector in range(decoder_checks.shape[0]): + decoder_dem.append( + stim.DemInstruction( + type="detector", + args=[], + targets=[stim.target_relative_detector_id(detector)], + ) + ) + for observable in range(decoder_logicals.shape[0]): + decoder_dem.append( + stim.DemInstruction( + type="logical_observable", + args=[], + targets=[stim.target_logical_observable_id(observable)], + ) + ) + return decoder_dem + + +def detector_partition_from_last_coordinate( + dem: stim.DetectorErrorModel, + *, + x_coordinate: int = 1, + z_coordinate: int = 3, +) -> tuple[np.ndarray, np.ndarray]: + """Partitions detectors using this repository's coordinate convention. + + This is not a universal Stim convention. For the supported repository + circuits, a detector whose final coordinate is exactly ``1`` is X-type and + one whose final coordinate is exactly ``3`` is Z-type. Missing coordinates + and every other final-coordinate value are rejected. + """ + if not isinstance(dem, stim.DetectorErrorModel): + raise ValueError("dem must be a stim.DetectorErrorModel.") + if x_coordinate == z_coordinate: + raise ValueError("X and Z detector coordinate values must be distinct.") + + coordinates = dem.get_detector_coordinates() + x_detectors: list[int] = [] + z_detectors: list[int] = [] + for detector in range(dem.num_detectors): + detector_coordinates = coordinates.get(detector) + if not detector_coordinates: + raise ValueError( + f"Detector {detector} has no coordinates; expected final " + f"coordinate {x_coordinate} or {z_coordinate}." + ) + role = detector_coordinates[-1] + if role == x_coordinate: + x_detectors.append(detector) + elif role == z_coordinate: + z_detectors.append(detector) + else: + raise ValueError( + f"Detector {detector} has unknown final coordinate {role!r}; " + f"expected {x_coordinate} for X or {z_coordinate} for Z." + ) + return _readonly_int_array(np.asarray(x_detectors)), _readonly_int_array( + np.asarray(z_detectors) + ) + + def gari_transform( checks: scipy.sparse.csc_matrix, logicals: scipy.sparse.csc_matrix, diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index f60ca70f..7a5f930c 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -17,8 +17,15 @@ import numpy as np import pytest import scipy.sparse +import stim -from _tesseract_py_util.gari import GariTransform, gari_transform +from _tesseract_py_util.gari import ( + GariTransform, + _matrices_to_decoder_dem, + dem_to_matrices, + detector_partition_from_last_coordinate, + gari_transform, +) _X_DETECTORS = [0, 2] @@ -102,6 +109,45 @@ def test_exact_tiny_transform(): assert transform.virtual_z_rows == slice(4, 5) assert transform.virtual_x_rows == slice(5, 6) + source_dem = stim.DetectorErrorModel(""" + error(0.125) D0 D0 D1 ^ D2 D2 L0 L0 L2 + detector(0, 1) D0 + detector(0, 3) D1 + detector(0, 1) D2 + detector(0, 3) D3 + logical_observable L4 + """) + checks, logicals, probabilities = dem_to_matrices(source_dem) + assert checks.shape == (4, 1) + assert logicals.shape == (5, 1) + assert _column_support(checks, 0) == [1] + assert _column_support(logicals, 0) == [2] + np.testing.assert_array_equal(probabilities, [0.125]) + x_detectors, z_detectors = detector_partition_from_last_coordinate( + source_dem + ) + np.testing.assert_array_equal(x_detectors, _X_DETECTORS) + np.testing.assert_array_equal(z_detectors, _Z_DETECTORS) + + decoder_probabilities = np.linspace( + 0.1, 0.5, num=transform.checks.shape[1] + ) + decoder_dem = _matrices_to_decoder_dem( + transform.checks, transform.logicals, decoder_probabilities + ) + reparsed_dem = stim.DetectorErrorModel(str(decoder_dem)) + round_trip_checks, round_trip_logicals, round_trip_probabilities = ( + dem_to_matrices(reparsed_dem) + ) + assert reparsed_dem.num_detectors == transform.checks.shape[0] + assert reparsed_dem.num_observables == transform.logicals.shape[0] + assert reparsed_dem.num_errors == transform.checks.shape[1] + assert (round_trip_checks != transform.checks).nnz == 0 + assert (round_trip_logicals != transform.logicals).nnz == 0 + np.testing.assert_allclose( + round_trip_probabilities, decoder_probabilities + ) + def test_exhaustive_equivalence_and_virtual_constraints(): checks, logicals = _tiny_source() @@ -256,6 +302,29 @@ def test_rejects_invalid_inputs(): nonbinary_checks.data[0] = 2 _assert_rejected(nonbinary_checks, logicals, "binary values") + for dem_text, message in [ + ("error(0.1) D0\ndetector D0", "has no coordinates"), + ("error(0.1) D0\ndetector(0, 2) D0", "unknown final coordinate"), + ]: + with pytest.raises(ValueError, match=message): + detector_partition_from_last_coordinate( + stim.DetectorErrorModel(dem_text) + ) + + transform = _tiny_transform() + with pytest.raises(ValueError, match="one value per decoder column"): + _matrices_to_decoder_dem( + transform.checks, + transform.logicals, + np.full(transform.checks.shape[1] - 1, 0.1), + ) + with pytest.raises(ValueError, match="no detector support"): + _matrices_to_decoder_dem( + scipy.sparse.csc_matrix((1, 1)), + scipy.sparse.csc_matrix([[1]]), + np.asarray([0.1]), + ) + if __name__ == "__main__": raise SystemExit(pytest.main([__file__])) From 9f742a600e11e3285e41738ebdf0fe50357b8604 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sat, 25 Jul 2026 18:50:05 -0700 Subject: [PATCH 03/43] Add documented GARI prior policies --- src/py/_tesseract_py_util/gari.py | 302 ++++++++++++++++++++++++- src/py/_tesseract_py_util/gari_test.py | 187 ++++++++++++++- 2 files changed, 487 insertions(+), 2 deletions(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index c30d358c..f1304715 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -69,9 +69,10 @@ import dataclasses import numbers -from collections.abc import Sequence +from collections.abc import Callable, Sequence import numpy as np +import scipy.optimize import scipy.sparse import stim @@ -664,3 +665,302 @@ def gari_transform( virtual_z_rows=virtual_z_rows, virtual_x_rows=virtual_x_rows, ) + + +def _validated_source_probabilities( + transform: GariTransform, source_probabilities: np.ndarray +) -> np.ndarray: + if not isinstance(transform, GariTransform): + raise ValueError("transform must be a GariTransform.") + try: + probabilities = np.asarray(source_probabilities, dtype=np.float64) + except (TypeError, ValueError) as ex: + raise ValueError( + "source_probabilities must be a one-dimensional numeric array." + ) from ex + if probabilities.ndim != 1: + raise ValueError("source_probabilities must be one-dimensional.") + source_column_count = ( + len(transform.e_z_columns) + + len(transform.e_x_columns) + + len(transform.e_y_columns) + ) + if len(probabilities) != source_column_count: + raise ValueError( + "source_probabilities must contain one value per source column; " + f"found {len(probabilities)} for {source_column_count} columns." + ) + if not np.all(np.isfinite(probabilities)): + raise ValueError( + "source_probabilities must contain only finite values." + ) + if np.any(probabilities <= 0) or np.any(probabilities > 0.5): + raise ValueError("source_probabilities must lie in (0, 0.5].") + result = probabilities.copy() + result.setflags(write=False) + return result + + +def _physical_probability_blocks( + transform: GariTransform, source_probabilities: np.ndarray +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + probabilities = _validated_source_probabilities( + transform, source_probabilities + ) + return ( + probabilities[transform.e_z_columns], + probabilities[transform.e_x_columns], + probabilities[transform.e_y_columns], + ) + + +def paper_prior_probabilities( + transform: GariTransform, source_probabilities: np.ndarray +) -> np.ndarray: + """Returns the published GARI initialization in decoder-column order. + + Physical ``e_Z``, ``e_X``, and ``e_Y`` variables retain their source + probabilities. Every auxiliary variable is assigned probability exactly + ``0.5``, giving it zero log-likelihood-ratio cost. This is the literature + reference policy, but those zero-cost branches can produce a very large + Tesseract search space. + """ + p_e_z, p_e_x, p_e_y = _physical_probability_blocks( + transform, source_probabilities + ) + return np.concatenate( + [ + p_e_z, + p_e_x, + p_e_y, + np.full(len(p_e_z), 0.5), + np.full(len(p_e_x), 0.5), + ] + ) + + +def _xor_parity_probability(probabilities: np.ndarray) -> float: + if len(probabilities) == 1: + return float(probabilities[0]) + if np.any(probabilities == 0.5): + return 0.5 + log_even_bias = np.sum(np.log1p(-2 * probabilities), dtype=np.float64) + return float(-0.5 * np.expm1(log_even_bias)) + + +def _auxiliary_xor_probabilities( + base_probabilities: np.ndarray, + y_probabilities: np.ndarray, + matching: scipy.sparse.csc_matrix, +) -> np.ndarray: + matching_rows = matching.tocsr() + result = np.empty(len(base_probabilities), dtype=np.float64) + for row, base_probability in enumerate(base_probabilities): + start = matching_rows.indptr[row] + stop = matching_rows.indptr[row + 1] + y_columns = matching_rows.indices[start:stop] + parity_probabilities = np.concatenate( + [np.asarray([base_probability]), y_probabilities[y_columns]] + ) + result[row] = _xor_parity_probability(parity_probabilities) + return result + + +def tesseract_xor_prior_probabilities( + transform: GariTransform, source_probabilities: np.ndarray +) -> np.ndarray: + """Returns experimental independent-XOR marginals for Tesseract. + + Each auxiliary probability is the independent Bernoulli parity marginal + implied by ``bar(e)_Z = e_Z XOR U e_Y`` or + ``bar(e)_X = e_X XOR V e_Y``. The computation uses log-domain products + for numerical stability and does not clip invalid inputs. + + This is a Tesseract-specific experimental heuristic, not the published + GARI prior. It can represent evidence already present in the physical + variables and virtual constraints, and is not claimed to preserve the + exact source-model maximum-likelihood objective. + """ + p_e_z, p_e_x, p_e_y = _physical_probability_blocks( + transform, source_probabilities + ) + p_bar_e_z = _auxiliary_xor_probabilities( + p_e_z, p_e_y, transform.u + ) + p_bar_e_x = _auxiliary_xor_probabilities( + p_e_x, p_e_y, transform.v + ) + return np.concatenate([p_e_z, p_e_x, p_e_y, p_bar_e_z, p_bar_e_x]) + + +def _source_to_auxiliary_cost_matrix( + transform: GariTransform, +) -> scipy.sparse.csc_matrix: + e_z_count = len(transform.e_z_columns) + e_x_count = len(transform.e_x_columns) + return scipy.sparse.bmat( + [ + [ + scipy.sparse.identity(e_z_count, format="csc"), + scipy.sparse.csc_matrix((e_z_count, e_x_count)), + ], + [ + scipy.sparse.csc_matrix((e_x_count, e_z_count)), + scipy.sparse.identity(e_x_count, format="csc"), + ], + [transform.u.T, transform.v.T], + ], + format="csc", + ) + + +def _probabilities_from_nonnegative_costs(costs: np.ndarray) -> np.ndarray: + return np.exp(-np.logaddexp(0, costs)) + + +def tesseract_lp_maximin_prior_probabilities( + transform: GariTransform, source_probabilities: np.ndarray +) -> np.ndarray: + """Balances nonnegative physical and auxiliary costs for Tesseract. + + For source costs ``c = log((1-p)/p)`` and auxiliary costs ``g``, this + experimental policy maximizes a common lower bound ``t`` subject to + ``A g + t <= c`` and ``-g + t <= 0``. The returned physical costs are the + residuals ``c - A g`` and the remaining costs are ``g``. + + This maximin objective is a practical Tesseract adaptation of exploratory + mode Q. It is not part of the GARI paper, changes the augmented search + objective, and is not claimed to preserve exact maximum-likelihood + decoding for every augmented assignment. Solver failure is a hard error; + there is no fallback or clipping. + """ + p_e_z, p_e_x, p_e_y = _physical_probability_blocks( + transform, source_probabilities + ) + physical_probabilities = np.concatenate([p_e_z, p_e_x, p_e_y]) + source_costs = np.log1p(-physical_probabilities) - np.log( + physical_probabilities + ) + cost_matrix = _source_to_auxiliary_cost_matrix(transform) + auxiliary_count = cost_matrix.shape[1] + + upper_constraints = scipy.sparse.hstack( + [cost_matrix, np.ones((len(source_costs), 1))], format="csc" + ) + lower_constraints = scipy.sparse.hstack( + [ + -scipy.sparse.identity(auxiliary_count, format="csc"), + np.ones((auxiliary_count, 1)), + ], + format="csc", + ) + constraints = scipy.sparse.vstack( + [upper_constraints, lower_constraints], format="csc" + ) + bounds = np.concatenate( + [source_costs, np.zeros(auxiliary_count, dtype=np.float64)] + ) + objective = np.zeros(auxiliary_count + 1, dtype=np.float64) + objective[-1] = -1 + result = scipy.optimize.linprog( + objective, + A_ub=constraints, + b_ub=bounds, + bounds=[(0, None)] * (auxiliary_count + 1), + method="highs", + ) + if not result.success: + raise RuntimeError( + "LP maximin prior solver failed: " + str(result.message) + ) + if result.x is None: + raise RuntimeError( + "LP maximin prior solver returned an invalid solution." + ) + + solution = np.asarray(result.x, dtype=np.float64) + if solution.shape != (auxiliary_count + 1,): + raise RuntimeError( + "LP maximin prior solver returned an invalid solution." + ) + if not np.all(np.isfinite(solution)): + raise RuntimeError( + "LP maximin prior solver returned non-finite costs." + ) + auxiliary_costs = solution[:-1] + residual_costs = source_costs - np.asarray( + cost_matrix @ auxiliary_costs + ).reshape(-1) + if ( + solution[-1] < 0 + or np.any(auxiliary_costs < 0) + or np.any(residual_costs < 0) + ): + raise RuntimeError( + "LP maximin prior solver returned negative costs." + ) + feasibility_tolerance = 1e-8 + minimum_cost = solution[-1] + if np.any(auxiliary_costs < minimum_cost - feasibility_tolerance) or np.any( + residual_costs < minimum_cost - feasibility_tolerance + ): + raise RuntimeError( + "LP maximin prior solver returned a solution that violates the " + "maximin constraints." + ) + return np.concatenate( + [ + _probabilities_from_nonnegative_costs(residual_costs), + _probabilities_from_nonnegative_costs(auxiliary_costs), + ] + ) + + +def _validated_decoder_probabilities( + transform: GariTransform, probabilities: np.ndarray +) -> np.ndarray: + try: + result = np.asarray(probabilities, dtype=np.float64) + except (TypeError, ValueError) as ex: + raise ValueError( + "prior_function must return a one-dimensional numeric array." + ) from ex + if result.ndim != 1: + raise ValueError("prior_function must return a one-dimensional array.") + decoder_column_count = transform.checks.shape[1] + if len(result) != decoder_column_count: + raise ValueError( + "prior_function must return one value per GARI decoder column; " + f"found {len(result)} for {decoder_column_count} columns." + ) + if not np.all(np.isfinite(result)): + raise ValueError("prior_function returned a non-finite probability.") + if np.any(result <= 0) or np.any(result > 0.5): + raise ValueError("prior_function probabilities must lie in (0, 0.5].") + return result + + +def build_gari_decoder_dem( + transform: GariTransform, + source_probabilities: np.ndarray, + *, + prior_function: Callable[[GariTransform, np.ndarray], np.ndarray], +) -> stim.DetectorErrorModel: + """Builds an augmented GARI decoder DEM using an explicit prior policy. + + ``prior_function`` may be one of this module's three built-in policies or + a user-defined callable. Its output is validated before serialization. + The resulting DEM is for decoding only and must not be sampled as a + physical noise model. + """ + probabilities = _validated_source_probabilities( + transform, source_probabilities + ) + if not callable(prior_function): + raise ValueError("prior_function must be callable.") + decoder_probabilities = _validated_decoder_probabilities( + transform, prior_function(transform, probabilities) + ) + return _matrices_to_decoder_dem( + transform.checks, transform.logicals, decoder_probabilities + ) diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 7a5f930c..33f02b82 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -16,15 +16,21 @@ import numpy as np import pytest +import scipy.optimize import scipy.sparse import stim +import _tesseract_py_util.gari as gari_module from _tesseract_py_util.gari import ( GariTransform, _matrices_to_decoder_dem, + build_gari_decoder_dem, dem_to_matrices, detector_partition_from_last_coordinate, gari_transform, + paper_prior_probabilities, + tesseract_lp_maximin_prior_probabilities, + tesseract_xor_prior_probabilities, ) @@ -109,6 +115,73 @@ def test_exact_tiny_transform(): assert transform.virtual_z_rows == slice(4, 5) assert transform.virtual_x_rows == slice(5, 6) + source_probabilities = np.asarray([0.1, 0.2, 0.3]) + np.testing.assert_array_equal( + paper_prior_probabilities(transform, source_probabilities), + [0.1, 0.2, 0.3, 0.5, 0.5], + ) + np.testing.assert_allclose( + tesseract_xor_prior_probabilities( + transform, source_probabilities + ), + [0.1, 0.2, 0.3, 0.34, 0.38], + ) + + source_checks, source_logicals = _tiny_source() + source_permutation = [2, 0, 1] + permuted_transform = gari_transform( + source_checks[:, source_permutation], + source_logicals[:, source_permutation], + x_detectors=_X_DETECTORS, + z_detectors=_Z_DETECTORS, + ) + permuted_probabilities = source_probabilities[source_permutation] + np.testing.assert_array_equal( + paper_prior_probabilities( + permuted_transform, permuted_probabilities + ), + [0.1, 0.2, 0.3, 0.5, 0.5], + ) + np.testing.assert_allclose( + tesseract_xor_prior_probabilities( + permuted_transform, permuted_probabilities + ), + [0.1, 0.2, 0.3, 0.34, 0.38], + ) + + lp_probabilities = tesseract_lp_maximin_prior_probabilities( + transform, source_probabilities + ) + lp_costs = np.log1p(-lp_probabilities) - np.log(lp_probabilities) + source_costs = np.log1p(-source_probabilities) - np.log( + source_probabilities + ) + cost_matrix = np.asarray([[1, 0], [0, 1], [1, 1]]) + np.testing.assert_allclose( + lp_costs[:3] + cost_matrix @ lp_costs[3:], source_costs + ) + assert np.min(lp_costs) == pytest.approx(source_costs[2] / 3) + assert np.all(lp_costs >= 0) + + custom_probabilities = np.linspace( + 0.05, 0.25, num=transform.checks.shape[1] + ) + + def custom_prior(callback_transform, callback_probabilities): + assert callback_transform is transform + assert not callback_probabilities.flags.writeable + return custom_probabilities + + custom_dem = build_gari_decoder_dem( + transform, + source_probabilities, + prior_function=custom_prior, + ) + _, _, serialized_custom_probabilities = dem_to_matrices(custom_dem) + np.testing.assert_allclose( + serialized_custom_probabilities, custom_probabilities + ) + source_dem = stim.DetectorErrorModel(""" error(0.125) D0 D0 D1 ^ D2 D2 L0 L0 L2 detector(0, 1) D0 @@ -209,6 +282,39 @@ def test_unmatched_pure_columns_still_receive_barred_variables(): assert _column_support(transform.checks, 3) == [7] assert _column_support(transform.checks, 8) == [3, 7] + np.testing.assert_allclose( + tesseract_xor_prior_probabilities( + transform, np.asarray([0.1, 0.15, 0.2, 0.25, 0.3]) + ), + [0.1, 0.15, 0.2, 0.25, 0.3, 0.34, 0.15, 0.38, 0.25], + ) + + repeated_y_transform = gari_transform( + scipy.sparse.csc_matrix( + [ + [1, 0, 1, 1], + [0, 1, 1, 1], + [1, 0, 1, 1], + [0, 1, 1, 1], + ] + ), + scipy.sparse.csc_matrix((1, 4)), + x_detectors=_X_DETECTORS, + z_detectors=_Z_DETECTORS, + ) + np.testing.assert_allclose( + tesseract_xor_prior_probabilities( + repeated_y_transform, np.asarray([0.1, 0.2, 0.3, 0.4]) + ), + [0.1, 0.2, 0.3, 0.4, 0.468, 0.476], + ) + np.testing.assert_array_equal( + tesseract_xor_prior_probabilities( + _tiny_transform(), np.asarray([0.1, 0.2, 0.5]) + )[-2:], + [0.5, 0.5], + ) + def _column_support(matrix: scipy.sparse.csc_matrix, column: int) -> list[int]: return matrix[:, column].tocoo().row.tolist() @@ -265,7 +371,7 @@ def test_rejects_unsupported_projection_structure(): ) -def test_rejects_invalid_inputs(): +def test_rejects_invalid_inputs(monkeypatch): checks, logicals = _tiny_source() for x_detectors, z_detectors, message in [ ([0], [1, 3], "complete partition"), @@ -325,6 +431,85 @@ def test_rejects_invalid_inputs(): np.asarray([0.1]), ) + transform = _tiny_transform() + for probabilities, message in [ + (np.asarray([0.1, 0.2]), "one value per source column"), + (np.asarray([[0.1, 0.2, 0.3]]), "one-dimensional"), + (np.asarray([0.0, 0.2, 0.3]), r"\(0, 0.5\]"), + (np.asarray([0.1, 0.2, 0.6]), r"\(0, 0.5\]"), + (np.asarray([0.1, np.nan, 0.3]), "finite"), + ]: + with pytest.raises(ValueError, match=message): + paper_prior_probabilities(transform, probabilities) + + source_probabilities = np.asarray([0.1, 0.2, 0.3]) + invalid_custom_priors = [ + (lambda _transform, _probabilities: np.asarray([0.1]), "one value"), + ( + lambda _transform, _probabilities: np.full((1, 5), 0.1), + "one-dimensional", + ), + ( + lambda _transform, _probabilities: np.asarray( + [0.1, 0.1, 0.1, 0.1, np.nan] + ), + "non-finite", + ), + ( + lambda _transform, _probabilities: np.asarray( + [0.1, 0.1, 0.1, 0.1, 0.0] + ), + r"\(0, 0.5\]", + ), + ( + lambda _transform, _probabilities: np.asarray( + [0.1, 0.1, 0.1, 0.1, 0.6] + ), + r"\(0, 0.5\]", + ), + ] + for prior_function, message in invalid_custom_priors: + with pytest.raises(ValueError, match=message): + build_gari_decoder_dem( + transform, + source_probabilities, + prior_function=prior_function, + ) + with pytest.raises(ValueError, match="must be callable"): + build_gari_decoder_dem( + transform, + source_probabilities, + prior_function=None, + ) + + failed_result = scipy.optimize.OptimizeResult( + success=False, message="planned solver failure" + ) + monkeypatch.setattr( + gari_module.scipy.optimize, + "linprog", + lambda *_args, **_kwargs: failed_result, + ) + with pytest.raises(RuntimeError, match="planned solver failure"): + tesseract_lp_maximin_prior_probabilities( + transform, source_probabilities + ) + + infeasible_result = scipy.optimize.OptimizeResult( + success=True, + message="claimed success", + x=np.asarray([0.0, 0.0, 1.0]), + ) + monkeypatch.setattr( + gari_module.scipy.optimize, + "linprog", + lambda *_args, **_kwargs: infeasible_result, + ) + with pytest.raises(RuntimeError, match="maximin constraints"): + tesseract_lp_maximin_prior_probabilities( + transform, source_probabilities + ) + if __name__ == "__main__": raise SystemExit(pytest.main([__file__])) From e7fd38c5b660bb0ad798c01357c7fc63aaf7713b Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sat, 25 Jul 2026 19:18:15 -0700 Subject: [PATCH 04/43] Add GARI converter and layout artifact --- src/py/BUILD | 12 +- src/py/_tesseract_py_util/gari.py | 152 +++++----- src/py/_tesseract_py_util/gari_test.py | 75 ++--- src/py/gari_convert.py | 373 +++++++++++++++++++++++++ 4 files changed, 500 insertions(+), 112 deletions(-) create mode 100644 src/py/gari_convert.py diff --git a/src/py/BUILD b/src/py/BUILD index e0bf9d87..78a107fc 100644 --- a/src/py/BUILD +++ b/src/py/BUILD @@ -80,6 +80,7 @@ py_test( ], imports = ["..", "."], ) + py_test( name = "tesseract_sinter_compat_test", srcs = ["tesseract_sinter_compat_test.py"], @@ -93,7 +94,16 @@ py_test( imports = ["..", "."], ) - +py_binary( + name = "gari_convert", + srcs = ["gari_convert.py"], + imports = ["."], + visibility = ["//visibility:public"], + deps = [ + "//src/py/_tesseract_py_util:gari", + "@pypi//stim", + ], +) py_test( name = "stub_test", diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index f1304715..a3b551c4 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -49,8 +49,10 @@ The corresponding decoder syndrome is ``[s_X, s_Z, 0, 0]``. The logical map stays on the original physical variables: -``[L_eZ, L_eX, L_eY, 0, 0]``. The augmented system is a decoder model with -structural variables; it is not a physical noise model to sample directly. +``[L_eZ, L_eX, L_eY, 0, 0]``. These are the GARI transformed matrices. They can +be stored using Stim's DEM syntax, but the resulting GARI error model is only +a matrix storage and decoding representation. It is not a physical detector +error model and must not be sampled. For certain single-basis CSS memory experiments, the paper instead evaluates the relevant logical observable on ``bar(e)_X`` or ``bar(e)_Z`` to support its @@ -59,7 +61,7 @@ but is not implemented by this generic transform. Every pure ``e_Z`` and ``e_X`` column receives a barred counterpart, including -columns that are not the projection of any ``e_Y`` column. Such an unmatched +columns that are not the projection of any ``e_Y`` column. Such an unused pure column has an all-zero row in ``U`` or ``V``; its virtual identity constraint therefore only copies ``e`` to ``bar(e)``. This deliberate redundancy keeps the five-block structure uniform and the physical top-left blocks zero. @@ -83,7 +85,7 @@ @dataclasses.dataclass(frozen=True) class GariTransform: - """A validated GARI augmented check system.""" + """Validated GARI transformed matrices and their block metadata.""" checks: scipy.sparse.csc_matrix logicals: scipy.sparse.csc_matrix @@ -315,57 +317,55 @@ def dem_to_matrices( return checks, logicals, np.asarray(probabilities, dtype=np.float64) -def _matrices_to_decoder_dem( +def _matrices_to_gari_error_model( checks: scipy.sparse.csc_matrix, logicals: scipy.sparse.csc_matrix, probabilities: np.ndarray, ) -> stim.DetectorErrorModel: - """Serializes matrices as an augmented decoder model. + """Stores GARI transformed matrices using Stim's DEM syntax. - The result describes structural variables and constraints used for - decoding. It is not a physical noise model and must not be sampled to - generate physical shots. + The result is a GARI error model for decoding and interchange. It is not a + physical detector error model and must not be sampled to generate shots. """ - decoder_checks = _canonical_binary_csc(checks, name="checks") - decoder_logicals = _canonical_binary_csc(logicals, name="logicals") - if decoder_checks.shape[1] != decoder_logicals.shape[1]: + gari_checks = _canonical_binary_csc(checks, name="checks") + gari_logicals = _canonical_binary_csc(logicals, name="logicals") + if gari_checks.shape[1] != gari_logicals.shape[1]: raise ValueError( - "checks and logicals must have the same decoder column count; " - f"found {decoder_checks.shape[1]} and " - f"{decoder_logicals.shape[1]}." + "checks and logicals must have the same GARI column count; " + f"found {gari_checks.shape[1]} and {gari_logicals.shape[1]}." ) probability_array = np.asarray(probabilities, dtype=np.float64) if probability_array.ndim != 1: raise ValueError("probabilities must be one-dimensional.") - if len(probability_array) != decoder_checks.shape[1]: + if len(probability_array) != gari_checks.shape[1]: raise ValueError( - "probabilities must contain one value per decoder column; " - f"found {len(probability_array)} for " - f"{decoder_checks.shape[1]} columns." + "probabilities must contain one value per GARI column; " + f"found {len(probability_array)} for {gari_checks.shape[1]} " + "columns." ) if not np.all(np.isfinite(probability_array)): raise ValueError("probabilities must contain only finite values.") if np.any(probability_array < 0) or np.any(probability_array > 1): raise ValueError("probabilities must lie in [0, 1].") - decoder_dem = stim.DetectorErrorModel() + gari_error_model = stim.DetectorErrorModel() for column, probability in enumerate(probability_array): detector_targets = [ stim.target_relative_detector_id(detector) - for detector in _column_support(decoder_checks, column) + for detector in _column_support(gari_checks, column) ] if not detector_targets: - logical_support = list(_column_support(decoder_logicals, column)) + logical_support = list(_column_support(gari_logicals, column)) raise ValueError( - f"Decoder column {column} has no detector support; logical " + f"GARI column {column} has no detector support; logical " f"support is {logical_support}." ) targets = detector_targets targets.extend( stim.target_logical_observable_id(observable) - for observable in _column_support(decoder_logicals, column) + for observable in _column_support(gari_logicals, column) ) - decoder_dem.append( + gari_error_model.append( stim.DemInstruction( type="error", args=[float(probability)], @@ -375,63 +375,57 @@ def _matrices_to_decoder_dem( # Explicit declarations preserve trailing unused detector and observable # dimensions when the matrices are serialized and parsed again. - for detector in range(decoder_checks.shape[0]): - decoder_dem.append( + for detector in range(gari_checks.shape[0]): + gari_error_model.append( stim.DemInstruction( type="detector", args=[], targets=[stim.target_relative_detector_id(detector)], ) ) - for observable in range(decoder_logicals.shape[0]): - decoder_dem.append( + for observable in range(gari_logicals.shape[0]): + gari_error_model.append( stim.DemInstruction( type="logical_observable", args=[], targets=[stim.target_logical_observable_id(observable)], ) ) - return decoder_dem + return gari_error_model -def detector_partition_from_last_coordinate( +def detector_partition_from_fourth_coordinate( dem: stim.DetectorErrorModel, - *, - x_coordinate: int = 1, - z_coordinate: int = 3, ) -> tuple[np.ndarray, np.ndarray]: - """Partitions detectors using this repository's coordinate convention. + """Partitions detectors using the repository's fourth-coordinate rule. - This is not a universal Stim convention. For the supported repository - circuits, a detector whose final coordinate is exactly ``1`` is X-type and - one whose final coordinate is exactly ``3`` is Z-type. Missing coordinates - and every other final-coordinate value are rejected. + This is the color-code-style convention followed by the test-data circuits + associated with this repository, not a universal Stim convention. The + fourth coordinate is a finite integer: values at most ``2`` identify X + detectors, while values at least ``3`` identify Z detectors. """ if not isinstance(dem, stim.DetectorErrorModel): raise ValueError("dem must be a stim.DetectorErrorModel.") - if x_coordinate == z_coordinate: - raise ValueError("X and Z detector coordinate values must be distinct.") coordinates = dem.get_detector_coordinates() x_detectors: list[int] = [] z_detectors: list[int] = [] for detector in range(dem.num_detectors): detector_coordinates = coordinates.get(detector) - if not detector_coordinates: + if detector_coordinates is None or len(detector_coordinates) < 4: raise ValueError( - f"Detector {detector} has no coordinates; expected final " - f"coordinate {x_coordinate} or {z_coordinate}." + f"Detector {detector} must have at least four coordinates." ) - role = detector_coordinates[-1] - if role == x_coordinate: - x_detectors.append(detector) - elif role == z_coordinate: - z_detectors.append(detector) - else: + role = detector_coordinates[3] + if not np.isfinite(role) or not float(role).is_integer(): raise ValueError( - f"Detector {detector} has unknown final coordinate {role!r}; " - f"expected {x_coordinate} for X or {z_coordinate} for Z." + f"Detector {detector} has invalid fourth coordinate " + f"{role!r}; expected a finite integer." ) + if role <= 2: + x_detectors.append(detector) + else: + z_detectors.append(detector) return _readonly_int_array(np.asarray(x_detectors)), _readonly_int_array( np.asarray(z_detectors) ) @@ -444,7 +438,7 @@ def gari_transform( x_detectors: Sequence[int], z_detectors: Sequence[int], ) -> GariTransform: - """Constructs the validated GARI augmented system over GF(2). + """Constructs validated GARI transformed matrices over GF(2). ``x_detectors`` and ``z_detectors`` partition the source detector rows. Their sequence order determines the order within the physical X and @@ -457,8 +451,8 @@ def gari_transform( z_detectors: Source rows containing Z-type checks. Returns: - The augmented checks, physical logical map, matching matrices, source - column classes, detector mapping, and row block slices. + The transformed checks, physical logical map, projection matrices, + source column classes, detector mapping, and row block slices. Raises: ValueError: The inputs do not satisfy the supported correlated CSS @@ -536,13 +530,13 @@ def gari_transform( if x_projection not in d_x_lookup: raise ValueError( f"Source column {source_column} has X-side projection " - f"{list(x_projection)}, which does not match a D_X column." + f"{list(x_projection)}, which does not equal a D_X column." ) z_projection = _column_support(d_z_prime, local_y_column) if z_projection not in d_z_lookup: raise ValueError( f"Source column {source_column} has Z-side projection " - f"{list(z_projection)}, which does not match a D_Z column." + f"{list(z_projection)}, which does not equal a D_Z column." ) u_rows.append(d_x_lookup[x_projection][0]) v_rows.append(d_z_lookup[z_projection][0]) @@ -717,7 +711,7 @@ def _physical_probability_blocks( def paper_prior_probabilities( transform: GariTransform, source_probabilities: np.ndarray ) -> np.ndarray: - """Returns the published GARI initialization in decoder-column order. + """Returns the published GARI initialization in GARI column order. Physical ``e_Z``, ``e_X``, and ``e_Y`` variables retain their source probabilities. Every auxiliary variable is assigned probability exactly @@ -751,14 +745,14 @@ def _xor_parity_probability(probabilities: np.ndarray) -> float: def _auxiliary_xor_probabilities( base_probabilities: np.ndarray, y_probabilities: np.ndarray, - matching: scipy.sparse.csc_matrix, + projection_matrix: scipy.sparse.csc_matrix, ) -> np.ndarray: - matching_rows = matching.tocsr() + projection_rows = projection_matrix.tocsr() result = np.empty(len(base_probabilities), dtype=np.float64) for row, base_probability in enumerate(base_probabilities): - start = matching_rows.indptr[row] - stop = matching_rows.indptr[row + 1] - y_columns = matching_rows.indices[start:stop] + start = projection_rows.indptr[row] + stop = projection_rows.indptr[row + 1] + y_columns = projection_rows.indices[start:stop] parity_probabilities = np.concatenate( [np.asarray([base_probability]), y_probabilities[y_columns]] ) @@ -829,10 +823,10 @@ def tesseract_lp_maximin_prior_probabilities( residuals ``c - A g`` and the remaining costs are ``g``. This maximin objective is a practical Tesseract adaptation of exploratory - mode Q. It is not part of the GARI paper, changes the augmented search - objective, and is not claimed to preserve exact maximum-likelihood - decoding for every augmented assignment. Solver failure is a hard error; - there is no fallback or clipping. + mode Q. It is not part of the GARI paper, changes the GARI error-model + search objective, and is not claimed to preserve exact maximum-likelihood + decoding for every GARI assignment. Solver failure is a hard error; there + is no fallback or clipping. """ p_e_z, p_e_x, p_e_y = _physical_probability_blocks( transform, source_probabilities @@ -916,7 +910,7 @@ def tesseract_lp_maximin_prior_probabilities( ) -def _validated_decoder_probabilities( +def _validated_gari_probabilities( transform: GariTransform, probabilities: np.ndarray ) -> np.ndarray: try: @@ -927,11 +921,11 @@ def _validated_decoder_probabilities( ) from ex if result.ndim != 1: raise ValueError("prior_function must return a one-dimensional array.") - decoder_column_count = transform.checks.shape[1] - if len(result) != decoder_column_count: + gari_column_count = transform.checks.shape[1] + if len(result) != gari_column_count: raise ValueError( - "prior_function must return one value per GARI decoder column; " - f"found {len(result)} for {decoder_column_count} columns." + "prior_function must return one value per GARI column; " + f"found {len(result)} for {gari_column_count} columns." ) if not np.all(np.isfinite(result)): raise ValueError("prior_function returned a non-finite probability.") @@ -940,27 +934,27 @@ def _validated_decoder_probabilities( return result -def build_gari_decoder_dem( +def build_gari_error_model( transform: GariTransform, source_probabilities: np.ndarray, *, prior_function: Callable[[GariTransform, np.ndarray], np.ndarray], ) -> stim.DetectorErrorModel: - """Builds an augmented GARI decoder DEM using an explicit prior policy. + """Builds a GARI error model using an explicit prior policy. ``prior_function`` may be one of this module's three built-in policies or a user-defined callable. Its output is validated before serialization. - The resulting DEM is for decoding only and must not be sampled as a - physical noise model. + Stim's DEM syntax is used only to store the GARI transformed matrices. The + result is not a physical detector error model and must not be sampled. """ probabilities = _validated_source_probabilities( transform, source_probabilities ) if not callable(prior_function): raise ValueError("prior_function must be callable.") - decoder_probabilities = _validated_decoder_probabilities( + gari_probabilities = _validated_gari_probabilities( transform, prior_function(transform, probabilities) ) - return _matrices_to_decoder_dem( - transform.checks, transform.logicals, decoder_probabilities + return _matrices_to_gari_error_model( + transform.checks, transform.logicals, gari_probabilities ) diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 33f02b82..e1929d86 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -23,10 +23,10 @@ import _tesseract_py_util.gari as gari_module from _tesseract_py_util.gari import ( GariTransform, - _matrices_to_decoder_dem, - build_gari_decoder_dem, + _matrices_to_gari_error_model, + build_gari_error_model, dem_to_matrices, - detector_partition_from_last_coordinate, + detector_partition_from_fourth_coordinate, gari_transform, paper_prior_probabilities, tesseract_lp_maximin_prior_probabilities, @@ -71,7 +71,7 @@ def _tiny_transform() -> GariTransform: ) -def _augmented_error( +def _gari_error_assignment( transform: GariTransform, source_error: np.ndarray ) -> np.ndarray: e_z = source_error[transform.e_z_columns] @@ -172,43 +172,47 @@ def custom_prior(callback_transform, callback_probabilities): assert not callback_probabilities.flags.writeable return custom_probabilities - custom_dem = build_gari_decoder_dem( + custom_gari_error_model = build_gari_error_model( transform, source_probabilities, prior_function=custom_prior, ) - _, _, serialized_custom_probabilities = dem_to_matrices(custom_dem) + _, _, serialized_custom_probabilities = dem_to_matrices( + custom_gari_error_model + ) np.testing.assert_allclose( serialized_custom_probabilities, custom_probabilities ) source_dem = stim.DetectorErrorModel(""" - error(0.125) D0 D0 D1 ^ D2 D2 L0 L0 L2 - detector(0, 1) D0 - detector(0, 3) D1 - detector(0, 1) D2 - detector(0, 3) D3 + error(0.125) D0 D0 D3 ^ D2 D2 L0 L0 L2 + detector(0, 0, 0, 0, 99) D0 + detector(0, 0, 0, 1) D1 + detector(0, 0, 0, 2) D2 + detector(0, 0, 0, 3) D3 + detector(0, 0, 0, 4) D4 + detector(0, 0, 0, 5, -99) D5 logical_observable L4 """) checks, logicals, probabilities = dem_to_matrices(source_dem) - assert checks.shape == (4, 1) + assert checks.shape == (6, 1) assert logicals.shape == (5, 1) - assert _column_support(checks, 0) == [1] + assert _column_support(checks, 0) == [3] assert _column_support(logicals, 0) == [2] np.testing.assert_array_equal(probabilities, [0.125]) - x_detectors, z_detectors = detector_partition_from_last_coordinate( + x_detectors, z_detectors = detector_partition_from_fourth_coordinate( source_dem ) - np.testing.assert_array_equal(x_detectors, _X_DETECTORS) - np.testing.assert_array_equal(z_detectors, _Z_DETECTORS) + np.testing.assert_array_equal(x_detectors, [0, 1, 2]) + np.testing.assert_array_equal(z_detectors, [3, 4, 5]) - decoder_probabilities = np.linspace( + gari_probabilities = np.linspace( 0.1, 0.5, num=transform.checks.shape[1] ) - decoder_dem = _matrices_to_decoder_dem( - transform.checks, transform.logicals, decoder_probabilities + gari_error_model = _matrices_to_gari_error_model( + transform.checks, transform.logicals, gari_probabilities ) - reparsed_dem = stim.DetectorErrorModel(str(decoder_dem)) + reparsed_dem = stim.DetectorErrorModel(str(gari_error_model)) round_trip_checks, round_trip_logicals, round_trip_probabilities = ( dem_to_matrices(reparsed_dem) ) @@ -218,7 +222,7 @@ def custom_prior(callback_transform, callback_probabilities): assert (round_trip_checks != transform.checks).nnz == 0 assert (round_trip_logicals != transform.logicals).nnz == 0 np.testing.assert_allclose( - round_trip_probabilities, decoder_probabilities + round_trip_probabilities, gari_probabilities ) @@ -228,7 +232,7 @@ def test_exhaustive_equivalence_and_virtual_constraints(): for bits in itertools.product([0, 1], repeat=checks.shape[1]): source_error = np.asarray(bits, dtype=np.uint8) - gari_error = _augmented_error(transform, source_error) + gari_error = _gari_error_assignment(transform, source_error) source_syndrome = np.asarray(checks @ source_error).reshape(-1) % 2 expected_syndrome = np.concatenate( [source_syndrome[[0, 2]], source_syndrome[[1, 3]], [0, 0]] @@ -255,7 +259,7 @@ def test_exhaustive_equivalence_and_virtual_constraints(): assert consistent_count == 8 -def test_unmatched_pure_columns_still_receive_barred_variables(): +def test_pure_columns_without_y_projections_receive_barred_variables(): # The second e_Z and e_X columns are not used by the e_Y projection. checks = scipy.sparse.csc_matrix( [ @@ -276,7 +280,7 @@ def test_unmatched_pure_columns_still_receive_barred_variables(): np.testing.assert_array_equal(transform.v.toarray(), [[1], [0]]) # All three original variable blocks remain zero in the physical rows. assert transform.checks[:4, :5].nnz == 0 - # The unmatched pure variables are copied by their identity constraints. + # Pure variables not used by an e_Y projection are copied by identity. assert _column_support(transform.checks, 1) == [5] assert _column_support(transform.checks, 6) == [1, 5] assert _column_support(transform.checks, 3) == [7] @@ -409,23 +413,30 @@ def test_rejects_invalid_inputs(monkeypatch): _assert_rejected(nonbinary_checks, logicals, "binary values") for dem_text, message in [ - ("error(0.1) D0\ndetector D0", "has no coordinates"), - ("error(0.1) D0\ndetector(0, 2) D0", "unknown final coordinate"), + ("error(0.1) D0\ndetector D0", "at least four coordinates"), + ( + "error(0.1) D0\ndetector(0, 0, 2) D0", + "at least four coordinates", + ), + ( + "error(0.1) D0\ndetector(0, 0, 0, 2.5) D0", + "finite integer", + ), ]: with pytest.raises(ValueError, match=message): - detector_partition_from_last_coordinate( + detector_partition_from_fourth_coordinate( stim.DetectorErrorModel(dem_text) ) transform = _tiny_transform() - with pytest.raises(ValueError, match="one value per decoder column"): - _matrices_to_decoder_dem( + with pytest.raises(ValueError, match="one value per GARI column"): + _matrices_to_gari_error_model( transform.checks, transform.logicals, np.full(transform.checks.shape[1] - 1, 0.1), ) with pytest.raises(ValueError, match="no detector support"): - _matrices_to_decoder_dem( + _matrices_to_gari_error_model( scipy.sparse.csc_matrix((1, 1)), scipy.sparse.csc_matrix([[1]]), np.asarray([0.1]), @@ -470,13 +481,13 @@ def test_rejects_invalid_inputs(monkeypatch): ] for prior_function, message in invalid_custom_priors: with pytest.raises(ValueError, match=message): - build_gari_decoder_dem( + build_gari_error_model( transform, source_probabilities, prior_function=prior_function, ) with pytest.raises(ValueError, match="must be callable"): - build_gari_decoder_dem( + build_gari_error_model( transform, source_probabilities, prior_function=None, diff --git a/src/py/gari_convert.py b/src/py/gari_convert.py new file mode 100644 index 00000000..0b6ebab7 --- /dev/null +++ b/src/py/gari_convert.py @@ -0,0 +1,373 @@ +#!/usr/bin/env python3 +# 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. + +"""Converts Stim circuits into GARI error model and detector-layout files. + +The ``.dem`` file stores the GARI transformed check and logical matrices using +Stim syntax. It is not a physical detector error model and must not be sampled. +The layout JSON maps source detector samples into the GARI detector space. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import tempfile +from pathlib import Path + +import stim + +from _tesseract_py_util.gari import ( + build_gari_error_model, + dem_to_matrices, + detector_partition_from_fourth_coordinate, + gari_transform, + paper_prior_probabilities, + tesseract_lp_maximin_prior_probabilities, + tesseract_xor_prior_probabilities, +) + + +_LAYOUT_SCHEMA = "tesseract.gari_layout.v1" +_BASIS_CONVENTION = "color-code-style-fourth-coordinate" +_PRIOR_FUNCTIONS = { + "paper": paper_prior_probabilities, + "xor": tesseract_xor_prior_probabilities, + "lp-maximin": tesseract_lp_maximin_prior_probabilities, +} + + +def _output_paths( + circuit_path: Path, + prior_policy: str, + output_prefix: Path | None, +) -> tuple[Path, Path]: + prefix = output_prefix or ( + circuit_path.parent + / "gari" + / f"{circuit_path.stem}.gari-{prior_policy}" + ) + return Path(f"{prefix}.dem"), Path(f"{prefix}.layout.json") + + +def _circuit_paths(circuit_directory: Path) -> list[Path]: + if circuit_directory.is_symlink() or not circuit_directory.is_dir(): + raise ValueError( + f"Circuit directory is not a directory: {circuit_directory}" + ) + paths = sorted( + ( + path + for path in circuit_directory.rglob( + "*.stim", recurse_symlinks=False + ) + if path.is_file() + ), + key=lambda path: path.relative_to(circuit_directory).as_posix(), + ) + if not paths: + raise ValueError( + f"No .stim circuits found under {circuit_directory}." + ) + return paths + + +def _layout_dict(transform, prior_policy: str) -> dict[str, object]: + blocks = { + "physical_x": transform.physical_x_rows, + "physical_z": transform.physical_z_rows, + "virtual_z": transform.virtual_z_rows, + "virtual_x": transform.virtual_x_rows, + } + return { + "schema": _LAYOUT_SCHEMA, + "source_detector_count": len(transform.source_to_gari_detectors), + "gari_detector_count": transform.checks.shape[0], + "source_to_gari": [ + int(value) for value in transform.source_to_gari_detectors + ], + "row_blocks": { + name: [int(rows.start), int(rows.stop)] + for name, rows in blocks.items() + }, + "detector_order": "physical_then_virtual", + "logical_placement": "physical", + "prior_policy": prior_policy, + } + + +def _write_gari_outputs( + error_model_path: Path, + error_model_text: str, + layout_path: Path, + layout_text: str, + *, + force: bool, +) -> None: + outputs = [ + (error_model_path, error_model_text), + (layout_path, layout_text), + ] + paths = [path for path, _ in outputs] + if any(path.is_dir() for path in paths): + raise IsADirectoryError("A GARI output path is a directory.") + existing = [path for path in paths if os.path.lexists(path)] + if existing and not force: + names = ", ".join(str(path) for path in existing) + raise FileExistsError( + f"Output already exists: {names}. Use --force to replace both " + "GARI output files." + ) + + error_model_path.parent.mkdir(parents=True, exist_ok=True) + scratch_paths: list[Path] = [] + backups: dict[Path, Path] = {} + published: list[Path] = [] + + def scratch_file(contents: str) -> Path: + descriptor, name = tempfile.mkstemp( + dir=error_model_path.parent, prefix=".gari-convert-" + ) + path = Path(name) + scratch_paths.append(path) + with os.fdopen(descriptor, "w", encoding="utf-8") as file: + file.write(contents) + return path + + try: + staged = [scratch_file(contents) for _, contents in outputs] + for path in existing: + backup = scratch_file("") + os.replace(path, backup) + backups[path] = backup + for temporary, final in zip(staged, paths): + os.replace(temporary, final) + published.append(final) + except BaseException: + for path in published: + path.unlink(missing_ok=True) + for path, backup in backups.items(): + if os.path.lexists(backup): + os.replace(backup, path) + raise + finally: + for path in scratch_paths: + path.unlink(missing_ok=True) + + +def _convert_circuit( + circuit_path: Path, + *, + prior_policy: str, + basis_convention: str, + output_prefix: Path | None, + force: bool, +): + if basis_convention != _BASIS_CONVENTION: + raise ValueError( + f"Unsupported basis convention {basis_convention!r}." + ) + if prior_policy not in _PRIOR_FUNCTIONS: + raise ValueError(f"Unknown GARI prior policy {prior_policy!r}.") + + error_model_path, layout_path = _output_paths( + circuit_path, prior_policy, output_prefix + ) + for path in [error_model_path, layout_path]: + aliases_input = circuit_path.resolve(strict=False) == path.resolve( + strict=False + ) + if not aliases_input and os.path.lexists(path): + try: + aliases_input = os.path.samefile(circuit_path, path) + except OSError: + pass + if aliases_input: + raise ValueError( + f"Output path {path} aliases source circuit {circuit_path}; " + "refusing to overwrite the input." + ) + + circuit = stim.Circuit.from_file(str(circuit_path)) + # Use the same analyzer policy as the Tesseract and Simplex CLIs. The GARI + # transformation consumes undecomposed errors and performs its own flattening. + source_error_model = circuit.detector_error_model( + decompose_errors=False, + flatten_loops=True, + allow_gauge_detectors=True, + approximate_disjoint_errors=1, + ignore_decomposition_failures=False, + block_decomposition_from_introducing_remnant_edges=False, + ) + checks, logicals, probabilities = dem_to_matrices(source_error_model) + x_detectors, z_detectors = detector_partition_from_fourth_coordinate( + source_error_model + ) + transform = gari_transform( + checks, + logicals, + x_detectors=x_detectors, + z_detectors=z_detectors, + ) + gari_error_model = build_gari_error_model( + transform, + probabilities, + prior_function=_PRIOR_FUNCTIONS[prior_policy], + ) + + error_model_text = str(gari_error_model) + if not error_model_text.endswith("\n"): + error_model_text += "\n" + layout_text = json.dumps( + _layout_dict(transform, prior_policy), indent=2, sort_keys=True + ) + "\n" + _write_gari_outputs( + error_model_path, + error_model_text, + layout_path, + layout_text, + force=force, + ) + return source_error_model, transform, error_model_path, layout_path + + +def _convert_directory( + circuit_directory: Path, + *, + prior_policy: str, + basis_convention: str, + force: bool, +) -> int: + circuit_paths = _circuit_paths(circuit_directory) + failures = 0 + for circuit_path in circuit_paths: + relative_path = circuit_path.relative_to(circuit_directory) + try: + _convert_circuit( + circuit_path, + prior_policy=prior_policy, + basis_convention=basis_convention, + output_prefix=None, + force=force, + ) + except (OSError, RuntimeError, ValueError) as ex: + failures += 1 + print(f"ERROR {relative_path}: {ex}", file=sys.stderr) + else: + print(f"OK {relative_path}") + + print( + f"\nRepository scan: {circuit_directory}\n" + f"Circuits found: {len(circuit_paths)}\n" + f"Converted: {len(circuit_paths) - failures}\n" + f"Failed: {failures}\n" + "GARI error model .dem files are storage only; do not sample." + ) + return int(failures != 0) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description=( + "Convert correlated CSS Stim circuits into GARI error model .dem " + "storage files and detector-layout JSON files." + ) + ) + inputs = parser.add_mutually_exclusive_group(required=True) + inputs.add_argument("--circuit", type=Path) + inputs.add_argument( + "--circuit-directory", + type=Path, + help="Recursively convert .stim circuits in deterministic order.", + ) + parser.add_argument( + "--prior-policy", required=True, choices=list(_PRIOR_FUNCTIONS) + ) + parser.add_argument( + "--basis-convention", + choices=[_BASIS_CONVENTION], + default=_BASIS_CONVENTION, + help=( + "Use the repository testdata's color-code-style fourth " + "coordinate: values <= 2 are X and values >= 3 are Z." + ), + ) + parser.add_argument( + "--output-prefix", + type=Path, + help="Custom output prefix for --circuit only.", + ) + parser.add_argument( + "--force", + action="store_true", + help="Replace existing GARI output files.", + ) + args = parser.parse_args(argv) + + if args.circuit_directory is not None: + if args.output_prefix is not None: + parser.error("--output-prefix can only be used with --circuit.") + try: + return _convert_directory( + args.circuit_directory, + prior_policy=args.prior_policy, + basis_convention=args.basis_convention, + force=args.force, + ) + except (OSError, ValueError) as ex: + print(f"gari_convert: {ex}", file=sys.stderr) + return 1 + + assert args.circuit is not None + try: + source_error_model, transform, error_model_path, layout_path = ( + _convert_circuit( + args.circuit, + prior_policy=args.prior_policy, + basis_convention=args.basis_convention, + output_prefix=args.output_prefix, + force=args.force, + ) + ) + except (OSError, RuntimeError, ValueError) as ex: + print(f"gari_convert: {ex}", file=sys.stderr) + return 1 + + row_counts = [ + ("Physical X rows", transform.physical_x_rows), + ("Physical Z rows", transform.physical_z_rows), + ("Virtual Z rows", transform.virtual_z_rows), + ("Virtual X rows", transform.virtual_x_rows), + ] + print("GARI transformed-matrix outputs created\n") + print(f"Source circuit: {args.circuit}") + print(f"Source detectors: {source_error_model.num_detectors}") + print(f"GARI detectors: {transform.checks.shape[0]}") + for label, rows in row_counts: + print(f"{label + ':':23}{rows.stop - rows.start}") + print(f"Prior policy: {args.prior_policy}") + print("Logical placement: physical") + print("Detector order: physical_then_virtual\n") + print("GARI error model (.dem storage only; do not sample):") + print(f" {error_model_path}\n") + print("Detector layout:") + print(f" {layout_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 0de20c578ad1746ee7d41848d63451fb0d05f7c4 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sat, 25 Jul 2026 20:26:12 -0700 Subject: [PATCH 05/43] Add GARI decoding example and documentation --- src/py/BUILD | 12 + src/py/README.md | 36 ++ src/py/_tesseract_py_util/gari.py | 322 ++++++--------- src/py/_tesseract_py_util/gari_test.py | 517 +++---------------------- src/py/gari_convert.py | 98 ++--- src/py/gari_example.py | 249 ++++++++++++ 6 files changed, 509 insertions(+), 725 deletions(-) create mode 100644 src/py/gari_example.py diff --git a/src/py/BUILD b/src/py/BUILD index 78a107fc..a8e1da59 100644 --- a/src/py/BUILD +++ b/src/py/BUILD @@ -105,6 +105,18 @@ py_binary( ], ) +py_binary( + name = "gari_example", + srcs = ["gari_example.py"], + imports = ["..", "."], + visibility = ["//visibility:public"], + deps = [ + "//src:lib_tesseract_decoder", + "@pypi//numpy", + "@pypi//stim", + ], +) + py_test( name = "stub_test", srcs = ["stub_test.py"], diff --git a/src/py/README.md b/src/py/README.md index 8b566e29..4aec3278 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -678,3 +678,39 @@ nice_calibrated_dem = demutil.regeneralize_spatial_dem( ) # Result will have error probability (0.1 + 0.2) / 2 = 0.15 ``` + +### GARI transformed-matrix workflow + +Convert one correlated CSS Stim circuit with: + +```bash +bazel run --jobs=1 //src/py:gari_convert -- \ + --circuit path/to/circuit.stim \ + --prior-policy xor +``` + +Outputs default to the circuit's sibling `gari/` directory as +`-gari-.dem` and `-gari--layout.json`. Replace +`--circuit PATH` with `--circuit-directory PATH` to scan a circuit tree +sequentially and deterministically. The scan continues after failures and exits +nonzero if any circuit fails. Repository test data uses a color-code-style +fourth coordinate: values at most `2` identify X detectors and values at least +`3` identify Z detectors. + +The `.dem` stores GARI transformed matrices using Stim syntax; it is not a +physical detector error model and must not be sampled. Decode samples from the +original circuit with the companion layout using: + +```bash +bazel run --jobs=1 //src/py:gari_example -- \ + --circuit path/to/circuit.stim \ + --dem path/to/model-gari-xor.dem \ + --gari-layout path/to/model-gari-xor-layout.json \ + --shots 10 \ + --seed 0 +``` + +The example samples only the original circuit, scatters its physical detector +data according to the layout, leaves virtual detector entries zero, and uses +the single `physical_then_virtual` detector order. A 10-shot run is a +functional smoke check, not a benchmark or mathematical proof. diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index a3b551c4..f131318c 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -1,17 +1,3 @@ -# 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. - """Graph augmentation and rewiring for inference (GARI). This module implements the matrix construction from A. S. Maan et al., @@ -50,15 +36,13 @@ The corresponding decoder syndrome is ``[s_X, s_Z, 0, 0]``. The logical map stays on the original physical variables: ``[L_eZ, L_eX, L_eY, 0, 0]``. These are the GARI transformed matrices. They can -be stored using Stim's DEM syntax, but the resulting GARI error model is only -a matrix storage and decoding representation. It is not a physical detector -error model and must not be sampled. +be stored using Stim's DEM syntax, but the resulting GARI DEM is only a matrix +storage and decoding representation. It is not a physical detector error model +and must not be sampled. For certain single-basis CSS memory experiments, the paper instead evaluates -the relevant logical observable on ``bar(e)_X`` or ``bar(e)_Z`` to support its -message-passing convergence and early-stopping strategy. That specialized -logical placement is decoder- and experiment-specific; it is documented here -but is not implemented by this generic transform. +the logical observable on ``bar(e)_X`` or ``bar(e)_Z``. That placement is +experiment-specific and is not implemented by this generic transform. Every pure ``e_Z`` and ``e_X`` column receives a barred counterpart, including columns that are not the projection of any ``e_Y`` column. Such an unused pure @@ -111,10 +95,10 @@ def _canonical_binary_csc( coordinate_matrix = matrix.tocoo(copy=True) stored_values = np.asarray(coordinate_matrix.data) - if stored_values.size and not np.all(np.isfinite(stored_values)): + if not np.all(np.isfinite(stored_values)): raise ValueError(f"{name} must contain only finite binary values.") is_binary = (stored_values == 0) | (stored_values == 1) - if stored_values.size and not np.all(is_binary): + if not np.all(is_binary): bad_value = stored_values[np.flatnonzero(~is_binary)[0]] raise ValueError( f"{name} must contain only binary values 0 or 1; found " @@ -131,7 +115,7 @@ def _canonical_binary_csc( ).tocsc() result.sum_duplicates() duplicate_sum_is_binary = (result.data == 0) | (result.data == 1) - if result.data.size and not np.all(duplicate_sum_is_binary): + if not np.all(duplicate_sum_is_binary): bad_value = result.data[np.flatnonzero(~duplicate_sum_is_binary)[0]] raise ValueError( f"{name} must be canonical after combining duplicate entries; " @@ -233,21 +217,10 @@ def dem_to_matrices( ]: """Extracts canonical binary matrices and probabilities from a Stim DEM. - The DEM is flattened before extraction. Stim separator targets are treated - as decomposition annotations: all detector and observable targets in an - error instruction are combined by symmetric difference. Repeated targets - therefore cancel over GF(2). Declared detector and observable dimensions - are retained even when their final rows are unused by every error. - - Args: - dem: Source detector error model. - - Returns: - ``(checks, logicals, probabilities)``, with one column and one - probability per flattened error instruction. - - Raises: - ValueError: An error instruction has invalid arguments or targets. + The DEM is flattened before extraction. Detector and observable targets in + each error are combined by symmetric difference, so repeated targets + cancel over GF(2). Declared dimensions are retained even when trailing rows + are unused. The result has one column and probability per flattened error. """ if not isinstance(dem, stim.DetectorErrorModel): raise ValueError("dem must be a stim.DetectorErrorModel.") @@ -280,19 +253,9 @@ def dem_to_matrices( ) source_column = len(probabilities) for detector in detectors: - if detector < 0 or detector >= dem.num_detectors: - raise ValueError( - f"Error column {source_column} references detector " - f"{detector}, outside [0, {dem.num_detectors})." - ) detector_rows.append(detector) detector_columns.append(source_column) for observable in observables: - if observable < 0 or observable >= dem.num_observables: - raise ValueError( - f"Error column {source_column} references observable " - f"{observable}, outside [0, {dem.num_observables})." - ) logical_rows.append(observable) logical_columns.append(source_column) probabilities.append(probability) @@ -317,15 +280,16 @@ def dem_to_matrices( return checks, logicals, np.asarray(probabilities, dtype=np.float64) -def _matrices_to_gari_error_model( +def _matrices_to_gari_dem( checks: scipy.sparse.csc_matrix, logicals: scipy.sparse.csc_matrix, probabilities: np.ndarray, ) -> stim.DetectorErrorModel: """Stores GARI transformed matrices using Stim's DEM syntax. - The result is a GARI error model for decoding and interchange. It is not a - physical detector error model and must not be sampled to generate shots. + The result is a GARI matrix representation for decoding and interchange. + It is not a physical detector error model and must not be sampled to + generate shots. """ gari_checks = _canonical_binary_csc(checks, name="checks") gari_logicals = _canonical_binary_csc(logicals, name="logicals") @@ -348,10 +312,12 @@ def _matrices_to_gari_error_model( if np.any(probability_array < 0) or np.any(probability_array > 1): raise ValueError("probabilities must lie in [0, 1].") - gari_error_model = stim.DetectorErrorModel() + detector_target = stim.target_relative_detector_id + logical_target = stim.target_logical_observable_id + gari_dem = stim.DetectorErrorModel() for column, probability in enumerate(probability_array): detector_targets = [ - stim.target_relative_detector_id(detector) + detector_target(detector) for detector in _column_support(gari_checks, column) ] if not detector_targets: @@ -362,36 +328,23 @@ def _matrices_to_gari_error_model( ) targets = detector_targets targets.extend( - stim.target_logical_observable_id(observable) + logical_target(observable) for observable in _column_support(gari_logicals, column) ) - gari_error_model.append( - stim.DemInstruction( - type="error", - args=[float(probability)], - targets=targets, - ) - ) + gari_dem.append("error", float(probability), targets) - # Explicit declarations preserve trailing unused detector and observable - # dimensions when the matrices are serialized and parsed again. - for detector in range(gari_checks.shape[0]): - gari_error_model.append( - stim.DemInstruction( - type="detector", - args=[], - targets=[stim.target_relative_detector_id(detector)], - ) + # One trailing declaration preserves each dimension after serialization. + if gari_checks.shape[0]: + gari_dem.append( + "detector", [], [detector_target(gari_checks.shape[0] - 1)] ) - for observable in range(gari_logicals.shape[0]): - gari_error_model.append( - stim.DemInstruction( - type="logical_observable", - args=[], - targets=[stim.target_logical_observable_id(observable)], - ) + if gari_logicals.shape[0]: + gari_dem.append( + "logical_observable", + [], + [logical_target(gari_logicals.shape[0] - 1)], ) - return gari_error_model + return gari_dem def detector_partition_from_fourth_coordinate( @@ -426,9 +379,7 @@ def detector_partition_from_fourth_coordinate( x_detectors.append(detector) else: z_detectors.append(detector) - return _readonly_int_array(np.asarray(x_detectors)), _readonly_int_array( - np.asarray(z_detectors) - ) + return _readonly_int_array(x_detectors), _readonly_int_array(z_detectors) def gari_transform( @@ -559,14 +510,17 @@ def gari_transform( shape=(len(e_x_columns), y_column_count), dtype=np.uint8, ) - if not np.all(np.diff(u.indptr) == 1): - raise ValueError("Every U column must contain exactly one nonzero.") - if not np.all(np.diff(v.indptr) == 1): - raise ValueError("Every V column must contain exactly one nonzero.") - if not _sparse_equal(_gf2_product(d_x, u), d_x_prime): - raise ValueError("D_X @ U does not equal the e_Y X-side projection.") - if not _sparse_equal(_gf2_product(d_z, v), d_z_prime): - raise ValueError("D_Z @ V does not equal the e_Y Z-side projection.") + for factor, name in ((u, "U"), (v, "V")): + if not np.all(np.diff(factor.indptr) == 1): + raise ValueError( + f"Every {name} column must contain exactly one nonzero." + ) + for base, factor, projection, message in ( + (d_x, u, d_x_prime, "D_X @ U does not equal the e_Y X-side projection."), + (d_z, v, d_z_prime, "D_Z @ V does not equal the e_Y Z-side projection."), + ): + if not _sparse_equal(_gf2_product(base, factor), projection): + raise ValueError(message) x_row_count = len(x_rows) z_row_count = len(z_rows) @@ -581,37 +535,14 @@ def gari_transform( identity_x = scipy.sparse.identity(e_x_count, dtype=np.uint8, format="csc") augmented_checks = scipy.sparse.bmat( [ - [ - zero((x_row_count, e_z_count), dtype=np.uint8), - zero((x_row_count, e_x_count), dtype=np.uint8), - zero((x_row_count, y_column_count), dtype=np.uint8), - d_x, - zero((x_row_count, e_x_count), dtype=np.uint8), - ], - [ - zero((z_row_count, e_z_count), dtype=np.uint8), - zero((z_row_count, e_x_count), dtype=np.uint8), - zero((z_row_count, y_column_count), dtype=np.uint8), - zero((z_row_count, e_z_count), dtype=np.uint8), - d_z, - ], - [ - identity_z, - zero((e_z_count, e_x_count), dtype=np.uint8), - u, - identity_z, - zero((e_z_count, e_x_count), dtype=np.uint8), - ], - [ - zero((e_x_count, e_z_count), dtype=np.uint8), - identity_x, - v, - zero((e_x_count, e_z_count), dtype=np.uint8), - identity_x, - ], + [None, None, None, d_x, None], + [None, None, None, None, d_z], + [identity_z, None, u, identity_z, None], + [None, identity_x, v, None, identity_x], ], format="csc", - ).astype(np.uint8) + dtype=np.uint8, + ) augmented_logicals = scipy.sparse.hstack( [ @@ -661,38 +592,49 @@ def gari_transform( ) +def _validated_probabilities( + values: np.ndarray, + *, + expected_count: int, + name: str, + column_kind: str, +) -> np.ndarray: + try: + result = np.asarray(values, dtype=np.float64) + except (TypeError, ValueError) as ex: + raise ValueError(f"{name} must be a numeric array.") from ex + if result.ndim != 1: + raise ValueError(f"{name} must be one-dimensional.") + if len(result) != expected_count: + raise ValueError( + f"{name} must contain one value per {column_kind} column; " + f"found {len(result)} for {expected_count} columns." + ) + if not np.all(np.isfinite(result)): + raise ValueError(f"{name} contains a non-finite probability.") + if np.any(result <= 0) or np.any(result > 0.5): + raise ValueError(f"{name} must lie in (0, 0.5].") + result = result.copy() + result.setflags(write=False) + return result + + def _validated_source_probabilities( transform: GariTransform, source_probabilities: np.ndarray ) -> np.ndarray: if not isinstance(transform, GariTransform): raise ValueError("transform must be a GariTransform.") - try: - probabilities = np.asarray(source_probabilities, dtype=np.float64) - except (TypeError, ValueError) as ex: - raise ValueError( - "source_probabilities must be a one-dimensional numeric array." - ) from ex - if probabilities.ndim != 1: - raise ValueError("source_probabilities must be one-dimensional.") source_column_count = ( len(transform.e_z_columns) + len(transform.e_x_columns) + len(transform.e_y_columns) ) - if len(probabilities) != source_column_count: - raise ValueError( - "source_probabilities must contain one value per source column; " - f"found {len(probabilities)} for {source_column_count} columns." - ) - if not np.all(np.isfinite(probabilities)): - raise ValueError( - "source_probabilities must contain only finite values." - ) - if np.any(probabilities <= 0) or np.any(probabilities > 0.5): - raise ValueError("source_probabilities must lie in (0, 0.5].") - result = probabilities.copy() - result.setflags(write=False) - return result + return _validated_probabilities( + source_probabilities, + expected_count=source_column_count, + name="source_probabilities", + column_kind="source", + ) def _physical_probability_blocks( @@ -792,26 +734,17 @@ def _source_to_auxiliary_cost_matrix( ) -> scipy.sparse.csc_matrix: e_z_count = len(transform.e_z_columns) e_x_count = len(transform.e_x_columns) + identity = scipy.sparse.identity return scipy.sparse.bmat( [ - [ - scipy.sparse.identity(e_z_count, format="csc"), - scipy.sparse.csc_matrix((e_z_count, e_x_count)), - ], - [ - scipy.sparse.csc_matrix((e_x_count, e_z_count)), - scipy.sparse.identity(e_x_count, format="csc"), - ], + [identity(e_z_count, format="csc"), None], + [None, identity(e_x_count, format="csc")], [transform.u.T, transform.v.T], ], format="csc", ) -def _probabilities_from_nonnegative_costs(costs: np.ndarray) -> np.ndarray: - return np.exp(-np.logaddexp(0, costs)) - - def tesseract_lp_maximin_prior_probabilities( transform: GariTransform, source_probabilities: np.ndarray ) -> np.ndarray: @@ -822,11 +755,10 @@ def tesseract_lp_maximin_prior_probabilities( ``A g + t <= c`` and ``-g + t <= 0``. The returned physical costs are the residuals ``c - A g`` and the remaining costs are ``g``. - This maximin objective is a practical Tesseract adaptation of exploratory - mode Q. It is not part of the GARI paper, changes the GARI error-model - search objective, and is not claimed to preserve exact maximum-likelihood - decoding for every GARI assignment. Solver failure is a hard error; there - is no fallback or clipping. + This is a practical Tesseract adaptation of exploratory mode Q, not part of + the GARI paper. It changes the search objective and is not claimed to + preserve exact maximum-likelihood decoding. Solver failure is a hard error; + there is no fallback or clipping. """ p_e_z, p_e_x, p_e_y = _physical_probability_blocks( transform, source_probabilities @@ -838,19 +770,16 @@ def tesseract_lp_maximin_prior_probabilities( cost_matrix = _source_to_auxiliary_cost_matrix(transform) auxiliary_count = cost_matrix.shape[1] - upper_constraints = scipy.sparse.hstack( - [cost_matrix, np.ones((len(source_costs), 1))], format="csc" - ) - lower_constraints = scipy.sparse.hstack( + constraints = scipy.sparse.bmat( [ - -scipy.sparse.identity(auxiliary_count, format="csc"), - np.ones((auxiliary_count, 1)), + [cost_matrix, np.ones((len(source_costs), 1))], + [ + -scipy.sparse.identity(auxiliary_count, format="csc"), + np.ones((auxiliary_count, 1)), + ], ], format="csc", ) - constraints = scipy.sparse.vstack( - [upper_constraints, lower_constraints], format="csc" - ) bounds = np.concatenate( [source_costs, np.zeros(auxiliary_count, dtype=np.float64)] ) @@ -867,12 +796,9 @@ def tesseract_lp_maximin_prior_probabilities( raise RuntimeError( "LP maximin prior solver failed: " + str(result.message) ) - if result.x is None: - raise RuntimeError( - "LP maximin prior solver returned an invalid solution." - ) - - solution = np.asarray(result.x, dtype=np.float64) + solution = np.asarray( + [] if result.x is None else result.x, dtype=np.float64 + ) if solution.shape != (auxiliary_count + 1,): raise RuntimeError( "LP maximin prior solver returned an invalid solution." @@ -893,54 +819,25 @@ def tesseract_lp_maximin_prior_probabilities( raise RuntimeError( "LP maximin prior solver returned negative costs." ) - feasibility_tolerance = 1e-8 minimum_cost = solution[-1] - if np.any(auxiliary_costs < minimum_cost - feasibility_tolerance) or np.any( - residual_costs < minimum_cost - feasibility_tolerance + if np.any(auxiliary_costs < minimum_cost - 1e-8) or np.any( + residual_costs < minimum_cost - 1e-8 ): raise RuntimeError( "LP maximin prior solver returned a solution that violates the " "maximin constraints." ) - return np.concatenate( - [ - _probabilities_from_nonnegative_costs(residual_costs), - _probabilities_from_nonnegative_costs(auxiliary_costs), - ] - ) - - -def _validated_gari_probabilities( - transform: GariTransform, probabilities: np.ndarray -) -> np.ndarray: - try: - result = np.asarray(probabilities, dtype=np.float64) - except (TypeError, ValueError) as ex: - raise ValueError( - "prior_function must return a one-dimensional numeric array." - ) from ex - if result.ndim != 1: - raise ValueError("prior_function must return a one-dimensional array.") - gari_column_count = transform.checks.shape[1] - if len(result) != gari_column_count: - raise ValueError( - "prior_function must return one value per GARI column; " - f"found {len(result)} for {gari_column_count} columns." - ) - if not np.all(np.isfinite(result)): - raise ValueError("prior_function returned a non-finite probability.") - if np.any(result <= 0) or np.any(result > 0.5): - raise ValueError("prior_function probabilities must lie in (0, 0.5].") - return result + gari_costs = np.concatenate([residual_costs, auxiliary_costs]) + return np.exp(-np.logaddexp(0, gari_costs)) -def build_gari_error_model( +def build_gari_dem( transform: GariTransform, source_probabilities: np.ndarray, *, prior_function: Callable[[GariTransform, np.ndarray], np.ndarray], ) -> stim.DetectorErrorModel: - """Builds a GARI error model using an explicit prior policy. + """Builds a GARI DEM using an explicit prior policy. ``prior_function`` may be one of this module's three built-in policies or a user-defined callable. Its output is validated before serialization. @@ -952,9 +849,12 @@ def build_gari_error_model( ) if not callable(prior_function): raise ValueError("prior_function must be callable.") - gari_probabilities = _validated_gari_probabilities( - transform, prior_function(transform, probabilities) + gari_probabilities = _validated_probabilities( + prior_function(transform, probabilities), + expected_count=transform.checks.shape[1], + name="prior_function probabilities", + column_kind="GARI", ) - return _matrices_to_gari_error_model( + return _matrices_to_gari_dem( transform.checks, transform.logicals, gari_probabilities ) diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index e1929d86..5d44a21c 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -1,30 +1,12 @@ -# 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. - import itertools import numpy as np import pytest -import scipy.optimize import scipy.sparse import stim -import _tesseract_py_util.gari as gari_module from _tesseract_py_util.gari import ( - GariTransform, - _matrices_to_gari_error_model, - build_gari_error_model, + build_gari_dem, dem_to_matrices, detector_partition_from_fourth_coordinate, gari_transform, @@ -34,59 +16,31 @@ ) -_X_DETECTORS = [0, 2] -_Z_DETECTORS = [1, 3] - - -def _tiny_source() -> tuple[ - scipy.sparse.csc_matrix, scipy.sparse.csc_matrix -]: - # Columns are e_Z, e_X, e_Y. Rows are interleaved X, Z, X, Z. - checks = scipy.sparse.csc_matrix( - [ - [1, 0, 1], - [0, 1, 1], - [1, 0, 1], - [0, 1, 1], - ], - dtype=np.uint8, - ) - logicals = scipy.sparse.csc_matrix( - [ - [1, 0, 1], - [0, 1, 0], - ], - dtype=np.uint8, +def _tiny_model(): + source_dem = stim.DetectorErrorModel(""" + error(0.1) D0 D2 L0 + error(0.2) D1 D3 L1 + error(0.3) D0 D1 D2 D3 L0 + detector(0, 0, 0, 0) D0 + detector(0, 0, 0, 3) D1 + detector(0, 0, 0, 2) D2 + detector(0, 0, 0, 4) D3 + """) + checks, logicals, probabilities = dem_to_matrices(source_dem) + x_detectors, z_detectors = detector_partition_from_fourth_coordinate( + source_dem ) - return checks, logicals - - -def _tiny_transform() -> GariTransform: - checks, logicals = _tiny_source() - return gari_transform( + transform = gari_transform( checks, logicals, - x_detectors=_X_DETECTORS, - z_detectors=_Z_DETECTORS, - ) - - -def _gari_error_assignment( - transform: GariTransform, source_error: np.ndarray -) -> np.ndarray: - e_z = source_error[transform.e_z_columns] - e_x = source_error[transform.e_x_columns] - e_y = source_error[transform.e_y_columns] - bar_e_z = (e_z + transform.u @ e_y) % 2 - bar_e_x = (e_x + transform.v @ e_y) % 2 - return np.concatenate([e_z, e_x, e_y, bar_e_z, bar_e_x]).astype( - np.uint8 + x_detectors=x_detectors, + z_detectors=z_detectors, ) + return checks, logicals, probabilities, transform -def test_exact_tiny_transform(): - transform = _tiny_transform() - +def test_tiny_transform(): + checks, logicals, _, transform = _tiny_model() np.testing.assert_array_equal(transform.e_z_columns, [0]) np.testing.assert_array_equal(transform.e_x_columns, [1]) np.testing.assert_array_equal(transform.e_y_columns, [2]) @@ -110,43 +64,44 @@ def test_exact_tiny_transform(): np.testing.assert_array_equal( transform.source_to_gari_detectors, [0, 2, 1, 3] ) - assert transform.physical_x_rows == slice(0, 2) - assert transform.physical_z_rows == slice(2, 4) - assert transform.virtual_z_rows == slice(4, 5) - assert transform.virtual_x_rows == slice(5, 6) + assert ( + transform.physical_x_rows, + transform.physical_z_rows, + transform.virtual_z_rows, + transform.virtual_x_rows, + ) == (slice(0, 2), slice(2, 4), slice(4, 5), slice(5, 6)) + + for source_error in itertools.product([0, 1], repeat=3): + e_z, e_x, e_y = source_error + source_error = np.asarray(source_error, dtype=np.uint8) + gari_error = np.asarray( + [e_z, e_x, e_y, e_z ^ e_y, e_x ^ e_y], dtype=np.uint8 + ) + source_syndrome = np.asarray(checks @ source_error).reshape(-1) % 2 + expected = np.concatenate( + [source_syndrome[[0, 2, 1, 3]], np.zeros(2, dtype=np.uint8)] + ) + np.testing.assert_array_equal( + np.asarray(transform.checks @ gari_error).reshape(-1) % 2, + expected, + ) + np.testing.assert_array_equal( + np.asarray(transform.logicals @ gari_error).reshape(-1) % 2, + np.asarray(logicals @ source_error).reshape(-1) % 2, + ) + - source_probabilities = np.asarray([0.1, 0.2, 0.3]) +def test_prior_probabilities_and_gari_dem_round_trip(): + _, _, source_probabilities, transform = _tiny_model() np.testing.assert_array_equal( paper_prior_probabilities(transform, source_probabilities), [0.1, 0.2, 0.3, 0.5, 0.5], ) - np.testing.assert_allclose( - tesseract_xor_prior_probabilities( - transform, source_probabilities - ), - [0.1, 0.2, 0.3, 0.34, 0.38], - ) - - source_checks, source_logicals = _tiny_source() - source_permutation = [2, 0, 1] - permuted_transform = gari_transform( - source_checks[:, source_permutation], - source_logicals[:, source_permutation], - x_detectors=_X_DETECTORS, - z_detectors=_Z_DETECTORS, - ) - permuted_probabilities = source_probabilities[source_permutation] - np.testing.assert_array_equal( - paper_prior_probabilities( - permuted_transform, permuted_probabilities - ), - [0.1, 0.2, 0.3, 0.5, 0.5], + xor_probabilities = tesseract_xor_prior_probabilities( + transform, source_probabilities ) np.testing.assert_allclose( - tesseract_xor_prior_probabilities( - permuted_transform, permuted_probabilities - ), - [0.1, 0.2, 0.3, 0.34, 0.38], + xor_probabilities, [0.1, 0.2, 0.3, 0.34, 0.38] ) lp_probabilities = tesseract_lp_maximin_prior_probabilities( @@ -156,370 +111,22 @@ def test_exact_tiny_transform(): source_costs = np.log1p(-source_probabilities) - np.log( source_probabilities ) - cost_matrix = np.asarray([[1, 0], [0, 1], [1, 1]]) np.testing.assert_allclose( - lp_costs[:3] + cost_matrix @ lp_costs[3:], source_costs + lp_costs[:3] + np.asarray([[1, 0], [0, 1], [1, 1]]) @ lp_costs[3:], + source_costs, ) - assert np.min(lp_costs) == pytest.approx(source_costs[2] / 3) - assert np.all(lp_costs >= 0) - custom_probabilities = np.linspace( - 0.05, 0.25, num=transform.checks.shape[1] - ) - - def custom_prior(callback_transform, callback_probabilities): - assert callback_transform is transform - assert not callback_probabilities.flags.writeable - return custom_probabilities - - custom_gari_error_model = build_gari_error_model( + gari_dem = build_gari_dem( transform, source_probabilities, - prior_function=custom_prior, - ) - _, _, serialized_custom_probabilities = dem_to_matrices( - custom_gari_error_model - ) - np.testing.assert_allclose( - serialized_custom_probabilities, custom_probabilities - ) - - source_dem = stim.DetectorErrorModel(""" - error(0.125) D0 D0 D3 ^ D2 D2 L0 L0 L2 - detector(0, 0, 0, 0, 99) D0 - detector(0, 0, 0, 1) D1 - detector(0, 0, 0, 2) D2 - detector(0, 0, 0, 3) D3 - detector(0, 0, 0, 4) D4 - detector(0, 0, 0, 5, -99) D5 - logical_observable L4 - """) - checks, logicals, probabilities = dem_to_matrices(source_dem) - assert checks.shape == (6, 1) - assert logicals.shape == (5, 1) - assert _column_support(checks, 0) == [3] - assert _column_support(logicals, 0) == [2] - np.testing.assert_array_equal(probabilities, [0.125]) - x_detectors, z_detectors = detector_partition_from_fourth_coordinate( - source_dem - ) - np.testing.assert_array_equal(x_detectors, [0, 1, 2]) - np.testing.assert_array_equal(z_detectors, [3, 4, 5]) - - gari_probabilities = np.linspace( - 0.1, 0.5, num=transform.checks.shape[1] - ) - gari_error_model = _matrices_to_gari_error_model( - transform.checks, transform.logicals, gari_probabilities - ) - reparsed_dem = stim.DetectorErrorModel(str(gari_error_model)) - round_trip_checks, round_trip_logicals, round_trip_probabilities = ( - dem_to_matrices(reparsed_dem) - ) - assert reparsed_dem.num_detectors == transform.checks.shape[0] - assert reparsed_dem.num_observables == transform.logicals.shape[0] - assert reparsed_dem.num_errors == transform.checks.shape[1] - assert (round_trip_checks != transform.checks).nnz == 0 - assert (round_trip_logicals != transform.logicals).nnz == 0 - np.testing.assert_allclose( - round_trip_probabilities, gari_probabilities - ) - - -def test_exhaustive_equivalence_and_virtual_constraints(): - checks, logicals = _tiny_source() - transform = _tiny_transform() - - for bits in itertools.product([0, 1], repeat=checks.shape[1]): - source_error = np.asarray(bits, dtype=np.uint8) - gari_error = _gari_error_assignment(transform, source_error) - source_syndrome = np.asarray(checks @ source_error).reshape(-1) % 2 - expected_syndrome = np.concatenate( - [source_syndrome[[0, 2]], source_syndrome[[1, 3]], [0, 0]] - ) - gari_syndrome = np.asarray(transform.checks @ gari_error).reshape(-1) % 2 - np.testing.assert_array_equal(gari_syndrome, expected_syndrome) - np.testing.assert_array_equal( - np.asarray(transform.logicals @ gari_error).reshape(-1) % 2, - np.asarray(logicals @ source_error).reshape(-1) % 2, - ) - - consistent_count = 0 - for bits in itertools.product([0, 1], repeat=transform.checks.shape[1]): - gari_error = np.asarray(bits, dtype=np.uint8) - syndrome = np.asarray(transform.checks @ gari_error).reshape(-1) % 2 - if np.any(syndrome[transform.virtual_z_rows]) or np.any( - syndrome[transform.virtual_x_rows] - ): - continue - consistent_count += 1 - e_z, e_x, e_y, bar_e_z, bar_e_x = gari_error - assert bar_e_z == (e_z ^ e_y) - assert bar_e_x == (e_x ^ e_y) - assert consistent_count == 8 - - -def test_pure_columns_without_y_projections_receive_barred_variables(): - # The second e_Z and e_X columns are not used by the e_Y projection. - checks = scipy.sparse.csc_matrix( - [ - [1, 0, 0, 0, 1], - [0, 0, 1, 0, 1], - [0, 1, 0, 0, 0], - [0, 0, 0, 1, 0], - ] - ) - transform = gari_transform( - checks, - scipy.sparse.csc_matrix((1, 5)), - x_detectors=_X_DETECTORS, - z_detectors=_Z_DETECTORS, - ) - - np.testing.assert_array_equal(transform.u.toarray(), [[1], [0]]) - np.testing.assert_array_equal(transform.v.toarray(), [[1], [0]]) - # All three original variable blocks remain zero in the physical rows. - assert transform.checks[:4, :5].nnz == 0 - # Pure variables not used by an e_Y projection are copied by identity. - assert _column_support(transform.checks, 1) == [5] - assert _column_support(transform.checks, 6) == [1, 5] - assert _column_support(transform.checks, 3) == [7] - assert _column_support(transform.checks, 8) == [3, 7] - - np.testing.assert_allclose( - tesseract_xor_prior_probabilities( - transform, np.asarray([0.1, 0.15, 0.2, 0.25, 0.3]) - ), - [0.1, 0.15, 0.2, 0.25, 0.3, 0.34, 0.15, 0.38, 0.25], - ) - - repeated_y_transform = gari_transform( - scipy.sparse.csc_matrix( - [ - [1, 0, 1, 1], - [0, 1, 1, 1], - [1, 0, 1, 1], - [0, 1, 1, 1], - ] - ), - scipy.sparse.csc_matrix((1, 4)), - x_detectors=_X_DETECTORS, - z_detectors=_Z_DETECTORS, - ) - np.testing.assert_allclose( - tesseract_xor_prior_probabilities( - repeated_y_transform, np.asarray([0.1, 0.2, 0.3, 0.4]) - ), - [0.1, 0.2, 0.3, 0.4, 0.468, 0.476], - ) - np.testing.assert_array_equal( - tesseract_xor_prior_probabilities( - _tiny_transform(), np.asarray([0.1, 0.2, 0.5]) - )[-2:], - [0.5, 0.5], - ) - - -def _column_support(matrix: scipy.sparse.csc_matrix, column: int) -> list[int]: - return matrix[:, column].tocoo().row.tolist() - - -def _assert_rejected( - checks, - logicals, - message, - *, - x_detectors=_X_DETECTORS, - z_detectors=_Z_DETECTORS, -): - with pytest.raises(ValueError, match=message): - gari_transform( - scipy.sparse.csc_matrix(checks), - scipy.sparse.csc_matrix(logicals), - x_detectors=x_detectors, - z_detectors=z_detectors, - ) - - -def test_rejects_unsupported_projection_structure(): - for x_projection, z_projection, message in [ - ([1, 0], [1, 1], "X-side projection"), - ([1, 1], [1, 0], "Z-side projection"), - ]: - _assert_rejected( - [ - [1, 0, x_projection[0]], - [0, 1, z_projection[0]], - [1, 0, x_projection[1]], - [0, 1, z_projection[1]], - ], - scipy.sparse.csc_matrix((1, 3)), - message, - ) - - duplicate_cases = [ - ( - "D_X", - [[1, 1, 0, 1], [0, 0, 1, 1], [1, 1, 0, 1], [0, 0, 1, 1]], - ), - ( - "D_Z", - [[1, 0, 0, 1], [0, 1, 1, 1], [1, 0, 0, 1], [0, 1, 1, 1]], - ), - ] - for side, checks in duplicate_cases: - _assert_rejected( - checks, - scipy.sparse.csc_matrix((1, 4)), - f"{side} has duplicate columns", - ) - - -def test_rejects_invalid_inputs(monkeypatch): - checks, logicals = _tiny_source() - for x_detectors, z_detectors, message in [ - ([0], [1, 3], "complete partition"), - ([0, 2], [1, 2, 3], "disjoint"), - ([0, 0, 2], [1, 3], "more than once"), - ([0, 2], [1, 4], "outside the detector range"), - ]: - _assert_rejected( - checks, - logicals, - message, - x_detectors=x_detectors, - z_detectors=z_detectors, - ) - - detectorless_checks = scipy.sparse.hstack( - [checks, scipy.sparse.csc_matrix((4, 1))], format="csc" - ) - detectorless_logicals = scipy.sparse.hstack( - [logicals, scipy.sparse.csc_matrix([[0], [1]])], format="csc" - ) - _assert_rejected( - detectorless_checks, - detectorless_logicals, - r"column 3.*logical support is \[1\]", - ) - _assert_rejected( - checks, - scipy.sparse.csc_matrix((1, 4)), - "same source column count", - ) - - nonbinary_checks = checks.astype(float) - nonbinary_checks.data[0] = 2 - _assert_rejected(nonbinary_checks, logicals, "binary values") - - for dem_text, message in [ - ("error(0.1) D0\ndetector D0", "at least four coordinates"), - ( - "error(0.1) D0\ndetector(0, 0, 2) D0", - "at least four coordinates", - ), - ( - "error(0.1) D0\ndetector(0, 0, 0, 2.5) D0", - "finite integer", - ), - ]: - with pytest.raises(ValueError, match=message): - detector_partition_from_fourth_coordinate( - stim.DetectorErrorModel(dem_text) - ) - - transform = _tiny_transform() - with pytest.raises(ValueError, match="one value per GARI column"): - _matrices_to_gari_error_model( - transform.checks, - transform.logicals, - np.full(transform.checks.shape[1] - 1, 0.1), - ) - with pytest.raises(ValueError, match="no detector support"): - _matrices_to_gari_error_model( - scipy.sparse.csc_matrix((1, 1)), - scipy.sparse.csc_matrix([[1]]), - np.asarray([0.1]), - ) - - transform = _tiny_transform() - for probabilities, message in [ - (np.asarray([0.1, 0.2]), "one value per source column"), - (np.asarray([[0.1, 0.2, 0.3]]), "one-dimensional"), - (np.asarray([0.0, 0.2, 0.3]), r"\(0, 0.5\]"), - (np.asarray([0.1, 0.2, 0.6]), r"\(0, 0.5\]"), - (np.asarray([0.1, np.nan, 0.3]), "finite"), - ]: - with pytest.raises(ValueError, match=message): - paper_prior_probabilities(transform, probabilities) - - source_probabilities = np.asarray([0.1, 0.2, 0.3]) - invalid_custom_priors = [ - (lambda _transform, _probabilities: np.asarray([0.1]), "one value"), - ( - lambda _transform, _probabilities: np.full((1, 5), 0.1), - "one-dimensional", - ), - ( - lambda _transform, _probabilities: np.asarray( - [0.1, 0.1, 0.1, 0.1, np.nan] - ), - "non-finite", - ), - ( - lambda _transform, _probabilities: np.asarray( - [0.1, 0.1, 0.1, 0.1, 0.0] - ), - r"\(0, 0.5\]", - ), - ( - lambda _transform, _probabilities: np.asarray( - [0.1, 0.1, 0.1, 0.1, 0.6] - ), - r"\(0, 0.5\]", - ), - ] - for prior_function, message in invalid_custom_priors: - with pytest.raises(ValueError, match=message): - build_gari_error_model( - transform, - source_probabilities, - prior_function=prior_function, - ) - with pytest.raises(ValueError, match="must be callable"): - build_gari_error_model( - transform, - source_probabilities, - prior_function=None, - ) - - failed_result = scipy.optimize.OptimizeResult( - success=False, message="planned solver failure" - ) - monkeypatch.setattr( - gari_module.scipy.optimize, - "linprog", - lambda *_args, **_kwargs: failed_result, - ) - with pytest.raises(RuntimeError, match="planned solver failure"): - tesseract_lp_maximin_prior_probabilities( - transform, source_probabilities - ) - - infeasible_result = scipy.optimize.OptimizeResult( - success=True, - message="claimed success", - x=np.asarray([0.0, 0.0, 1.0]), - ) - monkeypatch.setattr( - gari_module.scipy.optimize, - "linprog", - lambda *_args, **_kwargs: infeasible_result, - ) - with pytest.raises(RuntimeError, match="maximin constraints"): - tesseract_lp_maximin_prior_probabilities( - transform, source_probabilities - ) + prior_function=tesseract_xor_prior_probabilities, + ) + checks, logicals, probabilities = dem_to_matrices(gari_dem) + assert gari_dem.num_detectors == transform.checks.shape[0] + assert gari_dem.num_observables == transform.logicals.shape[0] + assert (checks != transform.checks).nnz == 0 + assert (logicals != transform.logicals).nnz == 0 + np.testing.assert_allclose(probabilities, xor_probabilities) if __name__ == "__main__": diff --git a/src/py/gari_convert.py b/src/py/gari_convert.py index 0b6ebab7..47e64db6 100644 --- a/src/py/gari_convert.py +++ b/src/py/gari_convert.py @@ -1,19 +1,6 @@ #!/usr/bin/env python3 -# 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. - -"""Converts Stim circuits into GARI error model and detector-layout files. + +"""Converts Stim circuits into GARI DEM and detector-layout files. The ``.dem`` file stores the GARI transformed check and logical matrices using Stim syntax. It is not a physical detector error model and must not be sampled. @@ -32,7 +19,7 @@ import stim from _tesseract_py_util.gari import ( - build_gari_error_model, + build_gari_dem, dem_to_matrices, detector_partition_from_fourth_coordinate, gari_transform, @@ -51,6 +38,14 @@ } +def _workspace_path(value: str | Path) -> Path: + path = Path(value) + workspace = os.environ.get("BUILD_WORKSPACE_DIRECTORY") + if workspace and not path.is_absolute(): + return Path(workspace) / path + return path + + def _output_paths( circuit_path: Path, prior_policy: str, @@ -59,9 +54,9 @@ def _output_paths( prefix = output_prefix or ( circuit_path.parent / "gari" - / f"{circuit_path.stem}.gari-{prior_policy}" + / f"{circuit_path.stem}-gari-{prior_policy}" ) - return Path(f"{prefix}.dem"), Path(f"{prefix}.layout.json") + return Path(f"{prefix}.dem"), Path(f"{prefix}-layout.json") def _circuit_paths(circuit_directory: Path) -> list[Path]: @@ -72,9 +67,7 @@ def _circuit_paths(circuit_directory: Path) -> list[Path]: paths = sorted( ( path - for path in circuit_directory.rglob( - "*.stim", recurse_symlinks=False - ) + for path in circuit_directory.rglob("*.stim", recurse_symlinks=False) if path.is_file() ), key=lambda path: path.relative_to(circuit_directory).as_posix(), @@ -111,15 +104,15 @@ def _layout_dict(transform, prior_policy: str) -> dict[str, object]: def _write_gari_outputs( - error_model_path: Path, - error_model_text: str, + gari_dem_path: Path, + gari_dem_text: str, layout_path: Path, layout_text: str, *, force: bool, ) -> None: outputs = [ - (error_model_path, error_model_text), + (gari_dem_path, gari_dem_text), (layout_path, layout_text), ] paths = [path for path, _ in outputs] @@ -133,14 +126,14 @@ def _write_gari_outputs( "GARI output files." ) - error_model_path.parent.mkdir(parents=True, exist_ok=True) + gari_dem_path.parent.mkdir(parents=True, exist_ok=True) scratch_paths: list[Path] = [] backups: dict[Path, Path] = {} published: list[Path] = [] def scratch_file(contents: str) -> Path: descriptor, name = tempfile.mkstemp( - dir=error_model_path.parent, prefix=".gari-convert-" + dir=gari_dem_path.parent, prefix=".gari-convert-" ) path = Path(name) scratch_paths.append(path) @@ -173,21 +166,16 @@ def _convert_circuit( circuit_path: Path, *, prior_policy: str, - basis_convention: str, output_prefix: Path | None, force: bool, ): - if basis_convention != _BASIS_CONVENTION: - raise ValueError( - f"Unsupported basis convention {basis_convention!r}." - ) if prior_policy not in _PRIOR_FUNCTIONS: raise ValueError(f"Unknown GARI prior policy {prior_policy!r}.") - error_model_path, layout_path = _output_paths( + gari_dem_path, layout_path = _output_paths( circuit_path, prior_policy, output_prefix ) - for path in [error_model_path, layout_path]: + for path in [gari_dem_path, layout_path]: aliases_input = circuit_path.resolve(strict=False) == path.resolve( strict=False ) @@ -223,33 +211,32 @@ def _convert_circuit( x_detectors=x_detectors, z_detectors=z_detectors, ) - gari_error_model = build_gari_error_model( + gari_dem = build_gari_dem( transform, probabilities, prior_function=_PRIOR_FUNCTIONS[prior_policy], ) - error_model_text = str(gari_error_model) - if not error_model_text.endswith("\n"): - error_model_text += "\n" + gari_dem_text = str(gari_dem) + if not gari_dem_text.endswith("\n"): + gari_dem_text += "\n" layout_text = json.dumps( _layout_dict(transform, prior_policy), indent=2, sort_keys=True ) + "\n" _write_gari_outputs( - error_model_path, - error_model_text, + gari_dem_path, + gari_dem_text, layout_path, layout_text, force=force, ) - return source_error_model, transform, error_model_path, layout_path + return source_error_model, transform, gari_dem_path, layout_path def _convert_directory( circuit_directory: Path, *, prior_policy: str, - basis_convention: str, force: bool, ) -> int: circuit_paths = _circuit_paths(circuit_directory) @@ -260,7 +247,6 @@ def _convert_directory( _convert_circuit( circuit_path, prior_policy=prior_policy, - basis_convention=basis_convention, output_prefix=None, force=force, ) @@ -275,7 +261,7 @@ def _convert_directory( f"Circuits found: {len(circuit_paths)}\n" f"Converted: {len(circuit_paths) - failures}\n" f"Failed: {failures}\n" - "GARI error model .dem files are storage only; do not sample." + "GARI DEM files store matrices only; do not sample them." ) return int(failures != 0) @@ -283,15 +269,15 @@ def _convert_directory( def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( description=( - "Convert correlated CSS Stim circuits into GARI error model .dem " + "Convert correlated CSS Stim circuits into GARI matrix .dem " "storage files and detector-layout JSON files." ) ) inputs = parser.add_mutually_exclusive_group(required=True) - inputs.add_argument("--circuit", type=Path) + inputs.add_argument("--circuit", type=_workspace_path) inputs.add_argument( "--circuit-directory", - type=Path, + type=_workspace_path, help="Recursively convert .stim circuits in deterministic order.", ) parser.add_argument( @@ -308,7 +294,7 @@ def main(argv: list[str] | None = None) -> int: ) parser.add_argument( "--output-prefix", - type=Path, + type=_workspace_path, help="Custom output prefix for --circuit only.", ) parser.add_argument( @@ -321,24 +307,18 @@ def main(argv: list[str] | None = None) -> int: if args.circuit_directory is not None: if args.output_prefix is not None: parser.error("--output-prefix can only be used with --circuit.") - try: + try: + if args.circuit_directory is not None: return _convert_directory( args.circuit_directory, prior_policy=args.prior_policy, - basis_convention=args.basis_convention, force=args.force, ) - except (OSError, ValueError) as ex: - print(f"gari_convert: {ex}", file=sys.stderr) - return 1 - - assert args.circuit is not None - try: - source_error_model, transform, error_model_path, layout_path = ( + assert args.circuit is not None + source_error_model, transform, gari_dem_path, layout_path = ( _convert_circuit( args.circuit, prior_policy=args.prior_policy, - basis_convention=args.basis_convention, output_prefix=args.output_prefix, force=args.force, ) @@ -362,8 +342,8 @@ def main(argv: list[str] | None = None) -> int: print(f"Prior policy: {args.prior_policy}") print("Logical placement: physical") print("Detector order: physical_then_virtual\n") - print("GARI error model (.dem storage only; do not sample):") - print(f" {error_model_path}\n") + print("GARI DEM (.dem matrix storage only; do not sample):") + print(f" {gari_dem_path}\n") print("Detector layout:") print(f" {layout_path}") return 0 diff --git a/src/py/gari_example.py b/src/py/gari_example.py new file mode 100644 index 00000000..d193f8a6 --- /dev/null +++ b/src/py/gari_example.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +"""Decode source-circuit samples using saved GARI matrix artifacts. + +The ``.dem`` stores transformed matrices, not a physical error model; only the +source circuit is sampled, and virtual detector entries remain zero. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +from pathlib import Path + +import numpy as np +import stim +import tesseract_decoder + + +_LAYOUT_SCHEMA = "tesseract.gari_layout.v1" +_DETECTOR_ORDER = "physical_then_virtual" +_LOGICAL_PLACEMENT = "physical" +_PRIOR_POLICIES = {"paper", "xor", "lp-maximin"} +_ROW_BLOCKS = ("physical_x", "physical_z", "virtual_z", "virtual_x") + + +def _workspace_path(path: Path) -> Path: + workspace = os.environ.get("BUILD_WORKSPACE_DIRECTORY") + return ( + Path(workspace) / path + if workspace and not path.is_absolute() + else path + ) + + +def _required_count(layout: dict[str, object], name: str) -> int: + value = layout.get(name) + if type(value) is not int or value < 0: + raise ValueError(f"Layout field {name!r} must be a nonnegative integer.") + return value + + +def _required_text(layout: dict[str, object], name: str) -> str: + value = layout.get(name) + if not isinstance(value, str) or not value: + raise ValueError(f"Layout field {name!r} must be a nonempty string.") + return value + + +def _validate_row_blocks( + layout: dict[str, object], source_count: int, gari_count: int +) -> None: + blocks = layout.get("row_blocks") + if not isinstance(blocks, dict): + raise ValueError("Layout field 'row_blocks' must be an object.") + expected_start = 0 + for name in _ROW_BLOCKS: + interval = blocks.get(name) + if ( + not isinstance(interval, list) + or len(interval) != 2 + or any(type(endpoint) is not int for endpoint in interval) + ): + raise ValueError( + f"row_blocks[{name!r}] must contain two integer endpoints." + ) + start, stop = interval + if start != expected_start: + raise ValueError( + f"row_blocks[{name!r}] must start at {expected_start}; " + f"found {start}." + ) + if stop < start: + raise ValueError(f"row_blocks[{name!r}] is not a valid interval.") + expected_start = stop + + if expected_start != gari_count: + raise ValueError( + f"Row blocks must end at GARI detector count {gari_count}; " + f"found {expected_start}." + ) + physical_stop = blocks["physical_z"][1] + if physical_stop != source_count: + raise ValueError( + "Physical row blocks must contain exactly one row per source " + f"detector; found {physical_stop} for {source_count}." + ) + + +def _load_layout(path: Path) -> tuple[int, int, tuple[int, ...], str, str, str]: + with path.open(encoding="utf-8") as file: + layout = json.load(file) + if not isinstance(layout, dict): + raise ValueError("GARI layout must be a JSON object.") + if layout.get("schema") != _LAYOUT_SCHEMA: + raise ValueError( + f"GARI layout schema must be {_LAYOUT_SCHEMA!r}; " + f"found {layout.get('schema')!r}." + ) + + source_count = _required_count(layout, "source_detector_count") + gari_count = _required_count(layout, "gari_detector_count") + if gari_count < source_count: + raise ValueError( + "GARI detector count must not be smaller than the source count." + ) + mapping = layout.get("source_to_gari") + if not isinstance(mapping, list): + raise ValueError("Layout field 'source_to_gari' must be a list.") + if len(mapping) != source_count: + raise ValueError( + "Layout field 'source_to_gari' must contain one target per " + f"source detector; found {len(mapping)} for {source_count}." + ) + for source, target in enumerate(mapping): + if type(target) is not int: + raise ValueError( + f"source_to_gari[{source}] must be an integer; found " + f"{target!r}." + ) + if target < 0 or target >= gari_count: + raise ValueError( + f"source_to_gari[{source}]={target} is outside [0, " + f"{gari_count})." + ) + if target >= source_count: + raise ValueError( + f"source_to_gari[{source}]={target} refers to a virtual row." + ) + if len(set(mapping)) != len(mapping): + raise ValueError("Layout field 'source_to_gari' must be injective.") + + prior_policy = _required_text(layout, "prior_policy") + if prior_policy not in _PRIOR_POLICIES: + raise ValueError(f"Unknown GARI prior policy {prior_policy!r}.") + metadata = { + "logical_placement": _LOGICAL_PLACEMENT, + "detector_order": _DETECTOR_ORDER, + } + for name, expected in metadata.items(): + value = _required_text(layout, name) + if value != expected: + raise ValueError( + f"Layout field {name!r} must be {expected!r}; found {value!r}." + ) + _validate_row_blocks(layout, source_count, gari_count) + return ( + source_count, gari_count, tuple(mapping), + prior_policy, _LOGICAL_PLACEMENT, _DETECTOR_ORDER, + ) + + +def _run( + circuit_path: Path, + dem_path: Path, + layout_path: Path, + *, + shots: int, + seed: int, +) -> None: + if shots <= 0: + raise ValueError("shots must be positive.") + if seed < 0 or seed >= 2**64: + raise ValueError("seed must be in [0, 2**64).") + + circuit = stim.Circuit.from_file(str(circuit_path)) + gari_dem = stim.DetectorErrorModel.from_file(str(dem_path)) + layout_values = _load_layout(layout_path) + source_count, gari_count, source_to_gari = layout_values[:3] + prior_policy, logical_placement, detector_order = layout_values[3:] + + count_checks = ( + ("Circuit detectors", circuit.num_detectors, source_count), + ("GARI DEM detectors", gari_dem.num_detectors, gari_count), + ( + "Circuit and GARI DEM observables", + circuit.num_observables, + gari_dem.num_observables, + ), + ) + for name, actual, expected in count_checks: + if actual != expected: + raise ValueError( + f"{name} differ: found {actual}, expected {expected}." + ) + + source_samples, actual_observables = circuit.compile_detector_sampler( + seed=seed + ).sample(shots=shots, separate_observables=True) + gari_samples = np.zeros((shots, gari_count), dtype=np.bool_) + gari_samples[:, np.asarray(source_to_gari, dtype=np.int64)] = source_samples + + config = tesseract_decoder.tesseract.TesseractConfig( + dem=gari_dem, det_orders=[list(range(gari_count))] + ) + predictions = config.compile_decoder().decode_batch(gari_samples) + if predictions.shape != actual_observables.shape: + raise RuntimeError( + f"Decoder returned observable shape {predictions.shape}; " + f"expected {actual_observables.shape}." + ) + failures = np.any(predictions != actual_observables, axis=1) + logical_failures = int(np.count_nonzero(failures)) + + print("GARI saved-artifact decoding completed") + print(f"Source circuit: {circuit_path}") + print(f"GARI DEM file: {dem_path} (.dem matrix storage; not sampled)") + print(f"Detector layout: {layout_path}") + print(f"Source detectors: {source_count}") + print(f"GARI detectors: {gari_count}") + print(f"Prior policy: {prior_policy}") + print(f"Logical placement: {logical_placement}") + print(f"Detector order: {detector_order}") + print(f"Shots: {shots}") + print(f"Logical failures: {logical_failures}/{shots}") + print("This small run is a functional smoke check, not a benchmark or proof.") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Sample a circuit and decode it using a saved GARI DEM." + ) + parser.add_argument("--circuit", required=True, type=Path) + parser.add_argument( + "--dem", required=True, type=Path, + help="Storage-only GARI matrix file; this file is never sampled.", + ) + parser.add_argument("--gari-layout", required=True, type=Path) + parser.add_argument("--shots", required=True, type=int) + parser.add_argument("--seed", required=True, type=int) + args = parser.parse_args(argv) + + try: + _run( + _workspace_path(args.circuit), + _workspace_path(args.dem), + _workspace_path(args.gari_layout), + shots=args.shots, + seed=args.seed, + ) + except (OSError, RuntimeError, ValueError) as ex: + print(f"gari_example: {ex}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From e50c83483c53d2d35db84ff57cdd11010bf24dbc Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sun, 26 Jul 2026 18:03:04 -0700 Subject: [PATCH 06/43] Simplify GARI source DEM pipeline --- src/py/README.md | 4 + src/py/_tesseract_py_util/BUILD | 1 - src/py/_tesseract_py_util/gari.py | 358 ++++++++----------------- src/py/_tesseract_py_util/gari_test.py | 5 + src/py/gari_convert.py | 12 +- src/py/gari_example.py | 149 +++------- 6 files changed, 168 insertions(+), 361 deletions(-) diff --git a/src/py/README.md b/src/py/README.md index 4aec3278..358e492e 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -697,6 +697,10 @@ nonzero if any circuit fails. Repository test data uses a color-code-style fourth coordinate: values at most `2` identify X detectors and values at least `3` identify Z detectors. +GARI conversion always uses a source DEM generated with +`decompose_errors=False` and `flatten_loops=True`, then fully flattened; +decomposed error instructions containing `^` are not supported. + The `.dem` stores GARI transformed matrices using Stim syntax; it is not a physical detector error model and must not be sampled. Decode samples from the original circuit with the companion layout using: diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index 64d297d3..55c9fc41 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -22,7 +22,6 @@ py_library( srcs = ["gari.py"], visibility = ["//:__subpackages__"], deps = [ - ":_tesseract_py_util", "@pypi//numpy", "@pypi//scipy", "@pypi//stim", diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index f131318c..9fcf1b21 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -40,6 +40,11 @@ storage and decoding representation. It is not a physical detector error model and must not be sampled. +GARI source DEMs must be generated with ``decompose_errors=False`` and +``flatten_loops=True``, then fully flattened. Each undecomposed Stim ``error`` +instruction is one source matrix column. Instructions containing Stim's ``^`` +decomposition separator are not supported. + For certain single-basis CSS memory experiments, the paper instead evaluates the logical observable on ``bar(e)_X`` or ``bar(e)_Z``. That placement is experiment-specific and is not implemented by this generic transform. @@ -54,7 +59,6 @@ from __future__ import annotations import dataclasses -import numbers from collections.abc import Callable, Sequence import numpy as np @@ -62,10 +66,6 @@ import scipy.sparse import stim -from _tesseract_py_util.decompose_errors import ( - undecomposed_error_detectors_and_observables, -) - @dataclasses.dataclass(frozen=True) class GariTransform: @@ -85,81 +85,35 @@ class GariTransform: virtual_x_rows: slice +def circuit_to_gari_source_dem( + circuit: stim.Circuit, +) -> stim.DetectorErrorModel: + """Creates the flattened, undecomposed source DEM required by GARI.""" + # flatten_loops removes repeats; flattened also resolves detector shifts. + return circuit.detector_error_model( + decompose_errors=False, + flatten_loops=True, + allow_gauge_detectors=True, + approximate_disjoint_errors=1, + ).flattened() + + def _canonical_binary_csc( matrix: scipy.sparse.spmatrix, *, name: str ) -> scipy.sparse.csc_matrix: if not scipy.sparse.issparse(matrix): raise ValueError(f"{name} must be a sparse matrix.") - if matrix.ndim != 2: - raise ValueError(f"{name} must be two-dimensional.") - - coordinate_matrix = matrix.tocoo(copy=True) - stored_values = np.asarray(coordinate_matrix.data) - if not np.all(np.isfinite(stored_values)): - raise ValueError(f"{name} must contain only finite binary values.") - is_binary = (stored_values == 0) | (stored_values == 1) - if not np.all(is_binary): - bad_value = stored_values[np.flatnonzero(~is_binary)[0]] - raise ValueError( - f"{name} must contain only binary values 0 or 1; found " - f"{bad_value!r}." - ) - - result = scipy.sparse.coo_matrix( - ( - stored_values.astype(np.int64), - (coordinate_matrix.row, coordinate_matrix.col), - ), - shape=coordinate_matrix.shape, - dtype=np.int64, - ).tocsc() + if np.any((matrix.data != 0) & (matrix.data != 1)): + raise ValueError(f"{name} must contain only binary values 0 or 1.") + result = matrix.astype(np.int64).tocsc(copy=True) result.sum_duplicates() - duplicate_sum_is_binary = (result.data == 0) | (result.data == 1) - if not np.all(duplicate_sum_is_binary): - bad_value = result.data[np.flatnonzero(~duplicate_sum_is_binary)[0]] - raise ValueError( - f"{name} must be canonical after combining duplicate entries; " - f"found stored value {bad_value!r}." - ) + if np.any((result.data != 0) & (result.data != 1)): + raise ValueError(f"{name} must contain only binary values 0 or 1.") result.eliminate_zeros() result.sort_indices() return result.astype(np.uint8) -def _validated_detector_indices( - detectors: Sequence[int], *, name: str, detector_count: int -) -> np.ndarray: - try: - values = list(detectors) - except TypeError as ex: - raise ValueError(f"{name} must be a one-dimensional sequence.") from ex - - result: list[int] = [] - seen: dict[int, int] = {} - for position, value in enumerate(values): - if isinstance(value, (bool, np.bool_)) or not isinstance( - value, numbers.Integral - ): - raise ValueError( - f"{name}[{position}] must be an integer detector index; " - f"found {value!r}." - ) - index = int(value) - if index < 0 or index >= detector_count: - raise ValueError( - f"{name}[{position}] = {index} is outside the detector " - f"range [0, {detector_count})." - ) - if index in seen: - raise ValueError( - f"{name} contains detector {index} more than once " - f"(positions {seen[index]} and {position})." - ) - seen[index] = position - result.append(index) - return np.asarray(result, dtype=np.int64) - - def _column_support( matrix: scipy.sparse.csc_matrix, column: int ) -> tuple[int, ...]: @@ -170,95 +124,58 @@ def _column_support( def _projection_lookup( projections: scipy.sparse.csc_matrix, - source_columns: np.ndarray, *, name: str, -) -> dict[tuple[int, ...], tuple[int, int]]: - lookup: dict[tuple[int, ...], tuple[int, int]] = {} - for local_column, source_column in enumerate(source_columns): +) -> dict[tuple[int, ...], int]: + lookup: dict[tuple[int, ...], int] = {} + for local_column in range(projections.shape[1]): support = _column_support(projections, local_column) if support in lookup: - _, previous_source_column = lookup[support] - raise ValueError( - f"{name} has duplicate columns from source columns " - f"{previous_source_column} and {int(source_column)}." - ) - lookup[support] = (local_column, int(source_column)) + raise ValueError(f"{name} has duplicate columns.") + lookup[support] = local_column return lookup -def _gf2_product( - left: scipy.sparse.csc_matrix, right: scipy.sparse.csc_matrix -) -> scipy.sparse.csc_matrix: - product = (left @ right).tocsc() - product.sum_duplicates() - product.data %= 2 - product.eliminate_zeros() - product.sort_indices() - return product.astype(np.uint8) - - -def _sparse_equal( - left: scipy.sparse.csc_matrix, right: scipy.sparse.csc_matrix -) -> bool: - return left.shape == right.shape and (left != right).nnz == 0 - - -def _readonly_int_array(values: np.ndarray) -> np.ndarray: - result = np.asarray(values, dtype=np.int64).copy() - result.setflags(write=False) - return result - - def dem_to_matrices( dem: stim.DetectorErrorModel, ) -> tuple[ scipy.sparse.csc_matrix, scipy.sparse.csc_matrix, np.ndarray ]: - """Extracts canonical binary matrices and probabilities from a Stim DEM. + """Extracts matrices from a flattened DEM made with no decomposition. - The DEM is flattened before extraction. Detector and observable targets in - each error are combined by symmetric difference, so repeated targets - cancel over GF(2). Declared dimensions are retained even when trailing rows - are unused. The result has one column and probability per flattened error. + Each Stim ``error`` instruction becomes one source matrix column. A ``^`` + separator is rejected because GARI requires ``decompose_errors=False``. """ - if not isinstance(dem, stim.DetectorErrorModel): - raise ValueError("dem must be a stim.DetectorErrorModel.") - - flattened = dem.flattened() detector_rows: list[int] = [] detector_columns: list[int] = [] logical_rows: list[int] = [] logical_columns: list[int] = [] probabilities: list[float] = [] - for instruction in flattened: - if instruction.type != "error": - continue - arguments = instruction.args_copy() - if len(arguments) != 1: + for instruction in dem: + if isinstance(instruction, stim.DemRepeatBlock) or ( + instruction.type == "shift_detectors" + ): raise ValueError( - "Each Stim error instruction must contain exactly one " - f"probability; found {len(arguments)} in {instruction}." + "GARI requires a fully flattened DEM generated with " + "decompose_errors=False." ) - probability = float(arguments[0]) - if not np.isfinite(probability) or probability < 0 or probability > 1: + if instruction.type != "error": + continue + targets = instruction.targets_copy() + if any(target.is_separator() for target in targets): raise ValueError( - f"Stim error probability must be finite and in [0, 1]; " - f"found {probability!r}." + "GARI requires a DEM generated with decompose_errors=False." ) - - detectors, observables = undecomposed_error_detectors_and_observables( - instruction - ) - source_column = len(probabilities) - for detector in detectors: - detector_rows.append(detector) - detector_columns.append(source_column) - for observable in observables: - logical_rows.append(observable) - logical_columns.append(source_column) - probabilities.append(probability) + column = len(probabilities) + probabilities.append(float(instruction.args_copy()[0])) + for target in targets: + if target.is_relative_detector_id(): + detector_rows.append(target.val) + detector_columns.append(column) + elif target.is_logical_observable_id(): + logical_rows.append(target.val) + logical_columns.append(column) source_column_count = len(probabilities) checks = scipy.sparse.csc_matrix( @@ -291,54 +208,35 @@ def _matrices_to_gari_dem( It is not a physical detector error model and must not be sampled to generate shots. """ - gari_checks = _canonical_binary_csc(checks, name="checks") - gari_logicals = _canonical_binary_csc(logicals, name="logicals") - if gari_checks.shape[1] != gari_logicals.shape[1]: - raise ValueError( - "checks and logicals must have the same GARI column count; " - f"found {gari_checks.shape[1]} and {gari_logicals.shape[1]}." - ) - probability_array = np.asarray(probabilities, dtype=np.float64) - if probability_array.ndim != 1: - raise ValueError("probabilities must be one-dimensional.") - if len(probability_array) != gari_checks.shape[1]: - raise ValueError( - "probabilities must contain one value per GARI column; " - f"found {len(probability_array)} for {gari_checks.shape[1]} " - "columns." - ) - if not np.all(np.isfinite(probability_array)): - raise ValueError("probabilities must contain only finite values.") - if np.any(probability_array < 0) or np.any(probability_array > 1): - raise ValueError("probabilities must lie in [0, 1].") + gari_checks = checks.tocsc() + gari_logicals = logicals.tocsc() detector_target = stim.target_relative_detector_id logical_target = stim.target_logical_observable_id gari_dem = stim.DetectorErrorModel() - for column, probability in enumerate(probability_array): - detector_targets = [ + for column, probability in enumerate(probabilities): + targets = [ detector_target(detector) for detector in _column_support(gari_checks, column) ] - if not detector_targets: - logical_support = list(_column_support(gari_logicals, column)) - raise ValueError( - f"GARI column {column} has no detector support; logical " - f"support is {logical_support}." - ) - targets = detector_targets targets.extend( logical_target(observable) for observable in _column_support(gari_logicals, column) ) gari_dem.append("error", float(probability), targets) - # One trailing declaration preserves each dimension after serialization. - if gari_checks.shape[0]: + # Declare only dimensions not already implied by the error targets. + if gari_checks.shape[0] and ( + not gari_checks.nnz + or np.max(gari_checks.indices) < gari_checks.shape[0] - 1 + ): gari_dem.append( "detector", [], [detector_target(gari_checks.shape[0] - 1)] ) - if gari_logicals.shape[0]: + if gari_logicals.shape[0] and ( + not gari_logicals.nnz + or np.max(gari_logicals.indices) < gari_logicals.shape[0] - 1 + ): gari_dem.append( "logical_observable", [], @@ -357,9 +255,6 @@ def detector_partition_from_fourth_coordinate( fourth coordinate is a finite integer: values at most ``2`` identify X detectors, while values at least ``3`` identify Z detectors. """ - if not isinstance(dem, stim.DetectorErrorModel): - raise ValueError("dem must be a stim.DetectorErrorModel.") - coordinates = dem.get_detector_coordinates() x_detectors: list[int] = [] z_detectors: list[int] = [] @@ -379,7 +274,9 @@ def detector_partition_from_fourth_coordinate( x_detectors.append(detector) else: z_detectors.append(detector) - return _readonly_int_array(x_detectors), _readonly_int_array(z_detectors) + return np.asarray(x_detectors, dtype=np.int64), np.asarray( + z_detectors, dtype=np.int64 + ) def gari_transform( @@ -418,27 +315,19 @@ def gari_transform( ) detector_count = source_checks.shape[0] - x_rows = _validated_detector_indices( - x_detectors, name="x_detectors", detector_count=detector_count - ) - z_rows = _validated_detector_indices( - z_detectors, name="z_detectors", detector_count=detector_count - ) - overlap = sorted(set(x_rows.tolist()) & set(z_rows.tolist())) - if overlap: - raise ValueError( - "x_detectors and z_detectors must be disjoint; detectors " - f"{overlap} appear in both." - ) - missing = sorted( - set(range(detector_count)) - - set(x_rows.tolist()) - - set(z_rows.tolist()) - ) - if missing: + x_rows = np.asarray(x_detectors) + z_rows = np.asarray(z_detectors) + if any( + rows.ndim != 1 or (rows.size and rows.dtype.kind not in "iu") + for rows in (x_rows, z_rows) + ): + raise ValueError("x_detectors and z_detectors must contain integers.") + x_rows = x_rows.astype(np.int64) + z_rows = z_rows.astype(np.int64) + partition = np.concatenate([x_rows, z_rows]) + if not np.array_equal(np.sort(partition), np.arange(detector_count)): raise ValueError( - "x_detectors and z_detectors must form a complete partition; " - f"missing detectors {missing}." + "x_detectors and z_detectors must partition all detector rows." ) x_checks = source_checks[x_rows, :].tocsc() @@ -470,8 +359,8 @@ def gari_transform( d_z = z_checks[:, e_x_columns].tocsc() d_x_prime = x_checks[:, e_y_columns].tocsc() d_z_prime = z_checks[:, e_y_columns].tocsc() - d_x_lookup = _projection_lookup(d_x, e_z_columns, name="D_X") - d_z_lookup = _projection_lookup(d_z, e_x_columns, name="D_Z") + d_x_lookup = _projection_lookup(d_x, name="D_X") + d_z_lookup = _projection_lookup(d_z, name="D_Z") u_rows: list[int] = [] v_rows: list[int] = [] @@ -489,8 +378,8 @@ def gari_transform( f"Source column {source_column} has Z-side projection " f"{list(z_projection)}, which does not equal a D_Z column." ) - u_rows.append(d_x_lookup[x_projection][0]) - v_rows.append(d_z_lookup[z_projection][0]) + u_rows.append(d_x_lookup[x_projection]) + v_rows.append(d_z_lookup[z_projection]) y_column_count = len(e_y_columns) y_indices = np.arange(y_column_count, dtype=np.int64) @@ -510,18 +399,6 @@ def gari_transform( shape=(len(e_x_columns), y_column_count), dtype=np.uint8, ) - for factor, name in ((u, "U"), (v, "V")): - if not np.all(np.diff(factor.indptr) == 1): - raise ValueError( - f"Every {name} column must contain exactly one nonzero." - ) - for base, factor, projection, message in ( - (d_x, u, d_x_prime, "D_X @ U does not equal the e_Y X-side projection."), - (d_z, v, d_z_prime, "D_Z @ V does not equal the e_Y Z-side projection."), - ): - if not _sparse_equal(_gf2_product(base, factor), projection): - raise ValueError(message) - x_row_count = len(x_rows) z_row_count = len(z_rows) e_z_count = len(e_z_columns) @@ -569,22 +446,23 @@ def gari_transform( source_to_gari[z_rows] = x_row_count + np.arange( z_row_count, dtype=np.int64 ) - if len(np.unique(source_to_gari)) != detector_count: - raise ValueError("The source-to-GARI detector mapping is not injective.") - if np.any(source_to_gari < 0) or np.any( - source_to_gari >= x_row_count + z_row_count + for values in ( + e_z_columns, + e_x_columns, + e_y_columns, + source_to_gari, ): - raise ValueError("The source-to-GARI detector mapping is out of range.") + values.setflags(write=False) return GariTransform( checks=augmented_checks, logicals=augmented_logicals, u=u, v=v, - e_z_columns=_readonly_int_array(e_z_columns), - e_x_columns=_readonly_int_array(e_x_columns), - e_y_columns=_readonly_int_array(e_y_columns), - source_to_gari_detectors=_readonly_int_array(source_to_gari), + e_z_columns=e_z_columns, + e_x_columns=e_x_columns, + e_y_columns=e_y_columns, + source_to_gari_detectors=source_to_gari, physical_x_rows=physical_x_rows, physical_z_rows=physical_z_rows, virtual_z_rows=virtual_z_rows, @@ -597,51 +475,37 @@ def _validated_probabilities( *, expected_count: int, name: str, - column_kind: str, ) -> np.ndarray: try: result = np.asarray(values, dtype=np.float64) except (TypeError, ValueError) as ex: raise ValueError(f"{name} must be a numeric array.") from ex - if result.ndim != 1: - raise ValueError(f"{name} must be one-dimensional.") - if len(result) != expected_count: + if ( + result.shape != (expected_count,) + or not np.all(np.isfinite(result)) + or np.any(result <= 0) + or np.any(result > 0.5) + ): raise ValueError( - f"{name} must contain one value per {column_kind} column; " - f"found {len(result)} for {expected_count} columns." + f"{name} must contain {expected_count} finite values in (0, 0.5]." ) - if not np.all(np.isfinite(result)): - raise ValueError(f"{name} contains a non-finite probability.") - if np.any(result <= 0) or np.any(result > 0.5): - raise ValueError(f"{name} must lie in (0, 0.5].") result = result.copy() result.setflags(write=False) return result -def _validated_source_probabilities( +def _physical_probability_blocks( transform: GariTransform, source_probabilities: np.ndarray -) -> np.ndarray: - if not isinstance(transform, GariTransform): - raise ValueError("transform must be a GariTransform.") - source_column_count = ( +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + source_count = ( len(transform.e_z_columns) + len(transform.e_x_columns) + len(transform.e_y_columns) ) - return _validated_probabilities( + probabilities = _validated_probabilities( source_probabilities, - expected_count=source_column_count, + expected_count=source_count, name="source_probabilities", - column_kind="source", - ) - - -def _physical_probability_blocks( - transform: GariTransform, source_probabilities: np.ndarray -) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - probabilities = _validated_source_probabilities( - transform, source_probabilities ) return ( probabilities[transform.e_z_columns], @@ -844,8 +708,15 @@ def build_gari_dem( Stim's DEM syntax is used only to store the GARI transformed matrices. The result is not a physical detector error model and must not be sampled. """ - probabilities = _validated_source_probabilities( - transform, source_probabilities + source_count = ( + len(transform.e_z_columns) + + len(transform.e_x_columns) + + len(transform.e_y_columns) + ) + probabilities = _validated_probabilities( + source_probabilities, + expected_count=source_count, + name="source_probabilities", ) if not callable(prior_function): raise ValueError("prior_function must be callable.") @@ -853,7 +724,6 @@ def build_gari_dem( prior_function(transform, probabilities), expected_count=transform.checks.shape[1], name="prior_function probabilities", - column_kind="GARI", ) return _matrices_to_gari_dem( transform.checks, transform.logicals, gari_probabilities diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 5d44a21c..d0624560 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -40,6 +40,11 @@ def _tiny_model(): def test_tiny_transform(): + with pytest.raises(ValueError, match="decompose_errors=False"): + dem_to_matrices(stim.DetectorErrorModel("error(0.1) D0 ^ D1")) + with pytest.raises(ValueError, match="fully flattened"): + dem_to_matrices(stim.DetectorErrorModel("shift_detectors 1")) + checks, logicals, _, transform = _tiny_model() np.testing.assert_array_equal(transform.e_z_columns, [0]) np.testing.assert_array_equal(transform.e_x_columns, [1]) diff --git a/src/py/gari_convert.py b/src/py/gari_convert.py index 47e64db6..d3536756 100644 --- a/src/py/gari_convert.py +++ b/src/py/gari_convert.py @@ -20,6 +20,7 @@ from _tesseract_py_util.gari import ( build_gari_dem, + circuit_to_gari_source_dem, dem_to_matrices, detector_partition_from_fourth_coordinate, gari_transform, @@ -191,16 +192,7 @@ def _convert_circuit( ) circuit = stim.Circuit.from_file(str(circuit_path)) - # Use the same analyzer policy as the Tesseract and Simplex CLIs. The GARI - # transformation consumes undecomposed errors and performs its own flattening. - source_error_model = circuit.detector_error_model( - decompose_errors=False, - flatten_loops=True, - allow_gauge_detectors=True, - approximate_disjoint_errors=1, - ignore_decomposition_failures=False, - block_decomposition_from_introducing_remnant_edges=False, - ) + source_error_model = circuit_to_gari_source_dem(circuit) checks, logicals, probabilities = dem_to_matrices(source_error_model) x_detectors, z_detectors = detector_partition_from_fourth_coordinate( source_error_model diff --git a/src/py/gari_example.py b/src/py/gari_example.py index d193f8a6..ff9556d0 100644 --- a/src/py/gari_example.py +++ b/src/py/gari_example.py @@ -21,8 +21,6 @@ _LAYOUT_SCHEMA = "tesseract.gari_layout.v1" _DETECTOR_ORDER = "physical_then_virtual" _LOGICAL_PLACEMENT = "physical" -_PRIOR_POLICIES = {"paper", "xor", "lp-maximin"} -_ROW_BLOCKS = ("physical_x", "physical_z", "virtual_z", "virtual_x") def _workspace_path(path: Path) -> Path: @@ -34,120 +32,54 @@ def _workspace_path(path: Path) -> Path: ) -def _required_count(layout: dict[str, object], name: str) -> int: - value = layout.get(name) - if type(value) is not int or value < 0: - raise ValueError(f"Layout field {name!r} must be a nonnegative integer.") - return value - - -def _required_text(layout: dict[str, object], name: str) -> str: - value = layout.get(name) - if not isinstance(value, str) or not value: - raise ValueError(f"Layout field {name!r} must be a nonempty string.") - return value - - -def _validate_row_blocks( - layout: dict[str, object], source_count: int, gari_count: int -) -> None: - blocks = layout.get("row_blocks") - if not isinstance(blocks, dict): - raise ValueError("Layout field 'row_blocks' must be an object.") - expected_start = 0 - for name in _ROW_BLOCKS: - interval = blocks.get(name) - if ( - not isinstance(interval, list) - or len(interval) != 2 - or any(type(endpoint) is not int for endpoint in interval) - ): - raise ValueError( - f"row_blocks[{name!r}] must contain two integer endpoints." - ) - start, stop = interval - if start != expected_start: - raise ValueError( - f"row_blocks[{name!r}] must start at {expected_start}; " - f"found {start}." - ) - if stop < start: - raise ValueError(f"row_blocks[{name!r}] is not a valid interval.") - expected_start = stop - - if expected_start != gari_count: - raise ValueError( - f"Row blocks must end at GARI detector count {gari_count}; " - f"found {expected_start}." - ) - physical_stop = blocks["physical_z"][1] - if physical_stop != source_count: - raise ValueError( - "Physical row blocks must contain exactly one row per source " - f"detector; found {physical_stop} for {source_count}." - ) - - def _load_layout(path: Path) -> tuple[int, int, tuple[int, ...], str, str, str]: with path.open(encoding="utf-8") as file: layout = json.load(file) - if not isinstance(layout, dict): - raise ValueError("GARI layout must be a JSON object.") - if layout.get("schema") != _LAYOUT_SCHEMA: + if not isinstance(layout, dict) or layout.get("schema") != _LAYOUT_SCHEMA: raise ValueError( - f"GARI layout schema must be {_LAYOUT_SCHEMA!r}; " - f"found {layout.get('schema')!r}." + f"GARI layout must use schema {_LAYOUT_SCHEMA!r}." ) - source_count = _required_count(layout, "source_detector_count") - gari_count = _required_count(layout, "gari_detector_count") - if gari_count < source_count: + source_count = layout.get("source_detector_count") + gari_count = layout.get("gari_detector_count") + if ( + type(source_count) is not int + or type(gari_count) is not int + or source_count < 0 + or gari_count < source_count + ): raise ValueError( - "GARI detector count must not be smaller than the source count." + "Layout detector counts must be nonnegative integers with " + "gari_detector_count >= source_detector_count." ) + mapping = layout.get("source_to_gari") - if not isinstance(mapping, list): - raise ValueError("Layout field 'source_to_gari' must be a list.") - if len(mapping) != source_count: + if not isinstance(mapping, list) or len(mapping) != source_count: raise ValueError( - "Layout field 'source_to_gari' must contain one target per " - f"source detector; found {len(mapping)} for {source_count}." + "Layout source_to_gari must contain one entry per source detector." + ) + if any( + type(target) is not int or not 0 <= target < gari_count + for target in mapping + ): + raise ValueError("Layout source_to_gari contains an invalid target.") + if len(set(mapping)) != source_count: + raise ValueError("Layout source_to_gari must be injective.") + + prior_policy = layout.get("prior_policy") + logical_placement = layout.get("logical_placement") + detector_order = layout.get("detector_order") + if not isinstance(prior_policy, str) or not prior_policy: + raise ValueError("Layout prior_policy must be a nonempty string.") + if logical_placement != _LOGICAL_PLACEMENT: + raise ValueError("Layout logical_placement must be 'physical'.") + if detector_order != _DETECTOR_ORDER: + raise ValueError( + "Layout detector_order must be 'physical_then_virtual'." ) - for source, target in enumerate(mapping): - if type(target) is not int: - raise ValueError( - f"source_to_gari[{source}] must be an integer; found " - f"{target!r}." - ) - if target < 0 or target >= gari_count: - raise ValueError( - f"source_to_gari[{source}]={target} is outside [0, " - f"{gari_count})." - ) - if target >= source_count: - raise ValueError( - f"source_to_gari[{source}]={target} refers to a virtual row." - ) - if len(set(mapping)) != len(mapping): - raise ValueError("Layout field 'source_to_gari' must be injective.") - - prior_policy = _required_text(layout, "prior_policy") - if prior_policy not in _PRIOR_POLICIES: - raise ValueError(f"Unknown GARI prior policy {prior_policy!r}.") - metadata = { - "logical_placement": _LOGICAL_PLACEMENT, - "detector_order": _DETECTOR_ORDER, - } - for name, expected in metadata.items(): - value = _required_text(layout, name) - if value != expected: - raise ValueError( - f"Layout field {name!r} must be {expected!r}; found {value!r}." - ) - _validate_row_blocks(layout, source_count, gari_count) return ( source_count, gari_count, tuple(mapping), - prior_policy, _LOGICAL_PLACEMENT, _DETECTOR_ORDER, + prior_policy, logical_placement, detector_order, ) @@ -166,9 +98,14 @@ def _run( circuit = stim.Circuit.from_file(str(circuit_path)) gari_dem = stim.DetectorErrorModel.from_file(str(dem_path)) - layout_values = _load_layout(layout_path) - source_count, gari_count, source_to_gari = layout_values[:3] - prior_policy, logical_placement, detector_order = layout_values[3:] + ( + source_count, + gari_count, + source_to_gari, + prior_policy, + logical_placement, + detector_order, + ) = _load_layout(layout_path) count_checks = ( ("Circuit detectors", circuit.num_detectors, source_count), From 47c0568bda99fbe438562350cddb145b81fe07e0 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sun, 26 Jul 2026 18:50:32 -0700 Subject: [PATCH 07/43] Further simplify GARI implementation --- src/py/_tesseract_py_util/BUILD | 1 - src/py/_tesseract_py_util/gari.py | 124 +++---------------------- src/py/_tesseract_py_util/gari_test.py | 41 +------- 3 files changed, 16 insertions(+), 150 deletions(-) diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index 55c9fc41..2cffce56 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -37,7 +37,6 @@ py_test( ":gari", "@pypi//numpy", "@pypi//pytest", - "@pypi//scipy", "@pypi//stim", ], ) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 9fcf1b21..60a7aa86 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -89,31 +89,12 @@ def circuit_to_gari_source_dem( circuit: stim.Circuit, ) -> stim.DetectorErrorModel: """Creates the flattened, undecomposed source DEM required by GARI.""" - # flatten_loops removes repeats; flattened also resolves detector shifts. return circuit.detector_error_model( decompose_errors=False, flatten_loops=True, - allow_gauge_detectors=True, - approximate_disjoint_errors=1, ).flattened() -def _canonical_binary_csc( - matrix: scipy.sparse.spmatrix, *, name: str -) -> scipy.sparse.csc_matrix: - if not scipy.sparse.issparse(matrix): - raise ValueError(f"{name} must be a sparse matrix.") - if np.any((matrix.data != 0) & (matrix.data != 1)): - raise ValueError(f"{name} must contain only binary values 0 or 1.") - result = matrix.astype(np.int64).tocsc(copy=True) - result.sum_duplicates() - if np.any((result.data != 0) & (result.data != 1)): - raise ValueError(f"{name} must contain only binary values 0 or 1.") - result.eliminate_zeros() - result.sort_indices() - return result.astype(np.uint8) - - def _column_support( matrix: scipy.sparse.csc_matrix, column: int ) -> tuple[int, ...]: @@ -153,13 +134,6 @@ def dem_to_matrices( probabilities: list[float] = [] for instruction in dem: - if isinstance(instruction, stim.DemRepeatBlock) or ( - instruction.type == "shift_detectors" - ): - raise ValueError( - "GARI requires a fully flattened DEM generated with " - "decompose_errors=False." - ) if instruction.type != "error": continue targets = instruction.targets_copy() @@ -226,17 +200,11 @@ def _matrices_to_gari_dem( gari_dem.append("error", float(probability), targets) # Declare only dimensions not already implied by the error targets. - if gari_checks.shape[0] and ( - not gari_checks.nnz - or np.max(gari_checks.indices) < gari_checks.shape[0] - 1 - ): + if gari_dem.num_detectors < gari_checks.shape[0]: gari_dem.append( "detector", [], [detector_target(gari_checks.shape[0] - 1)] ) - if gari_logicals.shape[0] and ( - not gari_logicals.nnz - or np.max(gari_logicals.indices) < gari_logicals.shape[0] - 1 - ): + if gari_dem.num_observables < gari_logicals.shape[0]: gari_dem.append( "logical_observable", [], @@ -252,8 +220,8 @@ def detector_partition_from_fourth_coordinate( This is the color-code-style convention followed by the test-data circuits associated with this repository, not a universal Stim convention. The - fourth coordinate is a finite integer: values at most ``2`` identify X - detectors, while values at least ``3`` identify Z detectors. + fourth-coordinate values at most ``2`` identify X detectors, while values + at least ``3`` identify Z detectors. """ coordinates = dem.get_detector_coordinates() x_detectors: list[int] = [] @@ -265,11 +233,6 @@ def detector_partition_from_fourth_coordinate( f"Detector {detector} must have at least four coordinates." ) role = detector_coordinates[3] - if not np.isfinite(role) or not float(role).is_integer(): - raise ValueError( - f"Detector {detector} has invalid fourth coordinate " - f"{role!r}; expected a finite integer." - ) if role <= 2: x_detectors.append(detector) else: @@ -306,8 +269,8 @@ def gari_transform( ValueError: The inputs do not satisfy the supported correlated CSS structure. """ - source_checks = _canonical_binary_csc(checks, name="checks") - source_logicals = _canonical_binary_csc(logicals, name="logicals") + source_checks = checks.tocsc() + source_logicals = logicals.tocsc() if source_checks.shape[1] != source_logicals.shape[1]: raise ValueError( "checks and logicals must have the same source column count; " @@ -315,15 +278,8 @@ def gari_transform( ) detector_count = source_checks.shape[0] - x_rows = np.asarray(x_detectors) - z_rows = np.asarray(z_detectors) - if any( - rows.ndim != 1 or (rows.size and rows.dtype.kind not in "iu") - for rows in (x_rows, z_rows) - ): - raise ValueError("x_detectors and z_detectors must contain integers.") - x_rows = x_rows.astype(np.int64) - z_rows = z_rows.astype(np.int64) + x_rows = np.asarray(x_detectors, dtype=np.int64) + z_rows = np.asarray(z_detectors, dtype=np.int64) partition = np.concatenate([x_rows, z_rows]) if not np.array_equal(np.sort(partition), np.arange(detector_count)): raise ValueError( @@ -446,14 +402,6 @@ def gari_transform( source_to_gari[z_rows] = x_row_count + np.arange( z_row_count, dtype=np.int64 ) - for values in ( - e_z_columns, - e_x_columns, - e_y_columns, - source_to_gari, - ): - values.setflags(write=False) - return GariTransform( checks=augmented_checks, logicals=augmented_logicals, @@ -476,37 +424,20 @@ def _validated_probabilities( expected_count: int, name: str, ) -> np.ndarray: - try: - result = np.asarray(values, dtype=np.float64) - except (TypeError, ValueError) as ex: - raise ValueError(f"{name} must be a numeric array.") from ex - if ( - result.shape != (expected_count,) - or not np.all(np.isfinite(result)) - or np.any(result <= 0) - or np.any(result > 0.5) + result = np.asarray(values, dtype=np.float64) + if result.shape != (expected_count,) or not np.all( + (result > 0) & (result <= 0.5) ): raise ValueError( f"{name} must contain {expected_count} finite values in (0, 0.5]." ) - result = result.copy() - result.setflags(write=False) return result def _physical_probability_blocks( transform: GariTransform, source_probabilities: np.ndarray ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - source_count = ( - len(transform.e_z_columns) - + len(transform.e_x_columns) - + len(transform.e_y_columns) - ) - probabilities = _validated_probabilities( - source_probabilities, - expected_count=source_count, - name="source_probabilities", - ) + probabilities = np.asarray(source_probabilities, dtype=np.float64) return ( probabilities[transform.e_z_columns], probabilities[transform.e_x_columns], @@ -660,37 +591,10 @@ def tesseract_lp_maximin_prior_probabilities( raise RuntimeError( "LP maximin prior solver failed: " + str(result.message) ) - solution = np.asarray( - [] if result.x is None else result.x, dtype=np.float64 - ) - if solution.shape != (auxiliary_count + 1,): - raise RuntimeError( - "LP maximin prior solver returned an invalid solution." - ) - if not np.all(np.isfinite(solution)): - raise RuntimeError( - "LP maximin prior solver returned non-finite costs." - ) - auxiliary_costs = solution[:-1] + auxiliary_costs = np.asarray(result.x[:-1]) residual_costs = source_costs - np.asarray( cost_matrix @ auxiliary_costs ).reshape(-1) - if ( - solution[-1] < 0 - or np.any(auxiliary_costs < 0) - or np.any(residual_costs < 0) - ): - raise RuntimeError( - "LP maximin prior solver returned negative costs." - ) - minimum_cost = solution[-1] - if np.any(auxiliary_costs < minimum_cost - 1e-8) or np.any( - residual_costs < minimum_cost - 1e-8 - ): - raise RuntimeError( - "LP maximin prior solver returned a solution that violates the " - "maximin constraints." - ) gari_costs = np.concatenate([residual_costs, auxiliary_costs]) return np.exp(-np.logaddexp(0, gari_costs)) @@ -718,8 +622,6 @@ def build_gari_dem( expected_count=source_count, name="source_probabilities", ) - if not callable(prior_function): - raise ValueError("prior_function must be callable.") gari_probabilities = _validated_probabilities( prior_function(transform, probabilities), expected_count=transform.checks.shape[1], diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index d0624560..b2a38191 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -1,8 +1,5 @@ -import itertools - import numpy as np import pytest -import scipy.sparse import stim from _tesseract_py_util.gari import ( @@ -36,21 +33,14 @@ def _tiny_model(): x_detectors=x_detectors, z_detectors=z_detectors, ) - return checks, logicals, probabilities, transform + return probabilities, transform def test_tiny_transform(): with pytest.raises(ValueError, match="decompose_errors=False"): dem_to_matrices(stim.DetectorErrorModel("error(0.1) D0 ^ D1")) - with pytest.raises(ValueError, match="fully flattened"): - dem_to_matrices(stim.DetectorErrorModel("shift_detectors 1")) - checks, logicals, _, transform = _tiny_model() - np.testing.assert_array_equal(transform.e_z_columns, [0]) - np.testing.assert_array_equal(transform.e_x_columns, [1]) - np.testing.assert_array_equal(transform.e_y_columns, [2]) - np.testing.assert_array_equal(transform.u.toarray(), [[1]]) - np.testing.assert_array_equal(transform.v.toarray(), [[1]]) + _, transform = _tiny_model() np.testing.assert_array_equal( transform.checks.toarray(), [ @@ -69,35 +59,10 @@ def test_tiny_transform(): np.testing.assert_array_equal( transform.source_to_gari_detectors, [0, 2, 1, 3] ) - assert ( - transform.physical_x_rows, - transform.physical_z_rows, - transform.virtual_z_rows, - transform.virtual_x_rows, - ) == (slice(0, 2), slice(2, 4), slice(4, 5), slice(5, 6)) - - for source_error in itertools.product([0, 1], repeat=3): - e_z, e_x, e_y = source_error - source_error = np.asarray(source_error, dtype=np.uint8) - gari_error = np.asarray( - [e_z, e_x, e_y, e_z ^ e_y, e_x ^ e_y], dtype=np.uint8 - ) - source_syndrome = np.asarray(checks @ source_error).reshape(-1) % 2 - expected = np.concatenate( - [source_syndrome[[0, 2, 1, 3]], np.zeros(2, dtype=np.uint8)] - ) - np.testing.assert_array_equal( - np.asarray(transform.checks @ gari_error).reshape(-1) % 2, - expected, - ) - np.testing.assert_array_equal( - np.asarray(transform.logicals @ gari_error).reshape(-1) % 2, - np.asarray(logicals @ source_error).reshape(-1) % 2, - ) def test_prior_probabilities_and_gari_dem_round_trip(): - _, _, source_probabilities, transform = _tiny_model() + source_probabilities, transform = _tiny_model() np.testing.assert_array_equal( paper_prior_probabilities(transform, source_probabilities), [0.1, 0.2, 0.3, 0.5, 0.5], From 5f1533cef38a47e86eaa5232fbefd7537353fb2f Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sun, 26 Jul 2026 20:12:51 -0700 Subject: [PATCH 08/43] Clarify and simplify GARI prior policies --- src/py/_tesseract_py_util/gari.py | 122 ++++++++++++++---------------- 1 file changed, 56 insertions(+), 66 deletions(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 60a7aa86..a568853f 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -418,22 +418,6 @@ def gari_transform( ) -def _validated_probabilities( - values: np.ndarray, - *, - expected_count: int, - name: str, -) -> np.ndarray: - result = np.asarray(values, dtype=np.float64) - if result.shape != (expected_count,) or not np.all( - (result > 0) & (result <= 0.5) - ): - raise ValueError( - f"{name} must contain {expected_count} finite values in (0, 0.5]." - ) - return result - - def _physical_probability_blocks( transform: GariTransform, source_probabilities: np.ndarray ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: @@ -452,9 +436,8 @@ def paper_prior_probabilities( Physical ``e_Z``, ``e_X``, and ``e_Y`` variables retain their source probabilities. Every auxiliary variable is assigned probability exactly - ``0.5``, giving it zero log-likelihood-ratio cost. This is the literature - reference policy, but those zero-cost branches can produce a very large - Tesseract search space. + ``0.5``. In Tesseract this gives the auxiliary variable zero search cost, + which can produce a very large search space. """ p_e_z, p_e_x, p_e_y = _physical_probability_blocks( transform, source_probabilities @@ -470,15 +453,6 @@ def paper_prior_probabilities( ) -def _xor_parity_probability(probabilities: np.ndarray) -> float: - if len(probabilities) == 1: - return float(probabilities[0]) - if np.any(probabilities == 0.5): - return 0.5 - log_even_bias = np.sum(np.log1p(-2 * probabilities), dtype=np.float64) - return float(-0.5 * np.expm1(log_even_bias)) - - def _auxiliary_xor_probabilities( base_probabilities: np.ndarray, y_probabilities: np.ndarray, @@ -493,7 +467,15 @@ def _auxiliary_xor_probabilities( parity_probabilities = np.concatenate( [np.asarray([base_probability]), y_probabilities[y_columns]] ) - result[row] = _xor_parity_probability(parity_probabilities) + if len(parity_probabilities) == 1: + result[row] = base_probability + elif np.any(parity_probabilities == 0.5): + result[row] = 0.5 + else: + log_even_bias = np.sum( + np.log1p(-2 * parity_probabilities), dtype=np.float64 + ) + result[row] = -0.5 * np.expm1(log_even_bias) return result @@ -508,9 +490,8 @@ def tesseract_xor_prior_probabilities( for numerical stability and does not clip invalid inputs. This is a Tesseract-specific experimental heuristic, not the published - GARI prior. It can represent evidence already present in the physical - variables and virtual constraints, and is not claimed to preserve the - exact source-model maximum-likelihood objective. + GARI prior. It only defines auxiliary search weights and makes no claim + about decoding optimality. """ p_e_z, p_e_x, p_e_y = _physical_probability_blocks( transform, source_probabilities @@ -524,36 +505,22 @@ def tesseract_xor_prior_probabilities( return np.concatenate([p_e_z, p_e_x, p_e_y, p_bar_e_z, p_bar_e_x]) -def _source_to_auxiliary_cost_matrix( - transform: GariTransform, -) -> scipy.sparse.csc_matrix: - e_z_count = len(transform.e_z_columns) - e_x_count = len(transform.e_x_columns) - identity = scipy.sparse.identity - return scipy.sparse.bmat( - [ - [identity(e_z_count, format="csc"), None], - [None, identity(e_x_count, format="csc")], - [transform.u.T, transform.v.T], - ], - format="csc", - ) - - def tesseract_lp_maximin_prior_probabilities( transform: GariTransform, source_probabilities: np.ndarray ) -> np.ndarray: """Balances nonnegative physical and auxiliary costs for Tesseract. - For source costs ``c = log((1-p)/p)`` and auxiliary costs ``g``, this - experimental policy maximizes a common lower bound ``t`` subject to - ``A g + t <= c`` and ``-g + t <= 0``. The returned physical costs are the - residuals ``c - A g`` and the remaining costs are ``g``. - - This is a practical Tesseract adaptation of exploratory mode Q, not part of - the GARI paper. It changes the search objective and is not claimed to - preserve exact maximum-likelihood decoding. Solver failure is a hard error; - there is no fallback or clipping. + The source costs ``c = log((1-p)/p)`` are ordered as ``[e_Z, e_X, e_Y]``. + The auxiliary costs ``g`` are ordered as ``[bar(e)_Z, bar(e)_X]``. The + incidence matrix is ``A = [[I, 0], [0, I], [U.T, V.T]]``, so the residual + physical costs are ``r = c - A g``. + + The LP maximizes a common floor ``t`` subject to ``r >= t``, ``g >= t``, + and nonnegative ``g`` and ``t``. It returns ``[r, g]`` converted back to + probabilities in GARI column order. This is a practical Tesseract + adaptation of exploratory mode Q, not part of the GARI paper. It only + defines search costs and makes no claim about decoding optimality. Solver + failure is a hard error; there is no fallback or clipping. """ p_e_z, p_e_x, p_e_y = _physical_probability_blocks( transform, source_probabilities @@ -562,7 +529,16 @@ def tesseract_lp_maximin_prior_probabilities( source_costs = np.log1p(-physical_probabilities) - np.log( physical_probabilities ) - cost_matrix = _source_to_auxiliary_cost_matrix(transform) + # A maps [bar(e)_Z, bar(e)_X] costs into [e_Z, e_X, e_Y] costs. + identity = scipy.sparse.identity + cost_matrix = scipy.sparse.bmat( + [ + [identity(len(p_e_z), format="csc"), None], + [None, identity(len(p_e_x), format="csc")], + [transform.u.T, transform.v.T], + ], + format="csc", + ) auxiliary_count = cost_matrix.shape[1] constraints = scipy.sparse.bmat( @@ -575,15 +551,16 @@ def tesseract_lp_maximin_prior_probabilities( ], format="csc", ) - bounds = np.concatenate( + constraint_limits = np.concatenate( [source_costs, np.zeros(auxiliary_count, dtype=np.float64)] ) objective = np.zeros(auxiliary_count + 1, dtype=np.float64) + # scipy.optimize.linprog minimizes, so minimizing -t maximizes t. objective[-1] = -1 result = scipy.optimize.linprog( objective, A_ub=constraints, - b_ub=bounds, + b_ub=constraint_limits, bounds=[(0, None)] * (auxiliary_count + 1), method="highs", ) @@ -612,20 +589,33 @@ def build_gari_dem( Stim's DEM syntax is used only to store the GARI transformed matrices. The result is not a physical detector error model and must not be sampled. """ + def validated_probabilities( + values: np.ndarray, expected_count: int, name: str + ) -> np.ndarray: + result = np.asarray(values, dtype=np.float64) + if result.shape != (expected_count,) or not np.all( + (result > 0) & (result <= 0.5) + ): + raise ValueError( + f"{name} must contain {expected_count} finite values in " + "(0, 0.5]." + ) + return result + source_count = ( len(transform.e_z_columns) + len(transform.e_x_columns) + len(transform.e_y_columns) ) - probabilities = _validated_probabilities( + probabilities = validated_probabilities( source_probabilities, - expected_count=source_count, - name="source_probabilities", + source_count, + "source_probabilities", ) - gari_probabilities = _validated_probabilities( + gari_probabilities = validated_probabilities( prior_function(transform, probabilities), - expected_count=transform.checks.shape[1], - name="prior_function probabilities", + transform.checks.shape[1], + "prior_function probabilities", ) return _matrices_to_gari_dem( transform.checks, transform.logicals, gari_probabilities From f0dc7f0af32938ff7cb6a4ab6138de886aaccf1f Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sun, 26 Jul 2026 20:38:22 -0700 Subject: [PATCH 09/43] Replace GARI maximin with max-barred-cost prior --- src/py/_tesseract_py_util/gari.py | 58 +++++++++++++------------- src/py/_tesseract_py_util/gari_test.py | 5 ++- src/py/gari_convert.py | 4 +- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index a568853f..9570acd3 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -505,22 +505,21 @@ def tesseract_xor_prior_probabilities( return np.concatenate([p_e_z, p_e_x, p_e_y, p_bar_e_z, p_bar_e_x]) -def tesseract_lp_maximin_prior_probabilities( +def tesseract_lp_max_barred_cost_prior_probabilities( transform: GariTransform, source_probabilities: np.ndarray ) -> np.ndarray: - """Balances nonnegative physical and auxiliary costs for Tesseract. + """Maximizes the total barred-variable search cost for Tesseract. The source costs ``c = log((1-p)/p)`` are ordered as ``[e_Z, e_X, e_Y]``. The auxiliary costs ``g`` are ordered as ``[bar(e)_Z, bar(e)_X]``. The incidence matrix is ``A = [[I, 0], [0, I], [U.T, V.T]]``, so the residual physical costs are ``r = c - A g``. - The LP maximizes a common floor ``t`` subject to ``r >= t``, ``g >= t``, - and nonnegative ``g`` and ``t``. It returns ``[r, g]`` converted back to - probabilities in GARI column order. This is a practical Tesseract - adaptation of exploratory mode Q, not part of the GARI paper. It only - defines search costs and makes no claim about decoding optimality. Solver - failure is a hard error; there is no fallback or clipping. + The LP maximizes ``sum(g)`` subject to ``A g <= c`` and ``g >= 0``. It + returns ``[r, g]`` converted back to probabilities in GARI column order. + This experimental policy is not part of the GARI paper. It only defines + search costs and makes no claim about decoding optimality. Solver failure + is a hard error; there is no fallback. """ p_e_z, p_e_x, p_e_y = _physical_probability_blocks( transform, source_probabilities @@ -541,37 +540,38 @@ def tesseract_lp_maximin_prior_probabilities( ) auxiliary_count = cost_matrix.shape[1] - constraints = scipy.sparse.bmat( - [ - [cost_matrix, np.ones((len(source_costs), 1))], - [ - -scipy.sparse.identity(auxiliary_count, format="csc"), - np.ones((auxiliary_count, 1)), - ], - ], - format="csc", - ) - constraint_limits = np.concatenate( - [source_costs, np.zeros(auxiliary_count, dtype=np.float64)] - ) - objective = np.zeros(auxiliary_count + 1, dtype=np.float64) - # scipy.optimize.linprog minimizes, so minimizing -t maximizes t. - objective[-1] = -1 + # A guarded alternative is to first maximize a common floor t, then + # maximize sum(g) while requiring t >= t_star - numerical_tolerance. + objective = -np.ones(auxiliary_count, dtype=np.float64) result = scipy.optimize.linprog( objective, - A_ub=constraints, - b_ub=constraint_limits, - bounds=[(0, None)] * (auxiliary_count + 1), + A_ub=cost_matrix, + b_ub=source_costs, + bounds=(0, None), method="highs", ) if not result.success: raise RuntimeError( - "LP maximin prior solver failed: " + str(result.message) + "LP max-barred-cost prior solver failed: " + str(result.message) ) - auxiliary_costs = np.asarray(result.x[:-1]) + tolerance = 1e-7 * max( + 1.0, float(np.max(source_costs, initial=0.0)) + ) + auxiliary_costs = np.asarray(result.x) + if np.min(auxiliary_costs, initial=0.0) < -tolerance: + raise RuntimeError( + "LP max-barred-cost solver returned an infeasible solution." + ) + auxiliary_costs = np.maximum(auxiliary_costs, 0.0) residual_costs = source_costs - np.asarray( cost_matrix @ auxiliary_costs ).reshape(-1) + if np.min(residual_costs, initial=0.0) < -tolerance: + raise RuntimeError( + "LP max-barred-cost solver returned an infeasible solution." + ) + # Normalize only active-constraint noise accepted by the LP solver. + residual_costs = np.maximum(residual_costs, 0.0) gari_costs = np.concatenate([residual_costs, auxiliary_costs]) return np.exp(-np.logaddexp(0, gari_costs)) diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index b2a38191..0bf60f8c 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -8,7 +8,7 @@ detector_partition_from_fourth_coordinate, gari_transform, paper_prior_probabilities, - tesseract_lp_maximin_prior_probabilities, + tesseract_lp_max_barred_cost_prior_probabilities, tesseract_xor_prior_probabilities, ) @@ -74,7 +74,7 @@ def test_prior_probabilities_and_gari_dem_round_trip(): xor_probabilities, [0.1, 0.2, 0.3, 0.34, 0.38] ) - lp_probabilities = tesseract_lp_maximin_prior_probabilities( + lp_probabilities = tesseract_lp_max_barred_cost_prior_probabilities( transform, source_probabilities ) lp_costs = np.log1p(-lp_probabilities) - np.log(lp_probabilities) @@ -85,6 +85,7 @@ def test_prior_probabilities_and_gari_dem_round_trip(): lp_costs[:3] + np.asarray([[1, 0], [0, 1], [1, 1]]) @ lp_costs[3:], source_costs, ) + np.testing.assert_allclose(lp_costs[3:].sum(), source_costs[2]) gari_dem = build_gari_dem( transform, diff --git a/src/py/gari_convert.py b/src/py/gari_convert.py index d3536756..2d8c038c 100644 --- a/src/py/gari_convert.py +++ b/src/py/gari_convert.py @@ -25,7 +25,7 @@ detector_partition_from_fourth_coordinate, gari_transform, paper_prior_probabilities, - tesseract_lp_maximin_prior_probabilities, + tesseract_lp_max_barred_cost_prior_probabilities, tesseract_xor_prior_probabilities, ) @@ -35,7 +35,7 @@ _PRIOR_FUNCTIONS = { "paper": paper_prior_probabilities, "xor": tesseract_xor_prior_probabilities, - "lp-maximin": tesseract_lp_maximin_prior_probabilities, + "lp-max-barred-cost": tesseract_lp_max_barred_cost_prior_probabilities, } From c5abf7fc2c1ea5ca08f36f200d54eb2b6cd47ef3 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sun, 26 Jul 2026 21:27:41 -0700 Subject: [PATCH 10/43] Simplify GARI helpers and use short-beam example --- src/py/README.md | 5 +-- src/py/_tesseract_py_util/gari.py | 51 ++++++++++++------------------- src/py/gari_convert.py | 12 +++----- src/py/gari_example.py | 47 +++++++++++++--------------- 4 files changed, 49 insertions(+), 66 deletions(-) diff --git a/src/py/README.md b/src/py/README.md index 358e492e..0979ccb8 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -715,6 +715,7 @@ bazel run --jobs=1 //src/py:gari_example -- \ ``` The example samples only the original circuit, scatters its physical detector -data according to the layout, leaves virtual detector entries zero, and uses -the single `physical_then_virtual` detector order. A 10-shot run is a +data according to the `physical_then_virtual` layout, and leaves virtual +detector entries zero. It uses the `tesseract-short-beam` preset with one +deterministic index-based (`DetIndex`) detector ordering. A 10-shot run is a functional smoke check, not a benchmark or mathematical proof. diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 9570acd3..0a0d45cf 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -95,7 +95,7 @@ def circuit_to_gari_source_dem( ).flattened() -def _column_support( +def _nonzero_column_rows( matrix: scipy.sparse.csc_matrix, column: int ) -> tuple[int, ...]: start = matrix.indptr[column] @@ -103,14 +103,14 @@ def _column_support( return tuple(int(v) for v in matrix.indices[start:stop]) -def _projection_lookup( +def _unique_column_index_by_rows( projections: scipy.sparse.csc_matrix, *, name: str, ) -> dict[tuple[int, ...], int]: lookup: dict[tuple[int, ...], int] = {} for local_column in range(projections.shape[1]): - support = _column_support(projections, local_column) + support = _nonzero_column_rows(projections, local_column) if support in lookup: raise ValueError(f"{name} has duplicate columns.") lookup[support] = local_column @@ -191,11 +191,11 @@ def _matrices_to_gari_dem( for column, probability in enumerate(probabilities): targets = [ detector_target(detector) - for detector in _column_support(gari_checks, column) + for detector in _nonzero_column_rows(gari_checks, column) ] targets.extend( logical_target(observable) - for observable in _column_support(gari_logicals, column) + for observable in _nonzero_column_rows(gari_logicals, column) ) gari_dem.append("error", float(probability), targets) @@ -305,7 +305,9 @@ def gari_transform( ) if detectorless_columns.size: source_column = int(detectorless_columns[0]) - logical_support = list(_column_support(source_logicals, source_column)) + logical_support = list( + _nonzero_column_rows(source_logicals, source_column) + ) raise ValueError( f"Source column {source_column} is detectorless; logical support " f"is {logical_support}." @@ -315,20 +317,20 @@ def gari_transform( d_z = z_checks[:, e_x_columns].tocsc() d_x_prime = x_checks[:, e_y_columns].tocsc() d_z_prime = z_checks[:, e_y_columns].tocsc() - d_x_lookup = _projection_lookup(d_x, name="D_X") - d_z_lookup = _projection_lookup(d_z, name="D_Z") + d_x_lookup = _unique_column_index_by_rows(d_x, name="D_X") + d_z_lookup = _unique_column_index_by_rows(d_z, name="D_Z") u_rows: list[int] = [] v_rows: list[int] = [] for local_y_column, source_column_value in enumerate(e_y_columns): source_column = int(source_column_value) - x_projection = _column_support(d_x_prime, local_y_column) + x_projection = _nonzero_column_rows(d_x_prime, local_y_column) if x_projection not in d_x_lookup: raise ValueError( f"Source column {source_column} has X-side projection " f"{list(x_projection)}, which does not equal a D_X column." ) - z_projection = _column_support(d_z_prime, local_y_column) + z_projection = _nonzero_column_rows(d_z_prime, local_y_column) if z_projection not in d_z_lookup: raise ValueError( f"Source column {source_column} has Z-side projection " @@ -453,30 +455,17 @@ def paper_prior_probabilities( ) -def _auxiliary_xor_probabilities( +def _barred_xor_probabilities( base_probabilities: np.ndarray, y_probabilities: np.ndarray, projection_matrix: scipy.sparse.csc_matrix, ) -> np.ndarray: - projection_rows = projection_matrix.tocsr() - result = np.empty(len(base_probabilities), dtype=np.float64) - for row, base_probability in enumerate(base_probabilities): - start = projection_rows.indptr[row] - stop = projection_rows.indptr[row + 1] - y_columns = projection_rows.indices[start:stop] - parity_probabilities = np.concatenate( - [np.asarray([base_probability]), y_probabilities[y_columns]] + """Returns marginals of ``base XOR projection_matrix @ e_Y``.""" + with np.errstate(divide="ignore"): + log_even_bias = np.log1p(-2 * base_probabilities) + ( + projection_matrix @ np.log1p(-2 * y_probabilities) ) - if len(parity_probabilities) == 1: - result[row] = base_probability - elif np.any(parity_probabilities == 0.5): - result[row] = 0.5 - else: - log_even_bias = np.sum( - np.log1p(-2 * parity_probabilities), dtype=np.float64 - ) - result[row] = -0.5 * np.expm1(log_even_bias) - return result + return -0.5 * np.expm1(log_even_bias) def tesseract_xor_prior_probabilities( @@ -496,10 +485,10 @@ def tesseract_xor_prior_probabilities( p_e_z, p_e_x, p_e_y = _physical_probability_blocks( transform, source_probabilities ) - p_bar_e_z = _auxiliary_xor_probabilities( + p_bar_e_z = _barred_xor_probabilities( p_e_z, p_e_y, transform.u ) - p_bar_e_x = _auxiliary_xor_probabilities( + p_bar_e_x = _barred_xor_probabilities( p_e_x, p_e_y, transform.v ) return np.concatenate([p_e_z, p_e_x, p_e_y, p_bar_e_z, p_bar_e_x]) diff --git a/src/py/gari_convert.py b/src/py/gari_convert.py index 2d8c038c..91995191 100644 --- a/src/py/gari_convert.py +++ b/src/py/gari_convert.py @@ -209,9 +209,7 @@ def _convert_circuit( prior_function=_PRIOR_FUNCTIONS[prior_policy], ) - gari_dem_text = str(gari_dem) - if not gari_dem_text.endswith("\n"): - gari_dem_text += "\n" + gari_dem_text = str(gari_dem).rstrip("\n") + "\n" layout_text = json.dumps( _layout_dict(transform, prior_policy), indent=2, sort_keys=True ) + "\n" @@ -296,9 +294,8 @@ def main(argv: list[str] | None = None) -> int: ) args = parser.parse_args(argv) - if args.circuit_directory is not None: - if args.output_prefix is not None: - parser.error("--output-prefix can only be used with --circuit.") + if args.circuit_directory is not None and args.output_prefix is not None: + parser.error("--output-prefix can only be used with --circuit.") try: if args.circuit_directory is not None: return _convert_directory( @@ -306,7 +303,6 @@ def main(argv: list[str] | None = None) -> int: prior_policy=args.prior_policy, force=args.force, ) - assert args.circuit is not None source_error_model, transform, gari_dem_path, layout_path = ( _convert_circuit( args.circuit, @@ -333,7 +329,7 @@ def main(argv: list[str] | None = None) -> int: print(f"{label + ':':23}{rows.stop - rows.start}") print(f"Prior policy: {args.prior_policy}") print("Logical placement: physical") - print("Detector order: physical_then_virtual\n") + print("GARI row order: physical_then_virtual\n") print("GARI DEM (.dem matrix storage only; do not sample):") print(f" {gari_dem_path}\n") print("Detector layout:") diff --git a/src/py/gari_example.py b/src/py/gari_example.py index ff9556d0..3744830e 100644 --- a/src/py/gari_example.py +++ b/src/py/gari_example.py @@ -21,6 +21,7 @@ _LAYOUT_SCHEMA = "tesseract.gari_layout.v1" _DETECTOR_ORDER = "physical_then_virtual" _LOGICAL_PLACEMENT = "physical" +_DECODER_PRESET = "tesseract-short-beam" def _workspace_path(path: Path) -> Path: @@ -32,7 +33,7 @@ def _workspace_path(path: Path) -> Path: ) -def _load_layout(path: Path) -> tuple[int, int, tuple[int, ...], str, str, str]: +def _load_layout(path: Path) -> tuple[int, int, tuple[int, ...], str]: with path.open(encoding="utf-8") as file: layout = json.load(file) if not isinstance(layout, dict) or layout.get("schema") != _LAYOUT_SCHEMA: @@ -67,20 +68,15 @@ def _load_layout(path: Path) -> tuple[int, int, tuple[int, ...], str, str, str]: raise ValueError("Layout source_to_gari must be injective.") prior_policy = layout.get("prior_policy") - logical_placement = layout.get("logical_placement") - detector_order = layout.get("detector_order") if not isinstance(prior_policy, str) or not prior_policy: raise ValueError("Layout prior_policy must be a nonempty string.") - if logical_placement != _LOGICAL_PLACEMENT: + if layout.get("logical_placement") != _LOGICAL_PLACEMENT: raise ValueError("Layout logical_placement must be 'physical'.") - if detector_order != _DETECTOR_ORDER: + if layout.get("detector_order") != _DETECTOR_ORDER: raise ValueError( "Layout detector_order must be 'physical_then_virtual'." ) - return ( - source_count, gari_count, tuple(mapping), - prior_policy, logical_placement, detector_order, - ) + return source_count, gari_count, tuple(mapping), prior_policy def _run( @@ -98,14 +94,9 @@ def _run( circuit = stim.Circuit.from_file(str(circuit_path)) gari_dem = stim.DetectorErrorModel.from_file(str(dem_path)) - ( - source_count, - gari_count, - source_to_gari, - prior_policy, - logical_placement, - detector_order, - ) = _load_layout(layout_path) + source_count, gari_count, source_to_gari, prior_policy = _load_layout( + layout_path + ) count_checks = ( ("Circuit detectors", circuit.num_detectors, source_count), @@ -128,17 +119,21 @@ def _run( gari_samples = np.zeros((shots, gari_count), dtype=np.bool_) gari_samples[:, np.asarray(source_to_gari, dtype=np.int64)] = source_samples - config = tesseract_decoder.tesseract.TesseractConfig( - dem=gari_dem, det_orders=[list(range(gari_count))] - ) - predictions = config.compile_decoder().decode_batch(gari_samples) + decoder = tesseract_decoder.make_tesseract_sinter_decoders_dict()[ + _DECODER_PRESET + ] + decoder.num_det_orders = 1 + compiled_decoder = decoder.compile_decoder_for_dem(dem=gari_dem) + predictions = compiled_decoder.decoder.decode_batch(gari_samples) + decoder_order_count = len(compiled_decoder.decoder.config.det_orders) if predictions.shape != actual_observables.shape: raise RuntimeError( f"Decoder returned observable shape {predictions.shape}; " f"expected {actual_observables.shape}." ) - failures = np.any(predictions != actual_observables, axis=1) - logical_failures = int(np.count_nonzero(failures)) + logical_failures = int( + np.count_nonzero(np.any(predictions != actual_observables, axis=1)) + ) print("GARI saved-artifact decoding completed") print(f"Source circuit: {circuit_path}") @@ -147,8 +142,10 @@ def _run( print(f"Source detectors: {source_count}") print(f"GARI detectors: {gari_count}") print(f"Prior policy: {prior_policy}") - print(f"Logical placement: {logical_placement}") - print(f"Detector order: {detector_order}") + print(f"Decoder preset: {_DECODER_PRESET}") + print(f"Decoder order count: {decoder_order_count}") + print(f"Logical placement: {_LOGICAL_PLACEMENT}") + print(f"GARI row order: {_DETECTOR_ORDER}") print(f"Shots: {shots}") print(f"Logical failures: {logical_failures}/{shots}") print("This small run is a functional smoke check, not a benchmark or proof.") From 2eec303b8670ef1ade8be82c59c4387979fbc8fa Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sun, 26 Jul 2026 21:40:42 -0700 Subject: [PATCH 11/43] Add GARI transform block metadata --- src/py/_tesseract_py_util/gari.py | 25 ++++++++++++++++++++++++- src/py/_tesseract_py_util/gari_test.py | 8 ++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 0a0d45cf..ca85a450 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -69,20 +69,28 @@ @dataclasses.dataclass(frozen=True) class GariTransform: - """Validated GARI transformed matrices and their block metadata.""" + """Validated GARI matrices with source-index arrays and transformed slices.""" checks: scipy.sparse.csc_matrix logicals: scipy.sparse.csc_matrix u: scipy.sparse.csc_matrix v: scipy.sparse.csc_matrix + source_checks_shape: tuple[int, int] + d_x_shape: tuple[int, int] + d_z_shape: tuple[int, int] e_z_columns: np.ndarray e_x_columns: np.ndarray e_y_columns: np.ndarray source_to_gari_detectors: np.ndarray + physical_rows: slice + virtual_rows: slice physical_x_rows: slice physical_z_rows: slice virtual_z_rows: slice virtual_x_rows: slice + physical_columns: slice + barred_z_columns: slice + barred_x_columns: slice def circuit_to_gari_source_dem( @@ -398,6 +406,13 @@ def gari_transform( virtual_x_rows = slice( virtual_z_rows.stop, virtual_z_rows.stop + e_x_count ) + physical_columns = slice(0, e_z_count + e_x_count + y_column_count) + barred_z_columns = slice( + physical_columns.stop, physical_columns.stop + e_z_count + ) + barred_x_columns = slice( + barred_z_columns.stop, barred_z_columns.stop + e_x_count + ) source_to_gari = np.empty(detector_count, dtype=np.int64) source_to_gari[x_rows] = np.arange(x_row_count, dtype=np.int64) @@ -409,14 +424,22 @@ def gari_transform( logicals=augmented_logicals, u=u, v=v, + source_checks_shape=source_checks.shape, + d_x_shape=d_x.shape, + d_z_shape=d_z.shape, e_z_columns=e_z_columns, e_x_columns=e_x_columns, e_y_columns=e_y_columns, source_to_gari_detectors=source_to_gari, + physical_rows=slice(0, physical_z_rows.stop), + virtual_rows=slice(virtual_z_rows.start, virtual_x_rows.stop), physical_x_rows=physical_x_rows, physical_z_rows=physical_z_rows, virtual_z_rows=virtual_z_rows, virtual_x_rows=virtual_x_rows, + physical_columns=physical_columns, + barred_z_columns=barred_z_columns, + barred_x_columns=barred_x_columns, ) diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 0bf60f8c..affa1649 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -59,6 +59,14 @@ def test_tiny_transform(): np.testing.assert_array_equal( transform.source_to_gari_detectors, [0, 2, 1, 3] ) + assert transform.source_checks_shape == (4, 3) + assert transform.d_x_shape == (2, 1) + assert transform.d_z_shape == (2, 1) + assert transform.physical_rows == slice(0, 4) + assert transform.virtual_rows == slice(4, 6) + assert transform.physical_columns == slice(0, 3) + assert transform.barred_z_columns == slice(3, 4) + assert transform.barred_x_columns == slice(4, 5) def test_prior_probabilities_and_gari_dem_round_trip(): From 4fb2c78e9560b06454fd9edfbc7b5e0fe23e4e18 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sun, 26 Jul 2026 21:53:56 -0700 Subject: [PATCH 12/43] Simplify GARI decoding example --- src/py/_tesseract_py_util/gari.py | 2 +- src/py/gari_example.py | 121 ++++++------------------------ 2 files changed, 24 insertions(+), 99 deletions(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index ca85a450..6ff927ca 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -186,7 +186,7 @@ def _matrices_to_gari_dem( ) -> stim.DetectorErrorModel: """Stores GARI transformed matrices using Stim's DEM syntax. - The result is a GARI matrix representation for decoding and interchange. + The result is a GARI matrix representation for decoding. It is not a physical detector error model and must not be sampled to generate shots. """ diff --git a/src/py/gari_example.py b/src/py/gari_example.py index 3744830e..deb58af1 100644 --- a/src/py/gari_example.py +++ b/src/py/gari_example.py @@ -3,6 +3,7 @@ The ``.dem`` stores transformed matrices, not a physical error model; only the source circuit is sampled, and virtual detector entries remain zero. +The layout must be the unchanged companion file written by ``gari_convert``. """ from __future__ import annotations @@ -10,7 +11,6 @@ import argparse import json import os -import sys from pathlib import Path import numpy as np @@ -18,9 +18,6 @@ import tesseract_decoder -_LAYOUT_SCHEMA = "tesseract.gari_layout.v1" -_DETECTOR_ORDER = "physical_then_virtual" -_LOGICAL_PLACEMENT = "physical" _DECODER_PRESET = "tesseract-short-beam" @@ -33,52 +30,6 @@ def _workspace_path(path: Path) -> Path: ) -def _load_layout(path: Path) -> tuple[int, int, tuple[int, ...], str]: - with path.open(encoding="utf-8") as file: - layout = json.load(file) - if not isinstance(layout, dict) or layout.get("schema") != _LAYOUT_SCHEMA: - raise ValueError( - f"GARI layout must use schema {_LAYOUT_SCHEMA!r}." - ) - - source_count = layout.get("source_detector_count") - gari_count = layout.get("gari_detector_count") - if ( - type(source_count) is not int - or type(gari_count) is not int - or source_count < 0 - or gari_count < source_count - ): - raise ValueError( - "Layout detector counts must be nonnegative integers with " - "gari_detector_count >= source_detector_count." - ) - - mapping = layout.get("source_to_gari") - if not isinstance(mapping, list) or len(mapping) != source_count: - raise ValueError( - "Layout source_to_gari must contain one entry per source detector." - ) - if any( - type(target) is not int or not 0 <= target < gari_count - for target in mapping - ): - raise ValueError("Layout source_to_gari contains an invalid target.") - if len(set(mapping)) != source_count: - raise ValueError("Layout source_to_gari must be injective.") - - prior_policy = layout.get("prior_policy") - if not isinstance(prior_policy, str) or not prior_policy: - raise ValueError("Layout prior_policy must be a nonempty string.") - if layout.get("logical_placement") != _LOGICAL_PLACEMENT: - raise ValueError("Layout logical_placement must be 'physical'.") - if layout.get("detector_order") != _DETECTOR_ORDER: - raise ValueError( - "Layout detector_order must be 'physical_then_virtual'." - ) - return source_count, gari_count, tuple(mapping), prior_policy - - def _run( circuit_path: Path, dem_path: Path, @@ -87,37 +38,19 @@ def _run( shots: int, seed: int, ) -> None: - if shots <= 0: - raise ValueError("shots must be positive.") - if seed < 0 or seed >= 2**64: - raise ValueError("seed must be in [0, 2**64).") - circuit = stim.Circuit.from_file(str(circuit_path)) gari_dem = stim.DetectorErrorModel.from_file(str(dem_path)) - source_count, gari_count, source_to_gari, prior_policy = _load_layout( - layout_path - ) - - count_checks = ( - ("Circuit detectors", circuit.num_detectors, source_count), - ("GARI DEM detectors", gari_dem.num_detectors, gari_count), - ( - "Circuit and GARI DEM observables", - circuit.num_observables, - gari_dem.num_observables, - ), - ) - for name, actual, expected in count_checks: - if actual != expected: - raise ValueError( - f"{name} differ: found {actual}, expected {expected}." - ) + with layout_path.open(encoding="utf-8") as file: + layout = json.load(file) source_samples, actual_observables = circuit.compile_detector_sampler( seed=seed ).sample(shots=shots, separate_observables=True) - gari_samples = np.zeros((shots, gari_count), dtype=np.bool_) - gari_samples[:, np.asarray(source_to_gari, dtype=np.int64)] = source_samples + gari_samples = np.zeros( + (source_samples.shape[0], gari_dem.num_detectors), dtype=np.bool_ + ) + source_to_gari = np.asarray(layout["source_to_gari"]) + gari_samples[:, source_to_gari] = source_samples decoder = tesseract_decoder.make_tesseract_sinter_decoders_dict()[ _DECODER_PRESET @@ -127,10 +60,7 @@ def _run( predictions = compiled_decoder.decoder.decode_batch(gari_samples) decoder_order_count = len(compiled_decoder.decoder.config.det_orders) if predictions.shape != actual_observables.shape: - raise RuntimeError( - f"Decoder returned observable shape {predictions.shape}; " - f"expected {actual_observables.shape}." - ) + raise ValueError("Circuit and GARI observable counts differ.") logical_failures = int( np.count_nonzero(np.any(predictions != actual_observables, axis=1)) ) @@ -139,19 +69,19 @@ def _run( print(f"Source circuit: {circuit_path}") print(f"GARI DEM file: {dem_path} (.dem matrix storage; not sampled)") print(f"Detector layout: {layout_path}") - print(f"Source detectors: {source_count}") - print(f"GARI detectors: {gari_count}") - print(f"Prior policy: {prior_policy}") + print(f"Source detectors: {circuit.num_detectors}") + print(f"GARI detectors: {gari_dem.num_detectors}") + print(f"Prior policy: {layout['prior_policy']}") print(f"Decoder preset: {_DECODER_PRESET}") print(f"Decoder order count: {decoder_order_count}") - print(f"Logical placement: {_LOGICAL_PLACEMENT}") - print(f"GARI row order: {_DETECTOR_ORDER}") + print(f"Logical placement: {layout['logical_placement']}") + print(f"GARI row order: {layout['detector_order']}") print(f"Shots: {shots}") print(f"Logical failures: {logical_failures}/{shots}") print("This small run is a functional smoke check, not a benchmark or proof.") -def main(argv: list[str] | None = None) -> int: +def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser( description="Sample a circuit and decode it using a saved GARI DEM." ) @@ -165,19 +95,14 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--seed", required=True, type=int) args = parser.parse_args(argv) - try: - _run( - _workspace_path(args.circuit), - _workspace_path(args.dem), - _workspace_path(args.gari_layout), - shots=args.shots, - seed=args.seed, - ) - except (OSError, RuntimeError, ValueError) as ex: - print(f"gari_example: {ex}", file=sys.stderr) - return 1 - return 0 + _run( + _workspace_path(args.circuit), + _workspace_path(args.dem), + _workspace_path(args.gari_layout), + shots=args.shots, + seed=args.seed, + ) if __name__ == "__main__": - raise SystemExit(main()) + main() From ec06ca07d55b96535d7786a70af23ef3e809a3a2 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sun, 26 Jul 2026 21:59:18 -0700 Subject: [PATCH 13/43] Remove unused GARI layout row blocks --- src/py/gari_convert.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/py/gari_convert.py b/src/py/gari_convert.py index 91995191..1fdfa9a2 100644 --- a/src/py/gari_convert.py +++ b/src/py/gari_convert.py @@ -81,12 +81,6 @@ def _circuit_paths(circuit_directory: Path) -> list[Path]: def _layout_dict(transform, prior_policy: str) -> dict[str, object]: - blocks = { - "physical_x": transform.physical_x_rows, - "physical_z": transform.physical_z_rows, - "virtual_z": transform.virtual_z_rows, - "virtual_x": transform.virtual_x_rows, - } return { "schema": _LAYOUT_SCHEMA, "source_detector_count": len(transform.source_to_gari_detectors), @@ -94,10 +88,6 @@ def _layout_dict(transform, prior_policy: str) -> dict[str, object]: "source_to_gari": [ int(value) for value in transform.source_to_gari_detectors ], - "row_blocks": { - name: [int(rows.start), int(rows.stop)] - for name, rows in blocks.items() - }, "detector_order": "physical_then_virtual", "logical_placement": "physical", "prior_policy": prior_policy, From 6bc578802b006d3f1ce5c96b0a0f22ce5c2b860d Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 08:21:44 -0700 Subject: [PATCH 14/43] Fix GARI utility package initialization --- src/py/_tesseract_py_util/BUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index 2cffce56..f5b64eca 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -22,6 +22,7 @@ py_library( srcs = ["gari.py"], visibility = ["//:__subpackages__"], deps = [ + ":_tesseract_py_util", "@pypi//numpy", "@pypi//scipy", "@pypi//stim", From 7d9f97c35762d208ceaaedcc9a724bb3cf933fae Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 14:15:24 -0700 Subject: [PATCH 15/43] Add compact GARI circuit conversion entry point --- src/py/_tesseract_py_util/BUILD | 14 ++++++++ src/py/_tesseract_py_util/gari.py | 57 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index f5b64eca..797b4e9e 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -1,5 +1,6 @@ load("@rules_python//python:py_test.bzl", "py_test") load("@rules_python//python:py_library.bzl", "py_library") +load("@rules_python//python:py_binary.bzl", "py_binary") py_library( name = "_tesseract_py_util", @@ -29,6 +30,19 @@ py_library( ], ) +py_binary( + name = "gari_main", + srcs = ["gari.py"], + main = "gari.py", + visibility = ["//visibility:public"], + deps = [ + ":_tesseract_py_util", + "@pypi//numpy", + "@pypi//scipy", + "@pypi//stim", + ], +) + py_test( name = "gari_test", srcs = ["gari_test.py"], diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 6ff927ca..fe106641 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -59,7 +59,10 @@ from __future__ import annotations import dataclasses +import json +import sys from collections.abc import Callable, Sequence +from pathlib import Path import numpy as np import scipy.optimize @@ -632,3 +635,57 @@ def validated_probabilities( return _matrices_to_gari_dem( transform.checks, transform.logicals, gari_probabilities ) + + +def circuit_to_gari( + circuit: stim.Circuit, + *, + prior_function: Callable[[GariTransform, np.ndarray], np.ndarray], +) -> tuple[stim.DetectorErrorModel, dict[str, object]]: + """Converts one circuit into a GARI matrix DEM and v1 layout.""" + source_dem = circuit_to_gari_source_dem(circuit) + checks, logicals, probabilities = dem_to_matrices(source_dem) + x_detectors, z_detectors = detector_partition_from_fourth_coordinate( + source_dem + ) + transform = gari_transform( + checks, + logicals, + x_detectors=x_detectors, + z_detectors=z_detectors, + ) + gari_dem = build_gari_dem( + transform, probabilities, prior_function=prior_function + ) + layout = { + "schema": "tesseract.gari_layout.v1", + "source_detector_count": len(transform.source_to_gari_detectors), + "gari_detector_count": transform.checks.shape[0], + "source_to_gari": transform.source_to_gari_detectors.tolist(), + "detector_order": "physical_then_virtual", + } + return gari_dem, layout + + +if __name__ == "__main__": + circuit_name, prior_name = sys.argv[1:] + circuit_path = Path(circuit_name) + prior_function = { + "paper": paper_prior_probabilities, + "xor": tesseract_xor_prior_probabilities, + "lp-max-barred-cost": ( + tesseract_lp_max_barred_cost_prior_probabilities + ), + }[prior_name] + gari_dem, gari_layout = circuit_to_gari( + stim.Circuit.from_file(str(circuit_path)), + prior_function=prior_function, + ) + output_prefix = circuit_path.with_suffix("") + Path(f"{output_prefix}-gari-{prior_name}.dem").write_text( + str(gari_dem).rstrip("\n") + "\n", encoding="utf-8" + ) + Path(f"{output_prefix}-gari-{prior_name}-layout.json").write_text( + json.dumps(gari_layout, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) From 0dc37213aff5877f51846fa3b21ffd6ea1227b73 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 14:16:46 -0700 Subject: [PATCH 16/43] Remove standalone GARI scripts --- src/py/BUILD | 23 --- src/py/README.md | 41 ----- src/py/gari_convert.py | 331 ----------------------------------------- src/py/gari_example.py | 108 -------------- 4 files changed, 503 deletions(-) delete mode 100644 src/py/gari_convert.py delete mode 100644 src/py/gari_example.py diff --git a/src/py/BUILD b/src/py/BUILD index a8e1da59..fedad49e 100644 --- a/src/py/BUILD +++ b/src/py/BUILD @@ -94,29 +94,6 @@ py_test( imports = ["..", "."], ) -py_binary( - name = "gari_convert", - srcs = ["gari_convert.py"], - imports = ["."], - visibility = ["//visibility:public"], - deps = [ - "//src/py/_tesseract_py_util:gari", - "@pypi//stim", - ], -) - -py_binary( - name = "gari_example", - srcs = ["gari_example.py"], - imports = ["..", "."], - visibility = ["//visibility:public"], - deps = [ - "//src:lib_tesseract_decoder", - "@pypi//numpy", - "@pypi//stim", - ], -) - py_test( name = "stub_test", srcs = ["stub_test.py"], diff --git a/src/py/README.md b/src/py/README.md index 0979ccb8..8b566e29 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -678,44 +678,3 @@ nice_calibrated_dem = demutil.regeneralize_spatial_dem( ) # Result will have error probability (0.1 + 0.2) / 2 = 0.15 ``` - -### GARI transformed-matrix workflow - -Convert one correlated CSS Stim circuit with: - -```bash -bazel run --jobs=1 //src/py:gari_convert -- \ - --circuit path/to/circuit.stim \ - --prior-policy xor -``` - -Outputs default to the circuit's sibling `gari/` directory as -`-gari-.dem` and `-gari--layout.json`. Replace -`--circuit PATH` with `--circuit-directory PATH` to scan a circuit tree -sequentially and deterministically. The scan continues after failures and exits -nonzero if any circuit fails. Repository test data uses a color-code-style -fourth coordinate: values at most `2` identify X detectors and values at least -`3` identify Z detectors. - -GARI conversion always uses a source DEM generated with -`decompose_errors=False` and `flatten_loops=True`, then fully flattened; -decomposed error instructions containing `^` are not supported. - -The `.dem` stores GARI transformed matrices using Stim syntax; it is not a -physical detector error model and must not be sampled. Decode samples from the -original circuit with the companion layout using: - -```bash -bazel run --jobs=1 //src/py:gari_example -- \ - --circuit path/to/circuit.stim \ - --dem path/to/model-gari-xor.dem \ - --gari-layout path/to/model-gari-xor-layout.json \ - --shots 10 \ - --seed 0 -``` - -The example samples only the original circuit, scatters its physical detector -data according to the `physical_then_virtual` layout, and leaves virtual -detector entries zero. It uses the `tesseract-short-beam` preset with one -deterministic index-based (`DetIndex`) detector ordering. A 10-shot run is a -functional smoke check, not a benchmark or mathematical proof. diff --git a/src/py/gari_convert.py b/src/py/gari_convert.py deleted file mode 100644 index 1fdfa9a2..00000000 --- a/src/py/gari_convert.py +++ /dev/null @@ -1,331 +0,0 @@ -#!/usr/bin/env python3 - -"""Converts Stim circuits into GARI DEM and detector-layout files. - -The ``.dem`` file stores the GARI transformed check and logical matrices using -Stim syntax. It is not a physical detector error model and must not be sampled. -The layout JSON maps source detector samples into the GARI detector space. -""" - -from __future__ import annotations - -import argparse -import json -import os -import sys -import tempfile -from pathlib import Path - -import stim - -from _tesseract_py_util.gari import ( - build_gari_dem, - circuit_to_gari_source_dem, - dem_to_matrices, - detector_partition_from_fourth_coordinate, - gari_transform, - paper_prior_probabilities, - tesseract_lp_max_barred_cost_prior_probabilities, - tesseract_xor_prior_probabilities, -) - - -_LAYOUT_SCHEMA = "tesseract.gari_layout.v1" -_BASIS_CONVENTION = "color-code-style-fourth-coordinate" -_PRIOR_FUNCTIONS = { - "paper": paper_prior_probabilities, - "xor": tesseract_xor_prior_probabilities, - "lp-max-barred-cost": tesseract_lp_max_barred_cost_prior_probabilities, -} - - -def _workspace_path(value: str | Path) -> Path: - path = Path(value) - workspace = os.environ.get("BUILD_WORKSPACE_DIRECTORY") - if workspace and not path.is_absolute(): - return Path(workspace) / path - return path - - -def _output_paths( - circuit_path: Path, - prior_policy: str, - output_prefix: Path | None, -) -> tuple[Path, Path]: - prefix = output_prefix or ( - circuit_path.parent - / "gari" - / f"{circuit_path.stem}-gari-{prior_policy}" - ) - return Path(f"{prefix}.dem"), Path(f"{prefix}-layout.json") - - -def _circuit_paths(circuit_directory: Path) -> list[Path]: - if circuit_directory.is_symlink() or not circuit_directory.is_dir(): - raise ValueError( - f"Circuit directory is not a directory: {circuit_directory}" - ) - paths = sorted( - ( - path - for path in circuit_directory.rglob("*.stim", recurse_symlinks=False) - if path.is_file() - ), - key=lambda path: path.relative_to(circuit_directory).as_posix(), - ) - if not paths: - raise ValueError( - f"No .stim circuits found under {circuit_directory}." - ) - return paths - - -def _layout_dict(transform, prior_policy: str) -> dict[str, object]: - return { - "schema": _LAYOUT_SCHEMA, - "source_detector_count": len(transform.source_to_gari_detectors), - "gari_detector_count": transform.checks.shape[0], - "source_to_gari": [ - int(value) for value in transform.source_to_gari_detectors - ], - "detector_order": "physical_then_virtual", - "logical_placement": "physical", - "prior_policy": prior_policy, - } - - -def _write_gari_outputs( - gari_dem_path: Path, - gari_dem_text: str, - layout_path: Path, - layout_text: str, - *, - force: bool, -) -> None: - outputs = [ - (gari_dem_path, gari_dem_text), - (layout_path, layout_text), - ] - paths = [path for path, _ in outputs] - if any(path.is_dir() for path in paths): - raise IsADirectoryError("A GARI output path is a directory.") - existing = [path for path in paths if os.path.lexists(path)] - if existing and not force: - names = ", ".join(str(path) for path in existing) - raise FileExistsError( - f"Output already exists: {names}. Use --force to replace both " - "GARI output files." - ) - - gari_dem_path.parent.mkdir(parents=True, exist_ok=True) - scratch_paths: list[Path] = [] - backups: dict[Path, Path] = {} - published: list[Path] = [] - - def scratch_file(contents: str) -> Path: - descriptor, name = tempfile.mkstemp( - dir=gari_dem_path.parent, prefix=".gari-convert-" - ) - path = Path(name) - scratch_paths.append(path) - with os.fdopen(descriptor, "w", encoding="utf-8") as file: - file.write(contents) - return path - - try: - staged = [scratch_file(contents) for _, contents in outputs] - for path in existing: - backup = scratch_file("") - os.replace(path, backup) - backups[path] = backup - for temporary, final in zip(staged, paths): - os.replace(temporary, final) - published.append(final) - except BaseException: - for path in published: - path.unlink(missing_ok=True) - for path, backup in backups.items(): - if os.path.lexists(backup): - os.replace(backup, path) - raise - finally: - for path in scratch_paths: - path.unlink(missing_ok=True) - - -def _convert_circuit( - circuit_path: Path, - *, - prior_policy: str, - output_prefix: Path | None, - force: bool, -): - if prior_policy not in _PRIOR_FUNCTIONS: - raise ValueError(f"Unknown GARI prior policy {prior_policy!r}.") - - gari_dem_path, layout_path = _output_paths( - circuit_path, prior_policy, output_prefix - ) - for path in [gari_dem_path, layout_path]: - aliases_input = circuit_path.resolve(strict=False) == path.resolve( - strict=False - ) - if not aliases_input and os.path.lexists(path): - try: - aliases_input = os.path.samefile(circuit_path, path) - except OSError: - pass - if aliases_input: - raise ValueError( - f"Output path {path} aliases source circuit {circuit_path}; " - "refusing to overwrite the input." - ) - - circuit = stim.Circuit.from_file(str(circuit_path)) - source_error_model = circuit_to_gari_source_dem(circuit) - checks, logicals, probabilities = dem_to_matrices(source_error_model) - x_detectors, z_detectors = detector_partition_from_fourth_coordinate( - source_error_model - ) - transform = gari_transform( - checks, - logicals, - x_detectors=x_detectors, - z_detectors=z_detectors, - ) - gari_dem = build_gari_dem( - transform, - probabilities, - prior_function=_PRIOR_FUNCTIONS[prior_policy], - ) - - gari_dem_text = str(gari_dem).rstrip("\n") + "\n" - layout_text = json.dumps( - _layout_dict(transform, prior_policy), indent=2, sort_keys=True - ) + "\n" - _write_gari_outputs( - gari_dem_path, - gari_dem_text, - layout_path, - layout_text, - force=force, - ) - return source_error_model, transform, gari_dem_path, layout_path - - -def _convert_directory( - circuit_directory: Path, - *, - prior_policy: str, - force: bool, -) -> int: - circuit_paths = _circuit_paths(circuit_directory) - failures = 0 - for circuit_path in circuit_paths: - relative_path = circuit_path.relative_to(circuit_directory) - try: - _convert_circuit( - circuit_path, - prior_policy=prior_policy, - output_prefix=None, - force=force, - ) - except (OSError, RuntimeError, ValueError) as ex: - failures += 1 - print(f"ERROR {relative_path}: {ex}", file=sys.stderr) - else: - print(f"OK {relative_path}") - - print( - f"\nRepository scan: {circuit_directory}\n" - f"Circuits found: {len(circuit_paths)}\n" - f"Converted: {len(circuit_paths) - failures}\n" - f"Failed: {failures}\n" - "GARI DEM files store matrices only; do not sample them." - ) - return int(failures != 0) - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser( - description=( - "Convert correlated CSS Stim circuits into GARI matrix .dem " - "storage files and detector-layout JSON files." - ) - ) - inputs = parser.add_mutually_exclusive_group(required=True) - inputs.add_argument("--circuit", type=_workspace_path) - inputs.add_argument( - "--circuit-directory", - type=_workspace_path, - help="Recursively convert .stim circuits in deterministic order.", - ) - parser.add_argument( - "--prior-policy", required=True, choices=list(_PRIOR_FUNCTIONS) - ) - parser.add_argument( - "--basis-convention", - choices=[_BASIS_CONVENTION], - default=_BASIS_CONVENTION, - help=( - "Use the repository testdata's color-code-style fourth " - "coordinate: values <= 2 are X and values >= 3 are Z." - ), - ) - parser.add_argument( - "--output-prefix", - type=_workspace_path, - help="Custom output prefix for --circuit only.", - ) - parser.add_argument( - "--force", - action="store_true", - help="Replace existing GARI output files.", - ) - args = parser.parse_args(argv) - - if args.circuit_directory is not None and args.output_prefix is not None: - parser.error("--output-prefix can only be used with --circuit.") - try: - if args.circuit_directory is not None: - return _convert_directory( - args.circuit_directory, - prior_policy=args.prior_policy, - force=args.force, - ) - source_error_model, transform, gari_dem_path, layout_path = ( - _convert_circuit( - args.circuit, - prior_policy=args.prior_policy, - output_prefix=args.output_prefix, - force=args.force, - ) - ) - except (OSError, RuntimeError, ValueError) as ex: - print(f"gari_convert: {ex}", file=sys.stderr) - return 1 - - row_counts = [ - ("Physical X rows", transform.physical_x_rows), - ("Physical Z rows", transform.physical_z_rows), - ("Virtual Z rows", transform.virtual_z_rows), - ("Virtual X rows", transform.virtual_x_rows), - ] - print("GARI transformed-matrix outputs created\n") - print(f"Source circuit: {args.circuit}") - print(f"Source detectors: {source_error_model.num_detectors}") - print(f"GARI detectors: {transform.checks.shape[0]}") - for label, rows in row_counts: - print(f"{label + ':':23}{rows.stop - rows.start}") - print(f"Prior policy: {args.prior_policy}") - print("Logical placement: physical") - print("GARI row order: physical_then_virtual\n") - print("GARI DEM (.dem matrix storage only; do not sample):") - print(f" {gari_dem_path}\n") - print("Detector layout:") - print(f" {layout_path}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/py/gari_example.py b/src/py/gari_example.py deleted file mode 100644 index deb58af1..00000000 --- a/src/py/gari_example.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 -"""Decode source-circuit samples using saved GARI matrix artifacts. - -The ``.dem`` stores transformed matrices, not a physical error model; only the -source circuit is sampled, and virtual detector entries remain zero. -The layout must be the unchanged companion file written by ``gari_convert``. -""" - -from __future__ import annotations - -import argparse -import json -import os -from pathlib import Path - -import numpy as np -import stim -import tesseract_decoder - - -_DECODER_PRESET = "tesseract-short-beam" - - -def _workspace_path(path: Path) -> Path: - workspace = os.environ.get("BUILD_WORKSPACE_DIRECTORY") - return ( - Path(workspace) / path - if workspace and not path.is_absolute() - else path - ) - - -def _run( - circuit_path: Path, - dem_path: Path, - layout_path: Path, - *, - shots: int, - seed: int, -) -> None: - circuit = stim.Circuit.from_file(str(circuit_path)) - gari_dem = stim.DetectorErrorModel.from_file(str(dem_path)) - with layout_path.open(encoding="utf-8") as file: - layout = json.load(file) - - source_samples, actual_observables = circuit.compile_detector_sampler( - seed=seed - ).sample(shots=shots, separate_observables=True) - gari_samples = np.zeros( - (source_samples.shape[0], gari_dem.num_detectors), dtype=np.bool_ - ) - source_to_gari = np.asarray(layout["source_to_gari"]) - gari_samples[:, source_to_gari] = source_samples - - decoder = tesseract_decoder.make_tesseract_sinter_decoders_dict()[ - _DECODER_PRESET - ] - decoder.num_det_orders = 1 - compiled_decoder = decoder.compile_decoder_for_dem(dem=gari_dem) - predictions = compiled_decoder.decoder.decode_batch(gari_samples) - decoder_order_count = len(compiled_decoder.decoder.config.det_orders) - if predictions.shape != actual_observables.shape: - raise ValueError("Circuit and GARI observable counts differ.") - logical_failures = int( - np.count_nonzero(np.any(predictions != actual_observables, axis=1)) - ) - - print("GARI saved-artifact decoding completed") - print(f"Source circuit: {circuit_path}") - print(f"GARI DEM file: {dem_path} (.dem matrix storage; not sampled)") - print(f"Detector layout: {layout_path}") - print(f"Source detectors: {circuit.num_detectors}") - print(f"GARI detectors: {gari_dem.num_detectors}") - print(f"Prior policy: {layout['prior_policy']}") - print(f"Decoder preset: {_DECODER_PRESET}") - print(f"Decoder order count: {decoder_order_count}") - print(f"Logical placement: {layout['logical_placement']}") - print(f"GARI row order: {layout['detector_order']}") - print(f"Shots: {shots}") - print(f"Logical failures: {logical_failures}/{shots}") - print("This small run is a functional smoke check, not a benchmark or proof.") - - -def main(argv: list[str] | None = None) -> None: - parser = argparse.ArgumentParser( - description="Sample a circuit and decode it using a saved GARI DEM." - ) - parser.add_argument("--circuit", required=True, type=Path) - parser.add_argument( - "--dem", required=True, type=Path, - help="Storage-only GARI matrix file; this file is never sampled.", - ) - parser.add_argument("--gari-layout", required=True, type=Path) - parser.add_argument("--shots", required=True, type=int) - parser.add_argument("--seed", required=True, type=int) - args = parser.parse_args(argv) - - _run( - _workspace_path(args.circuit), - _workspace_path(args.dem), - _workspace_path(args.gari_layout), - shots=args.shots, - seed=args.seed, - ) - - -if __name__ == "__main__": - main() From 09bd58eca449fce940fb886e033ffaba22cf5c3d Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 14:21:59 -0700 Subject: [PATCH 17/43] Simplify GARI matrix construction --- src/py/_tesseract_py_util/gari.py | 71 ++++++++----------------------- 1 file changed, 17 insertions(+), 54 deletions(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index fe106641..993e3dac 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -187,39 +187,31 @@ def _matrices_to_gari_dem( logicals: scipy.sparse.csc_matrix, probabilities: np.ndarray, ) -> stim.DetectorErrorModel: - """Stores GARI transformed matrices using Stim's DEM syntax. - - The result is a GARI matrix representation for decoding. - It is not a physical detector error model and must not be sampled to - generate shots. - """ - gari_checks = checks.tocsc() - gari_logicals = logicals.tocsc() - + """Stores GARI transformed matrices using Stim's DEM syntax.""" detector_target = stim.target_relative_detector_id logical_target = stim.target_logical_observable_id gari_dem = stim.DetectorErrorModel() for column, probability in enumerate(probabilities): targets = [ detector_target(detector) - for detector in _nonzero_column_rows(gari_checks, column) + for detector in _nonzero_column_rows(checks, column) ] targets.extend( logical_target(observable) - for observable in _nonzero_column_rows(gari_logicals, column) + for observable in _nonzero_column_rows(logicals, column) ) gari_dem.append("error", float(probability), targets) # Declare only dimensions not already implied by the error targets. - if gari_dem.num_detectors < gari_checks.shape[0]: + if gari_dem.num_detectors < checks.shape[0]: gari_dem.append( - "detector", [], [detector_target(gari_checks.shape[0] - 1)] + "detector", [], [detector_target(checks.shape[0] - 1)] ) - if gari_dem.num_observables < gari_logicals.shape[0]: + if gari_dem.num_observables < logicals.shape[0]: gari_dem.append( "logical_observable", [], - [logical_target(gari_logicals.shape[0] - 1)], + [logical_target(logicals.shape[0] - 1)], ) return gari_dem @@ -238,13 +230,7 @@ def detector_partition_from_fourth_coordinate( x_detectors: list[int] = [] z_detectors: list[int] = [] for detector in range(dem.num_detectors): - detector_coordinates = coordinates.get(detector) - if detector_coordinates is None or len(detector_coordinates) < 4: - raise ValueError( - f"Detector {detector} must have at least four coordinates." - ) - role = detector_coordinates[3] - if role <= 2: + if coordinates[detector][3] <= 2: x_detectors.append(detector) else: z_detectors.append(detector) @@ -275,10 +261,6 @@ def gari_transform( Returns: The transformed checks, physical logical map, projection matrices, source column classes, detector mapping, and row block slices. - - Raises: - ValueError: The inputs do not satisfy the supported correlated CSS - structure. """ source_checks = checks.tocsc() source_logicals = logicals.tocsc() @@ -297,8 +279,8 @@ def gari_transform( "x_detectors and z_detectors must partition all detector rows." ) - x_checks = source_checks[x_rows, :].tocsc() - z_checks = source_checks[z_rows, :].tocsc() + x_checks = source_checks[x_rows, :] + z_checks = source_checks[z_rows, :] x_support_counts = np.diff(x_checks.indptr) z_support_counts = np.diff(z_checks.indptr) @@ -315,38 +297,22 @@ def gari_transform( (x_support_counts == 0) & (z_support_counts == 0) ) if detectorless_columns.size: - source_column = int(detectorless_columns[0]) - logical_support = list( - _nonzero_column_rows(source_logicals, source_column) - ) raise ValueError( - f"Source column {source_column} is detectorless; logical support " - f"is {logical_support}." + f"Source column {int(detectorless_columns[0])} is detectorless." ) - d_x = x_checks[:, e_z_columns].tocsc() - d_z = z_checks[:, e_x_columns].tocsc() - d_x_prime = x_checks[:, e_y_columns].tocsc() - d_z_prime = z_checks[:, e_y_columns].tocsc() + d_x = x_checks[:, e_z_columns] + d_z = z_checks[:, e_x_columns] + d_x_prime = x_checks[:, e_y_columns] + d_z_prime = z_checks[:, e_y_columns] d_x_lookup = _unique_column_index_by_rows(d_x, name="D_X") d_z_lookup = _unique_column_index_by_rows(d_z, name="D_Z") u_rows: list[int] = [] v_rows: list[int] = [] - for local_y_column, source_column_value in enumerate(e_y_columns): - source_column = int(source_column_value) + for local_y_column in range(len(e_y_columns)): x_projection = _nonzero_column_rows(d_x_prime, local_y_column) - if x_projection not in d_x_lookup: - raise ValueError( - f"Source column {source_column} has X-side projection " - f"{list(x_projection)}, which does not equal a D_X column." - ) z_projection = _nonzero_column_rows(d_z_prime, local_y_column) - if z_projection not in d_z_lookup: - raise ValueError( - f"Source column {source_column} has Z-side projection " - f"{list(z_projection)}, which does not equal a D_Z column." - ) u_rows.append(d_x_lookup[x_projection]) v_rows.append(d_z_lookup[z_projection]) @@ -418,10 +384,7 @@ def gari_transform( ) source_to_gari = np.empty(detector_count, dtype=np.int64) - source_to_gari[x_rows] = np.arange(x_row_count, dtype=np.int64) - source_to_gari[z_rows] = x_row_count + np.arange( - z_row_count, dtype=np.int64 - ) + source_to_gari[partition] = np.arange(detector_count, dtype=np.int64) return GariTransform( checks=augmented_checks, logicals=augmented_logicals, From 812dfdad1dbc82092df472f99c1e60779c4b9004 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 14:37:18 -0700 Subject: [PATCH 18/43] Package GARI Python utilities --- BUILD | 3 +++ 1 file changed, 3 insertions(+) diff --git a/BUILD b/BUILD index 0d6a8b6d..176a72d5 100644 --- a/BUILD +++ b/BUILD @@ -22,10 +22,13 @@ py_wheel( "//src:tesseract_decoder", "//src/py:generated_stubs", "//src/py/_tesseract_py_util:_tesseract_py_util", + "//src/py/_tesseract_py_util:gari", ":package_data", ], version = "$(VERSION)", requires=[ + "numpy", + "scipy", "stim", ], python_tag="$(TARGET_VERSION)", From 8b976615c002dd83ebc9004ca9a19302363f49b7 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 14:51:52 -0700 Subject: [PATCH 19/43] Add GARI decoding tutorial --- docs/tutorial.ipynb | 80 ++++++++++++++++++++++++++++++--------------- docs/tutorial.py | 55 +++++++++++++++++++++++-------- 2 files changed, 95 insertions(+), 40 deletions(-) diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index e09f3a9c..c3a2338c 100644 --- a/docs/tutorial.ipynb +++ b/docs/tutorial.ipynb @@ -131,43 +131,69 @@ { "cell_type": "markdown", "metadata": { - "id": "Xp7MyK0XVs_6" + "id": "gari-correlated-decoding" }, "source": [ - "## Decode with new correlated matching!" + "## Decoding correlated errors\n", + "\n", + "### GARI\n", + "\n", + "Graph augmentation and rewiring for inference (GARI) transforms a correlated\n", + "CSS detector matrix into a block form for Tesseract; see [Decoding correlated\n", + "errors in quantum LDPC codes](https://doi.org/10.1038/s41467-026-70556-3).\n", + "Sample only the original circuit: the GARI matrix DEM stores the transformed\n", + "matrices, and its virtual syndrome entries are initialized to zero." ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "vufQ8G5iVx7b", - "outputId": "3b0517a3-e65e-42b7-eb25-fbb068c4a912" + "id": "gari-transform-example" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Logical error rate: 20/10000\n" - ] - } - ], + "outputs": [], "source": [ - "dem = circuit.detector_error_model(decompose_errors=True)\n", - "matching_corr = pymatching.Matching.from_detector_error_model(\n", - " model=dem, enable_correlations=True\n", - " )\n", - "predicted_observables_corr = matching_corr.decode_batch(\n", - " shots=detector_outcomes,\n", - " enable_correlations=True\n", - " )\n", - "num_errors_corr = np.sum(np.any(predicted_observables_corr != actual_observables, axis=1))\n", + "import tesseract_decoder\n", + "from _tesseract_py_util.gari import (\n", + " circuit_to_gari,\n", + " tesseract_xor_prior_probabilities,\n", + ")\n", "\n", - "print(f\"Logical error rate: {num_errors_corr}/{num_shots}\")" + "circuit = stim.Circuit.from_file(\n", + " \"testdata/colorcodes/\"\n", + " \"r=3,d=3,p=0.001,noise=si1000,c=superdense_color_code_Z,q=13,gates=cz.stim\"\n", + ")\n", + "gari_dem, gari_layout = circuit_to_gari(\n", + " circuit,\n", + " prior_function=tesseract_xor_prior_probabilities,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "939724c3", + "metadata": { + "id": "gari-decode-example" + }, + "outputs": [], + "source": [ + "sampler = circuit.compile_detector_sampler(seed=2384753)\n", + "source_syndromes, actual_observables = sampler.sample(\n", + " shots=10,\n", + " separate_observables=True,\n", + ")\n", + "gari_syndromes = np.zeros((10, gari_dem.num_detectors), dtype=bool)\n", + "gari_syndromes[:, gari_layout[\"source_to_gari\"]] = source_syndromes\n", + "\n", + "short_beam = tesseract_decoder.make_tesseract_sinter_decoders_dict()[\n", + " \"tesseract-short-beam\"\n", + "]\n", + "short_beam.num_det_orders = 0 # One ascending physical-then-virtual order.\n", + "gari_decoder = short_beam.compile_decoder_for_dem(dem=gari_dem).decoder\n", + "predicted_observables = gari_decoder.decode_batch(gari_syndromes)\n", + "logical_failures = np.any(predicted_observables != actual_observables, axis=1).sum()\n", + "print(f\"Logical failures: {logical_failures}/10\")" ] }, { diff --git a/docs/tutorial.py b/docs/tutorial.py index 350d3c42..9f223673 100644 --- a/docs/tutorial.py +++ b/docs/tutorial.py @@ -66,21 +66,50 @@ print(f"Logical error rate: {num_errors}/{num_shots}") -# %% [markdown] id="Xp7MyK0XVs_6" -# ## Decode with new correlated matching! +# %% [markdown] id="gari-correlated-decoding" +# ## Decoding correlated errors +# +# ### GARI +# +# Graph augmentation and rewiring for inference (GARI) transforms a correlated +# CSS detector matrix into a block form for Tesseract; see [Decoding correlated +# errors in quantum LDPC codes](https://doi.org/10.1038/s41467-026-70556-3). +# Sample only the original circuit: the GARI matrix DEM stores the transformed +# matrices, and its virtual syndrome entries are initialized to zero. -# %% 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)) +# %% id="gari-transform-example" +import tesseract_decoder +from _tesseract_py_util.gari import ( + circuit_to_gari, + tesseract_xor_prior_probabilities, +) -print(f"Logical error rate: {num_errors_corr}/{num_shots}") +circuit = stim.Circuit.from_file( + "testdata/colorcodes/" + "r=3,d=3,p=0.001,noise=si1000,c=superdense_color_code_Z,q=13,gates=cz.stim" +) +gari_dem, gari_layout = circuit_to_gari( + circuit, + prior_function=tesseract_xor_prior_probabilities, +) + +# %% id="gari-decode-example" +sampler = circuit.compile_detector_sampler(seed=2384753) +source_syndromes, actual_observables = sampler.sample( + shots=10, + separate_observables=True, +) +gari_syndromes = np.zeros((10, gari_dem.num_detectors), dtype=bool) +gari_syndromes[:, gari_layout["source_to_gari"]] = source_syndromes + +short_beam = tesseract_decoder.make_tesseract_sinter_decoders_dict()[ + "tesseract-short-beam" +] +short_beam.num_det_orders = 0 # One ascending physical-then-virtual order. +gari_decoder = short_beam.compile_decoder_for_dem(dem=gari_dem).decoder +predicted_observables = gari_decoder.decode_batch(gari_syndromes) +logical_failures = np.any(predicted_observables != actual_observables, axis=1).sum() +print(f"Logical failures: {logical_failures}/10") # %% [markdown] id="a-AMqTUeuqOe" # ## Getting a Color Code Circuit From e660dd5f34cfc1a6e1f3a031f57c206888137f70 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 15:31:39 -0700 Subject: [PATCH 20/43] Trim GARI prior test coverage --- src/py/_tesseract_py_util/gari_test.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index affa1649..f0baaa7d 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -8,7 +8,6 @@ detector_partition_from_fourth_coordinate, gari_transform, paper_prior_probabilities, - tesseract_lp_max_barred_cost_prior_probabilities, tesseract_xor_prior_probabilities, ) @@ -82,19 +81,6 @@ def test_prior_probabilities_and_gari_dem_round_trip(): xor_probabilities, [0.1, 0.2, 0.3, 0.34, 0.38] ) - lp_probabilities = tesseract_lp_max_barred_cost_prior_probabilities( - transform, source_probabilities - ) - lp_costs = np.log1p(-lp_probabilities) - np.log(lp_probabilities) - source_costs = np.log1p(-source_probabilities) - np.log( - source_probabilities - ) - np.testing.assert_allclose( - lp_costs[:3] + np.asarray([[1, 0], [0, 1], [1, 1]]) @ lp_costs[3:], - source_costs, - ) - np.testing.assert_allclose(lp_costs[3:].sum(), source_costs[2]) - gari_dem = build_gari_dem( transform, source_probabilities, From 59076b0aeb7a449488ed05b7c4a2e9919186e317 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 15:40:44 -0700 Subject: [PATCH 21/43] Relocate and expand GARI tutorial --- docs/tutorial.ipynb | 191 ++++++++++++++++++++++++++++++++------------ docs/tutorial.py | 118 +++++++++++++++++---------- 2 files changed, 214 insertions(+), 95 deletions(-) diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index 3e01fda7..d993db22 100644 --- a/docs/tutorial.ipynb +++ b/docs/tutorial.ipynb @@ -131,69 +131,43 @@ { "cell_type": "markdown", "metadata": { - "id": "gari-correlated-decoding" + "id": "Xp7MyK0XVs_6" }, "source": [ - "## Decoding correlated errors\n", - "\n", - "### GARI\n", - "\n", - "Graph augmentation and rewiring for inference (GARI) transforms a correlated\n", - "CSS detector matrix into a block form for Tesseract; see [Decoding correlated\n", - "errors in quantum LDPC codes](https://doi.org/10.1038/s41467-026-70556-3).\n", - "Sample only the original circuit: the GARI matrix DEM stores the transformed\n", - "matrices, and its virtual syndrome entries are initialized to zero." + "## Decode with new correlated matching!" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { - "id": "gari-transform-example" - }, - "outputs": [], - "source": [ - "import tesseract_decoder\n", - "from _tesseract_py_util.gari import (\n", - " circuit_to_gari,\n", - " tesseract_xor_prior_probabilities,\n", - ")\n", - "\n", - "circuit = stim.Circuit.from_file(\n", - " \"testdata/colorcodes/\"\n", - " \"r=3,d=3,p=0.001,noise=si1000,c=superdense_color_code_Z,q=13,gates=cz.stim\"\n", - ")\n", - "gari_dem, gari_layout = circuit_to_gari(\n", - " circuit,\n", - " prior_function=tesseract_xor_prior_probabilities,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "939724c3", - "metadata": { - "id": "gari-decode-example" + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "vufQ8G5iVx7b", + "outputId": "3b0517a3-e65e-42b7-eb25-fbb068c4a912" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Logical error rate: 20/10000\n" + ] + } + ], "source": [ - "sampler = circuit.compile_detector_sampler(seed=2384753)\n", - "source_syndromes, actual_observables = sampler.sample(\n", - " shots=10,\n", - " separate_observables=True,\n", - ")\n", - "gari_syndromes = np.zeros((10, gari_dem.num_detectors), dtype=bool)\n", - "gari_syndromes[:, gari_layout[\"source_to_gari\"]] = source_syndromes\n", + "dem = circuit.detector_error_model(decompose_errors=True)\n", + "matching_corr = pymatching.Matching.from_detector_error_model(\n", + " model=dem, enable_correlations=True\n", + " )\n", + "predicted_observables_corr = matching_corr.decode_batch(\n", + " shots=detector_outcomes,\n", + " enable_correlations=True\n", + " )\n", + "num_errors_corr = np.sum(np.any(predicted_observables_corr != actual_observables, axis=1))\n", "\n", - "short_beam = tesseract_decoder.make_tesseract_sinter_decoders_dict()[\n", - " \"tesseract-short-beam\"\n", - "]\n", - "short_beam.num_det_orders = 0 # One ascending physical-then-virtual order.\n", - "gari_decoder = short_beam.compile_decoder_for_dem(dem=gari_dem).decoder\n", - "predicted_observables = gari_decoder.decode_batch(gari_syndromes)\n", - "logical_failures = np.any(predicted_observables != actual_observables, axis=1).sum()\n", - "print(f\"Logical failures: {logical_failures}/10\")" + "print(f\"Logical error rate: {num_errors_corr}/{num_shots}\")" ] }, { @@ -932,6 +906,117 @@ "print_results(results)" ] }, + { + "cell_type": "markdown", + "id": "2b9319e5", + "metadata": { + "id": "gari-correlated-decoding" + }, + "source": [ + "# Decoding Correlated Errors with Tesseract\n", + "\n", + "## GARI\n", + "\n", + "Graph augmentation and rewiring for inference (GARI) transforms a correlated\n", + "CSS detector matrix into a block form for Tesseract; see [Decoding correlated\n", + "errors in quantum LDPC codes](https://doi.org/10.1038/s41467-026-70556-3).\n", + "This example uses the repository's d3 superdense color-code memory-Z circuit\n", + "and the XOR prior policy. Run it from the repository root so the test-data\n", + "path resolves." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd1644de", + "metadata": { + "id": "gari-transform-example" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import stim\n", + "import tesseract_decoder\n", + "from _tesseract_py_util.gari import (\n", + " circuit_to_gari,\n", + " tesseract_xor_prior_probabilities,\n", + ")\n", + "\n", + "circuit = stim.Circuit.from_file(\n", + " \"testdata/colorcodes/\"\n", + " \"r=3,d=3,p=0.001,noise=si1000,c=superdense_color_code_Z,q=13,gates=cz.stim\"\n", + ")\n", + "gari_dem, gari_layout = circuit_to_gari(\n", + " circuit,\n", + " prior_function=tesseract_xor_prior_probabilities,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "34e07e3a", + "metadata": { + "id": "gari-syndrome-layout" + }, + "source": [ + "Sample detection events only from the original circuit. The GARI matrix DEM\n", + "stores the transformed matrices for decoding and is not sampled. Copy the\n", + "source syndrome into its physical rows; the added virtual entries stay zero." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d22c7d2c", + "metadata": { + "id": "gari-sample-example" + }, + "outputs": [], + "source": [ + "gari_shots = 10\n", + "sampler = circuit.compile_detector_sampler(seed=2384753)\n", + "source_syndromes, actual_observables = sampler.sample(\n", + " shots=gari_shots,\n", + " separate_observables=True,\n", + ")\n", + "gari_syndromes = np.zeros(\n", + " (gari_shots, gari_dem.num_detectors), dtype=bool\n", + ")\n", + "gari_syndromes[:, gari_layout[\"source_to_gari\"]] = source_syndromes" + ] + }, + { + "cell_type": "markdown", + "id": "abc39564", + "metadata": { + "id": "gari-detector-order" + }, + "source": [ + "The layout is physical-then-virtual. Setting `num_det_orders=0` selects one\n", + "ascending detector order, so Tesseract processes the rows in that order." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc0c802c", + "metadata": { + "id": "gari-decode-example" + }, + "outputs": [], + "source": [ + "short_beam = tesseract_decoder.make_tesseract_sinter_decoders_dict()[\n", + " \"tesseract-short-beam\"\n", + "]\n", + "short_beam.num_det_orders = 0\n", + "gari_decoder = short_beam.compile_decoder_for_dem(dem=gari_dem).decoder\n", + "predicted_observables = gari_decoder.decode_batch(gari_syndromes)\n", + "logical_failures = np.count_nonzero(\n", + " np.any(predicted_observables != actual_observables, axis=1)\n", + ")\n", + "print(f\"Logical failures: {logical_failures}/{gari_shots}\")" + ] + }, { "cell_type": "markdown", "metadata": { diff --git a/docs/tutorial.py b/docs/tutorial.py index 3fbe38c1..ee46339f 100644 --- a/docs/tutorial.py +++ b/docs/tutorial.py @@ -66,50 +66,21 @@ print(f"Logical error rate: {num_errors}/{num_shots}") -# %% [markdown] id="gari-correlated-decoding" -# ## Decoding correlated errors -# -# ### GARI -# -# Graph augmentation and rewiring for inference (GARI) transforms a correlated -# CSS detector matrix into a block form for Tesseract; see [Decoding correlated -# errors in quantum LDPC codes](https://doi.org/10.1038/s41467-026-70556-3). -# Sample only the original circuit: the GARI matrix DEM stores the transformed -# matrices, and its virtual syndrome entries are initialized to zero. - -# %% id="gari-transform-example" -import tesseract_decoder -from _tesseract_py_util.gari import ( - circuit_to_gari, - tesseract_xor_prior_probabilities, -) +# %% [markdown] id="Xp7MyK0XVs_6" +# ## Decode with new correlated matching! -circuit = stim.Circuit.from_file( - "testdata/colorcodes/" - "r=3,d=3,p=0.001,noise=si1000,c=superdense_color_code_Z,q=13,gates=cz.stim" -) -gari_dem, gari_layout = circuit_to_gari( - circuit, - prior_function=tesseract_xor_prior_probabilities, -) - -# %% id="gari-decode-example" -sampler = circuit.compile_detector_sampler(seed=2384753) -source_syndromes, actual_observables = sampler.sample( - shots=10, - separate_observables=True, -) -gari_syndromes = np.zeros((10, gari_dem.num_detectors), dtype=bool) -gari_syndromes[:, gari_layout["source_to_gari"]] = source_syndromes +# %% 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)) -short_beam = tesseract_decoder.make_tesseract_sinter_decoders_dict()[ - "tesseract-short-beam" -] -short_beam.num_det_orders = 0 # One ascending physical-then-virtual order. -gari_decoder = short_beam.compile_decoder_for_dem(dem=gari_dem).decoder -predicted_observables = gari_decoder.decode_batch(gari_syndromes) -logical_failures = np.any(predicted_observables != actual_observables, axis=1).sum() -print(f"Logical failures: {logical_failures}/10") +print(f"Logical error rate: {num_errors_corr}/{num_shots}") # %% [markdown] id="a-AMqTUeuqOe" # ## Getting a Color Code Circuit @@ -373,6 +344,69 @@ def run_tesseract_decoder(decoder, dets, obs): results = run_tesseract_decoder(tesseract_config2.compile_decoder(), dets, obs) print_results(results) +# %% [markdown] id="gari-correlated-decoding" +# # Decoding Correlated Errors with Tesseract +# +# ## GARI +# +# Graph augmentation and rewiring for inference (GARI) transforms a correlated +# CSS detector matrix into a block form for Tesseract; see [Decoding correlated +# errors in quantum LDPC codes](https://doi.org/10.1038/s41467-026-70556-3). +# This example uses the repository's d3 superdense color-code memory-Z circuit +# and the XOR prior policy. Run it from the repository root so the test-data +# path resolves. + +# %% id="gari-transform-example" +import numpy as np +import stim +import tesseract_decoder +from _tesseract_py_util.gari import ( + circuit_to_gari, + tesseract_xor_prior_probabilities, +) + +circuit = stim.Circuit.from_file( + "testdata/colorcodes/" + "r=3,d=3,p=0.001,noise=si1000,c=superdense_color_code_Z,q=13,gates=cz.stim" +) +gari_dem, gari_layout = circuit_to_gari( + circuit, + prior_function=tesseract_xor_prior_probabilities, +) + +# %% [markdown] id="gari-syndrome-layout" +# Sample detection events only from the original circuit. The GARI matrix DEM +# stores the transformed matrices for decoding and is not sampled. Copy the +# source syndrome into its physical rows; the added virtual entries stay zero. + +# %% id="gari-sample-example" +gari_shots = 10 +sampler = circuit.compile_detector_sampler(seed=2384753) +source_syndromes, actual_observables = sampler.sample( + shots=gari_shots, + separate_observables=True, +) +gari_syndromes = np.zeros( + (gari_shots, gari_dem.num_detectors), dtype=bool +) +gari_syndromes[:, gari_layout["source_to_gari"]] = source_syndromes + +# %% [markdown] id="gari-detector-order" +# The layout is physical-then-virtual. Setting `num_det_orders=0` selects one +# ascending detector order, so Tesseract processes the rows in that order. + +# %% id="gari-decode-example" +short_beam = tesseract_decoder.make_tesseract_sinter_decoders_dict()[ + "tesseract-short-beam" +] +short_beam.num_det_orders = 0 +gari_decoder = short_beam.compile_decoder_for_dem(dem=gari_dem).decoder +predicted_observables = gari_decoder.decode_batch(gari_syndromes) +logical_failures = np.count_nonzero( + np.any(predicted_observables != actual_observables, axis=1) +) +print(f"Logical failures: {logical_failures}/{gari_shots}") + # %% [markdown] id="BoEALeo3OYGp" # # Decoding Wild Stabilizer Codes under Code Capacity Noise with Tesseract # From d6ee6e9a82465c7e8c5cd692d245ab078f0032c9 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 15:45:41 -0700 Subject: [PATCH 22/43] Rename correlated-decoding tutorial section --- docs/tutorial.ipynb | 2 +- docs/tutorial.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index d993db22..f6c5e6d5 100644 --- a/docs/tutorial.ipynb +++ b/docs/tutorial.ipynb @@ -913,7 +913,7 @@ "id": "gari-correlated-decoding" }, "source": [ - "# Decoding Correlated Errors with Tesseract\n", + "# Faster Methods of Decoding Correlated Errors with Tesseract\n", "\n", "## GARI\n", "\n", diff --git a/docs/tutorial.py b/docs/tutorial.py index ee46339f..bfbdde36 100644 --- a/docs/tutorial.py +++ b/docs/tutorial.py @@ -345,7 +345,7 @@ def run_tesseract_decoder(decoder, dets, obs): print_results(results) # %% [markdown] id="gari-correlated-decoding" -# # Decoding Correlated Errors with Tesseract +# # Faster Methods of Decoding Correlated Errors with Tesseract # # ## GARI # From db102c7925fdd0f00f0c06fa4bd212522e9a4854 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 15:58:58 -0700 Subject: [PATCH 23/43] Reuse tutorial color-code data for GARI --- docs/tutorial.ipynb | 19 ++++--------------- docs/tutorial.py | 19 ++++--------------- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index f6c5e6d5..d6f21ea2 100644 --- a/docs/tutorial.ipynb +++ b/docs/tutorial.ipynb @@ -920,9 +920,8 @@ "Graph augmentation and rewiring for inference (GARI) transforms a correlated\n", "CSS detector matrix into a block form for Tesseract; see [Decoding correlated\n", "errors in quantum LDPC codes](https://doi.org/10.1038/s41467-026-70556-3).\n", - "This example uses the repository's d3 superdense color-code memory-Z circuit\n", - "and the XOR prior policy. Run it from the repository root so the test-data\n", - "path resolves." + "This example reuses the superdense color-code memory-Z circuit and sampled\n", + "data from above, and applies the XOR prior policy." ] }, { @@ -934,18 +933,11 @@ }, "outputs": [], "source": [ - "import numpy as np\n", - "import stim\n", - "import tesseract_decoder\n", "from _tesseract_py_util.gari import (\n", " circuit_to_gari,\n", " tesseract_xor_prior_probabilities,\n", ")\n", "\n", - "circuit = stim.Circuit.from_file(\n", - " \"testdata/colorcodes/\"\n", - " \"r=3,d=3,p=0.001,noise=si1000,c=superdense_color_code_Z,q=13,gates=cz.stim\"\n", - ")\n", "gari_dem, gari_layout = circuit_to_gari(\n", " circuit,\n", " prior_function=tesseract_xor_prior_probabilities,\n", @@ -974,11 +966,8 @@ "outputs": [], "source": [ "gari_shots = 10\n", - "sampler = circuit.compile_detector_sampler(seed=2384753)\n", - "source_syndromes, actual_observables = sampler.sample(\n", - " shots=gari_shots,\n", - " separate_observables=True,\n", - ")\n", + "source_syndromes = dets[:gari_shots]\n", + "actual_observables = obs[:gari_shots]\n", "gari_syndromes = np.zeros(\n", " (gari_shots, gari_dem.num_detectors), dtype=bool\n", ")\n", diff --git a/docs/tutorial.py b/docs/tutorial.py index bfbdde36..7f3df2a0 100644 --- a/docs/tutorial.py +++ b/docs/tutorial.py @@ -352,23 +352,15 @@ def run_tesseract_decoder(decoder, dets, obs): # Graph augmentation and rewiring for inference (GARI) transforms a correlated # CSS detector matrix into a block form for Tesseract; see [Decoding correlated # errors in quantum LDPC codes](https://doi.org/10.1038/s41467-026-70556-3). -# This example uses the repository's d3 superdense color-code memory-Z circuit -# and the XOR prior policy. Run it from the repository root so the test-data -# path resolves. +# This example reuses the superdense color-code memory-Z circuit and sampled +# data from above, and applies the XOR prior policy. # %% id="gari-transform-example" -import numpy as np -import stim -import tesseract_decoder from _tesseract_py_util.gari import ( circuit_to_gari, tesseract_xor_prior_probabilities, ) -circuit = stim.Circuit.from_file( - "testdata/colorcodes/" - "r=3,d=3,p=0.001,noise=si1000,c=superdense_color_code_Z,q=13,gates=cz.stim" -) gari_dem, gari_layout = circuit_to_gari( circuit, prior_function=tesseract_xor_prior_probabilities, @@ -381,11 +373,8 @@ def run_tesseract_decoder(decoder, dets, obs): # %% id="gari-sample-example" gari_shots = 10 -sampler = circuit.compile_detector_sampler(seed=2384753) -source_syndromes, actual_observables = sampler.sample( - shots=gari_shots, - separate_observables=True, -) +source_syndromes = dets[:gari_shots] +actual_observables = obs[:gari_shots] gari_syndromes = np.zeros( (gari_shots, gari_dem.num_detectors), dtype=bool ) From d32af3bdac9b145fed2ccd21fa2eb2128d679e63 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 16:02:21 -0700 Subject: [PATCH 24/43] Simplify GARI tutorial variable names --- docs/tutorial.ipynb | 16 ++++++---------- docs/tutorial.py | 16 ++++++---------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index d6f21ea2..712968df 100644 --- a/docs/tutorial.ipynb +++ b/docs/tutorial.ipynb @@ -965,13 +965,9 @@ }, "outputs": [], "source": [ - "gari_shots = 10\n", - "source_syndromes = dets[:gari_shots]\n", - "actual_observables = obs[:gari_shots]\n", - "gari_syndromes = np.zeros(\n", - " (gari_shots, gari_dem.num_detectors), dtype=bool\n", - ")\n", - "gari_syndromes[:, gari_layout[\"source_to_gari\"]] = source_syndromes" + "num_shots = 10\n", + "gari_dets = np.zeros((num_shots, gari_dem.num_detectors), dtype=bool)\n", + "gari_dets[:, gari_layout[\"source_to_gari\"]] = dets[:num_shots]" ] }, { @@ -999,11 +995,11 @@ "]\n", "short_beam.num_det_orders = 0\n", "gari_decoder = short_beam.compile_decoder_for_dem(dem=gari_dem).decoder\n", - "predicted_observables = gari_decoder.decode_batch(gari_syndromes)\n", + "predicted_observables = gari_decoder.decode_batch(gari_dets)\n", "logical_failures = np.count_nonzero(\n", - " np.any(predicted_observables != actual_observables, axis=1)\n", + " np.any(predicted_observables != obs[:num_shots], axis=1)\n", ")\n", - "print(f\"Logical failures: {logical_failures}/{gari_shots}\")" + "print(f\"Logical failures: {logical_failures}/{num_shots}\")" ] }, { diff --git a/docs/tutorial.py b/docs/tutorial.py index 7f3df2a0..214aca93 100644 --- a/docs/tutorial.py +++ b/docs/tutorial.py @@ -372,13 +372,9 @@ def run_tesseract_decoder(decoder, dets, obs): # source syndrome into its physical rows; the added virtual entries stay zero. # %% id="gari-sample-example" -gari_shots = 10 -source_syndromes = dets[:gari_shots] -actual_observables = obs[:gari_shots] -gari_syndromes = np.zeros( - (gari_shots, gari_dem.num_detectors), dtype=bool -) -gari_syndromes[:, gari_layout["source_to_gari"]] = source_syndromes +num_shots = 10 +gari_dets = np.zeros((num_shots, gari_dem.num_detectors), dtype=bool) +gari_dets[:, gari_layout["source_to_gari"]] = dets[:num_shots] # %% [markdown] id="gari-detector-order" # The layout is physical-then-virtual. Setting `num_det_orders=0` selects one @@ -390,11 +386,11 @@ def run_tesseract_decoder(decoder, dets, obs): ] short_beam.num_det_orders = 0 gari_decoder = short_beam.compile_decoder_for_dem(dem=gari_dem).decoder -predicted_observables = gari_decoder.decode_batch(gari_syndromes) +predicted_observables = gari_decoder.decode_batch(gari_dets) logical_failures = np.count_nonzero( - np.any(predicted_observables != actual_observables, axis=1) + np.any(predicted_observables != obs[:num_shots], axis=1) ) -print(f"Logical failures: {logical_failures}/{gari_shots}") +print(f"Logical failures: {logical_failures}/{num_shots}") # %% [markdown] id="BoEALeo3OYGp" # # Decoding Wild Stabilizer Codes under Code Capacity Noise with Tesseract From 8d50842c79f682a68da7142873c69e9183f09a2c Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 16:04:48 -0700 Subject: [PATCH 25/43] Remove unrelated Python BUILD formatting diff --- src/py/BUILD | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/py/BUILD b/src/py/BUILD index e6e09ca9..7160760f 100644 --- a/src/py/BUILD +++ b/src/py/BUILD @@ -80,7 +80,6 @@ py_test( ], imports = ["..", "."], ) - py_test( name = "tesseract_sinter_compat_test", srcs = ["tesseract_sinter_compat_test.py"], @@ -94,6 +93,8 @@ py_test( imports = ["..", "."], ) + + py_test( name = "stub_test", srcs = ["stub_test.py"], From dbf8b896ad01c21b1e5c8e80760100548e09efc0 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 30 Jul 2026 21:12:57 -0700 Subject: [PATCH 26/43] Simplify GARI utility integration --- BUILD | 1 - docs/tutorial.ipynb | 19 ++- docs/tutorial.py | 17 +- src/py/_tesseract_py_util/BUILD | 28 +--- src/py/_tesseract_py_util/__init__.py | 13 +- src/py/_tesseract_py_util/gari.py | 175 +++++++------------- src/py/_tesseract_py_util/gari_test.py | 35 ++-- src/py/_tesseract_py_util/generalize_dem.py | 56 ++++++- src/tesseract.pybind.cc | 4 +- 9 files changed, 170 insertions(+), 178 deletions(-) diff --git a/BUILD b/BUILD index 176a72d5..a02f45be 100644 --- a/BUILD +++ b/BUILD @@ -22,7 +22,6 @@ py_wheel( "//src:tesseract_decoder", "//src/py:generated_stubs", "//src/py/_tesseract_py_util:_tesseract_py_util", - "//src/py/_tesseract_py_util:gari", ":package_data", ], version = "$(VERSION)", diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index 712968df..13e72ca5 100644 --- a/docs/tutorial.ipynb +++ b/docs/tutorial.ipynb @@ -921,7 +921,15 @@ "CSS detector matrix into a block form for Tesseract; see [Decoding correlated\n", "errors in quantum LDPC codes](https://doi.org/10.1038/s41467-026-70556-3).\n", "This example reuses the superdense color-code memory-Z circuit and sampled\n", - "data from above, and applies the XOR prior policy." + "data from above, and applies the XOR prior policy.\n", + "\n", + "From a repository checkout, the same one-circuit conversion can be written to\n", + "`model-gari-xor.dem` and `model-gari-xor-layout.json` with:\n", + "\n", + "```bash\n", + "bazel run --jobs=1 //src/py/_tesseract_py_util:generalize_dem -- gari \\\n", + " --circuit circuit_file.stim --prior xor --out-prefix model\n", + "```" ] }, { @@ -933,14 +941,11 @@ }, "outputs": [], "source": [ - "from _tesseract_py_util.gari import (\n", - " circuit_to_gari,\n", - " tesseract_xor_prior_probabilities,\n", - ")\n", + "from tesseract_decoder.demutil import gari\n", "\n", - "gari_dem, gari_layout = circuit_to_gari(\n", + "gari_dem, gari_layout = gari.circuit_to_gari(\n", " circuit,\n", - " prior_function=tesseract_xor_prior_probabilities,\n", + " prior_function=gari.tesseract_xor_prior_probabilities,\n", ")" ] }, diff --git a/docs/tutorial.py b/docs/tutorial.py index 214aca93..226d2c3b 100644 --- a/docs/tutorial.py +++ b/docs/tutorial.py @@ -354,16 +354,21 @@ def run_tesseract_decoder(decoder, dets, obs): # errors in quantum LDPC codes](https://doi.org/10.1038/s41467-026-70556-3). # This example reuses the superdense color-code memory-Z circuit and sampled # data from above, and applies the XOR prior policy. +# +# From a repository checkout, the same one-circuit conversion can be written to +# `model-gari-xor.dem` and `model-gari-xor-layout.json` with: +# +# ```bash +# bazel run --jobs=1 //src/py/_tesseract_py_util:generalize_dem -- gari \ +# --circuit circuit_file.stim --prior xor --out-prefix model +# ``` # %% id="gari-transform-example" -from _tesseract_py_util.gari import ( - circuit_to_gari, - tesseract_xor_prior_probabilities, -) +from tesseract_decoder.demutil import gari -gari_dem, gari_layout = circuit_to_gari( +gari_dem, gari_layout = gari.circuit_to_gari( circuit, - prior_function=tesseract_xor_prior_probabilities, + prior_function=gari.tesseract_xor_prior_probabilities, ) # %% [markdown] id="gari-syndrome-layout" diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index 797b4e9e..7783b2a3 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -6,24 +6,10 @@ py_library( name = "_tesseract_py_util", srcs = glob( ["*.py"], - exclude = [ - "*_test.py", - "gari.py", - ], + exclude = ["*_test.py"], ), visibility = ["//:__subpackages__"], deps = [ - "@pypi//stim", - "@pypi//numpy", - ], -) - -py_library( - name = "gari", - srcs = ["gari.py"], - visibility = ["//:__subpackages__"], - deps = [ - ":_tesseract_py_util", "@pypi//numpy", "@pypi//scipy", "@pypi//stim", @@ -31,14 +17,14 @@ py_library( ) py_binary( - name = "gari_main", - srcs = ["gari.py"], - main = "gari.py", + name = "generalize_dem", + srcs = ["generalize_dem.py"], + imports = ["..", "."], + main = "generalize_dem.py", visibility = ["//visibility:public"], deps = [ ":_tesseract_py_util", "@pypi//numpy", - "@pypi//scipy", "@pypi//stim", ], ) @@ -46,13 +32,13 @@ py_binary( py_test( name = "gari_test", srcs = ["gari_test.py"], - imports = ["..", "."], + imports = ["..", ".", "../.."], visibility = ["//:__subpackages__"], deps = [ - ":gari", "@pypi//numpy", "@pypi//pytest", "@pypi//stim", + "//src:lib_tesseract_decoder", ], ) diff --git a/src/py/_tesseract_py_util/__init__.py b/src/py/_tesseract_py_util/__init__.py index fe103fec..30077285 100644 --- a/src/py/_tesseract_py_util/__init__.py +++ b/src/py/_tesseract_py_util/__init__.py @@ -13,10 +13,19 @@ # limitations under the License. """ -This module is a dispatcher for DEMfunctionality such as decomposition and re-generalization, -and related utilities, in `decompose_errors.py` and `generalize_dem.py`. +This module exposes detector-error-model utilities. """ +import importlib + from _tesseract_py_util.demutil import decompose_errors from _tesseract_py_util.generalize_dem import \ generalize as regeneralize_spatial_dem + + +def __getattr__(name: str): + if name == "gari": + module = importlib.import_module("_tesseract_py_util.gari") + globals()[name] = module + return module + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 993e3dac..5a10f484 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -59,10 +59,7 @@ from __future__ import annotations import dataclasses -import json -import sys from collections.abc import Callable, Sequence -from pathlib import Path import numpy as np import scipy.optimize @@ -70,30 +67,18 @@ import stim -@dataclasses.dataclass(frozen=True) +@dataclasses.dataclass class GariTransform: - """Validated GARI matrices with source-index arrays and transformed slices.""" + """GARI matrices and their source-column and detector mappings.""" checks: scipy.sparse.csc_matrix logicals: scipy.sparse.csc_matrix u: scipy.sparse.csc_matrix v: scipy.sparse.csc_matrix - source_checks_shape: tuple[int, int] - d_x_shape: tuple[int, int] - d_z_shape: tuple[int, int] e_z_columns: np.ndarray e_x_columns: np.ndarray e_y_columns: np.ndarray source_to_gari_detectors: np.ndarray - physical_rows: slice - virtual_rows: slice - physical_x_rows: slice - physical_z_rows: slice - virtual_z_rows: slice - virtual_x_rows: slice - physical_columns: slice - barred_z_columns: slice - barred_x_columns: slice def circuit_to_gari_source_dem( @@ -114,18 +99,43 @@ def _nonzero_column_rows( return tuple(int(v) for v in matrix.indices[start:stop]) -def _unique_column_index_by_rows( - projections: scipy.sparse.csc_matrix, +def _projection_matrix( + pure_columns: scipy.sparse.csc_matrix, + mixed_projections: scipy.sparse.csc_matrix, + mixed_source_columns: np.ndarray, *, - name: str, -) -> dict[tuple[int, ...], int]: + matrix_name: str, + pure_name: str, +) -> scipy.sparse.csc_matrix: lookup: dict[tuple[int, ...], int] = {} - for local_column in range(projections.shape[1]): - support = _nonzero_column_rows(projections, local_column) + for local_column in range(pure_columns.shape[1]): + support = _nonzero_column_rows(pure_columns, local_column) if support in lookup: - raise ValueError(f"{name} has duplicate columns.") + raise ValueError(f"{pure_name} has duplicate columns.") lookup[support] = local_column - return lookup + + rows: list[int] = [] + for local_column, source_column in enumerate(mixed_source_columns): + support = _nonzero_column_rows(mixed_projections, local_column) + if support not in lookup: + raise ValueError( + f"{matrix_name} mixed source column {int(source_column)} has " + f"no corresponding {pure_name} pure column." + ) + rows.append(lookup[support]) + + column_count = len(mixed_source_columns) + return scipy.sparse.csc_matrix( + ( + np.ones(column_count, dtype=np.uint8), + ( + np.asarray(rows, dtype=np.int64), + np.arange(column_count, dtype=np.int64), + ), + ), + shape=(pure_columns.shape[1], column_count), + dtype=np.uint8, + ) def dem_to_matrices( @@ -133,10 +143,13 @@ def dem_to_matrices( ) -> tuple[ scipy.sparse.csc_matrix, scipy.sparse.csc_matrix, np.ndarray ]: - """Extracts matrices from a flattened DEM made with no decomposition. + """Extracts matrices from a flattened, undecomposed source DEM. - Each Stim ``error`` instruction becomes one source matrix column. A ``^`` - separator is rejected because GARI requires ``decompose_errors=False``. + Each Stim ``error`` instruction becomes exactly one source matrix column. + The input must already be flattened and should be generated with + ``decompose_errors=False``. This function does not merge duplicate + instructions or reconstruct correlations split across instructions. A + Stim ``^`` decomposition separator is rejected. """ detector_rows: list[int] = [] detector_columns: list[int] = [] @@ -230,7 +243,14 @@ def detector_partition_from_fourth_coordinate( x_detectors: list[int] = [] z_detectors: list[int] = [] for detector in range(dem.num_detectors): - if coordinates[detector][3] <= 2: + coordinate = coordinates.get(detector) + if coordinate is None or len(coordinate) < 4: + raise ValueError( + f"Detector {detector} is missing the fourth coordinate " + "required by GARI's color-code-style convention (<= 2 for " + "X detectors; >= 3 for Z detectors)." + ) + if coordinate[3] <= 2: x_detectors.append(detector) else: z_detectors.append(detector) @@ -260,7 +280,7 @@ def gari_transform( Returns: The transformed checks, physical logical map, projection matrices, - source column classes, detector mapping, and row block slices. + source column classes, and detector mapping. """ source_checks = checks.tocsc() source_logicals = logicals.tocsc() @@ -305,37 +325,20 @@ def gari_transform( d_z = z_checks[:, e_x_columns] d_x_prime = x_checks[:, e_y_columns] d_z_prime = z_checks[:, e_y_columns] - d_x_lookup = _unique_column_index_by_rows(d_x, name="D_X") - d_z_lookup = _unique_column_index_by_rows(d_z, name="D_Z") - - u_rows: list[int] = [] - v_rows: list[int] = [] - for local_y_column in range(len(e_y_columns)): - x_projection = _nonzero_column_rows(d_x_prime, local_y_column) - z_projection = _nonzero_column_rows(d_z_prime, local_y_column) - u_rows.append(d_x_lookup[x_projection]) - v_rows.append(d_z_lookup[z_projection]) - - y_column_count = len(e_y_columns) - y_indices = np.arange(y_column_count, dtype=np.int64) - u = scipy.sparse.csc_matrix( - ( - np.ones(y_column_count, dtype=np.uint8), - (np.asarray(u_rows, dtype=np.int64), y_indices), - ), - shape=(len(e_z_columns), y_column_count), - dtype=np.uint8, - ) - v = scipy.sparse.csc_matrix( - ( - np.ones(y_column_count, dtype=np.uint8), - (np.asarray(v_rows, dtype=np.int64), y_indices), - ), - shape=(len(e_x_columns), y_column_count), - dtype=np.uint8, + u = _projection_matrix( + d_x, + d_x_prime, + e_y_columns, + matrix_name="U", + pure_name="D_X", + ) + v = _projection_matrix( + d_z, + d_z_prime, + e_y_columns, + matrix_name="V", + pure_name="D_Z", ) - x_row_count = len(x_rows) - z_row_count = len(z_rows) e_z_count = len(e_z_columns) e_x_count = len(e_x_columns) zero = scipy.sparse.csc_matrix @@ -367,22 +370,6 @@ def gari_transform( format="csc", ).astype(np.uint8) - physical_x_rows = slice(0, x_row_count) - physical_z_rows = slice(x_row_count, x_row_count + z_row_count) - virtual_z_rows = slice( - physical_z_rows.stop, physical_z_rows.stop + e_z_count - ) - virtual_x_rows = slice( - virtual_z_rows.stop, virtual_z_rows.stop + e_x_count - ) - physical_columns = slice(0, e_z_count + e_x_count + y_column_count) - barred_z_columns = slice( - physical_columns.stop, physical_columns.stop + e_z_count - ) - barred_x_columns = slice( - barred_z_columns.stop, barred_z_columns.stop + e_x_count - ) - source_to_gari = np.empty(detector_count, dtype=np.int64) source_to_gari[partition] = np.arange(detector_count, dtype=np.int64) return GariTransform( @@ -390,22 +377,10 @@ def gari_transform( logicals=augmented_logicals, u=u, v=v, - source_checks_shape=source_checks.shape, - d_x_shape=d_x.shape, - d_z_shape=d_z.shape, e_z_columns=e_z_columns, e_x_columns=e_x_columns, e_y_columns=e_y_columns, source_to_gari_detectors=source_to_gari, - physical_rows=slice(0, physical_z_rows.stop), - virtual_rows=slice(virtual_z_rows.start, virtual_x_rows.stop), - physical_x_rows=physical_x_rows, - physical_z_rows=physical_z_rows, - virtual_z_rows=virtual_z_rows, - virtual_x_rows=virtual_x_rows, - physical_columns=physical_columns, - barred_z_columns=barred_z_columns, - barred_x_columns=barred_x_columns, ) @@ -628,27 +603,3 @@ def circuit_to_gari( "detector_order": "physical_then_virtual", } return gari_dem, layout - - -if __name__ == "__main__": - circuit_name, prior_name = sys.argv[1:] - circuit_path = Path(circuit_name) - prior_function = { - "paper": paper_prior_probabilities, - "xor": tesseract_xor_prior_probabilities, - "lp-max-barred-cost": ( - tesseract_lp_max_barred_cost_prior_probabilities - ), - }[prior_name] - gari_dem, gari_layout = circuit_to_gari( - stim.Circuit.from_file(str(circuit_path)), - prior_function=prior_function, - ) - output_prefix = circuit_path.with_suffix("") - Path(f"{output_prefix}-gari-{prior_name}.dem").write_text( - str(gari_dem).rstrip("\n") + "\n", encoding="utf-8" - ) - Path(f"{output_prefix}-gari-{prior_name}-layout.json").write_text( - json.dumps(gari_layout, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index f0baaa7d..cbe03630 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -2,14 +2,7 @@ import pytest import stim -from _tesseract_py_util.gari import ( - build_gari_dem, - dem_to_matrices, - detector_partition_from_fourth_coordinate, - gari_transform, - paper_prior_probabilities, - tesseract_xor_prior_probabilities, -) +from tesseract_decoder.demutil import gari def _tiny_model(): @@ -22,11 +15,11 @@ def _tiny_model(): detector(0, 0, 0, 2) D2 detector(0, 0, 0, 4) D3 """) - checks, logicals, probabilities = dem_to_matrices(source_dem) - x_detectors, z_detectors = detector_partition_from_fourth_coordinate( + checks, logicals, probabilities = gari.dem_to_matrices(source_dem) + x_detectors, z_detectors = gari.detector_partition_from_fourth_coordinate( source_dem ) - transform = gari_transform( + transform = gari.gari_transform( checks, logicals, x_detectors=x_detectors, @@ -37,7 +30,7 @@ def _tiny_model(): def test_tiny_transform(): with pytest.raises(ValueError, match="decompose_errors=False"): - dem_to_matrices(stim.DetectorErrorModel("error(0.1) D0 ^ D1")) + gari.dem_to_matrices(stim.DetectorErrorModel("error(0.1) D0 ^ D1")) _, transform = _tiny_model() np.testing.assert_array_equal( @@ -58,35 +51,27 @@ def test_tiny_transform(): np.testing.assert_array_equal( transform.source_to_gari_detectors, [0, 2, 1, 3] ) - assert transform.source_checks_shape == (4, 3) - assert transform.d_x_shape == (2, 1) - assert transform.d_z_shape == (2, 1) - assert transform.physical_rows == slice(0, 4) - assert transform.virtual_rows == slice(4, 6) - assert transform.physical_columns == slice(0, 3) - assert transform.barred_z_columns == slice(3, 4) - assert transform.barred_x_columns == slice(4, 5) def test_prior_probabilities_and_gari_dem_round_trip(): source_probabilities, transform = _tiny_model() np.testing.assert_array_equal( - paper_prior_probabilities(transform, source_probabilities), + gari.paper_prior_probabilities(transform, source_probabilities), [0.1, 0.2, 0.3, 0.5, 0.5], ) - xor_probabilities = tesseract_xor_prior_probabilities( + xor_probabilities = gari.tesseract_xor_prior_probabilities( transform, source_probabilities ) np.testing.assert_allclose( xor_probabilities, [0.1, 0.2, 0.3, 0.34, 0.38] ) - gari_dem = build_gari_dem( + gari_dem = gari.build_gari_dem( transform, source_probabilities, - prior_function=tesseract_xor_prior_probabilities, + prior_function=gari.tesseract_xor_prior_probabilities, ) - checks, logicals, probabilities = dem_to_matrices(gari_dem) + checks, logicals, probabilities = gari.dem_to_matrices(gari_dem) assert gari_dem.num_detectors == transform.checks.shape[0] assert gari_dem.num_observables == transform.logicals.shape[0] assert (checks != transform.checks).nnz == 0 diff --git a/src/py/_tesseract_py_util/generalize_dem.py b/src/py/_tesseract_py_util/generalize_dem.py index de57849b..f45e8f36 100644 --- a/src/py/_tesseract_py_util/generalize_dem.py +++ b/src/py/_tesseract_py_util/generalize_dem.py @@ -12,12 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json +import os +from pathlib import Path from typing import List import numpy as np import stim +def _command_path(path: str) -> str: + workspace = os.environ.get("BUILD_WORKSPACE_DIRECTORY") + if workspace and path != "-" and not Path(path).is_absolute(): + return str(Path(workspace, path)) + return path + + def get_dets_logicals(error: stim.DemInstruction): dets = set() logicals = set() @@ -165,12 +175,47 @@ def call_generalize( output_dem.to_file(output_fname) +def call_gari(circuit_fname: str, prior_name: str, output_prefix: str): + from _tesseract_py_util import gari + + prior_function = { + "paper": gari.paper_prior_probabilities, + "xor": gari.tesseract_xor_prior_probabilities, + "lp-max-barred-cost": gari.tesseract_lp_max_barred_cost_prior_probabilities, + }[prior_name] + gari_dem, layout = gari.circuit_to_gari( + stim.Circuit.from_file(circuit_fname), prior_function=prior_function + ) + output_name = f"{output_prefix}-gari-{prior_name}" + gari_dem.to_file(f"{output_name}.dem") + Path(f"{output_name}-layout.json").write_text( + json.dumps(layout, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + def main(): import argparse + import sys + argv = sys.argv[1:] parser = argparse.ArgumentParser( - description="Generalize detector error models using templates and scaffold." + description="Generalize detector error models or create GARI files." ) + if argv[:1] == ["gari"]: + parser.add_argument("--circuit", required=True) + parser.add_argument( + "--prior", + choices=("paper", "xor", "lp-max-barred-cost"), + required=True, + ) + parser.add_argument("--out-prefix", required=True) + args = parser.parse_args(argv[1:]) + call_gari( + _command_path(args.circuit), args.prior, _command_path(args.out_prefix) + ) + return + parser.add_argument( "--template", required=True, @@ -185,8 +230,13 @@ def main(): "--verbose", action="store_true", ) - args = parser.parse_args() - call_generalize(args.template, args.scaffold, args.out, verbose=args.verbose) + args = parser.parse_args(argv) + call_generalize( + [_command_path(path) for path in args.template], + _command_path(args.scaffold), + _command_path(args.out), + verbose=args.verbose, + ) if __name__ == "__main__": diff --git a/src/tesseract.pybind.cc b/src/tesseract.pybind.cc index 9f2808f4..0e3c7ef1 100644 --- a/src/tesseract.pybind.cc +++ b/src/tesseract.pybind.cc @@ -33,7 +33,9 @@ PYBIND11_MODULE(tesseract_decoder, tesseract) { add_visualization_module(tesseract); add_tesseract_module(tesseract); pybind_sinter_compat(tesseract); - tesseract.attr("demutil") = py::module::import("_tesseract_py_util"); + auto demutil = py::module::import("_tesseract_py_util"); + tesseract.attr("demutil") = demutil; + py::module::import("sys").attr("modules")["tesseract_decoder.demutil"] = demutil; // Adds a context manager to the python library that can be used to redirect C++'s stdout/stderr // to python's stdout/stderr at run time like From 09f5b0be1cda0080501645d9a98f99759c1ef422 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 30 Jul 2026 21:36:30 -0700 Subject: [PATCH 27/43] Simplify GARI module loading --- src/py/_tesseract_py_util/__init__.py | 13 ++----------- src/py/_tesseract_py_util/generalize_dem.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/py/_tesseract_py_util/__init__.py b/src/py/_tesseract_py_util/__init__.py index 30077285..fe103fec 100644 --- a/src/py/_tesseract_py_util/__init__.py +++ b/src/py/_tesseract_py_util/__init__.py @@ -13,19 +13,10 @@ # limitations under the License. """ -This module exposes detector-error-model utilities. +This module is a dispatcher for DEMfunctionality such as decomposition and re-generalization, +and related utilities, in `decompose_errors.py` and `generalize_dem.py`. """ -import importlib - from _tesseract_py_util.demutil import decompose_errors from _tesseract_py_util.generalize_dem import \ generalize as regeneralize_spatial_dem - - -def __getattr__(name: str): - if name == "gari": - module = importlib.import_module("_tesseract_py_util.gari") - globals()[name] = module - return module - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/py/_tesseract_py_util/generalize_dem.py b/src/py/_tesseract_py_util/generalize_dem.py index f45e8f36..01985969 100644 --- a/src/py/_tesseract_py_util/generalize_dem.py +++ b/src/py/_tesseract_py_util/generalize_dem.py @@ -198,11 +198,10 @@ def main(): import argparse import sys - argv = sys.argv[1:] - parser = argparse.ArgumentParser( - description="Generalize detector error models or create GARI files." - ) - if argv[:1] == ["gari"]: + if sys.argv[1:2] == ["gari"]: + parser = argparse.ArgumentParser( + description="Create GARI files from one circuit." + ) parser.add_argument("--circuit", required=True) parser.add_argument( "--prior", @@ -210,12 +209,15 @@ def main(): required=True, ) parser.add_argument("--out-prefix", required=True) - args = parser.parse_args(argv[1:]) + args = parser.parse_args(sys.argv[2:]) call_gari( _command_path(args.circuit), args.prior, _command_path(args.out_prefix) ) return + parser = argparse.ArgumentParser( + description="Generalize detector error models using templates and scaffold." + ) parser.add_argument( "--template", required=True, @@ -230,7 +232,7 @@ def main(): "--verbose", action="store_true", ) - args = parser.parse_args(argv) + args = parser.parse_args() call_generalize( [_command_path(path) for path in args.template], _command_path(args.scaffold), From 77f52e6285b752d5043bc87b5f9eb8da9af89bc2 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 30 Jul 2026 23:00:41 -0700 Subject: [PATCH 28/43] Refine GARI DEM utility interface --- src/py/_tesseract_py_util/BUILD | 4 +- src/py/_tesseract_py_util/__init__.py | 4 ++ src/py/_tesseract_py_util/demutil_test.py | 3 ++ src/py/_tesseract_py_util/gari.py | 41 +++++++++++++-------- src/py/_tesseract_py_util/gari_test.py | 18 ++++++++- src/py/_tesseract_py_util/generalize_dem.py | 9 ++++- src/tesseract.pybind.cc | 4 +- 7 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index 7783b2a3..6be5e885 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -32,13 +32,13 @@ py_binary( py_test( name = "gari_test", srcs = ["gari_test.py"], - imports = ["..", ".", "../.."], + imports = ["..", "."], visibility = ["//:__subpackages__"], deps = [ + ":_tesseract_py_util", "@pypi//numpy", "@pypi//pytest", "@pypi//stim", - "//src:lib_tesseract_decoder", ], ) diff --git a/src/py/_tesseract_py_util/__init__.py b/src/py/_tesseract_py_util/__init__.py index fe103fec..c9f6315b 100644 --- a/src/py/_tesseract_py_util/__init__.py +++ b/src/py/_tesseract_py_util/__init__.py @@ -17,6 +17,10 @@ and related utilities, in `decompose_errors.py` and `generalize_dem.py`. """ +import sys + from _tesseract_py_util.demutil import decompose_errors from _tesseract_py_util.generalize_dem import \ generalize as regeneralize_spatial_dem + +sys.modules["tesseract_decoder.demutil"] = sys.modules[__name__] diff --git a/src/py/_tesseract_py_util/demutil_test.py b/src/py/_tesseract_py_util/demutil_test.py index 7aee8897..ca9b6098 100644 --- a/src/py/_tesseract_py_util/demutil_test.py +++ b/src/py/_tesseract_py_util/demutil_test.py @@ -29,9 +29,12 @@ def _demo_dem() -> stim.DetectorErrorModel: def test_import_exposes_demutil_submodule(): + from tesseract_decoder.demutil import gari + assert hasattr(tesseract_decoder, "demutil") assert hasattr(demutil, "regeneralize_spatial_dem") assert hasattr(demutil, "decompose_errors") + assert hasattr(gari, "dem_to_matrices") def test_decompose_errors_rejects_unknown_method(): diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 5a10f484..04fc37db 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -40,10 +40,11 @@ storage and decoding representation. It is not a physical detector error model and must not be sampled. -GARI source DEMs must be generated with ``decompose_errors=False`` and -``flatten_loops=True``, then fully flattened. Each undecomposed Stim ``error`` -instruction is one source matrix column. Instructions containing Stim's ``^`` -decomposition separator are not supported. +GARI source DEMs must be undecomposed. Circuit conversion generates them with +``decompose_errors=False`` and ``flatten_loops=True``. Matrix extraction also +flattens its input before treating each Stim ``error`` instruction as one +source matrix column. Instructions containing Stim's ``^`` decomposition +separator are not supported. For certain single-basis CSS memory experiments, the paper instead evaluates the logical observable on ``bar(e)_X`` or ``bar(e)_Z``. That placement is @@ -143,14 +144,15 @@ def dem_to_matrices( ) -> tuple[ scipy.sparse.csc_matrix, scipy.sparse.csc_matrix, np.ndarray ]: - """Extracts matrices from a flattened, undecomposed source DEM. + """Extracts matrices from an undecomposed source DEM. - Each Stim ``error`` instruction becomes exactly one source matrix column. - The input must already be flattened and should be generated with - ``decompose_errors=False``. This function does not merge duplicate - instructions or reconstruct correlations split across instructions. A - Stim ``^`` decomposition separator is rejected. + Repeat blocks and detector shifts are flattened first. Each resulting Stim + ``error`` instruction becomes exactly one source matrix column. The input + should be generated with ``decompose_errors=False``. This function does + not merge duplicate instructions or reconstruct correlations split across + instructions. A Stim ``^`` decomposition separator is rejected. """ + dem = dem.flattened() detector_rows: list[int] = [] detector_columns: list[int] = [] logical_rows: list[int] = [] @@ -236,8 +238,8 @@ def detector_partition_from_fourth_coordinate( This is the color-code-style convention followed by the test-data circuits associated with this repository, not a universal Stim convention. The - fourth-coordinate values at most ``2`` identify X detectors, while values - at least ``3`` identify Z detectors. + fourth-coordinate values ``0``, ``1``, or ``2`` identify X detectors, + while values ``3``, ``4``, or ``5`` identify Z detectors. """ coordinates = dem.get_detector_coordinates() x_detectors: list[int] = [] @@ -247,13 +249,20 @@ def detector_partition_from_fourth_coordinate( if coordinate is None or len(coordinate) < 4: raise ValueError( f"Detector {detector} is missing the fourth coordinate " - "required by GARI's color-code-style convention (<= 2 for " - "X detectors; >= 3 for Z detectors)." + "required by GARI's color-code-style convention (0, 1, or 2 " + "for X detectors; 3, 4, or 5 for Z detectors)." ) - if coordinate[3] <= 2: + basis = coordinate[3] + if basis in (0, 1, 2): x_detectors.append(detector) - else: + elif basis in (3, 4, 5): z_detectors.append(detector) + else: + raise ValueError( + f"Detector {detector} has fourth coordinate {basis}; GARI's " + "color-code-style convention requires an integer from 0 to 2 " + "for X detectors or 3 to 5 for Z detectors." + ) return np.asarray(x_detectors, dtype=np.int64), np.asarray( z_detectors, dtype=np.int64 ) diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index cbe03630..2ccfca07 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -2,7 +2,7 @@ import pytest import stim -from tesseract_decoder.demutil import gari +from _tesseract_py_util import gari def _tiny_model(): @@ -29,6 +29,22 @@ def _tiny_model(): def test_tiny_transform(): + folded_dem = stim.DetectorErrorModel(""" + repeat 2 { + error(0.1) D0 L0 + shift_detectors 1 + } + """) + checks, logicals, probabilities = gari.dem_to_matrices(folded_dem) + np.testing.assert_array_equal(checks.toarray(), np.eye(2, dtype=np.uint8)) + np.testing.assert_array_equal(logicals.toarray(), [[1, 1]]) + np.testing.assert_allclose(probabilities, [0.1, 0.1]) + + with pytest.raises(ValueError, match="integer from 0 to 2"): + gari.detector_partition_from_fourth_coordinate( + stim.DetectorErrorModel("detector(0, 0, 0, 2.5) D0") + ) + with pytest.raises(ValueError, match="decompose_errors=False"): gari.dem_to_matrices(stim.DetectorErrorModel("error(0.1) D0 ^ D1")) diff --git a/src/py/_tesseract_py_util/generalize_dem.py b/src/py/_tesseract_py_util/generalize_dem.py index 01985969..7b06f15f 100644 --- a/src/py/_tesseract_py_util/generalize_dem.py +++ b/src/py/_tesseract_py_util/generalize_dem.py @@ -200,7 +200,11 @@ def main(): if sys.argv[1:2] == ["gari"]: parser = argparse.ArgumentParser( - description="Create GARI files from one circuit." + prog=f"{Path(sys.argv[0]).name} gari", + description=( + "Convert one Stim circuit into a GARI matrix DEM and " + "detector-layout JSON file." + ), ) parser.add_argument("--circuit", required=True) parser.add_argument( @@ -216,7 +220,8 @@ def main(): return parser = argparse.ArgumentParser( - description="Generalize detector error models using templates and scaffold." + description="Generalize detector error models using templates and scaffold.", + epilog="For GARI circuit conversion, run '%(prog)s gari --help'.", ) parser.add_argument( "--template", diff --git a/src/tesseract.pybind.cc b/src/tesseract.pybind.cc index 0e3c7ef1..9f2808f4 100644 --- a/src/tesseract.pybind.cc +++ b/src/tesseract.pybind.cc @@ -33,9 +33,7 @@ PYBIND11_MODULE(tesseract_decoder, tesseract) { add_visualization_module(tesseract); add_tesseract_module(tesseract); pybind_sinter_compat(tesseract); - auto demutil = py::module::import("_tesseract_py_util"); - tesseract.attr("demutil") = demutil; - py::module::import("sys").attr("modules")["tesseract_decoder.demutil"] = demutil; + tesseract.attr("demutil") = py::module::import("_tesseract_py_util"); // Adds a context manager to the python library that can be used to redirect C++'s stdout/stderr // to python's stdout/stderr at run time like From 839b17b8f9d5e21564d494974b3a07d10a236a41 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 30 Jul 2026 23:22:31 -0700 Subject: [PATCH 29/43] Simplify generalized DEM CLI paths --- src/py/_tesseract_py_util/generalize_dem.py | 25 +++++++++------------ 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/py/_tesseract_py_util/generalize_dem.py b/src/py/_tesseract_py_util/generalize_dem.py index 7b06f15f..89118753 100644 --- a/src/py/_tesseract_py_util/generalize_dem.py +++ b/src/py/_tesseract_py_util/generalize_dem.py @@ -21,13 +21,6 @@ import stim -def _command_path(path: str) -> str: - workspace = os.environ.get("BUILD_WORKSPACE_DIRECTORY") - if workspace and path != "-" and not Path(path).is_absolute(): - return str(Path(workspace, path)) - return path - - def get_dets_logicals(error: stim.DemInstruction): dets = set() logicals = set() @@ -198,6 +191,8 @@ def main(): import argparse import sys + os.chdir(os.environ.get("BUILD_WORKSPACE_DIRECTORY", ".")) + if sys.argv[1:2] == ["gari"]: parser = argparse.ArgumentParser( prog=f"{Path(sys.argv[0]).name} gari", @@ -214,14 +209,14 @@ def main(): ) parser.add_argument("--out-prefix", required=True) args = parser.parse_args(sys.argv[2:]) - call_gari( - _command_path(args.circuit), args.prior, _command_path(args.out_prefix) - ) + call_gari(args.circuit, args.prior, args.out_prefix) return parser = argparse.ArgumentParser( - description="Generalize detector error models using templates and scaffold.", - epilog="For GARI circuit conversion, run '%(prog)s gari --help'.", + description=( + "Generalize detector error models using templates and scaffold. " + "For GARI circuit conversion, run '%(prog)s gari --help'." + ), ) parser.add_argument( "--template", @@ -239,9 +234,9 @@ def main(): ) args = parser.parse_args() call_generalize( - [_command_path(path) for path in args.template], - _command_path(args.scaffold), - _command_path(args.out), + args.template, + args.scaffold, + args.out, verbose=args.verbose, ) From 91f5d2ee6760b3f55a720988ff79740c83242528 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Fri, 31 Jul 2026 11:03:48 -0700 Subject: [PATCH 30/43] Improve GARI conversion safety and output naming --- docs/tutorial.ipynb | 5 ++-- docs/tutorial.py | 5 ++-- src/py/_tesseract_py_util/gari.py | 19 +++++++++++-- src/py/_tesseract_py_util/gari_test.py | 3 +++ src/py/_tesseract_py_util/generalize_dem.py | 30 +++++++++++++++------ 5 files changed, 48 insertions(+), 14 deletions(-) diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index 13e72ca5..805c1dee 100644 --- a/docs/tutorial.ipynb +++ b/docs/tutorial.ipynb @@ -924,11 +924,12 @@ "data from above, and applies the XOR prior policy.\n", "\n", "From a repository checkout, the same one-circuit conversion can be written to\n", - "`model-gari-xor.dem` and `model-gari-xor-layout.json` with:\n", + "`gari_output/d5r5colorcode_p001_gari_xor.dem` and\n", + "`gari_output/d5r5colorcode_p001_gari_xor_layout.json` with:\n", "\n", "```bash\n", "bazel run --jobs=1 //src/py/_tesseract_py_util:generalize_dem -- gari \\\n", - " --circuit circuit_file.stim --prior xor --out-prefix model\n", + " --circuit d5r5colorcode_p001.stim --prior xor --out-dir gari_output\n", "```" ] }, diff --git a/docs/tutorial.py b/docs/tutorial.py index 226d2c3b..1c9660b2 100644 --- a/docs/tutorial.py +++ b/docs/tutorial.py @@ -356,11 +356,12 @@ def run_tesseract_decoder(decoder, dets, obs): # data from above, and applies the XOR prior policy. # # From a repository checkout, the same one-circuit conversion can be written to -# `model-gari-xor.dem` and `model-gari-xor-layout.json` with: +# `gari_output/d5r5colorcode_p001_gari_xor.dem` and +# `gari_output/d5r5colorcode_p001_gari_xor_layout.json` with: # # ```bash # bazel run --jobs=1 //src/py/_tesseract_py_util:generalize_dem -- gari \ -# --circuit circuit_file.stim --prior xor --out-prefix model +# --circuit d5r5colorcode_p001.stim --prior xor --out-dir gari_output # ``` # %% id="gari-transform-example" diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 04fc37db..dc6a7157 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -150,7 +150,8 @@ def dem_to_matrices( ``error`` instruction becomes exactly one source matrix column. The input should be generated with ``decompose_errors=False``. This function does not merge duplicate instructions or reconstruct correlations split across - instructions. A Stim ``^`` decomposition separator is rejected. + instructions. A Stim ``^`` decomposition separator and repeated detector + or logical targets within one instruction are rejected. """ dem = dem.flattened() detector_rows: list[int] = [] @@ -168,14 +169,28 @@ def dem_to_matrices( "GARI requires a DEM generated with decompose_errors=False." ) column = len(probabilities) - probabilities.append(float(instruction.args_copy()[0])) + seen_detectors: set[int] = set() + seen_logicals: set[int] = set() for target in targets: if target.is_relative_detector_id(): + if target.val in seen_detectors: + raise ValueError( + f"GARI cannot safely transform source error column " + f"{column}: repeated detector target D{target.val}." + ) + seen_detectors.add(target.val) detector_rows.append(target.val) detector_columns.append(column) elif target.is_logical_observable_id(): + if target.val in seen_logicals: + raise ValueError( + f"GARI cannot safely transform source error column " + f"{column}: repeated logical target L{target.val}." + ) + seen_logicals.add(target.val) logical_rows.append(target.val) logical_columns.append(column) + probabilities.append(float(instruction.args_copy()[0])) source_column_count = len(probabilities) checks = scipy.sparse.csc_matrix( diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 2ccfca07..e26593ac 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -48,6 +48,9 @@ def test_tiny_transform(): with pytest.raises(ValueError, match="decompose_errors=False"): gari.dem_to_matrices(stim.DetectorErrorModel("error(0.1) D0 ^ D1")) + with pytest.raises(ValueError, match="repeated detector target D0"): + gari.dem_to_matrices(stim.DetectorErrorModel("error(0.1) D0 D0")) + _, transform = _tiny_model() np.testing.assert_array_equal( transform.checks.toarray(), diff --git a/src/py/_tesseract_py_util/generalize_dem.py b/src/py/_tesseract_py_util/generalize_dem.py index 89118753..29eca6c4 100644 --- a/src/py/_tesseract_py_util/generalize_dem.py +++ b/src/py/_tesseract_py_util/generalize_dem.py @@ -168,7 +168,7 @@ def call_generalize( output_dem.to_file(output_fname) -def call_gari(circuit_fname: str, prior_name: str, output_prefix: str): +def call_gari(circuit_fname: str, prior_name: str, output_dir: str): from _tesseract_py_util import gari prior_function = { @@ -179,9 +179,11 @@ def call_gari(circuit_fname: str, prior_name: str, output_prefix: str): gari_dem, layout = gari.circuit_to_gari( stim.Circuit.from_file(circuit_fname), prior_function=prior_function ) - output_name = f"{output_prefix}-gari-{prior_name}" - gari_dem.to_file(f"{output_name}.dem") - Path(f"{output_name}-layout.json").write_text( + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + output_name = f"{Path(circuit_fname).stem}_gari_{prior_name.replace('-', '_')}" + gari_dem.to_file(output_path / f"{output_name}.dem") + (output_path / f"{output_name}_layout.json").write_text( json.dumps(layout, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) @@ -198,18 +200,30 @@ def main(): prog=f"{Path(sys.argv[0]).name} gari", description=( "Convert one Stim circuit into a GARI matrix DEM and " - "detector-layout JSON file." + "detector-layout JSON file. Output filenames are derived " + "from the circuit name and prior policy." ), ) - parser.add_argument("--circuit", required=True) + parser.add_argument( + "--circuit", required=True, help="Input Stim circuit file." + ) parser.add_argument( "--prior", choices=("paper", "xor", "lp-max-barred-cost"), required=True, + help="Prior policy used for the GARI matrix probabilities.", + ) + parser.add_argument( + "--out-dir", + required=True, + help=( + "Output directory, created if needed. Files are named " + "_gari_.dem and " + "_gari__layout.json." + ), ) - parser.add_argument("--out-prefix", required=True) args = parser.parse_args(sys.argv[2:]) - call_gari(args.circuit, args.prior, args.out_prefix) + call_gari(args.circuit, args.prior, args.out_dir) return parser = argparse.ArgumentParser( From 4542f77757393593263f077a0d2aaa703ee30cf1 Mon Sep 17 00:00:00 2001 From: arshpreetmaan <98537825+arshpreetmaan@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:16:57 -0700 Subject: [PATCH 31/43] cleanup --- src/py/_tesseract_py_util/generalize_dem.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/py/_tesseract_py_util/generalize_dem.py b/src/py/_tesseract_py_util/generalize_dem.py index 29eca6c4..e5306f88 100644 --- a/src/py/_tesseract_py_util/generalize_dem.py +++ b/src/py/_tesseract_py_util/generalize_dem.py @@ -247,12 +247,7 @@ def main(): action="store_true", ) args = parser.parse_args() - call_generalize( - args.template, - args.scaffold, - args.out, - verbose=args.verbose, - ) + call_generalize(args.template, args.scaffold, args.out, verbose=args.verbose) if __name__ == "__main__": From 0d3739e8012e3b93043ce55fca37f6bed624442c Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Fri, 31 Jul 2026 13:25:20 -0700 Subject: [PATCH 32/43] Clarify GARI public API --- src/py/_tesseract_py_util/gari.py | 16 ++++++++-------- src/py/_tesseract_py_util/gari_test.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index dc6a7157..0a2220df 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -82,7 +82,7 @@ class GariTransform: source_to_gari_detectors: np.ndarray -def circuit_to_gari_source_dem( +def _circuit_to_gari_source_dem( circuit: stim.Circuit, ) -> stim.DetectorErrorModel: """Creates the flattened, undecomposed source DEM required by GARI.""" @@ -246,7 +246,7 @@ def _matrices_to_gari_dem( return gari_dem -def detector_partition_from_fourth_coordinate( +def _detector_partition_from_fourth_coordinate( dem: stim.DetectorErrorModel, ) -> tuple[np.ndarray, np.ndarray]: """Partitions detectors using the repository's fourth-coordinate rule. @@ -283,7 +283,7 @@ def detector_partition_from_fourth_coordinate( ) -def gari_transform( +def _gari_transform( checks: scipy.sparse.csc_matrix, logicals: scipy.sparse.csc_matrix, *, @@ -553,7 +553,7 @@ def tesseract_lp_max_barred_cost_prior_probabilities( return np.exp(-np.logaddexp(0, gari_costs)) -def build_gari_dem( +def _build_gari_dem( transform: GariTransform, source_probabilities: np.ndarray, *, @@ -605,18 +605,18 @@ def circuit_to_gari( prior_function: Callable[[GariTransform, np.ndarray], np.ndarray], ) -> tuple[stim.DetectorErrorModel, dict[str, object]]: """Converts one circuit into a GARI matrix DEM and v1 layout.""" - source_dem = circuit_to_gari_source_dem(circuit) + source_dem = _circuit_to_gari_source_dem(circuit) checks, logicals, probabilities = dem_to_matrices(source_dem) - x_detectors, z_detectors = detector_partition_from_fourth_coordinate( + x_detectors, z_detectors = _detector_partition_from_fourth_coordinate( source_dem ) - transform = gari_transform( + transform = _gari_transform( checks, logicals, x_detectors=x_detectors, z_detectors=z_detectors, ) - gari_dem = build_gari_dem( + gari_dem = _build_gari_dem( transform, probabilities, prior_function=prior_function ) layout = { diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index e26593ac..8777fab3 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -16,10 +16,10 @@ def _tiny_model(): detector(0, 0, 0, 4) D3 """) checks, logicals, probabilities = gari.dem_to_matrices(source_dem) - x_detectors, z_detectors = gari.detector_partition_from_fourth_coordinate( + x_detectors, z_detectors = gari._detector_partition_from_fourth_coordinate( source_dem ) - transform = gari.gari_transform( + transform = gari._gari_transform( checks, logicals, x_detectors=x_detectors, @@ -41,7 +41,7 @@ def test_tiny_transform(): np.testing.assert_allclose(probabilities, [0.1, 0.1]) with pytest.raises(ValueError, match="integer from 0 to 2"): - gari.detector_partition_from_fourth_coordinate( + gari._detector_partition_from_fourth_coordinate( stim.DetectorErrorModel("detector(0, 0, 0, 2.5) D0") ) @@ -85,7 +85,7 @@ def test_prior_probabilities_and_gari_dem_round_trip(): xor_probabilities, [0.1, 0.2, 0.3, 0.34, 0.38] ) - gari_dem = gari.build_gari_dem( + gari_dem = gari._build_gari_dem( transform, source_probabilities, prior_function=gari.tesseract_xor_prior_probabilities, From d9f2615ae20982627c02bb628628dcfd34889d9d Mon Sep 17 00:00:00 2001 From: arshpreetmaan <98537825+arshpreetmaan@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:09:39 -0700 Subject: [PATCH 33/43] Update test assertions for gari --- src/py/_tesseract_py_util/demutil_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/_tesseract_py_util/demutil_test.py b/src/py/_tesseract_py_util/demutil_test.py index ca9b6098..b7f21a3a 100644 --- a/src/py/_tesseract_py_util/demutil_test.py +++ b/src/py/_tesseract_py_util/demutil_test.py @@ -34,7 +34,7 @@ def test_import_exposes_demutil_submodule(): assert hasattr(tesseract_decoder, "demutil") assert hasattr(demutil, "regeneralize_spatial_dem") assert hasattr(demutil, "decompose_errors") - assert hasattr(gari, "dem_to_matrices") + assert hasattr(gari, "circuit_to_gari") def test_decompose_errors_rejects_unknown_method(): From e2f89907226c603b353f688563ed2044a29bf29c Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 3 Aug 2026 13:55:07 -0700 Subject: [PATCH 34/43] Refactor GARI utility integration --- docs/tutorial.ipynb | 7 +- docs/tutorial.py | 7 +- src/py/_tesseract_py_util/BUILD | 16 ++-- src/py/_tesseract_py_util/__init__.py | 5 +- src/py/_tesseract_py_util/demutil_test.py | 4 +- src/py/_tesseract_py_util/gari.py | 91 ++++++++++++++++----- src/py/_tesseract_py_util/gari_test.py | 8 +- src/py/_tesseract_py_util/generalize_dem.py | 63 +------------- 8 files changed, 93 insertions(+), 108 deletions(-) diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index 805c1dee..82c1fb24 100644 --- a/docs/tutorial.ipynb +++ b/docs/tutorial.ipynb @@ -928,8 +928,9 @@ "`gari_output/d5r5colorcode_p001_gari_xor_layout.json` with:\n", "\n", "```bash\n", - "bazel run --jobs=1 //src/py/_tesseract_py_util:generalize_dem -- gari \\\n", - " --circuit d5r5colorcode_p001.stim --prior xor --out-dir gari_output\n", + "bazel run --jobs=1 //src/py/_tesseract_py_util:gari -- \\\n", + " --circuit \"$PWD/d5r5colorcode_p001.stim\" --prior xor \\\n", + " --out-dir \"$PWD/gari_output\"\n", "```" ] }, @@ -942,7 +943,7 @@ }, "outputs": [], "source": [ - "from tesseract_decoder.demutil import gari\n", + "gari = tesseract_decoder.demutil.gari\n", "\n", "gari_dem, gari_layout = gari.circuit_to_gari(\n", " circuit,\n", diff --git a/docs/tutorial.py b/docs/tutorial.py index 1c9660b2..768b93bb 100644 --- a/docs/tutorial.py +++ b/docs/tutorial.py @@ -360,12 +360,13 @@ def run_tesseract_decoder(decoder, dets, obs): # `gari_output/d5r5colorcode_p001_gari_xor_layout.json` with: # # ```bash -# bazel run --jobs=1 //src/py/_tesseract_py_util:generalize_dem -- gari \ -# --circuit d5r5colorcode_p001.stim --prior xor --out-dir gari_output +# bazel run --jobs=1 //src/py/_tesseract_py_util:gari -- \ +# --circuit "$PWD/d5r5colorcode_p001.stim" --prior xor \ +# --out-dir "$PWD/gari_output" # ``` # %% id="gari-transform-example" -from tesseract_decoder.demutil import gari +gari = tesseract_decoder.demutil.gari gari_dem, gari_layout = gari.circuit_to_gari( circuit, diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index 6be5e885..6f7e3260 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -4,27 +4,23 @@ load("@rules_python//python:py_binary.bzl", "py_binary") py_library( name = "_tesseract_py_util", - srcs = glob( - ["*.py"], - exclude = ["*_test.py"], - ), + srcs = glob(["*.py"], exclude=["*_test.py"]), visibility = ["//:__subpackages__"], deps = [ + "@pypi//stim", "@pypi//numpy", "@pypi//scipy", - "@pypi//stim", ], ) py_binary( - name = "generalize_dem", - srcs = ["generalize_dem.py"], - imports = ["..", "."], - main = "generalize_dem.py", + name = "gari", + srcs = ["gari.py"], + main = "gari.py", visibility = ["//visibility:public"], deps = [ - ":_tesseract_py_util", "@pypi//numpy", + "@pypi//scipy", "@pypi//stim", ], ) diff --git a/src/py/_tesseract_py_util/__init__.py b/src/py/_tesseract_py_util/__init__.py index c9f6315b..1cee2fbf 100644 --- a/src/py/_tesseract_py_util/__init__.py +++ b/src/py/_tesseract_py_util/__init__.py @@ -17,10 +17,7 @@ and related utilities, in `decompose_errors.py` and `generalize_dem.py`. """ -import sys - +from _tesseract_py_util import gari as gari from _tesseract_py_util.demutil import decompose_errors from _tesseract_py_util.generalize_dem import \ generalize as regeneralize_spatial_dem - -sys.modules["tesseract_decoder.demutil"] = sys.modules[__name__] diff --git a/src/py/_tesseract_py_util/demutil_test.py b/src/py/_tesseract_py_util/demutil_test.py index b7f21a3a..0935b7c3 100644 --- a/src/py/_tesseract_py_util/demutil_test.py +++ b/src/py/_tesseract_py_util/demutil_test.py @@ -29,12 +29,10 @@ def _demo_dem() -> stim.DetectorErrorModel: def test_import_exposes_demutil_submodule(): - from tesseract_decoder.demutil import gari - assert hasattr(tesseract_decoder, "demutil") assert hasattr(demutil, "regeneralize_spatial_dem") assert hasattr(demutil, "decompose_errors") - assert hasattr(gari, "circuit_to_gari") + assert hasattr(demutil.gari, "circuit_to_gari") def test_decompose_errors_rejects_unknown_method(): diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 0a2220df..e9a5d1a5 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -44,7 +44,8 @@ ``decompose_errors=False`` and ``flatten_loops=True``. Matrix extraction also flattens its input before treating each Stim ``error`` instruction as one source matrix column. Instructions containing Stim's ``^`` decomposition -separator are not supported. +separator are not supported. Repeated detector or logical targets are reduced +modulo two, following Stim's GF(2) parity semantics. For certain single-basis CSS memory experiments, the paper instead evaluates the logical observable on ``bar(e)_X`` or ``bar(e)_Z``. That placement is @@ -60,7 +61,9 @@ from __future__ import annotations import dataclasses +import json from collections.abc import Callable, Sequence +from pathlib import Path import numpy as np import scipy.optimize @@ -150,8 +153,8 @@ def dem_to_matrices( ``error`` instruction becomes exactly one source matrix column. The input should be generated with ``decompose_errors=False``. This function does not merge duplicate instructions or reconstruct correlations split across - instructions. A Stim ``^`` decomposition separator and repeated detector - or logical targets within one instruction are rejected. + instructions. A Stim ``^`` decomposition separator is rejected. Repeated + detector or logical targets within an instruction are reduced modulo two. """ dem = dem.flattened() detector_rows: list[int] = [] @@ -169,27 +172,17 @@ def dem_to_matrices( "GARI requires a DEM generated with decompose_errors=False." ) column = len(probabilities) - seen_detectors: set[int] = set() - seen_logicals: set[int] = set() + detectors: set[int] = set() + logicals: set[int] = set() for target in targets: if target.is_relative_detector_id(): - if target.val in seen_detectors: - raise ValueError( - f"GARI cannot safely transform source error column " - f"{column}: repeated detector target D{target.val}." - ) - seen_detectors.add(target.val) - detector_rows.append(target.val) - detector_columns.append(column) + detectors ^= {target.val} elif target.is_logical_observable_id(): - if target.val in seen_logicals: - raise ValueError( - f"GARI cannot safely transform source error column " - f"{column}: repeated logical target L{target.val}." - ) - seen_logicals.add(target.val) - logical_rows.append(target.val) - logical_columns.append(column) + logicals ^= {target.val} + detector_rows.extend(sorted(detectors)) + detector_columns.extend([column] * len(detectors)) + logical_rows.extend(sorted(logicals)) + logical_columns.extend([column] * len(logicals)) probabilities.append(float(instruction.args_copy()[0])) source_column_count = len(probabilities) @@ -627,3 +620,59 @@ def circuit_to_gari( "detector_order": "physical_then_virtual", } return gari_dem, layout + + +def call_gari(circuit_fname: str, prior_name: str, output_dir: str) -> None: + """Converts one circuit and writes its GARI DEM and layout files.""" + prior_function = { + "paper": paper_prior_probabilities, + "xor": tesseract_xor_prior_probabilities, + "lp-max-barred-cost": tesseract_lp_max_barred_cost_prior_probabilities, + }[prior_name] + gari_dem, layout = circuit_to_gari( + stim.Circuit.from_file(circuit_fname), + prior_function=prior_function, + ) + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + output_name = f"{Path(circuit_fname).stem}_gari_{prior_name.replace('-', '_')}" + gari_dem.to_file(output_path / f"{output_name}.dem") + (output_path / f"{output_name}_layout.json").write_text( + json.dumps(layout, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +def main() -> None: + import argparse + + parser = argparse.ArgumentParser( + description=( + "Convert one Stim circuit into a GARI matrix DEM and " + "detector-layout JSON file." + ) + ) + parser.add_argument( + "--circuit", required=True, help="Input Stim circuit file." + ) + parser.add_argument( + "--prior", + choices=("paper", "xor", "lp-max-barred-cost"), + required=True, + help="Prior policy used for the GARI matrix probabilities.", + ) + parser.add_argument( + "--out-dir", + required=True, + help=( + "Output directory, created if needed. Files are named " + "_gari_.dem and " + "_gari__layout.json." + ), + ) + args = parser.parse_args() + call_gari(args.circuit, args.prior, args.out_dir) + + +if __name__ == "__main__": + main() diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 8777fab3..de060581 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -48,8 +48,12 @@ def test_tiny_transform(): with pytest.raises(ValueError, match="decompose_errors=False"): gari.dem_to_matrices(stim.DetectorErrorModel("error(0.1) D0 ^ D1")) - with pytest.raises(ValueError, match="repeated detector target D0"): - gari.dem_to_matrices(stim.DetectorErrorModel("error(0.1) D0 D0")) + checks, logicals, probabilities = gari.dem_to_matrices( + stim.DetectorErrorModel("error(0.1) D0 D0 D1 L0 L1 L1") + ) + np.testing.assert_array_equal(checks.toarray(), [[0], [1]]) + np.testing.assert_array_equal(logicals.toarray(), [[1], [0]]) + np.testing.assert_allclose(probabilities, [0.1]) _, transform = _tiny_model() np.testing.assert_array_equal( diff --git a/src/py/_tesseract_py_util/generalize_dem.py b/src/py/_tesseract_py_util/generalize_dem.py index e5306f88..de57849b 100644 --- a/src/py/_tesseract_py_util/generalize_dem.py +++ b/src/py/_tesseract_py_util/generalize_dem.py @@ -12,9 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import os -from pathlib import Path from typing import List import numpy as np @@ -168,69 +165,11 @@ def call_generalize( output_dem.to_file(output_fname) -def call_gari(circuit_fname: str, prior_name: str, output_dir: str): - from _tesseract_py_util import gari - - prior_function = { - "paper": gari.paper_prior_probabilities, - "xor": gari.tesseract_xor_prior_probabilities, - "lp-max-barred-cost": gari.tesseract_lp_max_barred_cost_prior_probabilities, - }[prior_name] - gari_dem, layout = gari.circuit_to_gari( - stim.Circuit.from_file(circuit_fname), prior_function=prior_function - ) - output_path = Path(output_dir) - output_path.mkdir(parents=True, exist_ok=True) - output_name = f"{Path(circuit_fname).stem}_gari_{prior_name.replace('-', '_')}" - gari_dem.to_file(output_path / f"{output_name}.dem") - (output_path / f"{output_name}_layout.json").write_text( - json.dumps(layout, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - - def main(): import argparse - import sys - - os.chdir(os.environ.get("BUILD_WORKSPACE_DIRECTORY", ".")) - - if sys.argv[1:2] == ["gari"]: - parser = argparse.ArgumentParser( - prog=f"{Path(sys.argv[0]).name} gari", - description=( - "Convert one Stim circuit into a GARI matrix DEM and " - "detector-layout JSON file. Output filenames are derived " - "from the circuit name and prior policy." - ), - ) - parser.add_argument( - "--circuit", required=True, help="Input Stim circuit file." - ) - parser.add_argument( - "--prior", - choices=("paper", "xor", "lp-max-barred-cost"), - required=True, - help="Prior policy used for the GARI matrix probabilities.", - ) - parser.add_argument( - "--out-dir", - required=True, - help=( - "Output directory, created if needed. Files are named " - "_gari_.dem and " - "_gari__layout.json." - ), - ) - args = parser.parse_args(sys.argv[2:]) - call_gari(args.circuit, args.prior, args.out_dir) - return parser = argparse.ArgumentParser( - description=( - "Generalize detector error models using templates and scaffold. " - "For GARI circuit conversion, run '%(prog)s gari --help'." - ), + description="Generalize detector error models using templates and scaffold." ) parser.add_argument( "--template", From 8e786887d399aeefb6a5114aeb86d8dff652e393 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 3 Aug 2026 13:55:44 -0700 Subject: [PATCH 35/43] Add license headers --- src/py/_tesseract_py_util/gari.py | 14 ++++++++++++++ src/py/_tesseract_py_util/gari_test.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index e9a5d1a5..430fafde 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -1,3 +1,17 @@ +# Copyright 2025 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 +# +# http:#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. + """Graph augmentation and rewiring for inference (GARI). This module implements the matrix construction from A. S. Maan et al., diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index de060581..e1926bf9 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -1,3 +1,17 @@ +# Copyright 2025 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 +# +# http:#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. + import numpy as np import pytest import stim From e5cf735a3f0760f13ef2f5a8ece2b2dde5631f13 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 3 Aug 2026 15:53:58 -0700 Subject: [PATCH 36/43] Simplify GARI CLI and test public conversion --- docs/tutorial.ipynb | 12 +---- docs/tutorial.py | 10 ---- src/py/_tesseract_py_util/BUILD | 16 +------ src/py/_tesseract_py_util/gari.py | 2 + src/py/_tesseract_py_util/gari_test.py | 63 ++++++++++++++++++++++---- 5 files changed, 59 insertions(+), 44 deletions(-) diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index 82c1fb24..e00ef674 100644 --- a/docs/tutorial.ipynb +++ b/docs/tutorial.ipynb @@ -921,17 +921,7 @@ "CSS detector matrix into a block form for Tesseract; see [Decoding correlated\n", "errors in quantum LDPC codes](https://doi.org/10.1038/s41467-026-70556-3).\n", "This example reuses the superdense color-code memory-Z circuit and sampled\n", - "data from above, and applies the XOR prior policy.\n", - "\n", - "From a repository checkout, the same one-circuit conversion can be written to\n", - "`gari_output/d5r5colorcode_p001_gari_xor.dem` and\n", - "`gari_output/d5r5colorcode_p001_gari_xor_layout.json` with:\n", - "\n", - "```bash\n", - "bazel run --jobs=1 //src/py/_tesseract_py_util:gari -- \\\n", - " --circuit \"$PWD/d5r5colorcode_p001.stim\" --prior xor \\\n", - " --out-dir \"$PWD/gari_output\"\n", - "```" + "data from above, and applies the XOR prior policy." ] }, { diff --git a/docs/tutorial.py b/docs/tutorial.py index 768b93bb..5f3028e0 100644 --- a/docs/tutorial.py +++ b/docs/tutorial.py @@ -354,16 +354,6 @@ def run_tesseract_decoder(decoder, dets, obs): # errors in quantum LDPC codes](https://doi.org/10.1038/s41467-026-70556-3). # This example reuses the superdense color-code memory-Z circuit and sampled # data from above, and applies the XOR prior policy. -# -# From a repository checkout, the same one-circuit conversion can be written to -# `gari_output/d5r5colorcode_p001_gari_xor.dem` and -# `gari_output/d5r5colorcode_p001_gari_xor_layout.json` with: -# -# ```bash -# bazel run --jobs=1 //src/py/_tesseract_py_util:gari -- \ -# --circuit "$PWD/d5r5colorcode_p001.stim" --prior xor \ -# --out-dir "$PWD/gari_output" -# ``` # %% id="gari-transform-example" gari = tesseract_decoder.demutil.gari diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index 6f7e3260..284bcd67 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -1,6 +1,5 @@ load("@rules_python//python:py_test.bzl", "py_test") load("@rules_python//python:py_library.bzl", "py_library") -load("@rules_python//python:py_binary.bzl", "py_binary") py_library( name = "_tesseract_py_util", @@ -13,28 +12,17 @@ py_library( ], ) -py_binary( - name = "gari", - srcs = ["gari.py"], - main = "gari.py", - visibility = ["//visibility:public"], - deps = [ - "@pypi//numpy", - "@pypi//scipy", - "@pypi//stim", - ], -) - py_test( name = "gari_test", srcs = ["gari_test.py"], - imports = ["..", "."], + imports = ["..", ".", "../.."], visibility = ["//:__subpackages__"], deps = [ ":_tesseract_py_util", "@pypi//numpy", "@pypi//pytest", "@pypi//stim", + "//src:lib_tesseract_decoder", ], ) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 430fafde..266cf016 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -523,6 +523,8 @@ def tesseract_lp_max_barred_cost_prior_probabilities( format="csc", ) auxiliary_count = cost_matrix.shape[1] + if auxiliary_count == 0: + return physical_probabilities # A guarded alternative is to first maximize a common floor t, then # maximize sum(g) while requiring t >= t_star - numerical_tolerance. diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index e1926bf9..95b52176 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -12,23 +12,34 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json + import numpy as np import pytest import stim from _tesseract_py_util import gari +from tesseract_decoder import demutil -def _tiny_model(): - source_dem = stim.DetectorErrorModel(""" - error(0.1) D0 D2 L0 - error(0.2) D1 D3 L1 - error(0.3) D0 D1 D2 D3 L0 - detector(0, 0, 0, 0) D0 - detector(0, 0, 0, 3) D1 - detector(0, 0, 0, 2) D2 - detector(0, 0, 0, 4) D3 +def _tiny_circuit(): + return stim.Circuit(""" + R 0 1 2 3 4 5 + CORRELATED_ERROR(0.1) X0 X2 X4 + CORRELATED_ERROR(0.2) X1 X3 X5 + CORRELATED_ERROR(0.3) X0 X1 X2 X3 X4 + M 0 1 2 3 4 5 + DETECTOR(0, 0, 0, 0) rec[-6] + DETECTOR(0, 0, 0, 3) rec[-5] + DETECTOR(0, 0, 0, 2) rec[-4] + DETECTOR(0, 0, 0, 4) rec[-3] + OBSERVABLE_INCLUDE(0) rec[-2] + OBSERVABLE_INCLUDE(1) rec[-1] """) + + +def _tiny_model(): + source_dem = gari._circuit_to_gari_source_dem(_tiny_circuit()) checks, logicals, probabilities = gari.dem_to_matrices(source_dem) x_detectors, z_detectors = gari._detector_partition_from_fourth_coordinate( source_dem @@ -116,5 +127,39 @@ def test_prior_probabilities_and_gari_dem_round_trip(): np.testing.assert_allclose(probabilities, xor_probabilities) +def test_public_circuit_conversion_and_file_output(tmp_path): + public_gari = demutil.gari + circuit = _tiny_circuit() + gari_dem, layout = public_gari.circuit_to_gari( + circuit, + prior_function=public_gari.tesseract_xor_prior_probabilities, + ) + assert layout == { + "schema": "tesseract.gari_layout.v1", + "source_detector_count": 4, + "gari_detector_count": 6, + "source_to_gari": [0, 2, 1, 3], + "detector_order": "physical_then_virtual", + } + assert gari_dem.num_detectors == 6 + assert gari_dem.num_observables == 2 + + circuit_path = tmp_path / "tiny.stim" + circuit.to_file(circuit_path) + output_dir = tmp_path / "gari" + public_gari.call_gari(str(circuit_path), "xor", str(output_dir)) + output_name = "tiny_gari_xor" + written_dem = stim.DetectorErrorModel.from_file( + output_dir / f"{output_name}.dem" + ) + written_layout = json.loads( + (output_dir / f"{output_name}_layout.json").read_text( + encoding="utf-8" + ) + ) + assert str(written_dem) == str(gari_dem) + assert written_layout == layout + + if __name__ == "__main__": raise SystemExit(pytest.main([__file__])) From cbaa9a63deb646334f09f8f21ec0a0c075a9c1dd Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 3 Aug 2026 16:30:57 -0700 Subject: [PATCH 37/43] Address GARI dependency and test feedback --- .github/dependabot.yaml | 1 + src/py/_tesseract_py_util/gari.py | 2 +- src/py/_tesseract_py_util/gari_test.py | 16 ++++++++++------ src/py/requirements.in | 1 + src/py/requirements_lock.txt | 4 +++- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index b58f78a5..7c40dbc9 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -28,6 +28,7 @@ updates: directory: "/src/py" allow: - dependency-name: stim + - dependency-name: scipy - dependency-name: pytest - dependency-name: sinter - dependency-name: pybind11-stubgen diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 266cf016..cfca9ccd 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http:#www.apache.org/licenses/LICENSE-2.0 +# http://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, diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 95b52176..931f8a96 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http:#www.apache.org/licenses/LICENSE-2.0 +# http://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, @@ -25,9 +25,9 @@ def _tiny_circuit(): return stim.Circuit(""" R 0 1 2 3 4 5 - CORRELATED_ERROR(0.1) X0 X2 X4 - CORRELATED_ERROR(0.2) X1 X3 X5 - CORRELATED_ERROR(0.3) X0 X1 X2 X3 X4 + CORRELATED_ERROR(0.01) X0 X2 X4 + CORRELATED_ERROR(0.02) X1 X3 X5 + CORRELATED_ERROR(0.04) X0 X1 X2 X3 X4 M 0 1 2 3 4 5 DETECTOR(0, 0, 0, 0) rec[-6] DETECTOR(0, 0, 0, 3) rec[-5] @@ -105,14 +105,18 @@ def test_prior_probabilities_and_gari_dem_round_trip(): source_probabilities, transform = _tiny_model() np.testing.assert_array_equal( gari.paper_prior_probabilities(transform, source_probabilities), - [0.1, 0.2, 0.3, 0.5, 0.5], + [0.01, 0.02, 0.04, 0.5, 0.5], ) xor_probabilities = gari.tesseract_xor_prior_probabilities( transform, source_probabilities ) np.testing.assert_allclose( - xor_probabilities, [0.1, 0.2, 0.3, 0.34, 0.38] + xor_probabilities, [0.01, 0.02, 0.04, 0.0492, 0.0584] ) + lp_probabilities = gari.tesseract_lp_max_barred_cost_prior_probabilities( + transform, source_probabilities + ) + assert lp_probabilities[2] == pytest.approx(0.5) gari_dem = gari._build_gari_dem( transform, diff --git a/src/py/requirements.in b/src/py/requirements.in index 006da5b3..7473ce47 100644 --- a/src/py/requirements.in +++ b/src/py/requirements.in @@ -2,6 +2,7 @@ # list in ../../.github/dependabot.yaml. stim +scipy pytest sinter pybind11-stubgen diff --git a/src/py/requirements_lock.txt b/src/py/requirements_lock.txt index dddbaa2a..c22d74c0 100644 --- a/src/py/requirements_lock.txt +++ b/src/py/requirements_lock.txt @@ -801,7 +801,9 @@ scipy==1.16.3 \ --hash=sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa \ --hash=sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b \ --hash=sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d - # via sinter + # via + # -r src/py/requirements.in + # sinter sinter==1.16.0 \ --hash=sha256:fd0f7bff97cb951345893f53b347d76f99b53977590054abc3d057150f82037b # via -r src/py/requirements.in From d4bdcf478fac8869025df1abc7a2a5c8a920beb4 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 3 Aug 2026 16:47:41 -0700 Subject: [PATCH 38/43] Add Dependabot cooldowns --- .github/dependabot.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 7c40dbc9..b0c16a26 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -20,6 +20,8 @@ updates: directory: "/" schedule: interval: "monthly" + cooldown: + default-days: 7 labels: - "area/devops" - "area/health" @@ -35,6 +37,8 @@ updates: - dependency-name: jupytext schedule: interval: "monthly" + cooldown: + default-days: 7 versioning-strategy: "increase-if-necessary" groups: python-dependencies: From 26ab580699f47d28efe9e788cf3344563d6080a7 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 3 Aug 2026 21:38:37 -0700 Subject: [PATCH 39/43] Avoid repository-wide dependency changes --- .github/dependabot.yaml | 5 ----- src/py/requirements.in | 1 - src/py/requirements_lock.txt | 4 +--- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index b0c16a26..b58f78a5 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -20,8 +20,6 @@ updates: directory: "/" schedule: interval: "monthly" - cooldown: - default-days: 7 labels: - "area/devops" - "area/health" @@ -30,15 +28,12 @@ updates: directory: "/src/py" allow: - dependency-name: stim - - dependency-name: scipy - dependency-name: pytest - dependency-name: sinter - dependency-name: pybind11-stubgen - dependency-name: jupytext schedule: interval: "monthly" - cooldown: - default-days: 7 versioning-strategy: "increase-if-necessary" groups: python-dependencies: diff --git a/src/py/requirements.in b/src/py/requirements.in index 7473ce47..006da5b3 100644 --- a/src/py/requirements.in +++ b/src/py/requirements.in @@ -2,7 +2,6 @@ # list in ../../.github/dependabot.yaml. stim -scipy pytest sinter pybind11-stubgen diff --git a/src/py/requirements_lock.txt b/src/py/requirements_lock.txt index c22d74c0..dddbaa2a 100644 --- a/src/py/requirements_lock.txt +++ b/src/py/requirements_lock.txt @@ -801,9 +801,7 @@ scipy==1.16.3 \ --hash=sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa \ --hash=sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b \ --hash=sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d - # via - # -r src/py/requirements.in - # sinter + # via sinter sinter==1.16.0 \ --hash=sha256:fd0f7bff97cb951345893f53b347d76f99b53977590054abc3d057150f82037b # via -r src/py/requirements.in From b72586123c274fa1f3cf1615e47ce7409595bc2d Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 3 Aug 2026 22:05:28 -0700 Subject: [PATCH 40/43] Define LP prior --- src/py/_tesseract_py_util/gari.py | 64 +++++++++++++++++++------- src/py/_tesseract_py_util/gari_test.py | 22 ++++++++- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index cfca9ccd..fd29ddf8 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -492,18 +492,24 @@ def tesseract_xor_prior_probabilities( def tesseract_lp_max_barred_cost_prior_probabilities( transform: GariTransform, source_probabilities: np.ndarray ) -> np.ndarray: - """Maximizes the total barred-variable search cost for Tesseract. + """Balances physical and barred-variable search costs with two LPs. The source costs ``c = log((1-p)/p)`` are ordered as ``[e_Z, e_X, e_Y]``. The auxiliary costs ``g`` are ordered as ``[bar(e)_Z, bar(e)_X]``. The incidence matrix is ``A = [[I, 0], [0, I], [U.T, V.T]]``, so the residual physical costs are ``r = c - A g``. - The LP maximizes ``sum(g)`` subject to ``A g <= c`` and ``g >= 0``. It - returns ``[r, g]`` converted back to probabilities in GARI column order. - This experimental policy is not part of the GARI paper. It only defines - search costs and makes no claim about decoding optimality. Solver failure - is a hard error; there is no fallback. + Maximizing only ``sum(g)`` can put most of the cost on a few variables and + leave many physical or auxiliary costs at zero. A zero cost becomes + probability ``0.5`` and gives Tesseract no search preference. The first LP + instead maximizes a common floor for every entry of ``r`` and ``g``. The + second LP keeps that floor and then maximizes ``sum(g)``. This gives a more + balanced set of search costs while still favoring the barred variables. + + The result is ``[r, g]`` converted back to probabilities in GARI column + order. This experimental policy is not part of the GARI paper. It only + defines search costs and makes no claim about decoding optimality. Solver + failure is a hard error; there is no fallback. """ p_e_z, p_e_x, p_e_y = _physical_probability_blocks( transform, source_probabilities @@ -526,25 +532,51 @@ def tesseract_lp_max_barred_cost_prior_probabilities( if auxiliary_count == 0: return physical_probabilities - # A guarded alternative is to first maximize a common floor t, then - # maximize sum(g) while requiring t >= t_star - numerical_tolerance. + # First maximize t subject to every physical and auxiliary cost being at + # least t: c - A g >= t and g >= t. + floor_constraints = scipy.sparse.bmat( + [ + [cost_matrix, np.ones((len(source_costs), 1))], + [ + -identity(auxiliary_count, format="csc"), + np.ones((auxiliary_count, 1)), + ], + ], + format="csc", + ) + floor_objective = np.zeros(auxiliary_count + 1, dtype=np.float64) + floor_objective[-1] = -1 + floor_result = scipy.optimize.linprog( + floor_objective, + A_ub=floor_constraints, + b_ub=np.concatenate([source_costs, np.zeros(auxiliary_count)]), + bounds=(0, None), + method="highs", + ) + if not floor_result.success: + raise RuntimeError( + "LP prior floor solver failed: " + str(floor_result.message) + ) + tolerance = 1e-7 * max( + 1.0, float(np.max(source_costs, initial=0.0)) + ) + cost_floor = max(0.0, float(floor_result.x[-1]) - tolerance) + + # Then maximize the total barred cost without lowering the common floor. objective = -np.ones(auxiliary_count, dtype=np.float64) result = scipy.optimize.linprog( objective, A_ub=cost_matrix, - b_ub=source_costs, - bounds=(0, None), + b_ub=source_costs - cost_floor, + bounds=(cost_floor, None), method="highs", ) if not result.success: raise RuntimeError( - "LP max-barred-cost prior solver failed: " + str(result.message) + "LP prior barred-cost solver failed: " + str(result.message) ) - tolerance = 1e-7 * max( - 1.0, float(np.max(source_costs, initial=0.0)) - ) auxiliary_costs = np.asarray(result.x) - if np.min(auxiliary_costs, initial=0.0) < -tolerance: + if np.min(auxiliary_costs) < cost_floor - tolerance: raise RuntimeError( "LP max-barred-cost solver returned an infeasible solution." ) @@ -552,7 +584,7 @@ def tesseract_lp_max_barred_cost_prior_probabilities( residual_costs = source_costs - np.asarray( cost_matrix @ auxiliary_costs ).reshape(-1) - if np.min(residual_costs, initial=0.0) < -tolerance: + if np.min(residual_costs) < cost_floor - tolerance: raise RuntimeError( "LP max-barred-cost solver returned an infeasible solution." ) diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 931f8a96..0bbbc84a 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -103,8 +103,11 @@ def test_tiny_transform(): def test_prior_probabilities_and_gari_dem_round_trip(): source_probabilities, transform = _tiny_model() + paper_probabilities = gari.paper_prior_probabilities( + transform, source_probabilities + ) np.testing.assert_array_equal( - gari.paper_prior_probabilities(transform, source_probabilities), + paper_probabilities, [0.01, 0.02, 0.04, 0.5, 0.5], ) xor_probabilities = gari.tesseract_xor_prior_probabilities( @@ -116,7 +119,22 @@ def test_prior_probabilities_and_gari_dem_round_trip(): lp_probabilities = gari.tesseract_lp_max_barred_cost_prior_probabilities( transform, source_probabilities ) - assert lp_probabilities[2] == pytest.approx(0.5) + source_costs = np.log1p(-paper_probabilities[:3]) - np.log( + paper_probabilities[:3] + ) + lp_costs = np.log1p(-lp_probabilities) - np.log(lp_probabilities) + assert np.all(lp_costs > 0) + np.testing.assert_allclose( + lp_costs[2:], source_costs[2] / 3, rtol=1e-6 + ) + np.testing.assert_allclose( + [ + lp_costs[0] + lp_costs[3], + lp_costs[1] + lp_costs[4], + lp_costs[2] + lp_costs[3] + lp_costs[4], + ], + source_costs, + ) gari_dem = gari._build_gari_dem( transform, From e4987d66bbc494be26f4f757cb38d831dda11251 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 3 Aug 2026 22:47:16 -0700 Subject: [PATCH 41/43] Document GARI circuit requirements --- src/py/_tesseract_py_util/gari.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index fd29ddf8..a1418c17 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -645,7 +645,14 @@ def circuit_to_gari( *, prior_function: Callable[[GariTransform, np.ndarray], np.ndarray], ) -> tuple[stim.DetectorErrorModel, dict[str, object]]: - """Converts one circuit into a GARI matrix DEM and v1 layout.""" + """Converts a supported CSS circuit into a GARI matrix DEM and v1 layout. + + The source DEM is generated undecomposed (``decompose_errors=False``) and + flattened. Every detector must follow the repository's fourth-coordinate + convention: integer values 0–2 identify X detectors and 3–5 identify Z + detectors. The returned DEM stores transformed matrices for decoding and + must not be sampled. + """ source_dem = _circuit_to_gari_source_dem(circuit) checks, logicals, probabilities = dem_to_matrices(source_dem) x_detectors, z_detectors = _detector_partition_from_fourth_coordinate( From 58b1bce2fa68d4d9695c6bcde64e34a30fc4c86f Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Wed, 5 Aug 2026 11:58:25 -0700 Subject: [PATCH 42/43] Handle canceled GARI source errors --- src/py/_tesseract_py_util/gari.py | 7 ++++++- src/py/_tesseract_py_util/gari_test.py | 12 ++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index a1418c17..e86fd322 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -169,6 +169,7 @@ def dem_to_matrices( not merge duplicate instructions or reconstruct correlations split across instructions. A Stim ``^`` decomposition separator is rejected. Repeated detector or logical targets within an instruction are reduced modulo two. + Resulting no-op errors are omitted, and logical-only errors are rejected. """ dem = dem.flattened() detector_rows: list[int] = [] @@ -185,7 +186,6 @@ def dem_to_matrices( raise ValueError( "GARI requires a DEM generated with decompose_errors=False." ) - column = len(probabilities) detectors: set[int] = set() logicals: set[int] = set() for target in targets: @@ -193,6 +193,11 @@ def dem_to_matrices( detectors ^= {target.val} elif target.is_logical_observable_id(): logicals ^= {target.val} + if not detectors and not logicals: + continue + if not detectors: + raise ValueError("GARI does not support logical-only source errors.") + column = len(probabilities) detector_rows.extend(sorted(detectors)) detector_columns.extend([column] * len(detectors)) logical_rows.extend(sorted(logicals)) diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 0bbbc84a..530adaf8 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -74,11 +74,19 @@ def test_tiny_transform(): gari.dem_to_matrices(stim.DetectorErrorModel("error(0.1) D0 ^ D1")) checks, logicals, probabilities = gari.dem_to_matrices( - stim.DetectorErrorModel("error(0.1) D0 D0 D1 L0 L1 L1") + stim.DetectorErrorModel(""" + error(0.1) D0 D0 + error(0.2) D0 D0 D1 L0 L1 L1 + """) ) np.testing.assert_array_equal(checks.toarray(), [[0], [1]]) np.testing.assert_array_equal(logicals.toarray(), [[1], [0]]) - np.testing.assert_allclose(probabilities, [0.1]) + np.testing.assert_allclose(probabilities, [0.2]) + + with pytest.raises(ValueError, match="logical-only source errors"): + gari.dem_to_matrices( + stim.DetectorErrorModel("error(0.1) D0 D0 L0") + ) _, transform = _tiny_model() np.testing.assert_array_equal( From 7727e37aba4f5ebaeb5567c770a1a04717f2e54b Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Wed, 5 Aug 2026 16:26:49 -0700 Subject: [PATCH 43/43] Document GARI Python API --- src/py/README.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/py/README.md b/src/py/README.md index 8b566e29..0e9ada0e 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -678,3 +678,48 @@ nice_calibrated_dem = demutil.regeneralize_spatial_dem( ) # Result will have error probability (0.1 + 0.2) / 2 = 0.15 ``` + +#### GARI transformed matrices + +`demutil.gari.circuit_to_gari` converts a supported correlated CSS Stim +circuit into a GARI matrix DEM and companion layout for Tesseract. It +generates a flattened source DEM with `decompose_errors=False`. Detectors must +follow the repository's fourth-coordinate convention: values `0`–`2` identify +X detectors and `3`–`5` identify Z detectors. + +```python +import stim +from tesseract_decoder import demutil + +circuit = stim.Circuit.from_file("circuitFile.stim") +gari_dem, gari_layout = demutil.gari.circuit_to_gari( + circuit, + prior_function=demutil.gari.tesseract_xor_prior_probabilities, +) +``` + +`circuit_to_gari` returns: + +* `gari_dem`: the augmented detector and logical matrices stored using Stim + DEM syntax. +* `gari_layout`: a `tesseract.gari_layout.v1` dictionary containing the source + and GARI detector counts, the `source_to_gari` detector mapping, and the + `physical_then_virtual` detector order. + +Related public APIs: + +* `demutil.gari.dem_to_matrices(dem)` returns the sparse detector matrix, + sparse logical matrix, and one probability per source error column. +* `demutil.gari.GariTransform` is passed to prior-policy callbacks. It exposes + the transformed detector and logical matrices, the `U` and `V` projection + matrices, the source `e_Z`, `e_X`, and `e_Y` column indices, and the source + detector mapping. +* `paper_prior_probabilities`, `tesseract_xor_prior_probabilities`, and + `tesseract_lp_max_barred_cost_prior_probabilities` return one probability for + each transformed GARI column. A user-defined prior can follow the same + callable interface. + +The returned GARI matrix DEM stores transformed matrices for decoding and must +not be sampled. Sample from the original circuit and use the companion layout +to place its physical syndrome. See the +[GARI tutorial](../../docs/tutorial.ipynb) for a complete decoding example.