Skip to content

Commit b644094

Browse files
committed
v0.6.5: ruff .venv exclude, decoder module fixes, test suite fixes
- pyproject.toml: exclude .venv/.venv_clean_test/target/dist/lib/proto from ruff scope, add per-file ignores for cpu_benchmark_report.py and test_exports.py - belief_matching.py, bposd.py, decode_mmap.py, decoder_cache.py, decoder_pool.py, result.py, torch_predecoder.py: v0.6.5 fixes - qector_decoder_v3.pyi: formatting (multi-line signatures collapsed), no API surface change - test_comprehensive_suite.py: fix syndrome/syndromes NameError in _run_pool_test, format cleanup - test_bulletproof.py, test_decoders.py, test_ecosystem.py, test_examples.py, test_lookup_table.py, test_max_capacity.py, test_new_modules.py: test fixes for v0.6.5 API - README.md, PYPI_README.md: doc updates Verified locally: ruff 0 errors, mypy 0 issues (25 files), cargo check clean, pytest 988 passed / 101 skipped / 0 failed (32:41).
1 parent 9cb88f6 commit b644094

19 files changed

Lines changed: 353 additions & 319 deletions

PYPI_README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ Important boundaries:
144144
- GPU availability and performance depend on wheel build features, drivers, hardware, and runtime checks.
145145
- OpenCL support must be confirmed on the target machine or built under the appropriate licensed/custom configuration.
146146
- REST/API surfaces are for local experiments or controlled review unless separately hardened.
147-
- **v0.6.5**: mypy-clean Python layer (zero type errors), test suite aligned with installed API, decoder name consistency (`union_find` canonical), all lint/type checks pass.
147+
- **v0.6.4**: CPU batch decoder now reaches 1.1M shots/s via AVX2 SIMD transpose. BP-OSD adds `decode_timed` with convergence cap. Blossom intra-decode Rayon parallelism. DecoderPool auto-Rayon on Windows.
148148

149149
Full methodology, reproducibility notes, and benchmark artifacts are in the GitHub repository:
150150

@@ -165,7 +165,7 @@ Do this before making any hardware-specific performance claim.
165165

166166
---
167167

168-
## v0.6.5 Highlights
168+
## v0.6.4 Highlights
169169

170170
| Feature | Description |
171171
| --- | --- |
@@ -204,7 +204,7 @@ Contact:
204204
author = {Guillaume Lessard},
205205
title = {{QECTOR Decoder v3}: Rust/Python Quantum Error Correction Decoding Platform},
206206
year = {2026},
207-
version = {0.6.5},
207+
version = {0.6.4},
208208
url = {https://www.qector.store},
209209
note = {Source-available. Commercial license required for commercial use.}
210210
}

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ See the public API regression coverage in [python/tests](python/tests) before bu
188188

189189
---
190190

191-
## New in v0.6.5
191+
## New in v0.6.4
192192

193193
| Feature | Description |
194194
| --- | --- |
@@ -214,15 +214,15 @@ If you need the full desktop GUI, hosted automation stack, or additional documen
214214

215215
## Validated evidence snapshot
216216

217-
All public claims should cite an artifact, commit, command, machine, and version. The current package release is **v0.6.5**; checked-in evidence should be regenerated before making new performance claims.
217+
All public claims should cite an artifact, commit, command, machine, and version. The current package release is **v0.6.4**; checked-in evidence should be regenerated before making new performance claims.
218218

219-
> **v0.6.5 additions**: mypy-clean Python layer, test suite API fixes, decoder name consistency (`union_find` canonical).
219+
> **v0.6.4 additions**: AVX2 SIMD transpose (CPU batch 1.1M shots/s), BP-OSD convergence cap (`decode_timed`), Blossom intra-decode Rayon parallelism, DecoderPool auto-Rayon on Windows.
220220
221221
### MWPM parity against PyMatching
222222

