qLDPC 0.3.1__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 (68) hide show
  1. {qldpc-0.3.1 → qldpc-0.3.2}/PKG-INFO +1 -1
  2. {qldpc-0.3.1 → qldpc-0.3.2}/pyproject.toml +1 -1
  3. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/abstract/groups_test.py +4 -4
  4. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/abstract/rings.py +2 -3
  5. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/__init__.py +2 -0
  6. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/benchmarking.py +8 -27
  7. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/bookkeeping.py +15 -4
  8. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/common.py +24 -4
  9. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/common_test.py +8 -0
  10. qldpc-0.3.2/src/qldpc/circuits/conftest_probe.py +13 -0
  11. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/encoding.py +19 -9
  12. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/encoding_test.py +11 -2
  13. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/memory/memory.py +1 -1
  14. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/noise_model.py +11 -21
  15. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/transversal.py +45 -0
  16. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/transversal_test.py +18 -0
  17. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/codes/common.py +100 -28
  18. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/codes/common_test.py +22 -0
  19. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/decoders/custom.py +256 -139
  20. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/decoders/custom_test.py +41 -1
  21. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/decoders/dems.py +1 -1
  22. qldpc-0.3.2/src/qldpc/math.py +360 -0
  23. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/math_test.py +41 -6
  24. qldpc-0.3.1/src/qldpc/math.py +0 -168
  25. {qldpc-0.3.1 → qldpc-0.3.2}/README.md +0 -0
  26. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/__init__.py +0 -0
  27. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/abstract/__init__.py +0 -0
  28. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/abstract/groups.py +0 -0
  29. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/abstract/linalg.py +0 -0
  30. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/abstract/linalg_test.py +0 -0
  31. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/abstract/rings_test.py +0 -0
  32. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/abstract/wedderburn_artin.py +0 -0
  33. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/abstract/wedderburn_artin_test.py +0 -0
  34. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/cache.py +0 -0
  35. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/cache_test.py +0 -0
  36. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/benchmarking_test.py +0 -0
  37. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/bookkeeping_test.py +0 -0
  38. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/memory/__init__.py +0 -0
  39. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/memory/alpha_syndrome.py +0 -0
  40. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/memory/alpha_syndrome_test.py +0 -0
  41. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/memory/memory_test.py +0 -0
  42. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/memory/syndrome_measurement.py +0 -0
  43. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/memory/syndrome_measurement_test.py +0 -0
  44. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/circuits/noise_model_test.py +0 -0
  45. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/codes/__init__.py +0 -0
  46. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/codes/classical.py +0 -0
  47. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/codes/classical_test.py +0 -0
  48. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/codes/distance.py +0 -0
  49. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/codes/distance_test.py +0 -0
  50. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/codes/quantum.py +0 -0
  51. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/codes/quantum_test.py +0 -0
  52. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/conftest.py +0 -0
  53. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/decoders/__init__.py +0 -0
  54. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/decoders/dems_test.py +0 -0
  55. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/decoders/retrieval.py +0 -0
  56. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/decoders/retrieval_test.py +0 -0
  57. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/decoders/sinter.py +0 -0
  58. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/decoders/sinter_test.py +0 -0
  59. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/external/__init__.py +0 -0
  60. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/external/codes.py +0 -0
  61. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/external/codes_test.py +0 -0
  62. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/external/gap.py +0 -0
  63. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/external/gap_test.py +0 -0
  64. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/external/groups.py +0 -0
  65. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/external/groups_test.py +0 -0
  66. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/objects.py +0 -0
  67. {qldpc-0.3.1 → qldpc-0.3.2}/src/qldpc/objects_test.py +0 -0
  68. {qldpc-0.3.1 → 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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qLDPC"
3
- version = "0.3.1"
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"
@@ -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
@@ -50,6 +50,7 @@ from .transversal import (
50
50
  get_transversal_circuit,
51
51
  get_transversal_circuits,
52
52
  get_transversal_ops,
53
+ get_transversal_s,
53
54
  )
54
55
 
55
56
  __all__ = [
@@ -92,4 +93,5 @@ __all__ = [
92
93
  "get_transversal_circuit",
93
94
  "get_transversal_circuits",
94
95
  "get_transversal_ops",
96
+ "get_transversal_s",
95
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."""
@@ -19,7 +19,7 @@ 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
24
  import galois
25
25
  import numpy as np
@@ -28,6 +28,19 @@ import stim
28
28
 
29
29
  from qldpc import codes, math
30
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
+
43
+
31
44
  CircuitOrTableau = TypeVar("CircuitOrTableau", stim.Circuit, stim.Tableau)
32
45
  Params = ParamSpec("Params")
33
46
 
@@ -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)
@@ -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)
@@ -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."""
@@ -276,7 +276,7 @@ def _get_basis_memory_experiment_parts(
276
276
  readout.append("SHIFT_COORDS", [], (1, 0, 0))
277
277
  check_support = code.get_matrix(basis)
278
278
  for kk, check_id in enumerate(basis_check_ids):
279
- data_support = np.where(check_support[kk])[0]
279
+ data_support = np.flatnonzero(check_support[kk])
280
280
  readout.append(
281
281
  "DETECTOR",
282
282
  [measurement_record.get_target_rec(data_ids[qq]) for qq in data_support]
@@ -110,20 +110,10 @@ import itertools
110
110
  import math
111
111
  import re
112
112
  from collections.abc import Callable, Collection, Iterable, Iterator, Mapping
113
- from typing import TYPE_CHECKING, TypeVar
114
113
 
115
114
  import stim
116
115
 
117
- try:
118
- import tsim
119
-
120
- stim_or_tsim_Circuit = TypeVar("stim_or_tsim_Circuit", stim.Circuit, tsim.Circuit)
121
- except ImportError: # pragma: no cover
122
- if not TYPE_CHECKING:
123
- tsim = None
124
- stim_or_tsim_Circuit = TypeVar("stim_or_tsim_Circuit", bound=stim.Circuit)
125
-
126
- from .common import with_remapped_qubits
116
+ from .common import stim_or_tsim_Circuit, tsim, with_remapped_qubits
127
117
 
128
118
  ####################################################################################################
129
119
  # global constants
@@ -238,7 +228,8 @@ CORRELATED_ERROR_NAMES = frozenset({"CORRELATED_ERROR", "E", "ELSE_CORRELATED_ER
238
228
  def as_noiseless_circuit(circuit: stim_or_tsim_Circuit) -> stim_or_tsim_Circuit:
239
229
  """Wrap a circuit in a noiseless, one-repitition stim.CircuitRepeatBlock."""
240
230
  if tsim is not None and isinstance(circuit, tsim.Circuit):
241
- return tsim.Circuit.from_stim_program(as_noiseless_circuit(circuit.stim_circuit))
231
+ output = as_noiseless_circuit(circuit.stim_circuit)
232
+ return tsim.Circuit.from_stim_program(output)
242
233
  block = stim.CircuitRepeatBlock(repeat_count=1, body=circuit.copy(), tag=DEFAULT_IMMUNE_OP_TAG)
243
234
  noiseless_circuit = stim.Circuit()
244
235
  noiseless_circuit.append(block)
@@ -930,16 +921,15 @@ class NoiseModel:
930
921
  The input circuit with added noise.
931
922
  """
932
923
  if tsim is not None and isinstance(circuit, tsim.Circuit):
933
- return tsim.Circuit.from_stim_program(
934
- self.noisy_circuit(
935
- circuit.stim_circuit,
936
- system_qubits=system_qubits,
937
- immune_qubits=immune_qubits,
938
- immune_op_tag=immune_op_tag,
939
- immunize_gates=immunize_gates,
940
- insert_ticks=insert_ticks,
941
- )
924
+ output = self.noisy_circuit(
925
+ circuit.stim_circuit,
926
+ system_qubits=system_qubits,
927
+ immune_qubits=immune_qubits,
928
+ immune_op_tag=immune_op_tag,
929
+ immunize_gates=immunize_gates,
930
+ insert_ticks=insert_ticks,
942
931
  )
932
+ return tsim.Circuit.from_stim_program(output)
943
933
 
944
934
  system_qubits = frozenset(
945
935
  range(circuit.num_qubits) if system_qubits is None else system_qubits
@@ -32,6 +32,51 @@ from .common import restrict_to_qubits
32
32
  from .encoding import _get_logical_tableau_from_code_data, get_encoder_and_decoder
33
33
 
34
34
 
35
+ @restrict_to_qubits
36
+ def get_transversal_s(code: codes.CSSCode, *, validate: bool = True) -> stim.Circuit:
37
+ """Get a physical circuit for a transversal logical S = diag(1, i) gate of the code.
38
+
39
+ The returned circuit applies one physical S or S_DAG gate to each qubit, and thereby enacts
40
+ a logical S gate on every logical qubit of the code.
41
+
42
+ This construction only supports self-dual codes with equivalent logicals (SWEL):
43
+ 1. Self dual = CSS code with identical X and Z stabilizers.
44
+ 2. Equivalent logicals = applying a Hadamard to every physical qubit enacts a logical Hadamard
45
+ on every logical qubit. This property depends on the choice of logical operator basis.
46
+ """
47
+ if validate:
48
+ logs_z = code.get_logical_ops(Pauli.Z)
49
+ if not np.array_equal(
50
+ code.canonicalized.matrix_x, code.canonicalized.matrix_z
51
+ ) or not np.array_equal(logs_z @ logs_z.T, np.eye(code.dimension, dtype=int)):
52
+ raise ValueError(
53
+ "Transversal S gate construction currently only supports SWEL stabilizer codes"
54
+ )
55
+
56
+ # Build a test circuit: physical S on all qubits
57
+ test_circuit = stim.Circuit()
58
+ test_circuit.append("S", range(len(code)))
59
+ test_tableau = test_circuit.to_tableau()
60
+
61
+ # Identify Pauli corrections in the "decoded" frame of logicals/stabilizers/destabilizers.
62
+ encoder, decoder = get_encoder_and_decoder(code)
63
+ decoded_tableau = encoder.then(test_tableau).then(decoder)
64
+ *_, x_signs, z_signs = decoded_tableau.to_numpy()
65
+ # A flip of an X-type operator is corrected by its Z-type dual, and vice versa.
66
+ decoded_frame_correction = stim.PauliString.from_numpy(xs=z_signs, zs=x_signs)
67
+
68
+ # Map the decoded-frame Pauli correction back to a physical Pauli. For SWEL codes this
69
+ # correction is purely Z-type (mod stabilizers), and appending a Z after an S makes it S_DAG,
70
+ # so we can realize the correction by emitting S_DAG (rather than S) on the corrected qubits.
71
+ physical_correction = decoded_frame_correction.after(encoder, targets=range(len(code)))
72
+ _, physical_zs = physical_correction.to_numpy()
73
+
74
+ circuit = stim.Circuit()
75
+ circuit.append("S", np.flatnonzero(~physical_zs))
76
+ circuit.append("S_DAG", np.flatnonzero(physical_zs))
77
+ return circuit
78
+
79
+
35
80
  @restrict_to_qubits
36
81
  def get_transversal_ops(
37
82
  code: codes.QuditCode,
@@ -26,6 +26,24 @@ import stim
26
26
  from qldpc import circuits, codes, external
27
27
 
28
28
 
29
+ def test_transversal_s() -> None:
30
+ """Build a physical circuit for the transversal logical S gate of a SWEL code."""
31
+ # the Steane code is SWEL, so its logical S gate is transversal
32
+ code = codes.SteaneCode()
33
+ circuit = circuits.get_transversal_s(code)
34
+
35
+ # the circuit is built entirely from single-qubit S and S_DAG gates (no Pauli corrections)
36
+ assert {instruction.name for instruction in circuit} <= {"S", "S_DAG"}
37
+
38
+ # the circuit implements a logical S gate
39
+ logical_tableau = circuits.get_logical_tableau(code, circuit)
40
+ assert logical_tableau == stim.Circuit("S 0").to_tableau()
41
+
42
+ # non-SWEL codes are rejected
43
+ with pytest.raises(ValueError, match="SWEL"):
44
+ circuits.get_transversal_s(codes.ToricCode(2))
45
+
46
+
29
47
  def test_transversal_ops() -> None:
30
48
  """Construct SWAP-transversal logical Cliffords of a code."""
31
49
  code: codes.CSSCode