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,35 @@
1
+ import collections
2
+
3
+ import sinter
4
+
5
+
6
+ def test_repr():
7
+ v = sinter.AnonTaskStats(shots=22, errors=3, discards=4, seconds=5)
8
+ assert eval(repr(v), {"sinter": sinter}) == v
9
+ v = sinter.AnonTaskStats()
10
+ assert eval(repr(v), {"sinter": sinter}) == v
11
+ v = sinter.AnonTaskStats(shots=22)
12
+ assert eval(repr(v), {"sinter": sinter}) == v
13
+ v = sinter.AnonTaskStats(shots=21, errors=4)
14
+ assert eval(repr(v), {"sinter": sinter}) == v
15
+ v = sinter.AnonTaskStats(shots=21, discards=4)
16
+ assert eval(repr(v), {"sinter": sinter}) == v
17
+ v = sinter.AnonTaskStats(seconds=4)
18
+ assert eval(repr(v), {"sinter": sinter}) == v
19
+
20
+
21
+ def test_add():
22
+ a0 = sinter.AnonTaskStats(shots=220, errors=30, discards=40, seconds=50)
23
+ b0 = sinter.AnonTaskStats(shots=50, errors=4, discards=3, seconds=2)
24
+ assert a0 + b0 == sinter.AnonTaskStats(shots=270, errors=34, discards=43, seconds=52)
25
+ assert a0 + sinter.AnonTaskStats() == a0
26
+
27
+ a = sinter.AnonTaskStats(shots=220, errors=30, discards=40, seconds=50, custom_counts=collections.Counter({'a': 10, 'b': 20}))
28
+ b = sinter.AnonTaskStats(shots=50, errors=4, discards=3, seconds=2, custom_counts=collections.Counter({'a': 1, 'c': 3}))
29
+ assert a + b == sinter.AnonTaskStats(shots=270, errors=34, discards=43, seconds=52, custom_counts=collections.Counter({'a': 11, 'b': 20, 'c': 3}))
30
+
31
+ assert a + sinter.AnonTaskStats() == a
32
+ assert sinter.AnonTaskStats() + b == b
33
+
34
+ assert a + b0 == sinter.AnonTaskStats(shots=270, errors=34, discards=43, seconds=52, custom_counts=collections.Counter({'a': 10, 'b': 20}))
35
+ assert a0 + b == sinter.AnonTaskStats(shots=270, errors=34, discards=43, seconds=52, custom_counts=collections.Counter({'a': 1, 'c': 3}))
@@ -0,0 +1,106 @@
1
+ import dataclasses
2
+ from typing import Optional, TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ import sinter
6
+
7
+
8
+ @dataclasses.dataclass(frozen=True)
9
+ class CollectionOptions:
10
+ """Describes options for how data is collected for a decoding problem.
11
+
12
+ Attributes:
13
+ max_shots: Defaults to None (unused). Stops the sampling process
14
+ after this many samples have been taken from the circuit.
15
+ max_errors: Defaults to None (unused). Stops the sampling process
16
+ after this many errors have been seen in samples taken from the
17
+ circuit. The actual number sampled errors may be larger due to
18
+ batching.
19
+ start_batch_size: Defaults to None (collector's choice). The very
20
+ first shots taken from the circuit will use a batch of this
21
+ size, and no other batches will be taken in parallel. Once this
22
+ initial fact finding batch is done, batches can be taken in
23
+ parallel and the normal batch size limiting processes take over.
24
+ max_batch_size: Defaults to None (unused). Limits batches from
25
+ taking more than this many shots at once. For example, this can
26
+ be used to ensure memory usage stays below some limit.
27
+ max_batch_seconds: Defaults to None (unused). When set, the recorded
28
+ data from previous shots is used to estimate how much time is
29
+ taken per shot. This information is then used to predict the
30
+ biggest batch size that can finish in under the given number of
31
+ seconds. Limits each batch to be no larger than that.
32
+ """
33
+
34
+ max_shots: Optional[int] = None
35
+ max_errors: Optional[int] = None
36
+ start_batch_size: Optional[int] = None
37
+ max_batch_size: Optional[int] = None
38
+ max_batch_seconds: Optional[float] = None
39
+
40
+ def __post_init__(self):
41
+ if self.max_shots is not None and self.max_shots < 0:
42
+ raise ValueError(f'max_shots is not None and max_shots={self.max_shots} < 0')
43
+ if self.max_errors is not None and self.max_errors < 0:
44
+ raise ValueError(f'max_errors is not None and max_errors={self.max_errors} < 0')
45
+ if self.start_batch_size is not None and self.start_batch_size <= 0:
46
+ raise ValueError(f'start_batch_size is not None and start_batch_size={self.start_batch_size} <= 0')
47
+ if self.max_batch_size is not None and self.max_batch_size <= 0:
48
+ raise ValueError(
49
+ f'max_batch_size={self.max_batch_size} is not None and max_batch_size <= 0')
50
+ if self.max_batch_seconds is not None and self.max_batch_seconds <= 0:
51
+ raise ValueError(
52
+ f'max_batch_seconds={self.max_batch_seconds} is not None and max_batch_seconds <= 0')
53
+
54
+ def __repr__(self) -> str:
55
+ terms = []
56
+ if self.max_shots is not None:
57
+ terms.append(f'max_shots={self.max_shots!r}')
58
+ if self.max_errors is not None:
59
+ terms.append(f'max_errors={self.max_errors!r}')
60
+ if self.start_batch_size is not None:
61
+ terms.append(f'start_batch_size={self.start_batch_size!r}')
62
+ if self.max_batch_size is not None:
63
+ terms.append(f'max_batch_size={self.max_batch_size!r}')
64
+ if self.max_batch_seconds is not None:
65
+ terms.append(f'max_batch_seconds={self.max_batch_seconds!r}')
66
+ return f'sinter.CollectionOptions({", ".join(terms)})'
67
+
68
+ def combine(self, other: 'sinter.CollectionOptions') -> 'sinter.CollectionOptions':
69
+ """Returns a combination of multiple collection options.
70
+
71
+ All fields are combined by taking the minimum from both collection
72
+ options objects, with None treated as being infinitely large.
73
+
74
+ Args:
75
+ other: The collections options to combine with.
76
+
77
+ Returns:
78
+ The combined collection options.
79
+
80
+ Examples:
81
+ >>> import sinter
82
+ >>> a = sinter.CollectionOptions(
83
+ ... max_shots=1_000_000,
84
+ ... start_batch_size=100,
85
+ ... )
86
+ >>> b = sinter.CollectionOptions(
87
+ ... max_shots=100_000,
88
+ ... max_errors=100,
89
+ ... )
90
+ >>> a.combine(b)
91
+ sinter.CollectionOptions(max_shots=100000, max_errors=100, start_batch_size=100)
92
+ """
93
+ return CollectionOptions(
94
+ max_shots=nullable_min(self.max_shots, other.max_shots),
95
+ max_errors=nullable_min(self.max_errors, other.max_errors),
96
+ start_batch_size=nullable_min(self.start_batch_size, other.start_batch_size),
97
+ max_batch_size=nullable_min(self.max_batch_size, other.max_batch_size),
98
+ max_batch_seconds=nullable_min(self.max_batch_seconds, other.max_batch_seconds))
99
+
100
+
101
+ def nullable_min(a: Optional[int], b: Optional[int]) -> Optional[int]:
102
+ if a is None:
103
+ return b
104
+ if b is None:
105
+ return a
106
+ return min(a, b)
@@ -0,0 +1,24 @@
1
+ import sinter
2
+
3
+
4
+ def test_repr():
5
+ v = sinter.CollectionOptions()
6
+ assert eval(repr(v), {"sinter": sinter}) == v
7
+ v = sinter.CollectionOptions(max_shots=100)
8
+ assert eval(repr(v), {"sinter": sinter}) == v
9
+ v = sinter.CollectionOptions(max_errors=100)
10
+ assert eval(repr(v), {"sinter": sinter}) == v
11
+ v = sinter.CollectionOptions(start_batch_size=10)
12
+ assert eval(repr(v), {"sinter": sinter}) == v
13
+ v = sinter.CollectionOptions(max_batch_size=100)
14
+ assert eval(repr(v), {"sinter": sinter}) == v
15
+ v = sinter.CollectionOptions(max_batch_seconds=30)
16
+ assert eval(repr(v), {"sinter": sinter}) == v
17
+ v = sinter.CollectionOptions(max_shots=100, max_errors=90, start_batch_size=80, max_batch_size=200, max_batch_seconds=30)
18
+ assert eval(repr(v), {"sinter": sinter}) == v
19
+
20
+
21
+ def test_combine():
22
+ a = sinter.CollectionOptions(max_shots=200, max_batch_seconds=300)
23
+ b = sinter.CollectionOptions(max_errors=100, max_batch_seconds=400)
24
+ assert a.combine(b) == sinter.CollectionOptions(max_errors=100, max_shots=200, max_batch_seconds=300)
@@ -0,0 +1,74 @@
1
+ import collections
2
+ import csv
3
+ import io
4
+ import json
5
+ from typing import Any, Optional
6
+
7
+
8
+ def escape_csv(text: Any, width: Optional[int]) -> str:
9
+ output = io.StringIO()
10
+ csv.writer(output).writerow([text])
11
+ text = output.getvalue().strip()
12
+ if width is not None:
13
+ text = text.rjust(width)
14
+ return text
15
+
16
+
17
+ def csv_line(*,
18
+ shots: Any,
19
+ errors: Any,
20
+ discards: Any,
21
+ seconds: Any,
22
+ decoder: Any,
23
+ strong_id: Any,
24
+ json_metadata: Any,
25
+ custom_counts: Any,
26
+ is_header: bool = False) -> str:
27
+ if isinstance(seconds, float):
28
+ if seconds < 1:
29
+ seconds = f'{seconds:0.3f}'
30
+ elif seconds < 10:
31
+ seconds = f'{seconds:0.2f}'
32
+ else:
33
+ seconds = f'{seconds:0.1f}'
34
+ if not is_header:
35
+ json_metadata = json.dumps(json_metadata,
36
+ separators=(',', ':'),
37
+ sort_keys=True)
38
+ if custom_counts:
39
+ custom_counts = escape_csv(
40
+ json.dumps(custom_counts,
41
+ separators=(',', ':'),
42
+ sort_keys=True), None)
43
+ else:
44
+ custom_counts = ''
45
+
46
+
47
+ shots = escape_csv(shots, 10)
48
+ if isinstance(errors, (dict, collections.Counter)):
49
+ errors = json.dumps(errors, separators=(',', ':'), sort_keys=True)
50
+ errors = escape_csv(errors, 10)
51
+ discards = escape_csv(discards, 10)
52
+ seconds = escape_csv(seconds, 8)
53
+ decoder = escape_csv(decoder, None)
54
+ strong_id = escape_csv(strong_id, None)
55
+ json_metadata = escape_csv(json_metadata, None)
56
+ return (f'{shots},'
57
+ f'{errors},'
58
+ f'{discards},'
59
+ f'{seconds},'
60
+ f'{decoder},'
61
+ f'{strong_id},'
62
+ f'{json_metadata},'
63
+ f'{custom_counts}')
64
+
65
+
66
+ CSV_HEADER = csv_line(shots='shots',
67
+ errors='errors',
68
+ discards='discards',
69
+ seconds='seconds',
70
+ strong_id='strong_id',
71
+ decoder='decoder',
72
+ json_metadata='json_metadata',
73
+ custom_counts='custom_counts',
74
+ is_header=True)
@@ -0,0 +1,173 @@
1
+ import collections
2
+ import json
3
+ import pathlib
4
+ from typing import Any, Dict, List, TYPE_CHECKING
5
+
6
+ from sinter._data._task_stats import TaskStats
7
+ from sinter._data._task import Task
8
+ from sinter._data._anon_task_stats import AnonTaskStats
9
+
10
+ if TYPE_CHECKING:
11
+ import sinter
12
+
13
+
14
+ class ExistingData:
15
+ def __init__(self):
16
+ self.data: Dict[str, TaskStats] = {}
17
+
18
+ def stats_for(self, case: Task) -> AnonTaskStats:
19
+ if isinstance(case, Task):
20
+ key = case.strong_id()
21
+ else:
22
+ raise NotImplementedError(f'{type(case)}')
23
+ if key not in self.data:
24
+ return AnonTaskStats()
25
+ return self.data[key].to_anon_stats()
26
+
27
+ def add_sample(self, sample: TaskStats) -> None:
28
+ k = sample.strong_id
29
+ current = self.data.get(k)
30
+ if current is not None:
31
+ self.data[k] = current + sample
32
+ else:
33
+ self.data[k] = sample
34
+
35
+ def __iadd__(self, other: 'ExistingData') -> 'ExistingData':
36
+ for sample in other.data.values():
37
+ self.add_sample(sample)
38
+ return self
39
+
40
+ @staticmethod
41
+ def from_file(path_or_file: Any) -> 'ExistingData':
42
+ expected_fields = {
43
+ "shots",
44
+ "discards",
45
+ "errors",
46
+ "seconds",
47
+ "strong_id",
48
+ "decoder",
49
+ "json_metadata",
50
+ }
51
+ # Import is done locally to reduce cost of importing sinter.
52
+ import csv
53
+ if isinstance(path_or_file, (str, pathlib.Path)):
54
+ with open(path_or_file) as csvfile:
55
+ return ExistingData.from_file(csvfile)
56
+ reader = csv.DictReader(path_or_file)
57
+ reader.fieldnames = [e.strip() for e in reader.fieldnames]
58
+ actual_fields = set(reader.fieldnames)
59
+ if not (expected_fields <= actual_fields):
60
+ raise ValueError(
61
+ f"Bad CSV data. "
62
+ f"Got columns {sorted(actual_fields)!r} "
63
+ f"but expected columns {sorted(expected_fields)!r}")
64
+ has_custom_counts = 'custom_counts' in actual_fields
65
+ result = ExistingData()
66
+ for row in reader:
67
+ if has_custom_counts:
68
+ custom_counts = row['custom_counts']
69
+ if custom_counts is None or custom_counts == '':
70
+ custom_counts = collections.Counter()
71
+ else:
72
+ custom_counts = json.loads(custom_counts)
73
+ if not isinstance(custom_counts, dict) or not all(isinstance(k, str) or not isinstance(v, int) for k, v in custom_counts.items()):
74
+ raise ValueError(f"{row['custom_counts']=} isn't empty or a dictionary from string keys to integer values.")
75
+ custom_counts = collections.Counter(custom_counts)
76
+ else:
77
+ custom_counts = collections.Counter()
78
+ result.add_sample(TaskStats(
79
+ shots=int(row['shots']),
80
+ discards=int(row['discards']),
81
+ errors=int(row['errors']),
82
+ custom_counts=custom_counts,
83
+ seconds=float(row['seconds']),
84
+ strong_id=row['strong_id'],
85
+ decoder=row['decoder'],
86
+ json_metadata=json.loads(row['json_metadata']),
87
+ ))
88
+ return result
89
+
90
+
91
+ def stats_from_csv_files(*paths_or_files: Any) -> List['sinter.TaskStats']:
92
+ """Reads and aggregates shot statistics from CSV files.
93
+
94
+ (An old alias of `read_stats_from_csv_files`, kept around for backwards
95
+ compatibility.)
96
+
97
+ Assumes the CSV file was written by printing `sinter.CSV_HEADER` and then
98
+ a list of `sinter.TaskStats`. When statistics from the same task appear
99
+ in multiple files (identified by the strong id being the same), the
100
+ statistics for that task are folded together (so only the total shots,
101
+ total errors, etc for each task are included in the results).
102
+
103
+ Args:
104
+ *paths_or_files: Each argument should be either a path (in the form of
105
+ a string or a pathlib.Path) or a TextIO object (e.g. as returned by
106
+ `open`). File data is read from each argument.
107
+
108
+ Returns:
109
+ A list of task stats, where each task appears only once in the list and
110
+ the stats associated with it are the totals aggregated from all files.
111
+
112
+ Examples:
113
+ >>> import sinter
114
+ >>> import io
115
+ >>> in_memory_file = io.StringIO()
116
+ >>> _ = in_memory_file.write('''
117
+ ... shots,errors,discards,seconds,decoder,strong_id,json_metadata
118
+ ... 1000,42,0,0.125,pymatching,9c31908e2b,"{""d"":9}"
119
+ ... 3000,24,0,0.125,pymatching,9c31908e2b,"{""d"":9}"
120
+ ... 1000,250,0,0.125,pymatching,deadbeef08,"{""d"":7}"
121
+ ... '''.strip())
122
+ >>> _ = in_memory_file.seek(0)
123
+ >>> stats = sinter.stats_from_csv_files(in_memory_file)
124
+ >>> for stat in stats:
125
+ ... print(repr(stat))
126
+ sinter.TaskStats(strong_id='9c31908e2b', decoder='pymatching', json_metadata={'d': 9}, shots=4000, errors=66, seconds=0.25)
127
+ sinter.TaskStats(strong_id='deadbeef08', decoder='pymatching', json_metadata={'d': 7}, shots=1000, errors=250, seconds=0.125)
128
+ """
129
+ result = ExistingData()
130
+ for p in paths_or_files:
131
+ result += ExistingData.from_file(p)
132
+ return list(result.data.values())
133
+
134
+
135
+ def read_stats_from_csv_files(*paths_or_files: Any) -> List['sinter.TaskStats']:
136
+ """Reads and aggregates shot statistics from CSV files.
137
+
138
+ Assumes the CSV file was written by printing `sinter.CSV_HEADER` and then
139
+ a list of `sinter.TaskStats`. When statistics from the same task appear
140
+ in multiple files (identified by the strong id being the same), the
141
+ statistics for that task are folded together (so only the total shots,
142
+ total errors, etc for each task are included in the results).
143
+
144
+ Args:
145
+ *paths_or_files: Each argument should be either a path (in the form of
146
+ a string or a pathlib.Path) or a TextIO object (e.g. as returned by
147
+ `open`). File data is read from each argument.
148
+
149
+ Returns:
150
+ A list of task stats, where each task appears only once in the list and
151
+ the stats associated with it are the totals aggregated from all files.
152
+
153
+ Examples:
154
+ >>> import sinter
155
+ >>> import io
156
+ >>> in_memory_file = io.StringIO()
157
+ >>> _ = in_memory_file.write('''
158
+ ... shots,errors,discards,seconds,decoder,strong_id,json_metadata
159
+ ... 1000,42,0,0.125,pymatching,9c31908e2b,"{""d"":9}"
160
+ ... 3000,24,0,0.125,pymatching,9c31908e2b,"{""d"":9}"
161
+ ... 1000,250,0,0.125,pymatching,deadbeef08,"{""d"":7}"
162
+ ... '''.strip())
163
+ >>> _ = in_memory_file.seek(0)
164
+ >>> stats = sinter.read_stats_from_csv_files(in_memory_file)
165
+ >>> for stat in stats:
166
+ ... print(repr(stat))
167
+ sinter.TaskStats(strong_id='9c31908e2b', decoder='pymatching', json_metadata={'d': 9}, shots=4000, errors=66, seconds=0.25)
168
+ sinter.TaskStats(strong_id='deadbeef08', decoder='pymatching', json_metadata={'d': 7}, shots=1000, errors=250, seconds=0.125)
169
+ """
170
+ result = ExistingData()
171
+ for p in paths_or_files:
172
+ result += ExistingData.from_file(p)
173
+ return list(result.data.values())
@@ -0,0 +1,41 @@
1
+ import collections
2
+ import pathlib
3
+ import tempfile
4
+
5
+ import sinter
6
+
7
+
8
+ def test_read_stats_from_csv_files():
9
+ with tempfile.TemporaryDirectory() as d:
10
+ d = pathlib.Path(d)
11
+
12
+ with open(d / 'tmp.csv', 'w') as f:
13
+ print("""
14
+ shots,errors,discards,seconds,decoder,strong_id,json_metadata
15
+ 300, 1, 20, 1.0,pymatching,abc123,"{""d"":3}"
16
+ 1000, 3, 40, 3.0,pymatching,abc123,"{""d"":3}"
17
+ 2000, 0, 10, 2.0,pymatching,def456,"{""d"":5}"
18
+ """.strip(), file=f)
19
+
20
+ assert sinter.read_stats_from_csv_files(d / 'tmp.csv') == [
21
+ sinter.TaskStats(strong_id='abc123', decoder='pymatching', json_metadata={'d': 3}, shots=1300, errors=4, discards=60, seconds=4.0),
22
+ sinter.TaskStats(strong_id='def456', decoder='pymatching', json_metadata={'d': 5}, shots=2000, errors=0, discards=10, seconds=2.0),
23
+ ]
24
+
25
+ with open(d / 'tmp2.csv', 'w') as f:
26
+ print("""
27
+ shots,errors,discards,seconds,decoder,strong_id,json_metadata,custom_counts
28
+ 300, 1, 20, 1.0,pymatching,abc123,"{""d"":3}","{""dets"":1234}"
29
+ 1000, 3, 40, 3.0,pymatching,abc123,"{""d"":3}",
30
+ 2000, 0, 10, 2.0,pymatching,def456,"{""d"":5}"
31
+ """.strip(), file=f)
32
+
33
+ assert sinter.read_stats_from_csv_files(d / 'tmp2.csv') == [
34
+ sinter.TaskStats(strong_id='abc123', decoder='pymatching', json_metadata={'d': 3}, shots=1300, errors=4, discards=60, seconds=4.0, custom_counts=collections.Counter({'dets': 1234})),
35
+ sinter.TaskStats(strong_id='def456', decoder='pymatching', json_metadata={'d': 5}, shots=2000, errors=0, discards=10, seconds=2.0),
36
+ ]
37
+
38
+ assert sinter.read_stats_from_csv_files(d / 'tmp.csv', d / 'tmp2.csv') == [
39
+ sinter.TaskStats(strong_id='abc123', decoder='pymatching', json_metadata={'d': 3}, shots=2600, errors=8, discards=120, seconds=8.0, custom_counts=collections.Counter({'dets': 1234})),
40
+ sinter.TaskStats(strong_id='def456', decoder='pymatching', json_metadata={'d': 5}, shots=4000, errors=0, discards=20, seconds=4.0),
41
+ ]