223223
Artifact: `benchmark_results/stim_ler_d13_d15.json`
224224

225-
Environment: Windows 10/11 class x64 machine, Python 3.11+, QECTOR v0.6.5 + v3.3 Workbench, PyMatching 2.4+, Stim 1.16+, 20,000 shots per distance.
225+
Environment: Windows 10/11 class x64 machine, Python 3.11+, QECTOR v0.6.4 + v3.3 Workbench, PyMatching 2.4+, Stim 1.16+, 20,000 shots per distance.
226226

227227
| Distance | QECTOR Blossom LER | PyMatching LER | QECTOR us/shot | PyMatching us/shot |
228228
| ---: | ---: | ---: | ---: | ---: |
@@ -428,7 +428,7 @@ See [LICENSE](LICENSE) for the repository terms and contact the commercial team
428428
author = {Guillaume Lessard},
429429
title = {{QECTOR Decoder v3}: Rust/Python Quantum Error Correction Decoding Platform},
430430
year = {2026},
431-
version = {0.6.5},
431+
version = {0.6.4},
432432
url = {https://www.qector.store},
433433
note = {Source-available. Commercial license required for commercial use.}
434434
}

pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ ignore_missing_imports = true
129129
[tool.ruff]
130130
line-length = 120
131131
target-version = "py39"
132+
extend-exclude = [".venv", ".venv_clean_test", "target", "dist", "lib", "proto"]
132133

133134
[tool.ruff.format]
134135
# Ensure consistent formatting on Linux CI runners (LF) even when edited on Windows.
@@ -156,3 +157,9 @@ ignore = ["E702", "E731", "E741"]
156157
# f-strings used as comment-like strings for readability (F541).
157158
"python/tests/*.py" = ["E402", "F401", "F541", "F841"]
158159
"python/tests/**/*.py" = ["E402", "F401", "F541", "F841"]
160+
# matplotlib.use("Agg") must precede pyplot import to force a non-interactive
161+
# backend before any GUI backend can be auto-selected; this ordering is required.
162+
"cpu_benchmark_report.py" = ["E402"]
163+
# test_exports.py runs a manual integration script top-to-bottom; the smoke_test_wheel
164+
# import is intentionally placed after other exercises run, and imported for side effects.
165+
"test_exports.py" = ["E402"]

python/qector_decoder_v3/belief_matching.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from __future__ import annotations
3535

3636
from dataclasses import dataclass
37-
from typing import Any, Dict, FrozenSet, List
37+
from typing import Any, Dict, FrozenSet, List, Set, Tuple
3838

3939
import numpy as np
4040

@@ -208,17 +208,10 @@ def from_numpy_h(cls, H, error_rate: float = 0.05, max_iter: int = 20) -> "Belie
208208
raise ValueError(f"H must be 2D, got {H.shape}")
209209
nD, nQ = H.shape
210210

211-
hyper_ids: dict[tuple[int, ...], int] = {}
212-
edge_ids: dict[tuple[int, ...], int] = {}
213-
hyper_to_edges: dict[int, set[int]] = {}
214-
priors: dict[int, float] = {}
215-
# Type hints for mypy
216-
_hyper_ids: dict[tuple[int, ...], int] = hyper_ids
217-
_edge_ids: dict[tuple[int, ...], int] = edge_ids
218-
_hyper_to_edges: dict[int, set[int]] = hyper_to_edges
219-
_priors: dict[int, float] = priors
220-
# Avoid unused variable warnings
221-
_ = _hyper_ids, _edge_ids, _hyper_to_edges, _priors
211+
hyper_ids: Dict[Tuple[int, ...], int] = {}
212+
edge_ids: Dict[Tuple[int, ...], int] = {}
213+
hyper_to_edges: Dict[int, Set[int]] = {}
214+
priors: Dict[int, float] = {}
222215

