tridec 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tridec/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ """tridec: vendor-portable GPU decoders for quantum LDPC codes.
2
+
3
+ Triton min-sum BP and Relay-BP decoders that consume any stim
4
+ DetectorErrorModel or raw parity-check matrices, with CPU reference
5
+ implementations, validated against the standard CPU references (ldpc,
6
+ relay-bp), running on NVIDIA (CUDA) and AMD (ROCm) GPUs.
7
+
8
+ Quickstart::
9
+
10
+ import stim, tridec
11
+
12
+ circuit = stim.Circuit.from_file("memory.stim")
13
+ dem = circuit.detector_error_model(decompose_errors=False)
14
+ decoder = tridec.from_dem(dem, backend="auto")
15
+
16
+ dets, obs = circuit.compile_detector_sampler(seed=0).sample(
17
+ 10_000, separate_observables=True)
18
+ pred = decoder.decode_batch(dets) # (shots, n_obs) bool
19
+ ler = (pred != obs).any(axis=1).mean()
20
+ """
21
+ from .api import (
22
+ BpDecoder,
23
+ RelayBpDecoder,
24
+ available_backends,
25
+ from_dem,
26
+ from_matrices,
27
+ resolve_backend,
28
+ )
29
+ from .dem import extract
30
+
31
+ # Single-sourced from the installed distribution metadata (pyproject.toml).
32
+ try:
33
+ from importlib.metadata import version as _dist_version
34
+ __version__ = _dist_version("tridec")
35
+ except Exception: # pragma: no cover - uninstalled source tree
36
+ __version__ = "0.1.0"
37
+
38
+ __all__ = [
39
+ "BpDecoder",
40
+ "RelayBpDecoder",
41
+ "available_backends",
42
+ "extract",
43
+ "from_dem",
44
+ "from_matrices",
45
+ "resolve_backend",
46
+ "__version__",
47
+ ]
@@ -0,0 +1,258 @@
1
+ """Optional CPU reference-decoder adapters on a SHARED DEM (import-guarded).
2
+
3
+ These wrap the standard CPU reference implementations — the `ldpc` package's
4
+ BP / BP-OSD / BP-LSD and IBM's `relay-bp` Rust decoder — behind the same
5
+ ``decode_batch(dets) -> predicted_observables`` surface as the native
6
+ backends, so a matched harness (``tridec.validation.run_matched``) can
7
+ decode the SAME shots with every decoder (apples-to-apples LER). They are the
8
+ validation targets the GPU kernels are held against.
9
+
10
+ Install with the ``decoders`` extra: ``pip install tridec[decoders]``.
11
+ The module imports without either package; each factory raises (or the
12
+ ``*_available()`` probes return False) when its dependency is missing.
13
+
14
+ Interface (every adapter):
15
+ * ``.name`` -- str identifier (e.g. ``"BPOSD-10"``),
16
+ * ``.config`` -- dict of pinned hyperparameters (provenance),
17
+ * ``.dem`` -- the shared ``stim.DetectorErrorModel`` it was built from,
18
+ * ``.tie_break`` -- declared deterministic tie-break (gate G2),
19
+ * ``.decode_batch(dets: bool[shots, n_det]) -> bool[shots, n_obs]``.
20
+
21
+ For an ldpc decoder, each shot's detector syndrome is decoded to an error
22
+ estimate ``e_hat`` (length n_err); predicted observables = ``(Lo @ e_hat) % 2``.
23
+ ldpc 2.4.x exposes only single-shot ``decoder.decode(syndrome)`` (no batched
24
+ entry point), so ldpc adapters loop over shots.
25
+ """
26
+ import numpy as np
27
+
28
+ from ..dem import extract
29
+
30
+ # Pinned min-sum BP hyperparameters shared across the BP-family adapters
31
+ # (the provenance constants the validation grid committed to).
32
+ _BP_MAX_ITER = 30
33
+ _BP_MS_SCALING = 0.625 # standard normalized-min-sum scaling factor
34
+ _BP_METHOD = "minimum_sum" # min-sum BP (the kernel target)
35
+ _BP_SCHEDULE = "parallel"
36
+
37
+
38
+ def ldpc_available():
39
+ """True iff the `ldpc` package is importable."""
40
+ try:
41
+ import ldpc # noqa: F401
42
+ except Exception:
43
+ return False
44
+ return True
45
+
46
+
47
+ def relay_bp_available():
48
+ """True iff relay-bp[stim] is importable (import-guarded membership)."""
49
+ try:
50
+ import relay_bp # noqa: F401
51
+ from relay_bp.stim import CheckMatrices # noqa: F401
52
+ except Exception:
53
+ return False
54
+ return True
55
+
56
+
57
+ class _LdpcAdapter:
58
+ """Base for ldpc-family adapters: build H/Lo/priors from the shared DEM,
59
+ decode each shot's syndrome to an error estimate, map to observables."""
60
+
61
+ def __init__(self, dem, name, config, decoder, tie_break):
62
+ self.dem = dem
63
+ self.name = name
64
+ self.config = dict(config)
65
+ # Declared deterministic tie-break (gate G2). No silent default: the
66
+ # matched harness asserts this is in APPROVED_TIE_BREAKS.
67
+ self.tie_break = tie_break
68
+ self._decoder = decoder
69
+ ex = extract(dem)
70
+ # Lo: (n_obs x n_err) GF2 map from error mechanisms to observables.
71
+ self._Lo = ex["Lo"].toarray().astype(np.uint8)
72
+ self._n_obs = ex["n_obs"]
73
+ self._n_err = ex["n_err"]
74
+ self._n_det = ex["n_det"]
75
+
76
+ def decode_batch(self, dets):
77
+ dets = np.asarray(dets, dtype=bool)
78
+ shots = dets.shape[0]
79
+ out = np.zeros((shots, self._n_obs), dtype=bool)
80
+ syn_u8 = dets.astype(np.uint8)
81
+ Lo = self._Lo
82
+ for i in range(shots):
83
+ e_hat = self._decoder.decode(syn_u8[i])
84
+ # predicted observables = (Lo @ e_hat) % 2
85
+ pred = (Lo @ np.asarray(e_hat, dtype=np.uint8)) & 1
86
+ out[i] = pred.astype(bool)
87
+ return out
88
+
89
+
90
+ def _priors(dem):
91
+ """Per-mechanism priors from the shared DEM, clipped for ldpc stability."""
92
+ pri = extract(dem)["priors"]
93
+ return list(np.clip(pri, 1e-6, 1 - 1e-6))
94
+
95
+
96
+ def make_bp(dem):
97
+ """Pure min-sum BP (no post-processing): ldpc.BpDecoder reference."""
98
+ from ldpc import BpDecoder
99
+
100
+ H = extract(dem)["H"]
101
+ cfg = dict(decoder="BpDecoder", bp_method=_BP_METHOD,
102
+ ms_scaling_factor=_BP_MS_SCALING, max_iter=_BP_MAX_ITER,
103
+ schedule=_BP_SCHEDULE)
104
+ dec = BpDecoder(H, error_channel=_priors(dem), max_iter=_BP_MAX_ITER,
105
+ bp_method=_BP_METHOD, ms_scaling_factor=_BP_MS_SCALING,
106
+ schedule=_BP_SCHEDULE)
107
+ return _LdpcAdapter(dem, "BP", cfg, dec, "min_sum_parallel_hard_decision")
108
+
109
+
110
+ def make_bposd0(dem):
111
+ """BP-OSD order-0 (osd_0): cheapest OSD post-processing."""
112
+ from ldpc import BpOsdDecoder
113
+
114
+ H = extract(dem)["H"]
115
+ cfg = dict(decoder="BpOsdDecoder", bp_method=_BP_METHOD,
116
+ ms_scaling_factor=_BP_MS_SCALING, max_iter=_BP_MAX_ITER,
117
+ schedule=_BP_SCHEDULE, osd_method="osd_0", osd_order=0)
118
+ dec = BpOsdDecoder(H, error_channel=_priors(dem), max_iter=_BP_MAX_ITER,
119
+ bp_method=_BP_METHOD, ms_scaling_factor=_BP_MS_SCALING,
120
+ schedule=_BP_SCHEDULE, osd_method="osd_0", osd_order=0)
121
+ return _LdpcAdapter(dem, "BPOSD-0", cfg, dec, "osd0_reliability_order")
122
+
123
+
124
+ def make_bposd10(dem):
125
+ """BP-OSD order-10 combination-sweep (osd_cs): the strong classical bar."""
126
+ from ldpc import BpOsdDecoder
127
+
128
+ H = extract(dem)["H"]
129
+ cfg = dict(decoder="BpOsdDecoder", bp_method=_BP_METHOD,
130
+ ms_scaling_factor=_BP_MS_SCALING, max_iter=_BP_MAX_ITER,
131
+ schedule=_BP_SCHEDULE, osd_method="osd_cs", osd_order=10)
132
+ dec = BpOsdDecoder(H, error_channel=_priors(dem), max_iter=_BP_MAX_ITER,
133
+ bp_method=_BP_METHOD, ms_scaling_factor=_BP_MS_SCALING,
134
+ schedule=_BP_SCHEDULE, osd_method="osd_cs", osd_order=10)
135
+ return _LdpcAdapter(dem, "BPOSD-10", cfg, dec, "osd_cs_order10")
136
+
137
+
138
+ def make_bplsd(dem):
139
+ """BP + Localised-Statistics Decoder (lsd_cs, order 10)."""
140
+ from ldpc import BpLsdDecoder
141
+
142
+ H = extract(dem)["H"]
143
+ lsd_order = 10
144
+ cfg = dict(decoder="BpLsdDecoder", bp_method=_BP_METHOD,
145
+ ms_scaling_factor=_BP_MS_SCALING, max_iter=_BP_MAX_ITER,
146
+ schedule=_BP_SCHEDULE, lsd_method="lsd_cs", lsd_order=lsd_order)
147
+ dec = BpLsdDecoder(H, error_channel=_priors(dem), max_iter=_BP_MAX_ITER,
148
+ bp_method=_BP_METHOD, ms_scaling_factor=_BP_MS_SCALING,
149
+ schedule=_BP_SCHEDULE, lsd_method="lsd_cs",
150
+ lsd_order=lsd_order)
151
+ return _LdpcAdapter(dem, "BPLSD", cfg, dec, "lsd_cs_order10")
152
+
153
+
154
+ # --------------------------------------------------------------------------- #
155
+ # Relay-BP (relay-bp[stim] >= 0.2.2) — IBM's Rust reference decoder. #
156
+ # --------------------------------------------------------------------------- #
157
+ # Construct-from-DEM:
158
+ # from relay_bp.stim import CheckMatrices
159
+ # cm = CheckMatrices.from_dem(dem) # -> .check_matrix (ndet x E csc),
160
+ # # .observables_matrix (nobs x E csc),
161
+ # # .error_priors (E,)
162
+ # dec = relay_bp.RelayDecoderF64(cm.check_matrix, error_priors=cm.error_priors,
163
+ # gamma0=, pre_iter=, num_sets=, set_max_iter=, gamma_dist_interval=,
164
+ # stop_nconv=, stopping_criterion='nconv') # disjoint-relay ensemble
165
+ # runner = relay_bp.ObservableDecoderRunner(dec, cm.observables_matrix,
166
+ # include_decode_result=False)
167
+ # Decode:
168
+ # runner.decode_observables_batch(syndromes uint8 [shots, n_det])
169
+ # -> predicted observables uint8 [shots, n_obs]
170
+ # This is the path relay_bp.stim.SinterDecoder_RelayBP uses internally, minus
171
+ # sinter's bit-packing — the runner is driven directly for a clean decode_batch.
172
+ _RELAY_BP_DEFAULTS = dict(
173
+ gamma0=0.1,
174
+ pre_iter=80,
175
+ num_sets=60,
176
+ set_max_iter=60,
177
+ gamma_dist_interval=(-0.24, 0.66),
178
+ stop_nconv=5,
179
+ stopping_criterion="nconv",
180
+ )
181
+
182
+
183
+ class RelayBPAdapter:
184
+ """Relay-BP adapter (in-process). Builds the relay-BP decoder from the SAME
185
+ shared DEM via ``relay_bp.stim.CheckMatrices.from_dem`` and decodes a batch
186
+ of syndromes straight to observables. G1 holds trivially: ``.dem is dem``."""
187
+
188
+ def __init__(self, dem, **params):
189
+ import importlib.metadata as _md
190
+
191
+ import relay_bp
192
+ from relay_bp.stim import CheckMatrices
193
+
194
+ self.dem = dem
195
+ self.name = "RelayBP"
196
+ try:
197
+ ver = _md.version("relay-bp")
198
+ except Exception: # pragma: no cover - metadata present once installed
199
+ ver = "unknown"
200
+ cfg = dict(_RELAY_BP_DEFAULTS)
201
+ cfg.update(params)
202
+ self.config = dict(decoder="RelayBP", relay_bp_version=ver, **cfg)
203
+ # Deterministic relay schedule (fixed gamma distribution + nconv stop).
204
+ self.tie_break = "relay_bp_nconv_disjoint_ensemble"
205
+
206
+ cm = CheckMatrices.from_dem(dem)
207
+ self._n_obs = cm.observables_matrix.shape[0]
208
+ decoder = relay_bp.RelayDecoderF64(
209
+ cm.check_matrix,
210
+ error_priors=cm.error_priors,
211
+ **cfg,
212
+ )
213
+ self._runner = relay_bp.ObservableDecoderRunner(
214
+ decoder, cm.observables_matrix, include_decode_result=False)
215
+
216
+ def decode_batch(self, dets):
217
+ dets = np.asarray(dets, dtype=bool)
218
+ pred = np.asarray(
219
+ self._runner.decode_observables_batch(dets.astype(np.uint8)))
220
+ pred = (pred % 2).astype(bool)
221
+ if pred.ndim == 1:
222
+ pred = pred.reshape(-1, 1)
223
+ return pred
224
+
225
+
226
+ def make_relay_bp(dem, **params):
227
+ return RelayBPAdapter(dem, **params)
228
+
229
+
230
+ # Registry: name -> factory(dem).
231
+ _FACTORIES = {
232
+ "BPOSD-0": make_bposd0,
233
+ "BPOSD-10": make_bposd10,
234
+ "BPLSD": make_bplsd,
235
+ "BP": make_bp,
236
+ }
237
+
238
+ DEFAULT_DECODERS = ("BPOSD-0", "BPOSD-10", "BPLSD", "BP")
239
+
240
+
241
+ def build_decoders(dem, which=DEFAULT_DECODERS, include_relay=False):
242
+ """Construct all requested adapters from ONE shared DEM object.
243
+
244
+ Every returned adapter has ``.dem is dem`` (provenance for the matched
245
+ harness). ``which`` selects/orders the ldpc-family adapters by registry
246
+ name. Relay-BP is OPT-IN via ``include_relay=True`` and is added ONLY when
247
+ its package is available (import-guarded), so the core set always builds.
248
+ """
249
+ decoders = []
250
+ for name in which:
251
+ if name not in _FACTORIES:
252
+ raise KeyError(f"unknown decoder {name!r}; known: {sorted(_FACTORIES)}")
253
+ decoders.append(_FACTORIES[name](dem))
254
+
255
+ if include_relay and relay_bp_available():
256
+ decoders.append(make_relay_bp(dem))
257
+
258
+ return decoders