qcodes 0.1.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.
Files changed (128) hide show
  1. qcodes/__init__.py +65 -0
  2. qcodes/actions.py +207 -0
  3. qcodes/config/__init__.py +2 -0
  4. qcodes/config/config.py +396 -0
  5. qcodes/config/qcodesrc.json +11 -0
  6. qcodes/config/qcodesrc_schema.json +55 -0
  7. qcodes/data/__init__.py +0 -0
  8. qcodes/data/data_array.py +520 -0
  9. qcodes/data/data_set.py +803 -0
  10. qcodes/data/format.py +309 -0
  11. qcodes/data/gnuplot_format.py +366 -0
  12. qcodes/data/hdf5_format.py +385 -0
  13. qcodes/data/io.py +266 -0
  14. qcodes/data/location.py +166 -0
  15. qcodes/data/manager.py +163 -0
  16. qcodes/instrument/__init__.py +0 -0
  17. qcodes/instrument/base.py +630 -0
  18. qcodes/instrument/function.py +134 -0
  19. qcodes/instrument/ip.py +236 -0
  20. qcodes/instrument/metaclass.py +45 -0
  21. qcodes/instrument/mock.py +278 -0
  22. qcodes/instrument/parameter.py +1001 -0
  23. qcodes/instrument/remote.py +529 -0
  24. qcodes/instrument/server.py +189 -0
  25. qcodes/instrument/sweep_values.py +286 -0
  26. qcodes/instrument/visa.py +198 -0
  27. qcodes/instrument_drivers/AlazarTech/ATS.py +1001 -0
  28. qcodes/instrument_drivers/AlazarTech/ATS9870.py +266 -0
  29. qcodes/instrument_drivers/AlazarTech/ATS_acquisition_controllers.py +143 -0
  30. qcodes/instrument_drivers/AlazarTech/__init__.py +0 -0
  31. qcodes/instrument_drivers/QuTech/IVVI.py +390 -0
  32. qcodes/instrument_drivers/QuTech/__init__.py +0 -0
  33. qcodes/instrument_drivers/__init__.py +0 -0
  34. qcodes/instrument_drivers/agilent/Agilent_34400A.py +155 -0
  35. qcodes/instrument_drivers/agilent/E8527D.py +79 -0
  36. qcodes/instrument_drivers/agilent/HP33210A.py +104 -0
  37. qcodes/instrument_drivers/agilent/__init__.py +0 -0
  38. qcodes/instrument_drivers/agilent/test_suite.py +96 -0
  39. qcodes/instrument_drivers/ithaco/Ithaco_1211.py +110 -0
  40. qcodes/instrument_drivers/ithaco/__init__.py +0 -0
  41. qcodes/instrument_drivers/oxford/__init__.py +0 -0
  42. qcodes/instrument_drivers/oxford/mercuryiPS.py +358 -0
  43. qcodes/instrument_drivers/oxford/triton.py +227 -0
  44. qcodes/instrument_drivers/rigol/DG4000.py +591 -0
  45. qcodes/instrument_drivers/rigol/__init__.py +0 -0
  46. qcodes/instrument_drivers/rohde_schwarz/SGS100A.py +99 -0
  47. qcodes/instrument_drivers/rohde_schwarz/ZNB20.py +154 -0
  48. qcodes/instrument_drivers/rohde_schwarz/__init__.py +0 -0
  49. qcodes/instrument_drivers/signal_hound/USB_SA124B.py +567 -0
  50. qcodes/instrument_drivers/signal_hound/__init__.py +0 -0
  51. qcodes/instrument_drivers/stanford_research/SR560.py +116 -0
  52. qcodes/instrument_drivers/stanford_research/SR830.py +314 -0
  53. qcodes/instrument_drivers/stanford_research/SR865.py +305 -0
  54. qcodes/instrument_drivers/stanford_research/__init__.py +0 -0
  55. qcodes/instrument_drivers/tektronix/AWG5014.py +1228 -0
  56. qcodes/instrument_drivers/tektronix/AWG520.py +659 -0
  57. qcodes/instrument_drivers/tektronix/Keithley_2000.py +195 -0
  58. qcodes/instrument_drivers/tektronix/Keithley_2600.py +83 -0
  59. qcodes/instrument_drivers/tektronix/Keithley_2700.py +395 -0
  60. qcodes/instrument_drivers/tektronix/__init__.py +0 -0
  61. qcodes/instrument_drivers/test.py +94 -0
  62. qcodes/instrument_drivers/weinschel/Weinschel_8320.py +21 -0
  63. qcodes/instrument_drivers/weinschel/__init__.py +0 -0
  64. qcodes/instrument_drivers/weinschel/test_suite.py +27 -0
  65. qcodes/loops.py +1009 -0
  66. qcodes/measure.py +155 -0
  67. qcodes/plots/__init__.py +0 -0
  68. qcodes/plots/base.py +280 -0
  69. qcodes/plots/colors.py +135 -0
  70. qcodes/plots/pyqtgraph.py +384 -0
  71. qcodes/plots/qcmatplotlib.py +206 -0
  72. qcodes/process/__init__.py +0 -0
  73. qcodes/process/helpers.py +60 -0
  74. qcodes/process/qcodes_process.py +72 -0
  75. qcodes/process/server.py +395 -0
  76. qcodes/process/stream_queue.py +152 -0
  77. qcodes/station.py +178 -0
  78. qcodes/test.py +119 -0
  79. qcodes/tests/__init__.py +0 -0
  80. qcodes/tests/common.py +7 -0
  81. qcodes/tests/data_mocks.py +173 -0
  82. qcodes/tests/instrument_mocks.py +327 -0
  83. qcodes/tests/py35_syntax.py +13 -0
  84. qcodes/tests/test_combined_par.py +156 -0
  85. qcodes/tests/test_command.py +120 -0
  86. qcodes/tests/test_config.py +222 -0
  87. qcodes/tests/test_data.py +668 -0
  88. qcodes/tests/test_deferred_operations.py +105 -0
  89. qcodes/tests/test_driver_testcase.py +58 -0
  90. qcodes/tests/test_format.py +414 -0
  91. qcodes/tests/test_hdf5formatter.py +333 -0
  92. qcodes/tests/test_helpers.py +630 -0
  93. qcodes/tests/test_instrument.py +981 -0
  94. qcodes/tests/test_instrument_server.py +156 -0
  95. qcodes/tests/test_json.py +48 -0
  96. qcodes/tests/test_location_provider.py +109 -0
  97. qcodes/tests/test_loop.py +825 -0
  98. qcodes/tests/test_measure.py +49 -0
  99. qcodes/tests/test_metadata.py +49 -0
  100. qcodes/tests/test_multiprocessing.py +471 -0
  101. qcodes/tests/test_nested_attrs.py +100 -0
  102. qcodes/tests/test_parameter.py +151 -0
  103. qcodes/tests/test_plots.py +44 -0
  104. qcodes/tests/test_sweep_values.py +159 -0
  105. qcodes/tests/test_validators.py +482 -0
  106. qcodes/tests/test_visa.py +184 -0
  107. qcodes/utils/__init__.py +0 -0
  108. qcodes/utils/command.py +176 -0
  109. qcodes/utils/deferred_operations.py +276 -0
  110. qcodes/utils/helpers.py +417 -0
  111. qcodes/utils/metadata.py +42 -0
  112. qcodes/utils/nested_attrs.py +177 -0
  113. qcodes/utils/reload_code.py +98 -0
  114. qcodes/utils/threading.py +72 -0
  115. qcodes/utils/timing.py +89 -0
  116. qcodes/utils/validators.py +393 -0
  117. qcodes/version.py +1 -0
  118. qcodes/widgets/__init__.py +0 -0
  119. qcodes/widgets/display.py +47 -0
  120. qcodes/widgets/widgets.css +77 -0
  121. qcodes/widgets/widgets.js +237 -0
  122. qcodes/widgets/widgets.py +215 -0
  123. qcodes-0.1.0.dist-info/DESCRIPTION.rst +165 -0
  124. qcodes-0.1.0.dist-info/METADATA +194 -0
  125. qcodes-0.1.0.dist-info/RECORD +128 -0
  126. qcodes-0.1.0.dist-info/WHEEL +5 -0
  127. qcodes-0.1.0.dist-info/metadata.json +1 -0
  128. qcodes-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,803 @@
