qec 0.2.0__py3-none-any.whl → 0.2.2__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,264 @@
1
+ import numpy as np
2
+ import scipy
3
+ from typing import Union, Tuple
4
+ import ldpc.mod2
5
+ import time
6
+
7
+ from qec.code_constructions import CSSCode
8
+ from qec.utils.sparse_binary_utils import convert_to_binary_scipy_sparse
9
+
10
+ class HypergraphProductCode(CSSCode):
11
+ """
12
+ Implements a Hypergraph Product (HGP) code - derived from two classical linear binary codes.
13
+
14
+ Parameters
15
+ ----------
16
+ seed_matrix_1 :
17
+ A classical linear binary code used as a "seed" in the HGP construction method.
18
+ seed_matrix_2 :
19
+ A classical linear binary code used as a "seed" in the HGP construction method.
20
+ name : str, default = None
21
+ The name of the code. If None, the name is set to: "Hypergraph product code"
22
+
23
+ Attributes
24
+ ----------
25
+ seed_matrix_1 : scipy.sparse.spmatrix
26
+ The input seed_matrix_1 stored as a scipy sparse matrix.
27
+ seed_matrix_2 : scipy.sparse.spmatrix
28
+ The input seed_matrix_2 stored as a scipy sparse matrix.
29
+ _n1 : int
30
+ Number of columns in seed_matrix_1
31
+ _n2 : int
32
+ Number of columns in seed_matrix_2
33
+ _m1 : int
34
+ Number of rows in seed_matrix_1 (the number of columns of it's transpose)
35
+ _m2 : int
36
+ Number of rows in seed_matrix_2 (the number of columns of it's transpose)
37
+
38
+ Notes
39
+ -----
40
+
41
+ The X and Z stabilizer matrices are given by [1]_:
42
+
43
+ .. math::
44
+
45
+ \begin{align}
46
+ H_{X} &= \begin{pmatrix}
47
+ H_{1}\otimes I_{n_{2}} & \,\,I_{r_{1}}\otimes H_{2}^{T}
48
+ \end{pmatrix}\tag*{(1)}\\
49
+ H_{Z} &= \begin{pmatrix}
50
+ I_{n_{1}}\otimes H_{2} & \,\,H_{1}^{T}\otimes I_{r_{2}}
51
+ \end{pmatrix}~, \tag*{(2)}
52
+ \end{align}
53
+
54
+ where :math:`H_1` and :math:`H_2` correspond to the parity check matrix of the first and second "seed" codes.
55
+
56
+
57
+ .. [1] J.-P. Tillich and G. Zemor, “Quantum LDPC Codes With Positive Rate and Minimum Distance Proportional to the Square Root of the Blocklength”, IEEE Transactions on Information Theory 60, 1193 (2014)
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ seed_matrix_1 : Union[np.ndarray, scipy.sparse.spmatrix],
63
+ seed_matrix_2 : Union[np.ndarray, scipy.sparse.spmatrix],
64
+ name : str = None
65
+ ):
66
+
67
+ self.name = name if name else "Hypergraph product code"
68
+
69
+ if not all(isinstance(seed_m, (np.ndarray, scipy.sparse.spmatrix)) for seed_m in (seed_matrix_1, seed_matrix_2)):
70
+ raise TypeError("The seed matrices must be either numpy arrays or scipy sparse matrices.")
71
+
72
+ self.seed_matrix_1 = convert_to_binary_scipy_sparse(seed_matrix_1)
73
+ self.seed_matrix_2 = convert_to_binary_scipy_sparse(seed_matrix_2)
74
+
75
+ # maybe move the below to a private _construct_stabilizer_matrices function?
76
+ # --------------------------------------------------------------------------
77
+ self._n1 = seed_matrix_1.shape[1]
78
+ self._n2 = seed_matrix_2.shape[1]
79
+
80
+ self._m1 = seed_matrix_1.shape[0]
81
+ self._m2 = seed_matrix_2.shape[0]
82
+
83
+ x_left = scipy.sparse.kron(seed_matrix_1, scipy.sparse.eye(self._n2))
84
+ x_right = scipy.sparse.kron(scipy.sparse.eye(self._m1), seed_matrix_2.T)
85
+ self.x_stabilizer_matrix = scipy.sparse.hstack([x_left, x_right])
86
+
87
+ z_left = scipy.sparse.kron(scipy.sparse.eye(self._n1), seed_matrix_2)
88
+ z_right = scipy.sparse.kron(seed_matrix_1.T, scipy.sparse.eye(self._m2))
89
+ self.z_stabilizer_matrix = scipy.sparse.hstack([z_left, z_right])
90
+ # --------------------------------------------------------------------------
91
+
92
+ super().__init__(self.x_stabilizer_matrix, self.z_stabilizer_matrix, self.name)
93
+
94
+
95
+ def compute_exact_code_distance(self) -> int:
96
+ """
97
+ Computes the exact code distance of the HGP code.
98
+
99
+ Returns
100
+ -------
101
+ int
102
+ The distance of the code.
103
+
104
+ Notes
105
+ -----
106
+ The distance of a HGP code is given as:
107
+
108
+ .. math::
109
+
110
+ \min(d_1, d_2, d_1^T, d_2^T)
111
+
112
+ corresponding to the distance of the seed codes and the distance of their transposes.
113
+ """
114
+
115
+ rank_seed_m1 = ldpc.mod2.rank(self.seed_matrix_1)
116
+ rank_seed_m2 = ldpc.mod2.rank(self.seed_matrix_2)
117
+
118
+ if self.seed_matrix_1.shape[1] != rank_seed_m1:
119
+ self.d1 = ldpc.mod2.compute_exact_code_distance(self.seed_matrix_1)
120
+ else:
121
+ self.d1 = np.inf
122
+
123
+ if self.seed_matrix_2.shape[1] != rank_seed_m2:
124
+ self.d2 = ldpc.mod2.compute_exact_code_distance(self.seed_matrix_2)
125
+ else:
126
+ self.d2 = np.inf
127
+
128
+ # note: rank(A) = rank(A^T):
129
+ if self.seed_matrix_1.shape[0] != rank_seed_m1:
130
+ self.d1T = ldpc.mod2.compute_exact_code_distance(self.seed_matrix_1.T)
131
+ else:
132
+ self.d1T = np.inf
133
+
134
+ if self.seed_matrix_2.shape[0] != rank_seed_m2:
135
+ self.d2T = ldpc.mod2.compute_exact_code_distance(self.seed_matrix_2.T)
136
+ else:
137
+ self.d2T = np.inf
138
+
139
+ self.x_code_distance = min(self.d1T, self.d2)
140
+ self.z_code_distance = min(self.d1, self.d2T)
141
+ self.code_distance = min(self.x_code_distance, self.z_code_distance)
142
+
143
+ return self.code_distance
144
+
145
+
146
+ def estimate_min_distance(self, timeout_seconds: float = 0.025) -> int:
147
+ """
148
+ Estimate the minimum X and Z distance of the HGP code. Parameters
149
+ ----------
150
+ timeout_seconds : float, optional
151
+ Time limit in seconds for the full search. Default: 0.25
152
+
153
+ Returns
154
+ -------
155
+ int
156
+ Best estimate of the (overall) code distance found within time limit.
157
+
158
+ """
159
+
160
+ rank_seed_m1 = ldpc.mod2.rank(self.seed_matrix_1)
161
+ rank_seed_m2 = ldpc.mod2.rank(self.seed_matrix_2)
162
+
163
+
164
+ d1_timeout_seconds = timeout_seconds/4
165
+ if self.seed_matrix_1.shape[1] != rank_seed_m1:
166
+ d1_start_time = time.time()
167
+ d1_min_estimate, _, _ = ldpc.mod2.estimate_code_distance(self.seed_matrix_1, d1_timeout_seconds, 0)
168
+ d1_run_time = time.time() - d1_start_time
169
+ else:
170
+ d1_min_estimate = np.inf
171
+ d1_run_time = 0
172
+
173
+ d1T_timeout_seconds = (d1_timeout_seconds * 4 - d1_run_time)/3 if d1_run_time <= d1_timeout_seconds else timeout_seconds/4
174
+ if self.seed_matrix_1.shape[0] != rank_seed_m1:
175
+ d1T_start_time = time.time()
176
+ d1T_min_estimate, _, _ = ldpc.mod2.estimate_code_distance(self.seed_matrix_1.T, d1T_timeout_seconds, 0)
177
+ d1T_run_time = time.time() - d1T_start_time
178
+ else:
179
+ d1T_min_estimate = np.inf
180
+ d1T_run_time = 0
181
+
182
+ d2_timeout_seconds = (d1T_timeout_seconds * 3 - d1T_run_time)/2 if d1T_run_time <= d1T_timeout_seconds else timeout_seconds/4
183
+ if self.seed_matrix_2.shape[1] != rank_seed_m2:
184
+ d2_start_time = time.time()
185
+ d2_min_estimate, _, _ = ldpc.mod2.estimate_code_distance(self.seed_matrix_2, d2_timeout_seconds, 0)
186
+ d2_run_time = time.time() - d2_start_time
187
+ else:
188
+ d2_min_estimate = np.inf
189
+ d2_run_time = 0
190
+
191
+
192
+ d2T_timeout_seconds = (d2_timeout_seconds * 2 - d2_run_time) if d2_run_time <= d2_timeout_seconds else timeout_seconds/4
193
+ if self.seed_matrix_2.shape[0] != rank_seed_m2:
194
+ d2T_min_estimate, _, _ = ldpc.mod2.estimate_code_distance(self.seed_matrix_2.T, d2T_timeout_seconds, 0)
195
+ else:
196
+ d2T_min_estimate = np.inf
197
+
198
+ self.x_code_distance = min(d1T_min_estimate, d2_min_estimate)
199
+ self.z_code_distance = min(d1_min_estimate, d2T_min_estimate)
200
+ self.code_distance = min(self.x_code_distance, self.z_code_distance)
201
+
202
+
203
+ return self.code_distance
204
+
205
+
206
+
207
+ def compute_logical_basis(self) -> Tuple[scipy.sparse.spmatrix, scipy.sparse.spmatrix]:
208
+ """
209
+ Compute the logical operator basis for the given HGP code.
210
+
211
+ Returns
212
+ -------
213
+ Tuple[scipy.sparse.spmatrix, scipy.sparse.spmatrix]
214
+ Logical X and Z operator bases (lx, lz).
215
+ """
216
+
217
+ ker_h1 = ldpc.mod2.kernel(self.seed_matrix_1)
218
+ ker_h2 = ldpc.mod2.kernel(self.seed_matrix_2)
219
+ ker_h1T = ldpc.mod2.kernel(self.seed_matrix_1.T)
220
+ ker_h2T = ldpc.mod2.kernel(self.seed_matrix_2.T)
221
+
222
+ row_comp_h1 = ldpc.mod2.row_complement_basis(self.seed_matrix_1)
223
+ row_comp_h2 = ldpc.mod2.row_complement_basis(self.seed_matrix_2)
224
+ row_comp_h1T = ldpc.mod2.row_complement_basis(self.seed_matrix_1.T)
225
+ row_comp_h2T = ldpc.mod2.row_complement_basis(self.seed_matrix_2.T)
226
+
227
+ temp = scipy.sparse.kron(ker_h1, row_comp_h2)
228
+ lz1 = scipy.sparse.hstack([temp, scipy.sparse.csr_matrix((temp.shape[0], self._m1*self._m2), dtype=np.uint8)])
229
+
230
+ temp = scipy.sparse.kron(row_comp_h1T, ker_h2T)
231
+ lz2 = scipy.sparse.hstack([scipy.sparse.csr_matrix((temp.shape[0], self._n1*self._n2), dtype=np.uint8), temp])
232
+
233
+ self.z_logical_operator_basis = scipy.sparse.vstack([lz1, lz2], dtype=np.uint8)
234
+
235
+
236
+ temp = scipy.sparse.kron(row_comp_h1, ker_h2)
237
+ lx1 = scipy.sparse.hstack([temp, scipy.sparse.csr_matrix((temp.shape[0], self._m1*self._m2), dtype=np.uint8)])
238
+
239
+ temp = scipy.sparse.kron(ker_h1T, row_comp_h2T)
240
+ lx2 = scipy.sparse.hstack([scipy.sparse.csr_matrix((temp.shape[0], self._n1*self._n2), dtype=np.uint8), temp])
241
+
242
+ self.x_logical_operator_basis= scipy.sparse.vstack([lx1, lx2], dtype = np.uint8)
243
+
244
+ # Follows the way it is done in CSSCode -> move it into __init__?
245
+ #----------------------------------------------------------------
246
+ self.logical_qubit_count = self.x_logical_operator_basis.shape[0]
247
+ #----------------------------------------------------------------
248
+
249
+ return (self.x_logical_operator_basis, self.z_logical_operator_basis)
250
+
251
+
252
+ def __str__(self):
253
+ """
254
+ String representation of the HGP code. Includes the name and [[n, k, d]] properties of the code.
255
+
256
+ Returns
257
+ -------
258
+ str
259
+ String representation of the HGP code.
260
+ """
261
+
262
+ return f"{self.name} Hypergraphproduct Code: [[N={self.physical_qubit_count}, K={self.logical_qubit_count}, dx={self.x_code_distance}, dz={self.z_code_distance}]]"
263
+
264
+
@@ -45,7 +45,7 @@ class StabilizerCode(object):
45
45
  The number of logical qubits in the code.
46
46
  code_distance : int
47
47
  (Not computed by default) The distance of the code, if known or computed.
48
- logicals : scipy.sparse.spmatrix or None
48
+ logical_operator_basis : scipy.sparse.spmatrix or None
49
49
  A basis for the logical operators of the code.
50
50
  """
