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.

Files changed (62) hide show
  1. sinter/__init__.py +47 -0
  2. sinter/_collection/__init__.py +10 -0
  3. sinter/_collection/_collection.py +480 -0
  4. sinter/_collection/_collection_manager.py +581 -0
  5. sinter/_collection/_collection_manager_test.py +287 -0
  6. sinter/_collection/_collection_test.py +317 -0
  7. sinter/_collection/_collection_worker_loop.py +35 -0
  8. sinter/_collection/_collection_worker_state.py +259 -0
  9. sinter/_collection/_collection_worker_test.py +222 -0
  10. sinter/_collection/_mux_sampler.py +56 -0
  11. sinter/_collection/_printer.py +65 -0
  12. sinter/_collection/_sampler_ramp_throttled.py +66 -0
  13. sinter/_collection/_sampler_ramp_throttled_test.py +144 -0
  14. sinter/_command/__init__.py +0 -0
  15. sinter/_command/_main.py +39 -0
  16. sinter/_command/_main_collect.py +350 -0
  17. sinter/_command/_main_collect_test.py +482 -0
  18. sinter/_command/_main_combine.py +84 -0
  19. sinter/_command/_main_combine_test.py +153 -0
  20. sinter/_command/_main_plot.py +817 -0
  21. sinter/_command/_main_plot_test.py +445 -0
  22. sinter/_command/_main_predict.py +75 -0
  23. sinter/_command/_main_predict_test.py +36 -0
  24. sinter/_data/__init__.py +20 -0
  25. sinter/_data/_anon_task_stats.py +89 -0
  26. sinter/_data/_anon_task_stats_test.py +35 -0
  27. sinter/_data/_collection_options.py +106 -0
  28. sinter/_data/_collection_options_test.py +24 -0
  29. sinter/_data/_csv_out.py +74 -0
  30. sinter/_data/_existing_data.py +173 -0
  31. sinter/_data/_existing_data_test.py +41 -0
  32. sinter/_data/_task.py +311 -0
  33. sinter/_data/_task_stats.py +244 -0
  34. sinter/_data/_task_stats_test.py +140 -0
  35. sinter/_data/_task_test.py +38 -0
  36. sinter/_decoding/__init__.py +16 -0
  37. sinter/_decoding/_decoding.py +419 -0
  38. sinter/_decoding/_decoding_all_built_in_decoders.py +25 -0
  39. sinter/_decoding/_decoding_decoder_class.py +161 -0
  40. sinter/_decoding/_decoding_fusion_blossom.py +193 -0
  41. sinter/_decoding/_decoding_mwpf.py +302 -0
  42. sinter/_decoding/_decoding_pymatching.py +81 -0
  43. sinter/_decoding/_decoding_test.py +480 -0
  44. sinter/_decoding/_decoding_vacuous.py +38 -0
  45. sinter/_decoding/_perfectionist_sampler.py +38 -0
  46. sinter/_decoding/_sampler.py +72 -0
  47. sinter/_decoding/_stim_then_decode_sampler.py +222 -0
  48. sinter/_decoding/_stim_then_decode_sampler_test.py +192 -0
  49. sinter/_plotting.py +619 -0
  50. sinter/_plotting_test.py +108 -0
  51. sinter/_predict.py +381 -0
  52. sinter/_predict_test.py +227 -0
  53. sinter/_probability_util.py +519 -0
  54. sinter/_probability_util_test.py +281 -0
  55. sinter-1.15.0.data/data/README.md +332 -0
  56. sinter-1.15.0.data/data/readme_example_plot.png +0 -0
  57. sinter-1.15.0.data/data/requirements.txt +4 -0
  58. sinter-1.15.0.dist-info/METADATA +354 -0
  59. sinter-1.15.0.dist-info/RECORD +62 -0
  60. sinter-1.15.0.dist-info/WHEEL +5 -0
  61. sinter-1.15.0.dist-info/entry_points.txt +2 -0
  62. 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