qec 0.2.0__py3-none-any.whl → 0.2.1__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.
@@ -1,6 +1,609 @@
1
1
  from qec.stabilizer_code 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
14
+
15
+ logging.basicConfig(level=logging.DEBUG)
2
16
 
3
17
 
4
18
  class CSSCode(StabilizerCode):
5
- def __init__(self):
6
- NotImplemented
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
+ # Check if the input matrices are NumPy arrays or SciPy sparse matrices
80
+ if not isinstance(x_stabilizer_matrix, (np.ndarray, scipy.sparse.spmatrix)):
81
+ raise TypeError(
82
+ "Please provide x and z stabilizer matrices as either a numpy array or a scipy sparse matrix."
83
+ )
84
+
85
+ # Convert matrices to sparse representation and set them as class attributes (replaced the old code "convert_to_sparse")
86
+ self.x_stabilizer_matrix = convert_to_binary_scipy_sparse(x_stabilizer_matrix)
87
+ self.z_stabilizer_matrix = convert_to_binary_scipy_sparse(z_stabilizer_matrix)
88
+
89
+ # Calculate the number of physical qubits from the matrix dimension
90
+ self.physical_qubit_count = self.x_stabilizer_matrix.shape[1]
91
+
92
+ # Validate the number of qubits for both matrices
93
+ try:
94
+ assert self.physical_qubit_count == self.z_stabilizer_matrix.shape[1]
95
+ except AssertionError:
96
+ raise ValueError(
97
+ f"Input matrices x_stabilizer_matrix and z_stabilizer_matrix must have the same number of columns.\
98
+ Current column count, x_stabilizer_matrix: {x_stabilizer_matrix.shape[1]}; z_stabilizer_matrix: {z_stabilizer_matrix.shape[1]}"
99
+ )
100
+
101
+ # Validate if the input matrices commute
102
+ try:
103
+ assert not np.any(
104
+ (self.x_stabilizer_matrix @ self.z_stabilizer_matrix.T).data % 2
105
+ )
106
+ except AssertionError:
107
+ raise ValueError(
108
+ "Input matrices hx and hz do not commute. I.e. they do not satisfy\
109
+ the requirement that hx@hz.T = 0."
110
+ )
111
+
112
+ # Compute a basis of the logical operators
113
+ self.compute_logical_basis()
114
+
115
+ def compute_logical_basis(self):
116
+ """
117
+ Compute the logical operator basis for the given CSS code.
118
+
119
+ Returns
120
+ -------
121
+ Tuple[scipy.sparse.spmatrix, scipy.sparse.spmatrix]
122
+ Logical X and Z operator bases (lx, lz).
123
+
124
+ Notes
125
+ -----
126
+ This method uses the kernel of the X and Z stabilizer matrices to find operators that commute with all the stabilizers,
127
+ and then identifies the subsets of which are not themselves linear combinations of the stabilizers.
128
+ """
129
+
130
+ # Compute the kernel of hx and hz matrices
131
+
132
+ # Z logicals
133
+
134
+ # Compute the kernel of hx
135
+ ker_hx = ldpc.mod2.kernel(self.x_stabilizer_matrix) # kernel of X-stabilisers
136
+ # Sort the rows of ker_hx by weight
137
+ row_weights = ker_hx.getnnz(axis=1)
138
+ sorted_rows = np.argsort(row_weights)
139
+ ker_hx = ker_hx[sorted_rows, :]
140
+ # Z logicals are elements of ker_hx (that commute with all the X-stabilisers) that are not linear combinations of Z-stabilisers
141
+ logical_stack = scipy.sparse.vstack([self.z_stabilizer_matrix, ker_hx]).tocsr()
142
+ self.rank_hz = ldpc.mod2.rank(self.z_stabilizer_matrix)
143
+ # The first self.rank_hz pivot_rows of logical_stack are the Z-stabilisers. The remaining pivot_rows are the Z logicals
144
+ pivots = ldpc.mod2.pivot_rows(logical_stack)
145
+ self.z_logical_operator_basis = logical_stack[pivots[self.rank_hz :], :]
146
+
147
+ # X logicals
148
+
149
+ # Compute the kernel of hz
150
+ ker_hz = ldpc.mod2.kernel(self.z_stabilizer_matrix)
151
+ # Sort the rows of ker_hz by weight
152
+ row_weights = ker_hz.getnnz(axis=1)
153
+ sorted_rows = np.argsort(row_weights)
154
+ ker_hz = ker_hz[sorted_rows, :]
155
+ # X logicals are elements of ker_hz (that commute with all the Z-stabilisers) that are not linear combinations of X-stabilisers
156
+ logical_stack = scipy.sparse.vstack([self.x_stabilizer_matrix, ker_hz]).tocsr()
157
+ self.rank_hx = ldpc.mod2.rank(self.x_stabilizer_matrix)
158
+ # The first self.rank_hx pivot_rows of logical_stack are the X-stabilisers. The remaining pivot_rows are the X logicals
159
+ pivots = ldpc.mod2.pivot_rows(logical_stack)
160
+ self.x_logical_operator_basis = logical_stack[pivots[self.rank_hx :], :]
161
+
162
+ # set the dimension of the code (i.e. the number of logical qubits)
163
+ self.logical_qubit_count = self.x_logical_operator_basis.shape[0]
164
+
165
+ # find the minimum weight logical operators
166
+ self.x_code_distance = self.physical_qubit_count
167
+ self.z_code_distance = self.physical_qubit_count
168
+
169
+ for i in range(self.logical_qubit_count):
170
+ if self.x_logical_operator_basis[i].nnz < self.x_code_distance:
171
+ self.x_code_distance = self.x_logical_operator_basis[i].nnz
172
+ if self.z_logical_operator_basis[i].nnz < self.z_code_distance:
173
+ self.z_code_distance = self.z_logical_operator_basis[i].nnz
174
+ self.code_distance = np.min([self.x_code_distance, self.z_code_distance])
175
+
176
+ # FIXME: How does this differ from rank_hx and rank_hz descibed above (ldpc.mod2.rank())?
177
+ # compute the hx and hz rank
178
+ self.rank_hx = self.physical_qubit_count - ker_hx.shape[0]
179
+ self.rank_hz = self.physical_qubit_count - ker_hz.shape[0]
180
+
181
+ return (self.x_logical_operator_basis, self.z_logical_operator_basis)
182
+
183
+ # TODO: Add a function to save the logical operator basis to a file
184
+
185
+ def check_valid_logical_xz_basis(self) -> bool:
186
+ """
187
+ Validate that the stored logical operators form a proper logical basis for the code.
188
+
189
+ Checks that they commute with the stabilizers, pairwise anti-commute, and have full rank.
190
+
191
+ Returns
192
+ -------
193
+ bool
194
+ True if the logical operators form a valid basis, otherwise False.
195
+ """
196
+
197
+ # If logical bases are not computed yet, compute them
198
+ if (
199
+ self.x_logical_operator_basis is None
200
+ or self.z_logical_operator_basis is None
201
+ ):
202
+ self.x_logical_operator_basis, self.z_logical_operator_basis = (
203
+ self.compute_logical_basis(
204
+ self.x_stabilizer_matrix, self.z_stabilizer_matrix
205
+ )
206
+ )
207
+ self.logical_qubit_count = self.x_logical_operator_basis.shape[0]
208
+
209
+ try:
210
+ # Test dimension
211
+ assert (
212
+ self.logical_qubit_count
213
+ == self.z_logical_operator_basis.shape[0]
214
+ == self.x_logical_operator_basis.shape[0]
215
+ ), "Logical operator basis dimensions do not match."
216
+
217
+ # Check logical basis linearly independent (i.e. full rank)
218
+ assert (
219
+ ldpc.mod2.rank(self.x_logical_operator_basis)
220
+ == self.logical_qubit_count
221
+ ), "X logical operator basis is not full rank, and hence not linearly independent."
222
+ assert (
223
+ ldpc.mod2.rank(self.z_logical_operator_basis)
224
+ == self.logical_qubit_count
225
+ ), "Z logical operator basis is not full rank, and hence not linearly independent."
226
+
227
+ # Perform various tests to validate the logical bases
228
+
229
+ # Check that the logical operators commute with the stabilizers
230
+ try:
231
+ assert not np.any(
232
+ (self.x_logical_operator_basis @ self.z_stabilizer_matrix.T).data
233
+ % 2
234
+ ), "X logical operators do not commute with Z stabilizers."
235
+ except AssertionError as e:
236
+ logging.error(e)
237
+ return False
238
+
239
+ try:
240
+ assert not np.any(
241
+ (self.z_logical_operator_basis @ self.x_stabilizer_matrix.T).data
242
+ % 2
243
+ ), "Z logical operators do not commute with X stabilizers."
244
+ except AssertionError as e:
245
+ logging.error(e)
246
+ return False
247
+
248
+ # Check that the logical operators anticommute with each other (by checking that the rank of the product is full rank)
249
+ test = self.x_logical_operator_basis @ self.z_logical_operator_basis.T
250
+ test.data = test.data % 2
251
+ assert (
252
+ ldpc.mod2.rank(test) == self.logical_qubit_count
253
+ ), "Logical operators do not pairwise anticommute."
254
+
255
+ test = self.z_logical_operator_basis @ self.x_logical_operator_basis.T
256
+ test.data = test.data % 2
257
+ assert (
258
+ ldpc.mod2.rank(test) == self.logical_qubit_count
259
+ ), "Logical operators do not pairwise anticommute."
260
+
261
+ # TODO: Check that the logical operators are not themselves stabilizers?
262
+
263
+ except AssertionError as e:
264
+ logging.error(e)
265
+ return False
266
+
267
+ return True
268
+
269
+ def compute_exact_code_distance(
270
+ self, timeout: float = 0.5
271
+ ) -> Tuple[Optional[int], Optional[int], float]:
272
+ """
273
+ Compute the exact distance of the CSS code by searching through linear combinations
274
+ of logical operators and stabilisers, ensuring balanced progress between X and Z searches.
275
+
276
+ Parameters
277
+ ----------
278
+ timeout : float, optional
279
+ The time limit (in seconds) for the exhaustive search. Default is 0.5 seconds.
280
+ To obtain the exact distance, set to `np.inf`.
281
+
282
+ Returns
283
+ -------
284
+ Tuple[Optional[int], Optional[int], float]
285
+ A tuple containing:
286
+ - The best-known X distance of the code (or None if no X distance was found)
287
+ - The best-known Z distance of the code (or None if no Z distance was found)
288
+ - The fraction of total combinations considered before timeout
289
+
290
+ Notes
291
+ -----
292
+ - Searches X and Z combinations in an interleaved manner to ensure balanced progress
293
+ - For each type (X/Z):
294
+ - We compute the row span of both stabilisers and logical operators
295
+ - For every logical operator in the logical span, we add (mod 2) each stabiliser
296
+ - We compute the Hamming weight of each candidate operator
297
+ - We track the minimal Hamming weight encountered
298
+ """
299
+ start_time = time.time()
300
+
301
+ # Get stabiliser spans
302
+ x_stabiliser_span = ldpc.mod2.row_span(self.x_stabilizer_matrix)[1:]
303
+ z_stabiliser_span = ldpc.mod2.row_span(self.z_stabilizer_matrix)[1:]
304
+
305
+ # Get logical spans
306
+ x_logical_span = ldpc.mod2.row_span(self.x_logical_operator_basis)[1:]
307
+ z_logical_span = ldpc.mod2.row_span(self.z_logical_operator_basis)[1:]
308
+
309
+ # Initialize distances
310
+ if self.x_code_distance is None:
311
+ x_code_distance = np.inf
312
+ else:
313
+ x_code_distance = self.x_code_distance
314
+
315
+ if self.z_code_distance is None:
316
+ z_code_distance = np.inf
317
+ else:
318
+ z_code_distance = self.z_code_distance
319
+
320
+ # Prepare iterators for both X and Z combinations
321
+ x_combinations = (
322
+ (x_l, x_s) for x_l in x_logical_span for x_s in x_stabiliser_span
323
+ )
324
+ z_combinations = (
325
+ (z_l, z_s) for z_l in z_logical_span for z_s in z_stabiliser_span
326
+ )
327
+
328
+ total_x_combinations = x_stabiliser_span.shape[0] * x_logical_span.shape[0]
329
+ total_z_combinations = z_stabiliser_span.shape[0] * z_logical_span.shape[0]
330
+ total_combinations = total_x_combinations + total_z_combinations
331
+ combinations_considered = 0
332
+
333
+ # Create iterables that we can exhaust
334
+ x_iter = iter(x_combinations)
335
+ z_iter = iter(z_combinations)
336
+ x_exhausted = False
337
+ z_exhausted = False
338
+
339
+ while not (x_exhausted and z_exhausted):
340
+ if time.time() - start_time > timeout:
341
+ break
342
+
343
+ # Try X combination if not exhausted
344
+ if not x_exhausted:
345
+ try:
346
+ x_logical, x_stabiliser = next(x_iter)
347
+ candidate_x = x_logical + x_stabiliser
348
+ candidate_x.data %= 2
349
+ x_weight = candidate_x.getnnz()
350
+ if x_weight < x_code_distance:
351
+ x_code_distance = x_weight
352
+ combinations_considered += 1
353
+ except StopIteration:
354
+ x_exhausted = True
355
+
356
+ # Try Z combination if not exhausted
357
+ if not z_exhausted:
358
+ try:
359
+ z_logical, z_stabiliser = next(z_iter)
360
+ candidate_z = z_logical + z_stabiliser
361
+ candidate_z.data %= 2
362
+ z_weight = candidate_z.getnnz()
363
+ if z_weight < z_code_distance:
364
+ z_code_distance = z_weight
365
+ combinations_considered += 1
366
+ except StopIteration:
367
+ z_exhausted = True
368
+
369
+ # Update code distances
370
+ self.x_code_distance = x_code_distance if x_code_distance != np.inf else None
371
+ self.z_code_distance = z_code_distance if z_code_distance != np.inf else None
372
+ self.code_distance = (
373
+ min(x_code_distance, z_code_distance)
374
+ if x_code_distance != np.inf and z_code_distance != np.inf
375
+ else None
376
+ )
377
+
378
+ # Calculate fraction of combinations considered
379
+ fraction_considered = combinations_considered / total_combinations
380
+
381
+ return (
382
+ int(x_code_distance) if x_code_distance != np.inf else None,
383
+ int(z_code_distance) if z_code_distance != np.inf else None,
384
+ fraction_considered,
385
+ )
386
+
387
+ def estimate_min_distance(
388
+ self,
389
+ timeout_seconds: float = 0.25,
390
+ p: float = 0.25,
391
+ reduce_logical_basis: bool = False,
392
+ decoder: Optional[BpOsdDecoder] = None,
393
+ ) -> int:
394
+ """
395
+ Estimate the minimum distance of the CSS code using a BP+OSD decoder-based search.
396
+
397
+ Parameters
398
+ ----------
399
+ timeout_seconds : float, optional
400
+ Time limit in seconds for the search. Default: 0.25
401
+ p : float, optional
402
+ Probability for including each logical operator in trial combinations. Default: 0.25
403
+ reduce_logical_basis : bool, optional
404
+ Whether to attempt reducing the logical operator basis. Default: False
405
+ decoder : Optional[BpOsdDecoder], optional
406
+ Pre-configured BP+OSD decoder. If None, initializes with default settings.
407
+
408
+ Returns
409
+ -------
410
+ int
411
+ Best estimate of code distance found within time limit.
412
+ """
413
+ start_time = time.time()
414
+
415
+ # Ensure logical operator bases are computed
416
+ if (
417
+ self.x_logical_operator_basis is None
418
+ or self.z_logical_operator_basis is None
419
+ ):
420
+ self.compute_logical_basis()
421
+
422
+ # Setup decoders for X and Z logical operators
423
+ bp_osd_x, x_stack, _, x_min_distance, x_max_distance = (
424
+ self._setup_distance_estimation_decoder(
425
+ self.x_stabilizer_matrix, self.x_logical_operator_basis, decoder
426
+ )
427
+ )
428
+ bp_osd_z, z_stack, _, z_min_distance, z_max_distance = (
429
+ self._setup_distance_estimation_decoder(
430
+ self.z_stabilizer_matrix, self.z_logical_operator_basis, decoder
431
+ )
432
+ )
433
+
434
+ candidate_logicals_x = []
435
+ candidate_logicals_z = []
436
+
437
+ # Search loop
438
+ with tqdm(total=timeout_seconds, desc="Estimating distance") as pbar:
439
+ while time.time() - start_time < timeout_seconds:
440
+ elapsed = time.time() - start_time
441
+ pbar.update(elapsed - pbar.n)
442
+
443
+ # Generate random logical combinations for X
444
+ dummy_syndrome_x = (
445
+ self._generate_random_logical_combination_for_distance_estimation(
446
+ x_stack, p, self.x_stabilizer_matrix.shape[0]
447
+ )
448
+ )
449
+ candidate_x = bp_osd_x.decode(dummy_syndrome_x)
450
+ x_weight = np.count_nonzero(candidate_x)
451
+
452
+ if x_weight < x_min_distance:
453
+ x_min_distance = x_weight
454
+
455
+ if x_weight < x_max_distance and reduce_logical_basis:
456
+ candidate_logicals_x.append(candidate_x)
457
+
458
+ # Generate random logical combinations for Z
459
+ dummy_syndrome_z = (
460
+ self._generate_random_logical_combination_for_distance_estimation(
461
+ z_stack, p, self.z_stabilizer_matrix.shape[0]
462
+ )
463
+ )
464
+ candidate_z = bp_osd_z.decode(dummy_syndrome_z)
465
+ z_weight = np.count_nonzero(candidate_z)
466
+
467
+ if z_weight < z_min_distance:
468
+ z_min_distance = z_weight
469
+
470
+ if z_weight < z_max_distance and reduce_logical_basis:
471
+ candidate_logicals_z.append(candidate_z)
472
+
473
+ # Update progress bar description
474
+ pbar.set_description(
475
+ f"Estimating distance: dx <= {x_min_distance}, dz <= {z_min_distance}"
476
+ )
477
+
478
+ # Update distances and reduce logical bases if applicable
479
+ self.x_code_distance = x_min_distance
480
+ self.z_code_distance = z_min_distance
481
+ self.code_distance = min(x_min_distance, z_min_distance)
482
+
483
+ if reduce_logical_basis:
484
+ self._reduce_logical_operator_basis(
485
+ candidate_logicals_x, candidate_logicals_z
486
+ )
487
+
488
+ return self.code_distance
489
+
490
+ def _setup_distance_estimation_decoder(
491
+ self, stabilizer_matrix, logical_operator_basis, decoder=None
492
+ ) -> Tuple[BpOsdDecoder, scipy.sparse.spmatrix, scipy.sparse.spmatrix, int, int]:
493
+ """
494
+ Helper function to set up the BP+OSD decoder for distance estimation.
495
+
496
+ Parameters
497
+ ----------
498
+ stabilizer_matrix : scipy.sparse.spmatrix
499
+ Stabilizer matrix of the code.
500
+ logical_operator_basis : scipy.sparse.spmatrix
501
+ Logical operator basis of the code.
502
+ decoder : Optional[BpOsdDecoder], optional
503
+ Pre-configured decoder. If None, initializes with default settings.
504
+
505
+ Returns
506
+ -------
507
+ Tuple[BpOsdDecoder, scipy.sparse.spmatrix, scipy.sparse.spmatrix, int, int]
508
+ Decoder, stacked matrix, stabilizer matrix, minimum distance, and maximum distance.
509
+ """
510
+ # Remove redundant rows from stabilizer matrix
511
+ p_rows = ldpc.mod2.pivot_rows(stabilizer_matrix)
512
+ full_rank_stabilizer_matrix = stabilizer_matrix[p_rows]
513
+
514
+ # Build a stacked matrix of stabilizers and logicals
515
+ stack = scipy.sparse.vstack(
516
+ [full_rank_stabilizer_matrix, logical_operator_basis]
517
+ ).tocsr()
518
+
519
+ # Initial distance estimate from current logicals
520
+ min_distance = np.min(logical_operator_basis.getnnz(axis=1))
521
+ max_distance = np.max(logical_operator_basis.getnnz(axis=1))
522
+
523
+ # Set up BP+OSD decoder if not provided
524
+ if decoder is None:
525
+ decoder = BpOsdDecoder(
526
+ stack,
527
+ error_rate=0.1,
528
+ max_iter=10,
529
+ bp_method="ms",
530
+ schedule="parallel",
531
+ ms_scaling_factor=1.0,
532
+ osd_method="osd_0",
533
+ osd_order=0,
534
+ )
535
+
536
+ return decoder, stack, full_rank_stabilizer_matrix, min_distance, max_distance
537
+
538
+ def _generate_random_logical_combination_for_distance_estimation(
539
+ self, stack: scipy.sparse.spmatrix, p: float, stabilizer_count: int
540
+ ) -> np.ndarray:
541
+ """
542
+ Generate a random logical combination for the BP+OSD decoder.
543
+
544
+ Parameters
545
+ ----------
546
+ stack : scipy.sparse.spmatrix
547
+ The stacked stabilizer and logical operator matrix.
548
+ p : float
549
+ Probability for including each logical operator in the combination.
550
+ stabilizer_count : int
551
+ Number of stabilizer rows in the stacked matrix.
552
+
553
+ Returns
554
+ -------
555
+ np.ndarray
556
+ Randomly generated syndrome vector.
557
+ """
558
+ random_mask = np.random.choice([0, 1], size=stack.shape[0], p=[1 - p, p])
559
+ random_mask[:stabilizer_count] = (
560
+ 0 # Ensure no stabilizer-only rows are selected
561
+ )
562
+
563
+ while not np.any(random_mask):
564
+ random_mask = np.random.choice([0, 1], size=stack.shape[0], p=[1 - p, p])
565
+ random_mask[:stabilizer_count] = 0
566
+
567
+ dummy_syndrome = np.zeros(stack.shape[0], dtype=np.uint8)
568
+ dummy_syndrome[np.nonzero(random_mask)[0]] = 1
569
+
570
+ return dummy_syndrome
571
+
572
+ def fix_logical_operators(self, fix_logical: str = "X"):
573
+ if not isinstance(fix_logical, str):
574
+ raise TypeError("fix_logical parameter must be a string")
575
+
576
+ if fix_logical.lower() == "x":
577
+ temp = self.z_logical_operator_basis @ self.x_logical_operator_basis.T
578
+ temp.data = temp.data % 2
579
+ temp = ldpc.mod2.inverse(temp)
580
+ self.z_logical_operator_basis = temp @ self.z_logical_operator_basis
581
+ self.z_logical_operator_basis.data = self.z_logical_operator_basis.data % 2
582
+
583
+ elif fix_logical.lower() == "z":
584
+ temp = self.x_logical_operator_basis @ self.z_logical_operator_basis.T
585
+ temp.data = temp.data % 2
586
+ temp = ldpc.mod2.inverse(temp)
587
+ self.x_logical_operator_basis = temp @ self.x_logical_operator_basis
588
+ self.x_logical_operator_basis.data = self.x_logical_operator_basis.data % 2
589
+ else:
590
+ raise ValueError("Invalid fix_logical parameter")
591
+
592
+ @property
593
+ def logical_operator_weights(self) -> Tuple[np.ndarray, np.ndarray]:
594
+ x_weights = []
595
+ z_weights = []
596
+ for i in range(self.logical_qubit_count):
597
+ x_weights.append(self.x_logical_operator_basis[i].nnz)
598
+ z_weights.append(self.z_logical_operator_basis[i].nnz)
599
+
600
+ return (np.array(x_weights), np.array(z_weights))
601
+
602
+ def __str__(self):
603
+ """
604
+ Return a string representation of the CSSCode object.
605
+
606
+ Returns:
607
+ str: String representation of the CSS code.
608
+ """
609
+ 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}]]"
@@ -45,7 +45,7 @@ class StabilizerCode(object):
45
45
  The number of logical qubits in the code.
