qec 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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