1
+ """DataSet class and factory functions."""
2
+
3
+ from enum import Enum
4
+ import time
5
+ import logging
6
+ from traceback import format_exc
7
+ from copy import deepcopy
8
+ from collections import OrderedDict
9
+
10
+ from .manager import get_data_manager, NoData
11
+ from .gnuplot_format import GNUPlotFormat
12
+ from .io import DiskIO
13
+ from .location import FormatLocation
14
+ from qcodes.utils.helpers import DelegateAttributes, full_class, deep_update
15
+
16
+
17
+ class DataMode(Enum):
18
+
19
+ """Server connection modes supported by a DataSet."""
20
+
21
+ LOCAL = 1
22
+ PUSH_TO_SERVER = 2
23
+ PULL_FROM_SERVER = 3
24
+
25
+
26
+ SERVER_MODES = set((DataMode.PULL_FROM_SERVER, DataMode.PUSH_TO_SERVER))
27
+
28
+
29
+ def new_data(location=None, loc_record=None, name=None, overwrite=False,
30
+ io=None, data_manager=False, mode=DataMode.LOCAL, **kwargs):
31
+ # NOTE(giulioungaretti): leave this docstrings as it is, because
32
+ # documenting the types is silly in this case.
33
+ """
34
+ Create a new DataSet.
35
+
36
+ Args:
37
+ location (str or callable or False, optional): If you provide a string,
38
+ it must be an unused location in the io manager. Can also be:
39
+ - a callable ``location provider`` with one required parameter
40
+ (the io manager), and one optional (``record`` dict),
41
+ which returns a location string when called
42
+ - ``False`` - denotes an only-in-memory temporary DataSet.
43
+ Note that the full path to or physical location of the data is a
44
+ combination of io + location. the default ``DiskIO`` sets the base
45
+ directory, which this location is a relative path inside.
46
+ Default ``DataSet.location_provider`` which is initially
47
+ ``FormatLocation()``
48
+
49
+ loc_record (dict, optional): If location is a callable, this will be
50
+ passed to it as ``record``
51
+
52
+ name (str, optional): overrides the ``name`` key in the ``loc_record``.
53
+
54
+ overwrite (bool): Are we allowed to overwrite an existing location?
55
+ Default False.
56
+
57
+ io (io_manager, optional): base physical location of the ``DataSet``.
58
+ Default ``DataSet.default_io`` is initially ``DiskIO('.')`` which
59
+ says the root data directory is the current working directory, ie
60
+ where you started the python session.
61
+
62
+ data_manager (Optional[bool]): use a manager for the
63
+ ``DataServer`` that offloads storage and syncing of this Defaults
64
+ to ``False`` i.e. this ``DataSet`` will store itself without extra
65
+ processes. Set to ``True`` to use the default from
66
+ ``get_data_manager()``.
67
+
68
+ mode (DataMode, optional): connection type to the ``DataServer``.
69
+ ``DataMode.LOCAL``: this DataSet doesn't communicate across
70
+ processes.
71
+ ``DataMode.PUSH_TO_SERVER``: no local copy of data, just pushes
72
+ each measurement to a ``DataServer``.
73
+ ``DataMode.PULL_FROM_SERVER``: pulls changes from the
74
+ ``DataServer`` on calling ``self.sync()``. Reverts to local if
75
+ and when it stops being the live measurement.
76
+ Default ``DataMode.LOCAL``.
77
+
78
+ arrays (Optional[List[qcodes.DataArray]): arrays to add to the DataSet.
79
+ Can be added later with ``self.add_array(array)``.
80
+
81
+ formatter (Formatter, optional): sets the file format/structure to
82
+ write (and read) with. Default ``DataSet.default_formatter`` which
83
+ is initially ``GNUPlotFormat()``.
84
+
85
+ write_period (float or None, optional): Only if ``mode=LOCAL``, seconds
86
+ between saves to disk. If not ``LOCAL``, the ``DataServer`` handles
87
+ this and generally writes more often. Use None to disable writing
88
+ from calls to ``self.store``. Default 5.
89
+
90
+ Returns:
91
+ A new ``DataSet`` object ready for storing new data in.
92
+ """
93
+ if io is None:
94
+ io = DataSet.default_io
95
+
96
+ if name is not None:
97
+ if not loc_record:
98
+ loc_record = {}
99
+ loc_record['name'] = name
100
+
101
+ if location is None:
102
+ location = DataSet.location_provider
103
+
104
+ if callable(location):
105
+ location = location(io, record=loc_record)
106
+
107
+ if location and (not overwrite) and io.list(location):
108
+ raise FileExistsError('"' + location + '" already has data')
109
+
110
+ if data_manager is True:
111
+ data_manager = get_data_manager()
112
+ else:
113
+ if mode != DataMode.LOCAL:
114
+ raise ValueError('DataSets without a data_manager must be local')
115
+
116
+ return DataSet(location=location, io=io, data_manager=data_manager,
117
+ mode=mode, **kwargs)
118
+
119
+
120
+ def load_data(location=None, data_manager=None, formatter=None, io=None):
121
+ """
122
+ Load an existing DataSet.
123
+
124
+ The resulting ``DataSet.mode`` is determined automatically from location:
125
+ PULL_FROM_SERVER if this is the live DataSet, otherwise LOCAL
126
+
127
+ Args:
128
+ location (str, optional): the location to load from. Default is the
129
+ current live DataSet.
130
+ Note that the full path to or physical location of the data is a
131
+ combination of io + location. the default ``DiskIO`` sets the base
132
+ directory, which this location is a relative path inside.
133
+
134
+ data_manager (DataManager or False, optional): manager for the
135
+ ``DataServer`` that offloads storage and syncing of this
136
+ ``DataSet``. Usually omitted (default None) to use the default
137
+ from ``get_data_manager()``. If ``False``, this ``DataSet`` will
138
+ store itself. ``load_data`` will not start a DataManager but may
139
+ query an existing one to determine (and pull) the live data.
140
+
141
+ formatter (Formatter, optional): sets the file format/structure to
142
+ read with. Default ``DataSet.default_formatter`` which
143
+ is initially ``GNUPlotFormat()``.
144
+
145
+ io (io_manager, optional): base physical location of the ``DataSet``.
146
+ Default ``DataSet.default_io`` is initially ``DiskIO('.')`` which
147
+ says the root data directory is the current working directory, ie
148
+ where you started the python session.
149
+
150
+ Returns:
151
+ A new ``DataSet`` object loaded with pre-existing data.
152
+ """
153
+ if data_manager is None:
154
+ data_manager = get_data_manager(only_existing=True)
155
+
156
+ if location is None:
157
+ if not data_manager:
158
+ raise RuntimeError('Live data requested but DataManager does '
159
+ 'not exist or was requested not to be used')
160
+
161
+ return _get_live_data(data_manager)
162
+
163
+ elif location is False:
164
+ raise ValueError('location=False means a temporary DataSet, '
165
+ 'which is incompatible with load_data')
166
+
167
+ elif (data_manager and
168
+ location == data_manager.ask('get_data', 'location')):
169
+ return _get_live_data(data_manager)
170
+
171
+ else:
172
+ data = DataSet(location=location, formatter=formatter, io=io,
173
+ mode=DataMode.LOCAL)
174
+ data.read_metadata()
175
+ data.read()
176
+ return data
177
+
178
+
179
+ def _get_live_data(data_manager):
180
+ live_data = data_manager.ask('get_data')
181
+ if live_data is None or isinstance(live_data, NoData):
182
+ raise RuntimeError('DataManager has no live data')
183
+
184
+ live_data.mode = DataMode.PULL_FROM_SERVER
185
+ return live_data
186
+
187
+
188
+ class DataSet(DelegateAttributes):
189
+
190
+ """
191
+ A container for one complete measurement loop.
192
+
193
+ May contain many individual arrays with potentially different
194
+ sizes and dimensionalities.
195
+
196
+ Normally a DataSet should not be instantiated directly, but through
197
+ ``new_data`` or ``load_data``.
198
+
199
+ Args:
200
+ location (str or False): A location in the io manager, or ``False`` for
201
+ an only-in-memory temporary DataSet.
202
+ Note that the full path to or physical location of the data is a
203
+ combination of io + location. the default ``DiskIO`` sets the base
204
+ directory, which this location is a relative path inside.
205
+
206
+ io (io_manager, optional): base physical location of the ``DataSet``.
207
+ Default ``DataSet.default_io`` is initially ``DiskIO('.')`` which
208
+ says the root data directory is the current working directory, ie
209
+ where you started the python session.
210
+
211
+ data_manager (Optional[bool]): use a manager for the
212
+ ``DataServer`` that offloads storage and syncing of this Defaults
213
+ to ``False`` i.e. this ``DataSet`` will store itself without extra
214
+ processes. Set to ``True`` to use the default from
215
+ ``get_data_manager()``.
216
+
217
+ mode (DataMode, optional): connection type to the ``DataServer``.
218
+ ``DataMode.LOCAL``: this DataSet doesn't communicate across
219
+ processes.
220
+ ``DataMode.PUSH_TO_SERVER``: no local copy of data, just pushes
221
+ each measurement to a ``DataServer``.
222
+ ``DataMode.PULL_FROM_SERVER``: pulls changes from the
223
+ ``DataServer`` on calling ``self.sync()``. Reverts to local if
224
+ and when it stops being the live measurement.
225
+ Default ``DataMode.LOCAL``.
226
+
227
+ arrays (Optional[List[qcodes.DataArray]): arrays to add to the DataSet.
228
+ Can be added later with ``self.add_array(array)``.
229
+
230
+ formatter (Formatter, optional): sets the file format/structure to
231
+ write (and read) with. Default ``DataSet.default_formatter`` which
232
+ is initially ``GNUPlotFormat()``.
233
+
234
+ write_period (float or None, optional): Only if ``mode=LOCAL``, seconds
235
+ between saves to disk. If not ``LOCAL``, the ``DataServer`` handles
236
+ this and generally writes more often. Use None to disable writing
237
+ from calls to ``self.store``. Default 5.
238
+
239
+ Attributes:
240
+ background_functions (OrderedDict[callable]): Class attribute,
241
+ ``{key: fn}``: ``fn`` is a callable accepting no arguments, and
242
+ ``key`` is a name to identify the function and help you attach and
243
+ remove it.
244
+
245
+ In ``DataSet.complete`` we call each of these periodically, in the
246
+ order that they were attached.
247
+
248
+ Note that because this is a class attribute, the functions will
249
+ apply to every DataSet. If you want specific functions for one
250
+ DataSet you can override this with an instance attribute.
251
+ """
252
+
253
+ # ie data_set.arrays['vsd'] === data_set.vsd
254
+ delegate_attr_dicts = ['arrays']
255
+
256
+ default_io = DiskIO('.')
257
+ default_formatter = GNUPlotFormat()
258
+ location_provider = FormatLocation()
259
+
260
+ background_functions = OrderedDict()
261
+
262
+ def __init__(self, location=None, mode=DataMode.LOCAL, arrays=None,
263
+ data_manager=False, formatter=None, io=None, write_period=5):
264
+ if location is False or isinstance(location, str):
265
+ self.location = location
266
+ else:
267
+ raise ValueError('unrecognized location ' + repr(location))
268
+
269
+ # TODO: when you change formatter or io (and there's data present)
270
+ # make it all look unsaved
271
+ self.formatter = formatter or self.default_formatter
272
+ self.io = io or self.default_io
273
+
274
+ self.write_period = write_period
275
+ self.last_write = 0
276
+ self.last_store = -1
277
+
278
+ self.metadata = {}
279
+
280
+ self.arrays = {}
281
+ if arrays:
282
+ self.action_id_map = self._clean_array_ids(arrays)
283
+ for array in arrays:
284
+ self.add_array(array)
285
+
286
+ if data_manager is True and mode in SERVER_MODES:
287
+ data_manager = get_data_manager()
288
+
289
+ if mode == DataMode.LOCAL:
290
+ self._init_local()
291
+ elif mode == DataMode.PUSH_TO_SERVER:
292
+ self._init_push_to_server(data_manager)
293
+ elif mode == DataMode.PULL_FROM_SERVER:
294
+ self._init_live(data_manager)
295
+ else:
296
+ raise ValueError('unrecognized DataSet mode', mode)
297
+
298
+ def _init_local(self):
299
+ self.mode = DataMode.LOCAL
300
+
301
+ if self.arrays:
302
+ for array in self.arrays.values():
303
+ array.init_data()
304
+
305
+ def _init_push_to_server(self, data_manager):
306
+ self.mode = DataMode.PUSH_TO_SERVER
307
+
308
+ # If some code was not available when data_manager was started,
309
+ # we can't unpickle it on the other end.
310
+ # So we'll try, then restart if this error occurs, then try again.
311
+ #
312
+ # This still has a pitfall, if code has been *changed* since
313
+ # starting the server, it will still have the old version and
314
+ # everything will look fine but it won't have the new behavior.
315
+ # If the user does that, they need to manually restart the server,
316
+ # using:
317
+ # data_manager.restart()
318
+ try:
319
+ data_manager.ask('new_data', self)
320
+ except AttributeError:
321
+ data_manager.restart()
322
+ data_manager.ask('new_data', self)
323
+
324
+ # need to set data_manager *after* sending to data_manager because
325
+ # we can't (and shouldn't) send data_manager itself through a queue
326
+ self.data_manager = data_manager
327
+
328
+ def init_on_server(self):
329
+ """
330
+ Configure this DataSet as the DataServer copy.
331
+
332
+ Should be run only by the DataServer itself.
333
+ """
334
+ if not self.arrays:
335
+ raise RuntimeError('A server-side DataSet needs DataArrays.')
336
+
337
+ self._init_local()
338
+
339
+ def _init_live(self, data_manager):
340
+ self.mode = DataMode.PULL_FROM_SERVER
341
+ self.data_manager = data_manager
342
+ with data_manager.query_lock:
343
+ if self.is_on_server:
344
+ live_obj = data_manager.ask('get_data')
345
+ self.arrays = live_obj.arrays
346
+ else:
347
+ self._init_local()
348
+
349
+ @property
350
+ def is_live_mode(self):
351
+ """
352
+ Indicate whether this DataSet thinks it is live in the DataServer.
353
+
354
+ Does not actually talk to the DataServer or sync with it.
355
+ """
356
+ return self.mode in SERVER_MODES and self.data_manager and True
357
+
358
+ @property
359
+ def is_on_server(self):
360
+ """
361
+ Check whether this DataSet is actually live in the DataServer.
362
+
363
+ If it thought it was but isn't, convert it to mode=LOCAL
364
+ """
365
+ if not self.is_live_mode or self.location is False:
366
+ return False
367
+
368
+ with self.data_manager.query_lock:
369
+ live_location = self.data_manager.ask('get_data', 'location')
370
+ return self.location == live_location
371
+
372
+ def sync(self):
373
+ """
374
+ Synchronize this DataSet with the DataServer or storage.
375
+
376
+ If this DataSet is on the server, asks the server for changes.
377
+ If not, reads the entire DataSet from disk.
378
+
379
+ Returns:
380
+ bool: True if this DataSet is live on the server
381
+ """
382
+ # TODO: sync implies bidirectional... and it could be!
383
+ # we should keep track of last sync timestamp and last modification
384
+ # so we can tell whether this one, the other one, or both copies have
385
+ # changed (and I guess throw an error if both did? Would be cool if we
386
+ # could find a robust and intuitive way to make modifications to the
387
+ # version on the DataServer from the main copy)
388
+ if not self.is_live_mode:
389
+ # LOCAL DataSet - just read it in
390
+ # Compare timestamps to avoid overwriting unsaved data
391
+ if self.last_store > self.last_write:
392
+ return True
393
+ try:
394
+ self.read()
395
+ except IOError:
396
+ # if no files exist, they probably haven't been created yet.
397
+ pass
398
+ return False
399
+ # TODO - for remote live plotting, maybe set some timestamp
400
+ # threshold and call it static after it's been dormant a long time?
401
+ # I'm thinking like a minute, or ten? Maybe it's configurable?
402
+
403
+ with self.data_manager.query_lock:
404
+ if self.is_on_server:
405
+ synced_indices = {
406
+ array_id: array.get_synced_index()
407
+ for array_id, array in self.arrays.items()
408
+ }
409
+
410
+ changes = self.data_manager.ask('get_changes', synced_indices)
411
+
412
+ for array_id, array_changes in changes.items():
413
+ self.arrays[array_id].apply_changes(**array_changes)
414
+
415
+ measuring = self.data_manager.ask('get_measuring')
416
+ if not measuring:
417
+ # we must have *just* stopped measuring
418
+ # but the DataSet is still on the server,
419
+ # so we got the data, and don't need to read.
420
+ self.mode = DataMode.LOCAL
421
+ return False
422
+ return True
423
+ else:
424
+ # this DataSet *thought* it was on the server, but it wasn't,
425
+ # so we haven't synced yet and need to read from storage
426
+ self.mode = DataMode.LOCAL
427
+ self.read()
428
+ return False
429
+
430
+ def fraction_complete(self):
431
+ """
432
+ Get the fraction of this DataSet which has data in it.
433
+
434
+ Returns:
435
+ float: the average of all measured (not setpoint) arrays'
436
+ ``fraction_complete()`` values, independent of the individual
437
+ array sizes. If there are no measured arrays, returns zero.
438
+ """
439
+ array_count, total = 0, 0
440
+
441
+ for array in self.arrays.values():
442
+ if not array.is_setpoint:
443
+ array_count += 1
444
+ total += array.fraction_complete()
445
+
446
+ return total / (array_count or 1)
447
+
448
+ def complete(self, delay=1.5):
449
+ """
450
+ Periodically sync the DataSet and display percent complete status.
451
+
452
+ Also, each period, execute functions stored in (class attribute)
453
+ ``self.background_functions``. If a function fails, we log its
454
+ traceback and continue on. If any one function fails twice in
455
+ a row, it gets removed.
456
+
457
+ Args:
458
+ delay (float): seconds between iterations. Default 1.5
459
+ """
460
+ logging.info(
461
+ 'waiting for DataSet <{}> to complete'.format(self.location))
462
+
463
+ failing = {key: False for key in self.background_functions}
464
+
465
+ completed = False
466
+ while True:
467
+ logging.info('DataSet: {:.0f}% complete'.format(
468
+ self.fraction_complete() * 100))
469
+
470
+ # first check if we're done
471
+ if self.sync() is False:
472
+ completed = True
473
+
474
+ # then even if we *are* done, execute the background functions
475
+ # because we want things like live plotting to get the final data
476
+ for key, fn in list(self.background_functions.items()):
477
+ try:
478
+ logging.debug('calling {}: {}'.format(key, repr(fn)))
479
+ fn()
480
+ failing[key] = False
481
+ except Exception:
482
+ logging.info(format_exc())
483
+ if failing[key]:
484
+ logging.warning(
485
+ 'background function {} failed twice in a row, '
486
+ 'removing it'.format(key))
487
+ del self.background_functions[key]
488
+ failing[key] = True
489
+
490
+ if completed:
491
+ break
492
+
493
+ # but only sleep if we're not already finished
494
+ time.sleep(delay)
495
+
496
+ logging.info('DataSet <{}> is complete'.format(self.location))
497
+
498
+ def get_changes(self, synced_indices):
499
+ """
500
+ Find changes since the last sync of this DataSet.
501
+
502
+ Args:
503
+ synced_indices (dict): ``{array_id: synced_index}`` where
504
+ synced_index is the last flat index which has already
505
+ been synced, for any (usually all) arrays in the DataSet.
506
+
507
+ Returns:
508
+ Dict[dict]: keys are ``array_id`` for each array with changes,
509
+ values are dicts as returned by ``DataArray.get_changes``
510
+ and required as kwargs to ``DataArray.apply_changes``.
511
+ Note that not all arrays in ``synced_indices`` need be
512
+ present in the return, only those with changes.
513
+ """
514
+ changes = {}
515
+
516
+ for array_id, synced_index in synced_indices.items():
517
+ array_changes = self.arrays[array_id].get_changes(synced_index)
518
+ if array_changes:
519
+ changes[array_id] = array_changes
520
+
521
+ return changes
522
+
523
+ def add_array(self, data_array):
524
+ """
525
+ Add one DataArray to this DataSet, and mark it as part of this DataSet.
526
+
527
+ Note: DO NOT just set ``data_set.arrays[id] = data_array``, because
528
+ this will not check if we are overwriting another array, nor set the
529
+ reference back to this DataSet, nor that the ``array_id`` in the array
530
+ matches how you're storing it here.
531
+
532
+ Args:
533
+ data_array (DataArray): the new array to add
534
+
535
+ Raises:
536
+ ValueError: if there is already an array with this id here.
537
+ """
538
+ # TODO: mask self.arrays so you *can't* set it directly?
539
+
540
+ if data_array.array_id in self.arrays:
541
+ raise ValueError('array_id {} already exists in this '
542
+ 'DataSet'.format(data_array.array_id))
543
+ self.arrays[data_array.array_id] = data_array
544
+
545
+ # back-reference to the DataSet
546
+ data_array.data_set = self
547
+
548
+ def _clean_array_ids(self, arrays):
549
+ """
550
+ replace action_indices tuple with compact string array_ids
551
+ stripping off as much extraneous info as possible
552
+ """
553
+ action_indices = [array.action_indices for array in arrays]
554
+ for array in arrays:
555
+ name = array.full_name
556
+ if array.is_setpoint and name and not name.endswith('_set'):
557
+ name += '_set'
558
+
559
+ array.array_id = name
560
+ array_ids = set([array.array_id for array in arrays])
561
+ for name in array_ids:
562
+ param_arrays = [array for array in arrays
563
+ if array.array_id == name]
564
+ self._clean_param_ids(param_arrays, name)
565
+
566
+ array_ids = [array.array_id for array in arrays]
567
+
568
+ return dict(zip(action_indices, array_ids))
569
+
570
+ def _clean_param_ids(self, arrays, name):
571
+ # strip off as many leading equal indices as possible
572
+ # and append the rest to the back of the name with underscores
573
+ param_action_indices = [list(array.action_indices) for array in arrays]
574
+ while all(len(ai) for ai in param_action_indices):
575
+ if len(set(ai[0] for ai in param_action_indices)) == 1:
576
+ for ai in param_action_indices:
577
+ ai[:1] = []
578
+ else:
579
+ break
580
+ for array, ai in zip(arrays, param_action_indices):
581
+ array.array_id = name + ''.join('_' + str(i) for i in ai)
582
+
583
+ def store(self, loop_indices, ids_values):
584
+ """
585
+ Insert data into one or more of our DataArrays.
586
+
587
+ If in ``PUSH_TO_SERVER`` mode, this is where we do that!
588
+ Otherwise we also periodically trigger a write to storage.
589
+
590
+ Args:
591
+ loop_indices (tuple): the indices within whatever loops we are
592
+ inside. May have fewer dimensions than some of the arrays
593
+ we are inserting into, if the corresponding value makes up
594
+ the remaining dimensionality.
595
+ values (Dict[Union[float, sequence]]): a dict whose keys are
596
+ array_ids, and values are single numbers or entire slices
597
+ to insert into that array.
598
+ """
599
+ if self.mode == DataMode.PUSH_TO_SERVER:
600
+ # Defers to the copy on the dataserver to call this identical
601
+ # function
602
+ self.data_manager.write('store_data', loop_indices, ids_values)
603
+ elif self.mode == DataMode.LOCAL:
604
+ # You will always end up in this block, either in the copy
605
+ # on the server (if you hit the if statement above) or else here
606
+ for array_id, value in ids_values.items():
607
+ self.arrays[array_id][loop_indices] = value
608
+ self.last_store = time.time()
609
+ if (self.write_period is not None and
610
+ time.time() > self.last_write + self.write_period):
611
+ self.write()
612
+ self.last_write = time.time()
613
+ else: # in PULL_FROM_SERVER mode; store() isn't legal
614
+ raise RuntimeError('This object is pulling from a DataServer, '
615
+ 'so data insertion is not allowed.')
616
+
617
+ def read(self):
618
+ """Read the whole DataSet from storage, overwriting the local data."""
619
+ if self.location is False:
620
+ return
621
+ self.formatter.read(self)
622
+
623
+ def read_metadata(self):
624
+ """Read the metadata from storage, overwriting the local data."""
625
+ if self.location is False:
626
+ return
627
+ self.formatter.read_metadata(self)
628
+
629
+ def write(self):
630
+ """
631
+ Writes updates to the DataSet to storage.
632
+ N.B. it is recommended to call data_set.finalize() when a DataSet is
633
+ no longer expected to change to ensure files get closed
634
+ """
635
+ if self.mode != DataMode.LOCAL:
636
+ raise RuntimeError('This object is connected to a DataServer, '
637
+ 'which handles writing automatically.')
638
+
639
+ if self.location is False:
640
+ return
641
+
642
+ self.formatter.write(self, self.io, self.location)
643
+
644
+ def write_copy(self, path=None, io_manager=None, location=None):
645
+ """
646
+ Write a new complete copy of this DataSet to storage.
647
+
648
+ Args:
649
+ path (str, optional): An absolute path on this system to write to.
650
+ If you specify this, you may not include either ``io_manager``
651
+ or ``location``.
652
+
653
+ io_manager (io_manager, optional): A new ``io_manager`` to use with
654
+ either the ``DataSet``'s same or a new ``location``.
655
+
656
+ location (str, optional): A new ``location`` to write to, using
657
+ either this ``DataSet``'s same or a new ``io_manager``.
658
+ """
659
+ if io_manager is not None or location is not None:
660
+ if path is not None:
661
+ raise TypeError('If you provide io_manager or location '
662
+ 'to write_copy, you may not provide path.')
663
+ if io_manager is None:
664
+ io_manager = self.io
665
+ elif location is None:
666
+ location = self.location
667
+ elif path is not None:
668
+ io_manager = DiskIO(None)
669
+ location = path
670
+ else:
671
+ raise TypeError('You must provide at least one argument '
672
+ 'to write_copy')
673
+
674
+ if location is False:
675
+ raise ValueError('write_copy needs a location, not False')
676
+
677
+ lsi_cache = {}
678
+ mr_cache = {}
679
+ for array_id, array in self.arrays.items():
680
+ lsi_cache[array_id] = array.last_saved_index
681
+ mr_cache[array_id] = array.modified_range
682
+ # array.clear_save() is not enough, we _need_ to set modified_range
683
+ # TODO - identify *when* clear_save is not enough, and fix it
684
+ # so we *can* use it. That said, maybe we will *still* want to
685
+ # use the full array here no matter what, or strip trailing NaNs
686
+ # separately, either here or in formatter.write?
687
+ array.last_saved_index = None
688
+ array.modified_range = (0, array.ndarray.size - 1)
689
+
690
+ try:
691
+ self.formatter.write(self, io_manager, location)
692
+ self.snapshot()
693
+ self.formatter.write_metadata(self, io_manager, location,
694
+ read_first=False)
695
+ finally:
696
+ for array_id, array in self.arrays.items():
697
+ array.last_saved_index = lsi_cache[array_id]
698
+ array.modified_range = mr_cache[array_id]
699
+
700
+ def add_metadata(self, new_metadata):
701
+ """
702
+ Update DataSet.metadata with additional data.
703
+
704
+ Args:
705
+ new_metadata (dict): new data to be deep updated into
706
+ the existing metadata
707
+ """
708
+ deep_update(self.metadata, new_metadata)
709
+
710
+ def save_metadata(self):
711
+ """Evaluate and save the DataSet's metadata."""
712
+ if self.location is not False:
713
+ self.snapshot()
714
+ self.formatter.write_metadata(self, self.io, self.location)
715
+
716
+ def finalize(self):
717
+ """
718
+ Mark the DataSet complete and write any remaining modifications.
719
+
720
+ Also closes the data file(s), if the ``Formatter`` we're using
721
+ supports that.
722
+ """
723
+ if self.mode == DataMode.PUSH_TO_SERVER:
724
+ # Just like .store, if this DataSet is on the DataServer,
725
+ # we defer to the copy there and execute this same method.
726
+ self.data_manager.ask('finalize_data')
727
+ elif self.mode == DataMode.LOCAL:
728
+ # You will always end up in this block, either in the copy
729
+ # on the server (if you hit the if statement above) or else here
730
+ self.write()
731
+
732
+ if hasattr(self.formatter, 'close_file'):
733
+ self.formatter.close_file(self)
734
+ else:
735
+ raise RuntimeError('This mode does not allow finalizing',
736
+ self.mode)
737
+ self.save_metadata()
738
+
739
+ def snapshot(self, update=False):
740
+ """JSON state of the DataSet."""
741
+ array_snaps = {}
742
+ for array_id, array in self.arrays.items():
743
+ array_snaps[array_id] = array.snapshot(update=update)
744
+
745
+ self.metadata.update({
746
+ '__class__': full_class(self),
747
+ 'location': self.location,
748
+ 'arrays': array_snaps,
749
+ 'formatter': full_class(self.formatter),
750
+ 'io': repr(self.io)
751
+ })
752
+ return deepcopy(self.metadata)
753
+
754
+ def get_array_metadata(self, array_id):
755
+ """
756
+ Get the metadata for a single contained DataArray.
757
+
758
+ Args:
759
+ array_id (str): the array to get metadata for.
760
+
761
+ Returns:
762
+ dict: metadata for this array.
763
+ """
764
+ try:
765
+ return self.metadata['arrays'][array_id]
766
+ except (AttributeError, KeyError):
767
+ return None
768
+
769
+ def __repr__(self):
770
+ """Rich information about the DataSet and contained arrays."""
771
+ out = type(self).__name__ + ':'
772
+
773
+ attrs = [['mode', self.mode],
774
+ ['location', repr(self.location)]]
775
+ attr_template = '\n {:8} = {}'
776
+ for var, val in attrs:
777
+ out += attr_template.format(var, val)
778
+
779
+ arr_info = [['<Type>', '<array_id>', '<array.name>', '<array.shape>']]
780
+
781
+ if hasattr(self, 'action_id_map'):
782
+ id_items = [
783
+ item for index, item in sorted(self.action_id_map.items())]
784
+ else:
785
+ id_items = self.arrays.keys()
786
+
787
+ for array_id in id_items:
788
+ array = self.arrays[array_id]
789
+ setp = 'Setpoint' if array.is_setpoint else 'Measured'
790
+ name = array.name or 'None'
791
+ array_id = array_id or 'None'
792
+ arr_info.append([setp, array_id, name, repr(array.shape)])
793
+
794
+ column_lengths = [max(len(row[i]) for row in arr_info)
795
+ for i in range(len(arr_info[0]))]
796
+ out_template = ('\n '
797
+ '{info[0]:{lens[0]}} | {info[1]:{lens[1]}} | '
798
+ '{info[2]:{lens[2]}} | {info[3]}')
799
+
800
+ for arr_info_i in arr_info:
801
+ out += out_template.format(info=arr_info_i, lens=column_lengths)
802
+
803
+ return out