46
46
  code_distance : int
47
47
  (Not computed by default) The distance of the code, if known or computed.
48
- logicals : scipy.sparse.spmatrix or None
48
+ logical_operator_basis : scipy.sparse.spmatrix or None
49
49
  A basis for the logical operators of the code.
50
50
  """
51
51
 
@@ -177,7 +177,7 @@ class StabilizerCode(object):
177
177
  kernel_h = ldpc.mod2.kernel(self.stabilizer_matrix)
178
178
 
179
179
  # Sort the rows of the kernel by weight
180
- row_weights = np.diff(kernel_h.indptr)
180
+ row_weights = kernel_h.getnnz(axis=1)
181
181
  sorted_rows = np.argsort(row_weights)
182
182
  kernel_h = kernel_h[sorted_rows, :]
183
183
 
@@ -377,166 +377,64 @@ class StabilizerCode(object):
377
377
  """
378
378
  return f"< Stabilizer Code, Name: {self.name}, Parameters: [[{self.physical_qubit_count}, {self.logical_qubit_count}, {self.code_distance}]] >"
379
379
 
380
- def reduce_logical_operator_basis(
381
- self,
382
- candidate_logicals: Union[Sequence, np.ndarray, scipy.sparse.spmatrix] = [],
383
- ):
384
- """
385
- Reduce the logical operator basis to include lower-weight logicals.
386
-
387
- Parameters
388
- ----------
389
- candidate_logicals : Union[Sequence, np.ndarray, scipy.sparse.spmatrix], optional
390
- A list or array of candidate logical operators to be considered for reducing the basis.
391
- Defaults to an empty list.
392
- """
393
- if len(candidate_logicals) != 0:
394
- # Convert candidates to a sparse matrix if they aren't already
395
- if not isinstance(candidate_logicals, scipy.sparse.spmatrix):
396
- candidate_logicals = scipy.sparse.csr_matrix(
397
- scipy.sparse.csr_matrix(candidate_logicals)
398
- )
399
-
400
- # Stack the candidate logicals with the existing logicals
401
- temp1 = scipy.sparse.vstack(
402
- [candidate_logicals, self.logical_operator_basis]
403
- ).tocsr()
404
-
405
- # Compute the Hamming weight over GF4 (number of qubits with non-identity operators)
406
- # Split into X and Z parts
407
- row_weights = binary_pauli_hamming_weight(temp1).flatten()
408
-
409
- # Sort the rows by Hamming weight (ascending)
410
- sorted_rows = np.argsort(row_weights)
411
- temp1 = temp1[sorted_rows, :]
412
-
413
- # Add the stabilizer matrix to the top of the stack
414
- temp1 = scipy.sparse.vstack([self.stabilizer_matrix, temp1])
415
-
416
- # Calculate the rank of the stabilizer matrix (todo: find way of removing this step)
417
- stabilizer_rank = ldpc.mod2.rank(self.stabilizer_matrix)
418
-
419
- # Perform row reduction to find a new logical basis
420
- p_rows = ldpc.mod2.pivot_rows(temp1)
421
- self.logical_operator_basis = temp1[p_rows[stabilizer_rank:]]
422
-
423
380
  def estimate_min_distance(
424
381
  self,
425
382
  timeout_seconds: float = 0.25,
426
383
  p: float = 0.25,
427
- max_iter: int = 10,
428
- error_rate: float = 0.1,
429
- bp_method: str = "ms",
430
- schedule: str = "parallel",
431
- ms_scaling_factor: float = 1.0,
432
- osd_method: str = "osd_0",
433
- osd_order: int = 0,
434
384
  reduce_logical_basis: bool = False,
385
+ decoder: Optional[BpOsdDecoder] = None,
435
386
  ) -> int:
