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.
Files changed (70) hide show
  1. {qldpc-0.3.0 → qldpc-0.3.2}/PKG-INFO +4 -1
  2. {qldpc-0.3.0 → qldpc-0.3.2}/pyproject.toml +5 -2
  3. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/__init__.py +0 -1
  4. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/groups.py +2 -4
  5. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/groups_test.py +4 -4
  6. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/rings.py +2 -3
  7. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/cache.py +13 -7
  8. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/__init__.py +6 -0
  9. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/benchmarking.py +8 -27
  10. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/bookkeeping.py +21 -12
  11. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/common.py +46 -26
  12. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/common_test.py +8 -0
  13. qldpc-0.3.2/src/qldpc/circuits/conftest_probe.py +13 -0
  14. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/encoding.py +21 -11
  15. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/encoding_test.py +11 -2
  16. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/alpha_syndrome.py +3 -3
  17. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/memory.py +3 -2
  18. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/syndrome_measurement.py +2 -2
  19. qldpc-0.3.2/src/qldpc/circuits/noise_model.py +1751 -0
  20. qldpc-0.3.2/src/qldpc/circuits/noise_model_test.py +1188 -0
  21. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/transversal.py +45 -0
  22. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/transversal_test.py +18 -0
  23. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/classical.py +8 -9
  24. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/common.py +126 -51
  25. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/common_test.py +22 -0
  26. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/distance.py +1 -1
  27. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/quantum.py +11 -12
  28. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/__init__.py +5 -1
  29. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/custom.py +267 -150
  30. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/custom_test.py +41 -1
  31. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/dems.py +78 -45
  32. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/dems_test.py +11 -0
  33. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/sinter.py +17 -13
  34. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/sinter_test.py +3 -3
  35. qldpc-0.3.2/src/qldpc/math.py +360 -0
  36. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/math_test.py +41 -6
  37. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/objects.py +2 -3
  38. qldpc-0.3.0/src/qldpc/circuits/noise_model.py +0 -864
  39. qldpc-0.3.0/src/qldpc/circuits/noise_model_test.py +0 -298
  40. qldpc-0.3.0/src/qldpc/math.py +0 -168
  41. {qldpc-0.3.0 → qldpc-0.3.2}/README.md +0 -0
  42. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/__init__.py +0 -0
  43. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/linalg.py +0 -0
  44. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/linalg_test.py +0 -0
  45. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/rings_test.py +0 -0
  46. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/wedderburn_artin.py +0 -0
  47. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/abstract/wedderburn_artin_test.py +0 -0
  48. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/cache_test.py +0 -0
  49. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/benchmarking_test.py +0 -0
  50. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/bookkeeping_test.py +0 -0
  51. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/__init__.py +0 -0
  52. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/alpha_syndrome_test.py +0 -0
  53. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/memory_test.py +0 -0
  54. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/circuits/memory/syndrome_measurement_test.py +0 -0
  55. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/__init__.py +0 -0
  56. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/classical_test.py +0 -0
  57. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/distance_test.py +0 -0
  58. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/codes/quantum_test.py +0 -0
  59. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/conftest.py +0 -0
  60. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/retrieval.py +0 -0
  61. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/decoders/retrieval_test.py +0 -0
  62. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/__init__.py +0 -0
  63. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/codes.py +0 -0
  64. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/codes_test.py +0 -0
  65. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/gap.py +0 -0
  66. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/gap_test.py +0 -0
  67. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/groups.py +0 -0
  68. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/external/groups_test.py +0 -0
  69. {qldpc-0.3.0 → qldpc-0.3.2}/src/qldpc/objects_test.py +0 -0
  70. {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.0
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.0"
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
@@ -1,5 +1,4 @@
1
1
  from .groups import (
2
- GF2,
3
2
  PSL,
4
3
  SL,
5
4
  AbelianGroup,
@@ -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.where(group.inversion_matrix[:, group.index(gg)]),
136
- [[group.index(~gg)]],
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.where(group.adjoint_lift(aa)[:, group.index(bb)]),
160
- [[group.index(aa * bb * ~aa)]],
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 and remove all-zero rows
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).row_reduce()
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 os
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(cache_name: str, *, cache_dir: str | None = None) -> str:
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 os.path.join(platformdirs.user_cache_dir(), "qldpc")
36
- return os.path.join(cache_dir, cache_name)
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(cache_name: str, *, cache_dir: str | None = None) -> diskcache.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(cache_name: str, key: Hashable, *, cache_dir: str | None = None) -> None:
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. If post-selecting, this DEM should _not_
306
- include any of the the detectors that are post-selected on. If dem_to_decode is None,
307
- this method builds
308
- dem_arrays = decoders.DetectorErrorModelArrays(dem)
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 - len(post_select)
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 (after post-selection):"
326
- f" {(dem.num_detectors - len(post_select), dem.num_observables)}"
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, data: Sequence[int], check: Sequence[int] = (), ancilla: Sequence[int] = ()
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.check = tuple(check)
50
- self.ancilla = tuple(ancilla)
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 that
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 current
172
- record. That is, if the current record holds n events numbered from 0 to n - 1, then events
173
- (0, 1, ...) in the appended record are added to the current record as (n, n+1, ...).
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
- from qldpc.abstract import GF2
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: stim.Circuit, qubit_map: Mapping[int, int] | Sequence[int], *, inverse: bool = False
51
- ) -> stim.Circuit:
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
- stim.Circuit: A remapped circuit.
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 = [_remap_target(target, qubit_map) for target in op.targets_copy()]
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.row_reduce():
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, *_ = decoded_tableau.to_numpy()
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
- assert not np.any(z2x[:, sector_s])
301
- assert not np.any(x2x[sector_l, sector_s])
302
- assert not np.any(x2x[sector_g, sector_s])
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
- assert not np.any(x2x[sector_g, sector_l])
306
- assert not np.any(x2z[sector_g, sector_l])
307
- assert not np.any(z2x[sector_g, sector_l])
308
- assert not np.any(z2z[sector_g, sector_l])
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.where(bases != Pauli.Z)[0])
46
- basis_prep.append("S", np.where(bases == Pauli.Y)[0])
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 requires
71
- defining a noise model. Computing a logical error rate, in turn, requires specifying a
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 qubits.
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.where(check_support[kk])[0]
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 retrieving
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