qec 0.0.11__py3-none-any.whl → 0.2.0__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.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