436
387
  """
437
388
  Estimate the minimum distance of the stabilizer code using a BP+OSD decoder-based search.
438
389
 
439
390
  Parameters
440
391
  ----------
441
- timeout_seconds : float, optional
442
- The time limit (in seconds) for searching random linear combinations.
443
- p : float, optional
444
- Probability used to randomly include or exclude each logical operator
445
- when generating trial logical operators.
446
- max_iter : int, optional
447
- Maximum number of BP decoder iterations.
448
- error_rate : float, optional
449
- Crossover probability for the BP+OSD decoder.
450
- bp_method : str, optional
451
- Belief Propagation method (e.g., "ms" for min-sum).
452
- schedule : str, optional
453
- Update schedule for BP (e.g., "parallel").
454
- ms_scaling_factor : float, optional
455
- Scaling factor for min-sum updates.
456
- osd_method : str, optional
457
- Order-statistic decoding method (e.g., "osd_0").
458
- osd_order : int, optional
459
- OSD order.
460
- reduce_logical_basis : bool, optional
461
- If True, attempts to reduce the logical operator basis to include lower-weight operators.
392
+ timeout_seconds : float
393
+ Time limit in seconds for the search. Default: 0.25
394
+ p : float
395
+ Probability for including each logical operator in trial combinations. Default: 0.25
396
+ reduce_logical_basis : bool
397
+ Whether to attempt reducing logical operator basis. Default: False
398
+ decoder : Optional[BpOsdDecoder]
399
+ Pre-configured BP+OSD decoder. If None, initialises with default settings.
462
400
 
463
401
  Returns
464
402
  -------
465
403
  int
466
- The best-known estimate of the code distance found within the time limit.
404
+ Best estimate of code distance found within time limit
467
405
  """
