iqm-benchmarks 1.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.

Potentially problematic release.


This version of iqm-benchmarks might be problematic. Click here for more details.

Files changed (42) hide show
  1. iqm/benchmarks/__init__.py +31 -0
  2. iqm/benchmarks/benchmark.py +109 -0
  3. iqm/benchmarks/benchmark_definition.py +264 -0
  4. iqm/benchmarks/benchmark_experiment.py +163 -0
  5. iqm/benchmarks/compressive_gst/__init__.py +20 -0
  6. iqm/benchmarks/compressive_gst/compressive_gst.py +1029 -0
  7. iqm/benchmarks/entanglement/__init__.py +18 -0
  8. iqm/benchmarks/entanglement/ghz.py +802 -0
  9. iqm/benchmarks/logging_config.py +29 -0
  10. iqm/benchmarks/optimization/__init__.py +18 -0
  11. iqm/benchmarks/optimization/qscore.py +719 -0
  12. iqm/benchmarks/quantum_volume/__init__.py +21 -0
  13. iqm/benchmarks/quantum_volume/clops.py +726 -0
  14. iqm/benchmarks/quantum_volume/quantum_volume.py +854 -0
  15. iqm/benchmarks/randomized_benchmarking/__init__.py +18 -0
  16. iqm/benchmarks/randomized_benchmarking/clifford_1q.pkl +0 -0
  17. iqm/benchmarks/randomized_benchmarking/clifford_2q.pkl +0 -0
  18. iqm/benchmarks/randomized_benchmarking/clifford_rb/__init__.py +19 -0
  19. iqm/benchmarks/randomized_benchmarking/clifford_rb/clifford_rb.py +386 -0
  20. iqm/benchmarks/randomized_benchmarking/interleaved_rb/__init__.py +19 -0
  21. iqm/benchmarks/randomized_benchmarking/interleaved_rb/interleaved_rb.py +555 -0
  22. iqm/benchmarks/randomized_benchmarking/mirror_rb/__init__.py +19 -0
  23. iqm/benchmarks/randomized_benchmarking/mirror_rb/mirror_rb.py +810 -0
  24. iqm/benchmarks/randomized_benchmarking/multi_lmfit.py +86 -0
  25. iqm/benchmarks/randomized_benchmarking/randomized_benchmarking_common.py +892 -0
  26. iqm/benchmarks/readout_mitigation.py +290 -0
  27. iqm/benchmarks/utils.py +521 -0
  28. iqm_benchmarks-1.3.dist-info/LICENSE +205 -0
  29. iqm_benchmarks-1.3.dist-info/METADATA +190 -0
  30. iqm_benchmarks-1.3.dist-info/RECORD +42 -0
  31. iqm_benchmarks-1.3.dist-info/WHEEL +5 -0
  32. iqm_benchmarks-1.3.dist-info/top_level.txt +2 -0
  33. mGST/LICENSE +21 -0
  34. mGST/README.md +54 -0
  35. mGST/additional_fns.py +962 -0
  36. mGST/algorithm.py +733 -0
  37. mGST/compatibility.py +238 -0
  38. mGST/low_level_jit.py +694 -0
  39. mGST/optimization.py +349 -0
  40. mGST/qiskit_interface.py +282 -0
  41. mGST/reporting/figure_gen.py +334 -0
  42. mGST/reporting/reporting.py +710 -0
