qec 0.0.11__py3-none-any.whl → 0.2.0__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,609 @@
1
+ from qec.utils.sparse_binary_utils import convert_to_binary_scipy_sparse
2
+ from qec.utils.binary_pauli_utils import (
3
+ symplectic_product,
4
+ check_binary_pauli_matrices_commute,
5
+ pauli_str_to_binary_pcm,
6
+ binary_pcm_to_pauli_str,
7
+ binary_pauli_hamming_weight,
8
+ )
9
+
10
+ import numpy as np
11
+ import scipy.sparse
12
+ from tqdm import tqdm
13
+ from ldpc import BpOsdDecoder
14
+ import ldpc.mod2
15
+ import time
16
+ from typing import Tuple, Optional, Union, Sequence
17
+ import logging
18
+
19
+ logging.basicConfig(level=logging.DEBUG)
20
+
21
+
22
+ class StabilizerCode(object):
23
+ """
24
+ A quantum stabilizer code, which defines and manipulates stabilizer generators,
25
+ computes logical operators, and stores parameters such as the number of physical qubits
26
+ and the number of logical qubits.
27
+
28
+ Parameters
29
+ ----------
30
+ stabilizers : np.typing.ArrayLike or scipy.sparse.spmatrix or list
31
+ Either a binary parity check matrix (with an even number of columns),
32
+ or a list of Pauli strings that specify the stabilizers of the code.
33
+ name : str, optional
34
+ A name for the code. Defaults to "stabilizer code".
35
+
36
+ Attributes
37
+ ----------
38
+ name : str
39
+ The name of the code.
40
+ stabilizer_matrix : scipy.sparse.spmatrix
41
+ The binary parity check matrix representation of the stabilizers.
42
+ phyical_qubit_count : int
43
+ The number of physical qubits in the code.
44
+ logical_qubit_count : int
45
+ The number of logical qubits in the code.
46
+ code_distance : int
47
+ (Not computed by default) The distance of the code, if known or computed.
48
+ logicals : scipy.sparse.spmatrix or None
49
+ A basis for the logical operators of the code.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ stabilizers: Union[np.ndarray, scipy.sparse.spmatrix, list],
55
+ name: str = None,
56
+ ):
57
+ """
58
+ Construct a StabilizerCode instance from either a parity check matrix or a list of
59
+ Pauli stabilizers.
60
+
61
+ Parameters
62
+ ----------
63
+ stabilizers : np.typing.ArrayLike or scipy.sparse.spmatrix or list
64
+ Either a binary parity check matrix (with an even number of columns),
65
+ or a list of Pauli strings that specify the stabilizers of the code.
66
+ name : str, optional
67
+ A name for the code. If None, it defaults to "stabilizer code".
68
+
69
+ Raises
70
+ ------
71
+ TypeError
72
+ If `stabilizers` is not an array-like, sparse matrix, or list of Pauli strings.
73
+ ValueError
74
+ If the parity check matrix does not have an even number of columns,
75
+ or the stabilizers do not mutually commute.
76
+ """
77
+ self.name = name if name else "stabilizer code"
78
+
79
+ self.stabilizer_matrix = None
80
+ self.physical_qubit_count = None
81
+ self.logical_qubit_count = None
82
+ self.code_distance = None
83
+ self.logical_operator_basis = None
84
+
85
+ if isinstance(stabilizers, list):
86
+ stabilizers = np.array(stabilizers)
87
+
88
+ if not isinstance(stabilizers, (np.ndarray, scipy.sparse.spmatrix)):
89
+ raise TypeError(
90
+ "Please provide either a parity check matrix or a list of Pauli stabilizers."
91
+ )
92
+
93
+ if isinstance(stabilizers, np.ndarray) and stabilizers.dtype.kind in {"U", "S"}:
94
+ self.stabilizer_matrix = pauli_str_to_binary_pcm(stabilizers)
95
+ else:
96
+ if stabilizers.shape[1] % 2 == 0:
97
+ self.stabilizer_matrix = convert_to_binary_scipy_sparse(stabilizers)
98
+ else:
99
+ raise ValueError(
100
+ "The parity check matrix must have an even number of columns."
101
+ )
102
+
103
+ self.physical_qubit_count = self.stabilizer_matrix.shape[1] // 2
104
+
105
+ # Check that stabilizers commute
106
+ if not self.check_stabilizers_commute():
107
+ raise ValueError("The stabilizers do not commute.")
108
+
109
+ # Compute the number of logical qubits
110
+ self.logical_qubit_count = self.physical_qubit_count - ldpc.mod2.rank(
111
+ self.stabilizer_matrix, method="dense"
112
+ )
113
+
114
+ # Compute a basis for the logical operators of the code
115
+ self.logical_operator_basis = self.compute_logical_basis()
116
+
117
+ @property
118
+ def pauli_stabilizers(self):
119
+ """
120
+ Get or set the stabilizers in Pauli string format.
121
+
122
+ Returns
123
+ -------
124
+ np.ndarray
125
+ An array of Pauli strings representing the stabilizers.
126
+ """
127
+ return binary_pcm_to_pauli_str(self.stabilizer_matrix)
128
+
129
+ @pauli_stabilizers.setter
130
+ def pauli_stabilizers(self, pauli_stabilizers: np.ndarray):
131
+ """
132
+ Set the stabilizers using Pauli strings.
133
+
134
+ Parameters
135
+ ----------
136
+ pauli_stabilizers : np.ndarray
137
+ An array of Pauli strings representing the stabilizers.
138
+
139
+ Raises
140
+ ------
141
+ AssertionError
142
+ If the newly set stabilizers do not commute.
143
+ """
144
+ self.stabilizer_matrix = pauli_str_to_binary_pcm(pauli_stabilizers)
145
+ if not self.check_stabilizers_commute():
146
+ raise ValueError("The stabilizers do not commute.")
147
+
148
+ def check_stabilizers_commute(self) -> bool:
149
+ """
150
+ Check whether the current set of stabilizers mutually commute.
151
+
152
+ Returns
153
+ -------
154
+ bool
155
+ True if all stabilizers commute, otherwise False.
156
+ """
157
+ return check_binary_pauli_matrices_commute(
158
+ self.stabilizer_matrix, self.stabilizer_matrix
159
+ )
160
+
161
+ def compute_logical_basis(self) -> scipy.sparse.spmatrix:
162
+ """
163
+ Compute a basis for the logical operators of the code by extending the parity check
164
+ matrix. The resulting basis operators are stored in `self.logicals`.
165
+
166
+ Returns
167
+ -------
168
+ scipy.sparse.spmatrix
169
+ A basis for the logical operators in binary representation.
170
+
171
+ Notes
172
+ -----
173
+ This method uses the kernel of the parity check matrix to find operators that
174
+ commute with all stabilizers, and then identifies a subset that spans the space
175
+ of logical operators.
176
+ """
177
+ kernel_h = ldpc.mod2.kernel(self.stabilizer_matrix)
178
+
179
+ # Sort the rows of the kernel by weight
180
+ row_weights = np.diff(kernel_h.indptr)
181
+ sorted_rows = np.argsort(row_weights)
182
+ kernel_h = kernel_h[sorted_rows, :]
183
+
184
+ swapped_kernel = scipy.sparse.hstack(
185
+ [
186
+ kernel_h[:, self.physical_qubit_count :],
187
+ kernel_h[:, : self.physical_qubit_count],
188
+ ]
189
+ )
190
+
191
+ logical_stack = scipy.sparse.vstack([self.stabilizer_matrix, swapped_kernel])
192
+ p_rows = ldpc.mod2.pivot_rows(logical_stack)
193
+
194
+ self.logical_operator_basis = logical_stack[
195
+ p_rows[self.stabilizer_matrix.shape[0] :]
196
+ ]
197
+
198
+ if self.logical_operator_basis.nnz == 0:
199
+ self.code_distance = np.inf
200
+ return self.logical_operator_basis
201
+
202
+ basis_minimum_hamming_weight = np.min(
203
+ binary_pauli_hamming_weight(self.logical_operator_basis).flatten()
204
+ )
205
+
206
+ # Update distance based on the minimum hamming weight of the logical operators in this basis
207
+ if self.code_distance is None:
208
+ self.code_distance = basis_minimum_hamming_weight
209
+ elif basis_minimum_hamming_weight < self.code_distance:
210
+ self.code_distance = basis_minimum_hamming_weight
211
+ else:
212
+ pass
213
+
214
+ return logical_stack[p_rows[self.stabilizer_matrix.shape[0] :]]
215
+
216
+ def check_valid_logical_basis(self) -> bool:
217
+ """
218
+ Validate that the stored logical operators form a proper logical basis for the code.
219
+
220
+ Checks that they commute with the stabilizers, pairwise anti-commute (in the symplectic
221
+ sense), and have full rank.
222
+
223
+ Returns
224
+ -------
225
+ bool
226
+ True if the logical operators form a valid basis, otherwise False.
227
+ """
228
+ try:
229
+ assert check_binary_pauli_matrices_commute(
230
+ self.stabilizer_matrix, self.logical_operator_basis
231
+ ), "Logical operators do not commute with stabilizers."
232
+
233
+ logical_product = symplectic_product(
234
+ self.logical_operator_basis, self.logical_operator_basis
235
+ )
236
+ logical_product.eliminate_zeros()
237
+ assert (
238
+ logical_product.nnz != 0
239
+ ), "The logical operators do not anti-commute with one another."
240
+
241
+ assert (
242
+ ldpc.mod2.rank(self.logical_operator_basis, method="dense")
243
+ == 2 * self.logical_qubit_count
244
+ ), "The logical operators do not form a basis for the code."
245
+
246
+ assert (
247
+ self.logical_operator_basis.shape[0] == 2 * self.logical_qubit_count
248
+ ), "The logical operators are not linearly independent."
249
+
250
+ except AssertionError as e:
251
+ logging.error(e)
252
+ return False
253
+
254
+ return True
255
+
256
+ def compute_exact_code_distance(
257
+ self, timeout: float = 0.5
258
+ ) -> Tuple[Optional[int], float]:
259
+ """
260
+ Compute the distance of the code by searching through linear combinations of
261
+ logical operators and stabilizers, returning a tuple of the minimal Hamming weight
262
+ found and the fraction of logical operators considered before timing out.
263
+
264
+ Parameters
265
+ ----------
266
+ timeout : float, optional
267
+ The time limit (in seconds) for the exhaustive search. Default is 0.5 seconds. To obtain the exact distance, set to `np.inf`.
268
+
269
+ Returns
270
+ -------
271
+ Tuple[Optional[int], float]
272
+ A tuple containing:
273
+ - The best-known distance of the code as an integer (or `None` if no distance was found).
274
+ - The fraction of logical combinations considered before the search ended.
275
+
276
+ Notes
277
+ -----
278
+ - We compute the row span of both the stabilizers and the logical operators.
279
+ - For every logical operator in the logical span, we add (mod 2) each stabilizer
280
+ in the stabilizer span to form candidate logical operators.
281
+ - We compute the Hamming weight of each candidate operator (i.e. how many qubits
282
+ are acted upon by the operator).
283
+ - We track the minimal Hamming weight encountered. If `timeout` is exceeded,
284
+ we immediately return the best distance found so far.
285
+
286
+ Examples
287
+ --------
288
+ >>> code = StabilizerCode(["XZZX", "ZZXX"])
289
+ >>> dist, fraction = code.compute_exact_code_distance(timeout=1.0)
290
+ >>> print(dist, fraction)
291
+ """
292
+ start_time = time.time()
293
+
294
+ stabilizer_span = ldpc.mod2.row_span(self.stabilizer_matrix)[1:]
295
+ logical_span = ldpc.mod2.row_span(self.logical_operator_basis)[1:]
296
+
297
+ if self.code_distance is None:
298
+ distance = np.inf
299
+ else:
300
+ distance = self.code_distance
301
+
302
+ logicals_considered = 0
303
+ total_logical_operators = stabilizer_span.shape[0] * logical_span.shape[0]
304
+
305
+ for logical in logical_span:
306
+ if time.time() - start_time > timeout:
307
+ break
308
+ for stabilizer in stabilizer_span:
309
+ if time.time() - start_time > timeout:
310
+ break
311
+ candidate_logical = logical + stabilizer
312
+ candidate_logical.data %= 2
313
+
314
+ hamming_weight = binary_pauli_hamming_weight(candidate_logical)[0]
315
+ if hamming_weight < distance:
316
+ distance = hamming_weight
317
+ logicals_considered += 1
318
+
319
+ self.code_distance = distance
320
+ fraction_considered = logicals_considered / total_logical_operators
321
+
322
+ return (
323
+ (int(distance), fraction_considered)
324
+ if distance != np.inf
325
+ else (None, fraction_considered)
326
+ )
327
+
328
+ def get_code_parameters(self) -> tuple:
329
+ """
330
+ Return the parameters of the code as a tuple: (n, k, d).
331
+
332
+ Returns
333
+ -------
334
+ tuple
335
+ A tuple of integers representing the number of physical qubits, logical qubits,
336
+ and the distance of the code.
337
+ """
338
+ return self.physical_qubit_count, self.logical_qubit_count, self.code_distance
339
+
340
+ def save_code(self, save_dense: bool = False):
341
+ """
342
+ Save the stabilizer code to disk.
343
+
344
+ Parameters
345
+ ----------
346
+ save_dense : bool, optional
347
+ If True, saves the parity check matrix as a dense format.
348
+ Otherwise, saves the parity check matrix as a sparse format.
349
+ """
350
+ pass
351
+
352
+ def load_code(self):
353
+ """
354
+ Load the stabilizer code from a saved file.
355
+ """
356
+ pass
357
+
358
+ def __repr__(self):
359
+ """
360
+ Return an unambiguous string representation of the StabilizerCode instance.
361
+
362
+ Returns
363
+ -------
364
+ str
365
+ An unambiguous representation for debugging and development.
366
+ """
367
+ return f"Name: {self.name}, Class: Stabilizer Code"
368
+
369
+ def __str__(self):
370
+ """
371
+ Return a string describing the stabilizer code, including its parameters.
372
+
373
+ Returns
374
+ -------
375
+ str
376
+ A human-readable string with the name, n, k, and d parameters of the code.
377
+ """
378
+ return f"< Stabilizer Code, Name: {self.name}, Parameters: [[{self.physical_qubit_count}, {self.logical_qubit_count}, {self.code_distance}]] >"
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
+ def estimate_min_distance(
424
+ self,
425
+ timeout_seconds: float = 0.25,
426
+ 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
+ reduce_logical_basis: bool = False,
435
+ ) -> int:
436
+ """
437
+ Estimate the minimum distance of the stabilizer code using a BP+OSD decoder-based search.
438
+
439
+ Parameters
440
+ ----------
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.
462
+
463
+ Returns
464
+ -------
465
+ int
466
+ The best-known estimate of the code distance found within the time limit.
467
+ """
468
+ if self.logical_operator_basis is None:
469
+ self.logical_operator_basis = self.compute_logical_basis()
470
+
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
511
+ bp_osd, stack, full_rank_stabilizer_matrix, min_distance, max_distance = (
512
+ decoder_setup()
513
+ )
514
+
515
+ # List to store candidate logical operators for basis reduction
516
+ candidate_logicals = []
517
+
518
+ # 2) Randomly search for better representatives of logical operators
519
+ start_time = time.time()
520
+ with tqdm(total=timeout_seconds, desc="Estimating distance") as pbar:
521
+ weight_one_syndromes_searched = 0
522
+ while time.time() - start_time < timeout_seconds:
523
+ elapsed = time.time() - start_time
524
+ # Update progress bar based on elapsed time
525
+ pbar.update(elapsed - pbar.n)
526
+
527
+ # Initialize an empty dummy syndrome
528
+ dummy_syndrome = np.zeros(stack.shape[0], dtype=np.uint8)
529
+
530
+ if weight_one_syndromes_searched < self.logical_operator_basis.shape[0]:
531
+ dummy_syndrome[
532
+ full_rank_stabilizer_matrix.shape[0]
533
+ + weight_one_syndromes_searched
534
+ ] = 1 # pick exactly one logical operator
535
+ weight_one_syndromes_searched += 1
536
+
537
+ else:
538
+ # Randomly pick a combination of logical rows
539
+ # (with probability p, set the corresponding row in the syndrome to 1)
540
+ while True:
541
+ random_mask = np.random.choice(
542
+ [0, 1],
543
+ size=self.logical_operator_basis.shape[0],
544
+ p=[1 - p, p],
545
+ )
546
+ if np.any(random_mask):
547
+ break
548
+ for idx, bit in enumerate(random_mask):
549
+ if bit == 1:
550
+ dummy_syndrome[self.stabilizer_matrix.shape[0] + idx] = 1
551
+
552
+ candidate = bp_osd.decode(dummy_syndrome)
553
+
554
+ w = np.count_nonzero(
555
+ candidate[: self.physical_qubit_count]
556
+ | candidate[self.physical_qubit_count :]
557
+ )
558
+
559
+ if w < min_distance:
560
+ 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
572
+ if (
573
+ len(candidate_logicals) >= self.logical_qubit_count
574
+ and reduce_logical_basis
575
+ ):
576
+ self.reduce_logical_operator_basis(candidate_logicals)
577
+ (
578
+ bp_osd,
579
+ stack,
580
+ full_rank_stabilizer_matrix,
581
+ min_distance,
582
+ max_distance,
583
+ ) = decoder_setup()
584
+ candidate_logicals = []
585
+ weight_one_syndromes_searched = 0
586
+
587
+ pbar.set_description(
588
+ f"Estimating distance: min-weight found <= {min_distance}, basis weights: {self.logical_basis_weights()}"
589
+ )
590
+
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())
596
+
597
+ # Update and return the estimated distance
598
+ self.code_distance = min_distance
599
+
600
+ def logical_basis_weights(self):
601
+ """
602
+ Return the Hamming weights of the logical operators in the current basis.
603
+
604
+ Returns
605
+ -------
606
+ np.ndarray
607
+ An array of integers representing the Hamming weights of the logical operators.
608
+ """
609
+ return binary_pauli_hamming_weight(self.logical_operator_basis).flatten()
qec/utils/__init__.py ADDED
File without changes