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,401 @@
1
+ import numpy as np
2
+ import scipy
3
+ import scipy.sparse
4
+ from qec.utils.sparse_binary_utils import convert_to_binary_scipy_sparse
5
+
6
+
7
+ def GF4_to_binary(GF4_matrix: np.typing.ArrayLike) -> scipy.sparse.csr_matrix:
8
+ """
9
+ Convert a matrix over GF4 (elements {0,1,2,3}) to a binary sparse matrix in CSR format.
10
+
11
+ Each entry (row i, column j) is mapped as follows:
12
+ - 0 => no 1's (row has [0, 0])
13
+ - 1 => one 1 in column 2*j ([1, 0])
14
+ - 2 => two 1's in columns 2*j and 2*j + 1 ([1, 1])
15
+ - 3 => one 1 in column 2*j + 1 ([0, 1])
16
+
17
+ Parameters
18
+ ----------
19
+ GF4_matrix : ArrayLike
20
+ Input matrix of shape (M, N) containing only elements from {0, 1, 2, 3}.
21
+ Can be a dense array-like or any SciPy sparse matrix format.
22
+
23
+ Returns
24
+ -------
25
+ scipy.sparse.csr_matrix
26
+ Binary sparse matrix in CSR format, of shape (M, 2*N).
27
+
28
+ Raises
29
+ ------
30
+ ValueError
31
+ If the input matrix has elements outside {0, 1, 2, 3}.
32
+
33
+ Examples
34
+ --------
35
+ >>> import numpy as np
36
+ >>> from scipy.sparse import scipy.sparse.csr_matrix
37
+ >>> mat = np.array([[0, 1],
38
+ ... [2, 3]])
39
+ >>> GF4_to_binary(mat).toarray()
40
+ array([[0, 1, 1, 0],
41
+ [1, 1, 0, 1]], dtype=uint8)
42
+ """
43
+ if scipy.sparse.issparse(GF4_matrix):
44
+ mat_coo = GF4_matrix.tocoo(copy=False)
45
+
46
+ if not np.all(np.isin(mat_coo.data, [1, 2, 3])):
47
+ raise ValueError(
48
+ "Input matrix must contain only elements from GF4: {0, 1, 2, 3}"
49
+ )
50
+
51
+ row_ids = []
52
+ col_ids = []
53
+ rows, cols = mat_coo.shape
54
+
55
+ for r, c, val in zip(mat_coo.row, mat_coo.col, mat_coo.data):
56
+ if val == 1:
57
+ row_ids.append(r)
58
+ col_ids.append(c)
59
+ elif val == 2:
60
+ row_ids.extend([r, r])
61
+ col_ids.extend([c, cols + c])
62
+ elif val == 3:
63
+ row_ids.append(r)
64
+ col_ids.append(cols + c)
65
+
66
+ data = np.ones(len(row_ids), dtype=np.uint8)
67
+ return scipy.sparse.csr_matrix(
68
+ (data, (row_ids, col_ids)), shape=(rows, 2 * cols)
69
+ )
70
+
71
+ GF4_matrix = np.asanyarray(GF4_matrix, dtype=int)
72
+ if not np.all(np.isin(GF4_matrix, [0, 1, 2, 3])):
73
+ raise ValueError(
74
+ "Input matrix must contain only elements from GF4: {0, 1, 2, 3}"
75
+ )
76
+
77
+ row_ids = []
78
+ col_ids = []
79
+ rows, cols = GF4_matrix.shape
80
+
81
+ for i in range(rows):
82
+ for j in range(cols):
83
+ val = GF4_matrix[i, j]
84
+ if val == 1:
85
+ row_ids.append(i)
86
+ col_ids.append(j)
87
+ elif val == 2:
88
+ row_ids.extend([i, i])
89
+ col_ids.extend([j, j + cols])
90
+ elif val == 3:
91
+ row_ids.append(i)
92
+ col_ids.append(j + cols)
93
+
94
+ data = np.ones(len(row_ids), dtype=np.uint8)
95
+ return scipy.sparse.csr_matrix((data, (row_ids, col_ids)), shape=(rows, 2 * cols))
96
+
97
+
98
+ def pauli_str_to_binary_pcm(
99
+ pauli_strings: np.typing.ArrayLike,
100
+ ) -> scipy.sparse.csr_matrix:
101
+ """
102
+ Convert an (M x 1) array of Pauli strings, where each string has length N, corresponding to the number of physical qubits, into a binary parity-check matrix (PCM) with dimensions (M x 2*N).
103
+
104
+ The mapping for each qubit j in the string is:
105
+ - 'I' => (0|0)
106
+ - 'X' => (1|0)
107
+ - 'Z' => (0|1)
108
+ - 'Y' => (1|1)
109
+ where the first element (a), in (a|b) is at column j and the second element (b) is at column j + N.
110
+
111
+ Parameters
112
+ ----------
113
+ pauli_strings : ArrayLike
114
+ Array of shape (M, 1), where each element is a string of Pauli operators
115
+ ('I', 'X', 'Y', 'Z'). Can be dense or any SciPy sparse matrix format with
116
+ an object/string dtype.
117
+
118
+ Returns
119
+ -------
120
+ scipy.sparse.csr_matrix
121
+ Binary parity-check matrix of shape (M, 2*N) in CSR format, where M is the number of stabilisers and
122
+ N is the number of physical qubits.
123
+ Raises
124
+ ------
125
+ ValueError
126
+ If any character in the Pauli strings is not one of {'I', 'X', 'Y', 'Z'}.
127
+
128
+ Examples
129
+ --------
130
+ >>> import numpy as np
131
+ >>> paulis = np.array([["XIZ"], ["YYI"]], dtype=object)
132
+ >>> pcm = pauli_str_to_binary_pcm(paulis)
133
+ >>> pcm.toarray()
134
+ array([[1, 0, 0, 0, 0, 1],
135
+ [1, 1, 0, 1, 1, 0]], dtype=uint8)
136
+ """
137
+
138
+ if scipy.sparse.issparse(pauli_strings):
139
+ if pauli_strings.dtype == object:
140
+ mat_coo = pauli_strings.tocoo(copy=False)
141
+ dense = np.full(pauli_strings.shape, "I", dtype=str)
142
+ for r, c, val in zip(mat_coo.row, mat_coo.col, mat_coo.data):
143
+ dense[r, c] = val
144
+ pauli_strings = dense
145
+ else:
146
+ pauli_strings = pauli_strings.toarray()
147
+
148
+ pauli_strings = np.asanyarray(pauli_strings, dtype=str)
149
+
150
+ if pauli_strings.size == 0:
151
+ return scipy.sparse.csr_matrix((0, 0))
152
+
153
+ row_ids = []
154
+ col_ids = []
155
+
156
+ m_stabilisers = pauli_strings.shape[0]
157
+ n_qubits = len(pauli_strings[0, 0])
158
+
159
+ for i, string in enumerate(pauli_strings):
160
+ if len(string[0]) != n_qubits:
161
+ raise ValueError("The Pauli strings do not have equal length.")
162
+ for j, char in enumerate(string[0]):
163
+ if char == "I":
164
+ continue
165
+ elif char == "X":
166
+ row_ids.append(i)
167
+ col_ids.append(j)
168
+ elif char == "Z":
169
+ row_ids.append(i)
170
+ col_ids.append(j + n_qubits)
171
+ elif char == "Y":
172
+ row_ids.extend([i, i])
173
+ col_ids.extend([j, j + n_qubits])
174
+ else:
175
+ raise ValueError(f"Invalid Pauli character '{char}' encountered.")
176
+
177
+ data = np.ones(len(row_ids), dtype=np.uint8)
178
+
179
+ return scipy.sparse.csr_matrix(
180
+ (data, (row_ids, col_ids)), shape=(m_stabilisers, 2 * n_qubits), dtype=np.uint8
181
+ )
182
+
183
+
184
+ def binary_pcm_to_pauli_str(binary_pcm: np.typing.ArrayLike) -> np.ndarray:
185
+ """
186
+ Convert a binary (M x 2*N) PCM corresponding to M stabilisers acting on N physical qubits,
187
+ back into an array (M x 1) of Pauli strings that have length N.
188
+
189
+ For each qubit j, columns (j | j + N) of the PCM encode:
190
+ - (0|0) => 'I'
191
+ - (1|0) => 'X'
192
+ - (0|1) => 'Z'
193
+ - (1|1) => 'Y'
194
+
195
+ Parameters
196
+ ----------
197
+ binary_pcm : ArrayLike
198
+ Binary matrix of shape (M, 2*N), in dense or any SciPy sparse matrix format.
199
+
200
+ Returns
201
+ -------
202
+ np.ndarray
203
+ Array of shape (M, 1), where each element is a string of Pauli operators with length N.
204
+
205
+ Examples
206
+ --------
207
+ >>> import numpy as np
208
+ >>> from scipy.sparse import scipy.sparse.csr_matrix
209
+ >>> pcm = np.array([[1, 0, 0, 0, 0, 1],
210
+ ... [1, 1, 0, 1, 1, 0]], dtype=np.uint8)
211
+ >>> pauli_str_to_return = binary_pcm_to_pauli_str(pcm)
212
+ >>> pauli_str_to_return
213
+ array([['XIZ'],
214
+ ['YYI']], dtype='<U3')
215
+ """
216
+ if scipy.sparse.issparse(binary_pcm):
217
+ binary_pcm = binary_pcm.toarray()
218
+
219
+ binary_pcm = np.asanyarray(binary_pcm, dtype=int)
220
+ n_rows, n_cols = binary_pcm.shape
221
+ n_qubits = n_cols // 2
222
+ pauli_strings = [""] * n_rows
223
+
224
+ for i in range(n_rows):
225
+ row = binary_pcm[i]
226
+ x_bits = row[:n_qubits]
227
+ z_bits = row[n_qubits:]
228
+ for x_bit, z_bit in zip(x_bits, z_bits):
229
+ if x_bit == 0 and z_bit == 0:
230
+ pauli_strings[i] += "I"
231
+ elif x_bit == 1 and z_bit == 0:
232
+ pauli_strings[i] += "X"
233
+ elif x_bit == 0 and z_bit == 1:
234
+ pauli_strings[i] += "Z"
235
+ else:
236
+ pauli_strings[i] += "Y"
237
+
238
+ return np.array(pauli_strings, dtype=str).reshape(-1, 1)
239
+
240
+
241
+ def symplectic_product(
242
+ a: np.typing.ArrayLike, b: np.typing.ArrayLike
243
+ ) -> scipy.sparse.csr_matrix:
244
+ """
245
+ Compute the symplectic product of two binary matrices in CSR format.
246
+
247
+ The input matrices (A,B) are first converted to binary sparse format (modulo 2)
248
+ and then partitioned into `x` and `z` components, where x and z have the same shape:
249
+
250
+ A = (a_x|a_z)
251
+ B = (b_x|b_z)
252
+
253
+ Then the symplectic product is computed as: (a_x * b_z^T + a_z * b_x^T) mod 2.
254
+
255
+ Parameters
256
+ ----------
257
+ a : array_like
258
+ A 2D array-like object with shape (M, 2N), which will be converted to
259
+ a binary sparse matrix (mod 2).
260
+ b : array_like
261
+ A 2D array-like object with shape (M, 2N), which will be converted to
262
+ a binary sparse matrix (mod 2). Must have the same shape as `a`.
263
+
264
+ Returns
265
+ -------
266
+ scipy.sparse.csr_matrix
267
+ The symplectic product of the two input matrices, stored in CSR format.
268
+
269
+ Raises
270
+ ------
271
+ AssertionError
272
+ If the shapes of `a` and `b` do not match.
273
+ AssertionError
274
+ If the number of columns of `a` (and `b`) is not even.
275
+
276
+ Notes
277
+ -----
278
+ This function is particularly useful for calculating commutation between Pauli operators,
279
+ where a result of 0 indicates commuting operators, and 1 indicates anti-commuting operators.
280
+
281
+ Examples
282
+ --------
283
+ >>> import numpy as np
284
+ >>> from qec.utils.sparse_binary_utils import convert_to_binary_scipy_sparse
285
+ >>> a_data = np.array([[1, 0, 0, 1],
286
+ ... [0, 1, 1, 0],
287
+ ... [1, 1, 0, 0]], dtype=int)
288
+ >>> b_data = np.array([[0, 1, 1, 0],
289
+ ... [1, 0, 0, 1],
290
+ ... [0, 1, 1, 0]], dtype=int)
291
+ >>> # Compute symplectic product
292
+ >>> sp = symplectic_product(a_data, b_data)
293
+ >>> sp.toarray()
294
+ array([[0, 0, 0],
295
+ [0, 0, 0],
296
+ [1, 1, 1]], dtype=int8)
297
+ """
298
+
299
+ a = convert_to_binary_scipy_sparse(a)
300
+ b = convert_to_binary_scipy_sparse(b)
301
+
302
+ assert (a.shape[1] == b.shape[1]), "Input matrices must have the same number of columns."
303
+ assert a.shape[1] % 2 == 0, "Input matrices must have an even number of columns."
304
+
305
+ n = a.shape[1] // 2
306
+
307
+ ax = a[:, :n]
308
+ az = a[:, n:]
309
+ bx = b[:, :n]
310
+ bz = b[:, n:]
311
+
312
+ sp = ax @ bz.T + az @ bx.T
313
+ sp.data %= 2
314
+
315
+ return sp
316
+
317
+
318
+ def check_binary_pauli_matrices_commute(
319
+ mat1: scipy.sparse.spmatrix, mat2: scipy.sparse.spmatrix
320
+ ) -> bool:
321
+ """
322
+ Check if two binary Pauli matrices commute.
323
+ """
324
+ symplectic_product_result = symplectic_product(mat1, mat2)
325
+ symplectic_product_result.eliminate_zeros()
326
+ return not np.any(symplectic_product_result.data)
327
+
328
+
329
+ def binary_pauli_hamming_weight(
330
+ mat: scipy.sparse.spmatrix,
331
+ ) -> np.ndarray:
332
+ """
333
+ Compute the row-wise Hamming weight of a binary Pauli matrix.
334
+
335
+ A binary Pauli matrix has 2*n columns, where the first n columns encode
336
+ the X part and the second n columns encode the Z part. The Hamming weight
337
+ for each row is the number of qubits that are acted upon by a non-identity
338
+ Pauli operator (X, Y, or Z). In other words, for each row, we count the
339
+ number of columns where either the X part or the Z part has a 1.
340
+
341
+ Parameters
342
+ ----------
343
+ mat : scipy.sparse.spmatrix
344
+ A binary Pauli matrix with an even number of columns (2*n). Each entry
345
+ must be 0 or 1, indicating whether the row has an X or Z component
346
+ for the corresponding qubit.
347
+
348
+ Returns
349
+ -------
350
+ np.ndarray
351
+ A 1D NumPy array of length `mat.shape[0]`, where the i-th entry is
352
+ the Hamming weight of the i-th row in `mat`.
353
+
354
+ Raises
355
+ ------
356
+ AssertionError
357
+ If the matrix does not have an even number of columns.
358
+
359
+ Notes
360
+ -----
361
+ Internally, this function:
362
+ 1. Splits the matrix into the X and Z parts.
363
+ 2. Computes an elementwise OR of the X and Z parts.
364
+ 3. Counts the non-zero entries per row (i.e., columns where the row has a 1).
365
+
366
+ Because the bitwise OR operator `|` is not directly supported for CSR
367
+ matrices, we achieve the OR operation by adding the two sparse matrices
368
+ and capping the sum at 1. Any entries with a value >= 1 in the sum
369
+ are set to 1, which corresponds to OR semantics for binary data.
370
+
371
+ Examples
372
+ --------
373
+ >>> import numpy as np
374
+ >>> from scipy.sparse import csr_matrix
375
+ >>> # Create a 2-row matrix, each row having 6 columns (for n=3 qubits).
376
+ >>> # Row 0: columns [0,2] are set -> X on qubits 0 and 2.
377
+ >>> # Row 1: columns [3,4,5] are set -> Z on qubit 1, Y on qubit 2.
378
+ >>> mat_data = np.array([[1,0,1,0,0,0],
379
+ ... [0,0,0,1,1,1]], dtype=np.uint8)
380
+ >>> mat_sparse = csr_matrix(mat_data)
381
+ >>> binary_pauli_hamming_weight(mat_sparse)
382
+ array([2, 2], dtype=int32)
383
+ """
384
+ assert mat.shape[1] % 2 == 0, "Input matrix must have an even number of columns."
385
+
386
+ # Determine the number of qubits from the total columns.
387
+ n = mat.shape[1] // 2
388
+
389
+ # Partition the matrix into X and Z parts.
390
+ x_part = mat[:, :n]
391
+ z_part = mat[:, n:]
392
+
393
+ # We want a bitwise OR. Since CSR matrices do not support a direct OR,
394
+ # we add and then cap at 1: (x_part + z_part >= 1) -> 1
395
+ xz_or = x_part.copy()
396
+ xz_or += z_part
397
+ # Clip values greater than 1 to 1.
398
+ xz_or.data[xz_or.data > 1] = 1
399
+
400
+ # The row-wise Hamming weight is the number of non-zero columns in each row.
401
+ return xz_or.getnnz(axis=1)
@@ -0,0 +1,272 @@
1
+ import logging
2
+ # Suppress debug and info messages from urllib3 and requests libraries
3
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
4
+ logging.getLogger("requests").setLevel(logging.WARNING)
5
+
6
+ from scipy.sparse import csr_matrix
7
+
8
+ import requests
9
+ from bs4 import BeautifulSoup
10
+ import json
11
+
12
+ def get_codetables_de_matrix(q, n, k, output_json_path=None, write_to_file=False):
13
+ """
14
+ Retrieve quantum code data from Markus Grassl's codetables.de website.
15
+
16
+ This function queries the URL:
17
+ ``https://codetables.de/QECC/QECC.php?q={q}&n={n}&k={k}``,
18
+ attempting to fetch data for a quantum code with the specified parameters
19
+ over GF(q). The HTML response is parsed to extract:
20
+
21
+ - The lower bound (``d_lower``) and upper bound (``d_upper``) on the code distance.
22
+ - The stabilizer matrix (as lines within a ``<pre>`` block).
23
+
24
+ The stabilizer matrix is then converted into a list of rows, each containing
25
+ the column indices of any '1' entries (the ``pcm``). The result is returned
26
+ as a dictionary, and optionally written to a JSON file.
27
+
28
+ Parameters
29
+ ----------
30
+ q : int
31
+ The field size (e.g. 2, 4, etc.).
32
+ n : int
33
+ The length of the code (number of physical qubits).
34
+ k : int
35
+ The dimension of the code (number of logical qubits).
36
+ output_json_path : str or None, optional
37
+ File path to which the resulting dictionary will be written if
38
+ ``write_to_file`` is set to True. If None and ``write_to_file`` is True,
39
+ raises a ValueError.
40
+ write_to_file : bool, optional
41
+ Whether to write the resulting dictionary to a JSON file.
42
+
43
+ Returns
44
+ -------
45
+ dict
46
+ A dictionary with the fields:
47
+ ``{"n", "k", "d_upper", "d_lower", "url", "pcm"}``.
48
+
49
+ - ``pcm`` is a list of lists, where each inner list contains the column
50
+ indices of '1's for that row of the stabilizer matrix.
51
+ - ``url`` is the codetables.de URL used for the query.
52
+ - ``d_upper`` and ``d_lower`` are the distance bounds, if found.
53
+
54
+ Raises
55
+ ------
56
+ ValueError
57
+ If the server response is not 200 OK, or if no valid stabilizer matrix
58
+ lines could be found in the HTML (i.e., no code data for those parameters).
59
+ Also raised if ``write_to_file`` is True and ``output_json_path`` is None.
60
+
61
+ Notes
62
+ -----
63
+ - Data is sourced from `codetables.de <https://codetables.de>`__,
64
+ maintained by Markus Grassl.
65
+ - The function does not return an actual matrix but rather a convenient
66
+ representation of it (the ``pcm``). Use ``pcm_to_csr_matrix`` or another
67
+ helper to convert it into a numerical/sparse form.
68
+ """
69
+ url = f"https://codetables.de/QECC/QECC.php?q={q}&n={n}&k={k}"
70
+ resp = requests.get(url)
71
+ if resp.status_code != 200:
72
+ raise ValueError(
73
+ f"Failed to retrieve data (status code: {resp.status_code}). URL was: {url}"
74
+ )
75
+
76
+ soup = BeautifulSoup(resp.text, "html.parser")
77
+
78
+ # 1) Extract lower and upper distance bounds from <table> elements
79
+ lower_bound = None
80
+ upper_bound = None
81
+ tables = soup.find_all("table")
82
+ for table in tables:
83
+ rows = table.find_all("tr")
84
+ for row in rows:
85
+ cells = row.find_all("td")
86
+ if len(cells) == 2:
87
+ heading = cells[0].get_text(strip=True).lower()
88
+ value = cells[1].get_text(strip=True)
89
+ if "lower bound" in heading:
90
+ lower_bound = value
91
+ elif "upper bound" in heading:
92
+ upper_bound = value
93
+
94
+ # 2) Extract the stabilizer matrix lines from <pre> tags
95
+ matrix_lines = []
96
+ for tag in soup.find_all("pre"):
97
+ text = tag.get_text()
98
+ if "stabilizer matrix" in text.lower():
99
+ lines = text.splitlines()
100
+ capture = False
101
+ for line in lines:
102
+ if "stabilizer matrix" in line.lower():
103
+ capture = True
104
+ continue
105
+ if capture:
106
+ # Stop at 'last modified:' or if the line is empty
107
+ if "last modified:" in line.lower():
108
+ break
109
+ if line.strip() != "":
110
+ matrix_lines.append(line.strip())
111
+
112
+ if not matrix_lines:
113
+ raise ValueError(f"No valid stabilizer matrix found at {url}")
114
+
115
+ # 3) Convert lines -> list of column-index lists
116
+ pcm_list = []
117
+ for line in matrix_lines:
118
+ line = line.strip().strip("[]").replace("|", " ")
119
+ elements = line.split()
120
+ row_cols = [i for i, val in enumerate(elements) if val == "1"]
121
+ pcm_list.append(row_cols)
122
+
123
+ if not pcm_list:
124
+ raise ValueError(f"No valid rows containing '1' found at {url}")
125
+
126
+ # 4) Build final dictionary
127
+ result_dict = {
128
+ "n": n,
129
+ "k": k,
130
+ "d_upper": upper_bound,
131
+ "d_lower": lower_bound,
132
+ "url": url,
133
+ "pcm": pcm_list,
134
+ }
135
+
136
+ # 5) Optionally write to JSON file
137
+ if write_to_file:
138
+ if output_json_path is None:
139
+ raise ValueError("output_json_path must be provided if write_to_file=True.")
140
+ with open(output_json_path, "w") as out_file:
141
+ json.dump(result_dict, out_file, indent=2)
142
+
143
+ return result_dict
144
+
145
+
146
+ def pcm_to_csr_matrix(pcm, num_cols=None):
147
+ """
148
+ Convert a "pcm" to a SciPy CSR matrix.
149
+
150
+ Each inner list of ``pcm`` is interpreted as the column indices in which
151
+ row `i` has a value of 1. The resulting CSR matrix will thus have as many
152
+ rows as ``len(pcm)``. The number of columns can either be:
153
+
154
+ - Inferred automatically (``num_cols=None``) by taking 1 + max(column index).
155
+ - Specified by the user. If a column index is >= num_cols, a ValueError is raised.
156
+
157
+ Parameters
158
+ ----------
159
+ pcm : list of lists of int
160
+ Each element ``pcm[i]`` is a list of column indices where row i has '1'.
161
+ num_cols : int or None, optional
162
+ The desired number of columns (width of the matrix).
163
+ If None, the width is auto-detected from the maximum column index.
164
+
165
+ Returns
166
+ -------
167
+ csr_matrix
168
+ A sparse matrix of shape ``(len(pcm), num_cols)``.
169
+
170
+ Raises
171
+ ------
172
+ ValueError
173
+ If any column index exceeds the specified ``num_cols``.
174
+ Also raised if no rows or invalid columns exist.
175
+
176
+ See Also
177
+ --------
178
+ get_codetables_de_matrix : Returns a dictionary with ``pcm`` field from codetables.de.
179
+
180
+ Notes
181
+ -----
182
+ Data is typically retrieved from `codetables.de <https://codetables.de>`__
183
+ and fed into this function to produce a numerical/sparse representation.
184
+ """
185
+ if not pcm:
186
+ # No rows at all => shape (0, num_cols) or (0, 0) if num_cols is None
187
+ if num_cols is None:
188
+ return csr_matrix((0, 0), dtype=int)
189
+ else:
190
+ return csr_matrix((0, num_cols), dtype=int)
191
+
192
+ row_indices = []
193
+ col_indices = []
194
+ data = []
195
+
196
+ max_col_found = -1
197
+
198
+ # Collect row/col for each '1' entry
199
+ for row_idx, col_list in enumerate(pcm):
200
+ for c in col_list:
201
+ row_indices.append(row_idx)
202
+ col_indices.append(c)
203
+ data.append(1)
204
+ if c > max_col_found:
205
+ max_col_found = c
206
+
207
+ num_rows = len(pcm)
208
+
209
+ # Auto-detect columns if not specified
210
+ if num_cols is None:
211
+ num_cols = max_col_found + 1
212
+ else:
213
+ # If the user specified num_cols, ensure the data fits
214
+ if max_col_found >= num_cols:
215
+ raise ValueError(
216
+ f"Column index {max_col_found} is out of range for a matrix of width {num_cols}."
217
+ )
218
+
219
+ return csr_matrix(
220
+ (data, (row_indices, col_indices)), shape=(num_rows, num_cols), dtype=int
221
+ )
222
+
223
+
224
+ def load_codetables_de_matrix_from_json(json_data):
225
+ """
226
+ Construct a CSR matrix from a codetables.de JSON/dict output.
227
+
228
+ This function takes either a dictionary (as returned by
229
+ ``get_codetables_de_matrix``) or a JSON string that decodes to the same
230
+ structure, and converts the ``pcm`` field into a SciPy CSR matrix.
231
+
232
+ Parameters
233
+ ----------
234
+ json_data : dict or str
235
+ Must contain at least the following keys:
236
+ ``{"n", "k", "d_upper", "d_lower", "url", "pcm"}``.
237
+ - ``pcm`` is a list of lists of column indices.
238
+
239
+ Returns
240
+ -------
241
+ csr_matrix
242
+ The stabilizer matrix in CSR format.
243
+ dict
244
+ The original dictionary that was passed in (or parsed from JSON).
245
+
246
+ Raises
247
+ ------
248
+ ValueError
249
+ If ``json_data`` is not a dict, if it cannot be parsed into one,
250
+ or if required keys are missing.
251
+
252
+ Notes
253
+ -----
254
+ - Data is assumed to come from Markus Grassl's `codetables.de <https://codetables.de>`__.
255
+ - This utility is helpful when the data is stored or transmitted in JSON form
256
+ but needs to be loaded back into a matrix representation for further processing.
257
+ """
258
+ if isinstance(json_data, str):
259
+ json_data = json.loads(json_data)
260
+
261
+ if not isinstance(json_data, dict):
262
+ raise ValueError(
263
+ "json_data must be a dict or a JSON string that decodes to a dict."
264
+ )
265
+
266
+ required_keys = {"n", "k", "d_upper", "d_lower", "url", "pcm"}
267
+ if not required_keys.issubset(json_data.keys()):
268
+ raise ValueError(f"JSON data missing required keys: {required_keys}")
269
+
270
+ pcm = json_data["pcm"]
271
+ sparse_matrix = pcm_to_csr_matrix(pcm)
272
+ return sparse_matrix, json_data