demsym 0.1.0__tar.gz
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.
- demsym-0.1.0/LICENSE +21 -0
- demsym-0.1.0/PKG-INFO +161 -0
- demsym-0.1.0/README.md +137 -0
- demsym-0.1.0/pyproject.toml +34 -0
- demsym-0.1.0/src/demsym/__init__.py +32 -0
- demsym-0.1.0/src/demsym/dem.py +78 -0
- demsym-0.1.0/src/demsym/element.py +107 -0
- demsym-0.1.0/src/demsym/gf2.py +75 -0
- demsym-0.1.0/src/demsym/nn.py +125 -0
- demsym-0.1.0/src/demsym/perms.py +45 -0
- demsym-0.1.0/src/demsym/solve.py +130 -0
- demsym-0.1.0/tests/test_compose_close.py +68 -0
- demsym-0.1.0/tests/test_gf2.py +48 -0
- demsym-0.1.0/tests/test_repetition.py +63 -0
- demsym-0.1.0/tests/test_torch_wrapper.py +59 -0
- demsym-0.1.0/tests/test_toric_end.py +46 -0
- demsym-0.1.0/tests/toric_cc.py +104 -0
demsym-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Olliver
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
demsym-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: demsym
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Exact twisted symmetries of stim detector error models, with PyTorch equivariant decoder wrappers
|
|
5
|
+
Project-URL: Homepage, https://github.com/dwatces/equivariant-quantum-ml
|
|
6
|
+
Author-email: Daniel Olliver <dwatces@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: detector-error-model,equivariance,floquet-code,neural-decoder,quantum-error-correction,stim,symmetry
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: numpy>=1.24
|
|
16
|
+
Requires-Dist: stim>=1.12
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pymatching; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
20
|
+
Requires-Dist: torch>=2.0; extra == 'dev'
|
|
21
|
+
Provides-Extra: torch
|
|
22
|
+
Requires-Dist: torch>=2.0; extra == 'torch'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# demsym
|
|
26
|
+
|
|
27
|
+
**Exact twisted symmetries of stim detector error models — solved, verified,
|
|
28
|
+
and wrapped into PyTorch equivariant decoders.**
|
|
29
|
+
|
|
30
|
+
A symmetry of a quantum error-correcting experiment is not just a permutation
|
|
31
|
+
of detectors. It acts on the *labels* too: logical observables pick up
|
|
32
|
+
syndrome-dependent corrections (the logical representative moves), and on some
|
|
33
|
+
codes the symmetry *mixes the logical qubits*. `demsym` works with the full
|
|
34
|
+
twisted action
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
syndrome: x -> x_g (x_g[perm[d]] = x[d])
|
|
38
|
+
label: c -> A c ⊕ X·x_g
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
with `A` invertible over GF(2) and `X` a per-observable syndrome-linear
|
|
42
|
+
correction. Given any stim circuit and a candidate detector permutation, it
|
|
43
|
+
**solves (A, X) directly from the detector error model** and verifies the
|
|
44
|
+
result exactly (multiset equality of all error mechanisms — no sampling, no
|
|
45
|
+
tolerance). A `None` is a real answer: the permutation is *not* a symmetry of
|
|
46
|
+
that DEM.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
pip install demsym # solver only (numpy + stim)
|
|
52
|
+
pip install 'demsym[torch]' # + the PyTorch equivariant wrapper
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Sixty seconds: the whole story on 20 detectors
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import stim, demsym
|
|
59
|
+
|
|
60
|
+
c = stim.Circuit.generated(
|
|
61
|
+
"repetition_code:memory", distance=5, rounds=4,
|
|
62
|
+
before_round_data_depolarization=0.04,
|
|
63
|
+
before_measure_flip_probability=0.01)
|
|
64
|
+
|
|
65
|
+
dem = demsym.Dem.from_circuit(c)
|
|
66
|
+
perm = demsym.perm_from_coords(c, lambda co: (8.0 - co[0],) + co[1:])
|
|
67
|
+
|
|
68
|
+
elem = demsym.solve_action(dem, perm, name="mirror")
|
|
69
|
+
print(elem) # Element('mirror', nd=20, A=I, |chi|=20)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Three things just happened:
|
|
73
|
+
|
|
74
|
+
1. **The mirror verifies** — it is an exact symmetry of the circuit-level DEM.
|
|
75
|
+
2. **The twist is real**: `|chi| != 0`. The logical observable is the last
|
|
76
|
+
data qubit; its mirror image is the first; the difference is a chain of
|
|
77
|
+
checks — a syndrome-linear label correction. Drop it
|
|
78
|
+
(`X = 0`) and verification fails. Naive "invariant" models mis-tie exactly
|
|
79
|
+
here.
|
|
80
|
+
3. Change the noise to `after_clifford_depolarization=0.01` and
|
|
81
|
+
`solve_action` returns `None` — **circuit-level extraction noise breaks
|
|
82
|
+
the spatial symmetry**. That is not a bug; it is a structural fact about
|
|
83
|
+
schedules, and finding it is the point of the tool.
|
|
84
|
+
|
|
85
|
+
Build the group and an exactly equivariant model:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
group = demsym.close([elem], dem) # verified closure, identity included
|
|
89
|
+
model = demsym.nn.TwistedEquivariant(
|
|
90
|
+
demsym.nn.mlp_backbone(dem.num_detectors, dem.num_observables), group)
|
|
91
|
+
# model(x): (batch, nd) -> (batch, 2^k) logits over joint observable classes
|
|
92
|
+
demsym.nn.equivariance_probe(model, group, sampled_shots) # ~1e-6
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## What this machinery found (the reason it exists)
|
|
96
|
+
|
|
97
|
+
Applied to memory experiments at circuit level (all numbers from logged runs;
|
|
98
|
+
preprint in preparation):
|
|
99
|
+
|
|
100
|
+
- **Surface code: obstruction.** No detector permutation implementing the
|
|
101
|
+
code's spatial symmetries verifies on circuit-level DEMs across 999
|
|
102
|
+
coordinate-map candidates and 1,728 extraction schedules (including
|
|
103
|
+
co-designed ones). Mechanism: hook errors de-symmetrize the schedule; a
|
|
104
|
+
mirror-axis ancilla cannot equal its own east-west swap.
|
|
105
|
+
- **Honeycomb Floquet code: exactness.** With completely naive uniform
|
|
106
|
+
extraction, the circuit-level DEM of a terminated Hastings–Haah honeycomb
|
|
107
|
+
memory carries an **exact p3 space group** (translations + C3, verified on
|
|
108
|
+
all 4,584 mechanism groups at L=6). Trivalence does the work: weight-2
|
|
109
|
+
checks make 1-bit schedules automatically covariant.
|
|
110
|
+
- **C3 mixes the logical qubits**: `A = [[1,1],[1,0]]`, order 3 in GL(2,F2) —
|
|
111
|
+
solved blind from the DEM. The correct decoder is therefore a 4-class
|
|
112
|
+
twisted-equivariant model; scalar per-observable equivariance cannot
|
|
113
|
+
express this.
|
|
114
|
+
- **Equivariance as a learnability switch.** Matched 63k-parameter MLPs on
|
|
115
|
+
the honeycomb task (p=0.002, MWPM joint accuracy 0.947, majority 0.291):
|
|
116
|
+
|
|
117
|
+
| train samples | plain | 9-element twisted tying |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| 250 | 0.258 | **0.363** |
|
|
120
|
+
| 1,000 | 0.267 | **0.404** |
|
|
121
|
+
| 16,000 | 0.274 | **0.600** |
|
|
122
|
+
| 64,000 | 0.290 | **0.744** |
|
|
123
|
+
| 256,000 | 0.391 | **0.806** |
|
|
124
|
+
|
|
125
|
+
The plain network at 64k samples — and at 22.5× the parameters (1.4M) — sits
|
|
126
|
+
at the majority baseline; the tied model at n=1,000 beats plain at
|
|
127
|
+
n=256,000 (≥256× sample efficiency). We claim **no** superiority over
|
|
128
|
+
matched-graph MWPM anywhere; the tied model remains ~14 pp below it on this
|
|
129
|
+
task.
|
|
130
|
+
|
|
131
|
+
## Validation
|
|
132
|
+
|
|
133
|
+
The solver is validated against the analytic symmetry action of
|
|
134
|
+
**Egorov, Bondesan & Welling, "The END: An Equivariant Neural Decoder"
|
|
135
|
+
(arXiv:2304.07362)** on their setting (toric code, code capacity): the blind
|
|
136
|
+
DEM solve recovers their action — including the 90°-rotation logical swap —
|
|
137
|
+
**uniquely among all 24 observable permutations**, for every element. That
|
|
138
|
+
bridge ships as a test (`tests/test_toric_end.py`).
|
|
139
|
+
|
|
140
|
+
## Scope and honest limits
|
|
141
|
+
|
|
142
|
+
- You supply candidate permutations (from detector coordinates via
|
|
143
|
+
`perm_from_coords`, or any construction you trust); demsym decides, exactly.
|
|
144
|
+
Automorphism *discovery* is out of scope for v0.1 — see AutDEC
|
|
145
|
+
(arXiv:2503.01738) for DEM-automorphism search via graph tools.
|
|
146
|
+
- `X` is gauge-fixed only modulo directions invisible to every mechanism:
|
|
147
|
+
equivariance is exact on **realizable** syndromes. Probe with sampled
|
|
148
|
+
shots, not random bit-vectors.
|
|
149
|
+
- Auto-enumeration of `A` candidates covers k ≤ 3 observables (GL(k,F2) =
|
|
150
|
+
1, 6, 168); beyond that, pass `A_candidates` (e.g. the observable
|
|
151
|
+
permutation matrices, as the toric validation does for k=4).
|
|
152
|
+
- The wrapper costs |G| backbone passes per forward: matched parameters, not
|
|
153
|
+
matched FLOPs.
|
|
154
|
+
- Twisted equivariant decoding itself is prior art (The END, above). What
|
|
155
|
+
demsym adds is the solve-from-DEM route — the one that still exists at
|
|
156
|
+
circuit level, where analytic lattice-path constructions don't apply —
|
|
157
|
+
plus the verified wrapper.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT. Author: Daniel Olliver (dwatces@gmail.com).
|
demsym-0.1.0/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# demsym
|
|
2
|
+
|
|
3
|
+
**Exact twisted symmetries of stim detector error models — solved, verified,
|
|
4
|
+
and wrapped into PyTorch equivariant decoders.**
|
|
5
|
+
|
|
6
|
+
A symmetry of a quantum error-correcting experiment is not just a permutation
|
|
7
|
+
of detectors. It acts on the *labels* too: logical observables pick up
|
|
8
|
+
syndrome-dependent corrections (the logical representative moves), and on some
|
|
9
|
+
codes the symmetry *mixes the logical qubits*. `demsym` works with the full
|
|
10
|
+
twisted action
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
syndrome: x -> x_g (x_g[perm[d]] = x[d])
|
|
14
|
+
label: c -> A c ⊕ X·x_g
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
with `A` invertible over GF(2) and `X` a per-observable syndrome-linear
|
|
18
|
+
correction. Given any stim circuit and a candidate detector permutation, it
|
|
19
|
+
**solves (A, X) directly from the detector error model** and verifies the
|
|
20
|
+
result exactly (multiset equality of all error mechanisms — no sampling, no
|
|
21
|
+
tolerance). A `None` is a real answer: the permutation is *not* a symmetry of
|
|
22
|
+
that DEM.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pip install demsym # solver only (numpy + stim)
|
|
28
|
+
pip install 'demsym[torch]' # + the PyTorch equivariant wrapper
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Sixty seconds: the whole story on 20 detectors
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import stim, demsym
|
|
35
|
+
|
|
36
|
+
c = stim.Circuit.generated(
|
|
37
|
+
"repetition_code:memory", distance=5, rounds=4,
|
|
38
|
+
before_round_data_depolarization=0.04,
|
|
39
|
+
before_measure_flip_probability=0.01)
|
|
40
|
+
|
|
41
|
+
dem = demsym.Dem.from_circuit(c)
|
|
42
|
+
perm = demsym.perm_from_coords(c, lambda co: (8.0 - co[0],) + co[1:])
|
|
43
|
+
|
|
44
|
+
elem = demsym.solve_action(dem, perm, name="mirror")
|
|
45
|
+
print(elem) # Element('mirror', nd=20, A=I, |chi|=20)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Three things just happened:
|
|
49
|
+
|
|
50
|
+
1. **The mirror verifies** — it is an exact symmetry of the circuit-level DEM.
|
|
51
|
+
2. **The twist is real**: `|chi| != 0`. The logical observable is the last
|
|
52
|
+
data qubit; its mirror image is the first; the difference is a chain of
|
|
53
|
+
checks — a syndrome-linear label correction. Drop it
|
|
54
|
+
(`X = 0`) and verification fails. Naive "invariant" models mis-tie exactly
|
|
55
|
+
here.
|
|
56
|
+
3. Change the noise to `after_clifford_depolarization=0.01` and
|
|
57
|
+
`solve_action` returns `None` — **circuit-level extraction noise breaks
|
|
58
|
+
the spatial symmetry**. That is not a bug; it is a structural fact about
|
|
59
|
+
schedules, and finding it is the point of the tool.
|
|
60
|
+
|
|
61
|
+
Build the group and an exactly equivariant model:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
group = demsym.close([elem], dem) # verified closure, identity included
|
|
65
|
+
model = demsym.nn.TwistedEquivariant(
|
|
66
|
+
demsym.nn.mlp_backbone(dem.num_detectors, dem.num_observables), group)
|
|
67
|
+
# model(x): (batch, nd) -> (batch, 2^k) logits over joint observable classes
|
|
68
|
+
demsym.nn.equivariance_probe(model, group, sampled_shots) # ~1e-6
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## What this machinery found (the reason it exists)
|
|
72
|
+
|
|
73
|
+
Applied to memory experiments at circuit level (all numbers from logged runs;
|
|
74
|
+
preprint in preparation):
|
|
75
|
+
|
|
76
|
+
- **Surface code: obstruction.** No detector permutation implementing the
|
|
77
|
+
code's spatial symmetries verifies on circuit-level DEMs across 999
|
|
78
|
+
coordinate-map candidates and 1,728 extraction schedules (including
|
|
79
|
+
co-designed ones). Mechanism: hook errors de-symmetrize the schedule; a
|
|
80
|
+
mirror-axis ancilla cannot equal its own east-west swap.
|
|
81
|
+
- **Honeycomb Floquet code: exactness.** With completely naive uniform
|
|
82
|
+
extraction, the circuit-level DEM of a terminated Hastings–Haah honeycomb
|
|
83
|
+
memory carries an **exact p3 space group** (translations + C3, verified on
|
|
84
|
+
all 4,584 mechanism groups at L=6). Trivalence does the work: weight-2
|
|
85
|
+
checks make 1-bit schedules automatically covariant.
|
|
86
|
+
- **C3 mixes the logical qubits**: `A = [[1,1],[1,0]]`, order 3 in GL(2,F2) —
|
|
87
|
+
solved blind from the DEM. The correct decoder is therefore a 4-class
|
|
88
|
+
twisted-equivariant model; scalar per-observable equivariance cannot
|
|
89
|
+
express this.
|
|
90
|
+
- **Equivariance as a learnability switch.** Matched 63k-parameter MLPs on
|
|
91
|
+
the honeycomb task (p=0.002, MWPM joint accuracy 0.947, majority 0.291):
|
|
92
|
+
|
|
93
|
+
| train samples | plain | 9-element twisted tying |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| 250 | 0.258 | **0.363** |
|
|
96
|
+
| 1,000 | 0.267 | **0.404** |
|
|
97
|
+
| 16,000 | 0.274 | **0.600** |
|
|
98
|
+
| 64,000 | 0.290 | **0.744** |
|
|
99
|
+
| 256,000 | 0.391 | **0.806** |
|
|
100
|
+
|
|
101
|
+
The plain network at 64k samples — and at 22.5× the parameters (1.4M) — sits
|
|
102
|
+
at the majority baseline; the tied model at n=1,000 beats plain at
|
|
103
|
+
n=256,000 (≥256× sample efficiency). We claim **no** superiority over
|
|
104
|
+
matched-graph MWPM anywhere; the tied model remains ~14 pp below it on this
|
|
105
|
+
task.
|
|
106
|
+
|
|
107
|
+
## Validation
|
|
108
|
+
|
|
109
|
+
The solver is validated against the analytic symmetry action of
|
|
110
|
+
**Egorov, Bondesan & Welling, "The END: An Equivariant Neural Decoder"
|
|
111
|
+
(arXiv:2304.07362)** on their setting (toric code, code capacity): the blind
|
|
112
|
+
DEM solve recovers their action — including the 90°-rotation logical swap —
|
|
113
|
+
**uniquely among all 24 observable permutations**, for every element. That
|
|
114
|
+
bridge ships as a test (`tests/test_toric_end.py`).
|
|
115
|
+
|
|
116
|
+
## Scope and honest limits
|
|
117
|
+
|
|
118
|
+
- You supply candidate permutations (from detector coordinates via
|
|
119
|
+
`perm_from_coords`, or any construction you trust); demsym decides, exactly.
|
|
120
|
+
Automorphism *discovery* is out of scope for v0.1 — see AutDEC
|
|
121
|
+
(arXiv:2503.01738) for DEM-automorphism search via graph tools.
|
|
122
|
+
- `X` is gauge-fixed only modulo directions invisible to every mechanism:
|
|
123
|
+
equivariance is exact on **realizable** syndromes. Probe with sampled
|
|
124
|
+
shots, not random bit-vectors.
|
|
125
|
+
- Auto-enumeration of `A` candidates covers k ≤ 3 observables (GL(k,F2) =
|
|
126
|
+
1, 6, 168); beyond that, pass `A_candidates` (e.g. the observable
|
|
127
|
+
permutation matrices, as the toric validation does for k=4).
|
|
128
|
+
- The wrapper costs |G| backbone passes per forward: matched parameters, not
|
|
129
|
+
matched FLOPs.
|
|
130
|
+
- Twisted equivariant decoding itself is prior art (The END, above). What
|
|
131
|
+
demsym adds is the solve-from-DEM route — the one that still exists at
|
|
132
|
+
circuit level, where analytic lattice-path constructions don't apply —
|
|
133
|
+
plus the verified wrapper.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT. Author: Daniel Olliver (dwatces@gmail.com).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "demsym"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Exact twisted symmetries of stim detector error models, with PyTorch equivariant decoder wrappers"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Daniel Olliver", email = "dwatces@gmail.com" }]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
dependencies = ["numpy>=1.24", "stim>=1.12"]
|
|
15
|
+
keywords = [
|
|
16
|
+
"quantum-error-correction", "stim", "detector-error-model",
|
|
17
|
+
"equivariance", "neural-decoder", "symmetry", "floquet-code",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
torch = ["torch>=2.0"]
|
|
28
|
+
dev = ["pytest", "torch>=2.0", "pymatching"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/dwatces/equivariant-quantum-ml"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/demsym"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""demsym — exact twisted symmetries of stim detector error models.
|
|
2
|
+
|
|
3
|
+
Given a stim circuit (or hand-built noise model): test candidate detector
|
|
4
|
+
permutations for exact twisted symmetry of the DEM, solving the label
|
|
5
|
+
action (A, X) over GF(2) — including actions that MIX logical observables
|
|
6
|
+
(A != I) and syndrome-dependent corrections (X != 0) — and wrap any
|
|
7
|
+
PyTorch backbone into an exactly equivariant decoder.
|
|
8
|
+
|
|
9
|
+
import demsym
|
|
10
|
+
dem = demsym.Dem.from_circuit(circuit)
|
|
11
|
+
perm = demsym.perm_from_coords(circuit, my_coord_map)
|
|
12
|
+
elem = demsym.solve_action(dem, perm, name="C3") # Element or None
|
|
13
|
+
group = demsym.close([elem], dem)
|
|
14
|
+
model = demsym.nn.TwistedEquivariant(backbone, group)
|
|
15
|
+
|
|
16
|
+
`None` from solve_action is a real answer: the permutation is not a
|
|
17
|
+
twisted symmetry of that DEM. See the README for what this machinery found
|
|
18
|
+
on the surface code (obstruction) vs the honeycomb Floquet code (exact
|
|
19
|
+
symmetry, with A mixing the logical qubits).
|
|
20
|
+
"""
|
|
21
|
+
from .dem import Dem
|
|
22
|
+
from .element import Element, apply_A, identity
|
|
23
|
+
from .gf2 import gl2_matrices, solve_gf2
|
|
24
|
+
from .perms import perm_from_coords
|
|
25
|
+
from .solve import close, solve_action
|
|
26
|
+
|
|
27
|
+
from . import nn # noqa: E402 (torch imported lazily inside)
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
__all__ = ["Dem", "Element", "apply_A", "identity", "gl2_matrices",
|
|
31
|
+
"solve_gf2", "perm_from_coords", "solve_action", "close", "nn",
|
|
32
|
+
"__version__"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Detector-error-model extraction and grouping.
|
|
2
|
+
|
|
3
|
+
The unit demsym works on is the multiset view of a DEM: error mechanisms
|
|
4
|
+
grouped by their detector set, each group holding the multiset of
|
|
5
|
+
(observable-mask, probability) pairs. Symmetry is then a statement about
|
|
6
|
+
this object — see `solve.solve_action`.
|
|
7
|
+
|
|
8
|
+
Grouping by detector set (rather than pairing individual mechanisms) is
|
|
9
|
+
load-bearing: stim merges error mechanisms that hit identical (detectors,
|
|
10
|
+
observables), so per-mechanism pairing across a candidate symmetry breaks on
|
|
11
|
+
merge collisions while the per-group multiset comparison does not.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Iterable
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Dem:
|
|
22
|
+
"""Grouped detector error model.
|
|
23
|
+
|
|
24
|
+
groups: detector-set -> sorted tuple of (obs_mask, prob) pairs, where
|
|
25
|
+
obs_mask packs observable indices as bits (bit i = observable i flipped).
|
|
26
|
+
"""
|
|
27
|
+
num_detectors: int
|
|
28
|
+
num_observables: int
|
|
29
|
+
groups: dict[frozenset[int], tuple[tuple[int, float], ...]] = \
|
|
30
|
+
field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def num_mechanisms(self) -> int:
|
|
34
|
+
return sum(len(g) for g in self.groups.values())
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_circuit(cls, circuit, prob_decimals: int = 9) -> "Dem":
|
|
38
|
+
"""Extract from a stim.Circuit (via its detector error model)."""
|
|
39
|
+
dem = circuit.detector_error_model(flatten_loops=True)
|
|
40
|
+
return cls.from_stim_dem(dem, prob_decimals=prob_decimals)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_stim_dem(cls, dem, prob_decimals: int = 9) -> "Dem":
|
|
44
|
+
"""Extract from a stim.DetectorErrorModel."""
|
|
45
|
+
mechs = []
|
|
46
|
+
off = 0
|
|
47
|
+
for inst in dem.flattened():
|
|
48
|
+
if inst.type == "shift_detectors":
|
|
49
|
+
t0 = inst.targets_copy()[0]
|
|
50
|
+
off += t0.val if hasattr(t0, "val") else int(t0)
|
|
51
|
+
elif inst.type == "error":
|
|
52
|
+
p = inst.args_copy()[0]
|
|
53
|
+
dets, ob = [], 0
|
|
54
|
+
for t in inst.targets_copy():
|
|
55
|
+
if t.is_relative_detector_id():
|
|
56
|
+
dets.append(t.val + off)
|
|
57
|
+
elif t.is_logical_observable_id():
|
|
58
|
+
ob ^= 1 << t.val
|
|
59
|
+
mechs.append((dets, ob, p))
|
|
60
|
+
return cls.from_mechanisms(mechs, dem.num_detectors,
|
|
61
|
+
dem.num_observables,
|
|
62
|
+
prob_decimals=prob_decimals)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_mechanisms(cls, mechanisms: Iterable[tuple], num_detectors: int,
|
|
66
|
+
num_observables: int,
|
|
67
|
+
prob_decimals: int = 9) -> "Dem":
|
|
68
|
+
"""Build from explicit (detector_ids, obs_mask, probability) triples.
|
|
69
|
+
|
|
70
|
+
Use this for code-capacity / hand-built noise models that never pass
|
|
71
|
+
through stim.
|
|
72
|
+
"""
|
|
73
|
+
by_v = defaultdict(list)
|
|
74
|
+
for dets, ob, p in mechanisms:
|
|
75
|
+
by_v[frozenset(int(d) for d in dets)].append(
|
|
76
|
+
(int(ob), round(float(p), prob_decimals)))
|
|
77
|
+
groups = {v: tuple(sorted(g)) for v, g in by_v.items()}
|
|
78
|
+
return cls(int(num_detectors), int(num_observables), groups)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Twisted symmetry elements of a detector error model.
|
|
2
|
+
|
|
3
|
+
An element acts on (syndrome, label) as
|
|
4
|
+
|
|
5
|
+
x -> x_g with x_g[perm[d]] = x[d]
|
|
6
|
+
c -> A c XOR X x_g
|
|
7
|
+
|
|
8
|
+
where c is the vector of logical-observable bits, A is invertible over
|
|
9
|
+
GF(2), and X (one row per observable) is a syndrome-linear correction.
|
|
10
|
+
The element is a symmetry when this map sends the DEM's mechanism multiset
|
|
11
|
+
to itself exactly (see `Element.verify`).
|
|
12
|
+
|
|
13
|
+
Gauge note: X is determined only modulo directions invisible to every
|
|
14
|
+
mechanism detector set, so two correct X's may differ as vectors while
|
|
15
|
+
acting identically on realizable syndromes. All checks here are therefore
|
|
16
|
+
multiset checks against the DEM, never X-equality.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from .dem import Dem
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def apply_A(Amat: np.ndarray, ob: int) -> int:
|
|
28
|
+
"""Apply a GF(2) matrix to a bit-packed observable mask."""
|
|
29
|
+
k = Amat.shape[0]
|
|
30
|
+
o = np.array([(ob >> i) & 1 for i in range(k)], dtype=np.int64)
|
|
31
|
+
o2 = Amat.dot(o) % 2
|
|
32
|
+
return int(sum((int(o2[i]) & 1) << i for i in range(k)))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Element:
|
|
37
|
+
"""A verified-or-candidate twisted symmetry element."""
|
|
38
|
+
name: str
|
|
39
|
+
perm: np.ndarray # (nd,) int64, detector d -> perm[d]
|
|
40
|
+
A: np.ndarray # (k, k) int64 over GF(2)
|
|
41
|
+
X: np.ndarray # (k, nd) int64 over GF(2)
|
|
42
|
+
|
|
43
|
+
def __post_init__(self):
|
|
44
|
+
self.perm = np.asarray(self.perm, dtype=np.int64)
|
|
45
|
+
self.A = np.asarray(self.A, dtype=np.int64) % 2
|
|
46
|
+
self.X = np.asarray(self.X, dtype=np.int64) % 2
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def num_detectors(self) -> int:
|
|
50
|
+
return len(self.perm)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def num_observables(self) -> int:
|
|
54
|
+
return self.A.shape[0]
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def is_identity_perm(self) -> bool:
|
|
58
|
+
return bool((self.perm == np.arange(len(self.perm))).all())
|
|
59
|
+
|
|
60
|
+
def chi_weights(self) -> list[int]:
|
|
61
|
+
return [int(r.sum()) for r in self.X]
|
|
62
|
+
|
|
63
|
+
def __matmul__(self, first: "Element") -> "Element":
|
|
64
|
+
"""self @ first = apply `first`, then `self` (function composition)."""
|
|
65
|
+
if len(first.perm) != len(self.perm):
|
|
66
|
+
raise ValueError("element sizes differ")
|
|
67
|
+
perm = self.perm[first.perm]
|
|
68
|
+
A = (self.A @ first.A) % 2
|
|
69
|
+
# X of `first` acts on the intermediate syndrome x_1; rewrite it on
|
|
70
|
+
# the final syndrome x_12 (x_1[e] = x_12[self.perm[e]]), then mix
|
|
71
|
+
# its rows through self.A and add self's own correction.
|
|
72
|
+
Y = np.zeros_like(first.X)
|
|
73
|
+
Y[:, self.perm] = first.X
|
|
74
|
+
X = ((self.A @ Y) + self.X) % 2
|
|
75
|
+
return Element(f"{self.name}.{first.name}", perm, A, X)
|
|
76
|
+
|
|
77
|
+
def verify(self, dem: Dem) -> bool:
|
|
78
|
+
"""Exact multiset check: does this element map the DEM to itself?"""
|
|
79
|
+
perm = self.perm
|
|
80
|
+
for v, group in dem.groups.items():
|
|
81
|
+
v2 = frozenset(int(perm[d]) for d in v)
|
|
82
|
+
tgt = dem.groups.get(v2)
|
|
83
|
+
if tgt is None:
|
|
84
|
+
return False
|
|
85
|
+
idx = [int(perm[d]) for d in v]
|
|
86
|
+
b = 0
|
|
87
|
+
for i in range(self.num_observables):
|
|
88
|
+
b |= int(self.X[i][idx].sum() % 2) << i
|
|
89
|
+
img = sorted((apply_A(self.A, o) ^ b, p) for o, p in group)
|
|
90
|
+
if tuple(img) != tgt:
|
|
91
|
+
return False
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
def __repr__(self):
|
|
95
|
+
a = "I" if (self.A == np.eye(self.num_observables,
|
|
96
|
+
dtype=np.int64)).all() \
|
|
97
|
+
else self.A.tolist()
|
|
98
|
+
return (f"Element({self.name!r}, nd={self.num_detectors}, "
|
|
99
|
+
f"A={a}, |chi|={self.chi_weights()})")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def identity(dem: Dem, name: str = "I") -> Element:
|
|
103
|
+
"""The identity element for a given DEM."""
|
|
104
|
+
k = dem.num_observables
|
|
105
|
+
return Element(name, np.arange(dem.num_detectors),
|
|
106
|
+
np.eye(k, dtype=np.int64),
|
|
107
|
+
np.zeros((k, dem.num_detectors), dtype=np.int64))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Dense GF(2) linear algebra, sized for detector-error-model problems."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def solve_gf2(A: np.ndarray, b: np.ndarray) -> np.ndarray | None:
|
|
8
|
+
"""Solve A x = b over GF(2). Returns one solution, or None if inconsistent.
|
|
9
|
+
|
|
10
|
+
A: (m, n) 0/1 matrix, b: (m,) 0/1 vector. Free variables are set to 0.
|
|
11
|
+
"""
|
|
12
|
+
A = (np.asarray(A).copy() % 2).astype(np.int8)
|
|
13
|
+
b = (np.asarray(b).copy() % 2).astype(np.int8)
|
|
14
|
+
m, n = A.shape
|
|
15
|
+
piv, r = [], 0
|
|
16
|
+
for col in range(n):
|
|
17
|
+
if r >= m:
|
|
18
|
+
break
|
|
19
|
+
rows = np.nonzero(A[r:, col])[0]
|
|
20
|
+
if len(rows) == 0:
|
|
21
|
+
continue
|
|
22
|
+
A[[r, r + rows[0]]] = A[[r + rows[0], r]]
|
|
23
|
+
b[[r, r + rows[0]]] = b[[r + rows[0], r]]
|
|
24
|
+
mask = A[:, col].astype(bool)
|
|
25
|
+
mask[r] = False
|
|
26
|
+
A[mask] ^= A[r]
|
|
27
|
+
b[mask] ^= b[r]
|
|
28
|
+
piv.append(col)
|
|
29
|
+
r += 1
|
|
30
|
+
if np.any(b[r:]):
|
|
31
|
+
return None
|
|
32
|
+
x = np.zeros(n, dtype=np.int64)
|
|
33
|
+
for k, col in enumerate(piv):
|
|
34
|
+
x[col] = b[k]
|
|
35
|
+
return x
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def gl2_matrices(k: int) -> list[np.ndarray]:
|
|
39
|
+
"""All invertible k x k matrices over GF(2). Sizes: 1, 6, 168 for k=1,2,3."""
|
|
40
|
+
if k > 3:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"enumerating GL({k},F2) ({_gl_order(k)} matrices) is not done "
|
|
43
|
+
"automatically; pass A_candidates explicitly (e.g. permutation "
|
|
44
|
+
"matrices, or the subgroup your lattice action can produce)")
|
|
45
|
+
out = []
|
|
46
|
+
for bits in range(1 << (k * k)):
|
|
47
|
+
M = np.array([[(bits >> (i * k + j)) & 1 for j in range(k)]
|
|
48
|
+
for i in range(k)], dtype=np.int64)
|
|
49
|
+
if _gf2_det(M):
|
|
50
|
+
out.append(M)
|
|
51
|
+
# identity first: it is the most common answer and exits the search early
|
|
52
|
+
out.sort(key=lambda M: 0 if (M == np.eye(k, dtype=np.int64)).all() else 1)
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _gf2_det(M: np.ndarray) -> int:
|
|
57
|
+
M = M.copy() % 2
|
|
58
|
+
n = M.shape[0]
|
|
59
|
+
for col in range(n):
|
|
60
|
+
rows = np.nonzero(M[col:, col])[0]
|
|
61
|
+
if len(rows) == 0:
|
|
62
|
+
return 0
|
|
63
|
+
r = col + rows[0]
|
|
64
|
+
M[[col, r]] = M[[r, col]]
|
|
65
|
+
mask = M[:, col].astype(bool)
|
|
66
|
+
mask[col] = False
|
|
67
|
+
M[mask] ^= M[col]
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _gl_order(k: int) -> int:
|
|
72
|
+
n = 1
|
|
73
|
+
for i in range(k):
|
|
74
|
+
n *= (1 << k) - (1 << i)
|
|
75
|
+
return n
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""PyTorch twisted-equivariant wrapper (requires the `torch` extra).
|
|
2
|
+
|
|
3
|
+
The model averages a backbone over the group with the label twist applied:
|
|
4
|
+
|
|
5
|
+
f(x)[c] = (1/|G|) sum_g backbone(x_g)[ A_g c XOR X_g x_g ]
|
|
6
|
+
|
|
7
|
+
where x_g[perm_g[d]] = x[d]. With elements verified against the DEM, f is
|
|
8
|
+
exactly equivariant on realizable syndromes (X is gauge-fixed only on
|
|
9
|
+
those — probe with sampled shots, not random bit vectors).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from .element import Element, apply_A
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _torch():
|
|
19
|
+
try:
|
|
20
|
+
import torch
|
|
21
|
+
return torch
|
|
22
|
+
except ImportError as e: # pragma: no cover
|
|
23
|
+
raise ImportError("demsym.nn requires torch: pip install "
|
|
24
|
+
"'demsym[torch]'") from e
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def TwistedEquivariant(backbone, elements: list[Element]):
|
|
28
|
+
"""Wrap a logits backbone into a twisted-equivariant model.
|
|
29
|
+
|
|
30
|
+
backbone: torch.nn.Module mapping (batch, num_detectors) floats to
|
|
31
|
+
(batch, 2**k) logits, where k = num_observables. Class c is the
|
|
32
|
+
bit-packed joint observable value (bit i = observable i).
|
|
33
|
+
elements: the FULL group including the identity — pass the output of
|
|
34
|
+
demsym.close(...).
|
|
35
|
+
"""
|
|
36
|
+
torch = _torch()
|
|
37
|
+
nn = torch.nn
|
|
38
|
+
|
|
39
|
+
if not elements:
|
|
40
|
+
raise ValueError("need at least the identity element")
|
|
41
|
+
k = elements[0].num_observables
|
|
42
|
+
nclass = 1 << k
|
|
43
|
+
if not any(e.is_identity_perm for e in elements):
|
|
44
|
+
raise ValueError("element list must include the identity "
|
|
45
|
+
"(use demsym.close, which always adds it)")
|
|
46
|
+
|
|
47
|
+
class _TwistedEquivariant(nn.Module):
|
|
48
|
+
def __init__(self):
|
|
49
|
+
super().__init__()
|
|
50
|
+
self.backbone = backbone
|
|
51
|
+
self.num_observables = k
|
|
52
|
+
invps, chis, cmaps = [], [], []
|
|
53
|
+
for e in elements:
|
|
54
|
+
invp = np.zeros(len(e.perm), dtype=np.int64)
|
|
55
|
+
invp[e.perm] = np.arange(len(e.perm))
|
|
56
|
+
invps.append(torch.tensor(invp, dtype=torch.long))
|
|
57
|
+
chis.append(torch.tensor(e.X.astype(np.float32)))
|
|
58
|
+
cmaps.append(torch.tensor(
|
|
59
|
+
[apply_A(e.A, c) for c in range(nclass)],
|
|
60
|
+
dtype=torch.long))
|
|
61
|
+
self.register_buffer("IP", torch.stack(invps)) # (|G|, nd)
|
|
62
|
+
self.register_buffer("CH", torch.stack(chis)) # (|G|, k, nd)
|
|
63
|
+
self.register_buffer("CM", torch.stack(cmaps)) # (|G|, 2^k)
|
|
64
|
+
|
|
65
|
+
def forward(self, x):
|
|
66
|
+
outs = []
|
|
67
|
+
for g in range(self.IP.shape[0]):
|
|
68
|
+
xg = x[:, self.IP[g]]
|
|
69
|
+
logit = self.backbone(xg)
|
|
70
|
+
b = torch.remainder(xg @ self.CH[g].T, 2.0).long()
|
|
71
|
+
bidx = torch.zeros_like(b[:, 0])
|
|
72
|
+
for i in range(self.num_observables):
|
|
73
|
+
bidx = bidx | (b[:, i] << i)
|
|
74
|
+
idx = self.CM[g][None, :].expand(x.shape[0], nclass) \
|
|
75
|
+
^ bidx[:, None]
|
|
76
|
+
outs.append(torch.gather(logit, 1, idx))
|
|
77
|
+
return torch.stack(outs).mean(dim=0)
|
|
78
|
+
|
|
79
|
+
return _TwistedEquivariant()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def mlp_backbone(num_detectors: int, num_observables: int, hidden: int = 128):
|
|
83
|
+
"""The matched-parameter 2-layer MLP used in the demsym experiments."""
|
|
84
|
+
torch = _torch()
|
|
85
|
+
nn = torch.nn
|
|
86
|
+
return nn.Sequential(nn.Linear(num_detectors, hidden), nn.ReLU(),
|
|
87
|
+
nn.Linear(hidden, hidden), nn.ReLU(),
|
|
88
|
+
nn.Linear(hidden, 1 << num_observables))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def equivariance_probe(model, elements: list[Element], x) -> float:
|
|
92
|
+
"""Worst-case |f(x) - twisted f(x_g)| over the group, on given syndromes.
|
|
93
|
+
|
|
94
|
+
x must be REALIZABLE syndromes (sampled shots): X carries a gauge
|
|
95
|
+
freedom invisible to mechanisms, so random bit-vectors would report
|
|
96
|
+
false violations. Expect ~1e-6 (float32 averaging noise) when the
|
|
97
|
+
construction is correct.
|
|
98
|
+
"""
|
|
99
|
+
torch = _torch()
|
|
100
|
+
nclass = 1 << elements[0].num_observables
|
|
101
|
+
x = torch.as_tensor(x, dtype=torch.float32,
|
|
102
|
+
device=next(model.parameters()).device)
|
|
103
|
+
worst = 0.0
|
|
104
|
+
model.eval()
|
|
105
|
+
with torch.no_grad():
|
|
106
|
+
fx = model(x)
|
|
107
|
+
for e in elements:
|
|
108
|
+
if e.is_identity_perm:
|
|
109
|
+
continue
|
|
110
|
+
invp = np.zeros(len(e.perm), dtype=np.int64)
|
|
111
|
+
invp[e.perm] = np.arange(len(e.perm))
|
|
112
|
+
xg = x[:, torch.tensor(invp, dtype=torch.long, device=x.device)]
|
|
113
|
+
fg = model(xg)
|
|
114
|
+
b = torch.remainder(
|
|
115
|
+
xg @ torch.tensor(e.X.astype(np.float32),
|
|
116
|
+
device=x.device).T, 2.0).long()
|
|
117
|
+
bidx = torch.zeros_like(b[:, 0])
|
|
118
|
+
for i in range(e.num_observables):
|
|
119
|
+
bidx = bidx | (b[:, i] << i)
|
|
120
|
+
cm = torch.tensor([apply_A(e.A, c) for c in range(nclass)],
|
|
121
|
+
device=x.device)
|
|
122
|
+
idx = cm[None, :].expand(x.shape[0], nclass) ^ bidx[:, None]
|
|
123
|
+
worst = max(worst,
|
|
124
|
+
float((fx - torch.gather(fg, 1, idx)).abs().max()))
|
|
125
|
+
return worst
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Candidate detector permutations from stim detector coordinates."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def perm_from_coords(circuit, fn, decimals: int = 6) -> np.ndarray:
|
|
8
|
+
"""Detector permutation induced by a map on detector coordinates.
|
|
9
|
+
|
|
10
|
+
fn receives each detector's coordinate tuple (as floats, from
|
|
11
|
+
DETECTOR(...) annotations) and returns the image coordinate tuple.
|
|
12
|
+
Every detector must carry coordinates, and fn must permute the
|
|
13
|
+
coordinate set exactly; raises ValueError otherwise (which is itself
|
|
14
|
+
informative: the map is not a symmetry of the detector layout).
|
|
15
|
+
|
|
16
|
+
Tip for periodic layouts: fold coordinates modulo the lattice size
|
|
17
|
+
inside fn.
|
|
18
|
+
"""
|
|
19
|
+
coords = circuit.get_detector_coordinates()
|
|
20
|
+
nd = circuit.num_detectors
|
|
21
|
+
if len(coords) != nd or any(len(c) == 0 for c in coords.values()):
|
|
22
|
+
raise ValueError("every detector needs a DETECTOR(...) coordinate "
|
|
23
|
+
"annotation to use perm_from_coords")
|
|
24
|
+
|
|
25
|
+
def key(c):
|
|
26
|
+
return tuple(round(float(x), decimals) for x in c)
|
|
27
|
+
|
|
28
|
+
index = {}
|
|
29
|
+
for d, c in coords.items():
|
|
30
|
+
kk = key(c)
|
|
31
|
+
if kk in index:
|
|
32
|
+
raise ValueError(f"two detectors share coordinates {kk}; "
|
|
33
|
+
"disambiguate (e.g. add a layer coordinate)")
|
|
34
|
+
index[kk] = d
|
|
35
|
+
perm = np.zeros(nd, dtype=np.int64)
|
|
36
|
+
for d, c in coords.items():
|
|
37
|
+
kk = key(fn(tuple(float(x) for x in c)))
|
|
38
|
+
if kk not in index:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"image of detector {d} at {key(c)} -> {kk} is not a "
|
|
41
|
+
"detector coordinate; the map does not permute the layout")
|
|
42
|
+
perm[d] = index[kk]
|
|
43
|
+
if len(set(perm.tolist())) != nd:
|
|
44
|
+
raise ValueError("coordinate map is not a bijection on detectors")
|
|
45
|
+
return perm
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Solve the twisted label action (A, X) for a candidate detector permutation.
|
|
2
|
+
|
|
3
|
+
Method (per candidate A): for every detector-set group v, the image group
|
|
4
|
+
perm(v) must carry the same multiset of (observable, probability) pairs
|
|
5
|
+
after o -> A o XOR b, where b = X·indicator(perm(v)) is a single unknown
|
|
6
|
+
GF(2) bit-vector per group. Each group constrains b to a consistent set;
|
|
7
|
+
bits forced to a single value become linear equations on the rows of X,
|
|
8
|
+
solved by GF(2) elimination; the candidate is accepted only after a final
|
|
9
|
+
gauge-free multiset verification of the full DEM.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from .dem import Dem
|
|
16
|
+
from .element import Element, apply_A
|
|
17
|
+
from .gf2 import gl2_matrices, solve_gf2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def solve_action(dem: Dem, perm, name: str = "g",
|
|
21
|
+
A_candidates: list[np.ndarray] | None = None
|
|
22
|
+
) -> Element | None:
|
|
23
|
+
"""Find (A, X) making `perm` a twisted symmetry of `dem`.
|
|
24
|
+
|
|
25
|
+
perm: array of length num_detectors, detector d -> perm[d]. Must be a
|
|
26
|
+
bijection (raises otherwise).
|
|
27
|
+
A_candidates: optional list of k x k GF(2) matrices to try, in order.
|
|
28
|
+
Defaults to all of GL(k, F2) for k <= 3; for larger k pass candidates
|
|
29
|
+
explicitly (e.g. the 2^k observable permutation matrices).
|
|
30
|
+
|
|
31
|
+
Returns a verified Element, or None if no candidate A admits a
|
|
32
|
+
consistent X. None is a real answer: the permutation is not a twisted
|
|
33
|
+
symmetry of this DEM under any tried A.
|
|
34
|
+
"""
|
|
35
|
+
perm = np.asarray(perm, dtype=np.int64)
|
|
36
|
+
nd = dem.num_detectors
|
|
37
|
+
if perm.shape != (nd,) or len(set(perm.tolist())) != nd:
|
|
38
|
+
raise ValueError(f"perm must be a bijection on {nd} detectors")
|
|
39
|
+
k = dem.num_observables
|
|
40
|
+
if A_candidates is None:
|
|
41
|
+
A_candidates = gl2_matrices(k)
|
|
42
|
+
|
|
43
|
+
groups = dem.groups
|
|
44
|
+
nb = 1 << k
|
|
45
|
+
for Amat in A_candidates:
|
|
46
|
+
Amat = np.asarray(Amat, dtype=np.int64) % 2
|
|
47
|
+
eq_rows = [[] for _ in range(k)]
|
|
48
|
+
eq_t = [[] for _ in range(k)]
|
|
49
|
+
ok = True
|
|
50
|
+
for v, group in groups.items():
|
|
51
|
+
v2 = frozenset(int(perm[d]) for d in v)
|
|
52
|
+
tgt = groups.get(v2)
|
|
53
|
+
if tgt is None:
|
|
54
|
+
ok = False
|
|
55
|
+
break
|
|
56
|
+
src = sorted((apply_A(Amat, o), p) for o, p in group)
|
|
57
|
+
cons = [b for b in range(nb)
|
|
58
|
+
if tuple(sorted((o ^ b, p) for o, p in src)) == tgt]
|
|
59
|
+
if not cons:
|
|
60
|
+
ok = False
|
|
61
|
+
break
|
|
62
|
+
row = None
|
|
63
|
+
for bit in range(k):
|
|
64
|
+
vals = {(b >> bit) & 1 for b in cons}
|
|
65
|
+
if len(vals) == 1:
|
|
66
|
+
if row is None:
|
|
67
|
+
row = np.zeros(nd, dtype=np.int8)
|
|
68
|
+
for d in v2:
|
|
69
|
+
row[d] = 1
|
|
70
|
+
eq_rows[bit].append(row)
|
|
71
|
+
eq_t[bit].append(vals.pop())
|
|
72
|
+
if not ok:
|
|
73
|
+
continue
|
|
74
|
+
X, good = [], True
|
|
75
|
+
for bit in range(k):
|
|
76
|
+
if eq_rows[bit]:
|
|
77
|
+
sol = solve_gf2(np.array(eq_rows[bit]),
|
|
78
|
+
np.array(eq_t[bit], dtype=np.int8))
|
|
79
|
+
if sol is None:
|
|
80
|
+
good = False
|
|
81
|
+
break
|
|
82
|
+
X.append(sol)
|
|
83
|
+
else:
|
|
84
|
+
X.append(np.zeros(nd, dtype=np.int64))
|
|
85
|
+
if not good:
|
|
86
|
+
continue
|
|
87
|
+
cand = Element(name, perm, Amat, np.stack(X))
|
|
88
|
+
if cand.verify(dem):
|
|
89
|
+
return cand
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def close(generators: list[Element], dem: Dem,
|
|
94
|
+
max_size: int = 1024) -> list[Element]:
|
|
95
|
+
"""Group closure of verified elements, with the identity included.
|
|
96
|
+
|
|
97
|
+
Every product is re-verified against the DEM (cheap, and a convention
|
|
98
|
+
self-check). Elements are deduplicated by their detector permutation.
|
|
99
|
+
Raises if the closure exceeds max_size or a product fails to verify.
|
|
100
|
+
"""
|
|
101
|
+
from .element import identity
|
|
102
|
+
if not generators:
|
|
103
|
+
return [identity(dem)]
|
|
104
|
+
els = {identity(dem).perm.tobytes(): identity(dem)}
|
|
105
|
+
frontier = []
|
|
106
|
+
for g in generators:
|
|
107
|
+
key = g.perm.tobytes()
|
|
108
|
+
if key not in els:
|
|
109
|
+
if not g.verify(dem):
|
|
110
|
+
raise ValueError(f"generator {g.name} does not verify")
|
|
111
|
+
els[key] = g
|
|
112
|
+
frontier.append(g)
|
|
113
|
+
while frontier:
|
|
114
|
+
new = []
|
|
115
|
+
for g in frontier:
|
|
116
|
+
for h in list(els.values()):
|
|
117
|
+
for prod in (g @ h, h @ g):
|
|
118
|
+
key = prod.perm.tobytes()
|
|
119
|
+
if key in els:
|
|
120
|
+
continue
|
|
121
|
+
if not prod.verify(dem):
|
|
122
|
+
raise AssertionError(
|
|
123
|
+
f"product {prod.name} of verified elements fails "
|
|
124
|
+
"verification — composition convention bug")
|
|
125
|
+
els[key] = prod
|
|
126
|
+
new.append(prod)
|
|
127
|
+
if len(els) > max_size:
|
|
128
|
+
raise ValueError(f"closure exceeds {max_size}")
|
|
129
|
+
frontier = new
|
|
130
|
+
return list(els.values())
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Composition law and verified group closure on the toric setting."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from demsym import close, identity, solve_action
|
|
6
|
+
|
|
7
|
+
from toric_cc import build
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture(scope="module")
|
|
11
|
+
def toric4():
|
|
12
|
+
return build(L=4, p=0.1)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def solved(dem, perms, name, cands):
|
|
16
|
+
e = solve_action(dem, perms[name], name=name, A_candidates=cands)
|
|
17
|
+
assert e is not None
|
|
18
|
+
return e
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_translation_closure_is_the_full_torus_group(toric4):
|
|
22
|
+
dem, perms = toric4
|
|
23
|
+
I4 = [np.eye(4, dtype=np.int64)]
|
|
24
|
+
t10 = solved(dem, perms, "T10", I4)
|
|
25
|
+
t01 = solved(dem, perms, "T01", I4)
|
|
26
|
+
grp = close([t10, t01], dem)
|
|
27
|
+
assert len(grp) == 16 # Z_4 x Z_4, identity included
|
|
28
|
+
assert all((e.A == np.eye(4, dtype=np.int64)).all() for e in grp)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_rotation_powers_close_to_order_4(toric4):
|
|
32
|
+
dem, perms = toric4
|
|
33
|
+
rot = solved(dem, perms, "ROT90",
|
|
34
|
+
[np.eye(4, dtype=np.int64)[[1, 0, 3, 2]]])
|
|
35
|
+
grp = close([rot], dem)
|
|
36
|
+
assert len(grp) == 4
|
|
37
|
+
r2 = rot @ rot
|
|
38
|
+
assert r2.verify(dem)
|
|
39
|
+
# C2 inverts both axes: A back to identity
|
|
40
|
+
assert (r2.A == np.eye(4, dtype=np.int64)).all()
|
|
41
|
+
r4 = r2 @ r2
|
|
42
|
+
assert r4.is_identity_perm
|
|
43
|
+
assert (r4.A == np.eye(4, dtype=np.int64)).all()
|
|
44
|
+
# X of the full cycle may be pure gauge; it must still verify as identity
|
|
45
|
+
assert r4.verify(dem)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_composition_is_associative_on_perms_and_A(toric4):
|
|
49
|
+
dem, perms = toric4
|
|
50
|
+
I4 = [np.eye(4, dtype=np.int64)]
|
|
51
|
+
t10 = solved(dem, perms, "T10", I4)
|
|
52
|
+
t01 = solved(dem, perms, "T01", I4)
|
|
53
|
+
rot = solved(dem, perms, "ROT90",
|
|
54
|
+
[np.eye(4, dtype=np.int64)[[1, 0, 3, 2]]])
|
|
55
|
+
a = (rot @ t10) @ t01
|
|
56
|
+
b = rot @ (t10 @ t01)
|
|
57
|
+
assert (a.perm == b.perm).all() and (a.A == b.A).all()
|
|
58
|
+
assert a.verify(dem) and b.verify(dem)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_identity_neutral(toric4):
|
|
62
|
+
dem, perms = toric4
|
|
63
|
+
I4 = [np.eye(4, dtype=np.int64)]
|
|
64
|
+
t10 = solved(dem, perms, "T10", I4)
|
|
65
|
+
e = identity(dem)
|
|
66
|
+
assert ((e @ t10).perm == t10.perm).all()
|
|
67
|
+
assert ((t10 @ e).perm == t10.perm).all()
|
|
68
|
+
assert (t10 @ e).verify(dem)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from demsym import Dem, gl2_matrices, solve_gf2
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_gl_sizes():
|
|
8
|
+
assert len(gl2_matrices(1)) == 1
|
|
9
|
+
assert len(gl2_matrices(2)) == 6
|
|
10
|
+
assert len(gl2_matrices(3)) == 168
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_gl_identity_first_and_invertible():
|
|
14
|
+
for k in (1, 2, 3):
|
|
15
|
+
mats = gl2_matrices(k)
|
|
16
|
+
assert (mats[0] == np.eye(k, dtype=np.int64)).all()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_gl_k4_requires_explicit_candidates():
|
|
20
|
+
with pytest.raises(ValueError):
|
|
21
|
+
gl2_matrices(4)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_solve_gf2_roundtrip():
|
|
25
|
+
rng = np.random.default_rng(0)
|
|
26
|
+
for _ in range(20):
|
|
27
|
+
m, n = rng.integers(3, 12, size=2)
|
|
28
|
+
A = rng.integers(0, 2, size=(m, n))
|
|
29
|
+
x = rng.integers(0, 2, size=n)
|
|
30
|
+
b = A.dot(x) % 2
|
|
31
|
+
sol = solve_gf2(A, b)
|
|
32
|
+
assert sol is not None
|
|
33
|
+
assert (A.dot(sol) % 2 == b).all()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_solve_gf2_inconsistent():
|
|
37
|
+
A = np.array([[1, 0], [1, 0]])
|
|
38
|
+
b = np.array([0, 1])
|
|
39
|
+
assert solve_gf2(A, b) is None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_dem_groups_merge_by_detector_set():
|
|
43
|
+
dem = Dem.from_mechanisms(
|
|
44
|
+
[({0, 1}, 0b01, 0.1), ({1, 0}, 0b10, 0.2), ({2}, 0, 0.3)],
|
|
45
|
+
num_detectors=3, num_observables=2)
|
|
46
|
+
assert dem.num_mechanisms == 3
|
|
47
|
+
assert len(dem.groups) == 2
|
|
48
|
+
assert dem.groups[frozenset({0, 1})] == ((1, 0.1), (2, 0.2))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""The 20-detector miniature of the whole story, on stim generated circuits:
|
|
2
|
+
|
|
3
|
+
- phenomenological noise: the mirror is an exact twisted symmetry, and the
|
|
4
|
+
twist is REAL (A = I with X = 0 does not verify — the moved logical needs
|
|
5
|
+
the syndrome-linear correction);
|
|
6
|
+
- circuit-level noise on the same code: the same mirror has NO solution —
|
|
7
|
+
extraction-schedule errors de-symmetrize the DEM (the obstruction).
|
|
8
|
+
"""
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pytest
|
|
11
|
+
import stim
|
|
12
|
+
|
|
13
|
+
from demsym import Dem, Element, identity, perm_from_coords, solve_action
|
|
14
|
+
|
|
15
|
+
D = 5
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def gen(**noise):
|
|
19
|
+
return stim.Circuit.generated("repetition_code:memory", distance=D,
|
|
20
|
+
rounds=4, **noise)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def mirror(c):
|
|
24
|
+
return (2.0 * (D - 1) - c[0],) + tuple(c[1:])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(scope="module")
|
|
28
|
+
def phenom():
|
|
29
|
+
c = gen(before_round_data_depolarization=0.04,
|
|
30
|
+
before_measure_flip_probability=0.01)
|
|
31
|
+
return c, Dem.from_circuit(c), perm_from_coords(c, mirror)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_mirror_verifies_with_twist(phenom):
|
|
35
|
+
c, dem, perm = phenom
|
|
36
|
+
e = solve_action(dem, perm, name="mirror")
|
|
37
|
+
assert e is not None and e.verify(dem)
|
|
38
|
+
assert (e.A == np.eye(1, dtype=np.int64)).all()
|
|
39
|
+
assert sum(e.chi_weights()) > 0
|
|
40
|
+
# the twist is forced: the untwisted candidate is NOT a symmetry
|
|
41
|
+
bare = Element("mirror_untwisted", perm, e.A, np.zeros_like(e.X))
|
|
42
|
+
assert not bare.verify(dem)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_identity_always_solves(phenom):
|
|
46
|
+
_, dem, _ = phenom
|
|
47
|
+
e = solve_action(dem, np.arange(dem.num_detectors))
|
|
48
|
+
assert e is not None and (e.A == np.eye(1, dtype=np.int64)).all()
|
|
49
|
+
assert identity(dem).verify(dem)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_circuit_level_noise_obstructs_the_mirror():
|
|
53
|
+
c = gen(after_clifford_depolarization=0.01,
|
|
54
|
+
before_measure_flip_probability=0.01)
|
|
55
|
+
dem = Dem.from_circuit(c)
|
|
56
|
+
perm = perm_from_coords(c, mirror)
|
|
57
|
+
assert solve_action(dem, perm, name="mirror") is None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_perm_from_coords_rejects_non_layout_maps(phenom):
|
|
61
|
+
c, _, _ = phenom
|
|
62
|
+
with pytest.raises(ValueError):
|
|
63
|
+
perm_from_coords(c, lambda co: (co[0] + 2.0,) + tuple(co[1:]))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Exact equivariance of the torch wrapper on realizable syndromes."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
import stim
|
|
5
|
+
|
|
6
|
+
from demsym import Dem, close, perm_from_coords, solve_action
|
|
7
|
+
|
|
8
|
+
torch = pytest.importorskip("torch")
|
|
9
|
+
|
|
10
|
+
from demsym.nn import TwistedEquivariant, equivariance_probe, mlp_backbone # noqa: E402
|
|
11
|
+
|
|
12
|
+
D = 5
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(scope="module")
|
|
16
|
+
def setting():
|
|
17
|
+
c = stim.Circuit.generated("repetition_code:memory", distance=D, rounds=4,
|
|
18
|
+
before_round_data_depolarization=0.04,
|
|
19
|
+
before_measure_flip_probability=0.01)
|
|
20
|
+
dem = Dem.from_circuit(c)
|
|
21
|
+
perm = perm_from_coords(c, lambda co: (2.0 * (D - 1) - co[0],)
|
|
22
|
+
+ tuple(co[1:]))
|
|
23
|
+
e = solve_action(dem, perm, name="mirror")
|
|
24
|
+
grp = close([e], dem)
|
|
25
|
+
assert len(grp) == 2
|
|
26
|
+
shots = c.compile_detector_sampler(seed=7).sample(128).astype(np.float32)
|
|
27
|
+
return dem, grp, shots
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_probe_is_exact_on_realizable_syndromes(setting):
|
|
31
|
+
dem, grp, shots = setting
|
|
32
|
+
torch.manual_seed(0)
|
|
33
|
+
model = TwistedEquivariant(mlp_backbone(dem.num_detectors, 1, hidden=16),
|
|
34
|
+
grp)
|
|
35
|
+
assert equivariance_probe(model, grp, shots) < 1e-5
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_forward_shape_and_gradients(setting):
|
|
39
|
+
dem, grp, shots = setting
|
|
40
|
+
torch.manual_seed(0)
|
|
41
|
+
model = TwistedEquivariant(mlp_backbone(dem.num_detectors, 1, hidden=16),
|
|
42
|
+
grp)
|
|
43
|
+
x = torch.tensor(shots[:32])
|
|
44
|
+
out = model(x)
|
|
45
|
+
assert out.shape == (32, 2)
|
|
46
|
+
loss = torch.nn.functional.cross_entropy(
|
|
47
|
+
out, torch.zeros(32, dtype=torch.long))
|
|
48
|
+
loss.backward()
|
|
49
|
+
grads = [p.grad for p in model.parameters()]
|
|
50
|
+
assert all(g is not None for g in grads)
|
|
51
|
+
assert any(float(g.abs().sum()) > 0 for g in grads)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_requires_identity_element(setting):
|
|
55
|
+
dem, grp, _ = setting
|
|
56
|
+
non_id = [e for e in grp if not e.is_identity_perm]
|
|
57
|
+
with pytest.raises(ValueError):
|
|
58
|
+
TwistedEquivariant(mlp_backbone(dem.num_detectors, 1, hidden=16),
|
|
59
|
+
non_id)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Validation against The END's analytic symmetry action (arXiv:2304.07362).
|
|
2
|
+
|
|
3
|
+
On their setting — toric code, code capacity, i.i.d. depolarizing — the
|
|
4
|
+
blind DEM solve must recover their Fig. 3 action UNIQUELY among all 24
|
|
5
|
+
observable permutation matrices, for all four lattice elements. This is the
|
|
6
|
+
ground-truth bridge: no analytic input, the action comes out of the DEM.
|
|
7
|
+
"""
|
|
8
|
+
import itertools
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from demsym import solve_action
|
|
14
|
+
|
|
15
|
+
from toric_cc import build
|
|
16
|
+
|
|
17
|
+
PERMS24 = [np.eye(4, dtype=np.int64)[list(p)]
|
|
18
|
+
for p in itertools.permutations(range(4))]
|
|
19
|
+
EXPECTED_A = {
|
|
20
|
+
"T10": np.eye(4, dtype=np.int64),
|
|
21
|
+
"T01": np.eye(4, dtype=np.int64),
|
|
22
|
+
"ROT90": np.eye(4, dtype=np.int64)[[1, 0, 3, 2]], # X1<->X2, Z1<->Z2
|
|
23
|
+
"MIR": np.eye(4, dtype=np.int64),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(scope="module")
|
|
28
|
+
def toric():
|
|
29
|
+
return build(L=6, p=0.1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize("name", ["T10", "T01", "ROT90", "MIR"])
|
|
33
|
+
def test_blind_solve_unique_and_matches_end(toric, name):
|
|
34
|
+
dem, perms = toric
|
|
35
|
+
sols = [s for A in PERMS24
|
|
36
|
+
if (s := solve_action(dem, perms[name], name=name,
|
|
37
|
+
A_candidates=[A])) is not None]
|
|
38
|
+
assert len(sols) == 1, f"{name}: A not unique among 24 label perms"
|
|
39
|
+
assert (sols[0].A == EXPECTED_A[name]).all()
|
|
40
|
+
assert sols[0].verify(dem)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_translations_carry_nonzero_twist(toric):
|
|
44
|
+
dem, perms = toric
|
|
45
|
+
e = solve_action(dem, perms["T10"], A_candidates=[np.eye(4, dtype=np.int64)])
|
|
46
|
+
assert e is not None and sum(e.chi_weights()) > 0
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Shared toric-code code-capacity construction (The END's setting).
|
|
2
|
+
|
|
3
|
+
L x L torus, qubits on edges; A(i,j) X-stabilizers are detectors 0..L^2-1,
|
|
4
|
+
B(i,j) Z-stabilizers are detectors L^2..2L^2-1. Four logical observables
|
|
5
|
+
gamma = (X1, X2, Z1, Z2) as bits 0..3. Mechanisms: X, Z, Y on each edge at
|
|
6
|
+
p/3 (i.i.d. depolarizing, perfect syndromes).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from demsym import Dem
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build(L: int = 6, p: float = 0.1):
|
|
16
|
+
def vdet(i, j):
|
|
17
|
+
return (i % L) * L + (j % L)
|
|
18
|
+
|
|
19
|
+
def pdet(i, j):
|
|
20
|
+
return L * L + (i % L) * L + (j % L)
|
|
21
|
+
|
|
22
|
+
def edge_dets_X(e):
|
|
23
|
+
t, i, j = e
|
|
24
|
+
if t == "h":
|
|
25
|
+
return {pdet(i, j), pdet(i - 1, j)}
|
|
26
|
+
return {pdet(i, j), pdet(i, j - 1)}
|
|
27
|
+
|
|
28
|
+
def edge_dets_Z(e):
|
|
29
|
+
t, i, j = e
|
|
30
|
+
if t == "h":
|
|
31
|
+
return {vdet(i, j), vdet(i, j + 1)}
|
|
32
|
+
return {vdet(i, j), vdet(i + 1, j)}
|
|
33
|
+
|
|
34
|
+
edges = [(t, i, j) for i in range(L) for j in range(L) for t in ("h", "v")]
|
|
35
|
+
XBAR1 = {("h", i, 0) for i in range(L)}
|
|
36
|
+
XBAR2 = {("v", 0, j) for j in range(L)}
|
|
37
|
+
ZBAR1 = {("h", 0, j) for j in range(L)}
|
|
38
|
+
ZBAR2 = {("v", i, 0) for i in range(L)}
|
|
39
|
+
|
|
40
|
+
def obs_of(e, pauli):
|
|
41
|
+
o = 0
|
|
42
|
+
if pauli in ("Z", "Y"):
|
|
43
|
+
o |= (e in XBAR1) << 0
|
|
44
|
+
o |= (e in XBAR2) << 1
|
|
45
|
+
if pauli in ("X", "Y"):
|
|
46
|
+
o |= (e in ZBAR1) << 2
|
|
47
|
+
o |= (e in ZBAR2) << 3
|
|
48
|
+
return o
|
|
49
|
+
|
|
50
|
+
mechs = []
|
|
51
|
+
for e in edges:
|
|
52
|
+
for pauli in "XZY":
|
|
53
|
+
dets = set()
|
|
54
|
+
if pauli in ("X", "Y"):
|
|
55
|
+
dets ^= edge_dets_X(e)
|
|
56
|
+
if pauli in ("Z", "Y"):
|
|
57
|
+
dets ^= edge_dets_Z(e)
|
|
58
|
+
mechs.append((dets, obs_of(e, pauli), p / 3))
|
|
59
|
+
dem = Dem.from_mechanisms(mechs, 2 * L * L, 4)
|
|
60
|
+
|
|
61
|
+
def perm_from_edgemap(emap):
|
|
62
|
+
vsets, psets = {}, {}
|
|
63
|
+
for i in range(L):
|
|
64
|
+
for j in range(L):
|
|
65
|
+
vs = frozenset({("h", i, j), ("h", i, (j - 1) % L),
|
|
66
|
+
("v", i, j), ("v", (i - 1) % L, j)})
|
|
67
|
+
ps = frozenset({("h", i, j), ("h", (i + 1) % L, j),
|
|
68
|
+
("v", i, j), ("v", i, (j + 1) % L)})
|
|
69
|
+
vsets[vs] = vdet(i, j)
|
|
70
|
+
psets[ps] = pdet(i, j)
|
|
71
|
+
perm = np.zeros(2 * L * L, dtype=np.int64)
|
|
72
|
+
for sets in (vsets, psets):
|
|
73
|
+
for s, d in sets.items():
|
|
74
|
+
img = frozenset(emap(e) for e in s)
|
|
75
|
+
tgt = vsets.get(img, psets.get(img))
|
|
76
|
+
assert tgt is not None, "edge map is not a lattice automorphism"
|
|
77
|
+
perm[d] = tgt
|
|
78
|
+
assert len(set(perm.tolist())) == 2 * L * L
|
|
79
|
+
return perm
|
|
80
|
+
|
|
81
|
+
def T10(e):
|
|
82
|
+
t, i, j = e
|
|
83
|
+
return (t, (i + 1) % L, j)
|
|
84
|
+
|
|
85
|
+
def T01(e):
|
|
86
|
+
t, i, j = e
|
|
87
|
+
return (t, i, (j + 1) % L)
|
|
88
|
+
|
|
89
|
+
def ROT(e):
|
|
90
|
+
t, i, j = e
|
|
91
|
+
if t == "h":
|
|
92
|
+
return ("v", j % L, (-i) % L)
|
|
93
|
+
return ("h", j % L, (-i - 1) % L)
|
|
94
|
+
|
|
95
|
+
def MIR(e):
|
|
96
|
+
t, i, j = e
|
|
97
|
+
if t == "h":
|
|
98
|
+
return ("h", i, (-j - 1) % L)
|
|
99
|
+
return ("v", i, (-j) % L)
|
|
100
|
+
|
|
101
|
+
perms = {name: perm_from_edgemap(f)
|
|
102
|
+
for name, f in (("T10", T10), ("T01", T01),
|
|
103
|
+
("ROT90", ROT), ("MIR", MIR))}
|
|
104
|
+
return dem, perms
|