qec 0.2.7__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qec/code_instances/saved_codes/1.json +30 -0
- qec/code_instances/saved_codes/steane.json +34 -0
- qec/code_instances/saved_codes/test.json +1 -0
- {qec-0.2.7.dist-info → qec-0.3.0.dist-info}/METADATA +1 -1
- qec-0.3.0.dist-info/RECORD +9 -0
- qec/code_constructions/__init__.py +0 -3
- qec/code_constructions/css_code.py +0 -773
- qec/code_constructions/hgp_code.py +0 -308
- qec/code_constructions/stabilizer_code.py +0 -591
- qec/code_instances/__init__.py +0 -1
- qec/code_instances/five_qubit_code.py +0 -67
- qec/codetables_de/__init__.py +0 -1
- qec/codetables_de/codetables_de.py +0 -93
- qec/utils/__init__.py +0 -0
- qec/utils/binary_pauli_utils.py +0 -403
- qec/utils/codetables_de_utils.py +0 -274
- qec/utils/sparse_binary_utils.py +0 -64
- qec-0.2.7.dist-info/RECORD +0 -18
- {qec-0.2.7.dist-info → qec-0.3.0.dist-info}/LICENSE +0 -0
- {qec-0.2.7.dist-info → qec-0.3.0.dist-info}/WHEEL +0 -0
- {qec-0.2.7.dist-info → qec-0.3.0.dist-info}/top_level.txt +0 -0
@@ -1,773 +0,0 @@
|
|
1
|
-
from qec.code_constructions import StabilizerCode
|
2
|
-
from qec.utils.sparse_binary_utils import convert_to_binary_scipy_sparse
|
3
|
-
|
4
|
-
# Added / ammended from old code
|
5
|
-
from typing import Union, Tuple
|
6
|
-
import numpy as np
|
7
|
-
import ldpc.mod2
|
8
|
-
import scipy
|
9
|
-
from ldpc import BpOsdDecoder
|
10
|
-
from tqdm import tqdm
|
11
|
-
import time
|
12
|
-
import logging
|
13
|
-
from typing import Optional, Sequence
|
14
|
-
|
15
|
-
logging.basicConfig(level=logging.DEBUG)
|
16
|
-
|
17
|
-
|
18
|
-
class CSSCode(StabilizerCode):
|
19
|
-
"""
|
20
|
-
A class for generating and manipulating Calderbank-Shor-Steane (CSS) quantum error-correcting codes.
|
21
|
-
|
22
|
-
Prameters
|
23
|
-
---------
|
24
|
-
x_stabilizer_matrix (hx): Union[np.ndarray, scipy.sparse.spmatrix]
|
25
|
-
The X-check matrix.
|
26
|
-
z_stabilizer_matrix (hz): Union[np.ndarray, scipy.sparse.spmatrix]
|
27
|
-
The Z-check matrix.
|
28
|
-
name: str, optional
|
29
|
-
A name for this CSS code. Defaults to "CSS code".
|
30
|
-
|
31
|
-
Attributes
|
32
|
-
----------
|
33
|
-
x_stabilizer_matrix (hx): Union[np.ndarray, scipy.sparse.spmatrix]
|
34
|
-
The X-check matrix.
|
35
|
-
z_stabilizer_matrix (hz): Union[np.ndarray, scipy.sparse.spmatrix]
|
36
|
-
The Z-check matrix.
|
37
|
-
name (str):
|
38
|
-
A name for this CSS code.
|
39
|
-
physical_qubit_count (N): int
|
40
|
-
The number of physical qubits in the code.
|
41
|
-
logical_qubit_count (K): int
|
42
|
-
The number of logical qubits in the code. Dimension of the code.
|
43
|
-
code_distance (d): int
|
44
|
-
(Not computed by default) Minimum distance of the code.
|
45
|
-
x_logical_operator_basis (lx): (Union[np.ndarray, scipy.sparse.spmatrix]
|
46
|
-
Logical X operator basis.
|
47
|
-
z_logical_operator_basis (lz): (Union[np.ndarray, scipy.sparse.spmatrix]
|
48
|
-
Logical Z operator basis.
|
49
|
-
"""
|
50
|
-
|
51
|
-
def __init__(
|
52
|
-
self,
|
53
|
-
x_stabilizer_matrix: Union[np.ndarray, scipy.sparse.spmatrix],
|
54
|
-
z_stabilizer_matrix: Union[np.ndarray, scipy.sparse.spmatrix],
|
55
|
-
name: str = None,
|
56
|
-
):
|
57
|
-
"""
|
58
|
-
Initialise a new instance of the CSSCode class.
|
59
|
-
|
60
|
-
Parameters
|
61
|
-
----------
|
62
|
-
x_stabilizer_matrix (hx): Union[np.ndarray, scipy.sparse.spmatrix]
|
63
|
-
The X-check matrix.
|
64
|
-
z_stabilizer_matrix (hz): Union[np.ndarray, scipy.sparse.spmatrix]
|
65
|
-
The Z-check matrix.
|
66
|
-
name: str, optional
|
67
|
-
A name for this CSS code. Defaults to "CSS code".
|
68
|
-
"""
|
69
|
-
|
70
|
-
# Assign a default name if none is provided
|
71
|
-
if name is None:
|
72
|
-
self.name = "CSS code"
|
73
|
-
else:
|
74
|
-
self.name = name
|
75
|
-
|
76
|
-
self.x_logical_operator_basis = None
|
77
|
-
self.z_logical_operator_basis = None
|
78
|
-
|
79
|
-
self.x_code_distance = None
|
80
|
-
self.z_code_distance = None
|
81
|
-
|
82
|
-
# Check if the input matrices are NumPy arrays or SciPy sparse matrices
|
83
|
-
if not isinstance(x_stabilizer_matrix, (np.ndarray, scipy.sparse.spmatrix)):
|
84
|
-
raise TypeError(
|
85
|
-
"Please provide x and z stabilizer matrices as either a numpy array or a scipy sparse matrix."
|
86
|
-
)
|
87
|
-
|
88
|
-
# Convert matrices to sparse representation and set them as class attributes (replaced the old code "convert_to_sparse")
|
89
|
-
self.x_stabilizer_matrix = convert_to_binary_scipy_sparse(x_stabilizer_matrix)
|
90
|
-
self.z_stabilizer_matrix = convert_to_binary_scipy_sparse(z_stabilizer_matrix)
|
91
|
-
|
92
|
-
# Calculate the number of physical qubits from the matrix dimension
|
93
|
-
self.physical_qubit_count = self.x_stabilizer_matrix.shape[1]
|
94
|
-
|
95
|
-
# Validate the number of qubits for both matrices
|
96
|
-
try:
|
97
|
-
assert self.physical_qubit_count == self.z_stabilizer_matrix.shape[1]
|
98
|
-
except AssertionError:
|
99
|
-
raise ValueError(
|
100
|
-
f"Input matrices x_stabilizer_matrix and z_stabilizer_matrix must have the same number of columns.\
|
101
|
-
Current column count, x_stabilizer_matrix: {x_stabilizer_matrix.shape[1]}; z_stabilizer_matrix: {z_stabilizer_matrix.shape[1]}"
|
102
|
-
)
|
103
|
-
|
104
|
-
# Validate if the input matrices commute
|
105
|
-
try:
|
106
|
-
assert not np.any(
|
107
|
-
(self.x_stabilizer_matrix @ self.z_stabilizer_matrix.T).data % 2
|
108
|
-
)
|
109
|
-
except AssertionError:
|
110
|
-
raise ValueError(
|
111
|
-
"Input matrices hx and hz do not commute. I.e. they do not satisfy\
|
112
|
-
the requirement that hx@hz.T = 0."
|
113
|
-
)
|
114
|
-
|
115
|
-
# Compute a basis of the logical operators
|
116
|
-
self.compute_logical_basis()
|
117
|
-
|
118
|
-
def compute_logical_basis(self):
|
119
|
-
"""
|
120
|
-
Compute the logical operator basis for the given CSS code.
|
121
|
-
|
122
|
-
Returns
|
123
|
-
-------
|
124
|
-
Tuple[scipy.sparse.spmatrix, scipy.sparse.spmatrix]
|
125
|
-
Logical X and Z operator bases (lx, lz).
|
126
|
-
|
127
|
-
Notes
|
128
|
-
-----
|
129
|
-
This method uses the kernel of the X and Z stabilizer matrices to find operators that commute with all the stabilizers,
|
130
|
-
and then identifies the subsets of which are not themselves linear combinations of the stabilizers.
|
131
|
-
"""
|
132
|
-
|
133
|
-
# Compute the kernel of hx and hz matrices
|
134
|
-
|
135
|
-
# Z logicals
|
136
|
-
|
137
|
-
# Compute the kernel of hx
|
138
|
-
ker_hx = ldpc.mod2.kernel(self.x_stabilizer_matrix) # kernel of X-stabilisers
|
139
|
-
# Sort the rows of ker_hx by weight
|
140
|
-
row_weights = ker_hx.getnnz(axis=1)
|
141
|
-
sorted_rows = np.argsort(row_weights)
|
142
|
-
ker_hx = ker_hx[sorted_rows, :]
|
143
|
-
# Z logicals are elements of ker_hx (that commute with all the X-stabilisers) that are not linear combinations of Z-stabilisers
|
144
|
-
logical_stack = scipy.sparse.vstack([self.z_stabilizer_matrix, ker_hx]).tocsr()
|
145
|
-
self.rank_hz = ldpc.mod2.rank(self.z_stabilizer_matrix)
|
146
|
-
# The first self.rank_hz pivot_rows of logical_stack are the Z-stabilisers. The remaining pivot_rows are the Z logicals
|
147
|
-
pivots = ldpc.mod2.pivot_rows(logical_stack)
|
148
|
-
self.z_logical_operator_basis = logical_stack[pivots[self.rank_hz :], :]
|
149
|
-
|
150
|
-
# X logicals
|
151
|
-
|
152
|
-
# Compute the kernel of hz
|
153
|
-
ker_hz = ldpc.mod2.kernel(self.z_stabilizer_matrix)
|
154
|
-
# Sort the rows of ker_hz by weight
|
155
|
-
row_weights = ker_hz.getnnz(axis=1)
|
156
|
-
sorted_rows = np.argsort(row_weights)
|
157
|
-
ker_hz = ker_hz[sorted_rows, :]
|
158
|
-
# X logicals are elements of ker_hz (that commute with all the Z-stabilisers) that are not linear combinations of X-stabilisers
|
159
|
-
logical_stack = scipy.sparse.vstack([self.x_stabilizer_matrix, ker_hz]).tocsr()
|
160
|
-
self.rank_hx = ldpc.mod2.rank(self.x_stabilizer_matrix)
|
161
|
-
# The first self.rank_hx pivot_rows of logical_stack are the X-stabilisers. The remaining pivot_rows are the X logicals
|
162
|
-
pivots = ldpc.mod2.pivot_rows(logical_stack)
|
163
|
-
self.x_logical_operator_basis = logical_stack[pivots[self.rank_hx :], :]
|
164
|
-
|
165
|
-
# set the dimension of the code (i.e. the number of logical qubits)
|
166
|
-
self.logical_qubit_count = self.x_logical_operator_basis.shape[0]
|
167
|
-
|
168
|
-
# find the minimum weight logical operators
|
169
|
-
self.x_code_distance = self.physical_qubit_count
|
170
|
-
self.z_code_distance = self.physical_qubit_count
|
171
|
-
|
172
|
-
for i in range(self.logical_qubit_count):
|
173
|
-
if self.x_logical_operator_basis[i].nnz < self.x_code_distance:
|
174
|
-
self.x_code_distance = self.x_logical_operator_basis[i].nnz
|
175
|
-
if self.z_logical_operator_basis[i].nnz < self.z_code_distance:
|
176
|
-
self.z_code_distance = self.z_logical_operator_basis[i].nnz
|
177
|
-
self.code_distance = np.min([self.x_code_distance, self.z_code_distance])
|
178
|
-
|
179
|
-
# FIXME: How does this differ from rank_hx and rank_hz descibed above (ldpc.mod2.rank())?
|
180
|
-
# compute the hx and hz rank
|
181
|
-
self.rank_hx = self.physical_qubit_count - ker_hx.shape[0]
|
182
|
-
self.rank_hz = self.physical_qubit_count - ker_hz.shape[0]
|
183
|
-
|
184
|
-
return (self.x_logical_operator_basis, self.z_logical_operator_basis)
|
185
|
-
|
186
|
-
# TODO: Add a function to save the logical operator basis to a file
|
187
|
-
|
188
|
-
def check_valid_logical_basis(self) -> bool:
|
189
|
-
"""
|
190
|
-
Validate that the stored logical operators form a proper logical basis for the code.
|
191
|
-
|
192
|
-
Checks that they commute with the stabilizers, pairwise anti-commute, and have full rank.
|
193
|
-
|
194
|
-
Returns
|
195
|
-
-------
|
196
|
-
bool
|
197
|
-
True if the logical operators form a valid basis, otherwise False.
|
198
|
-
"""
|
199
|
-
|
200
|
-
# If logical bases are not computed yet, compute them
|
201
|
-
if (
|
202
|
-
self.x_logical_operator_basis is None
|
203
|
-
or self.z_logical_operator_basis is None
|
204
|
-
):
|
205
|
-
self.x_logical_operator_basis, self.z_logical_operator_basis = (
|
206
|
-
self.compute_logical_basis(
|
207
|
-
self.x_stabilizer_matrix, self.z_stabilizer_matrix
|
208
|
-
)
|
209
|
-
)
|
210
|
-
self.logical_qubit_count = self.x_logical_operator_basis.shape[0]
|
211
|
-
|
212
|
-
try:
|
213
|
-
# Test dimension
|
214
|
-
assert (
|
215
|
-
self.logical_qubit_count
|
216
|
-
== self.z_logical_operator_basis.shape[0]
|
217
|
-
== self.x_logical_operator_basis.shape[0]
|
218
|
-
), "Logical operator basis dimensions do not match."
|
219
|
-
|
220
|
-
# Check logical basis linearly independent (i.e. full rank)
|
221
|
-
assert (
|
222
|
-
ldpc.mod2.rank(self.x_logical_operator_basis)
|
223
|
-
== self.logical_qubit_count
|
224
|
-
), "X logical operator basis is not full rank, and hence not linearly independent."
|
225
|
-
assert (
|
226
|
-
ldpc.mod2.rank(self.z_logical_operator_basis)
|
227
|
-
== self.logical_qubit_count
|
228
|
-
), "Z logical operator basis is not full rank, and hence not linearly independent."
|
229
|
-
|
230
|
-
# Perform various tests to validate the logical bases
|
231
|
-
|
232
|
-
# Check that the logical operators commute with the stabilizers
|
233
|
-
try:
|
234
|
-
assert not np.any(
|
235
|
-
(self.x_logical_operator_basis @ self.z_stabilizer_matrix.T).data
|
236
|
-
% 2
|
237
|
-
), "X logical operators do not commute with Z stabilizers."
|
238
|
-
except AssertionError as e:
|
239
|
-
logging.error(e)
|
240
|
-
return False
|
241
|
-
|
242
|
-
try:
|
243
|
-
assert not np.any(
|
244
|
-
(self.z_logical_operator_basis @ self.x_stabilizer_matrix.T).data
|
245
|
-
% 2
|
246
|
-
), "Z logical operators do not commute with X stabilizers."
|
247
|
-
except AssertionError as e:
|
248
|
-
logging.error(e)
|
249
|
-
return False
|
250
|
-
|
251
|
-
# Check that the logical operators anticommute with each other (by checking that the rank of the product is full rank)
|
252
|
-
test = self.x_logical_operator_basis @ self.z_logical_operator_basis.T
|
253
|
-
test.data = test.data % 2
|
254
|
-
assert (
|
255
|
-
ldpc.mod2.rank(test) == self.logical_qubit_count
|
256
|
-
), "Logical operators do not pairwise anticommute."
|
257
|
-
|
258
|
-
test = self.z_logical_operator_basis @ self.x_logical_operator_basis.T
|
259
|
-
test.data = test.data % 2
|
260
|
-
assert (
|
261
|
-
ldpc.mod2.rank(test) == self.logical_qubit_count
|
262
|
-
), "Logical operators do not pairwise anticommute."
|
263
|
-
|
264
|
-
# TODO: Check that the logical operators are not themselves stabilizers?
|
265
|
-
|
266
|
-
except AssertionError as e:
|
267
|
-
logging.error(e)
|
268
|
-
return False
|
269
|
-
|
270
|
-
return True
|
271
|
-
|
272
|
-
def compute_exact_code_distance(
|
273
|
-
self, timeout: float = 0.5
|
274
|
-
) -> Tuple[Optional[int], Optional[int], float]:
|
275
|
-
"""
|
276
|
-
Compute the exact distance of the CSS code by searching through linear combinations
|
277
|
-
of logical operators and stabilisers, ensuring balanced progress between X and Z searches.
|
278
|
-
|
279
|
-
Parameters
|
280
|
-
----------
|
281
|
-
timeout : float, optional
|
282
|
-
The time limit (in seconds) for the exhaustive search. Default is 0.5 seconds.
|
283
|
-
To obtain the exact distance, set to `np.inf`.
|
284
|
-
|
285
|
-
Returns
|
286
|
-
-------
|
287
|
-
Tuple[Optional[int], Optional[int], float]
|
288
|
-
A tuple containing:
|
289
|
-
- The best-known X distance of the code (or None if no X distance was found)
|
290
|
-
- The best-known Z distance of the code (or None if no Z distance was found)
|
291
|
-
- The fraction of total combinations considered before timeout
|
292
|
-
|
293
|
-
Notes
|
294
|
-
-----
|
295
|
-
- Searches X and Z combinations in an interleaved manner to ensure balanced progress
|
296
|
-
- For each type (X/Z):
|
297
|
-
- We compute the row span of both stabilisers and logical operators
|
298
|
-
- For every logical operator in the logical span, we add (mod 2) each stabiliser
|
299
|
-
- We compute the Hamming weight of each candidate operator
|
300
|
-
- We track the minimal Hamming weight encountered
|
301
|
-
"""
|
302
|
-
start_time = time.time()
|
303
|
-
|
304
|
-
# Get stabiliser spans
|
305
|
-
x_stabiliser_span = ldpc.mod2.row_span(self.x_stabilizer_matrix)[1:]
|
306
|
-
z_stabiliser_span = ldpc.mod2.row_span(self.z_stabilizer_matrix)[1:]
|
307
|
-
|
308
|
-
# Get logical spans
|
309
|
-
x_logical_span = ldpc.mod2.row_span(self.x_logical_operator_basis)[1:]
|
310
|
-
z_logical_span = ldpc.mod2.row_span(self.z_logical_operator_basis)[1:]
|
311
|
-
|
312
|
-
# Initialize distances
|
313
|
-
if self.x_code_distance is None:
|
314
|
-
x_code_distance = np.inf
|
315
|
-
else:
|
316
|
-
x_code_distance = self.x_code_distance
|
317
|
-
|
318
|
-
if self.z_code_distance is None:
|
319
|
-
z_code_distance = np.inf
|
320
|
-
else:
|
321
|
-
z_code_distance = self.z_code_distance
|
322
|
-
|
323
|
-
# Prepare iterators for both X and Z combinations
|
324
|
-
x_combinations = (
|
325
|
-
(x_l, x_s) for x_l in x_logical_span for x_s in x_stabiliser_span
|
326
|
-
)
|
327
|
-
z_combinations = (
|
328
|
-
(z_l, z_s) for z_l in z_logical_span for z_s in z_stabiliser_span
|
329
|
-
)
|
330
|
-
|
331
|
-
total_x_combinations = x_stabiliser_span.shape[0] * x_logical_span.shape[0]
|
332
|
-
total_z_combinations = z_stabiliser_span.shape[0] * z_logical_span.shape[0]
|
333
|
-
total_combinations = total_x_combinations + total_z_combinations
|
334
|
-
combinations_considered = 0
|
335
|
-
|
336
|
-
# Create iterables that we can exhaust
|
337
|
-
x_iter = iter(x_combinations)
|
338
|
-
z_iter = iter(z_combinations)
|
339
|
-
x_exhausted = False
|
340
|
-
z_exhausted = False
|
341
|
-
|
342
|
-
while not (x_exhausted and z_exhausted):
|
343
|
-
if time.time() - start_time > timeout:
|
344
|
-
break
|
345
|
-
|
346
|
-
# Try X combination if not exhausted
|
347
|
-
if not x_exhausted:
|
348
|
-
try:
|
349
|
-
x_logical, x_stabiliser = next(x_iter)
|
350
|
-
candidate_x = x_logical + x_stabiliser
|
351
|
-
candidate_x.data %= 2
|
352
|
-
x_weight = candidate_x.getnnz()
|
353
|
-
if x_weight < x_code_distance:
|
354
|
-
x_code_distance = x_weight
|
355
|
-
combinations_considered += 1
|
356
|
-
except StopIteration:
|
357
|
-
x_exhausted = True
|
358
|
-
|
359
|
-
# Try Z combination if not exhausted
|
360
|
-
if not z_exhausted:
|
361
|
-
try:
|
362
|
-
z_logical, z_stabiliser = next(z_iter)
|
363
|
-
candidate_z = z_logical + z_stabiliser
|
364
|
-
candidate_z.data %= 2
|
365
|
-
z_weight = candidate_z.getnnz()
|
366
|
-
if z_weight < z_code_distance:
|
367
|
-
z_code_distance = z_weight
|
368
|
-
combinations_considered += 1
|
369
|
-
except StopIteration:
|
370
|
-
z_exhausted = True
|
371
|
-
|
372
|
-
# Update code distances
|
373
|
-
self.x_code_distance = x_code_distance if x_code_distance != np.inf else None
|
374
|
-
self.z_code_distance = z_code_distance if z_code_distance != np.inf else None
|
375
|
-
self.code_distance = (
|
376
|
-
min(x_code_distance, z_code_distance)
|
377
|
-
if x_code_distance != np.inf and z_code_distance != np.inf
|
378
|
-
else None
|
379
|
-
)
|
380
|
-
|
381
|
-
# Calculate fraction of combinations considered
|
382
|
-
fraction_considered = combinations_considered / total_combinations
|
383
|
-
|
384
|
-
return (
|
385
|
-
int(x_code_distance) if x_code_distance != np.inf else None,
|
386
|
-
int(z_code_distance) if z_code_distance != np.inf else None,
|
387
|
-
fraction_considered,
|
388
|
-
)
|
389
|
-
|
390
|
-
def estimate_min_distance(
|
391
|
-
self,
|
392
|
-
timeout_seconds: float = 0.25,
|
393
|
-
p: float = 0.25,
|
394
|
-
reduce_logical_basis: bool = False,
|
395
|
-
decoder: Optional[BpOsdDecoder] = None,
|
396
|
-
) -> int:
|
397
|
-
"""
|
398
|
-
Estimate the minimum distance of the CSS code using a BP+OSD decoder-based search.
|
399
|
-
|
400
|
-
Parameters
|
401
|
-
----------
|
402
|
-
timeout_seconds : float, optional
|
403
|
-
Time limit in seconds for the search. Default: 0.25
|
404
|
-
p : float, optional
|
405
|
-
Probability for including each logical operator in trial combinations. Default: 0.25
|
406
|
-
reduce_logical_basis : bool, optional
|
407
|
-
Whether to attempt reducing the logical operator basis. Default: False
|
408
|
-
decoder : Optional[BpOsdDecoder], optional
|
409
|
-
Pre-configured BP+OSD decoder. If None, initializes with default settings.
|
410
|
-
|
411
|
-
Returns
|
412
|
-
-------
|
413
|
-
int
|
414
|
-
Best estimate of code distance found within the time limit.
|
415
|
-
"""
|
416
|
-
start_time = time.time()
|
417
|
-
|
418
|
-
# Ensure logical operator bases are computed
|
419
|
-
if (
|
420
|
-
self.x_logical_operator_basis is None
|
421
|
-
or self.z_logical_operator_basis is None
|
422
|
-
):
|
423
|
-
self.compute_logical_basis()
|
424
|
-
|
425
|
-
# Setup decoders and parameters for both X and Z
|
426
|
-
bp_osd_z, x_stack, full_rank_x, x_min_distance, x_max_distance = (
|
427
|
-
self._setup_distance_estimation_decoder(
|
428
|
-
self.x_stabilizer_matrix, self.x_logical_operator_basis, decoder
|
429
|
-
)
|
430
|
-
)
|
431
|
-
bp_osd_x, z_stack, full_rank_z, z_min_distance, z_max_distance = (
|
432
|
-
self._setup_distance_estimation_decoder(
|
433
|
-
self.z_stabilizer_matrix, self.z_logical_operator_basis, decoder
|
434
|
-
)
|
435
|
-
)
|
436
|
-
|
437
|
-
candidate_logicals_x = []
|
438
|
-
candidate_logicals_z = []
|
439
|
-
|
440
|
-
x_weight_one_searched = 0
|
441
|
-
z_weight_one_searched = 0
|
442
|
-
|
443
|
-
with tqdm(total=timeout_seconds, desc="Estimating distance") as pbar:
|
444
|
-
while time.time() - start_time < timeout_seconds:
|
445
|
-
elapsed = time.time() - start_time
|
446
|
-
pbar.update(elapsed - pbar.n)
|
447
|
-
|
448
|
-
if np.random.rand() < 0.5:
|
449
|
-
# X Logical operators
|
450
|
-
if x_weight_one_searched < self.z_logical_operator_basis.shape[0]:
|
451
|
-
dummy_syndrome_x = np.zeros(z_stack.shape[0], dtype=np.uint8)
|
452
|
-
dummy_syndrome_x[
|
453
|
-
full_rank_z.shape[0] + x_weight_one_searched
|
454
|
-
] = 1
|
455
|
-
x_weight_one_searched += 1
|
456
|
-
else:
|
457
|
-
dummy_syndrome_x = self._generate_random_logical_combination_for_distance_estimation(
|
458
|
-
z_stack, p, self.z_stabilizer_matrix.shape[0]
|
459
|
-
)
|
460
|
-
|
461
|
-
candidate_x = bp_osd_x.decode(dummy_syndrome_x)
|
462
|
-
x_weight = np.count_nonzero(candidate_x)
|
463
|
-
if x_weight < x_min_distance:
|
464
|
-
x_min_distance = x_weight
|
465
|
-
|
466
|
-
if x_weight < x_max_distance and reduce_logical_basis:
|
467
|
-
candidate_logicals_x.append(candidate_x)
|
468
|
-
|
469
|
-
# Reduce X logical operator basis independently
|
470
|
-
if len(candidate_logicals_x) >= 5:
|
471
|
-
self._reduce_logical_operator_basis(
|
472
|
-
candidate_logicals_x, []
|
473
|
-
)
|
474
|
-
(
|
475
|
-
bp_osd_x,
|
476
|
-
z_stack,
|
477
|
-
full_rank_z,
|
478
|
-
z_min_distance,
|
479
|
-
z_max_distance,
|
480
|
-
) = self._setup_distance_estimation_decoder(
|
481
|
-
self.z_stabilizer_matrix,
|
482
|
-
self.z_logical_operator_basis,
|
483
|
-
decoder,
|
484
|
-
)
|
485
|
-
candidate_logicals_x = []
|
486
|
-
x_weight_one_searched = 0
|
487
|
-
|
488
|
-
else:
|
489
|
-
# Z Logical operators
|
490
|
-
if z_weight_one_searched < self.x_logical_operator_basis.shape[0]:
|
491
|
-
dummy_syndrome_z = np.zeros(x_stack.shape[0], dtype=np.uint8)
|
492
|
-
dummy_syndrome_z[
|
493
|
-
full_rank_x.shape[0] + z_weight_one_searched
|
494
|
-
] = 1
|
495
|
-
z_weight_one_searched += 1
|
496
|
-
else:
|
497
|
-
dummy_syndrome_z = self._generate_random_logical_combination_for_distance_estimation(
|
498
|
-
x_stack, p, self.x_stabilizer_matrix.shape[0]
|
499
|
-
)
|
500
|
-
|
501
|
-
candidate_z = bp_osd_z.decode(dummy_syndrome_z)
|
502
|
-
z_weight = np.count_nonzero(candidate_z)
|
503
|
-
if z_weight < z_min_distance:
|
504
|
-
z_min_distance = z_weight
|
505
|
-
|
506
|
-
if z_weight < z_max_distance and reduce_logical_basis:
|
507
|
-
candidate_logicals_z.append(candidate_z)
|
508
|
-
|
509
|
-
# Reduce Z logical operator basis independently
|
510
|
-
if len(candidate_logicals_z) >= 5:
|
511
|
-
self._reduce_logical_operator_basis(
|
512
|
-
[], candidate_logicals_z
|
513
|
-
)
|
514
|
-
(
|
515
|
-
bp_osd_z,
|
516
|
-
x_stack,
|
517
|
-
full_rank_x,
|
518
|
-
x_min_distance,
|
519
|
-
x_max_distance,
|
520
|
-
) = self._setup_distance_estimation_decoder(
|
521
|
-
self.x_stabilizer_matrix,
|
522
|
-
self.x_logical_operator_basis,
|
523
|
-
decoder,
|
524
|
-
)
|
525
|
-
candidate_logicals_z = []
|
526
|
-
z_weight_one_searched = 0
|
527
|
-
|
528
|
-
x_weights, z_weights = self.logical_basis_weights()
|
529
|
-
pbar.set_description(
|
530
|
-
f"Estimating distance: dx <= {x_min_distance}, dz <= {z_min_distance}, x-weights: {np.mean(x_weights):.2f}, z-weights: {np.mean(z_weights):.2f}"
|
531
|
-
)
|
532
|
-
|
533
|
-
self._reduce_logical_operator_basis(candidate_logicals_x, candidate_logicals_z)
|
534
|
-
|
535
|
-
# Update distances
|
536
|
-
self.x_code_distance = x_min_distance
|
537
|
-
self.z_code_distance = z_min_distance
|
538
|
-
self.code_distance = min(x_min_distance, z_min_distance)
|
539
|
-
|
540
|
-
return self.code_distance
|
541
|
-
|
542
|
-
def _setup_distance_estimation_decoder(
|
543
|
-
self, stabilizer_matrix, logical_operator_basis, decoder=None
|
544
|
-
) -> Tuple[BpOsdDecoder, scipy.sparse.spmatrix, scipy.sparse.spmatrix, int, int]:
|
545
|
-
"""
|
546
|
-
Helper function to set up the BP+OSD decoder for distance estimation.
|
547
|
-
|
548
|
-
Parameters
|
549
|
-
----------
|
550
|
-
stabilizer_matrix : scipy.sparse.spmatrix
|
551
|
-
Stabilizer matrix of the code.
|
552
|
-
logical_operator_basis : scipy.sparse.spmatrix
|
553
|
-
Logical operator basis of the code.
|
554
|
-
decoder : Optional[BpOsdDecoder], optional
|
555
|
-
Pre-configured decoder. If None, initializes with default settings.
|
556
|
-
|
557
|
-
Returns
|
558
|
-
-------
|
559
|
-
Tuple[BpOsdDecoder, scipy.sparse.spmatrix, scipy.sparse.spmatrix, int, int]
|
560
|
-
Decoder, stacked matrix, stabilizer matrix, minimum distance, and maximum distance.
|
561
|
-
"""
|
562
|
-
# Remove redundant rows from stabilizer matrix
|
563
|
-
p_rows = ldpc.mod2.pivot_rows(stabilizer_matrix)
|
564
|
-
full_rank_stabilizer_matrix = stabilizer_matrix[p_rows]
|
565
|
-
|
566
|
-
# Build a stacked matrix of stabilizers and logicals
|
567
|
-
stack = scipy.sparse.vstack(
|
568
|
-
[full_rank_stabilizer_matrix, logical_operator_basis]
|
569
|
-
).tocsr()
|
570
|
-
|
571
|
-
# Initial distance estimate from current logicals
|
572
|
-
min_distance = np.min(logical_operator_basis.getnnz(axis=1))
|
573
|
-
max_distance = np.max(logical_operator_basis.getnnz(axis=1))
|
574
|
-
|
575
|
-
# Set up BP+OSD decoder if not provided
|
576
|
-
if decoder is None:
|
577
|
-
decoder = BpOsdDecoder(
|
578
|
-
stack,
|
579
|
-
error_rate=0.1,
|
580
|
-
max_iter=10,
|
581
|
-
bp_method="ms",
|
582
|
-
schedule="parallel",
|
583
|
-
ms_scaling_factor=1.0,
|
584
|
-
osd_method="osd_0",
|
585
|
-
osd_order=0,
|
586
|
-
)
|
587
|
-
|
588
|
-
return decoder, stack, full_rank_stabilizer_matrix, min_distance, max_distance
|
589
|
-
|
590
|
-
def _generate_random_logical_combination_for_distance_estimation(
|
591
|
-
self, stack: scipy.sparse.spmatrix, p: float, stabilizer_count: int
|
592
|
-
) -> np.ndarray:
|
593
|
-
"""
|
594
|
-
Generate a random logical combination for the BP+OSD decoder.
|
595
|
-
|
596
|
-
Parameters
|
597
|
-
----------
|
598
|
-
stack : scipy.sparse.spmatrix
|
599
|
-
The stacked stabilizer and logical operator matrix.
|
600
|
-
p : float
|
601
|
-
Probability for including each logical operator in the combination.
|
602
|
-
stabilizer_count : int
|
603
|
-
Number of stabilizer rows in the stacked matrix.
|
604
|
-
|
605
|
-
Returns
|
606
|
-
-------
|
607
|
-
np.ndarray
|
608
|
-
Randomly generated syndrome vector.
|
609
|
-
"""
|
610
|
-
random_mask = np.random.choice([0, 1], size=stack.shape[0], p=[1 - p, p])
|
611
|
-
random_mask[:stabilizer_count] = (
|
612
|
-
0 # Ensure no stabilizer-only rows are selected
|
613
|
-
)
|
614
|
-
|
615
|
-
while not np.any(random_mask):
|
616
|
-
random_mask = np.random.choice([0, 1], size=stack.shape[0], p=[1 - p, p])
|
617
|
-
random_mask[:stabilizer_count] = 0
|
618
|
-
|
619
|
-
dummy_syndrome = np.zeros(stack.shape[0], dtype=np.uint8)
|
620
|
-
dummy_syndrome[np.nonzero(random_mask)[0]] = 1
|
621
|
-
|
622
|
-
return dummy_syndrome
|
623
|
-
|
624
|
-
def _reduce_logical_operator_basis(
|
625
|
-
self,
|
626
|
-
candidate_logicals_x: Union[Sequence, np.ndarray, scipy.sparse.spmatrix] = [],
|
627
|
-
candidate_logicals_z: Union[Sequence, np.ndarray, scipy.sparse.spmatrix] = [],
|
628
|
-
):
|
629
|
-
"""
|
630
|
-
Reduce the logical operator bases (for X and Z) to include lower-weight logicals.
|
631
|
-
|
632
|
-
Parameters
|
633
|
-
----------
|
634
|
-
candidate_logicals_x : Union[Sequence, np.ndarray, scipy.sparse.spmatrix], optional
|
635
|
-
A list or array of candidate X logical operators to consider for reducing the X basis.
|
636
|
-
Defaults to an empty list.
|
637
|
-
candidate_logicals_z : Union[Sequence, np.ndarray, scipy.sparse.spmatrix], optional
|
638
|
-
A list or array of candidate Z logical operators to consider for reducing the Z basis.
|
639
|
-
Defaults to an empty list.
|
640
|
-
"""
|
641
|
-
# Reduce X logical operator basis
|
642
|
-
if candidate_logicals_x:
|
643
|
-
# Convert candidates to a sparse matrix if they aren't already
|
644
|
-
if not isinstance(candidate_logicals_x, scipy.sparse.spmatrix):
|
645
|
-
candidate_logicals_x = scipy.sparse.csr_matrix(candidate_logicals_x)
|
646
|
-
|
647
|
-
# Stack the candidate X logicals with the existing X logicals
|
648
|
-
temp_x = scipy.sparse.vstack(
|
649
|
-
[candidate_logicals_x, self.x_logical_operator_basis]
|
650
|
-
).tocsr()
|
651
|
-
|
652
|
-
# Calculate Hamming weights for sorting
|
653
|
-
x_row_weights = temp_x.getnnz(axis=1)
|
654
|
-
sorted_x_rows = np.argsort(x_row_weights)
|
655
|
-
temp_x = temp_x[sorted_x_rows, :]
|
656
|
-
|
657
|
-
# Add the X stabilizer matrix to the top of the stack
|
658
|
-
temp_x = scipy.sparse.vstack([self.x_stabilizer_matrix, temp_x]).tocsr()
|
659
|
-
|
660
|
-
# Determine rank of the X stabilizer matrix
|
661
|
-
rank_hx = ldpc.mod2.rank(self.x_stabilizer_matrix)
|
662
|
-
|
663
|
-
# Perform row reduction to find a new X logical basis
|
664
|
-
pivots_x = ldpc.mod2.pivot_rows(temp_x)
|
665
|
-
self.x_logical_operator_basis = temp_x[pivots_x[rank_hx:], :]
|
666
|
-
|
667
|
-
# Reduce Z logical operator basis
|
668
|
-
if candidate_logicals_z:
|
669
|
-
# Convert candidates to a sparse matrix if they aren't already
|
670
|
-
if not isinstance(candidate_logicals_z, scipy.sparse.spmatrix):
|
671
|
-
candidate_logicals_z = scipy.sparse.csr_matrix(candidate_logicals_z)
|
672
|
-
|
673
|
-
# Stack the candidate Z logicals with the existing Z logicals
|
674
|
-
temp_z = scipy.sparse.vstack(
|
675
|
-
[candidate_logicals_z, self.z_logical_operator_basis]
|
676
|
-
).tocsr()
|
677
|
-
|
678
|
-
# Calculate Hamming weights for sorting
|
679
|
-
z_row_weights = temp_z.getnnz(axis=1)
|
680
|
-
sorted_z_rows = np.argsort(z_row_weights)
|
681
|
-
temp_z = temp_z[sorted_z_rows, :]
|
682
|
-
|
683
|
-
# Add the Z stabilizer matrix to the top of the stack
|
684
|
-
temp_z = scipy.sparse.vstack([self.z_stabilizer_matrix, temp_z]).tocsr()
|
685
|
-
|
686
|
-
# Determine rank of the Z stabilizer matrix
|
687
|
-
rank_hz = ldpc.mod2.rank(self.z_stabilizer_matrix)
|
688
|
-
|
689
|
-
# Perform row reduction to find a new Z logical basis
|
690
|
-
pivots_z = ldpc.mod2.pivot_rows(temp_z)
|
691
|
-
self.z_logical_operator_basis = temp_z[pivots_z[rank_hz:], :]
|
692
|
-
|
693
|
-
def fix_logical_operators(self, fix_logical: str = "X"):
|
694
|
-
"""
|
695
|
-
Create a canonical basis of logical operators where X-logical and Z-logical operators pairwise anticommute.
|
696
|
-
|
697
|
-
Parameters
|
698
|
-
----------
|
699
|
-
fix_logical : str, optional
|
700
|
-
Specify which logical operator basis to fix. "X" adjusts Z-logicals based on X-logicals, and "Z" adjusts
|
701
|
-
X-logicals based on Z-logicals. Default is "X".
|
702
|
-
|
703
|
-
Raises
|
704
|
-
------
|
705
|
-
TypeError
|
706
|
-
If `fix_logical` is not a string.
|
707
|
-
ValueError
|
708
|
-
If `fix_logical` is not "X" or "Z".
|
709
|
-
|
710
|
-
Returns
|
711
|
-
-------
|
712
|
-
bool
|
713
|
-
True if the logical operator basis is valid after fixing; False otherwise.
|
714
|
-
|
715
|
-
Notes
|
716
|
-
-----
|
717
|
-
This method ensures that the symplectic product of the logical bases results in the identity matrix.
|
718
|
-
If any issues occur during the adjustment, the method logs an error.
|
719
|
-
"""
|
720
|
-
if not isinstance(fix_logical, str):
|
721
|
-
raise TypeError("fix_logical parameter must be a string")
|
722
|
-
|
723
|
-
if fix_logical.lower() == "x":
|
724
|
-
temp = self.z_logical_operator_basis @ self.x_logical_operator_basis.T
|
725
|
-
temp.data = temp.data % 2
|
726
|
-
temp = scipy.sparse.csr_matrix(ldpc.mod2.inverse(temp), dtype=np.uint8)
|
727
|
-
self.z_logical_operator_basis = temp @ self.z_logical_operator_basis
|
728
|
-
self.z_logical_operator_basis.data = self.z_logical_operator_basis.data % 2
|
729
|
-
|
730
|
-
elif fix_logical.lower() == "z":
|
731
|
-
temp = self.x_logical_operator_basis @ self.z_logical_operator_basis.T
|
732
|
-
temp.data = temp.data % 2
|
733
|
-
temp = scipy.sparse.csr_matrix(ldpc.mod2.inverse(temp), dtype=np.uint8)
|
734
|
-
self.x_logical_operator_basis = temp @ self.x_logical_operator_basis
|
735
|
-
self.x_logical_operator_basis.data = self.x_logical_operator_basis.data % 2
|
736
|
-
else:
|
737
|
-
raise ValueError("Invalid fix_logical parameter")
|
738
|
-
|
739
|
-
try:
|
740
|
-
assert self.check_valid_logical_basis()
|
741
|
-
except AssertionError:
|
742
|
-
logging.error("Logical basis is not valid after fixing logical operators.")
|
743
|
-
return False
|
744
|
-
|
745
|
-
try:
|
746
|
-
lx_lz = self.x_logical_operator_basis @ self.z_logical_operator_basis.T
|
747
|
-
lx_lz.data = lx_lz.data % 2
|
748
|
-
assert (
|
749
|
-
lx_lz != scipy.sparse.eye(self.logical_qubit_count, format="csr")
|
750
|
-
).nnz == 0
|
751
|
-
except AssertionError:
|
752
|
-
logging.error("Logical basis is not valid after fixing logical operators.")
|
753
|
-
return False
|
754
|
-
|
755
|
-
return True
|
756
|
-
|
757
|
-
def logical_basis_weights(self) -> Tuple[np.ndarray, np.ndarray]:
|
758
|
-
x_weights = []
|
759
|
-
z_weights = []
|
760
|
-
for i in range(self.logical_qubit_count):
|
761
|
-
x_weights.append(self.x_logical_operator_basis[i].nnz)
|
762
|
-
z_weights.append(self.z_logical_operator_basis[i].nnz)
|
763
|
-
|
764
|
-
return (np.array(x_weights), np.array(z_weights))
|
765
|
-
|
766
|
-
def __str__(self):
|
767
|
-
"""
|
768
|
-
Return a string representation of the CSSCode object.
|
769
|
-
|
770
|
-
Returns:
|
771
|
-
str: String representation of the CSS code.
|
772
|
-
"""
|
773
|
-
return f"{self.name} Code: [[N={self.physical_qubit_count}, K={self.logical_qubit_count}, dx<={self.x_code_distance}, dz<={self.z_code_distance}]]"
|