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