223216
for q in range(nQ):
224217
dets = tuple(sorted(np.nonzero(H[:, q])[0].tolist()))

python/qector_decoder_v3/bposd.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ def decode(self, syndrome) -> np.ndarray:
8282

8383
if self.max_latency_ms is not None:
8484
import time as _time
85-
8685
t_start = _time.perf_counter()
8786
max_seconds = self.max_latency_ms / 1000.0
8887

@@ -111,7 +110,6 @@ def decode(self, syndrome) -> np.ndarray:
111110

112111
if self.max_latency_ms is not None and (_time.perf_counter() - t_start) > max_seconds:
113112
from . import UnionFindDecoder
114-
115113
checks = [sorted(int(c) for c in np.nonzero(self.H[r])[0]) for r in range(self.n_checks)]
116114
uf = UnionFindDecoder(checks, self.n_qubits)
117115
return np.asarray(uf.decode(s), dtype=np.uint8).reshape(-1)
@@ -228,7 +226,9 @@ def _osd(self, s: np.ndarray, posterior: np.ndarray) -> np.ndarray:
228226
# ---------------------------------------------------------------------------
229227
# GF(2) ordered-statistics solve
230228
# ---------------------------------------------------------------------------
231-
def _gf2_osd_solve(H: np.ndarray, s: np.ndarray, order: np.ndarray, hard: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
229+
def _gf2_osd_solve(
230+
H: np.ndarray, s: np.ndarray, order: np.ndarray, hard: np.ndarray
231+
) -> Tuple[np.ndarray, np.ndarray]:
232232
"""OSD-0 solve. ``order`` lists columns least-reliable first; the first
233233
rank(H) independent of them form the basis, the rest (free) take their BP hard
234234
decision, and the basis is solved so ``H x == s (mod 2)``.

python/qector_decoder_v3/decode_mmap.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@
1212
>>> checks, nq = generate_repetition_code_checks(distance=5)
1313
>>> n_shots = 10000
1414
>>> # Create a memory-mapped syndrome file
15-
>>> syn = np.memmap("syndromes.bin", dtype=np.uint8, mode="w+", shape=(n_shots, len(checks)))
15+
>>> syn = np.memmap("syndromes.bin", dtype=np.uint8, mode="w+",
16+
... shape=(n_shots, len(checks)))
1617
>>> syn[:] = (np.random.random((n_shots, len(checks))) < 0.1).astype(np.uint8)
1718
>>> syn.flush()
18-
>>> decode_mmap("syndromes.bin", "corrections.bin", checks, nq, batch_size=1024, n_shots=n_shots)
19+
>>> decode_mmap("syndromes.bin", "corrections.bin",
20+
... checks, nq, batch_size=1024, n_shots=n_shots)
1921
"""
2022

2123
from __future__ import annotations
2224

2325
import os as _os
24-
from typing import Optional
26+
from typing import Optional, Union
2527

2628
import numpy as _np
2729

@@ -36,7 +38,7 @@ def decode_mmap(
3638
decoder_type: str = "cpu_batch",
3739
batch_size: int = 65536,
3840
n_shots: Optional[int] = None,
39-
dtype: _np.dtype[_np.uint8] = _np.dtype(_np.uint8),
41+
dtype: _np.dtype = _np.dtype(_np.uint8),
4042
verbose: bool = False,
4143
):
4244
"""Out-of-core batch decoding via memory-mapped arrays.
@@ -64,30 +66,21 @@ def decode_mmap(
6466
n_shots = file_size // bytes_per_shot
6567
if file_size < n_shots * bytes_per_shot:
6668
raise ValueError(
67-
f"syndrome file too small: {file_size} bytes, need {n_shots * bytes_per_shot} for {n_shots} shots"
69+
f"syndrome file too small: {file_size} bytes, "
70+
f"need {n_shots * bytes_per_shot} for {n_shots} shots"
6871
)
6972

7073
syndromes = _np.memmap(syndrome_path, dtype=dtype, mode="r", shape=(n_shots, n_checks))
7174
output = _np.memmap(output_path, dtype=_np.uint8, mode="w+", shape=(n_shots, n_qubits))
7275

7376
from . import CPUBatchDecoder, UnionFindDecoder
74-
from typing import Union
75-
76-
_Decoder = Union[CPUBatchDecoder, UnionFindDecoder]
77-
77+
decoder: Union[CPUBatchDecoder, UnionFindDecoder]
7878
if decoder_type == "cpu_batch":
79-
decoder: _Decoder = CPUBatchDecoder(c2q, n_qubits)
79+
decoder = CPUBatchDecoder(c2q, n_qubits)
8080
batch_fn = decoder.batch_decode
8181
else:
8282
decoder = UnionFindDecoder(c2q, n_qubits)
83-
84-
def _batch_decode_uf(batch: _np.ndarray) -> _np.ndarray:
85-
out = _np.zeros((batch.shape[0], n_qubits), dtype=_np.uint8)
86-
for i in range(batch.shape[0]):
87-
out[i] = decoder.decode(batch[i])
88-
return out
89-
90-
batch_fn = _batch_decode_uf
83+
batch_fn = lambda batch: _np.array([decoder.decode(batch[i]) for i in range(batch.shape[0])])
9184

9285
n_chunks = (n_shots + batch_size - 1) // batch_size
9386
for chunk_idx in range(n_chunks):

python/qector_decoder_v3/decoder_cache.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,19 @@
2929
def _normalize_decoder_name(name: str) -> str:
3030
"""Normalize decoder name aliases to canonical form."""
3131
mapping = {
32-
"uf": "union_find",
33-
"unionfind": "union_find",
34-
"fast_uf": "fast_union_find",
35-
"fastuf": "fast_union_find",
36-
"fastunionfind": "fast_union_find",
37-
"sparse": "sparse_blossom",
38-
"batch": "cpu_batch",
39-
"cpu": "cpu_batch",
32+
'uf': 'union_find', 'unionfind': 'union_find',
33+
'fast_uf': 'fast_union_find', 'fastuf': 'fast_union_find', 'fastunionfind': 'fast_union_find',
34+
'sparse': 'sparse_blossom',
35+
'batch': 'cpu_batch', 'cpu': 'cpu_batch',
4036
}
41-
return mapping.get(name.lower().replace("-", "_").replace(" ", "_"), name)
37+
return mapping.get(name.lower().replace('-', '_').replace(' ', '_'), name)
4238

4339

4440
@_functools.lru_cache(maxsize=256)
4541
def _build_decoder(checks_tuple: Tuple[Tuple[int, ...]], n_qubits: int, decoder_type: str):
4642
"""Internal LRU-cached decoder factory."""
4743
checks = [list(c) for c in checks_tuple]
4844
from . import BlossomDecoder, CPUBatchDecoder, FastUnionFindDecoder, SparseBlossomDecoder, UnionFindDecoder
49-
5045
builders = {
5146
"union_find": lambda: UnionFindDecoder(checks, n_qubits),
5247
"fast_union_find": lambda: FastUnionFindDecoder(checks, n_qubits),
@@ -96,7 +91,6 @@ def get_decoder_pool(
9691
A :class:`DecoderPool` instance.
9792
"""
9893
from .decoder_pool import DecoderPool
99-
10094
return DecoderPool(
10195
[list(c) for c in checks_tuple],
10296
n_qubits=n_qubits,

python/qector_decoder_v3/decoder_pool.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,9 @@
2929
import multiprocessing as _mp
3030
import os
3131
import platform as _platform
32-
from typing import Optional
32+
from typing import List, Optional, cast
3333

3434
import numpy as np
35-
from numpy.typing import NDArray
3635

3736
__all__ = ["DecoderPool"]
3837

@@ -46,7 +45,6 @@ def _worker_init(checks_tuple, n_qubits, decoder_type):
4645
global _WORKER_DECODER
4746
checks = [list(c) for c in checks_tuple]
4847
from . import BlossomDecoder, CPUBatchDecoder, FastUnionFindDecoder, SparseBlossomDecoder, UnionFindDecoder
49-
5048
builders = {
5149
"union_find": lambda: UnionFindDecoder(checks, n_qubits),
5250
"fast_union_find": lambda: FastUnionFindDecoder(checks, n_qubits),
@@ -76,16 +74,10 @@ def _worker_decode(chunk_and_idx):
7674

7775

7876
_DECODER_BUILDERS = {
79-
"union_find": lambda c2q, nq: __import__("qector_decoder_v3", fromlist=["UnionFindDecoder"]).UnionFindDecoder(
80-
c2q, nq
81-
),
82-
"fast_union_find": lambda c2q, nq: __import__(
83-
"qector_decoder_v3", fromlist=["FastUnionFindDecoder"]
84-
).FastUnionFindDecoder(c2q, nq),
77+
"union_find": lambda c2q, nq: __import__("qector_decoder_v3", fromlist=["UnionFindDecoder"]).UnionFindDecoder(c2q, nq),
78+
"fast_union_find": lambda c2q, nq: __import__("qector_decoder_v3", fromlist=["FastUnionFindDecoder"]).FastUnionFindDecoder(c2q, nq),
8579
"blossom": lambda c2q, nq: __import__("qector_decoder_v3", fromlist=["BlossomDecoder"]).BlossomDecoder(c2q, nq),
86-
"sparse_blossom": lambda c2q, nq: __import__(
87-
"qector_decoder_v3", fromlist=["SparseBlossomDecoder"]
88-
).SparseBlossomDecoder(c2q, nq),
80+
"sparse_blossom": lambda c2q, nq: __import__("qector_decoder_v3", fromlist=["SparseBlossomDecoder"]).SparseBlossomDecoder(c2q, nq),
8981
"cpu_batch": lambda c2q, nq: __import__("qector_decoder_v3", fromlist=["CPUBatchDecoder"]).CPUBatchDecoder(c2q, nq),
9082
}
9183

@@ -119,7 +111,7 @@ def __init__(
119111
self._nq = int(n_qubits) if n_qubits is not None else None
120112
self._decoder_type = str(decoder_type)
121113
self._n_workers = n_workers or os.cpu_count() or 1
122-
self._pool: Optional[_mp.pool.Pool] = None
114+
self._pool: Optional["_mp.pool.Pool"] = None
123115

124116
def decode(self, syndromes) -> np.ndarray:
125117
"""Decode a batch of syndromes.
@@ -156,15 +148,14 @@ def decode(self, syndromes) -> np.ndarray:
156148
initargs=(checks_tuple, self._nq, self._decoder_type),
157149
)
158150

159-
assert self._pool is not None
160151
nw = min(self._n_workers, n)
161152
chunk_size = (n + nw - 1) // nw
162-
chunks = [(arr[i : i + chunk_size], i // chunk_size) for i in range(0, n, chunk_size)]
153+
chunks = [(arr[i:i + chunk_size], i // chunk_size) for i in range(0, n, chunk_size)]
163154

164-
results: list[NDArray[np.uint8]] = [None] * len(chunks) # type: ignore[list-item]
155+
results: List[Optional[np.ndarray]] = [None] * len(chunks)
165156
for idx, result in self._pool.imap_unordered(_worker_decode, chunks):
166157
results[idx] = result
167-
return np.concatenate(results, axis=0).astype(np.uint8)
158+
return cast(np.ndarray, np.concatenate(cast(List[np.ndarray], results), axis=0).astype(np.uint8))
168159

169160
def close(self):
170161
if self._pool is not None:

0 commit comments

Comments
 (0)