qec 0.0.11__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.
@@ -0,0 +1,609 @@
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)
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
+ # 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}]]"