Dist m4ri 0.16.1
Computing distance of a classical or quantum CSS code
Loading...
Searching...
No Matches
dist_m4ri.py
Go to the documentation of this file.
1"""
2dist_m4ri.py: Python wrapper for the multithreaded dist_m4ri distance calculator.
3
4Provides high-level APIs for computing:
5- Classical code distance: compute_classical_distance(...)
6- Single-sided quantum code distance: compute_quantum_distance(...)
7- CSS quantum code distance: compute_css_distance(...)
8- Detector Error Model (DEM) distance: compute_dem_distance(...)
9- Certified lower bound on the distance of a Stim memory circuit from its code-sized model:
10 compute_model_lower_bound(...)
11
12Supports distance caching, codeword export, and fallback/option for the codedistance library.
13Since dist_m4ri natively handles multithreading (via POSIX threads and dynamic bracketing),
14no Python-level threading or subprocess-per-core logic is necessary.
15"""
16
17import os
18import sys
19import json
20import time
21import random
22import shutil
23import hashlib
24import numbers
25import tempfile
26import itertools
27import threading
28import subprocess
29from pathlib import Path
30from typing import List, Tuple, Union, Optional, Dict, Any, Set, Sequence, Callable
31
32_codedistance_mod = None
33_stim_mod = None
34_qec_cert_mod = None
35
36__version__ = "0.16.1"
37
38# Debug bitmap (debug=N): bits 1..32768 are passed to the dist_m4ri binary (1: summary, 2: progress, 4: periodic
39# status, 8: thread allocation and timing, 16: code parameters, 32: codeword supports, 64: arguments, 128: matrices,
40# 256: codeword dump, 2048: no size cutoff; see its --morehelp), the higher bits are used by the Python wrapper only.
41PY_DBG_COMMANDS = 1 << 16 # echo the dist_m4ri command lines and their run times
42PY_DBG_CACHE = 1 << 17 # cache messages
43PY_DBG_CIRCUITS = 1 << 18 # Stim circuit to DEM conversion details
44PY_DBG_KEEP_FILES = 1 << 19 # keep (and list) the temporary matrix, codeword, and DEM files
45BIN_DBG_MASK = 0xFFFF # the debug bits passed to the dist_m4ri binary
46VERBOSE_DEBUG = 3 | PY_DBG_COMMANDS | PY_DBG_CACHE # debug bits implied by verbose=True (--verbose)
47
48
49def parse_debug_value(val: Union[str, int]) -> int:
50 """
51 Parses a debug bitmap value: a non-negative decimal or hexadecimal (0x...) integer, as in the dist_m4ri binary
52 (no octal: leading zeros are ignored).
53
54 Args:
55 val: The value, e.g., "3" or "0x10004", or an int.
56
57 Returns:
58 The debug bitmap.
59
60 Raises:
61 ValueError: If the value is not a non-negative integer.
62 """
63 msg = f"invalid debug={val!r}: expected a non-negative decimal or hexadecimal (0x...) integer"
64 if isinstance(val, bool):
65 raise ValueError(msg)
66 if isinstance(val, int):
67 value = val
68 else:
69 s = str(val).strip()
70 try:
71 value = int(s[2:], 16) if s.lower().startswith("0x") else int(s, 10)
72 except ValueError:
73 raise ValueError(msg) from None
74 if value < 0:
75 raise ValueError(msg)
76 return value
77
78
80 """Lazily imports the codedistance library only when requested."""
81 global _codedistance_mod
82 if _codedistance_mod is None:
83 try:
84 import codedistance
85 _codedistance_mod = codedistance
86 except ImportError:
87 raise ImportError("codedistance library is requested but not installed.")
88 return _codedistance_mod
89
90
92 """Lazily imports stim only when requested."""
93 global _stim_mod
94 if _stim_mod is None:
95 try:
96 import stim
97 _stim_mod = stim
98 except ImportError:
99 raise ImportError("stim library is requested but not installed.")
100 return _stim_mod
101
102
104 """Lazily imports qec_cert (io, pipeline) only when requested."""
105 global _qec_cert_mod
106 if _qec_cert_mod is None:
107 try:
108 import qec_cert
109 from qec_cert import io as _qec_io, pipeline as _qec_pipeline
110 _qec_cert_mod = (qec_cert, _qec_io, _qec_pipeline)
111 except ImportError:
112 raise ImportError("qec_cert library is requested (engine='cert') but not installed.")
113 return _qec_cert_mod
114
115
116def _has_qec_cert() -> bool:
117 """Checks whether qec_cert is importable without raising ImportError."""
118 try:
120 return True
121 except ImportError:
122 return False
123
124
125def __getattr__(name: str) -> Any:
126 """Lazily resolves module attributes without loading heavy dependencies at startup."""
127 if name == "_HAS_STIM":
128 try:
129 import stim
130 return True
131 except ImportError:
132 return False
133 if name == "_HAS_CODEDISTANCE":
134 try:
135 import codedistance
136 return True
137 except ImportError:
138 return False
139 if name == "_HAS_QEC_CERT":
140 return _has_qec_cert()
141 raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
142
143# Global cache for distance results
144_distance_cache: Dict[str, Any] = {}
145_use_distance_cache: bool = True
146_distance_cache_file: Optional[str] = None
147_newer_cache_files: Set[str] = set() # cache files written by a newer version: ignored and never overwritten
148_last_run_stats: Dict[str, Any] = {}
149_last_css_stats: Dict[str, Dict[str, Any]] = {"X": {}, "Z": {}}
150
151# Aliases for backward compatibility with vecdec.py
152_css_distance_cache = _distance_cache
153_use_css_distance_cache = _use_distance_cache
154
155
156def set_distance_cache_file(filepath: Optional[Union[str, Path]] = None) -> None:
157 """
158 Sets the default JSON file for persistent distance caching.
159 If the file exists, its contents are loaded into memory.
160 """
161 global _distance_cache_file
162 if filepath is not None:
163 _distance_cache_file = str(Path(filepath).resolve())
164 load_distance_cache(_distance_cache_file)
165 else:
166 _distance_cache_file = None
167
168
169def load_distance_cache(filepath: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
170 """
171 Loads distance cache from a JSON file into memory.
172 Inspects cache version silently; a cache file written by a newer version is ignored with a warning, and it is
173 never overwritten by save_distance_cache().
174 """
175 global _distance_cache, _distance_cache_file
176 target_file = str(Path(filepath).resolve()) if filepath is not None else _distance_cache_file
177 if target_file and os.path.isfile(target_file):
178 try:
179 with open(target_file, "r") as f:
180 data = json.load(f)
181 if isinstance(data, dict):
182 cache_ver = data.pop("__version__", None)
183 # If cache is from a future incompatible version, skip loading (and do not overwrite it)
184 if cache_ver is not None and _parse_version(cache_ver) > _parse_version(__version__):
185 _newer_cache_files.add(target_file)
186 sys.stderr.write(
187 f"# Warning: Cache file '{target_file}' has newer version {cache_ver} "
188 f"(current {__version__}); ignoring incompatible cache (the file is not overwritten).\n"
189 )
190 return _distance_cache
191 # Sanitize any legacy CSS cache entries where dmax was set from dmin when dmax_X == dmax_Z == 0
192 for k, v in data.items():
193 if isinstance(v, dict) and k.startswith("css:"):
194 if v.get("dmax_X", 0) == 0 and v.get("dmax_Z", 0) == 0 and "dmax_X" in v:
195 v["dmax"] = 0
196 _distance_cache.update(data)
197 except Exception as e:
198 sys.stderr.write(f"# Warning: Failed to load distance cache from {target_file}: {e}\n")
199 return _distance_cache
200
201
202def save_distance_cache(filepath: Optional[Union[str, Path]] = None) -> None:
203 """
204 Saves the in-memory distance cache to a JSON file.
205 Silently writes "__version__": __version__ into the file.
206 Uses atomic write via a temporary file to prevent corruption.
207 A cache file written by a newer version (see load_distance_cache()) is not overwritten.
208 """
209 global _distance_cache, _distance_cache_file
210 target_file = str(Path(filepath).resolve()) if filepath is not None else _distance_cache_file
211 if not target_file or target_file in _newer_cache_files:
212 return
213
214 parent_dir = os.path.dirname(os.path.abspath(target_file)) or "."
215 os.makedirs(parent_dir, exist_ok=True)
216 fd, temp_path = tempfile.mkstemp(suffix=".tmp", prefix="dist_cache_", dir=parent_dir)
217 try:
218 cache_data = dict(_distance_cache)
219 cache_data["__version__"] = __version__
220 with open(fd, "w") as f:
221 json.dump(cache_data, f, indent=2)
222 os.replace(temp_path, target_file)
223 except Exception as e:
224 if os.path.exists(temp_path):
225 try: os.remove(temp_path)
226 except OSError: pass
227 sys.stderr.write(f"# Warning: Failed to save distance cache to {target_file}: {e}\n")
228
229
230def clear_distance_cache(cache_file: Optional[Union[str, Path]] = None, clear_file: bool = False) -> None:
231 """
232 Clears all cached distance calculations from memory, and optionally deletes the persistent JSON file.
233 """
234 global _distance_cache, _distance_cache_file
235 _distance_cache.clear()
236 target_file = str(Path(cache_file).resolve()) if cache_file is not None else _distance_cache_file
237 if clear_file and target_file and os.path.isfile(target_file):
238 try:
239 os.remove(target_file)
240 _newer_cache_files.discard(target_file)
241 except OSError:
242 pass
243
244
246 """Enables distance caching."""
247 global _use_distance_cache
248 _use_distance_cache = True
249
250
252 """Disables distance caching."""
253 global _use_distance_cache
254 _use_distance_cache = False
255
256
257def get_distance_cache() -> Dict[str, Any]:
258 """Returns the global distance cache dictionary."""
259 global _distance_cache
260 return _distance_cache
261
262
263def format_bounds_list(dmin: int, dmax: int, num_rw: int) -> List[int]:
264 """
265 Returns [dmin, dmax, num_rw] according to:
266 - [d, d, 0] if known exactly
267 - [dmin, 0, 0] if there is no upper bound (dmax == 0)
268 - [0, dmax, num_rw] if there is no lower bound (dmin <= 1)
269 - [dmin, dmax, num_rw] otherwise
270 """
271 eff_dmin = dmin if dmin > 1 else 0
272 eff_dmax = dmax if dmax > 0 else 0
273 eff_rw = num_rw if num_rw > 0 else 0
274 if eff_dmin > 0 and eff_dmin == eff_dmax:
275 return [eff_dmin, eff_dmax, 0]
276 elif eff_dmax == 0:
277 return [eff_dmin, 0, 0]
278 elif eff_dmin == 0:
279 return [0, eff_dmax, eff_rw]
280 else:
281 return [eff_dmin, eff_dmax, eff_rw]
282
283
284def format_bounds_str(bounds: List[int]) -> str:
285 """Formats a bounds list [dmin, dmax, num_rw] as a string, stating '(exact)' if bounds coincide."""
286 dmin, dmax, num_rw = bounds[0], bounds[1], bounds[2]
287 if dmin > 0 and dmin == dmax:
288 return f"{dmin} {dmax} {num_rw} (exact)"
289 return f"{dmin} {dmax} {num_rw}"
290
291
292def rw_infoset_find_prob(n: int, rank: int, w: int, pairs_l: int = 0) -> float:
293 """
294 Information-set estimate of the probability that a RW step finds a given codeword of weight w.
295
296 Model: uniform random information sets (as in the dist_m4ri binary, rw_infoset_find_prob()). A RW step
297 reduces H (rank r) with a random column order; a codeword is found if exactly one of its w positions is among
298 the k = n - r non-pivot columns: P1(w) = w C(n-w, k-1) / C(n, k). Codewords of weight w > r + 1 are never
299 found (P1 = 0). With the RW pairs (pairs_l > 0, the binary's option pairs), also if exactly two of its positions
300 are non-pivot columns and its other w - 2 positions avoid the l pivot rows of the buckets:
301 P2(w) = C(w, 2) C(n-w, k-2) / C(n, k) * C(r - w + 2, l) / C(r, l), and the probability is P1 + P2.
302
303 Args:
304 n: Block length (number of columns of H).
305 rank: Rank r of H.
306 w: Codeword weight.
307 pairs_l: The number l of pivot rows of the buckets of the RW pairs (0: no pairs).
308
309 Returns:
310 P1(w) + P2(w).
311 """
312 import math
313 k = n - rank
314 if n <= 0 or rank < 0 or k <= 0 or w < 1 or w > n or w - 1 > rank:
315 return 0.0
316
317 def log_binom(a: int, b: int) -> float:
318 return math.lgamma(a + 1) - math.lgamma(b + 1) - math.lgamma(a - b + 1)
319
320 p = math.exp(math.log(w) + log_binom(n - w, k - 1) - log_binom(n, k))
321 if pairs_l > 0 and w >= 2 and k >= 2 and w - 2 + pairs_l <= rank:
322 p += math.exp(log_binom(w, 2) + log_binom(n - w, k - 2) - log_binom(n, k) + log_binom(rank - w + 2, pairs_l)
323 - log_binom(rank, pairs_l))
324 return min(1.0, p)
325
326
328 n: int, rank: int, steps: int, d: int, dmin: int = 1, step_time: float = 0.0, target: float = 0.01,
329 steps_total: int = 0, pairs_l: int = 0
330) -> Optional[Dict[str, Any]]:
331 """
332 Information-set estimate of the probability that RW missed a codeword lighter than the upper bound d found.
333
334 Among the weights max(dmin, 1) <= w < d (not excluded by CC), the weight hardest to find (the smallest P(w),
335 see rw_infoset_find_prob()) is used: a single codeword of this weight is missed in `steps` RW steps with uniform
336 random permutations with probability (1 - P)^steps. The number of RW steps for the target miss probability is
337 given in total, with the fraction steps / steps_total of uniform steps as in the run (localized windows, used in
338 about half of the steps for n >= 500, are not counted). Same as print_rw_infoset_estimate() in the binary.
339
340 Args:
341 n: Block length (number of columns of H).
342 rank: Rank of H.
343 steps: Number of completed RW steps with uniform random permutations.
344 d: Smallest weight found (the upper bound dmax).
345 dmin: Certified lower bound (smaller weights are excluded).
346 step_time: Wall time per RW step in seconds (0: unknown).
347 target: Target miss probability (default: 0.01).
348 steps_total: Number of all completed RW steps (0: the same as `steps`).
349 pairs_l: The number l of pivot rows of the buckets of the RW pairs (0: no pairs), see rw_infoset_find_prob().
350
351 Returns:
352 Dictionary with the weight `w`, the probability `p_find` = P(w) per step, the miss probability `p_miss`,
353 the total number of RW steps `steps_target` for the target miss probability (None if the codeword cannot be
354 found), and the corresponding wall time `time_target` (None if unknown); or None if there are no weights to
355 check.
356 """
357 import math
358 w_lo, w_hi = max(dmin, 1), d - 1
359 if w_hi < w_lo or n <= 0:
360 return None
361 w_hard, p1 = w_hi, rw_infoset_find_prob(n, rank, w_hi, pairs_l) # the weight hardest to find (smallest P)
362 for w in range(w_lo, w_hi):
363 pw = rw_infoset_find_prob(n, rank, w, pairs_l)
364 if pw < p1:
365 w_hard, p1 = w, pw
366 steps_target: Optional[int]
367 if p1 <= 0.0:
368 p_miss, steps_target = 1.0, None
369 elif p1 >= 1.0:
370 p_miss, steps_target = (0.0 if steps > 0 else 1.0), 1
371 else:
372 lq = math.log1p(-p1)
373 p_miss = math.exp(steps * lq) if steps > 0 else 1.0
374 steps_target = math.ceil(math.log(target) / lq)
375 if steps_target is not None and steps > 0 and steps_total > steps:
376 steps_target = math.ceil(steps_target * steps_total / steps)
377 time_target = steps_target * step_time if (steps_target is not None and step_time > 0.0) else None
378 return {"w": w_hard, "p_find": p1, "p_miss": p_miss, "steps_target": steps_target, "time_target": time_target}
379
380
381def _format_duration(t: float) -> str:
382 """Formats a duration in seconds (s, min, h, or days), as in the dist_m4ri binary."""
383 if t < 120.0:
384 return f"{t:.3g} s"
385 if t < 7200.0:
386 return f"{t / 60.0:.3g} min"
387 if t < 172800.0:
388 return f"{t / 3600.0:.3g} h"
389 return f"{t / 86400.0:.3g} days"
390
391
392def _parse_stderr_stats(stderr: str) -> Dict[str, Any]:
393 """
394 Parses codeword and hit statistics from dist_m4ri stderr output (debug & 1), and with debug & 8 the CC rounds:
395 "cc_rounds", a list with an entry for each completed CC round, {"w": weight, "work": the CC thread time in s,
396 "predicted": the predicted work (None if not known), "time": the wall time of the round in s, "threads": the CC
397 threads (at the end of the round in method=3), "nodes": the calls of the CC recursion (None from binaries before
398 0.15.0)}, and "cc_nodes", the total of the nodes of these rounds.
399 """
400 import re
401 stats: Dict[str, Any] = {"extra_weights": [], "rw_converged": False, "hit_warnings": []}
402 if not stderr:
403 return stats
404 hit_warning: Optional[List[str]] = None
405 skip_estimate = False
406 for line in stderr.splitlines():
407 line_s = line.strip()
408 if line_s.startswith("# information-set estimate"): # printed by the binary after a hit warning
409 skip_estimate = True
410 elif skip_estimate and line_s.startswith("# "):
411 continue
412 else:
413 skip_estimate = False
414 if skip_estimate:
415 if hit_warning is not None:
416 stats["hit_warnings"].append(" ".join(hit_warning))
417 hit_warning = None
418 continue
419 if hit_warning is not None:
420 if line_s.startswith("# "): # continuation of a hit uniformity warning
421 hit_warning.append(line_s[1:].strip())
422 continue
423 stats["hit_warnings"].append(" ".join(hit_warning))
424 hit_warning = None
425 if line_s.startswith("# Warning: non-uniform RW hit counts"):
426 hit_warning = [line_s[len("# Warning: "):]]
427 continue
428 if line_s.startswith("# Note: drop=") and " not used: " in line_s: # the reduction (drop=FILE) is not exact
429 stats["drop_note"] = line_s.split(" not used: ", 1)[1].replace("; using the full DEM", "")
430 continue
431 if line_s.startswith("# Note: drop=") and "none of the listed detectors is in the DEM" in line_s:
432 stats["drop_none_listed"] = True
433 continue
434 if line_s.startswith("# RW information sets:"):
435 m_rw = re.search(
436 r"n=(\d+), rank\‍(H\‍)=(\d+), steps=(\d+) \‍(uniform permutations: (\d+)\‍), ksub=(\d+), "
437 r"(?:pairs=(\d+), )?([0-9.eE+-]+) s/step per thread, (\d+) RW threads", line_s
438 )
439 if m_rw:
440 stats["rw_n"] = int(m_rw.group(1))
441 stats["rw_rank"] = int(m_rw.group(2))
442 stats["rw_steps"] = int(m_rw.group(3))
443 stats["rw_steps_uniform"] = int(m_rw.group(4))
444 stats["rw_ksub"] = int(m_rw.group(5))
445 stats["rw_pairs_l"] = int(m_rw.group(6)) if m_rw.group(6) else 0
446 stats["rw_step_time"] = float(m_rw.group(7))
447 stats["rw_threads"] = int(m_rw.group(8))
448 continue
449 if line_s.startswith("# RW pairs (p=2"): # the pairs checked, and the codewords found by pairs and singles
450 m_p = re.search(r"([0-9.eE+-]+) pairs checked per step; .*: (\d+) from pairs, (\d+) from single columns",
451 line_s)
452 if m_p:
453 stats["rw_pairs_per_step"] = float(m_p.group(1))
454 stats["rw_hits_pairs"] = int(m_p.group(2))
455 stats["rw_hits_single"] = int(m_p.group(3))
456 continue
457 if line_s.startswith("# RW step types, w="): # the codewords found by the uniform and by the window steps
458 m_st = re.search(r"w=(\d+): uniform steps: cws=(\d+), hits=(\d+); window steps: cws=(\d+), hits=(\d+); "
459 r"both: cws=(\d+) \‍(of (\d+) cws\‍)", line_s)
460 if m_st:
461 v = [int(x) for x in m_st.groups()]
462 stats.setdefault("rw_step_types", {})[v[0]] = {
463 "uniform_cws": v[1], "uniform_hits": v[2], "window_cws": v[3], "window_hits": v[4],
464 "both_cws": v[5], "cws": v[6]}
465 continue
466 if line_s.startswith("# rounds of the DEM:"): # the input summary (debug & 1, a DEM)
467 m_r = re.search(r"D=(\d+) detectors and L=(\d+) columns per round \‍(estimated: score ([0-9.]+)\‍)", line_s)
468 m_n = re.search(r"best score ([0-9.]+)", line_s)
469 if m_r:
470 stats["rounds"] = {"D": int(m_r.group(1)), "L": int(m_r.group(2)), "score": float(m_r.group(3))}
471 elif m_n:
472 stats["rounds"] = {"D": 0, "L": 0, "score": float(m_n.group(1))}
473 continue
474 if line_s.startswith("# CC round w=") and " work: " in line_s: # debug & 8: a completed CC round
475 m_cc = re.match(r"# CC round w=(\d+) work: ([0-9.eE+-]+) thread-s measured, (unknown|([0-9.eE+-]+) "
476 r"thread-s) predicted \‍(([0-9.]+)s(?: on (\d+) threads|, (\d+) CC threads at the end)\‍)"
477 r"(?:, (\d+) nodes)?", line_s)
478 if m_cc:
479 g = m_cc.groups()
480 stats.setdefault("cc_rounds", []).append({
481 "w": int(g[0]), "work": float(g[1]), "predicted": float(g[3]) if g[3] else None,
482 "time": float(g[4]), "threads": int(g[5] or g[6]), "nodes": int(g[7]) if g[7] else None})
483 continue
484 if "RW convergence reached" in line_s: # progress line (debug & 2) or the stop reason (debug & 1)
485 stats["rw_converged"] = True
486 if line_s.startswith("# codewords accumulated: total=0"):
487 stats["total_cws"] = 0
488 elif line_s.startswith("# codewords accumulated:"):
489 m = re.search(
490 r"total=(\d+),\s*min_w=(\d+):\s*cws=(\d+),\s*total_hits=(\d+),\s*"
491 r"hits min=(\d+),\s*max=(\d+),\s*avg=([0-9.]+),\s*stdev=([0-9.]+)",
492 line_s
493 )
494 if m:
495 stats["total_cws"] = int(m.group(1))
496 stats["min_w"] = int(m.group(2))
497 stats["cws"] = int(m.group(3))
498 stats["total_hits"] = int(m.group(4))
499 stats["hits_min"] = int(m.group(5))
500 stats["hits_max"] = int(m.group(6))
501 stats["hits_avg"] = float(m.group(7))
502 stats["hits_stdev"] = float(m.group(8))
503 m_set = re.search(
504 r"<n>=([0-9.]+) over (\d+) cws of w=(\d+)\.\.(\d+) \‍(min_hits=(\d+), cov_cws=(-?\d+)\‍)", line_s
505 )
506 if m_set:
507 stats["avg_hits"] = float(m_set.group(1))
508 stats["set_cws"] = int(m_set.group(2))
509 stats["set_w_lo"] = int(m_set.group(3))
510 stats["set_w_hi"] = int(m_set.group(4))
511 stats["min_hits"] = int(m_set.group(5))
512 stats["cov_cws"] = int(m_set.group(6))
513 elif line_s.startswith("# codewords w="):
514 m_w = re.search(
515 r"w=(\d+):\s*cws=(\d+),\s*total_hits=(\d+),\s*"
516 r"hits min=(\d+),\s*max=(\d+),\s*avg=([0-9.]+),\s*stdev=([0-9.]+)",
517 line_s
518 )
519 if m_w:
520 stats["extra_weights"].append({
521 "w": int(m_w.group(1)),
522 "cws": int(m_w.group(2)),
523 "total_hits": int(m_w.group(3)),
524 "hits_min": int(m_w.group(4)),
525 "hits_max": int(m_w.group(5)),
526 "hits_avg": float(m_w.group(6)),
527 "hits_stdev": float(m_w.group(7)),
528 })
529 if hit_warning is not None:
530 stats["hit_warnings"].append(" ".join(hit_warning))
531 cc_rounds = stats.get("cc_rounds")
532 if cc_rounds and all(r["nodes"] is not None for r in cc_rounds):
533 stats["cc_nodes"] = sum(r["nodes"] for r in cc_rounds)
534 return stats
535
536
538 bounds: List[int],
539 method: Optional[int] = None,
540 label: str = "",
541 stats: Optional[Dict[str, Any]] = None,
542 dstop: int = 0
543) -> str:
544 """
545 Returns a human-readable explanation of [dmin, dmax, num_rw] following README.md.
546
547 Args:
548 bounds: [dmin, dmax, rw_steps] list.
549 method: Optional solver method (1=RW, 2=CC, 3=Bracketing).
550 label: Optional prefix/label (e.g. "dX", "dZ", "").
551 stats: Optional dictionary of codeword and hit statistics parsed from dist_m4ri.
552 dstop: Stop target of the search (0: none); once dmin >= dstop, the upper bound is not needed, and the
553 estimates of the probability that RW missed a lighter codeword are skipped.
554
555 Returns:
556 Multi-line formatted explanation string.
557 """
558 dmin, dmax, num_rw = bounds[0], bounds[1], bounds[2]
559 lines = []
560 prefix = f"{label} " if label else ""
561 stopped = dstop > 0 and dmin >= dstop and not (dmin > 0 and dmin == dmax)
562
563 # Lower bound explanation
564 if dmin > 0 and dmin == dmax:
565 lines.append(f" {prefix}Lower bound (dmin = {dmin}): Exact distance certified (dmin == dmax == {dmin}).")
566 elif dmin > 1:
567 lines.append(f" {prefix}Lower bound (dmin = {dmin}): All cluster weights w <= {dmin - 1} "
568 f"were exhaustively analyzed by CC without finding any non-trivial codewords.")
569 else:
570 lines.append(f" {prefix}Lower bound (dmin = {dmin}): No non-trivial lower bound certified (dmin <= 1).")
571
572 # Upper bound explanation
573 if dmax > 0:
574 lines.append(f" {prefix}Upper bound (dmax = {dmax}): Weight of the smallest non-trivial codeword discovered.")
575 else:
576 lines.append(f" {prefix}Upper bound (dmax = {dmax}): No non-trivial codeword discovered yet (dmax = 0).")
577 if stopped:
578 lines.append(f" {prefix}Stop target (dstop = {dstop}): reached, the search ended once dmin >= {dstop} was "
579 f"certified (no codeword of weight < {dstop}); the upper bound is not needed.")
580
581 # RW steps explanation (and why it is zero if num_rw == 0)
582 if num_rw > 0:
583 lines.append(f" {prefix}Random window steps (rw_steps = {num_rw}): {num_rw} completed random "
584 f"information set searches across worker threads.")
585 else:
586 if dmin > 0 and dmin == dmax:
587 lines.append(f" {prefix}Random window steps (rw_steps = 0): Set to 0 because the exact distance "
588 f"d = {dmin} was proven by Connected Cluster search or certified bounds coincided.")
589 elif method == 2:
590 lines.append(f" {prefix}Random window steps (rw_steps = 0): Set to 0 because Method 2 (Connected Cluster) "
591 f"is an exhaustive search that does not perform random information set (RW) sampling.")
592 else:
593 lines.append(f" {prefix}Random window steps (rw_steps = 0): 0 completed random information set steps.")
594
595 if stats and "total_cws" in stats:
596 if stats["total_cws"] > 0:
597 lines.append(
598 f" {prefix}Codewords accumulated: {stats['total_cws']} distinct "
599 f"(w = {stats['min_w']}: {stats['cws']} codewords, {stats['total_hits']} total hits; "
600 f"hits min = {stats['hits_min']}, max = {stats['hits_max']}, "
601 f"avg = {stats['hits_avg']:.2f}, stdev = {stats['hits_stdev']:.2f})."
602 )
603 if stats.get("min_hits", 0) > 0 and "avg_hits" in stats:
604 import math
605 conv_str = "CONVERGED" if stats.get("rw_converged") else "not converged"
606 lines.append(
607 f" {prefix}Hit convergence (min_hits = {stats['min_hits']}, "
608 f"cov_cws = {stats['cov_cws']}): <n> = {stats['avg_hits']:.2f} average hits per codeword "
609 f"({stats['set_cws']} codewords of weight {stats['set_w_lo']}..{stats['set_w_hi']}), "
610 f"P_fail ~ exp(-<n>) = {math.exp(-stats['avg_hits']):.2g} ({conv_str})."
611 )
612 for ew in stats.get("extra_weights", []):
613 lines.append(
614 f" {prefix}Codewords (w = {ew['w']}): {ew['cws']} codewords, "
615 f"{ew['total_hits']} total hits; hits min = {ew['hits_min']}, "
616 f"max = {ew['hits_max']}, avg = {ew['hits_avg']:.2f}, "
617 f"stdev = {ew['hits_stdev']:.2f}."
618 )
619 n_all, n_unif = stats.get("rw_steps", 0), stats.get("rw_steps_uniform", 0)
620 for w, st in sorted(stats.get("rw_step_types", {}).items()):
621 lines.append(
622 f" {prefix}Codewords (w = {w}) by RW step type: {st['uniform_cws']} found in the {n_unif} uniform "
623 f"steps ({st['uniform_hits']} hits), {st['window_cws']} in the {n_all - n_unif} window steps "
624 f"({st['window_hits']} hits), {st['both_cws']} in both (of {st['cws']} codewords)."
625 )
626 if stats.get("rw_pairs_l", 0) > 0 and "rw_pairs_per_step" in stats:
627 lines.append(
628 f" {prefix}RW pairs (p=2, buckets of l = {stats['rw_pairs_l']} pivot rows): "
629 f"{stats['rw_pairs_per_step']:.3g} pairs checked per RW step; codewords recorded with weight up to "
630 f"the upper bound: {stats['rw_hits_pairs']} from pairs, {stats['rw_hits_single']} from single "
631 f"columns."
632 )
633 for hw in stats.get("hit_warnings", []):
634 lines.append(f" {prefix}WARNING: {hw}")
635 else:
636 lines.append(f" {prefix}Codewords accumulated: 0 distinct non-trivial codewords found.")
637
638 # The RW upper bound is not certified: estimated probability that RW missed a lighter codeword
639 if stats and num_rw > 0 and dmax > max(dmin, 1) and not stopped:
640 import math
641 rw_threads = stats.get("rw_threads", 0)
642 step_wall = stats.get("rw_step_time", 0.0) / rw_threads if rw_threads > 0 else 0.0
643 avg = stats.get("avg_hits", 0.0)
644 if stats.get("min_hits", 0) > 0 and avg > 0.0:
645 p_hit = math.exp(-avg)
646 if p_hit > 0.01:
647 need = math.ceil(num_rw * math.log(100.0) / avg) # the average number of hits grows with the steps
648 t_str = f", ~{_format_duration((need - num_rw) * step_wall)} more" if step_wall > 0.0 else ""
649 lines.append(f" {prefix}Hit-based estimate: P_fail ~ exp(-<n>) = {p_hit:.2g}; 1% after about {need} "
650 f"RW steps in total{t_str}.")
651 else:
652 lines.append(f" {prefix}Hit-based estimate: P_fail ~ exp(-<n>) = {p_hit:.2g} (below 1%).")
653 n_unif = stats.get("rw_steps_uniform", 0)
654 if stats.get("rw_n", 0) > 0 and stats.get("rw_ksub", 0) == 0 and n_unif > 0:
655 n_cols, rank = stats["rw_n"], stats["rw_rank"]
656 pairs_l = stats.get("rw_pairs_l", 0)
657 est = rw_infoset_estimate(n_cols, rank, n_unif, dmax, dmin=max(dmin, 1), step_time=step_wall,
658 steps_total=stats.get("rw_steps", n_unif), pairs_l=pairs_l)
659 if est is not None and est["steps_target"] is None:
660 lines.append(f" {prefix}Information-set estimate: a codeword of weight {est['w']} cannot be found "
661 f"by RW (n = {n_cols}, rank(H) = {rank}).")
662 elif est is not None:
663 t_str = (f", ~{_format_duration(est['time_target'])}" if est["time_target"] is not None else "")
664 p_str = f" with pairs, l = {pairs_l}" if pairs_l > 0 else ""
665 lines.append(
666 f" {prefix}Information-set estimate (uniform random information sets{p_str}; n = {n_cols}, "
667 f"rank(H) = {rank}, {n_unif} of {stats.get('rw_steps', n_unif)} RW steps uniform): a single "
668 f"codeword of weight {est['w']} < {dmax} is found with probability {est['p_find']:.3g} per RW "
669 f"step and would be missed with probability {est['p_miss']:.2g}; 1% after about "
670 f"{est['steps_target']} RW steps in total{t_str}."
671 )
672 # (with min_hits=0, the binary does not count the hits: each codeword then has a single hit)
673 if stats.get("min_w") == dmax and "hits_avg" in stats and stats.get("min_hits", 0) > 0:
674 n_all = stats.get("rw_steps", n_unif)
675 p1 = rw_infoset_find_prob(n_cols, rank, dmax, pairs_l)
676 st = stats.get("rw_step_types", {}).get(dmax)
677 if st is not None and st["cws"] > 0: # the hits of the uniform steps only
678 n_ref, obs, note = n_unif, st["uniform_hits"] / st["cws"], " in the uniform steps"
679 else:
680 n_ref, obs = n_all, stats["hits_avg"]
681 note = "" if n_unif == n_all else ", including the steps with localized windows"
682 exp_hits = n_ref * p1
683 verdict = ("the uniform model is conservative here" if obs >= exp_hits else
684 "codewords are found less often than in the uniform model, the information-set "
685 "estimate may be optimistic")
686 lines.append(
687 f" {prefix}Model check: expected hits per codeword of weight {dmax} in {n_ref} uniform RW "
688 f"steps: {exp_hits:.3g}, observed average {obs:.2f}{note} ({verdict})."
689 )
690 elif stats.get("rw_n", 0) > 0:
691 lines.append(f" {prefix}Information-set estimate: not available (ksub > 0 or kwin > 0: no uniform "
692 f"random information sets).")
693
694 return "\n".join(lines)
695
696
698 H: Optional[Any] = None,
699 G: Optional[Any] = None,
700 L: Optional[Any] = None,
701 Hx: Optional[Any] = None,
702 Hz: Optional[Any] = None,
703 Lx: Optional[Any] = None,
704 Lz: Optional[Any] = None,
705 dem: Optional[Any] = None,
706 circuit: Optional[Any] = None,
707 pmin: float = 0.0,
708 cache_file: Optional[Union[str, Path]] = None,
709 start: Optional[Union[int, str, Sequence[int]]] = None,
710 simple: Optional[bool] = None,
711 full: Optional[bool] = None,
712 basis: Optional[str] = None,
713 rounds: Optional[int] = None,
714 p_noise: float = 0.001,
715 force_noise: bool = False,
716 start_scan: bool = False
717) -> Optional[Dict[str, Any]]:
718 """
719 Retrieves the cached distance entry (including bounds and cumulative rw_steps)
720 for a given code matrix, CSS code, or DEM.
721
722 If the expert CC start-column list `start` is given (see compute_classical_distance), the
723 separate start-list record (cache key suffix ':start=a,b,c', or ':start=a,b,c:scan' with start_scan=True) is
724 returned instead of the main record.
725
726 For a CSS code (Hx and Hz, optionally Lx / Lz), the entry is assembled from the records of its two sectors,
727 the quantum codes (H=Hx, G=Hz) -> dZ and (H=Hz, G=Hx) -> dX (shared with compute_quantum_distance()): the fields
728 dmin_X, dmax_X, rw_steps_X, dX, cws_X (the same for Z), and dist, dmin, dmax, rw_steps of d = min(dX, dZ).
729
730 For a Stim circuit (circuit, or dem with a .stim file), the cache key is that of the DEM processed as in
731 compute_dem_distance(), with the same options rounds, p_noise, and force_noise (simple and full are disabled since
732 version 0.13.0: ignored with a warning to stderr; basis does not change the DEM).
733
734 Returns:
735 dict with keys {"dist", "dmin", "dmax", "rw_steps", ...} or None if not cached.
736 """
737 global _distance_cache, _distance_cache_file
738 eff_cache_file = str(Path(cache_file).resolve()) if cache_file is not None else _distance_cache_file
739 if eff_cache_file:
740 load_distance_cache(eff_cache_file)
742
743 if H is not None:
744 entry = _distance_cache.get(_matrix_cache_key(H, G, L, classical=(G is None and L is None)) + sfx)
745 if entry:
746 entry = dict(entry)
747 entry["d_info"] = format_bounds_list(entry.get("dmin", 0), entry.get("dmax", 0), entry.get("rw_steps", 0))
748 return entry
749 elif Hx is not None or Hz is not None:
750 # a CSS code is cached as its two sectors (H=Hx, G=Hz) -> dZ and (H=Hz, G=Hx) -> dX, see compute_css_distance()
751 if Hx is None or Hz is None:
752 return None
753 _migrate_css_records(Hx, Hz, Lx, Lz, eff_cache_file)
754 codes = _css_sector_codes(Hx, Hz, Lx, Lz)
755 sec = {s: _distance_cache.get(_matrix_cache_key(*codes[s]) + sfx) for s in ("X", "Z")}
756 if not sec["X"] and not sec["Z"]:
757 return None
758 entry = {}
759 bounds = {}
760 for s in ("X", "Z"):
761 rec = sec[s] or {}
762 b = (rec.get("dmin", 0) or 0, rec.get("dmax", 0) or 0, rec.get("rw_steps", 0) or 0)
763 bounds[s] = b
764 entry[f"dmin_{s}"], entry[f"dmax_{s}"], entry[f"rw_steps_{s}"] = b
765 entry[f"d{s}"] = format_bounds_list(*b)
766 entry[f"cws_{s}"] = list(rec.get("cws", []))
767 dist, d_lo, d_hi, rw = _css_combine(bounds["X"], bounds["Z"])
768 entry.update({"dist": dist, "dmin": d_lo, "dmax": d_hi, "rw_steps": rw})
769 return entry
770 elif dem is not None or circuit is not None:
771 # the key of compute_dem_distance(): a Stim circuit is processed and converted in the same way
772 _warn_disabled_simple_full(simple, full)
773 dem_obj, _, _ = _dem_from_input(dem, circuit, basis=basis, rounds=rounds, p_noise=p_noise,
774 force_noise=force_noise)
775 entry = _distance_cache.get(_dem_cache_key(dem_obj, pmin) + sfx)
776 if entry:
777 entry = dict(entry)
778 entry["d_info"] = format_bounds_list(entry.get("dmin", 0), entry.get("dmax", 0), entry.get("rw_steps", 0))
779 return entry
780 return None
781
782
783# Backward-compatibility aliases
784clear_css_distance_cache = clear_distance_cache
785enable_css_distance_cache = enable_distance_cache
786disable_css_distance_cache = disable_distance_cache
787
788
790 """Returns a deterministic string representation for JSON-compatible cache keys."""
791 if A is None:
792 return "none"
793 if isinstance(A, (str, Path)):
794 path_str = str(Path(A).resolve())
795 if os.path.isfile(path_str):
796 try:
797 with open(path_str, "rb") as f:
798 content_h = hashlib.sha256(f.read()).hexdigest()
799 return f"file:{path_str}:{content_h}"
800 except Exception:
801 return f"file:{path_str}"
802 return path_str
803 if hasattr(A, 'shape') and hasattr(A, 'dtype') and hasattr(A, 'tobytes'):
804 h = hashlib.sha256(A.tobytes()).hexdigest()
805 dtype_str = getattr(A.dtype, 'str', str(A.dtype))
806 return f"ndarray:{A.shape}:{dtype_str}:{h}"
807 if hasattr(A, 'tocsr'):
808 csr = A.tocsr()
809 h = hashlib.sha256(csr.data.tobytes() + csr.indices.tobytes() + csr.indptr.tobytes()).hexdigest()
810 return f"csr:{csr.shape}:{h}"
811 if hasattr(A, 'tobytes'):
812 h = hashlib.sha256(A.tobytes()).hexdigest()
813 return f"bytes:{h}"
814 h = hashlib.sha256(str(A).encode('utf-8')).hexdigest()
815 return f"str_sha256:{h}"
816
817
818def create_unique_file(directory: Union[str, Path] = "tmp", extension: str = ".tmp") -> str:
819 """Creates a unique temporary file path and ensures the parent directory exists."""
820 try:
821 os.makedirs(directory, exist_ok=True)
822 fd, path = tempfile.mkstemp(suffix=extension, dir=directory)
823 except OSError:
824 fd, path = tempfile.mkstemp(suffix=extension)
825 os.close(fd)
826 return path
827
828
829def _remove_temp_files(files: Sequence[Optional[str]], debug: int = 0) -> None:
830 """Removes the temporary files `files` (None entries are skipped); with the debug bit PY_DBG_KEEP_FILES, the
831 files are kept and listed instead."""
832 for f in files:
833 if not f or not os.path.exists(f):
834 continue
835 if debug & PY_DBG_KEEP_FILES:
836 print(f"[dist_m4ri] Kept temporary file: {f}")
837 continue
838 try:
839 os.remove(f)
840 except OSError:
841 pass
842
843
844def read_sparse_vectors(filepath: str) -> List[List[int]]:
845 """
846 Reads a list of sparse vectors from a text file in NZLIST format,
847 converting from 1-based indexing (in the file) to 0-based indexing (in Python).
848
849 Args:
850 filepath (str): The path to the text file.
851
852 Returns:
853 list of list of int: A list where each element is a 0-based sparse vector.
854 """
855 sparse_vectors = []
856 if not os.path.exists(filepath) or os.path.getsize(filepath) == 0:
857 return sparse_vectors
858
859 with open(filepath, 'r') as f:
860 first_line = f.readline().strip()
861 if not first_line:
862 return sparse_vectors
863 if first_line != '%% NZLIST':
864 raise ValueError(f"Invalid file format in {filepath}: Missing '%% NZLIST' header.")
865
866 for line_num, line in enumerate(f, start=2):
867 line = line.strip()
868 if not line or line.startswith('%'):
869 continue
870 try:
871 parts = list(map(int, line.split()))
872 except ValueError:
873 raise ValueError(f"Non-integer data found on line {line_num}: {line}")
874
875 stated_length = parts[0]
876 vector_elements = [x - 1 for x in parts[1:]]
877 if len(vector_elements) != stated_length:
878 raise ValueError(
879 f"Length mismatch on line {line_num}. "
880 f"Expected {stated_length} elements, but found {len(vector_elements)}."
881 )
882 sparse_vectors.append(vector_elements)
883
884 return sparse_vectors
885
886
887def find_dist_m4ri_binary(custom_path: Optional[str] = None) -> str:
888 """Finds the dist_m4ri executable."""
889 if custom_path:
890 if os.path.isfile(custom_path) and os.access(custom_path, os.X_OK):
891 return os.path.abspath(custom_path)
892 raise FileNotFoundError(
893 f"Specified executable '{custom_path}' not found or not executable."
894 )
895
896 pkg_dir = os.path.dirname(os.path.abspath(__file__))
897 candidates = [
898 os.path.join(pkg_dir, "src", "dist_m4ri"),
899 os.path.join(pkg_dir, "dist_m4ri"),
900 os.path.join(pkg_dir, "bin", "dist_m4ri"),
901 os.path.join(pkg_dir, "..", "dist-m4ri", "src", "dist_m4ri"),
902 os.path.join(os.getcwd(), "src", "dist_m4ri"),
903 os.path.join(os.getcwd(), "dist_m4ri"),
904 os.path.join(os.getcwd(), "bin", "dist_m4ri"),
905 ]
906
907 for cand in candidates:
908 if os.path.isfile(cand) and os.access(cand, os.X_OK):
909 return os.path.abspath(cand)
910
911 which_path = shutil.which("dist_m4ri")
912 if which_path:
913 return which_path
914
915 raise FileNotFoundError(
916 "Could not find executable 'dist_m4ri'. Please run 'make -C src' to build it."
917 )
918
919
920def _parse_version(v_str: str) -> Tuple[int, ...]:
921 import re
922 parts = re.findall(r"\d+", v_str)
923 return tuple(int(p) for p in parts) if parts else (0,)
924
925
926def _binary_version(binary_path: Optional[str] = None) -> Tuple[Optional[str], Optional[str], Optional[str]]:
927 """
928 The dist_m4ri binary and its version (from `dist_m4ri --version`).
929
930 Returns:
931 (path, version, error): path is None if the binary is not found; version is None if it is unknown (then
932 error is the reason, or None if the binary does not support --version).
933 """
934 try:
935 path = find_dist_m4ri_binary(binary_path)
936 except (RuntimeError, FileNotFoundError):
937 return None, None, None
938 try:
939 proc = subprocess.run([path, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
940 timeout=2.0)
941 except Exception as e:
942 return path, None, str(e)
943 if proc.returncode == 0 and "version" in proc.stdout:
944 return path, proc.stdout.strip().split()[-1], None
945 return path, None, None
946
947
948def check_binary_compatibility(binary_path: Optional[str] = None) -> Optional[str]:
949 """
950 Checks if the backend dist_m4ri binary exists and is compatible (version >= __version__).
951 Returns a warning message string if missing or older, or None if compatible (silent).
952 """
953 path, bin_ver, error = _binary_version(binary_path)
954 if path is None:
955 return "Warning: backend binary 'dist_m4ri' not found (run 'make -C src' to build it)"
956 if error is not None:
957 return f"Warning: failed to check binary '{path}': {error}"
958 if bin_ver is None:
959 return (
960 f"Warning: backend binary '{path}' does not support --version "
961 f"(expected >= {__version__}; run 'make -C src' to rebuild)"
962 )
963 if _parse_version(bin_ver) < _parse_version(__version__):
964 return (
965 f"Warning: backend binary '{path}' is version {bin_ver} "
966 f"(expected >= {__version__}; run 'make -C src' to rebuild)"
967 )
968 return None # Compatible and up-to-date: silent!
969
970
971def parse_dist_m4ri_output(stdout: str) -> Tuple[int, int, int]:
972 """
973 Parses the standard output of dist_m4ri.
974 Expected format on stdout: "dmin dmax rw_steps", "dmin dmax", or a single integer.
975
976 Returns:
977 tuple (dmin, dmax, rw_steps)
978 """
979 lines = stdout.strip().split('\n')
980 for line in reversed(lines):
981 line = line.strip()
982 if not line or line.startswith('#'):
983 continue
984 parts = line.split()
985 if len(parts) >= 3:
986 try:
987 return int(parts[0]), int(parts[1]), int(parts[2])
988 except ValueError:
989 continue
990 elif len(parts) == 2:
991 try:
992 return int(parts[0]), int(parts[1]), 0
993 except ValueError:
994 continue
995 elif len(parts) == 1:
996 try:
997 val = int(parts[0])
998 return val, val, 0
999 except ValueError:
1000 continue
1001
1002 raise RuntimeError(f"Could not parse dist_m4ri output: {stdout}")
1003
1004
1006 """
1007 Structured result for code distance calculations containing:
1008 - dmin: lower bound on distance
1009 - dmax: upper bound on distance (minimum non-trivial codeword weight found)
1010 - rw_steps: cumulative number of completed random window information sets
1011 - cws: discovered codewords (if requested)
1012 """
1014 self,
1015 dmin: int,
1016 dmax: int,
1017 rw_steps: int = 0,
1018 cws: Optional[List[List[int]]] = None,
1019 cws_X: Optional[List[List[int]]] = None,
1020 cws_Z: Optional[List[List[int]]] = None,
1021 dmin_X: Optional[int] = None,
1022 dmax_X: Optional[int] = None,
1023 rw_steps_X: Optional[int] = None,
1024 dmin_Z: Optional[int] = None,
1025 dmax_Z: Optional[int] = None,
1026 rw_steps_Z: Optional[int] = None,
1027 ):
1028 self.dmin = dmin
1029 self.dmax = dmax
1030 self.rw_steps = rw_steps
1031 self.cws = cws
1032 self.cws_X = cws_X
1033 self.cws_Z = cws_Z
1034 self.dmin_X = dmin_X
1035 self.dmax_X = dmax_X
1036 self.rw_steps_X = rw_steps_X
1037 self.dmin_Z = dmin_Z
1038 self.dmax_Z = dmax_Z
1039 self.rw_steps_Z = rw_steps_Z
1040
1041 @property
1042 def is_exact(self) -> bool:
1043 return self.dmin > 0 and self.dmin == self.dmax
1044
1045 @property
1046 def dist(self) -> int:
1047 return self.dmin if self.is_exact else (self.dmax if self.dmax > 0 else self.dmin)
1048
1049 def __int__(self) -> int:
1050 return self.dist
1051
1052 def __index__(self) -> int:
1053 return self.dist
1054
1055 def __eq__(self, other: Any) -> bool:
1056 if isinstance(other, DistanceResult):
1057 return (self.dmin, self.dmax, self.rw_steps) == (other.dmin, other.dmax, other.rw_steps)
1058 if isinstance(other, (tuple, list)):
1059 return self._fields() == tuple(other)
1060 if isinstance(other, numbers.Integral): # int, bool, and numpy integers
1061 return self.dist == other
1062 return False
1063
1064 def _fields(self) -> tuple:
1065 """The result as a tuple (dmin, dmax, rw_steps[, cws] or [, cws_X, cws_Z])."""
1066 if self.cws_X is not None or self.cws_Z is not None:
1067 return (self.dmin, self.dmax, self.rw_steps, self.cws_X or [], self.cws_Z or [])
1068 if self.cws is not None:
1069 return (self.dmin, self.dmax, self.rw_steps, self.cws)
1070 return (self.dmin, self.dmax, self.rw_steps)
1071
1072 def __iter__(self):
1073 return iter(self._fields())
1074
1075 def __getitem__(self, index: int):
1076 return self._fields()[index]
1077
1078 def __len__(self) -> int: # (not len(tuple(self)): tuple() calls __len__)
1079 return len(self._fields())
1080
1081 def __str__(self) -> str:
1082 if self.is_exact:
1083 return f"{self.dmin} {self.dmax} {self.rw_steps} (exact)"
1084 return f"{self.dmin} {self.dmax} {self.rw_steps}"
1085
1086 def __repr__(self) -> str:
1087 if self.is_exact:
1088 return f"DistanceResult({self.dmin} {self.dmax} {self.rw_steps} (exact))"
1089 return f"DistanceResult(dmin={self.dmin}, dmax={self.dmax}, rw_steps={self.rw_steps})"
1090
1091
1092def check_finc_outc(finC: Optional[str], outC: Optional[str], verbose: bool = False) -> Optional[str]:
1093 """
1094 When finC and outC names are identical, an empty or non-existent file is silently ignored
1095 (with a warning if verbose is True).
1096
1097 Returns:
1098 The effective finC filepath to use (or None if ignored).
1099 """
1100 if not finC:
1101 return None
1102 if outC and (finC == outC or os.path.abspath(finC) == os.path.abspath(outC)):
1103 if not os.path.exists(finC) or os.path.getsize(finC) == 0:
1104 if verbose:
1105 print(f"[dist_m4ri] Warning: finC='{finC}' (identical to outC) is empty or non-existent; "
1106 f"silently ignoring input codewords.")
1107 return None
1108 return finC
1109
1110
1111# ---------------------------------------------------------------------------------------------------------------
1112# Expert CC options: `start` list of CC start columns, `trust_start`, and the disabled options noscan, cbeg, cend.
1113#
1114# - start=a,b,c: CC clusters are grown only from the listed columns, and they are NOT limited to larger column
1115# indices (unlike cbeg/cend in the dist_m4ri binary). The lower bound dmin (and an exact result) is valid only
1116# if a code symmetry maps every codeword to one whose support contains a listed column (e.g., one column per
1117# block of a quasi-cyclic code); the upper bound dmax is always valid. Results are cached under the separate
1118# key '<key>:start=a,b,c' (sorted, distinct columns). Only dmax and the codewords are merged into the main
1119# record, unless the expert option trust_start is set (then dmin and rw_steps are merged as well, and an
1120# existing exact start-list record is copied to the main record even if no calculation is needed).
1121# - start_scan (since version 0.15.1, with start): a cluster grown from a listed column c only uses the columns > c,
1122# as in the scan of all columns; valid if a code symmetry maps every codeword to one whose smallest column is
1123# listed, e.g., the smallest columns of the orbits of a group of code automorphisms (start=0,l for the two blocks
1124# of a two-block group-algebra code over an abelian group). Cached under '<key>:start=a,b,c:scan'.
1125# - noscan, cbeg, cend: disabled in the Python interface (ignored with a warning to stderr); use the dist_m4ri
1126# binary directly (noscan: CC only at w=wmax; cbeg/cend: start-column range for split multi-run use, where
1127# each column is eventually listed in one of the runs).
1128# - irred (since version 0.14.8): the CLI ignores irred=0 with a warning (see _warn_cli_conflicts()); the binary
1129# always runs with its default irred=1, so that the codewords recorded (and cached) are irreducible.
1130# ---------------------------------------------------------------------------------------------------------------
1131
1132def _normalize_start_list(start: Any) -> Optional[List[int]]:
1133 """
1134 Normalizes the expert CC start-column specification `start` into a sorted list of distinct
1135 non-negative (0-based) column indices, or None if unset.
1136
1137 Accepts None, an integer, a comma-separated string (e.g., "0,48"), or a sequence of integers.
1138 A single negative value (e.g., -1, the default of the dist_m4ri binary) means unset (all columns).
1139
1140 Raises:
1141 ValueError: on malformed input or on negative entries in a list of several columns.
1142 """
1143 import operator
1144 if start is None:
1145 return None
1146 err_msg = f"invalid start={start!r}: expected a comma-separated list of column indices, e.g., start=0,48"
1147 vals: List[int] = []
1148 if isinstance(start, str):
1149 s = start.strip()
1150 if not s:
1151 return None
1152 try:
1153 vals = [int(tok) for tok in s.split(",")]
1154 except ValueError:
1155 raise ValueError(err_msg) from None
1156 else:
1157 try:
1158 items = list(start)
1159 except TypeError:
1160 items = [start]
1161 for v in items:
1162 if isinstance(v, (bool, str, bytes)):
1163 raise ValueError(err_msg)
1164 try:
1165 vals.append(operator.index(v))
1166 except TypeError:
1167 raise ValueError(err_msg) from None
1168 if not vals:
1169 return None
1170 if len(vals) == 1 and vals[0] < 0:
1171 return None
1172 if any(v < 0 for v in vals):
1173 raise ValueError(f"invalid start={start!r}: column indices must be non-negative")
1174 return sorted(set(vals))
1175
1176
1177class _StartList(list):
1178 """A normalized CC start list (sorted distinct columns, see _normalize_start_list()) with the expert flag `scan`
1179 (start_scan=1 of the binary: a cluster grown from a listed column c only uses the columns > c), which selects
1180 its own cache key (see _start_key_suffix())."""
1181 scan: bool = False
1182
1183
1184def _scan_start_list(start_list: Optional[List[int]], start_scan: bool) -> Optional[List[int]]:
1185 """The start list with the flag `scan` set if start_scan (None or the list itself otherwise)."""
1186 if not start_list or not start_scan:
1187 return start_list
1188 out = _StartList(start_list)
1189 out.scan = True
1190 return out
1191
1192
1193def _start_key_suffix(start_list: Optional[List[int]]) -> str:
1194 """Returns the cache key suffix ':start=a,b,c' for a normalized start list ('' if unset), with ':scan' for
1195 start_scan."""
1196 if not start_list:
1197 return ""
1198 return ":start=" + ",".join(str(c) for c in start_list) + (":scan" if getattr(start_list, "scan", False) else "")
1199
1200
1201def _format_start_list(start_list: List[int], max_show: int = 8) -> str:
1202 """Formats a start list for messages, showing at most `max_show` entries (as the dist_m4ri binary)."""
1203 shown = ",".join(str(c) for c in start_list[:max_show])
1204 return shown + (",..." if len(start_list) > max_show else "")
1205
1206
1207_report_ignored = True # warnings on stderr for the inputs which are ignored (off: set_silent(), the CLI's --silent)
1208
1209
1210def set_silent(silent: bool = True) -> None:
1211 """
1212 Turns off (silent=True) or on again (silent=False) the warnings written to stderr for the inputs which are
1213 ignored (the CLI option --silent). The warnings on the validity of a result (e.g., for ksub, ortho=0, or start)
1214 are always written.
1215 """
1216 global _report_ignored
1217 _report_ignored = not silent
1218
1219
1220def _warn_ignored(msg: str) -> None:
1221 """Writes the warning '# Warning: msg' on an input which is ignored to stderr, unless set_silent() is on."""
1222 if _report_ignored:
1223 sys.stderr.write(f"# Warning: {msg}\n")
1224
1225
1226# The options of the dist_m4ri binary in the compute_*() functions, with their defaults there, which
1227# solver="codedistance" does not use (it uses the matrices or the DEM, num_steps, seed, codedistance_method, and
1228# codedistance_params): see _warn_codedistance_ignored(). (start, ortho, noscan, cbeg, cend, outD, and out_reduced
1229# have warnings of their own, and outC and do_cws are errors.)
1230_CODEDISTANCE_UNUSED: Dict[str, Any] = {
1231 "method": 3, "threads": None, "timeout": 60.0, "d_exp": 0, "dmin": 0, "dmax": 0, "d_min": 0, "d_max": 0,
1232 "dstop": 0, "wmin": 1, "wmax": 0, "smax": None, "dW": -1, "maxC": 0, "finC": None, "nothrottle": False,
1233 "chunk_size": 0, "ksub": 0, "kwin": 0, "win_mode": 0, "win_hyb": 0, "pairs": -1, "sort_rows": None,
1234 "cc_prune": None, "min_hits": None, "cov_cws": 100, "refresh": None, "dist_m4ri": None, "pmin": 0.0,
1235 "merge": True, "drop": None, "expand": False,
1236}
1237
1238
1239def _warn_codedistance_ignored(values: Dict[str, Any]) -> None:
1240 """
1241 With solver="codedistance": a warning (see _warn_ignored()) on the options of the dist_m4ri binary in `values`
1242 (the locals() of a compute_*() function) which are not at their defaults (see _CODEDISTANCE_UNUSED).
1243 """
1244 names = [k for k, v in _CODEDISTANCE_UNUSED.items() if k in values and values[k] != v]
1245 if names:
1246 _warn_ignored(f"{', '.join(k + '=' for k in names)}: not used with solver='codedistance'; ignored")
1247
1248
1249def _warn_disabled_options(noscan: Any = 0, cbeg: Any = None, cend: Any = None) -> None:
1250 """
1251 Issues a warning to stderr for each of the expert options noscan, cbeg, and cend that is set.
1252 These options are disabled in the Python interface: they are accepted for backward compatibility
1253 but ignored, since their results are not valid distance bounds on their own.
1254 """
1255 for name, val, is_flag in (("noscan", noscan, True), ("cbeg", cbeg, False), ("cend", cend, False)):
1256 if val is None:
1257 continue
1258 try:
1259 ival = int(val)
1260 active = (ival != 0) if is_flag else (ival >= 0)
1261 except (TypeError, ValueError):
1262 active = True
1263 if active:
1264 _warn_ignored(f"{name}={val} is disabled in the Python interface (ignored); use the dist_m4ri binary "
1265 f"directly")
1266
1267
1268def _start_warning_text(start_list: List[int]) -> str:
1269 """Returns the stderr warning for a CC start list (same wording as the dist_m4ri binary)."""
1270 if getattr(start_list, "scan", False):
1271 return (
1272 f"# WARNING: start={_format_start_list(start_list)} start_scan=1 (expert options): CC clusters are grown "
1273 f"only from {len(start_list)} listed column(s), each with\n"
1274 f"# the larger columns only; dmin is valid only if a code symmetry maps every minimum-weight codeword\n"
1275 f"# to one whose smallest column is listed\n"
1276 )
1277 return (
1278 f"# WARNING: start={_format_start_list(start_list)} (expert option): CC clusters are grown only from "
1279 f"{len(start_list)} listed column(s)\n"
1280 f"# (not limited to larger column indices); dmin is valid only if a code symmetry maps every\n"
1281 f"# minimum-weight codeword to one containing a listed column\n"
1282 )
1283
1284
1285def _ksub_warning_text(ksub: int) -> str:
1286 """Returns the stderr warning for the experimental RW option ksub (same wording as the dist_m4ri binary)."""
1287 return (
1288 f"# WARNING: ksub={ksub} is experimental and should not be used: a RW step reduces the span of ksub rows\n"
1289 f"# of a fixed basis of ker(H), so that a few codewords are found much more often than others; with\n"
1290 f"# min_hits, RW may end early, and exp(-<n>) underestimates the probability to miss a lighter codeword\n"
1291 )
1292
1293
1294def _warn_experimental_ksub(ksub: Any, method: int, solver: str = "dist_m4ri") -> None:
1295 """
1296 Issues the warning for the experimental RW option ksub > 0 to stderr (with or without min_hits), if RW runs
1297 (method=1 or 3) with the dist_m4ri binary.
1298 """
1299 try:
1300 ival = int(ksub or 0)
1301 except (TypeError, ValueError):
1302 return
1303 if ival > 0 and (int(method) & 1) and solver != "codedistance":
1304 sys.stderr.write(_ksub_warning_text(ival))
1305
1306
1308 """Returns the stderr warning for the expert option ortho=0 (same wording as the dist_m4ri binary)."""
1309 return (
1310 "# WARNING: ortho=0 (expert option): the rows of H and G are not checked for orthogonality\n"
1311 "# (H G^T = 0); the result is valid only if they are orthogonal\n"
1312 )
1313
1314
1315def _warn_ortho(ortho: Any, has_g: bool, solver: str = "dist_m4ri") -> None:
1316 """
1317 Issues the warning for the expert option ortho=False to stderr: the dist_m4ri binary does not check that the rows
1318 of H and G are orthogonal (if has_g: a matrix G is given), or the option has no effect (without G, or with the
1319 codedistance solver).
1320 """
1321 if ortho:
1322 return
1323 if solver == "codedistance":
1324 _warn_ignored("ortho=0 is ignored with solver='codedistance'")
1325 elif has_g:
1326 sys.stderr.write(_ortho_warning_text())
1327 else:
1328 _warn_ignored("ortho=0 has no effect without G (the check H G^T = 0 needs G)")
1329
1330
1331def _check_kwargs(func: Callable[..., Any], kwargs: Dict[str, Any], aliases: Sequence[str] = ("win",)) -> None:
1332 """
1333 Raises TypeError for the keyword arguments in kwargs (the **kwargs of func) other than the accepted aliases
1334 (`win` for kwin), e.g., misspelled or unsupported options, with the closest parameter names as hints.
1335 """
1336 unknown = [k for k in kwargs if k not in aliases]
1337 if not unknown:
1338 return
1339 import difflib
1340 import inspect
1341 names = [n for n, p in inspect.signature(func).parameters.items() if p.kind is not inspect.Parameter.VAR_KEYWORD]
1342 names += list(aliases)
1343 items = []
1344 for k in unknown:
1345 close = difflib.get_close_matches(k, names, n=1)
1346 items.append(f"'{k}'" + (f" (did you mean '{close[0]}'?)" if close else ""))
1347 if len(items) == 1:
1348 raise TypeError(f"{func.__name__}() got an unexpected keyword argument {items[0]}")
1349 raise TypeError(f"{func.__name__}() got unexpected keyword arguments {', '.join(items)}")
1350
1351
1353 start: Any,
1354 trust_start: bool = False,
1355 noscan: Any = 0,
1356 cbeg: Any = None,
1357 cend: Any = None,
1358 solver: str = "dist_m4ri",
1359 start_scan: bool = False
1360) -> Optional[List[int]]:
1361 """
1362 Validates the expert CC options of a compute_*_distance() call and issues the warnings to stderr.
1363
1364 Returns:
1365 The normalized start list (with the flag `scan` for start_scan, see _StartList), or None if unset (or not
1366 applicable to the selected solver).
1367 """
1368 _warn_disabled_options(noscan, cbeg, cend)
1369 start_list = _normalize_start_list(start)
1370 if start_list is None:
1371 if trust_start:
1372 _warn_ignored("trust_start=1 has no effect without a start list")
1373 if start_scan:
1374 _warn_ignored("start_scan=1 has no effect without a start list")
1375 return None
1376 if solver == "codedistance":
1377 _warn_ignored(f"start={_format_start_list(start_list)} is ignored with solver='codedistance'")
1378 return None
1379 start_list = _scan_start_list(start_list, start_scan)
1380 msg = _start_warning_text(start_list)
1381 if _use_distance_cache and trust_start:
1382 msg += "# trust_start=1: start-list results are accepted as valid and copied to the main cache record\n"
1383 elif _use_distance_cache:
1384 msg += (
1385 "# results are cached under a separate ':start=...' key; only dmax and codewords update the\n"
1386 "# main cache record (use trust_start=1 to also accept dmin)\n"
1387 )
1388 sys.stderr.write(msg)
1389 return start_list
1390
1391
1392def _seed_bounds_from_entry(eff_dmin: int, eff_dmax: int, entry: Optional[Dict[str, Any]]) -> Tuple[int, int]:
1393 """
1394 Tightens the bounds (eff_dmin, eff_dmax) with those stored in a cache entry (a CSS code is cached as the
1395 records of its two sectors, see _matrix_cache_key()).
1396 """
1397 if not entry:
1398 return eff_dmin, eff_dmax
1399 c_dmin = entry.get("dmin", 0) or 0
1400 c_dmax = entry.get("dmax", 0) or 0
1401 if c_dmax > 0:
1402 eff_dmax = min(eff_dmax, c_dmax) if eff_dmax > 0 else c_dmax
1403 if c_dmin > 1:
1404 eff_dmin = max(eff_dmin, c_dmin) if eff_dmin > 1 else c_dmin
1405 return eff_dmin, eff_dmax
1406
1407
1408def _union_cws(base: Optional[List[List[int]]], extra: Optional[List[List[int]]]) -> List[List[int]]:
1409 """Returns the union of two codeword lists (duplicates removed, sorted by weight)."""
1410 combined = [list(cw) for cw in (base or [])]
1411 seen = {tuple(cw) for cw in combined}
1412 for cw in (extra or []):
1413 t = tuple(cw)
1414 if t not in seen:
1415 combined.append(list(cw))
1416 seen.add(t)
1417 combined.sort(key=len)
1418 return combined
1419
1420
1421def _dist_from_bounds(dmin: int, dmax: int) -> int:
1422 """The distance reported for the bounds [dmin, dmax]: dmin if exact or without an upper bound, otherwise dmax."""
1423 return dmin if (dmin == dmax or dmax == 0) else dmax
1424
1425
1426def _single_entry_is_exact(entry: Dict[str, Any], need_cws: bool = False, dstop: int = 0) -> bool:
1427 """
1428 Cache hit test for a classical / quantum / DEM entry: the exact distance, or with the stop target dstop > 0 a
1429 lower bound dmin >= dstop (and codewords if needed).
1430 """
1431 c_dmin = entry.get("dmin", 0) or 0
1432 if not ((c_dmin > 0 and c_dmin == entry.get("dmax")) or (dstop > 0 and c_dmin >= dstop)):
1433 return False
1434 return (not need_cws) or bool(entry.get("cws"))
1435
1436
1437def _resolve_cert_engine(solver: str = "dist_m4ri", engine: Optional[str] = None) -> str:
1438 """
1439 Resolves the qec_cert delegation mode ('off', 'auto', or 'cert') from `solver` and `engine`.
1440 Default ('dist_m4ri', engine=None) returns 'off'.
1441 """
1442 if engine is not None:
1443 e = str(engine).strip().lower()
1444 if e in ("", "dist_m4ri", "m4ri", "native", "off", "0", "false", "none"):
1445 return "off"
1446 if e == "auto":
1447 return "auto"
1448 if e in ("cert", "qec_cert", "qec-cert"):
1449 return "cert"
1450 if e == "codedistance":
1451 return "off"
1452 raise ValueError(f"invalid engine={engine!r}: expected 'dist_m4ri', 'auto', or 'cert'")
1453 s = str(solver or "dist_m4ri").strip().lower()
1454 if s in ("dist_m4ri", "codedistance"):
1455 return "off"
1456 if s == "auto":
1457 return "auto"
1458 if s in ("cert", "qec_cert", "qec-cert"):
1459 return "cert"
1460 raise ValueError(f"invalid solver={solver!r}: expected 'dist_m4ri', 'codedistance', 'auto', or 'cert'")
1461
1462
1464 mode: str,
1465 *,
1466 method: int = 3,
1467 do_cws: bool = False,
1468 outC: Optional[str] = None,
1469 finC: Optional[str] = None,
1470 smax: Optional[int] = None,
1471 dW: int = -1,
1472 maxC: int = 0,
1473 start_list: Optional[List[int]] = None,
1474 outD: Optional[Any] = None,
1475 out_reduced: Optional[Any] = None,
1476 pmin: float = 0.0,
1477) -> bool:
1478 """
1479 Whether a distance calculation should be delegated to `qec_cert`.
1480 In 'auto' mode, delegates only when safe (no codeword enumeration, finC, smax, start list, outD, out_reduced,
1481 pmin > 0, or single-sided method 1/2, and `qec_cert` is importable).
1482 """
1483 if mode == "off":
1484 return False
1485 if do_cws or outC:
1486 if mode == "cert":
1487 raise ValueError("Codeword extraction is not supported with engine='cert'; use solver='dist_m4ri'.")
1488 return False
1489 if mode == "auto":
1490 if (
1491 method != 3 or finC or (smax is not None and smax > 0) or dW >= 0 or maxC > 0
1492 or start_list is not None or outD or out_reduced or pmin > 0.0
1493 ):
1494 return False
1495 return _has_qec_cert()
1497 if outD or out_reduced:
1498 _warn_ignored("outD and out_reduced are only used with solver='dist_m4ri'; ignored")
1499 return True
1500
1501
1502def _cert_to_csr(M: Any, qec_io: Any) -> Any:
1503 """Converts a matrix (file path, numpy array, or scipy sparse matrix) to a binary CSR matrix for qec_cert."""
1504 if M is None:
1505 return None
1506 if isinstance(M, (str, Path)):
1507 return qec_io.read_mtx(str(M))
1508 return qec_io.binary_csr(M)
1509
1510
1511def _cert_is_conditional(cert: Any) -> bool:
1512 """Whether a qec_cert Certificate (or dict) has a conditional lower bound (e.g., unverified symmetry)."""
1513 get = cert.get if isinstance(cert, dict) else lambda k, d=None: getattr(cert, k, d)
1514 if get("status") == "conditional" or bool(get("lb_conditional", False)):
1515 return True
1516 for s in get("stages", ()) or ():
1517 if isinstance(s, dict) and s.get("lb_conditional"):
1518 return True
1519 for r in get("reductions", ()) or ():
1520 if isinstance(r, dict) and (r.get("conditional") or (r.get("params") or {}).get("lb_conditional")):
1521 return True
1522 return False
1523
1524
1525def _cert_extract_bounds(cert: Any, certified_dmin: int = 1) -> Tuple[int, int]:
1526 """
1527 Extracts unconditional (dmin, dmax) from a qec_cert Certificate for dist_m4ri reporting and caching:
1528 - An exact result (dmin = dmax = d) only if status == 'exact', witness_verified is True, and not conditional.
1529 - For bounds, an upper bound dmax only if witness_verified is True, and a lower bound dmin only when
1530 status in ('exact', 'bounds', 'timeout') and not lb_conditional.
1531 """
1532 get = cert.get if isinstance(cert, dict) else lambda k, d=None: getattr(cert, k, d)
1533 status = str(get("status", "") or "")
1534 wit_ver = get("witness_verified") is True
1535 is_cond = _cert_is_conditional(cert)
1536 raw_lb = int(get("lower_bound") or 0)
1537 raw_ub = int(get("upper_bound") or 0)
1538 if is_cond and certified_dmin > 1 and _cert_wmin_discharged(cert, certified_dmin):
1539 is_cond = False
1540 if status == "exact" and wit_ver and not is_cond and raw_ub > 0 and raw_lb == raw_ub:
1541 return raw_ub, raw_ub
1542 c_ub = raw_ub if (wit_ver and raw_ub > 0) else 0
1543 c_lb = raw_lb if (status in ("exact", "bounds", "timeout") and not is_cond and raw_lb > 1) else 0
1544 if c_ub > 0 and c_lb > c_ub:
1545 c_lb = c_ub
1546 return c_lb, c_ub
1547
1548
1549def _cert_wmin_discharged(cert: Any, certified_dmin: int) -> bool:
1550 """
1551 True iff the certificate's conditionality is caused solely by an assumed lower bound
1552 lb_assumed_wmin <= certified_dmin (already certified in _distance_cache), with no trust
1553 flags, no precondition violation, and no other conditional stage or reduction.
1554 """
1555 if certified_dmin <= 1:
1556 return False
1557 get = cert.get if isinstance(cert, dict) else lambda k, d=None: getattr(cert, k, d)
1558 if get("status") == "conditional" or bool(get("precondition_violated", False)):
1559 return False
1560 if bool(get("trust_sym", False)) or bool(get("trust_quot", False)):
1561 return False
1562 inst = get("instance", None)
1563 inst_d = inst if isinstance(inst, dict) else {}
1564 if (
1565 bool(inst_d.get("trust_sym", False))
1566 or bool(inst_d.get("trust_quot", False))
1567 or bool(inst_d.get("precondition_violated", False))
1568 ):
1569 return False
1570 assumed_wmin = int(get("lb_assumed_wmin", None) or inst_d.get("lb_assumed_wmin", 0) or 0)
1571 if not (1 < assumed_wmin <= certified_dmin):
1572 return False
1573 sectors = inst_d.get("sectors")
1574 if isinstance(sectors, dict):
1575 for sec in sectors.values():
1576 if _cert_is_conditional(sec) and not _cert_wmin_discharged(sec, certified_dmin):
1577 return False
1578 stages = get("stages", ()) or ()
1579 pinned_ok = any(
1580 isinstance(s, dict) and s.get("name") == "polymer_pinning" and s.get("shift_verified") is True
1581 for s in stages
1582 )
1583 for s in stages:
1584 if isinstance(s, dict) and s.get("lb_conditional"):
1585 sname = str(s.get("name", "") or "")
1586 if bool(s.get("trust_sym", False)) or bool(s.get("trust_quot", False)):
1587 return False
1588 if sname == "search":
1589 sym_ok = bool(s.get("sym_verified", True)) or (
1590 s.get("sym_kind") == "pinned" and pinned_ok
1591 )
1592 if not sym_ok:
1593 return False
1594 elif sname != "p1_projection_lb" and not sname.startswith("fold_mod_"):
1595 return False
1596 reductions = get("reductions", ()) or ()
1597 pinned_red_ok = pinned_ok or any(
1598 isinstance(r, dict)
1599 and r.get("name") == "polymer_pinning"
1600 and (r.get("params") or {}).get("shift_verified") is True
1601 for r in reductions
1602 )
1603 for r in reductions:
1604 if not isinstance(r, dict):
1605 continue
1606 rp = r.get("params") or {}
1607 if r.get("conditional"):
1608 return False
1609 if rp.get("lb_conditional"):
1610 rname = str(r.get("name", "") or "")
1611 if rname == "cc_mitm_search":
1612 sym_ok = bool(rp.get("sym_verified", True)) or (
1613 rp.get("sym_kind") == "pinned" and pinned_red_ok
1614 )
1615 wmin_r = int(rp.get("wmin", 0) or 0)
1616 if not sym_ok or (wmin_r > certified_dmin):
1617 return False
1618 elif rname not in ("p1_projection_lb", "capped_spacetime_folding"):
1619 return False
1620 return True
1621
1622
1624 key_fn: Callable[[], str],
1625 run_cert_fn: Callable[[int, int], Any],
1626 *,
1627 label: str = "code",
1628 dmin: int = 0,
1629 dmax: int = 0,
1630 dstop: int = 0,
1631 wmax: int = 0,
1632 eff_cache_file: Optional[str] = None,
1633 debug: int = 0,
1634 verbose: bool = False,
1635) -> Dict[str, Any]:
1636 """
1637 Runs a qec_cert certification callback with dist_m4ri cache lookup and safe cache write-back.
1638 """
1639 global _distance_cache
1640 code_key: Optional[str] = None
1641 entry: Optional[Dict[str, Any]] = None
1642 eff_dmin, eff_dmax = dmin, dmax
1643 if _use_distance_cache:
1644 if eff_cache_file:
1645 load_distance_cache(eff_cache_file)
1646 code_key = key_fn()
1647 entry = _distance_cache.get(code_key)
1648 if entry is not None and _single_entry_is_exact(entry, need_cws=False, dstop=dstop):
1649 if verbose or (debug & PY_DBG_CACHE):
1650 c_dmin = entry.get("dmin", 0) or 0
1651 found = ("exact distance" if c_dmin == entry.get("dmax") else f"dmin={c_dmin} >= dstop={dstop}")
1652 print(f"[dist_m4ri] Cache hit for {label} distance ({found} known)!")
1653 return {
1654 "dist": entry["dist"], "dmin": entry["dmin"], "dmax": entry["dmax"],
1655 "rw_steps": entry.get("rw_steps", 0), "d_info": list(entry["d_info"]),
1656 "cws": list(entry.get("cws", [])), "cached": True,
1657 }
1658 eff_dmin, eff_dmax = _seed_bounds_from_entry(eff_dmin, eff_dmax, entry)
1659 w_lo = max(1, eff_dmin)
1660 w_hi = wmax if wmax > 0 else 40
1661 if eff_dmax > 0:
1662 w_hi = min(w_hi, max(1, eff_dmax - 1)) if wmax > 0 else max(1, eff_dmax - 1)
1663 if dstop > 0:
1664 w_hi = min(w_hi, max(1, dstop - 1)) if (wmax > 0 or eff_dmax > 0) else max(1, dstop - 1)
1665 cert = run_cert_fn(w_lo, max(w_lo, w_hi))
1666 c_lb, c_ub = _cert_extract_bounds(cert, (entry.get("dmin", 0) or 0) if entry else 0)
1667 p_dmin = (entry.get("dmin", 0) or 0) if entry else 0
1668 p_dmax = (entry.get("dmax", 0) or 0) if entry else 0
1669 p_rw = (entry.get("rw_steps", 0) or 0) if entry else 0
1670 p_cws = list(entry.get("cws", [])) if entry else []
1671 cache_dmin = max(p_dmin, c_lb)
1672 cache_dmax = min(p_dmax, c_ub) if (p_dmax > 0 and c_ub > 0) else max(p_dmax, c_ub)
1673 if cache_dmax > 0 and cache_dmin > cache_dmax:
1674 cache_dmin = cache_dmax
1675 if _use_distance_cache and code_key is not None and (c_lb > 1 or c_ub > 0):
1676 record = _sector_record(cache_dmin, cache_dmax, p_rw, p_cws)
1677 if entry is not None and "dmin_model" in entry:
1678 record["dmin_model"] = entry["dmin_model"]
1679 if record != entry:
1680 _distance_cache[code_key] = record
1681 if eff_cache_file:
1682 save_distance_cache(eff_cache_file)
1683 dmin_res = max(eff_dmin, cache_dmin)
1684 pos_ub = [x for x in (eff_dmax, cache_dmax) if x > 0]
1685 dmax_res = min(pos_ub) if pos_ub else 0
1686 if dmax_res > 0 and dmin_res > dmax_res:
1687 dmin_res = dmax_res
1688 dist = _dist_from_bounds(dmin_res, dmax_res)
1689 return {
1690 "dist": dist, "dmin": dmin_res, "dmax": dmax_res, "rw_steps": p_rw,
1691 "d_info": format_bounds_list(dmin_res, dmax_res, p_rw), "cws": p_cws, "cached": False,
1692 }
1693
1694
1696 plain_key: str,
1697 src: Dict[str, Any],
1698 trust: bool = False,
1699 rw_delta: Optional[Dict[str, int]] = None
1700) -> bool:
1701 """
1702 Merges the start-list cache record `src` (key plain_key + ':start=...') into the main record `plain_key`.
1703
1704 The upper bound dmax (minimum of the positive values) and the codewords (union) are always valid and
1705 are always merged. Only with `trust` (expert option trust_start) the lower bound dmin is merged as well
1706 (maximum, capped at dmax), and rw_steps are either incremented by the steps of the run, `rw_delta`
1707 (after a calculation), or set to the maximum of the two records (copy of a cached record, rw_delta=None).
1708 (A CSS code is cached as the records of its two sectors, see _matrix_cache_key().)
1709
1710 Args:
1711 plain_key: Main cache key.
1712 src: Start-list cache record.
1713 trust: Accept the start-list lower bound as valid.
1714 rw_delta: RW steps of the run, {'': steps}.
1715
1716 Returns:
1717 True if the main record was created or modified.
1718 """
1719 global _distance_cache
1720 prev = _distance_cache.get(plain_key)
1721 new: Dict[str, Any] = dict(prev) if prev else {}
1722 s_dmin = src.get("dmin", 0) or 0
1723 s_dmax = src.get("dmax", 0) or 0
1724 p_dmin = new.get("dmin", 0) or 0
1725 p_dmax = new.get("dmax", 0) or 0
1726 p_rw = new.get("rw_steps", 0) or 0
1727 dmax = min(p_dmax, s_dmax) if (p_dmax > 0 and s_dmax > 0) else (p_dmax if p_dmax > 0 else s_dmax)
1728 dmin, rw = p_dmin, p_rw
1729 if trust:
1730 dmin = max(p_dmin, s_dmin)
1731 if rw_delta is not None:
1732 rw = p_rw + (rw_delta.get("", 0) or 0)
1733 else:
1734 rw = max(p_rw, src.get("rw_steps", 0) or 0)
1735 if dmax > 0 and dmin > dmax:
1736 dmin = dmax
1737 s_cws = src.get("cws", []) or []
1738 if not prev and not (trust or s_dmax > 0 or s_cws):
1739 return False
1740 new["cws"] = _union_cws(new.get("cws", []), s_cws)
1741 new["dist"] = _dist_from_bounds(dmin, dmax)
1742 new["dmin"], new["dmax"], new["rw_steps"] = dmin, dmax, rw
1743 new["d_info"] = format_bounds_list(dmin, dmax, rw)
1744 _distance_cache[plain_key] = new
1745 return True
1746
1747
1749 plain_key: str,
1750 start_list: Optional[List[int]],
1751 is_hit: Callable[[Dict[str, Any]], bool],
1752 trust_start: bool,
1753 merge_copy: Callable[[Dict[str, Any]], bool],
1754 eff_cache_file: Optional[str],
1755 verbose: bool = False
1756) -> Tuple[str, Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
1757 """
1758 Resolves the cache record of a run with an optional expert CC start list.
1759
1760 Without a start list, or if the main record already holds the requested exact result, the main record
1761 is used. Otherwise the separate start-list record (key plain_key + ':start=a,b,c') is used; with
1762 trust_start, an existing exact start-list record is copied to the main record (via `merge_copy`), and
1763 the cache file is saved, even though no calculation is needed.
1764
1765 Returns:
1766 (code_key, cached_entry, plain_entry): the key and the existing record (or None) for the results of
1767 this run, and the main record (or None) used to seed the bounds of a start-list run.
1768 """
1769 plain_entry = _distance_cache.get(plain_key)
1770 if not start_list:
1771 return plain_key, plain_entry, None
1772 if plain_entry is not None and is_hit(plain_entry):
1773 if verbose:
1774 print(f"[dist_m4ri] Start list not needed: the main record '{plain_key}' has the exact distance")
1775 return plain_key, plain_entry, None
1776 code_key = plain_key + _start_key_suffix(start_list)
1777 cached_entry = _distance_cache.get(code_key)
1778 if trust_start and cached_entry is not None and is_hit(cached_entry):
1779 if merge_copy(cached_entry) and eff_cache_file:
1780 save_distance_cache(eff_cache_file)
1781 if verbose:
1782 print(f"[dist_m4ri] trust_start: copied the exact start-list record to the main record '{plain_key}'")
1783 return code_key, cached_entry, plain_entry
1784
1785
1787 dist_m4ri_path: Optional[str] = None,
1788 method: int = 3,
1789 finH: Optional[str] = None,
1790 finG: Optional[str] = None,
1791 finL: Optional[str] = None,
1792 fin: Optional[str] = None,
1793 finC: Optional[str] = None,
1794 fdem: Optional[str] = None,
1795 dmin: int = 0,
1796 dmax: int = 0,
1797 wmax: int = 0,
1798 wmin: int = 1,
1799 dexp: int = 0,
1800 dest: int = 0,
1801 dstop: int = 0,
1802 steps: Optional[int] = None,
1803 threads: Optional[int] = None,
1804 timeout: float = 60.0,
1805 smax: Optional[int] = None,
1806 start: Optional[Union[int, str, Sequence[int]]] = None,
1807 cbeg: Optional[int] = None,
1808 cend: Optional[int] = None,
1809 css: Optional[int] = None,
1810 noscan: int = 0,
1811 classical: int = -1,
1812 dW: int = -1,
1813 maxC: int = 0,
1814 pmin: float = 0.0,
1815 merge: bool = True,
1816 drop: Optional[str] = None,
1817 outD: Optional[str] = None,
1818 sort_rows: Optional[bool] = None,
1819 cc_prune: Optional[int] = None,
1820 outC: Optional[str] = None,
1821 seed: int = 0,
1822 debug: int = 0,
1823 nothrottle: bool = False,
1824 chunk_size: int = 0,
1825 ksub: int = 0,
1826 kwin: int = 0,
1827 win_mode: int = 0,
1828 win_hyb: int = 0,
1829 pairs: int = -1,
1830 min_hits: Optional[int] = None,
1831 cov_cws: int = 100,
1832 refresh: Optional[int] = None,
1833 verbose: bool = False,
1834 stop_event: Optional[threading.Event] = None,
1835 warn_start: bool = True,
1836 warn_ksub: bool = True,
1837 start_scan: bool = False,
1838 ortho: bool = True,
1839 warn_ortho: bool = True
1840) -> Tuple[int, int, int]:
1841 """
1842 Low-level invocation of the multithreaded dist_m4ri binary.
1843
1844 DEM input:
1845 merge: If True (default), the binary merges the equal columns of the DEM fdem (error mechanisms with the
1846 same detectors and observables). Exact: dropping two equal columns of a codeword gives a lighter one,
1847 so the distance does not change. Column indices (finC, outC, start) are those of the DEM; the codewords
1848 found use the first one of each set of equal columns. merge=False passes merge=0 (all columns are
1849 kept, e.g., to enumerate the codewords with each of them). Ignored without fdem.
1850 drop: The detectors of the DEM fdem to drop if this is verified to be exact, passed as drop=...: None (the
1851 binary's default, drop=auto: a search for the largest such set in the DEM), "auto" or "1" (the same), "0"
1852 (keep all detectors), or a file with the detectors to drop (0-based; otherwise, the binary uses the full DEM
1853 and prints a note, see _parse_stderr_stats()). Ignored without fdem.
1854 outD: File to which the binary writes the detectors dropped from the DEM fdem (0-based; only the comment line
1855 '# N of M detectors dropped ...' if none, see read_drop_file()). Ignored without fdem.
1856
1857 Orthogonality check:
1858 ortho: If True (default), the binary checks that the rows of H and G (finG, or fin without finL) are
1859 orthogonal (H G^T = 0) and stops with an error otherwise. False (expert option, since version 0.15.2)
1860 passes ortho=0: the check is skipped, e.g., to screen many codes which are orthogonal by construction; the
1861 result is then valid only if H G^T = 0. No effect without G. The cache key does not depend on it.
1862 warn_ortho: If True (default), issue the warning for ortho=False to stderr (see _warn_ortho()).
1863
1864 Row order:
1865 sort_rows: Passed as sort_rows=1 (True) or sort_rows=0 (False); None (default): the binary's default, i.e.,
1866 1 for matrices and 0 for a DEM. With sort_rows=1, the binary sorts the rows of H by increasing weight
1867 (stable). With cc_prune=0, this reduces the CC work for matrices with very different row weights (e.g.,
1868 1.7-4x with the hook columns of an extended CSS distance), while the detectors of a DEM are in time
1869 order, which is better for CC (use sort_rows=False also for matrices exported from a DEM); with the
1870 default cc_prune=1, the order of the rows only breaks ties. The distance and RW do not depend on the
1871 order of the rows; CC enumerates the same codewords, possibly in another order (with cc_prune=0, also with
1872 other hit counts).
1873
1874 CC search:
1875 cc_prune: Passed as cc_prune=N if not None (0, 1, or 2; None, the default: the binary's default 1). The CC
1876 search with smax=0: 0, the walk of the versions before 0.15.0 (a cluster is extended by the columns of the
1877 first unsatisfied row of its syndrome); 1, the branch and bound (the unsatisfied row with the fewest
1878 available columns, the columns of the earlier branches forbidden in the later ones, so that each codeword
1879 is found once, and pruning by lower bounds on the number of columns needed: an unsatisfied row without
1880 available columns, a packing of unsatisfied rows with disjoint sets of available columns); 2, also the
1881 coverage and fractional cover bounds. The same codewords and distances (doc/cc_completeness.tex); with
1882 the expert start list (and start_scan=False), cc_prune=1 and 2 forbid the columns listed before the start
1883 column. The cache key does not depend on it.
1884
1885 RW windows:
1886 kwin / win_mode: A window of kwin columns in every RW step (win_mode=0: a Tanner graph ball, 1: an index
1887 window). kwin=0 (default): the automatic hybrid for n >= 500, an index window of win_hyb columns in every
1888 other step and uniform random permutations in the others. kwin=-1 (any negative value): no windows,
1889 uniform random permutations in every step.
1890 win_hyb: Window size of the hybrid RW steps, passed as win_hyb=N if nonzero; 0 (default): 256 columns for a
1891 DEM, 512 for matrices (at most 3n/4). The windows of 192-256 columns find the minimum-weight codewords of
1892 circuit DEMs 2-10 times faster than 512, and windows of one round of columns are much slower. With
1893 debug & 1 (or verbose=True), the binary prints the rounds of a DEM (estimated from the time translation of
1894 its errors: D detectors and L columns per round, also in _last_run_stats["rounds"]) and the codewords found
1895 by the uniform and by the window steps (see _parse_stderr_stats()).
1896
1897 RW pairs:
1898 pairs: The RW pairs of non-pivot columns (p=2, as in Lee-Brickell and Stern), passed as pairs=N if not -1: a
1899 RW step also finds the codewords with two non-pivot columns, checking only the pairs which agree on N
1900 random pivot rows (0: off). -1 (default): auto, about one non-pivot column per bucket, with fewer rows
1901 while a codeword of the weight of the upper bound would be kept with probability below 0.8, but enough
1902 rows to keep the pairs of a step within about the work of its elimination (e.g., at least 6 for large
1903 circuit DEMs while the upper bound is far above the distance). The pairs find the minimum-weight
1904 codewords of circuit DEMs 7-26 times faster (per wall time) and lighter codewords of codes with a large
1905 distance. With debug & 1, the binary prints the pairs checked per step and the codewords found by pairs
1906 and by single columns (also in _last_run_stats). The pairs of disjoint codewords are skipped (their sum
1907 is reducible: the binary records only irreducible codewords, its default irred=1).
1908
1909 RW stopping criterion:
1910 min_hits: Passed as min_hits=N if not None and >= 0 (None, the default, or a negative value: the binary's
1911 default 5): RW ends once the average number of hits <n> per codeword reaches N, both for the codewords
1912 of the minimum weight found and for a representative set of cov_cws lowest-weight codewords (the hits of
1913 all RW steps, with or without windows, and those of CC and finC count). 0: off; then, unless the
1914 codewords are collected (outC or maxC), RW records only the codewords lighter than the upper bound, so
1915 that the repeated hits are not counted.
1916 cov_cws: Size of the representative set of lowest-weight codewords for min_hits, passed as cov_cws=N if not
1917 100 (the default, as in the binary; 0: only the minimum weight).
1918
1919 Stop target:
1920 dstop: The run (method=2 or 3) ends once CC has certified dmin >= dstop, i.e., that there is no codeword of
1921 weight < dstop, unless a lighter codeword is found (default: 0 = off). Unlike dmax, dstop is not an
1922 upper bound and is not reported as dmax: the result is (dmin, w, rw_steps), where w is the weight of the
1923 lightest codeword found (0: none).
1924
1925 Expert CC options:
1926 start: List of CC start columns (int, comma-separated string "a,b,c", or sequence of ints;
1927 None or a single negative value: all columns), passed as start=a,b,c (sorted, distinct).
1928 CC clusters are grown only from the listed columns, and they are not limited to larger
1929 column indices. WARNING: the returned dmin (and an exact result) is valid only if a code
1930 symmetry maps every minimum-weight codeword to one containing a listed column (e.g., one
1931 column per block of a quasi-cyclic code); dmax is always valid.
1932 start_scan: With start (expert option, since version 0.15.1): passed as start_scan=1, so that a cluster
1933 grown from a listed column c only uses the columns > c, as in the scan of all columns (not with fdem).
1934 WARNING: dmin is then valid only if a code symmetry maps every minimum-weight codeword to one whose
1935 smallest column is listed, e.g., if the listed columns are the smallest columns of the orbits of a
1936 group of code automorphisms, one per orbit (start=[0, l] for the two blocks of a two-block
1937 group-algebra code of length 2l over an abelian group). The later start columns then need much less
1938 work, the least if each orbit is a range of consecutive columns.
1939 warn_start: If True (default), issue the start-list warning to stderr.
1940 noscan / cbeg / cend: Disabled in the Python interface: accepted for backward compatibility,
1941 but ignored with a warning to stderr (use the dist_m4ri binary directly).
1942
1943 Experimental RW options:
1944 ksub: Experimental, should not be used: subspace dimension sampled from ker(H) in RW (0: full-matrix RW).
1945 A RW step reduces the span of ksub rows of a fixed basis of ker(H), so that a few codewords are found
1946 much more often than others (min_hits may end RW early).
1947 refresh: RW steps between the basis refreshes of the experimental ksub RW (passed if not None; None, the
1948 default: the binary's 5000 with ksub > 0; 0: no refresh).
1949 warn_ksub: If True (default), issue the warning for ksub > 0 to stderr (with method=1 or 3).
1950
1951 Debug output:
1952 debug: Debug bitmap; the bits in BIN_DBG_MASK are passed to the binary (its diagnostic output on stderr
1953 is printed), and PY_DBG_COMMANDS echoes the command line and the run time. verbose=True implies
1954 the bits VERBOSE_DEBUG.
1955
1956 Cancellation:
1957 stop_event: Optional threading.Event; once it is set, the binary is terminated and RuntimeError is raised
1958 (the binary is also terminated on any other exception, e.g., KeyboardInterrupt).
1959
1960 Returns:
1961 tuple (dmin, dmax, rw_steps)
1962 """
1963 global _last_run_stats
1964 exec_path = find_dist_m4ri_binary(dist_m4ri_path)
1965
1966 start_list = _normalize_start_list(start)
1967 _warn_disabled_options(noscan, cbeg, cend)
1968 if start_scan and not start_list:
1969 _warn_ignored("start_scan=1 has no effect without a start list")
1970 if start_scan and start_list and fdem:
1971 raise ValueError("start_scan=1 is not available with fdem (the equal columns of a DEM are merged)")
1972 start_list = _scan_start_list(start_list, start_scan)
1973 if start_list and warn_start:
1974 sys.stderr.write(_start_warning_text(start_list))
1975 if warn_ksub:
1976 _warn_experimental_ksub(ksub, method)
1977 has_g = not fdem and bool(finG or (fin and not finL)) # (fin=PREFIX: finG=PREFIX + 'Z.mtx' unless finL)
1978 if warn_ortho:
1979 _warn_ortho(ortho, has_g)
1980
1981 finC = check_finc_outc(finC, outC, verbose=False)
1982
1983 if cc_prune is not None and cc_prune not in (0, 1, 2):
1984 raise ValueError(f"invalid cc_prune={cc_prune!r}: cc_prune should be 0, 1, or 2")
1985
1986 if method == 2 and wmax <= 0:
1987 if dmax > 0:
1988 wmax = dmax
1989 elif timeout <= 0.0 and dstop <= 0:
1990 raise ValueError(
1991 "either parameter wmax>0, dmax>0, dstop>0, or timeout>0 should be specified for CC method=2."
1992 )
1993
1994 eff_debug = (debug | VERBOSE_DEBUG) if verbose else debug
1995 bin_debug = eff_debug & BIN_DBG_MASK # the higher bits are used by the Python wrapper only
1996 cmd = [exec_path, f"debug={bin_debug}", f"method={method}"]
1997
1998 if finH: cmd.append(f"finH={finH}")
1999 if finG: cmd.append(f"finG={finG}")
2000 if finL: cmd.append(f"finL={finL}")
2001 if fin: cmd.append(f"fin={fin}")
2002 if has_g and not ortho: cmd.append("ortho=0")
2003 if finC: cmd.append(f"finC={finC}")
2004 if fdem: cmd.append(f"fdem={fdem}")
2005 if dmin > 0: cmd.append(f"dmin={dmin}")
2006 if dmax > 0: cmd.append(f"dmax={dmax}")
2007 if dstop > 0: cmd.append(f"dstop={dstop}")
2008 if wmax > 0: cmd.append(f"wmax={wmax}")
2009 if wmin is not None and wmin != 1: cmd.append(f"wmin={wmin}")
2010 if dexp > 0: cmd.append(f"dexp={dexp}")
2011 elif dest > 0: cmd.append(f"dest={dest}")
2012 if steps is not None and steps >= 0: cmd.append(f"steps={steps}")
2013 if threads is not None and threads > 0: cmd.append(f"threads={threads}")
2014 if timeout is not None and timeout >= 0: cmd.append(f"timeout={timeout}")
2015 if smax is not None: cmd.append(f"smax={smax}")
2016 if start_list: cmd.append("start=" + ",".join(str(c) for c in start_list))
2017 if start_list and start_scan: cmd.append("start_scan=1")
2018 if css is not None: cmd.append(f"css={css}")
2019 if classical >= 0: cmd.append(f"classical={classical}")
2020 if dW >= 0: cmd.append(f"dW={dW}")
2021 if maxC > 0: cmd.append(f"maxC={maxC}")
2022 if pmin > 0.0: cmd.append(f"pmin={pmin}")
2023 if fdem and not merge: cmd.append("merge=0")
2024 if fdem and drop: cmd.append(f"drop={drop}")
2025 if fdem and outD: cmd.append(f"outD={outD}")
2026 if sort_rows is not None: cmd.append(f"sort_rows={1 if sort_rows else 0}")
2027 if cc_prune is not None: cmd.append(f"cc_prune={int(cc_prune)}")
2028 if outC: cmd.append(f"outC={outC}")
2029 if seed != 0: cmd.append(f"seed={seed}")
2030 if nothrottle: cmd.append("nothrottle=1")
2031 if chunk_size > 0: cmd.append(f"chunk_size={chunk_size}")
2032 if ksub > 0: cmd.append(f"ksub={ksub}")
2033 if kwin != 0: cmd.append(f"kwin={kwin}") # (kwin < 0: no windows)
2034 if win_mode != 0: cmd.append(f"win_mode={win_mode}")
2035 if win_hyb != 0: cmd.append(f"win_hyb={win_hyb}")
2036 if pairs != -1: cmd.append(f"pairs={pairs}")
2037 if min_hits is not None and min_hits >= 0: cmd.append(f"min_hits={min_hits}")
2038 if cov_cws != 100: cmd.append(f"cov_cws={cov_cws}")
2039 if refresh is not None: cmd.append(f"refresh={refresh}")
2040
2041 if eff_debug & PY_DBG_COMMANDS:
2042 print(f"[dist_m4ri] Running: {' '.join(cmd)}")
2043
2044 t_start = time.time()
2045 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
2046 try:
2047 if stop_event is None:
2048 stdout, stderr = proc.communicate()
2049 else:
2050 while True: # communicate() with a timeout keeps draining the pipes (no deadlock on a full pipe)
2051 try:
2052 stdout, stderr = proc.communicate(timeout=0.05)
2053 break
2054 except subprocess.TimeoutExpired:
2055 if stop_event.is_set():
2056 raise RuntimeError("dist_m4ri execution cancelled by stop_event")
2057 except BaseException: # cancelled, KeyboardInterrupt, ...: do not leave the child process running
2058 try:
2059 proc.terminate()
2060 proc.communicate(timeout=1.0)
2061 except Exception:
2062 try:
2063 proc.kill()
2064 proc.wait()
2065 except Exception:
2066 pass
2067 raise
2068
2069 if eff_debug & PY_DBG_COMMANDS:
2070 print(f"[dist_m4ri] Finished in {time.time() - t_start:.3f} s (exit code {proc.returncode})")
2071
2072 if proc.returncode != 0:
2073 raise RuntimeError(f"dist_m4ri failed with exit code {proc.returncode}:\n{stderr}")
2074
2075 _last_run_stats = _parse_stderr_stats(stderr)
2076 if (verbose or bin_debug > 0) and stderr:
2077 print(stderr.rstrip())
2078
2079 return parse_dist_m4ri_output(stdout)
2080
2081
2082_MTX_PY_MAX_NNZ = 1 << 16 # temporary MatrixMarket files with up to this many entries are formatted in Python
2083
2084
2085def _gf2_entries(matrix: Any) -> Tuple[int, int, Any, Any]:
2086 """
2087 The shape (m, n) and the nonzero entries (0-based row and column index arrays, in row-major order) of a binary
2088 matrix given as a numpy array (or an array-like, e.g., a list of lists) or a scipy sparse matrix (or array): an
2089 entry is nonzero iff its value is odd (floats are truncated to integers, and the duplicate entries of a sparse
2090 matrix are summed first), as the dist_m4ri binary reads the integer entries of a MatrixMarket file.
2091 """
2092 import numpy as np
2093
2094 def odd(v: Any) -> Any: # the mask of the odd entries
2095 if v.dtype == np.bool_:
2096 return v
2097 if not np.issubdtype(v.dtype, np.integer):
2098 v = v.astype(np.int64)
2099 v = v & 1
2100 return v.view(np.bool_) if v.itemsize == 1 else v != 0
2101
2102 if hasattr(matrix, "tocsr"): # a scipy sparse matrix or array
2103 csr = matrix.tocsr()
2104 if not csr.has_canonical_format: # duplicates summed and columns sorted (not in the caller's matrix)
2105 csr = csr.copy() if csr is matrix else csr
2106 csr.sum_duplicates()
2107 m, n = csr.shape
2108 keep = odd(csr.data)
2109 rows = np.repeat(np.arange(m, dtype=np.int64), np.diff(csr.indptr))[keep]
2110 return m, n, rows, csr.indices[keep]
2111 A = np.asarray(matrix)
2112 if A.ndim != 2:
2113 raise ValueError(f"a matrix should have two dimensions, got the shape {A.shape}")
2114 m, n = A.shape
2115 if m == 0 or n == 0:
2116 empty = np.zeros(0, dtype=np.int64)
2117 return m, n, empty, empty
2118 rows, cols = np.divmod(np.flatnonzero(odd(A)), n) # (flatnonzero of a bool array is fast, unlike nonzero)
2119 return m, n, rows, cols
2120
2121
2122def _write_mtx_gf2(path: str, matrix: Any) -> None:
2123 """
2124 Writes a binary matrix (numpy array, array-like, or scipy sparse matrix; see _gf2_entries()) to the file `path` in
2125 the MatrixMarket coordinate pattern format (the 1-based indices of the nonzero entries, in row-major order), which
2126 the dist_m4ri binary reads. With up to _MTX_PY_MAX_NNZ nonzero entries, the file is formatted in Python (e.g.,
2127 about 1 ms for 6000 entries); larger matrices are written by scipy.io.mmwrite, which is faster per entry, but has a
2128 fixed cost of about 10 ms per call on a 128-core machine (the thread pool of its parallel writer).
2129 """
2130 import numpy as np
2131 m, n, rows, cols = _gf2_entries(matrix)
2132 nnz = len(rows)
2133 if nnz > _MTX_PY_MAX_NNZ:
2134 from scipy.io import mmwrite
2135 from scipy.sparse import coo_matrix
2136 mmwrite(path, coo_matrix((np.ones(nnz, dtype=np.int8), (rows, cols)), shape=(m, n)), field="pattern",
2137 symmetry="general")
2138 return
2139 rc = np.empty(2 * nnz, dtype=np.int64) # the interleaved 1-based row and column indices
2140 rc[0::2], rc[1::2] = rows, cols
2141 rc += 1
2142 with open(path, "w") as f:
2143 f.write(f"%%MatrixMarket matrix coordinate pattern general\n{m} {n} {nnz}\n")
2144 f.write(("%d %d\n" * nnz) % tuple(rc.tolist()))
2145
2146
2147def _matrix_to_file(matrix, extension: str = ".mtx", temp_dir: str = "tmp") -> str:
2148 """
2149 The MatrixMarket file of a matrix for the dist_m4ri binary: a file path is returned as is; a numpy array (or an
2150 array-like) or a scipy sparse matrix is written to a new temporary file in temp_dir with _write_mtx_gf2() (its
2151 nonzero entries over GF(2), i.e., those with an odd value; before version 0.15.2, scipy.io.mmwrite wrote the int8
2152 entries, which took 10-13 ms per matrix with up to 1000 columns on a 128-core machine).
2153 """
2154 if isinstance(matrix, (str, Path)):
2155 return str(matrix)
2156 path = create_unique_file(directory=temp_dir, extension=extension)
2157 _write_mtx_gf2(path, matrix)
2158 return path
2159
2160
2161def has_noise(circuit: Any) -> bool:
2162 """
2163 Checks if a Stim circuit has noise: a noise channel (e.g., DEPOLARIZE1, X_ERROR, PAULI_CHANNEL_2, E, or
2164 HERALDED_ERASE), or a measurement with a flip probability (e.g., M(0.001) or MPP(0.01)), i.e., if strip_noise()
2165 changes it.
2166 """
2167 try:
2168 stim = _get_stim()
2169 except ImportError:
2170 return False
2171 if not isinstance(circuit, stim.Circuit):
2172 return False
2173 return circuit.without_noise() != circuit
2174
2175
2176def strip_noise(circuit: Any) -> Any:
2177 """
2178 Removes all noise from a Stim circuit (stim.Circuit or a .stim file), see stim.Circuit.without_noise(): the noise
2179 channels (e.g., DEPOLARIZE1, X_ERROR, PAULI_CHANNEL_2, E) are removed, the measurements lose their flip
2180 probabilities (M(0.001) becomes M), and heralded noise (e.g., HERALDED_ERASE) is replaced by MPAD, so that the
2181 measurement record does not change. E.g., add_noise(strip_noise(circuit), p) (or add_noise(circuit, p,
2182 force=True); CLI: --force-noise) gives circuits with the same noise model.
2183
2184 Returns:
2185 The noiseless stim.Circuit.
2186 """
2187 stim = _get_stim()
2188 if isinstance(circuit, (str, Path)):
2189 circuit = stim.Circuit.from_file(str(circuit))
2190 return circuit.without_noise()
2191
2192
2194 circuit: Any, p: float, num_qubits: int, active_qubits: Set[int]
2195) -> Any:
2196 """Recursively adds uniform circuit-level noise to a Stim circuit (see add_noise())."""
2197 stim = _get_stim()
2198 p1 = 0.1 * p # one-qubit depolarizing noise (after one-qubit gates, on idle qubits)
2199 noisy_circuit = stim.Circuit()
2200 for inst in circuit:
2201 if isinstance(inst, stim.CircuitRepeatBlock):
2202 noisy_body = _add_noise_recursive(
2203 inst.body_copy(), p, num_qubits, active_qubits
2204 )
2205 noisy_circuit.append(
2206 stim.CircuitRepeatBlock(inst.repeat_count, noisy_body)
2207 )
2208 continue
2209 name = inst.name
2210 if name == "TICK":
2211 idle_qubits = set(range(num_qubits)) - active_qubits
2212 if idle_qubits:
2213 noisy_circuit.append("DEPOLARIZE1", sorted(idle_qubits), p1)
2214 noisy_circuit.append(inst)
2215 active_qubits.clear()
2216 continue
2217 gd = stim.gate_data(name)
2218 noise_channel = gd.is_noisy_gate and (not gd.produces_measurements or 0 not in gd.num_parens_arguments_range)
2219 if name == "MPAD" or noise_channel or not (gd.is_unitary or gd.is_reset or gd.produces_measurements):
2220 # annotations (DETECTOR, OBSERVABLE_INCLUDE, QUBIT_COORDS, SHIFT_COORDS), MPAD (its targets are bits, not
2221 # qubits), and the noise channels of a noisy circuit (also HERALDED_ERASE): no noise, not an operation
2222 noisy_circuit.append(inst)
2223 continue
2224 targets = inst.targets_copy()
2225 # qubit_value is None for measurement records (rec[-k]), sweep bits (sweep[k]), and combiners (*)
2226 qubits = [t.qubit_value for t in targets if t.qubit_value is not None]
2227 active_qubits.update(qubits)
2228 flip = "Z_ERROR" if name.endswith("X") else "X_ERROR" # (RX, MX, MRX: X basis; Z or Y basis otherwise)
2229 if gd.produces_measurements and gd.is_single_qubit_gate: # M, MX, MY, MR, MRX, MRY
2230 if qubits:
2231 noisy_circuit.append(flip, qubits, p)
2232 noisy_circuit.append(inst)
2233 if gd.is_reset and qubits:
2234 noisy_circuit.append(flip, qubits, p)
2235 elif gd.is_reset and not gd.produces_measurements: # R, RX, RY
2236 noisy_circuit.append(inst)
2237 if qubits:
2238 noisy_circuit.append(flip, qubits, p)
2239 elif gd.is_unitary and gd.is_single_qubit_gate: # I, X, H, H_XY, S, SQRT_X, C_XYZ, ...
2240 noisy_circuit.append(inst)
2241 if qubits:
2242 noisy_circuit.append("DEPOLARIZE1", qubits, p1)
2243 elif gd.is_unitary and gd.is_two_qubit_gate: # CX, CZ, XCY, SWAP, ISWAP, SQRT_XX, CXSWAP, II, ...
2244 noisy_circuit.append(inst)
2245 pair_qubits: List[int] = []
2246 ctrl_qubits: List[int] = [] # the qubits of the classically controlled pairs (e.g., CX rec[-1] 5)
2247 for a, b in zip(targets[::2], targets[1::2]):
2248 if a.qubit_value is not None and b.qubit_value is not None:
2249 pair_qubits += [a.qubit_value, b.qubit_value]
2250 else:
2251 ctrl_qubits += [t.qubit_value for t in (a, b) if t.qubit_value is not None]
2252 if pair_qubits:
2253 noisy_circuit.append("DEPOLARIZE2", pair_qubits, p)
2254 if ctrl_qubits:
2255 noisy_circuit.append("DEPOLARIZE1", ctrl_qubits, p1)
2256 else: # multi-qubit measurements (MPP, MXX, MYY, MZZ) and Pauli product gates (SPP, SPP_DAG): no noise
2257 noisy_circuit.append(inst)
2258 return noisy_circuit
2259
2260
2261def add_noise(circuit: Any, p: float = 0.001, force: bool = False) -> Any:
2262 """
2263 Adds uniform circuit-level noise of strength p to a Stim circuit (the gates are classified by stim.gate_data()):
2264 - DEPOLARIZE2(p) after two-qubit gates (CX, CY, CZ, XCX, XCY, XCZ, YCX, YCY, YCZ, SWAP, ISWAP, ISWAP_DAG, SQRT_XX,
2265 SQRT_YY, SQRT_ZZ and their _DAG versions, CXSWAP, SWAPCX, CZSWAP, II); a classically controlled gate (e.g.,
2266 CX rec[-1] 5 or CZ sweep[0] 5) gets DEPOLARIZE1(p/10) on its qubit;
2267 - DEPOLARIZE1(p/10) after one-qubit gates (I, X, Y, Z, H, H_XY, H_YZ, S, S_DAG, SQRT_X, SQRT_Y and their _DAG
2268 versions, C_XYZ, C_ZYX, and the other variants), and on the qubits idle since the previous TICK (at each TICK,
2269 and at the end of the circuit);
2270 - X (Z) flips with probability p after Z-basis or Y-basis (X-basis) resets and before measurements, both before and
2271 after a measurement with reset (MR, MRX, MRY);
2272 - none for the multi-qubit measurements (MPP, MXX, MYY, MZZ) and the Pauli product gates (SPP, SPP_DAG).
2273 The noise of a noisy circuit is kept and the uniform noise is added to it, unless force=True: then it is replaced
2274 (see strip_noise()).
2275
2276 Args:
2277 circuit: stim.Circuit or path to a .stim file.
2278 p: Noise strength (for the distance, only which errors are possible matters); p=0: no noise is added.
2279 force: If True, all noise of the circuit is removed first (with p=0: only that).
2280
2281 Returns:
2282 The noisy stim.Circuit (a copy).
2283 """
2284 stim = _get_stim()
2285 if isinstance(circuit, (str, Path)):
2286 circuit = stim.Circuit.from_file(str(circuit))
2287 if force:
2288 circuit = strip_noise(circuit)
2289 if p <= 0.0:
2290 return circuit.copy()
2291 num_qubits = circuit.num_qubits
2292 active_qubits: Set[int] = set()
2293 noisy_circuit = _add_noise_recursive(circuit, p, num_qubits, active_qubits)
2294 if active_qubits:
2295 final_idle_qubits = set(range(num_qubits)) - active_qubits
2296 if final_idle_qubits:
2297 noisy_circuit.append("DEPOLARIZE1", sorted(final_idle_qubits), 0.1 * p)
2298 return noisy_circuit
2299
2300
2301def remove_empty_detectors(circuit: Any) -> Tuple[Any, int]:
2302 """Removes DETECTOR instructions that have no measurement record targets."""
2303 stim = _get_stim()
2304 new_circuit = stim.Circuit()
2305 removed = 0
2306 for inst in circuit:
2307 if isinstance(inst, stim.CircuitRepeatBlock):
2308 body, r = remove_empty_detectors(inst.body_copy())
2309 removed += r * inst.repeat_count
2310 new_circuit.append(stim.CircuitRepeatBlock(inst.repeat_count, body))
2311 elif inst.name == "DETECTOR":
2312 rec_targets = [
2313 t for t in inst.targets_copy()
2314 if t.is_measurement_record_target
2315 ]
2316 if not rec_targets:
2317 removed += 1
2318 else:
2319 new_circuit.append(inst)
2320 else:
2321 new_circuit.append(inst)
2322 return new_circuit, removed
2323
2324
2325def has_repeat_block(circuit: Any) -> bool:
2326 """Checks if a Stim circuit contains at least one REPEAT block."""
2327 stim = _get_stim()
2328 if isinstance(circuit, (str, Path)):
2329 circuit = stim.Circuit.from_file(str(circuit))
2330 if not isinstance(circuit, stim.Circuit):
2331 return False
2332 for inst in circuit:
2333 if isinstance(inst, stim.CircuitRepeatBlock):
2334 return True
2335 return False
2336
2337
2338def set_circuit_rounds(circuit: Any, rounds: int) -> Any:
2339 """Sets the repeat_count of CircuitRepeatBlocks in a Stim circuit.
2340
2341 For circuits with a single repeat block, sets its repeat_count to rounds.
2342 For circuits with two repeat blocks (e.g. preamble + periodic body),
2343 adjusts them to achieve total requested rounds.
2344
2345 Args:
2346 circuit: A stim.Circuit instance.
2347 rounds: The new repetition count to set for repeat blocks. If <= 0,
2348 repeat blocks are omitted.
2349
2350 Returns:
2351 A new stim.Circuit instance with modified repeat counts.
2352 """
2353 stim = _get_stim()
2354 if isinstance(circuit, (str, Path)):
2355 circuit = stim.Circuit.from_file(str(circuit))
2356 if not isinstance(circuit, stim.Circuit):
2357 return circuit
2358 if rounds < 0:
2359 raise ValueError(f"Invalid rounds={rounds}; must be >= 0.")
2360
2361 top_blocks = [
2362 inst.repeat_count for inst in circuit
2363 if isinstance(inst, stim.CircuitRepeatBlock)
2364 ]
2365 if len(top_blocks) > 1:
2366 sys.stderr.write(
2367 f"# Warning: Circuit has {len(top_blocks)} repeat blocks "
2368 f"({top_blocks}) which may not be handled correctly by this "
2369 "script.\n"
2370 )
2371
2372 new_circuit = stim.Circuit()
2373 if len(top_blocks) == 2:
2374 preamble = top_blocks[0]
2375 b1_count = min(preamble, rounds)
2376 b2_count = max(0, rounds - preamble)
2377 block_idx = 0
2378 for inst in circuit:
2379 if isinstance(inst, stim.CircuitRepeatBlock):
2380 cnt = b1_count if block_idx == 0 else b2_count
2381 block_idx += 1
2382 if cnt > 0:
2383 new_body = set_circuit_rounds(inst.body_copy(), cnt)
2384 new_circuit.append(stim.CircuitRepeatBlock(cnt, new_body))
2385 else:
2386 new_circuit.append(inst)
2387 else:
2388 for inst in circuit:
2389 if isinstance(inst, stim.CircuitRepeatBlock):
2390 if rounds > 0:
2391 new_body = set_circuit_rounds(inst.body_copy(), rounds)
2392 new_circuit.append(
2393 stim.CircuitRepeatBlock(rounds, new_body)
2394 )
2395 else:
2396 new_circuit.append(inst)
2397 return new_circuit
2398
2399
2400def count_circuit_rounds(circuit: Any) -> int:
2401 """Returns total repeat count of all repeat blocks in circuit, or 1 if unrolled."""
2402 stim = _get_stim()
2403 if isinstance(circuit, (str, Path)):
2404 circuit = stim.Circuit.from_file(str(circuit))
2405 if not isinstance(circuit, stim.Circuit):
2406 return 1
2407 blocks = [
2408 inst.repeat_count for inst in circuit
2409 if isinstance(inst, stim.CircuitRepeatBlock)
2410 ]
2411 if len(blocks) > 1:
2412 sys.stderr.write(
2413 f"# Warning: Circuit has {len(blocks)} repeat blocks "
2414 f"({blocks}) which may not be handled correctly by this "
2415 "script.\n"
2416 )
2417 return sum(blocks) if blocks else 1
2418
2419
2421 circuit: Any,
2422 filepath: Optional[Union[str, Path]] = None,
2423 basis_arg: Optional[str] = None
2424) -> str:
2425 """Determines the primary memory basis ('X' or 'Z') of a Stim circuit."""
2426 if basis_arg is not None and str(basis_arg).upper() in ["X", "Z"]:
2427 return str(basis_arg).upper()
2428
2429 if filepath is not None:
2430 stem = Path(filepath).stem.upper()
2431 parts = stem.replace("-", "_").split("_")
2432 if "X" in parts or "HX" in parts or stem.endswith("X"):
2433 return "X"
2434 if "Z" in parts or "HZ" in parts or stem.endswith("Z"):
2435 return "Z"
2436
2437 stim = _get_stim()
2438 if isinstance(circuit, (str, Path)):
2439 circuit = stim.Circuit.from_file(str(circuit))
2440
2441 rx_count = 0
2442 rz_count = 0
2443 for inst in circuit:
2444 if isinstance(inst, stim.CircuitRepeatBlock):
2445 break
2446 if inst.name == "RX":
2447 rx_count += len(inst.targets_copy())
2448 elif inst.name in ["R", "RZ"]:
2449 rz_count += len(inst.targets_copy())
2450 elif inst.name in ["M", "MX", "MZ", "MR", "MRX", "MRZ"]:
2451 break
2452 if rx_count > rz_count:
2453 return "X"
2454 if rz_count > rx_count:
2455 return "Z"
2456
2457 mx_count = 0
2458 mz_count = 0
2459 for inst in reversed(list(circuit)):
2460 if isinstance(inst, stim.CircuitRepeatBlock):
2461 break
2462 if inst.name in ["MX", "MRX"]:
2463 mx_count += len(inst.targets_copy())
2464 elif inst.name in ["M", "MZ", "MR", "MRZ"]:
2465 mz_count += len(inst.targets_copy())
2466 if mx_count > mz_count:
2467 return "X"
2468 return "Z"
2469
2470
2471def track_circuit_carriers(circuit: Any) -> List[List[int]]:
2472 """Dynamically tracks logical syndrome carriers across the circuit to measurements.
2473
2474 Accounts for physical qubit permutations via SWAP, CXSWAP, CZSWAP, ISWAP,
2475 and ISWAP_DAG gates, as well as CNOT/CZ syndrome handoffs onto fresh ancilla
2476 or routing qubits.
2477
2478 Returns:
2479 meas_carriers: List mapping each measurement index to a list of initial
2480 logical qubit IDs whose syndrome or state is carried into that
2481 measurement.
2482 """
2483 stim = _get_stim()
2484 if isinstance(circuit, (str, Path)):
2485 circuit = stim.Circuit.from_file(str(circuit))
2486 num_qubits = circuit.num_qubits
2487 phys_to_log = {q: q for q in range(num_qubits)}
2488
2489 tot_resets: Dict[int, int] = {q: 0 for q in range(num_qubits)}
2490 tot_meas: Dict[int, int] = {q: 0 for q in range(num_qubits)}
2491
2492 def pass1(block: Any, p_to_l: Dict[int, int], mult: int = 1) -> None:
2493 for inst in block:
2494 if isinstance(inst, stim.CircuitRepeatBlock):
2495 rep_count = inst.repeat_count
2496 body = inst.body_copy()
2497 map_copy = dict(p_to_l)
2498 for sub_inst in body:
2499 if not isinstance(sub_inst, stim.CircuitRepeatBlock):
2500 if sub_inst.name in [
2501 "SWAP", "CXSWAP", "CZSWAP", "ISWAP", "ISWAP_DAG"
2502 ]:
2503 t_vals = [
2504 t.value for t in sub_inst.targets_copy()
2505 if t.is_qubit_target
2506 ]
2507 for idx in range(0, len(t_vals), 2):
2508 q1, q2 = t_vals[idx], t_vals[idx + 1]
2509 map_copy[q1], map_copy[q2] = (
2510 map_copy[q2], map_copy[q1]
2511 )
2512 if map_copy == p_to_l:
2513 pass1(body, p_to_l, mult * rep_count)
2514 else:
2515 for _ in range(rep_count):
2516 pass1(body, p_to_l, mult)
2517 continue
2518 name = inst.name
2519 t_vals = [
2520 t.value for t in inst.targets_copy() if t.is_qubit_target
2521 ]
2522 if name in [
2523 "R", "RX", "RY", "RZ", "MR", "MRX", "MRY", "MRZ"
2524 ]:
2525 for q in t_vals:
2526 tot_resets[p_to_l[q]] += mult
2527 if name in [
2528 "M", "MX", "MY", "MZ", "MR", "MRX", "MRY", "MRZ"
2529 ]:
2530 for q in t_vals:
2531 tot_meas[p_to_l[q]] += mult
2532 if name in ["SWAP", "CXSWAP", "CZSWAP", "ISWAP", "ISWAP_DAG"]:
2533 for idx in range(0, len(t_vals), 2):
2534 q1, q2 = t_vals[idx], t_vals[idx + 1]
2535 p_to_l[q1], p_to_l[q2] = p_to_l[q2], p_to_l[q1]
2536
2537 pass1(circuit, dict(phys_to_log), 1)
2538 max_resets = max(tot_resets.values()) if tot_resets else 0
2539 max_meas = max(tot_meas.values()) if tot_meas else 0
2540 data_log = set()
2541 for q in range(num_qubits):
2542 if max_resets > 1:
2543 if tot_resets[q] <= 1:
2544 data_log.add(q)
2545 elif max_meas > 1:
2546 if tot_meas[q] <= 1:
2547 data_log.add(q)
2548
2549 active_carriers: Dict[int, Set[int]] = {
2550 q: {q} for q in range(num_qubits)
2551 }
2552 fresh_anc: Set[int] = set(range(num_qubits)) - data_log
2553 meas_carriers: List[List[int]] = []
2554
2555 def pass2(block: Any, p_to_l: Dict[int, int]) -> None:
2556 for inst in block:
2557 if isinstance(inst, stim.CircuitRepeatBlock):
2558 rep_count = inst.repeat_count
2559 body = inst.body_copy()
2560 if rep_count <= 0:
2561 continue
2562 map_copy = dict(p_to_l)
2563 for sub_inst in body:
2564 if not isinstance(sub_inst, stim.CircuitRepeatBlock):
2565 if sub_inst.name in [
2566 "SWAP", "CXSWAP", "CZSWAP", "ISWAP", "ISWAP_DAG"
2567 ]:
2568 t_vals = [
2569 t.value for t in sub_inst.targets_copy()
2570 if t.is_qubit_target
2571 ]
2572 for idx in range(0, len(t_vals), 2):
2573 q1, q2 = t_vals[idx], t_vals[idx + 1]
2574 map_copy[q1], map_copy[q2] = (
2575 map_copy[q2], map_copy[q1]
2576 )
2577 if map_copy == p_to_l:
2578 idx0 = len(meas_carriers)
2579 pass2(body, p_to_l)
2580 idx1 = len(meas_carriers)
2581 if rep_count > 1 and idx1 > idx0:
2582 body_carriers = meas_carriers[idx0:idx1]
2583 meas_carriers.extend(body_carriers * (rep_count - 1))
2584 else:
2585 for _ in range(rep_count):
2586 pass2(body, p_to_l)
2587 continue
2588 name = inst.name
2589 t_vals = [
2590 t.value for t in inst.targets_copy() if t.is_qubit_target
2591 ]
2592 if name in ["R", "RX", "RY", "RZ"]:
2593 for q in t_vals:
2594 lq = p_to_l[q]
2595 active_carriers[lq] = {lq}
2596 if lq not in data_log:
2597 fresh_anc.add(lq)
2598 elif name in ["M", "MX", "MY", "MZ"]:
2599 for q in t_vals:
2600 lq = p_to_l[q]
2601 meas_carriers.append(sorted(list(active_carriers[lq])))
2602 elif name in ["MR", "MRX", "MRY", "MRZ"]:
2603 for q in t_vals:
2604 lq = p_to_l[q]
2605 meas_carriers.append(sorted(list(active_carriers[lq])))
2606 active_carriers[lq] = {lq}
2607 if lq not in data_log:
2608 fresh_anc.add(lq)
2609 elif name in [
2610 "CX", "ZCX", "CY", "ZCY", "CZ", "ZCZ",
2611 "XCX", "XCY", "XCZ", "YCX", "YCY", "YCZ"
2612 ]:
2613 for idx in range(0, len(t_vals), 2):
2614 q1, q2 = t_vals[idx], t_vals[idx + 1]
2615 l1, l2 = p_to_l[q1], p_to_l[q2]
2616 if l1 not in data_log and l2 not in data_log:
2617 if l2 in fresh_anc and l1 not in fresh_anc:
2618 active_carriers[l2] = set(active_carriers[l1])
2619 fresh_anc.discard(l2)
2620 elif l1 in fresh_anc and l2 not in fresh_anc:
2621 active_carriers[l1] = set(active_carriers[l2])
2622 fresh_anc.discard(l1)
2623 else:
2624 comb = active_carriers[l1] | active_carriers[l2]
2625 active_carriers[l1] = set(comb)
2626 active_carriers[l2] = set(comb)
2627 fresh_anc.discard(l1)
2628 fresh_anc.discard(l2)
2629 elif l1 in data_log and l2 not in data_log:
2630 fresh_anc.discard(l2)
2631 elif l2 in data_log and l1 not in data_log:
2632 fresh_anc.discard(l1)
2633 elif name == "SWAP":
2634 for idx in range(0, len(t_vals), 2):
2635 q1, q2 = t_vals[idx], t_vals[idx + 1]
2636 p_to_l[q1], p_to_l[q2] = p_to_l[q2], p_to_l[q1]
2637 elif name in ["CXSWAP", "CZSWAP", "ISWAP", "ISWAP_DAG"]:
2638 for idx in range(0, len(t_vals), 2):
2639 q1, q2 = t_vals[idx], t_vals[idx + 1]
2640 l1, l2 = p_to_l[q1], p_to_l[q2]
2641 if l1 in data_log and l2 not in data_log:
2642 fresh_anc.discard(l2)
2643 elif l2 in data_log and l1 not in data_log:
2644 fresh_anc.discard(l1)
2645 p_to_l[q1], p_to_l[q2] = p_to_l[q2], p_to_l[q1]
2646
2647 pass2(circuit, dict(phys_to_log))
2648 return meas_carriers
2649
2650
2652 circ: Any,
2653 phys_to_log: Dict[int, int],
2654 active_gates_count: Dict[int, int]
2655) -> Any:
2656 """Rewrites a Stim circuit by tracking SWAPs virtually."""
2657 stim = _get_stim()
2658 new_circ = stim.Circuit()
2659 non_active_instructions = {
2660 "QUBIT_COORDS", "SHIFT_COORDS", "TICK",
2661 "DETECTOR", "OBSERVABLE_INCLUDE", "MPAD"
2662 }
2663
2664 for inst in circ:
2665 if isinstance(inst, stim.CircuitRepeatBlock):
2666 rep_count = inst.repeat_count
2667 body = inst.body_copy()
2668 map_copy = dict(phys_to_log)
2669 for sub_inst in body:
2670 if not isinstance(sub_inst, stim.CircuitRepeatBlock):
2671 if sub_inst.name in [
2672 "SWAP", "CXSWAP", "CZSWAP", "ISWAP", "ISWAP_DAG"
2673 ]:
2674 targets = sub_inst.targets_copy()
2675 for idx in range(0, len(targets), 2):
2676 q1 = targets[idx].value
2677 q2 = targets[idx + 1].value
2678 map_copy[q1], map_copy[q2] = (
2679 map_copy[q2], map_copy[q1]
2680 )
2681 if map_copy == phys_to_log:
2682 new_body = _rewrite_circuit_internal(
2683 body, phys_to_log, active_gates_count
2684 )
2685 new_circ.append(stim.CircuitRepeatBlock(rep_count, new_body))
2686 else:
2687 for _ in range(rep_count):
2688 unrolled_body = _rewrite_circuit_internal(
2689 body, phys_to_log, active_gates_count
2690 )
2691 new_circ += unrolled_body
2692 continue
2693
2694 name = inst.name
2695 targets = inst.targets_copy()
2696 args = inst.gate_args_copy()
2697
2698 if name == "SWAP":
2699 for idx in range(0, len(targets), 2):
2700 q1 = targets[idx].value
2701 q2 = targets[idx + 1].value
2702 phys_to_log[q1], phys_to_log[q2] = (
2703 phys_to_log[q2], phys_to_log[q1]
2704 )
2705 elif name == "CXSWAP":
2706 cx_targets = []
2707 for idx in range(0, len(targets), 2):
2708 q1 = targets[idx].value
2709 q2 = targets[idx + 1].value
2710 l1 = phys_to_log[q1]
2711 l2 = phys_to_log[q2]
2712 cx_targets.extend([l1, l2])
2713 active_gates_count[l1] = active_gates_count.get(l1, 0) + 1
2714 active_gates_count[l2] = active_gates_count.get(l2, 0) + 1
2715 phys_to_log[q1], phys_to_log[q2] = (
2716 phys_to_log[q2], phys_to_log[q1]
2717 )
2718 new_circ.append("CX", cx_targets, args)
2719 elif name == "CZSWAP":
2720 cz_targets = []
2721 for idx in range(0, len(targets), 2):
2722 q1 = targets[idx].value
2723 q2 = targets[idx + 1].value
2724 l1 = phys_to_log[q1]
2725 l2 = phys_to_log[q2]
2726 cz_targets.extend([l1, l2])
2727 active_gates_count[l1] = active_gates_count.get(l1, 0) + 1
2728 active_gates_count[l2] = active_gates_count.get(l2, 0) + 1
2729 phys_to_log[q1], phys_to_log[q2] = (
2730 phys_to_log[q2], phys_to_log[q1]
2731 )
2732 new_circ.append("CZ", cz_targets, args)
2733 elif name in ["ISWAP", "ISWAP_DAG"]:
2734 cz_targets = []
2735 s_targets = []
2736 for idx in range(0, len(targets), 2):
2737 q1 = targets[idx].value
2738 q2 = targets[idx + 1].value
2739 l1 = phys_to_log[q1]
2740 l2 = phys_to_log[q2]
2741 cz_targets.extend([l1, l2])
2742 s_targets.extend([l1, l2])
2743 active_gates_count[l1] = active_gates_count.get(l1, 0) + 1
2744 active_gates_count[l2] = active_gates_count.get(l2, 0) + 1
2745 phys_to_log[q1], phys_to_log[q2] = (
2746 phys_to_log[q2], phys_to_log[q1]
2747 )
2748 new_circ.append("CZ", cz_targets)
2749 s_gate = "S" if name == "ISWAP" else "S_DAG"
2750 new_circ.append(s_gate, s_targets)
2751 else:
2752 new_targets = []
2753 for t in targets:
2754 if t.is_qubit_target:
2755 l_q = phys_to_log[t.value]
2756 if name not in non_active_instructions:
2757 active_gates_count[l_q] = (
2758 active_gates_count.get(l_q, 0) + 1
2759 )
2760 if t.is_inverted_result_target:
2761 new_targets.append(stim.target_inv(l_q))
2762 else:
2763 new_targets.append(l_q)
2764 elif t.is_x_target:
2765 l_q = phys_to_log[t.value]
2766 if name not in non_active_instructions:
2767 active_gates_count[l_q] = (
2768 active_gates_count.get(l_q, 0) + 1
2769 )
2770 new_targets.append(stim.target_x(l_q))
2771 elif t.is_y_target:
2772 l_q = phys_to_log[t.value]
2773 if name not in non_active_instructions:
2774 active_gates_count[l_q] = (
2775 active_gates_count.get(l_q, 0) + 1
2776 )
2777 new_targets.append(stim.target_y(l_q))
2778 elif t.is_z_target:
2779 l_q = phys_to_log[t.value]
2780 if name not in non_active_instructions:
2781 active_gates_count[l_q] = (
2782 active_gates_count.get(l_q, 0) + 1
2783 )
2784 new_targets.append(stim.target_z(l_q))
2785 else:
2786 new_targets.append(t)
2787 new_circ.append(name, new_targets, args)
2788 return new_circ
2789
2790
2792 circuit: Any,
2793 verbose: bool = False,
2794 basis_arg: Optional[str] = None,
2795 filepath: Optional[Union[str, Path]] = None
2796) -> Dict[str, Any]:
2797 """Classifies qubits and checks via virtual SWAP rewriting and Pauli basis tracking.
2798
2799 Tracks both ancilla stabilizer evolution and data qubit initial basis
2800 evolution through 1-qubit and 2-qubit Clifford gates. Supports standard
2801 CSS codes as well as locally basis-rotated CSS codes (such as XZZX codes)
2802 via 2-coloring of the check-data Pauli compatibility graph.
2803
2804 Returns:
2805 Dictionary with keys:
2806 - 'x_ancillas': Sorted list of X-type (or X-sector) ancilla qubit IDs.
2807 - 'z_ancillas': Sorted list of Z-type (or Z-sector) ancilla qubit IDs.
2808 - 'data_qubits': Sorted list of data qubit IDs.
2809 - 'routing_qubits': Sorted list of idle/routing qubit IDs.
2810 - 'rotated_data_qubits': Sorted list of data qubits with rotated local basis.
2811 - 'is_css': True if the circuit implements a (possibly locally rotated) CSS code.
2812 - 'is_rotated_css': True if CSS only after local data basis rotations (e.g. XZZX).
2813 - 'basis': Primary memory basis ('X' or 'Z').
2814 - 'rewritten_circuit': Circuit with SWAPs virtually eliminated.
2815 """
2816 stim = _get_stim()
2817 if isinstance(circuit, (str, Path)):
2818 if filepath is None:
2819 filepath = circuit
2820 circuit = stim.Circuit.from_file(str(circuit))
2821
2822 all_qubits_set: Set[int] = set()
2823
2824 def collect_qubits(blk: Any) -> None:
2825 for inst in blk:
2826 if isinstance(inst, stim.CircuitRepeatBlock):
2827 collect_qubits(inst.body_copy())
2828 else:
2829 for t in inst.targets_copy():
2830 if (
2831 t.is_qubit_target or t.is_x_target
2832 or t.is_y_target or t.is_z_target
2833 ):
2834 all_qubits_set.add(t.value)
2835
2836 collect_qubits(circuit)
2837 max_q = max(all_qubits_set) if all_qubits_set else -1
2838 phys_to_log = {q: q for q in range(max_q + 1)}
2839 active_gates_count = {q: 0 for q in all_qubits_set}
2840
2841 def cap_repeats(blk: Any) -> Any:
2842 out = stim.Circuit()
2843 for inst in blk:
2844 if isinstance(inst, stim.CircuitRepeatBlock):
2845 out.append(
2846 stim.CircuitRepeatBlock(
2847 min(inst.repeat_count, 2),
2848 cap_repeats(inst.body_copy())
2849 )
2850 )
2851 else:
2852 out.append(inst)
2853 return out
2854
2855 capped_circuit = cap_repeats(circuit)
2856 rewritten_circ = _rewrite_circuit_internal(
2857 capped_circuit, phys_to_log, active_gates_count
2858 )
2859 routing_qubits = {
2860 q for q, count in active_gates_count.items() if count == 0
2861 }
2862 active_qubits = all_qubits_set - routing_qubits
2863
2864 flat_circ = rewritten_circ.flattened()
2865
2866 tot_resets: Dict[int, int] = {q: 0 for q in active_qubits}
2867 tot_meas: Dict[int, int] = {q: 0 for q in active_qubits}
2868 meas_records: List[int] = []
2869 used_in_detector: Set[int] = set()
2870 used_in_observable: Set[int] = set()
2871 first_detector_seen = False
2872 meas_before_first_det: Set[int] = set()
2873 r1_single_det_anc: Set[int] = set()
2874 multi_det_groups: List[Set[int]] = []
2875
2876 for inst in flat_circ:
2877 name = inst.name
2878 targets = inst.targets_copy()
2879 if name in ["R", "RX", "RY", "RZ"]:
2880 for t in targets:
2881 if t.is_qubit_target and t.value in tot_resets:
2882 tot_resets[t.value] += 1
2883 elif name in ["M", "MX", "MY", "MZ"]:
2884 for t in targets:
2885 if t.is_qubit_target:
2886 if t.value in tot_meas:
2887 tot_meas[t.value] += 1
2888 meas_records.append(t.value)
2889 if not first_detector_seen:
2890 meas_before_first_det.add(t.value)
2891 elif name in ["MR", "MRX", "MRY", "MRZ"]:
2892 for t in targets:
2893 if t.is_qubit_target:
2894 if t.value in tot_resets:
2895 tot_resets[t.value] += 1
2896 if t.value in tot_meas:
2897 tot_meas[t.value] += 1
2898 meas_records.append(t.value)
2899 if not first_detector_seen:
2900 meas_before_first_det.add(t.value)
2901 elif name == "DETECTOR":
2902 first_detector_seen = True
2903 recs = [
2904 meas_records[len(meas_records) + t.value]
2905 for t in targets
2906 if t.is_measurement_record_target
2907 and 0 <= len(meas_records) + t.value < len(meas_records)
2908 ]
2909 for q in recs:
2910 used_in_detector.add(q)
2911 if len(recs) == 1:
2912 r1_single_det_anc.add(recs[0])
2913 elif len(recs) > 1:
2914 multi_det_groups.append(set(recs))
2915 elif name == "OBSERVABLE_INCLUDE":
2916 for t in targets:
2917 if t.is_measurement_record_target:
2918 idx = len(meas_records) + t.value
2919 if 0 <= idx < len(meas_records):
2920 used_in_observable.add(meas_records[idx])
2921
2922 max_resets = max(tot_resets.values()) if tot_resets else 0
2923 max_meas = max(tot_meas.values()) if tot_meas else 0
2924
2925 data_qubits: Set[int] = set()
2926 anc_candidates: Set[int] = set()
2927
2928 for q in active_qubits:
2929 if max_resets > 1:
2930 is_data = (tot_resets[q] <= 1)
2931 elif max_meas > 1:
2932 is_data = (tot_meas[q] <= 1)
2933 else:
2934 if q not in used_in_detector:
2935 is_data = True
2936 else:
2937 is_data = (
2938 (q not in meas_before_first_det)
2939 or (q in used_in_observable)
2940 )
2941 if is_data:
2942 data_qubits.add(q)
2943 else:
2944 anc_candidates.add(q)
2945
2946 # First-round stabilizer & data-basis tracking
2947 reset_basis: Dict[int, str] = {}
2948 meas_basis: Dict[int, str] = {}
2949 first_round_insts: List[Tuple[str, List[int]]] = []
2950 measured_anc: Set[int] = set()
2951 has_any_anc_meas = False
2952
2953 for inst in flat_circ:
2954 name = inst.name
2955 t_vals = [t.value for t in inst.targets_copy() if t.is_qubit_target]
2956 if name in ["R", "RZ", "RX", "RY"]:
2957 if has_any_anc_meas and any(
2958 q in anc_candidates and q in measured_anc for q in t_vals
2959 ):
2960 break
2961 b = "X" if name == "RX" else ("Y" if name == "RY" else "Z")
2962 for q in t_vals:
2963 if q not in reset_basis:
2964 reset_basis[q] = b
2965 first_round_insts.append((name, t_vals))
2966 elif name in ["M", "MZ", "MX", "MY", "MR", "MRZ", "MRX", "MRY"]:
2967 if any(q in data_qubits for q in t_vals) and has_any_anc_meas:
2968 break
2969 if any(q in anc_candidates and q in measured_anc for q in t_vals):
2970 break
2971 b = (
2972 "X" if name in ["MX", "MRX"]
2973 else ("Y" if name in ["MY", "MRY"] else "Z")
2974 )
2975 for q in t_vals:
2976 if q in anc_candidates:
2977 meas_basis[q] = b
2978 measured_anc.add(q)
2979 has_any_anc_meas = True
2980 if q not in reset_basis and name in [
2981 "MR", "MRZ", "MRX", "MRY"
2982 ]:
2983 reset_basis[q] = b
2984 first_round_insts.append((name, t_vals))
2985 elif name in [
2986 "H", "H_XZ", "H_XY", "H_YZ", "S", "S_DAG",
2987 "SQRT_Z", "SQRT_Z_DAG", "SQRT_X", "SQRT_X_DAG",
2988 "SQRT_Y", "SQRT_Y_DAG",
2989 "CX", "ZCX", "CZ", "ZCZ", "CY", "ZCY",
2990 "XCX", "XCY", "XCZ", "YCX", "YCY", "YCZ"
2991 ]:
2992 first_round_insts.append((name, t_vals))
2993
2994 def conj_1q(gate: str, p: str) -> str:
2995 if p == "I":
2996 return "I"
2997 if gate in ["H", "H_XZ"]:
2998 return "Z" if p == "X" else ("X" if p == "Z" else "Y")
2999 if gate in ["S", "S_DAG", "SQRT_Z", "SQRT_Z_DAG", "H_XY"]:
3000 return "Y" if p == "X" else ("X" if p == "Y" else "Z")
3001 if gate in ["SQRT_X", "SQRT_X_DAG", "H_YZ"]:
3002 return "Z" if p == "Y" else ("Y" if p == "Z" else "X")
3003 if gate in ["SQRT_Y", "SQRT_Y_DAG"]:
3004 return "Z" if p == "X" else ("X" if p == "Z" else "Y")
3005 return p
3006
3007 mul_tab = {
3008 ("I", "I"): "I", ("I", "X"): "X", ("I", "Y"): "Y", ("I", "Z"): "Z",
3009 ("X", "I"): "X", ("X", "X"): "I", ("X", "Y"): "Z", ("X", "Z"): "Y",
3010 ("Y", "I"): "Y", ("Y", "X"): "Z", ("Y", "Y"): "I", ("Y", "Z"): "X",
3011 ("Z", "I"): "Z", ("Z", "X"): "Y", ("Z", "Y"): "X", ("Z", "Z"): "I",
3012 }
3013
3014 def conj_2q(gate: str, p1: str, p2: str) -> Tuple[str, str]:
3015 if p1 == "I" and p2 == "I":
3016 return "I", "I"
3017 if gate in ["CX", "ZCX"]:
3018 a, b = "Z", "X"
3019 elif gate in ["CZ", "ZCZ"]:
3020 a, b = "Z", "Z"
3021 elif gate in ["CY", "ZCY"]:
3022 a, b = "Z", "Y"
3023 elif gate == "XCX":
3024 a, b = "X", "X"
3025 elif gate == "XCY":
3026 a, b = "X", "Y"
3027 elif gate == "XCZ":
3028 a, b = "X", "Z"
3029 elif gate == "YCX":
3030 a, b = "Y", "X"
3031 elif gate == "YCY":
3032 a, b = "Y", "Y"
3033 elif gate == "YCZ":
3034 a, b = "Y", "Z"
3035 else:
3036 return p1, p2
3037 anti1 = (p1 != "I" and p1 != a)
3038 anti2 = (p2 != "I" and p2 != b)
3039 np1 = mul_tab[(p1, a)] if anti2 else p1
3040 np2 = mul_tab[(p2, b)] if anti1 else p2
3041 return np1, np2
3042
3043 # Simultaneously evolve all ancilla stabilizers and data initial bases
3044 # using bitmasks over sorted anc_list for O(1) gate updates.
3045 anc_list = sorted(list(anc_candidates))
3046 anc_to_bit = {anc: (1 << i) for i, anc in enumerate(anc_list)}
3047 all_anc_mask = (1 << len(anc_list)) - 1
3048 active_anc_mask = 0
3049 qx: Dict[int, int] = {q: 0 for q in all_qubits_set}
3050 qz: Dict[int, int] = {q: 0 for q in all_qubits_set}
3051
3052 for anc in anc_list:
3053 b = reset_basis.get(anc, meas_basis.get(anc, "Z"))
3054 bit = anc_to_bit[anc]
3055 if b in ["X", "Y"]:
3056 qx[anc] |= bit
3057 if b in ["Z", "Y"]:
3058 qz[anc] |= bit
3059 if anc not in reset_basis:
3060 active_anc_mask |= bit
3061
3062 data_init_basis: Dict[int, str] = {q: "Z" for q in data_qubits}
3063
3064 for name, t_vals in first_round_insts:
3065 if name in ["R", "RZ", "RX", "RY"]:
3066 b = "X" if name == "RX" else ("Y" if name == "RY" else "Z")
3067 for q in t_vals:
3068 if q in data_qubits:
3069 data_init_basis[q] = b
3070 if q in anc_to_bit:
3071 bit = anc_to_bit[q]
3072 if not (active_anc_mask & bit):
3073 active_anc_mask |= bit
3074 if b in ["X", "Y"]:
3075 qx[q] |= bit
3076 else:
3077 qx[q] &= ~bit
3078 if b in ["Z", "Y"]:
3079 qz[q] |= bit
3080 else:
3081 qz[q] &= ~bit
3082 elif name in ["M", "MZ", "MX", "MY", "MR", "MRZ", "MRX", "MRY"]:
3083 for q in t_vals:
3084 if q in anc_to_bit:
3085 active_anc_mask &= ~anc_to_bit[q]
3086 elif name in [
3087 "H", "H_XZ", "H_XY", "H_YZ", "S", "S_DAG",
3088 "SQRT_Z", "SQRT_Z_DAG", "SQRT_X", "SQRT_X_DAG",
3089 "SQRT_Y", "SQRT_Y_DAG"
3090 ]:
3091 for q in t_vals:
3092 if q in data_qubits:
3093 data_init_basis[q] = conj_1q(name, data_init_basis[q])
3094 if q not in qx:
3095 continue
3096 # on a data qubit, the Paulis of all checks are transformed, also of the checks already measured:
3097 # all checks are then expressed in the same frame (that of the data qubits at the end of the first
3098 # round), e.g., for X checks measured in the Hadamard frame of the data qubits before the closing H
3099 # gates (with active_anc_mask, such checks would look like Z checks; see the tests)
3100 mask = active_anc_mask if q in anc_candidates else all_anc_mask
3101 x_m = qx[q] & mask
3102 z_m = qz[q] & mask
3103 if name in ["H", "H_XZ", "SQRT_Y", "SQRT_Y_DAG"]:
3104 nx_m, nz_m = z_m, x_m
3105 elif name in [
3106 "S", "S_DAG", "SQRT_Z", "SQRT_Z_DAG", "H_XY"
3107 ]:
3108 nx_m, nz_m = x_m, x_m ^ z_m
3109 elif name in ["SQRT_X", "SQRT_X_DAG", "H_YZ"]:
3110 nx_m, nz_m = x_m ^ z_m, z_m
3111 else:
3112 nx_m, nz_m = x_m, z_m
3113 qx[q] = (qx[q] & ~mask) | nx_m
3114 qz[q] = (qz[q] & ~mask) | nz_m
3115 elif name in [
3116 "CX", "ZCX", "CZ", "ZCZ", "CY", "ZCY",
3117 "XCX", "XCY", "XCZ", "YCX", "YCY", "YCZ"
3118 ]:
3119 if name in ["CX", "ZCX"]:
3120 ax, az, bx, bz = 0, all_anc_mask, all_anc_mask, 0
3121 elif name in ["CZ", "ZCZ"]:
3122 ax, az, bx, bz = 0, all_anc_mask, 0, all_anc_mask
3123 elif name in ["CY", "ZCY"]:
3124 ax, az, bx, bz = 0, all_anc_mask, all_anc_mask, all_anc_mask
3125 elif name == "XCX":
3126 ax, az, bx, bz = all_anc_mask, 0, all_anc_mask, 0
3127 elif name == "XCY":
3128 ax, az, bx, bz = all_anc_mask, 0, all_anc_mask, all_anc_mask
3129 elif name == "XCZ":
3130 ax, az, bx, bz = all_anc_mask, 0, 0, all_anc_mask
3131 elif name == "YCX":
3132 ax, az, bx, bz = all_anc_mask, all_anc_mask, all_anc_mask, 0
3133 elif name == "YCY":
3134 ax, az, bx, bz = (
3135 all_anc_mask, all_anc_mask, all_anc_mask, all_anc_mask
3136 )
3137 elif name == "YCZ":
3138 ax, az, bx, bz = all_anc_mask, all_anc_mask, 0, all_anc_mask
3139 else:
3140 continue
3141
3142 for idx in range(0, len(t_vals), 2):
3143 q1, q2 = t_vals[idx], t_vals[idx + 1]
3144 if q1 not in qx or q2 not in qx:
3145 continue
3146 mask = active_anc_mask
3147 if q1 in anc_to_bit and not (active_anc_mask & anc_to_bit[q1]):
3148 mask &= ~anc_to_bit[q1]
3149 if q2 in anc_to_bit and not (active_anc_mask & anc_to_bit[q2]):
3150 mask &= ~anc_to_bit[q2]
3151 x1 = qx[q1] & mask
3152 z1 = qz[q1] & mask
3153 x2 = qx[q2] & mask
3154 z2 = qz[q2] & mask
3155 anti1 = (x1 & (az & mask)) ^ (z1 & (ax & mask))
3156 anti2 = (x2 & (bz & mask)) ^ (z2 & (bx & mask))
3157 nx1 = x1 ^ (anti2 & ax)
3158 nz1 = z1 ^ (anti2 & az)
3159 nx2 = x2 ^ (anti1 & bx)
3160 nz2 = z2 ^ (anti1 & bz)
3161 qx[q1] = (qx[q1] & ~mask) | nx1
3162 qz[q1] = (qz[q1] & ~mask) | nz1
3163 qx[q2] = (qx[q2] & ~mask) | nx2
3164 qz[q2] = (qz[q2] & ~mask) | nz2
3165
3166 anc_data_paulis: Dict[int, Dict[int, str]] = {
3167 anc: {} for anc in anc_list
3168 }
3169 for q in data_qubits:
3170 xm = qx[q]
3171 zm = qz[q]
3172 active_m = xm | zm
3173 while active_m:
3174 lsb = active_m & -active_m
3175 anc_idx = lsb.bit_length() - 1
3176 anc = anc_list[anc_idx]
3177 has_x = bool(xm & lsb)
3178 has_z = bool(zm & lsb)
3179 p_str = (
3180 "Y" if (has_x and has_z)
3181 else ("X" if has_x else "Z")
3182 )
3183 anc_data_paulis[anc][q] = p_str
3184 active_m ^= lsb
3185
3186 active_ancillas = [a for a in anc_list if len(anc_data_paulis[a]) > 0]
3187 idle_ancillas = [a for a in anc_list if len(anc_data_paulis[a]) == 0]
3188 routing_qubits.update(idle_ancillas)
3189
3190 # Check commutation among active ancillas
3191 checks_commute = True
3192 for i in range(len(active_ancillas)):
3193 a1 = active_ancillas[i]
3194 s1 = anc_data_paulis[a1]
3195 for j in range(i + 1, len(active_ancillas)):
3196 a2 = active_ancillas[j]
3197 s2 = anc_data_paulis[a2]
3198 anti = 0
3199 for q, p1 in s1.items():
3200 p2 = s2.get(q)
3201 if p2 is not None and p1 != p2:
3202 anti ^= 1
3203 if anti != 0:
3204 checks_commute = False
3205 break
3206 if not checks_commute:
3207 break
3208
3209 # Build check-relation graph on active_ancillas via shared data qubits
3210 adj: Dict[int, Dict[int, int]] = {a: {} for a in active_ancillas}
3211 bipartite_paulis = True
3212 for q in data_qubits:
3213 touching = [
3214 (a, anc_data_paulis[a][q])
3215 for a in active_ancillas
3216 if q in anc_data_paulis[a]
3217 ]
3218 if not touching:
3219 continue
3220 pauli_set = {p for _, p in touching}
3221 if len(pauli_set) > 2:
3222 bipartite_paulis = False
3223 break
3224 a0, p0 = touching[0]
3225 for ak, pk in touching[1:]:
3226 rel = 0 if pk == p0 else 1
3227 if ak in adj[a0] and adj[a0][ak] != rel:
3228 bipartite_paulis = False
3229 break
3230 adj[a0][ak] = rel
3231 adj[ak][a0] = rel
3232 if not bipartite_paulis:
3233 break
3234
3235 # 2-color connected components of active_ancillas
3236 color: Dict[int, int] = {}
3237 components: List[Tuple[List[int], List[int]]] = []
3238 if bipartite_paulis:
3239 for a in active_ancillas:
3240 if a in color:
3241 continue
3242 c0: List[int] = []
3243 c1: List[int] = []
3244 color[a] = 0
3245 queue = [a]
3246 idx_q = 0
3247 while idx_q < len(queue):
3248 u = queue[idx_q]
3249 idx_q += 1
3250 if color[u] == 0:
3251 c0.append(u)
3252 else:
3253 c1.append(u)
3254 for v, rel in adj[u].items():
3255 expected = color[u] ^ rel
3256 if v not in color:
3257 color[v] = expected
3258 queue.append(v)
3259 elif color[v] != expected:
3260 bipartite_paulis = False
3261 break
3262 if not bipartite_paulis:
3263 break
3264 if not bipartite_paulis:
3265 break
3266 components.append((c0, c1))
3267
3268 # Determine if all checks are already pure-X or pure-Z (standard CSS)
3269 all_pure_xz = True
3270 for a in active_ancillas:
3271 ps = set(anc_data_paulis[a].values())
3272 if not (ps == {"X"} or ps == {"Z"}):
3273 all_pure_xz = False
3274 break
3275
3276 # Detect color code (pure X and pure Z checks with identical support)
3277 native_basis = detect_basis(
3278 circuit, filepath=filepath, basis_arg=basis_arg
3279 )
3280 final_det_anc: Set[int] = set()
3281 for grp in multi_det_groups:
3282 if grp & data_qubits:
3283 final_det_anc.update(grp & set(active_ancillas))
3284
3285 x_ancillas: Set[int] = set()
3286 z_ancillas: Set[int] = set()
3287 rotated_data_qubits: Set[int] = set()
3288 is_css = False
3289 is_rotated_css = False
3290
3291 if checks_commute and bipartite_paulis and active_ancillas:
3292 if all_pure_xz:
3293 for a in active_ancillas:
3294 ps = set(anc_data_paulis[a].values())
3295 if ps == {"X"}:
3296 x_ancillas.add(a)
3297 else:
3298 z_ancillas.add(a)
3299 x_supps = {
3300 frozenset(anc_data_paulis[a].keys()) for a in x_ancillas
3301 }
3302 z_supps = {
3303 frozenset(anc_data_paulis[a].keys()) for a in z_ancillas
3304 }
3305 is_color_code = (
3306 len(x_supps) > 0 and x_supps == z_supps
3307 and len(x_ancillas) == len(z_ancillas)
3308 )
3309 is_css = not is_color_code
3310 is_rotated_css = False
3311 else:
3312 is_css = True
3313 is_rotated_css = True
3314 for c0, c1 in components:
3315 # Score which sector is the primary memory basis
3316 def score_primary(sector: List[int]) -> Tuple[int, int, int]:
3317 r1_cnt = sum(1 for a in sector if a in r1_single_det_anc)
3318 fin_cnt = sum(1 for a in sector if a in final_det_anc)
3319 init_cnt = sum(
3320 1 for a in sector
3321 if all(
3322 anc_data_paulis[a][q] == data_init_basis.get(q, "Z")
3323 for q in anc_data_paulis[a]
3324 )
3325 )
3326 return (r1_cnt, fin_cnt, init_cnt)
3327
3328 sc0 = score_primary(c0)
3329 sc1 = score_primary(c1)
3330 if sc0 >= sc1:
3331 primary_sec, minority_sec = c0, c1
3332 else:
3333 primary_sec, minority_sec = c1, c0
3334
3335 if native_basis == "Z":
3336 z_ancillas.update(primary_sec)
3337 x_ancillas.update(minority_sec)
3338 else:
3339 x_ancillas.update(primary_sec)
3340 z_ancillas.update(minority_sec)
3341
3342 # Determine rotated data qubits (where check Pauli != sector label)
3343 for q in data_qubits:
3344 for a in active_ancillas:
3345 if q in anc_data_paulis[a]:
3346 sector_label = "X" if a in x_ancillas else "Z"
3347 if anc_data_paulis[a][q] != sector_label:
3348 rotated_data_qubits.add(q)
3349 break
3350 else:
3351 # Non-CSS fallback classification by majority Pauli weight
3352 is_css = False
3353 is_rotated_css = False
3354 for a in active_ancillas:
3355 s = anc_data_paulis[a]
3356 nx = sum(1 for p in s.values() if p == "X")
3357 nz = sum(1 for p in s.values() if p == "Z")
3358 if nz > nx:
3359 z_ancillas.add(a)
3360 else:
3361 x_ancillas.add(a)
3362
3363 return {
3364 "x_ancillas": sorted(list(x_ancillas)),
3365 "z_ancillas": sorted(list(z_ancillas)),
3366 "data_qubits": sorted(list(data_qubits)),
3367 "routing_qubits": sorted(list(routing_qubits)),
3368 "rotated_data_qubits": sorted(list(rotated_data_qubits)),
3369 "is_css": is_css,
3370 "is_rotated_css": is_rotated_css,
3371 "basis": native_basis,
3372 "rewritten_circuit": rewritten_circ,
3373 }
3374
3375
3377 circuit: Any,
3378 verbose: bool = False,
3379 basis_arg: Optional[str] = None,
3380 filepath: Optional[Union[str, Path]] = None
3381) -> Dict[str, Any]:
3382 """Verifies qubit classification and returns basis tracking metadata."""
3384 circuit, verbose=verbose, basis_arg=basis_arg, filepath=filepath
3385 )
3386
3387
3389 circuit: Any,
3390 basis: str,
3391 verbose: bool = False,
3392 thorough: bool = True,
3393 thorough_res: Optional[Dict[str, Any]] = None,
3394 mask_out: Optional[List[bool]] = None
3395) -> Tuple[Any, int, int]:
3396 """Strips detectors corresponding to the minority basis from a Stim circuit.
3397
3398 Always uses ancilla and data basis tracking (and dynamic carrier tracking
3399 across SWAPs and syndrome handoffs) to identify each detector's basis.
3400
3401 Args:
3402 circuit: A stim.Circuit object or path to a .stim file.
3403 basis: Primary memory basis ('X' or 'Z'). Detectors of the opposite
3404 basis are removed.
3405 verbose: Whether to print warnings/statistics.
3406 thorough: Kept for API compatibility (basis tracking is always enabled).
3407 thorough_res: Optional precomputed result from classify_qubits_thorough.
3408 mask_out: Optional list, extended by one entry per detector of the circuit (in the order of the detectors of
3409 its DEM, REPEAT blocks unrolled): True if the detector is stripped, False if it is kept.
3410
3411 Returns:
3412 Tuple of (simplified_circuit, stripped_count, kept_count).
3413 """
3414 stim = _get_stim()
3415 if isinstance(circuit, (str, Path)):
3416 circuit = stim.Circuit.from_file(str(circuit))
3417
3418 t_res = (
3419 thorough_res
3420 if thorough_res is not None
3421 else classify_qubits_thorough(circuit, verbose=False, basis_arg=basis)
3422 )
3423 x_anc = set(t_res["x_ancillas"])
3424 z_anc = set(t_res["z_ancillas"])
3425 minority_basis = "Z" if basis.upper() == "X" else "X"
3426
3427 meas_carriers = track_circuit_carriers(circuit)
3428
3429 # Coordinate metadata for consistency check on standard (non-rotated) CSS
3430 qubit_coords: Dict[int, Tuple[float, ...]] = {}
3431
3432 def get_coords(blk: Any) -> None:
3433 for inst in blk:
3434 if isinstance(inst, stim.CircuitRepeatBlock):
3435 get_coords(inst.body_copy())
3436 elif inst.name == "QUBIT_COORDS":
3437 args = inst.gate_args_copy()
3438 for t in inst.targets_copy():
3439 if t.is_qubit_target:
3440 qubit_coords[t.value] = tuple(float(x) for x in args)
3441
3442 get_coords(circuit)
3443 x_coords = {
3444 qubit_coords[q][:2]
3445 for q in x_anc
3446 if q in qubit_coords and len(qubit_coords[q]) >= 2
3447 }
3448 z_coords = {
3449 qubit_coords[q][:2]
3450 for q in z_anc
3451 if q in qubit_coords and len(qubit_coords[q]) >= 2
3452 }
3453 layout_well_defined = (
3454 not t_res.get("is_rotated_css", False)
3455 and len(x_coords) > 0
3456 and len(z_coords) > 0
3457 and x_coords.isdisjoint(z_coords)
3458 )
3459
3460 meas_history: List[int] = []
3461 det_mask: List[bool] = [] # one entry per detector (REPEAT blocks unrolled): True if stripped
3462 stripped_count = 0
3463 kept_count = 0
3464 contradictions_c_count = 0
3465 floquet_suspected = False
3466
3467 def process_block(block: Any) -> Any:
3468 nonlocal stripped_count, kept_count
3469 nonlocal contradictions_c_count, floquet_suspected
3470 new_block = stim.Circuit()
3471 for inst in block:
3472 if isinstance(inst, stim.CircuitRepeatBlock):
3473 rep_count = inst.repeat_count
3474 if rep_count <= 0:
3475 continue
3476 body_copy = inst.body_copy()
3477 meas_before = len(meas_history)
3478 mask_before = len(det_mask)
3479 strip_before = stripped_count
3480 kept_before = kept_count
3481 new_body = process_block(body_copy)
3482 meas_after = len(meas_history)
3483 meas_per_rep = meas_after - meas_before
3484 if rep_count > 1:
3485 stripped_count += (stripped_count - strip_before) * (
3486 rep_count - 1
3487 )
3488 kept_count += (kept_count - kept_before) * (rep_count - 1)
3489 det_mask.extend(det_mask[mask_before:] * (rep_count - 1))
3490 if meas_per_rep > 0:
3491 meas_history.extend(
3492 meas_history[meas_before:meas_after]
3493 * (rep_count - 1)
3494 )
3495 new_block.append(stim.CircuitRepeatBlock(rep_count, new_body))
3496 elif inst.name in [
3497 "M", "MX", "MY", "MZ", "MR", "MRX", "MRY", "MRZ"
3498 ]:
3499 if inst.name in ["MY", "MRY"]:
3500 floquet_suspected = True
3501 for t in inst.targets_copy():
3502 if t.is_qubit_target:
3503 meas_history.append(t.value)
3504 new_block.append(inst)
3505 elif inst.name == "DETECTOR":
3506 cur_meas = len(meas_history)
3507 rec_targets = [
3508 t for t in inst.targets_copy()
3509 if t.is_measurement_record_target
3510 ]
3511 if not rec_targets:
3512 stripped_count += 1
3513 det_mask.append(True)
3514 continue
3515
3516 qubits = [
3517 meas_history[cur_meas + t.value]
3518 for t in rec_targets
3519 if 0 <= cur_meas + t.value < cur_meas
3520 ]
3521 is_x_qubit = any(q in x_anc for q in qubits)
3522 is_z_qubit = any(q in z_anc for q in qubits)
3523 class_qubit = (
3524 "Both" if (is_x_qubit and is_z_qubit)
3525 else "X" if is_x_qubit
3526 else "Z" if is_z_qubit
3527 else "Unknown"
3528 )
3529
3530 args = inst.gate_args_copy()
3531 class_b = "Unknown"
3532 if layout_well_defined and args and len(args) >= 2:
3533 det_xy = (float(args[0]), float(args[1]))
3534 if det_xy in x_coords:
3535 class_b = "X"
3536 elif det_xy in z_coords:
3537 class_b = "Z"
3538
3539 class_c = "Unknown"
3540 if meas_carriers:
3541 carriers = [
3542 c
3543 for t in rec_targets
3544 if 0 <= cur_meas + t.value < len(meas_carriers)
3545 for c in meas_carriers[cur_meas + t.value]
3546 ]
3547 is_x_c = any(c in x_anc for c in carriers)
3548 is_z_c = any(c in z_anc for c in carriers)
3549 class_c = (
3550 "Both" if (is_x_c and is_z_c)
3551 else "X" if is_x_c
3552 else "Z" if is_z_c
3553 else "Unknown"
3554 )
3555
3556 is_minority = False
3557 if class_c != "Unknown":
3558 if class_c == "Both":
3559 contradictions_c_count += 1
3560 floquet_suspected = True
3561 elif class_b != "Unknown" and class_c != class_b:
3562 contradictions_c_count += 1
3563 elif len(rec_targets) == 1 and class_c == minority_basis:
3564 contradictions_c_count += 1
3565 is_minority = (class_c == minority_basis)
3566 elif layout_well_defined and class_b != "Unknown":
3567 is_minority = (class_b == minority_basis)
3568 elif len(rec_targets) == 1:
3569 is_minority = False
3570 else:
3571 if class_qubit == "Both":
3572 floquet_suspected = True
3573 is_minority = (class_qubit == minority_basis)
3574
3575 if is_minority:
3576 stripped_count += 1
3577 else:
3578 kept_count += 1
3579 new_block.append(inst)
3580 det_mask.append(is_minority)
3581 else:
3582 new_block.append(inst)
3583 return new_block
3584
3585 simp_circuit = process_block(circuit)
3586 if mask_out is not None:
3587 mask_out.extend(det_mask)
3588
3589 if contradictions_c_count > 0 and verbose:
3590 sys.stderr.write(
3591 f"# Warning: {contradictions_c_count} detector contradiction(s) "
3592 "detected during dynamic carrier tracking.\n"
3593 )
3594 if floquet_suspected and verbose:
3595 sys.stderr.write(
3596 "# Warning: Floquet or dynamic measurement structure suspected; "
3597 "simplified circuit distance may be invalid.\n"
3598 )
3599
3600 return simp_circuit, stripped_count, kept_count
3601
3602
3603def _matrix_cache_key(H: Any, G: Optional[Any] = None, L: Optional[Any] = None, classical: bool = False) -> str:
3604 """
3605 Cache key of a classical code (H), or of a quantum code given by H with G (or with L). The two sectors of a CSS
3606 code are stored as the quantum codes (H=Hx, G=Hz) -> dZ and (H=Hz, G=Hx) -> dX (with L=Lx / L=Lz if given), so
3607 that compute_css_distance() and compute_quantum_distance() share their cache records.
3608 """
3609 h_state = get_sparse_array_state(H)
3610 if classical:
3611 return f"classical:{h_state}"
3612 if G is not None:
3613 return f"quantum:H={h_state}:G={get_sparse_array_state(G)}"
3614 return f"quantum:H={h_state}:L={get_sparse_array_state(L)}"
3615
3616
3617def _dem_cache_key(dem: Any, pmin: float = 0.0) -> str:
3618 """Cache key of a DEM (a stim.DetectorErrorModel or a .dem file) with the probability cutoff pmin."""
3619 dem_state = get_sparse_array_state(dem)
3620 return f"dem:{dem_state}" if pmin <= 0.0 else f"dem:{dem_state}:pmin={pmin}"
3621
3622
3623def _write_dem_file(dem: Any, path: str) -> None:
3624 """Writes a stim.DetectorErrorModel (flattened) or the text of a DEM to the file `path`."""
3625 if hasattr(dem, 'flattened'):
3626 dem.flattened().to_file(path)
3627 elif hasattr(dem, 'to_file'):
3628 dem.to_file(path)
3629 else:
3630 with open(path, 'w') as f:
3631 f.write(str(dem))
3632
3633
3634def _dem_error_columns(dem: Any, pmin: float = 0.0, with_prob: bool = False) -> List[Tuple[Any, ...]]:
3635 """
3636 The columns of a DEM (a stim.DetectorErrorModel, or a .dem file) as read by the dist_m4ri binary: the detector and
3637 observable sets of its error mechanisms, in their order in the flattened DEM (repeat blocks unrolled, with
3638 shift_detectors), after the probability cutoff pmin (an error with probability < pmin is skipped). The components
3639 of a decomposed error (separated by '^') are summed mod 2, so that a target listed twice cancels. With
3640 with_prob=True, each column also has the probability of the error, as written: (detectors, observables, p).
3641 """
3642 if isinstance(dem, (str, Path)):
3643 with open(str(dem)) as f:
3644 lines = f.read().splitlines()
3645 else:
3646 lines = str(dem.flattened() if hasattr(dem, 'flattened') else dem).splitlines()
3647 cols: List[Tuple[Any, ...]] = []
3648
3649 def block(i: int, shift: int) -> Tuple[int, int]:
3650 # runs the instructions from line i up to the closing brace (or the end): (next line, detector shift)
3651 while i < len(lines):
3652 s = lines[i].split('#', 1)[0].strip()
3653 i += 1
3654 if not s:
3655 continue
3656 if s.startswith('}'):
3657 return i, shift
3658 if s.startswith('repeat'):
3659 count = int(s.split()[1])
3660 body, end = i, i
3661 for _ in range(count):
3662 end, shift = block(body, shift)
3663 if count <= 0: # skip the body
3664 depth = 1
3665 while end < len(lines) and depth > 0:
3666 t = lines[end].split('#', 1)[0].strip()
3667 end += 1
3668 depth += t.startswith('repeat') - t.startswith('}')
3669 i = end
3670 elif s.startswith('shift_detectors'):
3671 shift += int(s.rsplit(None, 1)[-1])
3672 elif s.startswith('error'):
3673 p_text = s[s.index('(') + 1:s.index(')')].strip()
3674 if float(p_text) < pmin:
3675 continue
3676 dets: Set[int] = set()
3677 obs: Set[int] = set()
3678 for tok in s[s.index(')') + 1:].split():
3679 if tok[0] == 'D':
3680 dets ^= {int(tok[1:]) + shift}
3681 elif tok[0] == 'L':
3682 obs ^= {int(tok[1:])}
3683 col = (frozenset(dets), frozenset(obs))
3684 cols.append(col + (p_text,) if with_prob else col)
3685 return i, shift
3686
3687 block(0, 0)
3688 return cols
3689
3690
3691def _drop_mode(drop: Any) -> str:
3692 """
3693 The drop option of compute_dem_distance(): "auto" (for None, True, 1, "1", or "auto"), "0" (False, 0, or "0"),
3694 "css", or the name of an existing file (ValueError otherwise).
3695 """
3696 if drop is None or drop is True:
3697 return "auto"
3698 if drop is False:
3699 return "0"
3700 if isinstance(drop, numbers.Integral):
3701 if drop in (0, 1):
3702 return "auto" if drop == 1 else "0"
3703 raise ValueError(f"invalid drop={drop}: use 0, 1, 'auto', 'css', or a file with the detectors to drop")
3704 s = str(drop).strip()
3705 if s.lower() in ("1", "auto"):
3706 return "auto"
3707 if s == "0":
3708 return "0"
3709 if s.lower() == "css":
3710 return "css"
3711 if not os.path.isfile(s):
3712 raise ValueError(f"drop={s}: no such file (use 0, 1, auto, css, or a file with the detectors to drop)")
3713 return s
3714
3715
3716def _reduced_out_prefix(spec: Union[bool, str, Path], src: str, out_dir: Optional[Union[str, Path]] = None) -> str:
3717 """
3718 The prefix PREFIX of the files PREFIX_red.dem and PREFIX_red.json of compute_dem_distance(out_reduced=spec) for the
3719 input src (a file name, or e.g. 'stim.Circuit'): for True (also '', '1', 'true', 'yes', 'auto'), the basename of
3720 the input file (without a suffix _full or _simp; 'dem' for an object); a bare name goes to out_dir (default: the
3721 directory of the input file, as for --out-dem), and a path is used as given.
3722 """
3723 src_path = Path(src) if os.path.exists(src) else None
3724 eff_dir = Path(out_dir) if out_dir is not None else (src_path.parent if src_path is not None else Path("."))
3725 if spec is True or (isinstance(spec, str) and spec.strip().lower() in ("", "1", "true", "yes", "auto")):
3726 stem = src_path.stem if src_path is not None else "dem"
3727 if stem.endswith(("_full", "_simp")):
3728 stem = stem[:-5]
3729 return str(eff_dir / stem)
3730 spec_s = str(spec)
3731 return str(eff_dir / spec_s) if (not os.path.isabs(spec_s) and os.path.dirname(spec_s) == "") else spec_s
3732
3733
3734def read_drop_file(path: Union[str, Path]) -> Tuple[List[int], Optional[int]]:
3735 """
3736 Reads a drop file: the 0-based detector indices (k for Dk) separated by white space or commas, where lines starting
3737 with '#' or '%' are comments, e.g., the detectors dropped from a DEM written by the binary (outD=FILE).
3738
3739 Returns:
3740 (detectors, num_detectors): the sorted distinct detectors, and the number of detectors of the DEM from the
3741 comment line '# N of M detectors dropped ...' written by the binary (None if there is none).
3742 """
3743 import re
3744 dets: Set[int] = set()
3745 num: Optional[int] = None
3746 with open(str(path)) as f:
3747 for line in f:
3748 s = line.strip()
3749 if s.startswith('#') or s.startswith('%'):
3750 mm = re.match(r"#\s*\d+ of (\d+) detectors dropped", s)
3751 if mm and num is None:
3752 num = int(mm.group(1))
3753 continue
3754 dets.update(int(tok) for tok in s.replace(',', ' ').split())
3755 return sorted(dets), num
3756
3757
3759 dem: Any,
3760 dropped: Sequence[int],
3761 prefix: Union[str, Path],
3762 pmin: float = 0.0,
3763 num_detectors: Optional[int] = None,
3764 meta: Optional[Dict[str, Any]] = None
3765) -> Tuple[str, str]:
3766 """
3767 Writes the DEM (a stim.DetectorErrorModel or a .dem file) without the detectors `dropped` (0-based, e.g., those
3768 dropped by the binary, see compute_dem_distance(outD=..., out_reduced=...)) to PREFIX_red.dem: the errors which
3769 touch none of them (after the cutoff pmin, in the order of the flattened DEM; a decomposed error as the sum of its
3770 components), with the other detectors numbered consecutively, and the observables unchanged. The maps go to
3771 PREFIX_red.json: "detectors" (the original index of each detector of the reduced DEM, i.e., the complement of
3772 "dropped"), "errors" (the original index of each error of the reduced DEM, 0-based in the order of the flattened DEM
3773 after the cutoff pmin, as the columns of the codewords in outC), "dropped", "num_detectors", "num_errors", "pmin",
3774 and the entries of meta. If dropping the detectors is exact (as verified by the binary), the reduced DEM has the
3775 same distance, and its codewords map to codewords of the DEM with "errors".
3776
3777 Returns:
3778 (dem_path, json_path): the files written.
3779 """
3780 cols = _dem_error_columns(dem, pmin, with_prob=True)
3781 drop_set = set(int(r) for r in dropped)
3782 m = 1 + max([d for S, _, _ in cols for d in S] + list(drop_set), default=-1)
3783 if num_detectors is not None:
3784 m = max(m, int(num_detectors))
3785 k = 1 + max((o for _, O, _ in cols for o in O), default=-1)
3786 kept = [r for r in range(m) if r not in drop_set]
3787 new_index = {r: i for i, r in enumerate(kept)}
3788 dem_path, json_path = f"{prefix}_red.dem", f"{prefix}_red.json"
3789 errors: List[int] = []
3790 lines = [f"# reduced DEM: {len(drop_set)} of {m} detectors dropped (dist_m4ri.py {__version__}); the maps to the "
3791 f"original DEM are in {os.path.basename(json_path)}"]
3792 max_det = max_obs = -1
3793 for j, (S, O, p) in enumerate(cols):
3794 if S & drop_set:
3795 continue
3796 dets = sorted(new_index[d] for d in S)
3797 obs = sorted(O)
3798 if dets:
3799 max_det = max(max_det, dets[-1])
3800 if obs:
3801 max_obs = max(max_obs, obs[-1])
3802 lines.append(f"error({p})" + "".join(f" D{d}" for d in dets) + "".join(f" L{o}" for o in obs))
3803 errors.append(j)
3804 if max_det < len(kept) - 1: # (keeps the number of detectors and observables)
3805 lines.append(f"detector D{len(kept) - 1}")
3806 if max_obs < k - 1:
3807 lines.append(f"logical_observable L{k - 1}")
3808 parent_dir = os.path.dirname(os.path.abspath(dem_path))
3809 os.makedirs(parent_dir, exist_ok=True)
3810 with open(dem_path, "w") as f:
3811 f.write("\n".join(lines) + "\n")
3812 info: Dict[str, Any] = {"__version__": __version__}
3813 info.update(meta or {})
3814 info.update({"pmin": pmin, "num_detectors": m, "num_errors": len(cols), "dropped": sorted(drop_set),
3815 "detectors": kept, "errors": errors})
3816 with open(json_path, "w") as f:
3817 json.dump(info, f, separators=(",", ":"))
3818 f.write("\n")
3819 return dem_path, json_path
3820
3821
3823 cws: List[List[int]],
3824 dem: Any,
3825 pmin: float = 0.0,
3826 maxC: int = 0,
3827 verbose: bool = False
3828) -> List[List[int]]:
3829 """
3830 The expanded list of the DEM codewords cws (0-based columns of the DEM, see _dem_error_columns()): every version
3831 over the choices among equal columns (the same detectors and observables), which the binary merges (merge=1).
3832 Each codeword is mapped to the first columns of its sets of equal columns (two equal columns cancel), and every
3833 distinct result c is expanded into the prod_{j in c} |set(j)| codewords with one column from each set (sorted, in
3834 the order of cws). With maxC > 0, at most maxC codewords are returned, with a warning to stderr if some are
3835 dropped.
3836 """
3837 if not cws:
3838 return []
3839 cols = _dem_error_columns(dem, pmin)
3840 sets: Dict[Tuple[frozenset, frozenset], List[int]] = {}
3841 for j, c in enumerate(cols):
3842 sets.setdefault(c, []).append(j)
3843 members = [sets[c] for c in cols] # the sorted set of the columns equal to column j (j included)
3844 keys: List[Tuple[int, ...]] = []
3845 seen: Set[Tuple[int, ...]] = set()
3846 for cw in cws:
3847 if any(j < 0 or j >= len(members) for j in cw):
3848 raise ValueError(f"expand: codeword column out of range (the DEM has {len(members)} columns after the "
3849 f"pmin={pmin} cutoff)")
3850 odd: Set[int] = set()
3851 for j in cw:
3852 odd ^= {members[j][0]}
3853 key = tuple(sorted(odd))
3854 if key and key not in seen:
3855 seen.add(key)
3856 keys.append(key)
3857 total = 0
3858 for key in keys:
3859 num = 1
3860 for r in key:
3861 num *= len(members[r])
3862 total += num
3863 limit = total if maxC <= 0 else min(total, maxC)
3864 out: List[List[int]] = []
3865 for key in keys:
3866 if len(out) >= limit:
3867 break
3868 for choice in itertools.product(*(members[r] for r in key)):
3869 out.append(sorted(choice))
3870 if len(out) >= limit:
3871 break
3872 if limit < total:
3873 sys.stderr.write(f"# Warning: expand: the expanded list has {total} codewords ({len(keys)} up to equal "
3874 f"columns); only the first maxC={maxC} are written (the list is incomplete)\n")
3875 elif verbose:
3876 print(f"[dist_m4ri] expand: {len(keys)} codewords up to equal columns -> {total} codewords")
3877 return out
3878
3879
3880def _css_sector_codes(Hx: Any, Hz: Any, Lx: Optional[Any], Lz: Optional[Any]) -> Dict[str, Tuple[Any, Any, Any]]:
3881 """The quantum codes (H, G, L) of the two sectors of a CSS code: Z -> (Hx, Hz or Lx), X -> (Hz, Hx or Lz)."""
3882 return {
3883 "Z": (Hx, Hz if Lx is None else None, Lx),
3884 "X": (Hz, Hx if Lz is None else None, Lz),
3885 }
3886
3887
3889 x: Tuple[int, int, int], z: Tuple[int, int, int], dmax: int = 0
3890) -> Tuple[int, int, int, int]:
3891 """
3892 Bounds of the CSS distance d = min(dX, dZ) from the sector bounds x, z = (dmin, dmax, rw_steps), and an upper
3893 bound `dmax` on d supplied by the user (0: none).
3894
3895 Returns:
3896 (dist, dmin, dmax, rw_steps): dist is the smaller sector distance (see _dist_from_bounds(); the larger one if
3897 a sector distance is 0), at most the user's dmax; dmin = min(dmin_X, dmin_Z); dmax is the smallest of the
3898 positive upper bounds (0: none).
3899 """
3900 d_lo = min(x[0], z[0])
3901 uppers = [v for v in (x[1], z[1], dmax) if v > 0]
3902 d_hi = min(uppers) if uppers else 0
3903 if d_hi > 0 and d_lo > d_hi:
3904 d_lo = d_hi
3905 dist_x, dist_z = _dist_from_bounds(x[0], x[1]), _dist_from_bounds(z[0], z[1])
3906 dist = min(dist_x, dist_z) if (dist_x > 0 and dist_z > 0) else max(dist_x, dist_z)
3907 if dmax > 0 and dist > dmax:
3908 dist = dmax
3909 if d_lo > 0 and d_lo == d_hi:
3910 dist = d_lo
3911 return dist, d_lo, d_hi, x[2] + z[2]
3912
3913
3914def _sector_record(dmin: int, dmax: int, rw_steps: int, cws: List[List[int]]) -> Dict[str, Any]:
3915 """A cache record of a classical or quantum code (or of a CSS sector) with the bounds [dmin, dmax]."""
3916 if dmax > 0 and dmin > dmax:
3917 dmin = dmax
3918 return {
3919 "dist": _dist_from_bounds(dmin, dmax),
3920 "dmin": dmin,
3921 "dmax": dmax,
3922 "rw_steps": rw_steps,
3923 "d_info": format_bounds_list(dmin, dmax, rw_steps),
3924 "cws": cws,
3925 }
3926
3927
3929 Hx: Any, Hz: Any, Lx: Optional[Any] = None, Lz: Optional[Any] = None, eff_cache_file: Optional[str] = None
3930) -> bool:
3931 """
3932 Converts the CSS cache records written before version 0.12.0 (key 'css:X=<Hx>:Z=<Hz>[:Lx=<Lx>:Lz=<Lz>]', also
3933 with a ':start=...' suffix) into the records of the two sectors (see _matrix_cache_key()), and removes them.
3934
3935 The lower bounds dmin_X and dmin_Z are kept. An upper bound dmax_X or dmax_Z is kept only if a stored codeword
3936 confirms it, or if it is smaller than the upper bound of the other sector: earlier versions passed an upper bound
3937 dmax given to compute_css_distance() (a bound on min(dX, dZ)) to both sectors, where it was reported back as the
3938 upper bound of the sector, which is not valid for the sector with the larger distance. Otherwise, the weight of
3939 the lightest stored codeword (if any) is used.
3940
3941 Returns:
3942 True if a record was converted (the cache file, if any, is then saved).
3943 """
3944 global _distance_cache
3945 old = f"css:X={get_sparse_array_state(Hx)}:Z={get_sparse_array_state(Hz)}"
3946 if Lx is not None or Lz is not None:
3947 old = f"{old}:Lx={get_sparse_array_state(Lx)}:Lz={get_sparse_array_state(Lz)}"
3948 old_keys = [k for k in list(_distance_cache) if k == old or k.startswith(old + ":start=")]
3949 if not old_keys:
3950 return False
3951 codes = _css_sector_codes(Hx, Hz, Lx, Lz)
3952 base = {s: _matrix_cache_key(*codes[s]) for s in ("X", "Z")}
3953 for key in old_keys:
3954 rec = _distance_cache.pop(key)
3955 if not isinstance(rec, dict):
3956 continue
3957 sfx = key[len(old):]
3958 for s, other in (("X", "Z"), ("Z", "X")):
3959 dmin = int(rec.get(f"dmin_{s}", rec.get("dmin", 0)) or 0)
3960 dmax = int(rec.get(f"dmax_{s}", 0) or 0)
3961 other_dmax = int(rec.get(f"dmax_{other}", 0) or 0)
3962 cws = [list(cw) for cw in (rec.get(f"cws_{s}") or [])]
3963 w_cw = min((len(cw) for cw in cws), default=0)
3964 if dmax > 0 and w_cw != dmax and not (dmax < other_dmax):
3965 dmax = 0 # possibly the upper bound on min(dX, dZ) reported back by the binary
3966 if w_cw > 0:
3967 dmax = min(dmax, w_cw) if dmax > 0 else w_cw
3968 rw = int(rec.get(f"rw_steps_{s}", 0) or 0)
3969 skey = base[s] + sfx
3970 prev = _distance_cache.get(skey)
3971 if prev:
3972 p_dmax = prev.get("dmax", 0) or 0
3973 dmax = min(p_dmax, dmax) if (p_dmax > 0 and dmax > 0) else max(p_dmax, dmax)
3974 dmin = max(prev.get("dmin", 0) or 0, dmin)
3975 rw = max(prev.get("rw_steps", 0) or 0, rw)
3976 cws = _union_cws(prev.get("cws", []), cws)
3977 _distance_cache[skey] = _sector_record(dmin, dmax, rw, cws)
3978 if eff_cache_file:
3979 save_distance_cache(eff_cache_file)
3980 return True
3981
3982
3984 H: Any,
3985 G: Optional[Any],
3986 L: Optional[Any],
3987 classical: bool,
3988 kind: str,
3989 dist_m4ri: Optional[str],
3990 method: int,
3991 threads: Optional[int],
3992 timeout: float,
3993 num_steps: Optional[int],
3994 d_exp: int,
3995 dmin: int,
3996 dmax: int,
3997 dstop: int,
3998 wmin: int,
3999 wmax: int,
4000 smax: Optional[int],
4001 start_list: Optional[List[int]],
4002 dW: int,
4003 maxC: int,
4004 finC: Optional[str],
4005 outC: Optional[str],
4006 need_cws: bool,
4007 eff_cache_file: Optional[str],
4008 seed: int,
4009 debug: int,
4010 verbose: bool,
4011 nothrottle: bool,
4012 chunk_size: int,
4013 ksub: int,
4014 kwin: int,
4015 win_mode: int,
4016 min_hits: Optional[int],
4017 cov_cws: int,
4018 refresh: Optional[int],
4019 trust_start: bool,
4020 fdem: Optional[Any] = None,
4021 pmin: float = 0.0,
4022 merge: bool = True,
4023 key_fn: Optional[Callable[[], str]] = None,
4024 cws_out_fn: Optional[Callable[[List[List[int]]], List[List[int]]]] = None,
4025 fdrop: Optional[Union[str, Path, List[int]]] = None,
4026 outD: Optional[str] = None,
4027 sort_rows: Optional[bool] = None,
4028 win_hyb: int = 0,
4029 pairs: int = -1,
4030 cc_prune: Optional[int] = None,
4031 ortho: bool = True
4032) -> Dict[str, Any]:
4033 """
4034 Distance of a classical code (H), of a quantum code (H with G or L), or of a DEM (fdem: a stim DEM object or a .dem
4035 file, with the probability cutoff pmin, merge, the detectors to drop fdrop ("auto", "0", a file, or a list), and
4036 the file outD for the detectors dropped: see run_dist_m4ri()) with the dist_m4ri binary, using the distance cache
4037 shared by compute_classical_distance(), compute_quantum_distance(), the two sectors of compute_css_distance(), and
4038 compute_dem_distance() (the warnings for the expert options are issued by the callers). The row order sort_rows
4039 and the CC search cc_prune (see run_dist_m4ri()) do not change the result, nor the cache key; nor do the RW window
4040 size win_hyb, the RW pairs (pairs), and the expert option ortho (False: the binary does not check H G^T = 0) change
4041 the cache key.
4042
4043 A cache record (key: key_fn() if given, otherwise see _matrix_cache_key()) answers the request if it holds the
4044 exact distance, or with the stop target dstop > 0 a lower bound dmin >= dstop (and the codewords, if needed).
4045 Otherwise, the binary is run, starting from the cached bounds, and the merged bounds are stored. The returned
4046 codewords (those of the run merged with the cached ones, or the cached ones) are written to outC; with finC
4047 identical to outC, the codewords already in the file are kept. If given, cws_out_fn transforms the codewords
4048 returned and written to outC (e.g., the expansion over equal DEM columns, see _expand_equal_columns()); the cache
4049 keeps the codewords as found.
4050
4051 Returns:
4052 dict with the distance "dist" (see _dist_from_bounds()), the bounds "dmin", "dmax", "rw_steps", "d_info"
4053 (see format_bounds_list()), the codewords "cws" (empty unless need_cws), and "cached" (True if no binary run).
4054 """
4055 global _distance_cache, _last_run_stats
4056 _last_run_stats = {} # the statistics of the binary run (none if the result is cached)
4057 same_file = bool(finC and outC) and os.path.abspath(str(finC)) == os.path.abspath(str(outC))
4058 finC = check_finc_outc(finC, outC, verbose=verbose)
4059
4060 def write_out(cws_res: List[List[int]]) -> List[List[int]]:
4061 # the codewords returned (transformed by cws_out_fn) and written to outC; with finC identical to outC, the
4062 # codewords already in the file are kept
4063 if cws_out_fn is not None and cws_res:
4064 cws_res = cws_out_fn(cws_res)
4065 if outC and cws_res:
4066 out = cws_res
4067 if same_file and os.path.isfile(outC):
4068 out = _union_cws(read_sparse_vectors(outC), cws_res)
4069 _write_nzlist_file(outC, out)
4070 return cws_res
4071
4072 code_key = cached_entry = plain_key = plain_entry = None
4073 hit: Optional[Dict[str, Any]] = None # the result of a cache hit
4074 eff_dmin, eff_dmax = dmin, dmax
4075 if _use_distance_cache:
4076 if eff_cache_file:
4077 load_distance_cache(eff_cache_file)
4078 try:
4079 plain_key = key_fn() if key_fn is not None else _matrix_cache_key(H, G, L, classical)
4080 code_key, cached_entry, plain_entry = _resolve_start_cache(
4081 plain_key, start_list, lambda e: _single_entry_is_exact(e, need_cws, dstop), trust_start,
4082 lambda e: _merge_start_into_plain(plain_key, e, True), eff_cache_file, verbose
4083 )
4084 eff_dmin, eff_dmax = _seed_bounds_from_entry(eff_dmin, eff_dmax, plain_entry)
4085 if cached_entry is not None:
4086 c_dmin, c_dmax = cached_entry.get("dmin", 0) or 0, cached_entry.get("dmax", 0) or 0
4087 if _single_entry_is_exact(cached_entry, need_cws, dstop):
4088 c_rw = cached_entry.get("rw_steps", 0) or 0
4089 d_info = format_bounds_list(c_dmin, c_dmax, c_rw)
4090 found = "exact distance" if (c_dmin > 0 and c_dmin == c_dmax) else f"dmin={c_dmin} >= dstop={dstop}"
4091 if verbose:
4092 print(f"[dist_m4ri] Cache retrieval: SUCCESS (found cached {found} for '{code_key}')")
4093 print(f"[dist_m4ri] Cached result: dist={_dist_from_bounds(c_dmin, c_dmax)}, "
4094 f"bounds={format_bounds_str(d_info)}")
4095 elif debug & PY_DBG_CACHE:
4096 print(f"[dist_m4ri] Cache hit for {kind} distance ({found} known)!")
4097 cws_res = [list(cw) for cw in cached_entry.get("cws", [])] if need_cws else []
4098 hit = {"dist": _dist_from_bounds(c_dmin, c_dmax), "dmin": c_dmin, "dmax": c_dmax,
4099 "rw_steps": c_rw, "d_info": d_info, "cws": cws_res, "cached": True}
4100 else:
4101 if verbose:
4102 print(f"[dist_m4ri] Cache retrieval: PARTIAL (cached bounds: dmin={c_dmin}, dmax={c_dmax}, "
4103 f"rw_steps={cached_entry.get('rw_steps', 0)}; continuing search)")
4104 eff_dmin, eff_dmax = _seed_bounds_from_entry(eff_dmin, eff_dmax, cached_entry)
4105 elif verbose:
4106 print(f"[dist_m4ri] Cache retrieval: MISS (no entry for '{code_key}')")
4107 except Exception:
4108 code_key = cached_entry = plain_key = plain_entry = hit = None
4109 elif verbose:
4110 print("[dist_m4ri] Cache retrieval: DISABLED (cache is turned off)")
4111 if hit is not None:
4112 hit["cws"] = write_out(hit["cws"])
4113 return hit
4114
4115 if wmax > 0 and eff_dmin > max(wmax, dmin): # a cached lower bound beyond wmax (the binary needs dmin <= wmax)
4116 src = cached_entry or plain_entry
4117 if not (method & 1) and src is not None: # CC only: all weights up to wmax have been analyzed already
4118 c_dmin, c_dmax, c_rw = src.get("dmin", 0) or 0, src.get("dmax", 0) or 0, src.get("rw_steps", 0) or 0
4119 if verbose:
4120 print(f"[dist_m4ri] Cached lower bound dmin={c_dmin} > wmax={wmax}: no CC round needed")
4121 cws_res = write_out([list(cw) for cw in src.get("cws", [])] if need_cws else [])
4122 return {"dist": _dist_from_bounds(c_dmin, c_dmax), "dmin": c_dmin, "dmax": c_dmax, "rw_steps": c_rw,
4123 "d_info": format_bounds_list(c_dmin, c_dmax, c_rw), "cws": cws_res, "cached": True}
4124 eff_dmin = max(wmax, dmin)
4125
4126 temp_files: List[str] = []
4127 try:
4128 def input_file(M: Any, ext: str) -> Optional[str]:
4129 if M is None:
4130 return None
4131 if isinstance(M, (str, Path)) and os.path.exists(str(M)):
4132 return str(M)
4133 path = _matrix_to_file(M, extension=ext)
4134 temp_files.append(path)
4135 return path
4136
4137 file_dem = None
4138 if fdem is not None: # a DEM: the file, or the object written to a temporary file
4139 if isinstance(fdem, (str, Path)) and os.path.exists(str(fdem)):
4140 file_dem = str(fdem)
4141 else:
4142 file_dem = create_unique_file(extension=".dem")
4143 temp_files.append(file_dem)
4144 _write_dem_file(fdem, file_dem)
4145 file_H, file_G, file_L = input_file(H, "_H.mtx"), input_file(G, "_G.mtx"), input_file(L, "_L.mtx")
4146 file_drop = None
4147 if fdem is not None and fdrop is not None: # the detectors to drop: the file, or the list written to a file
4148 if isinstance(fdrop, (str, Path)):
4149 file_drop = str(fdrop)
4150 elif len(fdrop) > 0:
4151 file_drop = create_unique_file(extension="_drop.txt")
4152 temp_files.append(file_drop)
4153 with open(file_drop, "w") as f:
4154 f.write(" ".join(str(r) for r in fdrop) + "\n")
4155 outC_file = None
4156 if need_cws:
4157 outC_file = create_unique_file(extension="_cws.nz")
4158 temp_files.append(outC_file)
4159
4160 dmin_res, dmax_res, rw_steps = run_dist_m4ri(
4161 dist_m4ri_path=dist_m4ri,
4162 method=method,
4163 finH=file_H,
4164 finG=file_G,
4165 finL=file_L,
4166 fdem=file_dem,
4167 finC=finC,
4168 classical=-1 if fdem is not None else (1 if classical else 0),
4169 dmin=eff_dmin,
4170 dmax=eff_dmax,
4171 dstop=dstop,
4172 wmin=wmin,
4173 wmax=wmax,
4174 smax=smax,
4175 start=start_list,
4176 start_scan=bool(getattr(start_list, "scan", False)),
4177 warn_start=False,
4178 warn_ksub=False,
4179 ortho=ortho,
4180 warn_ortho=False,
4181 dexp=d_exp,
4182 steps=num_steps,
4183 threads=threads,
4184 timeout=timeout,
4185 pmin=pmin,
4186 merge=merge,
4187 drop=file_drop,
4188 outD=outD if fdem is not None else None,
4189 sort_rows=sort_rows,
4190 cc_prune=cc_prune,
4191 dW=dW,
4192 maxC=maxC,
4193 outC=outC_file,
4194 seed=seed,
4195 debug=debug,
4196 nothrottle=nothrottle,
4197 chunk_size=chunk_size,
4198 ksub=ksub,
4199 kwin=kwin,
4200 win_mode=win_mode,
4201 win_hyb=win_hyb,
4202 pairs=pairs,
4203 min_hits=min_hits,
4204 cov_cws=cov_cws,
4205 refresh=refresh,
4206 verbose=verbose
4207 )
4208
4209 cws: List[List[int]] = []
4210 if need_cws and outC_file and os.path.exists(outC_file):
4211 cws = read_sparse_vectors(outC_file)
4212 cws.sort(key=len)
4213
4214 if _use_distance_cache and code_key is not None:
4215 prev = cached_entry or {}
4216 p_dmax = prev.get("dmax", 0) or 0
4217 best_dmax = min(p_dmax, dmax_res) if (p_dmax > 0 and dmax_res > 0) else max(p_dmax, dmax_res)
4218 best_dmin = max(prev.get("dmin", 0) or 0, dmin_res)
4219 total_rw = (prev.get("rw_steps", 0) or 0) + rw_steps
4220 record = _sector_record(best_dmin, best_dmax, total_rw, _union_cws(prev.get("cws", []), cws))
4221 if "dmin_model" in prev: # (a bound from the code-sized model, see compute_model_lower_bound())
4222 record["dmin_model"] = prev["dmin_model"]
4223 _distance_cache[code_key] = record
4224 if plain_key is not None and code_key != plain_key:
4225 # start-list run: merge the valid part (or all, with trust_start) into the main record
4226 _merge_start_into_plain(plain_key, record, trust_start, {"": rw_steps})
4227 if eff_cache_file:
4228 save_distance_cache(eff_cache_file)
4229 res = {"dist": record["dist"], "dmin": record["dmin"], "dmax": record["dmax"], "rw_steps": total_rw,
4230 "d_info": record["d_info"], "cws": record["cws"] if need_cws else [], "cached": False}
4231 else:
4232 res = {"dist": _dist_from_bounds(dmin_res, dmax_res), "dmin": dmin_res, "dmax": dmax_res,
4233 "rw_steps": rw_steps, "d_info": format_bounds_list(dmin_res, dmax_res, rw_steps), "cws": cws,
4234 "cached": False}
4235 res["cws"] = write_out(res["cws"])
4236 return res
4237 finally:
4238 _remove_temp_files(temp_files, debug)
4239
4240
4242 H: Any,
4243 dist_m4ri: Optional[str] = None,
4244 method: int = 3,
4245 threads: Optional[int] = None,
4246 timeout: float = 60.0,
4247 num_steps: Optional[int] = None,
4248 d_exp: int = 0,
4249 d_min: int = 0,
4250 d_max: int = 0,
4251 dmin: int = 0,
4252 dmax: int = 0,
4253 dstop: int = 0,
4254 wmin: int = 1,
4255 wmax: int = 0,
4256 smax: Optional[int] = None,
4257 start: Optional[Union[int, str, Sequence[int]]] = None,
4258 cbeg: Optional[int] = None,
4259 cend: Optional[int] = None,
4260 noscan: int = 0,
4261 dW: int = -1,
4262 maxC: int = 0,
4263 finC: Optional[str] = None,
4264 outC: Optional[str] = None,
4265 do_cws: bool = False,
4266 return_info: bool = False,
4267 cache_file: Optional[Union[str, Path]] = None,
4268 solver: str = "dist_m4ri",
4269 codedistance_method: str = "QDistEvol",
4270 codedistance_params: Optional[Dict[str, Any]] = None,
4271 seed: int = 0,
4272 debug: int = 0,
4273 verbose: bool = False,
4274 nothrottle: bool = False,
4275 chunk_size: int = 0,
4276 ksub: int = 0,
4277 kwin: int = 0,
4278 win_mode: int = 0,
4279 win_hyb: int = 0,
4280 pairs: int = -1,
4281 min_hits: Optional[int] = None,
4282 cov_cws: int = 100,
4283 refresh: Optional[int] = None,
4284 trust_start: bool = False,
4285 start_scan: bool = False,
4286 sort_rows: Optional[bool] = None,
4287 cc_prune: Optional[int] = None,
4288 engine: Optional[str] = None,
4289 **kwargs
4290) -> Any:
4291 """
4292 Computes the minimum distance of a classical linear code given parity check matrix H.
4293
4294 Args:
4295 H: Parity check matrix (numpy array, scipy sparse matrix, or file path).
4296 dist_m4ri: Path to dist_m4ri executable (optional).
4297 method: Solver method (1=RW, 2=CC, 3=Bracketing default).
4298 threads: Maximum number of worker threads (default: all logical CPUs available; subject to throttling).
4299 timeout: Execution timeout in seconds (default: 60.0; 0: no limit).
4300 nothrottle: If True, the automatic thread throttling of the binary is disabled (default: False).
4301 chunk_size: Maximum RW batch claimed at once by a thread (default: 0: adaptive, 25-500 steps).
4302 num_steps: Maximum RW steps.
4303 min_hits: RW stops once the average number of hits per codeword reaches min_hits, both for the codewords of
4304 the minimum weight found and for a representative set of cov_cws lowest-weight codewords (see
4305 run_dist_m4ri()); passed if not None and >= 0 (None, the default: the binary's 5; 0: off, and then the
4306 repeated hits are not counted, unless codewords are collected with do_cws, outC, or maxC).
4307 cov_cws: Size of the representative set of lowest-weight codewords for min_hits (default: 100; 0: only the
4308 minimum weight).
4309 kwin / win_mode: A window of kwin columns in every RW step (alias: win; win_mode=0, the default: a Tanner
4310 graph ball, 1: an index window); kwin=0 (default): the automatic hybrid windows for n >= 500, see
4311 win_hyb and run_dist_m4ri(); kwin=-1: no windows (uniform random permutations in every step).
4312 refresh: RW steps between the basis refreshes of the experimental ksub RW (passed if not None; None,
4313 the default: the binary's 5000 with ksub > 0; 0: no refresh).
4314 d_exp: Expected distance estimate.
4315 d_min / dmin: Known lower bound on distance.
4316 d_max / dmax: Known upper bound on distance.
4317 dstop: Stop target for the lower bound (default: 0 = off): the search ends once CC has certified
4318 dmin >= dstop (method=2 or 3), unless a lighter codeword is found. Unlike dmax, dstop is not an upper
4319 bound, and it is not reported as dmax: d_info = [dmin, w, rw_steps] with dmin >= dstop and the weight w
4320 of the lightest codeword found (0: none). A cached lower bound dmin >= dstop answers the request without
4321 a calculation.
4322 wmin: Minimum distance of interest (terminate early if cw of weight <= wmin is found in RW or CC, default: 1).
4323 wmax: Maximum weight to search in CC.
4324 smax: Maximum syndrome weight for CC confinement profile.
4325 start: Expert option: list of CC start columns (int, comma-separated string "a,b,c", or
4326 sequence of ints; None or -1: all columns). CC clusters are grown only from the listed
4327 columns, and they are not limited to larger column indices. WARNING: the lower bound
4328 (and an exact result) is valid only if a code symmetry maps every minimum-weight codeword
4329 to one containing a listed column (e.g., one column per block of a quasi-cyclic code);
4330 the upper bound is always valid. Results are cached under the separate key
4331 '<key>:start=a,b,c'; only dmax and codewords update the main cache record.
4332 trust_start: Expert option: accept the start-list results as valid, i.e., also merge dmin and
4333 rw_steps into the main cache record, and copy an existing exact start-list record to the
4334 main record even if no calculation is needed (default: False).
4335 start_scan: Expert option, with start (default: False): a cluster grown from a listed column c only
4336 uses the columns > c (see run_dist_m4ri()), e.g., start=[0, l] for the two blocks of a two-block
4337 group-algebra code over an abelian group; cached under the key suffix ':start=a,b,c:scan' (not for a
4338 DEM: ignored with a warning).
4339 cbeg / cend / noscan: Disabled in the Python interface: accepted for backward compatibility,
4340 but ignored with a warning to stderr (use the dist_m4ri binary directly).
4341 sort_rows: Row order of H in the binary (see run_dist_m4ri()): True sorts the rows by increasing weight
4342 (faster CC for very different row weights with cc_prune=0), False keeps their order, and None (default)
4343 leaves the binary's default (sorted for matrices; the detectors of a DEM are kept in time order). The
4344 result and the cache key do not depend on it.
4345 cc_prune: The CC search of the binary (see run_dist_m4ri()): 0, the walk of the versions before 0.15.0; 1,
4346 the branch and bound (each codeword found once); 2, also the coverage and fractional cover bounds; None
4347 (default): the binary's default 1. The result and the cache key do not depend on it.
4348 win_hyb: Window size of the automatic hybrid RW steps (kwin=0, n >= 500: a window on every other step; see
4349 run_dist_m4ri()); 0 (default): 256 columns for a DEM, 512 for matrices. The cache key does not depend on
4350 it.
4351 pairs: The RW pairs of non-pivot columns (p=2, see run_dist_m4ri()): the number of random pivot rows of
4352 their buckets (0: off; -1, the default: auto). The cache key does not depend on it.
4353 ksub: Experimental, should not be used (a warning is printed to stderr for ksub > 0 with method=1 or 3):
4354 subspace dimension sampled from ker(H) in RW (default: 0, full-matrix RW).
4355 dW: Extra weight window above dmin to collect codewords (only irreducible ones: not the union of a
4356 codeword and a disjoint nonzero vector with zero syndrome; the binary's default irred=1).
4357 maxC: Maximum number of codewords to collect.
4358 finC: Input file with initial codewords.
4359 outC: Output file to save codewords (NZLIST format).
4360 do_cws: Whether to return extracted codewords.
4361 return_info: If True, return (dist, d_info) or (dist, d_info, cws) where d_info is [dmin, dmax, num_rw].
4362 cache_file: Optional JSON file path for persistent distance caching.
4363 solver: "dist_m4ri" or "codedistance".
4364 codedistance_method: Method if using codedistance library.
4365 codedistance_params: Extra parameters for codedistance library.
4366 seed: Random seed.
4367 debug: Debug bitmap: the bits in BIN_DBG_MASK for the dist_m4ri binary, and the PY_DBG_* bits.
4368 verbose: Verbose reporting flag.
4369
4370 Returns:
4371 dist or (dist, cws) if do_cws is True (or (dist, d_info) / (dist, d_info, cws) if return_info=True)
4372
4373 Raises:
4374 TypeError: for an unknown keyword argument (e.g., a misspelled option).
4375 ValueError: for an invalid option value (e.g., start, or cc_prune other than 0, 1, or 2), for method=2 with
4376 timeout <= 0 but without wmax, dmax, or dstop, or for do_cws / outC with solver="codedistance".
4377 FileNotFoundError: if the dist_m4ri binary is not found (see find_dist_m4ri_binary()).
4378 RuntimeError: if the binary exits with an error (e.g., for a missing input file).
4379 ImportError: if solver="codedistance" but the codedistance package is not installed.
4380 """
4381 eff_dmin = dmin if dmin > 0 else d_min
4382 eff_dmax = dmax if dmax > 0 else d_max
4383 _check_kwargs(compute_classical_distance, kwargs)
4384 cert_mode = _resolve_cert_engine(solver, engine)
4385 start_list = _prepare_start_option(start, trust_start, noscan, cbeg, cend, solver, start_scan=start_scan)
4386 _warn_experimental_ksub(ksub, method, solver)
4387
4388 if solver == "codedistance" or str(engine or "").strip().lower() == "codedistance":
4389 if do_cws or outC:
4390 raise ValueError("Codeword extraction is not supported with codedistance solver; use solver='dist_m4ri'.")
4392 codedistance = _get_codedistance()
4393 import numpy as np
4394 params = dict(codedistance_params or {})
4395 if num_steps is not None and "iterCount" not in params:
4396 params["iterCount"] = num_steps
4397
4398 H_mat = H.toarray() if hasattr(H, 'toarray') else (
4399 np.asarray(H, dtype=np.int8) if isinstance(H, (np.ndarray, list)) else None
4400 )
4401 res = codedistance.codeDistance(
4402 H_mat, None, tB=1, method=codedistance_method, params=params,
4403 seed=seed if seed != 0 else None
4404 )
4405 d = res.get("d", -1)
4406 if return_info:
4407 d_info = format_bounds_list(d, d, 0) if d > 0 else [0, 0, 0]
4408 return d, d_info
4409 return d
4410
4411 eff_cache_file = str(Path(cache_file).resolve()) if cache_file is not None else _distance_cache_file
4413 cert_mode, method=method, do_cws=do_cws, outC=outC, finC=finC, smax=smax, dW=dW, maxC=maxC,
4414 start_list=start_list,
4415 ):
4416 _, qec_io, qec_pipeline = _get_qec_cert()
4417 mode_arg = "ub" if method == 1 else "auto"
4418 res = _cert_code_distance(
4419 lambda: _matrix_cache_key(H, None, None, True),
4420 lambda w_lo, w_hi: qec_pipeline.certify(
4421 qec_io.DistanceProblem(_cert_to_csr(H, qec_io), None, None),
4422 wmin=w_lo, wmax=w_hi, threads=threads or (os.cpu_count() or 8),
4423 seed=seed if seed != 0 else 1,
4424 time_limit=float(timeout) if timeout and timeout > 0 else 0.0,
4425 mode=mode_arg,
4426 ),
4427 label="classical", dmin=eff_dmin, dmax=eff_dmax, dstop=dstop, wmax=wmax,
4428 eff_cache_file=eff_cache_file, debug=debug, verbose=verbose,
4429 )
4430 dist, d_info = res["dist"], res["d_info"]
4431 return (dist, d_info) if return_info else dist
4432
4433 # Solver is native multithreaded dist_m4ri (supports bounds caching and cumulative RW steps)
4435 H, None, None, True, "classical", dist_m4ri=dist_m4ri, method=method, threads=threads, timeout=timeout,
4436 num_steps=num_steps, d_exp=d_exp, dmin=eff_dmin, dmax=eff_dmax, dstop=dstop, wmin=wmin, wmax=wmax,
4437 smax=smax, start_list=start_list, dW=dW, maxC=maxC, finC=finC, outC=outC, need_cws=bool(do_cws or outC),
4438 eff_cache_file=eff_cache_file, seed=seed, debug=debug, verbose=verbose, nothrottle=nothrottle,
4439 chunk_size=chunk_size, ksub=ksub, kwin=kwin if kwin != 0 else int(kwargs.get("win", 0) or 0),
4440 win_mode=win_mode, min_hits=min_hits, cov_cws=cov_cws, refresh=refresh, trust_start=trust_start,
4441 sort_rows=sort_rows, win_hyb=win_hyb, pairs=pairs, cc_prune=cc_prune
4442 )
4443 dist, d_info, cws = res["dist"], res["d_info"], res["cws"]
4444 if return_info:
4445 return (dist, d_info, cws) if do_cws else (dist, d_info)
4446 return (dist, cws) if do_cws else dist
4447
4448
4450 H: Any,
4451 G: Optional[Any] = None,
4452 L: Optional[Any] = None,
4453 dist_m4ri: Optional[str] = None,
4454 method: int = 3,
4455 threads: Optional[int] = None,
4456 timeout: float = 60.0,
4457 num_steps: Optional[int] = None,
4458 d_exp: int = 0,
4459 d_min: int = 0,
4460 d_max: int = 0,
4461 dmin: int = 0,
4462 dmax: int = 0,
4463 dstop: int = 0,
4464 wmin: int = 1,
4465 wmax: int = 0,
4466 smax: Optional[int] = None,
4467 start: Optional[Union[int, str, Sequence[int]]] = None,
4468 cbeg: Optional[int] = None,
4469 cend: Optional[int] = None,
4470 noscan: int = 0,
4471 dW: int = -1,
4472 maxC: int = 0,
4473 finC: Optional[str] = None,
4474 outC: Optional[str] = None,
4475 do_cws: bool = False,
4476 return_info: bool = False,
4477 cache_file: Optional[Union[str, Path]] = None,
4478 solver: str = "dist_m4ri",
4479 codedistance_method: str = "QDistEvol",
4480 codedistance_params: Optional[Dict[str, Any]] = None,
4481 seed: int = 0,
4482 debug: int = 0,
4483 verbose: bool = False,
4484 nothrottle: bool = False,
4485 chunk_size: int = 0,
4486 ksub: int = 0,
4487 kwin: int = 0,
4488 win_mode: int = 0,
4489 win_hyb: int = 0,
4490 pairs: int = -1,
4491 min_hits: Optional[int] = None,
4492 cov_cws: int = 100,
4493 refresh: Optional[int] = None,
4494 trust_start: bool = False,
4495 start_scan: bool = False,
4496 sort_rows: Optional[bool] = None,
4497 cc_prune: Optional[int] = None,
4498 ortho: bool = True,
4499 engine: Optional[str] = None,
4500 **kwargs
4501) -> Any:
4502 """
4503 Computes the minimum distance of a single-sided quantum code given parity check matrix H
4504 and degeneracy generator G (or logical operator matrix L).
4505
4506 Args:
4507 H: Parity check matrix (numpy array, scipy sparse matrix, or file path).
4508 G: Degeneracy generator matrix (numpy array, scipy sparse matrix, or file path); the binary constructs the
4509 logical operators from H and G. Either G or L is required, not both (ValueError).
4510 L: Logical operator matrix (numpy array, scipy sparse matrix, or file path), instead of G.
4511 ortho: Expert option (default: True; since version 0.15.2): False skips the check of the binary that the rows
4512 of H and G are orthogonal (H G^T = 0; True: an error otherwise), e.g., to screen many codes which are
4513 orthogonal by construction; a warning is printed to stderr, and the result is valid only if H G^T = 0. No
4514 effect without G. The cache key does not depend on it.
4515 dist_m4ri: Path to dist_m4ri executable (optional).
4516 method: Solver method (1=RW, 2=CC, 3=Bracketing default).
4517 threads: Maximum number of worker threads (default: all logical CPUs available; subject to throttling).
4518 timeout: Execution timeout in seconds (default: 60.0; 0: no limit).
4519 nothrottle: If True, the automatic thread throttling of the binary is disabled (default: False).
4520 chunk_size: Maximum RW batch claimed at once by a thread (default: 0: adaptive, 25-500 steps).
4521 num_steps: Maximum RW steps.
4522 min_hits: RW stops once the average number of hits per codeword reaches min_hits, both for the codewords of
4523 the minimum weight found and for a representative set of cov_cws lowest-weight codewords (see
4524 run_dist_m4ri()); passed if not None and >= 0 (None, the default: the binary's 5; 0: off, and then the
4525 repeated hits are not counted, unless codewords are collected with do_cws, outC, or maxC).
4526 cov_cws: Size of the representative set of lowest-weight codewords for min_hits (default: 100; 0: only the
4527 minimum weight).
4528 kwin / win_mode: A window of kwin columns in every RW step (alias: win; win_mode=0, the default: a Tanner
4529 graph ball, 1: an index window); kwin=0 (default): the automatic hybrid windows for n >= 500, see
4530 win_hyb and run_dist_m4ri(); kwin=-1: no windows (uniform random permutations in every step).
4531 refresh: RW steps between the basis refreshes of the experimental ksub RW (passed if not None; None,
4532 the default: the binary's 5000 with ksub > 0; 0: no refresh).
4533 d_exp: Expected distance estimate.
4534 d_min / dmin: Known lower bound on distance.
4535 d_max / dmax: Known upper bound on distance.
4536 dstop: Stop target for the lower bound (default: 0 = off): the search ends once CC has certified
4537 dmin >= dstop (method=2 or 3), unless a lighter codeword is found. Unlike dmax, dstop is not an upper
4538 bound, and it is not reported as dmax: d_info = [dmin, w, rw_steps] with dmin >= dstop and the weight w
4539 of the lightest codeword found (0: none). A cached lower bound dmin >= dstop answers the request without
4540 a calculation.
4541 wmin: Minimum distance of interest (terminate early if cw of weight <= wmin is found in RW or CC, default: 1).
4542 wmax: Maximum weight to search in CC.
4543 smax: Maximum syndrome weight for CC confinement profile.
4544 start: Expert option: list of CC start columns (int, comma-separated string "a,b,c", or
4545 sequence of ints; None or -1: all columns). CC clusters are grown only from the listed
4546 columns, and they are not limited to larger column indices. WARNING: the lower bound
4547 (and an exact result) is valid only if a code symmetry maps every minimum-weight codeword
4548 to one containing a listed column (e.g., one column per block of a quasi-cyclic code);
4549 the upper bound is always valid. Results are cached under the separate key
4550 '<key>:start=a,b,c'; only dmax and codewords update the main cache record.
4551 trust_start: Expert option: accept the start-list results as valid, i.e., also merge dmin and
4552 rw_steps into the main cache record, and copy an existing exact start-list record to the
4553 main record even if no calculation is needed (default: False).
4554 start_scan: Expert option, with start (default: False): a cluster grown from a listed column c only
4555 uses the columns > c (see run_dist_m4ri()), e.g., start=[0, l] for the two blocks of a two-block
4556 group-algebra code over an abelian group; cached under the key suffix ':start=a,b,c:scan' (not for a
4557 DEM: ignored with a warning).
4558 cbeg / cend / noscan: Disabled in the Python interface: accepted for backward compatibility,
4559 but ignored with a warning to stderr (use the dist_m4ri binary directly).
4560 sort_rows: Row order of H in the binary (see run_dist_m4ri()): True sorts the rows by increasing weight
4561 (faster CC for very different row weights with cc_prune=0), False keeps their order, and None (default)
4562 leaves the binary's default (sorted for matrices; the detectors of a DEM are kept in time order). The
4563 result and the cache key do not depend on it.
4564 cc_prune: The CC search of the binary (see run_dist_m4ri()): 0, the walk of the versions before 0.15.0; 1,
4565 the branch and bound (each codeword found once); 2, also the coverage and fractional cover bounds; None
4566 (default): the binary's default 1. The result and the cache key do not depend on it.
4567 win_hyb: Window size of the automatic hybrid RW steps (kwin=0, n >= 500: a window on every other step; see
4568 run_dist_m4ri()); 0 (default): 256 columns for a DEM, 512 for matrices. The cache key does not depend on
4569 it.
4570 pairs: The RW pairs of non-pivot columns (p=2, see run_dist_m4ri()): the number of random pivot rows of
4571 their buckets (0: off; -1, the default: auto). The cache key does not depend on it.
4572 ksub: Experimental, should not be used (a warning is printed to stderr for ksub > 0 with method=1 or 3):
4573 subspace dimension sampled from ker(H) in RW (default: 0, full-matrix RW).
4574 dW: Extra weight window above dmin to collect codewords (only irreducible ones: not the union of a
4575 codeword and a disjoint nonzero vector with zero syndrome; the binary's default irred=1).
4576 maxC: Maximum number of codewords to collect.
4577 finC: Input file with initial codewords.
4578 outC: Output file to save codewords (NZLIST format).
4579 do_cws: Whether to return extracted codewords.
4580 return_info: If True, return (dist, d_info) or (dist, d_info, cws) where d_info is [dmin, dmax, num_rw].
4581 cache_file: Optional JSON file path for persistent distance caching.
4582 solver: "dist_m4ri" or "codedistance".
4583 codedistance_method: Method if using codedistance library.
4584 codedistance_params: Extra parameters for codedistance library.
4585 seed: Random seed.
4586 debug: Debug bitmap: the bits in BIN_DBG_MASK for the dist_m4ri binary, and the PY_DBG_* bits.
4587 verbose: Verbose reporting flag.
4588
4589 Returns:
4590 dist or (dist, cws) if do_cws is True (or (dist, d_info) / (dist, d_info, cws) if return_info=True)
4591
4592 Raises:
4593 TypeError: for an unknown keyword argument (e.g., a misspelled option).
4594 ValueError: if neither or both of G and L are given (or G is not given, with solver="codedistance"), for an
4595 invalid option value (e.g., start, or cc_prune other than 0, 1, or 2), for method=2 with timeout <= 0
4596 but without wmax, dmax, or dstop, or for do_cws / outC with solver="codedistance".
4597 FileNotFoundError: if the dist_m4ri binary is not found (see find_dist_m4ri_binary()).
4598 RuntimeError: if the binary exits with an error (e.g., for a missing input file, or with ortho=True for rows
4599 of H and G which are not orthogonal).
4600 ImportError: if solver="codedistance" but the codedistance package is not installed.
4601 """
4602 eff_dmin = dmin if dmin > 0 else d_min
4603 eff_dmax = dmax if dmax > 0 else d_max
4604 _check_kwargs(compute_quantum_distance, kwargs)
4605 cert_mode = _resolve_cert_engine(solver, engine)
4606 start_list = _prepare_start_option(start, trust_start, noscan, cbeg, cend, solver, start_scan=start_scan)
4607 _warn_experimental_ksub(ksub, method, solver)
4608
4609 if G is None and L is None:
4610 raise ValueError(
4611 "Either G (dual generator matrix) or L (logical operator matrix) "
4612 "must be specified for quantum distance."
4613 )
4614 if G is not None and L is not None:
4615 raise ValueError("Either G or L should be specified for quantum distance, not both.")
4616 _warn_ortho(ortho, G is not None, solver)
4617
4618 if solver == "codedistance" or str(engine or "").strip().lower() == "codedistance":
4619 if do_cws or outC:
4620 raise ValueError("Codeword extraction is not supported with codedistance solver; use solver='dist_m4ri'.")
4622 codedistance = _get_codedistance()
4623 import numpy as np
4624 params = dict(codedistance_params or {})
4625 if num_steps is not None and "iterCount" not in params:
4626 params["iterCount"] = num_steps
4627
4628 H_mat = H.toarray() if hasattr(H, 'toarray') else (
4629 np.asarray(H, dtype=np.int8) if isinstance(H, (np.ndarray, list)) else None
4630 )
4631 if G is None and L is not None:
4632 raise ValueError(
4633 "The 'codedistance' solver requires the stabilizer generator matrix G, "
4634 "not the logical operator matrix L. Use solver='dist_m4ri' with L."
4635 )
4636 dual_mat = G if G is not None else L
4637 dual_arr = dual_mat.toarray() if hasattr(dual_mat, 'toarray') else (
4638 np.asarray(dual_mat, dtype=np.int8) if isinstance(dual_mat, (np.ndarray, list)) else None
4639 )
4640
4641 res = codedistance.codeDistance(
4642 H_mat, dual_arr, tB=1, method=codedistance_method, params=params,
4643 seed=seed if seed != 0 else None
4644 )
4645 d = res.get("d", -1)
4646 if return_info:
4647 d_info = format_bounds_list(d, d, 0) if d > 0 else [0, 0, 0]
4648 return d, d_info
4649 return d
4650
4651 eff_cache_file = str(Path(cache_file).resolve()) if cache_file is not None else _distance_cache_file
4653 cert_mode, method=method, do_cws=do_cws, outC=outC, finC=finC, smax=smax, dW=dW, maxC=maxC,
4654 start_list=start_list,
4655 ):
4656 _, qec_io, qec_pipeline = _get_qec_cert()
4657 mode_arg = "ub" if method == 1 else "auto"
4658 res = _cert_code_distance(
4659 lambda: _matrix_cache_key(H, G, L, False),
4660 lambda w_lo, w_hi: qec_pipeline.certify(
4661 qec_io.DistanceProblem(
4662 _cert_to_csr(H, qec_io), _cert_to_csr(L, qec_io), _cert_to_csr(G, qec_io)
4663 ),
4664 wmin=w_lo, wmax=w_hi, threads=threads or (os.cpu_count() or 8),
4665 seed=seed if seed != 0 else 1,
4666 time_limit=float(timeout) if timeout and timeout > 0 else 0.0,
4667 mode=mode_arg,
4668 ),
4669 label="quantum", dmin=eff_dmin, dmax=eff_dmax, dstop=dstop, wmax=wmax,
4670 eff_cache_file=eff_cache_file, debug=debug, verbose=verbose,
4671 )
4672 dist, d_info = res["dist"], res["d_info"]
4673 return (dist, d_info) if return_info else dist
4674
4675 # Solver is native multithreaded dist_m4ri (the cache records are shared with the sectors of CSS codes)
4677 H, G, L, False, "quantum", dist_m4ri=dist_m4ri, method=method, threads=threads, timeout=timeout,
4678 num_steps=num_steps, d_exp=d_exp, dmin=eff_dmin, dmax=eff_dmax, dstop=dstop, wmin=wmin, wmax=wmax,
4679 smax=smax, start_list=start_list, dW=dW, maxC=maxC, finC=finC, outC=outC, need_cws=bool(do_cws or outC),
4680 eff_cache_file=eff_cache_file, seed=seed, debug=debug, verbose=verbose, nothrottle=nothrottle,
4681 chunk_size=chunk_size, ksub=ksub, kwin=kwin if kwin != 0 else int(kwargs.get("win", 0) or 0),
4682 win_mode=win_mode, min_hits=min_hits, cov_cws=cov_cws, refresh=refresh, trust_start=trust_start,
4683 sort_rows=sort_rows, win_hyb=win_hyb, pairs=pairs, cc_prune=cc_prune, ortho=ortho
4684 )
4685 dist, d_info, cws = res["dist"], res["d_info"], res["cws"]
4686 if return_info:
4687 return (dist, d_info, cws) if do_cws else (dist, d_info)
4688 return (dist, cws) if do_cws else dist
4689
4690
4691def _split_css_filename(filepath: Optional[str], sector: str) -> Optional[str]:
4692 """Generates sector-suffixed filename for CSS codewords (e.g. 'cws.nz' -> 'cws_X.nz')."""
4693 if not filepath:
4694 return None
4695 base, ext = os.path.splitext(filepath)
4696 if not ext:
4697 ext = ".nz"
4698 if base.endswith(f"_{sector}"):
4699 return f"{base}{ext}"
4700 elif base.endswith("_X") or base.endswith("_Z"):
4701 base = base[:-2]
4702 return f"{base}_{sector}{ext}"
4703
4704
4705def _css_sector_finc(finC: Optional[str], outC: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
4706 """
4707 Input codeword files (finC_X, finC_Z) of the two CSS sectors: the sector files (e.g. 'cws.nz' -> 'cws_X.nz' and
4708 'cws_Z.nz') which exist and are not empty; if there are none, finC itself for both sectors (codewords which are
4709 not valid in a sector are skipped by the binary), unless finC is identical to outC and does not exist or is
4710 empty (see check_finc_outc()).
4711 """
4712 if not finC:
4713 return None, None
4714 files = [_split_css_filename(finC, s) for s in ("X", "Z")]
4715 files = [f if (os.path.isfile(f) and os.path.getsize(f) > 0) else None for f in files]
4716 if files[0] or files[1]:
4717 return files[0], files[1]
4718 raw = check_finc_outc(finC, outC)
4719 return raw, raw
4720
4721
4723 Hx: Any,
4724 Hz: Any,
4725 Lx: Optional[Any] = None,
4726 Lz: Optional[Any] = None,
4727 dist_m4ri: Optional[str] = None,
4728 method: int = 3,
4729 threads: Optional[int] = None,
4730 timeout: float = 60.0,
4731 num_steps: Optional[int] = None,
4732 d_exp: int = 0,
4733 d_min: int = 0,
4734 d_max: int = 0,
4735 dmin: int = 0,
4736 dmax: int = 0,
4737 dstop: int = 0,
4738 wmin: int = 1,
4739 wmax: int = 0,
4740 smax: Optional[int] = None,
4741 start: Optional[Union[int, str, Sequence[int]]] = None,
4742 cbeg: Optional[int] = None,
4743 cend: Optional[int] = None,
4744 noscan: int = 0,
4745 dW: int = -1,
4746 maxC: int = 0,
4747 finC: Optional[str] = None,
4748 outC: Optional[str] = None,
4749 do_cws: bool = False,
4750 cache_file: Optional[Union[str, Path]] = None,
4751 solver: str = "dist_m4ri",
4752 codedistance_method: str = "QDistEvol",
4753 codedistance_params: Optional[Dict[str, Any]] = None,
4754 seed: int = 0,
4755 debug: int = 0,
4756 verbose: bool = False,
4757 nothrottle: bool = False,
4758 chunk_size: int = 0,
4759 ksub: int = 0,
4760 kwin: int = 0,
4761 win_mode: int = 0,
4762 win_hyb: int = 0,
4763 pairs: int = -1,
4764 min_hits: Optional[int] = None,
4765 cov_cws: int = 100,
4766 refresh: Optional[int] = None,
4767 trust_start: bool = False,
4768 start_scan: bool = False,
4769 sort_rows: Optional[bool] = None,
4770 cc_prune: Optional[int] = None,
4771 ortho: bool = True,
4772 engine: Optional[str] = None,
4773 **kwargs
4774) -> Tuple[Any, ...]:
4775 """
4776 Computes CSS quantum code distance d = min(d_X, d_Z).
4777
4778 Args:
4779 Hx: X-stabilizer parity check matrix (required, at least one row).
4780 Hz: Z-stabilizer parity check matrix (required, at least one row).
4781 Lx: Optional X-logical operator matrix (alternative to Hz as finG).
4782 Lz: Optional Z-logical operator matrix (alternative to Hx as finG).
4783 ortho: Expert option (default: True; since version 0.15.2): False skips the checks of the binary that the
4784 rows of Hx and Hz are orthogonal (Hx Hz^T = 0, in each sector with G; True: an error otherwise), e.g., to
4785 screen many codes which are orthogonal by construction; a warning is printed to stderr, and the result is
4786 valid only if Hx Hz^T = 0. No effect with both Lx and Lz. The cache key does not depend on it.
4787 dist_m4ri: Path to dist_m4ri executable (optional).
4788 method: Solver method (1=RW, 2=CC, 3=Bracketing default).
4789 threads: Maximum number of worker threads (default: all logical CPUs available; subject to throttling).
4790 timeout: Execution timeout in seconds (default: 60.0; 0: no limit).
4791 nothrottle: If True, the automatic thread throttling of the binary is disabled (default: False).
4792 chunk_size: Maximum RW batch claimed at once by a thread (default: 0: adaptive, 25-500 steps).
4793 num_steps: Maximum RW steps.
4794 min_hits: RW stops once the average number of hits per codeword reaches min_hits, both for the codewords of
4795 the minimum weight found and for a representative set of cov_cws lowest-weight codewords (see
4796 run_dist_m4ri()); passed if not None and >= 0 (None, the default: the binary's 5; 0: off, and then the
4797 repeated hits are not counted, unless codewords are collected with do_cws, outC, or maxC).
4798 cov_cws: Size of the representative set of lowest-weight codewords for min_hits (default: 100; 0: only the
4799 minimum weight).
4800 kwin / win_mode: A window of kwin columns in every RW step (alias: win; win_mode=0, the default: a Tanner
4801 graph ball, 1: an index window); kwin=0 (default): the automatic hybrid windows for n >= 500, see
4802 win_hyb and run_dist_m4ri(); kwin=-1: no windows (uniform random permutations in every step).
4803 refresh: RW steps between the basis refreshes of the experimental ksub RW (passed if not None; None,
4804 the default: the binary's 5000 with ksub > 0; 0: no refresh).
4805 d_exp: Expected distance estimate.
4806 d_min / dmin: Known lower bound on d = min(dX, dZ), inclusive (a lower bound of both sectors).
4807 d_max / dmax: Known upper bound on d = min(dX, dZ), inclusive. As it is not an upper bound of either
4808 sector, it is passed to both sector runs as the stop target dstop (each sector is needed only up to this
4809 weight), and it is not stored in the sector cache records; the returned d is at most dmax.
4810 dstop: Stop target for the lower bound of d (default: 0 = off): both sector runs end once CC has certified
4811 dmin >= dstop, unless a lighter codeword is found (see compute_classical_distance()).
4812 wmin: Minimum distance of interest (terminate early if cw of weight <= wmin is found in RW or CC, default: 1).
4813 wmax: Maximum weight to search in CC.
4814 smax: Maximum syndrome weight for CC confinement profile.
4815 start: Expert option: list of CC start columns (int, comma-separated string "a,b,c", or
4816 sequence of ints; None or -1: all columns). CC clusters are grown only from the listed
4817 columns, and they are not limited to larger column indices. WARNING: the lower bound
4818 (and an exact result) is valid only if a code symmetry maps every minimum-weight codeword
4819 to one containing a listed column (e.g., one column per block of a quasi-cyclic code);
4820 the upper bound is always valid. Results are cached under the separate key
4821 '<key>:start=a,b,c'; only dmax and codewords update the main cache record.
4822 trust_start: Expert option: accept the start-list results as valid, i.e., also merge dmin and
4823 rw_steps into the main cache record, and copy an existing exact start-list record to the
4824 main record even if no calculation is needed (default: False).
4825 start_scan: Expert option, with start (default: False): a cluster grown from a listed column c only
4826 uses the columns > c (see run_dist_m4ri()), e.g., start=[0, l] for the two blocks of a two-block
4827 group-algebra code over an abelian group; cached under the key suffix ':start=a,b,c:scan' (not for a
4828 DEM: ignored with a warning).
4829 cbeg / cend / noscan: Disabled in the Python interface: accepted for backward compatibility,
4830 but ignored with a warning to stderr (use the dist_m4ri binary directly).
4831 sort_rows: Row order of H in the binary (see run_dist_m4ri()): True sorts the rows by increasing weight
4832 (faster CC for very different row weights with cc_prune=0), False keeps their order, and None (default)
4833 leaves the binary's default (sorted for matrices; the detectors of a DEM are kept in time order). The
4834 result and the cache key do not depend on it.
4835 cc_prune: The CC search of the binary (see run_dist_m4ri()): 0, the walk of the versions before 0.15.0; 1,
4836 the branch and bound (each codeword found once); 2, also the coverage and fractional cover bounds; None
4837 (default): the binary's default 1. The result and the cache key do not depend on it.
4838 win_hyb: Window size of the automatic hybrid RW steps (kwin=0, n >= 500: a window on every other step; see
4839 run_dist_m4ri()); 0 (default): 256 columns for a DEM, 512 for matrices. The cache key does not depend on
4840 it.
4841 pairs: The RW pairs of non-pivot columns (p=2, see run_dist_m4ri()): the number of random pivot rows of
4842 their buckets (0: off; -1, the default: auto). The cache key does not depend on it.
4843 ksub: Experimental, should not be used (a warning is printed to stderr for ksub > 0 with method=1 or 3):
4844 subspace dimension sampled from ker(H) in RW (default: 0, full-matrix RW).
4845 dW: Extra weight window above dmin to collect codewords (only irreducible ones: not the union of a
4846 codeword and a disjoint nonzero vector with zero syndrome; the binary's default irred=1).
4847 maxC: Maximum number of codewords to collect.
4848 finC: Input file with initial codewords.
4849 outC: Output file to save codewords (NZLIST format).
4850 do_cws: Whether to return extracted X and Z codewords.
4851 cache_file: Optional JSON file path for persistent distance caching. The two sectors are cached as the
4852 quantum codes (H=Hx, G=Hz) -> dZ and (H=Hz, G=Hx) -> dX (with L=Lx / L=Lz if given), i.e., they share
4853 the records of compute_quantum_distance(); CSS records written before version 0.12.0 are converted.
4854 solver: "dist_m4ri" or "codedistance".
4855 codedistance_method: Method if using codedistance library.
4856 codedistance_params: Extra parameters for codedistance library.
4857 seed: Random seed.
4858 debug: Debug bitmap: the bits in BIN_DBG_MASK for the dist_m4ri binary, and the PY_DBG_* bits.
4859 verbose: Verbose reporting flag.
4860
4861 Returns:
4862 tuple (dist, dX_info, dZ_info, cws_X, cws_Z) if do_cws
4863 else (dist, dX_info, dZ_info)
4864
4865 Raises:
4866 ValueError: if Hx or Hz is missing or empty (use compute_quantum_distance() for one sector, or
4867 compute_classical_distance() for a classical code), for an invalid option value (e.g., start, or
4868 cc_prune other than 0, 1, or 2), for method=2 with timeout <= 0 but without wmax, dmax, or dstop, or
4869 for do_cws / outC with solver="codedistance".
4870 TypeError: for an unknown keyword argument (e.g., a misspelled option).
4871 FileNotFoundError: if the dist_m4ri binary is not found (see find_dist_m4ri_binary()).
4872 RuntimeError: if the binary exits with an error (e.g., for a missing input file, or with ortho=True for
4873 rows of Hx and Hz which are not orthogonal).
4874 ImportError: if solver="codedistance" but the codedistance package is not installed.
4875 """
4876 def _as_matrix(M: Any) -> Any: # an array-like (e.g., a list of rows) as a NumPy array
4877 if M is None or isinstance(M, (str, Path)) or hasattr(M, 'shape'):
4878 return M
4879 import numpy as np
4880 return np.asarray(M)
4881
4882 Hx, Hz, Lx, Lz = (_as_matrix(M) for M in (Hx, Hz, Lx, Lz))
4883
4884 def _nonempty(M: Any) -> bool:
4885 return M is not None and (isinstance(M, (str, Path)) or (hasattr(M, 'shape') and M.shape[0] > 0))
4886
4887 if not (_nonempty(Hx) and _nonempty(Hz)):
4888 raise ValueError(
4889 "compute_css_distance() requires both Hx and Hz (file names, or matrices with at least one row); "
4890 "use compute_quantum_distance() for one sector, or compute_classical_distance() for a classical code."
4891 )
4892
4893 eff_dmin = dmin if dmin > 0 else d_min
4894 eff_dmax = dmax if dmax > 0 else d_max # an upper bound on d = min(dX, dZ), not on either sector
4895 _check_kwargs(compute_css_distance, kwargs)
4896 cert_mode = _resolve_cert_engine(solver, engine)
4897 start_list = _prepare_start_option(start, trust_start, noscan, cbeg, cend, solver, start_scan=start_scan)
4898 _warn_experimental_ksub(ksub, method, solver)
4899 _warn_ortho(ortho, Lx is None or Lz is None, solver) # (G of the sectors: Hz unless Lx, Hx unless Lz)
4900
4901 if solver == "codedistance" or str(engine or "").strip().lower() == "codedistance":
4902 if do_cws or outC:
4903 raise ValueError("Codeword extraction is not supported with codedistance solver; use solver='dist_m4ri'.")
4905 codedistance = _get_codedistance()
4906 import numpy as np
4907 params = dict(codedistance_params or {})
4908 if num_steps is not None and "iterCount" not in params:
4909 params["iterCount"] = num_steps
4910
4911 Hx_mat = Hx.toarray() if hasattr(Hx, 'toarray') else (
4912 np.asarray(Hx, dtype=np.int8) if isinstance(Hx, (np.ndarray, list)) else None
4913 )
4914 Hz_mat = Hz.toarray() if hasattr(Hz, 'toarray') else (
4915 np.asarray(Hz, dtype=np.int8) if isinstance(Hz, (np.ndarray, list)) else None
4916 )
4917 sector_d: Dict[str, int] = {}
4918 sector_info: Dict[str, List[int]] = {}
4919 for s in ("Z", "X"):
4920 res_s = codedistance.CSScodeDistance(
4921 Hx_mat, Hz_mat, method=codedistance_method, params=params.copy(),
4922 component=s, seed=seed if seed != 0 else None
4923 )
4924 sector_d[s] = res_s.get("d", -1)
4925 sector_info[s] = format_bounds_list(sector_d[s], sector_d[s], 0) if sector_d[s] > 0 else [0, 0, 0]
4926 d_z, d_x = sector_d["Z"], sector_d["X"]
4927 dist = min(d_z, d_x) if (d_z > 0 and d_x > 0) else max(d_z, d_x)
4928 return (dist, sector_info["X"], sector_info["Z"])
4929
4930 # Solver is native multithreaded dist_m4ri: the two sectors are quantum codes, (H=Hx, G=Hz) -> dZ and
4931 # (H=Hz, G=Hx) -> dX (with L=Lx / L=Lz instead if given), with the same cache records as compute_quantum_distance().
4932 # A user dmax (a bound on min(dX, dZ)) is not an upper bound of either sector: it is passed as the stop target.
4933 eff_cache_file = str(Path(cache_file).resolve()) if cache_file is not None else _distance_cache_file
4934 stops = [v for v in (dstop, eff_dmax) if v > 0]
4935 sector_stop = min(stops) if stops else 0
4936 need_cws = bool(do_cws or outC)
4937
4938 # Sector-specific input codewords: the files finC_X / finC_Z if they exist, otherwise finC itself
4939 finC_X, finC_Z = _css_sector_finc(finC, outC)
4940 if verbose and finC and not (finC_X or finC_Z):
4941 print(f"[dist_m4ri] Warning: finC='{finC}' (identical to outC) and its sector files are empty or "
4942 f"non-existent; silently ignoring input codewords.")
4943 if _use_distance_cache:
4944 if eff_cache_file:
4945 load_distance_cache(eff_cache_file)
4946 try:
4947 if _migrate_css_records(Hx, Hz, Lx, Lz, eff_cache_file) and verbose:
4948 print("[dist_m4ri] Converted the CSS cache records written before version 0.12.0 into sector records")
4949 except Exception:
4950 pass
4951
4952 codes = _css_sector_codes(Hx, Hz, Lx, Lz)
4953 res: Dict[str, Dict[str, Any]] = {}
4954 for s, finC_s in (("Z", finC_Z), ("X", finC_X)):
4955 H_s, G_s, L_s = codes[s]
4956 if verbose:
4957 dual = ("Hx" if s == "X" else "Hz") if G_s is not None else ("Lz" if s == "X" else "Lx")
4958 print(f"[dist_m4ri] CSS sector d{s}: H={'Hz' if s == 'X' else 'Hx'} with "
4959 f"{'G' if G_s is not None else 'L'}={dual}")
4961 cert_mode, method=method, do_cws=do_cws, outC=outC, finC=finC_s, smax=smax,
4962 dW=dW, maxC=maxC, start_list=start_list,
4963 ):
4964 _, qec_io, qec_pipeline = _get_qec_cert()
4965 mode_arg = "ub" if method == 1 else "auto"
4966 res[s] = _cert_code_distance(
4967 lambda H_s=H_s, G_s=G_s, L_s=L_s: _matrix_cache_key(H_s, G_s, L_s, False),
4968 lambda w_lo, w_hi, H_s=H_s, G_s=G_s, L_s=L_s: qec_pipeline.certify(
4969 qec_io.DistanceProblem(
4970 _cert_to_csr(H_s, qec_io), _cert_to_csr(L_s, qec_io), _cert_to_csr(G_s, qec_io)
4971 ),
4972 wmin=w_lo, wmax=w_hi, threads=threads or (os.cpu_count() or 8),
4973 seed=seed if seed != 0 else 1,
4974 time_limit=float(timeout) if timeout and timeout > 0 else 0.0,
4975 mode=mode_arg,
4976 ),
4977 label=f"CSS {s}-sector", dmin=eff_dmin, dmax=0, dstop=sector_stop, wmax=wmax,
4978 eff_cache_file=eff_cache_file, debug=debug, verbose=verbose,
4979 )
4980 else:
4981 res[s] = _cached_code_distance(
4982 H_s, G_s, L_s, False, f"CSS {s}-sector", dist_m4ri=dist_m4ri, method=method, threads=threads,
4983 timeout=timeout, num_steps=num_steps, d_exp=d_exp, dmin=eff_dmin, dmax=0, dstop=sector_stop, wmin=wmin,
4984 wmax=wmax, smax=smax, start_list=start_list, dW=dW, maxC=maxC, finC=finC_s,
4985 outC=_split_css_filename(outC, s) if outC else None, need_cws=need_cws, eff_cache_file=eff_cache_file,
4986 seed=seed, debug=debug, verbose=verbose, nothrottle=nothrottle, chunk_size=chunk_size, ksub=ksub,
4987 kwin=kwin if kwin != 0 else int(kwargs.get("win", 0) or 0), win_mode=win_mode, min_hits=min_hits,
4988 cov_cws=cov_cws, refresh=refresh, trust_start=trust_start, sort_rows=sort_rows, win_hyb=win_hyb,
4989 pairs=pairs, cc_prune=cc_prune, ortho=ortho
4990 )
4991 _last_css_stats[s] = dict(_last_run_stats)
4992
4993 rx, rz = res["X"], res["Z"]
4994 dist, d_lo, d_hi, _ = _css_combine((rx["dmin"], rx["dmax"], rx["rw_steps"]),
4995 (rz["dmin"], rz["dmax"], rz["rw_steps"]), eff_dmax)
4996 if rx["cached"] and rz["cached"] and (debug & PY_DBG_CACHE) and not verbose:
4997 found = ("exact distance" if (d_lo > 0 and d_lo == d_hi) else
4998 f"dmin={d_lo} >= dstop={sector_stop}" if (sector_stop > 0 and d_lo >= sector_stop) else
4999 f"bounds dmin={d_lo}, dmax={d_hi}")
5000 print(f"[dist_m4ri] Cache hit for CSS distance ({found} known)!")
5001 if do_cws:
5002 return (dist, rx["d_info"], rz["d_info"], rx["cws"], rz["cws"])
5003 return (dist, rx["d_info"], rz["d_info"])
5004
5005
5007 out_spec: Optional[Union[bool, str, Path]],
5008 circuit_src: str,
5009 mode_tag: str,
5010 ext: str,
5011 out_dir: Optional[Union[str, Path]] = None
5012) -> Optional[str]:
5013 """Resolves output filepath for --out-dem or --out-stim."""
5014 if out_spec is None or out_spec is False:
5015 return None
5016 if isinstance(out_spec, str) and out_spec.strip().lower() in (
5017 "0", "false", "no", "off", "none"
5018 ):
5019 return None
5020
5021 src_path = (
5022 Path(circuit_src)
5023 if (circuit_src and circuit_src != "stim.Circuit")
5024 else None
5025 )
5026 eff_out_dir = (
5027 Path(out_dir)
5028 if out_dir is not None
5029 else (src_path.parent if src_path is not None else Path("."))
5030 )
5031
5032 is_auto = (out_spec is True) or (
5033 isinstance(out_spec, str)
5034 and out_spec.strip().lower() in ("", "1", "true", "yes", "auto")
5035 )
5036 if is_auto:
5037 if src_path is not None:
5038 stem = src_path.stem
5039 if stem.endswith("_full") or stem.endswith("_simp"):
5040 stem = stem[:-5]
5041 else:
5042 stem = "circuit"
5043 out_path = eff_out_dir / f"{stem}{mode_tag}{ext}"
5044 else:
5045 spec_str = str(out_spec)
5046 spec_path = Path(spec_str)
5047 if not spec_path.is_absolute() and os.path.dirname(spec_str) == "":
5048 out_path = eff_out_dir / spec_path
5049 else:
5050 out_path = spec_path
5051
5052 parent_dir = os.path.dirname(os.path.abspath(str(out_path)))
5053 if parent_dir:
5054 os.makedirs(parent_dir, exist_ok=True)
5055 return str(out_path)
5056
5057
5058def _circuit_dem(circuit: Any) -> Tuple[Any, bool, bool]:
5059 """
5060 The DEM of a Stim circuit: with decompose_errors=True if stim can decompose its errors (otherwise without). An
5061 error channel with disjoint cases which stim cannot write as independent error mechanisms (e.g., a PAULI_CHANNEL_1
5062 with X and Z but no Y errors, PAULI_CHANNEL_2, or ELSE_CORRELATED_ERROR) needs approximate_disjoint_errors=True,
5063 which is used only if the conversion fails without it: it only changes the probabilities of the error mechanisms,
5064 not which errors are possible (so neither the distance, unless pmin > 0, nor the codewords).
5065
5066 Returns:
5067 (dem, decompose_errors, approximate_disjoint_errors): the DEM, and the options used.
5068 """
5069 error: Optional[Exception] = None
5070 for approx in (False, True):
5071 for decomp in (True, False):
5072 try:
5073 return circuit.detector_error_model(decompose_errors=decomp, approximate_disjoint_errors=approx), \
5074 decomp, approx
5075 except Exception as e: # (stim raises ValueError)
5076 error = e
5077 assert error is not None
5078 raise error
5079
5080
5081def _warn_disabled_simple_full(simple: Optional[bool] = None, full: Optional[bool] = None, cli: bool = False) -> None:
5082 """
5083 Writes a warning to stderr if the options simple or full (CLI: --simple, --full) are given: they are disabled since
5084 version 0.13.0 (accepted for backward compatibility, but ignored). The DEM of a Stim circuit always has all
5085 detectors, and the binary drops detectors if this is exact (drop=auto, the default; e.g., the minority-basis
5086 detectors of a CSS circuit, which the simple mode stripped): as fast, with the distance of the full DEM (the
5087 simplified DEM only gives a lower bound when dropping them is not exact).
5088 """
5089 names = [f"--{n}" if cli else f"{n}=" for n, v in (("simple", simple), ("full", full)) if v is not None]
5090 if names:
5091 _warn_ignored(f"{', '.join(names)}: disabled since version 0.13.0, ignored: the DEM of a Stim circuit has all "
5092 f"detectors, and the binary drops detectors if this is exact (drop=auto, the default, e.g., the "
5093 f"minority-basis detectors of a CSS circuit; drop=0 keeps them)")
5094
5095
5097 dem: Optional[Any],
5098 circuit: Optional[Any],
5099 basis: Optional[str] = None,
5100 rounds: Optional[int] = None,
5101 p_noise: float = 0.001,
5102 out_dir: Optional[Union[str, Path]] = None,
5103 out_dem: Optional[Union[bool, str, Path]] = None,
5104 out_stim: Optional[Union[bool, str, Path]] = None,
5105 circ_verbose: bool = False,
5106 drop_out: Optional[List[int]] = None,
5107 force_noise: bool = False
5108) -> Tuple[Any, Optional[Any], Optional[str]]:
5109 """
5110 The DEM of compute_dem_distance() (also used by get_cached_distance() for the cache key). A DEM
5111 (stim.DetectorErrorModel or .dem file) is used as given. A Stim circuit (stim.Circuit or .stim file, also when
5112 given as `dem`) is processed: the empty detectors are removed, the qubits are classified, the REPEAT count is set
5113 (rounds), and circuit-level noise of strength p_noise is added to a noiseless circuit (see add_noise(); none for
5114 p_noise=0), or with force_noise=True replaces the noise of the circuit (see strip_noise(); with p_noise=0, all noise
5115 is removed); then it is converted to a DEM (with all detectors). The processed circuit and the DEM are saved if
5116 requested (out_stim, out_dem, out_dir; see compute_dem_distance()). For a CSS circuit, the list drop_out (if given)
5117 is extended by the minority-basis detectors (0-based detectors of the DEM, see strip_minority_detectors()), which
5118 the binary drops if this is exact (drop=css, passed as drop=FILE). The qubits are only classified for drop_out and
5119 circ_verbose.
5120
5121 Returns:
5122 (dem, circuit, saved_dem_path): the DEM (object or file name), the processed circuit (None for a DEM input),
5123 and the file to which the DEM was saved (None if it was not saved).
5124 """
5125 if dem is not None and circuit is not None:
5126 _warn_ignored("both 'dem' and 'circuit' were given; using 'dem' (the circuit is ignored).")
5127 circuit = None
5128 if dem is not None and isinstance(dem, (str, Path)) and str(dem).endswith(".stim"):
5129 circuit = dem
5130 dem = None
5131
5132 if rounds is not None and circuit is None:
5133 raise ValueError(
5134 "--rounds option is only supported for Stim circuit (.stim) inputs."
5135 )
5136
5137 saved_dem_path: Optional[str] = None
5138 if dem is None and circuit is not None:
5139 circuit_src = str(circuit) if isinstance(circuit, (str, Path)) else "stim.Circuit"
5140 if isinstance(circuit, (str, Path)):
5141 stim = _get_stim()
5142 circuit = stim.Circuit.from_file(str(circuit))
5143 if not hasattr(circuit, 'detector_error_model'):
5144 raise ValueError("Provided circuit object does not have detector_error_model() method.")
5145 has_rep = False
5146 if rounds is not None:
5147 if rounds < 0:
5148 raise ValueError(f"Invalid rounds={rounds}; must be >= 0.")
5149 has_rep = has_repeat_block(circuit)
5150 if not has_rep:
5151 sys.stderr.write(
5152 f"# Warning: Circuit '{circuit_src}' does not contain a "
5153 f"REPEAT block; ignoring rounds={rounds}.\n"
5154 )
5155
5156 circuit, empty_removed = remove_empty_detectors(circuit)
5157 if empty_removed > 0 and circ_verbose:
5158 print(f"[dist_m4ri] Removed {empty_removed} empty DETECTOR(s) from '{circuit_src}'")
5159
5160 filepath_hint = circuit_src if circuit_src != "stim.Circuit" else None
5161 t_res: Optional[Dict[str, Any]] = None # the classification of the qubits (only for drop=css and verbose)
5162 if circ_verbose or drop_out is not None:
5163 t_res = classify_qubits_thorough(circuit, verbose=False, basis_arg=basis, filepath=filepath_hint)
5164
5165 if rounds is not None and has_rep:
5166 circuit = set_circuit_rounds(circuit, rounds)
5167
5168 n_minority = None
5169 if t_res is not None and t_res["is_css"] and drop_out is not None:
5170 # the minority-basis detectors, for the exact reduction of the DEM in the binary (drop=css)
5171 mask: List[bool] = []
5172 strip_minority_detectors(circuit, t_res["basis"], verbose=False, thorough=True, thorough_res=t_res,
5173 mask_out=mask)
5174 if len(mask) == circuit.num_detectors:
5175 drop_out.extend(i for i, s in enumerate(mask) if s)
5176 n_minority = sum(mask)
5177 elif circ_verbose:
5178 print(f"[dist_m4ri] Warning: {len(mask)} detectors classified, but the circuit has "
5179 f"{circuit.num_detectors}; no detectors to drop")
5180
5181 mode_tag = "_full" # (the default names of the saved files, see _resolve_stim_dem_out_path())
5182
5183 if circ_verbose and t_res is not None:
5184 is_css_circ, is_rot_css = t_res["is_css"], t_res.get("is_rotated_css", False)
5185 css_type = "rotated-CSS (e.g. XZZX)" if is_rot_css else ("CSS" if is_css_circ else "non-CSS")
5186 rot_info = f", rotated_data={len(t_res.get('rotated_data_qubits', []))}" if is_rot_css else ""
5187 rnd_cnt = count_circuit_rounds(circuit)
5188 print(
5189 f"[dist_m4ri] Circuit basis tracking: type={css_type}, "
5190 f"basis={t_res['basis']}, data={len(t_res['data_qubits'])}, "
5191 f"X_anc={len(t_res['x_ancillas'])}, "
5192 f"Z_anc={len(t_res['z_ancillas'])}, "
5193 f"routing={len(t_res['routing_qubits'])}{rot_info}, "
5194 f"rounds={rnd_cnt}"
5195 )
5196 minority = (f", minority-basis={n_minority} (drop=css: the binary drops them if this is exact)"
5197 if n_minority is not None else "")
5198 print(f"[dist_m4ri] Circuit detectors: {circuit.num_detectors}{minority}")
5199
5200 if force_noise: # the noise of the circuit is replaced (p_noise=0: removed)
5201 noise_msg = (f"replaced the noise by circuit-level noise (p={p_noise})" if p_noise > 0.0 else
5202 "removed all noise (p_noise=0)")
5203 circuit = add_noise(circuit, p=p_noise, force=True)
5204 elif has_noise(circuit):
5205 noise_msg = "existing noise detected"
5206 elif p_noise > 0.0:
5207 noise_msg = f"added circuit-level noise (p={p_noise})"
5208 circuit = add_noise(circuit, p=p_noise)
5209 else:
5210 noise_msg = "no noise added (p_noise=0)"
5211
5212 stim_out_path = _resolve_stim_dem_out_path(out_stim, circuit_src, mode_tag, ".stim", out_dir=out_dir)
5213 if stim_out_path is not None:
5214 circuit.to_file(stim_out_path)
5215 if circ_verbose:
5216 print(f"[dist_m4ri] Saved Stim circuit to '{stim_out_path}'")
5217
5218 dem, decomp_used, approx_used = _circuit_dem(circuit)
5219 if circ_verbose:
5220 approx_msg = ", approximate_disjoint_errors=True" if approx_used else ""
5221 print(f"[dist_m4ri] Converted '{circuit_src}' to DEM ({noise_msg}, decompose_errors={decomp_used}"
5222 f"{approx_msg})")
5223 if all(hasattr(dem, a) for a in ("num_detectors", "num_observables", "num_errors")):
5224 print(
5225 f"[dist_m4ri] DEM size: {dem.num_detectors} detectors, {dem.num_observables} observables, "
5226 f"{dem.num_errors} error mechanisms"
5227 )
5228
5229 dem_out_path = _resolve_stim_dem_out_path(out_dem, circuit_src, mode_tag, ".dem", out_dir=out_dir)
5230 if dem_out_path is not None:
5231 _write_dem_file(dem, dem_out_path)
5232 saved_dem_path = dem_out_path
5233 if circ_verbose:
5234 print(f"[dist_m4ri] Saved DEM to '{dem_out_path}'")
5235 elif dem is not None and out_dem is not None and out_dem is not False:
5236 dem_src = str(dem) if isinstance(dem, (str, Path)) else "stim.Circuit"
5237 dem_out_path = _resolve_stim_dem_out_path(out_dem, dem_src, "_full", ".dem", out_dir=out_dir)
5238 if dem_out_path is not None:
5239 if isinstance(dem, (str, Path)) and os.path.exists(str(dem)):
5240 if os.path.abspath(str(dem)) != os.path.abspath(dem_out_path):
5241 shutil.copyfile(str(dem), dem_out_path)
5242 else:
5243 _write_dem_file(dem, dem_out_path)
5244 saved_dem_path = dem_out_path
5245 if circ_verbose:
5246 print(f"[dist_m4ri] Saved DEM to '{dem_out_path}'")
5247
5248 if dem is None:
5249 raise ValueError("Either 'dem' or 'circuit' must be provided.")
5250 return dem, circuit, saved_dem_path
5251
5252
5254 dem: Optional[Any] = None,
5255 circuit: Optional[Any] = None,
5256 dist_m4ri: Optional[str] = None,
5257 method: int = 3,
5258 threads: Optional[int] = None,
5259 timeout: float = 60.0,
5260 num_steps: Optional[int] = None,
5261 d_exp: int = 0,
5262 d_min: int = 0,
5263 d_max: int = 0,
5264 dmin: int = 0,
5265 dmax: int = 0,
5266 dstop: int = 0,
5267 wmin: int = 1,
5268 wmax: int = 0,
5269 smax: Optional[int] = None,
5270 start: Optional[Union[int, str, Sequence[int]]] = None,
5271 cbeg: Optional[int] = None,
5272 cend: Optional[int] = None,
5273 noscan: int = 0,
5274 dW: int = -1,
5275 maxC: int = 0,
5276 pmin: float = 0.0,
5277 merge: bool = True,
5278 expand: bool = False,
5279 drop: Optional[Union[bool, int, str, Path]] = None,
5280 outD: Optional[Union[str, Path]] = None,
5281 out_reduced: Optional[Union[bool, str, Path]] = None,
5282 finC: Optional[str] = None,
5283 outC: Optional[str] = None,
5284 do_cws: bool = False,
5285 cache_file: Optional[Union[str, Path]] = None,
5286 solver: str = "dist_m4ri",
5287 codedistance_method: str = "UndetectableErrorStim",
5288 codedistance_params: Optional[Dict[str, Any]] = None,
5289 seed: int = 0,
5290 debug: int = 0,
5291 verbose: bool = False,
5292 nothrottle: bool = False,
5293 chunk_size: int = 0,
5294 ksub: int = 0,
5295 kwin: int = 0,
5296 win_mode: int = 0,
5297 win_hyb: int = 0,
5298 pairs: int = -1,
5299 min_hits: Optional[int] = None,
5300 cov_cws: int = 100,
5301 refresh: Optional[int] = None,
5302 simple: Optional[bool] = None,
5303 full: Optional[bool] = None,
5304 basis: Optional[str] = None,
5305 rounds: Optional[int] = None,
5306 out_dir: Optional[Union[str, Path]] = None,
5307 out_dem: Optional[Union[bool, str, Path]] = None,
5308 out_stim: Optional[Union[bool, str, Path]] = None,
5309 p_noise: float = 0.001,
5310 force_noise: bool = False,
5311 trust_start: bool = False,
5312 start_scan: bool = False,
5313 sort_rows: Optional[bool] = None,
5314 cc_prune: Optional[int] = None,
5315 engine: Optional[str] = None,
5316 **kwargs
5317) -> Tuple[Any, ...]:
5318 """
5319 Computes minimum graph/hypergraph distance of a Stim Detector Error Model (DEM).
5320
5321 Args:
5322 dem: stim.DetectorErrorModel object or path to .dem / .stim file.
5323 circuit: stim.Circuit object or path to .stim file (converted to DEM; if stim cannot write an error channel with
5324 disjoint cases, e.g., a PAULI_CHANNEL_1 without Y errors, as independent error mechanisms, with
5325 approximate_disjoint_errors=True, which only changes the probabilities of the error mechanisms).
5326 dist_m4ri: Path to dist_m4ri executable (optional).
5327 method: Solver method (1=RW, 2=CC, 3=Bracketing default).
5328 threads: Maximum number of worker threads (default: all logical CPUs available; subject to throttling).
5329 timeout: Execution timeout in seconds (default: 60.0; 0: no limit).
5330 nothrottle: If True, the automatic thread throttling of the binary is disabled (default: False).
5331 chunk_size: Maximum RW batch claimed at once by a thread (default: 0: adaptive, 25-500 steps).
5332 num_steps: Maximum RW steps.
5333 min_hits: RW stops once the average number of hits per codeword reaches min_hits, both for the codewords of
5334 the minimum weight found and for a representative set of cov_cws lowest-weight codewords (see
5335 run_dist_m4ri()); passed if not None and >= 0 (None, the default: the binary's 5; 0: off, and then the
5336 repeated hits are not counted, unless codewords are collected with do_cws, outC, or maxC).
5337 cov_cws: Size of the representative set of lowest-weight codewords for min_hits (default: 100; 0: only the
5338 minimum weight).
5339 kwin / win_mode: A window of kwin columns in every RW step (alias: win; win_mode=0, the default: a Tanner
5340 graph ball, 1: an index window); kwin=0 (default): the automatic hybrid windows for n >= 500, see
5341 win_hyb and run_dist_m4ri(); kwin=-1: no windows (uniform random permutations in every step).
5342 refresh: RW steps between the basis refreshes of the experimental ksub RW (passed if not None; None,
5343 the default: the binary's 5000 with ksub > 0; 0: no refresh).
5344 d_exp: Expected distance estimate.
5345 d_min / dmin: Known lower bound on distance, inclusive.
5346 d_max / dmax: Known upper bound on distance, inclusive.
5347 dstop: Stop target for the lower bound (see compute_classical_distance(); default: 0 = off).
5348 wmin: Minimum distance of interest (terminate early if cw of weight <= wmin is found in RW or CC, default: 1).
5349 wmax: Maximum weight to search in CC.
5350 smax: Maximum syndrome weight for CC confinement profile.
5351 start: Expert option: list of CC start columns (int, comma-separated string "a,b,c", or
5352 sequence of ints; None or -1: all columns). CC clusters are grown only from the listed
5353 columns, and they are not limited to larger column indices. WARNING: the lower bound
5354 (and an exact result) is valid only if a code symmetry maps every minimum-weight codeword
5355 to one containing a listed column (e.g., one column per block of a quasi-cyclic code);
5356 the upper bound is always valid. Results are cached under the separate key
5357 '<key>:start=a,b,c'; only dmax and codewords update the main cache record.
5358 trust_start: Expert option: accept the start-list results as valid, i.e., also merge dmin and
5359 rw_steps into the main cache record, and copy an existing exact start-list record to the
5360 main record even if no calculation is needed (default: False).
5361 start_scan: Expert option, with start (default: False): a cluster grown from a listed column c only
5362 uses the columns > c (see run_dist_m4ri()), e.g., start=[0, l] for the two blocks of a two-block
5363 group-algebra code over an abelian group; cached under the key suffix ':start=a,b,c:scan' (not for a
5364 DEM: ignored with a warning).
5365 cbeg / cend / noscan: Disabled in the Python interface: accepted for backward compatibility,
5366 but ignored with a warning to stderr (use the dist_m4ri binary directly).
5367 sort_rows: Row order of H in the binary (see run_dist_m4ri()): True sorts the rows by increasing weight
5368 (faster CC for very different row weights with cc_prune=0), False keeps their order, and None (default)
5369 leaves the binary's default (sorted for matrices; the detectors of a DEM are kept in time order). The
5370 result and the cache key do not depend on it.
5371 cc_prune: The CC search of the binary (see run_dist_m4ri()): 0, the walk of the versions before 0.15.0; 1,
5372 the branch and bound (each codeword found once); 2, also the coverage and fractional cover bounds; None
5373 (default): the binary's default 1. The result and the cache key do not depend on it.
5374 win_hyb: Window size of the automatic hybrid RW steps (kwin=0, n >= 500: a window on every other step; see
5375 run_dist_m4ri()); 0 (default): 256 columns for a DEM, 512 for matrices. The cache key does not depend on
5376 it.
5377 pairs: The RW pairs of non-pivot columns (p=2, see run_dist_m4ri()): the number of random pivot rows of
5378 their buckets (0: off; -1, the default: auto). The cache key does not depend on it.
5379 ksub: Experimental, should not be used (a warning is printed to stderr for ksub > 0 with method=1 or 3):
5380 subspace dimension sampled from ker(H) in RW (default: 0, full-matrix RW).
5381 dW: Extra weight window above dmin to collect codewords (only irreducible ones: not the union of a
5382 codeword and a disjoint nonzero vector with zero syndrome; the binary's default irred=1).
5383 maxC: Maximum number of codewords to collect (with expand=True, also the maximum number of codewords of the
5384 expanded list returned and written to outC; a warning is printed to stderr if some are dropped).
5385 pmin: Probability cutoff for error mechanisms in DEM.
5386 Note: CC start columns (start) and the codewords (finC, outC, do_cws) index the error mechanisms in
5387 their order of appearance in the (flattened) DEM, after the pmin cutoff: with another pmin, the columns
5388 (and the codewords) change.
5389 merge: Merge the equal columns of the DEM (error mechanisms with the same detectors and observables) in the
5390 binary (default: True). Exact: dropping two equal columns of a codeword gives a lighter one, so the
5391 distance does not change (and the cache key does not depend on merge). Column indices (finC, outC,
5392 start, the returned codewords) are those of the DEM; the codewords found use the first one of each set
5393 of equal columns (merge=False: all columns are kept, e.g., to enumerate the codewords with each of them;
5394 or expand=True to expand the list found).
5395 expand: If True, the codewords returned and written to outC are expanded in Python: each codeword is listed
5396 once for every choice among equal columns (default: False; see _expand_equal_columns()). The binary
5397 and the cache keep the merged list. With dW <= 1, the expanded list is that of merge=False (with
5398 dW >= 2, merge=False also lists lighter codewords with two equal columns added). The expanded list can
5399 be much longer (it grows quickly with the weight); with maxC > 0, it is cut at maxC codewords.
5400 drop: The detectors which the binary drops if this is verified to be exact, so that the distance (and every
5401 codeword found) is that of the DEM, found from the DEM without these detectors and the errors touching
5402 them (fewer detectors and errors: as fast as the DEM with only the primary-basis detectors of a CSS
5403 circuit). None, True, 1, or "auto" (default): the binary searches for the largest such set in the DEM alone
5404 (drop=auto, see the README), e.g., the minority-basis detectors of a CSS circuit (with the usual noise).
5405 "css": for a Stim circuit classified as CSS, its minority-basis detectors (see strip_minority_detectors()),
5406 passed as a file (otherwise, e.g., for a DEM, drop=auto is used, with a warning). False or 0: no detectors
5407 are dropped. A file name: the file with the detectors to drop (0-based, k for Dk; see read_drop_file()),
5408 for any DEM. With "css" or a file, if dropping the detectors is not exact, a note is printed to stderr and
5409 the full DEM is used. The codewords found use only the errors touching no dropped detector.
5410 outD: File to which the binary writes the detectors dropped (0-based, see read_drop_file()); with a cached
5411 result, the binary is run only to find them (with dmin = dmax: no search).
5412 out_reduced: True or a prefix PREFIX: writes the DEM without the detectors dropped (see outD) to
5413 PREFIX_red.dem, and the maps to the original DEM (its detectors and errors) to PREFIX_red.json, see
5414 write_reduced_dem() (True: PREFIX = <basename>, in out_dir or the directory of the input file).
5415 finC: Input file with initial codewords.
5416 outC: Output file to save codewords (NZLIST format).
5417 do_cws: Whether to return extracted error mechanisms / codewords.
5418 cache_file: Optional JSON file path for persistent distance caching.
5419 solver: "dist_m4ri" or "codedistance".
5420 codedistance_method: Method if using codedistance library.
5421 codedistance_params: Extra parameters for codedistance library.
5422 seed: Random seed.
5423 debug: Debug bitmap: the bits in BIN_DBG_MASK for the dist_m4ri binary, and the PY_DBG_* bits.
5424 verbose: Verbose reporting flag.
5425 simple / full: Disabled since version 0.13.0 (accepted for backward compatibility, but ignored with a warning
5426 to stderr): the DEM of a Stim circuit always has all detectors, and the minority-basis detectors of a CSS
5427 circuit (which simple=True stripped) are dropped by the binary if this is exact (see drop). The distance
5428 of the simplified circuit (a lower bound) can still be found by passing the circuit stripped with
5429 strip_minority_detectors().
5430 basis: Optional primary memory basis override ('X' or 'Z') for .stim circuits, only used with drop="css" (the
5431 other basis is that of the minority-basis detectors) and for the verbose report of the classification.
5432 rounds: Optional number of REPEAT rounds for .stim circuits (warns if no REPEAT block).
5433 out_dir: Optional output directory for saved DEM and/or Stim circuit files.
5434 out_dem: Optional bool or filename to save the constructed DEM (True = auto-named <basename>_full.dem). The
5435 codewords (outC, do_cws) index its error lines (in the order of the flattened DEM): save the DEM in the
5436 same run, since a DEM generated again from the circuit may differ (e.g., with another stim version); with
5437 outC but without out_dem, a warning is printed to stderr for a Stim circuit.
5438 out_stim: Optional bool or filename to save the processed (noisy, rounds-adjusted) Stim circuit
5439 (True = auto-named <basename>_full.stim).
5440 p_noise: Strength of the uniform circuit-level noise added to a noiseless Stim circuit (see add_noise();
5441 default: 0.001; 0: no noise is added, and a noiseless circuit gives no distance, with a note). The
5442 distance does not depend on it (unless pmin > 0), only on which faults are possible. A circuit with noise
5443 (a noise channel or a noisy measurement such as M(0.001), see has_noise()) is used as is, unless
5444 force_noise=True.
5445 force_noise: If True, the noise of a Stim circuit is replaced by the uniform noise of strength p_noise (see
5446 strip_noise(); with p_noise=0, all noise is removed, e.g., to save the noiseless circuit with out_stim),
5447 e.g., to compare circuits with the same noise model (default: False).
5448
5449 Returns:
5450 tuple (dist, d_info, cws) if do_cws else (dist, d_info)
5451
5452 Raises:
5453 TypeError: for an unknown keyword argument (e.g., a misspelled option).
5454 ValueError: if neither dem nor circuit is given, for rounds without a Stim circuit (or rounds < 0), for
5455 p_noise outside [0, 0.5], for an invalid drop (e.g., a file which does not exist) or another invalid
5456 option value (e.g., start, or cc_prune other than 0, 1, or 2), for method=2 with timeout <= 0 but
5457 without wmax, dmax, or dstop, or for do_cws / outC with solver="codedistance".
5458 FileNotFoundError: if the dist_m4ri binary is not found (see find_dist_m4ri_binary()).
5459 RuntimeError: if the binary exits with an error.
5460 ImportError: if stim is needed (e.g., for a Stim circuit) or solver="codedistance", but the package is not
5461 installed.
5462 """
5463 global _last_run_stats
5464 eff_dmin = dmin if dmin > 0 else d_min
5465 eff_dmax = dmax if dmax > 0 else d_max
5466 _check_kwargs(compute_dem_distance, kwargs)
5467 cert_mode = _resolve_cert_engine(solver, engine)
5468 if start_scan: # (the binary merges the equal columns of a DEM, which changes the order of the columns)
5469 _warn_ignored("start_scan=1 is not available for a DEM (ignored)")
5470 start_scan = False
5471 start_list = _prepare_start_option(start, trust_start, noscan, cbeg, cend, solver, start_scan=start_scan)
5472 _warn_experimental_ksub(ksub, method, solver)
5473 _warn_disabled_simple_full(simple, full)
5474
5475 circ_verbose = verbose or bool(debug & PY_DBG_CIRCUITS) # Stim circuit to DEM conversion details
5476 if not 0.0 <= p_noise <= 0.5:
5477 raise ValueError(f"invalid p_noise={p_noise}: the noise strength must be in [0, 0.5]")
5478 drop_mode = _drop_mode(drop) # "auto", "0", "css", or a file name
5479 src_in = dem if dem is not None else circuit # (the input, for the names of the files of out_reduced)
5480 src_name = str(src_in) if isinstance(src_in, (str, Path)) else f"stim.{type(src_in).__name__}"
5481 is_codedist = solver == "codedistance" or str(engine or "").strip().lower() == "codedistance"
5482 css_list: List[int] = [] # the minority-basis detectors of a CSS circuit (drop=css)
5483 dem, circuit, saved_dem_path = _dem_from_input(
5484 dem, circuit, basis=basis, rounds=rounds, p_noise=p_noise, out_dir=out_dir, out_dem=out_dem,
5485 out_stim=out_stim, circ_verbose=circ_verbose,
5486 drop_out=css_list if (drop_mode == "css" and not is_codedist) else None, force_noise=force_noise
5487 )
5488 if getattr(dem, "num_errors", None) == 0: # (a stim object; e.g., a noiseless circuit with p_noise=0)
5489 sys.stderr.write("# Note: the DEM has no error mechanisms (no noise, e.g., p_noise=0 for a noiseless circuit "
5490 "or with force_noise): no distance computed\n")
5491 return (0, [0, 0, 0], []) if do_cws else (0, [0, 0, 0])
5492
5493 if is_codedist:
5494 if do_cws or outC:
5495 raise ValueError("Codeword extraction is not supported with codedistance solver; use solver='dist_m4ri'.")
5496 if outD or out_reduced:
5497 _warn_ignored("outD and out_reduced are only used with solver='dist_m4ri'; ignored")
5499 codedistance = _get_codedistance()
5500 params = dict(codedistance_params or {})
5501 params.setdefault("filterCircuit", False)
5502 if num_steps is not None and "iterCount" not in params:
5503 params["iterCount"] = num_steps
5504
5505 if circuit is not None:
5506 res = codedistance.circuitDistance(
5507 circuit, method=codedistance_method, params=params,
5508 seed=seed if seed != 0 else None
5509 )
5510 else:
5511 H, L, priors = codedistance.StimDEM2HL(dem)
5512 if "priors" not in params and len(priors) > 0:
5513 params["priors"] = priors
5514 res = codedistance.codeDistance(
5515 H, L, tB=1, method=codedistance_method, params=params,
5516 seed=seed if seed != 0 else None
5517 )
5518 d = res.get("d", -1)
5519 d_info = format_bounds_list(d, d, 0) if d > 0 else [0, 0, 0]
5520 return d, d_info
5521
5522 # Solver is native multithreaded dist_m4ri (the cache key is that of the processed DEM, see get_cached_distance())
5523 if outC and circuit is not None and saved_dem_path is None:
5524 sys.stderr.write(f"# Warning: outC={outC}: the codewords index the error lines of the DEM generated from the "
5525 f"Stim circuit in this run (after the pmin cutoff), which is not saved; save it with "
5526 f"--out-dem (out_dem=...), since a DEM generated again later (e.g., with another stim "
5527 f"version) may not fit\n")
5528 fdem_run = saved_dem_path if (saved_dem_path is not None and os.path.exists(saved_dem_path)) else dem
5529 temp_files: List[str] = []
5530 try:
5531 fdrop = drop_mode # "auto", "0", or a file (drop=css: the minority-basis detectors written to a file)
5532 if drop_mode == "css":
5533 if css_list:
5534 fdrop = create_unique_file(extension="_drop.txt")
5535 temp_files.append(fdrop)
5536 with open(fdrop, "w") as f:
5537 f.write(f"# {len(css_list)} minority-basis detectors (drop=css)\n" +
5538 " ".join(str(r) for r in css_list) + "\n")
5539 if circ_verbose:
5540 print(f"[dist_m4ri] drop=css: the binary drops the {len(css_list)} minority-basis detectors if "
5541 f"this is exact")
5542 else:
5543 fdrop = "auto"
5544 _warn_ignored("drop=css: no minority-basis detectors (they are only known for a Stim circuit "
5545 "classified as CSS); using drop=auto (the detectors to drop found from the DEM)")
5546 outd_file = str(outD) if outD else None
5547 if out_reduced and outd_file is None:
5548 outd_file = create_unique_file(extension="_outD.txt")
5549 temp_files.append(outd_file)
5550 if outd_file is not None and not isinstance(fdem_run, (str, Path)): # (the same DEM file for the runs)
5551 fdem_file = create_unique_file(extension=".dem")
5552 temp_files.append(fdem_file)
5553 _write_dem_file(fdem_run, fdem_file)
5554 fdem_run = fdem_file
5555 cws_out_fn = None
5556 if expand and (do_cws or outC):
5557 def cws_out_fn(cws_in: List[List[int]]) -> List[List[int]]:
5558 return _expand_equal_columns(cws_in, fdem_run, pmin=pmin, maxC=maxC, verbose=verbose)
5559 eff_cache_file = str(Path(cache_file).resolve()) if cache_file is not None else _distance_cache_file
5561 cert_mode, method=method, do_cws=do_cws, outC=outC, finC=finC, smax=smax,
5562 dW=dW, maxC=maxC, start_list=start_list, outD=outD, out_reduced=out_reduced, pmin=pmin
5563 ):
5564 _, _, qec_pipeline = _get_qec_cert()
5565 mode_arg = "ub" if method == 1 else "auto"
5566 t_lim = float(timeout) if timeout and timeout > 0 else 0.0
5567 n_thr = threads or (os.cpu_count() or 8)
5568 r_seed = seed if seed != 0 else 1
5569 res = _cert_code_distance(
5570 lambda: _dem_cache_key(dem, pmin),
5571 lambda w_lo, w_hi: (
5572 qec_pipeline.certify_circuit(
5573 circuit, wmin=w_lo, wmax=w_hi, threads=n_thr, seed=r_seed, time_limit=t_lim, mode=mode_arg
5574 )
5575 if circuit is not None else
5576 qec_pipeline.certify_dem(
5577 fdem_run, wmin=w_lo, wmax=w_hi, threads=n_thr, seed=r_seed, time_limit=t_lim, mode=mode_arg
5578 )
5579 ),
5580 label="DEM", dmin=eff_dmin, dmax=eff_dmax, dstop=dstop, wmax=wmax,
5581 eff_cache_file=eff_cache_file, debug=debug, verbose=verbose,
5582 )
5583 dist, d_info, cws = res["dist"], res["d_info"], res["cws"]
5584 return (dist, d_info, cws) if do_cws else (dist, d_info)
5586 None, None, None, False, "DEM", dist_m4ri=dist_m4ri, method=method, threads=threads, timeout=timeout,
5587 num_steps=num_steps, d_exp=d_exp, dmin=eff_dmin, dmax=eff_dmax, dstop=dstop, wmin=wmin, wmax=wmax,
5588 smax=smax if smax is not None else 0, start_list=start_list, dW=dW, maxC=maxC, finC=finC, outC=outC,
5589 need_cws=bool(do_cws or outC), eff_cache_file=eff_cache_file, seed=seed, debug=debug, verbose=verbose,
5590 nothrottle=nothrottle, chunk_size=chunk_size, ksub=ksub,
5591 kwin=kwin if kwin != 0 else int(kwargs.get("win", 0) or 0), win_mode=win_mode, min_hits=min_hits,
5592 cov_cws=cov_cws, refresh=refresh, trust_start=trust_start, fdem=fdem_run, pmin=pmin,
5593 merge=merge, key_fn=lambda: _dem_cache_key(dem, pmin), cws_out_fn=cws_out_fn, fdrop=fdrop, outD=outd_file,
5594 sort_rows=sort_rows, win_hyb=win_hyb, pairs=pairs, cc_prune=cc_prune
5595 )
5596 ran = not res["cached"]
5597 drop_stats = _last_run_stats if ran else {}
5598 if outd_file is not None and res["cached"]: # a run only to find the detectors dropped (bounds coincide)
5599 if verbose:
5600 print("[dist_m4ri] Cached result: running the binary only for the detectors dropped (outD)")
5601 cached_stats = _last_run_stats
5602 run_dist_m4ri(dist_m4ri_path=dist_m4ri, method=2, fdem=str(fdem_run), dmin=1, dmax=1, wmax=1, threads=1,
5603 pmin=pmin, merge=merge, drop=fdrop, outD=outd_file, debug=debug & ~BIN_DBG_MASK)
5604 drop_stats, _last_run_stats = _last_run_stats, cached_stats # (the statistics of the cached result)
5605 ran = True
5606 if ran and fdrop not in ("auto", "0") and drop_stats.get("drop_note"):
5607 what = "the minority-basis detectors (drop=css)" if drop_mode == "css" else f"the detectors in {fdrop}"
5608 sys.stderr.write(f"# Note: dropping {what} is not exact for this DEM ({drop_stats['drop_note']}); "
5609 f"the full DEM was used\n")
5610 elif ran and fdrop not in ("auto", "0") and drop_stats.get("drop_none_listed"):
5611 sys.stderr.write(f"# Note: none of the detectors in {fdrop} is in the DEM; nothing dropped\n")
5612 if drop_mode == "css" and fdrop != "auto" and outd_file is not None and os.path.isfile(outd_file):
5613 with open(outd_file) as f: # the comment line: drop=css instead of the temporary file
5614 text = f.read()
5615 with open(outd_file, "w") as f:
5616 f.write(text.replace(f"(drop={fdrop}", "(drop=css", 1))
5617 if out_reduced and outd_file is not None:
5618 dropped, num_det = read_drop_file(outd_file)
5619 prefix = _reduced_out_prefix(out_reduced, src_name, out_dir)
5620 drop_label = "css" if (drop_mode == "css" and fdrop != "auto") else str(fdrop)
5621 dem_red, json_red = write_reduced_dem(
5622 fdem_run, dropped, prefix, pmin=pmin, num_detectors=num_det,
5623 meta={"source": src_name, "dem": saved_dem_path if saved_dem_path else
5624 (str(fdem_run) if str(fdem_run) not in temp_files else None), "drop": drop_label})
5625 if verbose or (debug & PY_DBG_CIRCUITS):
5626 print(f"[dist_m4ri] Saved the reduced DEM ({len(dropped)} detectors dropped) to '{dem_red}' and the "
5627 f"maps to '{json_red}'")
5628 finally:
5629 _remove_temp_files(temp_files, debug)
5630 dist, d_info, cws = res["dist"], res["d_info"], res["cws"]
5631 return (dist, d_info, cws) if do_cws else (dist, d_info)
5632
5633
5634def _model_detector_groups(circuit: Any) -> Tuple[List[int], List[bool], int]:
5635 """
5636 The groups of the detectors of a Stim circuit for its code-sized model (see compute_model_lower_bound()): two
5637 detectors are in the same group if they share a measurement record other than a final data measurement. The final
5638 data measurements are the measurements in the final measurement block (after the last detector which precedes the
5639 last measurement) of qubits not measured before it, or all measurements of the final block if there are none
5640 (e.g., if every qubit is measured in each round). In a memory circuit, the groups are the chains of the detectors
5641 of the checks, linked by the measurements of their ancillas in consecutive rounds; the chain of a check of the
5642 measured basis ends with its detector of the final data measurements.
5643
5644 Returns:
5645 (group, final, num_final): the group of each detector (numbered in the order of their first detectors), for
5646 each group whether one of its detectors has a final data measurement, and the number of final data
5647 measurements.
5648 """
5649 stim = _get_stim()
5650 det_recs: List[List[int]] = [] # the measurement records of each detector (absolute indices)
5651 det_pos: List[int] = [] # its position in the flattened circuit
5652 meas_pos: List[int] = [] # the position of the instruction of each measurement record
5653 meas_qubits: List[frozenset] = [] # and its qubits
5654 last_gate_pos = -1 # the position of the last unitary gate (for end-of-circuit DETECTORs)
5655 for pos, inst in enumerate(circuit.flattened()):
5656 if inst.name == "DETECTOR":
5657 n = len(meas_pos)
5658 det_recs.append([n + t.value for t in inst.targets_copy() if t.is_measurement_record_target])
5659 det_pos.append(pos)
5660 else:
5661 gd = stim.gate_data(inst.name)
5662 if gd.produces_measurements:
5663 for grp in inst.target_groups(): # (one record per group, e.g., per Pauli product of MPP)
5664 meas_pos.append(pos)
5665 meas_qubits.append(frozenset() if inst.name == "MPAD" else
5666 frozenset(t.qubit_value for t in grp if t.qubit_value is not None))
5667 elif gd.is_unitary:
5668 last_gate_pos = pos
5669 last_meas = meas_pos[-1] if meas_pos else -1
5670 last_det = max((p for p in det_pos if p < last_meas), default=-1)
5671 cutoff = max(last_det, last_gate_pos)
5672 before: Set[int] = set() # the qubits measured before the final measurement block
5673 for p, qs in zip(meas_pos, meas_qubits):
5674 if p <= cutoff:
5675 before |= qs
5676 final_data = {r for r, (p, qs) in enumerate(zip(meas_pos, meas_qubits)) if p > cutoff and qs and not qs & before}
5677 if not final_data: # (e.g., all qubits are measured in every round): all measurements of the final block
5678 final_data = {r for r, p in enumerate(meas_pos) if p > cutoff}
5679 parent = list(range(len(det_recs)))
5680
5681 def find(a: int) -> int:
5682 while parent[a] != a:
5683 parent[a] = parent[parent[a]]
5684 a = parent[a]
5685 return a
5686
5687 first_det: Dict[int, int] = {} # the first detector of each measurement record (other than final data)
5688 for d, recs in enumerate(det_recs):
5689 for r in recs:
5690 if r not in final_data:
5691 a, b = find(d), find(first_det.setdefault(r, d))
5692 if a != b:
5693 parent[max(a, b)] = min(a, b) # (the root of a group is its first detector)
5694 ids: Dict[int, int] = {}
5695 group = [ids.setdefault(find(d), len(ids)) for d in range(len(det_recs))]
5696 final = [False] * len(ids)
5697 for d, recs in enumerate(det_recs):
5698 if any(r in final_data for r in recs):
5699 final[group[d]] = True
5700 return group, final, len(final_data)
5701
5702
5703def _dem_detector_groups(dem: Any, pmin: float = 0.0) -> Tuple[List[int], List[bool], int, int, int]:
5704 """
5705 Pure-DEM detector chains for compute_model_lower_bound() when no Stim circuit is available (or when the circuit
5706 groups are trivial). Uses detector coordinates (x, y, ..., t) when present in the DEM to group detectors with the
5707 same spatial coordinates across rounds, and falls back to linking consecutive-round detectors via dominant weight-2
5708 observable-free (ancilla measurement) error mechanisms when coordinates are absent.
5709
5710 Returns:
5711 (group, final, num_final, num_detectors, num_observables).
5712 """
5713 if isinstance(dem, (str, Path)):
5714 with open(str(dem)) as f:
5715 lines = f.read().splitlines()
5716 else:
5717 lines = str(dem.flattened() if hasattr(dem, "flattened") else dem).splitlines()
5718
5719 det_coords: Dict[int, Tuple[float, ...]] = {}
5720 max_det = -1
5721 max_obs = -1
5722
5723 def parse_block(i: int, det_shift: int, c_shift: Tuple[float, ...]) -> Tuple[int, int, Tuple[float, ...]]:
5724 nonlocal max_det, max_obs
5725 while i < len(lines):
5726 s = lines[i].split("#", 1)[0].strip()
5727 i += 1
5728 if not s:
5729 continue
5730 if s.startswith("}"):
5731 return i, det_shift, c_shift
5732 if s.startswith("repeat"):
5733 count = int(s.split()[1])
5734 body, end = i, i
5735 for _ in range(count):
5736 end, det_shift, c_shift = parse_block(body, det_shift, c_shift)
5737 if count <= 0:
5738 depth = 1
5739 while end < len(lines) and depth > 0:
5740 t = lines[end].split("#", 1)[0].strip()
5741 end += 1
5742 depth += t.startswith("repeat") - t.startswith("}")
5743 i = end
5744 elif s.startswith("shift_detectors"):
5745 if "(" in s and ")" in s:
5746 vals = [float(x) for x in s[s.index("(") + 1:s.index(")")].split(",") if x.strip()]
5747 n_c = max(len(c_shift), len(vals))
5748 c_shift = tuple(
5749 (c_shift[k] if k < len(c_shift) else 0.0) + (vals[k] if k < len(vals) else 0.0)
5750 for k in range(n_c)
5751 )
5752 rest = s[s.index(")") + 1:].strip()
5753 else:
5754 rest = s.split(None, 1)[1].strip() if len(s.split()) > 1 else "0"
5755 if rest:
5756 det_shift += int(rest.rsplit(None, 1)[-1])
5757 elif s.startswith("detector"):
5758 coords: Tuple[float, ...] = ()
5759 if "(" in s and ")" in s:
5760 vals = [float(x) for x in s[s.index("(") + 1:s.index(")")].split(",") if x.strip()]
5761 coords = tuple(vals[k] + (c_shift[k] if k < len(c_shift) else 0.0) for k in range(len(vals)))
5762 rest = s[s.index(")") + 1:]
5763 else:
5764 rest = s[len("detector"):]
5765 for tok in rest.split():
5766 if tok.startswith("D"):
5767 d = int(tok[1:]) + det_shift
5768 max_det = max(max_det, d)
5769 if coords:
5770 det_coords[d] = coords
5771 elif s.startswith("logical_observable"):
5772 for tok in s.split()[1:]:
5773 if tok.startswith("L"):
5774 max_obs = max(max_obs, int(tok[1:]))
5775 elif s.startswith("error"):
5776 p_text = s[s.index("(") + 1:s.index(")")].strip()
5777 if float(p_text) < pmin:
5778 continue
5779 for tok in s[s.index(")") + 1:].split():
5780 if tok.startswith("D"):
5781 max_det = max(max_det, int(tok[1:]) + det_shift)
5782 elif tok.startswith("L"):
5783 max_obs = max(max_obs, int(tok[1:]))
5784 return i, det_shift, c_shift
5785
5786 parse_block(0, 0, ())
5787 num_det = int(getattr(dem, "num_detectors", max_det + 1) or (max_det + 1))
5788 num_obs = int(getattr(dem, "num_observables", max_obs + 1) or (max_obs + 1))
5789 if num_det <= 0:
5790 return [], [], 0, 0, max(0, num_obs)
5791
5792 if len(det_coords) == num_det and all(len(c) >= 2 for c in det_coords.values()):
5793 spatial_ids: Dict[Tuple[float, ...], int] = {}
5794 group = [spatial_ids.setdefault(det_coords[d][:-1], len(spatial_ids)) for d in range(num_det)]
5795 if 1 <= len(spatial_ids) < num_det:
5796 max_t = max(c[-1] for c in det_coords.values())
5797 final = [False] * len(spatial_ids)
5798 for d in range(num_det):
5799 if det_coords[d][-1] == max_t:
5800 final[group[d]] = True
5801 if all(final):
5802 counts = [0] * len(spatial_ids)
5803 for g in group:
5804 counts[g] += 1
5805 max_c = max(counts, default=0)
5806 if max_c > 1 and 0 < sum(c == max_c for c in counts) < len(spatial_ids):
5807 final = [c == max_c for c in counts]
5808 return group, final, sum(final), num_det, max(0, num_obs)
5809
5810 cols_prob = _dem_error_columns(dem, pmin, with_prob=True)
5811 pair_prob: Dict[Tuple[int, int], float] = {}
5812 best_inc = [0.0] * num_det
5813 for dets, obs, p_text in cols_prob:
5814 if len(dets) == 2 and not obs:
5815 u, v = sorted(dets)
5816 if 0 <= u < v < num_det:
5817 p = float(p_text)
5818 if p > pair_prob.get((u, v), -1.0):
5819 pair_prob[(u, v)] = p
5820 if p > best_inc[u]:
5821 best_inc[u] = p
5822 if p > best_inc[v]:
5823 best_inc[v] = p
5824
5825 parent = list(range(num_det))
5826
5827 def find(a: int) -> int:
5828 while parent[a] != a:
5829 parent[a] = parent[parent[a]]
5830 a = parent[a]
5831 return a
5832
5833 has_next = [False] * num_det
5834 has_prev = [False] * num_det
5835 cand_edges = [
5836 (p, u, v) for (u, v), p in pair_prob.items()
5837 if p >= 0.5 * max(best_inc[u], best_inc[v])
5838 ]
5839 cand_edges.sort(key=lambda item: (-item[0], item[1], item[2]))
5840 for _, u, v in cand_edges:
5841 if not has_next[u] and not has_prev[v]:
5842 ru, rv = find(u), find(v)
5843 if ru != rv:
5844 parent[max(ru, rv)] = min(ru, rv)
5845 has_next[u] = True
5846 has_prev[v] = True
5847
5848 ids: Dict[int, int] = {}
5849 group = [ids.setdefault(find(d), len(ids)) for d in range(num_det)]
5850 counts = [0] * len(ids)
5851 for g in group:
5852 counts[g] += 1
5853 max_len = max(counts, default=0)
5854 if max_len > 1 and 0 < sum(c == max_len for c in counts) < len(ids):
5855 final = [c == max_len for c in counts]
5856 else:
5857 final = [True] * len(ids)
5858 return group, final, sum(final), num_det, max(0, num_obs)
5859
5860
5862 cols: List[Tuple[Any, ...]], group: List[int], use: List[bool], num_observables: int
5863) -> Dict[str, Any]:
5864 """
5865 The matrices of a code-sized model (see compute_model_lower_bound()): one row per group of detectors with use[g]
5866 (the sum of the rows of its detectors), and one column per distinct pair (model column, observables) of the error
5867 mechanisms `cols` (see _dem_error_columns()); the errors with no effect on the rows nor on the observables are
5868 dropped.
5869
5870 Returns:
5871 dict with "H" and "L" (scipy.sparse.csr_matrix), "col_map" (the model column of each error mechanism, -1 if
5872 it is dropped), "groups" (the group of each row), and "trivial" (the error mechanisms which flip an observable
5873 but no row: then the model has a codeword of weight 1).
5874 """
5875 import numpy as np
5876 import scipy.sparse as sp
5877 index: Dict[Tuple[frozenset, frozenset], int] = {}
5878 col_map: List[int] = []
5879 trivial: List[int] = []
5880 for j, (dets, obs) in enumerate(cols):
5881 rows: Set[int] = set()
5882 for d in dets:
5883 if use[group[d]]:
5884 rows ^= {group[d]}
5885 if not rows:
5886 col_map.append(-1)
5887 if obs:
5888 trivial.append(j)
5889 continue
5890 col_map.append(index.setdefault((frozenset(rows), obs), len(index)))
5891 used = sorted({g for rows, _ in index for g in rows})
5892 row_of = {g: i for i, g in enumerate(used)}
5893 hi: List[int] = []
5894 hj: List[int] = []
5895 li: List[int] = []
5896 lj: List[int] = []
5897 for c, (rows, obs) in enumerate(index):
5898 hi.extend(row_of[g] for g in rows)
5899 hj.extend([c] * len(rows))
5900 li.extend(obs)
5901 lj.extend([c] * len(obs))
5902 H = sp.csr_matrix((np.ones(len(hi), dtype=np.int8), (hi, hj)), shape=(len(used), len(index)))
5903 L = sp.csr_matrix((np.ones(len(li), dtype=np.int8), (li, lj)), shape=(num_observables, len(index)))
5904 return {"H": H, "L": L, "col_map": col_map, "groups": used, "trivial": trivial}
5905
5906
5908 circuit: Any = None,
5909 dist_m4ri: Optional[str] = None,
5910 method: int = 3,
5911 threads: Optional[int] = None,
5912 timeout: float = 60.0,
5913 num_steps: Optional[int] = None,
5914 dstop: int = 0,
5915 wmax: int = 0,
5916 minority: bool = False,
5917 pmin: float = 0.0,
5918 rounds: Optional[int] = None,
5919 p_noise: float = 0.001,
5920 force_noise: bool = False,
5921 out_model: Optional[Union[str, Path]] = None,
5922 cache_file: Optional[Union[str, Path]] = None,
5923 seed: int = 0,
5924 debug: int = 0,
5925 verbose: bool = False,
5926 nothrottle: bool = False,
5927 pairs: int = -1,
5928 sort_rows: Optional[bool] = None,
5929 cc_prune: Optional[int] = None,
5930 return_info: bool = False,
5931 dem: Optional[Any] = None,
5932 **kwargs
5933) -> Any:
5934 """
5935 Certified lower bound on the circuit-level distance of a Stim memory circuit from its code-sized model.
5936
5937 The model is a row reduction of the DEM (H, L) of the circuit (processed as in compute_dem_distance(), with the
5938 options rounds, p_noise, force_noise, and pmin): two detectors are linked if they share a measurement record other
5939 than a final data measurement, and each group of linked detectors gives one row, the sum of the rows of its
5940 detectors (see _model_detector_groups()). In a memory circuit, the groups are the chains of the detectors of the
5941 checks over the rounds: the chain of a check h of the measured basis telescopes to the parity of the final data
5942 measurements on its support, so that the column of an error mechanism is the syndrome of its net data error (a
5943 data error or a hook error), and the chains of the minority-basis checks (with minority=True) add the net change
5944 of their values between their first and last measurements. Equal columns are merged, and the errors with no effect
5945 on the rows nor on the observables (e.g., ancilla measurement errors) are dropped; the model has a few columns per
5946 data qubit. Since every row is a sum of detector rows, every codeword x of the DEM (Hx = 0, Lx != 0) gives a model
5947 codeword of weight at most |x|: the distance of the model, as certified by CC (its dmin), is a lower bound on the
5948 distance of the DEM. It ignores the timing within the rounds, so it is not tight in general (e.g., 5 for the
5949 bb_72 circuit of distance 6). Any circuit gives a valid bound, but a useful one needs this structure; if an error
5950 flips an observable but no row, the bound is 1 (with a note to stderr).
5951
5952 The binary runs on the model with the rows of the measured-basis chains first, and then (with minority=True) on
5953 the model with all rows, starting from that bound (the extra rows can only raise it): RW first, which ends as soon
5954 as it finds a model codeword of that weight (then the bound is not raised), and only otherwise with `method` (CC
5955 from dmin). The minority-basis rows are off by default: with depolarizing-type noise, every net data error which
5956 flips the measured-basis observable also occurs without a component of the other basis, so that a lightest model
5957 codeword has a version which the minority-basis rows do not detect (they never raised the bound of the benchmark
5958 circuits or of the surface-code circuits of stim, and cost up to 9 s for the mitten_270 circuits); they may raise
5959 it for other noise models (e.g., biased noise). The certified bound is stored as dmin in the cache record of the
5960 DEM (the same key as compute_dem_distance(); "dmin_model": the bound), so that a later compute_dem_distance()
5961 starts CC from it; the distances of the models are cached under their own keys.
5962
5963 Args:
5964 circuit: Stim memory circuit (stim.Circuit or .stim file).
5965 dist_m4ri: Path to dist_m4ri executable (optional).
5966 method: Solver method for the model (1=RW, 2=CC, 3=Bracketing default); only CC certifies a bound.
5967 threads / timeout / num_steps / seed / nothrottle / pairs: As for compute_dem_distance(), for each run of the
5968 binary (at most three).
5969 sort_rows / cc_prune: The row order and the CC search of the binary (see run_dist_m4ri(); None, the default:
5970 the binary's defaults, i.e., the rows of the model sorted by weight, and the branch and bound), for each
5971 run. The bound and the cache keys do not depend on them.
5972 dstop: Stop target (see compute_dem_distance()): the runs end once CC has certified dmin >= dstop.
5973 wmax: Maximum weight of the CC rounds on the model (0: until the bounds meet or the timeout).
5974 minority: If True, the groups without a final data measurement (e.g., the chains of the minority-basis
5975 checks of a CSS circuit) are rows of the model as well (default: False; CLI: --model-minority).
5976 pmin: Probability cutoff for the error mechanisms of the DEM.
5977 rounds / p_noise / force_noise: The processing of the circuit, as in compute_dem_distance().
5978 out_model: If given, the model (with all rows used) is written to PREFIX_HR.mtx and PREFIX_LR.mtx.
5979 cache_file: Optional JSON file path for persistent distance caching.
5980 debug / verbose: As for compute_dem_distance().
5981 return_info: If True, return (bound, info).
5982
5983 Returns:
5984 The certified lower bound, or (bound, info) with return_info=True, where info has "d_info" (the bounds
5985 [dmin, dmax, rw_steps] of the circuit's DEM from the cache record, with dmin including the bound; [bound, 0, 0]
5986 without the cache), "model_d_info" and "measured_d_info" (the bounds of the model and of its measured-basis
5987 part, [] if not run), the sizes "rows", "cols", "rows_measured", "cols_measured", "groups", "groups_final",
5988 "detectors", "final_data", "dem_cols", "trivial" (the error mechanisms which flip an observable but no row),
5989 the model "H", "L", and "col_map" (the model column of each error mechanism of the DEM, -1 if dropped), and
5990 "files" (the files written with out_model).
5991
5992 Raises:
5993 TypeError: for an unknown keyword argument (e.g., a misspelled option).
5994 ValueError: if the input is not a Stim circuit.
5995 """
5996 global _distance_cache
5997 _check_kwargs(compute_model_lower_bound, kwargs, aliases=())
5998 src = dem if dem is not None else circuit
5999 if src is None:
6000 raise ValueError("compute_model_lower_bound: a Stim circuit (.stim) or DEM (.dem) is required")
6001 is_dem_input = dem is not None or (
6002 isinstance(src, (str, Path)) and str(src).endswith(".dem")
6003 ) or (not isinstance(src, (str, Path)) and not hasattr(src, "detector_error_model"))
6004 if isinstance(src, (str, Path)) and not str(src).endswith((".stim", ".dem")):
6005 raise ValueError(f"compute_model_lower_bound: {src} is not a Stim circuit (.stim) or DEM (.dem)")
6006 if not 0.0 <= p_noise <= 0.5:
6007 raise ValueError(f"invalid p_noise={p_noise}: the noise strength must be in [0, 0.5]")
6008 circ_verbose = verbose or bool(debug & PY_DBG_CIRCUITS)
6009 dem_obj, circ, _ = _dem_from_input(
6010 src if is_dem_input else None, None if is_dem_input else src,
6011 rounds=rounds, p_noise=p_noise, circ_verbose=circ_verbose, force_noise=force_noise
6012 )
6013 dem = dem_obj
6014 info: Dict[str, Any] = {"d_info": [0, 0, 0], "model_d_info": [], "measured_d_info": [], "files": []}
6015 if getattr(dem, "num_errors", None) == 0:
6016 sys.stderr.write("# Note: the DEM has no error mechanisms (no noise, e.g., p_noise=0 for a noiseless circuit "
6017 "or with force_noise): no bound computed\n")
6018 return (0, info) if return_info else 0
6019 cols = _dem_error_columns(dem, pmin)
6020 if not cols:
6021 sys.stderr.write("# Note: the DEM has no error mechanisms (no noise, e.g., p_noise=0 for a noiseless circuit "
6022 "or with force_noise): no bound computed\n")
6023 return (0, info) if return_info else 0
6024 if circ is not None:
6025 group, final, num_final = _model_detector_groups(circ)
6026 num_det = int(getattr(dem, "num_detectors", len(group)))
6027 num_obs = int(getattr(dem, "num_observables", 0))
6028 if len(group) != num_det:
6029 raise ValueError(f"compute_model_lower_bound: {len(group)} detectors in the circuit, {num_det} in its DEM")
6030 if len(final) == len(group) and len(group) > 1:
6031 dem_group, dem_final, dem_num_final, _, _ = _dem_detector_groups(dem, pmin)
6032 if len(dem_group) == len(group) and len(dem_final) < len(group):
6033 group, final, num_final = dem_group, dem_final, dem_num_final
6034 else:
6035 group, final, num_final, num_det, num_obs = _dem_detector_groups(dem, pmin)
6036 part = _model_matrices(cols, group, final, num_obs) # the rows of the measured-basis chains
6037 full = _model_matrices(cols, group, [True] * len(final), num_obs) if minority else part
6038 info.update(rows=full["H"].shape[0], cols=full["H"].shape[1], rows_measured=part["H"].shape[0],
6039 cols_measured=part["H"].shape[1], groups=len(final), groups_final=sum(final), detectors=len(group),
6040 final_data=num_final, dem_cols=len(cols), trivial=len(full["trivial"]), H=full["H"], L=full["L"],
6041 col_map=full["col_map"])
6042 if verbose:
6043 print(f"[dist_m4ri] Code-sized model: {info['rows']} x {info['cols']} ({info['rows_measured']} rows of the "
6044 f"chains with final data measurements, {info['rows'] - info['rows_measured']} other), from "
6045 f"{len(cols)} error mechanisms and {len(group)} detectors in {len(final)} groups ({num_final} final data "
6046 f"measurements)")
6047 if out_model:
6048 from scipy.io import mmwrite
6049 for name, M in (("HR", full["H"]), ("LR", full["L"])):
6050 path = f"{out_model}_{name}.mtx"
6051 parent_dir = os.path.dirname(os.path.abspath(path))
6052 os.makedirs(parent_dir, exist_ok=True)
6053 mmwrite(path, M, symmetry="general")
6054 info["files"].append(path)
6055 if verbose:
6056 print(f"[dist_m4ri] Saved the code-sized model to '{info['files'][0]}' and '{info['files'][1]}'")
6057 eff_cache_file = str(Path(cache_file).resolve()) if cache_file is not None else _distance_cache_file
6058
6059 def run(model: Dict[str, Any], dmin: int, run_method: int, wmin: int = 1) -> List[int]:
6060 # the bounds [dmin, dmax, rw_steps] of the model
6062 model["H"], None, model["L"], False, "code-sized model", dist_m4ri=dist_m4ri, method=run_method,
6063 threads=threads, timeout=timeout, num_steps=num_steps, d_exp=0, dmin=dmin, dmax=0, dstop=dstop,
6064 wmin=wmin, wmax=wmax, smax=0, start_list=None, dW=-1, maxC=0, finC=None, outC=None, need_cws=False,
6065 eff_cache_file=eff_cache_file, seed=seed, debug=debug, verbose=verbose, nothrottle=nothrottle,
6066 chunk_size=0, ksub=0, kwin=0, win_mode=0, min_hits=None, cov_cws=100, refresh=None, trust_start=False,
6067 pairs=pairs, sort_rows=sort_rows, cc_prune=cc_prune)
6068 return list(res["d_info"])
6069
6070 if full["trivial"]:
6071 sys.stderr.write(f"# Note: code-sized model: {len(full['trivial'])} error mechanisms flip an observable but no "
6072 f"group of detectors (e.g., the first one, column {full['trivial'][0]} of the DEM), so that "
6073 f"the model has a codeword of weight 1: the bound is 1\n")
6074 bound = 1
6075 else:
6076 bound = 1
6077 if not part["trivial"]:
6078 info["measured_d_info"] = run(part, 0, method)
6079 bound = max(bound, info["measured_d_info"][0])
6080 info["model_d_info"] = info["measured_d_info"]
6081 if full is not part and (dstop <= 0 or bound < dstop) and (wmax <= 0 or bound <= wmax):
6082 # all rows: RW first (a model codeword of weight `bound` ends the search), then CC from the bound
6083 dmin = bound if bound > 1 else 0
6084 info["model_d_info"] = run(full, dmin, 1, wmin=bound)
6085 if method != 1 and not 0 < info["model_d_info"][1] <= bound:
6086 info["model_d_info"] = run(full, dmin, method)
6087 bound = max(bound, info["model_d_info"][0])
6088 info["d_info"] = [bound, 0, 0]
6089 if _use_distance_cache: # the bound as dmin in the cache record of the DEM
6090 if eff_cache_file:
6091 load_distance_cache(eff_cache_file)
6092 key = _dem_cache_key(dem, pmin)
6093 prev = _distance_cache.get(key) or {}
6094 p_dmin, p_dmax, p_rw = (prev.get("dmin", 0) or 0), (prev.get("dmax", 0) or 0), (prev.get("rw_steps", 0) or 0)
6095 if p_dmax > 0 and bound > p_dmax:
6096 sys.stderr.write(f"# Warning: the code-sized model bound {bound} exceeds the cached upper bound "
6097 f"{p_dmax} of the DEM: not stored\n")
6098 info["d_info"] = format_bounds_list(p_dmin, p_dmax, p_rw)
6099 else:
6100 record = _sector_record(max(p_dmin, bound), p_dmax, p_rw, prev.get("cws", []))
6101 record["dmin_model"] = max(bound, prev.get("dmin_model", 0) or 0)
6102 if record != prev:
6103 _distance_cache[key] = record
6104 if eff_cache_file:
6105 save_distance_cache(eff_cache_file)
6106 info["d_info"] = list(record["d_info"])
6107 if verbose:
6108 print(f"[dist_m4ri] Code-sized model: certified lower bound d >= {bound} on the circuit distance (model bounds "
6109 f"{info['model_d_info']}; measured-basis rows only: {info['measured_d_info']})")
6110 return (bound, info) if return_info else bound
6111
6112
6113def _write_nzlist_file(filepath: str, cws: List[List[int]]) -> None:
6114 """Writes codewords to a text file in NZLIST format (1-based indices).
6115 Skips creating the file if cws is empty.
6116 """
6117 if not cws:
6118 return
6119 with open(filepath, "w") as f:
6120 f.write("%% NZLIST\n")
6121 f.write(f"% {len(cws)} codewords\n")
6122 for cw in cws:
6123 f.write(f"{len(cw)} " + " ".join(str(idx + 1) for idx in cw) + "\n")
6124
6125
6126def parse_cli_args(argv: List[str]) -> Dict[str, Any]:
6127 """Parses CLI arguments supporting both key=value pairs and standard --flags."""
6128 args: Dict[str, Any] = {
6129 "method": 3,
6130 "finH": None,
6131 "finG": None,
6132 "finL": None,
6133 "fin": None,
6134 "fdem": None,
6135 "Hx": None,
6136 "Hz": None,
6137 "Lx": None,
6138 "Lz": None,
6139 "finC": None,
6140 "outC": None,
6141 "dmin": 0,
6142 "dmax": 0,
6143 "dstop": 0,
6144 "wmin": 1,
6145 "wmax": 0,
6146 "smax": None,
6147 "start": None,
6148 "cbeg": None,
6149 "cend": None,
6150 "trust_start": False,
6151 "start_scan": False,
6152 "css": None,
6153 "dexp": 0,
6154 "steps": None,
6155 "threads": None,
6156 "timeout": 60.0,
6157 "dW": -1,
6158 "maxC": 0,
6159 "pmin": 0.0,
6160 "merge": None,
6161 "expand": None,
6162 "drop": None,
6163 "outD": None,
6164 "out_reduced": None,
6165 "sort_rows": None,
6166 "cc_prune": None,
6167 "ortho": True,
6168 "noscan": 0,
6169 "irred": None,
6170 "classical": -1,
6171 "seed": 0,
6172 "debug": 0,
6173 "solver": "dist_m4ri",
6174 "engine": None,
6175 "cache_file": "tmp_dist_cache.json",
6176 "use_cache": True,
6177 "do_cws": False,
6178 "verbose": False,
6179 "silent": False,
6180 "nothrottle": False,
6181 "chunk_size": 0,
6182 "ksub": 0,
6183 "kwin": 0,
6184 "win_mode": 0,
6185 "win_hyb": 0,
6186 "pairs": -1,
6187 "min_hits": None,
6188 "cov_cws": 100,
6189 "refresh": None,
6190 "simple": None,
6191 "full": None,
6192 "basis": None,
6193 "rounds": None,
6194 "p_noise": None,
6195 "force_noise": False,
6196 "out_dir": None,
6197 "out_dem": None,
6198 "out_stim": None,
6199 "model_lb": False,
6200 "model_minority": False,
6201 "out_model": None,
6202 "morehelp": False,
6203 "morehelp_topic": None,
6204 "version": False,
6205 "unrecognized": [],
6206 "ignored_files": [],
6207 }
6208
6209 def _has_later_input_file(start_idx: int) -> bool:
6210 for tok in argv[start_idx:]:
6211 if (
6212 not tok.startswith("-")
6213 and "=" not in tok
6214 and tok.endswith((".stim", ".dem", ".mtx", ".mmx"))
6215 and os.path.exists(tok)
6216 ):
6217 return True
6218 return False
6219
6220 def _add_help_topic(topic: Optional[str]) -> None: # --morehelp TOPIC, --help TOPIC (several: comma-separated)
6221 args["morehelp"] = True
6222 if topic and topic != "1":
6223 args["morehelp_topic"] = f"{args['morehelp_topic']},{topic}" if args["morehelp_topic"] else topic
6224
6225 def _help_topic_at(j: int) -> Optional[str]: # the topic after --morehelp / --help: not an option or a file
6226 nxt = argv[j] if j < len(argv) else None
6227 return nxt if nxt and not nxt.startswith("-") and "=" not in nxt and not os.path.exists(nxt) else None
6228
6229 i = 0
6230 while i < len(argv):
6231 arg = argv[i]
6232 if not arg:
6233 i += 1
6234 continue
6235
6236 if arg in ("--version", "-version", "version"):
6237 args["version"] = True
6238 i += 1
6239 continue
6240
6241 if arg in ("--morehelp", "-morehelp", "--more-help", "-more-help", "morehelp"):
6242 topic = _help_topic_at(i + 1)
6243 _add_help_topic(topic) # --morehelp TOPIC: only the entries of this parameter
6244 i += 2 if topic else 1
6245 continue
6246
6247 if arg in ("-h", "--help", "help", "-?"):
6248 topic = _help_topic_at(i + 1)
6249 if topic:
6250 _add_help_topic(topic) # --help TOPIC: the same as --morehelp TOPIC
6251 else:
6252 args["help"] = True
6253 i += 2 if topic else 1
6254 continue
6255
6256 if arg in ("--no-throttle", "-no-throttle", "nothrottle", "--nothrottle"):
6257 args["nothrottle"] = True
6258 i += 1
6259 continue
6260
6261 if arg in ("-v", "--verbose", "verbose", "-verbose"):
6262 args["verbose"] = True
6263 i += 1
6264 continue
6265
6266 if arg in ("-q", "--silent", "-silent", "silent", "--quiet"):
6267 args["silent"] = True
6268 i += 1
6269 continue
6270
6271 if arg in ("--no-cache", "-no-cache", "nocache", "--nocache"):
6272 args["use_cache"] = False
6273 args["cache_file"] = None
6274 i += 1
6275 continue
6276
6277 if arg in ("--trust-start", "-trust-start", "--trust_start", "-trust_start", "trust_start", "trust-start"):
6278 args["trust_start"] = True
6279 i += 1
6280 continue
6281
6282 if arg in ("--start-scan", "-start-scan", "--start_scan", "-start_scan", "start_scan", "start-scan"):
6283 args["start_scan"] = True
6284 i += 1
6285 continue
6286
6287 if arg in ("--cws", "-cws", "cws", "do_cws=1", "--do_cws"):
6288 args["do_cws"] = True
6289 i += 1
6290 continue
6291
6292 if arg in ("--model-lb", "-model-lb", "--model_lb", "-model_lb", "model_lb", "model-lb"):
6293 args["model_lb"] = True
6294 i += 1
6295 continue
6296
6297 if arg in ("--model-minority", "-model-minority", "--model_minority", "-model_minority", "model_minority",
6298 "model-minority"):
6299 args["model_minority"] = True
6300 i += 1
6301 continue
6302
6303 if arg in (
6304 "--simple", "-simple", "--simplified", "-simplified",
6305 "simple", "simplified"
6306 ):
6307 args["simple"] = True # disabled since version 0.13.0 (ignored with a warning)
6308 i += 1
6309 continue
6310
6311 if arg in ("--full", "-full", "full"):
6312 args["full"] = True # disabled since version 0.13.0 (ignored with a warning)
6313 i += 1
6314 continue
6315
6316 if arg in ("--force-noise", "-force-noise", "--force_noise", "-force_noise", "force_noise", "force-noise",
6317 "--force", "-force"):
6318 args["force_noise"] = True
6319 i += 1
6320 continue
6321
6322 if arg in ("--rounds", "-rounds", "rounds"):
6323 if (
6324 i + 1 < len(argv)
6325 and "=" not in argv[i + 1]
6326 and argv[i + 1].lstrip("-").isdigit()
6327 ):
6328 args["rounds"] = int(argv[i + 1])
6329 i += 2
6330 else:
6331 args["rounds"] = 2
6332 i += 1
6333 continue
6334
6335 if arg in ("--out-dem", "-out-dem", "--out_dem", "-out_dem", "out_dem"):
6336 next_tok = argv[i + 1] if i + 1 < len(argv) else None
6337 if (
6338 next_tok is not None
6339 and not next_tok.startswith("-")
6340 and "=" not in next_tok
6341 and not next_tok.endswith((".stim", ".mtx", ".mmx"))
6342 and not (
6343 next_tok.endswith(".dem")
6344 and os.path.exists(next_tok)
6345 and args["fdem"] is None
6346 and not _has_later_input_file(i + 2)
6347 )
6348 ):
6349 args["out_dem"] = next_tok
6350 i += 2
6351 else:
6352 args["out_dem"] = True
6353 i += 1
6354 continue
6355
6356 if arg in ("--out-stim", "-out-stim", "--out_stim", "-out_stim", "out_stim"):
6357 next_tok = argv[i + 1] if i + 1 < len(argv) else None
6358 if (
6359 next_tok is not None
6360 and not next_tok.startswith("-")
6361 and "=" not in next_tok
6362 and not next_tok.endswith((".dem", ".mtx", ".mmx"))
6363 and not (
6364 next_tok.endswith(".stim")
6365 and os.path.exists(next_tok)
6366 and args["fdem"] is None
6367 and not _has_later_input_file(i + 2)
6368 )
6369 ):
6370 args["out_stim"] = next_tok
6371 i += 2
6372 else:
6373 args["out_stim"] = True
6374 i += 1
6375 continue
6376
6377 if arg in ("--out-reduced", "-out-reduced", "--out_reduced", "-out_reduced", "out_reduced", "out-reduced"):
6378 next_tok = argv[i + 1] if i + 1 < len(argv) else None
6379 if ( # the prefix: not an option, nor an existing input file
6380 next_tok is not None
6381 and not next_tok.startswith("-")
6382 and "=" not in next_tok
6383 and not (next_tok.endswith((".stim", ".dem", ".mtx", ".mmx")) and os.path.exists(next_tok))
6384 ):
6385 args["out_reduced"] = next_tok
6386 i += 2
6387 else:
6388 args["out_reduced"] = True
6389 i += 1
6390 continue
6391
6392 key = None
6393 val = None
6394 if "=" in arg:
6395 key, val = arg.split("=", 1)
6396 if key.startswith("--"):
6397 key = key[2:]
6398 elif key.startswith("-"):
6399 key = key[1:]
6400 elif arg.startswith("--") or arg.startswith("-"):
6401 key = arg.lstrip("-")
6402 if i + 1 < len(argv) and not argv[i + 1].startswith("-") and "=" not in argv[i + 1]:
6403 val = argv[i + 1]
6404 i += 1
6405 else:
6406 val = "1"
6407 else:
6408 if os.path.exists(arg):
6409 if arg.endswith(".dem") or arg.endswith(".stim"):
6410 args["fdem"] = arg
6411 elif arg.endswith(".mmx") or arg.endswith(".mtx"):
6412 if args["finH"] is None:
6413 args["finH"] = arg
6414 elif args["finG"] is None and args["finL"] is None:
6415 args["finG"] = arg
6416 else:
6417 args["ignored_files"].append(arg)
6418 else:
6419 args["ignored_files"].append(arg)
6420 else:
6421 args["unrecognized"].append(arg)
6422 i += 1
6423 continue
6424
6425 if key:
6426 key_lower = key.lower()
6427 if key_lower in ("cache", "cache_file", "cachefile"):
6428 if val.lower() in ("0", "none", "false", "off", "no"):
6429 args["use_cache"] = False
6430 args["cache_file"] = None
6431 else:
6432 args["use_cache"] = True
6433 args["cache_file"] = val
6434 elif key_lower in ("fdem", "dem"):
6435 args["fdem"] = val
6436 elif key_lower == "finh":
6437 args["finH"] = val
6438 elif key_lower == "fing":
6439 args["finG"] = val
6440 elif key_lower == "finl":
6441 args["finL"] = val
6442 elif key_lower == "fin":
6443 args["fin"] = val
6444 elif key_lower in ("hx", "finhx"):
6445 args["Hx"] = val
6446 elif key_lower in ("hz", "finhz"):
6447 args["Hz"] = val
6448 elif key_lower in ("lx", "finlx"):
6449 args["Lx"] = val
6450 elif key_lower in ("lz", "finlz"):
6451 args["Lz"] = val
6452 elif key_lower == "finc":
6453 args["finC"] = val
6454 elif key_lower == "outc":
6455 args["outC"] = val
6456 args["do_cws"] = True
6457 elif key_lower in ("method", "m"):
6458 args["method"] = int(val)
6459 elif key_lower in ("dmin", "d_min"):
6460 args["dmin"] = int(val)
6461 elif key_lower in ("dmax", "d_max"):
6462 args["dmax"] = int(val)
6463 elif key_lower == "dstop":
6464 args["dstop"] = int(val)
6465 if args["dstop"] < 0:
6466 raise ValueError(f"invalid '{arg}': dstop must be non-negative")
6467 elif key_lower in ("wmin", "w_min"):
6468 args["wmin"] = int(val)
6469 elif key_lower in ("wmax", "w_max"):
6470 args["wmax"] = int(val)
6471 elif key_lower == "smax":
6472 args["smax"] = int(val)
6473 elif key_lower == "start":
6474 args["start"] = _normalize_start_list(val)
6475 elif key_lower in ("trust_start", "trust-start", "truststart"):
6476 args["trust_start"] = (
6477 bool(int(val)) if val.isdigit() else (val.lower() not in ("0", "false", "no", "off"))
6478 )
6479 elif key_lower in ("start_scan", "start-scan", "startscan"):
6480 if val not in ("0", "1"):
6481 raise ValueError(f"invalid '{arg}': start_scan should be 0 or 1")
6482 args["start_scan"] = val == "1"
6483 elif key_lower == "cbeg":
6484 args["cbeg"] = int(val)
6485 elif key_lower == "cend":
6486 args["cend"] = int(val)
6487 elif key_lower == "css":
6488 args["css"] = int(val)
6489 elif key_lower in ("dexp", "dest", "d_exp"):
6490 args["dexp"] = int(val)
6491 elif key_lower in ("steps", "num_steps", "nsteps"):
6492 args["steps"] = int(val)
6493 elif key_lower in ("threads", "num_threads", "t"):
6494 args["threads"] = int(val)
6495 elif key_lower in ("timeout", "time"):
6496 args["timeout"] = float(val)
6497 elif key_lower == "dw":
6498 args["dW"] = int(val)
6499 elif key_lower in ("maxc", "max_c"):
6500 args["maxC"] = int(val)
6501 elif key_lower == "pmin":
6502 args["pmin"] = float(val)
6503 elif key_lower in ("merge", "expand"):
6504 if val.lower() in ("1", "true", "yes", "on"):
6505 args[key_lower] = True
6506 elif val.lower() in ("0", "false", "no", "off"):
6507 args[key_lower] = False
6508 else:
6509 raise ValueError(f"invalid '{arg}': {key_lower} should be 0 or 1")
6510 elif key_lower in ("sort_rows", "sort-rows", "sortrows"):
6511 if val.lower() in ("1", "true", "yes", "on"):
6512 args["sort_rows"] = True
6513 elif val.lower() in ("0", "false", "no", "off"):
6514 args["sort_rows"] = False
6515 else:
6516 raise ValueError(f"invalid '{arg}': sort_rows should be 0 or 1")
6517 elif key_lower in ("cc_prune", "cc-prune", "ccprune"):
6518 if val not in ("0", "1", "2"):
6519 raise ValueError(f"invalid '{arg}': cc_prune should be 0, 1, or 2")
6520 args["cc_prune"] = int(val)
6521 elif key_lower == "ortho":
6522 if val not in ("0", "1"):
6523 raise ValueError(f"invalid '{arg}': ortho should be 0 or 1")
6524 args["ortho"] = val == "1"
6525 elif key_lower == "drop": # (the values take precedence over files with these names, as in the binary)
6526 if val.lower() in ("1", "true", "yes", "on", "auto"):
6527 args["drop"] = True # drop=auto
6528 elif val.lower() in ("0", "false", "no", "off"):
6529 args["drop"] = False
6530 elif val.lower() == "css":
6531 args["drop"] = "css"
6532 else:
6533 args["drop"] = val # a file with the detectors to drop
6534 elif key_lower == "outd":
6535 args["outD"] = val
6536 elif key_lower in ("out-reduced", "out_reduced", "outreduced"):
6537 if val.lower() in ("0", "false", "no", "off", "none"):
6538 args["out_reduced"] = None
6539 elif val.lower() in ("1", "true", "yes", "auto", ""):
6540 args["out_reduced"] = True
6541 else:
6542 args["out_reduced"] = val
6543 elif key_lower in ("morehelp", "more-help", "more_help"):
6544 _add_help_topic(val) # (morehelp=1: the full help)
6545 elif key_lower in ("help", "h"):
6546 if val in ("", "1"):
6547 args["help"] = True
6548 else:
6549 _add_help_topic(val) # help=TOPIC: the same as morehelp=TOPIC
6550 elif key_lower == "noscan":
6551 args["noscan"] = int(val)
6552 elif key_lower == "irred": # (disabled: ignored with a warning, see _warn_cli_conflicts())
6553 args["irred"] = int(val)
6554 elif key_lower == "classical":
6555 args["classical"] = int(val)
6556 elif key_lower == "seed":
6557 args["seed"] = int(val)
6558 elif key_lower in ("debug", "dbg"):
6559 args["debug"] |= parse_debug_value(val) # OR-combined (the default is 0)
6560 elif key_lower == "solver":
6561 args["solver"] = val
6562 elif key_lower == "engine":
6563 args["engine"] = val
6564 elif key_lower in ("verbose", "v"):
6565 args["verbose"] = bool(int(val)) if val.isdigit() else (val.lower() not in ("0", "false", "no", "off"))
6566 elif key_lower == "cws":
6567 args["do_cws"] = bool(int(val)) if val.isdigit() else (val.lower() not in ("0", "false", "no"))
6568 elif key_lower in ("nothrottle", "no_throttle"):
6569 args["nothrottle"] = (
6570 bool(int(val)) if val.isdigit() else (val.lower() not in ("0", "false", "no", "off"))
6571 )
6572 elif key_lower in ("chunk_size", "chunksize", "batch", "chunk"):
6573 args["chunk_size"] = int(val)
6574 elif key_lower == "ksub":
6575 args["ksub"] = int(val)
6576 elif key_lower in ("kwin", "win"):
6577 args["kwin"] = int(val)
6578 elif key_lower in ("win_mode", "winmode"):
6579 args["win_mode"] = int(val)
6580 elif key_lower in ("win_hyb", "winhyb"):
6581 args["win_hyb"] = int(val)
6582 if args["win_hyb"] < 0:
6583 raise ValueError(f"invalid '{arg}': win_hyb should be 0 (256 columns for a DEM, 512 for "
6584 f"matrices) or positive")
6585 elif key_lower == "pairs":
6586 args["pairs"] = int(val)
6587 if not -1 <= args["pairs"] <= 64:
6588 raise ValueError(f"invalid '{arg}': pairs should be -1 (auto), 0 (off), or 1 to 64 (random pivot "
6589 f"rows of the buckets)")
6590 elif key_lower in ("min_hits", "minhits", "max_hits", "maxhits"):
6591 args["min_hits"] = int(val)
6592 elif key_lower in ("cov_cws", "covcws"):
6593 args["cov_cws"] = int(val)
6594 elif key_lower == "refresh":
6595 args["refresh"] = int(val)
6596 elif key_lower in ("simple", "simplified", "full"): # disabled since version 0.13.0 (ignored, a warning)
6597 args["full" if key_lower == "full" else "simple"] = (
6598 bool(int(val)) if val.isdigit()
6599 else (val.lower() not in ("0", "false", "no", "off"))
6600 )
6601 elif key_lower == "basis":
6602 if val.upper() not in ("X", "Z"):
6603 raise ValueError(
6604 f"Invalid basis '{val}'; must be 'X' or 'Z'."
6605 )
6606 args["basis"] = val.upper()
6607 elif key_lower == "rounds":
6608 args["rounds"] = int(val)
6609 elif key_lower in ("p_noise", "p-noise", "pnoise"):
6610 args["p_noise"] = float(val)
6611 if not 0.0 <= args["p_noise"] <= 0.5:
6612 raise ValueError(f"invalid '{arg}': the noise strength must be in [0, 0.5]")
6613 elif key_lower in ("force_noise", "force-noise", "forcenoise", "force"):
6614 args["force_noise"] = (
6615 bool(int(val)) if val.isdigit() else (val.lower() not in ("0", "false", "no", "off"))
6616 )
6617 elif key_lower in ("out-dir", "out_dir", "outdir"):
6618 args["out_dir"] = val
6619 elif key_lower in ("out-dem", "out_dem", "outdem"):
6620 if val.lower() in ("0", "false", "no", "off", "none"):
6621 args["out_dem"] = None
6622 elif val.lower() in ("1", "true", "yes", "auto", ""):
6623 args["out_dem"] = True
6624 else:
6625 args["out_dem"] = val
6626 elif key_lower in ("model-lb", "model_lb", "modellb"):
6627 args["model_lb"] = (
6628 bool(int(val)) if val.isdigit() else (val.lower() not in ("0", "false", "no", "off"))
6629 )
6630 elif key_lower in ("model-minority", "model_minority", "modelminority"):
6631 args["model_minority"] = (
6632 bool(int(val)) if val.isdigit() else (val.lower() not in ("0", "false", "no", "off"))
6633 )
6634 elif key_lower in ("out-model", "out_model", "outmodel"):
6635 args["out_model"] = val
6636 elif key_lower in ("out-stim", "out_stim", "outstim"):
6637 if val.lower() in ("0", "false", "no", "off", "none"):
6638 args["out_stim"] = None
6639 elif val.lower() in ("1", "true", "yes", "auto", ""):
6640 args["out_stim"] = True
6641 else:
6642 args["out_stim"] = val
6643 else:
6644 args["unrecognized"].append(arg)
6645
6646 i += 1
6647
6648 # Auto-infer classical mode when not explicitly set (matching src/util_io.c)
6649 if args["classical"] == -1:
6650 if (
6651 args["finG"] is not None or args["finL"] is not None
6652 or args["Hz"] is not None or args["Lz"] is not None
6653 or args["Lx"] is not None
6654 or args["fdem"] is not None or args["fin"] is not None
6655 ):
6656 args["classical"] = 0
6657 elif args["finH"] is not None or args["Hx"] is not None:
6658 args["classical"] = 1
6659
6660 return args
6661
6662
6663def print_cli_short_help(file: Optional[Any] = None) -> None:
6664 text = f"""dist_m4ri.py (version {__version__}): Multithreaded distance calculator Python CLI
6665Usage: dist_m4ri.py [key=val | --flag val ...]
6666
6667Allowed parameters:
6668 fdem, finH, finG, finL, fin, Hx, Hz, Lx, Lz, pmin, merge, drop, outD,
6669 --out-reduced, classical, --simple, --full, basis, --rounds, --out-dir,
6670 --out-dem, --out-stim, --p-noise, --force-noise, --model-lb, --model-minority,
6671 --out-model, method, dmin, dmax, dstop, dexp (dest), steps, wmin, wmax,
6672 timeout, threads, nothrottle, chunk_size (batch), ksub, kwin (win), win_mode,
6673 win_hyb, pairs, min_hits, cov_cws, refresh, smax, noscan, start, start_scan,
6674 cbeg, cend, finC, outC, maxC, expand, dW, irred, seed, debug, solver, engine,
6675 cache, --no-cache, --verbose, --silent, --cws, --trust-start, sort_rows,
6676 cc_prune, ortho
6677
6678Help options:
6679 -h, --help : display help for commonly used parameters (fits 80 rows; --help TOPIC: as --morehelp TOPIC)
6680 --morehelp : display full help for all available parameters (or for TOPIC: --morehelp TOPIC)
6681 --version : display the versions of dist_m4ri.py and of the dist_m4ri binary
6682"""
6683 print(text, file=file)
6684
6685
6686def print_cli_help(file: Optional[Any] = None) -> None:
6687 help_text = f"""dist_m4ri.py (version {__version__}): Multithreaded distance calculator Python CLI
6688Usage: dist_m4ri.py [key=val | --flag val ...]
6689
6690Input matrices & models:
6691 fdem=FILE Detector Error Model (.dem) or Stim circuit (.stim) file
6692 finH=FILE Parity check matrix H (classical) or Hx (CSS quantum) (.mmx/.mtx)
6693 finG=FILE, finL=FILE Hz check matrix or Lx logical operator matrix (quantum CSS)
6694 fin=PREFIX Base prefix for CSS matrices (loads ${{fin}}X.mtx, ${{fin}}Z.mtx, e.g. try -> tryX.mtx)
6695 Hx=FILE, Hz=FILE CSS check matrices, both required (computes dX and dZ)
6696 Lx=FILE, Lz=FILE CSS logical operators (optional, constructed if omitted)
6697 pmin=PROB Minimum error probability threshold for DEM errors (default: 0.0)
6698 merge=0|1 Merge equal DEM columns (exact; codewords use the first one) (default: 1)
6699 drop=auto|0|css|FILE DEM detectors to drop if verified exact: auto=1 (default), 0 (none), css, or a file
6700 outD=FILE Write the DEM detectors dropped (0-based) to FILE
6701 --out-reduced [PFX] Save the reduced DEM and its maps to the DEM: PFX_red.dem and PFX_red.json
6702 classical=0|1 1: classical code (finH only), 0: quantum CSS (auto-detected)
6703 basis=X|Z Override primary memory basis ('X' or 'Z') for .stim circuits (with drop=css)
6704 --rounds [N] Set the REPEAT count of a .stim circuit (N=2 if omitted; warns if no REPEAT)
6705 --out-dir DIR Output directory for saved .dem/.stim files (default: input file dir)
6706 --out-dem [FILE] Save constructed DEM (default: <basename>_full.dem)
6707 --out-stim [FILE] Save processed/noisy Stim circuit (default: <basename>_full.stim)
6708 --p-noise P Strength of the noise added to a noiseless .stim circuit (default: 0.001; 0: none)
6709 --force-noise Replace the noise of a .stim circuit by that of --p-noise (with P=0: remove it)
6710 --model-lb Certified lower bound for a .stim circuit from its code-sized model
6711 --model-minority With --model-lb, also the rows of the minority-basis checks (default: off)
6712
6713Method and distance bounds:
6714 method=1|2|3 1=RW (upper bound), 2=CC (lower bound/exact), 3=Bracketing (default: 3)
6715 dmin=N Certified lower bound, inclusive (CC starts from dmin) (default: 0)
6716 dmax=N Known upper bound, inclusive (CC up to w=dmax-1 only) (default: 0)
6717 dstop=N Stop once CC certifies dmin>=dstop (not an upper bound) (default: 0)
6718 dexp=N Expected distance estimate (alias: dest) (default: 0)
6719
6720Search limits and stopping criteria:
6721 steps=N Maximum RW steps / information sets (default: 100000)
6722 wmax=N Maximum cluster weight to search in CC (0=until bound/timeout)
6723 wmin=N Stop immediately if cw with weight <= wmin is found (default: 1)
6724 min_hits=N Stop RW when lowest-weight cws are hit N times on average (default: 5)
6725 timeout=SEC Execution timeout in seconds, 0 for infinite (default: 60.0)
6726
6727Multithreading & execution:
6728 threads=N Max worker threads (default: all logical CPUs; throttled unless nothrottle=1)
6729 solver=NAME Distance solver backend: 'dist_m4ri' (default), 'cert', 'auto', or 'codedistance' (also engine=)
6730
6731Codeword collection & caching:
6732 --cws Collect and output non-trivial minimum-weight codewords
6733 outC=FILE Save output codewords (for CSS, auto-suffixed _X.nz / _Z.nz)
6734 finC=FILE Input initial candidate codewords (.nz file)
6735 expand=0|1 DEM codewords: list all choices among equal columns (merge) (default: 0)
6736 cache=FILE Persistent JSON cache file (default: tmp_dist_cache.json)
6737 --no-cache / nocache Disable persistent JSON caching
6738 --verbose / -v Explain bounds, steps, cache status, and miss probability estimates
6739 --silent / -q No warnings on stderr for the inputs which are ignored
6740
6741Extra parameters (see --morehelp for details):
6742 smax=N (0) Max syndrome weight for CC confinement profile (0 to disable)
6743 start=LIST (-1) Expert: CC from listed columns only, e.g. start=0,48 (also start_scan=1; see --morehelp)
6744 --trust-start Expert: accept start-list results as valid (copied to main cache record)
6745 noscan, cbeg, cend Disabled in Python (ignored with a warning; also irred=0, --simple, --full)
6746 ortho=0|1 (1) Expert: 0 skips the check H G^T = 0 of G (valid only if it holds)
6747 sort_rows=0|1 Sort the rows of H by weight for CC (default: 1 for matrices, 0 for a DEM)
6748 cc_prune=0|1|2 (1) CC search: 0 first-row walk, 1 branch and bound, 2 also cover bounds
6749 nothrottle=1 (0) Disable automatic thread throttling (also --no-throttle)
6750 chunk_size=N (0) Max RW batch per claim (default: 0 for adaptive 25-500, alias: batch)
6751 ksub=N (0) EXPERIMENTAL, do not use: RW subspace sketch dimension (0: full matrix)
6752 kwin=N (0) RW localized window size (0: auto/hybrid for n>=500, -1: none, alias: win)
6753 win_mode=0|1 (0) RW window metric: 0=Tanner graph BFS, 1=index proximity
6754 win_hyb=N (0) Hybrid RW window size (0: 256 for a DEM, 512 for matrices)
6755 pairs=N (-1) RW pairs of non-pivot columns (p=2; -1: auto, 0: off)
6756 cov_cws=N (100) Number of lowest-weight cws used for the min_hits statistic
6757 refresh=N (5000) Basis refresh interval in RW steps (experimental ksub only; 0 = off)
6758 maxC=N (0) Maximum number of codewords to collect (0: unlimited)
6759 dW=N (0) Extra weight window above dmin to collect codewords
6760 seed=N (0) Random number generator seed
6761 debug=N (0) Debug bitmap, OR-combined (1: summary, 2: progress, 4: status, ...)
6762
6763Help options:
6764 -h, --help [TOPIC] Display this help message (with TOPIC: the same as --morehelp TOPIC)
6765 --morehelp [TOPIC] Display full help for all parameters (or only for TOPIC, e.g., maxC or windows)
6766 --version Display the versions of dist_m4ri.py and of the dist_m4ri binary
6767"""
6768 print(help_text, file=file)
6769
6770
6772 """The full help text of the CLI (--morehelp)."""
6773 return f"""dist_m4ri.py (version {__version__}): Multithreaded distance calculator Python CLI
6774Usage: dist_m4ri.py [key=val | --flag val ...]
6775
6776Required input (at least one matrix/model specification):
6777 fdem=FILE Detector Error Model (.dem) or Stim circuit (.stim) file.
6778 Automatically constructs parity check H and logical L matrices. An error
6779 channel which stim cannot write as independent errors (e.g., PAULI_CHANNEL_1
6780 without Y errors) is converted with approximate_disjoint_errors=True (only the
6781 probabilities change).
6782 finH=FILE Parity check matrix file in Matrix Market (.mmx / .mtx) format.
6783 For classical codes, this is check matrix H. For CSS codes, Hx.
6784 finG=FILE Generator / Hz matrix for quantum CSS codes in Matrix Market format.
6785 finL=FILE Logical operator matrix Lx for quantum CSS codes in Matrix Market format.
6786 Note: For a quantum CSS code, either finL (Lx) or finG (Hz) is required.
6787 fin=PREFIX Base prefix for CSS matrices (loads ${{fin}}X.mtx and ${{fin}}Z.mtx, e.g. try -> tryX.mtx).
6788 Hx=FILE, Hz=FILE CSS check matrices Hx and Hz, both required: computes both dX and dZ, and
6789 d = min(dX, dZ) (finH=Hx with finG=Hz gives dZ only).
6790 Lx=FILE, Lz=FILE Alternative syntax for specifying CSS logical operator matrices.
6791 ortho=0|1 Expert option (default: 1): 1, the dist_m4ri binary checks that the rows of H
6792 and G (finG, fin without finL, or Hx and Hz in each sector of a CSS code without
6793 Lx or Lz) are orthogonal, H G^T = 0, and stops with an error otherwise; 0 skips
6794 the check (a warning is printed), e.g., to screen many codes which are
6795 orthogonal by construction (the check takes O(nnz(G) times the column weight
6796 of H) operations). The result is valid only if H G^T = 0. The cache key does
6797 not depend on it. No effect without G (a warning).
6798 pmin=PROB Minimum error probability threshold for DEM parsing (default: 0.0).
6799 Error mechanisms with probability < pmin are filtered out. The column
6800 indices (start, finC, outC) count only the error mechanisms kept: with
6801 another pmin, the columns (and the codewords) change.
6802 merge=0|1 Merge the equal columns of the DEM (error mechanisms with the same detectors
6803 and observables) in the dist_m4ri binary (default: 1). Exact: dropping two
6804 equal columns of a codeword gives a lighter one, so the distance does not
6805 change (nor does the cache key). Column indices (finC, outC, start) are those
6806 of the DEM; the codewords found use the first one of each set of equal columns
6807 (use merge=0 to enumerate the codewords with each of them, or expand=1 to
6808 expand the list found).
6809 drop=auto|0|css|FILE The detectors which the binary drops from the DEM if this is verified to be
6810 exact (default: auto; drop=1 is the same as drop=auto; drop=0 keeps all
6811 detectors). Exact if every error touching them has a twin, an error with the
6812 same other detectors which touches none of them (or none if it has no other
6813 detector), with the same observables (or with L + A H for one matrix A):
6814 then the distance of the DEM is found from the smaller DEM without these
6815 detectors and the errors touching them, and the codewords found (outC) are
6816 codewords of the DEM which use only errors touching no dropped detector.
6817 drop=auto: the binary searches for the largest such set (with the same
6818 observables) in the DEM alone, e.g., the minority-basis detectors of a CSS
6819 circuit with the usual noise (see the README). drop=css: for a Stim circuit
6820 classified as CSS (including locally rotated CSS such as XZZX codes, by
6821 ancilla and data basis tracking), its minority-basis detectors (the basis
6822 other than the memory basis, see basis); otherwise (e.g., for a DEM),
6823 drop=auto is used, with a warning. drop=FILE: the detectors in FILE, for any
6824 DEM (a text file with the 0-based detector indices, k for Dk, separated by
6825 white space or commas; lines starting with '#' or '%' are comments; e.g.,
6826 written by outD). With css or FILE, if dropping the detectors is not exact, a
6827 note is printed and the full DEM is used. The values 0, 1, auto, and css take
6828 precedence over files with these names (write ./1 for a file named 1).
6829 outD=FILE Write the detectors dropped from the DEM by the binary to FILE (0-based, in
6830 the format of a drop file; only the comment line '# 0 of M detectors
6831 dropped ...' if none). With a cached result, the binary is run only for
6832 this (with dmin = dmax: no search).
6833 --out-reduced [PREFIX]
6834 Save the DEM without the detectors dropped (see drop and outD) to
6835 PREFIX_red.dem: the errors touching none of them (after the cutoff pmin, in
6836 the order of the flattened DEM), with the other detectors numbered
6837 consecutively. Its maps to the DEM go to PREFIX_red.json: "detectors" (the
6838 original index of each detector of the reduced DEM, i.e., those not in
6839 outD), "errors" (the original index of each error, as the columns of outC),
6840 "dropped", "num_detectors", "num_errors", "pmin", "source", "dem", "drop",
6841 and "__version__" (also out_reduced=PREFIX; default PREFIX: the basename of
6842 the input file, without _full or _simp, in --out-dir or the directory of
6843 the input file). The reduced DEM has the same distance, and its codewords
6844 are mapped to codewords of the DEM by "errors".
6845 classical=0|1 Code type override:
6846 1: Classical linear code (finH only; ignored with a warning if finG, finL,
6847 fin, fdem, Hz, Lx, or Lz is given).
6848 0: Quantum CSS code (requires logicals or Hz).
6849 Default: auto-detected (1 if only finH is given; 0 otherwise).
6850 --simple / --full Disabled since version 0.13.0 (also simple=1, full=1): accepted for backward
6851 compatibility, but ignored with a warning to stderr. The DEM of a Stim circuit
6852 always has all detectors: the binary drops detectors if this is exact (see
6853 drop; e.g., the minority-basis detectors which --simple stripped), which is
6854 as fast and gives the distance of the full DEM (the stripped DEM only gives a
6855 lower bound when dropping them is not exact).
6856 basis=X|Z Explicitly set primary memory basis ('X' or 'Z') for .stim circuits
6857 (default: auto-detected from filename or initial/final resets/measurements);
6858 with drop=css, the detectors of the other basis are dropped if this is exact
6859 (otherwise only used for the --verbose report; ignored with a warning).
6860 --rounds [N] Construct DEM from a .stim circuit with N repetitions in its REPEAT block
6861 (default: 2 when --rounds is given without a number; also rounds=N).
6862 Issues a warning and continues with the original circuit if no REPEAT block.
6863 --out-dir DIR Output directory for saving constructed DEM and/or processed Stim circuit
6864 files (also out_dir=DIR; default: directory of the input .stim/.dem file).
6865 --out-dem [FILE] Save constructed (flattened) DEM to FILE. If given without a filename (or
6866 out_dem=1), automatically saves as <basename>_full.dem.
6867 The codewords (outC) index its error lines: save the DEM in the same run (a DEM
6868 generated again from the circuit may differ, e.g., with another stim version).
6869 --out-stim [FILE] Save processed (noisy, rounds-adjusted) Stim circuit to FILE. If given without
6870 a filename (or out_stim=1), automatically saves as <basename>_full.stim.
6871 --p-noise P Strength p of the uniform circuit-level noise added to a noiseless Stim circuit
6872 (also p_noise=P; default: 0.001; 0: no noise is added, and then a noiseless
6873 circuit gives no distance): DEPOLARIZE2(p) after two-qubit gates (CX, CZ,
6874 XCY, SWAP, ISWAP, SQRT_XX, CXSWAP, ..., also II), DEPOLARIZE1(p/10) after
6875 one-qubit gates (I, X, H, H_XY, S, SQRT_X, C_XYZ, ...), on the qubit of a
6876 classically controlled gate (e.g., CX rec[-1] 5), and on the qubits idle since
6877 the previous TICK, and X (Z) flips with probability p after Z-basis or Y-basis
6878 (X-basis) resets and before measurements (before and after MR, MRX, MRY); the
6879 multi-qubit measurements (MPP, MXX, MYY, MZZ) and Pauli product gates (SPP,
6880 SPP_DAG) get no noise. A circuit with noise (a noise channel, or a noisy
6881 measurement such as M(0.001)) is used as is, unless --force-noise. The
6882 distance does not depend on p (unless pmin > 0), only on which faults exist.
6883 --force-noise Replace the noise of a Stim circuit (also --force or force_noise=1): all of its
6884 noise is removed (noise channels, measurement flip probabilities, heralded
6885 noise; see stim's Circuit.without_noise()), and then the uniform noise of
6886 --p-noise is added (none with --p-noise 0, e.g., to save the noiseless circuit
6887 with --out-stim), e.g., to compare circuits with the same noise model.
6888 --model-lb Certified lower bound on the distance of a Stim memory circuit (fdem=FILE.stim)
6889 from its code-sized model, instead of the distance of its DEM (also
6890 model_lb=1; see compute_model_lower_bound()): the detectors linked by shared
6891 measurements, other than the final data measurements, form chains (one per
6892 check of a memory circuit), and each chain gives a row of the model, the sum
6893 of its detector rows (the chain of a check of the measured basis telescopes
6894 to the parity of the final data measurements on the check; the chains of the
6895 minority-basis checks only with --model-minority), with the equal columns
6896 merged. Each codeword of the DEM gives a model codeword of no larger weight,
6897 so the model distance certified by CC is a lower bound on the circuit
6898 distance; it ignores the timing within the rounds (not tight in general, e.g.,
6899 5 for the bb_72 circuit of distance 6). The binary runs on the model with the
6900 measured-basis rows, then (with --model-minority) with all rows from that bound
6901 (RW first; method, threads, timeout, steps, dstop, wmax, seed, nothrottle,
6902 pairs, sort_rows, and cc_prune apply to each run). The bound is stored as dmin
6903 in the cache record of the DEM, and the bounds of the circuit are printed.
6904 --model-minority With --model-lb, also the chains of the minority-basis checks are rows of the
6905 model (also model_minority=1; default: off). With depolarizing-type noise,
6906 they do not raise the bound (a lightest model codeword has a version without
6907 components of the other basis), but they may for other noise models.
6908 --out-model PREFIX With --model-lb, write the model to PREFIX_HR.mtx and PREFIX_LR.mtx (also
6909 out_model=PREFIX).
6910 (positional files) A file name without a key (the file must exist): a .dem or .stim file is used
6911 as fdem=FILE, and a .mtx or .mmx file as finH=FILE, or as finG=FILE if finH is
6912 already given (and neither finG nor finL); other files are ignored.
6913 (conflicting inputs) The input is selected in the order fdem, Hx/Hz (CSS code), and
6914 finH/finG/finL/fin (fin only supplies the missing finH and finG, while the
6915 binary ignores fin followed by finH, finG, or finL, and fin after them is an
6916 error); the ignored inputs (e.g., matrices with fdem, pmin, merge, expand,
6917 drop, or outD without fdem, basis, --p-noise, or --force-noise without a Stim
6918 circuit, basis without drop=css, fin with finH, classical=1 with finG,
6919 ortho=0 without G) give a warning (unless --silent).
6920
6921Calculation method:
6922 method=1|2|3 Calculation method (default: 3):
6923 1: Random Window (RW) algorithm (upper bound on distance).
6924 Repeatedly samples random information sets to find low-weight
6925 codewords. Fast for discovering small errors.
6926 2: Connected Cluster (CC) algorithm (lower bound / exact distance).
6927 Exhaustive cluster search finding certified lower bound dmin or
6928 exact distance if run to completion.
6929 3: Bracketing mode (concurrent RW and CC).
6930 Dynamically allocates worker threads between CC (lower bound) and
6931 RW (upper bound) to converge on the exact distance rapidly, based on
6932 the measured CC and RW times, the bounds, and the remaining timeout.
6933
6934Distance bounds and guidance:
6935 dmin=N Known certified lower bound on distance (default: 0).
6936 CC search begins at weight w = max(1, dmin).
6937 dmax=N Known upper bound on distance (default: 0).
6938 CC ends once dmin = dmax (with outC, after the rounds w = dmax..dmax+dW);
6939 RW ignores candidate codewords of weight >= dmax unless collecting codewords
6940 or needed for min_hits. For a CSS code (Hx=, Hz=), dmax is a bound on
6941 d = min(dX, dZ), not on either sector: it is used as dstop for both sectors.
6942 dstop=N Stop target for the lower bound (default: 0 = off): the search (method=2
6943 or 3) ends once CC has certified dmin >= dstop, unless a lighter codeword is
6944 found. Unlike dmax, dstop is not an upper bound and is not reported as dmax
6945 (the bounds are 'dmin w steps' with dmin >= dstop, w: the lightest codeword
6946 found, or 0); a cached lower bound dmin >= dstop answers the request without
6947 a calculation. E.g., dstop=U wmin=U-1 decides whether d >= U.
6948 dexp=N Expected code distance estimate (alias: dest) (default: 0).
6949 Hint for method=3: as long as RW has found no codeword, CC rounds at
6950 w > dexp run only on threads which cannot run RW (CC pauses if RW can
6951 use all threads); CC resumes once RW finds a codeword or ends.
6952
6953Search limits and stopping criteria:
6954 steps=N Maximum number of RW steps / information sets across all threads
6955 (default: 100000).
6956 wmax=N Maximum cluster weight to analyze in CC (default: 0 = until bound/timeout;
6957 with method=2, a known dmax is used as wmax).
6958 wmin=N Minimum distance threshold (default: 1).
6959 If a codeword of weight w <= wmin is discovered, search halts immediately
6960 (not when collecting codewords with outC or maxC).
6961 timeout=SEC Execution timeout in seconds (default: 60.0; set 0 for infinite).
6962 In method=3, it guides the CC vs RW thread balance, and a CC round predicted
6963 not to finish in time is not started while RW runs (in method=2, and with
6964 steps=0, it is started anyway: it may still find a codeword of weight dmin).
6965
6966Multithreading & throttling:
6967 threads=N Maximum number of worker threads to allocate (default: all logical CPUs
6968 available to the process, i.e., its CPU affinity mask, e.g., with taskset).
6969 Subject to automatic thread throttling unless nothrottle=1 is specified:
6970 - Small codes, methods 1 and 3 (<= 4 threads for n < 60, or for m*n < 2e4
6971 with a small RW workload m*n*steps < 5e7; otherwise <= 16 threads for
6972 n < 150 or m*n < 1e5, if m*n*steps < 2e8; m: number of rows of H).
6973 Method 2 starts 4 threads, and the others once a CC round takes longer
6974 (predicted > 2 ms on the threads started, or running for 5 ms).
6975 - Large memory matrices (above 15 MB of dense RW working memory per thread:
6976 at most 32 RW threads, within a quarter of the physical memory).
6977 - Small RW step counts (RW threads clamped to <= (steps + 9) / 10).
6978 The last two limits apply to RW threads only: CC (method=2) is not
6979 limited, and in method=3 CC rounds can use all threads.
6980 CC rounds with few start columns per thread are split into work units
6981 which also fix the first 1-3 cluster extensions (balanced load).
6982 nothrottle=1 Disable automatic thread throttling (also --no-throttle).
6983 Forces allocation of the exact number of threads requested.
6984 chunk_size=N Maximum RW batch claimed at once by a thread (default: 0 = adaptive
6985 25-500). Near the end of the steps, batches shrink to at most
6986 ceil(remaining / (4 * RW threads)) steps. Alias: batch=N.
6987 ksub=N EXPERIMENTAL, should not be used (a warning is printed): subspace sketch
6988 dimension for RW (default: 0 = full matrix).
6989 Precomputes N = ker(H) once; each thread echelonizes a compact
6990 ksub x n sampled subspace in L1/L2 cache (e.g. ksub=32 or 64).
6991 Automatically falls back to ksub=0 if m < nu = dim(ker H) (a warning is
6992 printed with --verbose or debug&1). Each step reduces the span of ksub
6993 distinct rows of N, so that a few codewords are found much more often than
6994 others, and min_hits may end RW early.
6995 kwin=N Localized column permutation window size W around a random seed
6996 column j0, used in every RW step (alias: win=N). Default: 0 = automatic
6997 hybrid for n>=500: every other step uses an index window (win_mode=1) of
6998 win_hyb columns, and the other steps uniform random permutations (also
6999 for codewords which are not local in the column order). kwin=-1 (any
7000 negative value): no windows, uniform random permutations in every step.
7001 win_mode=0|1 Locality metric for kwin > 0: 0 = Tanner graph BFS neighbors
7002 (default), 1 = contiguous column index window.
7003 win_hyb=N Window size of the hybrid RW steps (kwin=0, n>=500; at most 3n/4).
7004 Default: 0 = 256 columns for a DEM, 512 for matrices. The window
7005 columns come first in the elimination order; in a DEM, they are errors
7006 close in time, which touch few detectors: windows of 192-256 columns find
7007 circuit codewords 2-10 times faster than 512, and windows of one round of
7008 columns much slower. With --verbose (or debug&1), the binary prints the
7009 rounds of a DEM (estimated from the time translation of its errors: D
7010 detectors and L columns per round) and the codewords found by the
7011 uniform and by the window steps.
7012 pairs=N RW pairs of non-pivot columns (p=2, as in Lee-Brickell and Stern): a RW
7013 step also finds the codewords with exactly two non-pivot columns,
7014 checking only the pairs of columns which agree on N random pivot rows
7015 (at most 64; 0 = off). Default: -1 = auto, about one non-pivot column per
7016 bucket, with fewer rows while a codeword of the weight of the upper bound
7017 would be kept with probability below 0.8, but enough rows to keep the
7018 pairs of a step within about the work of its elimination (e.g., at least
7019 6 for large circuit DEMs while the upper bound is far above the
7020 distance). The pairs find the minimum-weight codewords of circuit DEMs
7021 7-26 times faster, and lighter codewords of codes with a large distance.
7022 Not with ksub. With --verbose (or debug&1), the binary prints the pairs
7023 checked per step and the codewords found by pairs and by single columns.
7024 min_hits=N QDistRnd-style RW stopping criterion (default: 5, 0 = off; alias: max_hits).
7025 Stops RW when the average number of hits <n> per codeword (by all RW steps,
7026 with or without windows, and by CC or finC) reaches min_hits, both for the
7027 codewords of the minimum weight found and for a representative set of
7028 cov_cws lowest-weight codewords (heavier codewords are kept until enough
7029 lighter ones are found). A single codeword suffices. A lighter codeword is
7030 missed with probability about exp(-<n>), assuming that lighter codewords
7031 are found at least as often. With debug&1 (or --verbose), a warning is
7032 printed at the end if the hit counts of codewords of the same weight are
7033 strongly non-uniform (then exp(-<n>) is optimistic), together with an
7034 information-set estimate (uniform random information sets) of the
7035 probability to miss a lighter codeword and of the RW steps needed for 1%.
7036 With --verbose, both estimates are always shown for a result which is not
7037 certified. With min_hits=0, RW records only the codewords lighter than the
7038 upper bound (unless collecting codewords with outC, maxC, or --cws), so
7039 that the repeated hits are not counted.
7040 cov_cws=N Size of the representative set of lowest-weight codewords for min_hits
7041 (default: 100; 0 = only the minimum weight). Without outC and maxC, at
7042 most cov_cws minimum-weight codewords are tracked (all if cov_cws=0).
7043 With fewer minimum-weight codewords, a smaller cov_cws may stop RW
7044 earlier.
7045 refresh=N Periodic adaptive basis refresh interval in RW steps, only with the
7046 experimental ksub > 0 (default: 5000 when ksub > 0, 0 to disable).
7047 Re-echelonizes N and replaces one heavier basis row by a discovered
7048 minimum-weight codeword.
7049
7050Connected Cluster (CC) search options:
7051 smax=N Maximum syndrome weight for confinement profile (default: 0).
7052 When smax > 0, tracks minimum syndrome weights for each error weight.
7053 When smax=0, confinement is not computed. The binary disables it if
7054 dmin > 1 (also a lower bound from the cache), as the small cluster weights
7055 are then skipped.
7056 sort_rows=0|1 Sort the rows of H by increasing weight (stable) in the dist_m4ri binary
7057 (default: the binary's, 1 for matrices and 0 for a DEM). With cc_prune=0,
7058 CC extends a cluster by the columns of the first row of its syndrome (in
7059 row order), so that light rows first reduce the CC work for matrices with
7060 very different row weights (e.g., 1.7-4x with the hook columns of an
7061 extended CSS distance), while the detectors of a DEM are in time order,
7062 which is better (use sort_rows=0 also for matrices exported from a DEM).
7063 With cc_prune=1 or 2, the order of the rows only breaks ties. The
7064 distance, RW, and the cache key do not depend on the order of the rows; CC
7065 enumerates the same codewords (outC), possibly in another order (with
7066 cc_prune=0, also with other hit counts).
7067 cc_prune=0|1|2 The CC search of the dist_m4ri binary (with smax=0; default: the binary's,
7068 1; also --cc-prune N). 0: the walk of the versions before 0.15.0, which
7069 extends a cluster by the columns of the first unsatisfied row of its
7070 syndrome. 1: the branch and bound: it branches on the unsatisfied row
7071 with the fewest available columns, the columns of the earlier branches are
7072 forbidden in the later ones (each codeword is found once: one CC hit), and
7073 a branch is pruned by lower bounds on the number of columns needed (an
7074 unsatisfied row without available columns, or a packing of unsatisfied
7075 rows with disjoint sets of available columns). 2: also the coverage and
7076 fractional cover bounds (fewer nodes, more work per node). All values
7077 find the same codewords and distances (doc/cc_completeness.tex); with the
7078 expert start list (and start_scan=0), cc_prune=1 and 2 forbid the columns
7079 listed before the start column. The cache key does not depend on it.
7080 start=LIST Expert option: comma-separated list of CC start columns (0-based), e.g.,
7081 start=0,48,96 (default: -1 = all columns; start=N is a one-element list).
7082 CC clusters are grown only from the listed columns, and they are NOT
7083 limited to larger column indices. Use, e.g., one column per block of a
7084 quasi-cyclic code. WARNING: dmin (and an exact result) is valid only if a
7085 code symmetry maps every minimum-weight codeword to one containing a listed
7086 column; dmax is always valid. For DEM input, columns index the error
7087 mechanisms in their order in the (flattened) DEM, after the pmin cutoff.
7088 Results are cached under a separate key '<key>:start=a,b,c'; only dmax and
7089 codewords are merged into the main cache record. (Before version 0.10.0,
7090 start=N limited CC to clusters started at column N using only larger
7091 columns; this is now cbeg=N cend=N of the dist_m4ri binary.)
7092 start_scan=0|1 Expert option, with start (also --start-scan; default: 0): 1: a cluster grown
7093 from a listed column c only uses the columns > c, as in the scan of all
7094 columns. WARNING: dmin is then valid only if a code symmetry maps every
7095 minimum-weight codeword to one whose smallest column is listed, e.g., if
7096 the listed columns are the smallest columns of the orbits of a group of code
7097 automorphisms, one per orbit (start=0,l for the two blocks of a two-block
7098 group-algebra code of length 2l over an abelian group). The later start
7099 columns then need much less work, the least if each orbit is a range of
7100 consecutive columns. Cached under '<key>:start=a,b,c:scan'. Not for a DEM.
7101 --trust-start Expert option (also trust_start=1): accept the start-list results as valid,
7102 i.e., also merge dmin and rw_steps into the main cache record, and copy an
7103 existing exact start-list record to the main record even if no calculation
7104 is needed. Only use it if the code symmetry assumption above holds.
7105 noscan, cbeg, cend Disabled in the Python interface: accepted for backward compatibility, but
7106 ignored with a warning to stderr. These expert options of the dist_m4ri
7107 binary do not give valid bounds on their own: noscan=1 runs CC only at
7108 weight w=wmax (method=2); cbeg/cend limit CC start columns to a range for
7109 split runs, where every column is eventually covered by one of the runs.
7110 Run the dist_m4ri binary directly to use them (see its --morehelp).
7111
7112Codeword collection and export:
7113 --cws Collect and display non-trivial minimum-weight codewords.
7114 outC=FILE Export found codewords of weight up to min_w + dW (min_w: minimum weight
7115 found) to file in .nz format. In method=2/3, once the distance d is
7116 known, CC rounds w = d..d+dW enumerate all such irreducible codewords
7117 (see irred; unless the timeout is hit; for a DEM, up to the choice of
7118 equal columns: see merge).
7119 In CSS mode, automatically saves X-codewords to FILE_X.nz and
7120 Z-codewords to FILE_Z.nz. The codewords of the run are merged with the
7121 cached ones; with finC=FILE (the same file), those already in FILE are kept
7122 (also reducible ones recorded before version 0.14.8).
7123 finC=FILE Import initial candidate codewords from file in .nz format.
7124 In CSS mode (Hx=, Hz=), each sector reads its own file FILE_X.nz or FILE_Z.nz
7125 if at least one of them exists and is not empty (a sector without its file
7126 gets no input codewords); otherwise, FILE itself is given to both sectors
7127 (the binary skips the codewords which are not valid in a sector). If FILE
7128 is the same file as outC, a missing or empty file is ignored. The binary
7129 replaces a reducible codeword by an irreducible part (see irred).
7130 maxC=N Maximum number of codewords to collect (default: 0 = unlimited); with
7131 expand=1, also the maximum number of codewords of the expanded list.
7132 dW=N Extra weight window above minimum distance to collect codewords
7133 (w <= min_w + dW) (default: 0).
7134 irred=0|1 Disabled in the Python interface: irred=0, an expert option of the
7135 dist_m4ri binary, is ignored with a warning to stderr. Only irreducible
7136 codewords are recorded (irred=1, the default of the binary): a codeword
7137 which is the union of a codeword and a disjoint nonzero vector with zero
7138 syndrome (e.g., a trivial weight-3 vector of a circuit DEM, from weight
7139 d+3 on) is not. Minimum-weight codewords are always irreducible.
7140 expand=0|1 Python only, for a DEM: list each codeword found once for every choice
7141 among equal columns, which the binary merges (see merge; default: 0). The
7142 binary and the cache keep the merged list. With dW <= 1, the expanded list
7143 is that of merge=0 (with dW >= 2, merge=0 also lists lighter codewords with
7144 two equal columns added). The list grows quickly with the weight: with
7145 maxC > 0, at most maxC codewords are written (a warning if some are dropped).
7146 (DEM codewords) The indices refer to the error lines of the DEM in the order of the flattened
7147 DEM, after the pmin cutoff. For a Stim circuit, save the DEM with --out-dem in
7148 the same run (a warning is printed otherwise): a DEM generated again from the
7149 circuit may differ (e.g., with another stim version).
7150
7151Distance caching (Python CLI):
7152 cache=FILE Path to persistent JSON cache file (default: tmp_dist_cache.json).
7153 --no-cache / nocache Disable reading and writing to the persistent JSON cache.
7154
7155General options:
7156 solver=NAME, engine=NAME
7157 Distance calculation engine: 'dist_m4ri' (default), 'cert' ('qec_cert'),
7158 'auto' (delegates distance-only calls to qec_cert if installed), or
7159 'codedistance'.
7160 --verbose / -v Enable verbose output with detailed explanations of bounds,
7161 timings, steps, and cache status (implies the debug bits 1, 2,
7162 65536, and 131072). For a RW upper bound which is not certified,
7163 also estimates the probability that a lighter codeword was missed
7164 (from the average hits <n>, and from uniform random information sets
7165 with n, rank(H), and the RW steps), with the RW steps and time needed
7166 for a 1% miss probability.
7167 --silent / -q No warnings on stderr for the inputs which are ignored (e.g., matrices with
7168 fdem, the options of the binary with solver=codedistance, or the options
7169 not used with --model-lb); the warnings on the validity of a result are
7170 still written. In Python: set_silent().
7171 seed=N Random number generator seed (default: 0; the binary replaces seed<=0 by
7172 time(NULL) + 10*pid - 1000*seed).
7173 debug=N Debug bitmap (default: 0), a decimal or hexadecimal (0x...) integer;
7174 multiple debug arguments are OR-combined. Bits 1 to 32768 are passed
7175 to the dist_m4ri binary, which prints diagnostic output to stderr (see
7176 its --morehelp; lower bits are more informative):
7177 1: summary (input matrices, rounds of a DEM, RW windows, stop reason,
7178 codeword statistics, codewords found by each kind of RW step),
7179 2: progress (run plan, finished CC rounds, new upper bounds),
7180 4: periodic status (after 1, 2, 4, ... s, then every 60 s),
7181 8: thread allocation and timing, 16: code parameters (ranks, k),
7182 32: codeword supports, 64: arguments, 128: matrices,
7183 256: codeword dump, 2048: no size cutoff.
7184 Bits 65536 and above are used by the Python wrapper only:
7185 65536: dist_m4ri command lines and run times,
7186 131072: cache messages,
7187 262144: Stim circuit to DEM conversion details,
7188 524288: keep (and list) the temporary files.
7189
7190Option groups (--help GROUP prints all entries of GROUP):
7191 windows kwin, win_mode, win_hyb
7192 stopping, stop steps, wmin, timeout, min_hits, cov_cws
7193 bounds dmin, dmax, dstop, dexp, wmax
7194 dem fdem, pmin, merge, drop, outD, --out-reduced, expand
7195 stim fdem, basis, --rounds, --out-dir, --out-dem, --out-stim, --p-noise,
7196 --force-noise, --model-lb, --model-minority, --out-model
7197 matrices, matrix finH, finG, finL, fin, Hx, Lx, classical, ortho
7198 codewords --cws, outC, finC, maxC, dW, irred, expand
7199 threading threads, nothrottle, chunk_size
7200 cache cache, --no-cache, --trust-start
7201 expert start, start_scan, --trust-start, noscan
7202 experimental ksub, refresh
7203
7204Help options:
7205 -h, --help [TOPIC] Display summary help message (fits 80 rows); with TOPIC, the same as
7206 --morehelp TOPIC (also --help=TOPIC).
7207 --morehelp [TOPIC] Display this full help message with all parameters, or only the entries of
7208 TOPIC, e.g., --morehelp maxC (also --morehelp=TOPIC; several topics separated
7209 by commas, e.g., maxC,dW; the names are case-insensitive, without '-' or '_'
7210 (and '=VALUE' in --help=TOPIC and --morehelp=TOPIC: a separate argument with
7211 '=' is not a TOPIC), and include aliases such as dest or batch; an unknown
7212 name gives the closest names on stderr, and the exit code 1). A group name
7213 (see Option groups, also as singular or plural, e.g., window) gives all
7214 entries of the group, and a parameter of a group also the other entries of
7215 its group (see also).
7216 --version Display the versions of dist_m4ri.py and of the dist_m4ri binary.
7217"""
7218
7219
7220def _help_entries(text: str) -> List[Tuple[str, List[str], Dict[str, str]]]:
7221 """
7222 The entries of a help text (see _cli_morehelp_text()): (section header, the lines of the entry, its names). An
7223 entry starts with a line indented by 2 spaces (the names, then the description), and continues with the lines
7224 indented by more; a section header is a line without indentation ending with ':'. The names (a dict: normalized
7225 name -> name as written) are those in the first column (e.g., 'Hx=FILE, Hz=FILE' -> Hx, Hz; '--no-cache /
7226 nocache'), without the values (FILE, [N], ...; but all words of a note such as '(DEM codewords)'), and the aliases
7227 in the description ('alias: dest', 'Alias: win=N', 'also --no-throttle', 'also rounds=N'); normalized: lowercase,
7228 without '-' and '_'.
7229 """
7230 import re
7231 entries: List[Tuple[str, List[str], Dict[str, str]]] = []
7232 section = ""
7233 for line in text.splitlines():
7234 if line and not line[0].isspace() and line.rstrip().endswith(":"):
7235 section = line.rstrip()
7236 continue
7237 m = re.match(r"^ (\S.*?)(?:\s{2,}|$)", line)
7238 if m: # a new entry
7239 names: Dict[str, str] = {}
7240 for tok in re.split(r"[,/\s]+", m.group(1)):
7241 tok = tok.strip("()[]").split("=", 1)[0]
7242 # (not a value, e.g., FILE or N; all words of a note, e.g., '(DEM codewords)')
7243 if tok.strip("-") and (m.group(1).startswith("(") or not tok.isupper()):
7244 names.setdefault(_help_name_key(tok), tok)
7245 entries.append((section, [line], names))
7246 elif line.startswith(" ") and entries:
7247 entries[-1][1].append(line)
7248 for _, lines, names in entries:
7249 desc = " ".join(s.strip() for s in lines)
7250 for a in re.findall(r"(?i)\balias(?:es)?:\s*(-{0,2}[\w-]+)", desc) + re.findall(r"\balso (--?[\w-]+)", desc) + \
7251 re.findall(r"\balso ([\w-]+)=", desc):
7252 names.setdefault(_help_name_key(a), a)
7253 return entries
7254
7255
7256def _help_name_key(name: str) -> str:
7257 """A name of a help entry normalized for matching (see _help_entries()): lowercase, without '-' and '_'."""
7258 return name.lower().replace("-", "").replace("_", "")
7259
7260
7261def _help_group_members(entry: Tuple[str, List[str], Dict[str, str]]) -> Optional[Set[str]]:
7262 """
7263 The normalized names of the members of a group (an entry of the section 'Option groups' of the full help: the
7264 names listed after its first column), or None for another entry.
7265 """
7266 import re
7267 section, lines, _ = entry
7268 if not section.startswith("Option groups"):
7269 return None
7270 m = re.match(r"^ \S.*?(?:\s{2,}|$)", lines[0])
7271 text = " ".join([lines[0][m.end():] if m else ""] + [s.strip() for s in lines[1:]])
7272 return {_help_name_key(t) for t in re.split(r"[,\s]+", text) if t.strip("-")}
7273
7274
7275def _help_group_key_match(gkey: str, key: str) -> bool:
7276 """Whether the topic key matches the name gkey of a group, also as singular or plural (e.g., window, windows)."""
7277 return key in (gkey, gkey + "s") or (gkey.endswith("s") and key == gkey[:-1])
7278
7279
7280def print_cli_morehelp(file: Optional[Any] = None, topic: Optional[str] = None) -> int:
7281 """
7282 Prints the full help of the CLI (--morehelp), or with `topic` (one or more names separated by commas, see
7283 _help_entries()) only the entries of these parameters, in the order of the full help under their section headers.
7284 A topic may also be a group of the section 'Option groups' (also as singular or plural, e.g., window): then all
7285 its members are printed; and an entry of a group also gives the other members of its groups and these groups
7286 (one level: 'see also'). Returns 0, or 1 if some topic has no entry (then the closest names are suggested on
7287 stderr).
7288 """
7289 import difflib
7290 text = _cli_morehelp_text()
7291 keys = [(t, _help_name_key(t.split("=", 1)[0])) for t in (s.strip() for s in (topic or "").split(","))]
7292 keys = [(t, k) for t, k in keys if k]
7293 if not keys:
7294 print(text, file=file)
7295 return 0
7296 entries = _help_entries(text)
7297 members = [_help_group_members(e) for e in entries]
7298 status = 0
7299 asked: Set[int] = set()
7300 for t, key in keys:
7301 found = [i for i, e in enumerate(entries)
7302 if (key in e[2] if members[i] is None else any(_help_group_key_match(g, key) for g in e[2]))]
7303 if not found:
7304 names = {k: v for e in entries for k, v in e[2].items()}
7305 close = [names[k] for k in difflib.get_close_matches(key, list(names), n=5, cutoff=0.6)]
7306 hint = f"; did you mean {', '.join(close)}?" if close else ""
7307 sys.stderr.write(f"dist_m4ri.py: no help entry for '{t}'{hint} (--morehelp lists all parameters)\n")
7308 status = 1
7309 asked.update(found)
7310 chosen: Set[int] = set(asked)
7311 for g, mem in enumerate(members): # the groups asked for or with a member asked for: the group and its members
7312 if mem is None:
7313 continue
7314 in_group = [i for i, e in enumerate(entries) if members[i] is None and mem & set(e[2])]
7315 if g in asked or asked & set(in_group):
7316 chosen.add(g)
7317 chosen.update(in_group)
7318 out: List[str] = []
7319 section = None
7320 for i in sorted(chosen): # in the order of the full help, under their section headers
7321 sec, lines, _ = entries[i]
7322 if sec != section:
7323 if out:
7324 out.append("")
7325 out.append(sec)
7326 section = sec
7327 out.extend(lines)
7328 if out:
7329 print("\n".join(out), file=file)
7330 return status
7331
7332
7333def _warn_cli_conflicts(args: Dict[str, Any]) -> None:
7334 """
7335 Writes warnings to stderr for the CLI inputs which are ignored. The input is selected in the order fdem (a DEM or
7336 a Stim circuit), Hx / Hz (a CSS code), and finH / finG / finL / fin (one sector of a quantum code, or a classical
7337 code); fin=PREFIX supplies only the missing finH (PREFIX + 'X.mtx') and finG (PREFIX + 'Z.mtx', unless finL). The
7338 disabled options --simple and --full always give a warning (see _warn_disabled_simple_full()), and so does the
7339 expert option irred of the dist_m4ri binary, unless irred=1 (the codewords recorded are always irreducible). The
7340 expert option ortho=0 gives a warning here for a DEM or a classical code (for a quantum code, see _warn_ortho()).
7341 """
7342 def given(*keys: str) -> List[str]:
7343 return [k for k in keys if args.get(k) is not None]
7344
7345 _warn_disabled_simple_full(args.get("simple"), args.get("full"), cli=True) # (not passed on)
7346 if args.get("irred") is not None and args["irred"] != 1: # (not passed on: the binary's default irred=1)
7347 _warn_ignored(f"irred={args['irred']} is disabled in the Python interface (ignored: only irreducible "
7348 f"codewords are recorded); use the dist_m4ri binary directly")
7349 stim_opts = [opt for opt, on in (("basis=", args.get("basis") is not None),
7350 ("--out-stim", args.get("out_stim") is not None),
7351 ("--p-noise", args.get("p_noise") is not None),
7352 ("--force-noise", bool(args.get("force_noise")))) if on]
7353 warns = []
7354 if args.get("fdem"):
7355 ign = given("finH", "finG", "finL", "fin", "Hx", "Hz", "Lx", "Lz")
7356 if ign:
7357 warns.append(f"fdem={args['fdem']} given: ignoring {', '.join(k + '=' for k in ign)}")
7358 if stim_opts and not str(args["fdem"]).endswith(".stim"):
7359 warns.append(f"{', '.join(stim_opts)}: only used for a Stim circuit (.stim); ignored")
7360 elif (args.get("basis") is not None and str(args.get("drop")).lower() != "css"
7361 and not args.get("verbose")): # (with --verbose, the basis of the classification is reported)
7362 warns.append("basis=: only used with drop=css (the minority-basis detectors of a CSS circuit); ignored")
7363 if args.get("expand") and not (args.get("outC") or args.get("do_cws")):
7364 warns.append("expand=: only used with outC= or --cws; ignored")
7365 else:
7366 dem_opts = stim_opts + [opt for opt, on in (("--out-dir", args.get("out_dir") is not None),
7367 ("--out-dem", args.get("out_dem") is not None)) if on]
7368 if (args.get("pmin") or 0.0) > 0.0:
7369 dem_opts.append("pmin=")
7370 if args.get("merge") is not None:
7371 dem_opts.append("merge=")
7372 if args.get("expand") is not None:
7373 dem_opts.append("expand=")
7374 if args.get("drop") is not None:
7375 dem_opts.append("drop=")
7376 if args.get("outD") is not None:
7377 dem_opts.append("outD=")
7378 if args.get("out_reduced") is not None:
7379 dem_opts.append("--out-reduced")
7380 if dem_opts:
7381 warns.append(f"{', '.join(dem_opts)}: only used with fdem= (a DEM or Stim circuit); ignored")
7382 if args.get("Hx") is not None or args.get("Hz") is not None:
7383 ign = given("finH", "finG", "finL", "fin")
7384 if ign:
7385 warns.append(f"Hx=/Hz= given (CSS code): ignoring {', '.join(k + '=' for k in ign)}")
7386 else:
7387 ign = given("Lx", "Lz")
7388 if ign:
7389 warns.append(f"{', '.join(k + '=' for k in ign)}: only used for a CSS code (Hx= and Hz=); ignored")
7390 ign = given("finH", "finG", "finL")
7391 if args.get("fin") is not None and ign:
7392 warns.append(f"fin={args['fin']} together with {', '.join(k + '=' for k in ign)}: the explicit file "
7393 "names take precedence")
7394 if args.get("classical") == 1 and given("finG", "finL", "fin", "fdem", "Hz", "Lx", "Lz"):
7395 warns.append("classical=1 ignored: the input is a quantum code or a DEM")
7396 if args.get("ortho") is False: # (a quantum code: compute_quantum_distance() and compute_css_distance() warn)
7397 quantum = bool(given("Hx", "Hz", "finG", "finL", "fin")) or args.get("classical") == 0
7398 if args.get("fdem") or not quantum:
7399 warns.append("ortho=0: only used for a quantum code with G (finG=, fin=, or Hx= and Hz=); ignored")
7400 if args.get("css") is not None:
7401 warns.append(f"css={args['css']}: not used by the Python interface (CSS codes only); ignored")
7402 for f in args.get("ignored_files") or []:
7403 warns.append(f"{f}: a positional file which is not used (not a .dem, .stim, .mtx, or .mmx file, or a third "
7404 f"matrix file); ignored")
7405 if args.get("model_lb"): # the code-sized model of a Stim circuit (compute_model_lower_bound())
7406 ign = [opt for opt, on in (("outC=", args.get("outC") is not None), ("finC=", args.get("finC") is not None),
7407 ("--cws", bool(args.get("do_cws")) and args.get("outC") is None),
7408 ("drop=", args.get("drop") is not None),
7409 ("outD=", args.get("outD") is not None),
7410 ("--out-reduced", args.get("out_reduced") is not None),
7411 ("--out-dir", args.get("out_dir") is not None),
7412 ("--out-dem", args.get("out_dem") is not None),
7413 ("--out-stim", args.get("out_stim") is not None),
7414 ("start=", args.get("start") is not None), ("dmin=", bool(args.get("dmin"))),
7415 ("dmax=", bool(args.get("dmax"))), ("dexp=", bool(args.get("dexp"))),
7416 ("maxC=", bool(args.get("maxC"))), ("dW=", args.get("dW", -1) >= 0),
7417 ("merge=", args.get("merge") is not None),
7418 ("expand=", args.get("expand") is not None),
7419 ("wmin=", args.get("wmin", 1) != 1), ("smax=", args.get("smax") is not None),
7420 ("noscan=", bool(args.get("noscan"))), ("cbeg=", args.get("cbeg") is not None),
7421 ("cend=", args.get("cend") is not None),
7422 ("--trust-start", bool(args.get("trust_start"))),
7423 ("start_scan=", bool(args.get("start_scan"))),
7424 ("chunk_size=", bool(args.get("chunk_size"))), ("ksub=", bool(args.get("ksub"))),
7425 ("kwin=", bool(args.get("kwin"))), ("win_mode=", bool(args.get("win_mode"))),
7426 ("win_hyb=", bool(args.get("win_hyb"))),
7427 ("min_hits=", args.get("min_hits") is not None),
7428 ("cov_cws=", args.get("cov_cws", 100) != 100),
7429 ("refresh=", args.get("refresh") is not None),
7430 ("solver=", args.get("solver") not in (None, "dist_m4ri")),
7431 ("engine=", args.get("engine") not in (None, "dist_m4ri"))) if on]
7432 if ign:
7433 warns.append(f"{', '.join(ign)}: not used with --model-lb; ignored")
7434 else:
7435 if args.get("out_model") is not None:
7436 warns.append("--out-model: only used with --model-lb; ignored")
7437 if args.get("model_minority"):
7438 warns.append("--model-minority: only used with --model-lb; ignored")
7439 for w in warns:
7440 _warn_ignored(w)
7441
7442
7443def main(argv: Optional[List[str]] = None) -> int:
7444 """The CLI (see print_cli_help()); with --silent, set_silent() is on during the run, and restored at its end."""
7445 report = _report_ignored
7446 try:
7447 return _main(argv)
7448 finally:
7449 set_silent(not report)
7450
7451
7452def _main(argv: Optional[List[str]] = None) -> int:
7453 if argv is None:
7454 argv = sys.argv[1:]
7455
7456 try:
7457 args = parse_cli_args(argv)
7458 except ValueError as e:
7459 sys.stderr.write(f"Error: {e}\n")
7460 return 1
7461 if args.get("silent"): # no warnings on the inputs which are ignored
7462 set_silent(True)
7463
7464 if args.get("version"):
7465 print(f"dist_m4ri.py version {__version__}")
7466 bin_path, bin_ver, _ = _binary_version()
7467 if bin_ver is not None:
7468 print(f"dist_m4ri version {bin_ver} (binary '{bin_path}')")
7469 compat_warn = check_binary_compatibility()
7470 if compat_warn:
7471 print(f"dist_m4ri.py: {compat_warn}", file=sys.stderr)
7472 return 0
7473
7474 if args.get("morehelp") or args.get("help"):
7475 compat_warn = check_binary_compatibility()
7476 if compat_warn:
7477 print(f"dist_m4ri.py: {compat_warn}", file=sys.stderr)
7478 if args.get("morehelp"):
7479 return print_cli_morehelp(topic=args.get("morehelp_topic"))
7481 return 0
7482
7483 if args.get("unrecognized"):
7484 for u in args["unrecognized"]:
7485 print(f"dist_m4ri.py: unrecognized parameter '{u}'", file=sys.stderr)
7486 print_cli_short_help(file=sys.stderr)
7487 return 255
7488
7489 if (
7490 not args.get("fdem") and not args.get("finH")
7491 and not args.get("Hx") and not args.get("Hz") and not args.get("fin")
7492 ):
7493 print("dist_m4ri.py: no input matrix or model specified", file=sys.stderr)
7494 print_cli_short_help(file=sys.stderr)
7495 return 255
7496
7498
7499 # When finC and outC are identical, an empty or non-existent file is silently ignored (with a warning if verbose)
7500 # by the compute_*_distance() functions (for CSS codes, after checking the sector files finC_X and finC_Z)
7501
7502 cache_file = args["cache_file"] if args["use_cache"] else None
7503 if not args["use_cache"]:
7505
7506 try:
7507 if args.get("model_lb"): # the certified lower bound of a Stim circuit or DEM from its code-sized model
7508 if not args["fdem"] or not str(args["fdem"]).endswith((".stim", ".dem")):
7509 raise ValueError("--model-lb needs a Stim circuit (.stim) or DEM (.dem) (fdem=FILE)")
7510 bound, info = compute_model_lower_bound(
7511 circuit=args["fdem"],
7512 method=args["method"],
7513 threads=args["threads"],
7514 timeout=args["timeout"],
7515 num_steps=args["steps"],
7516 dstop=args["dstop"],
7517 wmax=args["wmax"],
7518 minority=args["model_minority"],
7519 pmin=args["pmin"],
7520 rounds=args["rounds"],
7521 p_noise=args["p_noise"] if args["p_noise"] is not None else 0.001,
7522 force_noise=args["force_noise"],
7523 out_model=args["out_model"],
7524 cache_file=cache_file,
7525 seed=args["seed"],
7526 debug=args["debug"],
7527 verbose=args["verbose"],
7528 nothrottle=args["nothrottle"],
7529 pairs=args["pairs"],
7530 sort_rows=args["sort_rows"],
7531 cc_prune=args["cc_prune"],
7532 return_info=True
7533 )
7534 if args["verbose"]:
7535 print("=== Code-Sized Model Lower Bound ===")
7536 print(f" Model: {info.get('rows', 0)} rows ({info.get('rows_measured', 0)} chains of the "
7537 f"measured-basis checks, {info.get('rows', 0) - info.get('rows_measured', 0)} other) x "
7538 f"{info.get('cols', 0)} columns, from {info.get('dem_cols', 0)} error mechanisms and "
7539 f"{info.get('detectors', 0)} detectors of the DEM.")
7540 if info["model_d_info"]:
7541 print(f" Model bounds (CC certifies dmin): {format_bounds_str(info['model_d_info'])}"
7542 + (f"; measured-basis rows only: {format_bounds_str(info['measured_d_info'])}"
7543 if info["measured_d_info"] and info["measured_d_info"] != info["model_d_info"] else ""))
7544 print(f" Certified lower bound on the circuit distance: d >= {bound}.")
7545 print(f" Circuit bounds (with the cache): {format_bounds_str(info['d_info'])}")
7546 print(format_bounds_str(info["d_info"]))
7547 return 0
7548
7549 if args["fdem"]:
7551 dem=args["fdem"],
7552 method=args["method"],
7553 threads=args["threads"],
7554 timeout=args["timeout"],
7555 num_steps=args["steps"],
7556 d_exp=args["dexp"],
7557 dmin=args["dmin"],
7558 dmax=args["dmax"],
7559 dstop=args["dstop"],
7560 wmin=args["wmin"],
7561 wmax=args["wmax"],
7562 smax=args["smax"],
7563 start=args["start"],
7564 cbeg=args["cbeg"],
7565 cend=args["cend"],
7566 noscan=args["noscan"],
7567 dW=args["dW"],
7568 maxC=args["maxC"],
7569 pmin=args["pmin"],
7570 merge=args["merge"] is not False,
7571 expand=bool(args["expand"]),
7572 drop=args["drop"],
7573 outD=args["outD"],
7574 out_reduced=args["out_reduced"],
7575 finC=args["finC"],
7576 outC=args["outC"],
7577 do_cws=args["do_cws"] or (args["outC"] is not None),
7578 cache_file=cache_file,
7579 solver=args["solver"],
7580 engine=args["engine"],
7581 seed=args["seed"],
7582 debug=args["debug"],
7583 verbose=args["verbose"],
7584 nothrottle=args["nothrottle"],
7585 chunk_size=args["chunk_size"],
7586 ksub=args["ksub"],
7587 kwin=args["kwin"],
7588 win_mode=args["win_mode"],
7589 win_hyb=args["win_hyb"],
7590 pairs=args["pairs"],
7591 sort_rows=args["sort_rows"],
7592 cc_prune=args["cc_prune"],
7593 min_hits=args["min_hits"],
7594 cov_cws=args["cov_cws"],
7595 refresh=args["refresh"],
7596 basis=args["basis"],
7597 rounds=args["rounds"],
7598 p_noise=args["p_noise"] if args["p_noise"] is not None else 0.001,
7599 force_noise=args["force_noise"],
7600 out_dir=args["out_dir"],
7601 out_dem=args["out_dem"],
7602 out_stim=args["out_stim"],
7603 trust_start=args["trust_start"],
7604 start_scan=args["start_scan"]
7605 )
7606 dist, d_info = res[0], res[1] # (compute_*_distance() writes the codewords to outC)
7607
7608 if args["verbose"]:
7609 print("=== DEM Distance Results ===")
7610 print(explain_bounds(
7611 d_info, method=args["method"], label="DEM", stats=_last_run_stats, dstop=args["dstop"]
7612 ))
7613 print(f" Summary bounds: {format_bounds_str(d_info)}")
7614 print(format_bounds_str(d_info))
7615 return 0
7616
7617 if args.get("rounds") is not None:
7618 raise ValueError(
7619 "--rounds option is only supported for Stim circuit (.stim) inputs."
7620 )
7621
7622 if args["Hx"] is not None or args["Hz"] is not None:
7624 Hx=args["Hx"],
7625 Hz=args["Hz"],
7626 Lx=args["Lx"],
7627 Lz=args["Lz"],
7628 method=args["method"],
7629 threads=args["threads"],
7630 timeout=args["timeout"],
7631 num_steps=args["steps"],
7632 d_exp=args["dexp"],
7633 dmin=args["dmin"],
7634 dmax=args["dmax"],
7635 dstop=args["dstop"],
7636 wmin=args["wmin"],
7637 wmax=args["wmax"],
7638 smax=args["smax"],
7639 start=args["start"],
7640 cbeg=args["cbeg"],
7641 cend=args["cend"],
7642 noscan=args["noscan"],
7643 dW=args["dW"],
7644 maxC=args["maxC"],
7645 finC=args["finC"],
7646 outC=args["outC"],
7647 do_cws=args["do_cws"] or (args["outC"] is not None),
7648 cache_file=cache_file,
7649 solver=args["solver"],
7650 engine=args["engine"],
7651 seed=args["seed"],
7652 debug=args["debug"],
7653 verbose=args["verbose"],
7654 nothrottle=args["nothrottle"],
7655 chunk_size=args["chunk_size"],
7656 ksub=args["ksub"],
7657 kwin=args["kwin"],
7658 win_mode=args["win_mode"],
7659 win_hyb=args["win_hyb"],
7660 pairs=args["pairs"],
7661 sort_rows=args["sort_rows"],
7662 cc_prune=args["cc_prune"],
7663 min_hits=args["min_hits"],
7664 cov_cws=args["cov_cws"],
7665 refresh=args["refresh"],
7666 trust_start=args["trust_start"],
7667 start_scan=args["start_scan"],
7668 ortho=args["ortho"]
7669 )
7670 dist, dx_info, dz_info = res[0], res[1], res[2] # (the sector files outC_X / outC_Z are written by
7671 # compute_css_distance())
7672
7673 # d = min(dX, dZ) is exact if its bounds coincide (a user dmax is a bound on d, see compute_css_distance)
7674 _, d_lo, d_hi, _ = _css_combine(tuple(dx_info), tuple(dz_info), args["dmax"])
7675 exact_tag = " (exact)" if (d_lo > 0 and d_lo == d_hi) else ""
7676 stops = [v for v in (args["dstop"], args["dmax"]) if v > 0]
7677 sector_stop = min(stops) if stops else 0
7678
7679 if args["verbose"]:
7680 print("=== CSS Quantum Code Distance Results ===")
7681 if dx_info:
7682 print("--- X-Component Distance (dX) ---")
7683 print(explain_bounds(
7684 dx_info, method=args["method"], label="dX",
7685 stats=_last_css_stats.get("X"), dstop=sector_stop
7686 ))
7687 print(f" dX bounds: {format_bounds_str(dx_info)}")
7688 if dz_info:
7689 print("--- Z-Component Distance (dZ) ---")
7690 print(explain_bounds(
7691 dz_info, method=args["method"], label="dZ",
7692 stats=_last_css_stats.get("Z"), dstop=sector_stop
7693 ))
7694 print(f" dZ bounds: {format_bounds_str(dz_info)}")
7695 print("--- Overall CSS Code Distance ---")
7696 print(f" d = min(dX, dZ) = {dist}{exact_tag}")
7697
7698 dx_str = format_bounds_str(dx_info) if dx_info else "none"
7699 dz_str = format_bounds_str(dz_info) if dz_info else "none"
7700 print(f"dX: {dx_str} dZ: {dz_str} (d = {dist}){exact_tag}")
7701 return 0
7702
7703 # Handle fin prefix (e.g. fin=examples/try -> tryX.mtx and tryZ.mtx)
7704 finH = args["finH"]
7705 finG = args["finG"]
7706 if args["fin"]:
7707 if finH is None: finH = f"{args['fin']}X.mtx"
7708 if finG is None and args["finL"] is None: finG = f"{args['fin']}Z.mtx"
7709
7710 # Quantum single-sided distance (finH with finG or finL, or classical=0)
7711 if finH and (finG is not None or args["finL"] is not None or args["classical"] == 0):
7713 H=finH,
7714 G=finG,
7715 L=args["finL"],
7716 method=args["method"],
7717 threads=args["threads"],
7718 timeout=args["timeout"],
7719 num_steps=args["steps"],
7720 d_exp=args["dexp"],
7721 dmin=args["dmin"],
7722 dmax=args["dmax"],
7723 dstop=args["dstop"],
7724 wmin=args["wmin"],
7725 wmax=args["wmax"],
7726 smax=args["smax"],
7727 start=args["start"],
7728 cbeg=args["cbeg"],
7729 cend=args["cend"],
7730 noscan=args["noscan"],
7731 dW=args["dW"],
7732 maxC=args["maxC"],
7733 finC=args["finC"],
7734 outC=args["outC"],
7735 do_cws=args["do_cws"] or (args["outC"] is not None),
7736 return_info=True,
7737 cache_file=cache_file,
7738 solver=args["solver"],
7739 engine=args["engine"],
7740 seed=args["seed"],
7741 debug=args["debug"],
7742 verbose=args["verbose"],
7743 nothrottle=args["nothrottle"],
7744 chunk_size=args["chunk_size"],
7745 ksub=args["ksub"],
7746 kwin=args["kwin"],
7747 win_mode=args["win_mode"],
7748 win_hyb=args["win_hyb"],
7749 pairs=args["pairs"],
7750 sort_rows=args["sort_rows"],
7751 cc_prune=args["cc_prune"],
7752 min_hits=args["min_hits"],
7753 cov_cws=args["cov_cws"],
7754 refresh=args["refresh"],
7755 trust_start=args["trust_start"],
7756 start_scan=args["start_scan"],
7757 ortho=args["ortho"]
7758 )
7759 dist, d_info = res[0], res[1] # (compute_*_distance() writes the codewords to outC)
7760
7761 if args["verbose"]:
7762 print("=== Quantum Code Distance Results (Single-Sided) ===")
7763 print(explain_bounds(
7764 d_info, method=args["method"], label="Quantum", stats=_last_run_stats, dstop=args["dstop"]
7765 ))
7766 print(f" Summary bounds: {format_bounds_str(d_info)}")
7767 print(format_bounds_str(d_info))
7768 return 0
7769
7770 # Classical distance (finH only or classical=1)
7771 if finH:
7773 H=finH,
7774 method=args["method"],
7775 threads=args["threads"],
7776 timeout=args["timeout"],
7777 num_steps=args["steps"],
7778 d_exp=args["dexp"],
7779 dmin=args["dmin"],
7780 dmax=args["dmax"],
7781 dstop=args["dstop"],
7782 wmin=args["wmin"],
7783 wmax=args["wmax"],
7784 smax=args["smax"],
7785 start=args["start"],
7786 cbeg=args["cbeg"],
7787 cend=args["cend"],
7788 noscan=args["noscan"],
7789 dW=args["dW"],
7790 maxC=args["maxC"],
7791 finC=args["finC"],
7792 outC=args["outC"],
7793 do_cws=args["do_cws"] or (args["outC"] is not None),
7794 return_info=True,
7795 cache_file=cache_file,
7796 solver=args["solver"],
7797 engine=args["engine"],
7798 seed=args["seed"],
7799 debug=args["debug"],
7800 verbose=args["verbose"],
7801 nothrottle=args["nothrottle"],
7802 chunk_size=args["chunk_size"],
7803 ksub=args["ksub"],
7804 kwin=args["kwin"],
7805 win_mode=args["win_mode"],
7806 win_hyb=args["win_hyb"],
7807 pairs=args["pairs"],
7808 sort_rows=args["sort_rows"],
7809 cc_prune=args["cc_prune"],
7810 min_hits=args["min_hits"],
7811 cov_cws=args["cov_cws"],
7812 refresh=args["refresh"],
7813 trust_start=args["trust_start"],
7814 start_scan=args["start_scan"]
7815 )
7816 dist, d_info = res[0], res[1] # (compute_*_distance() writes the codewords to outC)
7817
7818 if args["verbose"]:
7819 print("=== Classical Code Distance Results ===")
7820 print(explain_bounds(
7821 d_info, method=args["method"], label="Classical", stats=_last_run_stats, dstop=args["dstop"]
7822 ))
7823 print(f" Summary bounds: {format_bounds_str(d_info)}")
7824 print(format_bounds_str(d_info))
7825 return 0
7826 except ValueError as e:
7827 sys.stderr.write(f"Error: {e}\n")
7828 return 1
7829
7830 return 0
7831
7832
7833if __name__ == "__main__":
7834 sys.exit(main())
bool __eq__(self, Any other)
__init__(self, int dmin, int dmax, int rw_steps=0, Optional[List[List[int]]] cws=None, Optional[List[List[int]]] cws_X=None, Optional[List[List[int]]] cws_Z=None, Optional[int] dmin_X=None, Optional[int] dmax_X=None, Optional[int] rw_steps_X=None, Optional[int] dmin_Z=None, Optional[int] dmax_Z=None, Optional[int] rw_steps_Z=None)
__getitem__(self, int index)
Dict[str, Any] _cert_code_distance(Callable[[], str] key_fn, Callable[[int, int], Any] run_cert_fn, *, str label="code", int dmin=0, int dmax=0, int dstop=0, int wmax=0, Optional[str] eff_cache_file=None, int debug=0, bool verbose=False)
str _format_start_list(List[int] start_list, int max_show=8)
str _help_name_key(str name)
str find_dist_m4ri_binary(Optional[str] custom_path=None)
Definition dist_m4ri.py:887
int main(Optional[List[str]] argv=None)
str create_unique_file(Union[str, Path] directory="tmp", str extension=".tmp")
Definition dist_m4ri.py:818
str format_bounds_str(List[int] bounds)
Definition dist_m4ri.py:284
List[int] format_bounds_list(int dmin, int dmax, int num_rw)
Definition dist_m4ri.py:263
int parse_debug_value(Union[str, int] val)
Definition dist_m4ri.py:49
Any __getattr__(str name)
Definition dist_m4ri.py:125
None _write_mtx_gf2(str path, Any matrix)
str detect_basis(Any circuit, Optional[Union[str, Path]] filepath=None, Optional[str] basis_arg=None)
Tuple[List[int], Optional[int]] read_drop_file(Union[str, Path] path)
bool has_repeat_block(Any circuit)
Tuple[str, Optional[Dict[str, Any]], Optional[Dict[str, Any]]] _resolve_start_cache(str plain_key, Optional[List[int]] start_list, Callable[[Dict[str, Any]], bool] is_hit, bool trust_start, Callable[[Dict[str, Any]], bool] merge_copy, Optional[str] eff_cache_file, bool verbose=False)
Any strip_noise(Any circuit)
None _warn_disabled_options(Any noscan=0, Any cbeg=None, Any cend=None)
None set_distance_cache_file(Optional[Union[str, Path]] filepath=None)
Definition dist_m4ri.py:156
str _matrix_cache_key(Any H, Optional[Any] G=None, Optional[Any] L=None, bool classical=False)
Dict[str, Any] get_distance_cache()
Definition dist_m4ri.py:257
None _warn_cli_conflicts(Dict[str, Any] args)
int count_circuit_rounds(Any circuit)
int _dist_from_bounds(int dmin, int dmax)
Tuple[List[int], List[bool], int] _model_detector_groups(Any circuit)
bool _cert_is_conditional(Any cert)
str _matrix_to_file(matrix, str extension=".mtx", str temp_dir="tmp")
bool _can_delegate_to_cert(str mode, *, int method=3, bool do_cws=False, Optional[str] outC=None, Optional[str] finC=None, Optional[int] smax=None, int dW=-1, int maxC=0, Optional[List[int]] start_list=None, Optional[Any] outD=None, Optional[Any] out_reduced=None, float pmin=0.0)
None _warn_ortho(Any ortho, bool has_g, str solver="dist_m4ri")
Tuple[Optional[str], Optional[str], Optional[str]] _binary_version(Optional[str] binary_path=None)
Definition dist_m4ri.py:926
Tuple[int,...] _parse_version(str v_str)
Definition dist_m4ri.py:920
bool _has_qec_cert()
Definition dist_m4ri.py:116
None _write_nzlist_file(str filepath, List[List[int]] cws)
Dict[str, Any] parse_cli_args(List[str] argv)
Tuple[Any, int] remove_empty_detectors(Any circuit)
_get_qec_cert()
Definition dist_m4ri.py:103
Optional[Dict[str, Any]] rw_infoset_estimate(int n, int rank, int steps, int d, int dmin=1, float step_time=0.0, float target=0.01, int steps_total=0, int pairs_l=0)
Definition dist_m4ri.py:330
None _warn_disabled_simple_full(Optional[bool] simple=None, Optional[bool] full=None, bool cli=False)
Tuple[int, int, Any, Any] _gf2_entries(Any matrix)
None _write_dem_file(Any dem, str path)
Any add_noise(Any circuit, float p=0.001, bool force=False)
List[Tuple[Any,...]] _dem_error_columns(Any dem, float pmin=0.0, bool with_prob=False)
List[Tuple[str, List[str], Dict[str, str]]] _help_entries(str text)
Tuple[Any,...] compute_dem_distance(Optional[Any] dem=None, Optional[Any] circuit=None, Optional[str] dist_m4ri=None, int method=3, Optional[int] threads=None, float timeout=60.0, Optional[int] num_steps=None, int d_exp=0, int d_min=0, int d_max=0, int dmin=0, int dmax=0, int dstop=0, int wmin=1, int wmax=0, Optional[int] smax=None, Optional[Union[int, str, Sequence[int]]] start=None, Optional[int] cbeg=None, Optional[int] cend=None, int noscan=0, int dW=-1, int maxC=0, float pmin=0.0, bool merge=True, bool expand=False, Optional[Union[bool, int, str, Path]] drop=None, Optional[Union[str, Path]] outD=None, Optional[Union[bool, str, Path]] out_reduced=None, Optional[str] finC=None, Optional[str] outC=None, bool do_cws=False, Optional[Union[str, Path]] cache_file=None, str solver="dist_m4ri", str codedistance_method="UndetectableErrorStim", Optional[Dict[str, Any]] codedistance_params=None, int seed=0, int debug=0, bool verbose=False, bool nothrottle=False, int chunk_size=0, int ksub=0, int kwin=0, int win_mode=0, int win_hyb=0, int pairs=-1, Optional[int] min_hits=None, int cov_cws=100, Optional[int] refresh=None, Optional[bool] simple=None, Optional[bool] full=None, Optional[str] basis=None, Optional[int] rounds=None, Optional[Union[str, Path]] out_dir=None, Optional[Union[bool, str, Path]] out_dem=None, Optional[Union[bool, str, Path]] out_stim=None, float p_noise=0.001, bool force_noise=False, bool trust_start=False, bool start_scan=False, Optional[bool] sort_rows=None, Optional[int] cc_prune=None, Optional[str] engine=None, **kwargs)
Dict[str, Any] classify_qubits_thorough(Any circuit, bool verbose=False, Optional[str] basis_arg=None, Optional[Union[str, Path]] filepath=None)
Optional[List[int]] _scan_start_list(Optional[List[int]] start_list, bool start_scan)
bool _merge_start_into_plain(str plain_key, Dict[str, Any] src, bool trust=False, Optional[Dict[str, int]] rw_delta=None)
Tuple[Any, Optional[Any], Optional[str]] _dem_from_input(Optional[Any] dem, Optional[Any] circuit, Optional[str] basis=None, Optional[int] rounds=None, float p_noise=0.001, Optional[Union[str, Path]] out_dir=None, Optional[Union[bool, str, Path]] out_dem=None, Optional[Union[bool, str, Path]] out_stim=None, bool circ_verbose=False, Optional[List[int]] drop_out=None, bool force_noise=False)
Dict[str, Any] verify_qubit_metadata(Any circuit, bool verbose=False, Optional[str] basis_arg=None, Optional[Union[str, Path]] filepath=None)
Optional[List[int]] _normalize_start_list(Any start)
Tuple[int, int, int] parse_dist_m4ri_output(str stdout)
Definition dist_m4ri.py:971
List[List[int]] _union_cws(Optional[List[List[int]]] base, Optional[List[List[int]]] extra)
str _ksub_warning_text(int ksub)
bool _migrate_css_records(Any Hx, Any Hz, Optional[Any] Lx=None, Optional[Any] Lz=None, Optional[str] eff_cache_file=None)
Tuple[int, int, int] run_dist_m4ri(Optional[str] dist_m4ri_path=None, int method=3, Optional[str] finH=None, Optional[str] finG=None, Optional[str] finL=None, Optional[str] fin=None, Optional[str] finC=None, Optional[str] fdem=None, int dmin=0, int dmax=0, int wmax=0, int wmin=1, int dexp=0, int dest=0, int dstop=0, Optional[int] steps=None, Optional[int] threads=None, float timeout=60.0, Optional[int] smax=None, Optional[Union[int, str, Sequence[int]]] start=None, Optional[int] cbeg=None, Optional[int] cend=None, Optional[int] css=None, int noscan=0, int classical=-1, int dW=-1, int maxC=0, float pmin=0.0, bool merge=True, Optional[str] drop=None, Optional[str] outD=None, Optional[bool] sort_rows=None, Optional[int] cc_prune=None, Optional[str] outC=None, int seed=0, int debug=0, bool nothrottle=False, int chunk_size=0, int ksub=0, int kwin=0, int win_mode=0, int win_hyb=0, int pairs=-1, Optional[int] min_hits=None, int cov_cws=100, Optional[int] refresh=None, bool verbose=False, Optional[threading.Event] stop_event=None, bool warn_start=True, bool warn_ksub=True, bool start_scan=False, bool ortho=True, bool warn_ortho=True)
Dict[str, Any] _sector_record(int dmin, int dmax, int rw_steps, List[List[int]] cws)
Optional[Dict[str, Any]] get_cached_distance(Optional[Any] H=None, Optional[Any] G=None, Optional[Any] L=None, Optional[Any] Hx=None, Optional[Any] Hz=None, Optional[Any] Lx=None, Optional[Any] Lz=None, Optional[Any] dem=None, Optional[Any] circuit=None, float pmin=0.0, Optional[Union[str, Path]] cache_file=None, Optional[Union[int, str, Sequence[int]]] start=None, Optional[bool] simple=None, Optional[bool] full=None, Optional[str] basis=None, Optional[int] rounds=None, float p_noise=0.001, bool force_noise=False, bool start_scan=False)
Definition dist_m4ri.py:717
None _check_kwargs(Callable[..., Any] func, Dict[str, Any] kwargs, Sequence[str] aliases=("win",))
bool _help_group_key_match(str gkey, str key)
str _start_key_suffix(Optional[List[int]] start_list)
Dict[str, Any] _model_matrices(List[Tuple[Any,...]] cols, List[int] group, List[bool] use, int num_observables)
Tuple[Any, bool, bool] _circuit_dem(Any circuit)
None _warn_experimental_ksub(Any ksub, int method, str solver="dist_m4ri")
str explain_bounds(List[int] bounds, Optional[int] method=None, str label="", Optional[Dict[str, Any]] stats=None, int dstop=0)
Definition dist_m4ri.py:543
None clear_distance_cache(Optional[Union[str, Path]] cache_file=None, bool clear_file=False)
Definition dist_m4ri.py:230
str _reduced_out_prefix(Union[bool, str, Path] spec, str src, Optional[Union[str, Path]] out_dir=None)
str _dem_cache_key(Any dem, float pmin=0.0)
None _warn_ignored(str msg)
List[List[int]] read_sparse_vectors(str filepath)
Definition dist_m4ri.py:844
Tuple[Any, int, int] strip_minority_detectors(Any circuit, str basis, bool verbose=False, bool thorough=True, Optional[Dict[str, Any]] thorough_res=None, Optional[List[bool]] mask_out=None)
int _main(Optional[List[str]] argv=None)
str _cli_morehelp_text()
Tuple[List[int], List[bool], int, int, int] _dem_detector_groups(Any dem, float pmin=0.0)
Any _add_noise_recursive(Any circuit, float p, int num_qubits, Set[int] active_qubits)
str get_sparse_array_state(A)
Definition dist_m4ri.py:789
Any compute_classical_distance(Any H, Optional[str] dist_m4ri=None, int method=3, Optional[int] threads=None, float timeout=60.0, Optional[int] num_steps=None, int d_exp=0, int d_min=0, int d_max=0, int dmin=0, int dmax=0, int dstop=0, int wmin=1, int wmax=0, Optional[int] smax=None, Optional[Union[int, str, Sequence[int]]] start=None, Optional[int] cbeg=None, Optional[int] cend=None, int noscan=0, int dW=-1, int maxC=0, Optional[str] finC=None, Optional[str] outC=None, bool do_cws=False, bool return_info=False, Optional[Union[str, Path]] cache_file=None, str solver="dist_m4ri", str codedistance_method="QDistEvol", Optional[Dict[str, Any]] codedistance_params=None, int seed=0, int debug=0, bool verbose=False, bool nothrottle=False, int chunk_size=0, int ksub=0, int kwin=0, int win_mode=0, int win_hyb=0, int pairs=-1, Optional[int] min_hits=None, int cov_cws=100, Optional[int] refresh=None, bool trust_start=False, bool start_scan=False, Optional[bool] sort_rows=None, Optional[int] cc_prune=None, Optional[str] engine=None, **kwargs)
None set_silent(bool silent=True)
bool _single_entry_is_exact(Dict[str, Any] entry, bool need_cws=False, int dstop=0)
List[List[int]] _expand_equal_columns(List[List[int]] cws, Any dem, float pmin=0.0, int maxC=0, bool verbose=False)
Optional[str] _resolve_stim_dem_out_path(Optional[Union[bool, str, Path]] out_spec, str circuit_src, str mode_tag, str ext, Optional[Union[str, Path]] out_dir=None)
None _warn_codedistance_ignored(Dict[str, Any] values)
Optional[str] check_binary_compatibility(Optional[str] binary_path=None)
Definition dist_m4ri.py:948
Any _cert_to_csr(Any M, Any qec_io)
str _ortho_warning_text()
Dict[str, Any] _parse_stderr_stats(str stderr)
Definition dist_m4ri.py:392
None enable_distance_cache()
Definition dist_m4ri.py:245
float rw_infoset_find_prob(int n, int rank, int w, int pairs_l=0)
Definition dist_m4ri.py:292
Any compute_quantum_distance(Any H, Optional[Any] G=None, Optional[Any] L=None, Optional[str] dist_m4ri=None, int method=3, Optional[int] threads=None, float timeout=60.0, Optional[int] num_steps=None, int d_exp=0, int d_min=0, int d_max=0, int dmin=0, int dmax=0, int dstop=0, int wmin=1, int wmax=0, Optional[int] smax=None, Optional[Union[int, str, Sequence[int]]] start=None, Optional[int] cbeg=None, Optional[int] cend=None, int noscan=0, int dW=-1, int maxC=0, Optional[str] finC=None, Optional[str] outC=None, bool do_cws=False, bool return_info=False, Optional[Union[str, Path]] cache_file=None, str solver="dist_m4ri", str codedistance_method="QDistEvol", Optional[Dict[str, Any]] codedistance_params=None, int seed=0, int debug=0, bool verbose=False, bool nothrottle=False, int chunk_size=0, int ksub=0, int kwin=0, int win_mode=0, int win_hyb=0, int pairs=-1, Optional[int] min_hits=None, int cov_cws=100, Optional[int] refresh=None, bool trust_start=False, bool start_scan=False, Optional[bool] sort_rows=None, Optional[int] cc_prune=None, bool ortho=True, Optional[str] engine=None, **kwargs)
None print_cli_short_help(Optional[Any] file=None)
None print_cli_help(Optional[Any] file=None)
None save_distance_cache(Optional[Union[str, Path]] filepath=None)
Definition dist_m4ri.py:202
Optional[str] check_finc_outc(Optional[str] finC, Optional[str] outC, bool verbose=False)
_get_codedistance()
Definition dist_m4ri.py:79
Dict[str, Any] load_distance_cache(Optional[Union[str, Path]] filepath=None)
Definition dist_m4ri.py:169
bool _cert_wmin_discharged(Any cert, int certified_dmin)
Any _rewrite_circuit_internal(Any circ, Dict[int, int] phys_to_log, Dict[int, int] active_gates_count)
Any set_circuit_rounds(Any circuit, int rounds)
Any compute_model_lower_bound(Any circuit=None, Optional[str] dist_m4ri=None, int method=3, Optional[int] threads=None, float timeout=60.0, Optional[int] num_steps=None, int dstop=0, int wmax=0, bool minority=False, float pmin=0.0, Optional[int] rounds=None, float p_noise=0.001, bool force_noise=False, Optional[Union[str, Path]] out_model=None, Optional[Union[str, Path]] cache_file=None, int seed=0, int debug=0, bool verbose=False, bool nothrottle=False, int pairs=-1, Optional[bool] sort_rows=None, Optional[int] cc_prune=None, bool return_info=False, Optional[Any] dem=None, **kwargs)
int print_cli_morehelp(Optional[Any] file=None, Optional[str] topic=None)
Optional[Set[str]] _help_group_members(Tuple[str, List[str], Dict[str, str]] entry)
Optional[str] _split_css_filename(Optional[str] filepath, str sector)
Tuple[int, int, int, int] _css_combine(Tuple[int, int, int] x, Tuple[int, int, int] z, int dmax=0)
str _start_warning_text(List[int] start_list)
Tuple[str, str] write_reduced_dem(Any dem, Sequence[int] dropped, Union[str, Path] prefix, float pmin=0.0, Optional[int] num_detectors=None, Optional[Dict[str, Any]] meta=None)
str _resolve_cert_engine(str solver="dist_m4ri", Optional[str] engine=None)
Tuple[int, int] _cert_extract_bounds(Any cert, int certified_dmin=1)
List[List[int]] track_circuit_carriers(Any circuit)
Dict[str, Tuple[Any, Any, Any]] _css_sector_codes(Any Hx, Any Hz, Optional[Any] Lx, Optional[Any] Lz)
str _format_duration(float t)
Definition dist_m4ri.py:381
Optional[List[int]] _prepare_start_option(Any start, bool trust_start=False, Any noscan=0, Any cbeg=None, Any cend=None, str solver="dist_m4ri", bool start_scan=False)
None disable_distance_cache()
Definition dist_m4ri.py:251
Tuple[int, int] _seed_bounds_from_entry(int eff_dmin, int eff_dmax, Optional[Dict[str, Any]] entry)
Dict[str, Any] _cached_code_distance(Any H, Optional[Any] G, Optional[Any] L, bool classical, str kind, Optional[str] dist_m4ri, int method, Optional[int] threads, float timeout, Optional[int] num_steps, int d_exp, int dmin, int dmax, int dstop, int wmin, int wmax, Optional[int] smax, Optional[List[int]] start_list, int dW, int maxC, Optional[str] finC, Optional[str] outC, bool need_cws, Optional[str] eff_cache_file, int seed, int debug, bool verbose, bool nothrottle, int chunk_size, int ksub, int kwin, int win_mode, Optional[int] min_hits, int cov_cws, Optional[int] refresh, bool trust_start, Optional[Any] fdem=None, float pmin=0.0, bool merge=True, Optional[Callable[[], str]] key_fn=None, Optional[Callable[[List[List[int]]], List[List[int]]]] cws_out_fn=None, Optional[Union[str, Path, List[int]]] fdrop=None, Optional[str] outD=None, Optional[bool] sort_rows=None, int win_hyb=0, int pairs=-1, Optional[int] cc_prune=None, bool ortho=True)
None _remove_temp_files(Sequence[Optional[str]] files, int debug=0)
Definition dist_m4ri.py:829
bool has_noise(Any circuit)
Tuple[Any,...] compute_css_distance(Any Hx, Any Hz, Optional[Any] Lx=None, Optional[Any] Lz=None, Optional[str] dist_m4ri=None, int method=3, Optional[int] threads=None, float timeout=60.0, Optional[int] num_steps=None, int d_exp=0, int d_min=0, int d_max=0, int dmin=0, int dmax=0, int dstop=0, int wmin=1, int wmax=0, Optional[int] smax=None, Optional[Union[int, str, Sequence[int]]] start=None, Optional[int] cbeg=None, Optional[int] cend=None, int noscan=0, int dW=-1, int maxC=0, Optional[str] finC=None, Optional[str] outC=None, bool do_cws=False, Optional[Union[str, Path]] cache_file=None, str solver="dist_m4ri", str codedistance_method="QDistEvol", Optional[Dict[str, Any]] codedistance_params=None, int seed=0, int debug=0, bool verbose=False, bool nothrottle=False, int chunk_size=0, int ksub=0, int kwin=0, int win_mode=0, int win_hyb=0, int pairs=-1, Optional[int] min_hits=None, int cov_cws=100, Optional[int] refresh=None, bool trust_start=False, bool start_scan=False, Optional[bool] sort_rows=None, Optional[int] cc_prune=None, bool ortho=True, Optional[str] engine=None, **kwargs)
str _drop_mode(Any drop)
Tuple[Optional[str], Optional[str]] _css_sector_finc(Optional[str] finC, Optional[str] outC)