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
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)})'