51
51
 
@@ -177,7 +177,7 @@ class StabilizerCode(object):
177
177
  kernel_h = ldpc.mod2.kernel(self.stabilizer_matrix)
178
178
 
179
179
  # Sort the rows of the kernel by weight
180
- row_weights = np.diff(kernel_h.indptr)
180
+ row_weights = kernel_h.getnnz(axis=1)
181
181
  sorted_rows = np.argsort(row_weights)
182
182
  kernel_h = kernel_h[sorted_rows, :]
183
183
 
@@ -377,166 +377,64 @@ class StabilizerCode(object):
377
377
  """
378
378
  return f"< Stabilizer Code, Name: {self.name}, Parameters: [[{self.physical_qubit_count}, {self.logical_qubit_count}, {self.code_distance}]] >"
379
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
380
  def estimate_min_distance(
424
381
  self,
425
382
  timeout_seconds: float = 0.25,
426
383
  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
384
  reduce_logical_basis: bool = False,
385
+ decoder: Optional[BpOsdDecoder] = None,
435
386
  ) -> int:
436
387
  """
437
388
  Estimate the minimum distance of the stabilizer code using a BP+OSD decoder-based search.
438
389
 
439
390
  Parameters
440
391
  ----------
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.
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.
462
400
 
463
401
  Returns