mGST/low_level_jit.py ADDED
@@ -0,0 +1,694 @@
1
+ """
2
+ All functions compiled with numba, such as tensor contractions, derivatives...
3
+ """
4
+
5
+ import os
6
+
7
+ from numba import njit, prange
8
+ import numpy as np
9
+
10
+
11
+ def kill_files(folder):
12
+ """Delete all files in the specified folder.
13
+
14
+ This function iterates over all files in the given folder and attempts to delete them.
15
+ If a file cannot be deleted, an error message is printed.
16
+
17
+ Parameters
18
+ ----------
19
+ folder : str
20
+ The path to the folder containing the files to be deleted.
21
+ """
22
+ for the_file in os.listdir(folder):
23
+ file_path = os.path.join(folder, the_file)
24
+ try:
25
+ if os.path.isfile(file_path):
26
+ os.unlink(file_path)
27
+ except OSError as e:
28
+ print(f"Failed on filepath: {file_path}. Error: {e}")
29
+
30
+
31
+ def kill_numba_cache():
32
+ """Delete all __pycache__ folders in the project directory tree.
33
+
34
+ This function iterates through the project directory tree, looking for __pycache__ folders.
35
+ When found, it attempts to delete all files within the __pycache__ folders using the
36
+ `kill_files` function. If the files cannot be deleted, an error message is printed.
37
+ """
38
+ root_folder = os.path.realpath(__file__ + "/../../")
39
+ for root, dirnames, _ in os.walk(root_folder):
40
+ for dirname in dirnames:
41
+ if dirname == "__pycache__":
42
+ try:
43
+ kill_files(root + "/" + dirname)
44
+ except OSError as e:
45
+ print(f"Failed on {root}. Error: {e}")
46
+
47
+
48
+ @njit(cache=True)
49
+ def local_basis(x, b, length):
50
+ """Convert a base-10 integer to an integer in a specified base with a fixed number of digits.
51
+
52
+ This function takes an integer `x` in base-10 and converts it to base `b`.
53
+ The result is returned as an array of length `length` with leading zeros.
54
+
55
+ Parameters
56
+ ----------
57
+ x : int
58
+ The input number in base-10 to be converted.
59
+ b : int
60
+ The target base to convert the input number to.
61
+ length : int
62
+ The number of output digits in the target base representation.
63
+
64
+ Returns
65
+ -------
66
+ numpy.ndarray
67
+ A numpy array of integers representing the base-`b` digits of the converted number,
68
+ with leading zeros if necessary. The length of the array is `length`.
69
+ """
70
+ r = np.zeros(length).astype(np.int32)
71
+ k = 1
72
+ while x > 0:
73
+ r[-k] = x % b
74
+ x //= b
75
+ k += 1
76
+ return r
77
+
78
+
79
+ @njit(cache=True)
80
+ def contract(X, j_vec):
81
+ """Contract a sequence of matrices in the given order.
82
+
83
+ This function computes the product of a sequence of matrices specified by
84
+ the indices in `j_vec`. The result is the contracted product of the matrices
85
+ in the given order.
86
+
87
+ Parameters
88
+ ----------
89
+ X : numpy.ndarray
90
+ A 3D array containing the input matrices, of shape (n_matrices, n_rows, n_columns).
91
+ j_vec : numpy.ndarray
92
+ A 1D array of indices specifying the order in which to contract the matrices in X.
93
+
94
+ Returns
95
+ -------
96
+ numpy.ndarray
97
+ The contracted product of the matrices specified by the indices in `j_vec`.
98
+ """
99
+ j_vec = j_vec[j_vec >= 0]
100
+ res = np.eye(X[0].shape[0])
101
+ res = res.astype(np.complex128)
102
+ for j in j_vec:
103
+ res = res.dot(X[j])
104
+ return res
105
+
106
+
107
+ @njit(cache=True, fastmath=True)
108
+ def objf(X, E, rho, J, y):
109
+ """Calculate the objective function value for matrices, POVM elements, and target values.
110
+
111
+ This function computes the objective function value based on input matrices X, POVM elements E,
112
+ density matrix rho, and target values y.
113
+
114
+ Parameters
115
+ ----------
116
+ X : numpy.ndarray
117
+ A 3D array containing the input matrices, of shape (n_matrices, n_rows, n_columns).
118
+ E : numpy.ndarray
119
+ A 2D array representing the POVM elements, of shape (n_povm, r).
120
+ rho : numpy.ndarray
121
+ A 1D array representing the density matrix.
122
+ J : numpy.ndarray
123
+ A 2D array representing the indices for which the objective function will be evaluated.
124
+ y : numpy.ndarray
125
+ A 2D array of shape (n_povm, len(J)) containing the target values.
126
+
127
+ Returns
128
+ -------
129
+ float
130
+ The objective function value for the given set of matrices, POVM elements,
131
+ and target values, normalized by m and n_povm.
132
+ """
133
+ m = len(J)
134
+ n_povm = y.shape[0]
135
+ objf_ = 0
136
+ for i in prange(m): # pylint: disable=not-an-iterable
137
+ j = J[i][J[i] >= 0]
138
+ C = contract(X, j)
139
+ for o in range(n_povm):
140
+ objf_ += abs(E[o].conj() @ C @ rho - y[o, i]) ** 2
141
+ return objf_ / m / n_povm
142
+
143
+
144
+ @njit(cache=True)
145
+ def MVE_lower(X_true, E_true, rho_true, X, E, rho, J, n_povm):
146
+ """Compute the lower bound of the mean value error (MVE) between true and estimated parameters.
147
+
148
+ This function calculates the lower bound of the MVE between the true parameters (X_true,
149
+ E_true, rho_true) and the estimated parameters (X, E, rho) based on the provided J indices.
150
+
151
+ Parameters
152
+ ----------
153
+ X_true : numpy.ndarray
154
+ A 3D array containing true input matrices, of shape (n_matrices, n_rows, n_columns).
155
+ E_true : numpy.ndarray
156
+ A 2D array representing the true POVM elements, of shape (n_povm, r).
157
+ rho_true : numpy.ndarray
158
+ A 1D array representing the true density matrix.
159
+ X : numpy.ndarray
160
+ A 3D array containing estimated input matrices, of shape (n_matrices, n_rows, n_columns).
161
+ E : numpy.ndarray
162
+ A 2D array representing the estimated POVM elements, of shape (n_povm, r).
163
+ rho : numpy.ndarray
164
+ A 1D array representing the estimated density matrix.
165
+ J : numpy.ndarray
166
+ A 2D array representing indices for which the objective function will be evaluated.
167
+ n_povm : int
168
+ The number of POVM elements.
169
+
170
+ Returns
171
+ -------
172
+ tuple of float
173
+ A tuple containing the lower bound of the mean value error and the maximum distance.
174
+ """
175
+ m = len(J)
176
+ dist: float = 0
177
+ max_dist: float = 0
178
+ curr: float = 0
179
+ for i in range(m):
180
+ j = J[i]
181
+ C_t = contract(X_true, j)
182
+ C = contract(X, j)
183
+ curr = 0
184
+ for k in range(n_povm):
185
+ y_t = E_true[k].conj() @ C_t @ rho_true
186
+ y = E[k].conj() @ C @ rho
187
+ curr += np.abs(y_t - y)
188
+ curr = curr / 2
189
+ dist += curr
190
+ max_dist = max(max_dist, curr)
191
+ return dist / m, max_dist
192
+
193
+
194
+ @njit(cache=True)
195
+ def Mp_norm_lower(X_true, E_true, rho_true, X, E, rho, J, n_povm, p):
196
+ """Compute the Mp-norm lower bound of the distance between true and estimated parameters.
197
+
198
+ This function calculates the lower bound of the Mp-norm between the true parameters (X_true,
199
+ E_true, rho_true) and the estimated parameters (X, E, rho) based on the provided J indices.
200
+
201
+ Parameters
202
+ ----------
203
+ X_true : numpy.ndarray
204
+ A 3D array containing true input matrices, of shape (n_matrices, n_rows, n_columns).
205
+ E_true : numpy.ndarray
206
+ A 2D array representing the true POVM elements, of shape (n_povm, r).
207
+ rho_true : numpy.ndarray
208
+ A 1D array representing the true density matrix.
209
+ X : numpy.ndarray
210
+ A 3D array containing estimated input matrices, of shape (n_matrices, n_rows, n_columns).
211
+ E : numpy.ndarray
212
+ A 2D array representing the estimated POVM elements, of shape (n_povm, r).
213
+ rho : numpy.ndarray
214
+ A 1D array representing the estimated density matrix.
215
+ J : numpy.ndarray
216
+ A 2D array representing indices for which the objective function will be evaluated.
217
+ n_povm : int
218
+ The number of POVM elements.
219
+ p : float
220
+ The order of the Mp-norm (p > 0).
221
+
222
+ Returns
223
+ -------
224
+ tuple of float
225
+ A tuple containing the Mp-norm lower bound and the maximum distance.
226
+ """
227
+ m = len(J)
228
+ dist: float = 0
229
+ max_dist: float = 0
230
+ curr: float = 0
231
+ for i in range(m):
232
+ j = J[i]
233
+ C_t = contract(X_true, j)
234
+ C = contract(X, j)
235
+ for k in range(n_povm):
236
+ y_t = E_true[k].conj() @ C_t @ rho_true
237
+ y = E[k].conj() @ C @ rho
238
+ dist += np.abs(y_t - y) ** p
239
+ max_dist = max(max_dist, curr)
240
+ return dist ** (1 / p) / m / n_povm, max_dist ** (1 / p)
241
+
242
+
243
+ @njit(cache=True)
244
+ def dK(X, K, E, rho, J, y, d, r, rK):
245
+ """Compute the derivative of the Kraus operator K with respect to its parameters.
246
+
247
+ This function calculates the derivative of the Kraus operator K, based on the
248
+ input matrices X, E, and rho, as well as the isometry condition.
249
+
250
+ Parameters
251
+ ----------
252
+ X : numpy.ndarray
253
+ The input matrix X, of shape (pdim, pdim).
254
+ K : numpy.ndarray
255
+ The Kraus operator K, reshaped to (d, rK, -1).
256
+ E : numpy.ndarray
257
+ A 2D array representing the POVM elements, of shape (n_povm, r).
258
+ rho : numpy.ndarray
259
+ A 1D array representing the density matrix.
260
+ J : numpy.ndarray
261
+ A 2D array representing the indices for which the derivatives will be computed.
262
+ y : numpy.ndarray
263
+ A 2D array of shape (n_povm, len(J)) containing the target values.
264
+ d : int
265
+ The number of Kraus operators.
266
+ r : int
267
+ The rank of the problem.
268
+ rK : int
269
+ The number of rows in the reshaped Kraus operator K.
270
+
271
+ Returns
272
+ -------
273
+ numpy.ndarray
274
+ The derivative of the Kraus operator K with respect to its parameters,
275
+ reshaped to (d, rK, pdim, pdim), and scaled by 2/m/n_povm.
276
+ """
277
+ K = K.reshape(d, rK, -1)
278
+ pdim = int(np.sqrt(r))
279
+ n_povm = y.shape[0]
280
+ dK_ = np.zeros((d, rK, r))
281
+ dK_ = np.ascontiguousarray(dK_.astype(np.complex128))
282
+ m = len(J)
283
+ for k in range(d):
284
+ for n in range(m):
285
+ j = J[n][J[n] >= 0]
286
+ for i, j_curr in enumerate(j):
287
+ if j_curr == k:
288
+ for o in range(n_povm):
289
+ L = E[o].conj() @ contract(X, j[:i])
290
+ R = contract(X, j[i + 1 :]) @ rho
291
+ D_ind = L @ X[k] @ R - y[o, n]
292
+ dK_[k] += D_ind * K[k].conj() @ np.kron(L.reshape(pdim, pdim).T, R.reshape(pdim, pdim).T)
293
+ return dK_.reshape(d, rK, pdim, pdim) * 2 / m / n_povm
294
+
295
+
296
+ @njit(cache=True)
297
+ def dK_dMdM(X, K, E, rho, J, y, d, r, rK):
298
+ """Compute the derivatives of K, dM10, and dM11 for the given input parameters.
299
+
300
+ This function calculates the derivatives of K, dM10, and dM11 based on the input matrices X,
301
+ matrix K, POVM elements E, density matrix rho, and target values y.
302
+
303
+ Parameters
304
+ ----------
305
+ X : numpy.ndarray
306
+ A 3D array containing input matrices, of shape (n_matrices, n_rows, n_columns).
307
+ K : numpy.ndarray
308
+ A 1D array representing the matrix K.
309
+ E : numpy.ndarray
310
+ A 2D array representing the POVM elements, of shape (n_povm, r).
311
+ rho : numpy.ndarray
312
+ A 1D array representing the density matrix.
313
+ J : numpy.ndarray
314
+ A 2D array representing indices for which the objective function will be evaluated.
315
+ y : numpy.ndarray
316
+ A 2D array of shape (n_povm, len(J)) containing target values.
317
+ d : int
318
+ The number of dimensions for the matrix K.
319
+ r : int
320
+ The number of rows for the matrix K.
321
+ rK : int
322
+ The number of columns for the matrix K.
323
+
324
+ Returns
325
+ -------
326
+ tuple of numpy.ndarray
327
+ A tuple containing the derivatives of K, dM10, and dM11, each of which is a numpy.ndarray.
328
+ """
329
+ K = K.reshape(d, rK, -1)
330
+ pdim = int(np.sqrt(r))
331
+ n = d * rK * r
332
+ n_povm = y.shape[0]
333
+ dK_ = np.zeros((d, rK, r)).astype(np.complex128)
334
+ dM11 = np.zeros(n**2).astype(np.complex128)
335
+ dM10 = np.zeros(n**2).astype(np.complex128)
336
+ m = len(J)
337
+ for n in range(m):
338
+ j = J[n][J[n] >= 0]
339
+ dM = np.ascontiguousarray(np.zeros((n_povm, d, rK, r)).astype(np.complex128))
340
+ for i, _ in enumerate(j):
341
+ k = j[i]
342
+ C = contract(X, j[:i])
343
+ R = contract(X, j[i + 1 :]) @ rho
344
+ for o in range(n_povm):
345
+ L = E[o].conj() @ C
346
+ D_ind = L @ X[k] @ R - y[o, n]
347
+ dM_loc = K[k].conj() @ np.kron(L.reshape((pdim, pdim)).T, R.reshape((pdim, pdim)).T)
348
+ dM[o, k, :, :] += dM_loc
349
+ dK_[k] += D_ind * dM_loc
350
+ for o in range(n_povm):
351
+ dM11 += np.kron(dM[o].conj().reshape(-1), dM[o].reshape(-1))
352
+ dM10 += np.kron(dM[o].reshape(-1), dM[o].reshape(-1))
353
+ return (dK_.reshape((d, rK, pdim, pdim)) * 2 / m / n_povm, 2 * dM10 / m / n_povm, 2 * dM11 / m / n_povm)
354
+
355
+
356
+ @njit(cache=True, parallel=False)
357
+ def ddM(X, K, E, rho, J, y, d, r, rK):
358
+ """Compute the second derivative of the objective function with respect to matrix elements.
359
+
360
+ This function calculates the second derivative of the objective function for a given
361
+ set of input parameters.
362
+
363
+ Parameters
364
+ ----------
365
+ X : ndarray
366
+ Array of input matrices.
367
+ K : ndarray
368
+ Array of Kraus operators.
369
+ E : ndarray
370
+ Array of measurement operators.
371
+ rho : ndarray
372
+ Array of quantum states.
373
+ J : ndarray
374
+ Array of indices corresponding to the sequence of operations.
375
+ y : ndarray
376
+ Array of observed probabilities.
377
+ d : int
378
+ Number of Kraus operators.
379
+ r : int
380
+ Dimension of the local basis.
381
+ rK : int
382
+ Number of rows in the Kraus operator matrix.
383
+
384
+ Returns
385
+ -------
386
+ ddK : ndarray, shape (d, d, rK, rK, pdim, pdim, pdim, pdim)
387
+ Second derivative of the objective function with respect to matrix elements, reshaped
388
+ for easier manipulation.
389
+ dconjdK : ndarray, shape (d, d, rK, rK, pdim, pdim, pdim, pdim)
390
+ Conjugate of the second derivative of the objective function with respect to matrix
391
+ elements, reshaped for easier manipulation.
392
+ """
393
+ # pylint: disable=too-many-branches, too-many-nested-blocks
394
+ pdim = int(np.sqrt(r))
395
+ n_povm = y.shape[0]
396
+ ddK = np.zeros((d**2, rK**2, r, r))
397
+ ddK = np.ascontiguousarray(ddK.astype(np.complex128))
398
+ dconjdK = np.zeros((d**2, rK**2, r, r))
399
+ dconjdK = np.ascontiguousarray(ddK.astype(np.complex128))
400
+ m = len(J)
401
+ for k in range(d**2):
402
+ k1, k2 = local_basis(k, d, 2)
403
+ for n in range(m):
404
+ j = J[n][J[n] >= 0]
405
+ for i1, j_1 in enumerate(j):
406
+ if j_1 == k1:
407
+ for i2, j_2 in enumerate(j):
408
+ if j_2 == k2:
409
+ L0 = contract(X, j[: min(i1, i2)])
410
+ C = contract(X, j[min(i1, i2) + 1 : max(i1, i2)]).reshape(pdim, pdim, pdim, pdim)
411
+ R = contract(X, j[max(i1, i2) + 1 :]) @ rho
412
+ for o in range(n_povm):
413
+ L = E[o].conj() @ L0
414
+ if i1 == i2:
415
+ D_ind = L @ X[k1] @ R - y[o, n]
416
+ elif i1 < i2:
417
+ D_ind = L @ X[k1] @ C.reshape(r, r) @ X[k2] @ R - y[o, n]
418
+ else:
419
+ D_ind = L @ X[k2] @ C.reshape(r, r) @ X[k1] @ R - y[o, n]
420
+
421
+ ddK_loc = np.zeros((rK**2, r, r)).astype(np.complex128)
422
+ dconjdK_loc = np.zeros((rK**2, r, r)).astype(np.complex128)
423
+ for rk1 in range(rK):
424
+ for rk2 in range(rK):
425
+ if i1 < i2:
426
+ ddK_loc[rk1 * rK + rk2] = np.kron(
427
+ L.reshape(pdim, pdim) @ K[k1, rk1].conj(),
428
+ R.reshape(pdim, pdim) @ K[k2, rk2].T.conj(),
429
+ ) @ np.ascontiguousarray(C.transpose(1, 3, 0, 2)).reshape(r, r)
430
+
431
+ ddK_loc[rk1 * rK + rk2] = np.ascontiguousarray(
432
+ ddK_loc[rk1 * rK + rk2]
433
+ .reshape(pdim, pdim, pdim, pdim)
434
+ .transpose(0, 3, 2, 1)
435
+ ).reshape(r, r)
436
+
437
+ dconjdK_loc[rk1 * rK + rk2] = np.kron(
438
+ L.reshape(pdim, pdim) @ K[k1, rk1].conj(),
439
+ R.reshape(pdim, pdim).T @ K[k2, rk2].T,
440
+ ) @ np.ascontiguousarray(C.transpose(1, 2, 3, 0)).reshape(r, r)
441
+
442
+ dconjdK_loc[rk1 * rK + rk2] = np.ascontiguousarray(
443
+ dconjdK_loc[rk1 * rK + rk2]
444
+ .reshape(pdim, pdim, pdim, pdim)
445
+ .transpose(0, 2, 3, 1)
446
+ ).reshape(r, r)
447
+
448
+ elif i1 == i2:
449
+ dconjdK_loc[rk1 * rK + rk2] = np.outer(L, R)
450
+
451
+ elif i1 > i2:
452
+ ddK_loc[rk1 * rK + rk2] = np.kron(
453
+ L.reshape(pdim, pdim) @ K[k2, rk2].conj(),
454
+ R.reshape(pdim, pdim) @ K[k1, rk1].T.conj(),
455
+ ) @ np.ascontiguousarray(C.transpose(1, 3, 0, 2)).reshape(r, r)
456
+
457
+ ddK_loc[rk1 * rK + rk2] = np.ascontiguousarray(
458
+ ddK_loc[rk1 * rK + rk2]
459
+ .reshape(pdim, pdim, pdim, pdim)
460
+ .transpose(3, 0, 1, 2)
461
+ ).reshape(r, r)
462
+
463
+ dconjdK_loc[rk1 * rK + rk2] = np.kron(
464
+ L.reshape(pdim, pdim).T @ K[k2, rk2],
465
+ R.reshape(pdim, pdim) @ K[k1, rk1].T.conj(),
466
+ ) @ np.ascontiguousarray(C.transpose((0, 3, 2, 1))).reshape(r, r)
467
+
468
+ dconjdK_loc[rk1 * rK + rk2] = np.ascontiguousarray(
469
+ dconjdK_loc[rk1 * rK + rk2]
470
+ .reshape(pdim, pdim, pdim, pdim)
471
+ .transpose(2, 0, 1, 3)
472
+ ).reshape(r, r)
473
+
474
+ ddK[k1 * d + k2] += D_ind * ddK_loc
475
+ dconjdK[k1 * d + k2] += D_ind * dconjdK_loc
476
+ return (
477
+ ddK.reshape(d, d, rK, rK, pdim, pdim, pdim, pdim) * 2 / m / n_povm,
478
+ dconjdK.reshape(d, d, rK, rK, pdim, pdim, pdim, pdim) * 2 / m / n_povm,
479
+ )
480
+
481
+
482
+ @njit(parallel=True, cache=True)
483
+ def dA(X, A, B, J, y, r, pdim, n_povm):
484
+ """Compute the derivative of A with respect to the objective function.
485
+
486
+ This function calculates the gradient of A for a given set of input parameters.
487
+
488
+ Parameters
489
+ ----------
490
+ X : ndarray
491
+ Array of input matrices.
492
+ A : ndarray
493
+ Array of measurement operators.
494
+ B : ndarray
495
+ Array of quantum states.
496
+ J : ndarray
497
+ Array of indices corresponding to the sequence of operations.
498
+ y : ndarray
499
+ Array of observed probabilities.
500
+ r : int
501
+ Number of elements in each measurement operator.
502
+ pdim : int
503
+ Dimension of the density matrices.
504
+ n_povm : int
505
+ Number of measurement operators.
506
+
507
+ Returns
508
+ -------
509
+ dA : ndarray
510
+ Derivative of A with respect to the objective function.
511
+ """
512
+ A = np.ascontiguousarray(A)
513
+ B = np.ascontiguousarray(B)
514
+ E = np.zeros((n_povm, r)).astype(np.complex128)
515
+ for k in range(n_povm):
516
+ E[k] = (A[k].T.conj() @ A[k]).reshape(-1)
517
+ rho = (B @ B.T.conj()).reshape(-1)
518
+ dA_ = np.zeros((n_povm, pdim, pdim))
519
+ dA_ = dA_.astype(np.complex128)
520
+ m = len(J)
521
+ for n in prange(m): # pylint: disable=not-an-iterable
522
+ jE = J[n][J[n] >= 0][0]
523
+ j = J[n][J[n] >= 0][1:]
524
+ inner_deriv = contract(X, j) @ rho
525
+ D_ind = E[jE].conj().dot(inner_deriv) - y[n]
526
+ dA_[jE] += D_ind * A[jE] @ inner_deriv.reshape(pdim, pdim).T.conj()
527
+ return dA_
528
+
529
+
530
+ @njit(parallel=True, cache=True)
531
+ def dB(X, A, B, J, y, pdim):
532
+ """Compute the derivative of B with respect to the objective function.
533
+
534
+ This function calculates the gradient of B for a given set of input parameters.
535
+
536
+ Parameters
537
+ ----------
538
+ X : ndarray
539
+ Array of input matrices.
540
+ A : ndarray
541
+ Array of measurement operators.
542
+ B : ndarray
543
+ Array of quantum states.
544
+ J : ndarray
545
+ Array of indices corresponding to the sequence of operations.
546
+ y : ndarray
547
+ Array of observed probabilities.
548
+ pdim : int
549
+ Dimension of the density matrices.
550
+
551
+ Returns
552
+ -------
553
+ dB : ndarray
554
+ Derivative of B with respect to the objective function.
555
+ """
556
+ A = np.ascontiguousarray(A)
557
+ B = np.ascontiguousarray(B)
558
+ E = (A.T.conj() @ A).reshape(-1)
559
+ rho = (B @ B.T.conj()).reshape(-1)
560
+ dB_ = np.zeros((pdim, pdim))
561
+ dB_ = dB_.astype(np.complex128)
562
+ m = len(J)
563
+ for n in prange(m): # pylint: disable=not-an-iterable
564
+ jE = J[n][J[n] >= 0][0]
565
+ j = J[n][J[n] >= 0][1:]
566
+ inner_deriv = E[jE].conj().dot(contract(X, j))
567
+ D_ind = inner_deriv.dot(rho) - y[n]
568
+ dB_ += D_ind * inner_deriv.reshape(pdim, pdim).conj() @ B
569
+ return dB_
570
+
571
+
572
+ @njit(parallel=True, cache=True)
573
+ def ddA_derivs(X, A, B, J, y, r, pdim, n_povm):
574
+ """Calculate the derivatives of a given POVM element with respect to its parameters.
575
+
576
+ This function computes the derivatives of the POVM element based on input matrices
577
+ A, B, and X, as well as the isometry condition. The derivatives are only dependent
578
+ on one POVM element, and different POVM elements are connected via the isometry condition.
579
+
580
+ Parameters
581
+ ----------
582
+ X : numpy.ndarray
583
+ The input matrix X, of shape (pdim, pdim).
584
+ A : numpy.ndarray
585
+ A 3D array of shape (n_povm, pdim, pdim) representing the POVM elements.
586
+ B : numpy.ndarray
587
+ A 2D array of shape (pdim, pdim) representing the isometry matrix.
588
+ J : numpy.ndarray
589
+ A 2D array representing the indices for which the derivatives will be computed.
590
+ y : numpy.ndarray
591
+ A 2D array of shape (n_povm, len(J)) containing the target values.
592
+ r : int
593
+ The rank of the problem.
594
+ pdim : int
595
+ The dimension of the input matrices A and B.
596
+ n_povm : int
597
+ The number of POVM elements.
598
+
599
+ Returns
600
+ -------
601
+ tuple of numpy.ndarray
602
+ A tuple containing the computed derivatives:
603
+ - dA: The derivative of the POVM element A with respect to its parameters,
604
+ of shape (n_povm, pdim, pdim).
605
+ - dMdM: The product of the derivatives dM and dM, of shape (n_povm, r, r).
606
+ - dMconjdM: The product of the conjugate of dM and dM, of shape (n_povm, r, r).
607
+ - dconjdA: The product of the conjugate of dA, of shape (n_povm, r, r).
608
+ """
609
+ A = np.ascontiguousarray(A)
610
+ B = np.ascontiguousarray(B)
611
+ E = np.zeros((n_povm, r)).astype(np.complex128)
612
+ for k in range(n_povm):
613
+ E[k] = (A[k].T.conj() @ A[k]).reshape(-1)
614
+ rho = (B @ B.T.conj()).reshape(-1)
615
+ dA_ = np.zeros((n_povm, pdim, pdim)).astype(np.complex128)
616
+ dM = np.zeros((pdim, pdim)).astype(np.complex128)
617
+ dMdM = np.zeros((n_povm, r, r)).astype(np.complex128)
618
+ dMconjdM = np.zeros((n_povm, r, r)).astype(np.complex128)
619
+ dconjdA = np.zeros((n_povm, r, r)).astype(np.complex128)
620
+ m = len(J)
621
+ for n in prange(m): # pylint: disable=not-an-iterable
622
+ j = J[n][J[n] >= 0]
623
+ R = contract(X, j) @ rho
624
+ for o in range(n_povm):
625
+ D_ind = E[o].conj() @ R - y[o, n]
626
+ dM = A[o].conj() @ R.reshape(pdim, pdim).T
627
+ dMdM[o] += np.outer(dM, dM)
628
+ dMconjdM[o] += np.outer(dM.conj(), dM)
629
+ dA_[o] += D_ind * dM
630
+ dconjdA[o] += D_ind * np.kron(np.eye(pdim).astype(np.complex128), R.reshape(pdim, pdim).T)
631
+ return dA_ * 2 / m / n_povm, dMdM * 2 / m / n_povm, dMconjdM * 2 / m / n_povm, dconjdA * 2 / m / n_povm
632
+
633
+
634
+ @njit(parallel=True, cache=True)
635
+ def ddB_derivs(X, A, B, J, y, r, pdim):
636
+ """Calculate the derivatives of the isometry matrix B with respect to its parameters.
637
+
638
+ This function computes the derivatives of the isometry matrix B based on input matrices A and X,
639
+ as well as the isometry condition.
640
+
641
+ Parameters
642
+ ----------
643
+ X : numpy.ndarray
644
+ The input matrix X, of shape (pdim, pdim).
645
+ A : numpy.ndarray
646
+ A 3D array of shape (n_povm, pdim, pdim) representing the POVM elements.
647
+ B : numpy.ndarray
648
+ A 2D array of shape (pdim, pdim) representing the isometry matrix.
649
+ J : numpy.ndarray
650
+ A 2D array representing the indices for which the derivatives will be computed.
651
+ y : numpy.ndarray
652
+ A 2D array of shape (n_povm, len(J)) containing the target values.
653
+ r : int
654
+ The rank of the problem.
655
+ pdim : int
656
+ The dimension of the input matrices A and B.
657
+
658
+ Returns
659
+ -------
660
+ tuple of numpy.ndarray
661
+ A tuple containing the computed derivatives:
662
+ - dB: The derivative of the isometry matrix B with respect to its parameters,
663
+ of shape (pdim, pdim).
664
+ - dMdM: The product of the derivatives dM and dM, of shape (r, r).
665
+ - dMconjdM: The product of the conjugate of dM and dM, of shape (r, r).
666
+ - dconjdB: The product of the conjugate of dB, of shape (r, r).
667
+ """
668
+ n_povm = A.shape[0]
669
+ A = np.ascontiguousarray(A)
670
+ B = np.ascontiguousarray(B)
671
+ E = np.zeros((n_povm, r)).astype(np.complex128)
672
+ for k in range(n_povm):
673
+ E[k] = (A[k].T.conj() @ A[k]).reshape(-1)
674
+ rho = (B @ B.T.conj()).reshape(-1)
675
+ dB_ = np.zeros((pdim, pdim)).astype(np.complex128)
676
+ dM = np.zeros((pdim, pdim)).astype(np.complex128)
677
+ dMdM = np.zeros((r, r)).astype(np.complex128)
678
+ dMconjdM = np.zeros((r, r)).astype(np.complex128)
679
+ dconjdB = np.zeros((r, r)).astype(np.complex128)
680
+ m = len(J)
681
+ for n in prange(m): # pylint: disable=not-an-iterable
682
+ j = J[n][J[n] >= 0]
683
+ C = contract(X, j)
684
+ for o in range(n_povm):
685
+ L = E[o].conj() @ C
686
+ D_ind = L @ rho - y[o, n]
687
+
688
+ dM = L.reshape(pdim, pdim) @ B.conj()
689
+ dMdM += np.outer(dM, dM)
690
+ dMconjdM += np.outer(dM.conj(), dM)
691
+
692
+ dB_ += D_ind * dM
693
+ dconjdB += D_ind * np.kron(L.reshape(pdim, pdim), np.eye(pdim).astype(np.complex128))
694
+ return dB_ * 2 / m / n_povm, dMdM * 2 / m / n_povm, dMconjdM * 2 / m / n_povm, dconjdB.T * 2 / m / n_povm