468
406
  if self.logical_operator_basis is None:
469
407
  self.logical_operator_basis = self.compute_logical_basis()
470
408
 
471
- def decoder_setup():
472
- # # Remove redundnant rows from stabilizer matrix
473
- p_rows = ldpc.mod2.pivot_rows(self.stabilizer_matrix)
474
- full_rank_stabilizer_matrix = self.stabilizer_matrix[p_rows]
475
- # full_rank_stabilizer_matrix = self.stabilizer_matrix
476
-
477
- # Build a stacked matrix of stabilizers and logicals
478
- stack = scipy.sparse.vstack(
479
- [full_rank_stabilizer_matrix, self.logical_operator_basis]
480
- ).tocsr()
481
-
482
- # Initial distance estimate from the current logicals
483
-
484
- min_distance = np.min(
485
- binary_pauli_hamming_weight(self.logical_operator_basis)
486
- )
487
-
488
- max_distance = np.max(self.logical_basis_weights())
489
-
490
- # Set up BP+OSD decoder
491
- bp_osd = BpOsdDecoder(
492
- stack,
493
- error_rate=error_rate,
494
- max_iter=max_iter,
495
- bp_method=bp_method,
496
- schedule=schedule,
497
- ms_scaling_factor=ms_scaling_factor,
498
- osd_method=osd_method,
499
- osd_order=osd_order,
500
- )
501
-
502
- return (
503
- bp_osd,
504
- stack,
505
- full_rank_stabilizer_matrix,
506
- min_distance,
507
- max_distance,
508
- )
509
-
510
- # setup the decoder
409
+ # Initial setup of decoder and parameters
511
410
  bp_osd, stack, full_rank_stabilizer_matrix, min_distance, max_distance = (
512
- decoder_setup()
411
+ self._setup_distance_estimation_decoder(decoder)
513
412
  )
