qcodespp 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 (82) hide show
  1. qcodespp/__init__.py +77 -0
  2. qcodespp/actions.py +237 -0
  3. qcodespp/data/__init__.py +0 -0
  4. qcodespp/data/data_array.py +555 -0
  5. qcodespp/data/data_set.py +926 -0
  6. qcodespp/data/format.py +338 -0
  7. qcodespp/data/gnuplot_format.py +425 -0
  8. qcodespp/data/hdf5_format.py +537 -0
  9. qcodespp/data/hdf5_format_hickle.py +63 -0
  10. qcodespp/data/io.py +241 -0
  11. qcodespp/data/location.py +171 -0
  12. qcodespp/instrument_drivers/Keysight/Keysight_34450A.py +9 -0
  13. qcodespp/instrument_drivers/Keysight/__init__.py +0 -0
  14. qcodespp/instrument_drivers/Keysight/private/Keysight_344xxA.py +456 -0
  15. qcodespp/instrument_drivers/Keysight/private/__init__.py +0 -0
  16. qcodespp/instrument_drivers/Lakeshore/Model_340.py +483 -0
  17. qcodespp/instrument_drivers/Lakeshore/__init__.py +0 -0
  18. qcodespp/instrument_drivers/QDevil/159fit_params_high_latest.json +386 -0
  19. qcodespp/instrument_drivers/QDevil/159fit_params_low_latest.json +386 -0
  20. qcodespp/instrument_drivers/QDevil/QDAC2.py +3229 -0
  21. qcodespp/instrument_drivers/QDevil/QDAC2_Array.py +300 -0
  22. qcodespp/instrument_drivers/QDevil/QSwitch_elab.py +652 -0
  23. qcodespp/instrument_drivers/QDevil/__init__.py +0 -0
  24. qcodespp/instrument_drivers/ZI/ZIHF2LI.py +415 -0
  25. qcodespp/instrument_drivers/ZI/ZIMFLI.py +764 -0
  26. qcodespp/instrument_drivers/ZI/ZIMFLI_sync.py +108 -0
  27. qcodespp/instrument_drivers/ZI/ZIUHFLI.py +2179 -0
  28. qcodespp/instrument_drivers/ZI/__init__.py +1 -0
  29. qcodespp/instrument_drivers/__init__.py +0 -0
  30. qcodespp/instrument_drivers/cryogenic/CryogenicSMS120C.py +599 -0
  31. qcodespp/instrument_drivers/cryogenic/__init__.py +0 -0
  32. qcodespp/instrument_drivers/instrument_mocks.py +379 -0
  33. qcodespp/instrument_drivers/multiplexer_instrument.py +287 -0
  34. qcodespp/instrument_drivers/national_instruments/PXI_4461.py +451 -0
  35. qcodespp/instrument_drivers/national_instruments/PXI_4462.py +290 -0
  36. qcodespp/instrument_drivers/national_instruments/PXI_6259.py +436 -0
  37. qcodespp/instrument_drivers/national_instruments/PXI_6733.py +230 -0
  38. qcodespp/instrument_drivers/national_instruments/PXI_6733_e.py +243 -0
  39. qcodespp/instrument_drivers/national_instruments/__init__.py +0 -0
  40. qcodespp/instrument_drivers/oxford/__init__.py +0 -0
  41. qcodespp/instrument_drivers/oxford/mercuryiPS_serial.py +658 -0
  42. qcodespp/instrument_drivers/oxford/triton.py +434 -0
  43. qcodespp/instrument_drivers/tektronix/Keithley_2000_Scan.py +65 -0
  44. qcodespp/instrument_drivers/tektronix/Keithley_2182a.py +190 -0
  45. qcodespp/instrument_drivers/tektronix/Keithley_2400.py +158 -0
  46. qcodespp/instrument_drivers/tektronix/Keithley_2450.py +242 -0
  47. qcodespp/instrument_drivers/tektronix/Keithley_2700.py +397 -0
  48. qcodespp/instrument_drivers/tektronix/Keithley_6500.py +161 -0
  49. qcodespp/instrument_drivers/tektronix/__init__.py +0 -0
  50. qcodespp/instrument_drivers/yokogawa/GS200.py +513 -0
  51. qcodespp/instrument_drivers/yokogawa/__init__.py +0 -0
  52. qcodespp/loops.py +1342 -0
  53. qcodespp/measure.py +310 -0
  54. qcodespp/parameters.py +311 -0
  55. qcodespp/plotting/RemotePlot.py +469 -0
  56. qcodespp/plotting/RemotePlotClient.py +367 -0
  57. qcodespp/plotting/RemoteQtPlotWidgets.py +871 -0
  58. qcodespp/plotting/__init__.py +0 -0
  59. qcodespp/plotting/analysis_tools.py +195 -0
  60. qcodespp/plotting/colors.py +135 -0
  61. qcodespp/plotting/offline/__init__.py +0 -0
  62. qcodespp/plotting/offline/datatypes.py +999 -0
  63. qcodespp/plotting/offline/design.py +906 -0
  64. qcodespp/plotting/offline/design.ui +1578 -0
  65. qcodespp/plotting/offline/editor.py +2966 -0
  66. qcodespp/plotting/offline/filters.py +561 -0
  67. qcodespp/plotting/offline/fits.py +1171 -0
  68. qcodespp/plotting/offline/helpers.py +288 -0
  69. qcodespp/plotting/offline/iconGadget.png +0 -0
  70. qcodespp/plotting/offline/main.py +55 -0
  71. qcodespp/plotting/offline/popupwindows.py +1848 -0
  72. qcodespp/plotting/offline/qcodes_pp_extension.py +328 -0
  73. qcodespp/plotting/offline/sidebars.py +1108 -0
  74. qcodespp/station.py +290 -0
  75. qcodespp/utils/__init__.py +0 -0
  76. qcodespp/utils/helpers.py +618 -0
  77. qcodespp/utils/visa_helpers.py +61 -0
  78. qcodespp/version.py +12 -0
  79. qcodespp-0.1.0.dist-info/METADATA +91 -0
  80. qcodespp-0.1.0.dist-info/RECORD +82 -0
  81. qcodespp-0.1.0.dist-info/WHEEL +4 -0
  82. qcodespp-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,926 @@
