pelagos-decoder 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.
- pelagos/__init__.py +34 -0
- pelagos/_process_worker.py +56 -0
- pelagos/adapter.py +858 -0
- pelagos/autotune.py +793 -0
- pelagos/decoder.py +3891 -0
- pelagos/graph.py +479 -0
- pelagos/kernels.py +2208 -0
- pelagos/process.py +287 -0
- pelagos/stim/__init__.py +11 -0
- pelagos/stim/sinter/__init__.py +11 -0
- pelagos/stim/sinter/check_matrices.py +112 -0
- pelagos/utils.py +995 -0
- pelagos_decoder-0.1.0.dist-info/METADATA +317 -0
- pelagos_decoder-0.1.0.dist-info/RECORD +16 -0
- pelagos_decoder-0.1.0.dist-info/WHEEL +4 -0
- pelagos_decoder-0.1.0.dist-info/licenses/LICENSE +201 -0
pelagos/adapter.py
ADDED
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
# (C) Copyright IBM 2026
|
|
2
|
+
#
|
|
3
|
+
# This code is licensed under the Apache License, Version 2.0. You may
|
|
4
|
+
# obtain a copy of this license in the LICENSE file in the root directory
|
|
5
|
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
#
|
|
7
|
+
# Any modifications or derivative works of this code must retain this
|
|
8
|
+
# copyright notice, and modified files need to carry a notice indicating
|
|
9
|
+
# that they have been altered from the originals.
|
|
10
|
+
#
|
|
11
|
+
# Modified 2026 by AMD: opt-in process-per-GPU dispatch and parallel preparation.
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
Triton adapter for Relay-BP decoder providing compatibility with relay_bp interface.
|
|
15
|
+
|
|
16
|
+
This module provides adapter classes that wrap the Triton GPU implementation
|
|
17
|
+
to match the interface expected by the relay_bp package, enabling seamless
|
|
18
|
+
integration with existing benchmarking and analysis tools.
|
|
19
|
+
|
|
20
|
+
The adapter handles:
|
|
21
|
+
- Interface compatibility between Triton and Rust backends
|
|
22
|
+
- Observable decoding with error detection
|
|
23
|
+
- Batch processing with detailed iteration tracking
|
|
24
|
+
- Matrix format conversion and validation
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
import torch
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from typing import Dict, List, Optional
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import scipy.sparse as sp
|
|
34
|
+
except Exception:
|
|
35
|
+
sp = None
|
|
36
|
+
|
|
37
|
+
from .decoder import RelayBPDecoder as TritonDecoder
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class DecodeResult:
|
|
42
|
+
"""Result container for decoding with detailed statistics.
|
|
43
|
+
|
|
44
|
+
This is intentionally shaped to look like the Rust ``DecodeResult`` /
|
|
45
|
+
``ObservableDecodeResult`` exposed through the relay_bp Python bindings.
|
|
46
|
+
This allows higher-level code to treat Triton
|
|
47
|
+
and Rust results uniformly.
|
|
48
|
+
|
|
49
|
+
Canonical (Rust-compatible) fields:
|
|
50
|
+
decoding: Physical error pattern (Array1<Bit> in Rust)
|
|
51
|
+
success: Whether the decoder successfully satisfied all checks
|
|
52
|
+
iterations: Number of BP iterations performed
|
|
53
|
+
|
|
54
|
+
Additional Triton-only convenience fields:
|
|
55
|
+
converged: Whether the lane converged according to Relay-BP stopping rule
|
|
56
|
+
error_detected: Whether a logical error was detected (from observables mismatch)
|
|
57
|
+
observables: Predicted observable values
|
|
58
|
+
unconverged_indices: Optional batch-level array of indices for unconverged syndromes.
|
|
59
|
+
found_count: Number of valid solutions found for the shot (0..stop_nconv). Set by the
|
|
60
|
+
detailed-batch methods of RelayDecoder.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# Rust-compatible public API
|
|
64
|
+
decoding: np.ndarray
|
|
65
|
+
success: bool
|
|
66
|
+
iterations: int
|
|
67
|
+
|
|
68
|
+
# Quality metric
|
|
69
|
+
decoding_quality: float = float("inf")
|
|
70
|
+
|
|
71
|
+
# Only populated when true errors are available (from_errors path).
|
|
72
|
+
unconverged_no_error: Optional[bool] = None
|
|
73
|
+
unconverged_logical_error: Optional[bool] = None
|
|
74
|
+
better_decoding_quality_error: Optional[bool] = None
|
|
75
|
+
worse_decoding_quality_error: Optional[bool] = None
|
|
76
|
+
|
|
77
|
+
# Triton-specific extras
|
|
78
|
+
converged: bool = False
|
|
79
|
+
error_detected: bool = False
|
|
80
|
+
observables: np.ndarray = None # type: ignore[assignment]
|
|
81
|
+
unconverged_indices: Optional[np.ndarray] = None
|
|
82
|
+
found_count: Optional[int] = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class RelayDecoder:
|
|
87
|
+
"""Adapter for Triton Relay-BP decoder providing relay_bp interface compatibility.
|
|
88
|
+
|
|
89
|
+
This class wraps the Triton GPU implementation to match the interface expected
|
|
90
|
+
by the relay_bp package, enabling seamless integration with existing tools.
|
|
91
|
+
|
|
92
|
+
The adapter handles matrix format conversion, parameter mapping, and provides
|
|
93
|
+
the same interface as the Rust implementation for benchmarking consistency.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
check_matrix,
|
|
99
|
+
*,
|
|
100
|
+
# High-level configuration dictionaries
|
|
101
|
+
msl_config: Optional[dict] = None,
|
|
102
|
+
relay_config: Optional[dict] = None,
|
|
103
|
+
data_type: Optional[str] = None,
|
|
104
|
+
backend_config: Optional[dict] = None,
|
|
105
|
+
**kwargs,
|
|
106
|
+
):
|
|
107
|
+
"""Create a Triton Relay-BP decoder.
|
|
108
|
+
|
|
109
|
+
High-level *config* style:
|
|
110
|
+
- ``msl_config``: Min-sum configuration dict (error_priors, alpha, gamma0, ...)
|
|
111
|
+
- ``relay_config``: Relay-BP configuration dict
|
|
112
|
+
- ``data_type``: decoder numeric type ("float", "float32", "int", ...)
|
|
113
|
+
- ``backend_config``: Triton backend-specific knobs (dtype, perf, device, fast_max_unconverged_pct, ...)
|
|
114
|
+
"""
|
|
115
|
+
if msl_config is None or relay_config is None:
|
|
116
|
+
raise ValueError("msl_config and relay_config must be provided to Triton RelayDecoder")
|
|
117
|
+
|
|
118
|
+
msl_config = dict(msl_config)
|
|
119
|
+
relay_config = dict(relay_config)
|
|
120
|
+
backend_config = dict(backend_config or {})
|
|
121
|
+
|
|
122
|
+
# Min-sum / memory configuration
|
|
123
|
+
error_priors = msl_config.get("error_priors")
|
|
124
|
+
if error_priors is None:
|
|
125
|
+
raise ValueError("msl_config must contain 'error_priors'")
|
|
126
|
+
|
|
127
|
+
alpha = msl_config.get("alpha", None)
|
|
128
|
+
beta = msl_config.get("beta", None)
|
|
129
|
+
alpha_iteration_scaling_factor = msl_config.get("alpha_iteration_scaling_factor", 1.0)
|
|
130
|
+
gamma0 = msl_config.get("gamma0", 0.1)
|
|
131
|
+
|
|
132
|
+
# Relay configuration
|
|
133
|
+
pre_iter = relay_config.get("pre_iter", 80)
|
|
134
|
+
num_sets = relay_config.get("num_sets", 300)
|
|
135
|
+
set_max_iter = relay_config.get("set_max_iter", 60)
|
|
136
|
+
gamma_dist_interval = tuple(relay_config.get("gamma_dist_interval", (-0.24, 0.66)))
|
|
137
|
+
explicit_gammas = relay_config.get("explicit_gammas", None)
|
|
138
|
+
gamma_dist_interval_by_error = relay_config.get("gamma_dist_interval_by_error", None)
|
|
139
|
+
stop_nconv = int(relay_config.get("stop_nconv", 1))
|
|
140
|
+
self.stop_nconv = stop_nconv
|
|
141
|
+
seed = relay_config.get("seed", 0)
|
|
142
|
+
gamma_draw = str(relay_config.get("gamma_draw", "broadcast"))
|
|
143
|
+
# Correction for shots with no valid solution: "first_leg" or "last_leg".
|
|
144
|
+
unconverged_correction = str(relay_config.get("unconverged_correction", "first_leg"))
|
|
145
|
+
|
|
146
|
+
# Backend/Triton-specific configuration
|
|
147
|
+
data_type_cfg = backend_config.get("data_type", data_type)
|
|
148
|
+
# Normalize common aliases
|
|
149
|
+
_dtype_aliases = {
|
|
150
|
+
"float": "float32",
|
|
151
|
+
"half": "float16",
|
|
152
|
+
"double": "float64",
|
|
153
|
+
"fp16": "float16",
|
|
154
|
+
"fp32": "float32",
|
|
155
|
+
"fp64": "float64",
|
|
156
|
+
}
|
|
157
|
+
data_type_cfg = _dtype_aliases.get(data_type_cfg, data_type_cfg)
|
|
158
|
+
if data_type_cfg not in ("float16", "float32", "float64"):
|
|
159
|
+
raise ValueError(f"Unsupported data_type for Triton backend: {data_type_cfg!r}")
|
|
160
|
+
self.data_type = data_type_cfg
|
|
161
|
+
|
|
162
|
+
perf = backend_config.get("perf", "throughput")
|
|
163
|
+
device = backend_config.get("device", (0,))
|
|
164
|
+
# Normalize device specification so that the underlying Triton decoder
|
|
165
|
+
# always sees a type it can handle. Its _parse_device_spec expects a
|
|
166
|
+
# List[int], int, or str. Older call sites (and some defaults here)
|
|
167
|
+
# may use a tuple, so we convert tuples to lists.
|
|
168
|
+
# Also handle edge case where device might be integer 0 (should be [0])
|
|
169
|
+
if isinstance(device, tuple):
|
|
170
|
+
device = list(device)
|
|
171
|
+
elif isinstance(device, int):
|
|
172
|
+
# If device is a single integer, treat it as a single GPU ID [device]
|
|
173
|
+
# (not "auto-detect device GPUs" which would fail for device=0)
|
|
174
|
+
device = [device]
|
|
175
|
+
fast_max_unconverged_pct = backend_config.get("fast_max_unconverged_pct", None)
|
|
176
|
+
bitpack_output = bool(backend_config.get("bitpack_output", False))
|
|
177
|
+
scheduling = backend_config.get("scheduling", "flood-fill")
|
|
178
|
+
layered_k_iters = backend_config.get("layered_k_iters", 0)
|
|
179
|
+
layered_block_policy = backend_config.get("layered_block_policy", "contiguous")
|
|
180
|
+
layered_num_blocks = backend_config.get("layered_num_blocks", None)
|
|
181
|
+
layered_target_edges_per_block = backend_config.get("layered_target_edges_per_block", None)
|
|
182
|
+
layered_block_seed = backend_config.get("layered_block_seed", 42)
|
|
183
|
+
|
|
184
|
+
# Algorithm mode: "relay" (default) or "plain"
|
|
185
|
+
algo = backend_config.get("algo", "relay")
|
|
186
|
+
# Convert input to CSR matrix format (handles various sparse matrix types)
|
|
187
|
+
if sp is not None:
|
|
188
|
+
self.H_csr = sp.csr_matrix(check_matrix)
|
|
189
|
+
else:
|
|
190
|
+
# Fallback for environments without SciPy (not recommended)
|
|
191
|
+
H = np.asarray(check_matrix, dtype=np.uint8)
|
|
192
|
+
from scipy.sparse import csr_matrix as _csr # raises if SciPy missing
|
|
193
|
+
|
|
194
|
+
self.H_csr = _csr(H)
|
|
195
|
+
|
|
196
|
+
self.N = int(self.H_csr.shape[1])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
_plain = num_sets == 0
|
|
200
|
+
_gamma_interval = (0.0, 0.0) if _plain else tuple(gamma_dist_interval)
|
|
201
|
+
|
|
202
|
+
# Initialize underlying Triton decoder with mapped parameters
|
|
203
|
+
execution = backend_config.get("execution", "thread")
|
|
204
|
+
decoder_type = TritonDecoder
|
|
205
|
+
process_options = {}
|
|
206
|
+
if execution == "process":
|
|
207
|
+
from .process import ProcessRelayBPDecoder
|
|
208
|
+
decoder_type = ProcessRelayBPDecoder
|
|
209
|
+
process_options["worker_timeout_s"] = backend_config.get("worker_timeout_s", 300)
|
|
210
|
+
elif execution != "thread":
|
|
211
|
+
raise ValueError("execution must be 'thread' or 'process'")
|
|
212
|
+
self._process_execution = execution == "process"
|
|
213
|
+
self._dec = decoder_type(
|
|
214
|
+
self.H_csr,
|
|
215
|
+
error_priors=error_priors,
|
|
216
|
+
pre_iter=int(pre_iter),
|
|
217
|
+
num_sets=int(num_sets),
|
|
218
|
+
set_max_iter=int(set_max_iter),
|
|
219
|
+
gamma0=float(gamma0),
|
|
220
|
+
gamma_dist_interval=_gamma_interval,
|
|
221
|
+
stop_nconv=int(stop_nconv),
|
|
222
|
+
normalized_min_sum_alpha=alpha,
|
|
223
|
+
offset_min_sum_beta=beta,
|
|
224
|
+
data_type=self.data_type,
|
|
225
|
+
device=device,
|
|
226
|
+
seed=int(seed) if seed is not None else 0,
|
|
227
|
+
gamma_draw=gamma_draw,
|
|
228
|
+
bitpack_output=bitpack_output,
|
|
229
|
+
algo=algo,
|
|
230
|
+
perf=perf,
|
|
231
|
+
alpha_iteration_scaling_factor=float(alpha_iteration_scaling_factor),
|
|
232
|
+
explicit_gammas=explicit_gammas,
|
|
233
|
+
gamma_dist_interval_by_error=gamma_dist_interval_by_error,
|
|
234
|
+
fast_max_unconverged_pct=fast_max_unconverged_pct,
|
|
235
|
+
scheduling=scheduling,
|
|
236
|
+
layered_k_iters=layered_k_iters,
|
|
237
|
+
layered_block_policy=layered_block_policy,
|
|
238
|
+
layered_num_blocks=layered_num_blocks,
|
|
239
|
+
layered_target_edges_per_block=layered_target_edges_per_block,
|
|
240
|
+
layered_block_seed=layered_block_seed,
|
|
241
|
+
unconverged_correction=unconverged_correction,
|
|
242
|
+
**process_options,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def close(self):
|
|
246
|
+
"""Release persistent workers when using process execution."""
|
|
247
|
+
if self._process_execution:
|
|
248
|
+
self._dec.close()
|
|
249
|
+
|
|
250
|
+
def __enter__(self):
|
|
251
|
+
return self
|
|
252
|
+
|
|
253
|
+
def __exit__(self, *exc):
|
|
254
|
+
self.close()
|
|
255
|
+
|
|
256
|
+
def _decode_inner(self, S_torch):
|
|
257
|
+
"""Single seam: dispatch to the Triton decoder.
|
|
258
|
+
|
|
259
|
+
Returns a result dict with the keys consumed by the rest of the
|
|
260
|
+
adapter (errors, iterations, found_count, valid_mask,
|
|
261
|
+
unconverged_indices).
|
|
262
|
+
"""
|
|
263
|
+
return self._dec.decode(S_torch)
|
|
264
|
+
|
|
265
|
+
def log_prior_ratios(self) -> np.ndarray:
|
|
266
|
+
"""Return the decoder's log-prior-ratios: ln((1-p)/p).
|
|
267
|
+
|
|
268
|
+
These match the Rust ``Decoder::log_prior_ratios()`` semantics and
|
|
269
|
+
are used for quality metric computation. Non-finite values
|
|
270
|
+
are already zeroed during decoder initialisation.
|
|
271
|
+
"""
|
|
272
|
+
return self._dec.wj.cpu().numpy().astype(np.float64)
|
|
273
|
+
|
|
274
|
+
def override_llr_columns(self, cols, value: float):
|
|
275
|
+
"""Override prior LLR on given columns (see TritonDecoder.override_llr_columns).
|
|
276
|
+
|
|
277
|
+
Returns a restore-callable, so a caller can patch per-column
|
|
278
|
+
priors for a round and then revert them.
|
|
279
|
+
"""
|
|
280
|
+
return self._dec.override_llr_columns(cols, value)
|
|
281
|
+
|
|
282
|
+
def decode(self, syndrome: np.ndarray | torch.Tensor):
|
|
283
|
+
"""Decode syndrome using Triton GPU implementation.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
syndrome: Syndrome vector as numpy array or torch tensor
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Dict containing decoding results with relay_bp interface compatibility.
|
|
290
|
+
If fast_max_unconverged_pct is set in decoder, also includes "unconverged_indices" key.
|
|
291
|
+
"""
|
|
292
|
+
# Triton decoder expects batched input; wrap single syndrome as batch of size 1.
|
|
293
|
+
# Keep on CPU. decode() handles GPU placement.
|
|
294
|
+
if isinstance(syndrome, np.ndarray):
|
|
295
|
+
s = torch.from_numpy(syndrome.astype(np.uint8, copy=False)).view(1, -1)
|
|
296
|
+
else:
|
|
297
|
+
s = syndrome.to(dtype=torch.uint8).view(1, -1)
|
|
298
|
+
|
|
299
|
+
out = self._dec.decode(s)
|
|
300
|
+
bits = out["errors"][0].to(torch.int8)
|
|
301
|
+
# Use found_count > 0 for converged (at least one valid solution found).
|
|
302
|
+
converged = bool(out["found_count"][0].item() > 0)
|
|
303
|
+
iters = int(out.get("iterations", torch.tensor([self._dec.pre_iter]))[0].item())
|
|
304
|
+
|
|
305
|
+
result = {
|
|
306
|
+
"bits": bits,
|
|
307
|
+
"posterior_llr": None,
|
|
308
|
+
"converged": converged,
|
|
309
|
+
"num_ensembles": 0,
|
|
310
|
+
"iterations": iters,
|
|
311
|
+
"selected_idx": 0,
|
|
312
|
+
}
|
|
313
|
+
# Include unconverged_indices if present (when fast_max_unconverged_pct is set)
|
|
314
|
+
if "unconverged_indices" in out:
|
|
315
|
+
result["unconverged_indices"] = out["unconverged_indices"].cpu().numpy()
|
|
316
|
+
return result
|
|
317
|
+
|
|
318
|
+
def decode_detailed(self, detectors: np.ndarray | torch.Tensor):
|
|
319
|
+
"""Decode detectors with detailed results (matches Rust interface).
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
detectors: Detector vector as numpy array or torch tensor
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
DecodeResult object with detailed decoding information
|
|
326
|
+
"""
|
|
327
|
+
result = self.decode(detectors)
|
|
328
|
+
bits = result["bits"].cpu().numpy()
|
|
329
|
+
converged = bool(result["converged"])
|
|
330
|
+
iters = int(result["iterations"])
|
|
331
|
+
|
|
332
|
+
# For single-shot decode we expose "decoding" as the physical error pattern and
|
|
333
|
+
# "success" as convergence. This matches the Rust DecodeResult fields consumed
|
|
334
|
+
# by BaseRelayDecoder._produce_output.
|
|
335
|
+
return DecodeResult(
|
|
336
|
+
decoding=bits,
|
|
337
|
+
success=converged,
|
|
338
|
+
iterations=iters,
|
|
339
|
+
converged=converged,
|
|
340
|
+
error_detected=not converged,
|
|
341
|
+
observables=bits,
|
|
342
|
+
unconverged_indices=result.get("unconverged_indices"),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
def decode_batch(self, detectors: np.ndarray | torch.Tensor, return_iterations=False):
|
|
346
|
+
"""Decode batch of detectors (matches Rust interface).
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
detectors: Batch of detector vectors as numpy array or torch tensor
|
|
350
|
+
return_iterations: when True, also return the per-shot BP iteration
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Batch of decoded error vectors as numpy array.
|
|
354
|
+
If fast_max_unconverged_pct is set in decoder, returns tuple (errors, unconverged_indices).
|
|
355
|
+
If return_iterations is True, the per-shot iteration array is appended.
|
|
356
|
+
"""
|
|
357
|
+
# Keep on CPU. decode() handles GPU placement.
|
|
358
|
+
if isinstance(detectors, np.ndarray):
|
|
359
|
+
detectors = torch.from_numpy(detectors.astype(np.uint8, copy=False))
|
|
360
|
+
else:
|
|
361
|
+
detectors = detectors.to(dtype=torch.uint8)
|
|
362
|
+
|
|
363
|
+
out = self._dec.decode(detectors)
|
|
364
|
+
errors = out["errors"].to(torch.int8).cpu().numpy()
|
|
365
|
+
result = [errors]
|
|
366
|
+
if self._dec.fast_max_unconverged_pct is not None and "unconverged_indices" in out:
|
|
367
|
+
result.append(out["unconverged_indices"].cpu().numpy())
|
|
368
|
+
if return_iterations:
|
|
369
|
+
iters_t = out.get("iterations")
|
|
370
|
+
if iters_t is None:
|
|
371
|
+
iters = np.full(errors.shape[0], int(self._dec.pre_iter), dtype=np.int32)
|
|
372
|
+
else:
|
|
373
|
+
iters = iters_t.to(torch.int32).cpu().numpy()
|
|
374
|
+
result.append(iters)
|
|
375
|
+
return result[0] if len(result) == 1 else tuple(result)
|
|
376
|
+
|
|
377
|
+
def decode_batch_with_prior_tensors(
|
|
378
|
+
self, detectors: torch.Tensor, prior_llr: torch.Tensor, *, return_iterations=False
|
|
379
|
+
):
|
|
380
|
+
"""Decode a batch with a per-shot [B, V] prior LLR, returning also the
|
|
381
|
+
posterior. GPU-resident.
|
|
382
|
+
"""
|
|
383
|
+
out = self._dec.decode(
|
|
384
|
+
detectors, return_last_posterior=True, prior_llr=prior_llr
|
|
385
|
+
)
|
|
386
|
+
errors = out["errors"]
|
|
387
|
+
post = out["last_posterior"]
|
|
388
|
+
if return_iterations:
|
|
389
|
+
iters_t = out.get("iterations")
|
|
390
|
+
if iters_t is None:
|
|
391
|
+
iters_t = torch.full(
|
|
392
|
+
(detectors.shape[0],),
|
|
393
|
+
int(self._dec.pre_iter),
|
|
394
|
+
dtype=torch.int32,
|
|
395
|
+
device=errors.device,
|
|
396
|
+
)
|
|
397
|
+
return errors, post, iters_t
|
|
398
|
+
return errors, post
|
|
399
|
+
|
|
400
|
+
def decode_detailed_batch(self, detectors: np.ndarray | torch.Tensor):
|
|
401
|
+
"""Decode batch of detectors with detailed results (matches Rust interface).
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
detectors: Batch of detector vectors as numpy array or torch tensor
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
List of DecodeResult objects with detailed decoding information.
|
|
408
|
+
If fast_max_unconverged_pct is set in decoder, returns tuple (results, unconverged_indices).
|
|
409
|
+
"""
|
|
410
|
+
# Keep on CPU. decode() handles GPU placement.
|
|
411
|
+
if isinstance(detectors, np.ndarray):
|
|
412
|
+
detectors = torch.from_numpy(detectors.astype(np.uint8, copy=False))
|
|
413
|
+
else:
|
|
414
|
+
detectors = detectors.to(dtype=torch.uint8)
|
|
415
|
+
|
|
416
|
+
out = self._dec.decode(detectors)
|
|
417
|
+
|
|
418
|
+
# Extract unconverged_indices if available
|
|
419
|
+
unconverged_indices = None
|
|
420
|
+
if self._dec.fast_max_unconverged_pct is not None and "unconverged_indices" in out:
|
|
421
|
+
unconverged_indices = out["unconverged_indices"].cpu().numpy()
|
|
422
|
+
|
|
423
|
+
# Use found_count > 0 for success (at least one valid solution found).
|
|
424
|
+
B = detectors.shape[0]
|
|
425
|
+
found_count = out["found_count"].cpu().numpy()
|
|
426
|
+
success_mask = found_count > 0
|
|
427
|
+
|
|
428
|
+
results: List[DecodeResult] = []
|
|
429
|
+
for i in range(B):
|
|
430
|
+
bits = out["errors"][i].to(torch.int8)
|
|
431
|
+
success = bool(success_mask[i])
|
|
432
|
+
iters = int(
|
|
433
|
+
out.get(
|
|
434
|
+
"iterations",
|
|
435
|
+
torch.tensor([self._dec.pre_iter]),
|
|
436
|
+
)[i].item()
|
|
437
|
+
)
|
|
438
|
+
bits_np = bits.cpu().numpy()
|
|
439
|
+
results.append(
|
|
440
|
+
DecodeResult(
|
|
441
|
+
decoding=bits_np,
|
|
442
|
+
success=success,
|
|
443
|
+
iterations=iters,
|
|
444
|
+
converged=success,
|
|
445
|
+
error_detected=not success,
|
|
446
|
+
observables=bits_np,
|
|
447
|
+
unconverged_indices=unconverged_indices,
|
|
448
|
+
found_count=int(found_count[i]),
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
return results
|
|
452
|
+
|
|
453
|
+
def decode_detailed_batch_from_syndromes(
|
|
454
|
+
self, detectors: np.ndarray | torch.Tensor
|
|
455
|
+
) -> List[DecodeResult]:
|
|
456
|
+
"""Decode a batch of detector syndromes and return Rust-compatible results.
|
|
457
|
+
|
|
458
|
+
This is the primary entry point for higher-level decoders
|
|
459
|
+
that expect DecodeResult-like objects given detector error model syndromes.
|
|
460
|
+
|
|
461
|
+
Success/converged is determined by found_count > 0 (at least one valid solution found)
|
|
462
|
+
"""
|
|
463
|
+
# Keep on CPU. decode() handles GPU placement.
|
|
464
|
+
if isinstance(detectors, np.ndarray):
|
|
465
|
+
S_torch = torch.from_numpy(detectors.astype(np.uint8, copy=False))
|
|
466
|
+
else:
|
|
467
|
+
S_torch = detectors.to(dtype=torch.uint8)
|
|
468
|
+
|
|
469
|
+
out = self._decode_inner(S_torch)
|
|
470
|
+
B = S_torch.shape[0]
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
errors = out["errors"].to(torch.int8)
|
|
474
|
+
iters = out.get(
|
|
475
|
+
"iterations",
|
|
476
|
+
torch.full((B,), int(self._dec.pre_iter), dtype=torch.int32),
|
|
477
|
+
)
|
|
478
|
+
found_count = out["found_count"].cpu().numpy()
|
|
479
|
+
success_mask = found_count > 0
|
|
480
|
+
|
|
481
|
+
# Also get unconverged_indices for compatibility (used by fast_max_unconverged_pct feature)
|
|
482
|
+
unconverged_indices_gpu = out.get("unconverged_indices", None)
|
|
483
|
+
unconverged_indices_np = None
|
|
484
|
+
if unconverged_indices_gpu is not None and unconverged_indices_gpu.numel() > 0:
|
|
485
|
+
unconverged_indices_np = unconverged_indices_gpu.cpu().numpy()
|
|
486
|
+
|
|
487
|
+
# Transfer decoded errors to CPU and recompute quality
|
|
488
|
+
all_bits = errors.cpu().numpy() # (B, V) int8
|
|
489
|
+
all_iters = iters.cpu().numpy()
|
|
490
|
+
wj_np = self.log_prior_ratios() # (N,) float64
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
results: List[DecodeResult] = []
|
|
494
|
+
for i in range(B):
|
|
495
|
+
bits_np = all_bits[i]
|
|
496
|
+
success = bool(success_mask[i])
|
|
497
|
+
iterations = int(all_iters[i])
|
|
498
|
+
# Recompute quality
|
|
499
|
+
dq = float(np.dot(bits_np.astype(np.float64), wj_np)) if success else float("inf")
|
|
500
|
+
result = DecodeResult(
|
|
501
|
+
decoding=bits_np,
|
|
502
|
+
success=success,
|
|
503
|
+
iterations=iterations,
|
|
504
|
+
decoding_quality=dq,
|
|
505
|
+
converged=success,
|
|
506
|
+
error_detected=not success,
|
|
507
|
+
observables=bits_np,
|
|
508
|
+
unconverged_indices=unconverged_indices_np,
|
|
509
|
+
found_count=int(found_count[i]),
|
|
510
|
+
)
|
|
511
|
+
results.append(result)
|
|
512
|
+
return results
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
class ObservableDecoderRunner:
|
|
516
|
+
"""Observable decoder runner providing batch processing with error detection.
|
|
517
|
+
|
|
518
|
+
This class handles batch decoding of error patterns and computes observable
|
|
519
|
+
predictions with logical error detection, matching the relay_bp interface.
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
def __init__(
|
|
523
|
+
self,
|
|
524
|
+
decoder: RelayDecoder,
|
|
525
|
+
observables_matrix,
|
|
526
|
+
include_decode_result: bool = True,
|
|
527
|
+
**kwargs,
|
|
528
|
+
):
|
|
529
|
+
self.decoder = decoder
|
|
530
|
+
if sp is not None and sp.issparse(observables_matrix):
|
|
531
|
+
self._obs = observables_matrix.tocsr()
|
|
532
|
+
else:
|
|
533
|
+
self._obs = (
|
|
534
|
+
sp.csr_matrix(observables_matrix)
|
|
535
|
+
if sp is not None
|
|
536
|
+
else np.asarray(observables_matrix, dtype=np.uint8)
|
|
537
|
+
)
|
|
538
|
+
self.O, self.N = self._obs.shape
|
|
539
|
+
self.M = int(decoder.H_csr.shape[0])
|
|
540
|
+
# Cache for batched concatenation to avoid allocation overhead
|
|
541
|
+
self._cached_concat_errors = None
|
|
542
|
+
self._cached_concat_iterations = None
|
|
543
|
+
self._cached_concat_valid = None
|
|
544
|
+
|
|
545
|
+
def _mul_mod2(self, A_csr, x: np.ndarray | torch.Tensor) -> np.ndarray:
|
|
546
|
+
"""Compute matrix-vector multiplication modulo 2 for observable computation.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
A_csr: Sparse matrix (observables or check matrix)
|
|
550
|
+
x: Vector (error pattern or decoded bits)
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Result of A @ x mod 2 as uint8 array
|
|
554
|
+
"""
|
|
555
|
+
if sp is not None and sp.issparse(A_csr):
|
|
556
|
+
y = A_csr @ (np.asarray(x, dtype=np.int8))
|
|
557
|
+
return (np.asarray(y) % 2).astype(np.uint8)
|
|
558
|
+
A = np.asarray(A_csr, dtype=np.uint8)
|
|
559
|
+
y = (A @ np.asarray(x, dtype=np.int8)) % 2
|
|
560
|
+
return y.astype(np.uint8)
|
|
561
|
+
|
|
562
|
+
def from_errors_decode_observables_detailed_batch(
|
|
563
|
+
self, errors: np.ndarray, parallel: bool = False
|
|
564
|
+
) -> List[DecodeResult]:
|
|
565
|
+
"""Decode batch of error patterns and compute observable predictions with error detection.
|
|
566
|
+
|
|
567
|
+
This method implements the core benchmarking functionality, processing a batch
|
|
568
|
+
of error patterns and returning detailed statistics for each pattern including
|
|
569
|
+
iteration counts, convergence status, and logical error detection.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
errors: [B,N] array of error patterns to decode
|
|
573
|
+
parallel: Whether to use parallel processing (unused in Triton implementation)
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
List of DecodeResult objects containing detailed decoding statistics.
|
|
577
|
+
If fast_max_unconverged_pct is set and unconverged cases exist, the list will have an
|
|
578
|
+
'unconverged_indices' attribute containing the indices of unconverged syndromes.
|
|
579
|
+
"""
|
|
580
|
+
# Use new methods to avoid code duplication
|
|
581
|
+
gpu_result = self._decode_gpu_only(errors)
|
|
582
|
+
return self._process_gpu_results(gpu_result)
|
|
583
|
+
|
|
584
|
+
def _decode_gpu_only(self, errors: np.ndarray) -> Dict[str, torch.Tensor]:
|
|
585
|
+
"""Decode batch on GPU and return GPU tensors (no CPU transfer).
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
errors: [B,N] array of error patterns to decode
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Dictionary with GPU tensors: errors, true_obs, and decoder output dict
|
|
592
|
+
"""
|
|
593
|
+
B, N = errors.shape
|
|
594
|
+
assert N == self.decoder.N, f"N mismatch: errors has {N}, decoder has {self.decoder.N}"
|
|
595
|
+
|
|
596
|
+
if self.decoder._process_execution:
|
|
597
|
+
from .process import prepare_batch
|
|
598
|
+
true_obs, syndromes = prepare_batch(
|
|
599
|
+
self.decoder.H_csr, self._obs, errors, self.decoder._dec.num_gpus,
|
|
600
|
+
self._mul_mod2,
|
|
601
|
+
)
|
|
602
|
+
return {"out": self.decoder._decode_inner(torch.from_numpy(syndromes)),
|
|
603
|
+
"true_obs": true_obs, "errors": errors}
|
|
604
|
+
|
|
605
|
+
# Compute true observables from error patterns
|
|
606
|
+
true_obs = self._mul_mod2(self._obs, errors.T).T # (B,O)
|
|
607
|
+
|
|
608
|
+
# Build syndromes from errors via H @ e mod 2
|
|
609
|
+
H = self.decoder.H_csr
|
|
610
|
+
if sp is not None and sp.issparse(H):
|
|
611
|
+
S = (H @ errors.T) % 2
|
|
612
|
+
S = np.asarray(S, dtype=np.uint8).T
|
|
613
|
+
else:
|
|
614
|
+
H_np = np.asarray(H, dtype=np.uint8)
|
|
615
|
+
S = ((H_np @ errors.T) % 2).astype(np.uint8).T
|
|
616
|
+
|
|
617
|
+
# If decoder uses check permutation, apply it to syndromes to match permuted graph
|
|
618
|
+
# The graph was built with H[check_permutation, :], so syndromes need to be permuted too
|
|
619
|
+
if self.decoder._dec.check_permutation is not None:
|
|
620
|
+
perm = self.decoder._dec.check_permutation
|
|
621
|
+
S = S[:, perm]
|
|
622
|
+
|
|
623
|
+
# Decode on GPU (multi-GPU handled transparently by decode()).
|
|
624
|
+
# Keep tensor on CPU here. decode() places each GPU's slice directly,
|
|
625
|
+
# avoiding a redundant CPU -> GPU 0 -> GPU N round-trip.
|
|
626
|
+
S_torch = torch.from_numpy(S)
|
|
627
|
+
out = self.decoder._decode_inner(S_torch)
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
"out": out,
|
|
631
|
+
"true_obs": true_obs,
|
|
632
|
+
"errors": errors,
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _process_gpu_results(self, gpu_result: Dict[str, torch.Tensor]) -> List[DecodeResult]:
|
|
637
|
+
"""Transfer GPU results to CPU and process into DecodeResult list.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
gpu_result: Dictionary from _decode_gpu_only() with out, true_obs, errors
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
List of DecodeResult objects (same as from_errors_decode_observables_detailed_batch)
|
|
644
|
+
"""
|
|
645
|
+
out = gpu_result["out"]
|
|
646
|
+
true_obs = gpu_result["true_obs"]
|
|
647
|
+
errors = gpu_result["errors"]
|
|
648
|
+
B = errors.shape[0]
|
|
649
|
+
|
|
650
|
+
# Batch CPU transfers (single sync point)
|
|
651
|
+
errors_gpu = out["errors"].to(torch.uint8)
|
|
652
|
+
iterations_gpu = out.get(
|
|
653
|
+
"iterations",
|
|
654
|
+
torch.full(
|
|
655
|
+
(B,),
|
|
656
|
+
int(self.decoder._dec.pre_iter),
|
|
657
|
+
device=out["errors"].device,
|
|
658
|
+
dtype=torch.int32,
|
|
659
|
+
),
|
|
660
|
+
).to(torch.int32)
|
|
661
|
+
found_count_gpu = out["found_count"].to(torch.int32)
|
|
662
|
+
unconverged_indices_gpu = out.get("unconverged_indices", None)
|
|
663
|
+
|
|
664
|
+
# Single batched transfer (all tensors transferred together)
|
|
665
|
+
bits = errors_gpu.cpu().numpy()
|
|
666
|
+
iters = iterations_gpu.cpu().numpy()
|
|
667
|
+
found_count = found_count_gpu.cpu().numpy()
|
|
668
|
+
|
|
669
|
+
# Use found_count > 0 for success (at least one valid solution found).
|
|
670
|
+
success_mask = found_count > 0
|
|
671
|
+
|
|
672
|
+
unconverged_indices = None
|
|
673
|
+
if (
|
|
674
|
+
self.decoder._dec.fast_max_unconverged_pct is not None
|
|
675
|
+
and unconverged_indices_gpu is not None
|
|
676
|
+
):
|
|
677
|
+
unconverged_indices = unconverged_indices_gpu.cpu().numpy()
|
|
678
|
+
|
|
679
|
+
pred_obs = self._mul_mod2(self._obs, bits.T).T
|
|
680
|
+
|
|
681
|
+
# Compute quality metrics
|
|
682
|
+
# Rust: get_decoding_quality(errors) = sum(log_prior_ratios[i] for i where errors[i]==1 and is_finite)
|
|
683
|
+
# Our log_prior_ratios() already zeros non-finite entries, so the dot product is equivalent.
|
|
684
|
+
wj_np = self.decoder.log_prior_ratios() # (N,) float64
|
|
685
|
+
decoded_quality = (bits.astype(np.float64) * wj_np).sum(axis=1) # (B,) float64
|
|
686
|
+
true_quality = (errors.astype(np.float64) * wj_np).sum(axis=1) # (B,) float64
|
|
687
|
+
|
|
688
|
+
res: List[DecodeResult] = []
|
|
689
|
+
for i in range(B):
|
|
690
|
+
err_det = bool((true_obs[i] != pred_obs[i]).any())
|
|
691
|
+
success = bool(success_mask[i])
|
|
692
|
+
it = int(iters[i])
|
|
693
|
+
obs = pred_obs[i].astype(np.uint8)
|
|
694
|
+
dq = float(decoded_quality[i]) if success else float("inf")
|
|
695
|
+
better_quality = dq < true_quality[i]
|
|
696
|
+
res.append(
|
|
697
|
+
DecodeResult(
|
|
698
|
+
decoding=bits[i],
|
|
699
|
+
success=success,
|
|
700
|
+
iterations=it,
|
|
701
|
+
decoding_quality=dq,
|
|
702
|
+
unconverged_no_error=bool(not err_det and not success),
|
|
703
|
+
unconverged_logical_error=bool(err_det and not success),
|
|
704
|
+
better_decoding_quality_error=bool(success and err_det and better_quality),
|
|
705
|
+
worse_decoding_quality_error=bool(success and err_det and not better_quality),
|
|
706
|
+
converged=success,
|
|
707
|
+
error_detected=err_det,
|
|
708
|
+
observables=obs,
|
|
709
|
+
unconverged_indices=unconverged_indices,
|
|
710
|
+
)
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
return res
|
|
714
|
+
|
|
715
|
+
def _process_gpu_results_batched(
|
|
716
|
+
self,
|
|
717
|
+
chunk_gpu_results: List[Dict[str, torch.Tensor]],
|
|
718
|
+
chunk_errors_list: List[np.ndarray],
|
|
719
|
+
chunk_true_obs_list: List[np.ndarray],
|
|
720
|
+
) -> List[List[DecodeResult]]:
|
|
721
|
+
"""Process multiple GPU results with a single batched transfer.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
chunk_gpu_results: List of GPU result dictionaries from _decode_gpu_only()
|
|
725
|
+
chunk_errors_list: List of error arrays (CPU, already available)
|
|
726
|
+
chunk_true_obs_list: List of true observable arrays (CPU, already available)
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
List of DecodeResult lists (one per batch)
|
|
730
|
+
"""
|
|
731
|
+
if not chunk_gpu_results:
|
|
732
|
+
return []
|
|
733
|
+
|
|
734
|
+
# Concatenate all batches for single transfer
|
|
735
|
+
# Ensure all tensors are on the same device before concatenation
|
|
736
|
+
errors_list = []
|
|
737
|
+
iterations_list = []
|
|
738
|
+
found_count_list = []
|
|
739
|
+
batch_sizes = []
|
|
740
|
+
|
|
741
|
+
for r in chunk_gpu_results:
|
|
742
|
+
B = r["out"]["errors"].shape[0]
|
|
743
|
+
batch_sizes.append(B)
|
|
744
|
+
errors_list.append(r["out"]["errors"].to(torch.uint8))
|
|
745
|
+
iterations_list.append(
|
|
746
|
+
r["out"]
|
|
747
|
+
.get(
|
|
748
|
+
"iterations",
|
|
749
|
+
torch.full(
|
|
750
|
+
(B,),
|
|
751
|
+
int(self.decoder._dec.pre_iter),
|
|
752
|
+
device=r["out"]["errors"].device,
|
|
753
|
+
dtype=torch.int32,
|
|
754
|
+
),
|
|
755
|
+
)
|
|
756
|
+
.to(torch.int32)
|
|
757
|
+
)
|
|
758
|
+
found_count_list.append(r["out"]["found_count"].to(torch.int32))
|
|
759
|
+
|
|
760
|
+
# Concatenate using pre-allocated buffers to avoid allocation overhead
|
|
761
|
+
total_B = sum(batch_sizes)
|
|
762
|
+
V = errors_list[0].shape[1]
|
|
763
|
+
device_str = str(errors_list[0].device)
|
|
764
|
+
|
|
765
|
+
# Pre-allocate buffers if needed
|
|
766
|
+
if self._cached_concat_errors is None or self._cached_concat_errors.shape[0] < total_B:
|
|
767
|
+
self._cached_concat_errors = torch.empty(
|
|
768
|
+
(total_B, V), dtype=torch.uint8, device=device_str
|
|
769
|
+
)
|
|
770
|
+
self._cached_concat_iterations = torch.empty(
|
|
771
|
+
(total_B,), dtype=torch.int32, device=device_str
|
|
772
|
+
)
|
|
773
|
+
self._cached_concat_valid = torch.empty(
|
|
774
|
+
(total_B,), dtype=torch.int32, device=device_str
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
# Copy into pre-allocated buffers
|
|
778
|
+
offset = 0
|
|
779
|
+
for err, it, fc in zip(errors_list, iterations_list, found_count_list):
|
|
780
|
+
B = err.shape[0]
|
|
781
|
+
self._cached_concat_errors[offset : offset + B] = err
|
|
782
|
+
self._cached_concat_iterations[offset : offset + B] = it
|
|
783
|
+
self._cached_concat_valid[offset : offset + B] = fc
|
|
784
|
+
offset += B
|
|
785
|
+
|
|
786
|
+
all_errors_gpu = self._cached_concat_errors[:total_B]
|
|
787
|
+
all_iterations_gpu = self._cached_concat_iterations[:total_B]
|
|
788
|
+
all_found_count_gpu = self._cached_concat_valid[:total_B]
|
|
789
|
+
|
|
790
|
+
# single batched transfer (one sync point for all batches)
|
|
791
|
+
all_bits = all_errors_gpu.cpu().numpy()
|
|
792
|
+
all_iters = all_iterations_gpu.cpu().numpy()
|
|
793
|
+
all_found_count = all_found_count_gpu.cpu().numpy()
|
|
794
|
+
|
|
795
|
+
# Gamma-scaled LLRs for quality computation
|
|
796
|
+
wj_np = self.decoder.log_prior_ratios() # (N,) float64
|
|
797
|
+
|
|
798
|
+
# Process each batch (unconverged_indices handled per batch since they're batch-local)
|
|
799
|
+
results = []
|
|
800
|
+
batch_start = 0
|
|
801
|
+
for batch_idx, (gpu_result, errors, true_obs) in enumerate(
|
|
802
|
+
zip(chunk_gpu_results, chunk_errors_list, chunk_true_obs_list)
|
|
803
|
+
):
|
|
804
|
+
B = errors.shape[0]
|
|
805
|
+
batch_end = batch_start + B
|
|
806
|
+
|
|
807
|
+
# Extract batch slice from batched transfer
|
|
808
|
+
bits = all_bits[batch_start:batch_end]
|
|
809
|
+
iters = all_iters[batch_start:batch_end]
|
|
810
|
+
found_count = all_found_count[batch_start:batch_end]
|
|
811
|
+
|
|
812
|
+
# Use found_count > 0 for success (at least one valid solution found).
|
|
813
|
+
success_mask = found_count > 0
|
|
814
|
+
|
|
815
|
+
# Handle unconverged_indices for this batch (small, transfer separately)
|
|
816
|
+
unconverged_indices = None
|
|
817
|
+
if self.decoder._dec.fast_max_unconverged_pct is not None:
|
|
818
|
+
uidx_gpu = gpu_result["out"].get("unconverged_indices", None)
|
|
819
|
+
if uidx_gpu is not None and len(uidx_gpu) > 0:
|
|
820
|
+
unconverged_indices = uidx_gpu.cpu().numpy()
|
|
821
|
+
|
|
822
|
+
pred_obs = self._mul_mod2(self._obs, bits.T).T
|
|
823
|
+
|
|
824
|
+
# Compute quality
|
|
825
|
+
decoded_quality = (bits.astype(np.float64) * wj_np).sum(axis=1)
|
|
826
|
+
true_quality = (errors.astype(np.float64) * wj_np).sum(axis=1)
|
|
827
|
+
|
|
828
|
+
res: List[DecodeResult] = []
|
|
829
|
+
for i in range(B):
|
|
830
|
+
err_det = bool((true_obs[i] != pred_obs[i]).any())
|
|
831
|
+
success = bool(success_mask[i])
|
|
832
|
+
it = int(iters[i])
|
|
833
|
+
obs = pred_obs[i].astype(np.uint8)
|
|
834
|
+
dq = float(decoded_quality[i]) if success else float("inf")
|
|
835
|
+
better_quality = dq < true_quality[i]
|
|
836
|
+
res.append(
|
|
837
|
+
DecodeResult(
|
|
838
|
+
decoding=bits[i],
|
|
839
|
+
success=success,
|
|
840
|
+
iterations=it,
|
|
841
|
+
decoding_quality=dq,
|
|
842
|
+
unconverged_no_error=bool(not err_det and not success),
|
|
843
|
+
unconverged_logical_error=bool(err_det and not success),
|
|
844
|
+
better_decoding_quality_error=bool(success and err_det and better_quality),
|
|
845
|
+
worse_decoding_quality_error=bool(
|
|
846
|
+
success and err_det and not better_quality
|
|
847
|
+
),
|
|
848
|
+
converged=success,
|
|
849
|
+
error_detected=err_det,
|
|
850
|
+
observables=obs,
|
|
851
|
+
unconverged_indices=unconverged_indices,
|
|
852
|
+
)
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
results.append(res)
|
|
856
|
+
batch_start = batch_end
|
|
857
|
+
|
|
858
|
+
return results
|