514
413
 
515
- # List to store candidate logical operators for basis reduction
414
+ # Initialize storage for candidate logicals and tracking
516
415
  candidate_logicals = []
416
+ weight_one_syndromes_searched = 0
517
417
 
518
- # 2) Randomly search for better representatives of logical operators
418
+ # Main search loop
519
419
  start_time = time.time()
520
420
  with tqdm(total=timeout_seconds, desc="Estimating distance") as pbar:
521
- weight_one_syndromes_searched = 0
522
421
  while time.time() - start_time < timeout_seconds:
422
+ # Update progress bar
523
423
  elapsed = time.time() - start_time
524
- # Update progress bar based on elapsed time
525
424
  pbar.update(elapsed - pbar.n)
526
425
 
527
426
  # Initialize an empty dummy syndrome
528
427
  dummy_syndrome = np.zeros(stack.shape[0], dtype=np.uint8)
529
428
 
530
429
  if weight_one_syndromes_searched < self.logical_operator_basis.shape[0]:
430
+ # Try each logical operator individually first
531
431
  dummy_syndrome[
532
432
  full_rank_stabilizer_matrix.shape[0]
533
433
  + weight_one_syndromes_searched
534
- ] = 1 # pick exactly one logical operator
434
+ ] = 1
535
435
  weight_one_syndromes_searched += 1