1
+ """DataSetPP class and factory functions."""
2
+
3
+ import time
4
+ from datetime import datetime, date
5
+ import logging
6
+ import glob
7
+ import numpy as np
8
+ from traceback import format_exc
9
+ from copy import deepcopy
10
+ from collections import OrderedDict
11
+ from typing import Dict, Callable
12
+
13
+ from qcodespp.data.gnuplot_format import GNUPlotFormat
14
+ from qcodespp.data.io import DiskIO
15
+ from qcodespp.data.location import FormatLocation
16
+ from qcodespp.data.data_array import DataArray
17
+ from qcodes.utils.helpers import DelegateAttributes, full_class, deep_update
18
+ from qcodespp.station import Station
19
+ from qcodespp.parameters import Parameter
20
+ from qcodes import config
21
+ import os
22
+
23
+ from uuid import uuid4
24
+ log = logging.getLogger(__name__)
25
+
26
+ def new_data(location=None, loc_record=None, name=None, overwrite=False,
27
+ io=None, backup_location=None, force_write=False, **kwargs):
28
+ """
29
+ Create a new DataSetPP, the text-based data set of qcodespp.
30
+
31
+ Args:
32
+ location (str or callable or False, optional): If you provide a string,
33
+ it must be an unused location in the io manager. Can also be:
34
+
35
+ - a callable ``location provider`` with one required parameter
36
+ (the io manager), and one optional (``record`` dict),
37
+ which returns a location string when called
38
+ - ``False`` - denotes an only-in-memory temporary DataSetPP.
39
+
40
+ Note that the full path to or physical location of the data is a
41
+ combination of io + location. the default ``DiskIO`` sets the base
42
+ directory, which this location is a relative path inside.
43
+ Default ``DataSetPP.location_provider`` which is initially
44
+ ``FormatLocation()``
45
+
46
+ loc_record (dict, optional): If location is a callable, this will be
47
+ passed to it as ``record``
48
+
49
+ name (str, optional): overrides the ``name`` key in the ``loc_record``.
50
+
51
+ overwrite (bool): Are we allowed to overwrite an existing location?
52
+ Default False.
53
+
54
+ io (io_manager, optional): base physical location of the ``DataSetPP``.
55
+ Default ``DataSetPP.default_io`` is initially ``DiskIO('.')`` which
56
+ says the root data directory is the current working directory, ie
57
+ where you started the python session.
58
+
59
+ arrays (Optional[List[qcodes.DataArray]): arrays to add to the DataSetPP.
60
+ Can be added later with ``self.add_array(array)``.
61
+
62
+ formatter (Formatter, optional): sets the file format/structure to
63
+ write (and read) with. Default ``DataSetPP.default_formatter`` which
64
+ is initially ``GNUPlotFormat()``.
65
+
66
+ write_period (float or None, optional):seconds
67
+ between saves to disk.
68
+ Returns:
69
+ A new ``DataSetPP`` object ready for storing new data in.
70
+ """
71
+ if io is None:
72
+ io = DataSetPP.default_io
73
+
74
+ if location is None:
75
+ location = DataSetPP.location_provider
76
+
77
+ if name is not None:
78
+ if not loc_record:
79
+ loc_record = {}
80
+ loc_record['name'] = name
81
+
82
+ if callable(location):
83
+ location = location(io, record=loc_record)
84
+
85
+ if location and (not overwrite) and io.list(location):
86
+ raise FileExistsError('"' + location + '" already has data')
87
+
88
+ return DataSetPP(location=location, io=io, backup_location=backup_location, force_write=force_write, name=name, **kwargs)
89
+
90
+ def load_data(location=None, formatter=None, io=None, include_metadata=True):
91
+ """
92
+ Load an existing DataSetPP.
93
+
94
+ Args:
95
+ location (str, optional): the location to load from. Default is the
96
+ current live DataSetPP.
97
+ Note that the full path to or physical location of the data is a
98
+ combination of io + location. the default ``DiskIO`` sets the base
99
+ directory, which this location is a relative path inside.
100
+
101
+ formatter (Formatter, optional): sets the file format/structure to
102
+ read with. Default ``DataSetPP.default_formatter`` which
103
+ is initially ``GNUPlotFormat()``.
104
+
105
+ io (io_manager, optional): base physical location of the ``DataSetPP``.
106
+ Default ``DataSetPP.default_io`` is initially ``DiskIO('.')`` which
107
+ says the root data directory is the current working directory, ie
108
+ where you started the python session.
109
+
110
+ Returns:
111
+ A new ``DataSetPP`` object loaded with pre-existing data.
112
+ """
113
+ if location is False:
114
+ raise ValueError('location=False means a temporary DataSetPP, '
115
+ 'which is incompatible with load_data')
116
+
117
+ data = DataSetPP(location=location, formatter=formatter, io=io)
118
+ if include_metadata==True:
119
+ data.read_metadata()
120
+ data.read()
121
+ else:
122
+ data.read(include_metadata=False)
123
+ return data
124
+
125
+ def load_data_num(number, datafolder="data", delimiter='_',leadingzeros=3,include_metadata=True):
126
+ """
127
+ Load a qcodespp DataSetPP using the counter as identifier.
128
+
129
+ Typically qcodespp DataSetPPs are forced to use the format counter_name_date_time,
130
+ where the counter is a zero-padded integer. This function will search for
131
+ a folder with the given counter number, and load the data from it.
132
+
133
+ Args:
134
+ number (str or int): the dataset's counter number
135
+ datafolder (str, optional): the folder to load from. Default is the
136
+ current live DataSetPP.
137
+ Note that the full path to or physical location of the data is a
138
+ combination of io + location. the default ``DiskIO`` sets the base
139
+ directory, which this location is a relative path inside.
140
+ delimiter (str, optional): The character after the number. Almost always
141
+ underscore but may be specified if necessary.
142
+
143
+ Returns:
144
+ A new ``DataSetPP`` object loaded with pre-existing data.
145
+ """
146
+ number=str(number).split('_')[0].zfill(leadingzeros) #Split included here to account for a potential fail point in backwards compatibility.
147
+ #There were cases where the user had to explicitly include the delimiter.
148
+ datapaths = [glob.glob('{}/#{}{}*/'.format(datafolder,number,delimiter))]
149
+ if np.shape(datapaths[0])[0]>1:
150
+ raise ValueError('Multiple data sets found! Check numbering or delimiter.')
151
+ elif np.shape(datapaths[0])[0]==0:
152
+ raise ValueError('No dataset found!')
153
+ else:
154
+ data = load_data(datapaths[0][0],include_metadata=include_metadata)
155
+ return data
156
+
157
+ def load_data_nums(listofnumbers, datafolder="data",delimiter='_',leadingzeros=3,include_metadata=True):
158
+ """
159
+ Loads numerous DataSetPPs from the specified folder by counter number.
160
+
161
+ Args:
162
+ litsofnumbers (list of strings or ints): list of desired dataset numbers.
163
+ datafolder (str, optional): the folder to load from. Default is the
164
+ current live DataSetPP.
165
+ Note that the full path to or physical location of the data is a
166
+ combination of io + location. the default ``DiskIO`` sets the base
167
+ directory, which this location is a relative path inside.
168
+ delimiter (str, optional): The character after the number. Almost always
169
+ underscore but may be specified if necessary.
170
+
171
+ Returns:
172
+ An array containing ``DataSetPP`` objects loaded with pre-existing data.
173
+ """
174
+
175
+ data=[]
176
+ for i,number in enumerate(listofnumbers):
177
+ number=str(number).split('_')[0].zfill(leadingzeros) #Split included here to account for a potential fail point in backwards compatibility.
178
+ #There were cases where the user had to explicitly include the delimiter.
179
+ datapaths = [glob.glob('{}/#{}{}*/'.format(datafolder,number,delimiter))]
180
+ if np.shape(datapaths[0])[0]>1:
181
+ raise ValueError('Multiple data sets with number {} found! check numbering or choice of delimiter.'.format(number))
182
+ elif np.shape(datapaths[0])[0]==0:
183
+ raise ValueError('No dataset with number {} found! check numbering. '.format(number))
184
+ else:
185
+ data.append(load_data(datapaths[0][0],include_metadata=include_metadata))
186
+
187
+ return data
188
+
189
+ def set_data_format(fmt='data/#{counter}_{name}_{date}_{time}'):
190
+ """
191
+ Set the default format for storing DataSetPPs. It is not recommended to alter this: instead use set_data_folder.
192
+
193
+ Args:
194
+ fmt (str): A format string for the location of the data, with wildcards determined by the FormatLocation class.
195
+ """
196
+
197
+ DataSetPP.location_provider=FormatLocation(fmt=fmt)
198
+
199
+ def set_data_folder(folder='data'):
200
+ """
201
+ Set the default folder for storing DataSetPPs.
202
+
203
+ Args:
204
+ folder (str): Folder name relative to the current working directory, e.g. location of the current
205
+ Jupyter notebook. The folder will be created if it does not exist.
206
+ """
207
+
208
+ fmt=folder+'/#{counter}_{name}_{date}_{time}'
209
+ DataSetPP.default_folder=folder
210
+ DataSetPP.location_provider=FormatLocation(fmt=fmt)
211
+
212
+ class DataSetPP(DelegateAttributes):
213
+
214
+ """
215
+ A container for one complete measurement from qcodespp.Measure or qcodespp.Loop.
216
+
217
+ A DataSetPP consists of multiple DataArrays with potentially different
218
+ sizes and dimensionalities. It is accompanied by metadata containing snapshots
219
+ of different qcodespp classes, e.g. Instruments and Parameters in the Station.
220
+
221
+ A DataSetPP should not be instantiated directly, but constructed by qcodespp.Measure
222
+ or qcodespp.Loop. A pre-existing DataSetPP can be loaded with qcodespp.load_data,
223
+ load_data_num, or load_data_nums.
224
+
225
+ The default format for storage is (a) text file(s) with GNUPlotFormat, where the
226
+ DataArrays are converted to numpy arrays. This means that each DataArray must be
227
+ rectangular, and all elements must be of the same type. Currently, types are limited
228
+ to float or str; however, almost any type other than str can be converted to a float,
229
+ and this is done automatically; e.g. boolean --> (0,1).
230
+ DataArrays which are also Setpoints can only be of type float.
231
+
232
+ Args:
233
+ location (str or False): A location in the io manager, or ``False`` for
234
+ an only-in-memory temporary DataSetPP.
235
+ Note that the full path to or physical location of the data is a
236
+ combination of io + location. the default ``DiskIO`` sets the base
237
+ directory, which this location is a relative path inside.
238
+
239
+ io (io_manager, optional): base physical location of the ``DataSetPP``.
240
+ Default ``DataSetPP.default_io`` is initially ``DiskIO('.')`` which
241
+ says the root data directory is the current working directory, ie
242
+ where you started the python session.
243
+
244
+ arrays (Optional[List[qcodes.DataArray]): arrays to add to the DataSetPP.
245
+ Can be added later with ``self.add_array(array)``.
246
+
247
+ formatter (Formatter, optional): sets the file format/structure to
248
+ write (and read) with. Default ``DataSetPP.default_formatter`` which
249
+ is initially ``GNUPlotFormat()``.
250
+
251
+ write_period (float or None, optional): Only if ``mode=LOCAL``, seconds
252
+ between saves to disk. If not ``LOCAL``, the ``DataServer`` handles
253
+ this and generally writes more often. Use None to disable writing
254
+ from calls to ``self.store``. Default 5.
255
+
256
+ Attributes:
257
+ background_functions (OrderedDict[callable]): Class attribute,
258
+ ``{key: fn}``: ``fn`` is a callable accepting no arguments, and
259
+ ``key`` is a name to identify the function and help you attach and
260
+ remove it.
261
+
262
+ In ``DataSetPP.complete`` we call each of these periodically, in the
263
+ order that they were attached.
264
+
265
+ Note that because this is a class attribute, the functions will
266
+ apply to every DataSetPP. If you want specific functions for one
267
+ DataSetPP you can override this with an instance attribute.
268
+ """
269
+
270
+ # ie data_set.arrays['vsd'] === data_set.vsd
271
+ delegate_attr_dicts = ['arrays']
272
+
273
+ default_io = DiskIO('.')
274
+ default_formatter = GNUPlotFormat()
275
+ location_provider = FormatLocation()
276
+
277
+ background_functions: Dict[str, Callable] = OrderedDict()
278
+
279
+ def __init__(self, location=None, arrays=None, formatter=None, io=None,
280
+ write_period=5, backup_location=None,force_write=False,name=None):
281
+ if location is False or isinstance(location, str):
282
+ self.location = location
283
+ else:
284
+ raise ValueError('unrecognized location ' + repr(location))
285
+
286
+ if isinstance(backup_location,str):
287
+ self.backup_location=backup_location
288
+ elif 'backup_location' in config['core'].keys():
289
+ self.backup_location=config['core']['backup_location']
290
+ if os.access(self.backup_location, os.W_OK) is False and os.path.exists(self.backup_location) is False:
291
+ try:
292
+ os.makedirs(self.backup_location)
293
+ except Exception as e:
294
+ log.warning(f'Backup location specified in qcodes.config["core"]["backup_location"] '
295
+ 'could not be created. Try another location \n {e}')
296
+ if os.access(self.backup_location, os.W_OK) is False:
297
+ log.warning('Backup location specified in qcodes.config["core"]["backup_location"] is not writable. '
298
+ 'Try another location')
299
+ elif backup_location is None:
300
+ self.backup_location='C:/Users/'+os.getlogin()+'/AppData/Local/qcodes-elab/data_backup'
301
+ if os.access(self.backup_location, os.W_OK) is False and os.path.exists(self.backup_location) is False:
302
+ try:
303
+ os.makedirs(self.backup_location)
304
+ except Exception as e:
305
+ log.warning(f'Default backup location {self.backup_location} '
306
+ 'could not be created. \n {e} '
307
+ 'This usually is not a problem but you may like to specify/create one. '
308
+ 'Specify it globally for this session using qcodes.config["core"]["backup_location"]="*your backup location*",'
309
+ 'or specify it for this DataSet by specifying backup_location="*your backup location*" in e.g. new_data() or get_data_set()')
310
+
311
+ if os.access(self.backup_location, os.W_OK) is False:
312
+ log.warning(f'Default backup_location, C:/Users/'+os.getlogin()+'/AppData/Local/qcodes-elab/data_backup cannot be used. '
313
+ 'This usually is not a problem but you may like to specify one. '
314
+ 'Specify it globally for this session using qcodes.config["core"]["backup_location"]="*your backup location*",'
315
+ 'or specify it for this DataSet by specifying backup_location="*your backup location*" in e.g. new_data() or get_data_set()')
316
+ else:
317
+ self.backup_location=self.location
318
+ log.warning('No backup_location specified for saving data. This usually is not a problem but you may like to specify one. '
319
+ 'Specify it globally for this session using qcodes.config["core"]["backup_location"]="*your backup location*",'
320
+ 'or specify it for this DataSet by specifying backup_location="*your backup location*" in e.g. new_data() or get_data_set()')
321
+
322
+ self.backup_used=False
323
+ self.writing_skipped=False
324
+ self._backup_warning=False
325
+ self._skipped_warning=False
326
+
327
+ self.finalized=False
328
+
329
+ self.publisher = None
330
+
331
+ self.name=name
332
+
333
+ if self.name is not None:
334
+ forbiddenchars=['[',']','<','>',':','\"','/','\\','|','?','*']
335
+ for char in forbiddenchars:
336
+ if char in self.name:
337
+ raise ValueError(f'{char} cannot be used in a filename on Windows')
338
+
339
+ # TODO: when you change formatter or io (and there's data present)
340
+ # make it all look unsaved
341
+ self.formatter = formatter or self.default_formatter
342
+ self.io = io or self.default_io
343
+
344
+ self.write_period = write_period
345
+ self.last_write = 0
346
+ self.last_store = -1
347
+ self.force_write=force_write
348
+
349
+ self.metadata = {}
350
+ self.uuid = uuid4().hex
351
+
352
+ self.arrays = _PrettyPrintDict()
353
+ if arrays:
354
+ self.action_id_map = self._clean_array_ids(arrays)
355
+ for array in arrays:
356
+ self.add_array(array)
357
+
358
+ if self.arrays:
359
+ for array in self.arrays.values():
360
+ array.init_data()
361
+
362
+ if self.name is not None and isinstance(self.formatter,GNUPlotFormat):
363
+ for group in self.formatter.group_arrays(self.arrays):
364
+ pathlength=len(self.io.base_location+'/'+self.location+'/'+group.name+self.formatter.extension)
365
+ if pathlength>246:
366
+ loc_record={}
367
+ loc_record['name'] = self.name[:-(pathlength-246)]
368
+ self.location = self.location_provider(io, record=loc_record)
369
+ log.warning('DataSetPP filename has been automatically shortened to avoid Windows maximum character limit')
370
+
371
+ # Make this dataset a class attribute so that it can be accessed by other functions, most notably to set its publisher in live plotting.
372
+ DataSetPP.default_dataset = self
373
+
374
+ def sync(self):
375
+ """
376
+ Synchronize this DataSetPP with the DataServer or storage.
377
+
378
+ If this DataSetPP is on the server, asks the server for changes.
379
+ If not, reads the entire DataSetPP from disk.
380
+
381
+ Returns:
382
+ bool: True if this DataSetPP is live on the server
383
+ """
384
+ # TODO: sync implies bidirectional... and it could be!
385
+ # we should keep track of last sync timestamp and last modification
386
+ # so we can tell whether this one, the other one, or both copies have
387
+ # changed (and I guess throw an error if both did? Would be cool if we
388
+ # could find a robust and intuitive way to make modifications to the
389
+ # version on the DataServer from the main copy)
390
+
391
+ # LOCAL DataSetPP - no need to sync just use local data
392
+ return False
393
+
394
+ def fraction_complete(self):
395
+ """
396
+ Get the fraction of this DataSetPP which has data in it.
397
+
398
+ Returns:
399
+ float: the average of all measured (not setpoint) arrays'
400
+ ``fraction_complete()`` values, independent of the individual
401
+ array sizes. If there are no measured arrays, returns zero.
402
+ """
403
+ array_count, total = 0, 0
404
+
405
+ for array in self.arrays.values():
406
+ if not array.is_setpoint:
407
+ array_count += 1
408
+ total += array.fraction_complete()
409
+
410
+ return total / (array_count or 1)
411
+
412
+ def complete(self, delay=1.5):
413
+ """
414
+ Periodically sync the DataSetPP and display percent complete status.
415
+
416
+ Also, each period, execute functions stored in (class attribute)
417
+ ``self.background_functions``. If a function fails, we log its
418
+ traceback and continue on. If any one function fails twice in
419
+ a row, it gets removed.
420
+
421
+ Args:
422
+ delay (float): seconds between iterations. Default 1.5
423
+ """
424
+ log.info(
425
+ 'waiting for DataSetPP <{}> to complete'.format(self.location))
426
+
427
+ failing = {key: False for key in self.background_functions}
428
+
429
+ completed = False
430
+ while True:
431
+ log.info('DataSetPP: {:.0f}% complete'.format(
432
+ self.fraction_complete() * 100))
433
+
434
+ # first check if we're done
435
+ if self.sync() is False:
436
+ completed = True
437
+
438
+ # then even if we *are* done, execute the background functions
439
+ # because we want things like live plotting to get the final data
440
+ for key, fn in list(self.background_functions.items()):
441
+ try:
442
+ log.debug('calling {}: {}'.format(key, repr(fn)))
443
+ fn()
444
+ failing[key] = False
445
+ except Exception:
446
+ log.info(format_exc())
447
+ if failing[key]:
448
+ log.warning(
449
+ 'background function {} failed twice in a row, '
450
+ 'removing it'.format(key))
451
+ del self.background_functions[key]
452
+ failing[key] = True
453
+
454
+ if completed:
455
+ break
456
+
457
+ # but only sleep if we're not already finished
458
+ time.sleep(delay)
459
+
460
+ log.info('DataSetPP <{}> is complete'.format(self.location))
461
+
462
+ def get_changes(self, synced_indices):
463
+ """
464
+ Find changes since the last sync of this DataSetPP.
465
+
466
+ Args:
467
+ synced_indices (dict): ``{array_id: synced_index}`` where
468
+ synced_index is the last flat index which has already
469
+ been synced, for any (usually all) arrays in the DataSetPP.
470
+
471
+ Returns:
472
+ Dict[dict]: keys are ``array_id`` for each array with changes,
473
+ values are dicts as returned by ``DataArray.get_changes``
474
+ and required as kwargs to ``DataArray.apply_changes``.
475
+ Note that not all arrays in ``synced_indices`` need be
476
+ present in the return, only those with changes.
477
+ """
478
+ changes = {}
479
+
480
+ for array_id, synced_index in synced_indices.items():
481
+ array_changes = self.arrays[array_id].get_changes(synced_index)
482
+ if array_changes:
483
+ changes[array_id] = array_changes
484
+
485
+ return changes
486
+
487
+ def add_array(self, data_array):
488
+ """
489
+ Add one DataArray to this DataSetPP, and mark it as part of this DataSetPP.
490
+
491
+ Note: DO NOT just set ``data_set.arrays[id] = data_array``, because
492
+ this will not check if we are overwriting another array, nor set the
493
+ reference back to this DataSetPP, nor that the ``array_id`` in the array
494
+ matches how you're storing it here.
495
+
496
+ Args:
497
+ data_array (DataArray): the new array to add
498
+
499
+ Raises:
500
+ ValueError: if there is already an array with this id here.
501
+ """
502
+ # TODO: mask self.arrays so you *can't* set it directly?
503
+ if data_array.array_id in self.arrays:
504
+ raise ValueError('array_id {} already exists in this '
505
+ 'DataSetPP'.format(data_array.array_id))
506
+ self.arrays[data_array.array_id] = data_array
507
+
508
+ # back-reference to the DataSetPP
509
+ data_array.data_set = self
510
+
511
+ def remove_array(self, array_id):
512
+ """ Remove an array from a dataset
513
+
514
+ Throws an exception when the array specified is refereced by other
515
+ arrays in the dataset.
516
+
517
+ Args:
518
+ array_id (str): array_id of array to be removed
519
+ """
520
+ for a in self.arrays:
521
+ sa = self.arrays[a].set_arrays
522
+ if array_id in [a.array_id for a in sa]:
523
+ raise Exception(
524
+ 'cannot remove array %s as it is referenced by a' % array_id)
525
+ _ = self.arrays.pop(array_id)
526
+ self.action_id_map = self._clean_array_ids(self.arrays.values())
527
+
528
+ def _clean_array_ids(self, arrays):
529
+ """
530
+ replace action_indices tuple with compact string array_ids
531
+ stripping off as much extraneous info as possible
532
+ """
533
+ action_indices = [array.action_indices for array in arrays]
534
+ for array in arrays:
535
+ name = array.full_name
536
+ if array.is_setpoint and name and not name.endswith('_set'):
537
+ name += '_set'
538
+
539
+ array.array_id = name
540
+ array_ids = set([array.array_id for array in arrays])
541
+ for name in array_ids:
542
+ param_arrays = [array for array in arrays
543
+ if array.array_id == name]
544
+ self._clean_param_ids(param_arrays, name)
545
+
546
+ array_ids = [array.array_id for array in arrays]
547
+
548
+ return dict(zip(action_indices, array_ids))
549
+
550
+ def _clean_param_ids(self, arrays, name):
551
+ # strip off as many leading equal indices as possible
552
+ # and append the rest to the back of the name with underscores
553
+ param_action_indices = [list(array.action_indices) for array in arrays]
554
+ while all(len(ai) for ai in param_action_indices):
555
+ if len(set(ai[0] for ai in param_action_indices)) == 1:
556
+ for ai in param_action_indices:
557
+ ai[:1] = []
558
+ else:
559
+ break
560
+ for array, ai in zip(arrays, param_action_indices):
561
+ try:
562
+ array.array_id = name + ''.join('_' + str(ai[0]))
563
+ except:
564
+ array.array_id = name + ''.join('_' + str(i) for i in ai)
565
+
566
+ def store(self, loop_indices, ids_values):
567
+ """
568
+ Insert data into one or more of our DataArrays.
569
+
570
+ Args:
571
+ loop_indices (tuple): the indices within whatever loops we are
572
+ inside. May have fewer dimensions than some of the arrays
573
+ we are inserting into, if the corresponding value makes up
574
+ the remaining dimensionality.
575
+ values (Dict[Union[float, sequence]]): a dict whose keys are
576
+ array_ids, and values are single numbers or entire slices
577
+ to insert into that array.
578
+ """
579
+ for array_id, value in ids_values.items():
580
+ if type(loop_indices) is tuple:
581
+ self.arrays[array_id][loop_indices] = value
582
+ elif loop_indices=='all': # from Measure
583
+ self.arrays[array_id][:] = value
584
+ self.last_store = time.time()
585
+
586
+ if self.publisher is not None:
587
+ self.publisher.store(loop_indices, ids_values, uuid=self.uuid)
588
+
589
+ if (self.write_period is not None and
590
+ time.time() > self.last_write + self.write_period):
591
+ log.debug('Attempting to write')
592
+ self.write()
593
+ self.last_write = time.time()
594
+ # The below could be useful but as it writes at every single
595
+ # step of the loop its too verbose even at debug
596
+ # else:
597
+ # log.debug('.store method: This is not the right time to write')
598
+
599
+ def default_parameter_name(self, paramname='amplitude'):
600
+ """ Return name of default parameter for plotting
601
+
602
+ The default parameter is determined by looking into
603
+ metdata['default_parameter_name']. If this variable is not present,
604
+ then the closest match to the argument paramname is tried.
605
+
606
+ Args:
607
+ paramname (str): Name to match to parameter name
608
+
609
+ Returns:
610
+ name ( Union[str, None] ): name of the default parameter
611
+ """
612
+
613
+ arraynames = self.arrays.keys()
614
+
615
+ # overrule parameter name from the metadata
616
+ if self.metadata.get('default_parameter_name', False):
617
+ paramname = self.metadata['default_parameter_name']
618
+
619
+ # try to return the exact name
620
+ if paramname in arraynames:
621
+ return paramname
622
+
623
+ # try find something similar
624
+ vv = [v for v in arraynames if v.endswith(paramname)]
625
+ if (len(vv) > 0):
626
+ return vv[0]
627
+ vv = [v for v in arraynames if v.startswith(paramname)]
628
+ if (len(vv) > 0):
629
+ return vv[0]
630
+
631
+ # try to get the first non-setpoint array
632
+ vv = [v for v in arraynames if not self.arrays[v].is_setpoint]
633
+ if (len(vv) > 0):
634
+ return sorted(vv)[0]
635
+
636
+ # fallback: any array found
637
+ try:
638
+ name = sorted((list(arraynames)))[0]
639
+ return name
640
+ except IndexError:
641
+ pass
642
+ return None
643
+
644
+ def default_parameter_array(self, paramname='amplitude'):
645
+ """ Return default parameter array
646
+
647
+ Args:
648
+ paramname (str): Name to match to parameter name.
649
+ Defaults to 'amplitude'
650
+
651
+ Returns:
652
+ array (DataArray): array corresponding to the default parameter
653
+
654
+ See also:
655
+ default_parameter_name
656
+
657
+ """
658
+ paramname = self.default_parameter_name(paramname=paramname)
659
+ return getattr(self, paramname, None)
660
+
661
+ def read(self,include_metadata=True):
662
+ """Read the whole DataSetPP from storage, overwriting the local data."""
663
+ if self.location is False:
664
+ return
665
+ self.formatter.read(self,include_metadata)
666
+
667
+ def read_metadata(self):
668
+ """Read the metadata from storage, overwriting the local data."""
669
+ if self.location is False:
670
+ return
671
+ self.formatter.read_metadata(self)
672
+
673
+ def write(self, write_metadata=False, only_complete=True, filename=None, force_rewrite=False):
674
+ """
675
+ Writes updates to the DataSetPP to storage.
676
+
677
+ N.B. it is recommended to call data_set.finalize() when a DataSetPP is
678
+ no longer expected to change to ensure files get closed
679
+
680
+ Args:
681
+ write_metadata (bool): write the metadata to disk
682
+ only_complete (bool): passed on to the match_save_range inside
683
+ self.formatter.write. Used to ensure that all new data gets
684
+ saved even when some columns are strange.
685
+ filename (Optional[str]): The filename (minus extension) to use.
686
+ The file gets saved in the usual location.
687
+ """
688
+ if self.location is False:
689
+ return
690
+ # Only the gnuplot formatter has a "filename" kwarg
691
+ try:
692
+ if force_rewrite or self._backup_warning or self._skipped_warning:
693
+ force_rewrite=True
694
+ else:
695
+ force_rewrite=False
696
+ if isinstance(self.formatter, GNUPlotFormat):
697
+ self.formatter.write(self,
698
+ self.io,
699
+ self.location,
700
+ write_metadata=write_metadata,
701
+ only_complete=only_complete,
702
+ filename=filename,
703
+ force_write=self.force_write,
704
+ force_rewrite=force_rewrite)
705
+ else:
706
+ self.formatter.write(self,
707
+ self.io,
708
+ self.location,
709
+ write_metadata=write_metadata,
710
+ only_complete=only_complete)
711
+ if self._skipped_warning==True or self._backup_warning==True:
712
+ log.warning(f'{datetime.now().replace(microsecond=0)}: Writing data to primary location resumed')
713
+ self._skipped_warning=False
714
+ self._backup_warning=False
715
+ except Exception as e:
716
+ log.warning(f'{datetime.now().replace(microsecond=0)}: Data could not be written to primary location: '+str(e))
717
+ try:
718
+ if isinstance(self.formatter, GNUPlotFormat):
719
+ self.formatter.write(self,
720
+ self.io,
721
+ self.backup_location+f'/{date.today()} #{self.location_provider.counter}/',
722
+ write_metadata=write_metadata,
723
+ only_complete=only_complete,
724
+ filename=filename,
725
+ force_write=self.force_write)
726
+ else:
727
+ self.formatter.write(self,
728
+ self.io,
729
+ self.backup_location+f'/{date.today()} #{self.location_provider.counter}/',
730
+ write_metadata=write_metadata,
731
+ only_complete=only_complete)
732
+
733
+ self.backup_used=True
734
+ self._backup_warning=True
735
+ log.warning(f'{datetime.now().replace(microsecond=0)}: Data written to backup location: {self.backup_location}')
736
+
737
+ except Exception as e:
738
+ log.warning(f'{datetime.now().replace(microsecond=0)}: Data could not be written to backup location: '+str(e))
739
+ self.writing_skipped=True
740
+ self._skipped_warning=True
741
+
742
+ def write_copy(self, path=None, io_manager=None, location=None):
743
+ """
744
+ Write a new complete copy of this DataSetPP to storage.
745
+
746
+ Args:
747
+ path (str, optional): An absolute path on this system to write to.
748
+ If you specify this, you may not include either ``io_manager``
749
+ or ``location``.
750
+
751
+ io_manager (io_manager, optional): A new ``io_manager`` to use with
752
+ either the ``DataSetPP``'s same or a new ``location``.
753
+
754
+ location (str, optional): A new ``location`` to write to, using
755
+ either this ``DataSetPP``'s same or a new ``io_manager``.
756
+ """
757
+ if io_manager is not None or location is not None:
758
+ if path is not None:
759
+ raise TypeError('If you provide io_manager or location '
760
+ 'to write_copy, you may not provide path.')
761
+ if io_manager is None:
762
+ io_manager = self.io
763
+ elif location is None:
764
+ location = self.location
765
+ elif path is not None:
766
+ io_manager = DiskIO(None)
767
+ location = path
768
+ else:
769
+ raise TypeError('You must provide at least one argument '
770
+ 'to write_copy')
771
+
772
+ if location is False:
773
+ raise ValueError('write_copy needs a location, not False')
774
+
775
+ lsi_cache = {}
776
+ mr_cache = {}
777
+ for array_id, array in self.arrays.items():
778
+ lsi_cache[array_id] = array.last_saved_index
779
+ mr_cache[array_id] = array.modified_range
780
+ # array.clear_save() is not enough, we _need_ to set modified_range
781
+ # TODO - identify *when* clear_save is not enough, and fix it
782
+ # so we *can* use it. That said, maybe we will *still* want to
783
+ # use the full array here no matter what, or strip trailing NaNs
784
+ # separately, either here or in formatter.write?
785
+ array.last_saved_index = None
786
+ array.modified_range = (0, array.ndarray.size - 1)
787
+
788
+ try:
789
+ self.formatter.write(self, io_manager, location, force_write=True)
790
+ self.snapshot()
791
+ self.formatter.write_metadata(self, io_manager, location,
792
+ read_first=False)
793
+ finally:
794
+ for array_id, array in self.arrays.items():
795
+ array.last_saved_index = lsi_cache[array_id]
796
+ array.modified_range = mr_cache[array_id]
797
+
798
+ def add_metadata(self, new_metadata):
799
+ """
800
+ Update DataSetPP.metadata with additional data.
801
+
802
+ Args:
803
+ new_metadata (dict): new data to be deep updated into
804
+ the existing metadata
805
+ """
806
+ deep_update(self.metadata, new_metadata)
807
+
808
+ if self.publisher is not None:
809
+ self.publisher.add_metadata(new_metadata, uuid=self.uuid)
810
+
811
+ def save_metadata(self):
812
+ """Evaluate and save the DataSetPP's metadata."""
813
+ if self.publisher is not None:
814
+ self.publisher.add_metadata(self.metadata, uuid=self.uuid)
815
+
816
+ if self.location is not False:
817
+ self.snapshot()
818
+ self.formatter.write_metadata(self, self.io, self.location)
819
+
820
+ def finalize(self, filename=None, write_metadata=True, force_rewrite=False):
821
+ """
822
+ Mark the DataSetPP complete and write any remaining modifications.
823
+
824
+ Also closes the data file(s), if the ``Formatter`` we're using
825
+ supports that.
826
+
827
+ Args:
828
+ filename (Optional[str]): The file name (minus extension) to
829
+ write to. The location of the file is the usual one.
830
+ write_metadata (bool): Whether to save a snapshot. For e.g. dumping
831
+ raw data inside a loop, a snapshot is not wanted.
832
+ """
833
+ log.debug('Finalising the DataSetPP. Writing.')
834
+ # write all new data, not only (to?) complete columns
835
+ self.write(only_complete=False, filename=filename, force_rewrite=force_rewrite)
836
+
837
+ if hasattr(self.formatter, 'close_file'):
838
+ self.formatter.close_file(self)
839
+
840
+ if write_metadata:
841
+ self.save_metadata()
842
+
843
+ if self.publisher is not None:
844
+ self.publisher.finalize(uuid=self.uuid)
845
+
846
+ self.finalized=True
847
+
848
+ def snapshot(self, update=False):
849
+ """JSON state of the DataSetPP."""
850
+ array_snaps = {}
851
+ for array_id, array in self.arrays.items():
852
+ array_snaps[array_id] = array.snapshot(update=update)
853
+
854
+ self.metadata.update({
855
+ '__class__': full_class(self),
856
+ 'location': self.location,
857
+ 'arrays': array_snaps,
858
+ 'formatter': full_class(self.formatter),
859
+ 'io': repr(self.io),
860
+ 'uuid': self.uuid
861
+ })
862
+ return deepcopy(self.metadata)
863
+
864
+ def get_array_metadata(self, array_id):
865
+ """
866
+ Get the metadata for a single contained DataArray.
867
+
868
+ Args:
869
+ array_id (str): the array to get metadata for.
870
+
871
+ Returns:
872
+ dict: metadata for this array.
873
+ """
874
+ try:
875
+ return self.metadata['arrays'][array_id]
876
+ except (AttributeError, KeyError):
877
+ return None
878
+
879
+ def __repr__(self):
880
+ """Rich information about the DataSetPP and contained arrays."""
881
+ out = type(self).__name__ + ':'
882
+
883
+ attrs = [['location', repr(self.location)]]
884
+ attr_template = '\n {:8} = {}'
885
+ for var, val in attrs:
886
+ out += attr_template.format(var, val)
887
+
888
+ arr_info = [['<Type>', '<array_id>', '<array.shape>']]
889
+
890
+ if hasattr(self, 'action_id_map'):
891
+ id_items = [
892
+ item for index, item in sorted(self.action_id_map.items())]
893
+ else:
894
+ id_items = self.arrays.keys()
895
+
896
+ for array_id in id_items:
897
+ array = self.arrays[array_id]
898
+ setp = 'Setpoint' if array.is_setpoint else 'Measured'
899
+ array_id = array_id or 'None'
900
+ arr_info.append([setp, array_id, repr(array.shape)])
901
+
902
+ column_lengths = [max(len(row[i]) for row in arr_info)
903
+ for i in range(len(arr_info[0]))]
904
+ out_template = ('\n '
905
+ '{info[0]:{lens[0]}} | {info[1]:{lens[1]}} | '
906
+ '{info[2]}')
907
+
908
+ for arr_info_i in arr_info:
909
+ out += out_template.format(info=arr_info_i, lens=column_lengths)
910
+
911
+ return out
912
+
913
+ class _PrettyPrintDict(dict):
914
+ """
915
+ simple wrapper for a dict to repr its items on separate lines
916
+ with a bit of indentation
917
+ """
918
+
919
+ def __repr__(self):
920
+ body = '\n '.join([repr(k) + ': ' + self._indent(repr(v))
921
+ for k, v in self.items()])
922
+ return '{\n ' + body + '\n}'
923
+
924
+ def _indent(self, s):
925
+ lines = s.split('\n')
926
+ return '\n '.join(lines)