sinter 1.15.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.
Potentially problematic release.
This version of sinter might be problematic. Click here for more details.
- sinter/__init__.py +47 -0
- sinter/_collection/__init__.py +10 -0
- sinter/_collection/_collection.py +480 -0
- sinter/_collection/_collection_manager.py +581 -0
- sinter/_collection/_collection_manager_test.py +287 -0
- sinter/_collection/_collection_test.py +317 -0
- sinter/_collection/_collection_worker_loop.py +35 -0
- sinter/_collection/_collection_worker_state.py +259 -0
- sinter/_collection/_collection_worker_test.py +222 -0
- sinter/_collection/_mux_sampler.py +56 -0
- sinter/_collection/_printer.py +65 -0
- sinter/_collection/_sampler_ramp_throttled.py +66 -0
- sinter/_collection/_sampler_ramp_throttled_test.py +144 -0
- sinter/_command/__init__.py +0 -0
- sinter/_command/_main.py +39 -0
- sinter/_command/_main_collect.py +350 -0
- sinter/_command/_main_collect_test.py +482 -0
- sinter/_command/_main_combine.py +84 -0
- sinter/_command/_main_combine_test.py +153 -0
- sinter/_command/_main_plot.py +817 -0
- sinter/_command/_main_plot_test.py +445 -0
- sinter/_command/_main_predict.py +75 -0
- sinter/_command/_main_predict_test.py +36 -0
- sinter/_data/__init__.py +20 -0
- sinter/_data/_anon_task_stats.py +89 -0
- sinter/_data/_anon_task_stats_test.py +35 -0
- sinter/_data/_collection_options.py +106 -0
- sinter/_data/_collection_options_test.py +24 -0
- sinter/_data/_csv_out.py +74 -0
- sinter/_data/_existing_data.py +173 -0
- sinter/_data/_existing_data_test.py +41 -0
- sinter/_data/_task.py +311 -0
- sinter/_data/_task_stats.py +244 -0
- sinter/_data/_task_stats_test.py +140 -0
- sinter/_data/_task_test.py +38 -0
- sinter/_decoding/__init__.py +16 -0
- sinter/_decoding/_decoding.py +419 -0
- sinter/_decoding/_decoding_all_built_in_decoders.py +25 -0
- sinter/_decoding/_decoding_decoder_class.py +161 -0
- sinter/_decoding/_decoding_fusion_blossom.py +193 -0
- sinter/_decoding/_decoding_mwpf.py +302 -0
- sinter/_decoding/_decoding_pymatching.py +81 -0
- sinter/_decoding/_decoding_test.py +480 -0
- sinter/_decoding/_decoding_vacuous.py +38 -0
- sinter/_decoding/_perfectionist_sampler.py +38 -0
- sinter/_decoding/_sampler.py +72 -0
- sinter/_decoding/_stim_then_decode_sampler.py +222 -0
- sinter/_decoding/_stim_then_decode_sampler_test.py +192 -0
- sinter/_plotting.py +619 -0
- sinter/_plotting_test.py +108 -0
- sinter/_predict.py +381 -0
- sinter/_predict_test.py +227 -0
- sinter/_probability_util.py +519 -0
- sinter/_probability_util_test.py +281 -0
- sinter-1.15.0.data/data/README.md +332 -0
- sinter-1.15.0.data/data/readme_example_plot.png +0 -0
- sinter-1.15.0.data/data/requirements.txt +4 -0
- sinter-1.15.0.dist-info/METADATA +354 -0
- sinter-1.15.0.dist-info/RECORD +62 -0
- sinter-1.15.0.dist-info/WHEEL +5 -0
- sinter-1.15.0.dist-info/entry_points.txt +2 -0
- sinter-1.15.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import math
|
|
3
|
+
import pathlib
|
|
4
|
+
from typing import Any, Dict, Union, Callable, Sequence, TYPE_CHECKING, overload
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import sinter
|
|
11
|
+
|
|
12
|
+
# Go on a magical journey looking for scipy's linear regression type.
|
|
13
|
+
try:
|
|
14
|
+
from scipy.stats._stats_py import LinregressResult
|
|
15
|
+
except ImportError:
|
|
16
|
+
try:
|
|
17
|
+
from scipy.stats._stats_mstats_common import LinregressResult
|
|
18
|
+
except ImportError:
|
|
19
|
+
from scipy.stats import linregress
|
|
20
|
+
LinregressResult = type(linregress([0, 1], [0, 1]))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def log_binomial(*, p: Union[float, np.ndarray], n: int, hits: int) -> np.ndarray:
|
|
24
|
+
r"""Approximates the natural log of a binomial distribution's probability.
|
|
25
|
+
|
|
26
|
+
When working with large binomials, it's often necessary to work in log space
|
|
27
|
+
to represent the result. For example, suppose that out of two million
|
|
28
|
+
samples 200_000 are hits. The maximum likelihood estimate is p=0.2. Even if
|
|
29
|
+
this is the true probability, the chance of seeing *exactly* 20% hits out of
|
|
30
|
+
a million shots is roughly 10^-217322. Whereas the smallest representable
|
|
31
|
+
double is roughly 10^-324. But ln(10^-217322) ~= -500402.4 is representable.
|
|
32
|
+
|
|
33
|
+
This method evaluates $\ln(P(hits = B(n, p)))$, with all computations done
|
|
34
|
+
in log space to ensure intermediate values can be represented as floating
|
|
35
|
+
point numbers without underflowing to 0 or overflowing to infinity. This
|
|
36
|
+
method can be broadcast over multiple hypothesis probabilities by giving a
|
|
37
|
+
numpy array for `p` instead of a single float.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
p: The hypotehsis probability. The independent probability of a hit
|
|
41
|
+
occurring for each sample. This can also be an array of
|
|
42
|
+
probabilities, in which case the function is broadcast over the
|
|
43
|
+
array.
|
|
44
|
+
n: The number of samples that were taken.
|
|
45
|
+
hits: The number of hits that were observed amongst the samples that
|
|
46
|
+
were taken.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
$\ln(P(hits = B(n, p)))$
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
>>> import sinter
|
|
53
|
+
>>> sinter.log_binomial(p=0.5, n=100, hits=50)
|
|
54
|
+
array(-2.5308762, dtype=float32)
|
|
55
|
+
>>> sinter.log_binomial(p=0.2, n=1_000_000, hits=1_000)
|
|
56
|
+
array(-216626.97, dtype=float32)
|
|
57
|
+
>>> sinter.log_binomial(p=0.1, n=1_000_000, hits=1_000)
|
|
58
|
+
array(-99654.86, dtype=float32)
|
|
59
|
+
>>> sinter.log_binomial(p=0.01, n=1_000_000, hits=1_000)
|
|
60
|
+
array(-6742.573, dtype=float32)
|
|
61
|
+
>>> sinter.log_binomial(p=[0.01, 0.1, 0.2], n=1_000_000, hits=1_000)
|
|
62
|
+
array([ -6742.573, -99654.86 , -216626.97 ], dtype=float32)
|
|
63
|
+
"""
|
|
64
|
+
# Clamp probabilities into the valid [0, 1] range (in case float error put them outside it).
|
|
65
|
+
p_clipped = np.clip(p, 0, 1)
|
|
66
|
+
|
|
67
|
+
result = np.zeros(shape=p_clipped.shape, dtype=np.float32)
|
|
68
|
+
misses = n - hits
|
|
69
|
+
|
|
70
|
+
# Handle p=0 and p=1 cases separately, to avoid arithmetic warnings.
|
|
71
|
+
if hits:
|
|
72
|
+
result[p_clipped == 0] = -np.inf
|
|
73
|
+
if misses:
|
|
74
|
+
result[p_clipped == 1] = -np.inf
|
|
75
|
+
|
|
76
|
+
# Multiply p**hits and (1-p)**misses onto the total, in log space.
|
|
77
|
+
result[p_clipped != 0] += np.log(p_clipped[p_clipped != 0]) * float(hits)
|
|
78
|
+
result[p_clipped != 1] += np.log1p(-p_clipped[p_clipped != 1]) * float(misses)
|
|
79
|
+
|
|
80
|
+
# Multiply (n choose hits) onto the total, in log space.
|
|
81
|
+
log_n_choose_hits = log_factorial(n) - log_factorial(misses) - log_factorial(hits)
|
|
82
|
+
result += log_n_choose_hits
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def log_factorial(n: int) -> float:
|
|
88
|
+
r"""Approximates $\ln(n!)$; the natural logarithm of a factorial.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
n: The input to the factorial.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Evaluates $ln(n!)$ using `math.lgamma(n+1)`.
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
>>> import sinter
|
|
98
|
+
>>> sinter.log_factorial(0)
|
|
99
|
+
0.0
|
|
100
|
+
>>> sinter.log_factorial(1)
|
|
101
|
+
0.0
|
|
102
|
+
>>> sinter.log_factorial(2)
|
|
103
|
+
0.693147180559945
|
|
104
|
+
>>> sinter.log_factorial(100)
|
|
105
|
+
363.73937555556347
|
|
106
|
+
"""
|
|
107
|
+
return math.lgamma(n + 1)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def binary_search(*, func: Callable[[int], float], min_x: int, max_x: int, target: float) -> int:
|
|
111
|
+
"""Performs an approximate granular binary search over a monotonically ascending function."""
|
|
112
|
+
while max_x > min_x + 1:
|
|
113
|
+
med_x = (min_x + max_x) // 2
|
|
114
|
+
out = func(med_x)
|
|
115
|
+
if out < target:
|
|
116
|
+
min_x = med_x
|
|
117
|
+
elif out > target:
|
|
118
|
+
max_x = med_x
|
|
119
|
+
else:
|
|
120
|
+
return med_x
|
|
121
|
+
fmax = func(max_x)
|
|
122
|
+
fmin = func(min_x)
|
|
123
|
+
dmax = 0 if fmax == target else fmax - target
|
|
124
|
+
dmin = 0 if fmin == target else fmin - target
|
|
125
|
+
return max_x if abs(dmax) < abs(dmin) else min_x
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def binary_intercept(*, func: Callable[[float], float], start_x: float, step: float, target_y: float, atol: float) -> float:
|
|
129
|
+
"""Performs an approximate granular binary search over a monotonically ascending function."""
|
|
130
|
+
start_y = func(start_x)
|
|
131
|
+
if abs(start_y - target_y) <= atol:
|
|
132
|
+
return start_x
|
|
133
|
+
while (func(start_x + step) >= target_y) == (start_y >= target_y):
|
|
134
|
+
step *= 2
|
|
135
|
+
if np.isinf(step) or step == 0:
|
|
136
|
+
raise ValueError("Failed.")
|
|
137
|
+
xs = [start_x, start_x + step]
|
|
138
|
+
min_x = min(xs)
|
|
139
|
+
max_x = max(xs)
|
|
140
|
+
increasing = func(min_x) < func(max_x)
|
|
141
|
+
|
|
142
|
+
while True:
|
|
143
|
+
med_x = (min_x + max_x) / 2
|
|
144
|
+
med_y = func(med_x)
|
|
145
|
+
if abs(med_y - target_y) <= atol:
|
|
146
|
+
return med_x
|
|
147
|
+
assert med_x not in [min_x, max_x]
|
|
148
|
+
if (med_y < target_y) == increasing:
|
|
149
|
+
min_x = med_x
|
|
150
|
+
else:
|
|
151
|
+
max_x = med_x
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def least_squares_cost(*, xs: np.ndarray, ys: np.ndarray, intercept: float, slope: float) -> float:
|
|
155
|
+
assert len(xs.shape) == 1
|
|
156
|
+
assert xs.shape == ys.shape
|
|
157
|
+
return np.sum((intercept + slope*xs - ys)**2)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def least_squares_through_point(*, xs: np.ndarray, ys: np.ndarray, required_x: float, required_y: float) -> 'LinregressResult':
|
|
161
|
+
# Local import to reduce initial cost of importing sinter.
|
|
162
|
+
from scipy.optimize import leastsq
|
|
163
|
+
from scipy.stats import linregress
|
|
164
|
+
|
|
165
|
+
# HACK: get scipy's linear regression result type
|
|
166
|
+
LinregressResult = type(linregress([0, 1], [0, 1]))
|
|
167
|
+
|
|
168
|
+
xs2 = xs - required_x
|
|
169
|
+
ys2 = ys - required_y
|
|
170
|
+
|
|
171
|
+
def err(slope: float) -> float:
|
|
172
|
+
return least_squares_cost(xs=xs2, ys=ys2, intercept=0, slope=slope)
|
|
173
|
+
|
|
174
|
+
(best_slope,), _ = leastsq(func=err, x0=0.0)
|
|
175
|
+
intercept = required_y - required_x * best_slope
|
|
176
|
+
return LinregressResult(best_slope, intercept, None, None, None, intercept_stderr=False)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def least_squares_with_slope(*, xs: np.ndarray, ys: np.ndarray, required_slope: float) -> 'LinregressResult':
|
|
180
|
+
def err(intercept: float) -> float:
|
|
181
|
+
return least_squares_cost(xs=xs, ys=ys, intercept=intercept, slope=required_slope)
|
|
182
|
+
|
|
183
|
+
# Local import to reduce initial cost of importing sinter.
|
|
184
|
+
from scipy.optimize import leastsq
|
|
185
|
+
|
|
186
|
+
# HACK: get scipy's linear regression result type
|
|
187
|
+
from scipy.stats import linregress
|
|
188
|
+
LinregressResult = type(linregress([0, 1], [0, 1]))
|
|
189
|
+
|
|
190
|
+
(best_intercept,), _ = leastsq(func=err, x0=0.0)
|
|
191
|
+
return LinregressResult(required_slope, best_intercept, None, None, None, intercept_stderr=False)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclasses.dataclass(frozen=True)
|
|
195
|
+
class Fit:
|
|
196
|
+
"""The result of a fitting process.
|
|
197
|
+
|
|
198
|
+
Attributes:
|
|
199
|
+
low: The hypothesis with the smallest parameter whose cost or score was
|
|
200
|
+
still "close to" the cost of the best hypothesis. For example, this
|
|
201
|
+
could be a hypothesis whose squared error was within some tolerance
|
|
202
|
+
of the best fit's square error, or whose likelihood was within some
|
|
203
|
+
maximum Bayes factor of the max likelihood hypothesis.
|
|
204
|
+
best: The max likelihood hypothesis. The hypothesis that had the lowest
|
|
205
|
+
squared error, or the best fitting score.
|
|
206
|
+
high: The hypothesis with the larger parameter whose cost or score was
|
|
207
|
+
still "close to" the cost of the best hypothesis. For example, this
|
|
208
|
+
could be a hypothesis whose squared error was within some tolerance
|
|
209
|
+
of the best fit's square error, or whose likelihood was within some
|
|
210
|
+
maximum Bayes factor of the max likelihood hypothesis.
|
|
211
|
+
"""
|
|
212
|
+
low: Optional[float]
|
|
213
|
+
best: Optional[float]
|
|
214
|
+
high: Optional[float]
|
|
215
|
+
|
|
216
|
+
def __repr__(self) -> str:
|
|
217
|
+
return f'sinter.Fit(low={self.low!r}, best={self.best!r}, high={self.high!r})'
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def fit_line_y_at_x(*,
|
|
221
|
+
xs: Sequence[float],
|
|
222
|
+
ys: Sequence[float],
|
|
223
|
+
target_x: float,
|
|
224
|
+
max_extra_squared_error: float) -> 'sinter.Fit':
|
|
225
|
+
"""Performs a line fit, focusing on the line's y coord at a given x coord.
|
|
226
|
+
|
|
227
|
+
Finds the y value at the given x of the best fit, but also the minimum and
|
|
228
|
+
maximum values for y at the given x amongst all possible line fits whose
|
|
229
|
+
squared error cost is within the given `max_extra_squared_error` cost of the
|
|
230
|
+
best fit.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
xs: The x coordinates of points to fit.
|
|
234
|
+
ys: The y coordinates of points to fit.
|
|
235
|
+
target_x: The fit values are the value of y at this x coordinate.
|
|
236
|
+
max_extra_squared_error: When computing the low and high fits, this is
|
|
237
|
+
the maximum additional squared error that can be introduced by
|
|
238
|
+
varying the slope away from the best fit.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
A sinter.Fit containing the best fit for y at the given x, as well as
|
|
242
|
+
low and high fits that are as far as possible from the best fit while
|
|
243
|
+
respecting the given max_extra_squared_error.
|
|
244
|
+
|
|
245
|
+
Examples:
|
|
246
|
+
>>> import sinter
|
|
247
|
+
>>> sinter.fit_line_y_at_x(
|
|
248
|
+
... xs=[1, 2, 3],
|
|
249
|
+
... ys=[10, 12, 14],
|
|
250
|
+
... target_x=4,
|
|
251
|
+
... max_extra_squared_error=1,
|
|
252
|
+
... )
|
|
253
|
+
sinter.Fit(low=14.47247314453125, best=16.0, high=17.52752685546875)
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
# Local import to reduce initial cost of importing sinter.
|
|
257
|
+
from scipy.stats import linregress
|
|
258
|
+
|
|
259
|
+
xs = np.array(xs, dtype=np.float64)
|
|
260
|
+
ys = np.array(ys, dtype=np.float64)
|
|
261
|
+
fit = linregress(xs, ys)
|
|
262
|
+
base_cost = least_squares_cost(xs=xs, ys=ys, intercept=fit.intercept, slope=fit.slope)
|
|
263
|
+
base_y = float(fit.intercept + target_x * fit.slope)
|
|
264
|
+
|
|
265
|
+
def cost_for_y(y2: float) -> float:
|
|
266
|
+
fit2 = least_squares_through_point(xs=xs, ys=ys, required_x=target_x, required_y=y2)
|
|
267
|
+
return least_squares_cost(xs=xs, ys=ys, intercept=fit2.intercept, slope=fit2.slope)
|
|
268
|
+
|
|
269
|
+
low_y = binary_intercept(start_x=base_y, step=-1, target_y=base_cost + max_extra_squared_error, func=cost_for_y, atol=1e-5)
|
|
270
|
+
high_y = binary_intercept(start_x=base_y, step=1, target_y=base_cost + max_extra_squared_error, func=cost_for_y, atol=1e-5)
|
|
271
|
+
return Fit(low=low_y, best=base_y, high=high_y)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def fit_line_slope(*,
|
|
275
|
+
xs: Sequence[float],
|
|
276
|
+
ys: Sequence[float],
|
|
277
|
+
max_extra_squared_error: float) -> 'sinter.Fit':
|
|
278
|
+
"""Performs a line fit of the given points, focusing on the line's slope.
|
|
279
|
+
|
|
280
|
+
Finds the slope of the best fit, but also the minimum and maximum slopes
|
|
281
|
+
for line fits whose squared error cost is within the given
|
|
282
|
+
`max_extra_squared_error` cost of the best fit.
|
|
283
|
+
|
|
284
|
+
Note that the extra squared error is computed while including a specific
|
|
285
|
+
offset of some specific line. So the low/high estimates are for specific
|
|
286
|
+
lines, not for the general class of lines with a given slope, adding
|
|
287
|
+
together the contributions of all lines in that class.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
xs: The x coordinates of points to fit.
|
|
291
|
+
ys: The y coordinates of points to fit.
|
|
292
|
+
max_extra_squared_error: When computing the low and high fits, this is
|
|
293
|
+
the maximum additional squared error that can be introduced by
|
|
294
|
+
varying the slope away from the best fit.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
A sinter.Fit containing the best fit, as well as low and high fits that
|
|
298
|
+
are as far as possible from the best fit while respective the given
|
|
299
|
+
max_extra_squared_error.
|
|
300
|
+
|
|
301
|
+
Examples:
|
|
302
|
+
>>> import sinter
|
|
303
|
+
>>> sinter.fit_line_slope(
|
|
304
|
+
... xs=[1, 2, 3],
|
|
305
|
+
... ys=[10, 12, 14],
|
|
306
|
+
... max_extra_squared_error=1,
|
|
307
|
+
... )
|
|
308
|
+
sinter.Fit(low=1.2928924560546875, best=2.0, high=2.7071075439453125)
|
|
309
|
+
"""
|
|
310
|
+
# Local import to reduce initial cost of importing sinter.
|
|
311
|
+
from scipy.stats import linregress
|
|
312
|
+
|
|
313
|
+
xs = np.array(xs, dtype=np.float64)
|
|
314
|
+
ys = np.array(ys, dtype=np.float64)
|
|
315
|
+
fit = linregress(xs, ys)
|
|
316
|
+
base_cost = least_squares_cost(xs=xs, ys=ys, intercept=fit.intercept, slope=fit.slope)
|
|
317
|
+
|
|
318
|
+
def cost_for_slope(slope: float) -> float:
|
|
319
|
+
fit2 = least_squares_with_slope(xs=xs, ys=ys, required_slope=slope)
|
|
320
|
+
return least_squares_cost(xs=xs, ys=ys, intercept=fit2.intercept, slope=fit2.slope)
|
|
321
|
+
|
|
322
|
+
low_slope = binary_intercept(start_x=fit.slope, step=-1, target_y=base_cost + max_extra_squared_error, func=cost_for_slope, atol=1e-5)
|
|
323
|
+
high_slope = binary_intercept(start_x=fit.slope, step=1, target_y=base_cost + max_extra_squared_error, func=cost_for_slope, atol=1e-5)
|
|
324
|
+
return Fit(low=float(low_slope), best=float(fit.slope), high=float(high_slope))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def fit_binomial(
|
|
328
|
+
*,
|
|
329
|
+
num_shots: int,
|
|
330
|
+
num_hits: int,
|
|
331
|
+
max_likelihood_factor: float) -> 'sinter.Fit':
|
|
332
|
+
"""Determine hypothesis probabilities compatible with the given hit ratio.
|
|
333
|
+
|
|
334
|
+
The result includes the best fit (the max likelihood hypothis) as well as
|
|
335
|
+
the smallest and largest probabilities whose likelihood is within the given
|
|
336
|
+
factor of the maximum likelihood hypothesis.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
num_shots: The number of samples that were taken.
|
|
340
|
+
num_hits: The number of hits that were seen in the samples.
|
|
341
|
+
max_likelihood_factor: The maximum Bayes factor between the low/high
|
|
342
|
+
hypotheses and the best hypothesis (the max likelihood hypothesis).
|
|
343
|
+
This value should be larger than 1 (as opposed to between 0 and 1).
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
A `sinter.Fit` with the low, best, and high hypothesis probabilities.
|
|
347
|
+
|
|
348
|
+
Examples:
|
|
349
|
+
>>> import sinter
|
|
350
|
+
>>> sinter.fit_binomial(
|
|
351
|
+
... num_shots=100_000_000,
|
|
352
|
+
... num_hits=2,
|
|
353
|
+
... max_likelihood_factor=1000,
|
|
354
|
+
... )
|
|
355
|
+
sinter.Fit(low=2e-10, best=2e-08, high=1.259e-07)
|
|
356
|
+
>>> sinter.fit_binomial(
|
|
357
|
+
... num_shots=10,
|
|
358
|
+
... num_hits=5,
|
|
359
|
+
... max_likelihood_factor=9,
|
|
360
|
+
... )
|
|
361
|
+
sinter.Fit(low=0.202, best=0.5, high=0.798)
|
|
362
|
+
"""
|
|
363
|
+
if max_likelihood_factor < 1:
|
|
364
|
+
raise ValueError(f'max_likelihood_factor={max_likelihood_factor} < 1')
|
|
365
|
+
if num_shots == 0:
|
|
366
|
+
return Fit(low=0, high=1, best=0.5)
|
|
367
|
+
log_max_likelihood = log_binomial(p=num_hits / num_shots, n=num_shots, hits=num_hits)
|
|
368
|
+
target_log_likelihood = log_max_likelihood - math.log(max_likelihood_factor)
|
|
369
|
+
acc = 100
|
|
370
|
+
low = binary_search(
|
|
371
|
+
func=lambda exp_err: log_binomial(p=exp_err / (acc * num_shots), n=num_shots, hits=num_hits),
|
|
372
|
+
target=target_log_likelihood,
|
|
373
|
+
min_x=0,
|
|
374
|
+
max_x=num_hits * acc) / acc
|
|
375
|
+
high = binary_search(
|
|
376
|
+
func=lambda exp_err: -log_binomial(p=exp_err / (acc * num_shots), n=num_shots, hits=num_hits),
|
|
377
|
+
target=-target_log_likelihood,
|
|
378
|
+
min_x=num_hits * acc,
|
|
379
|
+
max_x=num_shots * acc) / acc
|
|
380
|
+
return Fit(best=num_hits / num_shots, low=low / num_shots, high=high / num_shots)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@overload
|
|
384
|
+
def shot_error_rate_to_piece_error_rate(shot_error_rate: float, *, pieces: float, values: float = 1) -> float:
|
|
385
|
+
pass
|
|
386
|
+
@overload
|
|
387
|
+
def shot_error_rate_to_piece_error_rate(shot_error_rate: 'sinter.Fit', *, pieces: float, values: float = 1) -> 'sinter.Fit':
|
|
388
|
+
pass
|
|
389
|
+
def shot_error_rate_to_piece_error_rate(shot_error_rate: Union[float, 'sinter.Fit'], *, pieces: float, values: float = 1) -> Union[float, 'sinter.Fit']:
|
|
390
|
+
"""Convert from total error rate to per-piece error rate.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
shot_error_rate: The rate at which shots fail. If this is set to a sinter.Fit,
|
|
394
|
+
the conversion broadcasts over the low,best,high of the fit.
|
|
395
|
+
pieces: The number of xor-pieces we want to subdivide each shot into,
|
|
396
|
+
as if each piece was an independent chance for the shot to fail and
|
|
397
|
+
the total chance of a shot failing was the xor of each piece
|
|
398
|
+
failing.
|
|
399
|
+
values: The number of or-pieces each shot's failure is being formed out
|
|
400
|
+
of.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Let N = `pieces` (number of rounds)
|
|
404
|
+
Let V = `values` (number of observables)
|
|
405
|
+
Let S = `shot_error_rate`
|
|
406
|
+
Let R = the returned result
|
|
407
|
+
|
|
408
|
+
R satisfies the following property. Let X be the probability of each
|
|
409
|
+
observable flipping, each round. R will be the probability that any of
|
|
410
|
+
the observables is flipped after 1 round, given this X. X is chosen to
|
|
411
|
+
satisfy the following condition. If a Bernoulli distribution with
|
|
412
|
+
probability X is sampled V*N times, and the results grouped into V
|
|
413
|
+
groups of N, and each group is reduced to a single value using XOR, and
|
|
414
|
+
then the reduced group values are reduced to a single final value using
|
|
415
|
+
OR, then this final value will be True with probability S.
|
|
416
|
+
|
|
417
|
+
Or, in other words, if a shot consists of N rounds which V independent
|
|
418
|
+
observables must survive, then R is like the per-round failure for
|
|
419
|
+
any of the observables.
|
|
420
|
+
|
|
421
|
+
Examples:
|
|
422
|
+
>>> import sinter
|
|
423
|
+
>>> sinter.shot_error_rate_to_piece_error_rate(
|
|
424
|
+
... shot_error_rate=0.1,
|
|
425
|
+
... pieces=2,
|
|
426
|
+
... )
|
|
427
|
+
0.05278640450004207
|
|
428
|
+
>>> sinter.shot_error_rate_to_piece_error_rate(
|
|
429
|
+
... shot_error_rate=0.05278640450004207,
|
|
430
|
+
... pieces=1 / 2,
|
|
431
|
+
... )
|
|
432
|
+
0.10000000000000003
|
|
433
|
+
>>> sinter.shot_error_rate_to_piece_error_rate(
|
|
434
|
+
... shot_error_rate=1e-9,
|
|
435
|
+
... pieces=100,
|
|
436
|
+
... )
|
|
437
|
+
1.000000082740371e-11
|
|
438
|
+
>>> sinter.shot_error_rate_to_piece_error_rate(
|
|
439
|
+
... shot_error_rate=0.6,
|
|
440
|
+
... pieces=10,
|
|
441
|
+
... values=2,
|
|
442
|
+
... )
|
|
443
|
+
0.12052311142021144
|
|
444
|
+
"""
|
|
445
|
+
|
|
446
|
+
if isinstance(shot_error_rate, Fit):
|
|
447
|
+
return Fit(
|
|
448
|
+
low=shot_error_rate_to_piece_error_rate(shot_error_rate=shot_error_rate.low, pieces=pieces, values=values),
|
|
449
|
+
best=shot_error_rate_to_piece_error_rate(shot_error_rate=shot_error_rate.best, pieces=pieces, values=values),
|
|
450
|
+
high=shot_error_rate_to_piece_error_rate(shot_error_rate=shot_error_rate.high, pieces=pieces, values=values),
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
if not (0 <= shot_error_rate <= 1):
|
|
454
|
+
raise ValueError(f'need (0 <= shot_error_rate={shot_error_rate} <= 1)')
|
|
455
|
+
if pieces <= 0:
|
|
456
|
+
raise ValueError('need pieces > 0')
|
|
457
|
+
if not isinstance(pieces, (int, float)):
|
|
458
|
+
raise ValueError('need isinstance(pieces, (int, float)')
|
|
459
|
+
if not isinstance(values, (int, float)):
|
|
460
|
+
raise ValueError('need isinstance(values, (int, float)')
|
|
461
|
+
if pieces == 1:
|
|
462
|
+
return shot_error_rate
|
|
463
|
+
if values != 1:
|
|
464
|
+
p = 1 - (1 - shot_error_rate)**(1 / values)
|
|
465
|
+
p = shot_error_rate_to_piece_error_rate(p, pieces=pieces)
|
|
466
|
+
return 1 - (1 - p)**values
|
|
467
|
+
|
|
468
|
+
if shot_error_rate > 0.5:
|
|
469
|
+
return 1 - shot_error_rate_to_piece_error_rate(1 - shot_error_rate, pieces=pieces)
|
|
470
|
+
assert 0 <= shot_error_rate <= 0.5
|
|
471
|
+
randomize_rate = 2*shot_error_rate
|
|
472
|
+
round_randomize_rate = 1 - (1 - randomize_rate)**(1 / pieces)
|
|
473
|
+
round_error_rate = round_randomize_rate / 2
|
|
474
|
+
|
|
475
|
+
if round_error_rate == 0:
|
|
476
|
+
# The intermediate numbers got too small. Fallback to division approximation.
|
|
477
|
+
return shot_error_rate / pieces
|
|
478
|
+
|
|
479
|
+
return round_error_rate
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def comma_separated_key_values(path: str) -> Dict[str, Any]:
|
|
483
|
+
"""Converts paths like 'folder/d=5,r=3.stim' into dicts like {'d':5,'r':3}.
|
|
484
|
+
|
|
485
|
+
On the command line, specifying `--metadata_func auto` results in this
|
|
486
|
+
method being used to extra metadata from the circuit file paths. Integers
|
|
487
|
+
and floats will be parsed into their values, instead of being stored as
|
|
488
|
+
strings.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
path: A file path where the name of the file has a series of terms like
|
|
492
|
+
'a=b' separated by commas and ending in '.stim'.
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
A dictionary from named keys to parsed values.
|
|
496
|
+
|
|
497
|
+
Examples:
|
|
498
|
+
>>> import sinter
|
|
499
|
+
>>> sinter.comma_separated_key_values("folder/d=5,r=3.5,x=abc.stim")
|
|
500
|
+
{'d': 5, 'r': 3.5, 'x': 'abc'}
|
|
501
|
+
"""
|
|
502
|
+
name = pathlib.Path(path).name
|
|
503
|
+
if '.' in name:
|
|
504
|
+
name = name[:name.rindex('.')]
|
|
505
|
+
result = {}
|
|
506
|
+
for term in name.split(','):
|
|
507
|
+
parts = term.split('=')
|
|
508
|
+
if len(parts) != 2:
|
|
509
|
+
raise ValueError(f"Expected a path with a filename containing comma-separated key=value terms like 'a=2,b=3.stim', but got {path!r}.")
|
|
510
|
+
k, v = parts
|
|
511
|
+
try:
|
|
512
|
+
v = int(v)
|
|
513
|
+
except ValueError:
|
|
514
|
+
try:
|
|
515
|
+
v = float(v)
|
|
516
|
+
except ValueError:
|
|
517
|
+
pass
|
|
518
|
+
result[k] = v
|
|
519
|
+
return result
|