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.
- qcodespp/__init__.py +77 -0
- qcodespp/actions.py +237 -0
- qcodespp/data/__init__.py +0 -0
- qcodespp/data/data_array.py +555 -0
- qcodespp/data/data_set.py +926 -0
- qcodespp/data/format.py +338 -0
- qcodespp/data/gnuplot_format.py +425 -0
- qcodespp/data/hdf5_format.py +537 -0
- qcodespp/data/hdf5_format_hickle.py +63 -0
- qcodespp/data/io.py +241 -0
- qcodespp/data/location.py +171 -0
- qcodespp/instrument_drivers/Keysight/Keysight_34450A.py +9 -0
- qcodespp/instrument_drivers/Keysight/__init__.py +0 -0
- qcodespp/instrument_drivers/Keysight/private/Keysight_344xxA.py +456 -0
- qcodespp/instrument_drivers/Keysight/private/__init__.py +0 -0
- qcodespp/instrument_drivers/Lakeshore/Model_340.py +483 -0
- qcodespp/instrument_drivers/Lakeshore/__init__.py +0 -0
- qcodespp/instrument_drivers/QDevil/159fit_params_high_latest.json +386 -0
- qcodespp/instrument_drivers/QDevil/159fit_params_low_latest.json +386 -0
- qcodespp/instrument_drivers/QDevil/QDAC2.py +3229 -0
- qcodespp/instrument_drivers/QDevil/QDAC2_Array.py +300 -0
- qcodespp/instrument_drivers/QDevil/QSwitch_elab.py +652 -0
- qcodespp/instrument_drivers/QDevil/__init__.py +0 -0
- qcodespp/instrument_drivers/ZI/ZIHF2LI.py +415 -0
- qcodespp/instrument_drivers/ZI/ZIMFLI.py +764 -0
- qcodespp/instrument_drivers/ZI/ZIMFLI_sync.py +108 -0
- qcodespp/instrument_drivers/ZI/ZIUHFLI.py +2179 -0
- qcodespp/instrument_drivers/ZI/__init__.py +1 -0
- qcodespp/instrument_drivers/__init__.py +0 -0
- qcodespp/instrument_drivers/cryogenic/CryogenicSMS120C.py +599 -0
- qcodespp/instrument_drivers/cryogenic/__init__.py +0 -0
- qcodespp/instrument_drivers/instrument_mocks.py +379 -0
- qcodespp/instrument_drivers/multiplexer_instrument.py +287 -0
- qcodespp/instrument_drivers/national_instruments/PXI_4461.py +451 -0
- qcodespp/instrument_drivers/national_instruments/PXI_4462.py +290 -0
- qcodespp/instrument_drivers/national_instruments/PXI_6259.py +436 -0
- qcodespp/instrument_drivers/national_instruments/PXI_6733.py +230 -0
- qcodespp/instrument_drivers/national_instruments/PXI_6733_e.py +243 -0
- qcodespp/instrument_drivers/national_instruments/__init__.py +0 -0
- qcodespp/instrument_drivers/oxford/__init__.py +0 -0
- qcodespp/instrument_drivers/oxford/mercuryiPS_serial.py +658 -0
- qcodespp/instrument_drivers/oxford/triton.py +434 -0
- qcodespp/instrument_drivers/tektronix/Keithley_2000_Scan.py +65 -0
- qcodespp/instrument_drivers/tektronix/Keithley_2182a.py +190 -0
- qcodespp/instrument_drivers/tektronix/Keithley_2400.py +158 -0
- qcodespp/instrument_drivers/tektronix/Keithley_2450.py +242 -0
- qcodespp/instrument_drivers/tektronix/Keithley_2700.py +397 -0
- qcodespp/instrument_drivers/tektronix/Keithley_6500.py +161 -0
- qcodespp/instrument_drivers/tektronix/__init__.py +0 -0
- qcodespp/instrument_drivers/yokogawa/GS200.py +513 -0
- qcodespp/instrument_drivers/yokogawa/__init__.py +0 -0
- qcodespp/loops.py +1342 -0
- qcodespp/measure.py +310 -0
- qcodespp/parameters.py +311 -0
- qcodespp/plotting/RemotePlot.py +469 -0
- qcodespp/plotting/RemotePlotClient.py +367 -0
- qcodespp/plotting/RemoteQtPlotWidgets.py +871 -0
- qcodespp/plotting/__init__.py +0 -0
- qcodespp/plotting/analysis_tools.py +195 -0
- qcodespp/plotting/colors.py +135 -0
- qcodespp/plotting/offline/__init__.py +0 -0
- qcodespp/plotting/offline/datatypes.py +999 -0
- qcodespp/plotting/offline/design.py +906 -0
- qcodespp/plotting/offline/design.ui +1578 -0
- qcodespp/plotting/offline/editor.py +2966 -0
- qcodespp/plotting/offline/filters.py +561 -0
- qcodespp/plotting/offline/fits.py +1171 -0
- qcodespp/plotting/offline/helpers.py +288 -0
- qcodespp/plotting/offline/iconGadget.png +0 -0
- qcodespp/plotting/offline/main.py +55 -0
- qcodespp/plotting/offline/popupwindows.py +1848 -0
- qcodespp/plotting/offline/qcodes_pp_extension.py +328 -0
- qcodespp/plotting/offline/sidebars.py +1108 -0
- qcodespp/station.py +290 -0
- qcodespp/utils/__init__.py +0 -0
- qcodespp/utils/helpers.py +618 -0
- qcodespp/utils/visa_helpers.py +61 -0
- qcodespp/version.py +12 -0
- qcodespp-0.1.0.dist-info/METADATA +91 -0
- qcodespp-0.1.0.dist-info/RECORD +82 -0
- qcodespp-0.1.0.dist-info/WHEEL +4 -0
- 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)
|