464
402
  -------
465
403
  int
466
- The best-known estimate of the code distance found within the time limit.
404
+ Best estimate of code distance found within time limit
467
405
  """
468
406
  if self.logical_operator_basis is None:
469
407
  self.logical_operator_basis = self.compute_logical_basis()
470
408
 
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
409
+ # Initial setup of decoder and parameters
511
410
  bp_osd, stack, full_rank_stabilizer_matrix, min_distance, max_distance = (
512
- decoder_setup()
411
+ self._setup_distance_estimation_decoder(decoder)
513
412
  )
514
413
 
515
- # List to store candidate logical operators for basis reduction
414
+ # Initialize storage for candidate logicals and tracking
516
415
  candidate_logicals = []
416
+ weight_one_syndromes_searched = 0
517
417
 
518
- # 2) Randomly search for better representatives of logical operators
418
+ # Main search loop
519
419
  start_time = time.time()
520
420
  with tqdm(total=timeout_seconds, desc="Estimating distance") as pbar:
521
- weight_one_syndromes_searched = 0
522
421
  while time.time() - start_time < timeout_seconds:
422
+ # Update progress bar
523
423
  elapsed = time.time() - start_time
524
- # Update progress bar based on elapsed time
525
424
  pbar.update(elapsed - pbar.n)
526
425
 
527
426
  # Initialize an empty dummy syndrome
528
427
  dummy_syndrome = np.zeros(stack.shape[0], dtype=np.uint8)
529
428
 
530
429
  if weight_one_syndromes_searched < self.logical_operator_basis.shape[0]:
430
+ # Try each logical operator individually first
531
431
  dummy_syndrome[
532
432
  full_rank_stabilizer_matrix.shape[0]
533
433
  + weight_one_syndromes_searched
534
- ] = 1 # pick exactly one logical operator
434
+ ] = 1
535
435
  weight_one_syndromes_searched += 1
536
-
537
436
  else:
538
437
  # Randomly pick a combination of logical rows
539
- # (with probability p, set the corresponding row in the syndrome to 1)
540
438
  while True:
541
439
  random_mask = np.random.choice(
542
440
  [0, 1],
@@ -550,7 +448,6 @@ class StabilizerCode(object):
550
448
  dummy_syndrome[self.stabilizer_matrix.shape[0] + idx] = 1
551
449
 
552
450
  candidate = bp_osd.decode(dummy_syndrome)
553
-
554
451
  w = np.count_nonzero(
555
452
  candidate[: self.physical_qubit_count]
556
453
  | candidate[self.physical_qubit_count :]
@@ -558,44 +455,129 @@ class StabilizerCode(object):
558
455
 
559
456
  if w < min_distance:
560
457
  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
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
572
468
  if (
573
469
  len(candidate_logicals) >= self.logical_qubit_count
574
470
  and reduce_logical_basis
575
471
  ):
576
- self.reduce_logical_operator_basis(candidate_logicals)
472
+ self._reduce_logical_operator_basis(candidate_logicals)
577
473
  (
578
474
  bp_osd,
579
475
  stack,
580
476
  full_rank_stabilizer_matrix,
581
477
  min_distance,
582
478
  max_distance,
583
- ) = decoder_setup()
479
+ ) = self._setup_distance_estimation_decoder(decoder)
584
480
  candidate_logicals = []
585
481
  weight_one_syndromes_searched = 0
586
482
 
587
483
  pbar.set_description(
588
- f"Estimating distance: min-weight found <= {min_distance}, basis weights: {self.logical_basis_weights()}"
484
+ f"Estimating distance: min-weight found <= {min_distance}, "
485
+ f"basis weights: {self.logical_basis_weights()}"
589
486
  )
590
487
 
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())
488
+ # Final basis reduction if needed
489
+ if reduce_logical_basis and candidate_logicals:
490
+ self._reduce_logical_operator_basis(candidate_logicals)
596
491
 
597
- # Update and return the estimated distance
598
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:]]
599
581
 
600
582
  def logical_basis_weights(self):
601
583
  """
