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
sinter/_data/_task.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from typing import Any, Dict, Optional, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from sinter._data._collection_options import CollectionOptions
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
import sinter
|
|
15
|
+
import stim
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Task:
|
|
19
|
+
"""A decoding problem that sinter can sample from.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
circuit: The annotated noisy circuit to sample detection event data
|
|
23
|
+
and logical observable data form.
|
|
24
|
+
decoder: The decoder to use to predict the logical observable data
|
|
25
|
+
from the detection event data. This can be set to None if it
|
|
26
|
+
will be specified later (e.g. by the call to `collect`).
|
|
27
|
+
detector_error_model: Specifies the error model to give to the decoder.
|
|
28
|
+
Defaults to None, indicating that it should be automatically derived
|
|
29
|
+
using `stim.Circuit.detector_error_model`.
|
|
30
|
+
postselection_mask: Defaults to None (unused). A bit packed bitmask
|
|
31
|
+
identifying detectors that must not fire. Shots where the
|
|
32
|
+
indicated detectors fire are discarded.
|
|
33
|
+
postselected_observables_mask: Defaults to None (unused). A bit
|
|
34
|
+
packed bitmask identifying observable indices to postselect on.
|
|
35
|
+
Anytime the decoder's predicted flip for one of these
|
|
36
|
+
observables doesn't agree with the actual measured flip value of
|
|
37
|
+
the observable, the shot is discarded instead of counting as an
|
|
38
|
+
error.
|
|
39
|
+
json_metadata: Defaults to None. Custom additional data describing
|
|
40
|
+
the problem. Must be JSON serializable. For example, this could
|
|
41
|
+
be a dictionary with "physical_error_rate" and "code_distance"
|
|
42
|
+
keys.
|
|
43
|
+
collection_options: Specifies custom options for collecting this
|
|
44
|
+
single task. These options are merged with the global options
|
|
45
|
+
to determine what happens.
|
|
46
|
+
|
|
47
|
+
For example, if a task has `collection_options` set to
|
|
48
|
+
`sinter.CollectionOptions(max_shots=1000, max_errors=100)` and
|
|
49
|
+
`sinter.collect` was called with `max_shots=500` and
|
|
50
|
+
`max_errors=200`, then either 500 shots or 100 errors will be
|
|
51
|
+
collected for the task (whichever comes first).
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
>>> import sinter
|
|
55
|
+
>>> import stim
|
|
56
|
+
>>> task = sinter.Task(
|
|
57
|
+
... circuit=stim.Circuit.generated(
|
|
58
|
+
... 'repetition_code:memory',
|
|
59
|
+
... rounds=10,
|
|
60
|
+
... distance=10,
|
|
61
|
+
... before_round_data_depolarization=1e-3,
|
|
62
|
+
... ),
|
|
63
|
+
... )
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
*,
|
|
69
|
+
circuit: Optional['stim.Circuit'] = None,
|
|
70
|
+
decoder: Optional[str] = None,
|
|
71
|
+
detector_error_model: Optional['stim.DetectorErrorModel'] = None,
|
|
72
|
+
postselection_mask: Optional[np.ndarray] = None,
|
|
73
|
+
postselected_observables_mask: Optional[np.ndarray] = None,
|
|
74
|
+
json_metadata: Any = None,
|
|
75
|
+
collection_options: 'sinter.CollectionOptions' = CollectionOptions(),
|
|
76
|
+
skip_validation: bool = False,
|
|
77
|
+
circuit_path: Optional[Union[str, pathlib.Path]] = None,
|
|
78
|
+
_unvalidated_strong_id: Optional[str] = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Args:
|
|
82
|
+
circuit: The annotated noisy circuit to sample detection event data
|
|
83
|
+
and logical observable data form.
|
|
84
|
+
decoder: The decoder to use to predict the logical observable data
|
|
85
|
+
from the detection event data. This can be set to None if it
|
|
86
|
+
will be specified later (e.g. by the call to `collect`).
|
|
87
|
+
detector_error_model: Specifies the error model to give to the decoder.
|
|
88
|
+
Defaults to None, indicating that it should be automatically derived
|
|
89
|
+
using `stim.Circuit.detector_error_model`.
|
|
90
|
+
postselection_mask: Defaults to None (unused). A bit packed bitmask
|
|
91
|
+
identifying detectors that must not fire. Shots where the
|
|
92
|
+
indicated detectors fire are discarded.
|
|
93
|
+
postselected_observables_mask: Defaults to None (unused). A bit
|
|
94
|
+
packed bitmask identifying observable indices to postselect on.
|
|
95
|
+
Anytime the decoder's predicted flip for one of these
|
|
96
|
+
observables doesn't agree with the actual measured flip value of
|
|
97
|
+
the observable, the shot is discarded instead of counting as an
|
|
98
|
+
error.
|
|
99
|
+
json_metadata: Defaults to None. Custom additional data describing
|
|
100
|
+
the problem. Must be JSON serializable. For example, this could
|
|
101
|
+
be a dictionary with "physical_error_rate" and "code_distance"
|
|
102
|
+
keys.
|
|
103
|
+
collection_options: Specifies custom options for collecting this
|
|
104
|
+
single task. These options are merged with the global options
|
|
105
|
+
to determine what happens.
|
|
106
|
+
|
|
107
|
+
For example, if a task has `collection_options` set to
|
|
108
|
+
`sinter.CollectionOptions(max_shots=1000, max_errors=100)` and
|
|
109
|
+
`sinter.collect` was called with `max_shots=500` and
|
|
110
|
+
`max_errors=200`, then either 500 shots or 100 errors will be
|
|
111
|
+
collected for the task (whichever comes first).
|
|
112
|
+
skip_validation: Defaults to False. Normally the arguments given to
|
|
113
|
+
this method are checked for consistency (e.g. the detector error
|
|
114
|
+
model should have the same number of detectors as the circuit).
|
|
115
|
+
Setting this argument to True will skip doing the consistency
|
|
116
|
+
checks. Note that this can result in confusing errors later, if
|
|
117
|
+
the arguments are not actually consistent.
|
|
118
|
+
circuit_path: Typically set to None. If the circuit isn't specified,
|
|
119
|
+
this is the filepath to read it from. Not included in the strong
|
|
120
|
+
id.
|
|
121
|
+
_unvalidated_strong_id: Must be set to None unless `skip_validation`
|
|
122
|
+
is set to True. Otherwise, if this is specified then it should
|
|
123
|
+
be equal to the value returned by self.strong_id().
|
|
124
|
+
"""
|
|
125
|
+
if not skip_validation:
|
|
126
|
+
if circuit_path is None and circuit is None:
|
|
127
|
+
raise ValueError('circuit_path is None and circuit is None')
|
|
128
|
+
if _unvalidated_strong_id is not None:
|
|
129
|
+
raise ValueError("_unvalidated_strong_id is not None and not skip_validation")
|
|
130
|
+
dem = detector_error_model
|
|
131
|
+
if circuit is not None:
|
|
132
|
+
num_dets = circuit.num_detectors
|
|
133
|
+
num_obs = circuit.num_observables
|
|
134
|
+
if dem is not None:
|
|
135
|
+
if circuit.num_detectors != dem.num_detectors:
|
|
136
|
+
raise ValueError(f"circuit.num_detectors={num_dets!r} != detector_error_model.num_detectors={dem.num_detectors!r}")
|
|
137
|
+
if num_obs != dem.num_observables:
|
|
138
|
+
raise ValueError(f"circuit.num_observables={num_obs!r} != detector_error_model.num_observables={dem.num_observables!r}")
|
|
139
|
+
if postselection_mask is not None:
|
|
140
|
+
shape = (math.ceil(num_dets / 8),)
|
|
141
|
+
if postselection_mask.shape != shape:
|
|
142
|
+
raise ValueError(f"postselection_mask.shape={postselection_mask.shape!r} != (math.ceil(circuit.num_detectors / 8),)={shape!r}")
|
|
143
|
+
if postselected_observables_mask is not None:
|
|
144
|
+
shape = (math.ceil(num_obs / 8),)
|
|
145
|
+
if postselected_observables_mask.shape != shape:
|
|
146
|
+
raise ValueError(f"postselected_observables_mask.shape={postselected_observables_mask.shape!r} != (math.ceil(circuit.num_observables / 8),)={shape!r}")
|
|
147
|
+
if postselection_mask is not None:
|
|
148
|
+
if not isinstance(postselection_mask, np.ndarray):
|
|
149
|
+
raise ValueError(f"not isinstance(postselection_mask={postselection_mask!r}, np.ndarray)")
|
|
150
|
+
if postselection_mask.dtype != np.uint8:
|
|
151
|
+
raise ValueError(f"postselection_mask.dtype={postselection_mask.dtype!r} != np.uint8")
|
|
152
|
+
if postselected_observables_mask is not None:
|
|
153
|
+
if not isinstance(postselected_observables_mask, np.ndarray):
|
|
154
|
+
raise ValueError(f"not isinstance(postselected_observables_mask={postselected_observables_mask!r}, np.ndarray)")
|
|
155
|
+
if postselected_observables_mask.dtype != np.uint8:
|
|
156
|
+
raise ValueError(f"postselected_observables_mask.dtype={postselected_observables_mask.dtype!r} != np.uint8")
|
|
157
|
+
self.circuit_path = None if circuit_path is None else pathlib.Path(circuit_path)
|
|
158
|
+
self.circuit = circuit
|
|
159
|
+
self.decoder = decoder
|
|
160
|
+
self.detector_error_model = detector_error_model
|
|
161
|
+
self.postselection_mask = postselection_mask
|
|
162
|
+
self.postselected_observables_mask = postselected_observables_mask
|
|
163
|
+
self.json_metadata = json_metadata
|
|
164
|
+
self.collection_options = collection_options
|
|
165
|
+
self._unvalidated_strong_id = _unvalidated_strong_id
|
|
166
|
+
|
|
167
|
+
def strong_id_value(self) -> Dict[str, Any]:
|
|
168
|
+
"""Contains all raw values that affect the strong id.
|
|
169
|
+
|
|
170
|
+
This value is converted into the actual strong id by:
|
|
171
|
+
- Serializing it into text using JSON.
|
|
172
|
+
- Serializing the JSON text into bytes using UTF8.
|
|
173
|
+
- Hashing the UTF8 bytes using SHA256.
|
|
174
|
+
|
|
175
|
+
Examples:
|
|
176
|
+
>>> import sinter
|
|
177
|
+
>>> import stim
|
|
178
|
+
>>> task = sinter.Task(
|
|
179
|
+
... circuit=stim.Circuit('H 0'),
|
|
180
|
+
... detector_error_model=stim.DetectorErrorModel(),
|
|
181
|
+
... decoder='pymatching',
|
|
182
|
+
... )
|
|
183
|
+
>>> task.strong_id_value()
|
|
184
|
+
{'circuit': 'H 0', 'decoder': 'pymatching', 'decoder_error_model': '', 'postselection_mask': None, 'json_metadata': None}
|
|
185
|
+
"""
|
|
186
|
+
if self.circuit is None:
|
|
187
|
+
raise ValueError("Can't compute strong_id until `circuit` is set.")
|
|
188
|
+
if self.decoder is None:
|
|
189
|
+
raise ValueError("Can't compute strong_id until `decoder` is set.")
|
|
190
|
+
if self.detector_error_model is None:
|
|
191
|
+
raise ValueError("Can't compute strong_id until `detector_error_model` is set.")
|
|
192
|
+
result = {
|
|
193
|
+
'circuit': str(self.circuit),
|
|
194
|
+
'decoder': self.decoder,
|
|
195
|
+
'decoder_error_model': str(self.detector_error_model),
|
|
196
|
+
'postselection_mask':
|
|
197
|
+
None
|
|
198
|
+
if self.postselection_mask is None
|
|
199
|
+
else [int(e) for e in self.postselection_mask],
|
|
200
|
+
'json_metadata': self.json_metadata,
|
|
201
|
+
}
|
|
202
|
+
if self.postselected_observables_mask is not None:
|
|
203
|
+
result['postselected_observables_mask'] = [int(e) for e in self.postselected_observables_mask]
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
def strong_id_text(self) -> str:
|
|
207
|
+
"""The text that is serialized and hashed to get the strong id.
|
|
208
|
+
|
|
209
|
+
This value is converted into the actual strong id by:
|
|
210
|
+
- Serializing into bytes using UTF8.
|
|
211
|
+
- Hashing the UTF8 bytes using SHA256.
|
|
212
|
+
|
|
213
|
+
Examples:
|
|
214
|
+
>>> import sinter
|
|
215
|
+
>>> import stim
|
|
216
|
+
>>> task = sinter.Task(
|
|
217
|
+
... circuit=stim.Circuit('H 0'),
|
|
218
|
+
... detector_error_model=stim.DetectorErrorModel(),
|
|
219
|
+
... decoder='pymatching',
|
|
220
|
+
... )
|
|
221
|
+
>>> task.strong_id_text()
|
|
222
|
+
'{"circuit": "H 0", "decoder": "pymatching", "decoder_error_model": "", "postselection_mask": null, "json_metadata": null}'
|
|
223
|
+
"""
|
|
224
|
+
return json.dumps(self.strong_id_value())
|
|
225
|
+
|
|
226
|
+
def strong_id_bytes(self) -> bytes:
|
|
227
|
+
"""The bytes that are hashed to get the strong id.
|
|
228
|
+
|
|
229
|
+
This value is converted into the actual strong id by:
|
|
230
|
+
- Hashing these bytes using SHA256.
|
|
231
|
+
|
|
232
|
+
Examples:
|
|
233
|
+
>>> import sinter
|
|
234
|
+
>>> import stim
|
|
235
|
+
>>> task = sinter.Task(
|
|
236
|
+
... circuit=stim.Circuit('H 0'),
|
|
237
|
+
... detector_error_model=stim.DetectorErrorModel(),
|
|
238
|
+
... decoder='pymatching',
|
|
239
|
+
... )
|
|
240
|
+
>>> task.strong_id_bytes()
|
|
241
|
+
b'{"circuit": "H 0", "decoder": "pymatching", "decoder_error_model": "", "postselection_mask": null, "json_metadata": null}'
|
|
242
|
+
"""
|
|
243
|
+
return self.strong_id_text().encode('utf8')
|
|
244
|
+
|
|
245
|
+
def _recomputed_strong_id(self) -> str:
|
|
246
|
+
return hashlib.sha256(self.strong_id_bytes()).hexdigest()
|
|
247
|
+
|
|
248
|
+
def strong_id(self) -> str:
|
|
249
|
+
"""Computes a cryptographically unique identifier for this task.
|
|
250
|
+
|
|
251
|
+
This value is affected by:
|
|
252
|
+
- The exact circuit.
|
|
253
|
+
- The exact detector error model.
|
|
254
|
+
- The decoder.
|
|
255
|
+
- The json metadata.
|
|
256
|
+
- The postselection mask.
|
|
257
|
+
|
|
258
|
+
Examples:
|
|
259
|
+
>>> import sinter
|
|
260
|
+
>>> import stim
|
|
261
|
+
>>> task = sinter.Task(
|
|
262
|
+
... circuit=stim.Circuit(),
|
|
263
|
+
... detector_error_model=stim.DetectorErrorModel(),
|
|
264
|
+
... decoder='pymatching',
|
|
265
|
+
... )
|
|
266
|
+
>>> task.strong_id()
|
|
267
|
+
'7424ea021693d4abc1c31c12e655a48779f61a7c2969e457ae4fe400c852bee5'
|
|
268
|
+
"""
|
|
269
|
+
if self._unvalidated_strong_id is None:
|
|
270
|
+
self._unvalidated_strong_id = self._recomputed_strong_id()
|
|
271
|
+
return self._unvalidated_strong_id
|
|
272
|
+
|
|
273
|
+
def __repr__(self) -> str:
|
|
274
|
+
terms = []
|
|
275
|
+
if self.circuit is not None:
|
|
276
|
+
terms.append(f'circuit={self.circuit!r}')
|
|
277
|
+
if self.decoder is not None:
|
|
278
|
+
terms.append(f'decoder={self.decoder!r}')
|
|
279
|
+
if self.detector_error_model is not None:
|
|
280
|
+
terms.append(f'detector_error_model={self.detector_error_model!r}')
|
|
281
|
+
if self.postselection_mask is not None:
|
|
282
|
+
nd = self.circuit.num_detectors
|
|
283
|
+
bits = list(np.unpackbits(self.postselection_mask, count=nd, bitorder='little'))
|
|
284
|
+
terms.append(f'''postselection_mask=np.packbits({bits!r}, bitorder='little')''')
|
|
285
|
+
if self.postselected_observables_mask is not None:
|
|
286
|
+
no = self.circuit.num_observables
|
|
287
|
+
bits = list(np.unpackbits(self.postselected_observables_mask, count=no, bitorder='little'))
|
|
288
|
+
terms.append(f'''postselected_observables_mask=np.packbits({bits!r}, bitorder='little')''')
|
|
289
|
+
if self.json_metadata is not None:
|
|
290
|
+
terms.append(f'json_metadata={self.json_metadata!r}')
|
|
291
|
+
if self.collection_options != CollectionOptions():
|
|
292
|
+
terms.append(f'collection_options={self.collection_options!r}')
|
|
293
|
+
if self.circuit_path is not None:
|
|
294
|
+
terms.append(f'circuit_path={self.circuit_path!r}')
|
|
295
|
+
return f'sinter.Task({", ".join(terms)})'
|
|
296
|
+
|
|
297
|
+
def __eq__(self, other: Any) -> bool:
|
|
298
|
+
if not isinstance(other, Task):
|
|
299
|
+
return NotImplemented
|
|
300
|
+
if self._unvalidated_strong_id is not None and other._unvalidated_strong_id is not None:
|
|
301
|
+
return self._unvalidated_strong_id == other._unvalidated_strong_id
|
|
302
|
+
return (
|
|
303
|
+
self.circuit_path == other.circuit_path and
|
|
304
|
+
self.circuit == other.circuit and
|
|
305
|
+
self.decoder == other.decoder and
|
|
306
|
+
self.detector_error_model == other.detector_error_model and
|
|
307
|
+
np.array_equal(self.postselection_mask, other.postselection_mask) and
|
|
308
|
+
np.array_equal(self.postselected_observables_mask, other.postselected_observables_mask) and
|
|
309
|
+
self.json_metadata == other.json_metadata and
|
|
310
|
+
self.collection_options == other.collection_options
|
|
311
|
+
)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import dataclasses
|
|
3
|
+
from typing import Counter, List, Any
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from typing import Union
|
|
6
|
+
from typing import overload
|
|
7
|
+
|
|
8
|
+
from sinter._data._anon_task_stats import AnonTaskStats
|
|
9
|
+
from sinter._data._csv_out import csv_line
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _is_equal_json_values(json1: Any, json2: Any):
|
|
13
|
+
if json1 == json2:
|
|
14
|
+
return True
|
|
15
|
+
|
|
16
|
+
if type(json1) == type(json2):
|
|
17
|
+
if isinstance(json1, dict):
|
|
18
|
+
return json1.keys() == json2.keys() and all(_is_equal_json_values(json1[k], json2[k]) for k in json1.keys())
|
|
19
|
+
elif isinstance(json1, (list, tuple)):
|
|
20
|
+
return len(json1) == len(json2) and all(_is_equal_json_values(a, b) for a, b in zip(json1, json2))
|
|
21
|
+
elif isinstance(json1, (list, tuple)) and isinstance(json2, (list, tuple)):
|
|
22
|
+
return _is_equal_json_values(tuple(json1), tuple(json2))
|
|
23
|
+
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclasses.dataclass(frozen=True)
|
|
28
|
+
class TaskStats:
|
|
29
|
+
"""Statistics sampled from a task.
|
|
30
|
+
|
|
31
|
+
The rows in the CSV files produced by sinter correspond to instances of
|
|
32
|
+
`sinter.TaskStats`. For example, a row can be produced by printing a
|
|
33
|
+
`sinter.TaskStats`.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
strong_id: The cryptographically unique identifier of the task, from
|
|
37
|
+
`sinter.Task.strong_id()`.
|
|
38
|
+
decoder: The name of the decoder that was used to decode the task.
|
|
39
|
+
Errors are counted when this decoder made a wrong prediction.
|
|
40
|
+
json_metadata: A JSON-encodable value (such as a dictionary from strings
|
|
41
|
+
to integers) that were included with the task in order to describe
|
|
42
|
+
what the task was. This value can be a huge variety of things, but
|
|
43
|
+
typically it will be a dictionary with fields such as 'd' for the
|
|
44
|
+
code distance.
|
|
45
|
+
shots: Number of times the task was sampled.
|
|
46
|
+
errors: Number of times a sample resulted in an error.
|
|
47
|
+
discards: Number of times a sample resulted in a discard. Note that
|
|
48
|
+
discarded a task is not an error.
|
|
49
|
+
seconds: The amount of CPU core time spent sampling the tasks, in
|
|
50
|
+
seconds.
|
|
51
|
+
custom_counts: A counter mapping string keys to integer values. Used for
|
|
52
|
+
tracking arbitrary values, such as per-observable error counts or
|
|
53
|
+
the number of times detectors fired. The meaning of the information
|
|
54
|
+
in the counts is not specified; the only requirement is that it
|
|
55
|
+
should be correct to add each key's counts when merging statistics.
|
|
56
|
+
|
|
57
|
+
Although this field is an editable object, it's invalid to edit the
|
|
58
|
+
counter after the stats object is initialized.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# Information describing the problem that was sampled.
|
|
62
|
+
strong_id: str
|
|
63
|
+
decoder: str
|
|
64
|
+
json_metadata: Any
|
|
65
|
+
|
|
66
|
+
# Information describing the results of sampling.
|
|
67
|
+
shots: int = 0
|
|
68
|
+
errors: int = 0
|
|
69
|
+
discards: int = 0
|
|
70
|
+
seconds: float = 0
|
|
71
|
+
custom_counts: Counter[str] = dataclasses.field(default_factory=collections.Counter)
|
|
72
|
+
|
|
73
|
+
def __post_init__(self):
|
|
74
|
+
assert isinstance(self.errors, int)
|
|
75
|
+
assert isinstance(self.shots, int)
|
|
76
|
+
assert isinstance(self.discards, int)
|
|
77
|
+
assert isinstance(self.seconds, (int, float))
|
|
78
|
+
assert isinstance(self.custom_counts, collections.Counter)
|
|
79
|
+
assert isinstance(self.decoder, str)
|
|
80
|
+
assert isinstance(self.strong_id, str)
|
|
81
|
+
assert self.json_metadata is None or isinstance(self.json_metadata, (int, float, str, dict, list, tuple))
|
|
82
|
+
assert self.errors >= 0
|
|
83
|
+
assert self.discards >= 0
|
|
84
|
+
assert self.seconds >= 0
|
|
85
|
+
assert self.shots >= self.errors + self.discards
|
|
86
|
+
assert all(isinstance(k, str) and isinstance(v, int) for k, v in self.custom_counts.items())
|
|
87
|
+
|
|
88
|
+
def with_edits(
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
strong_id: Optional[str] = None,
|
|
92
|
+
decoder: Optional[str] = None,
|
|
93
|
+
json_metadata: Optional[Any] = None,
|
|
94
|
+
shots: Optional[int] = None,
|
|
95
|
+
errors: Optional[int] = None,
|
|
96
|
+
discards: Optional[int] = None,
|
|
97
|
+
seconds: Optional[float] = None,
|
|
98
|
+
custom_counts: Optional[Counter[str]] = None,
|
|
99
|
+
) -> 'TaskStats':
|
|
100
|
+
return TaskStats(
|
|
101
|
+
strong_id=self.strong_id if strong_id is None else strong_id,
|
|
102
|
+
decoder=self.decoder if decoder is None else decoder,
|
|
103
|
+
json_metadata=self.json_metadata if json_metadata is None else json_metadata,
|
|
104
|
+
shots=self.shots if shots is None else shots,
|
|
105
|
+
errors=self.errors if errors is None else errors,
|
|
106
|
+
discards=self.discards if discards is None else discards,
|
|
107
|
+
seconds=self.seconds if seconds is None else seconds,
|
|
108
|
+
custom_counts=self.custom_counts if custom_counts is None else custom_counts,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@overload
|
|
112
|
+
def __add__(self, other: AnonTaskStats) -> AnonTaskStats:
|
|
113
|
+
pass
|
|
114
|
+
@overload
|
|
115
|
+
def __add__(self, other: 'TaskStats') -> 'TaskStats':
|
|
116
|
+
pass
|
|
117
|
+
def __add__(self, other: Union[AnonTaskStats, 'TaskStats']) -> Union[AnonTaskStats, 'TaskStats']:
|
|
118
|
+
if isinstance(other, AnonTaskStats):
|
|
119
|
+
return self.to_anon_stats() + other
|
|
120
|
+
|
|
121
|
+
if isinstance(other, TaskStats):
|
|
122
|
+
if self.strong_id != other.strong_id:
|
|
123
|
+
raise ValueError(f'{self.strong_id=} != {other.strong_id=}')
|
|
124
|
+
if not _is_equal_json_values(self.json_metadata, other.json_metadata) or self.decoder != other.decoder:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"A stat had the same strong id as another, but their other identifying information (json_metadata, decoder) differed.\n"
|
|
127
|
+
"The strong id is supposed to be a cryptographic hash that uniquely identifies what was sampled, so this is an error.\n"
|
|
128
|
+
"\n"
|
|
129
|
+
"This failure can occur when post-processing data (e.g. combining X basis stats and Z basis stats into synthetic both-basis stats).\n"
|
|
130
|
+
"To fix it, ensure any post-processing sets the strong id of the synthetic data in some cryptographically secure way.\n"
|
|
131
|
+
"\n"
|
|
132
|
+
"In some cases this can be caused by attempting to add a value that has gone through JSON serialization+parsing to one\n"
|
|
133
|
+
"that hasn't, which causes things like tuples transforming into lists.\n"
|
|
134
|
+
"\n"
|
|
135
|
+
f"The two stats:\n1. {self!r}\n2. {other!r}")
|
|
136
|
+
|
|
137
|
+
total = self.to_anon_stats() + other.to_anon_stats()
|
|
138
|
+
return TaskStats(
|
|
139
|
+
decoder=self.decoder,
|
|
140
|
+
strong_id=self.strong_id,
|
|
141
|
+
json_metadata=self.json_metadata,
|
|
142
|
+
shots=total.shots,
|
|
143
|
+
errors=total.errors,
|
|
144
|
+
discards=total.discards,
|
|
145
|
+
seconds=total.seconds,
|
|
146
|
+
custom_counts=total.custom_counts,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return NotImplemented
|
|
150
|
+
__radd__ = __add__
|
|
151
|
+
|
|
152
|
+
def to_anon_stats(self) -> AnonTaskStats:
|
|
153
|
+
"""Returns a `sinter.AnonTaskStats` with the same statistics.
|
|
154
|
+
|
|
155
|
+
Examples:
|
|
156
|
+
>>> import sinter
|
|
157
|
+
>>> stat = sinter.TaskStats(
|
|
158
|
+
... strong_id='test',
|
|
159
|
+
... json_metadata={'a': [1, 2, 3]},
|
|
160
|
+
... decoder='pymatching',
|
|
161
|
+
... shots=22,
|
|
162
|
+
... errors=3,
|
|
163
|
+
... discards=4,
|
|
164
|
+
... seconds=5,
|
|
165
|
+
... )
|
|
166
|
+
>>> stat.to_anon_stats()
|
|
167
|
+
sinter.AnonTaskStats(shots=22, errors=3, discards=4, seconds=5)
|
|
168
|
+
"""
|
|
169
|
+
return AnonTaskStats(
|
|
170
|
+
shots=self.shots,
|
|
171
|
+
errors=self.errors,
|
|
172
|
+
discards=self.discards,
|
|
173
|
+
seconds=self.seconds,
|
|
174
|
+
custom_counts=self.custom_counts.copy(),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def to_csv_line(self) -> str:
|
|
178
|
+
"""Converts into a line that can be printed into a CSV file.
|
|
179
|
+
|
|
180
|
+
Examples:
|
|
181
|
+
>>> import sinter
|
|
182
|
+
>>> stat = sinter.TaskStats(
|
|
183
|
+
... strong_id='test',
|
|
184
|
+
... json_metadata={'a': [1, 2, 3]},
|
|
185
|
+
... decoder='pymatching',
|
|
186
|
+
... shots=22,
|
|
187
|
+
... errors=3,
|
|
188
|
+
... seconds=5,
|
|
189
|
+
... )
|
|
190
|
+
>>> print(sinter.CSV_HEADER)
|
|
191
|
+
shots, errors, discards, seconds,decoder,strong_id,json_metadata,custom_counts
|
|
192
|
+
>>> print(stat.to_csv_line())
|
|
193
|
+
22, 3, 0, 5,pymatching,test,"{""a"":[1,2,3]}",
|
|
194
|
+
"""
|
|
195
|
+
return csv_line(
|
|
196
|
+
shots=self.shots,
|
|
197
|
+
errors=self.errors,
|
|
198
|
+
seconds=self.seconds,
|
|
199
|
+
discards=self.discards,
|
|
200
|
+
strong_id=self.strong_id,
|
|
201
|
+
decoder=self.decoder,
|
|
202
|
+
json_metadata=self.json_metadata,
|
|
203
|
+
custom_counts=self.custom_counts,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _split_custom_counts(self, custom_keys: List[str]) -> List['TaskStats']:
|
|
207
|
+
result = []
|
|
208
|
+
for k in custom_keys:
|
|
209
|
+
m = self.json_metadata
|
|
210
|
+
if isinstance(m, dict):
|
|
211
|
+
m = dict(m)
|
|
212
|
+
m.setdefault('custom_error_count_key', k)
|
|
213
|
+
m.setdefault('original_error_count', self.errors)
|
|
214
|
+
result.append(TaskStats(
|
|
215
|
+
strong_id=f'{self.strong_id}:{k}',
|
|
216
|
+
decoder=self.decoder,
|
|
217
|
+
json_metadata=m,
|
|
218
|
+
shots=self.shots,
|
|
219
|
+
errors=self.custom_counts[k],
|
|
220
|
+
discards=self.discards,
|
|
221
|
+
seconds=self.seconds,
|
|
222
|
+
custom_counts=self.custom_counts,
|
|
223
|
+
))
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
def __str__(self) -> str:
|
|
227
|
+
return self.to_csv_line()
|
|
228
|
+
|
|
229
|
+
def __repr__(self) -> str:
|
|
230
|
+
terms = []
|
|
231
|
+
terms.append(f'strong_id={self.strong_id!r}')
|
|
232
|
+
terms.append(f'decoder={self.decoder!r}')
|
|
233
|
+
terms.append(f'json_metadata={self.json_metadata!r}')
|
|
234
|
+
if self.shots:
|
|
235
|
+
terms.append(f'shots={self.shots!r}')
|
|
236
|
+
if self.errors:
|
|
237
|
+
terms.append(f'errors={self.errors!r}')
|
|
238
|
+
if self.discards:
|
|
239
|
+
terms.append(f'discards={self.discards!r}')
|
|
240
|
+
if self.seconds:
|
|
241
|
+
terms.append(f'seconds={self.seconds!r}')
|
|
242
|
+
if self.custom_counts:
|
|
243
|
+
terms.append(f'custom_counts={self.custom_counts!r}')
|
|
244
|
+
return f'sinter.TaskStats({", ".join(terms)})'
|