qLDPC 0.3.0__tar.gz → 0.3.2__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.
- {qldpc-0.3.0 → qldpc-0.3.2}/PKG-INFO +4 -1
- {qldpc-0.3.0 → qldpc-0.3.2}/pyproject.toml +5 -2
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/__init__.py +0 -1
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/groups.py +2 -4
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/groups_test.py +4 -4
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/rings.py +2 -3
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/cache.py +13 -7
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/__init__.py +6 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/benchmarking.py +8 -27
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/bookkeeping.py +21 -12
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/common.py +46 -26
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/common_test.py +8 -0
- qldpc-0.3.2/src/qldpc/circuits/conftest_probe.py +13 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/encoding.py +21 -11
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/encoding_test.py +11 -2
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/alpha_syndrome.py +3 -3
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/memory.py +3 -2
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/syndrome_measurement.py +2 -2
- qldpc-0.3.2/src/qldpc/circuits/noise_model.py +1751 -0
- qldpc-0.3.2/src/qldpc/circuits/noise_model_test.py +1188 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/transversal.py +45 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/transversal_test.py +18 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/classical.py +8 -9
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/common.py +126 -51
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/common_test.py +22 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/distance.py +1 -1
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/quantum.py +11 -12
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/__init__.py +5 -1
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/custom.py +267 -150
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/custom_test.py +41 -1
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/dems.py +78 -45
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/dems_test.py +11 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/sinter.py +17 -13
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/sinter_test.py +3 -3
- qldpc-0.3.2/src/qldpc/math.py +360 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/math_test.py +41 -6
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/objects.py +2 -3
- qldpc-0.3.0/src/qldpc/circuits/noise_model.py +0 -864
- qldpc-0.3.0/src/qldpc/circuits/noise_model_test.py +0 -298
- qldpc-0.3.0/src/qldpc/math.py +0 -168
- {qldpc-0.3.0 → qldpc-0.3.2}/README.md +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/__init__.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/linalg.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/linalg_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/rings_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/wedderburn_artin.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/wedderburn_artin_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/cache_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/benchmarking_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/bookkeeping_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/__init__.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/alpha_syndrome_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/memory_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/syndrome_measurement_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/__init__.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/classical_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/distance_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/quantum_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/conftest.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/retrieval.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/retrieval_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/__init__.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/codes.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/codes_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/gap.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/gap_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/groups.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/groups_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/objects_test.py +0 -0
- {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qLDPC
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Tools for constructing and analyzing quantum low density parity check (qLDPC) codes.
|
|
5
5
|
Keywords: quantum computing,quantum error correction,low density parity check codes,LDPC
|
|
6
6
|
Author: Michael A. Perlin
|
|
@@ -24,15 +24,18 @@ Requires-Dist: scipy>=1.14.1
|
|
|
24
24
|
Requires-Dist: sinter>=1.16.0
|
|
25
25
|
Requires-Dist: stim>=1.16.0
|
|
26
26
|
Requires-Dist: sympy>=1.12
|
|
27
|
+
Requires-Dist: bloqade-tsim>=0.1.4 ; extra == 'dev'
|
|
27
28
|
Requires-Dist: checks-superstaq>=0.5.62 ; extra == 'dev'
|
|
28
29
|
Requires-Dist: jupyter>=1.1.1 ; extra == 'dev'
|
|
29
30
|
Requires-Dist: python-lsp-server[all]>=1.14.0 ; extra == 'dev'
|
|
30
31
|
Requires-Dist: relay-bp[stim]==0.2.1 ; extra == 'dev'
|
|
31
32
|
Requires-Dist: relay-bp[stim]==0.2.1 ; extra == 'relay-bp'
|
|
33
|
+
Requires-Dist: bloqade-tsim>=0.1.4 ; extra == 'tsim'
|
|
32
34
|
Requires-Python: >=3.10
|
|
33
35
|
Project-URL: Repository, https://github.com/qLDPCOrg/qLDPC
|
|
34
36
|
Provides-Extra: dev
|
|
35
37
|
Provides-Extra: relay-bp
|
|
38
|
+
Provides-Extra: tsim
|
|
36
39
|
Description-Content-Type: text/markdown
|
|
37
40
|
|
|
38
41
|
# qLDPC
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "qLDPC"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "Tools for constructing and analyzing quantum low density parity check (qLDPC) codes."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -38,12 +38,14 @@ dependencies = [
|
|
|
38
38
|
|
|
39
39
|
[project.optional-dependencies]
|
|
40
40
|
dev = [
|
|
41
|
+
"bloqade-tsim>=0.1.4",
|
|
41
42
|
"checks-superstaq>=0.5.62",
|
|
42
43
|
"jupyter>=1.1.1",
|
|
43
44
|
"python-lsp-server[all]>=1.14.0",
|
|
44
45
|
"relay-bp[stim]==0.2.1",
|
|
45
46
|
]
|
|
46
47
|
relay-bp = ["relay-bp[stim]==0.2.1"]
|
|
48
|
+
tsim = ["bloqade-tsim>=0.1.4"]
|
|
47
49
|
|
|
48
50
|
[project.urls]
|
|
49
51
|
Repository = "https://github.com/qLDPCOrg/qLDPC"
|
|
@@ -56,7 +58,8 @@ build-backend = "uv_build"
|
|
|
56
58
|
|
|
57
59
|
[tool.ruff]
|
|
58
60
|
line-length = 100
|
|
59
|
-
lint.extend-select = ["I"]
|
|
61
|
+
lint.extend-select = ["I", "W505"]
|
|
62
|
+
lint.pycodestyle.max-doc-length = 100
|
|
60
63
|
|
|
61
64
|
[tool.mypy]
|
|
62
65
|
pretty = true
|
|
@@ -48,15 +48,13 @@ import sympy.core
|
|
|
48
48
|
|
|
49
49
|
from qldpc import external
|
|
50
50
|
|
|
51
|
-
GF2: type[galois.FieldArray] = galois.GF(2)
|
|
52
|
-
|
|
53
51
|
|
|
54
52
|
def resolve_field(
|
|
55
53
|
field: int | type[galois.FieldArray] | None,
|
|
56
54
|
) -> type[galois.FieldArray]: # pragma: no cover
|
|
57
55
|
"""Parse a finite field argument to obtain an actual finite field."""
|
|
58
56
|
if field is None:
|
|
59
|
-
return GF2
|
|
57
|
+
return galois.GF2
|
|
60
58
|
if isinstance(field, int):
|
|
61
59
|
return galois.GF(field)
|
|
62
60
|
return field
|
|
@@ -921,7 +919,7 @@ class ProjectiveSpecialLinearGroup(Group):
|
|
|
921
919
|
"""Generating matrices of PSL, constructed out of the generating matrices of SL."""
|
|
922
920
|
field = resolve_field(field)
|
|
923
921
|
gen_x, gen_w = SpecialLinearGroup.get_generating_mats(dimension, field)
|
|
924
|
-
if field is GF2:
|
|
922
|
+
if field is galois.GF2:
|
|
925
923
|
return gen_x, gen_w
|
|
926
924
|
return (
|
|
927
925
|
np.kron(np.linalg.inv(gen_x), gen_x).view(field),
|
|
@@ -132,8 +132,8 @@ def assert_valid_lifts(group: abstract.Group) -> None:
|
|
|
132
132
|
# invert elements: g -> g**(-1)
|
|
133
133
|
assert all(
|
|
134
134
|
np.array_equal(
|
|
135
|
-
np.
|
|
136
|
-
[
|
|
135
|
+
np.flatnonzero(group.inversion_matrix[:, group.index(gg)]),
|
|
136
|
+
[group.index(~gg)],
|
|
137
137
|
)
|
|
138
138
|
for gg in group_members
|
|
139
139
|
)
|
|
@@ -156,8 +156,8 @@ def assert_valid_lifts(group: abstract.Group) -> None:
|
|
|
156
156
|
else:
|
|
157
157
|
assert all(
|
|
158
158
|
np.array_equal(
|
|
159
|
-
np.
|
|
160
|
-
[
|
|
159
|
+
np.flatnonzero(group.adjoint_lift(aa)[:, group.index(bb)]),
|
|
160
|
+
[group.index(aa * bb * ~aa)],
|
|
161
161
|
)
|
|
162
162
|
for aa, bb in itertools.product(group_members, repeat=2)
|
|
163
163
|
)
|
|
@@ -1082,10 +1082,9 @@ def _get_block_howell_form(matrix: galois.FieldArray, *, right: bool = False) ->
|
|
|
1082
1082
|
if right and size > 1:
|
|
1083
1083
|
matrix = matrix.transpose(0, 1, 3, 2)
|
|
1084
1084
|
|
|
1085
|
-
# row-reduce as an expanded 2-D matrix
|
|
1085
|
+
# row-reduce as an expanded 2-D matrix, keeping a basis of the row space (no all-zero rows)
|
|
1086
1086
|
shape = (num_block_rows * size, num_block_cols * size)
|
|
1087
|
-
matrix = matrix.transpose(0, 2, 1, 3).reshape(shape).view(field).
|
|
1088
|
-
matrix = matrix[qldpc.math.first_nonzero_cols(matrix) < matrix.shape[1]].view(field)
|
|
1087
|
+
matrix = matrix.transpose(0, 2, 1, 3).reshape(shape).view(field).row_space()
|
|
1089
1088
|
|
|
1090
1089
|
if size > 1:
|
|
1091
1090
|
# insert zero rows to shift pivots down so that they always lie on the diagonal of a block
|
|
@@ -18,7 +18,7 @@ limitations under the License.
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import functools
|
|
21
|
-
import
|
|
21
|
+
import pathlib
|
|
22
22
|
import sys
|
|
23
23
|
import warnings
|
|
24
24
|
from collections.abc import Callable, Hashable
|
|
@@ -30,13 +30,17 @@ import platformdirs
|
|
|
30
30
|
Params = ParamSpec("Params")
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def get_disk_cache_path(
|
|
33
|
+
def get_disk_cache_path(
|
|
34
|
+
cache_name: str, *, cache_dir: pathlib.Path | str | None = None
|
|
35
|
+
) -> pathlib.Path:
|
|
34
36
|
"""Retrieve the path of a cache."""
|
|
35
|
-
cache_dir = cache_dir or
|
|
36
|
-
return
|
|
37
|
+
cache_dir = cache_dir or pathlib.Path(platformdirs.user_cache_dir()) / "qldpc"
|
|
38
|
+
return pathlib.Path(cache_dir) / cache_name
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
def get_disk_cache(
|
|
41
|
+
def get_disk_cache(
|
|
42
|
+
cache_name: str, *, cache_dir: pathlib.Path | str | None = None
|
|
43
|
+
) -> diskcache.Cache:
|
|
40
44
|
"""Retrieve a dictionary-like cache object."""
|
|
41
45
|
if running_with_pytest():
|
|
42
46
|
return {}
|
|
@@ -46,7 +50,7 @@ def get_disk_cache(cache_name: str, *, cache_dir: str | None = None) -> diskcach
|
|
|
46
50
|
def use_disk_cache(
|
|
47
51
|
cache_name: str,
|
|
48
52
|
*,
|
|
49
|
-
cache_dir: str | None = None,
|
|
53
|
+
cache_dir: pathlib.Path | str | None = None,
|
|
50
54
|
key_func: Callable[..., Hashable] | None = None,
|
|
51
55
|
) -> Callable[[Callable[Params, Any]], Callable[Params, Any]]:
|
|
52
56
|
"""Decorator to cache results to disk."""
|
|
@@ -82,7 +86,9 @@ def running_with_pytest() -> bool:
|
|
|
82
86
|
return "pytest" in sys.modules
|
|
83
87
|
|
|
84
88
|
|
|
85
|
-
def clear_entry(
|
|
89
|
+
def clear_entry(
|
|
90
|
+
cache_name: str, key: Hashable, *, cache_dir: pathlib.Path | str | None = None
|
|
91
|
+
) -> None:
|
|
86
92
|
"""Clear an entry from a local cache."""
|
|
87
93
|
cache = get_disk_cache(cache_name, cache_dir=cache_dir)
|
|
88
94
|
if key in cache:
|
|
@@ -12,6 +12,7 @@ from .bookkeeping import (
|
|
|
12
12
|
from .common import (
|
|
13
13
|
get_pauli_product_measurements,
|
|
14
14
|
get_unaddressed_measurements,
|
|
15
|
+
remap_qubit_target,
|
|
15
16
|
restrict_to_qubits,
|
|
16
17
|
with_remapped_qubits,
|
|
17
18
|
)
|
|
@@ -40,6 +41,7 @@ from .noise_model import (
|
|
|
40
41
|
DepolarizingNoiseModel,
|
|
41
42
|
NoiseModel,
|
|
42
43
|
NoiseRule,
|
|
44
|
+
PauliChannel,
|
|
43
45
|
SI1000NoiseModel,
|
|
44
46
|
as_noiseless_circuit,
|
|
45
47
|
)
|
|
@@ -48,6 +50,7 @@ from .transversal import (
|
|
|
48
50
|
get_transversal_circuit,
|
|
49
51
|
get_transversal_circuits,
|
|
50
52
|
get_transversal_ops,
|
|
53
|
+
get_transversal_s,
|
|
51
54
|
)
|
|
52
55
|
|
|
53
56
|
__all__ = [
|
|
@@ -60,6 +63,7 @@ __all__ = [
|
|
|
60
63
|
"Record",
|
|
61
64
|
"get_pauli_product_measurements",
|
|
62
65
|
"get_unaddressed_measurements",
|
|
66
|
+
"remap_qubit_target",
|
|
63
67
|
"restrict_to_qubits",
|
|
64
68
|
"with_remapped_qubits",
|
|
65
69
|
"get_encoder_and_decoder",
|
|
@@ -82,10 +86,12 @@ __all__ = [
|
|
|
82
86
|
"DepolarizingNoiseModel",
|
|
83
87
|
"NoiseModel",
|
|
84
88
|
"NoiseRule",
|
|
89
|
+
"PauliChannel",
|
|
85
90
|
"SI1000NoiseModel",
|
|
86
91
|
"as_noiseless_circuit",
|
|
87
92
|
"get_transversal_automorphism_group",
|
|
88
93
|
"get_transversal_circuit",
|
|
89
94
|
"get_transversal_circuits",
|
|
90
95
|
"get_transversal_ops",
|
|
96
|
+
"get_transversal_s",
|
|
91
97
|
]
|
|
@@ -302,11 +302,10 @@ def get_logical_error_and_discard_rate(
|
|
|
302
302
|
post_select_observables: The observables to post-select on. Any time the decoder's
|
|
303
303
|
prediction for these observable flips do not agree with the actual observable flips, the
|
|
304
304
|
shot is discarded rather than counting as an error.
|
|
305
|
-
dem_to_decode: The detector error model to decode.
|
|
306
|
-
|
|
307
|
-
this method
|
|
308
|
-
|
|
309
|
-
dem_to_decode = dem_arrays.post_selected_on(post_select).simplified().to_dem()
|
|
305
|
+
dem_to_decode: The detector error model to decode. This DEM should include _all_ detectors,
|
|
306
|
+
including any that are post-selected on: the decoder receives every detector bit, for
|
|
307
|
+
consistency with how sinter works. If dem_to_decode is None, this method decodes with
|
|
308
|
+
the (simplified) DEM sampled from circuit_or_dem.
|
|
310
309
|
|
|
311
310
|
Returns:
|
|
312
311
|
A fraction of samples in which at least one observable was decoded incorrectly.
|
|
@@ -318,12 +317,12 @@ def get_logical_error_and_discard_rate(
|
|
|
318
317
|
|
|
319
318
|
if dem_to_decode is not None:
|
|
320
319
|
same_num_observables = dem_to_decode.num_observables == dem.num_observables
|
|
321
|
-
same_num_detectors = dem_to_decode.num_detectors == dem.num_detectors
|
|
320
|
+
same_num_detectors = dem_to_decode.num_detectors == dem.num_detectors
|
|
322
321
|
if not same_num_observables or not same_num_detectors:
|
|
323
322
|
raise ValueError(
|
|
324
323
|
f"Incompatible detector error models."
|
|
325
|
-
"\n(num_detectors, num_observables) in the DEM to sample
|
|
326
|
-
f" {(dem.num_detectors
|
|
324
|
+
"\n(num_detectors, num_observables) in the DEM to sample:"
|
|
325
|
+
f" {(dem.num_detectors, dem.num_observables)}"
|
|
327
326
|
"\n(num_detectors, num_observables) in the DEM to decode:"
|
|
328
327
|
f" {(dem_to_decode.num_detectors, dem_to_decode.num_observables)}"
|
|
329
328
|
)
|
|
@@ -341,31 +340,13 @@ def get_logical_error_and_discard_rate(
|
|
|
341
340
|
|
|
342
341
|
# if applicable, post-select on flag detectors
|
|
343
342
|
if postselection_mask is not None:
|
|
344
|
-
# remove shots in which post-selection detectors fired
|
|
345
343
|
shot_mask = ~np.any(det_data & postselection_mask, axis=1)
|
|
346
344
|
det_data = det_data[shot_mask]
|
|
347
345
|
obs_data = obs_data[shot_mask]
|
|
348
|
-
|
|
349
|
-
# remove post-selected detectors from the detector sample data
|
|
350
|
-
detector_mask = np.ones(dem.num_detectors, dtype=bool)
|
|
351
|
-
detector_mask[list(post_select)] = False
|
|
352
|
-
det_data_unpacked = np.unpackbits(
|
|
353
|
-
det_data, count=dem.num_detectors, bitorder="little", axis=1
|
|
354
|
-
)
|
|
355
|
-
det_data = np.packbits(det_data_unpacked[:, detector_mask], bitorder="little", axis=1)
|
|
356
|
-
|
|
357
|
-
# record the fraction of shots that were discarded
|
|
358
346
|
discard_rate += np.sum(~shot_mask) / num_samples
|
|
359
347
|
|
|
360
|
-
if dem_to_decode is None:
|
|
361
|
-
# remove the post-selected detectors from the DEM
|
|
362
|
-
dem_arrays = dem_arrays.post_selected_on(post_select).simplified()
|
|
363
|
-
dem = dem_arrays.to_dem()
|
|
364
|
-
|
|
365
|
-
# compile a decoder for this detector error model
|
|
366
|
-
compiled_sinter_decoder = sinter_decoder.compile_decoder_for_dem(dem_to_decode or dem)
|
|
367
|
-
|
|
368
348
|
# decode and identify incorrectly predicted observable flips
|
|
349
|
+
compiled_sinter_decoder = sinter_decoder.compile_decoder_for_dem(dem_to_decode or dem)
|
|
369
350
|
predicted_flips = compiled_sinter_decoder.decode_shots_bit_packed(det_data)
|
|
370
351
|
incorrectly_predicted_flips = obs_data ^ predicted_flips
|
|
371
352
|
|
|
@@ -43,11 +43,22 @@ class QubitIDs:
|
|
|
43
43
|
checks_z: tuple[int, ...] = ()
|
|
44
44
|
|
|
45
45
|
def __init__(
|
|
46
|
-
self,
|
|
46
|
+
self,
|
|
47
|
+
data: int | Sequence[int],
|
|
48
|
+
check: int | Sequence[int] = (),
|
|
49
|
+
ancilla: int | Sequence[int] = (),
|
|
47
50
|
) -> None:
|
|
48
|
-
self.data = tuple(data)
|
|
49
|
-
self.
|
|
50
|
-
self.
|
|
51
|
+
self.data = tuple(data if isinstance(data, Sequence) else range(data))
|
|
52
|
+
check_start = self.data[-1] + 1 if self.data else 0
|
|
53
|
+
self.check = tuple(
|
|
54
|
+
check if isinstance(check, Sequence) else range(check_start, check_start + check)
|
|
55
|
+
)
|
|
56
|
+
ancilla_start = self.check[-1] + 1 if self.check else check_start
|
|
57
|
+
self.ancilla = tuple(
|
|
58
|
+
ancilla
|
|
59
|
+
if isinstance(ancilla, Sequence)
|
|
60
|
+
else range(ancilla_start, ancilla_start + ancilla)
|
|
61
|
+
)
|
|
51
62
|
|
|
52
63
|
def __iter__(self) -> Iterator[tuple[int, ...]]:
|
|
53
64
|
"""Iterate over the collections of qubits tracked by this QubitIDs object."""
|
|
@@ -111,8 +122,8 @@ class Record(Mapping[Hashable, list[int]]):
|
|
|
111
122
|
"""An organized record of events in a Stim circuit.
|
|
112
123
|
|
|
113
124
|
A record is essentially a dictionary that maps some key (such as a qubit index) to an ordered
|
|
114
|
-
list of the events (such as measurements or detectors) associated with that key. The events
|
|
115
|
-
a Record keeps track of are assumed to be indexed from zero.
|
|
125
|
+
list of the events (such as measurements or detectors) associated with that key. The events
|
|
126
|
+
that a Record keeps track of are assumed to be indexed from zero.
|
|
116
127
|
|
|
117
128
|
Record is subclassed by MeasurementRecord to keep track of measurements in a circuit, and
|
|
118
129
|
by DetectorRecord to keep track of the detectors in a circuit.
|
|
@@ -136,9 +147,6 @@ class Record(Mapping[Hashable, list[int]]):
|
|
|
136
147
|
def __repr__(self) -> str:
|
|
137
148
|
return f"{self.__class__.__name__}({dict(self.key_to_events)})"
|
|
138
149
|
|
|
139
|
-
def __str__(self) -> str:
|
|
140
|
-
return repr(self)
|
|
141
|
-
|
|
142
150
|
def __len__(self) -> int:
|
|
143
151
|
"""The number of keys associated with events in this record."""
|
|
144
152
|
return len(self.key_to_events)
|
|
@@ -168,9 +176,10 @@ class Record(Mapping[Hashable, list[int]]):
|
|
|
168
176
|
def append(self, record: Mapping[Hashable, Iterable[int] | int], repeat: int = 1) -> None:
|
|
169
177
|
"""Append the given record to this one.
|
|
170
178
|
|
|
171
|
-
All event numbers in the appended record are increased by the number of events in the
|
|
172
|
-
record. That is, if the current record holds n events numbered from 0 to n - 1,
|
|
173
|
-
(0, 1, ...) in the appended record are added to the current record as
|
|
179
|
+
All event numbers in the appended record are increased by the number of events in the
|
|
180
|
+
current record. That is, if the current record holds n events numbered from 0 to n - 1,
|
|
181
|
+
then events (0, 1, ...) in the appended record are added to the current record as
|
|
182
|
+
(n, n+1, ...).
|
|
174
183
|
"""
|
|
175
184
|
assert repeat >= 0
|
|
176
185
|
_record = { # convert input record into dict[Hashable, list[int]]
|
|
@@ -19,14 +19,27 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import functools
|
|
21
21
|
from collections.abc import Mapping, Sequence
|
|
22
|
-
from typing import Callable, ParamSpec, TypeVar
|
|
22
|
+
from typing import TYPE_CHECKING, Callable, ParamSpec, TypeVar
|
|
23
23
|
|
|
24
|
+
import galois
|
|
24
25
|
import numpy as np
|
|
25
26
|
import numpy.typing as npt
|
|
26
27
|
import stim
|
|
27
28
|
|
|
28
29
|
from qldpc import codes, math
|
|
29
|
-
|
|
30
|
+
|
|
31
|
+
####################################################################################################
|
|
32
|
+
# try to import tsim and define a circuit type that may be either of stim.Circuit or tsim.Circuit
|
|
33
|
+
try:
|
|
34
|
+
import tsim
|
|
35
|
+
|
|
36
|
+
stim_or_tsim_Circuit = TypeVar("stim_or_tsim_Circuit", stim.Circuit, tsim.Circuit)
|
|
37
|
+
except ImportError: # pragma: no cover
|
|
38
|
+
if not TYPE_CHECKING:
|
|
39
|
+
tsim = None
|
|
40
|
+
stim_or_tsim_Circuit = TypeVar("stim_or_tsim_Circuit", bound=stim.Circuit)
|
|
41
|
+
####################################################################################################
|
|
42
|
+
|
|
30
43
|
|
|
31
44
|
CircuitOrTableau = TypeVar("CircuitOrTableau", stim.Circuit, stim.Tableau)
|
|
32
45
|
Params = ParamSpec("Params")
|
|
@@ -39,7 +52,7 @@ def restrict_to_qubits(
|
|
|
39
52
|
|
|
40
53
|
@functools.wraps(func)
|
|
41
54
|
def qubit_func(*args: Params.args, **kwargs: Params.kwargs) -> stim.Circuit:
|
|
42
|
-
if any(isinstance(arg, codes.QuditCode) and arg.field is not GF2 for arg in args):
|
|
55
|
+
if any(isinstance(arg, codes.QuditCode) and arg.field is not galois.GF2 for arg in args):
|
|
43
56
|
raise ValueError("Circuit methods are only supported for qubit codes")
|
|
44
57
|
return func(*args, **kwargs)
|
|
45
58
|
|
|
@@ -47,8 +60,11 @@ def restrict_to_qubits(
|
|
|
47
60
|
|
|
48
61
|
|
|
49
62
|
def with_remapped_qubits(
|
|
50
|
-
circuit:
|
|
51
|
-
|
|
63
|
+
circuit: stim_or_tsim_Circuit,
|
|
64
|
+
qubit_map: Mapping[int, int] | Sequence[int],
|
|
65
|
+
*,
|
|
66
|
+
inverse: bool = False,
|
|
67
|
+
) -> stim_or_tsim_Circuit:
|
|
52
68
|
"""The same circuit, but with relabeled qubits.
|
|
53
69
|
|
|
54
70
|
Qubits not in qubit_map get mapped to themselves.
|
|
@@ -60,8 +76,12 @@ def with_remapped_qubits(
|
|
|
60
76
|
inverse: If True, invert the provided qubit_map. Default: False.
|
|
61
77
|
|
|
62
78
|
Returns:
|
|
63
|
-
|
|
79
|
+
The input circuit with remapped qubits.
|
|
64
80
|
"""
|
|
81
|
+
if tsim is not None and isinstance(circuit, tsim.Circuit):
|
|
82
|
+
output = with_remapped_qubits(circuit.stim_circuit, qubit_map, inverse=inverse)
|
|
83
|
+
return tsim.Circuit.from_stim_program(output)
|
|
84
|
+
|
|
65
85
|
qubit_map = (
|
|
66
86
|
qubit_map
|
|
67
87
|
if isinstance(qubit_map, Mapping)
|
|
@@ -81,7 +101,7 @@ def with_remapped_qubits(
|
|
|
81
101
|
new_circuit.append(block)
|
|
82
102
|
|
|
83
103
|
else:
|
|
84
|
-
new_targets = [
|
|
104
|
+
new_targets = [remap_qubit_target(target, qubit_map) for target in op.targets_copy()]
|
|
85
105
|
new_op = stim.CircuitInstruction(
|
|
86
106
|
name=op.name, targets=new_targets, gate_args=op.gate_args_copy(), tag=op.tag
|
|
87
107
|
)
|
|
@@ -90,6 +110,25 @@ def with_remapped_qubits(
|
|
|
90
110
|
return new_circuit
|
|
91
111
|
|
|
92
112
|
|
|
113
|
+
def remap_qubit_target(target: stim.GateTarget, qubit_map: Mapping[int, int]) -> stim.GateTarget:
|
|
114
|
+
"""Remap the qubit addressed by a stim.GateTarget, if any."""
|
|
115
|
+
if target.qubit_value is None:
|
|
116
|
+
return target
|
|
117
|
+
|
|
118
|
+
new_qubit_value = qubit_map.get(target.qubit_value, target.qubit_value)
|
|
119
|
+
if target.is_x_target or target.is_z_target or target.is_y_target:
|
|
120
|
+
return stim.target_pauli(
|
|
121
|
+
new_qubit_value,
|
|
122
|
+
target.pauli_type,
|
|
123
|
+
invert=target.is_inverted_result_target,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if target.is_inverted_result_target:
|
|
127
|
+
return stim.target_inv(new_qubit_value)
|
|
128
|
+
|
|
129
|
+
return stim.GateTarget(new_qubit_value)
|
|
130
|
+
|
|
131
|
+
|
|
93
132
|
def get_pauli_product_measurements(
|
|
94
133
|
pauli_strings: Sequence[stim.PauliString] | npt.NDArray[np.int_],
|
|
95
134
|
qubits: Sequence[int] | None = None,
|
|
@@ -123,22 +162,3 @@ def get_unaddressed_measurements(circuit: stim.Circuit) -> list[int]:
|
|
|
123
162
|
measurements[target.value] for target in instruction.targets_copy()
|
|
124
163
|
}
|
|
125
164
|
return sorted(set(measurements) - addressed_measurements)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _remap_target(target: stim.GateTarget, qubit_map: Mapping[int, int]) -> stim.GateTarget:
|
|
129
|
-
"""Remap the qubit addressed by a stim.GateTarget, if any."""
|
|
130
|
-
if target.qubit_value is None:
|
|
131
|
-
return target
|
|
132
|
-
|
|
133
|
-
new_qubit_value = qubit_map.get(target.qubit_value, target.qubit_value)
|
|
134
|
-
if target.is_x_target or target.is_z_target or target.is_y_target:
|
|
135
|
-
return stim.target_pauli(
|
|
136
|
-
new_qubit_value,
|
|
137
|
-
target.pauli_type,
|
|
138
|
-
invert=target.is_inverted_result_target,
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
if target.is_inverted_result_target:
|
|
142
|
-
return stim.target_inv(new_qubit_value)
|
|
143
|
-
|
|
144
|
-
return stim.GateTarget(new_qubit_value)
|
|
@@ -23,6 +23,7 @@ import numpy as np
|
|
|
23
23
|
import pytest
|
|
24
24
|
import stim
|
|
25
25
|
import sympy.combinatorics as comb
|
|
26
|
+
import tsim
|
|
26
27
|
|
|
27
28
|
from qldpc import circuits, codes
|
|
28
29
|
from qldpc.math import symplectic_conjugate
|
|
@@ -95,6 +96,13 @@ def test_qubit_remap(pytestconfig: pytest.Config, num_qubits: int = 8) -> None:
|
|
|
95
96
|
circuit_b = stim.Circuit("MPP X1*!Y3 \n M !4")
|
|
96
97
|
assert circuit_a == circuit_b
|
|
97
98
|
|
|
99
|
+
# a tsim.Circuit input is remapped and returned as a tsim.Circuit
|
|
100
|
+
tsim_remapped = circuits.with_remapped_qubits(
|
|
101
|
+
tsim.Circuit.from_stim_program(circuit), qubit_map
|
|
102
|
+
)
|
|
103
|
+
assert isinstance(tsim_remapped, tsim.Circuit)
|
|
104
|
+
assert tsim_remapped.stim_circuit == circuits.with_remapped_qubits(circuit, qubit_map)
|
|
105
|
+
|
|
98
106
|
|
|
99
107
|
def test_finding_unaddressed_measurements() -> None:
|
|
100
108
|
"""Identify measurements in a circuit that are not addressed by any detectors."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import qldpc.circuits.common as c
|
|
4
|
+
c._TSIM_BRANCH_HITS = 0
|
|
5
|
+
_orig = c.with_remapped_qubits
|
|
6
|
+
import functools
|
|
7
|
+
def wrapped(circuit, *a, **k):
|
|
8
|
+
if c.tsim is not None and isinstance(circuit, c.tsim.Circuit):
|
|
9
|
+
c._TSIM_BRANCH_HITS += 1
|
|
10
|
+
return _orig(circuit, *a, **k)
|
|
11
|
+
c.with_remapped_qubits = wrapped
|
|
12
|
+
def pytest_sessionfinish(session, exitstatus):
|
|
13
|
+
print("\nTSIM_BRANCH_HITS_ON_WITH_REMAPPED:", c._TSIM_BRANCH_HITS)
|
|
@@ -20,11 +20,11 @@ from __future__ import annotations
|
|
|
20
20
|
from collections.abc import Sequence
|
|
21
21
|
from typing import ParamSpec, TypeVar
|
|
22
22
|
|
|
23
|
+
import galois
|
|
23
24
|
import numpy as np
|
|
24
25
|
import stim
|
|
25
26
|
|
|
26
27
|
from qldpc import codes, math
|
|
27
|
-
from qldpc.abstract import GF2
|
|
28
28
|
from qldpc.objects import Pauli
|
|
29
29
|
|
|
30
30
|
from .bookkeeping import QubitIDs
|
|
@@ -198,7 +198,7 @@ def get_state_stabilizers(
|
|
|
198
198
|
cols_other_z = other_qubits + num_qubits
|
|
199
199
|
|
|
200
200
|
# build a matrix of stabilizers for the entire circuit output, determined by the flows
|
|
201
|
-
matrix = GF2.Zeros((num_rows, num_columns))
|
|
201
|
+
matrix = galois.GF2.Zeros((num_rows, num_columns))
|
|
202
202
|
for gg, flow in enumerate(flow_generators):
|
|
203
203
|
pauli_string = flow.output_copy()
|
|
204
204
|
if pauli_string:
|
|
@@ -226,7 +226,7 @@ def get_state_stabilizers(
|
|
|
226
226
|
|
|
227
227
|
# identify stabilizers that are supported entirely on the data qubits of the code
|
|
228
228
|
stabilizers = []
|
|
229
|
-
for row in matrix.
|
|
229
|
+
for row in matrix.row_space():
|
|
230
230
|
state_xs = row[cols_state_x]
|
|
231
231
|
state_zs = row[cols_state_z]
|
|
232
232
|
other_xs = row[cols_other_x]
|
|
@@ -294,17 +294,27 @@ def _get_logical_tableau_from_code_data(
|
|
|
294
294
|
sector_l = slice(dimension)
|
|
295
295
|
sector_g = slice(dimension, dimension + gauge_dimension)
|
|
296
296
|
sector_s = slice(dimension + gauge_dimension, len(encoder))
|
|
297
|
-
x2x, x2z, z2x, z2z,
|
|
297
|
+
x2x, x2z, z2x, z2z, _, z_signs = decoded_tableau.to_numpy()
|
|
298
298
|
|
|
299
299
|
# sanity check: stabilizers, logicals, and gauge operators should not pick up destabilizers
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
ops_acquired_destabilizers = (
|
|
301
|
+
np.any(z2x[:, sector_s])
|
|
302
|
+
or np.any(x2x[sector_l, sector_s])
|
|
303
|
+
or np.any(x2x[sector_g, sector_s])
|
|
304
|
+
)
|
|
303
305
|
|
|
304
306
|
# sanity check: gauge operators should not pick up logical factors
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
gauges_acquired_logicals = (
|
|
308
|
+
np.any(x2x[sector_g, sector_l])
|
|
309
|
+
or np.any(x2z[sector_g, sector_l])
|
|
310
|
+
or np.any(z2x[sector_g, sector_l])
|
|
311
|
+
or np.any(z2z[sector_g, sector_l])
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# sanity check: no stabilizers get flipped
|
|
315
|
+
stabilizers_flipped = np.any(z_signs[sector_s])
|
|
316
|
+
|
|
317
|
+
if ops_acquired_destabilizers or gauges_acquired_logicals or stabilizers_flipped:
|
|
318
|
+
raise ValueError("The provided physical circuit does not implement a logical operation")
|
|
309
319
|
|
|
310
320
|
return logical_tableau
|
|
@@ -42,8 +42,8 @@ def test_encoding_circuit(pytestconfig: pytest.Config) -> None:
|
|
|
42
42
|
size=code.dimension + code.gauge_dimension,
|
|
43
43
|
)
|
|
44
44
|
basis_prep = stim.Circuit()
|
|
45
|
-
basis_prep.append("H", np.
|
|
46
|
-
basis_prep.append("S", np.
|
|
45
|
+
basis_prep.append("H", np.flatnonzero(bases != Pauli.Z))
|
|
46
|
+
basis_prep.append("S", np.flatnonzero(bases == Pauli.Y))
|
|
47
47
|
simulator.do(basis_prep)
|
|
48
48
|
|
|
49
49
|
encoder = circuits.get_encoding_circuit(code, only_zero=only_zero)
|
|
@@ -104,6 +104,15 @@ def test_logical_tableau() -> None:
|
|
|
104
104
|
reconstructed_logical_tableau = circuits.get_logical_tableau(code, physical_circuit)
|
|
105
105
|
assert logical_circuit.to_tableau() == reconstructed_logical_tableau
|
|
106
106
|
|
|
107
|
+
# logical Pauli operations (which flip logical operator signs) are valid logical operations
|
|
108
|
+
logical_x = math.op_to_string(code.get_logical_ops(Pauli.X, symplectic=True)[0])
|
|
109
|
+
reconstructed_logical_tableau = circuits.get_logical_tableau(code, logical_x.to_tableau())
|
|
110
|
+
assert stim.Circuit("X 0").to_tableau() == reconstructed_logical_tableau
|
|
111
|
+
|
|
112
|
+
# a physical circuit that does not preserve the code space is not a logical operation
|
|
113
|
+
with pytest.raises(ValueError, match="does not implement a logical operation"):
|
|
114
|
+
circuits.get_logical_tableau(code, stim.Circuit("X 0"))
|
|
115
|
+
|
|
107
116
|
|
|
108
117
|
def test_state_stabilizers(pytestconfig: pytest.Config) -> None:
|
|
109
118
|
"""Identify the stabilizers of state prepared by a circuit."""
|
|
@@ -67,9 +67,9 @@ class AlphaSyndrome(SyndromeMeasurementStrategy):
|
|
|
67
67
|
This strategy uses a Monte Carlo tree serch (MCTS) to construct a syndrome measurement
|
|
68
68
|
circuit that minimizes logical error rates.
|
|
69
69
|
|
|
70
|
-
The MCTS requires building and simulating noisy evaluation circuits, which naturally
|
|
71
|
-
defining a noise model. Computing a logical error rate, in turn, requires
|
|
72
|
-
decoder. The "decoder' and "custom_decoders" arguments to AlphaSyndrome are
|
|
70
|
+
The MCTS requires building and simulating noisy evaluation circuits, which naturally
|
|
71
|
+
requires defining a noise model. Computing a logical error rate, in turn, requires
|
|
72
|
+
specifying a decoder. The "decoder' and "custom_decoders" arguments to AlphaSyndrome are
|
|
73
73
|
|
|
74
74
|
Args:
|
|
75
75
|
noise_model: The noise model append to the syndrome measurement circuit.
|
|
@@ -196,7 +196,8 @@ def get_memory_experiment_parts(
|
|
|
196
196
|
See help(qldpc.circuits.get_memory_experiment) for additional information.
|
|
197
197
|
|
|
198
198
|
Returns:
|
|
199
|
-
initialization: A circuit that sets the coordinates and initializes the state of data
|
|
199
|
+
initialization: A circuit that sets the coordinates and initializes the state of data
|
|
200
|
+
qubits.
|
|
200
201
|
qec_cycle: A circuit for one logical QEC cycle, with num_rounds syndrome measurements.
|
|
201
202
|
readout: A circuit that reads out final stabilizers.
|
|
202
203
|
measurement_record: A record of all measurements in the above circuits.
|
|
@@ -275,7 +276,7 @@ def _get_basis_memory_experiment_parts(
|
|
|
275
276
|
readout.append("SHIFT_COORDS", [], (1, 0, 0))
|
|
276
277
|
check_support = code.get_matrix(basis)
|
|
277
278
|
for kk, check_id in enumerate(basis_check_ids):
|
|
278
|
-
data_support = np.
|
|
279
|
+
data_support = np.flatnonzero(check_support[kk])
|
|
279
280
|
readout.append(
|
|
280
281
|
"DETECTOR",
|
|
281
282
|
[measurement_record.get_target_rec(data_ids[qq]) for qq in data_support]
|
|
@@ -70,8 +70,8 @@ class EdgeColoring(SyndromeMeasurementStrategy):
|
|
|
70
70
|
Args:
|
|
71
71
|
strategy: The graph coloration strategy passed to nx.greedy_color when coloring edges.
|
|
72
72
|
Defaults to "smallest_last".
|
|
73
|
-
subgraph_kwargs: Keyword arguments to pass to code.get_syndrome_subgraphs when
|
|
74
|
-
the Tanner subgraphs of a code.
|
|
73
|
+
subgraph_kwargs: Keyword arguments to pass to code.get_syndrome_subgraphs when
|
|
74
|
+
retrieving the Tanner subgraphs of a code.
|
|
75
75
|
"""
|
|
76
76
|
self.strategy = strategy
|
|
77
77
|
self.subgraph_kwargs = subgraph_kwargs
|