wath 1.0.0__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.
wath/graph/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .mst import minimum_spanning_tree, graphs
wath/graph/mst.py ADDED
@@ -0,0 +1,115 @@
1
+ from typing import Hashable
2
+
3
+
4
+ class _Graph():
5
+
6
+ def __init__(self):
7
+ self._edges = []
8
+ self._nodes = set()
9
+
10
+ def add_edge(self, a, b):
11
+ self._edges.append((a, b))
12
+ self._nodes.add(a)
13
+ self._nodes.add(b)
14
+
15
+ def __or__(self, other):
16
+ self._edges.extend(other._edges)
17
+ self._nodes.update(other._nodes)
18
+ return self
19
+
20
+ def __contains__(self, item):
21
+ return item in self._nodes
22
+
23
+
24
+ def graphs(
25
+ edges: list[tuple[Hashable, Hashable]]
26
+ ) -> list[list[tuple[Hashable, Hashable]]]:
27
+ """
28
+ graphs
29
+
30
+ Args:
31
+ edges: list of edges
32
+
33
+ Returns:
34
+ list of graphs
35
+ """
36
+ graphs: list[_Graph] = []
37
+ for a, b in edges:
38
+ extended = []
39
+ for i, g in enumerate(graphs):
40
+ if a in g and b in g:
41
+ g.add_edge(a, b)
42
+ break
43
+ elif a in g or b in g:
44
+ g.add_edge(a, b)
45
+ extended.append(i)
46
+ else:
47
+ if len(extended) == 0:
48
+ g = _Graph()
49
+ g.add_edge(a, b)
50
+ graphs.append(g)
51
+ if len(extended) == 2:
52
+ graphs[extended[0]] = graphs[extended[0]] | graphs[extended[1]]
53
+ del graphs[extended[1]]
54
+
55
+ return [[tuple(e) for e in g._edges] for g in graphs]
56
+
57
+
58
+ def minimum_spanning_tree(
59
+ edges: list[tuple[Hashable, Hashable]]
60
+ | list[tuple[Hashable, Hashable, float]]
61
+ ) -> list[tuple[Hashable, Hashable]]:
62
+ """
63
+ minimum spanning tree
64
+
65
+ Args:
66
+ edges: list of edges
67
+
68
+ Returns:
69
+ list of edges in minimum spanning tree
70
+ """
71
+ if not edges:
72
+ return []
73
+ if len(edges) == 1:
74
+ return [(edges[0][0], edges[0][1])]
75
+
76
+ edges = [(u, v, 0 if len(w) == 0 else w[0]) for u, v, *w in edges]
77
+
78
+ # Create a set of nodes and map each node to an index
79
+ nodes = set()
80
+ for u, v, _ in edges:
81
+ nodes.add(u)
82
+ nodes.add(v)
83
+ nodes = list(nodes)
84
+ nodes_map = {n: i for i, n in enumerate(nodes)}
85
+
86
+ edges = [(nodes_map[u], nodes_map[v], w) for u, v, w in edges]
87
+
88
+ # Kruskal's algorithm
89
+ edges.sort(key=lambda e: e[2])
90
+ tree = []
91
+ parent = list(range(len(nodes)))
92
+ rank = [0] * len(nodes)
93
+
94
+ def find(u):
95
+ if parent[u] != u:
96
+ parent[u] = find(parent[u])
97
+ return parent[u]
98
+
99
+ def union(u, v):
100
+ u = find(u)
101
+ v = find(v)
102
+ if u == v:
103
+ return
104
+ if rank[u] < rank[v]:
105
+ u, v = v, u
106
+ parent[v] = u
107
+ if rank[u] == rank[v]:
108
+ rank[u] += 1
109
+
110
+ for u, v, _ in edges:
111
+ if find(u) != find(v):
112
+ union(u, v)
113
+ tree.append((nodes[u], nodes[v]))
114
+
115
+ return tree
wath/math/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .bayes import bayesian_correction
2
+ from .fit import (fit_circle, fit_cross_point, fit_pole, goodness_of_fit,
3
+ lin_fit)
4
+ from .signal.demodulate import getFTMatrix
wath/math/bayes.py ADDED
@@ -0,0 +1,474 @@
1
+ import itertools
2
+ import math
3
+ from collections import defaultdict
4
+
5
+ import numpy as np
6
+ from scipy.linalg import logm
7
+
8
+ from .fibheap import FibHeap, FibNode
9
+ from .paulis import string_to_matrices
10
+
11
+
12
+ def input_state_set(n):
13
+ """
14
+ Input states for CTMP calibration.
15
+
16
+ Args:
17
+ n: number of qubits
18
+
19
+ Returns:
20
+ a (M x n) array, each row is a state
21
+ M is the smallest integer such that
22
+ comb(M, M // 2) >= n
23
+ """
24
+ r = 2
25
+ while True:
26
+ if math.comb(2 * r - 1, r - 1) >= n:
27
+ break
28
+ r += 1
29
+ ret = np.zeros((2 * r, n), dtype=np.int8)
30
+
31
+ for i, x in enumerate(itertools.combinations(range(2 * r), r=r)):
32
+ if i >= n:
33
+ break
34
+ ret[x, i] = 1
35
+ return ret
36
+
37
+
38
+ def _generate_standrad_information(key, bayes_matrices):
39
+ ret = []
40
+ for i, k in enumerate(key):
41
+ tmp = sorted([(p, j) for j, p in enumerate(bayes_matrices[i][k])],
42
+ key=lambda x: abs(x[0]),
43
+ reverse=True)
44
+ ret.append((tuple(tmp), i))
45
+ return tuple(ret)
46
+
47
+
48
+ def _generate_population(std_info, population):
49
+ state, ret = [0] * len(std_info), population
50
+ for i in range(len(std_info)):
51
+ state[std_info[i][-1]] = std_info[i][0][0][1]
52
+ ret *= std_info[i][0][0][0]
53
+ return ret, tuple(state)
54
+
55
+
56
+ def _add_std_info_to_heap(state, population, heap, heap_nodes, max_n, min_p):
57
+ if state in heap_nodes.keys():
58
+ new_key = heap_nodes[state].key[0] * heap_nodes[state].key[
59
+ -1] + population
60
+ heap.change(heap_nodes[state],
61
+ (abs(new_key), state, (-1 if new_key < 0 else 1)))
62
+ elif heap.key_number < max_n:
63
+ heap_nodes[state] = FibNode(
64
+ (abs(population), state, (-1 if population < 0 else 1)))
65
+ heap.insert(heap_nodes[state])
66
+ else:
67
+ if abs(population) < max(min_p, heap.min_node.key[0]):
68
+ return False
69
+ trash = heap.extract_min()
70
+ heap_nodes.pop(trash.key[1])
71
+ heap_nodes[state] = FibNode(
72
+ (abs(population), state, (-1 if population < 0 else 1)))
73
+ heap.insert(heap_nodes[state])
74
+ return True
75
+
76
+
77
+ def _update_count(population, std_info, heap, heap_nodes, max_n, min_p):
78
+ queue = FibHeap()
79
+
80
+ p, state = _generate_population(std_info, population)
81
+ queue.insert(FibNode((-abs(p), state, std_info, 1 if p > 0 else -1)))
82
+
83
+ for _ in range(max_n):
84
+ node = queue.extract_min()
85
+ if node is None or not _add_std_info_to_heap(
86
+ node.key[1], -node.key[0] * node.key[-1], heap, heap_nodes,
87
+ max_n, min_p):
88
+ break
89
+
90
+ tmp_info = sorted(list(node.key[2]),
91
+ reverse=True,
92
+ key=lambda x:
93
+ (len(x[0]), abs(x[0][1][0] / x[0][0][0])
94
+ if len(x[0]) > 1 else 0))
95
+
96
+ for i in range(len(std_info)):
97
+ if len(tmp_info[i][0]) == 1:
98
+ break
99
+ new_info = tuple([
100
+ *tmp_info[:i], (tmp_info[i][0][1:], tmp_info[i][1]),
101
+ *tmp_info[i + 1:]
102
+ ])
103
+ p, state = _generate_population(new_info, population)
104
+ if heap.key_number < max_n or p > min(min_p, heap.min_node.key[0]):
105
+ queue.insert(
106
+ FibNode((-abs(p), state, new_info, (-1 if p < 0 else 1))))
107
+ elif abs(p) < max(min_p, heap.min_node.key[0]):
108
+ break
109
+ heap.consolidate()
110
+
111
+
112
+ def bayesian_correction_automatic_trimming(state,
113
+ correction_matrices,
114
+ size_lim=None,
115
+ eps=None):
116
+ from .fit.readout import count_state
117
+
118
+ counts = count_state(state)
119
+ shots = len(state)
120
+
121
+ if size_lim is None:
122
+ size_lim = shots * 4
123
+ if eps is None:
124
+ eps = 0.5 / size_lim
125
+
126
+ heap_nodes = {}
127
+ heap = FibHeap()
128
+
129
+ for key, value in sorted(counts.items(), key=lambda x: x[1],
130
+ reverse=False):
131
+ std_info = _generate_standrad_information(key, correction_matrices)
132
+ _update_count(value / shots, std_info, heap, heap_nodes, size_lim, eps)
133
+
134
+ result = {}
135
+ while heap.key_number:
136
+ node = heap.extract_min()
137
+ if node is None:
138
+ break
139
+ result[node.key[1]] = node.key[0] * node.key[-1]
140
+ return result
141
+
142
+
143
+ def bayesian_correction_in_subspace(state, correction_matrices, subspace):
144
+ """Apply a correction matrix to a state.
145
+
146
+ Args:
147
+ state (np.array, dtype=int): The state to be corrected.
148
+ correction_matrices (np.array): A list of correction matrices.
149
+ subspace (np.array, dtype=int): The basis of subspace.
150
+
151
+ See also:
152
+ https://doi.org/10.1103/PhysRevA.103.042605
153
+
154
+ Returns:
155
+ np.array: The corrected state.
156
+
157
+ Examples:
158
+ >>> state = np.random.randint(2, size = (101, 1024, 4))
159
+ >>> PgPe = np.array([[0.1, 0.8], [0.03, 0.91], [0.02, 0.87], [0.05, 0.9]])
160
+ >>> correction_matrices = np.array(
161
+ [np.array([[Pe, Pe - 1], [-Pg, 1 - Pg]]) / (Pe - Pg) for Pg, Pe in PgPe])
162
+ >>> subspace = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [1, 1, 0, 0]])
163
+ >>> result = bayesian_correction(state, correction_matrices, subspace)
164
+ >>> result.shape
165
+ (101, 5)
166
+ """
167
+ num_qubits = state.shape[-1]
168
+ site_index = np.arange(num_qubits)
169
+
170
+ shape = tuple([*state.shape[:-1], len(subspace)])
171
+ state = state.reshape(-1, num_qubits)
172
+
173
+ if len(subspace) < len(state):
174
+ ret = []
175
+ for target_state in subspace:
176
+ A = np.prod(correction_matrices[site_index, target_state, state],
177
+ axis=-1)
178
+ ret.append(A)
179
+ ret = np.array(ret).T.reshape(shape)
180
+ else:
181
+ ret = []
182
+ for bit_string in state:
183
+ A = np.prod(correction_matrices[site_index, subspace, bit_string],
184
+ axis=-1)
185
+ ret.append(A)
186
+ ret = np.array(ret).reshape(shape)
187
+ ret = ret.mean(axis=-2)
188
+ return ret
189
+
190
+
191
+ def bayesian_correction(state,
192
+ correction_matrices,
193
+ *,
194
+ subspace=None,
195
+ size_lim=1024,
196
+ eps=1e-6):
197
+ """Apply a correction matrix to a state.
198
+
199
+ Args:
200
+ state (np.array, dtype=int): The state to be corrected.
201
+ correction_matrices (np.array): A list of correction matrices.
202
+ subspace (np.array, dtype=int): The basis of subspace.
203
+ size_lim (int): The maximum size of the heap.
204
+ eps (float): The minimum probability of the state.
205
+
206
+ Returns:
207
+ np.array: The corrected state.
208
+
209
+ Examples:
210
+ >>> state = np.random.randint(2, size = (101, 1024, 4))
211
+ >>> PgPe = np.array([[0.1, 0.8], [0.03, 0.91], [0.02, 0.87], [0.05, 0.9]])
212
+ >>> correction_matrices = np.array(
213
+ [np.array([[Pe, Pe - 1], [-Pg, 1 - Pg]]) / (Pe - Pg) for Pg, Pe in PgPe])
214
+ >>> result = bayesian_correction(state, correction_matrices)
215
+ >>> result.shape
216
+ (101, 1024)
217
+ """
218
+ if subspace is None:
219
+ state = np.array(state)
220
+ if state.ndim == 2:
221
+ return bayesian_correction_automatic_trimming(
222
+ state, correction_matrices, size_lim, eps)
223
+ shape = state.shape[:-2]
224
+ state = state.reshape(-1, state.shape[-2], state.shape[-1])
225
+ ret = []
226
+ for s in state:
227
+ ret.append(
228
+ bayesian_correction_automatic_trimming(s, correction_matrices,
229
+ size_lim, eps))
230
+ return np.array(ret).reshape(shape)
231
+ else:
232
+ return bayesian_correction_in_subspace(state, correction_matrices,
233
+ subspace)
234
+
235
+
236
+ def extract_matrices(input_states, output_states):
237
+ """
238
+ Extract stochastic matrices from input and output states.
239
+
240
+ The stochastic matrix A is defined as
241
+ P_out = A P_in
242
+ where P_in and P_out are the probability vectors of prepared states
243
+ and measured states, respectively. P_in and P_out are column vectors,
244
+ and the order of the basis is |00>, |01>, |10>, |11>. A is a (4x4)
245
+ stochastic matrix.
246
+
247
+ Args:
248
+ input_states: a (N x n) array, each row is a state
249
+ output_states: a (N x M x n) array, each row is a state
250
+
251
+ Returns:
252
+ a dictionary of (4x4) stochastic matrices
253
+ the key is a tuple of two qubit indices
254
+ """
255
+ num_qubits = input_states.shape[-1]
256
+ matrices = {}
257
+ for i, j in itertools.combinations(range(num_qubits), r=2):
258
+ x = input_states[:, [i, j]]
259
+ y = output_states[:, :, [i, j]]
260
+
261
+ data = [[], [], [], []]
262
+ for k, (input_state, output_state) in enumerate(zip(x, y)):
263
+ data[2 * input_state[0] + input_state[1]].extend(
264
+ list(2 * output_state[:, 0] + output_state[:, 1]))
265
+ mat = np.zeros((4, 4), dtype=float)
266
+ for k, d in enumerate(data):
267
+ col = np.array(
268
+ [np.count_nonzero(np.array(d) == l) for l in range(4)])
269
+ mat[:, k] = col / col.sum()
270
+ matrices[(i, j)] = mat
271
+ return matrices
272
+
273
+
274
+ def get_error_rates(matrices, num_qubits):
275
+ """
276
+ Get the error rates from the stochastic matrices.
277
+
278
+ Args:
279
+ matrices: a dictionary of (4x4) stochastic matrices
280
+ num_qubits: number of qubits
281
+
282
+ Returns:
283
+ gamma: the total error rate
284
+ rates1: a dictionary of single-qubit error rates
285
+ rates2: a dictionary of two-qubit error rates
286
+ """
287
+ rates1 = defaultdict(lambda: np.array([0.0, 0.0]))
288
+ rates2 = {}
289
+ swap = np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])
290
+
291
+ for i, j in itertools.combinations(range(num_qubits), r=2):
292
+ if (i, j) in matrices:
293
+ G = logm(matrices[(i, j)])
294
+ elif (j, i) in matrices:
295
+ G = swap @ logm(matrices[(j, i)]) @ swap
296
+ else:
297
+ continue
298
+ G = np.clip(G.real, 0, np.inf)
299
+ rates2[(i, j)] = np.diag(G[::-1, :])
300
+ rates1[i] += np.array([G[2, 0] + G[3, 1], G[0, 2] + G[1, 3]
301
+ ]) / (2 * (num_qubits - 1))
302
+ rates1[j] += np.array([G[1, 0] + G[3, 2], G[0, 1] + G[2, 3]
303
+ ]) / (2 * (num_qubits - 1))
304
+
305
+ gamma = np.max(list(rates1.values()), axis=-1).sum() + np.max(
306
+ list(rates2.values()), axis=-1).sum()
307
+ return gamma, dict(rates1), rates2
308
+
309
+
310
+ def _flip(x, gamma, rate1, *rates):
311
+ seed = np.random.random()
312
+ for qubit, r in rate1.items():
313
+ seed -= r[x[qubit]] / gamma
314
+ if seed < 0:
315
+ x[qubit] ^= 1
316
+ return
317
+ for order, rate in enumerate(rates, start=2):
318
+ for qubits, r in rate.items():
319
+ index = 0
320
+ for qubit in qubits:
321
+ index = (index << 1) + x[qubit]
322
+ seed -= r[index] / gamma
323
+ if seed < 0:
324
+ for qubit in qubits:
325
+ x[qubit] ^= 1
326
+ return
327
+
328
+
329
+ def exception(state,
330
+ e_ops: np.ndarray | list[str],
331
+ correction_matrices: np.ndarray | None = None,
332
+ gamma=None,
333
+ rates1=None,
334
+ rates2=None):
335
+ """Calculate the exceptions of the operators.
336
+
337
+ Consider a simple case when A is a tensor product of 2 x 2 stochastic matrices.
338
+ and observable operator has a tensor product form.
339
+
340
+ Args:
341
+ state (np.array, dtype=int): The bit string of the state. The shape
342
+ should be (..., shots, num_qubits).
343
+ e_ops (np.array): A list of operators. Each operator should be diagonal.
344
+ correction_matrices (np.array): A list of correction matrices.
345
+
346
+ See also:
347
+ https://doi.org/10.1103/PhysRevA.103.042605
348
+
349
+ Returns:
350
+ np.array: The exceptions of the operators.
351
+
352
+ Examples:
353
+ >>> state = np.random.randint(2, size = (101, 1024, 4))
354
+ >>> errors = [[0.05, 0.1], [0.02, 0.03], [0.01, 0.08], [0.02, 0.03]]
355
+ >>> correction_matrices = np.array([
356
+ np.linalg.inv(np.array([[1 - eps, eta], [eps, 1 - eta]]))
357
+ for eps, eta in errors
358
+ ])
359
+ >>> ops = ['0III', '0II1', 'ZIZZ']
360
+ >>> result = exception(state, ops, correction_matrices)
361
+ >>> result.shape
362
+ (101, 3)
363
+ """
364
+
365
+ *datashape, shots, num_qubits = state.shape
366
+ site_index = np.arange(num_qubits)
367
+
368
+ if e_ops and isinstance(e_ops[0], str):
369
+ e_ops = [string_to_matrices(s, diag=True) for s in e_ops]
370
+ e_ops = np.asarray(e_ops)
371
+
372
+ *n_ops, num_qubits_, _, _ = e_ops.shape
373
+ assert num_qubits == num_qubits_
374
+
375
+ if gamma is not None and rates1 is not None:
376
+ if rates2 is None:
377
+ if correction_matrices is None:
378
+ correction_matrices = []
379
+ for i in range(num_qubits):
380
+ r0, r1 = rates1.get(i, (0, 0))
381
+ if r0 + r1 == 0:
382
+ correction_matrices.append(np.eye(2))
383
+ else:
384
+ eps = r0 / (r0 + r1) * (1 - np.exp(-r0 - r1))
385
+ eta = r1 / (r0 + r1) * (1 - np.exp(-r0 - r1))
386
+ correction_matrices.append(
387
+ np.linalg.inv(
388
+ np.array([[1 - eps, eta], [eps, 1 - eta]])))
389
+ else:
390
+ e_ops = e_ops[..., [0, 1], [0, 1]]
391
+ alpha = np.random.poisson(gamma, (*datashape, shots))
392
+ state = np.copy(state).reshape(-1, num_qubits)
393
+ for n, s in zip(alpha.reshape(-1), state):
394
+ for _ in range(n):
395
+ _flip(s, gamma, rates1, rates2)
396
+ state = state.reshape(*datashape, shots, num_qubits)
397
+ sign = (-1)**(alpha & 1)
398
+ return np.exp(2 * gamma) * np.moveaxis(
399
+ (sign * e_ops[..., site_index, state].prod(axis=-1)).mean(
400
+ axis=-1), 0, -1)
401
+
402
+ if correction_matrices is None:
403
+ M = e_ops
404
+ else:
405
+ correction_matrices = np.asarray(correction_matrices)
406
+ num_qubits_, _, _ = correction_matrices.shape
407
+ assert num_qubits == num_qubits_
408
+ M = e_ops @ correction_matrices
409
+
410
+ return M[..., site_index, :,
411
+ state.reshape(-1, num_qubits).astype(np.int8)].sum(axis=-1).prod(
412
+ axis=1).reshape(*datashape, shots,
413
+ *n_ops).mean(axis=len(datashape))
414
+
415
+
416
+ def measure(op: str):
417
+ """
418
+ Measure the operator.
419
+
420
+ Covnert the operator to the form of 'I', 'Z' by
421
+ adding '-X/2' and 'Y/2' gates.
422
+ return the covnerted operator and the circuit.
423
+
424
+ Args:
425
+ op: the operator to be measured
426
+ the operator is a string of 'I', 'X', 'Y', 'Z'
427
+
428
+ Returns:
429
+ operator, circuit
430
+ the operator is a string of 'I', 'Z'
431
+ the circuit is a list of (gate, qubit_index)
432
+
433
+ Examples:
434
+ >>> measure('X')
435
+ ('Z', [('-Y/2', 0)])
436
+ >>> measure('Y')
437
+ ('Z', [('X/2', 0)])
438
+ >>> measure('Z')
439
+ ('Z', [])
440
+ >>> measure('I')
441
+ ('I', [])
442
+ """
443
+ e_op = []
444
+ circ = []
445
+
446
+ sign = 1
447
+ op = op.strip()
448
+ if op.startswith('+'):
449
+ op = op[1:]
450
+ if op.startswith('-'):
451
+ sign = -1
452
+ op = op[1:]
453
+ if op.startswith('i'):
454
+ op = op[1:]
455
+ sign *= 1j
456
+ if sign == -1:
457
+ e_op.append('-')
458
+ elif sign == 1j:
459
+ e_op.append('i')
460
+ elif sign == -1j:
461
+ e_op.append('-i')
462
+
463
+ for i, c in enumerate(op):
464
+ if c == 'X':
465
+ e_op.append('Z')
466
+ circ.append(('-Y/2', i))
467
+ elif c == 'Y':
468
+ e_op.append('Z')
469
+ circ.append(('X/2', i))
470
+ elif c in ['I', 'Z', '0', '1']:
471
+ e_op.append(c)
472
+ else:
473
+ raise ValueError(f"Unknown operator {c}")
474
+ return ''.join(e_op), circ