From 924a494beeddecef7a0d2c11e50e7f198c1c933f Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Fri, 7 Aug 2026 14:55:36 -0700 Subject: [PATCH 1/9] Clean up DEM utility module and add CLI --- BUILD | 5 + src/py/README.md | 32 ++++- src/py/_tesseract_py_util/BUILD | 28 +++-- src/py/_tesseract_py_util/__init__.py | 12 +- src/py/_tesseract_py_util/decompose_errors.py | 20 +++ .../decompose_errors_cli.py | 100 +++++++++++++++ .../decompose_errors_cli_test.py | 114 ++++++++++++++++++ .../decompose_errors_test.py | 90 +++++++++++++- src/py/_tesseract_py_util/demutil.py | 42 ------- src/py/_tesseract_py_util/demutil_test.py | 96 --------------- 10 files changed, 381 insertions(+), 158 deletions(-) create mode 100644 src/py/_tesseract_py_util/decompose_errors_cli.py create mode 100644 src/py/_tesseract_py_util/decompose_errors_cli_test.py delete mode 100644 src/py/_tesseract_py_util/demutil.py delete mode 100644 src/py/_tesseract_py_util/demutil_test.py diff --git a/BUILD b/BUILD index a02f45be..d1a41e2f 100644 --- a/BUILD +++ b/BUILD @@ -24,6 +24,11 @@ py_wheel( "//src/py/_tesseract_py_util:_tesseract_py_util", ":package_data", ], + entry_points = { + "console_scripts": [ + "tesseract-dem-decompose = _tesseract_py_util.decompose_errors_cli:main", + ], + }, version = "$(VERSION)", requires=[ "numpy", diff --git a/src/py/README.md b/src/py/README.md index 0e9ada0e..7d2185b1 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -621,8 +621,8 @@ The `tesseract_decoder.demutil` module provides utilities for manipulating `stim **Example Usage**: ```python -import tesseract_decoder.demutil as demutil import stim +from tesseract_decoder import demutil dem = stim.DetectorErrorModel(""" detector(0, 0, 0) D0 @@ -648,6 +648,34 @@ nice_matchable_dem3 = demutil.decompose_errors( ) ``` +#### Command-line decomposition + +Installed wheels provide `tesseract-dem-decompose`, a composable command that +uses the same decomposition implementations as the Python API: + +```bash +tesseract-dem-decompose \ + --method=last-coordinate-index \ + --out output.dem \ + input.dem +``` + +The input defaults to standard input and `--out` defaults to standard output, +so the command can also be used in a pipeline: + +```bash +tesseract-dem-decompose --method=stim-surfacecode-coords \ + < input.dem > output.dem +``` + +Pass `--strip-undecomposable-errors` to drop errors that cannot be decomposed +instead of returning an error. From a source checkout, the same command can be +run with: + +```bash +bazel run --jobs=1 //src/py/_tesseract_py_util:decompose_errors_cli -- --help +``` + * `demutil.regeneralize_spatial_dem(templates: list[stim.DetectorErrorModel], scaffold: stim.DetectorErrorModel, verbose: bool = False) -> stim.DetectorErrorModel` * Updates the error probabilities in a `scaffold` DEM by averaging probabilities from matching errors in a list of `template` DEMs. Errors are matched based on their spatial geometry (relative coordinates of detectors). * **Important:** The scaffold errors must have the same structure and **same absolute coordinates** (for the first detector) as the template errors to be matched. @@ -655,8 +683,8 @@ nice_matchable_dem3 = demutil.decompose_errors( **Example Usage**: ```python -import tesseract_decoder.demutil as demutil import stim +from tesseract_decoder import demutil # Take one or more DEMs **with detector coordinates**, aggregate the error probabilities template1 = stim.DetectorErrorModel(""" diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index 284bcd67..972a006a 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -1,3 +1,4 @@ +load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//python:py_test.bzl", "py_test") load("@rules_python//python:py_library.bzl", "py_library") @@ -12,6 +13,17 @@ py_library( ], ) +py_binary( + name = "decompose_errors_cli", + srcs = ["decompose_errors_cli.py"], + imports = [".."], + visibility = ["//visibility:public"], + deps = [ + ":_tesseract_py_util", + "@pypi//stim", + ], +) + py_test( name = "gari_test", srcs = ["gari_test.py"], @@ -26,29 +38,27 @@ py_test( ], ) - py_test( - name = "demutil_test", - srcs = ["demutil_test.py"], + name = "decompose_errors_test", + srcs = ["decompose_errors_test.py"], + imports = ["..", ".", "../.."], visibility = ["//:__subpackages__"], deps = [ + ":_tesseract_py_util", "@pypi//pytest", "@pypi//stim", "//src:lib_tesseract_decoder", - ":_tesseract_py_util", ], - imports = ["..", ".", "../.."], ) - py_test( - name = "decompose_errors_test", - srcs = ["decompose_errors_test.py"], + name = "decompose_errors_cli_test", + srcs = ["decompose_errors_cli_test.py"], + imports = ["..", "."], visibility = ["//:__subpackages__"], deps = [ ":_tesseract_py_util", "@pypi//pytest", "@pypi//stim", ], - imports = ["..", "."], ) diff --git a/src/py/_tesseract_py_util/__init__.py b/src/py/_tesseract_py_util/__init__.py index 1cee2fbf..3db86cd3 100644 --- a/src/py/_tesseract_py_util/__init__.py +++ b/src/py/_tesseract_py_util/__init__.py @@ -12,12 +12,10 @@ # See the License for the specific language governing permissions and # 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`. -""" +"""Utilities exported through the public ``tesseract_decoder.demutil`` facade.""" 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 +from _tesseract_py_util.decompose_errors import decompose_errors as decompose_errors +from _tesseract_py_util.generalize_dem import generalize as regeneralize_spatial_dem + +__all__ = ["decompose_errors", "gari", "regeneralize_spatial_dem"] diff --git a/src/py/_tesseract_py_util/decompose_errors.py b/src/py/_tesseract_py_util/decompose_errors.py index 1afcfd68..8225e313 100644 --- a/src/py/_tesseract_py_util/decompose_errors.py +++ b/src/py/_tesseract_py_util/decompose_errors.py @@ -354,6 +354,26 @@ def stim_surface_code_det_component(detector_id: int) -> int: ) +def decompose_errors( + dem: stim.DetectorErrorModel, + method: str = "stim-surfacecode-coords", + strip_undecomposable_errors: bool = False, +) -> stim.DetectorErrorModel: + """Dispatches to a decomposition strategy selected by name.""" + if method == "stim-surfacecode-coords": + return decompose_errors_for_stim_surface_code_coords( + dem, strip_undecomposable_errors=strip_undecomposable_errors + ) + if method == "last-coordinate-index": + return decompose_errors_using_last_coordinate_index( + dem, strip_undecomposable_errors=strip_undecomposable_errors + ) + raise ValueError( + "Unknown decomposition method " + f"{method!r}. Expected 'stim-surfacecode-coords' or 'last-coordinate-index'." + ) + + def undecompose_errors(dem: stim.DetectorErrorModel) -> stim.DetectorErrorModel: """Returns a detector error model with any error decompositions removed. diff --git a/src/py/_tesseract_py_util/decompose_errors_cli.py b/src/py/_tesseract_py_util/decompose_errors_cli.py new file mode 100644 index 00000000..6b699ca9 --- /dev/null +++ b/src/py/_tesseract_py_util/decompose_errors_cli.py @@ -0,0 +1,100 @@ +# 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 +# +# 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. + +"""Command-line interface for decomposing Stim detector error models.""" + +import argparse +from collections.abc import Sequence +import sys + +import stim + +from _tesseract_py_util.decompose_errors import decompose_errors + + +_METHODS = ("stim-surfacecode-coords", "last-coordinate-index") + + +def call_decompose_errors( + input_path: str, + output_path: str, + *, + method: str, + strip_undecomposable_errors: bool, +) -> None: + """Reads, decomposes, and writes one detector error model.""" + if input_path == "-": + dem = stim.DetectorErrorModel(sys.stdin.read()) + else: + dem = stim.DetectorErrorModel.from_file(input_path) + + output_dem = decompose_errors( + dem, + method=method, + strip_undecomposable_errors=strip_undecomposable_errors, + ) + if output_path == "-": + print(output_dem) + else: + output_dem.to_file(output_path) + + +def _create_argument_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Decompose errors in a Stim detector error model." + ) + parser.add_argument( + "input", + nargs="?", + default="-", + help="Input DEM file (default: standard input; use '-' for standard input).", + ) + parser.add_argument( + "-o", + "--out", + default="-", + help="Output DEM file (default: standard output; use '-' for standard output).", + ) + parser.add_argument( + "--method", + choices=_METHODS, + default="stim-surfacecode-coords", + help="Detector-component convention used for decomposition.", + ) + parser.add_argument( + "--strip-undecomposable-errors", + action="store_true", + help="Drop errors that cannot be decomposed instead of failing.", + ) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + parser = _create_argument_parser() + args = parser.parse_args(argv) + try: + call_decompose_errors( + args.input, + args.out, + method=args.method, + strip_undecomposable_errors=args.strip_undecomposable_errors, + ) + except (IndexError, KeyError, OSError, ValueError) as ex: + print(f"{parser.prog}: error: {ex}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/py/_tesseract_py_util/decompose_errors_cli_test.py b/src/py/_tesseract_py_util/decompose_errors_cli_test.py new file mode 100644 index 00000000..174b6a08 --- /dev/null +++ b/src/py/_tesseract_py_util/decompose_errors_cli_test.py @@ -0,0 +1,114 @@ +# 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 +# +# 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 io +from pathlib import Path +import sys + +import pytest +import stim + +from _tesseract_py_util import decompose_errors_cli + + +def _decomposable_dem() -> stim.DetectorErrorModel: + return stim.DetectorErrorModel(""" + detector(2, 0, 0) D0 + detector(0, 0, 1) D1 + error(0.1) D0 + error(0.2) D1 + error(0.3) D0 D1 + """) + + +def _expected_decomposed_dem() -> stim.DetectorErrorModel: + return stim.DetectorErrorModel(""" + detector(2, 0, 0) D0 + detector(0, 0, 1) D1 + error(0.1) D0 + error(0.2) D1 + error(0.3) D0 ^ D1 + """) + + +def test_main_reads_stdin_and_writes_stdout(monkeypatch, capsys): + monkeypatch.setattr(sys, "stdin", io.StringIO(str(_decomposable_dem()))) + + exit_code = decompose_errors_cli.main(["--method", "last-coordinate-index"]) + + captured = capsys.readouterr() + assert exit_code == 0 + assert captured.err == "" + assert stim.DetectorErrorModel(captured.out) == _expected_decomposed_dem() + + +def test_main_reads_and_writes_files(tmp_path: Path): + input_path = tmp_path / "input.dem" + output_path = tmp_path / "output.dem" + _decomposable_dem().to_file(input_path) + + exit_code = decompose_errors_cli.main([str(input_path), "--out", str(output_path)]) + + assert exit_code == 0 + assert stim.DetectorErrorModel.from_file(output_path) == _expected_decomposed_dem() + + +def test_main_forwards_strip_undecomposable_errors(monkeypatch, capsys): + dem = stim.DetectorErrorModel(""" + detector(0) D0 + detector(1) D1 + error(0.1) D0 D1 + error(0.1) D0 + """) + monkeypatch.setattr(sys, "stdin", io.StringIO(str(dem))) + + exit_code = decompose_errors_cli.main( + ["--method", "last-coordinate-index", "--strip-undecomposable-errors"] + ) + + captured = capsys.readouterr() + assert exit_code == 0 + assert captured.err == "" + assert stim.DetectorErrorModel(captured.out) == stim.DetectorErrorModel(""" + detector(0) D0 + detector(1) D1 + error(0.1) D0 + """) + + +def test_main_reports_decomposition_failure_on_stderr(monkeypatch, capsys): + dem = stim.DetectorErrorModel(""" + detector(0) D0 + detector(1) D1 + error(0.1) D0 D1 + error(0.1) D0 + """) + monkeypatch.setattr(sys, "stdin", io.StringIO(str(dem))) + + exit_code = decompose_errors_cli.main(["--method", "last-coordinate-index"]) + + captured = capsys.readouterr() + assert exit_code == 1 + assert captured.out == "" + assert "needs to be decomposed into components" in captured.err + + +def test_main_rejects_unknown_method(capsys): + with pytest.raises(SystemExit) as ex_info: + decompose_errors_cli.main(["--method", "unknown"]) + + captured = capsys.readouterr() + assert ex_info.value.code == 2 + assert captured.out == "" + assert "invalid choice" in captured.err diff --git a/src/py/_tesseract_py_util/decompose_errors_test.py b/src/py/_tesseract_py_util/decompose_errors_test.py index 1ef8e23f..76420a5b 100644 --- a/src/py/_tesseract_py_util/decompose_errors_test.py +++ b/src/py/_tesseract_py_util/decompose_errors_test.py @@ -2,14 +2,100 @@ import pytest import stim +import tesseract_decoder from _tesseract_py_util.decompose_errors import ( + decompose_errors, decompose_errors_for_stim_surface_code_coords, decompose_errors_using_detector_coordinate_assignment, decompose_errors_using_last_coordinate_index, detector_coord_to_basis_for_stim_surface_code_convention, get_component_obs_matching_undecomposed_obs, - reduce_set_symmetric_difference, reduce_symmetric_difference, - undecompose_errors) + reduce_set_symmetric_difference, + reduce_symmetric_difference, + undecompose_errors, +) +from tesseract_decoder import demutil + + +def _demo_dem() -> stim.DetectorErrorModel: + return stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(2, 0, 1) D1 + error(0.1) D0 + error(0.2) D1 + error(0.3) D0 D1 + """) + + +def test_import_exposes_demutil_facade(): + assert tesseract_decoder.demutil is demutil + assert hasattr(demutil, "regeneralize_spatial_dem") + assert demutil.decompose_errors is decompose_errors + assert hasattr(demutil.gari, "circuit_to_gari") + + +def test_decompose_errors_rejects_unknown_method(): + with pytest.raises(ValueError, match="Unknown decomposition method"): + demutil.decompose_errors(_demo_dem(), method="bad-method") + + +def test_decompose_errors_public_default_method(): + actual = demutil.decompose_errors(_demo_dem()) + expected = stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(2, 0, 1) D1 + error(0.1) D0 + error(0.2) D1 + error(0.3) D1 ^ D0 + """) + assert actual == expected + + +def test_regeneralize_spatial_dem_averages_template_probabilities(): + template_1 = stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(2, 0, 0) D1 + error(0.1) D0 + error(0.2) D1 + """) + template_2 = stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(2, 0, 0) D1 + error(0.3) D0 + error(0.4) D1 + """) + scaffold = stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(2, 0, 0) D1 + error(0.9) D0 + error(0.9) D1 + """) + + out = demutil.regeneralize_spatial_dem( + templates=[template_1, template_2], scaffold=scaffold + ) + + probs = [inst.args_copy()[0] for inst in out if inst.type == "error"] + assert probs == pytest.approx([0.2, 0.3]) + + +def test_decompose_errors_public_strip_undecomposable_errors(): + dem = stim.DetectorErrorModel(""" +detector(0) D0 +detector(1) D1 +error(0.1) D0 D1 +error(0.1) D0 +""") + + actual = demutil.decompose_errors( + dem, method="last-coordinate-index", strip_undecomposable_errors=True + ) + expected = stim.DetectorErrorModel(""" +detector(0) D0 +detector(1) D1 +error(0.1) D0 +""") + assert actual == expected @pytest.mark.parametrize( diff --git a/src/py/_tesseract_py_util/demutil.py b/src/py/_tesseract_py_util/demutil.py deleted file mode 100644 index cc9aeee2..00000000 --- a/src/py/_tesseract_py_util/demutil.py +++ /dev/null @@ -1,42 +0,0 @@ -# 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 stim -from _tesseract_py_util.decompose_errors import \ - decompose_errors_for_stim_surface_code_coords as \ - decompose_errors_for_stim_surface_code_coords -from _tesseract_py_util.decompose_errors import \ - decompose_errors_using_last_coordinate_index as \ - decompose_errors_using_last_coordinate_index - - -def decompose_errors( - dem: stim.DetectorErrorModel, - method: str = "stim-surfacecode-coords", - strip_undecomposable_errors: bool = False, -) -> stim.DetectorErrorModel: - """Dispatch decomposition strategy by method name.""" - if method == "stim-surfacecode-coords": - return decompose_errors_for_stim_surface_code_coords( - dem, strip_undecomposable_errors=strip_undecomposable_errors - ) - if method == "last-coordinate-index": - return decompose_errors_using_last_coordinate_index( - dem, strip_undecomposable_errors=strip_undecomposable_errors - ) - raise ValueError( - "Unknown decomposition method " - f"{method!r}. Expected 'stim-surfacecode-coords' or 'last-coordinate-index'." - ) diff --git a/src/py/_tesseract_py_util/demutil_test.py b/src/py/_tesseract_py_util/demutil_test.py deleted file mode 100644 index 0935b7c3..00000000 --- a/src/py/_tesseract_py_util/demutil_test.py +++ /dev/null @@ -1,96 +0,0 @@ -# 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 pytest -import stim -import tesseract_decoder -from tesseract_decoder import demutil - - -def _demo_dem() -> stim.DetectorErrorModel: - return stim.DetectorErrorModel(""" - detector(0, 0, 0) D0 - detector(2, 0, 1) D1 - error(0.1) D0 - error(0.2) D1 - error(0.3) D0 D1 - """) - - -def test_import_exposes_demutil_submodule(): - assert hasattr(tesseract_decoder, "demutil") - assert hasattr(demutil, "regeneralize_spatial_dem") - assert hasattr(demutil, "decompose_errors") - assert hasattr(demutil.gari, "circuit_to_gari") - - -def test_decompose_errors_rejects_unknown_method(): - with pytest.raises(ValueError, match="Unknown decomposition method"): - demutil.decompose_errors(_demo_dem(), method="bad-method") - - -def test_regeneralize_spatial_dem_averages_template_probabilities(): - template_1 = stim.DetectorErrorModel(""" - detector(0, 0, 0) D0 - detector(2, 0, 0) D1 - error(0.1) D0 - error(0.2) D1 - """) - template_2 = stim.DetectorErrorModel(""" - detector(0, 0, 0) D0 - detector(2, 0, 0) D1 - error(0.3) D0 - error(0.4) D1 - """) - scaffold = stim.DetectorErrorModel(""" - detector(0, 0, 0) D0 - detector(2, 0, 0) D1 - error(0.9) D0 - error(0.9) D1 - """) - - out = demutil.regeneralize_spatial_dem( - templates=[template_1, template_2], scaffold=scaffold - ) - - probs = [inst.args_copy()[0] for inst in out if inst.type == "error"] - assert probs == pytest.approx([0.2, 0.3]) - - -def test_decompose_errors_top_level_strip_undecomposable_errors(): - dem = stim.DetectorErrorModel(""" -detector(0) D0 -detector(1) D1 -# Error with multiple components (D0 and D1) -error(0.1) D0 D1 -# D0 exists as a standalone error -error(0.1) D0 -# D1 DOES NOT exist as a standalone error -""") - - # Should pass with strip_undecomposable_errors=True - decomposed_dem = demutil.decompose_errors( - dem, method="last-coordinate-index", strip_undecomposable_errors=True - ) - - expected_dem = stim.DetectorErrorModel(""" -detector(0) D0 -detector(1) D1 -error(0.1) D0 -""") - assert str(decomposed_dem) == str(expected_dem) - - -if __name__ == "__main__": - raise SystemExit(pytest.main([__file__])) From 96c69ae471dddce93e78d2a58f6c82c540f8ec46 Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Fri, 7 Aug 2026 15:39:48 -0700 Subject: [PATCH 2/9] Co-locate decomposition CLI with library module --- BUILD | 2 +- src/py/_tesseract_py_util/BUILD | 4 +- src/py/_tesseract_py_util/decompose_errors.py | 74 ++++++++++++- .../decompose_errors_cli.py | 100 ------------------ .../decompose_errors_cli_test.py | 12 +-- 5 files changed, 82 insertions(+), 110 deletions(-) delete mode 100644 src/py/_tesseract_py_util/decompose_errors_cli.py diff --git a/BUILD b/BUILD index d1a41e2f..c2c91e88 100644 --- a/BUILD +++ b/BUILD @@ -26,7 +26,7 @@ py_wheel( ], entry_points = { "console_scripts": [ - "tesseract-dem-decompose = _tesseract_py_util.decompose_errors_cli:main", + "tesseract-dem-decompose = _tesseract_py_util.decompose_errors:main", ], }, version = "$(VERSION)", diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index 972a006a..bf7d34ef 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -15,11 +15,11 @@ py_library( py_binary( name = "decompose_errors_cli", - srcs = ["decompose_errors_cli.py"], + srcs = ["decompose_errors.py"], imports = [".."], + main = "decompose_errors.py", visibility = ["//visibility:public"], deps = [ - ":_tesseract_py_util", "@pypi//stim", ], ) diff --git a/src/py/_tesseract_py_util/decompose_errors.py b/src/py/_tesseract_py_util/decompose_errors.py index 8225e313..a8e1832f 100644 --- a/src/py/_tesseract_py_util/decompose_errors.py +++ b/src/py/_tesseract_py_util/decompose_errors.py @@ -13,8 +13,9 @@ # limitations under the License. import itertools +import sys from collections import defaultdict -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Sequence from functools import reduce import stim @@ -424,3 +425,74 @@ def undecompose_errors(dem: stim.DetectorErrorModel) -> stim.DetectorErrorModel: ) ) return undecomposed_dem + + +def call_decompose_errors( + input_path: str, + output_path: str, + *, + method: str, + strip_undecomposable_errors: bool, +) -> None: + """Reads, decomposes, and writes one detector error model.""" + if input_path == "-": + dem = stim.DetectorErrorModel(sys.stdin.read()) + else: + dem = stim.DetectorErrorModel.from_file(input_path) + + output_dem = decompose_errors( + dem, + method=method, + strip_undecomposable_errors=strip_undecomposable_errors, + ) + if output_path == "-": + print(output_dem) + else: + output_dem.to_file(output_path) + + +def main(argv: Sequence[str] | None = None) -> int: + import argparse + + parser = argparse.ArgumentParser( + description="Decompose errors in a Stim detector error model." + ) + parser.add_argument( + "input", + nargs="?", + default="-", + help="Input DEM file (default: standard input; use '-' for standard input).", + ) + parser.add_argument( + "-o", + "--out", + default="-", + help="Output DEM file (default: standard output; use '-' for standard output).", + ) + parser.add_argument( + "--method", + choices=("stim-surfacecode-coords", "last-coordinate-index"), + default="stim-surfacecode-coords", + help="Detector-component convention used for decomposition.", + ) + parser.add_argument( + "--strip-undecomposable-errors", + action="store_true", + help="Drop errors that cannot be decomposed instead of failing.", + ) + args = parser.parse_args(argv) + try: + call_decompose_errors( + args.input, + args.out, + method=args.method, + strip_undecomposable_errors=args.strip_undecomposable_errors, + ) + except (IndexError, KeyError, OSError, ValueError) as ex: + print(f"{parser.prog}: error: {ex}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/py/_tesseract_py_util/decompose_errors_cli.py b/src/py/_tesseract_py_util/decompose_errors_cli.py deleted file mode 100644 index 6b699ca9..00000000 --- a/src/py/_tesseract_py_util/decompose_errors_cli.py +++ /dev/null @@ -1,100 +0,0 @@ -# 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 -# -# 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. - -"""Command-line interface for decomposing Stim detector error models.""" - -import argparse -from collections.abc import Sequence -import sys - -import stim - -from _tesseract_py_util.decompose_errors import decompose_errors - - -_METHODS = ("stim-surfacecode-coords", "last-coordinate-index") - - -def call_decompose_errors( - input_path: str, - output_path: str, - *, - method: str, - strip_undecomposable_errors: bool, -) -> None: - """Reads, decomposes, and writes one detector error model.""" - if input_path == "-": - dem = stim.DetectorErrorModel(sys.stdin.read()) - else: - dem = stim.DetectorErrorModel.from_file(input_path) - - output_dem = decompose_errors( - dem, - method=method, - strip_undecomposable_errors=strip_undecomposable_errors, - ) - if output_path == "-": - print(output_dem) - else: - output_dem.to_file(output_path) - - -def _create_argument_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description="Decompose errors in a Stim detector error model." - ) - parser.add_argument( - "input", - nargs="?", - default="-", - help="Input DEM file (default: standard input; use '-' for standard input).", - ) - parser.add_argument( - "-o", - "--out", - default="-", - help="Output DEM file (default: standard output; use '-' for standard output).", - ) - parser.add_argument( - "--method", - choices=_METHODS, - default="stim-surfacecode-coords", - help="Detector-component convention used for decomposition.", - ) - parser.add_argument( - "--strip-undecomposable-errors", - action="store_true", - help="Drop errors that cannot be decomposed instead of failing.", - ) - return parser - - -def main(argv: Sequence[str] | None = None) -> int: - parser = _create_argument_parser() - args = parser.parse_args(argv) - try: - call_decompose_errors( - args.input, - args.out, - method=args.method, - strip_undecomposable_errors=args.strip_undecomposable_errors, - ) - except (IndexError, KeyError, OSError, ValueError) as ex: - print(f"{parser.prog}: error: {ex}", file=sys.stderr) - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/py/_tesseract_py_util/decompose_errors_cli_test.py b/src/py/_tesseract_py_util/decompose_errors_cli_test.py index 174b6a08..7c722d08 100644 --- a/src/py/_tesseract_py_util/decompose_errors_cli_test.py +++ b/src/py/_tesseract_py_util/decompose_errors_cli_test.py @@ -19,7 +19,7 @@ import pytest import stim -from _tesseract_py_util import decompose_errors_cli +from _tesseract_py_util.decompose_errors import main def _decomposable_dem() -> stim.DetectorErrorModel: @@ -45,7 +45,7 @@ def _expected_decomposed_dem() -> stim.DetectorErrorModel: def test_main_reads_stdin_and_writes_stdout(monkeypatch, capsys): monkeypatch.setattr(sys, "stdin", io.StringIO(str(_decomposable_dem()))) - exit_code = decompose_errors_cli.main(["--method", "last-coordinate-index"]) + exit_code = main(["--method", "last-coordinate-index"]) captured = capsys.readouterr() assert exit_code == 0 @@ -58,7 +58,7 @@ def test_main_reads_and_writes_files(tmp_path: Path): output_path = tmp_path / "output.dem" _decomposable_dem().to_file(input_path) - exit_code = decompose_errors_cli.main([str(input_path), "--out", str(output_path)]) + exit_code = main([str(input_path), "--out", str(output_path)]) assert exit_code == 0 assert stim.DetectorErrorModel.from_file(output_path) == _expected_decomposed_dem() @@ -73,7 +73,7 @@ def test_main_forwards_strip_undecomposable_errors(monkeypatch, capsys): """) monkeypatch.setattr(sys, "stdin", io.StringIO(str(dem))) - exit_code = decompose_errors_cli.main( + exit_code = main( ["--method", "last-coordinate-index", "--strip-undecomposable-errors"] ) @@ -96,7 +96,7 @@ def test_main_reports_decomposition_failure_on_stderr(monkeypatch, capsys): """) monkeypatch.setattr(sys, "stdin", io.StringIO(str(dem))) - exit_code = decompose_errors_cli.main(["--method", "last-coordinate-index"]) + exit_code = main(["--method", "last-coordinate-index"]) captured = capsys.readouterr() assert exit_code == 1 @@ -106,7 +106,7 @@ def test_main_reports_decomposition_failure_on_stderr(monkeypatch, capsys): def test_main_rejects_unknown_method(capsys): with pytest.raises(SystemExit) as ex_info: - decompose_errors_cli.main(["--method", "unknown"]) + main(["--method", "unknown"]) captured = capsys.readouterr() assert ex_info.value.code == 2 From c84e5ad33fdf8292941d4ecba5969ac6997aa82e Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Fri, 7 Aug 2026 17:09:28 -0700 Subject: [PATCH 3/9] Fix Linux CI and remove CLI test target --- .github/workflows/ci.yml | 2 +- src/BUILD | 9 ++ src/py/_tesseract_py_util/BUILD | 12 -- .../decompose_errors_cli_test.py | 114 ------------------ 4 files changed, 10 insertions(+), 127 deletions(-) delete mode 100644 src/py/_tesseract_py_util/decompose_errors_cli_test.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd2bda78..4b5eeb6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,4 +82,4 @@ jobs: brew install clang-format - name: Bazel tests - run: bazel test src/... //docs:tutorial_jupytext_sync_test + run: bazel test --define=tesseract_portable=true src/... //docs:tutorial_jupytext_sync_test diff --git a/src/BUILD b/src/BUILD index 9aa162b2..0310addc 100644 --- a/src/BUILD +++ b/src/BUILD @@ -23,6 +23,14 @@ config_setting( values = {"compilation_mode": "dbg"}, ) +# CI restores its Linux disk cache across runners with different CPUs. Avoid +# caching binaries containing instructions specific to whichever runner built first. +config_setting( + name = "portable_linux", + constraint_values = ["@platforms//os:linux"], + define_values = {"tesseract_portable": "true"}, +) + OPT_COPTS = select({ "//conditions:default": [ "-Ofast", @@ -33,6 +41,7 @@ OPT_COPTS = select({ "//conditions:default": ["-std=c++20"], }) + select({ "@platforms//os:macos": ["-mmacosx-version-min=10.15",], + ":portable_linux": [], "//conditions:default": ["-march=native",], }) diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index bf7d34ef..4db692e1 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -50,15 +50,3 @@ py_test( "//src:lib_tesseract_decoder", ], ) - -py_test( - name = "decompose_errors_cli_test", - srcs = ["decompose_errors_cli_test.py"], - imports = ["..", "."], - visibility = ["//:__subpackages__"], - deps = [ - ":_tesseract_py_util", - "@pypi//pytest", - "@pypi//stim", - ], -) diff --git a/src/py/_tesseract_py_util/decompose_errors_cli_test.py b/src/py/_tesseract_py_util/decompose_errors_cli_test.py deleted file mode 100644 index 7c722d08..00000000 --- a/src/py/_tesseract_py_util/decompose_errors_cli_test.py +++ /dev/null @@ -1,114 +0,0 @@ -# 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 -# -# 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 io -from pathlib import Path -import sys - -import pytest -import stim - -from _tesseract_py_util.decompose_errors import main - - -def _decomposable_dem() -> stim.DetectorErrorModel: - return stim.DetectorErrorModel(""" - detector(2, 0, 0) D0 - detector(0, 0, 1) D1 - error(0.1) D0 - error(0.2) D1 - error(0.3) D0 D1 - """) - - -def _expected_decomposed_dem() -> stim.DetectorErrorModel: - return stim.DetectorErrorModel(""" - detector(2, 0, 0) D0 - detector(0, 0, 1) D1 - error(0.1) D0 - error(0.2) D1 - error(0.3) D0 ^ D1 - """) - - -def test_main_reads_stdin_and_writes_stdout(monkeypatch, capsys): - monkeypatch.setattr(sys, "stdin", io.StringIO(str(_decomposable_dem()))) - - exit_code = main(["--method", "last-coordinate-index"]) - - captured = capsys.readouterr() - assert exit_code == 0 - assert captured.err == "" - assert stim.DetectorErrorModel(captured.out) == _expected_decomposed_dem() - - -def test_main_reads_and_writes_files(tmp_path: Path): - input_path = tmp_path / "input.dem" - output_path = tmp_path / "output.dem" - _decomposable_dem().to_file(input_path) - - exit_code = main([str(input_path), "--out", str(output_path)]) - - assert exit_code == 0 - assert stim.DetectorErrorModel.from_file(output_path) == _expected_decomposed_dem() - - -def test_main_forwards_strip_undecomposable_errors(monkeypatch, capsys): - dem = stim.DetectorErrorModel(""" - detector(0) D0 - detector(1) D1 - error(0.1) D0 D1 - error(0.1) D0 - """) - monkeypatch.setattr(sys, "stdin", io.StringIO(str(dem))) - - exit_code = main( - ["--method", "last-coordinate-index", "--strip-undecomposable-errors"] - ) - - captured = capsys.readouterr() - assert exit_code == 0 - assert captured.err == "" - assert stim.DetectorErrorModel(captured.out) == stim.DetectorErrorModel(""" - detector(0) D0 - detector(1) D1 - error(0.1) D0 - """) - - -def test_main_reports_decomposition_failure_on_stderr(monkeypatch, capsys): - dem = stim.DetectorErrorModel(""" - detector(0) D0 - detector(1) D1 - error(0.1) D0 D1 - error(0.1) D0 - """) - monkeypatch.setattr(sys, "stdin", io.StringIO(str(dem))) - - exit_code = main(["--method", "last-coordinate-index"]) - - captured = capsys.readouterr() - assert exit_code == 1 - assert captured.out == "" - assert "needs to be decomposed into components" in captured.err - - -def test_main_rejects_unknown_method(capsys): - with pytest.raises(SystemExit) as ex_info: - main(["--method", "unknown"]) - - captured = capsys.readouterr() - assert ex_info.value.code == 2 - assert captured.out == "" - assert "invalid choice" in captured.err From 04f27fba644c58e49abeb67573d2735e7e53c97b Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Fri, 7 Aug 2026 18:29:34 -0700 Subject: [PATCH 4/9] Match existing DEM utility CLI pattern --- BUILD | 5 --- src/py/README.md | 23 +++++------ src/py/_tesseract_py_util/BUILD | 12 ------ src/py/_tesseract_py_util/decompose_errors.py | 38 ++++++++----------- 4 files changed, 26 insertions(+), 52 deletions(-) diff --git a/BUILD b/BUILD index c2c91e88..a02f45be 100644 --- a/BUILD +++ b/BUILD @@ -24,11 +24,6 @@ py_wheel( "//src/py/_tesseract_py_util:_tesseract_py_util", ":package_data", ], - entry_points = { - "console_scripts": [ - "tesseract-dem-decompose = _tesseract_py_util.decompose_errors:main", - ], - }, version = "$(VERSION)", requires=[ "numpy", diff --git a/src/py/README.md b/src/py/README.md index 7d2185b1..21877c82 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -610,7 +610,7 @@ print(f"Logical error rate: {result.errors / result.shots}") The `tesseract_decoder.demutil` module provides utilities for manipulating `stim.DetectorErrorModel` objects, specifically for decomposing complex error mechanisms into simpler components and regeneralizing spatial error models. #### Functions -* `demutil.decompose_errors(dem: stim.DetectorErrorModel, method: str, strip_undecomposable_errors: bool = False) -> stim.DetectorErrorModel` +* `demutil.decompose_errors(dem: stim.DetectorErrorModel, method: str = "stim-surfacecode-coords", strip_undecomposable_errors: bool = False) -> stim.DetectorErrorModel` * Decomposes error mechanisms in a DEM into simpler components based on the specified method. * Supported methods: * `"stim-surfacecode-coords"`: Decomposes errors based on the spatial coordinates of detectors, assuming a surface code layout where coordinates indicate X or Z basis. @@ -650,31 +650,28 @@ nice_matchable_dem3 = demutil.decompose_errors( #### Command-line decomposition -Installed wheels provide `tesseract-dem-decompose`, a composable command that -uses the same decomposition implementations as the Python API: +Like the other DEM utility modules, `decompose_errors.py` can also be run +directly: ```bash -tesseract-dem-decompose \ +python src/py/_tesseract_py_util/decompose_errors.py \ --method=last-coordinate-index \ --out output.dem \ input.dem ``` -The input defaults to standard input and `--out` defaults to standard output, -so the command can also be used in a pipeline: +The input defaults to standard input, `--out` defaults to standard output, and +`--method` defaults to `stim-surfacecode-coords`, so the command can also be +used in a pipeline: ```bash -tesseract-dem-decompose --method=stim-surfacecode-coords \ +python src/py/_tesseract_py_util/decompose_errors.py \ + --method=stim-surfacecode-coords \ < input.dem > output.dem ``` Pass `--strip-undecomposable-errors` to drop errors that cannot be decomposed -instead of returning an error. From a source checkout, the same command can be -run with: - -```bash -bazel run --jobs=1 //src/py/_tesseract_py_util:decompose_errors_cli -- --help -``` +instead of returning an error. * `demutil.regeneralize_spatial_dem(templates: list[stim.DetectorErrorModel], scaffold: stim.DetectorErrorModel, verbose: bool = False) -> stim.DetectorErrorModel` * Updates the error probabilities in a `scaffold` DEM by averaging probabilities from matching errors in a list of `template` DEMs. Errors are matched based on their spatial geometry (relative coordinates of detectors). diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index 4db692e1..81820e09 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -1,4 +1,3 @@ -load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//python:py_test.bzl", "py_test") load("@rules_python//python:py_library.bzl", "py_library") @@ -13,17 +12,6 @@ py_library( ], ) -py_binary( - name = "decompose_errors_cli", - srcs = ["decompose_errors.py"], - imports = [".."], - main = "decompose_errors.py", - visibility = ["//visibility:public"], - deps = [ - "@pypi//stim", - ], -) - py_test( name = "gari_test", srcs = ["gari_test.py"], diff --git a/src/py/_tesseract_py_util/decompose_errors.py b/src/py/_tesseract_py_util/decompose_errors.py index a8e1832f..1f6548c6 100644 --- a/src/py/_tesseract_py_util/decompose_errors.py +++ b/src/py/_tesseract_py_util/decompose_errors.py @@ -15,7 +15,7 @@ import itertools import sys from collections import defaultdict -from collections.abc import Callable, Iterable, Sequence +from collections.abc import Callable, Iterable from functools import reduce import stim @@ -428,30 +428,29 @@ def undecompose_errors(dem: stim.DetectorErrorModel) -> stim.DetectorErrorModel: def call_decompose_errors( - input_path: str, - output_path: str, - *, + input_fname: str, + output_fname: str, method: str, strip_undecomposable_errors: bool, ) -> None: """Reads, decomposes, and writes one detector error model.""" - if input_path == "-": + if input_fname == "-": dem = stim.DetectorErrorModel(sys.stdin.read()) else: - dem = stim.DetectorErrorModel.from_file(input_path) + dem = stim.DetectorErrorModel.from_file(input_fname) output_dem = decompose_errors( dem, method=method, strip_undecomposable_errors=strip_undecomposable_errors, ) - if output_path == "-": + if output_fname == "-": print(output_dem) else: - output_dem.to_file(output_path) + output_dem.to_file(output_fname) -def main(argv: Sequence[str] | None = None) -> int: +def main() -> None: import argparse parser = argparse.ArgumentParser( @@ -480,19 +479,14 @@ def main(argv: Sequence[str] | None = None) -> int: action="store_true", help="Drop errors that cannot be decomposed instead of failing.", ) - args = parser.parse_args(argv) - try: - call_decompose_errors( - args.input, - args.out, - method=args.method, - strip_undecomposable_errors=args.strip_undecomposable_errors, - ) - except (IndexError, KeyError, OSError, ValueError) as ex: - print(f"{parser.prog}: error: {ex}", file=sys.stderr) - return 1 - return 0 + args = parser.parse_args() + call_decompose_errors( + args.input, + args.out, + method=args.method, + strip_undecomposable_errors=args.strip_undecomposable_errors, + ) if __name__ == "__main__": - raise SystemExit(main()) + main() From fdfd84c48a515f9a1e27ac8cffda9e6e425d8a1c Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Sat, 8 Aug 2026 19:54:59 -0700 Subject: [PATCH 5/9] Revert CI workflow change --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b5eeb6e..fd2bda78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,4 +82,4 @@ jobs: brew install clang-format - name: Bazel tests - run: bazel test --define=tesseract_portable=true src/... //docs:tutorial_jupytext_sync_test + run: bazel test src/... //docs:tutorial_jupytext_sync_test From 0596a1993d4d4b28806a717c07a13d12ef7ef265 Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Sat, 8 Aug 2026 19:59:52 -0700 Subject: [PATCH 6/9] Inline decomposer CLI and restore facade-test split --- src/py/_tesseract_py_util/BUILD | 18 +++- src/py/_tesseract_py_util/decompose_errors.py | 44 +++------ .../decompose_errors_test.py | 47 +-------- src/py/_tesseract_py_util/demutil_test.py | 96 +++++++++++++++++++ 4 files changed, 130 insertions(+), 75 deletions(-) create mode 100644 src/py/_tesseract_py_util/demutil_test.py diff --git a/src/py/_tesseract_py_util/BUILD b/src/py/_tesseract_py_util/BUILD index 81820e09..284bcd67 100644 --- a/src/py/_tesseract_py_util/BUILD +++ b/src/py/_tesseract_py_util/BUILD @@ -26,15 +26,29 @@ py_test( ], ) + +py_test( + name = "demutil_test", + srcs = ["demutil_test.py"], + visibility = ["//:__subpackages__"], + deps = [ + "@pypi//pytest", + "@pypi//stim", + "//src:lib_tesseract_decoder", + ":_tesseract_py_util", + ], + imports = ["..", ".", "../.."], +) + + py_test( name = "decompose_errors_test", srcs = ["decompose_errors_test.py"], - imports = ["..", ".", "../.."], visibility = ["//:__subpackages__"], deps = [ ":_tesseract_py_util", "@pypi//pytest", "@pypi//stim", - "//src:lib_tesseract_decoder", ], + imports = ["..", "."], ) diff --git a/src/py/_tesseract_py_util/decompose_errors.py b/src/py/_tesseract_py_util/decompose_errors.py index 1f6548c6..9400ba3b 100644 --- a/src/py/_tesseract_py_util/decompose_errors.py +++ b/src/py/_tesseract_py_util/decompose_errors.py @@ -427,30 +427,7 @@ def undecompose_errors(dem: stim.DetectorErrorModel) -> stim.DetectorErrorModel: return undecomposed_dem -def call_decompose_errors( - input_fname: str, - output_fname: str, - method: str, - strip_undecomposable_errors: bool, -) -> None: - """Reads, decomposes, and writes one detector error model.""" - if input_fname == "-": - dem = stim.DetectorErrorModel(sys.stdin.read()) - else: - dem = stim.DetectorErrorModel.from_file(input_fname) - - output_dem = decompose_errors( - dem, - method=method, - strip_undecomposable_errors=strip_undecomposable_errors, - ) - if output_fname == "-": - print(output_dem) - else: - output_dem.to_file(output_fname) - - -def main() -> None: +if __name__ == "__main__": import argparse parser = argparse.ArgumentParser( @@ -480,13 +457,18 @@ def main() -> None: help="Drop errors that cannot be decomposed instead of failing.", ) args = parser.parse_args() - call_decompose_errors( - args.input, - args.out, + + if args.input == "-": + dem = stim.DetectorErrorModel(sys.stdin.read()) + else: + dem = stim.DetectorErrorModel.from_file(args.input) + + output_dem = decompose_errors( + dem, method=args.method, strip_undecomposable_errors=args.strip_undecomposable_errors, ) - - -if __name__ == "__main__": - main() + if args.out == "-": + print(output_dem) + else: + output_dem.to_file(args.out) diff --git a/src/py/_tesseract_py_util/decompose_errors_test.py b/src/py/_tesseract_py_util/decompose_errors_test.py index 76420a5b..4b731d51 100644 --- a/src/py/_tesseract_py_util/decompose_errors_test.py +++ b/src/py/_tesseract_py_util/decompose_errors_test.py @@ -2,7 +2,6 @@ import pytest import stim -import tesseract_decoder from _tesseract_py_util.decompose_errors import ( decompose_errors, decompose_errors_for_stim_surface_code_coords, @@ -14,7 +13,6 @@ reduce_symmetric_difference, undecompose_errors, ) -from tesseract_decoder import demutil def _demo_dem() -> stim.DetectorErrorModel: @@ -27,20 +25,13 @@ def _demo_dem() -> stim.DetectorErrorModel: """) -def test_import_exposes_demutil_facade(): - assert tesseract_decoder.demutil is demutil - assert hasattr(demutil, "regeneralize_spatial_dem") - assert demutil.decompose_errors is decompose_errors - assert hasattr(demutil.gari, "circuit_to_gari") - - def test_decompose_errors_rejects_unknown_method(): with pytest.raises(ValueError, match="Unknown decomposition method"): - demutil.decompose_errors(_demo_dem(), method="bad-method") + decompose_errors(_demo_dem(), method="bad-method") -def test_decompose_errors_public_default_method(): - actual = demutil.decompose_errors(_demo_dem()) +def test_decompose_errors_default_method(): + actual = decompose_errors(_demo_dem()) expected = stim.DetectorErrorModel(""" detector(0, 0, 0) D0 detector(2, 0, 1) D1 @@ -51,35 +42,7 @@ def test_decompose_errors_public_default_method(): assert actual == expected -def test_regeneralize_spatial_dem_averages_template_probabilities(): - template_1 = stim.DetectorErrorModel(""" - detector(0, 0, 0) D0 - detector(2, 0, 0) D1 - error(0.1) D0 - error(0.2) D1 - """) - template_2 = stim.DetectorErrorModel(""" - detector(0, 0, 0) D0 - detector(2, 0, 0) D1 - error(0.3) D0 - error(0.4) D1 - """) - scaffold = stim.DetectorErrorModel(""" - detector(0, 0, 0) D0 - detector(2, 0, 0) D1 - error(0.9) D0 - error(0.9) D1 - """) - - out = demutil.regeneralize_spatial_dem( - templates=[template_1, template_2], scaffold=scaffold - ) - - probs = [inst.args_copy()[0] for inst in out if inst.type == "error"] - assert probs == pytest.approx([0.2, 0.3]) - - -def test_decompose_errors_public_strip_undecomposable_errors(): +def test_decompose_errors_strip_undecomposable_errors(): dem = stim.DetectorErrorModel(""" detector(0) D0 detector(1) D1 @@ -87,7 +50,7 @@ def test_decompose_errors_public_strip_undecomposable_errors(): error(0.1) D0 """) - actual = demutil.decompose_errors( + actual = decompose_errors( dem, method="last-coordinate-index", strip_undecomposable_errors=True ) expected = stim.DetectorErrorModel(""" diff --git a/src/py/_tesseract_py_util/demutil_test.py b/src/py/_tesseract_py_util/demutil_test.py new file mode 100644 index 00000000..0935b7c3 --- /dev/null +++ b/src/py/_tesseract_py_util/demutil_test.py @@ -0,0 +1,96 @@ +# 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 pytest +import stim +import tesseract_decoder +from tesseract_decoder import demutil + + +def _demo_dem() -> stim.DetectorErrorModel: + return stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(2, 0, 1) D1 + error(0.1) D0 + error(0.2) D1 + error(0.3) D0 D1 + """) + + +def test_import_exposes_demutil_submodule(): + assert hasattr(tesseract_decoder, "demutil") + assert hasattr(demutil, "regeneralize_spatial_dem") + assert hasattr(demutil, "decompose_errors") + assert hasattr(demutil.gari, "circuit_to_gari") + + +def test_decompose_errors_rejects_unknown_method(): + with pytest.raises(ValueError, match="Unknown decomposition method"): + demutil.decompose_errors(_demo_dem(), method="bad-method") + + +def test_regeneralize_spatial_dem_averages_template_probabilities(): + template_1 = stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(2, 0, 0) D1 + error(0.1) D0 + error(0.2) D1 + """) + template_2 = stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(2, 0, 0) D1 + error(0.3) D0 + error(0.4) D1 + """) + scaffold = stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(2, 0, 0) D1 + error(0.9) D0 + error(0.9) D1 + """) + + out = demutil.regeneralize_spatial_dem( + templates=[template_1, template_2], scaffold=scaffold + ) + + probs = [inst.args_copy()[0] for inst in out if inst.type == "error"] + assert probs == pytest.approx([0.2, 0.3]) + + +def test_decompose_errors_top_level_strip_undecomposable_errors(): + dem = stim.DetectorErrorModel(""" +detector(0) D0 +detector(1) D1 +# Error with multiple components (D0 and D1) +error(0.1) D0 D1 +# D0 exists as a standalone error +error(0.1) D0 +# D1 DOES NOT exist as a standalone error +""") + + # Should pass with strip_undecomposable_errors=True + decomposed_dem = demutil.decompose_errors( + dem, method="last-coordinate-index", strip_undecomposable_errors=True + ) + + expected_dem = stim.DetectorErrorModel(""" +detector(0) D0 +detector(1) D1 +error(0.1) D0 +""") + assert str(decomposed_dem) == str(expected_dem) + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__])) From 23319fab9d8af25f5e76f60b040969009474b290 Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Sat, 8 Aug 2026 20:10:06 -0700 Subject: [PATCH 7/9] Drop unused portable_linux config setting --- src/BUILD | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/BUILD b/src/BUILD index 0310addc..9aa162b2 100644 --- a/src/BUILD +++ b/src/BUILD @@ -23,14 +23,6 @@ config_setting( values = {"compilation_mode": "dbg"}, ) -# CI restores its Linux disk cache across runners with different CPUs. Avoid -# caching binaries containing instructions specific to whichever runner built first. -config_setting( - name = "portable_linux", - constraint_values = ["@platforms//os:linux"], - define_values = {"tesseract_portable": "true"}, -) - OPT_COPTS = select({ "//conditions:default": [ "-Ofast", @@ -41,7 +33,6 @@ OPT_COPTS = select({ "//conditions:default": ["-std=c++20"], }) + select({ "@platforms//os:macos": ["-mmacosx-version-min=10.15",], - ":portable_linux": [], "//conditions:default": ["-march=native",], }) From dd1d89f92803d1cb63491b81865097392e76158e Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Sat, 8 Aug 2026 20:12:33 -0700 Subject: [PATCH 8/9] Say tools instead of modules in the CLI docs --- src/py/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/README.md b/src/py/README.md index 21877c82..658932a8 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -650,7 +650,7 @@ nice_matchable_dem3 = demutil.decompose_errors( #### Command-line decomposition -Like the other DEM utility modules, `decompose_errors.py` can also be run +Like the other DEM utility tools, `decompose_errors.py` can also be run directly: ```bash From 696a621ec2ee1995898f3c9e451db1fa39590885 Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Sat, 8 Aug 2026 20:19:46 -0700 Subject: [PATCH 9/9] Use a constant DEM in the decomposer tests --- .../decompose_errors_test.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/py/_tesseract_py_util/decompose_errors_test.py b/src/py/_tesseract_py_util/decompose_errors_test.py index 4b731d51..3fbc12a4 100644 --- a/src/py/_tesseract_py_util/decompose_errors_test.py +++ b/src/py/_tesseract_py_util/decompose_errors_test.py @@ -15,23 +15,22 @@ ) -def _demo_dem() -> stim.DetectorErrorModel: - return stim.DetectorErrorModel(""" - detector(0, 0, 0) D0 - detector(2, 0, 1) D1 - error(0.1) D0 - error(0.2) D1 - error(0.3) D0 D1 - """) +DEMO_DEM = stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(2, 0, 1) D1 + error(0.1) D0 + error(0.2) D1 + error(0.3) D0 D1 + """) def test_decompose_errors_rejects_unknown_method(): with pytest.raises(ValueError, match="Unknown decomposition method"): - decompose_errors(_demo_dem(), method="bad-method") + decompose_errors(DEMO_DEM, method="bad-method") def test_decompose_errors_default_method(): - actual = decompose_errors(_demo_dem()) + actual = decompose_errors(DEMO_DEM) expected = stim.DetectorErrorModel(""" detector(0, 0, 0) D0 detector(2, 0, 1) D1