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.
- qcodes/__init__.py +65 -0
- qcodes/actions.py +207 -0
- qcodes/config/__init__.py +2 -0
- qcodes/config/config.py +396 -0
- qcodes/config/qcodesrc.json +11 -0
- qcodes/config/qcodesrc_schema.json +55 -0
- qcodes/data/__init__.py +0 -0
- qcodes/data/data_array.py +520 -0
- qcodes/data/data_set.py +803 -0
- qcodes/data/format.py +309 -0
- qcodes/data/gnuplot_format.py +366 -0
- qcodes/data/hdf5_format.py +385 -0
- qcodes/data/io.py +266 -0
- qcodes/data/location.py +166 -0
- qcodes/data/manager.py +163 -0
- qcodes/instrument/__init__.py +0 -0
- qcodes/instrument/base.py +630 -0
- qcodes/instrument/function.py +134 -0
- qcodes/instrument/ip.py +236 -0
- qcodes/instrument/metaclass.py +45 -0
- qcodes/instrument/mock.py +278 -0
- qcodes/instrument/parameter.py +1001 -0
- qcodes/instrument/remote.py +529 -0
- qcodes/instrument/server.py +189 -0
- qcodes/instrument/sweep_values.py +286 -0
- qcodes/instrument/visa.py +198 -0
- qcodes/instrument_drivers/AlazarTech/ATS.py +1001 -0
- qcodes/instrument_drivers/AlazarTech/ATS9870.py +266 -0
- qcodes/instrument_drivers/AlazarTech/ATS_acquisition_controllers.py +143 -0
- qcodes/instrument_drivers/AlazarTech/__init__.py +0 -0
- qcodes/instrument_drivers/QuTech/IVVI.py +390 -0
- qcodes/instrument_drivers/QuTech/__init__.py +0 -0
- qcodes/instrument_drivers/__init__.py +0 -0
- qcodes/instrument_drivers/agilent/Agilent_34400A.py +155 -0
- qcodes/instrument_drivers/agilent/E8527D.py +79 -0
- qcodes/instrument_drivers/agilent/HP33210A.py +104 -0
- qcodes/instrument_drivers/agilent/__init__.py +0 -0
- qcodes/instrument_drivers/agilent/test_suite.py +96 -0
- qcodes/instrument_drivers/ithaco/Ithaco_1211.py +110 -0
- qcodes/instrument_drivers/ithaco/__init__.py +0 -0
- qcodes/instrument_drivers/oxford/__init__.py +0 -0
- qcodes/instrument_drivers/oxford/mercuryiPS.py +358 -0
- qcodes/instrument_drivers/oxford/triton.py +227 -0
- qcodes/instrument_drivers/rigol/DG4000.py +591 -0
- qcodes/instrument_drivers/rigol/__init__.py +0 -0
- qcodes/instrument_drivers/rohde_schwarz/SGS100A.py +99 -0
- qcodes/instrument_drivers/rohde_schwarz/ZNB20.py +154 -0
- qcodes/instrument_drivers/rohde_schwarz/__init__.py +0 -0
- qcodes/instrument_drivers/signal_hound/USB_SA124B.py +567 -0
- qcodes/instrument_drivers/signal_hound/__init__.py +0 -0
- qcodes/instrument_drivers/stanford_research/SR560.py +116 -0
- qcodes/instrument_drivers/stanford_research/SR830.py +314 -0
- qcodes/instrument_drivers/stanford_research/SR865.py +305 -0
- qcodes/instrument_drivers/stanford_research/__init__.py +0 -0
- qcodes/instrument_drivers/tektronix/AWG5014.py +1228 -0
- qcodes/instrument_drivers/tektronix/AWG520.py +659 -0
- qcodes/instrument_drivers/tektronix/Keithley_2000.py +195 -0
- qcodes/instrument_drivers/tektronix/Keithley_2600.py +83 -0
- qcodes/instrument_drivers/tektronix/Keithley_2700.py +395 -0
- qcodes/instrument_drivers/tektronix/__init__.py +0 -0
- qcodes/instrument_drivers/test.py +94 -0
- qcodes/instrument_drivers/weinschel/Weinschel_8320.py +21 -0
- qcodes/instrument_drivers/weinschel/__init__.py +0 -0
- qcodes/instrument_drivers/weinschel/test_suite.py +27 -0
- qcodes/loops.py +1009 -0
- qcodes/measure.py +155 -0
- qcodes/plots/__init__.py +0 -0
- qcodes/plots/base.py +280 -0
- qcodes/plots/colors.py +135 -0
- qcodes/plots/pyqtgraph.py +384 -0
- qcodes/plots/qcmatplotlib.py +206 -0
- qcodes/process/__init__.py +0 -0
- qcodes/process/helpers.py +60 -0
- qcodes/process/qcodes_process.py +72 -0
- qcodes/process/server.py +395 -0
- qcodes/process/stream_queue.py +152 -0
- qcodes/station.py +178 -0
- qcodes/test.py +119 -0
- qcodes/tests/__init__.py +0 -0
- qcodes/tests/common.py +7 -0
- qcodes/tests/data_mocks.py +173 -0
- qcodes/tests/instrument_mocks.py +327 -0
- qcodes/tests/py35_syntax.py +13 -0
- qcodes/tests/test_combined_par.py +156 -0
- qcodes/tests/test_command.py +120 -0
- qcodes/tests/test_config.py +222 -0
- qcodes/tests/test_data.py +668 -0
- qcodes/tests/test_deferred_operations.py +105 -0
- qcodes/tests/test_driver_testcase.py +58 -0
- qcodes/tests/test_format.py +414 -0
- qcodes/tests/test_hdf5formatter.py +333 -0
- qcodes/tests/test_helpers.py +630 -0
- qcodes/tests/test_instrument.py +981 -0
- qcodes/tests/test_instrument_server.py +156 -0
- qcodes/tests/test_json.py +48 -0
- qcodes/tests/test_location_provider.py +109 -0
- qcodes/tests/test_loop.py +825 -0
- qcodes/tests/test_measure.py +49 -0
- qcodes/tests/test_metadata.py +49 -0
- qcodes/tests/test_multiprocessing.py +471 -0
- qcodes/tests/test_nested_attrs.py +100 -0
- qcodes/tests/test_parameter.py +151 -0
- qcodes/tests/test_plots.py +44 -0
- qcodes/tests/test_sweep_values.py +159 -0
- qcodes/tests/test_validators.py +482 -0
- qcodes/tests/test_visa.py +184 -0
- qcodes/utils/__init__.py +0 -0
- qcodes/utils/command.py +176 -0
- qcodes/utils/deferred_operations.py +276 -0
- qcodes/utils/helpers.py +417 -0
- qcodes/utils/metadata.py +42 -0
- qcodes/utils/nested_attrs.py +177 -0
- qcodes/utils/reload_code.py +98 -0
- qcodes/utils/threading.py +72 -0
- qcodes/utils/timing.py +89 -0
- qcodes/utils/validators.py +393 -0
- qcodes/version.py +1 -0
- qcodes/widgets/__init__.py +0 -0
- qcodes/widgets/display.py +47 -0
- qcodes/widgets/widgets.css +77 -0
- qcodes/widgets/widgets.js +237 -0
- qcodes/widgets/widgets.py +215 -0
- qcodes-0.1.0.dist-info/DESCRIPTION.rst +165 -0
- qcodes-0.1.0.dist-info/METADATA +194 -0
- qcodes-0.1.0.dist-info/RECORD +128 -0
- qcodes-0.1.0.dist-info/WHEEL +5 -0
- qcodes-0.1.0.dist-info/metadata.json +1 -0
- qcodes-0.1.0.dist-info/top_level.txt +1 -0
qcodes/data/format.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
from collections import namedtuple
|
|
2
|
+
from traceback import format_exc
|
|
3
|
+
from operator import attrgetter
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Formatter:
|
|
8
|
+
"""
|
|
9
|
+
Data file formatters
|
|
10
|
+
|
|
11
|
+
Formatters translate between DataSets and data files.
|
|
12
|
+
|
|
13
|
+
Each Formatter is expected to implement writing methods:
|
|
14
|
+
- ``write``: to write the ``DataArray``s
|
|
15
|
+
- ``write_metadata``: to write the metadata JSON structure
|
|
16
|
+
|
|
17
|
+
Optionally, if this Formatter keeps the data file(s) open
|
|
18
|
+
between write calls, it may implement:
|
|
19
|
+
- ``close_file``: to perform any final cleanup and release the
|
|
20
|
+
file and any other resources.
|
|
21
|
+
|
|
22
|
+
and reading methods:
|
|
23
|
+
- ``read`` or ``read_one_file`` to reconstruct the ``DataArray``s, either
|
|
24
|
+
all at once (``read``) or one file at a time, supplied by the base class
|
|
25
|
+
``read`` method that loops over all data files at the correct location.
|
|
26
|
+
|
|
27
|
+
- ``read_metadata``: to reload saved metadata. If a subclass overrides
|
|
28
|
+
``read``, this method should call ``read_metadata``, but keep it also
|
|
29
|
+
as a separate method because it occasionally gets called independently.
|
|
30
|
+
|
|
31
|
+
All of these methods accept a ``data_set`` argument, which should be a
|
|
32
|
+
``DataSet`` object. Even if you are loading a new data set from disk, this
|
|
33
|
+
object should already have attributes:
|
|
34
|
+
io: an IO manager (see qcodes.data.io)
|
|
35
|
+
location: a string, like a file path, that identifies the DataSet and
|
|
36
|
+
tells the IO manager where to store it
|
|
37
|
+
arrays: a dict of ``{array_id:DataArray}`` to read into.
|
|
38
|
+
- read will create entries that don't yet exist.
|
|
39
|
+
- write will write ALL DataArrays in the DataSet, using
|
|
40
|
+
last_saved_index and modified_range, as well as whether or not
|
|
41
|
+
it found the specified file, to determine how much to write.
|
|
42
|
+
"""
|
|
43
|
+
ArrayGroup = namedtuple('ArrayGroup', 'shape set_arrays data name')
|
|
44
|
+
|
|
45
|
+
def write(self, data_set, io_manager, location):
|
|
46
|
+
"""
|
|
47
|
+
Write the DataSet to storage.
|
|
48
|
+
|
|
49
|
+
Subclasses must override this method.
|
|
50
|
+
|
|
51
|
+
It is up to the Formatter to decide when to overwrite completely,
|
|
52
|
+
and when to just append or otherwise update the file(s).
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
data_set (DataSet): the data we are writing.
|
|
56
|
+
io_manager (io_manager): base physical location to write to.
|
|
57
|
+
location (str): the file location within the io_manager.
|
|
58
|
+
"""
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
|
|
61
|
+
def read(self, data_set):
|
|
62
|
+
"""
|
|
63
|
+
Read the entire ``DataSet``.
|
|
64
|
+
|
|
65
|
+
Find all files matching ``data_set.location`` (using io_manager.list)
|
|
66
|
+
and call ``read_one_file`` on each. Subclasses may either override
|
|
67
|
+
this method (if they use only one file or want to do their own
|
|
68
|
+
searching) or override ``read_one_file`` to use the search and
|
|
69
|
+
initialization functionality defined here.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
data_set (DataSet): the data to read into. Should already have
|
|
73
|
+
attributes ``io`` (an io manager), ``location`` (string),
|
|
74
|
+
and ``arrays`` (dict of ``{array_id: array}``, can be empty
|
|
75
|
+
or can already have some or all of the arrays present, they
|
|
76
|
+
expect to be overwritten)
|
|
77
|
+
"""
|
|
78
|
+
io_manager = data_set.io
|
|
79
|
+
location = data_set.location
|
|
80
|
+
|
|
81
|
+
data_files = io_manager.list(location)
|
|
82
|
+
if not data_files:
|
|
83
|
+
raise IOError('no data found at ' + location)
|
|
84
|
+
|
|
85
|
+
# in case the DataArrays exist but haven't been initialized
|
|
86
|
+
for array in data_set.arrays.values():
|
|
87
|
+
if array.ndarray is None:
|
|
88
|
+
array.init_data()
|
|
89
|
+
|
|
90
|
+
self.read_metadata(data_set)
|
|
91
|
+
|
|
92
|
+
ids_read = set()
|
|
93
|
+
for fn in data_files:
|
|
94
|
+
with io_manager.open(fn, 'r') as f:
|
|
95
|
+
try:
|
|
96
|
+
self.read_one_file(data_set, f, ids_read)
|
|
97
|
+
except ValueError:
|
|
98
|
+
logging.warning('error reading file ' + fn)
|
|
99
|
+
logging.warning(format_exc())
|
|
100
|
+
|
|
101
|
+
def write_metadata(self, data_set, io_manager, location, read_first=True):
|
|
102
|
+
"""
|
|
103
|
+
Write the metadata for this DataSet to storage.
|
|
104
|
+
|
|
105
|
+
Subclasses must override this method.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
data_set (DataSet): the data we are writing.
|
|
109
|
+
io_manager (io_manager): base physical location to write to.
|
|
110
|
+
location (str): the file location within the io_manager.
|
|
111
|
+
read_first (bool, optional): whether to first look for previously
|
|
112
|
+
saved metadata that may contain more information than the local
|
|
113
|
+
copy.
|
|
114
|
+
"""
|
|
115
|
+
raise NotImplementedError
|
|
116
|
+
|
|
117
|
+
def read_metadata(self, data_set):
|
|
118
|
+
"""
|
|
119
|
+
Read the metadata from this DataSet from storage.
|
|
120
|
+
|
|
121
|
+
Subclasses must override this method.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
data_set (DataSet): the data to read metadata into
|
|
125
|
+
"""
|
|
126
|
+
raise NotImplementedError
|
|
127
|
+
|
|
128
|
+
def read_one_file(self, data_set, f, ids_read):
|
|
129
|
+
"""
|
|
130
|
+
Read data from a single file into a ``DataSet``.
|
|
131
|
+
|
|
132
|
+
Formatter subclasses that break a DataSet into multiple data files may
|
|
133
|
+
choose to override either this method, which handles one file at a
|
|
134
|
+
time, or ``read`` which finds matching files on its own.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
data_set (DataSet): the data we are reading into.
|
|
138
|
+
|
|
139
|
+
f (file-like): a file-like object to read from, as provided by
|
|
140
|
+
``io_manager.open``.
|
|
141
|
+
|
|
142
|
+
ids_read (set): ``array_id``s that we have already read.
|
|
143
|
+
When you read an array, check that it's not in this set (except
|
|
144
|
+
setpoints, which can be in several files with different inner
|
|
145
|
+
loops) then add it to the set so other files know it should not
|
|
146
|
+
be read again.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ValueError: if a duplicate array_id of measured data is found
|
|
150
|
+
"""
|
|
151
|
+
raise NotImplementedError
|
|
152
|
+
|
|
153
|
+
def match_save_range(self, group, file_exists, only_complete=True):
|
|
154
|
+
"""
|
|
155
|
+
Find the save range that will joins all changes in an array group.
|
|
156
|
+
|
|
157
|
+
Matches all full-sized arrays: the data arrays plus the inner loop
|
|
158
|
+
setpoint array.
|
|
159
|
+
|
|
160
|
+
Note: if an outer loop has changed values (without the inner
|
|
161
|
+
loop or measured data changing) we won't notice it here. We assume
|
|
162
|
+
that before an iteration of the inner loop starts, the outer loop
|
|
163
|
+
setpoint gets set and then does not change later.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
group (Formatter.ArrayGroup): a ``namedtuple`` containing the
|
|
167
|
+
arrays that go together in one file, as tuple ``group.data``.
|
|
168
|
+
|
|
169
|
+
file_exists (bool): Does this file already exist? If True, and
|
|
170
|
+
all arrays in the group agree on ``last_saved_index``, we
|
|
171
|
+
assume the file has been written up to this index and we can
|
|
172
|
+
append to it. Otherwise we will set the returned range to start
|
|
173
|
+
from zero (so if the file does exist, it gets completely
|
|
174
|
+
overwritten).
|
|
175
|
+
|
|
176
|
+
only_complete (bool): Should we write all available new data,
|
|
177
|
+
or only complete rows? If True, we write only the range of
|
|
178
|
+
array indices which all arrays in the group list as modified,
|
|
179
|
+
so that future writes will be able to do a clean append to
|
|
180
|
+
the data file as more data arrives.
|
|
181
|
+
Default True.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Tuple(int, int): the first and last raveled indices that should
|
|
185
|
+
be saved.
|
|
186
|
+
"""
|
|
187
|
+
inner_setpoint = group.set_arrays[-1]
|
|
188
|
+
full_dim_data = (inner_setpoint, ) + group.data
|
|
189
|
+
|
|
190
|
+
# always return None if there are no modifications,
|
|
191
|
+
# even if there are last_saved_index inconsistencies
|
|
192
|
+
# so we don't do extra writing just to reshape the file
|
|
193
|
+
for array in full_dim_data:
|
|
194
|
+
if array.modified_range:
|
|
195
|
+
break
|
|
196
|
+
else:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
last_saved_index = inner_setpoint.last_saved_index
|
|
200
|
+
|
|
201
|
+
if last_saved_index is None or not file_exists:
|
|
202
|
+
return self._match_save_range_whole_file(
|
|
203
|
+
full_dim_data, only_complete)
|
|
204
|
+
|
|
205
|
+
# force overwrite if inconsistent last_saved_index
|
|
206
|
+
for array in group.data:
|
|
207
|
+
if array.last_saved_index != last_saved_index:
|
|
208
|
+
return self._match_save_range_whole_file(
|
|
209
|
+
full_dim_data, only_complete)
|
|
210
|
+
|
|
211
|
+
return self._match_save_range_incremental(
|
|
212
|
+
full_dim_data, last_saved_index, only_complete)
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _match_save_range_whole_file(arrays, only_complete):
|
|
216
|
+
max_save = None
|
|
217
|
+
agg = (min if only_complete else max)
|
|
218
|
+
for array in arrays:
|
|
219
|
+
array_max = array.last_saved_index
|
|
220
|
+
if array_max is None:
|
|
221
|
+
array_max = -1
|
|
222
|
+
mr = array.modified_range
|
|
223
|
+
if mr:
|
|
224
|
+
array_max = max(array_max, mr[1])
|
|
225
|
+
max_save = (array_max if max_save is None else
|
|
226
|
+
agg(max_save, array_max))
|
|
227
|
+
|
|
228
|
+
if max_save >= 0:
|
|
229
|
+
return (0, max_save)
|
|
230
|
+
else:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def _match_save_range_incremental(arrays, last_saved_index, only_complete):
|
|
235
|
+
mod_ranges = []
|
|
236
|
+
for array in arrays:
|
|
237
|
+
mr = array.modified_range
|
|
238
|
+
if not mr:
|
|
239
|
+
if only_complete:
|
|
240
|
+
return None
|
|
241
|
+
else:
|
|
242
|
+
continue
|
|
243
|
+
mod_ranges.append(mr)
|
|
244
|
+
|
|
245
|
+
mod_range = mod_ranges[0]
|
|
246
|
+
agg = (min if only_complete else max)
|
|
247
|
+
for mr in mod_ranges[1:]:
|
|
248
|
+
mod_range = (min(mod_range[0], mr[0]),
|
|
249
|
+
agg(mod_range[1], mr[1]))
|
|
250
|
+
|
|
251
|
+
if last_saved_index >= mod_range[1]:
|
|
252
|
+
return (0, last_saved_index)
|
|
253
|
+
elif last_saved_index >= mod_range[0]:
|
|
254
|
+
return (0, mod_range[1])
|
|
255
|
+
else:
|
|
256
|
+
return (last_saved_index + 1, mod_range[1])
|
|
257
|
+
|
|
258
|
+
def group_arrays(self, arrays):
|
|
259
|
+
"""
|
|
260
|
+
Find the sets of arrays which share all the same setpoint arrays.
|
|
261
|
+
|
|
262
|
+
Some Formatters use this grouping to determine which arrays to save
|
|
263
|
+
together in one file.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
arrays (Dict[DataArray]): all the arrays in a DataSet
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List[Formatter.ArrayGroup]: namedtuples giving:
|
|
270
|
+
shape (Tuple[int]): dimensions as in numpy
|
|
271
|
+
set_arrays (Tuple[DataArray]): the setpoints of this group
|
|
272
|
+
data (Tuple[DataArray]): measured arrays in this group
|
|
273
|
+
name (str): a unique name of this group, obtained by joining
|
|
274
|
+
the setpoint array ids.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
set_array_sets = tuple(set(array.set_arrays
|
|
278
|
+
for array in arrays.values()))
|
|
279
|
+
all_set_arrays = set()
|
|
280
|
+
for set_array_set in set_array_sets:
|
|
281
|
+
all_set_arrays.update(set_array_set)
|
|
282
|
+
|
|
283
|
+
grouped_data = [[] for _ in set_array_sets]
|
|
284
|
+
|
|
285
|
+
for array in arrays.values():
|
|
286
|
+
i = set_array_sets.index(array.set_arrays)
|
|
287
|
+
if array not in all_set_arrays: # array.set_arrays[-1] != array:
|
|
288
|
+
# don't include the setpoint array itself in the data
|
|
289
|
+
grouped_data[i].append(array)
|
|
290
|
+
|
|
291
|
+
out = []
|
|
292
|
+
id_getter = attrgetter('array_id')
|
|
293
|
+
for set_arrays, data in zip(set_array_sets, grouped_data):
|
|
294
|
+
leni = len(set_arrays)
|
|
295
|
+
if not data and any(1 for other_set_arrays in set_array_sets if
|
|
296
|
+
len(other_set_arrays) > leni and
|
|
297
|
+
other_set_arrays[:leni] == set_arrays):
|
|
298
|
+
# this is an outer loop that doesn't have any data of its own,
|
|
299
|
+
# so skip it.
|
|
300
|
+
# Inner-loop setpoints with no data is weird (we set values
|
|
301
|
+
# but didn't measure anything there?) but we should keep it.
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
group_name = '_'.join(sai.array_id for sai in set_arrays)
|
|
305
|
+
out.append(self.ArrayGroup(shape=set_arrays[-1].shape,
|
|
306
|
+
set_arrays=set_arrays,
|
|
307
|
+
data=tuple(sorted(data, key=id_getter)),
|
|
308
|
+
name=group_name))
|
|
309
|
+
return out
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import re
|
|
3
|
+
import math
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from qcodes.utils.helpers import deep_update, NumpyJSONEncoder
|
|
7
|
+
from .data_array import DataArray
|
|
8
|
+
from .format import Formatter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GNUPlotFormat(Formatter):
|
|
12
|
+
"""
|
|
13
|
+
Saves data in one or more gnuplot-format files. We make one file for
|
|
14
|
+
each set of matching dependent variables in the loop.
|
|
15
|
+
|
|
16
|
+
options:
|
|
17
|
+
|
|
18
|
+
extension (default 'dat'): file extension for data files
|
|
19
|
+
|
|
20
|
+
terminator (default '\\n'): newline character(s) to use on write
|
|
21
|
+
not used for reading, we will read any combination of \\r and \\n
|
|
22
|
+
|
|
23
|
+
separator (default '\\t'): field (column) separator, must be whitespace.
|
|
24
|
+
Only used for writing, we will read with any whitespace separation.
|
|
25
|
+
|
|
26
|
+
comment (default '# '): lines starting with this are not data
|
|
27
|
+
Comments are written with this full string, and identified on read
|
|
28
|
+
by just the string after stripping whitespace.
|
|
29
|
+
|
|
30
|
+
number_format (default 'g'): from the format mini-language, how to
|
|
31
|
+
format numeric data into a string
|
|
32
|
+
|
|
33
|
+
always_nest (default True): whether to always make a folder for files
|
|
34
|
+
or just make a single data file if all data has the same setpoints
|
|
35
|
+
|
|
36
|
+
These files are basically tab-separated values, but any quantity of
|
|
37
|
+
any whitespace characters is accepted.
|
|
38
|
+
|
|
39
|
+
Each row represents one setting of the setpoint variable(s)
|
|
40
|
+
the setpoint variable(s) are in the first column(s)
|
|
41
|
+
measured variable(s) come after.
|
|
42
|
+
|
|
43
|
+
The data is preceded by comment lines (starting with #).
|
|
44
|
+
We use three:
|
|
45
|
+
- one for the variable name
|
|
46
|
+
- the (longer) axis label, in quotes so a label can contain whitespace.
|
|
47
|
+
- for each dependent var, the (max) number of points in that dimension
|
|
48
|
+
(this also tells us how many dependent vars we have in this file)
|
|
49
|
+
|
|
50
|
+
# id1\tid2\t\id3...
|
|
51
|
+
# "label1"\t"label2"\t"label3"...
|
|
52
|
+
# 100\t250
|
|
53
|
+
1\t2\t3...
|
|
54
|
+
2\t3\t4...
|
|
55
|
+
|
|
56
|
+
For data of 2 dependent variables, gnuplot puts each inner loop into one
|
|
57
|
+
block, then increments the outer loop in the next block, separated by a
|
|
58
|
+
blank line.
|
|
59
|
+
|
|
60
|
+
We extend this to an arbitrary quantity of dependent variables by using
|
|
61
|
+
one blank line for each loop level that resets. (gnuplot *does* seem to
|
|
62
|
+
use 2 blank lines sometimes, to denote a whole new dataset, which sort
|
|
63
|
+
of corresponds to our situation.)
|
|
64
|
+
"""
|
|
65
|
+
def __init__(self, extension='dat', terminator='\n', separator='\t',
|
|
66
|
+
comment='# ', number_format='g', metadata_file=None):
|
|
67
|
+
self.metadata_file = metadata_file or 'snapshot.json'
|
|
68
|
+
# file extension: accept either with or without leading dot
|
|
69
|
+
self.extension = '.' + extension.lstrip('.')
|
|
70
|
+
|
|
71
|
+
# line terminator (only used for writing; will read any \r\n combo)
|
|
72
|
+
if terminator not in ('\r', '\n', '\r\n'):
|
|
73
|
+
raise ValueError(
|
|
74
|
+
r'GNUPlotFormat terminator must be \r, \n, or \r\n')
|
|
75
|
+
self.terminator = terminator
|
|
76
|
+
|
|
77
|
+
# field separator (only used for writing; will read any whitespace)
|
|
78
|
+
if not re.fullmatch(r'\s+', separator):
|
|
79
|
+
raise ValueError('GNUPlotFormat separator must be whitespace')
|
|
80
|
+
self.separator = separator
|
|
81
|
+
|
|
82
|
+
# beginning of a comment line. (when reading, just checks the
|
|
83
|
+
# non-whitespace character(s) of comment
|
|
84
|
+
self.comment = comment
|
|
85
|
+
self.comment_chars = comment.rstrip()
|
|
86
|
+
if not self.comment_chars:
|
|
87
|
+
raise ValueError('comment must have some non-whitespace')
|
|
88
|
+
self.comment_len = len(self.comment_chars)
|
|
89
|
+
|
|
90
|
+
# number format (only used for writing; will read any number)
|
|
91
|
+
self.number_format = '{:' + number_format + '}'
|
|
92
|
+
|
|
93
|
+
def read_one_file(self, data_set, f, ids_read):
|
|
94
|
+
"""
|
|
95
|
+
Called by Formatter.read to bring one data file into
|
|
96
|
+
a DataSet. Setpoint data may be duplicated across multiple files,
|
|
97
|
+
but each measured DataArray must only map to one file.
|
|
98
|
+
|
|
99
|
+
data_set: the DataSet we are reading into
|
|
100
|
+
f: a file-like object to read from
|
|
101
|
+
ids_read: a `set` of array_ids that we have already read.
|
|
102
|
+
when you read an array, check that it's not in this set (except
|
|
103
|
+
setpoints, which can be in several files with different inner loop)
|
|
104
|
+
then add it to the set so other files know not to read it again
|
|
105
|
+
"""
|
|
106
|
+
if not f.name.endswith(self.extension):
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
arrays = data_set.arrays
|
|
110
|
+
ids = self._read_comment_line(f).split()
|
|
111
|
+
labels = self._get_labels(self._read_comment_line(f))
|
|
112
|
+
shape = tuple(map(int, self._read_comment_line(f).split()))
|
|
113
|
+
ndim = len(shape)
|
|
114
|
+
|
|
115
|
+
set_arrays = ()
|
|
116
|
+
data_arrays = []
|
|
117
|
+
indexed_ids = list(enumerate(ids))
|
|
118
|
+
|
|
119
|
+
for i, array_id in indexed_ids[:ndim]:
|
|
120
|
+
snap = data_set.get_array_metadata(array_id)
|
|
121
|
+
|
|
122
|
+
# setpoint arrays
|
|
123
|
+
set_shape = shape[: i + 1]
|
|
124
|
+
if array_id in arrays:
|
|
125
|
+
set_array = arrays[array_id]
|
|
126
|
+
if set_array.shape != set_shape:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
'shapes do not match for set array: ' + array_id)
|
|
129
|
+
if array_id not in ids_read:
|
|
130
|
+
# it's OK for setpoints to be duplicated across
|
|
131
|
+
# multiple files, but we should only empty the
|
|
132
|
+
# array out the first time we see it, so subsequent
|
|
133
|
+
# reads can check for consistency
|
|
134
|
+
set_array.clear()
|
|
135
|
+
else:
|
|
136
|
+
set_array = DataArray(label=labels[i], array_id=array_id,
|
|
137
|
+
set_arrays=set_arrays, shape=set_shape,
|
|
138
|
+
is_setpoint=True, snapshot=snap)
|
|
139
|
+
set_array.init_data()
|
|
140
|
+
data_set.add_array(set_array)
|
|
141
|
+
|
|
142
|
+
set_arrays = set_arrays + (set_array, )
|
|
143
|
+
ids_read.add(array_id)
|
|
144
|
+
|
|
145
|
+
for i, array_id in indexed_ids[ndim:]:
|
|
146
|
+
snap = data_set.get_array_metadata(array_id)
|
|
147
|
+
|
|
148
|
+
# data arrays
|
|
149
|
+
if array_id in ids_read:
|
|
150
|
+
raise ValueError('duplicate data id found: ' + array_id)
|
|
151
|
+
|
|
152
|
+
if array_id in arrays:
|
|
153
|
+
data_array = arrays[array_id]
|
|
154
|
+
data_array.clear()
|
|
155
|
+
else:
|
|
156
|
+
data_array = DataArray(label=labels[i], array_id=array_id,
|
|
157
|
+
set_arrays=set_arrays, shape=shape,
|
|
158
|
+
snapshot=snap)
|
|
159
|
+
data_array.init_data()
|
|
160
|
+
data_set.add_array(data_array)
|
|
161
|
+
data_arrays.append(data_array)
|
|
162
|
+
ids_read.add(array_id)
|
|
163
|
+
|
|
164
|
+
indices = [0] * ndim
|
|
165
|
+
first_point = True
|
|
166
|
+
resetting = 0
|
|
167
|
+
for line in f:
|
|
168
|
+
if self._is_comment(line):
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# ignore leading or trailing whitespace (including in blank lines)
|
|
172
|
+
line = line.strip()
|
|
173
|
+
|
|
174
|
+
if not line:
|
|
175
|
+
# each consecutive blank line implies one more loop to reset
|
|
176
|
+
# when we read the next data point. Don't depend on the number
|
|
177
|
+
# of setpoints that change, as there could be weird cases, like
|
|
178
|
+
# bidirectional sweeps, or highly diagonal sweeps, where this
|
|
179
|
+
# is incorrect. Anyway this really only matters for >2D sweeps.
|
|
180
|
+
if not first_point:
|
|
181
|
+
resetting += 1
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
values = tuple(map(float, line.split()))
|
|
185
|
+
|
|
186
|
+
if resetting:
|
|
187
|
+
indices[-resetting - 1] += 1
|
|
188
|
+
indices[-resetting:] = [0] * resetting
|
|
189
|
+
resetting = 0
|
|
190
|
+
|
|
191
|
+
for value, set_array in zip(values[:ndim], set_arrays):
|
|
192
|
+
nparray = set_array.ndarray
|
|
193
|
+
myindices = tuple(indices[:nparray.ndim])
|
|
194
|
+
stored_value = nparray[myindices]
|
|
195
|
+
if math.isnan(stored_value):
|
|
196
|
+
nparray[myindices] = value
|
|
197
|
+
elif stored_value != value:
|
|
198
|
+
raise ValueError('inconsistent setpoint values',
|
|
199
|
+
stored_value, value, set_array.name,
|
|
200
|
+
myindices, indices)
|
|
201
|
+
|
|
202
|
+
for value, data_array in zip(values[ndim:], data_arrays):
|
|
203
|
+
# set .ndarray directly to avoid the overhead of __setitem__
|
|
204
|
+
# which updates modified_range on every call
|
|
205
|
+
data_array.ndarray[tuple(indices)] = value
|
|
206
|
+
|
|
207
|
+
indices[-1] += 1
|
|
208
|
+
first_point = False
|
|
209
|
+
|
|
210
|
+
# Since we skipped __setitem__, back up to the last read point and
|
|
211
|
+
# mark it as saved that far.
|
|
212
|
+
# Using mark_saved is better than directly setting last_saved_index
|
|
213
|
+
# because it also ensures modified_range is set correctly.
|
|
214
|
+
indices[-1] -= 1
|
|
215
|
+
for array in set_arrays + tuple(data_arrays):
|
|
216
|
+
array.mark_saved(array.flat_index(indices[:array.ndim]))
|
|
217
|
+
|
|
218
|
+
def _is_comment(self, line):
|
|
219
|
+
return line[:self.comment_len] == self.comment_chars
|
|
220
|
+
|
|
221
|
+
def _read_comment_line(self, f):
|
|
222
|
+
s = f.readline()
|
|
223
|
+
if not self._is_comment(s):
|
|
224
|
+
raise ValueError('expected a comment line, found:\n' + s)
|
|
225
|
+
return s[self.comment_len:]
|
|
226
|
+
|
|
227
|
+
def _get_labels(self, labelstr):
|
|
228
|
+
labelstr = labelstr.strip()
|
|
229
|
+
if labelstr[0] != '"' or labelstr[-1] != '"':
|
|
230
|
+
# fields are *not* quoted
|
|
231
|
+
return labelstr.split()
|
|
232
|
+
else:
|
|
233
|
+
# fields *are* quoted (and escaped)
|
|
234
|
+
parts = re.split('"\s+"', labelstr[1:-1])
|
|
235
|
+
return [l.replace('\\"', '"').replace('\\\\', '\\') for l in parts]
|
|
236
|
+
|
|
237
|
+
def write(self, data_set, io_manager, location, force_write=False):
|
|
238
|
+
"""
|
|
239
|
+
Write updates in this DataSet to storage.
|
|
240
|
+
|
|
241
|
+
Will choose append if possible, overwrite if not.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
data_set (DataSet): the data we're storing
|
|
245
|
+
io_manager (io_manager): the base location to write to
|
|
246
|
+
location (str): the file location within io_manager
|
|
247
|
+
"""
|
|
248
|
+
arrays = data_set.arrays
|
|
249
|
+
|
|
250
|
+
# puts everything with same dimensions together
|
|
251
|
+
groups = self.group_arrays(arrays)
|
|
252
|
+
existing_files = set(io_manager.list(location))
|
|
253
|
+
written_files = set()
|
|
254
|
+
|
|
255
|
+
# Every group gets it's own datafile
|
|
256
|
+
for group in groups:
|
|
257
|
+
fn = io_manager.join(location, group.name + self.extension)
|
|
258
|
+
|
|
259
|
+
written_files.add(fn)
|
|
260
|
+
|
|
261
|
+
file_exists = fn in existing_files
|
|
262
|
+
save_range = self.match_save_range(group, file_exists)
|
|
263
|
+
|
|
264
|
+
if save_range is None:
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
overwrite = save_range[0] == 0 or force_write
|
|
268
|
+
open_mode = 'w' if overwrite else 'a'
|
|
269
|
+
shape = group.set_arrays[-1].shape
|
|
270
|
+
|
|
271
|
+
with io_manager.open(fn, open_mode) as f:
|
|
272
|
+
if overwrite:
|
|
273
|
+
f.write(self._make_header(group))
|
|
274
|
+
|
|
275
|
+
for i in range(save_range[0], save_range[1] + 1):
|
|
276
|
+
indices = np.unravel_index(i, shape)
|
|
277
|
+
|
|
278
|
+
# insert a blank line for each loop that reset (to index 0)
|
|
279
|
+
# note that if *all* indices are zero (the first point)
|
|
280
|
+
# we won't put any blanks
|
|
281
|
+
for j, index in enumerate(reversed(indices)):
|
|
282
|
+
if index != 0:
|
|
283
|
+
if j:
|
|
284
|
+
f.write(self.terminator * j)
|
|
285
|
+
break
|
|
286
|
+
|
|
287
|
+
one_point = self._data_point(group, indices)
|
|
288
|
+
f.write(self.separator.join(one_point) + self.terminator)
|
|
289
|
+
|
|
290
|
+
# now that we've saved the data, mark it as such in the data.
|
|
291
|
+
# we mark the data arrays and the inner setpoint array. Outer
|
|
292
|
+
# setpoint arrays have different dimension (so would need a
|
|
293
|
+
# different unraveled index) but more importantly could have
|
|
294
|
+
# a different saved range anyway depending on whether there
|
|
295
|
+
# is outer data taken before or after the inner loop. Anyway we
|
|
296
|
+
# never look at the outer setpoint last_saved_index or
|
|
297
|
+
# modified_range, we just assume it's got the values we need.
|
|
298
|
+
for array in group.data + (group.set_arrays[-1],):
|
|
299
|
+
array.mark_saved(save_range[1])
|
|
300
|
+
|
|
301
|
+
def write_metadata(self, data_set, io_manager, location, read_first=True):
|
|
302
|
+
"""
|
|
303
|
+
Write all metadata in this DataSet to storage.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
data_set (DataSet): the data we're storing
|
|
307
|
+
|
|
308
|
+
io_manager (io_manager): the base location to write to
|
|
309
|
+
|
|
310
|
+
location (str): the file location within io_manager
|
|
311
|
+
|
|
312
|
+
read_first (bool, optional): read previously saved metadata before
|
|
313
|
+
writing? The current metadata will still be the used if
|
|
314
|
+
there are changes, but if the saved metadata has information
|
|
315
|
+
not present in the current metadata, it will be retained.
|
|
316
|
+
Default True.
|
|
317
|
+
"""
|
|
318
|
+
if read_first:
|
|
319
|
+
# In case the saved file has more metadata than we have here,
|
|
320
|
+
# read it in first. But any changes to the in-memory copy should
|
|
321
|
+
# override the saved file data.
|
|
322
|
+
memory_metadata = data_set.metadata
|
|
323
|
+
data_set.metadata = {}
|
|
324
|
+
self.read_metadata(data_set)
|
|
325
|
+
deep_update(data_set.metadata, memory_metadata)
|
|
326
|
+
|
|
327
|
+
fn = io_manager.join(location, self.metadata_file)
|
|
328
|
+
with io_manager.open(fn, 'w', encoding='utf8') as snap_file:
|
|
329
|
+
json.dump(data_set.metadata, snap_file, sort_keys=True,
|
|
330
|
+
indent=4, ensure_ascii=False, cls=NumpyJSONEncoder)
|
|
331
|
+
|
|
332
|
+
def read_metadata(self, data_set):
|
|
333
|
+
io_manager = data_set.io
|
|
334
|
+
location = data_set.location
|
|
335
|
+
fn = io_manager.join(location, self.metadata_file)
|
|
336
|
+
if io_manager.list(fn):
|
|
337
|
+
with io_manager.open(fn, 'r') as snap_file:
|
|
338
|
+
metadata = json.load(snap_file, encoding='utf8')
|
|
339
|
+
data_set.metadata.update(metadata)
|
|
340
|
+
|
|
341
|
+
def _make_header(self, group):
|
|
342
|
+
ids, labels = [], []
|
|
343
|
+
for array in group.set_arrays + group.data:
|
|
344
|
+
ids.append(array.array_id)
|
|
345
|
+
label = getattr(array, 'label', array.array_id)
|
|
346
|
+
label = label.replace('\\', '\\\\').replace('"', '\\"')
|
|
347
|
+
labels.append('"' + label + '"')
|
|
348
|
+
|
|
349
|
+
shape = [str(size) for size in group.set_arrays[-1].shape]
|
|
350
|
+
if len(shape) != len(group.set_arrays):
|
|
351
|
+
raise ValueError('array dimensionality does not match setpoints')
|
|
352
|
+
|
|
353
|
+
out = (self._comment_line(ids) + self._comment_line(labels) +
|
|
354
|
+
self._comment_line(shape))
|
|
355
|
+
|
|
356
|
+
return out
|
|
357
|
+
|
|
358
|
+
def _comment_line(self, items):
|
|
359
|
+
return self.comment + self.separator.join(items) + self.terminator
|
|
360
|
+
|
|
361
|
+
def _data_point(self, group, indices):
|
|
362
|
+
for array in group.set_arrays:
|
|
363
|
+
yield self.number_format.format(array[indices[:array.ndim]])
|
|
364
|
+
|
|
365
|
+
for array in group.data:
|
|
366
|
+
yield self.number_format.format(array[indices])
|