From f166b0fd602b4834b21331c33114ef7b2711a155 Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Sun, 26 Jul 2026 19:51:54 -0400 Subject: [PATCH 1/4] add DetectorErrorModelArrays.without_detectors --- src/qldpc/decoders/dems.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/qldpc/decoders/dems.py b/src/qldpc/decoders/dems.py index effb1c5ef..ea7d30003 100644 --- a/src/qldpc/decoders/dems.py +++ b/src/qldpc/decoders/dems.py @@ -339,13 +339,13 @@ def simplified(self) -> DetectorErrorModelArrays: """Simplify this DetectorErrorModelArrays object by merging errors.""" return DetectorErrorModelArrays(self.to_detector_error_model(), simplify=True) - def without_untriggered_detectors(self) -> DetectorErrorModelArrays: - """Drop all detectors that are not triggered by any error mechanism. + def without_detectors(self, detectors: Collection[int]) -> DetectorErrorModelArrays: + """Drop the given detectors, keeping all error mechanisms and observables. - Such detectors are deterministically 0, so removing them changes no sampling outcome -- it - only shrinks the DEM. Error mechanisms and observables are left untouched. + Remaining detectors get re-indexed to range(self.num_detectors). """ - keep = self.detector_flip_matrix.getnnz(axis=1) > 0 + keep = np.ones(self.num_detectors, dtype=bool) + keep[list(detectors)] = False if keep.all(): return self old_to_new = np.cumsum(keep) - 1 @@ -361,6 +361,14 @@ def without_untriggered_detectors(self) -> DetectorErrorModelArrays: simplify=False, ) + def without_untriggered_detectors(self): + """Drop all detectors that are not triggered by any error mechanism. + + Such detectors are deterministically 0, so removing them changes no sampling outcome -- it + only shrinks the DEM. Error mechanisms and observables are left untouched. + """ + return self.without_detectors(np.flatnonzero(self.detector_flip_matrix.getnnz(axis=1) == 0)) + def with_decomposed_errors(self, *, simplify: bool = True) -> DetectorErrorModelArrays: """Split error mechanisms according to their suggested decompositions. From 7de683e70859ffc4d1e6c33aeb6677dbc80fa1b8 Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Sun, 26 Jul 2026 19:53:02 -0400 Subject: [PATCH 2/4] type fix --- src/qldpc/decoders/dems.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qldpc/decoders/dems.py b/src/qldpc/decoders/dems.py index ea7d30003..295b8b0a9 100644 --- a/src/qldpc/decoders/dems.py +++ b/src/qldpc/decoders/dems.py @@ -361,7 +361,7 @@ def without_detectors(self, detectors: Collection[int]) -> DetectorErrorModelArr simplify=False, ) - def without_untriggered_detectors(self): + def without_untriggered_detectors(self) -> DetectorErrorModelArrays: """Drop all detectors that are not triggered by any error mechanism. Such detectors are deterministically 0, so removing them changes no sampling outcome -- it From 52b2093e9260c809e230c35c3ca5ee1bf0642a6d Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Sun, 26 Jul 2026 20:20:10 -0400 Subject: [PATCH 3/4] add DetectorErrorModelArrays.copy --- .github/workflows/continuous-integration.yaml | 3 ++- src/qldpc/decoders/dems.py | 21 ++++++++++++++----- src/qldpc/decoders/dems_test.py | 6 ++++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 35ac7d327..ce7b26607 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -94,4 +94,5 @@ jobs: - name: Install package and dependencies run: uv sync --extra dev - name: Run coverage - run: uv run python checks/coverage_.py + # `-j $(nproc)` runs one modular coverage job per vCPU + run: uv run python checks/coverage_.py -j $(nproc) diff --git a/src/qldpc/decoders/dems.py b/src/qldpc/decoders/dems.py index 295b8b0a9..c467fd9b3 100644 --- a/src/qldpc/decoders/dems.py +++ b/src/qldpc/decoders/dems.py @@ -171,6 +171,15 @@ def from_arrays( dem_arrays.suggested_decompositions = suggested_decompositions or {} return dem_arrays.simplified() if simplify else dem_arrays + def copy(self) -> DetectorErrorModelArrays: + """Return an independent copy of this DetectorErrorModelArrays.""" + dem_arrays = object.__new__(DetectorErrorModelArrays) + dem_arrays.detector_flip_matrix = self.detector_flip_matrix.copy() + dem_arrays.observable_flip_matrix = self.observable_flip_matrix.copy() + dem_arrays.error_probs = self.error_probs.copy() + dem_arrays.suggested_decompositions = dict(self.suggested_decompositions) + return dem_arrays + @property def num_errors(self) -> int: """The number of distinct circuit errors.""" @@ -308,7 +317,9 @@ def to_detector_error_model(self) -> stim.DetectorErrorModel: obs_targets = [stim.DemTarget.logical_observable_id(oo) for oo in observables] targets = det_targets + obs_targets - dem.append("error", prob, targets) + # use DemInstruction (rather than dem.append("error", prob, targets)) so that error + # mechanisms with no targets -- valid, deterministically silent errors -- can be emitted + dem.append(stim.DemInstruction("error", [prob], targets)) return dem @@ -340,14 +351,14 @@ def simplified(self) -> DetectorErrorModelArrays: return DetectorErrorModelArrays(self.to_detector_error_model(), simplify=True) def without_detectors(self, detectors: Collection[int]) -> DetectorErrorModelArrays: - """Drop the given detectors, keeping all error mechanisms and observables. + """Drop the given detectors. Drop or merge error mechanisms that re empty or redundant. - Remaining detectors get re-indexed to range(self.num_detectors). + Remaining detectors get re-indexed to range(num_remaining_detectors). """ keep = np.ones(self.num_detectors, dtype=bool) keep[list(detectors)] = False if keep.all(): - return self + return self.copy() old_to_new = np.cumsum(keep) - 1 suggested_decompositions = { error_index: _remap_decomposition_detectors(components, keep, old_to_new) @@ -358,7 +369,7 @@ def without_detectors(self, detectors: Collection[int]) -> DetectorErrorModelArr self.observable_flip_matrix, self.error_probs, suggested_decompositions, - simplify=False, + simplify=True, ) def without_untriggered_detectors(self) -> DetectorErrorModelArrays: diff --git a/src/qldpc/decoders/dems_test.py b/src/qldpc/decoders/dems_test.py index 0b173fe17..edfc6d1e9 100644 --- a/src/qldpc/decoders/dems_test.py +++ b/src/qldpc/decoders/dems_test.py @@ -248,9 +248,11 @@ def test_post_selection() -> None: def test_without_untriggered_detectors() -> None: """Drop detectors that no error mechanism triggers.""" - # with no dead detectors, the object is returned unchanged + # with no dead detectors, an equivalent (independent) copy is returned dem_arrays = decoders.DetectorErrorModelArrays(stim.DetectorErrorModel("error(0.1) D0")) - assert dem_arrays.without_untriggered_detectors() is dem_arrays + result = dem_arrays.without_untriggered_detectors() + assert result is not dem_arrays + assert result.to_dem() == dem_arrays.to_dem() # D1 is dead but appears in the decomposition D0 D1 ^ D2 D1, where it cancels; it must be # filtered out rather than remapped to a stale index (live detectors: D0 -> 0, D2 -> 1) From 9ebe9e32263a79b70b9ae564c36a9f5161804c4c Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Sun, 26 Jul 2026 20:34:08 -0400 Subject: [PATCH 4/4] remove accidental CI change --- .github/workflows/continuous-integration.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index ce7b26607..35ac7d327 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -94,5 +94,4 @@ jobs: - name: Install package and dependencies run: uv sync --extra dev - name: Run coverage - # `-j $(nproc)` runs one modular coverage job per vCPU - run: uv run python checks/coverage_.py -j $(nproc) + run: uv run python checks/coverage_.py