dense-evolution 8.3.0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dashboard_core/__init__.py +115 -0
- dashboard_core/_gate_tables.py +30 -0
- dashboard_core/band_structure.py +71 -0
- dashboard_core/circuit_builder_component.py +232 -0
- dashboard_core/circuit_diagram.py +216 -0
- dashboard_core/crypto_protocols.py +77 -0
- dashboard_core/engine.py +326 -0
- dashboard_core/graphical_builder.py +114 -0
- dashboard_core/hamiltonians.py +593 -0
- dashboard_core/mass_decomposition_tool.py +47 -0
- dashboard_core/mitigation.py +343 -0
- dashboard_core/native_hf_diagnostics.py +62 -0
- dashboard_core/noise_tools.py +125 -0
- dashboard_core/qasm_library.py +233 -0
- dashboard_core/qmmm.py +16 -0
- dashboard_core/rag_tool.py +45 -0
- dashboard_core/state_visuals.py +288 -0
- dashboard_core/system_limits.py +60 -0
- dashboard_core/vector_healing.py +102 -0
- dashboard_core/visuals.py +158 -0
- dashboard_core/vqe.py +533 -0
- dashboard_core/wormhole.py +580 -0
- dense_evolution/__init__.py +114 -0
- dense_evolution/autodiff.py +10 -0
- dense_evolution/backends/__init__.py +5 -0
- dense_evolution/backends/chunk/__init__.py +37 -0
- dense_evolution/backends/chunk/_engine_imports.py +57 -0
- dense_evolution/backends/chunk/circuit_chunker.py +55 -0
- dense_evolution/backends/chunk/core.py +432 -0
- dense_evolution/backends/chunk/disk_overflow.py +232 -0
- dense_evolution/backends/chunk/geometry.py +95 -0
- dense_evolution/backends/chunk/guard.py +190 -0
- dense_evolution/backends/chunk/kernels.py +531 -0
- dense_evolution/backends/mps.py +1569 -0
- dense_evolution/backends/statevector.py +616 -0
- dense_evolution/chunk.py +25 -0
- dense_evolution/circuits/__init__.py +20 -0
- dense_evolution/circuits/compiler.py +488 -0
- dense_evolution/circuits/diagram.py +94 -0
- dense_evolution/circuits/gates.py +91 -0
- dense_evolution/circuits/parser.py +632 -0
- dense_evolution/circuits/qft.py +66 -0
- dense_evolution/circuits/random_circuit.py +85 -0
- dense_evolution/circuits/registry.py +74 -0
- dense_evolution/circuits/topology.py +79 -0
- dense_evolution/circuits/trotter.py +265 -0
- dense_evolution/circuits/uccsd.py +275 -0
- dense_evolution/cli.py +199 -0
- dense_evolution/compiler.py +9 -0
- dense_evolution/config.py +49 -0
- dense_evolution/drawing.py +10 -0
- dense_evolution/entropy.py +9 -0
- dense_evolution/fermions.py +9 -0
- dense_evolution/gates.py +9 -0
- dense_evolution/harrison_tb.py +16 -0
- dense_evolution/healing.py +18 -0
- dense_evolution/interop/__init__.py +18 -0
- dense_evolution/interop/qiskit_pennylane.py +406 -0
- dense_evolution/measurement.py +10 -0
- dense_evolution/mitigation/__init__.py +54 -0
- dense_evolution/mitigation/healing.py +215 -0
- dense_evolution/mitigation/kl_divergence.py +93 -0
- dense_evolution/mitigation/magic_entropy.py +163 -0
- dense_evolution/mitigation/magic_entropy_shadows.py +262 -0
- dense_evolution/mitigation/renyi.py +168 -0
- dense_evolution/mitigation/stabilizer_renyi_entropy.py +103 -0
- dense_evolution/mitigation/zne.py +990 -0
- dense_evolution/mps.py +9 -0
- dense_evolution/native_hf/__init__.py +26 -0
- dense_evolution/native_hf/_libcint/LICENSE-libcint +10 -0
- dense_evolution/native_hf/_libcint/libdecint.dll +0 -0
- dense_evolution/native_hf/assembly.py +304 -0
- dense_evolution/native_hf/basis.py +117 -0
- dense_evolution/native_hf/boys.py +35 -0
- dense_evolution/native_hf/bridge.py +112 -0
- dense_evolution/native_hf/cartesian.py +64 -0
- dense_evolution/native_hf/coulomb.py +196 -0
- dense_evolution/native_hf/differentiable.py +53 -0
- dense_evolution/native_hf/gaussians.py +79 -0
- dense_evolution/native_hf/kinetic.py +52 -0
- dense_evolution/native_hf/libcint_bridge.py +167 -0
- dense_evolution/native_hf/overlap.py +91 -0
- dense_evolution/native_hf/scf.py +404 -0
- dense_evolution/noise/__init__.py +79 -0
- dense_evolution/noise/coherent_attack.py +264 -0
- dense_evolution/noise/cosmic_ray.py +61 -0
- dense_evolution/noise/density_matrix_channels.py +78 -0
- dense_evolution/noise/differentiable.py +66 -0
- dense_evolution/noise/kraus/__init__.py +6 -0
- dense_evolution/noise/kraus/amplitude_damping.py +47 -0
- dense_evolution/noise/kraus/bitflip.py +22 -0
- dense_evolution/noise/kraus/combined.py +16 -0
- dense_evolution/noise/kraus/depolarizing.py +47 -0
- dense_evolution/noise/kraus/ideal.py +10 -0
- dense_evolution/noise/kraus/phaseflip.py +21 -0
- dense_evolution/noise/kraus_channels.py +285 -0
- dense_evolution/noise/oscillating.py +32 -0
- dense_evolution/noise/pink.py +80 -0
- dense_evolution/observables.py +11 -0
- dense_evolution/parser.py +9 -0
- dense_evolution/physics/__init__.py +27 -0
- dense_evolution/physics/entropy.py +161 -0
- dense_evolution/physics/fermions.py +322 -0
- dense_evolution/physics/observables.py +523 -0
- dense_evolution/physics/qec.py +1113 -0
- dense_evolution/physics/spectral.py +143 -0
- dense_evolution/physics/states.py +43 -0
- dense_evolution/protocols/__init__.py +27 -0
- dense_evolution/protocols/bb84.py +133 -0
- dense_evolution/protocols/di_qkd_ghz.py +199 -0
- dense_evolution/protocols/dicka_protocol2.py +124 -0
- dense_evolution/qec.py +20 -0
- dense_evolution/qft.py +9 -0
- dense_evolution/qmmm/__init__.py +13 -0
- dense_evolution/qmmm/ase_bridge.py +97 -0
- dense_evolution/qmmm/forces.py +388 -0
- dense_evolution/qmmm/propagation.py +80 -0
- dense_evolution/qmmm/region.py +137 -0
- dense_evolution/random_circuit.py +15 -0
- dense_evolution/registry.py +9 -0
- dense_evolution/simulator.py +10 -0
- dense_evolution/solvers/__init__.py +19 -0
- dense_evolution/solvers/autodiff.py +169 -0
- dense_evolution/solvers/harrison_tb.py +189 -0
- dense_evolution/solvers/vhd_tb.py +187 -0
- dense_evolution/states.py +9 -0
- dense_evolution/topology.py +9 -0
- dense_evolution/trotter.py +9 -0
- dense_evolution/utils/__init__.py +13 -0
- dense_evolution/utils/drawing.py +101 -0
- dense_evolution/utils/mass_decomposition.py +246 -0
- dense_evolution/utils/measurement.py +94 -0
- dense_evolution/vhd_tb.py +16 -0
- dense_evolution-8.3.0.dist-info/METADATA +366 -0
- dense_evolution-8.3.0.dist-info/RECORD +165 -0
- dense_evolution-8.3.0.dist-info/WHEEL +5 -0
- dense_evolution-8.3.0.dist-info/entry_points.txt +2 -0
- dense_evolution-8.3.0.dist-info/licenses/license.md +58 -0
- dense_evolution-8.3.0.dist-info/top_level.txt +5 -0
- ia_utils/__init__.py +0 -0
- ia_utils/adversarial_vector_attack.py +196 -0
- ia_utils/rag.py +288 -0
- ia_utils/vector_healing.py +399 -0
- local_site/__init__.py +0 -0
- local_site/app/__init__.py +0 -0
- local_site/app/server.py +1009 -0
- mcp_server/__init__.py +0 -0
- mcp_server/client.py +324 -0
- mcp_server/config.py +32 -0
- mcp_server/models.py +347 -0
- mcp_server/molecules.py +71 -0
- mcp_server/server.py +119 -0
- mcp_server/tools/__init__.py +0 -0
- mcp_server/tools/chemistry_tools.py +225 -0
- mcp_server/tools/circuit_tools.py +83 -0
- mcp_server/tools/crypto_tools.py +66 -0
- mcp_server/tools/mitigation_tools.py +81 -0
- mcp_server/tools/noise_tools.py +60 -0
- mcp_server/tools/retrieval_tools.py +44 -0
- mcp_server/tools/system_tools.py +149 -0
- mcp_server/tools/wormhole_tools.py +142 -0
- mcp_server/utils/__init__.py +0 -0
- mcp_server/utils/cache.py +55 -0
- mcp_server/utils/images.py +67 -0
- mcp_server/utils/truncation.py +38 -0
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
"""Stabilizer-code quantum error correction utilities: generic Pauli-string
|
|
2
|
+
commutation/syndrome primitives, an erasure-aware decoder, and a
|
|
3
|
+
minimum-weight-perfect-matching (MWPM) decoder.
|
|
4
|
+
|
|
5
|
+
Promoted from Dense-Evolution-Discovery's Steane [[7,1,3]] code
|
|
6
|
+
investigation (scripts/steane_code_block6_erasure_conversion.py), where a
|
|
7
|
+
Steane-specific version of `erasure_aware_decode` was first built and
|
|
8
|
+
verified: on shots with exactly 2 simultaneous heralded erasures, it
|
|
9
|
+
achieved exactly 0 decoding failures across 94-12,469 such shots at every
|
|
10
|
+
tested physical error rate (0 failures out of >60,000 double-erasure
|
|
11
|
+
shots total, 40,000 trials x 10 p-values), versus ~25% failure for a
|
|
12
|
+
standard syndrome-only decoder blind to the erasure locations -- a clean
|
|
13
|
+
confirmation of the real erasure-correction bound below. This version is
|
|
14
|
+
code-agnostic (works from any stabilizer generator list, not a
|
|
15
|
+
hand-built Steane-specific table), so it moved here instead of staying
|
|
16
|
+
Discovery-repo-specific research code.
|
|
17
|
+
|
|
18
|
+
Erasure-aware decoding exploits a real, foundational fact: Grassl, Beth &
|
|
19
|
+
Pellizzari, "Codes for the quantum erasure channel", Phys. Rev. A 56, 33
|
|
20
|
+
(1997) -- a distance-d stabilizer code can correct up to (d-1) ERASURES
|
|
21
|
+
(known-location errors, e.g. a heralded lost photon in a dual-rail
|
|
22
|
+
photonic qubit), versus only floor((d-1)/2) arbitrary (unlocated)
|
|
23
|
+
errors. Erasure location information is worth roughly twice as much as
|
|
24
|
+
an ordinary syndrome bit, because knowing WHERE the error is removes
|
|
25
|
+
exactly the ambiguity a blind syndrome-only decoder has to guess at.
|
|
26
|
+
|
|
27
|
+
`pymatching_decode` (prog.txt Sezione 4.3) fills the complementary,
|
|
28
|
+
far more common case: a real decoder for when NO erasure locations are
|
|
29
|
+
known at all (the standard setting for e.g. a surface code under generic
|
|
30
|
+
physical noise) -- `erasure_aware_decode`'s brute force (4**k over
|
|
31
|
+
heralded qubits) has no answer when k=0 heralded qubits, by design
|
|
32
|
+
(returns None). Backed by `pymatching` (Apache-2.0, oscarhiggott/PyMatching,
|
|
33
|
+
the standard minimum-weight-perfect-matching decoder for stabilizer
|
|
34
|
+
codes), an optional dependency (`pip install dense-evolution[pymatching]`),
|
|
35
|
+
not a required one -- most callers of this module (erasure-aware
|
|
36
|
+
decoding, raw syndrome computation) never need it. Only works for
|
|
37
|
+
"graph-like" codes (see `pymatching_decode`'s own docstring for the
|
|
38
|
+
real >2-checks-per-qubit constraint) -- NOT Steane and similar small
|
|
39
|
+
non-topological codes.
|
|
40
|
+
|
|
41
|
+
`blind_minimum_weight_decode` (prog.txt Sezione 4.4) covers what neither
|
|
42
|
+
of the above two can: blind (no erasure locations) decoding for codes
|
|
43
|
+
`pymatching_decode` structurally can't handle, like Steane. Pure Python,
|
|
44
|
+
no new dependency -- brute-forces every possible error in increasing
|
|
45
|
+
WEIGHT order (0, 1, 2, ...), same `itertools.product` machinery
|
|
46
|
+
`erasure_aware_decode` uses, just searching over which qubits are
|
|
47
|
+
non-identity too instead of only over Pauli letters on a fixed known
|
|
48
|
+
set. Tried first: reusing `erasure_aware_decode` itself with every
|
|
49
|
+
qubit passed as "heralded" (`heralded_qubits=range(n_qubits)`) -- this
|
|
50
|
+
does NOT work (verified directly on Steane: returns None even for a
|
|
51
|
+
plain single-qubit error), because with no qubits assumed error-free,
|
|
52
|
+
many stabilizer-equivalent full-length errors share the same syndrome,
|
|
53
|
+
so `erasure_aware_decode`'s "exactly one match total" criterion is
|
|
54
|
+
essentially always violated. Minimum-weight selection (return the
|
|
55
|
+
lowest-weight match, not require a totally unique one across every
|
|
56
|
+
possible weight) is what makes blind decoding well-posed at all -- the
|
|
57
|
+
same principle `pymatching_decode`'s MWPM implements via a matching
|
|
58
|
+
graph instead of brute force.
|
|
59
|
+
|
|
60
|
+
`counts_in_intervals_dimension` answers a question upstream of all of
|
|
61
|
+
the above: IS a given stream of error/erasure timestamps actually
|
|
62
|
+
bursty, or Poissonian? It generalizes the "counts-in-spheres" fractal
|
|
63
|
+
dimension estimator used to measure large-scale cosmic homogeneity
|
|
64
|
+
(Scrimgeour et al. 2012, MNRAS 425, 116, arXiv:1205.6812) from 3-D
|
|
65
|
+
space to 1-D time: D~=1 means homogeneous arrivals, D<1 means temporal
|
|
66
|
+
clustering (e.g. cosmic-ray-correlated bursts, arXiv:2104.05219 --
|
|
67
|
+
the same physical noise source `cosmic_ray_burst_profile` in
|
|
68
|
+
`dense_evolution.mitigation.zne` models). Returns the fit's R^2
|
|
69
|
+
alongside D deliberately, never just the number -- a box-counting fit
|
|
70
|
+
through too few points spanning too narrow a range of scales can
|
|
71
|
+
report a fractal dimension as absurd as 142 in 3 dimensions purely
|
|
72
|
+
from fit noise, not from any real structure in the data.
|
|
73
|
+
"""
|
|
74
|
+
import itertools
|
|
75
|
+
from collections import deque
|
|
76
|
+
from typing import Optional, Sequence
|
|
77
|
+
|
|
78
|
+
import numpy as np
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
import pymatching
|
|
82
|
+
HAS_PYMATCHING = True
|
|
83
|
+
except ImportError: # pragma: no cover -- pymatching is always installed in CI (same as stim's equivalent branch in dense_evolution/interop/qiskit_pennylane.py)
|
|
84
|
+
HAS_PYMATCHING = False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _require_pymatching():
|
|
88
|
+
if not HAS_PYMATCHING:
|
|
89
|
+
raise ImportError(
|
|
90
|
+
"MWPM decoding requires the 'pymatching' package. "
|
|
91
|
+
"Install it with: pip install dense-evolution[pymatching]")
|
|
92
|
+
|
|
93
|
+
_ANTICOMMUTING_PAIRS = {
|
|
94
|
+
frozenset(('X', 'Z')), frozenset(('X', 'Y')), frozenset(('Y', 'Z')),
|
|
95
|
+
}
|
|
96
|
+
PAULIS = ('I', 'X', 'Y', 'Z')
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def pauli_commutes(p1: str, p2: str) -> bool:
|
|
100
|
+
"""Whether two equal-length Pauli strings (each character in IXYZ, no
|
|
101
|
+
global phase) commute -- the standard symplectic rule: they commute
|
|
102
|
+
iff the number of qubit positions where the local single-qubit
|
|
103
|
+
Paulis anticommute (X/Z, X/Y, or Y/Z, in either order; I commutes
|
|
104
|
+
with everything) is EVEN.
|
|
105
|
+
|
|
106
|
+
>>> pauli_commutes('XX', 'ZZ') # X,Z anticommute at both qubits -> 2 (even) -> commute
|
|
107
|
+
True
|
|
108
|
+
>>> pauli_commutes('XI', 'ZI') # X,Z anticommute at 1 qubit -> 1 (odd) -> anticommute
|
|
109
|
+
False
|
|
110
|
+
"""
|
|
111
|
+
if len(p1) != len(p2):
|
|
112
|
+
raise ValueError(f"Pauli strings must be equal length: {len(p1)} != {len(p2)}")
|
|
113
|
+
n_anticommuting = sum(
|
|
114
|
+
1 for a, b in zip(p1, p2)
|
|
115
|
+
if a != 'I' and b != 'I' and a != b and frozenset((a, b)) in _ANTICOMMUTING_PAIRS
|
|
116
|
+
)
|
|
117
|
+
return n_anticommuting % 2 == 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def compute_syndrome(pauli_error: str, stabilizers: Sequence[str]) -> tuple:
|
|
121
|
+
"""The syndrome (one bit per stabilizer generator, 1 = anticommutes /
|
|
122
|
+
detected, 0 = commutes / undetected) a given Pauli error string would
|
|
123
|
+
produce against `stabilizers` (a list of equal-length Pauli strings,
|
|
124
|
+
the code's stabilizer generators -- X-type, Z-type, or mixed; this
|
|
125
|
+
function doesn't assume a CSS structure)."""
|
|
126
|
+
return tuple(0 if pauli_commutes(pauli_error, g) else 1 for g in stabilizers)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _pauli_string(n_qubits: int, assignment: dict) -> str:
|
|
130
|
+
chars = ['I'] * n_qubits
|
|
131
|
+
for q, p in assignment.items():
|
|
132
|
+
chars[q] = p
|
|
133
|
+
return ''.join(chars)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
_PAULI_SYMPLECTIC = {'I': (0, 0), 'X': (1, 0), 'Z': (0, 1), 'Y': (1, 1)}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _pauli_to_symplectic(pauli_str: str) -> np.ndarray:
|
|
140
|
+
"""Length-n Pauli string -> length-2n GF(2) vector (n X-bits, then n
|
|
141
|
+
Z-bits). The Pauli group modulo global phase is isomorphic to
|
|
142
|
+
GF(2)^2n under this map, with Pauli multiplication (mod phase)
|
|
143
|
+
corresponding to vector XOR -- so two Pauli strings differ by a
|
|
144
|
+
stabilizer element (same coset) iff the XOR of their symplectic
|
|
145
|
+
vectors lies in the GF(2) span of the stabilizers' own symplectic
|
|
146
|
+
vectors."""
|
|
147
|
+
n = len(pauli_str)
|
|
148
|
+
v = np.zeros(2 * n, dtype=np.uint8)
|
|
149
|
+
for i, p in enumerate(pauli_str):
|
|
150
|
+
x, z = _PAULI_SYMPLECTIC[p]
|
|
151
|
+
v[i] = x
|
|
152
|
+
v[n + i] = z
|
|
153
|
+
return v
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _gf2_rref(matrix: np.ndarray):
|
|
157
|
+
"""Reduced row-echelon form of a GF(2) matrix, plus the column index
|
|
158
|
+
of each row's pivot. Pure Gaussian elimination mod 2 -- deciding
|
|
159
|
+
stabilizer-group membership needs this, not enumeration of the
|
|
160
|
+
2**k group elements (k = number of independent generators)."""
|
|
161
|
+
m = matrix.copy() % 2
|
|
162
|
+
rows, cols = m.shape
|
|
163
|
+
pivots = []
|
|
164
|
+
pivot_row = 0
|
|
165
|
+
for col in range(cols):
|
|
166
|
+
if pivot_row >= rows:
|
|
167
|
+
break
|
|
168
|
+
pivot = next((r for r in range(pivot_row, rows) if m[r, col]), None)
|
|
169
|
+
if pivot is None:
|
|
170
|
+
continue
|
|
171
|
+
m[[pivot_row, pivot]] = m[[pivot, pivot_row]]
|
|
172
|
+
for r in range(rows):
|
|
173
|
+
if r != pivot_row and m[r, col]:
|
|
174
|
+
m[r] ^= m[pivot_row]
|
|
175
|
+
pivots.append(col)
|
|
176
|
+
pivot_row += 1
|
|
177
|
+
return m, pivots
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _in_gf2_span(v: np.ndarray, rref: np.ndarray, pivots: list) -> bool:
|
|
181
|
+
"""Whether GF(2) vector v lies in the row space of `rref` (already in
|
|
182
|
+
reduced row-echelon form, with `pivots[i]` the pivot column of row
|
|
183
|
+
i) -- reduce v against each pivot row in turn, in the span iff the
|
|
184
|
+
residual is all-zero."""
|
|
185
|
+
v = v.copy() % 2
|
|
186
|
+
for row, col in enumerate(pivots):
|
|
187
|
+
if v[col]:
|
|
188
|
+
v ^= rref[row]
|
|
189
|
+
return not v.any()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def erasure_aware_decode(
|
|
193
|
+
observed_syndrome: tuple,
|
|
194
|
+
heralded_qubits: Sequence[int],
|
|
195
|
+
n_qubits: int,
|
|
196
|
+
stabilizers: Sequence[str],
|
|
197
|
+
) -> Optional[str]:
|
|
198
|
+
"""Erasure-aware decoder for any stabilizer code. Given the observed
|
|
199
|
+
syndrome and a list of qubits KNOWN to have been erased (e.g. a
|
|
200
|
+
heralded photon-loss event on a dual-rail-encoded qubit), brute-forces
|
|
201
|
+
every Pauli assignment (I/X/Y/Z, 4**len(heralded_qubits) combinations)
|
|
202
|
+
on just the heralded qubits and returns the unique full-length Pauli
|
|
203
|
+
string reproducing `observed_syndrome` exactly.
|
|
204
|
+
|
|
205
|
+
Returns `None` -- not a guess -- when there are zero heralded qubits,
|
|
206
|
+
when the observed syndrome is not explained by any assignment on the
|
|
207
|
+
heralded qubits alone, or when more than one assignment explains it
|
|
208
|
+
(ambiguous). Both `None` cases mean: fall back to a standard
|
|
209
|
+
syndrome-only decoder, or treat as a detected-but-uncorrectable
|
|
210
|
+
event -- this function will not silently return a wrong-but-plausible
|
|
211
|
+
correction. The number of heralded qubits this can actually resolve
|
|
212
|
+
unambiguously is bounded by the code's real distance (Grassl, Beth &
|
|
213
|
+
Pellizzari 1997: up to d-1 erasures) -- that bound emerges naturally
|
|
214
|
+
from the brute-force search itself (more heralded qubits than the
|
|
215
|
+
code can resolve typically yields zero or multiple matches), it is
|
|
216
|
+
not hard-coded here.
|
|
217
|
+
|
|
218
|
+
Cost is 4**len(heralded_qubits) syndrome computations, each O(n_qubits
|
|
219
|
+
* len(stabilizers)) -- fine for the small numbers of simultaneous
|
|
220
|
+
erasures a real per-shot noise rate produces (verified up to 2 in the
|
|
221
|
+
original Steane investigation; tractable up to 4-5 for most small
|
|
222
|
+
codes before it's worth switching to a smarter search).
|
|
223
|
+
"""
|
|
224
|
+
if not heralded_qubits:
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
matches = []
|
|
228
|
+
for assignment_paulis in itertools.product(PAULIS, repeat=len(heralded_qubits)):
|
|
229
|
+
assignment = dict(zip(heralded_qubits, assignment_paulis))
|
|
230
|
+
candidate = _pauli_string(n_qubits, assignment)
|
|
231
|
+
if compute_syndrome(candidate, stabilizers) == tuple(observed_syndrome):
|
|
232
|
+
matches.append(candidate)
|
|
233
|
+
|
|
234
|
+
if len(matches) != 1:
|
|
235
|
+
return None
|
|
236
|
+
return matches[0]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def pymatching_decode(
|
|
240
|
+
observed_syndrome: Sequence[int],
|
|
241
|
+
stabilizers: Sequence[str],
|
|
242
|
+
n_qubits: int,
|
|
243
|
+
error_type: str = 'X',
|
|
244
|
+
weights: Optional[Sequence[float]] = None,
|
|
245
|
+
) -> str:
|
|
246
|
+
"""MWPM syndrome decoder (via `pymatching`) for a single physical error
|
|
247
|
+
type, no erasure/heralding information needed -- the standard decoding
|
|
248
|
+
setting `erasure_aware_decode` deliberately doesn't cover (it always
|
|
249
|
+
returns None with zero heralded qubits).
|
|
250
|
+
|
|
251
|
+
Restricted to same-purpose stabilizers detecting ONE error type at a
|
|
252
|
+
time (the standard CSS setup: e.g. Z-type stabilizers decoding X
|
|
253
|
+
errors, or vice versa) -- NOT the fully general mixed-Pauli case
|
|
254
|
+
`compute_syndrome`/`erasure_aware_decode` accept. A code with both X-
|
|
255
|
+
and Z-type errors to correct needs two separate calls, one per type
|
|
256
|
+
(X-type stabilizers -> decode Z errors, Z-type stabilizers -> decode
|
|
257
|
+
X errors), each contributing its own half of the full correction --
|
|
258
|
+
the same two-pass structure any real CSS decoder uses, not a
|
|
259
|
+
limitation specific to this wrapper.
|
|
260
|
+
|
|
261
|
+
FURTHER REAL RESTRICTION (verified directly against pymatching, not
|
|
262
|
+
assumed from its docs): a matching-graph decoder needs every qubit
|
|
263
|
+
checked by AT MOST 2 stabilizers -- pymatching represents each
|
|
264
|
+
potential error as a graph EDGE between (at most) 2 detector nodes, a
|
|
265
|
+
structural fact about topological/surface codes (each qubit sits on
|
|
266
|
+
an edge between exactly 2 checks), not a general property of
|
|
267
|
+
stabilizer codes. Steane [[7,1,3]]'s weight-4 stabilizers are a real
|
|
268
|
+
counterexample -- qubit 6 is checked by all 3 X-stabilizers at once
|
|
269
|
+
(3 > 2), so `pymatching_decode` cannot be used for it at all
|
|
270
|
+
(confirmed: raises ValueError below, not just slow or approximate) --
|
|
271
|
+
use `erasure_aware_decode` for codes like that instead. Repetition
|
|
272
|
+
codes and the surface/toric code family satisfy the <=2 constraint by
|
|
273
|
+
construction; small non-topological codes generally do not.
|
|
274
|
+
|
|
275
|
+
The check matrix pymatching needs is built from `stabilizers` via this
|
|
276
|
+
module's own `pauli_commutes` (column q of row i is 1 iff stabilizer i
|
|
277
|
+
anticommutes with a lone `error_type` error on qubit q) rather than a
|
|
278
|
+
naive "non-identity entry" heuristic -- those disagree whenever a
|
|
279
|
+
stabilizer has the SAME letter as `error_type` at some qubit (e.g. an
|
|
280
|
+
'X' entry in a stabilizer being used to decode X errors: it commutes
|
|
281
|
+
with an X error there and must NOT count as detecting it, but is very
|
|
282
|
+
much non-identity).
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
observed_syndrome : sequence of int
|
|
287
|
+
One bit per stabilizer, same convention as `compute_syndrome`'s
|
|
288
|
+
return value (1 = that stabilizer detected/anticommuted).
|
|
289
|
+
stabilizers : sequence of str
|
|
290
|
+
The stabilizer generators used for this error type (e.g. every
|
|
291
|
+
Z-type generator, to decode X errors), each a length-`n_qubits`
|
|
292
|
+
Pauli string over IXYZ. Mixing generator types that detect
|
|
293
|
+
different error types in the same call silently produces a wrong
|
|
294
|
+
check matrix -- this function has no way to tell that apart from
|
|
295
|
+
a correctly-scoped single-type list, so it isn't validated here.
|
|
296
|
+
n_qubits : int
|
|
297
|
+
Number of physical qubits (columns of the check matrix / length
|
|
298
|
+
of the returned Pauli string).
|
|
299
|
+
error_type : str, optional
|
|
300
|
+
Which single-qubit Pauli error this decodes for -- one of 'X',
|
|
301
|
+
'Y', 'Z'. Defaults to 'X'.
|
|
302
|
+
weights : sequence of float, optional
|
|
303
|
+
Per-qubit edge weight for MWPM (e.g. -log(p_q) for a known
|
|
304
|
+
per-qubit physical error rate p_q) -- forwarded to
|
|
305
|
+
`pymatching.Matching.from_check_matrix`. Defaults to pymatching's
|
|
306
|
+
own default (uniform weight 1.0 for every qubit, i.e. no prior
|
|
307
|
+
assumption about which qubits are more error-prone).
|
|
308
|
+
|
|
309
|
+
Returns
|
|
310
|
+
-------
|
|
311
|
+
str
|
|
312
|
+
Length-`n_qubits` Pauli string (only 'I' and `error_type`) giving
|
|
313
|
+
the minimum-weight correction pymatching found.
|
|
314
|
+
|
|
315
|
+
Raises
|
|
316
|
+
------
|
|
317
|
+
ImportError
|
|
318
|
+
If `pymatching` isn't installed (`pip install dense-evolution[pymatching]`).
|
|
319
|
+
ValueError
|
|
320
|
+
If `error_type` isn't one of 'X'/'Y'/'Z', if `observed_syndrome`
|
|
321
|
+
doesn't have one entry per stabilizer, if a stabilizer's length
|
|
322
|
+
isn't `n_qubits`, or if every stabilizer commutes with every
|
|
323
|
+
possible `error_type` error (the check matrix would be all-zero --
|
|
324
|
+
almost always means `stabilizers` is the wrong generator type for
|
|
325
|
+
the requested `error_type`, not a real all-zero code).
|
|
326
|
+
|
|
327
|
+
Examples
|
|
328
|
+
--------
|
|
329
|
+
>>> # 3-qubit repetition code, Z-type stabilizers, decoding X errors
|
|
330
|
+
>>> stabilizers = ['ZZI', 'IZZ']
|
|
331
|
+
>>> syndrome = compute_syndrome('IXI', stabilizers) # X error on qubit 1
|
|
332
|
+
>>> pymatching_decode(syndrome, stabilizers, n_qubits=3, error_type='X')
|
|
333
|
+
'IXI'
|
|
334
|
+
"""
|
|
335
|
+
_require_pymatching()
|
|
336
|
+
|
|
337
|
+
if error_type not in ('X', 'Y', 'Z'):
|
|
338
|
+
raise ValueError(f"error_type must be one of 'X', 'Y', 'Z', got {error_type!r}")
|
|
339
|
+
if len(observed_syndrome) != len(stabilizers):
|
|
340
|
+
raise ValueError(
|
|
341
|
+
f"observed_syndrome has {len(observed_syndrome)} entries but there are "
|
|
342
|
+
f"{len(stabilizers)} stabilizers -- these must match one-to-one"
|
|
343
|
+
)
|
|
344
|
+
for i, s in enumerate(stabilizers):
|
|
345
|
+
if len(s) != n_qubits:
|
|
346
|
+
raise ValueError(f"stabilizers[{i}] has length {len(s)}, expected n_qubits={n_qubits}")
|
|
347
|
+
|
|
348
|
+
check_matrix = np.array(
|
|
349
|
+
[[0 if pauli_commutes(s[q], error_type) else 1 for q in range(n_qubits)] for s in stabilizers],
|
|
350
|
+
dtype=np.uint8,
|
|
351
|
+
)
|
|
352
|
+
if not check_matrix.any():
|
|
353
|
+
raise ValueError(
|
|
354
|
+
f"every stabilizer commutes with every possible {error_type} error -- "
|
|
355
|
+
f"stabilizers is very likely the wrong generator type to decode {error_type} "
|
|
356
|
+
f"errors (e.g. passing X-type stabilizers to decode X errors, which they "
|
|
357
|
+
f"cannot detect by construction)"
|
|
358
|
+
)
|
|
359
|
+
checks_per_qubit = check_matrix.sum(axis=0)
|
|
360
|
+
if (checks_per_qubit > 2).any():
|
|
361
|
+
bad_qubits = np.where(checks_per_qubit > 2)[0].tolist()
|
|
362
|
+
raise ValueError(
|
|
363
|
+
f"pymatching's matching-graph decoder needs every qubit checked by AT MOST 2 "
|
|
364
|
+
f"stabilizers (a graph edge connects at most 2 detector nodes) -- qubit(s) "
|
|
365
|
+
f"{bad_qubits} are each checked by {checks_per_qubit[bad_qubits].tolist()} "
|
|
366
|
+
f"stabilizers here. This is a real structural requirement of matching-graph "
|
|
367
|
+
f"decoding (true for the surface code and other topological codes, where each "
|
|
368
|
+
f"qubit sits on an edge between exactly 2 checks), NOT satisfied by every "
|
|
369
|
+
f"stabilizer code -- e.g. Steane [[7,1,3]]'s weight-4 stabilizers check some "
|
|
370
|
+
f"qubits 3 times, so pymatching_decode cannot be used for it; use "
|
|
371
|
+
f"erasure_aware_decode instead for codes like that."
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
matching = pymatching.Matching.from_check_matrix(check_matrix, weights=weights)
|
|
375
|
+
correction = matching.decode(np.asarray(observed_syndrome, dtype=np.uint8))
|
|
376
|
+
|
|
377
|
+
return ''.join(error_type if bit else 'I' for bit in correction)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def blind_minimum_weight_decode(
|
|
381
|
+
observed_syndrome: Sequence[int],
|
|
382
|
+
n_qubits: int,
|
|
383
|
+
stabilizers: Sequence[str],
|
|
384
|
+
max_weight: Optional[int] = None,
|
|
385
|
+
) -> Optional[str]:
|
|
386
|
+
"""Blind (no known erasure locations) minimum-weight decoder for any
|
|
387
|
+
stabilizer code -- including ones `pymatching_decode` structurally
|
|
388
|
+
cannot handle (Steane and other small non-"graph-like" codes; see
|
|
389
|
+
this module's docstring for why the naive `erasure_aware_decode(
|
|
390
|
+
..., heralded_qubits=range(n_qubits))` trick does NOT work here).
|
|
391
|
+
|
|
392
|
+
Searches every possible Pauli error in increasing WEIGHT order (0
|
|
393
|
+
non-identity qubits, then 1, then 2, ...), stopping at the first
|
|
394
|
+
weight with at least one match. Multiple matches at that weight are
|
|
395
|
+
NOT automatically ambiguous: in a degenerate code (e.g. Shor
|
|
396
|
+
[[9,1,3]]), two lowest-weight corrections that differ by a
|
|
397
|
+
stabilizer-group element are the SAME physical correction (applying
|
|
398
|
+
either one restores the code space identically), not two competing
|
|
399
|
+
guesses. This checks that directly -- via the corrections' symplectic
|
|
400
|
+
(GF(2)) representation, not by re-deriving it from scratch each call
|
|
401
|
+
-- and returns the (deterministic, first-found) representative when
|
|
402
|
+
every match is in the same stabilizer coset. Returns `None` only when
|
|
403
|
+
two matches at the minimum weight differ by an actual LOGICAL
|
|
404
|
+
operator (not in the stabilizer group), which is genuine ambiguity, or
|
|
405
|
+
when nothing matches by `max_weight`. This IS the same principle
|
|
406
|
+
`pymatching_decode`'s MWPM implements, via brute force instead of a
|
|
407
|
+
matching graph -- deliberately much slower, in exchange for working
|
|
408
|
+
on ANY stabilizer code, graph-like or not.
|
|
409
|
+
|
|
410
|
+
Cost: for a given weight w, `C(n_qubits, w) * 3**w` syndrome checks,
|
|
411
|
+
each O(n_qubits * len(stabilizers)) -- explodes quickly for w beyond
|
|
412
|
+
a handful (e.g. n_qubits=7: 21 at w=1, 189 at w=2, 945 at w=3), so
|
|
413
|
+
this is for the small-code, low-weight-error regime
|
|
414
|
+
`erasure_aware_decode` already targets, not a general substitute for
|
|
415
|
+
`pymatching_decode` at surface-code sizes.
|
|
416
|
+
|
|
417
|
+
Parameters
|
|
418
|
+
----------
|
|
419
|
+
observed_syndrome : sequence of int
|
|
420
|
+
One bit per stabilizer, same convention as `compute_syndrome`.
|
|
421
|
+
n_qubits : int
|
|
422
|
+
Number of physical qubits.
|
|
423
|
+
stabilizers : sequence of str
|
|
424
|
+
The code's stabilizer generators (any mix of Pauli types --
|
|
425
|
+
unlike `pymatching_decode`, not restricted to one error type per
|
|
426
|
+
call: this searches full IXYZ Pauli strings directly, same as
|
|
427
|
+
`erasure_aware_decode`).
|
|
428
|
+
max_weight : int, optional
|
|
429
|
+
Largest error weight to search before giving up. Defaults to
|
|
430
|
+
`n_qubits` (search everything) -- pass a small explicit bound
|
|
431
|
+
(matching the code's known error-correcting capability, e.g. 1
|
|
432
|
+
for a distance-3 code) to fail fast instead of paying the full
|
|
433
|
+
combinatorial cost on a syndrome nothing low-weight can explain.
|
|
434
|
+
|
|
435
|
+
Returns
|
|
436
|
+
-------
|
|
437
|
+
str or None
|
|
438
|
+
Length-`n_qubits` Pauli string (IXYZ), or `None` if ambiguous at
|
|
439
|
+
the minimum matching weight or unexplained up to `max_weight`.
|
|
440
|
+
|
|
441
|
+
Examples
|
|
442
|
+
--------
|
|
443
|
+
>>> # Steane [[7,1,3]], full X+Z stabilizer set -- exactly the code
|
|
444
|
+
>>> # pymatching_decode CANNOT handle (weight-4 stabilizers check some
|
|
445
|
+
>>> # qubits 3 times), blind single-qubit Z error, no erasure info:
|
|
446
|
+
>>> steane_x = ['IIIXXXX', 'IXXIIXX', 'XIXIXIX']
|
|
447
|
+
>>> steane_z = ['IIIZZZZ', 'IZZIIZZ', 'ZIZIZIZ']
|
|
448
|
+
>>> stabilizers = steane_x + steane_z
|
|
449
|
+
>>> syndrome = compute_syndrome('IIIZIII', stabilizers)
|
|
450
|
+
>>> blind_minimum_weight_decode(syndrome, n_qubits=7, stabilizers=stabilizers)
|
|
451
|
+
'IIIZIII'
|
|
452
|
+
"""
|
|
453
|
+
if max_weight is None:
|
|
454
|
+
max_weight = n_qubits
|
|
455
|
+
if not 0 <= max_weight <= n_qubits:
|
|
456
|
+
raise ValueError(f"max_weight must be between 0 and n_qubits={n_qubits}, got {max_weight}")
|
|
457
|
+
if len(observed_syndrome) != len(stabilizers):
|
|
458
|
+
raise ValueError(
|
|
459
|
+
f"observed_syndrome has {len(observed_syndrome)} entries but there are "
|
|
460
|
+
f"{len(stabilizers)} stabilizers -- these must match one-to-one"
|
|
461
|
+
)
|
|
462
|
+
for i, s in enumerate(stabilizers):
|
|
463
|
+
if len(s) != n_qubits:
|
|
464
|
+
raise ValueError(f"stabilizers[{i}] has length {len(s)}, expected n_qubits={n_qubits}")
|
|
465
|
+
|
|
466
|
+
target = tuple(observed_syndrome)
|
|
467
|
+
stab_rref, stab_pivots = _gf2_rref(
|
|
468
|
+
np.array([_pauli_to_symplectic(s) for s in stabilizers], dtype=np.uint8)
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
for weight in range(max_weight + 1):
|
|
472
|
+
matches = []
|
|
473
|
+
for qubits in itertools.combinations(range(n_qubits), weight):
|
|
474
|
+
for paulis in itertools.product(PAULIS[1:], repeat=weight):
|
|
475
|
+
assignment = dict(zip(qubits, paulis))
|
|
476
|
+
candidate = _pauli_string(n_qubits, assignment)
|
|
477
|
+
if compute_syndrome(candidate, stabilizers) == target:
|
|
478
|
+
matches.append(candidate)
|
|
479
|
+
if not matches:
|
|
480
|
+
continue
|
|
481
|
+
if len(matches) == 1:
|
|
482
|
+
return matches[0]
|
|
483
|
+
|
|
484
|
+
reference = matches[0]
|
|
485
|
+
reference_symplectic = _pauli_to_symplectic(reference)
|
|
486
|
+
same_coset = all(
|
|
487
|
+
_in_gf2_span(reference_symplectic ^ _pauli_to_symplectic(m), stab_rref, stab_pivots)
|
|
488
|
+
for m in matches[1:]
|
|
489
|
+
)
|
|
490
|
+
return reference if same_coset else None
|
|
491
|
+
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def counts_in_intervals_dimension(
|
|
496
|
+
event_times: Sequence[float],
|
|
497
|
+
window_sizes: Sequence[float],
|
|
498
|
+
min_reference_points: int = 5,
|
|
499
|
+
) -> tuple:
|
|
500
|
+
"""Correlation-dimension estimator for a 1-D point process (event
|
|
501
|
+
times), generalizing the "counts-in-spheres" statistic used to
|
|
502
|
+
measure the fractal dimension of galaxy distributions -- Scrimgeour
|
|
503
|
+
et al., "The WiggleZ Dark Energy Survey: the transition to
|
|
504
|
+
large-scale cosmic homogeneity," MNRAS 425, 116 (2012),
|
|
505
|
+
arXiv:1205.6812 -- from 3-D space down to 1-D time.
|
|
506
|
+
|
|
507
|
+
For a homogeneous (Poisson) point process, the expected number of
|
|
508
|
+
OTHER events within radius r of a given event scales as N(<=r) ~ r^1
|
|
509
|
+
exactly. Real burst-like noise (e.g. cosmic-ray-correlated error
|
|
510
|
+
events, arXiv:2104.05219, already modelled by `cosmic_ray_burst_profile`
|
|
511
|
+
in `dense_evolution.mitigation.zne`) clusters in time: events arrive
|
|
512
|
+
in tight groups separated by long, comparatively empty gaps, which
|
|
513
|
+
depresses this exponent below 1. This function measures that exponent
|
|
514
|
+
directly from a sequence of observed/simulated event times (e.g.
|
|
515
|
+
heralded-erasure timestamps, or detected-syndrome timestamps), instead
|
|
516
|
+
of assuming Poissonian noise or a particular burst model up front --
|
|
517
|
+
the measured dimension is then evidence for whether `decode_with_erasure_fallback`
|
|
518
|
+
style single-shot decoding or a burst-aware strategy is the right model
|
|
519
|
+
for a given noise source.
|
|
520
|
+
|
|
521
|
+
For each candidate radius r in `window_sizes`, every event at least r
|
|
522
|
+
away from both ends of the observed time range is used as a reference
|
|
523
|
+
point (events too close to either edge are skipped for that r, so a
|
|
524
|
+
partially-empty window near the boundary never silently deflates the
|
|
525
|
+
count -- the same edge correction real counts-in-spheres analyses
|
|
526
|
+
use). The mean count of other events within r of each valid reference
|
|
527
|
+
point is computed, and the dimension D is the slope of log(mean
|
|
528
|
+
count) vs log(r) over a linear least-squares fit -- with the fit's
|
|
529
|
+
R^2 returned alongside it, not hidden, because a narrow or
|
|
530
|
+
poorly-covered range of `window_sizes` can make the fitted slope
|
|
531
|
+
meaningless (see the warning below).
|
|
532
|
+
|
|
533
|
+
D ~= 1 : homogeneous/Poissonian arrivals, no clustering.
|
|
534
|
+
D < 1 : temporally clustered/bursty (e.g. cosmic-ray-correlated
|
|
535
|
+
error bursts) -- the smaller D, the tighter the clustering.
|
|
536
|
+
D > 1 : more regularly spaced than random (suppressed fluctuations,
|
|
537
|
+
"hyperuniform" arrivals) -- unusual for physical error
|
|
538
|
+
processes but not excluded by the statistic itself.
|
|
539
|
+
|
|
540
|
+
A LOW R^2 (rule of thumb: below ~0.98) means `window_sizes` does not
|
|
541
|
+
span enough dynamic range or lacks enough valid reference points for
|
|
542
|
+
the fit to be trustworthy -- widen the range (ideally 2-3 orders of
|
|
543
|
+
magnitude) and/or supply more events rather than trusting the
|
|
544
|
+
reported D. This mirrors a real, previously-made mistake: a spatial
|
|
545
|
+
box-counting fit through only 4 points spanning a narrow range of
|
|
546
|
+
scales produced a fractal dimension of 142 (physically impossible in
|
|
547
|
+
3 dimensions) from noise alone, not from any real structure in the
|
|
548
|
+
data -- always inspect R^2 before quoting D.
|
|
549
|
+
|
|
550
|
+
Cost is O(len(window_sizes) * n_events * log(n_events)) -- `event_times`
|
|
551
|
+
is sorted once up front, and each radius's per-reference-point count
|
|
552
|
+
is a pair of `np.searchsorted` calls (vectorized across all reference
|
|
553
|
+
points at once) rather than an O(n_events) brute-force scan per point.
|
|
554
|
+
|
|
555
|
+
Parameters
|
|
556
|
+
----------
|
|
557
|
+
event_times : sequence of float
|
|
558
|
+
Timestamps of observed/simulated events (need not be sorted).
|
|
559
|
+
window_sizes : sequence of float
|
|
560
|
+
Radii r to evaluate counts at, ideally spanning several orders of
|
|
561
|
+
magnitude and containing at least ~5-8 values.
|
|
562
|
+
min_reference_points : int, optional
|
|
563
|
+
Minimum number of edge-safe reference events required at a given
|
|
564
|
+
r for that r to be included in the fit. Radii with fewer valid
|
|
565
|
+
reference points (or a zero mean count) are silently dropped, not
|
|
566
|
+
zero-padded, so the returned `mean_counts` may be shorter than
|
|
567
|
+
`window_sizes`. Defaults to 5.
|
|
568
|
+
|
|
569
|
+
Returns
|
|
570
|
+
-------
|
|
571
|
+
dimension : float
|
|
572
|
+
Estimated scaling exponent D (the slope of the log-log fit).
|
|
573
|
+
r_squared : float
|
|
574
|
+
Coefficient of determination of the log-log linear fit --
|
|
575
|
+
inspect this before trusting `dimension` (see above).
|
|
576
|
+
mean_counts : dict[float, float]
|
|
577
|
+
Mean count of other events within each usable radius r, keyed by
|
|
578
|
+
the r values from `window_sizes` that had enough valid reference
|
|
579
|
+
points and a nonzero count.
|
|
580
|
+
|
|
581
|
+
Raises
|
|
582
|
+
------
|
|
583
|
+
ValueError
|
|
584
|
+
If `event_times` has fewer than 2 events, if any `window_sizes`
|
|
585
|
+
entry is not positive, or if fewer than 2 radii end up with
|
|
586
|
+
enough valid reference points and a nonzero count to fit a slope
|
|
587
|
+
at all.
|
|
588
|
+
|
|
589
|
+
Examples
|
|
590
|
+
--------
|
|
591
|
+
>>> import numpy as np
|
|
592
|
+
>>> rng = np.random.default_rng(0)
|
|
593
|
+
>>> poisson_events = np.sort(rng.uniform(0, 1000, 2000))
|
|
594
|
+
>>> radii = np.logspace(0, 2, 10) # 1 to 100
|
|
595
|
+
>>> D, r2, _ = counts_in_intervals_dimension(poisson_events, radii)
|
|
596
|
+
>>> 0.9 < D < 1.1 and r2 > 0.99
|
|
597
|
+
True
|
|
598
|
+
"""
|
|
599
|
+
event_times = np.asarray(event_times, dtype=float)
|
|
600
|
+
if event_times.size < 2:
|
|
601
|
+
raise ValueError(f"need at least 2 events, got {event_times.size}")
|
|
602
|
+
event_times = np.sort(event_times)
|
|
603
|
+
t_min, t_max = event_times[0], event_times[-1]
|
|
604
|
+
|
|
605
|
+
mean_counts = {}
|
|
606
|
+
for r in window_sizes:
|
|
607
|
+
if r <= 0:
|
|
608
|
+
raise ValueError(f"window_sizes must be positive, got {r}")
|
|
609
|
+
valid_refs = event_times[(event_times - r >= t_min) & (event_times + r <= t_max)]
|
|
610
|
+
if valid_refs.size < min_reference_points:
|
|
611
|
+
continue
|
|
612
|
+
# event_times is sorted (see above), so "count within r of t" is a
|
|
613
|
+
# pair of binary searches instead of an O(n) scan per reference
|
|
614
|
+
# point (prog.txt point 5c) -- both bounds inclusive, matching the
|
|
615
|
+
# brute-force `abs(event_times - t) <= r` this replaces exactly
|
|
616
|
+
# (verified: side='left'/'right' at t-r/t+r reproduces it for
|
|
617
|
+
# every t, including ties exactly on the r boundary).
|
|
618
|
+
lo = np.searchsorted(event_times, valid_refs - r, side='left')
|
|
619
|
+
hi = np.searchsorted(event_times, valid_refs + r, side='right')
|
|
620
|
+
counts = (hi - lo) - 1
|
|
621
|
+
mean_counts[float(r)] = float(np.mean(counts))
|
|
622
|
+
|
|
623
|
+
valid_items = sorted((r, c) for r, c in mean_counts.items() if c > 0)
|
|
624
|
+
if len(valid_items) < 2:
|
|
625
|
+
raise ValueError(
|
|
626
|
+
f"only {len(valid_items)} of {len(window_sizes)} window_sizes had at least "
|
|
627
|
+
f"{min_reference_points} edge-safe reference points with a nonzero count -- "
|
|
628
|
+
f"widen window_sizes, supply more events, or lower min_reference_points"
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
r_vals = np.array([r for r, _ in valid_items])
|
|
632
|
+
n_vals = np.array([c for _, c in valid_items])
|
|
633
|
+
log_r, log_n = np.log(r_vals), np.log(n_vals)
|
|
634
|
+
design = np.vstack([log_r, np.ones_like(log_r)]).T
|
|
635
|
+
slope, intercept = np.linalg.lstsq(design, log_n, rcond=None)[0]
|
|
636
|
+
predicted = slope * log_r + intercept
|
|
637
|
+
ss_res = np.sum((log_n - predicted) ** 2)
|
|
638
|
+
ss_tot = np.sum((log_n - np.mean(log_n)) ** 2)
|
|
639
|
+
r_squared = 1.0 - ss_res / ss_tot if ss_tot > 0 else 1.0
|
|
640
|
+
|
|
641
|
+
return float(slope), float(r_squared), dict(valid_items)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def decode_with_erasure_fallback(
|
|
645
|
+
observed_syndrome: Sequence[int],
|
|
646
|
+
heralded_qubits: Sequence[int],
|
|
647
|
+
n_qubits: int,
|
|
648
|
+
stabilizers: Sequence[str],
|
|
649
|
+
max_weight: Optional[int] = None,
|
|
650
|
+
) -> Optional[str]:
|
|
651
|
+
"""The real-world decoding POLICY, not just a raw decoder call: use
|
|
652
|
+
`erasure_aware_decode` when there are any heralded qubits and it
|
|
653
|
+
resolves the syndrome uniquely; fall back to `blind_minimum_weight_decode`
|
|
654
|
+
otherwise (zero heralded qubits, or erasure-aware decoding can't
|
|
655
|
+
resolve it -- ambiguous, or more heralds than the code's real
|
|
656
|
+
erasure-correcting capacity).
|
|
657
|
+
|
|
658
|
+
Promoted from Dense-Evolution-Discovery's cosmic-ray-burst-as-erasure
|
|
659
|
+
experiment (scripts/cosmic_ray_erasure_decoding.py), where this exact
|
|
660
|
+
fallback logic was first written inline in a Monte Carlo loop. Never
|
|
661
|
+
worse than always calling `blind_minimum_weight_decode` directly --
|
|
662
|
+
it only uses herald information when doing so actually helps, exactly
|
|
663
|
+
the policy a real erasure-aware QEC system would run, since a
|
|
664
|
+
real-time detector (e.g. a cosmic-ray/particle-impact monitor, or a
|
|
665
|
+
photon-loss herald) only ever adds information, it doesn't obligate a
|
|
666
|
+
decoder to use it past the point where it stops being useful.
|
|
667
|
+
|
|
668
|
+
Parameters
|
|
669
|
+
----------
|
|
670
|
+
observed_syndrome : sequence of int
|
|
671
|
+
One bit per stabilizer, same convention as `compute_syndrome`.
|
|
672
|
+
heralded_qubits : sequence of int
|
|
673
|
+
Qubits KNOWN to have been erased/disturbed this shot -- may be
|
|
674
|
+
empty (falls straight through to blind decoding).
|
|
675
|
+
n_qubits : int
|
|
676
|
+
Number of physical qubits.
|
|
677
|
+
stabilizers : sequence of str
|
|
678
|
+
The code's stabilizer generators (any mix of Pauli types).
|
|
679
|
+
max_weight : int, optional
|
|
680
|
+
Forwarded to `blind_minimum_weight_decode`'s fallback path only
|
|
681
|
+
(see its own docstring) -- `erasure_aware_decode` has no
|
|
682
|
+
equivalent parameter, its search is always exactly over the
|
|
683
|
+
heralded qubits.
|
|
684
|
+
|
|
685
|
+
Returns
|
|
686
|
+
-------
|
|
687
|
+
str or None
|
|
688
|
+
Length-`n_qubits` Pauli string (IXYZ), or `None` if neither
|
|
689
|
+
decoder can resolve the syndrome.
|
|
690
|
+
"""
|
|
691
|
+
if heralded_qubits:
|
|
692
|
+
result = erasure_aware_decode(observed_syndrome, heralded_qubits, n_qubits, stabilizers)
|
|
693
|
+
if result is not None:
|
|
694
|
+
return result
|
|
695
|
+
return blind_minimum_weight_decode(observed_syndrome, n_qubits, stabilizers, max_weight=max_weight)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def nearest_coset_decode(measured_bits: str, coset_a: Sequence[str], coset_b: Sequence[str]) -> int:
|
|
699
|
+
"""Nearest-coset (minimum Hamming distance) binary decoding: given a
|
|
700
|
+
measured bit string, decide which of two disjoint cosets of bit
|
|
701
|
+
strings it is closer to. Returns 0 if `measured_bits` is closer to
|
|
702
|
+
`coset_a`, 1 if closer to `coset_b` (ties broken toward `coset_a`).
|
|
703
|
+
|
|
704
|
+
This decodes a LOGICAL READOUT, not a syndrome -- distinct from
|
|
705
|
+
every other decoder in this module (`pymatching_decode`,
|
|
706
|
+
`blind_minimum_weight_decode`, `erasure_aware_decode`,
|
|
707
|
+
`decode_with_erasure_fallback`), which all turn a syndrome into an
|
|
708
|
+
error correction. For a CSS code whose logical |0>_L is proportional
|
|
709
|
+
to a superposition over one coset C of a classical linear code, and
|
|
710
|
+
|1>_L over the complementary coset C+1...1, a physical measurement in
|
|
711
|
+
the logical basis gives a bit string that exactly matches one
|
|
712
|
+
codeword in the noiseless case, and the NEAREST one under noise --
|
|
713
|
+
exactly the decoding rule Huang, Zhu, Ippoliti, Monroe & Gullans
|
|
714
|
+
(arXiv:2608.20676, "Continuous-angle logical rotations in the Steane
|
|
715
|
+
code") use for their real Steane-code logical Ramsey experiment.
|
|
716
|
+
|
|
717
|
+
Promoted from Dense-Evolution-Discovery's real reproduction of that
|
|
718
|
+
protocol (scripts/steane_continuous_logical_rotation.py) -- there,
|
|
719
|
+
`coset_a`/`coset_b` were the real 8-codeword-each cosets read
|
|
720
|
+
directly off this library's own Steane |0>_L / |1>_L statevectors
|
|
721
|
+
(not assumed from the paper's text), and this decoding rule matched
|
|
722
|
+
the paper's own theoretical logical-rotation model to within Monte
|
|
723
|
+
Carlo statistical noise across 6000 real circuit trials (7 data
|
|
724
|
+
qubits + 3 syndrome-extraction ancillas, real stochastic dephasing,
|
|
725
|
+
real projective measurement collapse).
|
|
726
|
+
|
|
727
|
+
Parameters
|
|
728
|
+
----------
|
|
729
|
+
measured_bits : str
|
|
730
|
+
The measured bit string, e.g. '0101101'.
|
|
731
|
+
coset_a, coset_b : sequence of str
|
|
732
|
+
Two disjoint sets of equal-length bit strings.
|
|
733
|
+
|
|
734
|
+
Returns
|
|
735
|
+
-------
|
|
736
|
+
int
|
|
737
|
+
0 or 1, indicating which coset `measured_bits` is nearest to.
|
|
738
|
+
|
|
739
|
+
Examples
|
|
740
|
+
--------
|
|
741
|
+
>>> from dense_evolution.qec import nearest_coset_decode
|
|
742
|
+
>>> coset_a = ['0000000', '1111000']
|
|
743
|
+
>>> coset_b = ['1111111', '0000111']
|
|
744
|
+
>>> nearest_coset_decode('0000000', coset_a, coset_b)
|
|
745
|
+
0
|
|
746
|
+
>>> nearest_coset_decode('1111110', coset_a, coset_b)
|
|
747
|
+
1
|
|
748
|
+
"""
|
|
749
|
+
def _min_hamming_distance(bits, coset):
|
|
750
|
+
x = int(bits, 2)
|
|
751
|
+
return min(bin(x ^ int(c, 2)).count('1') for c in coset)
|
|
752
|
+
|
|
753
|
+
d_a = _min_hamming_distance(measured_bits, coset_a)
|
|
754
|
+
d_b = _min_hamming_distance(measured_bits, coset_b)
|
|
755
|
+
return 0 if d_a <= d_b else 1
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def estimate_edge_probabilities_from_detection_events(check_matrix, events) -> np.ndarray:
|
|
759
|
+
"""Error probability of every qubit, from detection events (Spitz et al.).
|
|
760
|
+
|
|
761
|
+
Implements the exact inversion of S. T. Spitz, B. Tarasinski,
|
|
762
|
+
C. W. J. Beenakker and T. E. O'Brien, "Adaptive weight estimator for
|
|
763
|
+
quantum error correction in a time-dependent environment",
|
|
764
|
+
arXiv:1712.02360, Eqs. (13) and (16). For a code in which every qubit is
|
|
765
|
+
checked by at most two checks (repetition and surface codes), each qubit is
|
|
766
|
+
an edge between two checks, or between one check and the boundary. A qubit
|
|
767
|
+
shared by checks ``i`` and ``j`` has probability
|
|
768
|
+
|
|
769
|
+
``p = 1/2 - sqrt(1/4 - (<v_i v_j> - <v_i><v_j>) / (1 - 2 <v_i xor v_j>))``
|
|
770
|
+
|
|
771
|
+
where ``v`` are the detection events and ``<.>`` the average over cycles. A
|
|
772
|
+
qubit on the boundary of check ``i`` has
|
|
773
|
+
|
|
774
|
+
``p = 1/2 + (<v_i> - 1/2) / prod(1 - 2 p_ij)`` over the other qubits of ``i``.
|
|
775
|
+
|
|
776
|
+
The result can be passed as ``weights`` (``-log(p / (1 - p))``) to
|
|
777
|
+
`pymatching_decode`.
|
|
778
|
+
|
|
779
|
+
Parameters
|
|
780
|
+
----------
|
|
781
|
+
check_matrix : array_like of shape (n_checks, n_qubits)
|
|
782
|
+
0/1 matrix, entry ``[c, q] = 1`` when check ``c`` detects an error on
|
|
783
|
+
qubit ``q``. Every column has one or two ones; no two qubits may join
|
|
784
|
+
the same pair of checks.
|
|
785
|
+
events : array_like of shape (n_cycles, n_checks)
|
|
786
|
+
0/1 detection events: ``1`` when a check changed value since the
|
|
787
|
+
previous cycle (the syndrome of one cycle's new errors).
|
|
788
|
+
|
|
789
|
+
Returns
|
|
790
|
+
-------
|
|
791
|
+
numpy.ndarray of shape (n_qubits,)
|
|
792
|
+
Estimated error probability of each qubit, clipped to [0, 0.5].
|
|
793
|
+
|
|
794
|
+
Raises
|
|
795
|
+
------
|
|
796
|
+
ValueError
|
|
797
|
+
If the shapes do not match, ``events`` is not 0/1, a column of
|
|
798
|
+
``check_matrix`` does not have one or two ones, or two qubits join the
|
|
799
|
+
same pair of checks.
|
|
800
|
+
|
|
801
|
+
Notes
|
|
802
|
+
-----
|
|
803
|
+
Valid for independent errors and one error type at a time. Needs about
|
|
804
|
+
``1 / p`` cycles per qubit for a stable estimate (the paper's Eq. 18). A
|
|
805
|
+
pair of checks whose correlation is below the statistical noise gives a
|
|
806
|
+
probability near zero.
|
|
807
|
+
|
|
808
|
+
Examples
|
|
809
|
+
--------
|
|
810
|
+
>>> import numpy as np
|
|
811
|
+
>>> from dense_evolution.physics.qec import estimate_edge_probabilities_from_detection_events
|
|
812
|
+
>>> checks = np.array([[1, 0], [1, 1]])
|
|
813
|
+
>>> events = np.array([[1, 1]] * 20 + [[0, 0]] * 80)
|
|
814
|
+
>>> p = estimate_edge_probabilities_from_detection_events(checks, events)
|
|
815
|
+
>>> bool(p[0] < 0.5 and p[1] < 0.5)
|
|
816
|
+
True
|
|
817
|
+
"""
|
|
818
|
+
h = np.asarray(check_matrix, dtype=int)
|
|
819
|
+
v = np.asarray(events, dtype=float)
|
|
820
|
+
if h.ndim != 2:
|
|
821
|
+
raise ValueError("check_matrix must be 2-D (n_checks, n_qubits)")
|
|
822
|
+
if v.ndim != 2 or v.shape[1] != h.shape[0]:
|
|
823
|
+
raise ValueError(
|
|
824
|
+
f"events must have shape (n_cycles, {h.shape[0]}), got {v.shape}"
|
|
825
|
+
)
|
|
826
|
+
if v.shape[0] == 0:
|
|
827
|
+
raise ValueError("events needs at least one cycle")
|
|
828
|
+
if not np.isin(v, (0.0, 1.0)).all():
|
|
829
|
+
raise ValueError("events must contain only 0 and 1")
|
|
830
|
+
|
|
831
|
+
n_q = h.shape[1]
|
|
832
|
+
ends = []
|
|
833
|
+
for q in range(n_q):
|
|
834
|
+
rows = np.flatnonzero(h[:, q])
|
|
835
|
+
if len(rows) not in (1, 2):
|
|
836
|
+
raise ValueError(
|
|
837
|
+
f"qubit {q} is checked by {len(rows)} checks; need one or two"
|
|
838
|
+
)
|
|
839
|
+
ends.append(tuple(int(r) for r in rows))
|
|
840
|
+
pairs = [e for e in ends if len(e) == 2]
|
|
841
|
+
if len(set(pairs)) != len(pairs):
|
|
842
|
+
raise ValueError("two qubits join the same pair of checks")
|
|
843
|
+
|
|
844
|
+
mean = v.mean(axis=0)
|
|
845
|
+
p = np.zeros(n_q)
|
|
846
|
+
for q, e in enumerate(ends):
|
|
847
|
+
if len(e) == 2:
|
|
848
|
+
i, j = e
|
|
849
|
+
cov = (v[:, i] * v[:, j]).mean() - mean[i] * mean[j]
|
|
850
|
+
xor = np.abs(v[:, i] - v[:, j]).mean()
|
|
851
|
+
denom = 1.0 - 2.0 * xor
|
|
852
|
+
inside = 0.25 - cov / denom if denom > 0 else 0.25
|
|
853
|
+
p[q] = 0.5 - np.sqrt(max(inside, 0.0))
|
|
854
|
+
for q, e in enumerate(ends):
|
|
855
|
+
if len(e) == 1:
|
|
856
|
+
i = e[0]
|
|
857
|
+
prod = 1.0
|
|
858
|
+
for r, other in enumerate(ends):
|
|
859
|
+
if len(other) == 2 and i in other:
|
|
860
|
+
prod *= 1.0 - 2.0 * p[r]
|
|
861
|
+
if prod <= 0:
|
|
862
|
+
p[q] = 0.5
|
|
863
|
+
else:
|
|
864
|
+
p[q] = 0.5 + (mean[i] - 0.5) / prod
|
|
865
|
+
return np.clip(p, 0.0, 0.5)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def erasure_ml_decode(
|
|
869
|
+
observed_syndrome: tuple,
|
|
870
|
+
heralded_qubits: Sequence[int],
|
|
871
|
+
n_qubits: int,
|
|
872
|
+
stabilizers: Sequence[str],
|
|
873
|
+
) -> Optional[str]:
|
|
874
|
+
"""Maximum-likelihood decoder for erasures at known locations, for any
|
|
875
|
+
stabilizer code.
|
|
876
|
+
|
|
877
|
+
With the erased qubits known, the error is supported on them, and the
|
|
878
|
+
syndrome becomes a linear system over GF(2) in the X and Z bits of those
|
|
879
|
+
qubits (Delfosse & Zemor, arXiv:1703.01517; Kuo & Ouyang,
|
|
880
|
+
arXiv:2411.13509). Solving it by Gaussian elimination costs O(n^3), where
|
|
881
|
+
`erasure_aware_decode` tries 4**m assignments for m erased qubits. Any
|
|
882
|
+
solution is a valid correction when every zero-syndrome operator on the
|
|
883
|
+
erased qubits is a stabilizer element, so degenerate errors (several
|
|
884
|
+
errors that differ by a stabilizer) are decoded too, not rejected.
|
|
885
|
+
|
|
886
|
+
Parameters
|
|
887
|
+
----------
|
|
888
|
+
observed_syndrome : sequence of int
|
|
889
|
+
One bit per stabilizer, same convention as `compute_syndrome`.
|
|
890
|
+
heralded_qubits : sequence of int
|
|
891
|
+
Indices of the erased qubits.
|
|
892
|
+
n_qubits : int
|
|
893
|
+
Number of physical qubits.
|
|
894
|
+
stabilizers : sequence of str
|
|
895
|
+
Stabilizer generators as Pauli strings of length `n_qubits`.
|
|
896
|
+
|
|
897
|
+
Returns
|
|
898
|
+
-------
|
|
899
|
+
str or None
|
|
900
|
+
A Pauli string supported on the erased qubits that reproduces the
|
|
901
|
+
syndrome and is equivalent, up to a stabilizer, to every other
|
|
902
|
+
solution. `None` when no error on the erased qubits explains the
|
|
903
|
+
syndrome, or when the erased qubits contain a logical operator so the
|
|
904
|
+
correction is ambiguous. With no erased qubits it returns the identity
|
|
905
|
+
for a zero syndrome and `None` otherwise.
|
|
906
|
+
|
|
907
|
+
Raises
|
|
908
|
+
------
|
|
909
|
+
ValueError
|
|
910
|
+
If the syndrome length does not match the stabilizers, a stabilizer
|
|
911
|
+
has the wrong length, or an erased index is out of range.
|
|
912
|
+
|
|
913
|
+
Examples
|
|
914
|
+
--------
|
|
915
|
+
>>> stabs = ['IIIXXXX', 'IXXIIXX', 'XIXIXIX', 'IIIZZZZ', 'IZZIIZZ', 'ZIZIZIZ']
|
|
916
|
+
>>> syndrome = compute_syndrome('XIIIIIX', stabs)
|
|
917
|
+
>>> erasure_ml_decode(syndrome, [0, 6], 7, stabs)
|
|
918
|
+
'XIIIIIX'
|
|
919
|
+
"""
|
|
920
|
+
stabs = list(stabilizers)
|
|
921
|
+
if len(observed_syndrome) != len(stabs):
|
|
922
|
+
raise ValueError(
|
|
923
|
+
f"observed_syndrome has {len(observed_syndrome)} entries but there are "
|
|
924
|
+
f"{len(stabs)} stabilizers"
|
|
925
|
+
)
|
|
926
|
+
for i, s in enumerate(stabs):
|
|
927
|
+
if len(s) != n_qubits:
|
|
928
|
+
raise ValueError(f"stabilizers[{i}] has length {len(s)}, expected n_qubits={n_qubits}")
|
|
929
|
+
erased = sorted({int(q) for q in heralded_qubits})
|
|
930
|
+
if any(q < 0 or q >= n_qubits for q in erased):
|
|
931
|
+
raise ValueError(f"heralded_qubits must be in range(0, {n_qubits})")
|
|
932
|
+
|
|
933
|
+
syn = np.array(observed_syndrome, dtype=np.uint8) % 2
|
|
934
|
+
k = len(erased)
|
|
935
|
+
if k == 0:
|
|
936
|
+
return 'I' * n_qubits if not syn.any() else None
|
|
937
|
+
|
|
938
|
+
sym = np.array([_pauli_to_symplectic(s) for s in stabs], dtype=np.uint8)
|
|
939
|
+
sx, sz = sym[:, :n_qubits], sym[:, n_qubits:]
|
|
940
|
+
a = np.concatenate([sz[:, erased], sx[:, erased]], axis=1)
|
|
941
|
+
rref, pivots = _gf2_rref(np.concatenate([a, syn[:, None]], axis=1))
|
|
942
|
+
if 2 * k in pivots:
|
|
943
|
+
return None
|
|
944
|
+
|
|
945
|
+
sol = np.zeros(2 * k, dtype=np.uint8)
|
|
946
|
+
for row, col in enumerate(pivots):
|
|
947
|
+
sol[col] = rref[row, 2 * k]
|
|
948
|
+
free = [c for c in range(2 * k) if c not in pivots]
|
|
949
|
+
stab_rref, stab_pivots = _gf2_rref(sym)
|
|
950
|
+
|
|
951
|
+
def embed(v):
|
|
952
|
+
full = np.zeros(2 * n_qubits, dtype=np.uint8)
|
|
953
|
+
for j, q in enumerate(erased):
|
|
954
|
+
full[q] = v[j]
|
|
955
|
+
full[n_qubits + q] = v[k + j]
|
|
956
|
+
return full
|
|
957
|
+
|
|
958
|
+
for f in free:
|
|
959
|
+
vec = np.zeros(2 * k, dtype=np.uint8)
|
|
960
|
+
vec[f] = 1
|
|
961
|
+
for row, col in enumerate(pivots):
|
|
962
|
+
if rref[row, f]:
|
|
963
|
+
vec[col] = 1
|
|
964
|
+
if not _in_gf2_span(embed(vec), stab_rref, stab_pivots):
|
|
965
|
+
return None
|
|
966
|
+
|
|
967
|
+
full = embed(sol)
|
|
968
|
+
letters = {(0, 0): 'I', (1, 0): 'X', (0, 1): 'Z', (1, 1): 'Y'}
|
|
969
|
+
return ''.join(letters[(int(full[q]), int(full[n_qubits + q]))] for q in range(n_qubits))
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def _peel(h, erased, syn):
|
|
973
|
+
m = h.shape[0]
|
|
974
|
+
adj = {}
|
|
975
|
+
for q in erased:
|
|
976
|
+
rows = np.flatnonzero(h[:, q])
|
|
977
|
+
u, v = (int(rows[0]), int(rows[1])) if len(rows) == 2 else (int(rows[0]), m)
|
|
978
|
+
adj.setdefault(u, []).append((v, q))
|
|
979
|
+
adj.setdefault(v, []).append((u, q))
|
|
980
|
+
syn = [int(b) for b in syn] + [0]
|
|
981
|
+
seen, order, parent = set(), [], {}
|
|
982
|
+
starts = ([m] if m in adj else []) + [v for v in adj if v != m]
|
|
983
|
+
for s in starts:
|
|
984
|
+
if s in seen:
|
|
985
|
+
continue
|
|
986
|
+
seen.add(s)
|
|
987
|
+
queue = deque([s])
|
|
988
|
+
while queue:
|
|
989
|
+
u = queue.popleft()
|
|
990
|
+
order.append(u)
|
|
991
|
+
for v, q in adj[u]:
|
|
992
|
+
if v not in seen:
|
|
993
|
+
seen.add(v)
|
|
994
|
+
parent[v] = (u, q)
|
|
995
|
+
queue.append(v)
|
|
996
|
+
corr = set()
|
|
997
|
+
for u in reversed(order):
|
|
998
|
+
if u in parent:
|
|
999
|
+
p, q = parent[u]
|
|
1000
|
+
if syn[u]:
|
|
1001
|
+
corr.add(q)
|
|
1002
|
+
syn[p] ^= 1
|
|
1003
|
+
syn[u] = 0
|
|
1004
|
+
elif u != m and syn[u]:
|
|
1005
|
+
return None
|
|
1006
|
+
if any(syn[v] for v in range(m) if v not in seen):
|
|
1007
|
+
return None
|
|
1008
|
+
return corr
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def peeling_decode(stabilizers, observed_syndrome, heralded_qubits, n_qubits) -> Optional[str]:
|
|
1012
|
+
"""Peeling decoder for a CSS code whose X-type and Z-type checks each join
|
|
1013
|
+
every qubit to at most two checks (repetition and surface codes)."""
|
|
1014
|
+
stabs = list(stabilizers)
|
|
1015
|
+
syn = np.array(observed_syndrome, dtype=np.uint8)
|
|
1016
|
+
erased = sorted({int(q) for q in heralded_qubits})
|
|
1017
|
+
zi = [i for i, s in enumerate(stabs) if 'Z' in s and 'X' not in s]
|
|
1018
|
+
xi = [i for i, s in enumerate(stabs) if 'X' in s and 'Z' not in s]
|
|
1019
|
+
hz = np.array([[c != 'I' for c in stabs[i]] for i in zi], dtype=np.uint8)
|
|
1020
|
+
hx = np.array([[c != 'I' for c in stabs[i]] for i in xi], dtype=np.uint8)
|
|
1021
|
+
x_part = _peel(hz, erased, syn[zi])
|
|
1022
|
+
z_part = _peel(hx, erased, syn[xi])
|
|
1023
|
+
if x_part is None or z_part is None:
|
|
1024
|
+
return None
|
|
1025
|
+
return ''.join('IXZY'[(q in x_part) + 2 * (q in z_part)] for q in range(n_qubits))
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def _uf_grow(h, erased, syn):
|
|
1029
|
+
m, n = h.shape
|
|
1030
|
+
ends = []
|
|
1031
|
+
for q in range(n):
|
|
1032
|
+
rows = np.flatnonzero(h[:, q])
|
|
1033
|
+
ends.append((int(rows[0]), int(rows[1])) if len(rows) == 2 else (int(rows[0]), m))
|
|
1034
|
+
parent = list(range(m + 1))
|
|
1035
|
+
|
|
1036
|
+
def find(x):
|
|
1037
|
+
while parent[x] != x:
|
|
1038
|
+
parent[x] = parent[parent[x]]
|
|
1039
|
+
x = parent[x]
|
|
1040
|
+
return x
|
|
1041
|
+
|
|
1042
|
+
support = [0] * n
|
|
1043
|
+
for q in erased:
|
|
1044
|
+
support[q] = 2
|
|
1045
|
+
parent[find(ends[q][0])] = find(ends[q][1])
|
|
1046
|
+
s = [int(b) for b in syn] + [0]
|
|
1047
|
+
|
|
1048
|
+
def odd_roots():
|
|
1049
|
+
par = {}
|
|
1050
|
+
for v in range(m + 1):
|
|
1051
|
+
r = find(v)
|
|
1052
|
+
par[r] = par.get(r, 0) ^ s[v]
|
|
1053
|
+
return {r for r, p in par.items() if p and r != find(m)}
|
|
1054
|
+
|
|
1055
|
+
odd = odd_roots()
|
|
1056
|
+
while odd:
|
|
1057
|
+
grown = False
|
|
1058
|
+
for q in range(n):
|
|
1059
|
+
if support[q] < 2:
|
|
1060
|
+
ru, rv = find(ends[q][0]), find(ends[q][1])
|
|
1061
|
+
inc = (ru in odd) + (rv in odd and rv != ru)
|
|
1062
|
+
if inc:
|
|
1063
|
+
support[q] = min(2, support[q] + inc)
|
|
1064
|
+
grown = True
|
|
1065
|
+
if not grown:
|
|
1066
|
+
return None
|
|
1067
|
+
for q in range(n):
|
|
1068
|
+
if support[q] == 2:
|
|
1069
|
+
parent[find(ends[q][0])] = find(ends[q][1])
|
|
1070
|
+
odd = odd_roots()
|
|
1071
|
+
return [q for q in range(n) if support[q] == 2]
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def _css_split(stabilizers, observed_syndrome):
|
|
1075
|
+
stabs = list(stabilizers)
|
|
1076
|
+
syn = np.array(observed_syndrome, dtype=np.uint8)
|
|
1077
|
+
zi = [i for i, s in enumerate(stabs) if 'Z' in s and 'X' not in s]
|
|
1078
|
+
xi = [i for i, s in enumerate(stabs) if 'X' in s and 'Z' not in s]
|
|
1079
|
+
hz = np.array([[c != 'I' for c in stabs[i]] for i in zi], dtype=np.uint8)
|
|
1080
|
+
hx = np.array([[c != 'I' for c in stabs[i]] for i in xi], dtype=np.uint8)
|
|
1081
|
+
return hz, syn[zi], hx, syn[xi]
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def union_find_decode(stabilizers, observed_syndrome, heralded_qubits, n_qubits) -> Optional[str]:
|
|
1085
|
+
"""Union-Find decoder with erasures and Pauli errors (Delfosse and
|
|
1086
|
+
Nickerson, arXiv:1709.06218): clusters start from the erased qubits and
|
|
1087
|
+
grow by half-edges until every cluster has even syndrome parity or touches
|
|
1088
|
+
the boundary, then each grown cluster is peeled."""
|
|
1089
|
+
hz, sz, hx, sx = _css_split(stabilizers, observed_syndrome)
|
|
1090
|
+
erased = sorted({int(q) for q in heralded_qubits})
|
|
1091
|
+
parts = []
|
|
1092
|
+
for h, s in ((hz, sz), (hx, sx)):
|
|
1093
|
+
grown = _uf_grow(h, erased, s)
|
|
1094
|
+
if grown is None:
|
|
1095
|
+
return None
|
|
1096
|
+
part = _peel(h, grown, s)
|
|
1097
|
+
if part is None:
|
|
1098
|
+
return None
|
|
1099
|
+
parts.append(part)
|
|
1100
|
+
return ''.join('IXZY'[(q in parts[0]) + 2 * (q in parts[1])] for q in range(n_qubits))
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def matching_erasure_decode(stabilizers, observed_syndrome, heralded_qubits, n_qubits) -> str:
|
|
1104
|
+
"""Minimum-weight perfect matching with weight 0 on the erased qubits
|
|
1105
|
+
(Stace, Barrett and Doherty, arXiv:0904.3556), via pymatching."""
|
|
1106
|
+
import pymatching
|
|
1107
|
+
|
|
1108
|
+
hz, sz, hx, sx = _css_split(stabilizers, observed_syndrome)
|
|
1109
|
+
w = np.ones(n_qubits)
|
|
1110
|
+
w[list(heralded_qubits)] = 0.0
|
|
1111
|
+
xp = pymatching.Matching.from_check_matrix(hz, weights=w).decode(sz)
|
|
1112
|
+
zp = pymatching.Matching.from_check_matrix(hx, weights=w).decode(sx)
|
|
1113
|
+
return ''.join('IXZY'[int(xp[q]) + 2 * int(zp[q])] for q in range(n_qubits))
|