diff --git a/README.md b/README.md index 15331c43..17ba929c 100644 --- a/README.md +++ b/README.md @@ -144,9 +144,78 @@ Using a Detection Event File and Observable Flips: ./tesseract --in events.01 --in-format 01 --obs_in obs.01 --obs-in-format 01 --dem surface_code.dem --out decoded.txt ``` +Tesseract also accepts explicit detector traversal orders in a JSON file. The +file has the same list-of-lists form as Python's `TesseractConfig.det_orders`, +and every inner list must be a complete permutation of the DEM detector IDs: + +```json +[[0, 2, 1, 3], [3, 1, 2, 0]] +``` + +Pass it with `--detector-orders orders.json`. This replaces the orders normally +generated by `--num-det-orders`, `--det-order-seed`, and the `--det-order-*` +method flags. + Tesseract supports reading and writing from all of Stim's standard [output formats](https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md). +### Decoding with GARI + +Generate a GARI matrix DEM from a source circuit: + +```bash +python src/py/_tesseract_py_util/gari.py \ + --circuit circuit_file.stim \ + --prior xor \ + --out-dir gari_output +``` + +This writes `gari_output/circuit_file_gari_xor.dem`. Its physical detector rows preserve the +source detector IDs, and its added virtual rows form a suffix. GARI-aware detector orders can be +written in the generic CLI format when reordering is wanted: + +```python +import json +import stim +from tesseract_decoder import demutil + +circuit = stim.Circuit.from_file("circuit_file.stim") +gari_dem = stim.DetectorErrorModel.from_file("gari_output/circuit_file_gari_xor.dem") +orders = demutil.gari.build_detector_orders(circuit, gari_dem, num_det_orders=5) +with open("gari_orders.json", "w") as f: + json.dump(orders, f) +``` + +Sample from the source circuit and decode with the generated DEM: + +```bash +./bazel-bin/src/tesseract \ + --circuit circuit_file.stim \ + --dem gari_output/circuit_file_gari_xor.dem \ + --detector-orders gari_orders.json \ + --sample-num-shots 100 \ + --sample-seed 1234 \ + --threads 1 \ + --pqlimit 1000000 \ + --beam 5 \ + --beam-climbing \ + --no-revisit-dets \ + --print-stats \ + --stats-out gari-stats.json +``` + +The GARI matrix DEM is a decoding representation and must not be sampled. When a circuit and a +larger DEM are supplied together, both command-line decoders read or sample the circuit's detector +prefix and leave the DEM's virtual suffix zero. Their observable counts must agree. When reading a +source-width event file, pass both `--circuit` and `--dem`; with `--dem` alone the input width is the +full DEM width. + +For matrix analysis, `gari.py --row-order block` writes a `_block.dem` file in the internal +physical-X, physical-Z, virtual-Z, virtual-X row order. This research form does not accept source +syndromes as a direct prefix. See +[GARI transformed matrices](src/py/README.md#gari-transformed-matrices) for supported circuit +conventions and Python API details. + ### Performance Optimization Here are some tips for improving performance: diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index e00ef674..10f6303b 100644 --- a/docs/tutorial.ipynb +++ b/docs/tutorial.ipynb @@ -935,7 +935,7 @@ "source": [ "gari = tesseract_decoder.demutil.gari\n", "\n", - "gari_dem, gari_layout = gari.circuit_to_gari(\n", + "gari_dem = gari.circuit_to_gari(\n", " circuit,\n", " prior_function=gari.tesseract_xor_prior_probabilities,\n", ")" @@ -950,7 +950,8 @@ "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." + "source syndrome into the detector prefix; the added virtual entries stay\n", + "zero." ] }, { @@ -964,34 +965,26 @@ "source": [ "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]" - ] - }, - { - "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." + "gari_dets[:, :dets.shape[1]] = dets[:num_shots]" ] }, { "cell_type": "code", "execution_count": null, - "id": "bc0c802c", + "id": "3e397ee8", "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", + "gari_config = tesseract.TesseractConfig(\n", + " dem=gari_dem,\n", + " det_beam=15,\n", + " beam_climbing=True,\n", + " no_revisit_dets=True,\n", + " pqlimit=200_000,\n", + ")\n", + "gari_decoder = gari_config.compile_decoder()\n", "predicted_observables = gari_decoder.decode_batch(gari_dets)\n", "logical_failures = np.count_nonzero(\n", " np.any(predicted_observables != obs[:num_shots], axis=1)\n", diff --git a/docs/tutorial.py b/docs/tutorial.py index 5f3028e0..57a099c1 100644 --- a/docs/tutorial.py +++ b/docs/tutorial.py @@ -358,7 +358,7 @@ def run_tesseract_decoder(decoder, dets, obs): # %% id="gari-transform-example" gari = tesseract_decoder.demutil.gari -gari_dem, gari_layout = gari.circuit_to_gari( +gari_dem = gari.circuit_to_gari( circuit, prior_function=gari.tesseract_xor_prior_probabilities, ) @@ -366,23 +366,23 @@ def run_tesseract_decoder(decoder, dets, obs): # %% [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. +# source syndrome into the detector prefix; the added virtual entries stay +# zero. # %% id="gari-sample-example" 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 -# ascending detector order, so Tesseract processes the rows in that order. +gari_dets[:, :dets.shape[1]] = dets[:num_shots] # %% 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 +gari_config = tesseract.TesseractConfig( + dem=gari_dem, + det_beam=15, + beam_climbing=True, + no_revisit_dets=True, + pqlimit=200_000, +) +gari_decoder = gari_config.compile_decoder() predicted_observables = gari_decoder.decode_batch(gari_dets) logical_failures = np.count_nonzero( np.any(predicted_observables != obs[:num_shots], axis=1) diff --git a/src/py/README.md b/src/py/README.md index 658932a8..32d97097 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -17,7 +17,7 @@ Explanation of configuration arguments: * `verbose` - A boolean flag that, when `True`, enables verbose logging. This is useful for debugging and understanding the decoder's internal behavior, as it will print information about the search process. * `merge_errors` - A boolean flag that, when `True`, merges error channels with identical syndrome patterns before decoding. This is enabled by default. * `pqlimit` - An integer that sets a limit on the number of nodes in the priority queue. This can be used to constrain the memory usage of the decoder. The default value is `200000`. -* `det_orders` - A list of lists of integers, where each inner list represents an ordering of the detectors. This is used for "ensemble reordering," an optimization that tries different detector orderings to improve the search's convergence. The default is an empty list, meaning a single, fixed ordering is used. +* `det_orders` - A list of complete detector-ID permutations in traversal order. This is used for "ensemble reordering," an optimization that tries different detector orderings to improve the search's convergence. The default is an empty list, meaning a single, fixed ordering is used. * `det_penalty` - A floating-point value that adds a cost for each residual detection event. This encourages the decoder to prioritize paths that resolve more detection events, steering the search towards more complete solutions. The default value is `0.0`, meaning no penalty is applied. * `create_visualization` - A boolean flag that enables decoder visualization output when set to `True`. The default value is `False`. * `sparsify_errors` - Enables per-shot sparse error activation. When enabled, all errors up to `sparsify_base_degree` are always active, and selected higher-degree errors are reactivated per shot. @@ -707,29 +707,34 @@ nice_calibrated_dem = demutil.regeneralize_spatial_dem( #### 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. +circuit into a GARI matrix DEM. 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( +gari_dem = demutil.gari.circuit_to_gari( circuit, prior_function=demutil.gari.tesseract_xor_prior_probabilities, ) ``` -`circuit_to_gari` returns: +The returned DEM preserves the source detector IDs as a prefix and appends the +virtual detector rows. For matrix analysis, +`circuit_to_gari(..., row_order="block")` instead emits the internal +`[physical X, physical Z, virtual Z, virtual X]` row order. This research form +does not accept source syndromes as a direct prefix. -* `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. +`demutil.gari.build_detector_orders(circuit, gari_dem, num_det_orders, ...)` +uses the source circuit to build BFS, coordinate, or index orders and then +appends the virtual detector IDs. The resulting list has the same format as +`TesseractConfig.det_orders` and the Tesseract CLI's `--detector-orders` JSON +file. It applies to the default source-aligned GARI DEM, not the research-only +block form. Related public APIs: @@ -738,13 +743,14 @@ Related public APIs: * `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. + detector mapping into the internal block rows. * `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 +not be sampled. Sample from the original circuit, copy its syndrome into the +beginning of a zero-filled GARI syndrome, and leave the virtual suffix zero. +See the [GARI tutorial](../../docs/tutorial.ipynb) for a complete decoding example. diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index e86fd322..d7a8e8de 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -34,7 +34,8 @@ ``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 +Internally, columns are ordered as +``[e_Z, e_X, e_Y, bar(e)_Z, bar(e)_X]`` and rows as ``[physical X, physical Z, virtual Z, virtual X]``: :: @@ -47,8 +48,12 @@ 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: +By default, serialized GARI DEMs restore the physical rows to the source +detector order and append the virtual rows. A source syndrome can therefore be +copied into the beginning of a zero-filled GARI syndrome. The block row order +above remains available for research use. + +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 DEM is only a matrix storage and decoding representation. It is not a physical detector error model @@ -61,9 +66,17 @@ 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 -experiment-specific and is not implemented by this generic transform. +For a single-basis CSS memory experiment, the paper's message-passing decoder +evaluates the logical result using the barred variable aligned with the +relevant component matrix. For logical-Z memory it uses ``bar(e)_X`` and tests +convergence with ``D_Z bar(e)_X = s_Z``; for logical-X memory it analogously +uses ``bar(e)_Z`` and ``D_X bar(e)_Z = s_X``. This convention can also be +useful for downstream BP-style decoders. + +This module does not select a BP/message-passing convergence rule or move +logical targets to the barred variables. It emits the generic logical map +``[L_eZ, L_eX, L_eY, 0, 0]``, keeping logical targets on the original error +variables. 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 @@ -75,7 +88,6 @@ from __future__ import annotations import dataclasses -import json from collections.abc import Callable, Sequence from pathlib import Path @@ -395,6 +407,8 @@ def _gari_transform( dtype=np.uint8, ) + # Emit the generic [L_eZ, L_eX, L_eY, 0, 0] map. Barred-variable logical + # output and component convergence are choices for a downstream decoder. augmented_logicals = scipy.sparse.hstack( [ source_logicals[:, e_z_columns], @@ -604,6 +618,7 @@ def _build_gari_dem( source_probabilities: np.ndarray, *, prior_function: Callable[[GariTransform, np.ndarray], np.ndarray], + row_order: str, ) -> stim.DetectorErrorModel: """Builds a GARI DEM using an explicit prior policy. @@ -640,23 +655,38 @@ def validated_probabilities( transform.checks.shape[1], "prior_function probabilities", ) - return _matrices_to_gari_dem( - transform.checks, transform.logicals, gari_probabilities - ) + if row_order == "source": + source_detector_count = len(transform.source_to_gari_detectors) + rows = np.concatenate( + [ + transform.source_to_gari_detectors, + np.arange(source_detector_count, transform.checks.shape[0]), + ] + ) + checks = transform.checks[rows, :] + elif row_order == "block": + checks = transform.checks + else: + raise ValueError("row_order must be 'source' or 'block'.") + return _matrices_to_gari_dem(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 a supported CSS circuit into a GARI matrix DEM and v1 layout. + row_order: str = "source", +) -> stim.DetectorErrorModel: + """Converts a supported CSS circuit into a GARI matrix DEM. 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. + detectors. By default, source detector IDs are preserved and virtual + detector rows are appended. ``row_order="block"`` instead emits physical + X, physical Z, virtual Z, and virtual X row blocks for research use. 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) @@ -669,48 +699,75 @@ def circuit_to_gari( x_detectors=x_detectors, z_detectors=z_detectors, ) - gari_dem = _build_gari_dem( - transform, probabilities, prior_function=prior_function + return _build_gari_dem( + transform, + probabilities, + prior_function=prior_function, + row_order=row_order, ) - 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 -def call_gari(circuit_fname: str, prior_name: str, output_dir: str) -> None: - """Converts one circuit and writes its GARI DEM and layout files.""" +def build_detector_orders( + circuit: stim.Circuit, + gari_dem: stim.DetectorErrorModel, + num_det_orders: int, + *, + method: object | None = None, + seed: int = 0, +) -> list[list[int]]: + """Builds traversal orders for a source-aligned GARI DEM.""" + from tesseract_decoder import utils + + source_dem = _circuit_to_gari_source_dem(circuit) + source_detector_count = source_dem.num_detectors + if gari_dem.num_detectors < source_detector_count: + raise ValueError("The GARI DEM has fewer detectors than the source circuit.") + if method is None: + method = utils.DetOrder.DetIndex + virtual_detectors = list( + range(source_detector_count, gari_dem.num_detectors) + ) + source_positions = utils.build_det_orders( + source_dem, num_det_orders, method=method, seed=seed + ) + return [ + sorted(range(source_detector_count), key=positions.__getitem__) + + virtual_detectors + for positions in source_positions + ] + + +def call_gari( + circuit_fname: str, + prior_name: str, + output_dir: str, + *, + row_order: str = "source", +) -> None: + """Converts one circuit and writes its GARI matrix DEM.""" 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( + gari_dem = circuit_to_gari( stim.Circuit.from_file(circuit_fname), prior_function=prior_function, + row_order=row_order, ) output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) output_name = f"{Path(circuit_fname).stem}_gari_{prior_name.replace('-', '_')}" + if row_order == "block": + output_name += "_block" 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." - ) + description="Convert one Stim circuit into a GARI matrix DEM." ) parser.add_argument( "--circuit", required=True, help="Input Stim circuit file." @@ -726,12 +783,25 @@ def main() -> None: required=True, help=( "Output directory, created if needed. Files are named " - "_gari_.dem and " - "_gari__layout.json." + "_gari_.dem." + ), + ) + parser.add_argument( + "--row-order", + choices=("source", "block"), + default="source", + help=( + "Use source detector IDs followed by virtual rows (default), or " + "emit research-only GARI row blocks with a _block.dem suffix." ), ) args = parser.parse_args() - call_gari(args.circuit, args.prior, args.out_dir) + call_gari( + args.circuit, + args.prior, + args.out_dir, + row_order=args.row_order, + ) if __name__ == "__main__": diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 530adaf8..eedf7b5a 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -12,14 +12,12 @@ # 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 +from tesseract_decoder import demutil, utils def _tiny_circuit(): @@ -29,10 +27,10 @@ def _tiny_circuit(): 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] - DETECTOR(0, 0, 0, 2) rec[-4] + DETECTOR(0, 0, 0, 0) rec[-6] DETECTOR(0, 0, 0, 4) rec[-3] + DETECTOR(0, 0, 0, 2) rec[-4] OBSERVABLE_INCLUDE(0) rec[-2] OBSERVABLE_INCLUDE(1) rec[-1] """) @@ -105,7 +103,7 @@ def test_tiny_transform(): [[1, 0, 1, 0, 0], [0, 1, 0, 0, 0]], ) np.testing.assert_array_equal( - transform.source_to_gari_detectors, [0, 2, 1, 3] + transform.source_to_gari_detectors, [2, 0, 3, 1] ) @@ -148,6 +146,7 @@ def test_prior_probabilities_and_gari_dem_round_trip(): transform, source_probabilities, prior_function=gari.tesseract_xor_prior_probabilities, + row_order="block", ) checks, logicals, probabilities = gari.dem_to_matrices(gari_dem) assert gari_dem.num_detectors == transform.checks.shape[0] @@ -160,19 +159,41 @@ def test_prior_probabilities_and_gari_dem_round_trip(): 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( + gari_dem = 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 + _, transform = _tiny_model() + checks, _, _ = gari.dem_to_matrices(gari_dem) + np.testing.assert_array_equal( + checks.toarray(), transform.checks[[2, 0, 3, 1, 4, 5], :].toarray() + ) + source_positions = utils.build_det_orders( + gari._circuit_to_gari_source_dem(circuit), + 2, + method=utils.DetOrder.DetCoordinate, + seed=0, + ) + assert public_gari.build_detector_orders( + circuit, + gari_dem, + 2, + method=utils.DetOrder.DetCoordinate, + seed=0, + ) == [ + sorted(range(4), key=positions.__getitem__) + [4, 5] + for positions in source_positions + ] + + block_dem = public_gari.circuit_to_gari( + circuit, + prior_function=public_gari.tesseract_xor_prior_probabilities, + row_order="block", + ) + block_checks, _, _ = gari.dem_to_matrices(block_dem) + assert (block_checks != transform.checks).nnz == 0 circuit_path = tmp_path / "tiny.stim" circuit.to_file(circuit_path) @@ -182,13 +203,11 @@ def test_public_circuit_conversion_and_file_output(tmp_path): 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" - ) + public_gari.call_gari( + str(circuit_path), "xor", str(output_dir), row_order="block" ) assert str(written_dem) == str(gari_dem) - assert written_layout == layout + assert (output_dir / f"{output_name}_block.dem").is_file() if __name__ == "__main__": diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 7c8542a8..d8ece509 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -170,6 +170,17 @@ struct Args { config.merge_errors = !no_merge_errors; + size_t shot_detector_count = config.dem.count_detectors(); + if (!circuit_path.empty()) { + shot_detector_count = circuit.count_detectors(); + if (shot_detector_count > config.dem.count_detectors()) { + throw std::invalid_argument("Circuit detector count exceeds DEM detector count."); + } + if (circuit.count_observables() != config.dem.count_observables()) { + throw std::invalid_argument("Circuit and DEM observable counts differ."); + } + } + if (sample_num_shots > 0) { assert(!circuit_path.empty()); std::mt19937_64 rng(sample_seed); @@ -196,7 +207,7 @@ struct Args { } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); auto reader = stim::MeasureRecordReader::make( - shots_file, shots_in_format.id, 0, config.dem.count_detectors(), + shots_file, shots_in_format.id, 0, shot_detector_count, append_observables * config.dem.count_observables()); // Load the shots from a file diff --git a/src/tesseract.cc b/src/tesseract.cc index 0a180754..a3ef098b 100644 --- a/src/tesseract.cc +++ b/src/tesseract.cc @@ -174,10 +174,19 @@ TesseractDecoder::TesseractDecoder(TesseractConfig config_) : config(std::move(c config.det_orders.emplace_back(config.dem.count_detectors()); std::iota(config.det_orders[0].begin(), config.det_orders[0].end(), 0); } else { - for (size_t i = 0; i < config.det_orders.size(); ++i) { - if (config.det_orders[i].size() != config.dem.count_detectors()) { + const size_t num_detectors = config.dem.count_detectors(); + for (const auto& order : config.det_orders) { + if (order.size() != num_detectors) { throw std::invalid_argument( - "Each detector order list must have a size equal to the number of detectors."); + "Each detector order must be a complete permutation of the detector IDs."); + } + std::vector seen(num_detectors); + for (size_t detector : order) { + if (detector >= num_detectors || seen[detector]) { + throw std::invalid_argument( + "Each detector order must be a complete permutation of the detector IDs."); + } + seen[detector] = true; } } } diff --git a/src/tesseract.pybind.h b/src/tesseract.pybind.h index 19a79fd9..316123d8 100644 --- a/src/tesseract.pybind.h +++ b/src/tesseract.pybind.h @@ -116,8 +116,8 @@ void add_tesseract_module(py::module& root) { pqlimit : int, default=max_size_t The maximum size of the priority queue. det_orders : list[list[int]], default=empty - A list of detector orderings to use for decoding. If empty, the decoder - will generate its own orderings. + Detector IDs in traversal order. Each inner list must be a complete + permutation. If empty, the decoder generates its own orderings. det_penalty : float, default=0.0 A penalty value added to the cost of each detector visited. create_visualization: bool, defualt=False @@ -159,8 +159,8 @@ void add_tesseract_module(py::module& root) { pqlimit : int, default=max_size_t The maximum size of the priority queue. det_orders : list[list[int]], default=empty - A list of detector orderings to use for decoding. If empty, the decoder - will generate its own orderings. + Detector IDs in traversal order. Each inner list must be a complete + permutation. If empty, the decoder generates its own orderings. det_penalty : float, default=0.0 A penalty value added to the cost of each detector visited. create_visualization: bool, defualt=False @@ -190,7 +190,7 @@ void add_tesseract_module(py::module& root) { .def_readwrite("pqlimit", &TesseractConfig::pqlimit, "The maximum size of the priority queue.") .def_readwrite("det_orders", &TesseractConfig::det_orders, - "A list of pre-specified detector orderings.") + "Complete detector-ID permutations in traversal order.") .def_readwrite("det_penalty", &TesseractConfig::det_penalty, "The penalty cost added for each detector.") .def_readwrite("create_visualization", &TesseractConfig::create_visualization, diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index fd0d471e..abd2cbca 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -14,6 +14,7 @@ #include "tesseract.h" +#include #include #include #include @@ -559,3 +560,37 @@ TEST(tesseract, MoreThan64Observables) { ASSERT_EQ(flipped[i], i); } } + +TEST(utils, GariSourcePrefixB8Decodes) { + stim::DetectorErrorModel gari_dem(R"DEM( + error(0.1) D0 D3 L0 + error(0.1) D1 D6 L1 + detector D9 + )DEM"); + + FILE* file = std::tmpfile(); + ASSERT_NE(file, nullptr); + std::fputc('\x09', file); // D0 D3. + std::fputc('\x42', file); // D1 D6. + std::rewind(file); + auto format = stim::format_name_to_enum_map().at("b8"); + auto reader = stim::MeasureRecordReader::make(file, format.id, 0, 7, 0); + std::vector shots; + stim::SparseShot shot; + while (reader->start_and_read_entire_record(shot)) { + shots.push_back(shot); + shot.clear(); + } + fclose(file); + + ASSERT_EQ(shots.size(), 2); + EXPECT_EQ(shots[0].hits, (std::vector{0, 3})); + EXPECT_EQ(shots[1].hits, (std::vector{1, 6})); + + TesseractDecoder tesseract(TesseractConfig{gari_dem}); + SimplexDecoder simplex(SimplexConfig{gari_dem}); + EXPECT_EQ(tesseract.decode(shots[0].hits), (std::vector{0})); + EXPECT_EQ(tesseract.decode(shots[1].hits), (std::vector{1})); + EXPECT_EQ(simplex.decode(shots[0].hits), (std::vector{0})); + EXPECT_EQ(simplex.decode(shots[1].hits), (std::vector{1})); +} diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index 4b3975fc..5597dcb0 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -40,6 +40,7 @@ struct Args { bool det_order_bfs = false; bool det_order_index = false; bool det_order_coordinate = false; + std::string detector_orders_path; // Sampling options size_t sample_num_shots = 0; @@ -103,6 +104,12 @@ struct Args { throw std::invalid_argument( "Only one of --det-order-bfs, --det-order-index, or --det-order-coordinate may be set."); } + if (!detector_orders_path.empty() && + (det_order_flags > 0 || program.is_used("--num-det-orders") || + program.is_used("--det-order-seed"))) { + throw std::invalid_argument( + "--detector-orders cannot be combined with generated detector-order options."); + } int num_data_sources = int(sample_num_shots > 0) + int(!in_fname.empty()); if (num_data_sources != 1) { @@ -211,7 +218,18 @@ struct Args { config.merge_errors = !no_merge_errors; - // Sample orientations of the error model to use for the det priority + size_t shot_detector_count = config.dem.count_detectors(); + if (!circuit_path.empty()) { + shot_detector_count = circuit.count_detectors(); + if (shot_detector_count > config.dem.count_detectors()) { + throw std::invalid_argument("Circuit detector count exceeds DEM detector count."); + } + if (circuit.count_observables() != config.dem.count_observables()) { + throw std::invalid_argument("Circuit and DEM observable counts differ."); + } + } + + // Choose the detector traversal orders. { if (verbose) { auto detector_coords = get_detector_coords(config.dem); @@ -225,15 +243,37 @@ struct Args { std::cout << ")" << std::endl; } } - DetOrder order = DetOrder::DetIndex; - if (det_order_bfs) { - order = DetOrder::DetBFS; - } else if (det_order_index) { - order = DetOrder::DetIndex; - } else if (det_order_coordinate) { - order = DetOrder::DetCoordinate; + if (!detector_orders_path.empty()) { + std::ifstream input(detector_orders_path); + if (!input.is_open()) { + throw std::invalid_argument("Could not open the file: " + detector_orders_path); + } + try { + nlohmann::json orders = nlohmann::json::parse(input); + if (!orders.is_array() || orders.empty()) { + throw std::invalid_argument("Detector orders file must contain at least one order."); + } + for (const auto& order : orders) { + if (!order.is_array() || + !std::all_of(order.begin(), order.end(), + [](const auto& detector) { return detector.is_number_unsigned(); })) { + throw std::invalid_argument( + "Detector orders file must contain arrays of nonnegative integers."); + } + } + config.det_orders = orders.get>>(); + } catch (const nlohmann::json::exception& ex) { + throw std::invalid_argument("Invalid detector orders file: " + std::string(ex.what())); + } + } else { + DetOrder order = DetOrder::DetIndex; + if (det_order_bfs) { + order = DetOrder::DetBFS; + } else if (det_order_coordinate) { + order = DetOrder::DetCoordinate; + } + config.det_orders = build_det_orders(config.dem, num_det_orders, order, det_order_seed); } - config.det_orders = build_det_orders(config.dem, num_det_orders, order, det_order_seed); } if (sample_num_shots > 0) { @@ -262,7 +302,7 @@ struct Args { } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); auto reader = stim::MeasureRecordReader::make( - shots_file, shots_in_format.id, 0, config.dem.count_detectors(), + shots_file, shots_in_format.id, 0, shot_detector_count, append_observables * config.dem.count_observables()); // Load the shots from a file @@ -376,6 +416,12 @@ int main(int argc, char* argv[]) { .metavar("N") .default_value(static_cast(518278944)) .store_into(args.det_order_seed); + program.add_argument("--detector-orders") + .help( + "JSON file containing one or more detector-ID permutations, in the same format as " + "TesseractConfig.det_orders") + .metavar("FILE") + .store_into(args.detector_orders_path); program.add_argument("--sample-num-shots") .help( "If provided, will sample the requested number of shots from the " @@ -665,8 +711,9 @@ int main(int argc, char* argv[]) { {"beam_climbing", args.beam_climbing}, {"no_revisit_dets", args.no_revisit_dets}, {"pqlimit", args.pqlimit}, - {"num_det_orders", args.num_det_orders}, + {"num_det_orders", config.det_orders.empty() ? 1 : config.det_orders.size()}, {"det_order_seed", args.det_order_seed}, + {"detector_orders_path", args.detector_orders_path}, {"total_time_seconds", total_time_seconds}, {"num_errors", has_obs ? nlohmann::json(num_errors) : nullptr}, {"num_low_confidence", num_low_confidence},