qec 0.2.7__py3-none-any.whl → 0.3.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.
@@ -1,591 +0,0 @@
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()
@@ -1 +0,0 @@
1
- from .five_qubit_code import FiveQubitCode
@@ -1,67 +0,0 @@
1
- from qec.code_constructions import StabilizerCode
2
-
3
-
4
- class FiveQubitCode(StabilizerCode):
5
- """
6
- Five-Qubit Quantum Error-Correcting Code.
7
-
8
- The `FiveQubitCode` class implements the [[5, 1, 3]] quantum stabilizer code,
9
- which is the smallest possible quantum error-correcting code capable of
10
- correcting an arbitrary single-qubit error. This code encodes one logical
11
- qubit into five physical qubits and has a distance of three, allowing it
12
- to detect up to two errors and correct one.
13
-
14
- Parameters
15
- ----------
16
- None
17
-
18
- Attributes
19
- ----------
20
- code_distance : int
21
- The distance of the quantum code. For the five-qubit code, this is set to 3.
22
-
23
- Inherits
24
- --------
25
- StabilizerCode
26
- The base class providing functionalities for stabilizer-based quantum
27
- error-correcting codes, including initialization, distance computation,
28
- and parameter retrieval.
29
-
30
- Examples
31
- --------
32
- >>> five_qubit = FiveQubitCode()
33
- >>> five_qubit.phyiscal_qubit_count
34
- 5
35
- >>> five_qubit.logical_qubit_count
36
- 1
37
- >>> five_qubit.code_distance
38
- 3
39
- """
40
-
41
- def __init__(self):
42
- """
43
- Initialize the Five-Qubit Code with predefined Pauli stabilizers.
44
-
45
- The constructor sets up the stabilizer generators for the [[5, 1, 3]]
46
- quantum code using their corresponding Pauli strings. It then calls the
47
- superclass initializer to establish the stabilizer matrix and other
48
- essential parameters.
49
-
50
- Parameters
51
- ----------
52
- None
53
-
54
- Raises
55
- ------
56
- ValueError
57
- If the provided stabilizer generators do not satisfy the necessary
58
- commutation relations required for a valid stabilizer code.
59
- """
60
- # Define the Pauli stabilizer generators for the five-qubit code
61
- pauli_stabilizers = [["XZZXI"], ["IXZZX"], ["XIXZZ"], ["ZXIXZ"]]
62
-
63
- # Initialize the StabilizerCode with the defined stabilizers and a custom name
64
- super().__init__(stabilizers=pauli_stabilizers, name="5-Qubit Code")
65
-
66
- # Set the distance attribute specific to the five-qubit code
67
- self.code_distance = 3
@@ -1 +0,0 @@
1
- from .codetables_de import CodeTablesDE