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