diff --git a/src/py/README.md b/src/py/README.md index 0e9ada0e..658932a8 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. @@ -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,31 @@ nice_matchable_dem3 = demutil.decompose_errors( ) ``` +#### Command-line decomposition + +Like the other DEM utility tools, `decompose_errors.py` can also be run +directly: + +```bash +python src/py/_tesseract_py_util/decompose_errors.py \ + --method=last-coordinate-index \ + --out output.dem \ + input.dem +``` + +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 +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. + * `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 +680,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/__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..9400ba3b 100644 --- a/src/py/_tesseract_py_util/decompose_errors.py +++ b/src/py/_tesseract_py_util/decompose_errors.py @@ -13,6 +13,7 @@ # limitations under the License. import itertools +import sys from collections import defaultdict from collections.abc import Callable, Iterable from functools import reduce @@ -354,6 +355,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. @@ -404,3 +425,50 @@ def undecompose_errors(dem: stim.DetectorErrorModel) -> stim.DetectorErrorModel: ) ) return undecomposed_dem + + +if __name__ == "__main__": + 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() + + 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 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 1ef8e23f..3fbc12a4 100644 --- a/src/py/_tesseract_py_util/decompose_errors_test.py +++ b/src/py/_tesseract_py_util/decompose_errors_test.py @@ -3,13 +3,61 @@ import pytest import stim 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, +) + + +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") + + +def test_decompose_errors_default_method(): + actual = 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_decompose_errors_strip_undecomposable_errors(): + dem = stim.DetectorErrorModel(""" +detector(0) D0 +detector(1) D1 +error(0.1) D0 D1 +error(0.1) D0 +""") + + actual = 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'." - )