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,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)
|
sinter/_data/_csv_out.py
ADDED
|
@@ -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
|
+
]
|