From 647c3f50f2b46790406e6d42ea0651be02bf9abf Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sat, 25 Jul 2026 22:18:12 -0700 Subject: [PATCH 01/31] Add --gari-layout support to Tesseract CLI --- src/py/BUILD | 10 +- src/py/gari_cli_test.py | 206 +++++++++++++++++++++++++++++++++++++ src/tesseract_main.cc | 221 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 432 insertions(+), 5 deletions(-) create mode 100644 src/py/gari_cli_test.py diff --git a/src/py/BUILD b/src/py/BUILD index 7160760f..1b407320 100644 --- a/src/py/BUILD +++ b/src/py/BUILD @@ -93,7 +93,15 @@ py_test( imports = ["..", "."], ) - +py_test( + name = "gari_cli_test", + srcs = ["gari_cli_test.py"], + args = ["$(location //src:tesseract)"], + data = ["//src:tesseract"], + size = "medium", + visibility = ["//:__subpackages__"], + deps = ["@pypi//pytest"], +) py_test( name = "stub_test", diff --git a/src/py/gari_cli_test.py b/src/py/gari_cli_test.py new file mode 100644 index 00000000..c7049898 --- /dev/null +++ b/src/py/gari_cli_test.py @@ -0,0 +1,206 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from pathlib import Path +import subprocess +import sys + +import pytest + + +_TESSERACT = Path(sys.argv[1]) +_SCHEMA = "tesseract.gari_layout.v1" + + +@pytest.fixture +def gari_files(tmp_path): + circuit = tmp_path / "source.stim" + circuit.write_text( + """\ +X_ERROR(1) 0 3 +M 0 1 2 3 +DETECTOR rec[-4] +DETECTOR rec[-3] +DETECTOR rec[-2] +DETECTOR rec[-1] +OBSERVABLE_INCLUDE(0) rec[-4] +""", + encoding="utf-8", + ) + dem = tmp_path / "target.dem" + dem.write_text( + """\ +error(0.1) D0 +error(0.1) D1 L0 +error(0.1) D2 +error(0.1) D3 +error(0.1) D4 +error(0.1) D5 +""", + encoding="utf-8", + ) + shots = tmp_path / "source.01" + shots.write_text("1001\n", encoding="utf-8") + layout = tmp_path / "target-layout.json" + layout_data = { + "schema": _SCHEMA, + "source_detector_count": 4, + "gari_detector_count": 6, + "source_to_gari": [2, 0, 3, 1], + } + layout.write_text(json.dumps(layout_data), encoding="utf-8") + return { + "circuit": circuit, + "dem": dem, + "shots": shots, + "layout": layout, + "layout_data": layout_data, + "tmp_path": tmp_path, + } + + +def _run_tesseract(*args): + return subprocess.run( + [_TESSERACT, "--threads", "1", *map(str, args)], + capture_output=True, + text=True, + timeout=30, + check=False, + ) + + +def _decode(gari_files, *source_args): + output = gari_files["tmp_path"] / "predictions.01" + result = _run_tesseract( + "--dem", + gari_files["dem"], + "--gari-layout", + gari_files["layout"], + *source_args, + "--out", + output, + "--out-format", + "01", + "--num-det-orders", + "1", + "--det-order-index", + ) + assert result.returncode == 0, result.stderr + return output.read_text(encoding="utf-8") + + +def _write_layout(gari_files, data): + path = gari_files["tmp_path"] / "invalid-layout.json" + path.write_text(json.dumps(data), encoding="utf-8") + return path + + +def _assert_failure(result, expected, path=None): + assert result.returncode != 0 + output = result.stdout + result.stderr + assert expected in output + if path is not None: + assert str(path) in output + + +def test_tesseract_maps_sampled_and_01_shots(gari_files): + assert _decode( + gari_files, + "--circuit", + gari_files["circuit"], + "--sample-num-shots", + "1", + "--sample-seed", + "0", + ) == "1\n" + assert _decode( + gari_files, + "--in", + gari_files["shots"], + "--in-format", + "01", + ) == "1\n" + + +@pytest.mark.parametrize( + ("changes", "expected"), + [ + ({"schema": "wrong.schema"}, "field 'schema'"), + ({"source_to_gari": [2, 0, 3]}, "must contain 4 entries"), + ({"source_to_gari": [2, 0, 6, 1]}, "outside the target range"), + ({"source_to_gari": [2, 0, 2, 1]}, "injective mapping"), + ({"gari_detector_count": 7}, "but DEM"), + ], +) +def test_tesseract_rejects_invalid_layouts(gari_files, changes, expected): + data = {**gari_files["layout_data"], **changes} + layout = _write_layout(gari_files, data) + result = _run_tesseract( + "--dem", + gari_files["dem"], + "--gari-layout", + layout, + "--in", + gari_files["shots"], + "--in-format", + "01", + ) + _assert_failure(result, expected, layout) + + +def test_tesseract_rejects_source_and_no_layout_count_mismatches(gari_files): + data = { + **gari_files["layout_data"], + "source_detector_count": 3, + "source_to_gari": [2, 0, 3], + } + layout = _write_layout(gari_files, data) + result = _run_tesseract( + "--circuit", + gari_files["circuit"], + "--dem", + gari_files["dem"], + "--gari-layout", + layout, + "--sample-num-shots", + "1", + ) + _assert_failure(result, "but circuit", layout) + + result = _run_tesseract( + "--circuit", + gari_files["circuit"], + "--dem", + gari_files["dem"], + "--sample-num-shots", + "1", + ) + _assert_failure(result, "Supply --gari-layout") + + +def test_tesseract_requires_dem_with_layout(gari_files): + result = _run_tesseract( + "--circuit", + gari_files["circuit"], + "--gari-layout", + gari_files["layout"], + "--sample-num-shots", + "1", + ) + _assert_failure(result, "--gari-layout requires --dem") + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__])) diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index 1882fd09..cd8ebb6e 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -21,17 +21,160 @@ #include #include #include +#include #include #include +#include #include "common.h" #include "stim.h" #include "tesseract.h" #include "utils.h" +namespace { + +constexpr char kGariLayoutSchema[] = "tesseract.gari_layout.v1"; + +struct GariLayout { + std::string schema; + size_t source_detector_count; + size_t gari_detector_count; + std::vector source_to_gari; +}; + +std::invalid_argument gari_layout_error(const std::string& path, const std::string& detail) { + return std::invalid_argument("Invalid GARI layout '" + path + "': " + detail); +} + +const nlohmann::json& required_layout_field(const nlohmann::json& document, const std::string& path, + const char* field) { + auto item = document.find(field); + if (item == document.end()) { + throw gari_layout_error(path, "missing required field '" + std::string(field) + "'."); + } + return *item; +} + +size_t read_layout_size(const nlohmann::json& value, const std::string& path, + const std::string& field) { + if (!value.is_number_integer()) { + throw gari_layout_error( + path, "field '" + field + "' must be an integer, but is " + value.type_name() + "."); + } + + int64_t signed_value; + if (value.is_number_unsigned()) { + uint64_t unsigned_value = value.get(); + if (unsigned_value > static_cast(std::numeric_limits::max())) { + throw gari_layout_error(path, "field '" + field + "' is too large."); + } + signed_value = static_cast(unsigned_value); + } else { + signed_value = value.get(); + } + if (signed_value < 0) { + throw gari_layout_error(path, "field '" + field + "' must be nonnegative, but is " + + std::to_string(signed_value) + "."); + } + if (static_cast(signed_value) > std::numeric_limits::max()) { + throw gari_layout_error(path, "field '" + field + "' is too large."); + } + return static_cast(signed_value); +} + +GariLayout load_gari_layout(const std::string& path) { + std::ifstream input(path); + if (!input.is_open()) { + throw std::invalid_argument("Could not open GARI layout: " + path); + } + + nlohmann::json document; + try { + input >> document; + } catch (const nlohmann::json::exception& err) { + throw gari_layout_error(path, "could not parse JSON: " + std::string(err.what())); + } + if (!document.is_object()) { + throw gari_layout_error(path, "top-level JSON value must be an object."); + } + + const nlohmann::json& schema_json = required_layout_field(document, path, "schema"); + if (!schema_json.is_string()) { + throw gari_layout_error(path, "field 'schema' must be a string."); + } + std::string schema = schema_json.get(); + if (schema != kGariLayoutSchema) { + throw gari_layout_error(path, "field 'schema' must be '" + std::string(kGariLayoutSchema) + + "', but is '" + schema + "'."); + } + + size_t source_detector_count = + read_layout_size(required_layout_field(document, path, "source_detector_count"), path, + "source_detector_count"); + size_t gari_detector_count = read_layout_size( + required_layout_field(document, path, "gari_detector_count"), path, "gari_detector_count"); + if (gari_detector_count < source_detector_count) { + throw gari_layout_error(path, "field 'gari_detector_count' must be at least " + + std::to_string(source_detector_count) + ", but is " + + std::to_string(gari_detector_count) + "."); + } + + const nlohmann::json& mapping_json = required_layout_field(document, path, "source_to_gari"); + if (!mapping_json.is_array()) { + throw gari_layout_error(path, "field 'source_to_gari' must be an array."); + } + if (mapping_json.size() != source_detector_count) { + throw gari_layout_error(path, "field 'source_to_gari' must contain " + + std::to_string(source_detector_count) + " entries, but has " + + std::to_string(mapping_json.size()) + "."); + } + + std::vector source_to_gari; + source_to_gari.reserve(source_detector_count); + std::unordered_map target_to_source; + for (size_t source = 0; source < mapping_json.size(); ++source) { + std::string field = "source_to_gari[" + std::to_string(source) + "]"; + size_t target = read_layout_size(mapping_json[source], path, field); + if (target >= gari_detector_count) { + throw gari_layout_error(path, "field '" + field + "' is " + std::to_string(target) + + ", outside the target range [0, " + + std::to_string(gari_detector_count) + ")."); + } + auto [previous, inserted] = target_to_source.emplace(target, source); + if (!inserted) { + throw gari_layout_error(path, "source detectors " + std::to_string(previous->second) + + " and " + std::to_string(source) + + " both map to GARI detector " + std::to_string(target) + + "; v1 layouts require an injective mapping."); + } + source_to_gari.push_back(target); + } + return {schema, source_detector_count, gari_detector_count, std::move(source_to_gari)}; +} + +std::vector map_gari_hits(std::vector source_hits, const GariLayout& layout, + const std::string& path) { + for (uint64_t& source : source_hits) { + if (source >= layout.source_to_gari.size()) { + throw gari_layout_error(path, "source detector index " + std::to_string(source) + + " is outside the source range [0, " + + std::to_string(layout.source_detector_count) + ")."); + } + source = layout.source_to_gari[source]; + } + std::sort(source_hits.begin(), source_hits.end()); + return source_hits; +} + +} // namespace + struct Args { std::string circuit_path; std::string dem_path; + std::string gari_layout_path; + std::string gari_layout_schema; + size_t gari_source_detector_count = 0; + size_t gari_detector_count = 0; bool no_merge_errors = false; // Manifold orientation options @@ -97,6 +240,9 @@ struct Args { if (circuit_path.empty() and dem_path.empty()) { throw std::invalid_argument("Must provide at least one of --circuit or --dem"); } + if (!gari_layout_path.empty() and dem_path.empty()) { + throw std::invalid_argument("--gari-layout requires --dem."); + } int det_order_flags = int(det_order_bfs) + int(det_order_index) + int(det_order_coordinate); if (det_order_flags > 1) { @@ -211,6 +357,39 @@ struct Args { config.merge_errors = !no_merge_errors; + std::optional gari_layout; + if (!gari_layout_path.empty()) { + gari_layout = load_gari_layout(gari_layout_path); + size_t target_count = config.dem.count_detectors(); + if (target_count != gari_layout->gari_detector_count) { + throw gari_layout_error( + gari_layout_path, "field 'gari_detector_count' is " + + std::to_string(gari_layout->gari_detector_count) + ", but DEM '" + + dem_path + "' contains " + std::to_string(target_count) + + " detectors."); + } + if (!circuit_path.empty()) { + size_t source_count = circuit.count_detectors(); + if (source_count != gari_layout->source_detector_count) { + throw gari_layout_error(gari_layout_path, + "field 'source_detector_count' is " + + std::to_string(gari_layout->source_detector_count) + + ", but circuit '" + circuit_path + "' contains " + + std::to_string(source_count) + " detectors."); + } + } + gari_layout_schema = gari_layout->schema; + gari_source_detector_count = gari_layout->source_detector_count; + gari_detector_count = gari_layout->gari_detector_count; + } else if (!circuit_path.empty() and !dem_path.empty() and + circuit.count_detectors() != config.dem.count_detectors()) { + throw std::invalid_argument( + "Circuit '" + circuit_path + "' contains " + std::to_string(circuit.count_detectors()) + + " detectors, but DEM '" + dem_path + "' contains " + + std::to_string(config.dem.count_detectors()) + + ". Supply --gari-layout when the source and target detector layouts differ."); + } + // Sample orientations of the error model to use for the det priority { if (verbose) { @@ -246,11 +425,19 @@ struct Args { shots.resize(sample_num_shots); for (size_t k = 0; k < sample_num_shots; k++) { shots[k].obs_mask = obs_T[k]; + std::vector source_hits; for (size_t d = 0; d < num_detectors; d++) { if (dets[d][k]) { - shots[k].hits.push_back(d); + source_hits.push_back(d); } } + if (gari_layout) { + // The GARI matrix augments the physical syndrome with zero-valued virtual constraints. + // Sparse shots contain mapped physical hits only, so virtual rows remain zero. + shots[k].hits = map_gari_hits(std::move(source_hits), *gari_layout, gari_layout_path); + } else { + shots[k].hits = std::move(source_hits); + } } } @@ -261,14 +448,20 @@ struct Args { throw std::invalid_argument("Could not open the file: " + in_fname); } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); + size_t source_detector_count = + gari_layout ? gari_layout->source_detector_count : config.dem.count_detectors(); auto reader = stim::MeasureRecordReader::make( - shots_file, shots_in_format.id, 0, config.dem.count_detectors(), + shots_file, shots_in_format.id, 0, source_detector_count, append_observables * config.dem.count_observables()); // Load the shots from a file stim::SparseShot sparse_shot; sparse_shot.clear(); while (reader->start_and_read_entire_record(sparse_shot)) { + if (gari_layout) { + sparse_shot.hits = + map_gari_hits(std::move(sparse_shot.hits), *gari_layout, gari_layout_path); + } shots.push_back(sparse_shot); sparse_shot.clear(); } @@ -347,6 +540,14 @@ int main(int argc, char* argv[]) { Args args; program.add_argument("--circuit").help("Stim circuit file path").store_into(args.circuit_path); program.add_argument("--dem").help("Stim dem file path").store_into(args.dem_path); + program.add_argument("--gari-layout") + .help( + "JSON layout emitted by gari_convert. Maps detector data from the original circuit or " + "source shot file into the supplied GARI matrix file. Unmapped virtual detector " + "rows are treated as zero.") + .metavar("FILE") + .default_value(std::string("")) + .store_into(args.gari_layout_path); program.add_argument("--no-merge-errors") .help("If provided, will not merge identical error mechanisms.") .store_into(args.no_merge_errors); @@ -542,11 +743,16 @@ int main(int argc, char* argv[]) { std::cerr << program; return EXIT_FAILURE; } - args.validate(program); TesseractConfig config; std::vector shots; std::unique_ptr writer; - args.extract(config, shots, writer); + try { + args.validate(program); + args.extract(config, shots, writer); + } catch (const std::exception& err) { + std::cerr << err.what() << std::endl; + return EXIT_FAILURE; + } size_t num_observables = config.dem.count_observables(); std::vector> obs_predicted(shots.size(), stim::simd_bits<64>(num_observables)); @@ -680,6 +886,13 @@ int main(int argc, char* argv[]) { {"sparsify_max_degree", args.sparsify_max_degree}, {"sparsify_reactivate_limit", effective_sparsify_reactivate_limit}}; + if (!args.gari_layout_path.empty()) { + stats_json["gari_layout_path"] = args.gari_layout_path; + stats_json["gari_layout_schema"] = args.gari_layout_schema; + stats_json["source_detector_count"] = args.gari_source_detector_count; + stats_json["gari_detector_count"] = args.gari_detector_count; + } + if (args.stats_out_fname == "-") { std::cout << stats_json << std::endl; print_final_stats = false; From e022ceb508f5ff2e72f8a2a7f2c66d5746ca9277 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sat, 25 Jul 2026 22:23:57 -0700 Subject: [PATCH 02/31] Add --gari-layout support to Simplex CLI --- src/py/BUILD | 10 +- src/py/gari_cli_test.py | 46 ++++++--- src/simplex_main.cc | 223 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 257 insertions(+), 22 deletions(-) diff --git a/src/py/BUILD b/src/py/BUILD index 1b407320..94730811 100644 --- a/src/py/BUILD +++ b/src/py/BUILD @@ -96,8 +96,14 @@ py_test( py_test( name = "gari_cli_test", srcs = ["gari_cli_test.py"], - args = ["$(location //src:tesseract)"], - data = ["//src:tesseract"], + args = [ + "$(location //src:tesseract)", + "$(location //src:simplex)", + ], + data = [ + "//src:simplex", + "//src:tesseract", + ], size = "medium", visibility = ["//:__subpackages__"], deps = ["@pypi//pytest"], diff --git a/src/py/gari_cli_test.py b/src/py/gari_cli_test.py index c7049898..f3389177 100644 --- a/src/py/gari_cli_test.py +++ b/src/py/gari_cli_test.py @@ -20,7 +20,7 @@ import pytest -_TESSERACT = Path(sys.argv[1]) +_DECODERS = tuple(Path(argument) for argument in sys.argv[1:]) _SCHEMA = "tesseract.gari_layout.v1" @@ -71,9 +71,17 @@ def gari_files(tmp_path): } -def _run_tesseract(*args): +@pytest.fixture(params=_DECODERS, ids=lambda decoder: decoder.name) +def decoder(request): + return request.param + + +def _run_decoder(decoder, *args): + decoder_args = [decoder, "--threads", "1"] + if decoder.name == "tesseract": + decoder_args.extend(["--num-det-orders", "1", "--det-order-index"]) return subprocess.run( - [_TESSERACT, "--threads", "1", *map(str, args)], + [*decoder_args, *map(str, args)], capture_output=True, text=True, timeout=30, @@ -81,9 +89,10 @@ def _run_tesseract(*args): ) -def _decode(gari_files, *source_args): +def _decode(decoder, gari_files, *source_args): output = gari_files["tmp_path"] / "predictions.01" - result = _run_tesseract( + result = _run_decoder( + decoder, "--dem", gari_files["dem"], "--gari-layout", @@ -93,9 +102,6 @@ def _decode(gari_files, *source_args): output, "--out-format", "01", - "--num-det-orders", - "1", - "--det-order-index", ) assert result.returncode == 0, result.stderr return output.read_text(encoding="utf-8") @@ -115,8 +121,9 @@ def _assert_failure(result, expected, path=None): assert str(path) in output -def test_tesseract_maps_sampled_and_01_shots(gari_files): +def test_decoders_map_sampled_and_01_shots(decoder, gari_files): assert _decode( + decoder, gari_files, "--circuit", gari_files["circuit"], @@ -126,6 +133,7 @@ def test_tesseract_maps_sampled_and_01_shots(gari_files): "0", ) == "1\n" assert _decode( + decoder, gari_files, "--in", gari_files["shots"], @@ -144,10 +152,11 @@ def test_tesseract_maps_sampled_and_01_shots(gari_files): ({"gari_detector_count": 7}, "but DEM"), ], ) -def test_tesseract_rejects_invalid_layouts(gari_files, changes, expected): +def test_decoders_reject_invalid_layouts(decoder, gari_files, changes, expected): data = {**gari_files["layout_data"], **changes} layout = _write_layout(gari_files, data) - result = _run_tesseract( + result = _run_decoder( + decoder, "--dem", gari_files["dem"], "--gari-layout", @@ -160,14 +169,17 @@ def test_tesseract_rejects_invalid_layouts(gari_files, changes, expected): _assert_failure(result, expected, layout) -def test_tesseract_rejects_source_and_no_layout_count_mismatches(gari_files): +def test_decoders_reject_source_and_no_layout_count_mismatches( + decoder, gari_files +): data = { **gari_files["layout_data"], "source_detector_count": 3, "source_to_gari": [2, 0, 3], } layout = _write_layout(gari_files, data) - result = _run_tesseract( + result = _run_decoder( + decoder, "--circuit", gari_files["circuit"], "--dem", @@ -179,7 +191,8 @@ def test_tesseract_rejects_source_and_no_layout_count_mismatches(gari_files): ) _assert_failure(result, "but circuit", layout) - result = _run_tesseract( + result = _run_decoder( + decoder, "--circuit", gari_files["circuit"], "--dem", @@ -190,8 +203,9 @@ def test_tesseract_rejects_source_and_no_layout_count_mismatches(gari_files): _assert_failure(result, "Supply --gari-layout") -def test_tesseract_requires_dem_with_layout(gari_files): - result = _run_tesseract( +def test_decoders_require_dem_with_layout(decoder, gari_files): + result = _run_decoder( + decoder, "--circuit", gari_files["circuit"], "--gari-layout", diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 08cedd9b..0c9bd666 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -12,21 +12,166 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include #include #include #include +#include #include #include +#include #include +#include #include "common.h" #include "simplex.h" #include "stim.h" #include "utils.h" +namespace { + +constexpr char kGariLayoutSchema[] = "tesseract.gari_layout.v1"; + +struct GariLayout { + std::string schema; + size_t source_detector_count; + size_t gari_detector_count; + std::vector source_to_gari; +}; + +std::invalid_argument gari_layout_error(const std::string& path, const std::string& detail) { + return std::invalid_argument("Invalid GARI layout '" + path + "': " + detail); +} + +const nlohmann::json& required_layout_field(const nlohmann::json& document, const std::string& path, + const char* field) { + auto item = document.find(field); + if (item == document.end()) { + throw gari_layout_error(path, "missing required field '" + std::string(field) + "'."); + } + return *item; +} + +size_t read_layout_size(const nlohmann::json& value, const std::string& path, + const std::string& field) { + if (!value.is_number_integer()) { + throw gari_layout_error( + path, "field '" + field + "' must be an integer, but is " + value.type_name() + "."); + } + + int64_t signed_value; + if (value.is_number_unsigned()) { + uint64_t unsigned_value = value.get(); + if (unsigned_value > static_cast(std::numeric_limits::max())) { + throw gari_layout_error(path, "field '" + field + "' is too large."); + } + signed_value = static_cast(unsigned_value); + } else { + signed_value = value.get(); + } + if (signed_value < 0) { + throw gari_layout_error(path, "field '" + field + "' must be nonnegative, but is " + + std::to_string(signed_value) + "."); + } + if (static_cast(signed_value) > std::numeric_limits::max()) { + throw gari_layout_error(path, "field '" + field + "' is too large."); + } + return static_cast(signed_value); +} + +GariLayout load_gari_layout(const std::string& path) { + std::ifstream input(path); + if (!input.is_open()) { + throw std::invalid_argument("Could not open GARI layout: " + path); + } + + nlohmann::json document; + try { + input >> document; + } catch (const nlohmann::json::exception& err) { + throw gari_layout_error(path, "could not parse JSON: " + std::string(err.what())); + } + if (!document.is_object()) { + throw gari_layout_error(path, "top-level JSON value must be an object."); + } + + const nlohmann::json& schema_json = required_layout_field(document, path, "schema"); + if (!schema_json.is_string()) { + throw gari_layout_error(path, "field 'schema' must be a string."); + } + std::string schema = schema_json.get(); + if (schema != kGariLayoutSchema) { + throw gari_layout_error(path, "field 'schema' must be '" + std::string(kGariLayoutSchema) + + "', but is '" + schema + "'."); + } + + size_t source_detector_count = + read_layout_size(required_layout_field(document, path, "source_detector_count"), path, + "source_detector_count"); + size_t gari_detector_count = read_layout_size( + required_layout_field(document, path, "gari_detector_count"), path, "gari_detector_count"); + if (gari_detector_count < source_detector_count) { + throw gari_layout_error(path, "field 'gari_detector_count' must be at least " + + std::to_string(source_detector_count) + ", but is " + + std::to_string(gari_detector_count) + "."); + } + + const nlohmann::json& mapping_json = required_layout_field(document, path, "source_to_gari"); + if (!mapping_json.is_array()) { + throw gari_layout_error(path, "field 'source_to_gari' must be an array."); + } + if (mapping_json.size() != source_detector_count) { + throw gari_layout_error(path, "field 'source_to_gari' must contain " + + std::to_string(source_detector_count) + " entries, but has " + + std::to_string(mapping_json.size()) + "."); + } + + std::vector source_to_gari; + source_to_gari.reserve(source_detector_count); + std::unordered_map target_to_source; + for (size_t source = 0; source < mapping_json.size(); ++source) { + std::string field = "source_to_gari[" + std::to_string(source) + "]"; + size_t target = read_layout_size(mapping_json[source], path, field); + if (target >= gari_detector_count) { + throw gari_layout_error(path, "field '" + field + "' is " + std::to_string(target) + + ", outside the target range [0, " + + std::to_string(gari_detector_count) + ")."); + } + auto [previous, inserted] = target_to_source.emplace(target, source); + if (!inserted) { + throw gari_layout_error(path, "source detectors " + std::to_string(previous->second) + + " and " + std::to_string(source) + + " both map to GARI detector " + std::to_string(target) + + "; v1 layouts require an injective mapping."); + } + source_to_gari.push_back(target); + } + return {schema, source_detector_count, gari_detector_count, std::move(source_to_gari)}; +} + +std::vector map_gari_hits(std::vector source_hits, const GariLayout& layout, + const std::string& path) { + for (uint64_t& source : source_hits) { + if (source >= layout.source_to_gari.size()) { + throw gari_layout_error(path, "source detector index " + std::to_string(source) + + " is outside the source range [0, " + + std::to_string(layout.source_detector_count) + ")."); + } + source = layout.source_to_gari[source]; + } + std::sort(source_hits.begin(), source_hits.end()); + return source_hits; +} + +} // namespace + struct Args { std::string circuit_path; std::string dem_path; + std::string gari_layout_path; + std::string gari_layout_schema; + size_t gari_source_detector_count = 0; + size_t gari_detector_count = 0; bool no_merge_errors = false; // Sampling options @@ -83,6 +228,9 @@ struct Args { if (circuit_path.empty() and dem_path.empty()) { throw std::invalid_argument("Must provide at least one of --circuit or --dem"); } + if (!gari_layout_path.empty() and dem_path.empty()) { + throw std::invalid_argument("--gari-layout requires --dem."); + } int num_data_sources = int(sample_num_shots > 0) + int(!in_fname.empty()); if (num_data_sources != 1) { @@ -170,6 +318,39 @@ struct Args { config.merge_errors = !no_merge_errors; + std::optional gari_layout; + if (!gari_layout_path.empty()) { + gari_layout = load_gari_layout(gari_layout_path); + size_t target_count = config.dem.count_detectors(); + if (target_count != gari_layout->gari_detector_count) { + throw gari_layout_error( + gari_layout_path, "field 'gari_detector_count' is " + + std::to_string(gari_layout->gari_detector_count) + ", but DEM '" + + dem_path + "' contains " + std::to_string(target_count) + + " detectors."); + } + if (!circuit_path.empty()) { + size_t source_count = circuit.count_detectors(); + if (source_count != gari_layout->source_detector_count) { + throw gari_layout_error(gari_layout_path, + "field 'source_detector_count' is " + + std::to_string(gari_layout->source_detector_count) + + ", but circuit '" + circuit_path + "' contains " + + std::to_string(source_count) + " detectors."); + } + } + gari_layout_schema = gari_layout->schema; + gari_source_detector_count = gari_layout->source_detector_count; + gari_detector_count = gari_layout->gari_detector_count; + } else if (!circuit_path.empty() and !dem_path.empty() and + circuit.count_detectors() != config.dem.count_detectors()) { + throw std::invalid_argument( + "Circuit '" + circuit_path + "' contains " + std::to_string(circuit.count_detectors()) + + " detectors, but DEM '" + dem_path + "' contains " + + std::to_string(config.dem.count_detectors()) + + ". Supply --gari-layout when the source and target detector layouts differ."); + } + if (sample_num_shots > 0) { assert(!circuit_path.empty()); std::mt19937_64 rng(sample_seed); @@ -180,11 +361,19 @@ struct Args { shots.resize(sample_num_shots); for (size_t k = 0; k < sample_num_shots; k++) { shots[k].obs_mask = obs_T[k]; + std::vector source_hits; for (size_t d = 0; d < num_detectors; d++) { if (dets[d][k]) { - shots[k].hits.push_back(d); + source_hits.push_back(d); } } + if (gari_layout) { + // The GARI matrix augments the physical syndrome with zero-valued virtual constraints. + // Sparse shots contain mapped physical hits only, so virtual rows remain zero. + shots[k].hits = map_gari_hits(std::move(source_hits), *gari_layout, gari_layout_path); + } else { + shots[k].hits = std::move(source_hits); + } } } @@ -195,14 +384,20 @@ struct Args { throw std::invalid_argument("Could not open the file: " + in_fname); } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); + size_t source_detector_count = + gari_layout ? gari_layout->source_detector_count : config.dem.count_detectors(); auto reader = stim::MeasureRecordReader::make( - shots_file, shots_in_format.id, 0, config.dem.count_detectors(), + shots_file, shots_in_format.id, 0, source_detector_count, append_observables * config.dem.count_observables()); // Load the shots from a file stim::SparseShot sparse_shot; sparse_shot.clear(); while (reader->start_and_read_entire_record(sparse_shot)) { + if (gari_layout) { + sparse_shot.hits = + map_gari_hits(std::move(sparse_shot.hits), *gari_layout, gari_layout_path); + } shots.push_back(sparse_shot); sparse_shot.clear(); } @@ -274,6 +469,14 @@ int main(int argc, char* argv[]) { Args args; program.add_argument("--circuit").help("Stim circuit file path").store_into(args.circuit_path); program.add_argument("--dem").help("Stim dem file path").store_into(args.dem_path); + program.add_argument("--gari-layout") + .help( + "JSON layout emitted by gari_convert. Maps detector data from the original circuit or " + "source shot file into the supplied GARI matrix file. Unmapped virtual detector rows " + "are treated as zero.") + .metavar("FILE") + .default_value(std::string("")) + .store_into(args.gari_layout_path); program.add_argument("--no-merge-errors") .help("If provided, will not merge identical error mechanisms.") .store_into(args.no_merge_errors); @@ -417,11 +620,16 @@ int main(int argc, char* argv[]) { std::cerr << program; return EXIT_FAILURE; } - args.validate(); SimplexConfig config; std::vector shots; std::unique_ptr writer; - args.extract(config, shots, writer); + try { + args.validate(); + args.extract(config, shots, writer); + } catch (const std::exception& err) { + std::cerr << err.what() << std::endl; + return EXIT_FAILURE; + } size_t num_observables = config.dem.count_observables(); std::vector> obs_predicted(shots.size(), stim::simd_bits<64>(num_observables)); @@ -518,6 +726,13 @@ int main(int argc, char* argv[]) { {"num_shots", shot}, {"sample_num_shots", args.sample_num_shots}}; + if (!args.gari_layout_path.empty()) { + stats_json["gari_layout_path"] = args.gari_layout_path; + stats_json["gari_layout_schema"] = args.gari_layout_schema; + stats_json["source_detector_count"] = args.gari_source_detector_count; + stats_json["gari_detector_count"] = args.gari_detector_count; + } + if (args.stats_out_fname == "-") { std::cout << stats_json << std::endl; print_final_stats = false; From 601ca7dc232d9b1174d8860a0e11058d9551359d Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sat, 25 Jul 2026 22:35:44 -0700 Subject: [PATCH 03/31] Add GARI layout edge-case and format tests --- src/py/gari_cli_test.py | 226 ++++++++++++++++++++++++++++++---------- src/simplex_main.cc | 22 ++-- src/tesseract_main.cc | 22 ++-- 3 files changed, 203 insertions(+), 67 deletions(-) diff --git a/src/py/gari_cli_test.py b/src/py/gari_cli_test.py index f3389177..7aaea594 100644 --- a/src/py/gari_cli_test.py +++ b/src/py/gari_cli_test.py @@ -30,24 +30,24 @@ def gari_files(tmp_path): circuit.write_text( """\ X_ERROR(1) 0 3 -M 0 1 2 3 +M 0 1 2 3 4 5 +DETECTOR rec[-6] +DETECTOR rec[-5] DETECTOR rec[-4] DETECTOR rec[-3] -DETECTOR rec[-2] -DETECTOR rec[-1] -OBSERVABLE_INCLUDE(0) rec[-4] +OBSERVABLE_INCLUDE(0) rec[-6] +OBSERVABLE_INCLUDE(1) rec[-2] +OBSERVABLE_INCLUDE(2) rec[-1] """, encoding="utf-8", ) dem = tmp_path / "target.dem" dem.write_text( """\ -error(0.1) D0 -error(0.1) D1 L0 -error(0.1) D2 -error(0.1) D3 -error(0.1) D4 -error(0.1) D5 +error(0.1) D1 D2 L0 +error(0.1) D0 D3 +error(0.1) D4 L1 +error(0.1) D5 L2 """, encoding="utf-8", ) @@ -107,9 +107,10 @@ def _decode(decoder, gari_files, *source_args): return output.read_text(encoding="utf-8") -def _write_layout(gari_files, data): - path = gari_files["tmp_path"] / "invalid-layout.json" - path.write_text(json.dumps(data), encoding="utf-8") +def _write_layout(gari_files, data, name="invalid-layout.json"): + path = gari_files["tmp_path"] / name + contents = data if isinstance(data, str) else json.dumps(data) + path.write_text(contents, encoding="utf-8") return path @@ -131,7 +132,7 @@ def test_decoders_map_sampled_and_01_shots(decoder, gari_files): "1", "--sample-seed", "0", - ) == "1\n" + ) == "100\n" assert _decode( decoder, gari_files, @@ -139,45 +140,64 @@ def test_decoders_map_sampled_and_01_shots(decoder, gari_files): gari_files["shots"], "--in-format", "01", - ) == "1\n" - - -@pytest.mark.parametrize( - ("changes", "expected"), - [ - ({"schema": "wrong.schema"}, "field 'schema'"), - ({"source_to_gari": [2, 0, 3]}, "must contain 4 entries"), - ({"source_to_gari": [2, 0, 6, 1]}, "outside the target range"), - ({"source_to_gari": [2, 0, 2, 1]}, "injective mapping"), - ({"gari_detector_count": 7}, "but DEM"), - ], -) -def test_decoders_reject_invalid_layouts(decoder, gari_files, changes, expected): - data = {**gari_files["layout_data"], **changes} - layout = _write_layout(gari_files, data) - result = _run_decoder( - decoder, - "--dem", - gari_files["dem"], - "--gari-layout", - layout, - "--in", - gari_files["shots"], - "--in-format", - "01", - ) - _assert_failure(result, expected, layout) + ) == "100\n" -def test_decoders_reject_source_and_no_layout_count_mismatches( - decoder, gari_files -): - data = { - **gari_files["layout_data"], - "source_detector_count": 3, - "source_to_gari": [2, 0, 3], - } - layout = _write_layout(gari_files, data) +def test_decoders_reject_invalid_layouts_and_counts(decoder, gari_files): + base = gari_files["layout_data"] + cases = [ + ("{", "could not parse JSON"), + ([], "top-level JSON value must be an object"), + ( + {key: value for key, value in base.items() if key != "schema"}, + "missing required field 'schema'", + ), + ({**base, "schema": 1}, "field 'schema' must be a string"), + ({**base, "schema": "wrong.schema"}, "field 'schema' must be"), + ( + { + key: value + for key, value in base.items() + if key != "source_to_gari" + }, + "missing required field 'source_to_gari'", + ), + ({**base, "source_detector_count": True}, "must be an integer"), + ({**base, "gari_detector_count": 2**63}, "is too large"), + ({**base, "source_to_gari": {}}, "must be an array"), + ({**base, "source_to_gari": [2, 0, 3.5, 1]}, "source_to_gari[2]"), + ({**base, "source_to_gari": [2, 0, -1, 1]}, "must be nonnegative"), + ({**base, "source_to_gari": [2, 0, 3]}, "must contain 4 entries"), + ({**base, "source_to_gari": [2, 0, 3, 1, 4]}, "must contain 4 entries"), + ({**base, "source_to_gari": [2, 0, 6, 1]}, "outside the target range"), + ({**base, "source_to_gari": [2, 0, 2, 1]}, "injective mapping"), + ({**base, "gari_detector_count": 3}, "must be at least 4"), + ({**base, "gari_detector_count": 7}, "but DEM"), + ] + for index, (data, expected) in enumerate(cases): + layout = _write_layout(gari_files, data, f"invalid-layout-{index}.json") + result = _run_decoder( + decoder, + "--dem", + gari_files["dem"], + "--gari-layout", + layout, + "--in", + gari_files["shots"], + "--in-format", + "01", + ) + _assert_failure(result, expected, layout) + + source_count_layout = _write_layout( + gari_files, + { + **base, + "source_detector_count": 3, + "source_to_gari": [2, 0, 3], + }, + "source-count-layout.json", + ) result = _run_decoder( decoder, "--circuit", @@ -185,11 +205,11 @@ def test_decoders_reject_source_and_no_layout_count_mismatches( "--dem", gari_files["dem"], "--gari-layout", - layout, + source_count_layout, "--sample-num-shots", "1", ) - _assert_failure(result, "but circuit", layout) + _assert_failure(result, "but circuit", source_count_layout) result = _run_decoder( decoder, @@ -202,8 +222,6 @@ def test_decoders_reject_source_and_no_layout_count_mismatches( ) _assert_failure(result, "Supply --gari-layout") - -def test_decoders_require_dem_with_layout(decoder, gari_files): result = _run_decoder( decoder, "--circuit", @@ -216,5 +234,103 @@ def test_decoders_require_dem_with_layout(decoder, gari_files): _assert_failure(result, "--gari-layout requires --dem") +def test_decoders_read_multirecord_01_and_b8(decoder, gari_files): + records = { + "01": "1001100\n0110000\n0000000\n1111100\n", + "b8": bytes([0x19, 0x06, 0x00, 0x1F]), + } + for shot_format, contents in records.items(): + shots = gari_files["tmp_path"] / f"source-multiple.{shot_format}" + if isinstance(contents, bytes): + shots.write_bytes(contents) + else: + shots.write_text(contents, encoding="utf-8") + output = gari_files["tmp_path"] / f"predictions-{shot_format}.01" + stats = gari_files["tmp_path"] / f"stats-{shot_format}.json" + result = _run_decoder( + decoder, + "--dem", + gari_files["dem"], + "--gari-layout", + gari_files["layout"], + "--in", + shots, + "--in-format", + shot_format, + "--in-includes-appended-observables", + "--out", + output, + "--out-format", + "01", + "--stats-out", + stats, + ) + assert result.returncode == 0, result.stderr + assert output.read_text(encoding="utf-8") == "100\n000\n000\n100\n" + metadata = json.loads(stats.read_text(encoding="utf-8")) + assert metadata["num_shots"] == 4 + assert metadata["num_errors"] == 0 + assert metadata["gari_layout_path"] == str(gari_files["layout"]) + assert metadata["gari_layout_schema"] == _SCHEMA + assert metadata["source_detector_count"] == 4 + assert metadata["gari_detector_count"] == 6 + + +def test_decoders_preserve_no_layout_paths_and_document_help(decoder, gari_files): + ordinary_dem = gari_files["tmp_path"] / "ordinary.dem" + ordinary_dem.write_text( + """\ +error(0.1) D0 D3 L0 +error(0.1) D1 L1 +error(0.1) D2 L2 +""", + encoding="utf-8", + ) + output = gari_files["tmp_path"] / "ordinary-predictions.01" + result = _run_decoder( + decoder, + "--circuit", + gari_files["circuit"], + "--dem", + ordinary_dem, + "--sample-num-shots", + "1", + "--sample-seed", + "0", + "--out", + output, + "--out-format", + "01", + ) + assert result.returncode == 0, result.stderr + assert output.read_text(encoding="utf-8") == "100\n" + + # Without a circuit or GARI layout, file input is already in the target + # DEM's detector layout. D1 D2 therefore selects the logical-L0 mechanism. + target_shots = gari_files["tmp_path"] / "target-layout.01" + target_shots.write_text("011000\n", encoding="utf-8") + output = gari_files["tmp_path"] / "target-layout-predictions.01" + result = _run_decoder( + decoder, + "--dem", + gari_files["dem"], + "--in", + target_shots, + "--in-format", + "01", + "--out", + output, + "--out-format", + "01", + ) + assert result.returncode == 0, result.stderr + assert output.read_text(encoding="utf-8") == "100\n" + + help_result = _run_decoder(decoder, "--help") + assert help_result.returncode == 0 + assert "--gari-layout FILE" in help_result.stdout + assert "target detector layout" in help_result.stdout + + if __name__ == "__main__": raise SystemExit(pytest.main([__file__])) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 0c9bd666..2119b35f 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -63,7 +63,9 @@ size_t read_layout_size(const nlohmann::json& value, const std::string& path, if (value.is_number_unsigned()) { uint64_t unsigned_value = value.get(); if (unsigned_value > static_cast(std::numeric_limits::max())) { - throw gari_layout_error(path, "field '" + field + "' is too large."); + throw gari_layout_error(path, "field '" + field + "' is too large: actual " + + std::to_string(unsigned_value) + ", maximum " + + std::to_string(std::numeric_limits::max()) + "."); } signed_value = static_cast(unsigned_value); } else { @@ -74,7 +76,9 @@ size_t read_layout_size(const nlohmann::json& value, const std::string& path, std::to_string(signed_value) + "."); } if (static_cast(signed_value) > std::numeric_limits::max()) { - throw gari_layout_error(path, "field '" + field + "' is too large."); + throw gari_layout_error(path, "field '" + field + "' is too large: actual " + + std::to_string(signed_value) + ", maximum " + + std::to_string(std::numeric_limits::max()) + "."); } return static_cast(signed_value); } @@ -92,12 +96,14 @@ GariLayout load_gari_layout(const std::string& path) { throw gari_layout_error(path, "could not parse JSON: " + std::string(err.what())); } if (!document.is_object()) { - throw gari_layout_error(path, "top-level JSON value must be an object."); + throw gari_layout_error(path, "top-level JSON value must be an object, but is " + + std::string(document.type_name()) + "."); } const nlohmann::json& schema_json = required_layout_field(document, path, "schema"); if (!schema_json.is_string()) { - throw gari_layout_error(path, "field 'schema' must be a string."); + throw gari_layout_error(path, "field 'schema' must be a string, but is " + + std::string(schema_json.type_name()) + "."); } std::string schema = schema_json.get(); if (schema != kGariLayoutSchema) { @@ -118,7 +124,8 @@ GariLayout load_gari_layout(const std::string& path) { const nlohmann::json& mapping_json = required_layout_field(document, path, "source_to_gari"); if (!mapping_json.is_array()) { - throw gari_layout_error(path, "field 'source_to_gari' must be an array."); + throw gari_layout_error(path, "field 'source_to_gari' must be an array, but is " + + std::string(mapping_json.type_name()) + "."); } if (mapping_json.size() != source_detector_count) { throw gari_layout_error(path, "field 'source_to_gari' must contain " + @@ -519,7 +526,10 @@ int main(int argc, char* argv[]) { .default_value(size_t(0)) .store_into(args.shot_range_end); program.add_argument("--in") - .help("File to read detection events (and possibly observable flips) from") + .help( + "File to read detection events (and possibly observable flips) from. Without --circuit " + "or --gari-layout, detector data is assumed to use the supplied DEM's target detector " + "layout.") .metavar("filename") .default_value(std::string("")) .store_into(args.in_fname); diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index cd8ebb6e..637fd828 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -66,7 +66,9 @@ size_t read_layout_size(const nlohmann::json& value, const std::string& path, if (value.is_number_unsigned()) { uint64_t unsigned_value = value.get(); if (unsigned_value > static_cast(std::numeric_limits::max())) { - throw gari_layout_error(path, "field '" + field + "' is too large."); + throw gari_layout_error(path, "field '" + field + "' is too large: actual " + + std::to_string(unsigned_value) + ", maximum " + + std::to_string(std::numeric_limits::max()) + "."); } signed_value = static_cast(unsigned_value); } else { @@ -77,7 +79,9 @@ size_t read_layout_size(const nlohmann::json& value, const std::string& path, std::to_string(signed_value) + "."); } if (static_cast(signed_value) > std::numeric_limits::max()) { - throw gari_layout_error(path, "field '" + field + "' is too large."); + throw gari_layout_error(path, "field '" + field + "' is too large: actual " + + std::to_string(signed_value) + ", maximum " + + std::to_string(std::numeric_limits::max()) + "."); } return static_cast(signed_value); } @@ -95,12 +99,14 @@ GariLayout load_gari_layout(const std::string& path) { throw gari_layout_error(path, "could not parse JSON: " + std::string(err.what())); } if (!document.is_object()) { - throw gari_layout_error(path, "top-level JSON value must be an object."); + throw gari_layout_error(path, "top-level JSON value must be an object, but is " + + std::string(document.type_name()) + "."); } const nlohmann::json& schema_json = required_layout_field(document, path, "schema"); if (!schema_json.is_string()) { - throw gari_layout_error(path, "field 'schema' must be a string."); + throw gari_layout_error(path, "field 'schema' must be a string, but is " + + std::string(schema_json.type_name()) + "."); } std::string schema = schema_json.get(); if (schema != kGariLayoutSchema) { @@ -121,7 +127,8 @@ GariLayout load_gari_layout(const std::string& path) { const nlohmann::json& mapping_json = required_layout_field(document, path, "source_to_gari"); if (!mapping_json.is_array()) { - throw gari_layout_error(path, "field 'source_to_gari' must be an array."); + throw gari_layout_error(path, "field 'source_to_gari' must be an array, but is " + + std::string(mapping_json.type_name()) + "."); } if (mapping_json.size() != source_detector_count) { throw gari_layout_error(path, "field 'source_to_gari' must contain " + @@ -616,7 +623,10 @@ int main(int argc, char* argv[]) { .default_value(size_t(0)) .store_into(args.shot_range_end); program.add_argument("--in") - .help("File to read detection events (and possibly observable flips) from") + .help( + "File to read detection events (and possibly observable flips) from. Without --circuit " + "or --gari-layout, detector data is assumed to use the supplied DEM's target detector " + "layout.") .metavar("filename") .default_value(std::string("")) .store_into(args.in_fname); From 350719b429bed00cd50a2e5978fae0ab5d4190df Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sat, 25 Jul 2026 22:42:08 -0700 Subject: [PATCH 04/31] Remove GARI CLI integration tests --- src/py/BUILD | 16 +- src/py/gari_cli_test.py | 336 ---------------------------------------- 2 files changed, 1 insertion(+), 351 deletions(-) delete mode 100644 src/py/gari_cli_test.py diff --git a/src/py/BUILD b/src/py/BUILD index 94730811..7160760f 100644 --- a/src/py/BUILD +++ b/src/py/BUILD @@ -93,21 +93,7 @@ py_test( imports = ["..", "."], ) -py_test( - name = "gari_cli_test", - srcs = ["gari_cli_test.py"], - args = [ - "$(location //src:tesseract)", - "$(location //src:simplex)", - ], - data = [ - "//src:simplex", - "//src:tesseract", - ], - size = "medium", - visibility = ["//:__subpackages__"], - deps = ["@pypi//pytest"], -) + py_test( name = "stub_test", diff --git a/src/py/gari_cli_test.py b/src/py/gari_cli_test.py deleted file mode 100644 index 7aaea594..00000000 --- a/src/py/gari_cli_test.py +++ /dev/null @@ -1,336 +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 -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -from pathlib import Path -import subprocess -import sys - -import pytest - - -_DECODERS = tuple(Path(argument) for argument in sys.argv[1:]) -_SCHEMA = "tesseract.gari_layout.v1" - - -@pytest.fixture -def gari_files(tmp_path): - circuit = tmp_path / "source.stim" - circuit.write_text( - """\ -X_ERROR(1) 0 3 -M 0 1 2 3 4 5 -DETECTOR rec[-6] -DETECTOR rec[-5] -DETECTOR rec[-4] -DETECTOR rec[-3] -OBSERVABLE_INCLUDE(0) rec[-6] -OBSERVABLE_INCLUDE(1) rec[-2] -OBSERVABLE_INCLUDE(2) rec[-1] -""", - encoding="utf-8", - ) - dem = tmp_path / "target.dem" - dem.write_text( - """\ -error(0.1) D1 D2 L0 -error(0.1) D0 D3 -error(0.1) D4 L1 -error(0.1) D5 L2 -""", - encoding="utf-8", - ) - shots = tmp_path / "source.01" - shots.write_text("1001\n", encoding="utf-8") - layout = tmp_path / "target-layout.json" - layout_data = { - "schema": _SCHEMA, - "source_detector_count": 4, - "gari_detector_count": 6, - "source_to_gari": [2, 0, 3, 1], - } - layout.write_text(json.dumps(layout_data), encoding="utf-8") - return { - "circuit": circuit, - "dem": dem, - "shots": shots, - "layout": layout, - "layout_data": layout_data, - "tmp_path": tmp_path, - } - - -@pytest.fixture(params=_DECODERS, ids=lambda decoder: decoder.name) -def decoder(request): - return request.param - - -def _run_decoder(decoder, *args): - decoder_args = [decoder, "--threads", "1"] - if decoder.name == "tesseract": - decoder_args.extend(["--num-det-orders", "1", "--det-order-index"]) - return subprocess.run( - [*decoder_args, *map(str, args)], - capture_output=True, - text=True, - timeout=30, - check=False, - ) - - -def _decode(decoder, gari_files, *source_args): - output = gari_files["tmp_path"] / "predictions.01" - result = _run_decoder( - decoder, - "--dem", - gari_files["dem"], - "--gari-layout", - gari_files["layout"], - *source_args, - "--out", - output, - "--out-format", - "01", - ) - assert result.returncode == 0, result.stderr - return output.read_text(encoding="utf-8") - - -def _write_layout(gari_files, data, name="invalid-layout.json"): - path = gari_files["tmp_path"] / name - contents = data if isinstance(data, str) else json.dumps(data) - path.write_text(contents, encoding="utf-8") - return path - - -def _assert_failure(result, expected, path=None): - assert result.returncode != 0 - output = result.stdout + result.stderr - assert expected in output - if path is not None: - assert str(path) in output - - -def test_decoders_map_sampled_and_01_shots(decoder, gari_files): - assert _decode( - decoder, - gari_files, - "--circuit", - gari_files["circuit"], - "--sample-num-shots", - "1", - "--sample-seed", - "0", - ) == "100\n" - assert _decode( - decoder, - gari_files, - "--in", - gari_files["shots"], - "--in-format", - "01", - ) == "100\n" - - -def test_decoders_reject_invalid_layouts_and_counts(decoder, gari_files): - base = gari_files["layout_data"] - cases = [ - ("{", "could not parse JSON"), - ([], "top-level JSON value must be an object"), - ( - {key: value for key, value in base.items() if key != "schema"}, - "missing required field 'schema'", - ), - ({**base, "schema": 1}, "field 'schema' must be a string"), - ({**base, "schema": "wrong.schema"}, "field 'schema' must be"), - ( - { - key: value - for key, value in base.items() - if key != "source_to_gari" - }, - "missing required field 'source_to_gari'", - ), - ({**base, "source_detector_count": True}, "must be an integer"), - ({**base, "gari_detector_count": 2**63}, "is too large"), - ({**base, "source_to_gari": {}}, "must be an array"), - ({**base, "source_to_gari": [2, 0, 3.5, 1]}, "source_to_gari[2]"), - ({**base, "source_to_gari": [2, 0, -1, 1]}, "must be nonnegative"), - ({**base, "source_to_gari": [2, 0, 3]}, "must contain 4 entries"), - ({**base, "source_to_gari": [2, 0, 3, 1, 4]}, "must contain 4 entries"), - ({**base, "source_to_gari": [2, 0, 6, 1]}, "outside the target range"), - ({**base, "source_to_gari": [2, 0, 2, 1]}, "injective mapping"), - ({**base, "gari_detector_count": 3}, "must be at least 4"), - ({**base, "gari_detector_count": 7}, "but DEM"), - ] - for index, (data, expected) in enumerate(cases): - layout = _write_layout(gari_files, data, f"invalid-layout-{index}.json") - result = _run_decoder( - decoder, - "--dem", - gari_files["dem"], - "--gari-layout", - layout, - "--in", - gari_files["shots"], - "--in-format", - "01", - ) - _assert_failure(result, expected, layout) - - source_count_layout = _write_layout( - gari_files, - { - **base, - "source_detector_count": 3, - "source_to_gari": [2, 0, 3], - }, - "source-count-layout.json", - ) - result = _run_decoder( - decoder, - "--circuit", - gari_files["circuit"], - "--dem", - gari_files["dem"], - "--gari-layout", - source_count_layout, - "--sample-num-shots", - "1", - ) - _assert_failure(result, "but circuit", source_count_layout) - - result = _run_decoder( - decoder, - "--circuit", - gari_files["circuit"], - "--dem", - gari_files["dem"], - "--sample-num-shots", - "1", - ) - _assert_failure(result, "Supply --gari-layout") - - result = _run_decoder( - decoder, - "--circuit", - gari_files["circuit"], - "--gari-layout", - gari_files["layout"], - "--sample-num-shots", - "1", - ) - _assert_failure(result, "--gari-layout requires --dem") - - -def test_decoders_read_multirecord_01_and_b8(decoder, gari_files): - records = { - "01": "1001100\n0110000\n0000000\n1111100\n", - "b8": bytes([0x19, 0x06, 0x00, 0x1F]), - } - for shot_format, contents in records.items(): - shots = gari_files["tmp_path"] / f"source-multiple.{shot_format}" - if isinstance(contents, bytes): - shots.write_bytes(contents) - else: - shots.write_text(contents, encoding="utf-8") - output = gari_files["tmp_path"] / f"predictions-{shot_format}.01" - stats = gari_files["tmp_path"] / f"stats-{shot_format}.json" - result = _run_decoder( - decoder, - "--dem", - gari_files["dem"], - "--gari-layout", - gari_files["layout"], - "--in", - shots, - "--in-format", - shot_format, - "--in-includes-appended-observables", - "--out", - output, - "--out-format", - "01", - "--stats-out", - stats, - ) - assert result.returncode == 0, result.stderr - assert output.read_text(encoding="utf-8") == "100\n000\n000\n100\n" - metadata = json.loads(stats.read_text(encoding="utf-8")) - assert metadata["num_shots"] == 4 - assert metadata["num_errors"] == 0 - assert metadata["gari_layout_path"] == str(gari_files["layout"]) - assert metadata["gari_layout_schema"] == _SCHEMA - assert metadata["source_detector_count"] == 4 - assert metadata["gari_detector_count"] == 6 - - -def test_decoders_preserve_no_layout_paths_and_document_help(decoder, gari_files): - ordinary_dem = gari_files["tmp_path"] / "ordinary.dem" - ordinary_dem.write_text( - """\ -error(0.1) D0 D3 L0 -error(0.1) D1 L1 -error(0.1) D2 L2 -""", - encoding="utf-8", - ) - output = gari_files["tmp_path"] / "ordinary-predictions.01" - result = _run_decoder( - decoder, - "--circuit", - gari_files["circuit"], - "--dem", - ordinary_dem, - "--sample-num-shots", - "1", - "--sample-seed", - "0", - "--out", - output, - "--out-format", - "01", - ) - assert result.returncode == 0, result.stderr - assert output.read_text(encoding="utf-8") == "100\n" - - # Without a circuit or GARI layout, file input is already in the target - # DEM's detector layout. D1 D2 therefore selects the logical-L0 mechanism. - target_shots = gari_files["tmp_path"] / "target-layout.01" - target_shots.write_text("011000\n", encoding="utf-8") - output = gari_files["tmp_path"] / "target-layout-predictions.01" - result = _run_decoder( - decoder, - "--dem", - gari_files["dem"], - "--in", - target_shots, - "--in-format", - "01", - "--out", - output, - "--out-format", - "01", - ) - assert result.returncode == 0, result.stderr - assert output.read_text(encoding="utf-8") == "100\n" - - help_result = _run_decoder(decoder, "--help") - assert help_result.returncode == 0 - assert "--gari-layout FILE" in help_result.stdout - assert "target detector layout" in help_result.stdout - - -if __name__ == "__main__": - raise SystemExit(pytest.main([__file__])) From c76a28e15bbf1b860add600425397d7505aa9d93 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sun, 26 Jul 2026 23:54:19 -0700 Subject: [PATCH 05/31] Validate GARI observable counts --- src/simplex_main.cc | 9 +++++++++ src/tesseract_main.cc | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 2119b35f..e4eaccf9 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -345,6 +345,15 @@ struct Args { ", but circuit '" + circuit_path + "' contains " + std::to_string(source_count) + " detectors."); } + size_t circuit_observable_count = circuit.count_observables(); + size_t dem_observable_count = config.dem.count_observables(); + if (circuit_observable_count != dem_observable_count) { + throw gari_layout_error( + gari_layout_path, "circuit '" + circuit_path + "' contains " + + std::to_string(circuit_observable_count) + + " observables, but DEM '" + dem_path + "' contains " + + std::to_string(dem_observable_count) + "."); + } } gari_layout_schema = gari_layout->schema; gari_source_detector_count = gari_layout->source_detector_count; diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index 637fd828..2d566b43 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -384,6 +384,15 @@ struct Args { ", but circuit '" + circuit_path + "' contains " + std::to_string(source_count) + " detectors."); } + size_t circuit_observable_count = circuit.count_observables(); + size_t dem_observable_count = config.dem.count_observables(); + if (circuit_observable_count != dem_observable_count) { + throw gari_layout_error( + gari_layout_path, "circuit '" + circuit_path + "' contains " + + std::to_string(circuit_observable_count) + + " observables, but DEM '" + dem_path + "' contains " + + std::to_string(dem_observable_count) + "."); + } } gari_layout_schema = gari_layout->schema; gari_source_detector_count = gari_layout->source_detector_count; From cd6389935f256610935264ba52f9d070afe7abf1 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 00:27:30 -0700 Subject: [PATCH 06/31] Use GARI row order for Tesseract traversal --- src/simplex_main.cc | 10 +++--- src/tesseract_main.cc | 71 ++++++++++++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index e4eaccf9..657e8f04 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -348,11 +348,11 @@ struct Args { size_t circuit_observable_count = circuit.count_observables(); size_t dem_observable_count = config.dem.count_observables(); if (circuit_observable_count != dem_observable_count) { - throw gari_layout_error( - gari_layout_path, "circuit '" + circuit_path + "' contains " + - std::to_string(circuit_observable_count) + - " observables, but DEM '" + dem_path + "' contains " + - std::to_string(dem_observable_count) + "."); + throw gari_layout_error(gari_layout_path, "circuit '" + circuit_path + "' contains " + + std::to_string(circuit_observable_count) + + " observables, but DEM '" + dem_path + + "' contains " + + std::to_string(dem_observable_count) + "."); } } gari_layout_schema = gari_layout->schema; diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index 2d566b43..63f23bf9 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -34,9 +34,11 @@ namespace { constexpr char kGariLayoutSchema[] = "tesseract.gari_layout.v1"; +constexpr char kGariDetectorOrder[] = "physical_then_virtual"; struct GariLayout { std::string schema; + std::string detector_order; size_t source_detector_count; size_t gari_detector_count; std::vector source_to_gari; @@ -114,6 +116,19 @@ GariLayout load_gari_layout(const std::string& path) { "', but is '" + schema + "'."); } + const nlohmann::json& detector_order_json = + required_layout_field(document, path, "detector_order"); + if (!detector_order_json.is_string()) { + throw gari_layout_error(path, "field 'detector_order' must be a string, but is " + + std::string(detector_order_json.type_name()) + "."); + } + std::string detector_order = detector_order_json.get(); + if (detector_order != kGariDetectorOrder) { + throw gari_layout_error(path, "field 'detector_order' must be '" + + std::string(kGariDetectorOrder) + "', but is '" + + detector_order + "'."); + } + size_t source_detector_count = read_layout_size(required_layout_field(document, path, "source_detector_count"), path, "source_detector_count"); @@ -147,6 +162,12 @@ GariLayout load_gari_layout(const std::string& path) { ", outside the target range [0, " + std::to_string(gari_detector_count) + ")."); } + if (target >= source_detector_count) { + throw gari_layout_error(path, "field '" + field + "' is " + std::to_string(target) + + ", outside the physical detector range [0, " + + std::to_string(source_detector_count) + ") required by '" + + std::string(kGariDetectorOrder) + "'."); + } auto [previous, inserted] = target_to_source.emplace(target, source); if (!inserted) { throw gari_layout_error(path, "source detectors " + std::to_string(previous->second) + @@ -156,7 +177,8 @@ GariLayout load_gari_layout(const std::string& path) { } source_to_gari.push_back(target); } - return {schema, source_detector_count, gari_detector_count, std::move(source_to_gari)}; + return {schema, detector_order, source_detector_count, gari_detector_count, + std::move(source_to_gari)}; } std::vector map_gari_hits(std::vector source_hits, const GariLayout& layout, @@ -180,6 +202,7 @@ struct Args { std::string dem_path; std::string gari_layout_path; std::string gari_layout_schema; + std::string gari_detector_order; size_t gari_source_detector_count = 0; size_t gari_detector_count = 0; bool no_merge_errors = false; @@ -190,6 +213,7 @@ struct Args { bool det_order_bfs = false; bool det_order_index = false; bool det_order_coordinate = false; + bool explicit_det_order = false; // Sampling options size_t sample_num_shots = 0; @@ -256,6 +280,11 @@ struct Args { throw std::invalid_argument( "Only one of --det-order-bfs, --det-order-index, or --det-order-coordinate may be set."); } + explicit_det_order = det_order_flags > 0 || program.is_used("--num-det-orders") || + program.is_used("--det-order-seed"); + if (!gari_layout_path.empty() && program.is_used("--num-det-orders") && num_det_orders == 0) { + throw std::invalid_argument("--num-det-orders must be at least 1 with --gari-layout."); + } int num_data_sources = int(sample_num_shots > 0) + int(!in_fname.empty()); if (num_data_sources != 1) { @@ -387,14 +416,15 @@ struct Args { size_t circuit_observable_count = circuit.count_observables(); size_t dem_observable_count = config.dem.count_observables(); if (circuit_observable_count != dem_observable_count) { - throw gari_layout_error( - gari_layout_path, "circuit '" + circuit_path + "' contains " + - std::to_string(circuit_observable_count) + - " observables, but DEM '" + dem_path + "' contains " + - std::to_string(dem_observable_count) + "."); + throw gari_layout_error(gari_layout_path, "circuit '" + circuit_path + "' contains " + + std::to_string(circuit_observable_count) + + " observables, but DEM '" + dem_path + + "' contains " + + std::to_string(dem_observable_count) + "."); } } gari_layout_schema = gari_layout->schema; + gari_detector_order = gari_layout->detector_order; gari_source_detector_count = gari_layout->source_detector_count; gari_detector_count = gari_layout->gari_detector_count; } else if (!circuit_path.empty() and !dem_path.empty() and @@ -420,15 +450,20 @@ 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 (gari_layout && !explicit_det_order) { + config.det_orders.assign(1, std::vector(config.dem.count_detectors())); + std::iota(config.det_orders[0].begin(), config.det_orders[0].end(), 0); + } else { + 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; + } + 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) { @@ -560,7 +595,8 @@ int main(int argc, char* argv[]) { .help( "JSON layout emitted by gari_convert. Maps detector data from the original circuit or " "source shot file into the supplied GARI matrix file. Unmapped virtual detector " - "rows are treated as zero.") + "rows are treated as zero. A physical_then_virtual layout uses its row order as the " + "default Tesseract detector traversal; explicit detector-order options override it.") .metavar("FILE") .default_value(std::string("")) .store_into(args.gari_layout_path); @@ -578,8 +614,8 @@ int main(int argc, char* argv[]) { .store_into(args.det_order_bfs); program.add_argument("--det-order-index") .help( - "Randomly choose increasing or decreasing detector index order " - "(default if no method specified)") + "Randomly choose increasing or decreasing detector index order. Without a GARI layout, " + "this is the default method; with one, this flag overrides its identity traversal.") .flag() .store_into(args.det_order_index); program.add_argument("--det-order-coordinate") @@ -908,6 +944,7 @@ int main(int argc, char* argv[]) { if (!args.gari_layout_path.empty()) { stats_json["gari_layout_path"] = args.gari_layout_path; stats_json["gari_layout_schema"] = args.gari_layout_schema; + stats_json["gari_layout_detector_order"] = args.gari_detector_order; stats_json["source_detector_count"] = args.gari_source_detector_count; stats_json["gari_detector_count"] = args.gari_detector_count; } From 1dad5198981dfbb921398e5100cfe4c121733cf7 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 27 Jul 2026 16:41:33 -0700 Subject: [PATCH 07/31] Simplify GARI CLI layout handling --- src/simplex_main.cc | 208 ++++++++-------------------------- src/tesseract_main.cc | 252 ++++++++++-------------------------------- 2 files changed, 104 insertions(+), 356 deletions(-) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 657e8f04..27acc9ba 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -21,7 +21,6 @@ #include #include #include -#include #include "common.h" #include "simplex.h" @@ -33,9 +32,6 @@ namespace { constexpr char kGariLayoutSchema[] = "tesseract.gari_layout.v1"; struct GariLayout { - std::string schema; - size_t source_detector_count; - size_t gari_detector_count; std::vector source_to_gari; }; @@ -43,126 +39,63 @@ std::invalid_argument gari_layout_error(const std::string& path, const std::stri return std::invalid_argument("Invalid GARI layout '" + path + "': " + detail); } -const nlohmann::json& required_layout_field(const nlohmann::json& document, const std::string& path, - const char* field) { - auto item = document.find(field); - if (item == document.end()) { - throw gari_layout_error(path, "missing required field '" + std::string(field) + "'."); - } - return *item; -} - -size_t read_layout_size(const nlohmann::json& value, const std::string& path, - const std::string& field) { +size_t read_layout_size(const nlohmann::json& value, const std::string& path) { if (!value.is_number_integer()) { - throw gari_layout_error( - path, "field '" + field + "' must be an integer, but is " + value.type_name() + "."); + throw gari_layout_error(path, "detector counts and mapping entries must be integers."); } - - int64_t signed_value; if (value.is_number_unsigned()) { - uint64_t unsigned_value = value.get(); - if (unsigned_value > static_cast(std::numeric_limits::max())) { - throw gari_layout_error(path, "field '" + field + "' is too large: actual " + - std::to_string(unsigned_value) + ", maximum " + - std::to_string(std::numeric_limits::max()) + "."); + uint64_t result = value.get(); + if (result > std::numeric_limits::max()) { + throw gari_layout_error(path, "integer is too large."); } - signed_value = static_cast(unsigned_value); - } else { - signed_value = value.get(); + return static_cast(result); } - if (signed_value < 0) { - throw gari_layout_error(path, "field '" + field + "' must be nonnegative, but is " + - std::to_string(signed_value) + "."); + int64_t result = value.get(); + if (result < 0) { + throw gari_layout_error(path, "detector counts and mapping entries must be nonnegative."); } - if (static_cast(signed_value) > std::numeric_limits::max()) { - throw gari_layout_error(path, "field '" + field + "' is too large: actual " + - std::to_string(signed_value) + ", maximum " + - std::to_string(std::numeric_limits::max()) + "."); - } - return static_cast(signed_value); + return static_cast(result); } -GariLayout load_gari_layout(const std::string& path) { +GariLayout load_gari_layout(const std::string& path, size_t gari_detector_count) { std::ifstream input(path); if (!input.is_open()) { throw std::invalid_argument("Could not open GARI layout: " + path); } nlohmann::json document; - try { - input >> document; - } catch (const nlohmann::json::exception& err) { - throw gari_layout_error(path, "could not parse JSON: " + std::string(err.what())); - } - if (!document.is_object()) { - throw gari_layout_error(path, "top-level JSON value must be an object, but is " + - std::string(document.type_name()) + "."); + input >> document; + if (document.at("schema").get() != kGariLayoutSchema) { + throw gari_layout_error(path, "unsupported schema."); } - const nlohmann::json& schema_json = required_layout_field(document, path, "schema"); - if (!schema_json.is_string()) { - throw gari_layout_error(path, "field 'schema' must be a string, but is " + - std::string(schema_json.type_name()) + "."); - } - std::string schema = schema_json.get(); - if (schema != kGariLayoutSchema) { - throw gari_layout_error(path, "field 'schema' must be '" + std::string(kGariLayoutSchema) + - "', but is '" + schema + "'."); - } - - size_t source_detector_count = - read_layout_size(required_layout_field(document, path, "source_detector_count"), path, - "source_detector_count"); - size_t gari_detector_count = read_layout_size( - required_layout_field(document, path, "gari_detector_count"), path, "gari_detector_count"); - if (gari_detector_count < source_detector_count) { - throw gari_layout_error(path, "field 'gari_detector_count' must be at least " + - std::to_string(source_detector_count) + ", but is " + - std::to_string(gari_detector_count) + "."); - } - - const nlohmann::json& mapping_json = required_layout_field(document, path, "source_to_gari"); - if (!mapping_json.is_array()) { - throw gari_layout_error(path, "field 'source_to_gari' must be an array, but is " + - std::string(mapping_json.type_name()) + "."); - } - if (mapping_json.size() != source_detector_count) { - throw gari_layout_error(path, "field 'source_to_gari' must contain " + - std::to_string(source_detector_count) + " entries, but has " + - std::to_string(mapping_json.size()) + "."); + size_t source_detector_count = read_layout_size(document.at("source_detector_count"), path); + const auto& mapping = document.at("source_to_gari"); + if (read_layout_size(document.at("gari_detector_count"), path) != gari_detector_count || + gari_detector_count < source_detector_count || !mapping.is_array() || + mapping.size() != source_detector_count) { + throw gari_layout_error(path, "detector counts do not agree with the mapping."); } - std::vector source_to_gari; - source_to_gari.reserve(source_detector_count); - std::unordered_map target_to_source; - for (size_t source = 0; source < mapping_json.size(); ++source) { - std::string field = "source_to_gari[" + std::to_string(source) + "]"; - size_t target = read_layout_size(mapping_json[source], path, field); - if (target >= gari_detector_count) { - throw gari_layout_error(path, "field '" + field + "' is " + std::to_string(target) + - ", outside the target range [0, " + - std::to_string(gari_detector_count) + ")."); - } - auto [previous, inserted] = target_to_source.emplace(target, source); - if (!inserted) { - throw gari_layout_error(path, "source detectors " + std::to_string(previous->second) + - " and " + std::to_string(source) + - " both map to GARI detector " + std::to_string(target) + - "; v1 layouts require an injective mapping."); + GariLayout layout; + std::vector used(gari_detector_count); + layout.source_to_gari.reserve(source_detector_count); + for (const auto& entry : mapping) { + size_t target = read_layout_size(entry, path); + if (target >= gari_detector_count || used[target]) { + throw gari_layout_error(path, "source_to_gari must contain distinct GARI detector rows."); } - source_to_gari.push_back(target); + used[target] = true; + layout.source_to_gari.push_back(target); } - return {schema, source_detector_count, gari_detector_count, std::move(source_to_gari)}; + return layout; } std::vector map_gari_hits(std::vector source_hits, const GariLayout& layout, const std::string& path) { for (uint64_t& source : source_hits) { if (source >= layout.source_to_gari.size()) { - throw gari_layout_error(path, "source detector index " + std::to_string(source) + - " is outside the source range [0, " + - std::to_string(layout.source_detector_count) + ")."); + throw gari_layout_error(path, "source detector index is out of range."); } source = layout.source_to_gari[source]; } @@ -176,9 +109,6 @@ struct Args { std::string circuit_path; std::string dem_path; std::string gari_layout_path; - std::string gari_layout_schema; - size_t gari_source_detector_count = 0; - size_t gari_detector_count = 0; bool no_merge_errors = false; // Sampling options @@ -327,44 +257,21 @@ struct Args { std::optional gari_layout; if (!gari_layout_path.empty()) { - gari_layout = load_gari_layout(gari_layout_path); - size_t target_count = config.dem.count_detectors(); - if (target_count != gari_layout->gari_detector_count) { - throw gari_layout_error( - gari_layout_path, "field 'gari_detector_count' is " + - std::to_string(gari_layout->gari_detector_count) + ", but DEM '" + - dem_path + "' contains " + std::to_string(target_count) + - " detectors."); - } - if (!circuit_path.empty()) { + gari_layout = load_gari_layout(gari_layout_path, config.dem.count_detectors()); + if (sample_num_shots > 0) { size_t source_count = circuit.count_detectors(); - if (source_count != gari_layout->source_detector_count) { + if (source_count != gari_layout->source_to_gari.size()) { throw gari_layout_error(gari_layout_path, - "field 'source_detector_count' is " + - std::to_string(gari_layout->source_detector_count) + - ", but circuit '" + circuit_path + "' contains " + - std::to_string(source_count) + " detectors."); + "source_detector_count does not match the sampled circuit."); } - size_t circuit_observable_count = circuit.count_observables(); - size_t dem_observable_count = config.dem.count_observables(); - if (circuit_observable_count != dem_observable_count) { - throw gari_layout_error(gari_layout_path, "circuit '" + circuit_path + "' contains " + - std::to_string(circuit_observable_count) + - " observables, but DEM '" + dem_path + - "' contains " + - std::to_string(dem_observable_count) + "."); + if (circuit.count_observables() != config.dem.count_observables()) { + throw gari_layout_error(gari_layout_path, + "the circuit and DEM observable counts differ."); } } - gari_layout_schema = gari_layout->schema; - gari_source_detector_count = gari_layout->source_detector_count; - gari_detector_count = gari_layout->gari_detector_count; - } else if (!circuit_path.empty() and !dem_path.empty() and + } else if (sample_num_shots > 0 and !dem_path.empty() and circuit.count_detectors() != config.dem.count_detectors()) { - throw std::invalid_argument( - "Circuit '" + circuit_path + "' contains " + std::to_string(circuit.count_detectors()) + - " detectors, but DEM '" + dem_path + "' contains " + - std::to_string(config.dem.count_detectors()) + - ". Supply --gari-layout when the source and target detector layouts differ."); + throw std::invalid_argument("Circuit and DEM detector counts differ; supply --gari-layout."); } if (sample_num_shots > 0) { @@ -377,18 +284,15 @@ struct Args { shots.resize(sample_num_shots); for (size_t k = 0; k < sample_num_shots; k++) { shots[k].obs_mask = obs_T[k]; - std::vector source_hits; for (size_t d = 0; d < num_detectors; d++) { if (dets[d][k]) { - source_hits.push_back(d); + shots[k].hits.push_back(d); } } if (gari_layout) { // The GARI matrix augments the physical syndrome with zero-valued virtual constraints. // Sparse shots contain mapped physical hits only, so virtual rows remain zero. - shots[k].hits = map_gari_hits(std::move(source_hits), *gari_layout, gari_layout_path); - } else { - shots[k].hits = std::move(source_hits); + shots[k].hits = map_gari_hits(std::move(shots[k].hits), *gari_layout, gari_layout_path); } } } @@ -401,7 +305,7 @@ struct Args { } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); size_t source_detector_count = - gari_layout ? gari_layout->source_detector_count : config.dem.count_detectors(); + gari_layout ? gari_layout->source_to_gari.size() : config.dem.count_detectors(); auto reader = stim::MeasureRecordReader::make( shots_file, shots_in_format.id, 0, source_detector_count, append_observables * config.dem.count_observables()); @@ -487,9 +391,8 @@ int main(int argc, char* argv[]) { program.add_argument("--dem").help("Stim dem file path").store_into(args.dem_path); program.add_argument("--gari-layout") .help( - "JSON layout emitted by gari_convert. Maps detector data from the original circuit or " - "source shot file into the supplied GARI matrix file. Unmapped virtual detector rows " - "are treated as zero.") + "Companion JSON layout for a GARI matrix file. Maps source detector data into GARI " + "rows; virtual rows remain zero.") .metavar("FILE") .default_value(std::string("")) .store_into(args.gari_layout_path); @@ -535,10 +438,7 @@ int main(int argc, char* argv[]) { .default_value(size_t(0)) .store_into(args.shot_range_end); program.add_argument("--in") - .help( - "File to read detection events (and possibly observable flips) from. Without --circuit " - "or --gari-layout, detector data is assumed to use the supplied DEM's target detector " - "layout.") + .help("File to read detection events (and possibly observable flips) from") .metavar("filename") .default_value(std::string("")) .store_into(args.in_fname); @@ -639,16 +539,11 @@ int main(int argc, char* argv[]) { std::cerr << program; return EXIT_FAILURE; } + args.validate(); SimplexConfig config; std::vector shots; std::unique_ptr writer; - try { - args.validate(); - args.extract(config, shots, writer); - } catch (const std::exception& err) { - std::cerr << err.what() << std::endl; - return EXIT_FAILURE; - } + args.extract(config, shots, writer); size_t num_observables = config.dem.count_observables(); std::vector> obs_predicted(shots.size(), stim::simd_bits<64>(num_observables)); @@ -745,13 +640,6 @@ int main(int argc, char* argv[]) { {"num_shots", shot}, {"sample_num_shots", args.sample_num_shots}}; - if (!args.gari_layout_path.empty()) { - stats_json["gari_layout_path"] = args.gari_layout_path; - stats_json["gari_layout_schema"] = args.gari_layout_schema; - stats_json["source_detector_count"] = args.gari_source_detector_count; - stats_json["gari_detector_count"] = args.gari_detector_count; - } - if (args.stats_out_fname == "-") { std::cout << stats_json << std::endl; print_final_stats = false; diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index 63f23bf9..5e03f424 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -24,7 +24,6 @@ #include #include #include -#include #include "common.h" #include "stim.h" @@ -37,10 +36,6 @@ constexpr char kGariLayoutSchema[] = "tesseract.gari_layout.v1"; constexpr char kGariDetectorOrder[] = "physical_then_virtual"; struct GariLayout { - std::string schema; - std::string detector_order; - size_t source_detector_count; - size_t gari_detector_count; std::vector source_to_gari; }; @@ -48,146 +43,64 @@ std::invalid_argument gari_layout_error(const std::string& path, const std::stri return std::invalid_argument("Invalid GARI layout '" + path + "': " + detail); } -const nlohmann::json& required_layout_field(const nlohmann::json& document, const std::string& path, - const char* field) { - auto item = document.find(field); - if (item == document.end()) { - throw gari_layout_error(path, "missing required field '" + std::string(field) + "'."); - } - return *item; -} - -size_t read_layout_size(const nlohmann::json& value, const std::string& path, - const std::string& field) { +size_t read_layout_size(const nlohmann::json& value, const std::string& path) { if (!value.is_number_integer()) { - throw gari_layout_error( - path, "field '" + field + "' must be an integer, but is " + value.type_name() + "."); + throw gari_layout_error(path, "detector counts and mapping entries must be integers."); } - - int64_t signed_value; if (value.is_number_unsigned()) { - uint64_t unsigned_value = value.get(); - if (unsigned_value > static_cast(std::numeric_limits::max())) { - throw gari_layout_error(path, "field '" + field + "' is too large: actual " + - std::to_string(unsigned_value) + ", maximum " + - std::to_string(std::numeric_limits::max()) + "."); - } - signed_value = static_cast(unsigned_value); - } else { - signed_value = value.get(); - } - if (signed_value < 0) { - throw gari_layout_error(path, "field '" + field + "' must be nonnegative, but is " + - std::to_string(signed_value) + "."); + uint64_t result = value.get(); + if (result > std::numeric_limits::max()) { + throw gari_layout_error(path, "integer is too large."); + } + return static_cast(result); } - if (static_cast(signed_value) > std::numeric_limits::max()) { - throw gari_layout_error(path, "field '" + field + "' is too large: actual " + - std::to_string(signed_value) + ", maximum " + - std::to_string(std::numeric_limits::max()) + "."); + int64_t result = value.get(); + if (result < 0) { + throw gari_layout_error(path, "detector counts and mapping entries must be nonnegative."); } - return static_cast(signed_value); + return static_cast(result); } -GariLayout load_gari_layout(const std::string& path) { +GariLayout load_gari_layout(const std::string& path, size_t gari_detector_count) { std::ifstream input(path); if (!input.is_open()) { throw std::invalid_argument("Could not open GARI layout: " + path); } nlohmann::json document; - try { - input >> document; - } catch (const nlohmann::json::exception& err) { - throw gari_layout_error(path, "could not parse JSON: " + std::string(err.what())); - } - if (!document.is_object()) { - throw gari_layout_error(path, "top-level JSON value must be an object, but is " + - std::string(document.type_name()) + "."); + input >> document; + if (document.at("schema").get() != kGariLayoutSchema || + document.at("detector_order").get() != kGariDetectorOrder) { + throw gari_layout_error(path, "unsupported schema or detector order."); } - const nlohmann::json& schema_json = required_layout_field(document, path, "schema"); - if (!schema_json.is_string()) { - throw gari_layout_error(path, "field 'schema' must be a string, but is " + - std::string(schema_json.type_name()) + "."); - } - std::string schema = schema_json.get(); - if (schema != kGariLayoutSchema) { - throw gari_layout_error(path, "field 'schema' must be '" + std::string(kGariLayoutSchema) + - "', but is '" + schema + "'."); - } - - const nlohmann::json& detector_order_json = - required_layout_field(document, path, "detector_order"); - if (!detector_order_json.is_string()) { - throw gari_layout_error(path, "field 'detector_order' must be a string, but is " + - std::string(detector_order_json.type_name()) + "."); - } - std::string detector_order = detector_order_json.get(); - if (detector_order != kGariDetectorOrder) { - throw gari_layout_error(path, "field 'detector_order' must be '" + - std::string(kGariDetectorOrder) + "', but is '" + - detector_order + "'."); + size_t source_detector_count = read_layout_size(document.at("source_detector_count"), path); + const auto& mapping = document.at("source_to_gari"); + if (read_layout_size(document.at("gari_detector_count"), path) != gari_detector_count || + gari_detector_count < source_detector_count || !mapping.is_array() || + mapping.size() != source_detector_count) { + throw gari_layout_error(path, "detector counts do not agree with the mapping."); } - size_t source_detector_count = - read_layout_size(required_layout_field(document, path, "source_detector_count"), path, - "source_detector_count"); - size_t gari_detector_count = read_layout_size( - required_layout_field(document, path, "gari_detector_count"), path, "gari_detector_count"); - if (gari_detector_count < source_detector_count) { - throw gari_layout_error(path, "field 'gari_detector_count' must be at least " + - std::to_string(source_detector_count) + ", but is " + - std::to_string(gari_detector_count) + "."); - } - - const nlohmann::json& mapping_json = required_layout_field(document, path, "source_to_gari"); - if (!mapping_json.is_array()) { - throw gari_layout_error(path, "field 'source_to_gari' must be an array, but is " + - std::string(mapping_json.type_name()) + "."); - } - if (mapping_json.size() != source_detector_count) { - throw gari_layout_error(path, "field 'source_to_gari' must contain " + - std::to_string(source_detector_count) + " entries, but has " + - std::to_string(mapping_json.size()) + "."); - } - - std::vector source_to_gari; - source_to_gari.reserve(source_detector_count); - std::unordered_map target_to_source; - for (size_t source = 0; source < mapping_json.size(); ++source) { - std::string field = "source_to_gari[" + std::to_string(source) + "]"; - size_t target = read_layout_size(mapping_json[source], path, field); - if (target >= gari_detector_count) { - throw gari_layout_error(path, "field '" + field + "' is " + std::to_string(target) + - ", outside the target range [0, " + - std::to_string(gari_detector_count) + ")."); - } - if (target >= source_detector_count) { - throw gari_layout_error(path, "field '" + field + "' is " + std::to_string(target) + - ", outside the physical detector range [0, " + - std::to_string(source_detector_count) + ") required by '" + - std::string(kGariDetectorOrder) + "'."); - } - auto [previous, inserted] = target_to_source.emplace(target, source); - if (!inserted) { - throw gari_layout_error(path, "source detectors " + std::to_string(previous->second) + - " and " + std::to_string(source) + - " both map to GARI detector " + std::to_string(target) + - "; v1 layouts require an injective mapping."); - } - source_to_gari.push_back(target); + GariLayout layout; + std::vector used(source_detector_count); + layout.source_to_gari.reserve(source_detector_count); + for (const auto& entry : mapping) { + size_t target = read_layout_size(entry, path); + if (target >= source_detector_count || used[target]) { + throw gari_layout_error(path, "source_to_gari must permute the physical detector rows."); + } + used[target] = true; + layout.source_to_gari.push_back(target); } - return {schema, detector_order, source_detector_count, gari_detector_count, - std::move(source_to_gari)}; + return layout; } std::vector map_gari_hits(std::vector source_hits, const GariLayout& layout, const std::string& path) { for (uint64_t& source : source_hits) { if (source >= layout.source_to_gari.size()) { - throw gari_layout_error(path, "source detector index " + std::to_string(source) + - " is outside the source range [0, " + - std::to_string(layout.source_detector_count) + ")."); + throw gari_layout_error(path, "source detector index is out of range."); } source = layout.source_to_gari[source]; } @@ -201,10 +114,6 @@ struct Args { std::string circuit_path; std::string dem_path; std::string gari_layout_path; - std::string gari_layout_schema; - std::string gari_detector_order; - size_t gari_source_detector_count = 0; - size_t gari_detector_count = 0; bool no_merge_errors = false; // Manifold orientation options @@ -282,9 +191,6 @@ struct Args { } explicit_det_order = det_order_flags > 0 || program.is_used("--num-det-orders") || program.is_used("--det-order-seed"); - if (!gari_layout_path.empty() && program.is_used("--num-det-orders") && num_det_orders == 0) { - throw std::invalid_argument("--num-det-orders must be at least 1 with --gari-layout."); - } int num_data_sources = int(sample_num_shots > 0) + int(!in_fname.empty()); if (num_data_sources != 1) { @@ -395,45 +301,21 @@ struct Args { std::optional gari_layout; if (!gari_layout_path.empty()) { - gari_layout = load_gari_layout(gari_layout_path); - size_t target_count = config.dem.count_detectors(); - if (target_count != gari_layout->gari_detector_count) { - throw gari_layout_error( - gari_layout_path, "field 'gari_detector_count' is " + - std::to_string(gari_layout->gari_detector_count) + ", but DEM '" + - dem_path + "' contains " + std::to_string(target_count) + - " detectors."); - } - if (!circuit_path.empty()) { + gari_layout = load_gari_layout(gari_layout_path, config.dem.count_detectors()); + if (sample_num_shots > 0) { size_t source_count = circuit.count_detectors(); - if (source_count != gari_layout->source_detector_count) { + if (source_count != gari_layout->source_to_gari.size()) { throw gari_layout_error(gari_layout_path, - "field 'source_detector_count' is " + - std::to_string(gari_layout->source_detector_count) + - ", but circuit '" + circuit_path + "' contains " + - std::to_string(source_count) + " detectors."); + "source_detector_count does not match the sampled circuit."); } - size_t circuit_observable_count = circuit.count_observables(); - size_t dem_observable_count = config.dem.count_observables(); - if (circuit_observable_count != dem_observable_count) { - throw gari_layout_error(gari_layout_path, "circuit '" + circuit_path + "' contains " + - std::to_string(circuit_observable_count) + - " observables, but DEM '" + dem_path + - "' contains " + - std::to_string(dem_observable_count) + "."); + if (circuit.count_observables() != config.dem.count_observables()) { + throw gari_layout_error(gari_layout_path, + "the circuit and DEM observable counts differ."); } } - gari_layout_schema = gari_layout->schema; - gari_detector_order = gari_layout->detector_order; - gari_source_detector_count = gari_layout->source_detector_count; - gari_detector_count = gari_layout->gari_detector_count; - } else if (!circuit_path.empty() and !dem_path.empty() and + } else if (sample_num_shots > 0 and !dem_path.empty() and circuit.count_detectors() != config.dem.count_detectors()) { - throw std::invalid_argument( - "Circuit '" + circuit_path + "' contains " + std::to_string(circuit.count_detectors()) + - " detectors, but DEM '" + dem_path + "' contains " + - std::to_string(config.dem.count_detectors()) + - ". Supply --gari-layout when the source and target detector layouts differ."); + throw std::invalid_argument("Circuit and DEM detector counts differ; supply --gari-layout."); } // Sample orientations of the error model to use for the det priority @@ -450,10 +332,8 @@ struct Args { std::cout << ")" << std::endl; } } - if (gari_layout && !explicit_det_order) { - config.det_orders.assign(1, std::vector(config.dem.count_detectors())); - std::iota(config.det_orders[0].begin(), config.det_orders[0].end(), 0); - } else { + // An empty order list makes Tesseract traverse GARI rows in physical-then-virtual order. + if (!gari_layout || explicit_det_order) { DetOrder order = DetOrder::DetIndex; if (det_order_bfs) { order = DetOrder::DetBFS; @@ -476,18 +356,15 @@ struct Args { shots.resize(sample_num_shots); for (size_t k = 0; k < sample_num_shots; k++) { shots[k].obs_mask = obs_T[k]; - std::vector source_hits; for (size_t d = 0; d < num_detectors; d++) { if (dets[d][k]) { - source_hits.push_back(d); + shots[k].hits.push_back(d); } } if (gari_layout) { // The GARI matrix augments the physical syndrome with zero-valued virtual constraints. // Sparse shots contain mapped physical hits only, so virtual rows remain zero. - shots[k].hits = map_gari_hits(std::move(source_hits), *gari_layout, gari_layout_path); - } else { - shots[k].hits = std::move(source_hits); + shots[k].hits = map_gari_hits(std::move(shots[k].hits), *gari_layout, gari_layout_path); } } } @@ -500,7 +377,7 @@ struct Args { } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); size_t source_detector_count = - gari_layout ? gari_layout->source_detector_count : config.dem.count_detectors(); + gari_layout ? gari_layout->source_to_gari.size() : config.dem.count_detectors(); auto reader = stim::MeasureRecordReader::make( shots_file, shots_in_format.id, 0, source_detector_count, append_observables * config.dem.count_observables()); @@ -593,10 +470,9 @@ int main(int argc, char* argv[]) { program.add_argument("--dem").help("Stim dem file path").store_into(args.dem_path); program.add_argument("--gari-layout") .help( - "JSON layout emitted by gari_convert. Maps detector data from the original circuit or " - "source shot file into the supplied GARI matrix file. Unmapped virtual detector " - "rows are treated as zero. A physical_then_virtual layout uses its row order as the " - "default Tesseract detector traversal; explicit detector-order options override it.") + "Companion JSON layout for a GARI matrix file. Maps source detector data into GARI " + "rows; virtual rows remain zero. Detector-order options override the default GARI row " + "order.") .metavar("FILE") .default_value(std::string("")) .store_into(args.gari_layout_path); @@ -614,8 +490,8 @@ int main(int argc, char* argv[]) { .store_into(args.det_order_bfs); program.add_argument("--det-order-index") .help( - "Randomly choose increasing or decreasing detector index order. Without a GARI layout, " - "this is the default method; with one, this flag overrides its identity traversal.") + "Randomly choose increasing or decreasing detector index order " + "(default if no method specified)") .flag() .store_into(args.det_order_index); program.add_argument("--det-order-coordinate") @@ -668,10 +544,7 @@ int main(int argc, char* argv[]) { .default_value(size_t(0)) .store_into(args.shot_range_end); program.add_argument("--in") - .help( - "File to read detection events (and possibly observable flips) from. Without --circuit " - "or --gari-layout, detector data is assumed to use the supplied DEM's target detector " - "layout.") + .help("File to read detection events (and possibly observable flips) from") .metavar("filename") .default_value(std::string("")) .store_into(args.in_fname); @@ -798,16 +671,11 @@ int main(int argc, char* argv[]) { std::cerr << program; return EXIT_FAILURE; } + args.validate(program); TesseractConfig config; std::vector shots; std::unique_ptr writer; - try { - args.validate(program); - args.extract(config, shots, writer); - } catch (const std::exception& err) { - std::cerr << err.what() << std::endl; - return EXIT_FAILURE; - } + args.extract(config, shots, writer); size_t num_observables = config.dem.count_observables(); std::vector> obs_predicted(shots.size(), stim::simd_bits<64>(num_observables)); @@ -941,14 +809,6 @@ int main(int argc, char* argv[]) { {"sparsify_max_degree", args.sparsify_max_degree}, {"sparsify_reactivate_limit", effective_sparsify_reactivate_limit}}; - if (!args.gari_layout_path.empty()) { - stats_json["gari_layout_path"] = args.gari_layout_path; - stats_json["gari_layout_schema"] = args.gari_layout_schema; - stats_json["gari_layout_detector_order"] = args.gari_detector_order; - stats_json["source_detector_count"] = args.gari_source_detector_count; - stats_json["gari_detector_count"] = args.gari_detector_count; - } - if (args.stats_out_fname == "-") { std::cout << stats_json << std::endl; print_final_stats = false; From f02bbeff149c029c6b2e08598553989705a3171f Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Mon, 3 Aug 2026 12:58:32 -0700 Subject: [PATCH 08/31] Refactor GARI layout integration --- CMakeLists.txt | 5 +- src/BUILD | 1 + src/simplex_main.cc | 112 +++++------------------------------------ src/tesseract.test.cc | 48 ++++++++++++++++++ src/tesseract_main.cc | 114 ++++++------------------------------------ src/utils.cc | 88 ++++++++++++++++++++++++++++++++ src/utils.h | 14 ++++++ 7 files changed, 181 insertions(+), 201 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 23e560f4..027551b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,7 +86,10 @@ target_link_libraries(common PUBLIC libstim Threads::Threads) add_library(utils ${TESSERACT_SRC_DIR}/utils.cc ${TESSERACT_SRC_DIR}/utils.h) target_include_directories(utils PUBLIC ${TESSERACT_SRC_DIR}) target_compile_options(utils PRIVATE ${OPT_COPTS}) -target_link_libraries(utils PUBLIC common libstim Threads::Threads) +target_link_libraries(utils + PUBLIC common libstim Threads::Threads + PRIVATE nlohmann_json::nlohmann_json +) add_library(visualization ${TESSERACT_SRC_DIR}/visualization.cc ${TESSERACT_SRC_DIR}/visualization.h) target_include_directories(visualization PUBLIC ${TESSERACT_SRC_DIR}) diff --git a/src/BUILD b/src/BUILD index 9aa162b2..508ce957 100644 --- a/src/BUILD +++ b/src/BUILD @@ -110,6 +110,7 @@ cc_library( visibility = ["//visibility:public"], deps = [ ":libcommon", + "@nlohmann_json//:json", "@stim//:stim_lib", ], ) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 039143c0..2ece6a8b 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -12,11 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include #include #include #include -#include #include #include #include @@ -27,84 +25,6 @@ #include "stim.h" #include "utils.h" -namespace { - -constexpr char kGariLayoutSchema[] = "tesseract.gari_layout.v1"; - -struct GariLayout { - std::vector source_to_gari; -}; - -std::invalid_argument gari_layout_error(const std::string& path, const std::string& detail) { - return std::invalid_argument("Invalid GARI layout '" + path + "': " + detail); -} - -size_t read_layout_size(const nlohmann::json& value, const std::string& path) { - if (!value.is_number_integer()) { - throw gari_layout_error(path, "detector counts and mapping entries must be integers."); - } - if (value.is_number_unsigned()) { - uint64_t result = value.get(); - if (result > std::numeric_limits::max()) { - throw gari_layout_error(path, "integer is too large."); - } - return static_cast(result); - } - int64_t result = value.get(); - if (result < 0) { - throw gari_layout_error(path, "detector counts and mapping entries must be nonnegative."); - } - return static_cast(result); -} - -GariLayout load_gari_layout(const std::string& path, size_t gari_detector_count) { - std::ifstream input(path); - if (!input.is_open()) { - throw std::invalid_argument("Could not open GARI layout: " + path); - } - - nlohmann::json document; - input >> document; - if (document.at("schema").get() != kGariLayoutSchema) { - throw gari_layout_error(path, "unsupported schema."); - } - - size_t source_detector_count = read_layout_size(document.at("source_detector_count"), path); - const auto& mapping = document.at("source_to_gari"); - if (read_layout_size(document.at("gari_detector_count"), path) != gari_detector_count || - gari_detector_count < source_detector_count || !mapping.is_array() || - mapping.size() != source_detector_count) { - throw gari_layout_error(path, "detector counts do not agree with the mapping."); - } - - GariLayout layout; - std::vector used(gari_detector_count); - layout.source_to_gari.reserve(source_detector_count); - for (const auto& entry : mapping) { - size_t target = read_layout_size(entry, path); - if (target >= gari_detector_count || used[target]) { - throw gari_layout_error(path, "source_to_gari must contain distinct GARI detector rows."); - } - used[target] = true; - layout.source_to_gari.push_back(target); - } - return layout; -} - -std::vector map_gari_hits(std::vector source_hits, const GariLayout& layout, - const std::string& path) { - for (uint64_t& source : source_hits) { - if (source >= layout.source_to_gari.size()) { - throw gari_layout_error(path, "source detector index is out of range."); - } - source = layout.source_to_gari[source]; - } - std::sort(source_hits.begin(), source_hits.end()); - return source_hits; -} - -} // namespace - struct Args { std::string circuit_path; std::string dem_path; @@ -258,16 +178,8 @@ struct Args { std::optional gari_layout; if (!gari_layout_path.empty()) { gari_layout = load_gari_layout(gari_layout_path, config.dem.count_detectors()); - if (sample_num_shots > 0) { - size_t source_count = circuit.count_detectors(); - if (source_count != gari_layout->source_to_gari.size()) { - throw gari_layout_error(gari_layout_path, - "source_detector_count does not match the sampled circuit."); - } - if (circuit.count_observables() != config.dem.count_observables()) { - throw gari_layout_error(gari_layout_path, - "the circuit and DEM observable counts differ."); - } + if (!circuit_path.empty()) { + gari_layout->validate_source(circuit, config.dem); } } else if (sample_num_shots > 0 and !dem_path.empty() and circuit.count_detectors() != config.dem.count_detectors()) { @@ -289,11 +201,6 @@ struct Args { shots[k].hits.push_back(d); } } - if (gari_layout) { - // The GARI matrix augments the physical syndrome with zero-valued virtual constraints. - // Sparse shots contain mapped physical hits only, so virtual rows remain zero. - shots[k].hits = map_gari_hits(std::move(shots[k].hits), *gari_layout, gari_layout_path); - } } } @@ -305,7 +212,7 @@ struct Args { } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); size_t source_detector_count = - gari_layout ? gari_layout->source_to_gari.size() : config.dem.count_detectors(); + gari_layout ? gari_layout->source_detector_count() : config.dem.count_detectors(); auto reader = stim::MeasureRecordReader::make( shots_file, shots_in_format.id, 0, source_detector_count, append_observables * config.dem.count_observables()); @@ -314,16 +221,18 @@ struct Args { stim::SparseShot sparse_shot; sparse_shot.clear(); while (reader->start_and_read_entire_record(sparse_shot)) { - if (gari_layout) { - sparse_shot.hits = - map_gari_hits(std::move(sparse_shot.hits), *gari_layout, gari_layout_path); - } shots.push_back(sparse_shot); sparse_shot.clear(); } fclose(shots_file); } + if (gari_layout) { + for (auto& shot : shots) { + gari_layout->map_hits(shot.hits); + } + } + // Load observable flips, if applicable if (!obs_in_fname.empty()) { FILE* obs_file = fopen(obs_in_fname.c_str(), "r"); @@ -637,6 +546,9 @@ int main(int argc, char* argv[]) { {"num_errors", has_obs ? nlohmann::json(num_errors) : nullptr}, {"num_shots", shot}, {"sample_num_shots", args.sample_num_shots}}; + stats_json["gari_layout_path"] = args.gari_layout_path.empty() + ? nlohmann::json(nullptr) + : nlohmann::json(args.gari_layout_path); if (args.stats_out_fname == "-") { std::cout << stats_json << std::endl; diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index fd0d471e..4f5ab325 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -15,7 +15,9 @@ #include "tesseract.h" #include +#include #include +#include #include #include "gtest/gtest.h" @@ -559,3 +561,49 @@ TEST(tesseract, MoreThan64Observables) { ASSERT_EQ(flipped[i], i); } } + +static std::string write_gari_layout(const std::string& text) { + std::string path = testing::TempDir() + "gari_layout_test.json"; + std::ofstream(path) << text; + return path; +} + +TEST(utils, GariLayoutMapsAndValidatesSource) { + std::string path = + write_gari_layout(R"({"schema":"tesseract.gari_layout.v1","source_detector_count":2,)" + R"("gari_detector_count":4,"source_to_gari":[1,0],)" + R"("detector_order":"physical_then_virtual"})"); + GariLayout layout = load_gari_layout(path, 4); + std::vector hits{0, 0, 1}; + layout.map_hits(hits); + EXPECT_EQ(hits, (std::vector{0, 1, 1})); + + stim::Circuit circuit( + "M 0 1\nDETECTOR rec[-1]\nDETECTOR rec[-2]\n" + "OBSERVABLE_INCLUDE(0) rec[-1]"); + stim::DetectorErrorModel dem("error(0.1) D0 D1 D2 D3 L0"); + EXPECT_NO_THROW(layout.validate_source(circuit, dem)); + EXPECT_THROW(layout.validate_source(stim::Circuit("M 0\nDETECTOR rec[-1]"), dem), + std::invalid_argument); +} + +TEST(utils, GariLayoutRejectsInvalidV1) { + const std::string valid = R"({"schema":"tesseract.gari_layout.v1","source_detector_count":2,)" + R"("gari_detector_count":4,"source_to_gari":[1,0],)" + R"("detector_order":"physical_then_virtual"})"; + auto replacing = [&](const std::string& from, const std::string& to) { + std::string result = valid; + result.replace(result.find(from), from.size(), to); + return result; + }; + for (const std::string& text : + {replacing("[1,0]", "[0,3]"), replacing("[1,0]", "[0,0]"), std::string("{")}) { + std::string path = write_gari_layout(text); + try { + load_gari_layout(path, 4); + FAIL() << "Invalid layout was accepted."; + } catch (const std::invalid_argument& ex) { + EXPECT_NE(std::string(ex.what()).find(path), std::string::npos); + } + } +} diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index 5e9c3f7d..bd9b2fb5 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -30,86 +30,6 @@ #include "tesseract.h" #include "utils.h" -namespace { - -constexpr char kGariLayoutSchema[] = "tesseract.gari_layout.v1"; -constexpr char kGariDetectorOrder[] = "physical_then_virtual"; - -struct GariLayout { - std::vector source_to_gari; -}; - -std::invalid_argument gari_layout_error(const std::string& path, const std::string& detail) { - return std::invalid_argument("Invalid GARI layout '" + path + "': " + detail); -} - -size_t read_layout_size(const nlohmann::json& value, const std::string& path) { - if (!value.is_number_integer()) { - throw gari_layout_error(path, "detector counts and mapping entries must be integers."); - } - if (value.is_number_unsigned()) { - uint64_t result = value.get(); - if (result > std::numeric_limits::max()) { - throw gari_layout_error(path, "integer is too large."); - } - return static_cast(result); - } - int64_t result = value.get(); - if (result < 0) { - throw gari_layout_error(path, "detector counts and mapping entries must be nonnegative."); - } - return static_cast(result); -} - -GariLayout load_gari_layout(const std::string& path, size_t gari_detector_count) { - std::ifstream input(path); - if (!input.is_open()) { - throw std::invalid_argument("Could not open GARI layout: " + path); - } - - nlohmann::json document; - input >> document; - if (document.at("schema").get() != kGariLayoutSchema || - document.at("detector_order").get() != kGariDetectorOrder) { - throw gari_layout_error(path, "unsupported schema or detector order."); - } - - size_t source_detector_count = read_layout_size(document.at("source_detector_count"), path); - const auto& mapping = document.at("source_to_gari"); - if (read_layout_size(document.at("gari_detector_count"), path) != gari_detector_count || - gari_detector_count < source_detector_count || !mapping.is_array() || - mapping.size() != source_detector_count) { - throw gari_layout_error(path, "detector counts do not agree with the mapping."); - } - - GariLayout layout; - std::vector used(source_detector_count); - layout.source_to_gari.reserve(source_detector_count); - for (const auto& entry : mapping) { - size_t target = read_layout_size(entry, path); - if (target >= source_detector_count || used[target]) { - throw gari_layout_error(path, "source_to_gari must permute the physical detector rows."); - } - used[target] = true; - layout.source_to_gari.push_back(target); - } - return layout; -} - -std::vector map_gari_hits(std::vector source_hits, const GariLayout& layout, - const std::string& path) { - for (uint64_t& source : source_hits) { - if (source >= layout.source_to_gari.size()) { - throw gari_layout_error(path, "source detector index is out of range."); - } - source = layout.source_to_gari[source]; - } - std::sort(source_hits.begin(), source_hits.end()); - return source_hits; -} - -} // namespace - struct Args { std::string circuit_path; std::string dem_path; @@ -302,16 +222,8 @@ struct Args { std::optional gari_layout; if (!gari_layout_path.empty()) { gari_layout = load_gari_layout(gari_layout_path, config.dem.count_detectors()); - if (sample_num_shots > 0) { - size_t source_count = circuit.count_detectors(); - if (source_count != gari_layout->source_to_gari.size()) { - throw gari_layout_error(gari_layout_path, - "source_detector_count does not match the sampled circuit."); - } - if (circuit.count_observables() != config.dem.count_observables()) { - throw gari_layout_error(gari_layout_path, - "the circuit and DEM observable counts differ."); - } + if (!circuit_path.empty()) { + gari_layout->validate_source(circuit, config.dem); } } else if (sample_num_shots > 0 and !dem_path.empty() and circuit.count_detectors() != config.dem.count_detectors()) { @@ -361,11 +273,6 @@ struct Args { shots[k].hits.push_back(d); } } - if (gari_layout) { - // The GARI matrix augments the physical syndrome with zero-valued virtual constraints. - // Sparse shots contain mapped physical hits only, so virtual rows remain zero. - shots[k].hits = map_gari_hits(std::move(shots[k].hits), *gari_layout, gari_layout_path); - } } } @@ -377,7 +284,7 @@ struct Args { } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); size_t source_detector_count = - gari_layout ? gari_layout->source_to_gari.size() : config.dem.count_detectors(); + gari_layout ? gari_layout->source_detector_count() : config.dem.count_detectors(); auto reader = stim::MeasureRecordReader::make( shots_file, shots_in_format.id, 0, source_detector_count, append_observables * config.dem.count_observables()); @@ -386,16 +293,18 @@ struct Args { stim::SparseShot sparse_shot; sparse_shot.clear(); while (reader->start_and_read_entire_record(sparse_shot)) { - if (gari_layout) { - sparse_shot.hits = - map_gari_hits(std::move(sparse_shot.hits), *gari_layout, gari_layout_path); - } shots.push_back(sparse_shot); sparse_shot.clear(); } fclose(shots_file); } + if (gari_layout) { + for (auto& shot : shots) { + gari_layout->map_hits(shot.hits); + } + } + // Load observable flips, if applicable if (!obs_in_fname.empty()) { FILE* obs_file = fopen(obs_in_fname.c_str(), "r"); @@ -806,6 +715,11 @@ int main(int argc, char* argv[]) { {"sparsify_base_degree", args.sparsify_base_degree}, {"sparsify_max_degree", args.sparsify_max_degree}, {"sparsify_reactivate_limit", effective_sparsify_reactivate_limit}}; + stats_json["gari_layout_path"] = args.gari_layout_path.empty() + ? nlohmann::json(nullptr) + : nlohmann::json(args.gari_layout_path); + stats_json["gari_default_detector_order_used"] = + !args.gari_layout_path.empty() && !args.explicit_det_order; if (args.stats_out_fname == "-") { std::cout << stats_json << std::endl; diff --git a/src/utils.cc b/src/utils.cc index 56115d4b..7de2118f 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include #include #include @@ -27,6 +29,92 @@ #include "common.h" #include "stim.h" +static std::invalid_argument gari_layout_error(const std::string& path, const std::string& detail) { + return std::invalid_argument("Invalid GARI layout '" + path + "': " + detail); +} + +static size_t read_gari_layout_size(const nlohmann::json& value, const std::string& path) { + if (!value.is_number_integer()) { + throw gari_layout_error(path, "detector counts and mapping entries must be integers."); + } + if (value.is_number_unsigned()) { + uint64_t result = value.get(); + if (result > std::numeric_limits::max()) { + throw gari_layout_error(path, "integer is too large."); + } + return static_cast(result); + } + int64_t result = value.get(); + if (result < 0) { + throw gari_layout_error(path, "detector counts and mapping entries must be nonnegative."); + } + return static_cast(result); +} + +GariLayout load_gari_layout(const std::string& path, size_t expected_gari_detector_count) { + std::ifstream input(path); + if (!input.is_open()) { + throw std::invalid_argument("Could not open GARI layout: " + path); + } + + try { + nlohmann::json document; + input >> document; + if (document.at("schema").get() != "tesseract.gari_layout.v1") { + throw gari_layout_error(path, "unsupported schema."); + } + if (document.at("detector_order").get() != "physical_then_virtual") { + throw gari_layout_error(path, "unsupported detector order."); + } + + size_t source_detector_count = + read_gari_layout_size(document.at("source_detector_count"), path); + size_t gari_detector_count = read_gari_layout_size(document.at("gari_detector_count"), path); + const auto& mapping = document.at("source_to_gari"); + if (gari_detector_count != expected_gari_detector_count || + source_detector_count > gari_detector_count || !mapping.is_array() || + mapping.size() != source_detector_count) { + throw gari_layout_error(path, "detector counts do not agree with the mapping."); + } + + GariLayout layout; + layout.path = path; + std::vector used(source_detector_count); + layout.source_to_gari.reserve(source_detector_count); + for (const auto& entry : mapping) { + size_t target = read_gari_layout_size(entry, path); + if (target >= source_detector_count || used[target]) { + throw gari_layout_error(path, "source_to_gari must permute the physical detector rows."); + } + used[target] = true; + layout.source_to_gari.push_back(target); + } + return layout; + } catch (const nlohmann::json::exception& ex) { + throw gari_layout_error(path, ex.what()); + } +} + +void GariLayout::map_hits(std::vector& hits) const { + for (uint64_t& source : hits) { + if (source >= source_to_gari.size()) { + throw gari_layout_error(path, "source detector index is out of range."); + } + source = source_to_gari[source]; + } + std::sort(hits.begin(), hits.end()); +} + +void GariLayout::validate_source(const stim::Circuit& circuit, + const stim::DetectorErrorModel& gari_dem) const { + if (circuit.count_detectors() != source_detector_count()) { + throw gari_layout_error(path, "source_detector_count does not match the circuit."); + } + if (circuit.count_observables() != gari_dem.count_observables()) { + throw gari_layout_error(path, "the circuit and DEM observable counts differ."); + } +} + std::vector> get_detector_coords(const stim::DetectorErrorModel& dem) { std::vector> detector_coords; for (const stim::DemInstruction& instruction : dem.flattened().instructions) { diff --git a/src/utils.h b/src/utils.h index 3c5d6569..79a55929 100644 --- a/src/utils.h +++ b/src/utils.h @@ -44,6 +44,20 @@ std::vector> build_det_orders(const stim::DetectorErrorModel DetOrder method = DetOrder::DetIndex, uint64_t seed = 0); +struct GariLayout { + std::string path; + std::vector source_to_gari; + + size_t source_detector_count() const { + return source_to_gari.size(); + } + void map_hits(std::vector& hits) const; + void validate_source(const stim::Circuit& circuit, + const stim::DetectorErrorModel& gari_dem) const; +}; + +GariLayout load_gari_layout(const std::string& path, size_t expected_gari_detector_count); + const double INF = std::numeric_limits::infinity(); bool sampling_from_dem(uint64_t seed, size_t num_shots, stim::DetectorErrorModel dem, From 72ee32fa58bb22c169330e7df4c0ec3767e3f4f0 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 6 Aug 2026 12:10:27 -0700 Subject: [PATCH 09/31] Simplify GARI CLI integration --- src/simplex_main.cc | 19 ++------------- src/tesseract.test.cc | 22 ++++++++++++++--- src/tesseract_main.cc | 30 +++++++++-------------- src/utils.cc | 57 +++++++++++++++++++++++++++++++++++++++++++ src/utils.h | 8 ++++++ 5 files changed, 97 insertions(+), 39 deletions(-) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 2ece6a8b..680a07b0 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -188,20 +188,7 @@ struct Args { if (sample_num_shots > 0) { assert(!circuit_path.empty()); - std::mt19937_64 rng(sample_seed); - size_t num_detectors = circuit.count_detectors(); - const auto [dets, obs] = - stim::sample_batch_detection_events<64>(circuit, sample_num_shots, rng); - stim::simd_bit_table<64> obs_T = obs.transposed(); - shots.resize(sample_num_shots); - for (size_t k = 0; k < sample_num_shots; k++) { - shots[k].obs_mask = obs_T[k]; - for (size_t d = 0; d < num_detectors; d++) { - if (dets[d][k]) { - shots[k].hits.push_back(d); - } - } - } + sample_shots(sample_seed, circuit, sample_num_shots, shots); } if (!in_fname.empty()) { @@ -228,9 +215,7 @@ struct Args { } if (gari_layout) { - for (auto& shot : shots) { - gari_layout->map_hits(shot.hits); - } + gari_layout->map_shots(shots); } // Load observable flips, if applicable diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index 4f5ab325..0de63298 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -574,9 +574,10 @@ TEST(utils, GariLayoutMapsAndValidatesSource) { R"("gari_detector_count":4,"source_to_gari":[1,0],)" R"("detector_order":"physical_then_virtual"})"); GariLayout layout = load_gari_layout(path, 4); - std::vector hits{0, 0, 1}; - layout.map_hits(hits); - EXPECT_EQ(hits, (std::vector{0, 1, 1})); + std::vector shots(1); + shots[0].hits = {0, 0, 1}; + layout.map_shots(shots); + EXPECT_EQ(shots[0].hits, (std::vector{0, 1, 1})); stim::Circuit circuit( "M 0 1\nDETECTOR rec[-1]\nDETECTOR rec[-2]\n" @@ -585,6 +586,21 @@ TEST(utils, GariLayoutMapsAndValidatesSource) { EXPECT_NO_THROW(layout.validate_source(circuit, dem)); EXPECT_THROW(layout.validate_source(stim::Circuit("M 0\nDETECTOR rec[-1]"), dem), std::invalid_argument); + + stim::DetectorErrorModel source_dem = stim::ErrorAnalyzer::circuit_to_detector_error_model( + circuit, false, true, true, 1, false, false); + for (DetOrder method : {DetOrder::DetIndex, DetOrder::DetBFS, DetOrder::DetCoordinate}) { + auto source_orders = build_det_orders(source_dem, 2, method, 5); + auto gari_orders = build_gari_detector_orders(circuit, layout, 2, method, 5); + for (size_t order = 0; order < source_orders.size(); ++order) { + for (size_t position = 0; position < layout.source_detector_count(); ++position) { + EXPECT_EQ(gari_orders[order][position], + layout.source_to_gari[source_orders[order][position]]); + } + EXPECT_EQ(gari_orders[order][2], 2); + EXPECT_EQ(gari_orders[order][3], 3); + } + } } TEST(utils, GariLayoutRejectsInvalidV1) { diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index bd9b2fb5..65162e01 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -111,6 +111,10 @@ struct Args { } explicit_det_order = det_order_flags > 0 || program.is_used("--num-det-orders") || program.is_used("--det-order-seed"); + if (!gari_layout_path.empty() && explicit_det_order && num_det_orders > 0 && + circuit_path.empty()) { + throw std::invalid_argument("GARI detector ordering requires --circuit."); + } int num_data_sources = int(sample_num_shots > 0) + int(!in_fname.empty()); if (num_data_sources != 1) { @@ -254,26 +258,16 @@ struct Args { } 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 = + gari_layout ? build_gari_detector_orders(circuit, *gari_layout, num_det_orders, order, + det_order_seed) + : build_det_orders(config.dem, num_det_orders, order, det_order_seed); } } if (sample_num_shots > 0) { assert(!circuit_path.empty()); - std::mt19937_64 rng(sample_seed); - size_t num_detectors = circuit.count_detectors(); - const auto [dets, obs] = - stim::sample_batch_detection_events<64>(circuit, sample_num_shots, rng); - stim::simd_bit_table<64> obs_T = obs.transposed(); - shots.resize(sample_num_shots); - for (size_t k = 0; k < sample_num_shots; k++) { - shots[k].obs_mask = obs_T[k]; - for (size_t d = 0; d < num_detectors; d++) { - if (dets[d][k]) { - shots[k].hits.push_back(d); - } - } - } + sample_shots(sample_seed, circuit, sample_num_shots, shots); } if (!in_fname.empty()) { @@ -300,9 +294,7 @@ struct Args { } if (gari_layout) { - for (auto& shot : shots) { - gari_layout->map_hits(shot.hits); - } + gari_layout->map_shots(shots); } // Load observable flips, if applicable @@ -719,7 +711,7 @@ int main(int argc, char* argv[]) { ? nlohmann::json(nullptr) : nlohmann::json(args.gari_layout_path); stats_json["gari_default_detector_order_used"] = - !args.gari_layout_path.empty() && !args.explicit_det_order; + !args.gari_layout_path.empty() && config.det_orders.empty(); if (args.stats_out_fname == "-") { std::cout << stats_json << std::endl; diff --git a/src/utils.cc b/src/utils.cc index 7de2118f..9af481b9 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -79,6 +79,7 @@ GariLayout load_gari_layout(const std::string& path, size_t expected_gari_detect GariLayout layout; layout.path = path; + layout.gari_detector_count = gari_detector_count; std::vector used(source_detector_count); layout.source_to_gari.reserve(source_detector_count); for (const auto& entry : mapping) { @@ -105,6 +106,12 @@ void GariLayout::map_hits(std::vector& hits) const { std::sort(hits.begin(), hits.end()); } +void GariLayout::map_shots(std::vector& shots) const { + for (auto& shot : shots) { + map_hits(shot.hits); + } +} + void GariLayout::validate_source(const stim::Circuit& circuit, const stim::DetectorErrorModel& gari_dem) const { if (circuit.count_detectors() != source_detector_count()) { @@ -290,6 +297,56 @@ std::vector> build_det_orders(const stim::DetectorErrorModel throw std::invalid_argument("Unknown det order method"); } +std::vector> build_gari_detector_orders(const stim::Circuit& source_circuit, + const GariLayout& layout, + size_t num_det_orders, DetOrder method, + uint64_t seed) { + if (num_det_orders == 0) { + return {}; + } + if (source_circuit.count_detectors() != layout.source_detector_count()) { + throw gari_layout_error(layout.path, + "source_detector_count does not match the ordering circuit."); + } + if (layout.source_detector_count() > layout.gari_detector_count) { + throw gari_layout_error(layout.path, "source detector count exceeds GARI detector count."); + } + + std::vector> source_orders; + if (layout.source_detector_count() == 0) { + source_orders.resize(num_det_orders); + } else { + stim::DetectorErrorModel source_dem = stim::ErrorAnalyzer::circuit_to_detector_error_model( + source_circuit, /*decompose_errors=*/false, /*fold_loops=*/true, + /*allow_gauge_detectors=*/true, + /*approximate_disjoint_errors_threshold=*/1, + /*ignore_decomposition_failures=*/false, + /*block_decomposition_from_introducing_remnant_edges=*/false); + source_orders = build_det_orders(source_dem, num_det_orders, method, seed); + } + + std::vector> gari_orders; + gari_orders.reserve(source_orders.size()); + for (const auto& source_order : source_orders) { + if (source_order.size() != layout.source_detector_count()) { + throw gari_layout_error(layout.path, "source detector order has the wrong size."); + } + std::vector gari_order; + gari_order.reserve(layout.gari_detector_count); + // Existing orders contain source detector IDs. Map those IDs to physical + // GARI rows, then visit the virtual rows in their natural order. + for (size_t source : source_order) { + gari_order.push_back(layout.source_to_gari.at(source)); + } + for (size_t detector = layout.source_detector_count(); detector < layout.gari_detector_count; + ++detector) { + gari_order.push_back(detector); + } + gari_orders.push_back(std::move(gari_order)); + } + return gari_orders; +} + bool sampling_from_dem(uint64_t seed, size_t num_shots, stim::DetectorErrorModel dem, std::vector& shots) { stim::DemSampler sampler(dem, std::mt19937_64{seed}, num_shots); diff --git a/src/utils.h b/src/utils.h index 79a55929..d38feac3 100644 --- a/src/utils.h +++ b/src/utils.h @@ -46,18 +46,26 @@ std::vector> build_det_orders(const stim::DetectorErrorModel struct GariLayout { std::string path; + size_t gari_detector_count = 0; std::vector source_to_gari; size_t source_detector_count() const { return source_to_gari.size(); } void map_hits(std::vector& hits) const; + void map_shots(std::vector& shots) const; void validate_source(const stim::Circuit& circuit, const stim::DetectorErrorModel& gari_dem) const; }; GariLayout load_gari_layout(const std::string& path, size_t expected_gari_detector_count); +std::vector> build_gari_detector_orders(const stim::Circuit& source_circuit, + const GariLayout& layout, + size_t num_det_orders, + DetOrder method = DetOrder::DetIndex, + uint64_t seed = 0); + const double INF = std::numeric_limits::infinity(); bool sampling_from_dem(uint64_t seed, size_t num_shots, stim::DetectorErrorModel dem, From 06ac86c6fbd9583a16de4268b91f17b3d9cfdbfb Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 6 Aug 2026 12:17:38 -0700 Subject: [PATCH 10/31] Add GARI source-shot regression --- src/tesseract.test.cc | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index 0de63298..9be77f55 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -603,6 +603,41 @@ TEST(utils, GariLayoutMapsAndValidatesSource) { } } +TEST(utils, GariSourceShotRemappingDecodes) { + stim::Circuit circuit(R"STIM( + X_ERROR(1) 0 3 + M 0 1 2 3 4 5 + DETECTOR rec[-6] + DETECTOR rec[-5] + DETECTOR rec[-4] + DETECTOR rec[-3] + OBSERVABLE_INCLUDE(0) rec[-6] + OBSERVABLE_INCLUDE(1) rec[-2] + OBSERVABLE_INCLUDE(2) rec[-1] + )STIM"); + stim::DetectorErrorModel gari_dem(R"DEM( + error(0.1) D1 D2 L0 + error(0.1) D0 D3 + error(0.1) D4 L1 + error(0.1) D5 L2 + )DEM"); + GariLayout layout; + layout.gari_detector_count = 6; + layout.source_to_gari = {2, 0, 3, 1}; + + std::vector shots; + sample_shots(0, circuit, 1, shots); + ASSERT_EQ(shots.size(), 1); + EXPECT_EQ(shots[0].hits, (std::vector{0, 3})); + layout.map_shots(shots); + EXPECT_EQ(shots[0].hits, (std::vector{1, 2})); + + TesseractDecoder tesseract(TesseractConfig{gari_dem}); + SimplexDecoder simplex(SimplexConfig{gari_dem}); + EXPECT_EQ(tesseract.decode(shots[0].hits), (std::vector{0})); + EXPECT_EQ(simplex.decode(shots[0].hits), (std::vector{0})); +} + TEST(utils, GariLayoutRejectsInvalidV1) { const std::string valid = R"({"schema":"tesseract.gari_layout.v1","source_detector_count":2,)" R"("gari_detector_count":4,"source_to_gari":[1,0],)" From c7292570e16e801a2cef0f64e86543ce993d7bb4 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 6 Aug 2026 13:52:10 -0700 Subject: [PATCH 11/31] Keep GARI CLI changes focused --- CMakeLists.txt | 6 ++--- src/simplex_main.cc | 24 +++++++++++++------ src/tesseract_main.cc | 54 +++++++++++++++++++++++++------------------ 3 files changed, 50 insertions(+), 34 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 027551b7..4ca689a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,10 +86,8 @@ target_link_libraries(common PUBLIC libstim Threads::Threads) add_library(utils ${TESSERACT_SRC_DIR}/utils.cc ${TESSERACT_SRC_DIR}/utils.h) target_include_directories(utils PUBLIC ${TESSERACT_SRC_DIR}) target_compile_options(utils PRIVATE ${OPT_COPTS}) -target_link_libraries(utils - PUBLIC common libstim Threads::Threads - PRIVATE nlohmann_json::nlohmann_json -) +target_link_libraries(utils PUBLIC common libstim Threads::Threads) +target_link_libraries(utils PRIVATE nlohmann_json::nlohmann_json) add_library(visualization ${TESSERACT_SRC_DIR}/visualization.cc ${TESSERACT_SRC_DIR}/visualization.h) target_include_directories(visualization PUBLIC ${TESSERACT_SRC_DIR}) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 680a07b0..5d1d9302 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -181,14 +181,24 @@ struct Args { if (!circuit_path.empty()) { gari_layout->validate_source(circuit, config.dem); } - } else if (sample_num_shots > 0 and !dem_path.empty() and - circuit.count_detectors() != config.dem.count_detectors()) { - throw std::invalid_argument("Circuit and DEM detector counts differ; supply --gari-layout."); } if (sample_num_shots > 0) { assert(!circuit_path.empty()); - sample_shots(sample_seed, circuit, sample_num_shots, shots); + std::mt19937_64 rng(sample_seed); + size_t num_detectors = circuit.count_detectors(); + const auto [dets, obs] = + stim::sample_batch_detection_events<64>(circuit, sample_num_shots, rng); + stim::simd_bit_table<64> obs_T = obs.transposed(); + shots.resize(sample_num_shots); + for (size_t k = 0; k < sample_num_shots; k++) { + shots[k].obs_mask = obs_T[k]; + for (size_t d = 0; d < num_detectors; d++) { + if (dets[d][k]) { + shots[k].hits.push_back(d); + } + } + } } if (!in_fname.empty()) { @@ -531,9 +541,9 @@ int main(int argc, char* argv[]) { {"num_errors", has_obs ? nlohmann::json(num_errors) : nullptr}, {"num_shots", shot}, {"sample_num_shots", args.sample_num_shots}}; - stats_json["gari_layout_path"] = args.gari_layout_path.empty() - ? nlohmann::json(nullptr) - : nlohmann::json(args.gari_layout_path); + if (!args.gari_layout_path.empty()) { + stats_json["gari_layout_path"] = args.gari_layout_path; + } if (args.stats_out_fname == "-") { std::cout << stats_json << std::endl; diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index 65162e01..7236519b 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -229,9 +229,6 @@ struct Args { if (!circuit_path.empty()) { gari_layout->validate_source(circuit, config.dem); } - } else if (sample_num_shots > 0 and !dem_path.empty() and - circuit.count_detectors() != config.dem.count_detectors()) { - throw std::invalid_argument("Circuit and DEM detector counts differ; supply --gari-layout."); } // Sample orientations of the error model to use for the det priority @@ -248,26 +245,38 @@ struct Args { std::cout << ")" << std::endl; } } - // An empty order list makes Tesseract traverse GARI rows in physical-then-virtual order. - if (!gari_layout || explicit_det_order) { - 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; - } - config.det_orders = - gari_layout ? build_gari_detector_orders(circuit, *gari_layout, num_det_orders, order, - det_order_seed) - : build_det_orders(config.dem, num_det_orders, order, det_order_seed); + 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 (!gari_layout) { + config.det_orders = build_det_orders(config.dem, num_det_orders, order, det_order_seed); + } else if (explicit_det_order) { + config.det_orders = build_gari_detector_orders(circuit, *gari_layout, num_det_orders, order, + det_order_seed); } } if (sample_num_shots > 0) { assert(!circuit_path.empty()); - sample_shots(sample_seed, circuit, sample_num_shots, shots); + std::mt19937_64 rng(sample_seed); + size_t num_detectors = circuit.count_detectors(); + const auto [dets, obs] = + stim::sample_batch_detection_events<64>(circuit, sample_num_shots, rng); + stim::simd_bit_table<64> obs_T = obs.transposed(); + shots.resize(sample_num_shots); + for (size_t k = 0; k < sample_num_shots; k++) { + shots[k].obs_mask = obs_T[k]; + for (size_t d = 0; d < num_detectors; d++) { + if (dets[d][k]) { + shots[k].hits.push_back(d); + } + } + } } if (!in_fname.empty()) { @@ -707,11 +716,10 @@ int main(int argc, char* argv[]) { {"sparsify_base_degree", args.sparsify_base_degree}, {"sparsify_max_degree", args.sparsify_max_degree}, {"sparsify_reactivate_limit", effective_sparsify_reactivate_limit}}; - stats_json["gari_layout_path"] = args.gari_layout_path.empty() - ? nlohmann::json(nullptr) - : nlohmann::json(args.gari_layout_path); - stats_json["gari_default_detector_order_used"] = - !args.gari_layout_path.empty() && config.det_orders.empty(); + if (!args.gari_layout_path.empty()) { + stats_json["gari_layout_path"] = args.gari_layout_path; + stats_json["gari_default_detector_order_used"] = config.det_orders.empty(); + } if (args.stats_out_fname == "-") { std::cout << stats_json << std::endl; From f4dd173a1871331dc35bd519214d0a12f9b849a7 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 6 Aug 2026 16:18:43 -0700 Subject: [PATCH 12/31] Harden GARI source shot handling --- src/simplex_main.cc | 3 +++ src/tesseract.test.cc | 48 +++++++++++++++++++++++++++++++++++++++++++ src/tesseract_main.cc | 3 +++ 3 files changed, 54 insertions(+) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 5d1d9302..bd7ab824 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -181,6 +181,9 @@ struct Args { if (!circuit_path.empty()) { gari_layout->validate_source(circuit, config.dem); } + } else if (sample_num_shots > 0 and !dem_path.empty() and + circuit.count_detectors() != config.dem.count_detectors()) { + throw std::invalid_argument("Circuit and DEM detector counts differ; supply --gari-layout."); } if (sample_num_shots > 0) { diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index 9be77f55..6be8fe69 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -14,6 +14,7 @@ #include "tesseract.h" +#include #include #include #include @@ -638,6 +639,53 @@ TEST(utils, GariSourceShotRemappingDecodes) { EXPECT_EQ(simplex.decode(shots[0].hits), (std::vector{0})); } +TEST(utils, GariB8SourceWidthPreservesShotRecords) { + stim::DetectorErrorModel gari_dem(R"DEM( + error(0.1) D1 D2 L0 + error(0.1) D0 D5 L1 + detector D9 + )DEM"); + GariLayout layout = load_gari_layout( + write_gari_layout(R"({"schema":"tesseract.gari_layout.v1","source_detector_count":7,)" + R"("gari_detector_count":10,"source_to_gari":[2,0,4,1,6,3,5],)" + R"("detector_order":"physical_then_virtual"})"), + gari_dem.count_detectors()); + + std::string path = testing::TempDir() + "gari_source_shots.b8"; + { + std::ofstream output(path, std::ios::binary); + output.put('\x09'); // D0 D3. + output.put('\x42'); // D1 D6. + } + + FILE* file = fopen(path.c_str(), "rb"); + ASSERT_NE(file, nullptr); + auto format = stim::format_name_to_enum_map().at("b8"); + auto reader = stim::MeasureRecordReader::make( + file, format.id, 0, layout.source_detector_count(), 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})); + layout.map_shots(shots); + EXPECT_EQ(shots[0].hits, (std::vector{1, 2})); + EXPECT_EQ(shots[1].hits, (std::vector{0, 5})); + + 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})); +} + TEST(utils, GariLayoutRejectsInvalidV1) { const std::string valid = R"({"schema":"tesseract.gari_layout.v1","source_detector_count":2,)" R"("gari_detector_count":4,"source_to_gari":[1,0],)" diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index 7236519b..a247ab97 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -229,6 +229,9 @@ struct Args { if (!circuit_path.empty()) { gari_layout->validate_source(circuit, config.dem); } + } else if (sample_num_shots > 0 and !dem_path.empty() and + circuit.count_detectors() != config.dem.count_detectors()) { + throw std::invalid_argument("Circuit and DEM detector counts differ; supply --gari-layout."); } // Sample orientations of the error model to use for the det priority From bc4f26c75fd209a66264c739ddf5df493ff7cf5a Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 6 Aug 2026 17:16:13 -0700 Subject: [PATCH 13/31] Use valid hits in GARI mapping test --- src/tesseract.test.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index 6be8fe69..acb9ebb4 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -576,9 +576,9 @@ TEST(utils, GariLayoutMapsAndValidatesSource) { R"("detector_order":"physical_then_virtual"})"); GariLayout layout = load_gari_layout(path, 4); std::vector shots(1); - shots[0].hits = {0, 0, 1}; + shots[0].hits = {0, 1}; layout.map_shots(shots); - EXPECT_EQ(shots[0].hits, (std::vector{0, 1, 1})); + EXPECT_EQ(shots[0].hits, (std::vector{0, 1})); stim::Circuit circuit( "M 0 1\nDETECTOR rec[-1]\nDETECTOR rec[-2]\n" From 25f27b5bbeb4a484beb11fbf61fed57089ca0675 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sat, 8 Aug 2026 18:10:45 -0700 Subject: [PATCH 14/31] Fix GARI detector-order mapping --- src/tesseract.test.cc | 31 ++++++++++++++----------------- src/utils.cc | 13 ++++++------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index acb9ebb4..0d09ef9a 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -571,36 +571,33 @@ static std::string write_gari_layout(const std::string& text) { TEST(utils, GariLayoutMapsAndValidatesSource) { std::string path = - write_gari_layout(R"({"schema":"tesseract.gari_layout.v1","source_detector_count":2,)" - R"("gari_detector_count":4,"source_to_gari":[1,0],)" + write_gari_layout(R"({"schema":"tesseract.gari_layout.v1","source_detector_count":4,)" + R"("gari_detector_count":6,"source_to_gari":[2,0,3,1],)" R"("detector_order":"physical_then_virtual"})"); - GariLayout layout = load_gari_layout(path, 4); + GariLayout layout = load_gari_layout(path, 6); std::vector shots(1); shots[0].hits = {0, 1}; layout.map_shots(shots); - EXPECT_EQ(shots[0].hits, (std::vector{0, 1})); + EXPECT_EQ(shots[0].hits, (std::vector{0, 2})); stim::Circuit circuit( - "M 0 1\nDETECTOR rec[-1]\nDETECTOR rec[-2]\n" + "M 0 1 2 3\nDETECTOR(4) rec[-1]\nDETECTOR(1) rec[-2]\n" + "DETECTOR(3) rec[-3]\nDETECTOR(2) rec[-4]\n" "OBSERVABLE_INCLUDE(0) rec[-1]"); - stim::DetectorErrorModel dem("error(0.1) D0 D1 D2 D3 L0"); + stim::DetectorErrorModel dem("error(0.1) D0 D1 D2 D3 D4 D5 L0"); EXPECT_NO_THROW(layout.validate_source(circuit, dem)); EXPECT_THROW(layout.validate_source(stim::Circuit("M 0\nDETECTOR rec[-1]"), dem), std::invalid_argument); stim::DetectorErrorModel source_dem = stim::ErrorAnalyzer::circuit_to_detector_error_model( circuit, false, true, true, 1, false, false); - for (DetOrder method : {DetOrder::DetIndex, DetOrder::DetBFS, DetOrder::DetCoordinate}) { - auto source_orders = build_det_orders(source_dem, 2, method, 5); - auto gari_orders = build_gari_detector_orders(circuit, layout, 2, method, 5); - for (size_t order = 0; order < source_orders.size(); ++order) { - for (size_t position = 0; position < layout.source_detector_count(); ++position) { - EXPECT_EQ(gari_orders[order][position], - layout.source_to_gari[source_orders[order][position]]); - } - EXPECT_EQ(gari_orders[order][2], 2); - EXPECT_EQ(gari_orders[order][3], 3); - } + auto source_order = build_det_orders(source_dem, 1, DetOrder::DetCoordinate, 5)[0]; + auto gari_order = build_gari_detector_orders(circuit, layout, 1, DetOrder::DetCoordinate, 5)[0]; + if (source_order == std::vector{0, 3, 1, 2}) { + EXPECT_EQ(gari_order, (std::vector{2, 3, 1, 0, 4, 5})); + } else { + ASSERT_EQ(source_order, (std::vector{3, 0, 2, 1})); + EXPECT_EQ(gari_order, (std::vector{0, 1, 3, 2, 4, 5})); } } diff --git a/src/utils.cc b/src/utils.cc index 9af481b9..9b0a2933 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -331,16 +331,15 @@ std::vector> build_gari_detector_orders(const stim::Circuit& if (source_order.size() != layout.source_detector_count()) { throw gari_layout_error(layout.path, "source detector order has the wrong size."); } - std::vector gari_order; - gari_order.reserve(layout.gari_detector_count); - // Existing orders contain source detector IDs. Map those IDs to physical - // GARI rows, then visit the virtual rows in their natural order. - for (size_t source : source_order) { - gari_order.push_back(layout.source_to_gari.at(source)); + std::vector gari_order(layout.gari_detector_count); + // Source orders map each detector to its rank. Relabel the physical rows + // while converting those ranks to the sequence consumed by Tesseract. + for (size_t source = 0; source < source_order.size(); ++source) { + gari_order.at(source_order[source]) = layout.source_to_gari.at(source); } for (size_t detector = layout.source_detector_count(); detector < layout.gari_detector_count; ++detector) { - gari_order.push_back(detector); + gari_order[detector] = detector; } gari_orders.push_back(std::move(gari_order)); } From 544531873b159bc53a50138a51a1d0248835bd15 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Sat, 8 Aug 2026 18:11:08 -0700 Subject: [PATCH 15/31] Document GARI CLI usage --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.md b/README.md index 15331c43..353dd6be 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,43 @@ Using a Detection Event File and Observable Flips: 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 and its companion detector layout 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` and +`gari_output/circuit_file_gari_xor_layout.json`. Sample from the source circuit and decode with the +generated pair: + +```bash +./bazel-bin/src/tesseract \ + --circuit circuit_file.stim \ + --dem gari_output/circuit_file_gari_xor.dem \ + --gari-layout gari_output/circuit_file_gari_xor_layout.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. Detection events must +come from the source circuit, or from an input file in the source circuit's detector order, and the +layout must be paired with the generated DEM. 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: From 13150767b75c6353bd1968b40d0b5fed4f62d349 Mon Sep 17 00:00:00 2001 From: Aria Shahingohar Date: Mon, 10 Aug 2026 18:47:30 +0000 Subject: [PATCH 16/31] Fix geometric_distribution crash in GariSourceShotRemappingDecodes --- src/tesseract.test.cc | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index 0d09ef9a..19afa219 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -623,10 +623,8 @@ TEST(utils, GariSourceShotRemappingDecodes) { layout.gari_detector_count = 6; layout.source_to_gari = {2, 0, 3, 1}; - std::vector shots; - sample_shots(0, circuit, 1, shots); - ASSERT_EQ(shots.size(), 1); - EXPECT_EQ(shots[0].hits, (std::vector{0, 3})); + std::vector shots(1); + shots[0].hits = {0, 3}; layout.map_shots(shots); EXPECT_EQ(shots[0].hits, (std::vector{1, 2})); From 9220e3c6031c36720864039f424af4f8470551e6 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 20 Aug 2026 22:50:15 -0700 Subject: [PATCH 17/31] Generate GARI detector order sequences --- src/py/_tesseract_py_util/gari.py | 47 ++++++++++++++++++++++---- src/py/_tesseract_py_util/gari_test.py | 36 ++++++++++++++++---- 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index e86fd322..9750f163 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -650,7 +650,7 @@ def circuit_to_gari( *, 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. + """Converts a supported CSS circuit into a GARI matrix DEM and layout. The source DEM is generated undecomposed (``decompose_errors=False``) and flattened. Every detector must follow the repository's fourth-coordinate @@ -673,15 +673,48 @@ def circuit_to_gari( transform, probabilities, prior_function=prior_function ) layout = { - "schema": "tesseract.gari_layout.v1", + "schema": "tesseract.detector_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", + "dem_detector_count": transform.checks.shape[0], + "source_to_dem": transform.source_to_gari_detectors.tolist(), + "detector_orders": [list(range(transform.checks.shape[0]))], + "metadata": {"generator": "gari"}, } return gari_dem, layout +def build_detector_orders( + circuit: stim.Circuit, + detector_layout: dict[str, object], + num_det_orders: int, + *, + method: object | None = None, + seed: int = 0, +) -> list[list[int]]: + """Builds source-circuit detector orders for a GARI matrix DEM.""" + from tesseract_decoder import utils + + if method is None: + method = utils.DetOrder.DetIndex + source_dem = _circuit_to_gari_source_dem(circuit) + source_orders = ( + utils.build_det_orders(source_dem, num_det_orders, method, seed) + if source_dem.num_detectors + else [[] for _ in range(num_det_orders)] + ) + source_to_dem = np.asarray( + detector_layout["source_to_dem"], dtype=np.int64 + ) + dem_detector_count = int(detector_layout["dem_detector_count"]) + virtual_detectors = sorted( + set(range(dem_detector_count)).difference(source_to_dem.tolist()) + ) + return [ + source_to_dem[np.argsort(order)].tolist() + virtual_detectors + for order in source_orders + ] + + def call_gari(circuit_fname: str, prior_name: str, output_dir: str) -> None: """Converts one circuit and writes its GARI DEM and layout files.""" prior_function = { @@ -697,7 +730,7 @@ def call_gari(circuit_fname: str, prior_name: str, output_dir: str) -> None: output_path.mkdir(parents=True, exist_ok=True) output_name = f"{Path(circuit_fname).stem}_gari_{prior_name.replace('-', '_')}" gari_dem.to_file(output_path / f"{output_name}.dem") - (output_path / f"{output_name}_layout.json").write_text( + (output_path / f"{output_name}_detector_layout.json").write_text( json.dumps(layout, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) @@ -727,7 +760,7 @@ def main() -> None: help=( "Output directory, created if needed. Files are named " "_gari_.dem and " - "_gari__layout.json." + "_gari__detector_layout.json." ), ) args = parser.parse_args() diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 530adaf8..64ae322d 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -19,7 +19,7 @@ import stim from _tesseract_py_util import gari -from tesseract_decoder import demutil +from tesseract_decoder import demutil, tesseract, utils def _tiny_circuit(): @@ -165,15 +165,39 @@ def test_public_circuit_conversion_and_file_output(tmp_path): prior_function=public_gari.tesseract_xor_prior_probabilities, ) assert layout == { - "schema": "tesseract.gari_layout.v1", + "schema": "tesseract.detector_layout.v1", "source_detector_count": 4, - "gari_detector_count": 6, - "source_to_gari": [0, 2, 1, 3], - "detector_order": "physical_then_virtual", + "dem_detector_count": 6, + "source_to_dem": [0, 2, 1, 3], + "detector_orders": [[0, 1, 2, 3, 4, 5]], + "metadata": {"generator": "gari"}, } assert gari_dem.num_detectors == 6 assert gari_dem.num_observables == 2 + source_order = utils.build_det_orders( + gari._circuit_to_gari_source_dem(circuit), + 1, + utils.DetOrder.DetCoordinate, + 5, + )[0] + detector_orders = public_gari.build_detector_orders( + circuit, + layout, + 1, + method=utils.DetOrder.DetCoordinate, + seed=5, + ) + assert detector_orders == [ + np.asarray(layout["source_to_dem"])[ + np.argsort(source_order) + ].tolist() + + [4, 5] + ] + assert tesseract.TesseractConfig( + dem=gari_dem, det_orders=detector_orders + ).det_orders == detector_orders + circuit_path = tmp_path / "tiny.stim" circuit.to_file(circuit_path) output_dir = tmp_path / "gari" @@ -183,7 +207,7 @@ def test_public_circuit_conversion_and_file_output(tmp_path): output_dir / f"{output_name}.dem" ) written_layout = json.loads( - (output_dir / f"{output_name}_layout.json").read_text( + (output_dir / f"{output_name}_detector_layout.json").read_text( encoding="utf-8" ) ) From aa364189787cb2c13ea714a9f3b83661b0d482d9 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 20 Aug 2026 22:53:10 -0700 Subject: [PATCH 18/31] Add generic detector layout parsing --- src/simplex_main.cc | 4 +- src/tesseract.test.cc | 58 ++++++++++------- src/tesseract_main.cc | 4 +- src/utils.cc | 144 +++++++++++++++++++++++++++--------------- src/utils.h | 16 ++--- 5 files changed, 139 insertions(+), 87 deletions(-) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index bd7ab824..00369182 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -175,9 +175,9 @@ struct Args { config.merge_errors = !no_merge_errors; - std::optional gari_layout; + std::optional gari_layout; if (!gari_layout_path.empty()) { - gari_layout = load_gari_layout(gari_layout_path, config.dem.count_detectors()); + gari_layout = load_detector_layout(gari_layout_path, config.dem.count_detectors()); if (!circuit_path.empty()) { gari_layout->validate_source(circuit, config.dem); } diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index 19afa219..b457a056 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -563,18 +563,20 @@ TEST(tesseract, MoreThan64Observables) { } } -static std::string write_gari_layout(const std::string& text) { - std::string path = testing::TempDir() + "gari_layout_test.json"; +static std::string write_detector_layout(const std::string& text) { + std::string path = testing::TempDir() + "detector_layout_test.json"; std::ofstream(path) << text; return path; } -TEST(utils, GariLayoutMapsAndValidatesSource) { +TEST(utils, DetectorLayoutMapsAndValidatesSource) { std::string path = - write_gari_layout(R"({"schema":"tesseract.gari_layout.v1","source_detector_count":4,)" - R"("gari_detector_count":6,"source_to_gari":[2,0,3,1],)" - R"("detector_order":"physical_then_virtual"})"); - GariLayout layout = load_gari_layout(path, 6); + write_detector_layout(R"({"schema":"tesseract.detector_layout.v1",)" + R"("source_detector_count":4,"dem_detector_count":6,)" + R"("source_to_dem":[2,0,5,1],)" + R"("detector_orders":[[2,0,5,1,3,4]]})"); + DetectorLayout layout = load_detector_layout(path, 6); + EXPECT_EQ(layout.detector_orders, (std::vector>{{2, 0, 5, 1, 3, 4}})); std::vector shots(1); shots[0].hits = {0, 1}; layout.map_shots(shots); @@ -594,11 +596,18 @@ TEST(utils, GariLayoutMapsAndValidatesSource) { auto source_order = build_det_orders(source_dem, 1, DetOrder::DetCoordinate, 5)[0]; auto gari_order = build_gari_detector_orders(circuit, layout, 1, DetOrder::DetCoordinate, 5)[0]; if (source_order == std::vector{0, 3, 1, 2}) { - EXPECT_EQ(gari_order, (std::vector{2, 3, 1, 0, 4, 5})); + EXPECT_EQ(gari_order, (std::vector{2, 5, 1, 0, 3, 4})); } else { ASSERT_EQ(source_order, (std::vector{3, 0, 2, 1})); - EXPECT_EQ(gari_order, (std::vector{0, 1, 3, 2, 4, 5})); + EXPECT_EQ(gari_order, (std::vector{0, 1, 5, 2, 3, 4})); } + + DetectorLayout defaults = load_detector_layout( + write_detector_layout( + R"({"schema":"tesseract.detector_layout.v1","dem_detector_count":3})"), + 3); + EXPECT_EQ(defaults.source_to_dem, (std::vector{0, 1, 2})); + EXPECT_TRUE(defaults.detector_orders.empty()); } TEST(utils, GariSourceShotRemappingDecodes) { @@ -619,9 +628,9 @@ TEST(utils, GariSourceShotRemappingDecodes) { error(0.1) D4 L1 error(0.1) D5 L2 )DEM"); - GariLayout layout; - layout.gari_detector_count = 6; - layout.source_to_gari = {2, 0, 3, 1}; + DetectorLayout layout; + layout.dem_detector_count = 6; + layout.source_to_dem = {2, 0, 3, 1}; std::vector shots(1); shots[0].hits = {0, 3}; @@ -640,10 +649,10 @@ TEST(utils, GariB8SourceWidthPreservesShotRecords) { error(0.1) D0 D5 L1 detector D9 )DEM"); - GariLayout layout = load_gari_layout( - write_gari_layout(R"({"schema":"tesseract.gari_layout.v1","source_detector_count":7,)" - R"("gari_detector_count":10,"source_to_gari":[2,0,4,1,6,3,5],)" - R"("detector_order":"physical_then_virtual"})"), + DetectorLayout layout = load_detector_layout( + write_detector_layout(R"({"schema":"tesseract.detector_layout.v1",)" + R"("source_detector_count":7,"dem_detector_count":10,)" + R"("source_to_dem":[2,0,4,1,6,3,5]})"), gari_dem.count_detectors()); std::string path = testing::TempDir() + "gari_source_shots.b8"; @@ -681,20 +690,23 @@ TEST(utils, GariB8SourceWidthPreservesShotRecords) { EXPECT_EQ(simplex.decode(shots[1].hits), (std::vector{1})); } -TEST(utils, GariLayoutRejectsInvalidV1) { - const std::string valid = R"({"schema":"tesseract.gari_layout.v1","source_detector_count":2,)" - R"("gari_detector_count":4,"source_to_gari":[1,0],)" - R"("detector_order":"physical_then_virtual"})"; +TEST(utils, DetectorLayoutRejectsInvalidV1) { + const std::string valid = + R"({"schema":"tesseract.detector_layout.v1","source_detector_count":2,)" + R"("dem_detector_count":4,"source_to_dem":[1,0],)" + R"("detector_orders":[[1,0,2,3]]})"; auto replacing = [&](const std::string& from, const std::string& to) { std::string result = valid; result.replace(result.find(from), from.size(), to); return result; }; for (const std::string& text : - {replacing("[1,0]", "[0,3]"), replacing("[1,0]", "[0,0]"), std::string("{")}) { - std::string path = write_gari_layout(text); + {replacing("[1,0]", "[0,4]"), replacing("[1,0]", "[0,0]"), + replacing("[1,0,2,3]", "[0,1,2,2]"), replacing("[[1,0,2,3]]", "[]"), + std::string("{")}) { + std::string path = write_detector_layout(text); try { - load_gari_layout(path, 4); + load_detector_layout(path, 4); FAIL() << "Invalid layout was accepted."; } catch (const std::invalid_argument& ex) { EXPECT_NE(std::string(ex.what()).find(path), std::string::npos); diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index a247ab97..58f08ba5 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -223,9 +223,9 @@ struct Args { config.merge_errors = !no_merge_errors; - std::optional gari_layout; + std::optional gari_layout; if (!gari_layout_path.empty()) { - gari_layout = load_gari_layout(gari_layout_path, config.dem.count_detectors()); + gari_layout = load_detector_layout(gari_layout_path, config.dem.count_detectors()); if (!circuit_path.empty()) { gari_layout->validate_source(circuit, config.dem); } diff --git a/src/utils.cc b/src/utils.cc index 9b0a2933..ea7d40b0 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -29,96 +29,130 @@ #include "common.h" #include "stim.h" -static std::invalid_argument gari_layout_error(const std::string& path, const std::string& detail) { - return std::invalid_argument("Invalid GARI layout '" + path + "': " + detail); +static std::invalid_argument detector_layout_error(const std::string& path, + const std::string& detail) { + return std::invalid_argument("Invalid detector layout '" + path + "': " + detail); } -static size_t read_gari_layout_size(const nlohmann::json& value, const std::string& path) { +static size_t read_detector_layout_size(const nlohmann::json& value, const std::string& path) { if (!value.is_number_integer()) { - throw gari_layout_error(path, "detector counts and mapping entries must be integers."); + throw detector_layout_error(path, "detector counts and indices must be integers."); } if (value.is_number_unsigned()) { uint64_t result = value.get(); if (result > std::numeric_limits::max()) { - throw gari_layout_error(path, "integer is too large."); + throw detector_layout_error(path, "integer is too large."); } return static_cast(result); } int64_t result = value.get(); if (result < 0) { - throw gari_layout_error(path, "detector counts and mapping entries must be nonnegative."); + throw detector_layout_error(path, "detector counts and indices must be nonnegative."); } return static_cast(result); } -GariLayout load_gari_layout(const std::string& path, size_t expected_gari_detector_count) { +DetectorLayout load_detector_layout(const std::string& path, size_t expected_dem_detector_count) { std::ifstream input(path); if (!input.is_open()) { - throw std::invalid_argument("Could not open GARI layout: " + path); + throw std::invalid_argument("Could not open detector layout: " + path); } try { nlohmann::json document; input >> document; - if (document.at("schema").get() != "tesseract.gari_layout.v1") { - throw gari_layout_error(path, "unsupported schema."); - } - if (document.at("detector_order").get() != "physical_then_virtual") { - throw gari_layout_error(path, "unsupported detector order."); + if (document.at("schema").get() != "tesseract.detector_layout.v1") { + throw detector_layout_error(path, "unsupported schema."); } + size_t dem_detector_count = + read_detector_layout_size(document.at("dem_detector_count"), path); + if (dem_detector_count != expected_dem_detector_count) { + throw detector_layout_error(path, "dem_detector_count does not match the DEM."); + } size_t source_detector_count = - read_gari_layout_size(document.at("source_detector_count"), path); - size_t gari_detector_count = read_gari_layout_size(document.at("gari_detector_count"), path); - const auto& mapping = document.at("source_to_gari"); - if (gari_detector_count != expected_gari_detector_count || - source_detector_count > gari_detector_count || !mapping.is_array() || - mapping.size() != source_detector_count) { - throw gari_layout_error(path, "detector counts do not agree with the mapping."); + document.contains("source_detector_count") + ? read_detector_layout_size(document.at("source_detector_count"), path) + : dem_detector_count; + if (source_detector_count > dem_detector_count) { + throw detector_layout_error(path, "source detector count exceeds DEM detector count."); } - GariLayout layout; + DetectorLayout layout; layout.path = path; - layout.gari_detector_count = gari_detector_count; - std::vector used(source_detector_count); - layout.source_to_gari.reserve(source_detector_count); - for (const auto& entry : mapping) { - size_t target = read_gari_layout_size(entry, path); - if (target >= source_detector_count || used[target]) { - throw gari_layout_error(path, "source_to_gari must permute the physical detector rows."); + layout.dem_detector_count = dem_detector_count; + if (document.contains("source_to_dem")) { + const auto& mapping = document.at("source_to_dem"); + if (!mapping.is_array() || mapping.size() != source_detector_count) { + throw detector_layout_error(path, "source_to_dem has the wrong size."); + } + std::vector used(dem_detector_count); + layout.source_to_dem.reserve(source_detector_count); + for (const auto& entry : mapping) { + size_t target = read_detector_layout_size(entry, path); + if (target >= dem_detector_count || used[target]) { + throw detector_layout_error(path, "source_to_dem must contain unique DEM detector IDs."); + } + used[target] = true; + layout.source_to_dem.push_back(target); + } + } else { + layout.source_to_dem.resize(source_detector_count); + std::iota(layout.source_to_dem.begin(), layout.source_to_dem.end(), 0); + } + + if (document.contains("detector_orders")) { + const auto& orders = document.at("detector_orders"); + if (!orders.is_array() || orders.empty()) { + throw detector_layout_error(path, "detector_orders must be a nonempty array."); + } + for (const auto& order : orders) { + if (!order.is_array() || order.size() != dem_detector_count) { + throw detector_layout_error(path, "each detector order must include every DEM detector."); + } + std::vector used(dem_detector_count); + std::vector parsed_order; + parsed_order.reserve(dem_detector_count); + for (const auto& entry : order) { + size_t detector = read_detector_layout_size(entry, path); + if (detector >= dem_detector_count || used[detector]) { + throw detector_layout_error(path, "each detector order must be a permutation."); + } + used[detector] = true; + parsed_order.push_back(detector); + } + layout.detector_orders.push_back(std::move(parsed_order)); } - used[target] = true; - layout.source_to_gari.push_back(target); } return layout; } catch (const nlohmann::json::exception& ex) { - throw gari_layout_error(path, ex.what()); + throw detector_layout_error(path, ex.what()); } } -void GariLayout::map_hits(std::vector& hits) const { +void DetectorLayout::map_hits(std::vector& hits) const { for (uint64_t& source : hits) { - if (source >= source_to_gari.size()) { - throw gari_layout_error(path, "source detector index is out of range."); + if (source >= source_to_dem.size()) { + throw detector_layout_error(path, "source detector index is out of range."); } - source = source_to_gari[source]; + source = source_to_dem[source]; } std::sort(hits.begin(), hits.end()); } -void GariLayout::map_shots(std::vector& shots) const { +void DetectorLayout::map_shots(std::vector& shots) const { for (auto& shot : shots) { map_hits(shot.hits); } } -void GariLayout::validate_source(const stim::Circuit& circuit, - const stim::DetectorErrorModel& gari_dem) const { +void DetectorLayout::validate_source(const stim::Circuit& circuit, + const stim::DetectorErrorModel& dem) const { if (circuit.count_detectors() != source_detector_count()) { - throw gari_layout_error(path, "source_detector_count does not match the circuit."); + throw detector_layout_error(path, "source_detector_count does not match the circuit."); } - if (circuit.count_observables() != gari_dem.count_observables()) { - throw gari_layout_error(path, "the circuit and DEM observable counts differ."); + if (circuit.count_observables() != dem.count_observables()) { + throw detector_layout_error(path, "the circuit and DEM observable counts differ."); } } @@ -298,18 +332,18 @@ std::vector> build_det_orders(const stim::DetectorErrorModel } std::vector> build_gari_detector_orders(const stim::Circuit& source_circuit, - const GariLayout& layout, + const DetectorLayout& layout, size_t num_det_orders, DetOrder method, uint64_t seed) { if (num_det_orders == 0) { return {}; } if (source_circuit.count_detectors() != layout.source_detector_count()) { - throw gari_layout_error(layout.path, - "source_detector_count does not match the ordering circuit."); + throw detector_layout_error(layout.path, + "source_detector_count does not match the ordering circuit."); } - if (layout.source_detector_count() > layout.gari_detector_count) { - throw gari_layout_error(layout.path, "source detector count exceeds GARI detector count."); + if (layout.source_detector_count() > layout.dem_detector_count) { + throw detector_layout_error(layout.path, "source detector count exceeds DEM detector count."); } std::vector> source_orders; @@ -329,17 +363,23 @@ std::vector> build_gari_detector_orders(const stim::Circuit& gari_orders.reserve(source_orders.size()); for (const auto& source_order : source_orders) { if (source_order.size() != layout.source_detector_count()) { - throw gari_layout_error(layout.path, "source detector order has the wrong size."); + throw detector_layout_error(layout.path, "source detector order has the wrong size."); } - std::vector gari_order(layout.gari_detector_count); + std::vector gari_order(layout.dem_detector_count); // Source orders map each detector to its rank. Relabel the physical rows // while converting those ranks to the sequence consumed by Tesseract. for (size_t source = 0; source < source_order.size(); ++source) { - gari_order.at(source_order[source]) = layout.source_to_gari.at(source); + gari_order.at(source_order[source]) = layout.source_to_dem.at(source); + } + std::vector mapped(layout.dem_detector_count); + for (size_t detector : layout.source_to_dem) { + mapped[detector] = true; } - for (size_t detector = layout.source_detector_count(); detector < layout.gari_detector_count; - ++detector) { - gari_order[detector] = detector; + size_t position = layout.source_detector_count(); + for (size_t detector = 0; detector < layout.dem_detector_count; ++detector) { + if (!mapped[detector]) { + gari_order[position++] = detector; + } } gari_orders.push_back(std::move(gari_order)); } diff --git a/src/utils.h b/src/utils.h index d38feac3..e5632905 100644 --- a/src/utils.h +++ b/src/utils.h @@ -44,24 +44,24 @@ std::vector> build_det_orders(const stim::DetectorErrorModel DetOrder method = DetOrder::DetIndex, uint64_t seed = 0); -struct GariLayout { +struct DetectorLayout { std::string path; - size_t gari_detector_count = 0; - std::vector source_to_gari; + size_t dem_detector_count = 0; + std::vector source_to_dem; + std::vector> detector_orders; size_t source_detector_count() const { - return source_to_gari.size(); + return source_to_dem.size(); } void map_hits(std::vector& hits) const; void map_shots(std::vector& shots) const; - void validate_source(const stim::Circuit& circuit, - const stim::DetectorErrorModel& gari_dem) const; + void validate_source(const stim::Circuit& circuit, const stim::DetectorErrorModel& dem) const; }; -GariLayout load_gari_layout(const std::string& path, size_t expected_gari_detector_count); +DetectorLayout load_detector_layout(const std::string& path, size_t expected_dem_detector_count); std::vector> build_gari_detector_orders(const stim::Circuit& source_circuit, - const GariLayout& layout, + const DetectorLayout& layout, size_t num_det_orders, DetOrder method = DetOrder::DetIndex, uint64_t seed = 0); From d4d53533c0bc88e1f38fa617fe7897ca16c63a57 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 20 Aug 2026 22:58:09 -0700 Subject: [PATCH 19/31] Use generic detector layouts in the CLIs --- src/simplex_main.cc | 35 ++++++++++++-------------- src/tesseract.test.cc | 11 -------- src/tesseract_main.cc | 58 ++++++++++++++++++++----------------------- src/utils.cc | 55 ---------------------------------------- src/utils.h | 6 ----- 5 files changed, 43 insertions(+), 122 deletions(-) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 00369182..cf9445b6 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -28,7 +28,7 @@ struct Args { std::string circuit_path; std::string dem_path; - std::string gari_layout_path; + std::string detector_layout_path; bool no_merge_errors = false; // Sampling options @@ -85,10 +85,6 @@ struct Args { if (circuit_path.empty() and dem_path.empty()) { throw std::invalid_argument("Must provide at least one of --circuit or --dem"); } - if (!gari_layout_path.empty() and dem_path.empty()) { - throw std::invalid_argument("--gari-layout requires --dem."); - } - int num_data_sources = int(sample_num_shots > 0) + int(!in_fname.empty()); if (num_data_sources != 1) { throw std::invalid_argument("Requires exactly 1 source of shots."); @@ -175,15 +171,16 @@ struct Args { config.merge_errors = !no_merge_errors; - std::optional gari_layout; - if (!gari_layout_path.empty()) { - gari_layout = load_detector_layout(gari_layout_path, config.dem.count_detectors()); + std::optional detector_layout; + if (!detector_layout_path.empty()) { + detector_layout = load_detector_layout(detector_layout_path, config.dem.count_detectors()); if (!circuit_path.empty()) { - gari_layout->validate_source(circuit, config.dem); + detector_layout->validate_source(circuit, config.dem); } } else if (sample_num_shots > 0 and !dem_path.empty() and circuit.count_detectors() != config.dem.count_detectors()) { - throw std::invalid_argument("Circuit and DEM detector counts differ; supply --gari-layout."); + throw std::invalid_argument( + "Circuit and DEM detector counts differ; supply --detector-layout."); } if (sample_num_shots > 0) { @@ -212,7 +209,8 @@ struct Args { } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); size_t source_detector_count = - gari_layout ? gari_layout->source_detector_count() : config.dem.count_detectors(); + detector_layout ? detector_layout->source_detector_count() + : config.dem.count_detectors(); auto reader = stim::MeasureRecordReader::make( shots_file, shots_in_format.id, 0, source_detector_count, append_observables * config.dem.count_observables()); @@ -227,8 +225,8 @@ struct Args { fclose(shots_file); } - if (gari_layout) { - gari_layout->map_shots(shots); + if (detector_layout) { + detector_layout->map_shots(shots); } // Load observable flips, if applicable @@ -296,13 +294,12 @@ int main(int argc, char* argv[]) { Args args; program.add_argument("--circuit").help("Stim circuit file path").store_into(args.circuit_path); program.add_argument("--dem").help("Stim dem file path").store_into(args.dem_path); - program.add_argument("--gari-layout") + program.add_argument("--detector-layout") .help( - "Companion JSON layout for a GARI matrix file. Maps source detector data into GARI " - "rows; virtual rows remain zero.") + "JSON detector layout. Optionally maps source detector data into DEM detector rows.") .metavar("FILE") .default_value(std::string("")) - .store_into(args.gari_layout_path); + .store_into(args.detector_layout_path); program.add_argument("--no-merge-errors") .help("If provided, will not merge identical error mechanisms.") .store_into(args.no_merge_errors); @@ -544,8 +541,8 @@ int main(int argc, char* argv[]) { {"num_errors", has_obs ? nlohmann::json(num_errors) : nullptr}, {"num_shots", shot}, {"sample_num_shots", args.sample_num_shots}}; - if (!args.gari_layout_path.empty()) { - stats_json["gari_layout_path"] = args.gari_layout_path; + if (!args.detector_layout_path.empty()) { + stats_json["detector_layout_path"] = args.detector_layout_path; } if (args.stats_out_fname == "-") { diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index b457a056..d858d274 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -591,17 +591,6 @@ TEST(utils, DetectorLayoutMapsAndValidatesSource) { EXPECT_THROW(layout.validate_source(stim::Circuit("M 0\nDETECTOR rec[-1]"), dem), std::invalid_argument); - stim::DetectorErrorModel source_dem = stim::ErrorAnalyzer::circuit_to_detector_error_model( - circuit, false, true, true, 1, false, false); - auto source_order = build_det_orders(source_dem, 1, DetOrder::DetCoordinate, 5)[0]; - auto gari_order = build_gari_detector_orders(circuit, layout, 1, DetOrder::DetCoordinate, 5)[0]; - if (source_order == std::vector{0, 3, 1, 2}) { - EXPECT_EQ(gari_order, (std::vector{2, 5, 1, 0, 3, 4})); - } else { - ASSERT_EQ(source_order, (std::vector{3, 0, 2, 1})); - EXPECT_EQ(gari_order, (std::vector{0, 1, 5, 2, 3, 4})); - } - DetectorLayout defaults = load_detector_layout( write_detector_layout( R"({"schema":"tesseract.detector_layout.v1","dem_detector_count":3})"), diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index 58f08ba5..bb12874b 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -33,7 +33,7 @@ struct Args { std::string circuit_path; std::string dem_path; - std::string gari_layout_path; + std::string detector_layout_path; bool no_merge_errors = false; // Manifold orientation options @@ -100,10 +100,6 @@ struct Args { if (circuit_path.empty() and dem_path.empty()) { throw std::invalid_argument("Must provide at least one of --circuit or --dem"); } - if (!gari_layout_path.empty() and dem_path.empty()) { - throw std::invalid_argument("--gari-layout requires --dem."); - } - int det_order_flags = int(det_order_bfs) + int(det_order_index) + int(det_order_coordinate); if (det_order_flags > 1) { throw std::invalid_argument( @@ -111,11 +107,6 @@ struct Args { } explicit_det_order = det_order_flags > 0 || program.is_used("--num-det-orders") || program.is_used("--det-order-seed"); - if (!gari_layout_path.empty() && explicit_det_order && num_det_orders > 0 && - circuit_path.empty()) { - throw std::invalid_argument("GARI detector ordering requires --circuit."); - } - int num_data_sources = int(sample_num_shots > 0) + int(!in_fname.empty()); if (num_data_sources != 1) { throw std::invalid_argument("Requires exactly 1 source of shots."); @@ -223,15 +214,21 @@ struct Args { config.merge_errors = !no_merge_errors; - std::optional gari_layout; - if (!gari_layout_path.empty()) { - gari_layout = load_detector_layout(gari_layout_path, config.dem.count_detectors()); + std::optional detector_layout; + if (!detector_layout_path.empty()) { + detector_layout = load_detector_layout(detector_layout_path, config.dem.count_detectors()); if (!circuit_path.empty()) { - gari_layout->validate_source(circuit, config.dem); + detector_layout->validate_source(circuit, config.dem); + } + if (!detector_layout->detector_orders.empty() && explicit_det_order) { + throw std::invalid_argument( + "Detector-order options cannot be used when --detector-layout provides " + "detector_orders."); } } else if (sample_num_shots > 0 and !dem_path.empty() and circuit.count_detectors() != config.dem.count_detectors()) { - throw std::invalid_argument("Circuit and DEM detector counts differ; supply --gari-layout."); + throw std::invalid_argument( + "Circuit and DEM detector counts differ; supply --detector-layout."); } // Sample orientations of the error model to use for the det priority @@ -256,11 +253,10 @@ struct Args { } else if (det_order_coordinate) { order = DetOrder::DetCoordinate; } - if (!gari_layout) { + if (detector_layout && !detector_layout->detector_orders.empty()) { + config.det_orders = detector_layout->detector_orders; + } else { config.det_orders = build_det_orders(config.dem, num_det_orders, order, det_order_seed); - } else if (explicit_det_order) { - config.det_orders = build_gari_detector_orders(circuit, *gari_layout, num_det_orders, order, - det_order_seed); } } @@ -290,7 +286,8 @@ struct Args { } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); size_t source_detector_count = - gari_layout ? gari_layout->source_detector_count() : config.dem.count_detectors(); + detector_layout ? detector_layout->source_detector_count() + : config.dem.count_detectors(); auto reader = stim::MeasureRecordReader::make( shots_file, shots_in_format.id, 0, source_detector_count, append_observables * config.dem.count_observables()); @@ -305,8 +302,8 @@ struct Args { fclose(shots_file); } - if (gari_layout) { - gari_layout->map_shots(shots); + if (detector_layout) { + detector_layout->map_shots(shots); } // Load observable flips, if applicable @@ -381,14 +378,13 @@ int main(int argc, char* argv[]) { Args args; program.add_argument("--circuit").help("Stim circuit file path").store_into(args.circuit_path); program.add_argument("--dem").help("Stim dem file path").store_into(args.dem_path); - program.add_argument("--gari-layout") + program.add_argument("--detector-layout") .help( - "Companion JSON layout for a GARI matrix file. Maps source detector data into GARI " - "rows; virtual rows remain zero. Detector-order options override the default GARI row " - "order.") + "JSON detector layout. Optionally maps source detector data into DEM rows and supplies " + "detector traversal orders.") .metavar("FILE") .default_value(std::string("")) - .store_into(args.gari_layout_path); + .store_into(args.detector_layout_path); program.add_argument("--no-merge-errors") .help("If provided, will not merge identical error mechanisms.") .store_into(args.no_merge_errors); @@ -707,7 +703,8 @@ 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}, {"total_time_seconds", total_time_seconds}, {"num_errors", has_obs ? nlohmann::json(num_errors) : nullptr}, @@ -719,9 +716,8 @@ int main(int argc, char* argv[]) { {"sparsify_base_degree", args.sparsify_base_degree}, {"sparsify_max_degree", args.sparsify_max_degree}, {"sparsify_reactivate_limit", effective_sparsify_reactivate_limit}}; - if (!args.gari_layout_path.empty()) { - stats_json["gari_layout_path"] = args.gari_layout_path; - stats_json["gari_default_detector_order_used"] = config.det_orders.empty(); + if (!args.detector_layout_path.empty()) { + stats_json["detector_layout_path"] = args.detector_layout_path; } if (args.stats_out_fname == "-") { diff --git a/src/utils.cc b/src/utils.cc index ea7d40b0..37db19be 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -331,61 +331,6 @@ std::vector> build_det_orders(const stim::DetectorErrorModel throw std::invalid_argument("Unknown det order method"); } -std::vector> build_gari_detector_orders(const stim::Circuit& source_circuit, - const DetectorLayout& layout, - size_t num_det_orders, DetOrder method, - uint64_t seed) { - if (num_det_orders == 0) { - return {}; - } - if (source_circuit.count_detectors() != layout.source_detector_count()) { - throw detector_layout_error(layout.path, - "source_detector_count does not match the ordering circuit."); - } - if (layout.source_detector_count() > layout.dem_detector_count) { - throw detector_layout_error(layout.path, "source detector count exceeds DEM detector count."); - } - - std::vector> source_orders; - if (layout.source_detector_count() == 0) { - source_orders.resize(num_det_orders); - } else { - stim::DetectorErrorModel source_dem = stim::ErrorAnalyzer::circuit_to_detector_error_model( - source_circuit, /*decompose_errors=*/false, /*fold_loops=*/true, - /*allow_gauge_detectors=*/true, - /*approximate_disjoint_errors_threshold=*/1, - /*ignore_decomposition_failures=*/false, - /*block_decomposition_from_introducing_remnant_edges=*/false); - source_orders = build_det_orders(source_dem, num_det_orders, method, seed); - } - - std::vector> gari_orders; - gari_orders.reserve(source_orders.size()); - for (const auto& source_order : source_orders) { - if (source_order.size() != layout.source_detector_count()) { - throw detector_layout_error(layout.path, "source detector order has the wrong size."); - } - std::vector gari_order(layout.dem_detector_count); - // Source orders map each detector to its rank. Relabel the physical rows - // while converting those ranks to the sequence consumed by Tesseract. - for (size_t source = 0; source < source_order.size(); ++source) { - gari_order.at(source_order[source]) = layout.source_to_dem.at(source); - } - std::vector mapped(layout.dem_detector_count); - for (size_t detector : layout.source_to_dem) { - mapped[detector] = true; - } - size_t position = layout.source_detector_count(); - for (size_t detector = 0; detector < layout.dem_detector_count; ++detector) { - if (!mapped[detector]) { - gari_order[position++] = detector; - } - } - gari_orders.push_back(std::move(gari_order)); - } - return gari_orders; -} - bool sampling_from_dem(uint64_t seed, size_t num_shots, stim::DetectorErrorModel dem, std::vector& shots) { stim::DemSampler sampler(dem, std::mt19937_64{seed}, num_shots); diff --git a/src/utils.h b/src/utils.h index e5632905..b0f93cd5 100644 --- a/src/utils.h +++ b/src/utils.h @@ -60,12 +60,6 @@ struct DetectorLayout { DetectorLayout load_detector_layout(const std::string& path, size_t expected_dem_detector_count); -std::vector> build_gari_detector_orders(const stim::Circuit& source_circuit, - const DetectorLayout& layout, - size_t num_det_orders, - DetOrder method = DetOrder::DetIndex, - uint64_t seed = 0); - const double INF = std::numeric_limits::infinity(); bool sampling_from_dem(uint64_t seed, size_t num_shots, stim::DetectorErrorModel dem, From 6fa152e3485eb6bab78e94364310fb8b73e87031 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 20 Aug 2026 23:01:02 -0700 Subject: [PATCH 20/31] Document generic detector layouts --- README.md | 26 +++++++++++++++++++++++--- docs/tutorial.ipynb | 22 +++++++++++++--------- docs/tutorial.py | 22 +++++++++++++--------- src/py/README.md | 13 +++++++++---- 4 files changed, 58 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 353dd6be..fd7558ec 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,25 @@ Using a Detection Event File and Observable Flips: Tesseract supports reading and writing from all of Stim's standard [output formats](https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md). +### Detector Layouts + +Both command-line decoders accept a `tesseract.detector_layout.v1` JSON file through +`--detector-layout`. A basic Tesseract layout can provide explicit detector traversal sequences: + +```json +{ + "schema": "tesseract.detector_layout.v1", + "dem_detector_count": 4, + "detector_orders": [[0, 2, 1, 3]] +} +``` + +For data whose detector IDs differ from the DEM, add `source_detector_count` and a unique +`source_to_dem` entry for each source detector. These fields default to an identity mapping, and +unmapped DEM detectors stay zero. If `detector_orders` is omitted, Tesseract uses its existing +detector-order options; if it is present, do not also pass detector-order generation options. +Simplex uses the same source mapping but does not use detector orders. + ### Decoding with GARI Generate a GARI matrix DEM and its companion detector layout from a source circuit: @@ -159,14 +178,15 @@ python src/py/_tesseract_py_util/gari.py \ ``` This writes `gari_output/circuit_file_gari_xor.dem` and -`gari_output/circuit_file_gari_xor_layout.json`. Sample from the source circuit and decode with the -generated pair: +`gari_output/circuit_file_gari_xor_detector_layout.json`. The layout uses the generic +`tesseract.detector_layout.v1` schema to map source shots into the GARI matrix DEM and provide its +detector traversal order. Sample from the source circuit and decode with the generated pair: ```bash ./bazel-bin/src/tesseract \ --circuit circuit_file.stim \ --dem gari_output/circuit_file_gari_xor.dem \ - --gari-layout gari_output/circuit_file_gari_xor_layout.json \ + --detector-layout gari_output/circuit_file_gari_xor_detector_layout.json \ --sample-num-shots 100 \ --sample-seed 1234 \ --threads 1 \ diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index e00ef674..19884199 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, detector_layout = gari.circuit_to_gari(\n", " circuit,\n", " prior_function=gari.tesseract_xor_prior_probabilities,\n", ")" @@ -964,7 +964,7 @@ "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]" + "gari_dets[:, detector_layout[\"source_to_dem\"]] = dets[:num_shots]" ] }, { @@ -974,8 +974,8 @@ "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." + "Pass the detector order stored in the layout directly to Tesseract. The\n", + "remaining parameters are the standard short-beam configuration." ] }, { @@ -987,11 +987,15 @@ }, "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", + " det_orders=detector_layout[\"detector_orders\"],\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..2af819c3 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, detector_layout = gari.circuit_to_gari( circuit, prior_function=gari.tesseract_xor_prior_probabilities, ) @@ -371,18 +371,22 @@ def run_tesseract_decoder(decoder, dets, obs): # %% 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] +gari_dets[:, detector_layout["source_to_dem"]] = 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. +# Pass the detector order stored in the layout directly to Tesseract. The +# remaining parameters are the standard short-beam configuration. # %% 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, + det_orders=detector_layout["detector_orders"], +) +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..a2ab0973 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -717,7 +717,7 @@ 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, detector_layout = demutil.gari.circuit_to_gari( circuit, prior_function=demutil.gari.tesseract_xor_prior_probabilities, ) @@ -727,14 +727,19 @@ gari_dem, gari_layout = demutil.gari.circuit_to_gari( * `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. +* `detector_layout`: a `tesseract.detector_layout.v1` dictionary containing the + DEM and source detector counts, the `source_to_dem` mapping, one or more + `detector_orders`, and optional generator metadata. Tesseract accepts the + same dictionary when serialized as JSON through `--detector-layout`. Related public APIs: * `demutil.gari.dem_to_matrices(dem)` returns the sparse detector matrix, sparse logical matrix, and one probability per source error column. +* `demutil.gari.build_detector_orders(circuit, detector_layout, num_det_orders, ...)` + generates source-circuit-aware Tesseract traversal orders for the GARI DEM. + Assign its result to `detector_layout["detector_orders"]` before saving the + layout or pass it directly to `TesseractConfig(det_orders=...)`. * `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 From 6fdeb2ee7c94451331459c18067056d99f77ffab Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Thu, 20 Aug 2026 23:52:54 -0700 Subject: [PATCH 21/31] Fix C++ formatting --- src/simplex_main.cc | 6 ++---- src/tesseract.test.cc | 22 +++++++++------------- src/tesseract_main.cc | 6 ++---- src/utils.cc | 3 +-- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index cf9445b6..524e0fa9 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -209,8 +209,7 @@ struct Args { } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); size_t source_detector_count = - detector_layout ? detector_layout->source_detector_count() - : config.dem.count_detectors(); + detector_layout ? detector_layout->source_detector_count() : config.dem.count_detectors(); auto reader = stim::MeasureRecordReader::make( shots_file, shots_in_format.id, 0, source_detector_count, append_observables * config.dem.count_observables()); @@ -295,8 +294,7 @@ int main(int argc, char* argv[]) { program.add_argument("--circuit").help("Stim circuit file path").store_into(args.circuit_path); program.add_argument("--dem").help("Stim dem file path").store_into(args.dem_path); program.add_argument("--detector-layout") - .help( - "JSON detector layout. Optionally maps source detector data into DEM detector rows.") + .help("JSON detector layout. Optionally maps source detector data into DEM detector rows.") .metavar("FILE") .default_value(std::string("")) .store_into(args.detector_layout_path); diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index d858d274..5cdbc8e1 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -570,11 +570,10 @@ static std::string write_detector_layout(const std::string& text) { } TEST(utils, DetectorLayoutMapsAndValidatesSource) { - std::string path = - write_detector_layout(R"({"schema":"tesseract.detector_layout.v1",)" - R"("source_detector_count":4,"dem_detector_count":6,)" - R"("source_to_dem":[2,0,5,1],)" - R"("detector_orders":[[2,0,5,1,3,4]]})"); + std::string path = write_detector_layout(R"({"schema":"tesseract.detector_layout.v1",)" + R"("source_detector_count":4,"dem_detector_count":6,)" + R"("source_to_dem":[2,0,5,1],)" + R"("detector_orders":[[2,0,5,1,3,4]]})"); DetectorLayout layout = load_detector_layout(path, 6); EXPECT_EQ(layout.detector_orders, (std::vector>{{2, 0, 5, 1, 3, 4}})); std::vector shots(1); @@ -592,8 +591,7 @@ TEST(utils, DetectorLayoutMapsAndValidatesSource) { std::invalid_argument); DetectorLayout defaults = load_detector_layout( - write_detector_layout( - R"({"schema":"tesseract.detector_layout.v1","dem_detector_count":3})"), + write_detector_layout(R"({"schema":"tesseract.detector_layout.v1","dem_detector_count":3})"), 3); EXPECT_EQ(defaults.source_to_dem, (std::vector{0, 1, 2})); EXPECT_TRUE(defaults.detector_orders.empty()); @@ -680,10 +678,9 @@ TEST(utils, GariB8SourceWidthPreservesShotRecords) { } TEST(utils, DetectorLayoutRejectsInvalidV1) { - const std::string valid = - R"({"schema":"tesseract.detector_layout.v1","source_detector_count":2,)" - R"("dem_detector_count":4,"source_to_dem":[1,0],)" - R"("detector_orders":[[1,0,2,3]]})"; + const std::string valid = R"({"schema":"tesseract.detector_layout.v1","source_detector_count":2,)" + R"("dem_detector_count":4,"source_to_dem":[1,0],)" + R"("detector_orders":[[1,0,2,3]]})"; auto replacing = [&](const std::string& from, const std::string& to) { std::string result = valid; result.replace(result.find(from), from.size(), to); @@ -691,8 +688,7 @@ TEST(utils, DetectorLayoutRejectsInvalidV1) { }; for (const std::string& text : {replacing("[1,0]", "[0,4]"), replacing("[1,0]", "[0,0]"), - replacing("[1,0,2,3]", "[0,1,2,2]"), replacing("[[1,0,2,3]]", "[]"), - std::string("{")}) { + replacing("[1,0,2,3]", "[0,1,2,2]"), replacing("[[1,0,2,3]]", "[]"), std::string("{")}) { std::string path = write_detector_layout(text); try { load_detector_layout(path, 4); diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index bb12874b..81d2180b 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -286,8 +286,7 @@ struct Args { } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); size_t source_detector_count = - detector_layout ? detector_layout->source_detector_count() - : config.dem.count_detectors(); + detector_layout ? detector_layout->source_detector_count() : config.dem.count_detectors(); auto reader = stim::MeasureRecordReader::make( shots_file, shots_in_format.id, 0, source_detector_count, append_observables * config.dem.count_observables()); @@ -703,8 +702,7 @@ int main(int argc, char* argv[]) { {"beam_climbing", args.beam_climbing}, {"no_revisit_dets", args.no_revisit_dets}, {"pqlimit", args.pqlimit}, - {"num_det_orders", - config.det_orders.empty() ? 1 : config.det_orders.size()}, + {"num_det_orders", config.det_orders.empty() ? 1 : config.det_orders.size()}, {"det_order_seed", args.det_order_seed}, {"total_time_seconds", total_time_seconds}, {"num_errors", has_obs ? nlohmann::json(num_errors) : nullptr}, diff --git a/src/utils.cc b/src/utils.cc index 37db19be..c9537117 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -65,8 +65,7 @@ DetectorLayout load_detector_layout(const std::string& path, size_t expected_dem throw detector_layout_error(path, "unsupported schema."); } - size_t dem_detector_count = - read_detector_layout_size(document.at("dem_detector_count"), path); + size_t dem_detector_count = read_detector_layout_size(document.at("dem_detector_count"), path); if (dem_detector_count != expected_dem_detector_count) { throw detector_layout_error(path, "dem_detector_count does not match the DEM."); } From 283198cf913d02089eb6a15ccb8e2f3b278ede8e Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Fri, 21 Aug 2026 01:04:06 -0700 Subject: [PATCH 22/31] Clarify GARI logical placement --- src/py/_tesseract_py_util/gari.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 9750f163..da248a0a 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -61,9 +61,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 @@ -395,6 +403,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], From 59efadcdf61f27fbe37e7f655b1beb59ca3ee079 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Fri, 21 Aug 2026 13:15:31 -0700 Subject: [PATCH 23/31] Require explicit DEM for detector remapping --- src/simplex_main.cc | 2 +- src/tesseract.test.cc | 5 +++-- src/tesseract_main.cc | 2 +- src/utils.cc | 10 +++++++++- src/utils.h | 3 ++- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 524e0fa9..6b637e5a 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -175,7 +175,7 @@ struct Args { if (!detector_layout_path.empty()) { detector_layout = load_detector_layout(detector_layout_path, config.dem.count_detectors()); if (!circuit_path.empty()) { - detector_layout->validate_source(circuit, config.dem); + detector_layout->validate_source(circuit, config.dem, dem_path.empty()); } } else if (sample_num_shots > 0 and !dem_path.empty() and circuit.count_detectors() != config.dem.count_detectors()) { diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index 5cdbc8e1..b30b809a 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -586,8 +586,9 @@ TEST(utils, DetectorLayoutMapsAndValidatesSource) { "DETECTOR(3) rec[-3]\nDETECTOR(2) rec[-4]\n" "OBSERVABLE_INCLUDE(0) rec[-1]"); stim::DetectorErrorModel dem("error(0.1) D0 D1 D2 D3 D4 D5 L0"); - EXPECT_NO_THROW(layout.validate_source(circuit, dem)); - EXPECT_THROW(layout.validate_source(stim::Circuit("M 0\nDETECTOR rec[-1]"), dem), + EXPECT_NO_THROW(layout.validate_source(circuit, dem, false)); + EXPECT_THROW(layout.validate_source(circuit, dem, true), std::invalid_argument); + EXPECT_THROW(layout.validate_source(stim::Circuit("M 0\nDETECTOR rec[-1]"), dem, false), std::invalid_argument); DetectorLayout defaults = load_detector_layout( diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index 81d2180b..9ed89e02 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -218,7 +218,7 @@ struct Args { if (!detector_layout_path.empty()) { detector_layout = load_detector_layout(detector_layout_path, config.dem.count_detectors()); if (!circuit_path.empty()) { - detector_layout->validate_source(circuit, config.dem); + detector_layout->validate_source(circuit, config.dem, dem_path.empty()); } if (!detector_layout->detector_orders.empty() && explicit_det_order) { throw std::invalid_argument( diff --git a/src/utils.cc b/src/utils.cc index c9537117..649b2307 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -146,13 +146,21 @@ void DetectorLayout::map_shots(std::vector& shots) const { } void DetectorLayout::validate_source(const stim::Circuit& circuit, - const stim::DetectorErrorModel& dem) const { + const stim::DetectorErrorModel& dem, + bool dem_from_circuit) const { if (circuit.count_detectors() != source_detector_count()) { throw detector_layout_error(path, "source_detector_count does not match the circuit."); } if (circuit.count_observables() != dem.count_observables()) { throw detector_layout_error(path, "the circuit and DEM observable counts differ."); } + if (dem_from_circuit) { + for (size_t source = 0; source < source_to_dem.size(); source++) { + if (source_to_dem[source] != source) { + throw detector_layout_error(path, "non-identity source_to_dem requires an explicit DEM."); + } + } + } } std::vector> get_detector_coords(const stim::DetectorErrorModel& dem) { diff --git a/src/utils.h b/src/utils.h index b0f93cd5..7c68f4c3 100644 --- a/src/utils.h +++ b/src/utils.h @@ -55,7 +55,8 @@ struct DetectorLayout { } void map_hits(std::vector& hits) const; void map_shots(std::vector& shots) const; - void validate_source(const stim::Circuit& circuit, const stim::DetectorErrorModel& dem) const; + void validate_source(const stim::Circuit& circuit, const stim::DetectorErrorModel& dem, + bool dem_from_circuit) const; }; DetectorLayout load_detector_layout(const std::string& path, size_t expected_dem_detector_count); From 54cbeeb1fae5c2bb80154aa76881bca01a011021 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Fri, 21 Aug 2026 14:33:44 -0700 Subject: [PATCH 24/31] Validate circuit and DEM detector counts --- src/simplex_main.cc | 2 +- src/tesseract_main.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 6b637e5a..dbc8dd08 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -177,7 +177,7 @@ struct Args { if (!circuit_path.empty()) { detector_layout->validate_source(circuit, config.dem, dem_path.empty()); } - } else if (sample_num_shots > 0 and !dem_path.empty() and + } else if (!circuit_path.empty() and !dem_path.empty() and circuit.count_detectors() != config.dem.count_detectors()) { throw std::invalid_argument( "Circuit and DEM detector counts differ; supply --detector-layout."); diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index 9ed89e02..81186963 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -225,7 +225,7 @@ struct Args { "Detector-order options cannot be used when --detector-layout provides " "detector_orders."); } - } else if (sample_num_shots > 0 and !dem_path.empty() and + } else if (!circuit_path.empty() and !dem_path.empty() and circuit.count_detectors() != config.dem.count_detectors()) { throw std::invalid_argument( "Circuit and DEM detector counts differ; supply --detector-layout."); From 4d2afb436642010668af69423203f21788605c66 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Fri, 21 Aug 2026 15:16:01 -0700 Subject: [PATCH 25/31] Preserve source detector IDs in GARI DEMs --- docs/tutorial.ipynb | 21 +--- docs/tutorial.py | 12 +-- src/py/README.md | 34 +++---- src/py/_tesseract_py_util/gari.py | 131 ++++++++++++------------- src/py/_tesseract_py_util/gari_test.py | 61 ++++-------- 5 files changed, 107 insertions(+), 152 deletions(-) diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb index 19884199..10f6303b 100644 --- a/docs/tutorial.ipynb +++ b/docs/tutorial.ipynb @@ -935,7 +935,7 @@ "source": [ "gari = tesseract_decoder.demutil.gari\n", "\n", - "gari_dem, detector_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,24 +965,13 @@ "source": [ "num_shots = 10\n", "gari_dets = np.zeros((num_shots, gari_dem.num_detectors), dtype=bool)\n", - "gari_dets[:, detector_layout[\"source_to_dem\"]] = dets[:num_shots]" - ] - }, - { - "cell_type": "markdown", - "id": "abc39564", - "metadata": { - "id": "gari-detector-order" - }, - "source": [ - "Pass the detector order stored in the layout directly to Tesseract. The\n", - "remaining parameters are the standard short-beam configuration." + "gari_dets[:, :dets.shape[1]] = dets[:num_shots]" ] }, { "cell_type": "code", "execution_count": null, - "id": "bc0c802c", + "id": "3e397ee8", "metadata": { "id": "gari-decode-example" }, @@ -993,7 +983,6 @@ " beam_climbing=True,\n", " no_revisit_dets=True,\n", " pqlimit=200_000,\n", - " det_orders=detector_layout[\"detector_orders\"],\n", ")\n", "gari_decoder = gari_config.compile_decoder()\n", "predicted_observables = gari_decoder.decode_batch(gari_dets)\n", diff --git a/docs/tutorial.py b/docs/tutorial.py index 2af819c3..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, detector_layout = gari.circuit_to_gari( +gari_dem = gari.circuit_to_gari( circuit, prior_function=gari.tesseract_xor_prior_probabilities, ) @@ -366,16 +366,13 @@ 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[:, detector_layout["source_to_dem"]] = dets[:num_shots] - -# %% [markdown] id="gari-detector-order" -# Pass the detector order stored in the layout directly to Tesseract. The -# remaining parameters are the standard short-beam configuration. +gari_dets[:, :dets.shape[1]] = dets[:num_shots] # %% id="gari-decode-example" gari_config = tesseract.TesseractConfig( @@ -384,7 +381,6 @@ def run_tesseract_decoder(decoder, dets, obs): beam_climbing=True, no_revisit_dets=True, pqlimit=200_000, - det_orders=detector_layout["detector_orders"], ) gari_decoder = gari_config.compile_decoder() predicted_observables = gari_decoder.decode_batch(gari_dets) diff --git a/src/py/README.md b/src/py/README.md index a2ab0973..af5a14cc 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -707,49 +707,43 @@ 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, detector_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: - -* `gari_dem`: the augmented detector and logical matrices stored using Stim - DEM syntax. -* `detector_layout`: a `tesseract.detector_layout.v1` dictionary containing the - DEM and source detector counts, the `source_to_dem` mapping, one or more - `detector_orders`, and optional generator metadata. Tesseract accepts the - same dictionary when serialized as JSON through `--detector-layout`. +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. Related public APIs: * `demutil.gari.dem_to_matrices(dem)` returns the sparse detector matrix, sparse logical matrix, and one probability per source error column. -* `demutil.gari.build_detector_orders(circuit, detector_layout, num_det_orders, ...)` - generates source-circuit-aware Tesseract traversal orders for the GARI DEM. - Assign its result to `detector_layout["detector_orders"]` before saving the - layout or pass it directly to `TesseractConfig(det_orders=...)`. * `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 da248a0a..4147f755 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 @@ -83,7 +88,6 @@ from __future__ import annotations import dataclasses -import json from collections.abc import Callable, Sequence from pathlib import Path @@ -614,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. @@ -650,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 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) @@ -679,81 +699,45 @@ 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.detector_layout.v1", - "source_detector_count": len(transform.source_to_gari_detectors), - "dem_detector_count": transform.checks.shape[0], - "source_to_dem": transform.source_to_gari_detectors.tolist(), - "detector_orders": [list(range(transform.checks.shape[0]))], - "metadata": {"generator": "gari"}, - } - return gari_dem, layout -def build_detector_orders( - circuit: stim.Circuit, - detector_layout: dict[str, object], - num_det_orders: int, +def call_gari( + circuit_fname: str, + prior_name: str, + output_dir: str, *, - method: object | None = None, - seed: int = 0, -) -> list[list[int]]: - """Builds source-circuit detector orders for a GARI matrix DEM.""" - from tesseract_decoder import utils - - if method is None: - method = utils.DetOrder.DetIndex - source_dem = _circuit_to_gari_source_dem(circuit) - source_orders = ( - utils.build_det_orders(source_dem, num_det_orders, method, seed) - if source_dem.num_detectors - else [[] for _ in range(num_det_orders)] - ) - source_to_dem = np.asarray( - detector_layout["source_to_dem"], dtype=np.int64 - ) - dem_detector_count = int(detector_layout["dem_detector_count"]) - virtual_detectors = sorted( - set(range(dem_detector_count)).difference(source_to_dem.tolist()) - ) - return [ - source_to_dem[np.argsort(order)].tolist() + virtual_detectors - for order in source_orders - ] - - -def call_gari(circuit_fname: str, prior_name: str, output_dir: str) -> None: - """Converts one circuit and writes its GARI DEM and layout files.""" + 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}_detector_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." @@ -769,12 +753,25 @@ def main() -> None: required=True, help=( "Output directory, created if needed. Files are named " - "_gari_.dem and " - "_gari__detector_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 64ae322d..79dad772 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, tesseract, utils +from tesseract_decoder import demutil 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,43 +159,25 @@ 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.detector_layout.v1", - "source_detector_count": 4, - "dem_detector_count": 6, - "source_to_dem": [0, 2, 1, 3], - "detector_orders": [[0, 1, 2, 3, 4, 5]], - "metadata": {"generator": "gari"}, - } 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_order = utils.build_det_orders( - gari._circuit_to_gari_source_dem(circuit), - 1, - utils.DetOrder.DetCoordinate, - 5, - )[0] - detector_orders = public_gari.build_detector_orders( + block_dem = public_gari.circuit_to_gari( circuit, - layout, - 1, - method=utils.DetOrder.DetCoordinate, - seed=5, - ) - assert detector_orders == [ - np.asarray(layout["source_to_dem"])[ - np.argsort(source_order) - ].tolist() - + [4, 5] - ] - assert tesseract.TesseractConfig( - dem=gari_dem, det_orders=detector_orders - ).det_orders == detector_orders + 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) @@ -206,13 +187,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}_detector_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__": From 56bf938e04605d76c2f4723567c6c2a150587fe3 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Fri, 21 Aug 2026 15:19:42 -0700 Subject: [PATCH 26/31] Simplify augmented DEM input handling --- CMakeLists.txt | 1 - src/BUILD | 1 - src/simplex_main.cc | 35 +++-------- src/tesseract.test.cc | 119 +++--------------------------------- src/tesseract_main.cc | 49 +++------------ src/utils.cc | 137 ------------------------------------------ src/utils.h | 17 ------ 7 files changed, 26 insertions(+), 333 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ca689a0..23e560f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,7 +87,6 @@ add_library(utils ${TESSERACT_SRC_DIR}/utils.cc ${TESSERACT_SRC_DIR}/utils.h) target_include_directories(utils PUBLIC ${TESSERACT_SRC_DIR}) target_compile_options(utils PRIVATE ${OPT_COPTS}) target_link_libraries(utils PUBLIC common libstim Threads::Threads) -target_link_libraries(utils PRIVATE nlohmann_json::nlohmann_json) add_library(visualization ${TESSERACT_SRC_DIR}/visualization.cc ${TESSERACT_SRC_DIR}/visualization.h) target_include_directories(visualization PUBLIC ${TESSERACT_SRC_DIR}) diff --git a/src/BUILD b/src/BUILD index ddd4dea7..1b1407bf 100644 --- a/src/BUILD +++ b/src/BUILD @@ -128,7 +128,6 @@ cc_library( visibility = ["//visibility:public"], deps = [ ":libcommon", - "@nlohmann_json//:json", "@stim//:stim_lib", ], ) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index dbc8dd08..255de79b 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -17,7 +17,6 @@ #include #include #include -#include #include #include "common.h" @@ -28,7 +27,6 @@ struct Args { std::string circuit_path; std::string dem_path; - std::string detector_layout_path; bool no_merge_errors = false; // Sampling options @@ -171,16 +169,15 @@ struct Args { config.merge_errors = !no_merge_errors; - std::optional detector_layout; - if (!detector_layout_path.empty()) { - detector_layout = load_detector_layout(detector_layout_path, config.dem.count_detectors()); - if (!circuit_path.empty()) { - detector_layout->validate_source(circuit, config.dem, dem_path.empty()); + 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."); } - } else if (!circuit_path.empty() and !dem_path.empty() and - circuit.count_detectors() != config.dem.count_detectors()) { - throw std::invalid_argument( - "Circuit and DEM detector counts differ; supply --detector-layout."); } if (sample_num_shots > 0) { @@ -208,10 +205,8 @@ struct Args { throw std::invalid_argument("Could not open the file: " + in_fname); } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); - size_t source_detector_count = - detector_layout ? detector_layout->source_detector_count() : config.dem.count_detectors(); auto reader = stim::MeasureRecordReader::make( - shots_file, shots_in_format.id, 0, source_detector_count, + shots_file, shots_in_format.id, 0, shot_detector_count, append_observables * config.dem.count_observables()); // Load the shots from a file @@ -224,10 +219,6 @@ struct Args { fclose(shots_file); } - if (detector_layout) { - detector_layout->map_shots(shots); - } - // Load observable flips, if applicable if (!obs_in_fname.empty()) { FILE* obs_file = fopen(obs_in_fname.c_str(), "r"); @@ -293,11 +284,6 @@ int main(int argc, char* argv[]) { Args args; program.add_argument("--circuit").help("Stim circuit file path").store_into(args.circuit_path); program.add_argument("--dem").help("Stim dem file path").store_into(args.dem_path); - program.add_argument("--detector-layout") - .help("JSON detector layout. Optionally maps source detector data into DEM detector rows.") - .metavar("FILE") - .default_value(std::string("")) - .store_into(args.detector_layout_path); program.add_argument("--no-merge-errors") .help("If provided, will not merge identical error mechanisms.") .store_into(args.no_merge_errors); @@ -539,9 +525,6 @@ int main(int argc, char* argv[]) { {"num_errors", has_obs ? nlohmann::json(num_errors) : nullptr}, {"num_shots", shot}, {"sample_num_shots", args.sample_num_shots}}; - if (!args.detector_layout_path.empty()) { - stats_json["detector_layout_path"] = args.detector_layout_path; - } if (args.stats_out_fname == "-") { std::cout << stats_json << std::endl; diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index b30b809a..a2f295eb 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -16,7 +16,6 @@ #include #include -#include #include #include #include @@ -563,98 +562,21 @@ TEST(tesseract, MoreThan64Observables) { } } -static std::string write_detector_layout(const std::string& text) { - std::string path = testing::TempDir() + "detector_layout_test.json"; - std::ofstream(path) << text; - return path; -} - -TEST(utils, DetectorLayoutMapsAndValidatesSource) { - std::string path = write_detector_layout(R"({"schema":"tesseract.detector_layout.v1",)" - R"("source_detector_count":4,"dem_detector_count":6,)" - R"("source_to_dem":[2,0,5,1],)" - R"("detector_orders":[[2,0,5,1,3,4]]})"); - DetectorLayout layout = load_detector_layout(path, 6); - EXPECT_EQ(layout.detector_orders, (std::vector>{{2, 0, 5, 1, 3, 4}})); - std::vector shots(1); - shots[0].hits = {0, 1}; - layout.map_shots(shots); - EXPECT_EQ(shots[0].hits, (std::vector{0, 2})); - - stim::Circuit circuit( - "M 0 1 2 3\nDETECTOR(4) rec[-1]\nDETECTOR(1) rec[-2]\n" - "DETECTOR(3) rec[-3]\nDETECTOR(2) rec[-4]\n" - "OBSERVABLE_INCLUDE(0) rec[-1]"); - stim::DetectorErrorModel dem("error(0.1) D0 D1 D2 D3 D4 D5 L0"); - EXPECT_NO_THROW(layout.validate_source(circuit, dem, false)); - EXPECT_THROW(layout.validate_source(circuit, dem, true), std::invalid_argument); - EXPECT_THROW(layout.validate_source(stim::Circuit("M 0\nDETECTOR rec[-1]"), dem, false), - std::invalid_argument); - - DetectorLayout defaults = load_detector_layout( - write_detector_layout(R"({"schema":"tesseract.detector_layout.v1","dem_detector_count":3})"), - 3); - EXPECT_EQ(defaults.source_to_dem, (std::vector{0, 1, 2})); - EXPECT_TRUE(defaults.detector_orders.empty()); -} - -TEST(utils, GariSourceShotRemappingDecodes) { - stim::Circuit circuit(R"STIM( - X_ERROR(1) 0 3 - M 0 1 2 3 4 5 - DETECTOR rec[-6] - DETECTOR rec[-5] - DETECTOR rec[-4] - DETECTOR rec[-3] - OBSERVABLE_INCLUDE(0) rec[-6] - OBSERVABLE_INCLUDE(1) rec[-2] - OBSERVABLE_INCLUDE(2) rec[-1] - )STIM"); +TEST(utils, GariSourcePrefixB8Decodes) { stim::DetectorErrorModel gari_dem(R"DEM( - error(0.1) D1 D2 L0 - error(0.1) D0 D3 - error(0.1) D4 L1 - error(0.1) D5 L2 - )DEM"); - DetectorLayout layout; - layout.dem_detector_count = 6; - layout.source_to_dem = {2, 0, 3, 1}; - - std::vector shots(1); - shots[0].hits = {0, 3}; - layout.map_shots(shots); - EXPECT_EQ(shots[0].hits, (std::vector{1, 2})); - - TesseractDecoder tesseract(TesseractConfig{gari_dem}); - SimplexDecoder simplex(SimplexConfig{gari_dem}); - EXPECT_EQ(tesseract.decode(shots[0].hits), (std::vector{0})); - EXPECT_EQ(simplex.decode(shots[0].hits), (std::vector{0})); -} - -TEST(utils, GariB8SourceWidthPreservesShotRecords) { - stim::DetectorErrorModel gari_dem(R"DEM( - error(0.1) D1 D2 L0 - error(0.1) D0 D5 L1 + error(0.1) D0 D3 L0 + error(0.1) D1 D6 L1 detector D9 )DEM"); - DetectorLayout layout = load_detector_layout( - write_detector_layout(R"({"schema":"tesseract.detector_layout.v1",)" - R"("source_detector_count":7,"dem_detector_count":10,)" - R"("source_to_dem":[2,0,4,1,6,3,5]})"), - gari_dem.count_detectors()); - - std::string path = testing::TempDir() + "gari_source_shots.b8"; - { - std::ofstream output(path, std::ios::binary); - output.put('\x09'); // D0 D3. - output.put('\x42'); // D1 D6. - } - FILE* file = fopen(path.c_str(), "rb"); + 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, layout.source_detector_count(), 0); + file, format.id, 0, 7, 0); std::vector shots; stim::SparseShot shot; while (reader->start_and_read_entire_record(shot)) { @@ -666,9 +588,6 @@ TEST(utils, GariB8SourceWidthPreservesShotRecords) { ASSERT_EQ(shots.size(), 2); EXPECT_EQ(shots[0].hits, (std::vector{0, 3})); EXPECT_EQ(shots[1].hits, (std::vector{1, 6})); - layout.map_shots(shots); - EXPECT_EQ(shots[0].hits, (std::vector{1, 2})); - EXPECT_EQ(shots[1].hits, (std::vector{0, 5})); TesseractDecoder tesseract(TesseractConfig{gari_dem}); SimplexDecoder simplex(SimplexConfig{gari_dem}); @@ -677,25 +596,3 @@ TEST(utils, GariB8SourceWidthPreservesShotRecords) { EXPECT_EQ(simplex.decode(shots[0].hits), (std::vector{0})); EXPECT_EQ(simplex.decode(shots[1].hits), (std::vector{1})); } - -TEST(utils, DetectorLayoutRejectsInvalidV1) { - const std::string valid = R"({"schema":"tesseract.detector_layout.v1","source_detector_count":2,)" - R"("dem_detector_count":4,"source_to_dem":[1,0],)" - R"("detector_orders":[[1,0,2,3]]})"; - auto replacing = [&](const std::string& from, const std::string& to) { - std::string result = valid; - result.replace(result.find(from), from.size(), to); - return result; - }; - for (const std::string& text : - {replacing("[1,0]", "[0,4]"), replacing("[1,0]", "[0,0]"), - replacing("[1,0,2,3]", "[0,1,2,2]"), replacing("[[1,0,2,3]]", "[]"), std::string("{")}) { - std::string path = write_detector_layout(text); - try { - load_detector_layout(path, 4); - FAIL() << "Invalid layout was accepted."; - } catch (const std::invalid_argument& ex) { - EXPECT_NE(std::string(ex.what()).find(path), std::string::npos); - } - } -} diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index 81186963..d9110121 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -21,7 +21,6 @@ #include #include #include -#include #include #include @@ -33,7 +32,6 @@ struct Args { std::string circuit_path; std::string dem_path; - std::string detector_layout_path; bool no_merge_errors = false; // Manifold orientation options @@ -42,7 +40,6 @@ struct Args { bool det_order_bfs = false; bool det_order_index = false; bool det_order_coordinate = false; - bool explicit_det_order = false; // Sampling options size_t sample_num_shots = 0; @@ -105,8 +102,6 @@ struct Args { throw std::invalid_argument( "Only one of --det-order-bfs, --det-order-index, or --det-order-coordinate may be set."); } - explicit_det_order = det_order_flags > 0 || program.is_used("--num-det-orders") || - program.is_used("--det-order-seed"); int num_data_sources = int(sample_num_shots > 0) + int(!in_fname.empty()); if (num_data_sources != 1) { throw std::invalid_argument("Requires exactly 1 source of shots."); @@ -214,21 +209,15 @@ struct Args { config.merge_errors = !no_merge_errors; - std::optional detector_layout; - if (!detector_layout_path.empty()) { - detector_layout = load_detector_layout(detector_layout_path, config.dem.count_detectors()); - if (!circuit_path.empty()) { - detector_layout->validate_source(circuit, config.dem, dem_path.empty()); + 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 (!detector_layout->detector_orders.empty() && explicit_det_order) { - throw std::invalid_argument( - "Detector-order options cannot be used when --detector-layout provides " - "detector_orders."); + if (circuit.count_observables() != config.dem.count_observables()) { + throw std::invalid_argument("Circuit and DEM observable counts differ."); } - } else if (!circuit_path.empty() and !dem_path.empty() and - circuit.count_detectors() != config.dem.count_detectors()) { - throw std::invalid_argument( - "Circuit and DEM detector counts differ; supply --detector-layout."); } // Sample orientations of the error model to use for the det priority @@ -253,11 +242,7 @@ struct Args { } else if (det_order_coordinate) { order = DetOrder::DetCoordinate; } - if (detector_layout && !detector_layout->detector_orders.empty()) { - config.det_orders = detector_layout->detector_orders; - } else { - 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) { @@ -285,10 +270,8 @@ struct Args { throw std::invalid_argument("Could not open the file: " + in_fname); } stim::FileFormatData shots_in_format = stim::format_name_to_enum_map().at(in_format); - size_t source_detector_count = - detector_layout ? detector_layout->source_detector_count() : config.dem.count_detectors(); auto reader = stim::MeasureRecordReader::make( - shots_file, shots_in_format.id, 0, source_detector_count, + shots_file, shots_in_format.id, 0, shot_detector_count, append_observables * config.dem.count_observables()); // Load the shots from a file @@ -301,10 +284,6 @@ struct Args { fclose(shots_file); } - if (detector_layout) { - detector_layout->map_shots(shots); - } - // Load observable flips, if applicable if (!obs_in_fname.empty()) { FILE* obs_file = fopen(obs_in_fname.c_str(), "r"); @@ -377,13 +356,6 @@ int main(int argc, char* argv[]) { Args args; program.add_argument("--circuit").help("Stim circuit file path").store_into(args.circuit_path); program.add_argument("--dem").help("Stim dem file path").store_into(args.dem_path); - program.add_argument("--detector-layout") - .help( - "JSON detector layout. Optionally maps source detector data into DEM rows and supplies " - "detector traversal orders.") - .metavar("FILE") - .default_value(std::string("")) - .store_into(args.detector_layout_path); program.add_argument("--no-merge-errors") .help("If provided, will not merge identical error mechanisms.") .store_into(args.no_merge_errors); @@ -714,9 +686,6 @@ int main(int argc, char* argv[]) { {"sparsify_base_degree", args.sparsify_base_degree}, {"sparsify_max_degree", args.sparsify_max_degree}, {"sparsify_reactivate_limit", effective_sparsify_reactivate_limit}}; - if (!args.detector_layout_path.empty()) { - stats_json["detector_layout_path"] = args.detector_layout_path; - } if (args.stats_out_fname == "-") { std::cout << stats_json << std::endl; diff --git a/src/utils.cc b/src/utils.cc index 37c0a400..91450eae 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -17,10 +17,7 @@ #include #include #include -#include #include -#include -#include #include #include #include @@ -29,140 +26,6 @@ #include "common.h" #include "stim.h" -static std::invalid_argument detector_layout_error(const std::string& path, - const std::string& detail) { - return std::invalid_argument("Invalid detector layout '" + path + "': " + detail); -} - -static size_t read_detector_layout_size(const nlohmann::json& value, const std::string& path) { - if (!value.is_number_integer()) { - throw detector_layout_error(path, "detector counts and indices must be integers."); - } - if (value.is_number_unsigned()) { - uint64_t result = value.get(); - if (result > std::numeric_limits::max()) { - throw detector_layout_error(path, "integer is too large."); - } - return static_cast(result); - } - int64_t result = value.get(); - if (result < 0) { - throw detector_layout_error(path, "detector counts and indices must be nonnegative."); - } - return static_cast(result); -} - -DetectorLayout load_detector_layout(const std::string& path, size_t expected_dem_detector_count) { - std::ifstream input(path); - if (!input.is_open()) { - throw std::invalid_argument("Could not open detector layout: " + path); - } - - try { - nlohmann::json document; - input >> document; - if (document.at("schema").get() != "tesseract.detector_layout.v1") { - throw detector_layout_error(path, "unsupported schema."); - } - - size_t dem_detector_count = read_detector_layout_size(document.at("dem_detector_count"), path); - if (dem_detector_count != expected_dem_detector_count) { - throw detector_layout_error(path, "dem_detector_count does not match the DEM."); - } - size_t source_detector_count = - document.contains("source_detector_count") - ? read_detector_layout_size(document.at("source_detector_count"), path) - : dem_detector_count; - if (source_detector_count > dem_detector_count) { - throw detector_layout_error(path, "source detector count exceeds DEM detector count."); - } - - DetectorLayout layout; - layout.path = path; - layout.dem_detector_count = dem_detector_count; - if (document.contains("source_to_dem")) { - const auto& mapping = document.at("source_to_dem"); - if (!mapping.is_array() || mapping.size() != source_detector_count) { - throw detector_layout_error(path, "source_to_dem has the wrong size."); - } - std::vector used(dem_detector_count); - layout.source_to_dem.reserve(source_detector_count); - for (const auto& entry : mapping) { - size_t target = read_detector_layout_size(entry, path); - if (target >= dem_detector_count || used[target]) { - throw detector_layout_error(path, "source_to_dem must contain unique DEM detector IDs."); - } - used[target] = true; - layout.source_to_dem.push_back(target); - } - } else { - layout.source_to_dem.resize(source_detector_count); - std::iota(layout.source_to_dem.begin(), layout.source_to_dem.end(), 0); - } - - if (document.contains("detector_orders")) { - const auto& orders = document.at("detector_orders"); - if (!orders.is_array() || orders.empty()) { - throw detector_layout_error(path, "detector_orders must be a nonempty array."); - } - for (const auto& order : orders) { - if (!order.is_array() || order.size() != dem_detector_count) { - throw detector_layout_error(path, "each detector order must include every DEM detector."); - } - std::vector used(dem_detector_count); - std::vector parsed_order; - parsed_order.reserve(dem_detector_count); - for (const auto& entry : order) { - size_t detector = read_detector_layout_size(entry, path); - if (detector >= dem_detector_count || used[detector]) { - throw detector_layout_error(path, "each detector order must be a permutation."); - } - used[detector] = true; - parsed_order.push_back(detector); - } - layout.detector_orders.push_back(std::move(parsed_order)); - } - } - return layout; - } catch (const nlohmann::json::exception& ex) { - throw detector_layout_error(path, ex.what()); - } -} - -void DetectorLayout::map_hits(std::vector& hits) const { - for (uint64_t& source : hits) { - if (source >= source_to_dem.size()) { - throw detector_layout_error(path, "source detector index is out of range."); - } - source = source_to_dem[source]; - } - std::sort(hits.begin(), hits.end()); -} - -void DetectorLayout::map_shots(std::vector& shots) const { - for (auto& shot : shots) { - map_hits(shot.hits); - } -} - -void DetectorLayout::validate_source(const stim::Circuit& circuit, - const stim::DetectorErrorModel& dem, - bool dem_from_circuit) const { - if (circuit.count_detectors() != source_detector_count()) { - throw detector_layout_error(path, "source_detector_count does not match the circuit."); - } - if (circuit.count_observables() != dem.count_observables()) { - throw detector_layout_error(path, "the circuit and DEM observable counts differ."); - } - if (dem_from_circuit) { - for (size_t source = 0; source < source_to_dem.size(); source++) { - if (source_to_dem[source] != source) { - throw detector_layout_error(path, "non-identity source_to_dem requires an explicit DEM."); - } - } - } -} - std::vector> get_detector_coords(const stim::DetectorErrorModel& dem) { std::vector> detector_coords; for (const stim::DemInstruction& instruction : common::flatten(dem).instructions) { diff --git a/src/utils.h b/src/utils.h index 7c68f4c3..3c5d6569 100644 --- a/src/utils.h +++ b/src/utils.h @@ -44,23 +44,6 @@ std::vector> build_det_orders(const stim::DetectorErrorModel DetOrder method = DetOrder::DetIndex, uint64_t seed = 0); -struct DetectorLayout { - std::string path; - size_t dem_detector_count = 0; - std::vector source_to_dem; - std::vector> detector_orders; - - size_t source_detector_count() const { - return source_to_dem.size(); - } - void map_hits(std::vector& hits) const; - void map_shots(std::vector& shots) const; - void validate_source(const stim::Circuit& circuit, const stim::DetectorErrorModel& dem, - bool dem_from_circuit) const; -}; - -DetectorLayout load_detector_layout(const std::string& path, size_t expected_dem_detector_count); - const double INF = std::numeric_limits::infinity(); bool sampling_from_dem(uint64_t seed, size_t num_shots, stim::DetectorErrorModel dem, From ad44cf0c99d36e748d64dc2c4b012819f75f0ad3 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Fri, 21 Aug 2026 15:20:10 -0700 Subject: [PATCH 27/31] Document source-prefix GARI decoding --- README.md | 41 +++++++++++++---------------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index fd7558ec..6d3a370f 100644 --- a/README.md +++ b/README.md @@ -147,28 +147,9 @@ Using a Detection Event File and Observable Flips: Tesseract supports reading and writing from all of Stim's standard [output formats](https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md). -### Detector Layouts - -Both command-line decoders accept a `tesseract.detector_layout.v1` JSON file through -`--detector-layout`. A basic Tesseract layout can provide explicit detector traversal sequences: - -```json -{ - "schema": "tesseract.detector_layout.v1", - "dem_detector_count": 4, - "detector_orders": [[0, 2, 1, 3]] -} -``` - -For data whose detector IDs differ from the DEM, add `source_detector_count` and a unique -`source_to_dem` entry for each source detector. These fields default to an identity mapping, and -unmapped DEM detectors stay zero. If `detector_orders` is omitted, Tesseract uses its existing -detector-order options; if it is present, do not also pass detector-order generation options. -Simplex uses the same source mapping but does not use detector orders. - ### Decoding with GARI -Generate a GARI matrix DEM and its companion detector layout from a source circuit: +Generate a GARI matrix DEM from a source circuit: ```bash python src/py/_tesseract_py_util/gari.py \ @@ -177,16 +158,14 @@ python src/py/_tesseract_py_util/gari.py \ --out-dir gari_output ``` -This writes `gari_output/circuit_file_gari_xor.dem` and -`gari_output/circuit_file_gari_xor_detector_layout.json`. The layout uses the generic -`tesseract.detector_layout.v1` schema to map source shots into the GARI matrix DEM and provide its -detector traversal order. Sample from the source circuit and decode with the generated pair: +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. 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-layout gari_output/circuit_file_gari_xor_detector_layout.json \ --sample-num-shots 100 \ --sample-seed 1234 \ --threads 1 \ @@ -198,9 +177,15 @@ detector traversal order. Sample from the source circuit and decode with the gen --stats-out gari-stats.json ``` -The GARI matrix DEM is a decoding representation and must not be sampled. Detection events must -come from the source circuit, or from an input file in the source circuit's detector order, and the -layout must be paired with the generated DEM. See +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. From 1862efa0dbb5ae2d3d8189dd4109b5e8fb06a533 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Fri, 21 Aug 2026 15:22:44 -0700 Subject: [PATCH 28/31] Keep the augmented DEM diff focused --- src/simplex_main.cc | 1 + src/tesseract.test.cc | 1 - src/tesseract_main.cc | 2 ++ src/utils.cc | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 255de79b..d8ece509 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -83,6 +83,7 @@ struct Args { if (circuit_path.empty() and dem_path.empty()) { throw std::invalid_argument("Must provide at least one of --circuit or --dem"); } + int num_data_sources = int(sample_num_shots > 0) + int(!in_fname.empty()); if (num_data_sources != 1) { throw std::invalid_argument("Requires exactly 1 source of shots."); diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index a2f295eb..fcb21c79 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -17,7 +17,6 @@ #include #include #include -#include #include #include "gtest/gtest.h" diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index d9110121..e3c77890 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -97,11 +97,13 @@ struct Args { if (circuit_path.empty() and dem_path.empty()) { throw std::invalid_argument("Must provide at least one of --circuit or --dem"); } + int det_order_flags = int(det_order_bfs) + int(det_order_index) + int(det_order_coordinate); if (det_order_flags > 1) { throw std::invalid_argument( "Only one of --det-order-bfs, --det-order-index, or --det-order-coordinate may be set."); } + int num_data_sources = int(sample_num_shots > 0) + int(!in_fname.empty()); if (num_data_sources != 1) { throw std::invalid_argument("Requires exactly 1 source of shots."); diff --git a/src/utils.cc b/src/utils.cc index 91450eae..a72a8ced 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include From eeec84051d715ec6635adc2b8a1175c56fc98011 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Fri, 21 Aug 2026 16:42:49 -0700 Subject: [PATCH 29/31] Add explicit Tesseract detector orders --- README.md | 31 ++++++++++++++- src/py/README.md | 9 ++++- src/py/_tesseract_py_util/gari.py | 28 +++++++++++++ src/py/_tesseract_py_util/gari_test.py | 15 ++++++- src/py/utils_test.py | 31 +++++++++++---- src/tesseract.cc | 15 +++++-- src/tesseract.pybind.h | 10 ++--- src/tesseract_main.cc | 54 +++++++++++++++++++++----- src/utils.cc | 13 ++----- src/utils.pybind.h | 4 +- 10 files changed, 170 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 6d3a370f..17ba929c 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,18 @@ 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). @@ -159,13 +171,28 @@ python src/py/_tesseract_py_util/gari.py \ ``` 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. Sample from the source circuit and -decode with the generated DEM: +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 \ diff --git a/src/py/README.md b/src/py/README.md index af5a14cc..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. @@ -729,6 +729,13 @@ virtual detector rows. For matrix analysis, `[physical X, physical Z, virtual Z, virtual X]` row order. This research form does not accept source syndromes as a direct prefix. +`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: * `demutil.gari.dem_to_matrices(dem)` returns the sparse detector matrix, diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 4147f755..505a2b9b 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -707,6 +707,34 @@ def circuit_to_gari( ) +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 orders for a source-aligned GARI DEM, with virtual IDs last.""" + 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) + ) + return [ + order + virtual_detectors + for order in utils.build_det_orders( + source_dem, num_det_orders, method=method, seed=seed + ) + ] + + def call_gari( circuit_fname: str, prior_name: str, diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 79dad772..8f95d878 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -17,7 +17,7 @@ import stim from _tesseract_py_util import gari -from tesseract_decoder import demutil +from tesseract_decoder import demutil, utils def _tiny_circuit(): @@ -170,6 +170,19 @@ def test_public_circuit_conversion_and_file_output(tmp_path): np.testing.assert_array_equal( checks.toarray(), transform.checks[[2, 0, 3, 1, 4, 5], :].toarray() ) + source_orders = 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, + ) == [order + [4, 5] for order in source_orders] block_dem = public_gari.circuit_to_gari( circuit, diff --git a/src/py/utils_test.py b/src/py/utils_test.py index 6e953dda..b9177596 100644 --- a/src/py/utils_test.py +++ b/src/py/utils_test.py @@ -55,21 +55,38 @@ def test_build_det_orders_default_index(): def test_build_det_orders_bfs(): - assert tesseract_decoder.utils.build_det_orders( - _DETECTOR_ERROR_MODEL, - num_det_orders=1, + path_dem = stim.DetectorErrorModel(""" + error(0.1) D0 D1 + error(0.1) D1 D2 + error(0.1) D2 D3 + error(0.1) D3 D4 + """) + orders = tesseract_decoder.utils.build_det_orders( + path_dem, + num_det_orders=8, method=tesseract_decoder.utils.DetOrder.DetBFS, seed=0, - ) == [[0, 1]] + ) + for order in orders: + distances_from_start = [abs(detector - order[0]) for detector in order] + assert distances_from_start == sorted(distances_from_start) def test_build_det_orders_coordinate(): - assert tesseract_decoder.utils.build_det_orders( - _DETECTOR_ERROR_MODEL, + dem = stim.DetectorErrorModel(""" + detector(0) D0 + detector(3) D1 + detector(1) D2 + detector(4) D3 + detector(2) D4 + """) + order = tesseract_decoder.utils.build_det_orders( + dem, num_det_orders=1, method=tesseract_decoder.utils.DetOrder.DetCoordinate, seed=0, - ) == [[0, 1]] + )[0] + assert order in ([3, 1, 4, 2, 0], [0, 2, 4, 1, 3]) def test_build_det_orders_index(): 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_main.cc b/src/tesseract_main.cc index e3c77890..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) { @@ -222,7 +229,7 @@ struct Args { } } - // Sample orientations of the error model to use for the det priority + // Choose the detector traversal orders. { if (verbose) { auto detector_coords = get_detector_coords(config.dem); @@ -236,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) { @@ -387,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 " @@ -678,6 +713,7 @@ int main(int argc, char* argv[]) { {"pqlimit", args.pqlimit}, {"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}, diff --git a/src/utils.cc b/src/utils.cc index a72a8ced..dccef1f8 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -23,6 +23,7 @@ #include #include #include +#include #include "common.h" #include "stim.h" @@ -121,11 +122,7 @@ static std::vector> build_det_orders_bfs(const stim::Detecto } while (visited[start]); } } - std::vector inv_perm(graph.size()); - for (size_t i = 0; i < perm.size(); ++i) { - inv_perm[perm[i]] = i; - } - det_orders[det_order] = inv_perm; + det_orders[det_order] = std::move(perm); } return det_orders; } @@ -159,11 +156,7 @@ static std::vector> build_det_orders_coordinate( std::sort(perm.begin(), perm.end(), [&](const size_t& i, const size_t& j) { return inner_products[i] > inner_products[j]; }); - std::vector inv_perm(dem.count_detectors()); - for (size_t i = 0; i < perm.size(); ++i) { - inv_perm[perm[i]] = i; - } - det_orders[det_order] = inv_perm; + det_orders[det_order] = std::move(perm); } return det_orders; } diff --git a/src/utils.pybind.h b/src/utils.pybind.h index 118b87e5..0b2cb109 100644 --- a/src/utils.pybind.h +++ b/src/utils.pybind.h @@ -110,8 +110,8 @@ void add_utils_module(py::module& root) { Returns ------- list[list[int]] - A list of detector orderings. Each inner list maps a detector index - to its position in the ordering. + A list of detector orderings. Each inner list gives the detector + IDs in traversal order. )pbdoc"); m.def( "get_errors_from_dem", From 837acf89de8c8e39ed27f2ea5206642f8344fc38 Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Fri, 21 Aug 2026 16:53:10 -0700 Subject: [PATCH 30/31] Fix clang formatting in GARI test --- src/tesseract.test.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index fcb21c79..abd2cbca 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -574,8 +574,7 @@ TEST(utils, GariSourcePrefixB8Decodes) { 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); + 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)) { From 9488efdcb12fc73a3bc7e8a7bdd42a7bdcb4430a Mon Sep 17 00:00:00 2001 From: arshpreetmaan Date: Fri, 21 Aug 2026 18:00:02 -0700 Subject: [PATCH 31/31] Keep existing detector-order generation behavior --- src/py/_tesseract_py_util/gari.py | 12 +++++----- src/py/_tesseract_py_util/gari_test.py | 7 ++++-- src/py/utils_test.py | 31 ++++++-------------------- src/utils.cc | 13 ++++++++--- src/utils.pybind.h | 4 ++-- 5 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/py/_tesseract_py_util/gari.py b/src/py/_tesseract_py_util/gari.py index 505a2b9b..d7a8e8de 100644 --- a/src/py/_tesseract_py_util/gari.py +++ b/src/py/_tesseract_py_util/gari.py @@ -715,7 +715,7 @@ def build_detector_orders( method: object | None = None, seed: int = 0, ) -> list[list[int]]: - """Builds orders for a source-aligned GARI DEM, with virtual IDs last.""" + """Builds traversal orders for a source-aligned GARI DEM.""" from tesseract_decoder import utils source_dem = _circuit_to_gari_source_dem(circuit) @@ -727,11 +727,13 @@ def build_detector_orders( 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 [ - order + virtual_detectors - for order in utils.build_det_orders( - source_dem, num_det_orders, method=method, seed=seed - ) + sorted(range(source_detector_count), key=positions.__getitem__) + + virtual_detectors + for positions in source_positions ] diff --git a/src/py/_tesseract_py_util/gari_test.py b/src/py/_tesseract_py_util/gari_test.py index 8f95d878..eedf7b5a 100644 --- a/src/py/_tesseract_py_util/gari_test.py +++ b/src/py/_tesseract_py_util/gari_test.py @@ -170,7 +170,7 @@ def test_public_circuit_conversion_and_file_output(tmp_path): np.testing.assert_array_equal( checks.toarray(), transform.checks[[2, 0, 3, 1, 4, 5], :].toarray() ) - source_orders = utils.build_det_orders( + source_positions = utils.build_det_orders( gari._circuit_to_gari_source_dem(circuit), 2, method=utils.DetOrder.DetCoordinate, @@ -182,7 +182,10 @@ def test_public_circuit_conversion_and_file_output(tmp_path): 2, method=utils.DetOrder.DetCoordinate, seed=0, - ) == [order + [4, 5] for order in source_orders] + ) == [ + sorted(range(4), key=positions.__getitem__) + [4, 5] + for positions in source_positions + ] block_dem = public_gari.circuit_to_gari( circuit, diff --git a/src/py/utils_test.py b/src/py/utils_test.py index b9177596..6e953dda 100644 --- a/src/py/utils_test.py +++ b/src/py/utils_test.py @@ -55,38 +55,21 @@ def test_build_det_orders_default_index(): def test_build_det_orders_bfs(): - path_dem = stim.DetectorErrorModel(""" - error(0.1) D0 D1 - error(0.1) D1 D2 - error(0.1) D2 D3 - error(0.1) D3 D4 - """) - orders = tesseract_decoder.utils.build_det_orders( - path_dem, - num_det_orders=8, + assert tesseract_decoder.utils.build_det_orders( + _DETECTOR_ERROR_MODEL, + num_det_orders=1, method=tesseract_decoder.utils.DetOrder.DetBFS, seed=0, - ) - for order in orders: - distances_from_start = [abs(detector - order[0]) for detector in order] - assert distances_from_start == sorted(distances_from_start) + ) == [[0, 1]] def test_build_det_orders_coordinate(): - dem = stim.DetectorErrorModel(""" - detector(0) D0 - detector(3) D1 - detector(1) D2 - detector(4) D3 - detector(2) D4 - """) - order = tesseract_decoder.utils.build_det_orders( - dem, + assert tesseract_decoder.utils.build_det_orders( + _DETECTOR_ERROR_MODEL, num_det_orders=1, method=tesseract_decoder.utils.DetOrder.DetCoordinate, seed=0, - )[0] - assert order in ([3, 1, 4, 2, 0], [0, 2, 4, 1, 3]) + ) == [[0, 1]] def test_build_det_orders_index(): diff --git a/src/utils.cc b/src/utils.cc index dccef1f8..a72a8ced 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -23,7 +23,6 @@ #include #include #include -#include #include "common.h" #include "stim.h" @@ -122,7 +121,11 @@ static std::vector> build_det_orders_bfs(const stim::Detecto } while (visited[start]); } } - det_orders[det_order] = std::move(perm); + std::vector inv_perm(graph.size()); + for (size_t i = 0; i < perm.size(); ++i) { + inv_perm[perm[i]] = i; + } + det_orders[det_order] = inv_perm; } return det_orders; } @@ -156,7 +159,11 @@ static std::vector> build_det_orders_coordinate( std::sort(perm.begin(), perm.end(), [&](const size_t& i, const size_t& j) { return inner_products[i] > inner_products[j]; }); - det_orders[det_order] = std::move(perm); + std::vector inv_perm(dem.count_detectors()); + for (size_t i = 0; i < perm.size(); ++i) { + inv_perm[perm[i]] = i; + } + det_orders[det_order] = inv_perm; } return det_orders; } diff --git a/src/utils.pybind.h b/src/utils.pybind.h index 0b2cb109..118b87e5 100644 --- a/src/utils.pybind.h +++ b/src/utils.pybind.h @@ -110,8 +110,8 @@ void add_utils_module(py::module& root) { Returns ------- list[list[int]] - A list of detector orderings. Each inner list gives the detector - IDs in traversal order. + A list of detector orderings. Each inner list maps a detector index + to its position in the ordering. )pbdoc"); m.def( "get_errors_from_dem",