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,591 @@
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
+ logical_operator_basis : 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 = kernel_h.getnnz(axis=1)
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 estimate_min_distance(
381
+ self,
382
+ timeout_seconds: float = 0.25,
383
+ p: float = 0.25,
384
+ reduce_logical_basis: bool = False,
385
+ decoder: Optional[BpOsdDecoder] = None,
386
+ ) -> int:
387
+ """
388
+ Estimate the minimum distance of the stabilizer code using a BP+OSD decoder-based search.
389
+
390
+ Parameters
391
+ ----------
392
+ timeout_seconds : float
393
+ Time limit in seconds for the search. Default: 0.25
394
+ p : float
395
+ Probability for including each logical operator in trial combinations. Default: 0.25
396
+ reduce_logical_basis : bool
397
+ Whether to attempt reducing logical operator basis. Default: False
398
+ decoder : Optional[BpOsdDecoder]
399
+ Pre-configured BP+OSD decoder. If None, initialises with default settings.
400
+
401
+ Returns
402
+ -------
403
+ int
404
+ Best estimate of code distance found within time limit
405
+ """
406
+ if self.logical_operator_basis is None:
407
+ self.logical_operator_basis = self.compute_logical_basis()
408
+
409
+ # Initial setup of decoder and parameters
410
+ bp_osd, stack, full_rank_stabilizer_matrix, min_distance, max_distance = (
411
+ self._setup_distance_estimation_decoder(decoder)
412
+ )
413
+
414
+ # Initialize storage for candidate logicals and tracking
415
+ candidate_logicals = []
416
+ weight_one_syndromes_searched = 0
417
+
418
+ # Main search loop
419
+ start_time = time.time()
420
+ with tqdm(total=timeout_seconds, desc="Estimating distance") as pbar:
421
+ while time.time() - start_time < timeout_seconds:
422
+ # Update progress bar
423
+ elapsed = time.time() - start_time
424
+ pbar.update(elapsed - pbar.n)
425
+
426
+ # Initialize an empty dummy syndrome
427
+ dummy_syndrome = np.zeros(stack.shape[0], dtype=np.uint8)
428
+
429
+ if weight_one_syndromes_searched < self.logical_operator_basis.shape[0]:
430
+ # Try each logical operator individually first
431
+ dummy_syndrome[
432
+ full_rank_stabilizer_matrix.shape[0]
433
+ + weight_one_syndromes_searched
434
+ ] = 1
435
+ weight_one_syndromes_searched += 1
436
+ else:
437
+ # Randomly pick a combination of logical rows
438
+ while True:
439
+ random_mask = np.random.choice(
440
+ [0, 1],
441
+ size=self.logical_operator_basis.shape[0],
442
+ p=[1 - p, p],
443
+ )
444
+ if np.any(random_mask):
445
+ break
446
+ for idx, bit in enumerate(random_mask):
447
+ if bit == 1:
448
+ dummy_syndrome[self.stabilizer_matrix.shape[0] + idx] = 1
449
+
450
+ candidate = bp_osd.decode(dummy_syndrome)
451
+ w = np.count_nonzero(
452
+ candidate[: self.physical_qubit_count]
453
+ | candidate[self.physical_qubit_count :]
454
+ )
455
+
456
+ if w < min_distance:
457
+ min_distance = w
458
+ if w < max_distance and reduce_logical_basis:
459
+ lc = np.hstack(
460
+ [
461
+ candidate[self.physical_qubit_count :],
462
+ candidate[: self.physical_qubit_count],
463
+ ]
464
+ )
465
+ candidate_logicals.append(lc)
466
+
467
+ # Reduce logical operator basis if we have enough candidates
468
+ if (
469
+ len(candidate_logicals) >= self.logical_qubit_count
470
+ and reduce_logical_basis
471
+ ):
472
+ self._reduce_logical_operator_basis(candidate_logicals)
473
+ (
474
+ bp_osd,
475
+ stack,
476
+ full_rank_stabilizer_matrix,
477
+ min_distance,
478
+ max_distance,
479
+ ) = self._setup_distance_estimation_decoder(decoder)
480
+ candidate_logicals = []
481
+ weight_one_syndromes_searched = 0
482
+
483
+ pbar.set_description(
484
+ f"Estimating distance: min-weight found <= {min_distance}, "
485
+ f"basis weights: {self.logical_basis_weights()}"
486
+ )
487
+
488
+ # Final basis reduction if needed
489
+ if reduce_logical_basis and candidate_logicals:
490
+ self._reduce_logical_operator_basis(candidate_logicals)
491
+
492
+ self.code_distance = min_distance
493
+ return min_distance
494
+
495
+ def _setup_distance_estimation_decoder(
496
+ self, decoder: Optional[BpOsdDecoder] = None
497
+ ) -> Tuple[BpOsdDecoder, scipy.sparse.spmatrix, scipy.sparse.spmatrix, int, int]:
498
+ """
499
+ Set up decoder and initial parameters.
500
+
501
+ Parameters
502
+ ----------
503
+ decoder : Optional[BpOsdDecoder]
504
+ Pre-configured decoder. If None, initialises with default settings.
505
+
506
+ Returns
507
+ -------
508
+ Tuple[BpOsdDecoder, scipy.sparse.spmatrix, scipy.sparse.spmatrix, int, int]
509
+ Returns (decoder, stack matrix, full rank stabilizer matrix, min distance, max distance)
510
+ """
511
+ # Remove redundant rows from stabilizer matrix
512
+ p_rows = ldpc.mod2.pivot_rows(self.stabilizer_matrix)
513
+ full_rank_stabilizer_matrix = self.stabilizer_matrix[p_rows]
514
+
515
+ # Build a stacked matrix of stabilizers and logicals
516
+ stack = scipy.sparse.vstack(
517
+ [full_rank_stabilizer_matrix, self.logical_operator_basis]
518
+ ).tocsr()
519
+
520
+ # Initial distance estimate from current logicals
521
+ min_distance = np.min(binary_pauli_hamming_weight(self.logical_operator_basis))
522
+ max_distance = np.max(self.logical_basis_weights())
523
+
524
+ # Set up BP+OSD decoder if not provided
525
+ if decoder is None:
526
+ decoder = BpOsdDecoder(
527
+ stack,
528
+ error_rate=0.1,
529
+ max_iter=10,
530
+ bp_method="ms",
531
+ schedule="parallel",
532
+ ms_scaling_factor=1.0,
533
+ osd_method="osd_0",
534
+ osd_order=0,
535
+ )
536
+
537
+ return decoder, stack, full_rank_stabilizer_matrix, min_distance, max_distance
538
+
539
+ def _reduce_logical_operator_basis(
540
+ self,
541
+ candidate_logicals: Union[Sequence, np.ndarray, scipy.sparse.spmatrix] = [],
542
+ ):
543
+ """
544
+ Reduce the logical operator basis to include lower-weight logicals.
545
+
546
+ Parameters
547
+ ----------
548
+ candidate_logicals : Union[Sequence, np.ndarray, scipy.sparse.spmatrix], optional
549
+ A list or array of candidate logical operators to be considered for reducing the basis.
550
+ Defaults to an empty list.
551
+ """
552
+ if len(candidate_logicals) != 0:
553
+ # Convert candidates to a sparse matrix if they aren't already
554
+ if not isinstance(candidate_logicals, scipy.sparse.spmatrix):
555
+ candidate_logicals = scipy.sparse.csr_matrix(
556
+ scipy.sparse.csr_matrix(candidate_logicals)
557
+ )
558
+
559
+ # Stack the candidate logicals with the existing logicals
560
+ temp1 = scipy.sparse.vstack(
561
+ [candidate_logicals, self.logical_operator_basis]
562
+ ).tocsr()
563
+
564
+ # Compute the Hamming weight over GF4 (number of qubits with non-identity operators)
565
+ # Split into X and Z parts
566
+ row_weights = binary_pauli_hamming_weight(temp1).flatten()
567
+
568
+ # Sort the rows by Hamming weight (ascending)
569
+ sorted_rows = np.argsort(row_weights)
570
+ temp1 = temp1[sorted_rows, :]
571
+
572
+ # Add the stabilizer matrix to the top of the stack
573
+ temp1 = scipy.sparse.vstack([self.stabilizer_matrix, temp1])
574
+
575
+ # Calculate the rank of the stabilizer matrix (todo: find way of removing this step)
576
+ stabilizer_rank = ldpc.mod2.rank(self.stabilizer_matrix)
577
+
578
+ # Perform row reduction to find a new logical basis
579
+ p_rows = ldpc.mod2.pivot_rows(temp1)
580
+ self.logical_operator_basis = temp1[p_rows[stabilizer_rank:]]
581
+
582
+ def logical_basis_weights(self):
583
+ """
584
+ Return the Hamming weights of the logical operators in the current basis.
585
+
586
+ Returns
587
+ -------
588
+ np.ndarray
589
+ An array of integers representing the Hamming weights of the logical operators.
590
+ """
591
+ return binary_pauli_hamming_weight(self.logical_operator_basis).flatten()
qec/utils/__init__.py ADDED
File without changes