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