qec 0.3.0__py3-none-any.whl → 0.3.3__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,404 @@
1
+ import numpy as np
2
+ import numpy.typing
3
+ import scipy
4
+ import scipy.sparse
5
+ from qec.utils.sparse_binary_utils import convert_to_binary_scipy_sparse
6
+
7
+
8
+ def GF4_to_binary(GF4_matrix: numpy.typing.ArrayLike) -> scipy.sparse.csr_matrix:
9
+ """
10
+ Convert a matrix over GF4 (elements {0,1,2,3}) to a binary sparse matrix in CSR format.
11
+
12
+ Each entry (row i, column j) is mapped as follows:
13
+ - 0 => no 1's (row has [0, 0])
14
+ - 1 => one 1 in column 2*j ([1, 0])
15
+ - 2 => two 1's in columns 2*j and 2*j + 1 ([1, 1])
16
+ - 3 => one 1 in column 2*j + 1 ([0, 1])
17
+
18
+ Parameters
19
+ ----------
20
+ GF4_matrix : ArrayLike
21
+ Input matrix of shape (M, N) containing only elements from {0, 1, 2, 3}.
22
+ Can be a dense array-like or any SciPy sparse matrix format.
23
+
24
+ Returns
25
+ -------
26
+ scipy.sparse.csr_matrix
27
+ Binary sparse matrix in CSR format, of shape (M, 2*N).
28
+
29
+ Raises
30
+ ------
31
+ ValueError
32
+ If the input matrix has elements outside {0, 1, 2, 3}.
33
+
34
+ Examples
35
+ --------
36
+ >>> import numpy as np
37
+ >>> from scipy.sparse import scipy.sparse.csr_matrix
38
+ >>> mat = np.array([[0, 1],
39
+ ... [2, 3]])
40
+ >>> GF4_to_binary(mat).toarray()
41
+ array([[0, 1, 1, 0],
42
+ [1, 1, 0, 1]], dtype=uint8)
43
+ """
44
+ if scipy.sparse.issparse(GF4_matrix):
45
+ mat_coo = GF4_matrix.tocoo(copy=False)
46
+
47
+ if not np.all(np.isin(mat_coo.data, [1, 2, 3])):
48
+ raise ValueError(
49
+ "Input matrix must contain only elements from GF4: {0, 1, 2, 3}"
50
+ )
51
+
52
+ row_ids = []
53
+ col_ids = []
54
+ rows, cols = mat_coo.shape
55
+
56
+ for r, c, val in zip(mat_coo.row, mat_coo.col, mat_coo.data):
57
+ if val == 1:
58
+ row_ids.append(r)
59
+ col_ids.append(c)
60
+ elif val == 2:
61
+ row_ids.extend([r, r])
62
+ col_ids.extend([c, cols + c])
63
+ elif val == 3:
64
+ row_ids.append(r)
65
+ col_ids.append(cols + c)
66
+
67
+ data = np.ones(len(row_ids), dtype=np.uint8)
68
+ return scipy.sparse.csr_matrix(
69
+ (data, (row_ids, col_ids)), shape=(rows, 2 * cols)
70
+ )
71
+
72
+ GF4_matrix = np.asanyarray(GF4_matrix, dtype=int)
73
+ if not np.all(np.isin(GF4_matrix, [0, 1, 2, 3])):
74
+ raise ValueError(
75
+ "Input matrix must contain only elements from GF4: {0, 1, 2, 3}"
76
+ )
77
+
78
+ row_ids = []
79
+ col_ids = []
80
+ rows, cols = GF4_matrix.shape
81
+
82
+ for i in range(rows):
83
+ for j in range(cols):
84
+ val = GF4_matrix[i, j]
85
+ if val == 1:
86
+ row_ids.append(i)
87
+ col_ids.append(j)
88
+ elif val == 2:
89
+ row_ids.extend([i, i])
90
+ col_ids.extend([j, j + cols])
91
+ elif val == 3:
92
+ row_ids.append(i)
93
+ col_ids.append(j + cols)
94
+
95
+ data = np.ones(len(row_ids), dtype=np.uint8)
96
+ return scipy.sparse.csr_matrix((data, (row_ids, col_ids)), shape=(rows, 2 * cols))
97
+
98
+
99
+ def pauli_str_to_binary_pcm(
100
+ pauli_strings: numpy.typing.ArrayLike,
101
+ ) -> scipy.sparse.csr_matrix:
102
+ """
103
+ 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).
104
+
105
+ The mapping for each qubit j in the string is:
106
+ - 'I' => (0|0)
107
+ - 'X' => (1|0)
108
+ - 'Z' => (0|1)
109
+ - 'Y' => (1|1)
110
+ where the first element (a), in (a|b) is at column j and the second element (b) is at column j + N.
111
+
112
+ Parameters
113
+ ----------
114
+ pauli_strings : ArrayLike
115
+ Array of shape (M, 1), where each element is a string of Pauli operators
116
+ ('I', 'X', 'Y', 'Z'). Can be dense or any SciPy sparse matrix format with
117
+ an object/string dtype.
118
+
119
+ Returns
120
+ -------
121
+ scipy.sparse.csr_matrix
122
+ Binary parity-check matrix of shape (M, 2*N) in CSR format, where M is the number of stabilisers and
123
+ N is the number of physical qubits.
124
+ Raises
125
+ ------
126
+ ValueError
127
+ If any character in the Pauli strings is not one of {'I', 'X', 'Y', 'Z'}.
128
+
129
+ Examples
130
+ --------
131
+ >>> import numpy as np
132
+ >>> paulis = np.array([["XIZ"], ["YYI"]], dtype=object)
133
+ >>> pcm = pauli_str_to_binary_pcm(paulis)
134
+ >>> pcm.toarray()
135
+ array([[1, 0, 0, 0, 0, 1],
136
+ [1, 1, 0, 1, 1, 0]], dtype=uint8)
137
+ """
138
+
139
+ if scipy.sparse.issparse(pauli_strings):
140
+ if pauli_strings.dtype == object:
141
+ mat_coo = pauli_strings.tocoo(copy=False)
142
+ dense = np.full(pauli_strings.shape, "I", dtype=str)
143
+ for r, c, val in zip(mat_coo.row, mat_coo.col, mat_coo.data):
144
+ dense[r, c] = val
145
+ pauli_strings = dense
146
+ else:
147
+ pauli_strings = pauli_strings.toarray()
148
+
149
+ pauli_strings = np.asanyarray(pauli_strings, dtype=str)
150
+
151
+ if pauli_strings.size == 0:
152
+ return scipy.sparse.csr_matrix((0, 0))
153
+
154
+ row_ids = []
155
+ col_ids = []
156
+
157
+ m_stabilisers = pauli_strings.shape[0]
158
+ n_qubits = len(pauli_strings[0, 0])
159
+
160
+ for i, string in enumerate(pauli_strings):
161
+ if len(string[0]) != n_qubits:
162
+ raise ValueError("The Pauli strings do not have equal length.")
163
+ for j, char in enumerate(string[0]):
164
+ if char == "I":
165
+ continue
166
+ elif char == "X":
167
+ row_ids.append(i)
168
+ col_ids.append(j)
169
+ elif char == "Z":
170
+ row_ids.append(i)
171
+ col_ids.append(j + n_qubits)
172
+ elif char == "Y":
173
+ row_ids.extend([i, i])
174
+ col_ids.extend([j, j + n_qubits])
175
+ else:
176
+ raise ValueError(f"Invalid Pauli character '{char}' encountered.")
177
+
178
+ data = np.ones(len(row_ids), dtype=np.uint8)
179
+
180
+ return scipy.sparse.csr_matrix(
181
+ (data, (row_ids, col_ids)), shape=(m_stabilisers, 2 * n_qubits), dtype=np.uint8
182
+ )
183
+
184
+
185
+ def binary_pcm_to_pauli_str(binary_pcm: numpy.typing.ArrayLike) -> np.ndarray:
186
+ """
187
+ Convert a binary (M x 2*N) PCM corresponding to M stabilisers acting on N physical qubits,
188
+ back into an array (M x 1) of Pauli strings that have length N.
189
+
190
+ For each qubit j, columns (j | j + N) of the PCM encode:
191
+ - (0|0) => 'I'
192
+ - (1|0) => 'X'
193
+ - (0|1) => 'Z'
194
+ - (1|1) => 'Y'
195
+
196
+ Parameters
197
+ ----------
198
+ binary_pcm : ArrayLike
199
+ Binary matrix of shape (M, 2*N), in dense or any SciPy sparse matrix format.
200
+
201
+ Returns
202
+ -------
203
+ np.ndarray
204
+ Array of shape (M, 1), where each element is a string of Pauli operators with length N.
205
+
206
+ Examples
207
+ --------
208
+ >>> import numpy as np
209
+ >>> from scipy.sparse import scipy.sparse.csr_matrix
210
+ >>> pcm = np.array([[1, 0, 0, 0, 0, 1],
211
+ ... [1, 1, 0, 1, 1, 0]], dtype=np.uint8)
212
+ >>> pauli_str_to_return = binary_pcm_to_pauli_str(pcm)
213
+ >>> pauli_str_to_return
214
+ array([['XIZ'],
215
+ ['YYI']], dtype='<U3')
216
+ """
217
+ if scipy.sparse.issparse(binary_pcm):
218
+ binary_pcm = binary_pcm.toarray()
219
+
220
+ binary_pcm = np.asanyarray(binary_pcm, dtype=int)
221
+ n_rows, n_cols = binary_pcm.shape
222
+ n_qubits = n_cols // 2
223
+ pauli_strings = [""] * n_rows
224
+
225
+ for i in range(n_rows):
226
+ row = binary_pcm[i]
227
+ x_bits = row[:n_qubits]
228
+ z_bits = row[n_qubits:]
229
+ for x_bit, z_bit in zip(x_bits, z_bits):
230
+ if x_bit == 0 and z_bit == 0:
231
+ pauli_strings[i] += "I"
232
+ elif x_bit == 1 and z_bit == 0:
233
+ pauli_strings[i] += "X"
234
+ elif x_bit == 0 and z_bit == 1:
235
+ pauli_strings[i] += "Z"
236
+ else:
237
+ pauli_strings[i] += "Y"
238
+
239
+ return np.array(pauli_strings, dtype=str).reshape(-1, 1)
240
+
241
+
242
+ def symplectic_product(
243
+ a: numpy.typing.ArrayLike, b: numpy.typing.ArrayLike
244
+ ) -> scipy.sparse.csr_matrix:
245
+ """
246
+ Compute the symplectic product of two binary matrices in CSR format.
247
+
248
+ The input matrices (A,B) are first converted to binary sparse format (modulo 2)
249
+ and then partitioned into `x` and `z` components, where x and z have the same shape:
250
+
251
+ A = (a_x|a_z)
252
+ B = (b_x|b_z)
253
+
254
+ Then the symplectic product is computed as: (a_x * b_z^T + a_z * b_x^T) mod 2.
255
+
256
+ Parameters
257
+ ----------
258
+ a : array_like
259
+ A 2D array-like object with shape (M, 2N), which will be converted to
260
+ a binary sparse matrix (mod 2).
261
+ b : array_like
262
+ A 2D array-like object with shape (M, 2N), which will be converted to
263
+ a binary sparse matrix (mod 2). Must have the same shape as `a`.
264
+
265
+ Returns
266
+ -------
267
+ scipy.sparse.csr_matrix
268
+ The symplectic product of the two input matrices, stored in CSR format.
269
+
270
+ Raises
271
+ ------
272
+ AssertionError
273
+ If the shapes of `a` and `b` do not match.
274
+ AssertionError
275
+ If the number of columns of `a` (and `b`) is not even.
276
+
277
+ Notes
278
+ -----
279
+ This function is particularly useful for calculating commutation between Pauli operators,
280
+ where a result of 0 indicates commuting operators, and 1 indicates anti-commuting operators.
281
+
282
+ Examples
283
+ --------
284
+ >>> import numpy as np
285
+ >>> from qec.utils.sparse_binary_utils import convert_to_binary_scipy_sparse
286
+ >>> a_data = np.array([[1, 0, 0, 1],
287
+ ... [0, 1, 1, 0],
288
+ ... [1, 1, 0, 0]], dtype=int)
289
+ >>> b_data = np.array([[0, 1, 1, 0],
290
+ ... [1, 0, 0, 1],
291
+ ... [0, 1, 1, 0]], dtype=int)
292
+ >>> # Compute symplectic product
293
+ >>> sp = symplectic_product(a_data, b_data)
294
+ >>> sp.toarray()
295
+ array([[0, 0, 0],
296
+ [0, 0, 0],
297
+ [1, 1, 1]], dtype=int8)
298
+ """
299
+
300
+ a = convert_to_binary_scipy_sparse(a)
301
+ b = convert_to_binary_scipy_sparse(b)
302
+
303
+ assert (
304
+ a.shape[1] == b.shape[1]
305
+ ), "Input matrices must have the same number of columns."
306
+ assert a.shape[1] % 2 == 0, "Input matrices must have an even number of columns."
307
+
308
+ n = a.shape[1] // 2
309
+
310
+ ax = a[:, :n]
311
+ az = a[:, n:]
312
+ bx = b[:, :n]
313
+ bz = b[:, n:]
314
+
315
+ sp = ax @ bz.T + az @ bx.T
316
+ sp.data %= 2
317
+
318
+ return sp
319
+
320
+
321
+ def check_binary_pauli_matrices_commute(
322
+ mat1: scipy.sparse.spmatrix, mat2: scipy.sparse.spmatrix
323
+ ) -> bool:
324
+ """
325
+ Check if two binary Pauli matrices commute.
326
+ """
327
+ symplectic_product_result = symplectic_product(mat1, mat2)
328
+ symplectic_product_result.eliminate_zeros()
329
+ return not np.any(symplectic_product_result.data)
330
+
331
+
332
+ def binary_pauli_hamming_weight(
333
+ mat: scipy.sparse.spmatrix,
334
+ ) -> np.ndarray:
335
+ """
336
+ Compute the row-wise Hamming weight of a binary Pauli matrix.
337
+
338
+ A binary Pauli matrix has 2*n columns, where the first n columns encode
339
+ the X part and the second n columns encode the Z part. The Hamming weight
340
+ for each row is the number of qubits that are acted upon by a non-identity
341
+ Pauli operator (X, Y, or Z). In other words, for each row, we count the
342
+ number of columns where either the X part or the Z part has a 1.
343
+
344
+ Parameters
345
+ ----------
346
+ mat : scipy.sparse.spmatrix
347
+ A binary Pauli matrix with an even number of columns (2*n). Each entry
348
+ must be 0 or 1, indicating whether the row has an X or Z component
349
+ for the corresponding qubit.
350
+
351
+ Returns
352
+ -------
353
+ np.ndarray
354
+ A 1D NumPy array of length `mat.shape[0]`, where the i-th entry is
355
+ the Hamming weight of the i-th row in `mat`.
356
+
357
+ Raises
358
+ ------
359
+ AssertionError
360
+ If the matrix does not have an even number of columns.
361
+
362
+ Notes
363
+ -----
364
+ Internally, this function:
365
+ 1. Splits the matrix into the X and Z parts.
366
+ 2. Computes an elementwise OR of the X and Z parts.
367
+ 3. Counts the non-zero entries per row (i.e., columns where the row has a 1).
368
+
369
+ Because the bitwise OR operator `|` is not directly supported for CSR
370
+ matrices, we achieve the OR operation by adding the two sparse matrices
371
+ and capping the sum at 1. Any entries with a value >= 1 in the sum
372
+ are set to 1, which corresponds to OR semantics for binary data.
373
+
374
+ Examples
375
+ --------
376
+ >>> import numpy as np
377
+ >>> from scipy.sparse import csr_matrix
378
+ >>> # Create a 2-row matrix, each row having 6 columns (for n=3 qubits).
379
+ >>> # Row 0: columns [0,2] are set -> X on qubits 0 and 2.
380
+ >>> # Row 1: columns [3,4,5] are set -> Z on qubit 1, Y on qubit 2.
381
+ >>> mat_data = np.array([[1,0,1,0,0,0],
382
+ ... [0,0,0,1,1,1]], dtype=np.uint8)
383
+ >>> mat_sparse = csr_matrix(mat_data)
384
+ >>> binary_pauli_hamming_weight(mat_sparse)
385
+ array([2, 2], dtype=int32)
386
+ """
387
+ assert mat.shape[1] % 2 == 0, "Input matrix must have an even number of columns."
388
+
389
+ # Determine the number of qubits from the total columns.
390
+ n = mat.shape[1] // 2
391
+
392
+ # Partition the matrix into X and Z parts.
393
+ x_part = mat[:, :n]
394
+ z_part = mat[:, n:]
395
+
396
+ # We want a bitwise OR. Since CSR matrices do not support a direct OR,
397
+ # we add and then cap at 1: (x_part + z_part >= 1) -> 1
398
+ xz_or = x_part.copy()
399
+ xz_or += z_part
400
+ # Clip values greater than 1 to 1.
401
+ xz_or.data[xz_or.data > 1] = 1
402
+
403
+ # The row-wise Hamming weight is the number of non-zero columns in each row.
404
+ return xz_or.getnnz(axis=1)
@@ -0,0 +1,272 @@
1
+ import logging
2
+ import json
3
+ import requests
4
+ from scipy.sparse import csr_matrix
5
+ from bs4 import BeautifulSoup
6
+
7
+ # Suppress debug and info messages from urllib3 and requests libraries
8
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
9
+ logging.getLogger("requests").setLevel(logging.WARNING)
10
+
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