@@ -0,0 +1 @@
1
+ from .five_qubit_code import FiveQubitCode
@@ -1,4 +1,4 @@
1
- from qec.stabilizer_code.stabilizer_code import StabilizerCode
1
+ from qec.code_constructions import StabilizerCode
2
2
 
3
3
 
4
4
  class FiveQubitCode(StabilizerCode):
@@ -0,0 +1 @@
1
+ from .codetables_de import CodeTablesDE
@@ -1,4 +1,4 @@
1
- from qec.stabilizer_code import StabilizerCode
1
+ from qec.code_constructions import StabilizerCode
2
2
  from qec.utils.codetables_de_utils import get_codetables_de_matrix, pcm_to_csr_matrix
3
3
 
4
4
 
@@ -299,7 +299,9 @@ def symplectic_product(
299
299
  a = convert_to_binary_scipy_sparse(a)
300
300
  b = convert_to_binary_scipy_sparse(b)
301
301
 
302
- assert (a.shape[1] == b.shape[1]), "Input matrices must have the same number of columns."
302
+ assert (
303
+ a.shape[1] == b.shape[1]
304
+ ), "Input matrices must have the same number of columns."
303
305
  assert a.shape[1] % 2 == 0, "Input matrices must have an even number of columns."
304
306
 
305
307
  n = a.shape[1] // 2
@@ -1,4 +1,5 @@
1
1
  import logging
2
+
2
3
  # Suppress debug and info messages from urllib3 and requests libraries
3
4
  logging.getLogger("urllib3").setLevel(logging.WARNING)
4
5
  logging.getLogger("requests").setLevel(logging.WARNING)
@@ -9,6 +10,7 @@ import requests
9
10
  from bs4 import BeautifulSoup
10
11
  import json
11
12
 
13
+
12
14
  def get_codetables_de_matrix(q, n, k, output_json_path=None, write_to_file=False):
13
15
  """
14
16
  Retrieve quantum code data from Markus Grassl's codetables.de website.