536
-
537
436
  else:
538
437
  # Randomly pick a combination of logical rows
539
- # (with probability p, set the corresponding row in the syndrome to 1)
540
438
  while True:
541
439
  random_mask = np.random.choice(
542
440
  [0, 1],
@@ -550,7 +448,6 @@ class StabilizerCode(object):
550
448
  dummy_syndrome[self.stabilizer_matrix.shape[0] + idx] = 1
551
449
 
552
450
  candidate = bp_osd.decode(dummy_syndrome)
553
-
554
451
  w = np.count_nonzero(
555
452
  candidate[: self.physical_qubit_count]
556
453
  | candidate[self.physical_qubit_count :]
@@ -558,44 +455,129 @@ class StabilizerCode(object):
558
455
 
559
456
  if w < min_distance:
560
457
  min_distance = w
561
- if w < max_distance:
562
- if reduce_logical_basis:
563
- lc = np.hstack(
564
- [
565
- candidate[self.physical_qubit_count :],
566
- candidate[: self.physical_qubit_count],
567
- ]
568
- )
569
- candidate_logicals.append(lc)
570
-
571
- # 3) If requested, reduce the logical operator basis to include lower-weight operators
458
+ if w < max_distance and reduce_logical_basis:
459
+ lc = np.hstack(
460
+ [
461
+ candidate[self.physical_qubit_count :],
462
+ candidate[: self.physical_qubit_count],
463
+ ]
464
+ )
465
+ candidate_logicals.append(lc)
466
+
467
+ # Reduce logical operator basis if we have enough candidates
572
468
  if (
573
469
  len(candidate_logicals) >= self.logical_qubit_count
574
470
  and reduce_logical_basis
575
471
  ):
576
- self.reduce_logical_operator_basis(candidate_logicals)
472
+ self._reduce_logical_operator_basis(candidate_logicals)
577
473
  (
578
474
  bp_osd,
579
475
  stack,
580
476
  full_rank_stabilizer_matrix,
581
477
  min_distance,
582
478
  max_distance,
583
- ) = decoder_setup()
479
+ ) = self._setup_distance_estimation_decoder(decoder)
584
480
  candidate_logicals = []
585
481
  weight_one_syndromes_searched = 0
586
482
 
587
483
  pbar.set_description(
588
- f"Estimating distance: min-weight found <= {min_distance}, basis weights: {self.logical_basis_weights()}"
484
+ f"Estimating distance: min-weight found <= {min_distance}, "
485
+ f"basis weights: {self.logical_basis_weights()}"
589
486
  )
590
487
 
591
- if reduce_logical_basis and len(candidate_logicals) > 0:
592
- self.reduce_logical_operator_basis(candidate_logicals)
593
- candidate_logicals = []
594
- weight_one_syndromes_searched = 0
595
- max_distance = np.max(self.logical_basis_weights())
488
+ # Final basis reduction if needed
489
+ if reduce_logical_basis and candidate_logicals:
490
+ self._reduce_logical_operator_basis(candidate_logicals)
596
491
 
597
- # Update and return the estimated distance
598
492
  self.code_distance = min_distance
493
+ return min_distance
494
+
495
+ def _setup_distance_estimation_decoder(
496
+ self, decoder: Optional[BpOsdDecoder] = None
497
+ ) -> Tuple[BpOsdDecoder, scipy.sparse.spmatrix, scipy.sparse.spmatrix, int, int]:
498
+ """
499
+ Set up decoder and initial parameters.
500
+
501
+ Parameters
502
+ ----------
503
+ decoder : Optional[BpOsdDecoder]
504
+ Pre-configured decoder. If None, initialises with default settings.
505
+
506
+ Returns
507
+ -------
508
+ Tuple[BpOsdDecoder, scipy.sparse.spmatrix, scipy.sparse.spmatrix, int, int]
509
+ Returns (decoder, stack matrix, full rank stabilizer matrix, min distance, max distance)
510
+ """
511
+ # Remove redundant rows from stabilizer matrix
512
+ p_rows = ldpc.mod2.pivot_rows(self.stabilizer_matrix)
513
+ full_rank_stabilizer_matrix = self.stabilizer_matrix[p_rows]
514
+
515
+ # Build a stacked matrix of stabilizers and logicals
516
+ stack = scipy.sparse.vstack(
517
+ [full_rank_stabilizer_matrix, self.logical_operator_basis]
518
+ ).tocsr()
519
+
520
+ # Initial distance estimate from current logicals
521
+ min_distance = np.min(binary_pauli_hamming_weight(self.logical_operator_basis))
522
+ max_distance = np.max(self.logical_basis_weights())
523
+
524
+ # Set up BP+OSD decoder if not provided
525
+ if decoder is None:
526
+ decoder = BpOsdDecoder(
527
+ stack,
528
+ error_rate=0.1,
529
+ max_iter=10,
530
+ bp_method="ms",
531
+ schedule="parallel",
532
+ ms_scaling_factor=1.0,
533
+ osd_method="osd_0",
534
+ osd_order=0,
535
+ )
536
+
537
+ return decoder, stack, full_rank_stabilizer_matrix, min_distance, max_distance
538
+
539
+ def _reduce_logical_operator_basis(
540
+ self,
541
+ candidate_logicals: Union[Sequence, np.ndarray, scipy.sparse.spmatrix] = [],
542
+ ):
543
+ """
544
+ Reduce the logical operator basis to include lower-weight logicals.
545
+
546
+ Parameters
547
+ ----------
548
+ candidate_logicals : Union[Sequence, np.ndarray, scipy.sparse.spmatrix], optional
549
+ A list or array of candidate logical operators to be considered for reducing the basis.
550
+ Defaults to an empty list.
551
+ """
552
+ if len(candidate_logicals) != 0:
553
+ # Convert candidates to a sparse matrix if they aren't already
554
+ if not isinstance(candidate_logicals, scipy.sparse.spmatrix):
555
+ candidate_logicals = scipy.sparse.csr_matrix(
556
+ scipy.sparse.csr_matrix(candidate_logicals)
557
+ )
558
+
559
+ # Stack the candidate logicals with the existing logicals
560
+ temp1 = scipy.sparse.vstack(
561
+ [candidate_logicals, self.logical_operator_basis]
562
+ ).tocsr()
563
+
564
+ # Compute the Hamming weight over GF4 (number of qubits with non-identity operators)
565
+ # Split into X and Z parts
566
+ row_weights = binary_pauli_hamming_weight(temp1).flatten()
567
+
568
+ # Sort the rows by Hamming weight (ascending)
569
+ sorted_rows = np.argsort(row_weights)
570
+ temp1 = temp1[sorted_rows, :]
571
+
572
+ # Add the stabilizer matrix to the top of the stack
573
+ temp1 = scipy.sparse.vstack([self.stabilizer_matrix, temp1])
574
+
575
+ # Calculate the rank of the stabilizer matrix (todo: find way of removing this step)
576
+ stabilizer_rank = ldpc.mod2.rank(self.stabilizer_matrix)
577
+
578
+ # Perform row reduction to find a new logical basis
579
+ p_rows = ldpc.mod2.pivot_rows(temp1)
580
+ self.logical_operator_basis = temp1[p_rows[stabilizer_rank:]]
599
581
 
600
582
  def logical_basis_weights(self):
601
583
  """
@@ -299,7 +299,9 @@ def symplectic_product(
299
299
  a = convert_to_binary_scipy_sparse(a)
300
300
  b = convert_to_binary_scipy_sparse(b)
301
301
 
302
- assert (a.shape[1] == b.shape[1]), "Input matrices must have the same number of columns."
302
+ assert (
303
+ a.shape[1] == b.shape[1]
304
+ ), "Input matrices must have the same number of columns."
303
305
  assert a.shape[1] % 2 == 0, "Input matrices must have an even number of columns."
304
306
 
305
307
  n = a.shape[1] // 2
@@ -1,4 +1,5 @@
1
1
  import logging
2
+
2
3
  # Suppress debug and info messages from urllib3 and requests libraries
3
4
  logging.getLogger("urllib3").setLevel(logging.WARNING)
4
5
  logging.getLogger("requests").setLevel(logging.WARNING)
@@ -9,6 +10,7 @@ import requests
9
10
  from bs4 import BeautifulSoup
10
11
  import json
11
12
 
13
+
12
14
  def get_codetables_de_matrix(q, n, k, output_json_path=None, write_to_file=False):
13
15
  """
14
16
  Retrieve quantum code data from Markus Grassl's codetables.de website.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: qec
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Python Tools for Quantum Error Correction
5
5
  Author-email: Joschka Roffe <joschka@roffe.eu>
6
6
  License: MIT License
@@ -0,0 +1,16 @@
1
+ qec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ qec/quantum_codes/__init__.py,sha256=DQ1ztrq-vBpTyoehaMWOhals46tRj553Jmkq68bDk-E,117
3
+ qec/quantum_codes/codetables_de.py,sha256=loBDBOK2cbDJ5moKmIx2MXg6e30XEPrEYau19bbDgac,3623
4
+ qec/quantum_codes/five_qubit_code.py,sha256=0zrGLyIpfyKwYG7uL00yMcM5PdhQGF17_MiI2qTMhOk,2190
5
+ qec/stabilizer_code/__init__.py,sha256=L5UMjHBlvfQBhkNlEZYSkyaHvNOcDHjc3oxYibMYHRk,63
6
+ qec/stabilizer_code/css_code.py,sha256=JhNiBHqfwu4OgMVUsXl6yJ4L5KNW4Dn2Sf0beBdAl2s,24763
7
+ qec/stabilizer_code/stabilizer_code.py,sha256=I5u8JKZu88ioC4E2nBJ-00xCmnL8nU6kdAvwYOfmNRk,22138
8
+ qec/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ qec/utils/binary_pauli_utils.py,sha256=BSlngYDdRICu0aVu4u_m0bvLicohORyGxfk5eRER7TQ,13245
10
+ qec/utils/codetables_de_utils.py,sha256=S1wcVGJkkASQQ5s71QAsYBmpyE-3xTb6UsvgMfQtuiw,9469
11
+ qec/utils/sparse_binary_utils.py,sha256=Y9xfGKzOGFiVTyhb6iF6N7-5oMY6Ah9oLrnv8HhSBHA,1965
12
+ qec-0.2.1.dist-info/LICENSE,sha256=1b_xwNz1znYBfEaCL6pN2gNBAn8pQIjDRs_UhDp1EJI,1066
13
+ qec-0.2.1.dist-info/METADATA,sha256=AbWaMM6fYb65-0lUw6qWuywZigdtHseO-6QAbNZK0QM,2367
14
+ qec-0.2.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
15
+ qec-0.2.1.dist-info/top_level.txt,sha256=d8l_7pJ5u9uWdviNp0FUK-j8VPZqywkDek7qa4NDank,4
16
+ qec-0.2.1.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- qec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- qec/quantum_codes/__init__.py,sha256=DQ1ztrq-vBpTyoehaMWOhals46tRj553Jmkq68bDk-E,117
3
- qec/quantum_codes/codetables_de.py,sha256=loBDBOK2cbDJ5moKmIx2MXg6e30XEPrEYau19bbDgac,3623
4
- qec/quantum_codes/five_qubit_code.py,sha256=0zrGLyIpfyKwYG7uL00yMcM5PdhQGF17_MiI2qTMhOk,2190
5
- qec/stabilizer_code/__init__.py,sha256=L5UMjHBlvfQBhkNlEZYSkyaHvNOcDHjc3oxYibMYHRk,63
6
- qec/stabilizer_code/css_code.py,sha256=8BotcCuWrbnxnbZ1ZIJDI1jgr6-ohq-haPolc59TcWw,127
7
- qec/stabilizer_code/stabilizer_code.py,sha256=_3oQwq2UNkPmP2R2qcsKTzYO4CLDvQdaiGxsN4_4r0I,22804
8
- qec/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- qec/utils/binary_pauli_utils.py,sha256=FKxOMyEgUfSL1DF--8GUf4Nl6ytbK8Slyw7x2evhAac,13231
10
- qec/utils/codetables_de_utils.py,sha256=soCf3u2v-C5EYYMiL8Ta4H6UF8KhRCEkjxLd6qBJai4,9467
11
- qec/utils/sparse_binary_utils.py,sha256=Y9xfGKzOGFiVTyhb6iF6N7-5oMY6Ah9oLrnv8HhSBHA,1965
12
- qec-0.2.0.dist-info/LICENSE,sha256=1b_xwNz1znYBfEaCL6pN2gNBAn8pQIjDRs_UhDp1EJI,1066
13
- qec-0.2.0.dist-info/METADATA,sha256=DisbbTcVUey4dp5WelBc4aZeFcUkkwpsxRzMd44QncU,2367
14
- qec-0.2.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
15
- qec-0.2.0.dist-info/top_level.txt,sha256=d8l_7pJ5u9uWdviNp0FUK-j8VPZqywkDek7qa4NDank,4
16
- qec-0.2.0.dist-info/RECORD,,
File without changes
File without changes