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