dbdreader 0.6.0.dev1__cp313-cp313-win_amd64.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.
- _dbdreader.cp313-win_amd64.pyd +0 -0
- dbdreader/__init__.py +12 -0
- dbdreader/data/01600000.dcd +0 -0
- dbdreader/data/01600000.ebd +0 -0
- dbdreader/data/01600000.ecd +0 -0
- dbdreader/data/01600000.mcg +0 -0
- dbdreader/data/01600000.mlg +1904 -0
- dbdreader/data/01600001.dbd +0 -0
- dbdreader/data/01600001.dcd +0 -0
- dbdreader/data/01600001.ebd +0 -0
- dbdreader/data/01600001.ecd +0 -0
- dbdreader/data/02380107.ecd +0 -0
- dbdreader/data/02380108.ecd +0 -0
- dbdreader/data/02450133.tcd +0 -0
- dbdreader/data/02450137.tcd +0 -0
- dbdreader/data/amadeus-2014-203-00-000.SBD +0 -0
- dbdreader/data/amadeus-2014-203-00-000.TBD +0 -0
- dbdreader/data/amadeus-2014-204-05-000.dbd +0 -0
- dbdreader/data/amadeus-2014-204-05-000.ebd +0 -0
- dbdreader/data/amadeus-2014-204-05-000.sbd +0 -0
- dbdreader/data/amadeus-2014-204-05-000.tbd +0 -0
- dbdreader/data/amadeus-2014-204-05-001.sbd +0 -0
- dbdreader/data/amadeus-2014-204-05-001.tbd +0 -0
- dbdreader/data/amadeus-2014-204-05-002.sbd +0 -0
- dbdreader/data/amadeus-2014-204-05-002.tbd +0 -0
- dbdreader/data/ammonite-2008-028-01-000.mbd +2091 -1
- dbdreader/data/dbd2asc_output.txt +25 -0
- dbdreader/data/electa-2023-143-00-050.sbd +0 -0
- dbdreader/data/electa-2023-143-00-050.tbd +0 -0
- dbdreader/data/empty-2014-204-05-000.dbd +0 -0
- dbdreader/data/hal_1002-2024-183-4-4.sbd +0 -0
- dbdreader/data/hal_1002-2024-183-4-4.tbd +0 -0
- dbdreader/data/hal_1002-2024-183-4-6.tbd +0 -0
- dbdreader/data/invalid_encoding-2014-204-05-000.dbd +0 -0
- dbdreader/data/sebastian-2014-204-05-000.dbd +0 -0
- dbdreader/data/sebastian-2014-204-05-000.ebd +0 -0
- dbdreader/data/sebastian-2014-204-05-001.dbd +0 -0
- dbdreader/data/sebastian-2014-204-05-001.ebd +0 -0
- dbdreader/data/unit_887-2021-321-3-0.sbd +0 -0
- dbdreader/data/unit_887-2021-321-3-0.tbd +0 -0
- dbdreader/dbdreader.py +2174 -0
- dbdreader/decompress.py +247 -0
- dbdreader/scripts.py +279 -0
- dbdreader-0.6.0.dev1.dist-info/METADATA +272 -0
- dbdreader-0.6.0.dev1.dist-info/RECORD +50 -0
- dbdreader-0.6.0.dev1.dist-info/WHEEL +5 -0
- dbdreader-0.6.0.dev1.dist-info/entry_points.txt +3 -0
- dbdreader-0.6.0.dev1.dist-info/licenses/COPYING +340 -0
- dbdreader-0.6.0.dev1.dist-info/licenses/LICENSE +674 -0
- dbdreader-0.6.0.dev1.dist-info/top_level.txt +2 -0
dbdreader/dbdreader.py
ADDED
|
@@ -0,0 +1,2174 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
from itertools import chain
|
|
3
|
+
import os
|
|
4
|
+
import struct
|
|
5
|
+
import time
|
|
6
|
+
import numpy
|
|
7
|
+
from scipy.interpolate import interp1d as si_interp1d
|
|
8
|
+
import glob
|
|
9
|
+
import sys
|
|
10
|
+
import re
|
|
11
|
+
import datetime
|
|
12
|
+
from calendar import timegm
|
|
13
|
+
from collections import defaultdict, namedtuple
|
|
14
|
+
import logging
|
|
15
|
+
logger = logging.getLogger(os.path.basename(__file__))
|
|
16
|
+
|
|
17
|
+
import dbdreader.decompress
|
|
18
|
+
|
|
19
|
+
# If the environment varibale DBDREADER_C_EXTENSION is set to "1", then force to import
|
|
20
|
+
# the C implementation. Let it fail if this is not successful. If not set,
|
|
21
|
+
# *try * to import it, and fallback to the pure python implementation. If the
|
|
22
|
+
# environment variable is set to 0 force to use the pure python implementation.
|
|
23
|
+
|
|
24
|
+
match(os.environ.get("DBDREADER_C_EXTENSION")):
|
|
25
|
+
case "1":
|
|
26
|
+
import _dbdreader
|
|
27
|
+
logger.debug("Imported C-extension")
|
|
28
|
+
case "0":
|
|
29
|
+
import dbdreader._dbdreader as _dbdreader
|
|
30
|
+
logger.debug("Imported pure python implementation")
|
|
31
|
+
case _:
|
|
32
|
+
try:
|
|
33
|
+
import _dbdreader
|
|
34
|
+
logger.debug("Imported C-extension")
|
|
35
|
+
except ImportError:
|
|
36
|
+
import dbdreader._dbdreader as _dbdreader
|
|
37
|
+
logger.debug("Imported pure python implementation")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# make sure we interpret timestamps in the english language but don't
|
|
41
|
+
# bother if it cannot be import as happens on building doc on readthe
|
|
42
|
+
# docs
|
|
43
|
+
try:
|
|
44
|
+
import locale
|
|
45
|
+
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
|
|
46
|
+
except:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
# Parameters that a compatible with transforming from nmea to decimal format:
|
|
50
|
+
LATLON_PARAMS = ["m_lat",
|
|
51
|
+
"m_lon",
|
|
52
|
+
"c_wpt_lat",
|
|
53
|
+
"c_wpt_lon",
|
|
54
|
+
"x_last_wpt_lat",
|
|
55
|
+
"x_last_wpt_lon",
|
|
56
|
+
"m_gps_lat",
|
|
57
|
+
"m_gps_lon",
|
|
58
|
+
"u_lat_goto_l99",
|
|
59
|
+
"u_lon_goto_l99",
|
|
60
|
+
"m_last_gps_lat_1",
|
|
61
|
+
"m_last_gps_lon_1",
|
|
62
|
+
"m_last_gps_lat_2",
|
|
63
|
+
"m_last_gps_lon_2",
|
|
64
|
+
"m_last_gps_lat_3",
|
|
65
|
+
"m_last_gps_lon_3",
|
|
66
|
+
"m_last_gps_lat_4",
|
|
67
|
+
"m_last_gps_lon_4",
|
|
68
|
+
"m_gps_ignored_lat",
|
|
69
|
+
"m_gps_ignored_lon",
|
|
70
|
+
"m_gps_invalid_lat",
|
|
71
|
+
"m_gps_invalid_lon",
|
|
72
|
+
"m_gps_toofar_lat",
|
|
73
|
+
"m_gps_toofar_lon",
|
|
74
|
+
"xs_lat",
|
|
75
|
+
"xs_lon",
|
|
76
|
+
"s_ini_lat",
|
|
77
|
+
"s_ini_lon"]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def strptimeToEpoch(datestr, fmt):
|
|
81
|
+
''' Converts datestr into seconds
|
|
82
|
+
|
|
83
|
+
Function to convert a date string into seconds since Epoch.
|
|
84
|
+
This function is not affected by the time zone used by the OS and
|
|
85
|
+
interprets the date string in UTC.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
datestr: str
|
|
91
|
+
A string presenting the date, such as "2010 May 01"
|
|
92
|
+
|
|
93
|
+
fmt: str
|
|
94
|
+
Format to interpret strings. Example: "%Y %b %d"
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
int
|
|
99
|
+
time since epoch in seconds
|
|
100
|
+
'''
|
|
101
|
+
ts = time.strptime(datestr, fmt)
|
|
102
|
+
return timegm(ts)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def epochToDateTimeStr(seconds, dateformat="%Y%m%d", timeformat="%H:%M"):
|
|
106
|
+
''' Converts seconds since Epoch to date string
|
|
107
|
+
|
|
108
|
+
This function converts seconds since Epoch to a datestr and timestr
|
|
109
|
+
with user configurable formats.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
seconds: float or int
|
|
114
|
+
seconds since Epoch
|
|
115
|
+
dateformat: str
|
|
116
|
+
string defining how the date string should be formatted
|
|
117
|
+
timeformat: str
|
|
118
|
+
string defining how the time string should be formatted
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
(str, str)
|
|
123
|
+
datestring and timestring
|
|
124
|
+
'''
|
|
125
|
+
d = datetime.datetime.utcfromtimestamp(seconds)
|
|
126
|
+
datestr = d.strftime(dateformat)
|
|
127
|
+
timestr = d.strftime(timeformat)
|
|
128
|
+
return datestr, timestr
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _convertToDecimal(x):
|
|
132
|
+
''' Converts a latitiude or longitude in NMEA format to decimale degrees'''
|
|
133
|
+
sign = numpy.sign(x)
|
|
134
|
+
xAbs = numpy.abs(x)
|
|
135
|
+
degrees = numpy.floor(xAbs/100.)
|
|
136
|
+
minutes = xAbs-degrees*100
|
|
137
|
+
decimalFormat = degrees+minutes/60.
|
|
138
|
+
return decimalFormat*sign
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def toDec(x, y=None):
|
|
142
|
+
''' NMEA style to decimal degree converter
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
x: float
|
|
147
|
+
latitiude or longitude in NMEA format
|
|
148
|
+
y: float, optional
|
|
149
|
+
latitiude or longitude in NMEA format
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
float or tuple of floats
|
|
154
|
+
decimal latitude (longitude) or tuple of decimal latitude and longitude
|
|
155
|
+
'''
|
|
156
|
+
if not y == None:
|
|
157
|
+
X = _convertToDecimal(x)
|
|
158
|
+
Y = _convertToDecimal(y)
|
|
159
|
+
return X, Y
|
|
160
|
+
return _convertToDecimal(x)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# required (and only tested) encoding version.
|
|
164
|
+
ENCODING_VER = 5
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
DBD_ERROR_CACHE_NOT_FOUND = 1
|
|
168
|
+
DBD_ERROR_NO_VALID_PARAMETERS = 2
|
|
169
|
+
DBD_ERROR_NO_TIME_VARIABLE = 3
|
|
170
|
+
DBD_ERROR_NO_FILE_CRITERIUM_SPECIFIED = 4
|
|
171
|
+
DBD_ERROR_NO_FILES_FOUND = 5
|
|
172
|
+
DBD_ERROR_NO_DATA_TO_INTERPOLATE_TO = 6
|
|
173
|
+
DBD_ERROR_CACHEDIR_NOT_FOUND = 7
|
|
174
|
+
DBD_ERROR_ALL_FILES_BANNED = 8
|
|
175
|
+
DBD_ERROR_INVALID_DBD_FILE = 9
|
|
176
|
+
DBD_ERROR_INVALID_ENCODING = 10
|
|
177
|
+
DBD_ERROR_INVALID_FILE_CRITERION_SPECIFIED = 11
|
|
178
|
+
DBD_ERROR_NO_DATA_TO_INTERPOLATE = 12
|
|
179
|
+
DBD_ERROR_NO_DATA = 13
|
|
180
|
+
DBD_ERROR_READ_ERROR = 14
|
|
181
|
+
DBD_ERROR_DECOMPRESSION_ERROR = 15
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class DbdError(Exception):
|
|
186
|
+
MissingCacheFileData = namedtuple('MissingCacheFileData',
|
|
187
|
+
'missing_cache_files cache_dir')
|
|
188
|
+
|
|
189
|
+
def __init__(self, value=9, mesg=None, data=None):
|
|
190
|
+
self.value = value
|
|
191
|
+
self.mesg = mesg
|
|
192
|
+
self.data = data
|
|
193
|
+
|
|
194
|
+
def __str__(self):
|
|
195
|
+
if self.value == DBD_ERROR_NO_VALID_PARAMETERS:
|
|
196
|
+
mesg = 'The requested parameter(s) was(were) not found.'
|
|
197
|
+
elif self.value == DBD_ERROR_NO_TIME_VARIABLE:
|
|
198
|
+
mesg = 'The time variable was not found.'
|
|
199
|
+
elif self.value == DBD_ERROR_CACHE_NOT_FOUND:
|
|
200
|
+
mesg = 'Cache file was not found.'
|
|
201
|
+
elif self.value == DBD_ERROR_NO_FILE_CRITERIUM_SPECIFIED:
|
|
202
|
+
mesg = 'No file specification supplied (list of filenames or pattern)'
|
|
203
|
+
elif self.value == DBD_ERROR_NO_FILES_FOUND:
|
|
204
|
+
mesg = 'No files were found.'
|
|
205
|
+
elif self.value == DBD_ERROR_NO_DATA_TO_INTERPOLATE_TO:
|
|
206
|
+
mesg = 'No data to interpolate to.'
|
|
207
|
+
elif self.value == DBD_ERROR_CACHEDIR_NOT_FOUND:
|
|
208
|
+
mesg = 'Cache file directory does not exist.'
|
|
209
|
+
elif self.value == DBD_ERROR_ALL_FILES_BANNED:
|
|
210
|
+
mesg = 'All data files were banned.'
|
|
211
|
+
elif self.value == DBD_ERROR_INVALID_DBD_FILE:
|
|
212
|
+
mesg = 'Invalid DBD file.'
|
|
213
|
+
elif self.value == DBD_ERROR_INVALID_ENCODING:
|
|
214
|
+
mesg = 'Invalid encoding version encountered.'
|
|
215
|
+
elif self.value == DBD_ERROR_INVALID_FILE_CRITERION_SPECIFIED:
|
|
216
|
+
mesg = 'Invalid or conflicting file selection criterion/criteria specified.'
|
|
217
|
+
elif self.value == DBD_ERROR_NO_DATA_TO_INTERPOLATE:
|
|
218
|
+
mesg = 'One or more parameters that are to be interpolated, does/do not have any data.'
|
|
219
|
+
elif self.value == DBD_ERROR_NO_DATA:
|
|
220
|
+
mesg = 'One or more parameters do not have any data.'
|
|
221
|
+
elif self.value == DBD_ERROR_READ_ERROR:
|
|
222
|
+
mesg = 'Read error.'
|
|
223
|
+
elif self.value == DBD_ERROR_DECOMPRESSION_ERROR:
|
|
224
|
+
mesg = 'Decompression error.'
|
|
225
|
+
else:
|
|
226
|
+
mesg = f'Undefined error. ({self.value})'
|
|
227
|
+
if self.mesg:
|
|
228
|
+
mesg = " ".join((mesg, self.mesg))
|
|
229
|
+
return mesg
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
'''
|
|
233
|
+
|
|
234
|
+
how the sbd files work (and presumably the other files as well)
|
|
235
|
+
|
|
236
|
+
header is acii, somewhere in here is specified how many lines the header
|
|
237
|
+
is.
|
|
238
|
+
|
|
239
|
+
then if, not factored, a list follows with the size of each variable and
|
|
240
|
+
whether or not it is in the list. If it is factored, this list should be
|
|
241
|
+
read from file.
|
|
242
|
+
|
|
243
|
+
Then the binary bit begins. the first n bytes are to be discarded, where n
|
|
244
|
+
is the number of parameters in the file. Don t know why this is though.
|
|
245
|
+
|
|
246
|
+
then, for each cycle there is a sequence of bytes, setting which parameters
|
|
247
|
+
are not updated (0), are updated, but have the same value as before (1) or
|
|
248
|
+
are completely new (2). Only when new, they appear in the file. The block
|
|
249
|
+
length between each state-sequence depends thus on the size of the parameters
|
|
250
|
+
that are actually newly updated.
|
|
251
|
+
When reading 4 or 8 bytes and convert them into a float or double, requires
|
|
252
|
+
the order to be reversed.
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
The proposed api
|
|
256
|
+
|
|
257
|
+
class DBD:
|
|
258
|
+
methods:
|
|
259
|
+
init (opens a file)
|
|
260
|
+
close(closes a file)
|
|
261
|
+
get(param) (returns time and data for given variable)
|
|
262
|
+
|
|
263
|
+
private methods:
|
|
264
|
+
read_header
|
|
265
|
+
read_sensor_list
|
|
266
|
+
write_sensor_list
|
|
267
|
+
|
|
268
|
+
'''
|
|
269
|
+
|
|
270
|
+
def heading_interpolating_function_factory(t, v):
|
|
271
|
+
'''Interpolating function factory for heading
|
|
272
|
+
|
|
273
|
+
This function returns a function that is to be called with one
|
|
274
|
+
argument, time, (float or array of floats) and returns the
|
|
275
|
+
interpolated heading, taking into account proper cross-overs from
|
|
276
|
+
0 to 2π.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
t : array-like of float
|
|
281
|
+
base time vector
|
|
282
|
+
v : array-like of float
|
|
283
|
+
base value vector
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
-------
|
|
287
|
+
interpolating function of time
|
|
288
|
+
|
|
289
|
+
'''
|
|
290
|
+
x = numpy.cos(v)
|
|
291
|
+
y = numpy.sin(v)
|
|
292
|
+
xi = partial(numpy.interp, xp=t, fp=x, left=numpy.nan, right=numpy.nan)
|
|
293
|
+
yi = partial(numpy.interp, xp=t, fp=y, left=numpy.nan, right=numpy.nan)
|
|
294
|
+
return lambda _t: numpy.arctan2(yi(_t), xi(_t)) % (2*numpy.pi)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class DBDCache(object):
|
|
298
|
+
|
|
299
|
+
'''DBDCache manager
|
|
300
|
+
|
|
301
|
+
This class provides a convenient way to set the default path to the cache file
|
|
302
|
+
directory.
|
|
303
|
+
|
|
304
|
+
On linux : $HOME/.local/share/dbdreader
|
|
305
|
+
On windows : $HOME/.dbdreader
|
|
306
|
+
|
|
307
|
+
The class method set_cachedir() can be used to change the path during the script.
|
|
308
|
+
|
|
309
|
+
The purpose is to remove the need to specify a non-default path
|
|
310
|
+
for every DBD or MultiDBD object construction.
|
|
311
|
+
|
|
312
|
+
Examples
|
|
313
|
+
--------
|
|
314
|
+
|
|
315
|
+
>>> DBDCache() # sets default. This is typically called at the end of the dbdreader module file.
|
|
316
|
+
|
|
317
|
+
>>> DBDCache.set_cachedir("/tmp") # overrides the default.
|
|
318
|
+
|
|
319
|
+
Note
|
|
320
|
+
----
|
|
321
|
+
|
|
322
|
+
DBDCache can be called with an argument, and it sets the default
|
|
323
|
+
path to this string. The difference between DBDCache("/tmp") and
|
|
324
|
+
DBDCache.set_cachedir("/tmp") is that when set_cachedir() is
|
|
325
|
+
called and the target directory does not exist, an error is
|
|
326
|
+
raised, whereas a call using the class constructor will create the
|
|
327
|
+
directory if necessary.
|
|
328
|
+
|
|
329
|
+
'''
|
|
330
|
+
CACHEDIR = None
|
|
331
|
+
|
|
332
|
+
def __init__(self, cachedir=None):
|
|
333
|
+
if cachedir is None:
|
|
334
|
+
if DBDCache.CACHEDIR is None:
|
|
335
|
+
HOME = os.path.expanduser("~") # <- multiplatform proof
|
|
336
|
+
if sys.platform == 'linux':
|
|
337
|
+
cachedir = os.path.join(HOME, '.local/share/dbdreader')
|
|
338
|
+
else:
|
|
339
|
+
cachedir = os.path.join(HOME, '.dbdreader')
|
|
340
|
+
DBDCache.set_cachedir(cachedir, force_makedirs=True)
|
|
341
|
+
# else default value is set and used.
|
|
342
|
+
else:
|
|
343
|
+
# user path set. Let it fail if it does not exists.
|
|
344
|
+
DBDCache.set_cachedir(cachedir, force_makedirs=False)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@classmethod
|
|
348
|
+
def set_cachedir(cls, path, force_makedirs=False):
|
|
349
|
+
'''Set the cache directory path
|
|
350
|
+
|
|
351
|
+
Parameters
|
|
352
|
+
----------
|
|
353
|
+
path : string
|
|
354
|
+
path to cache directory
|
|
355
|
+
|
|
356
|
+
force_makedirs : bool
|
|
357
|
+
if True, forces the creation of subdirectories if needed.
|
|
358
|
+
if False, an Exception will be thrown if the directory does not exist.
|
|
359
|
+
'''
|
|
360
|
+
if not os.path.exists(path):
|
|
361
|
+
if force_makedirs:
|
|
362
|
+
os.makedirs(path)
|
|
363
|
+
else:
|
|
364
|
+
raise DbdError(DBD_ERROR_CACHEDIR_NOT_FOUND)
|
|
365
|
+
DBDCache.CACHEDIR = path
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class DBDList(list):
|
|
372
|
+
|
|
373
|
+
''' List that properly sorts dbd files.
|
|
374
|
+
|
|
375
|
+
Object subclassed from list. The sort method defaults to sorting dbd
|
|
376
|
+
files and friends in the right order.
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
----------
|
|
380
|
+
*p : variable length list of str
|
|
381
|
+
filenames
|
|
382
|
+
'''
|
|
383
|
+
REGEX = re.compile(r"-[0-9]+-[0-9]+-[0-9]+-[0-9]+\.[demnstDEMNST][bB][dD]")
|
|
384
|
+
|
|
385
|
+
def __init__(self,*p):
|
|
386
|
+
list.__init__(self,*p)
|
|
387
|
+
|
|
388
|
+
def _keyFilename(self, key):
|
|
389
|
+
match = DBDList.REGEX.search(key)
|
|
390
|
+
if match:
|
|
391
|
+
s, extension = os.path.splitext(match.group())
|
|
392
|
+
number_fields = s.split("-")
|
|
393
|
+
n=sum([int(i)*10**j for i,j in zip(number_fields[1:],[8,5,3,0])]) # first field is '', so skip over
|
|
394
|
+
r = f"{key[:match.span()[0]]}-{n}{extension.lower()}"
|
|
395
|
+
else:
|
|
396
|
+
r = key.lower()
|
|
397
|
+
return r
|
|
398
|
+
|
|
399
|
+
def sort(self,cmp=None, key=None, reverse=False):
|
|
400
|
+
''' sorts filenames ensuring dbd files are in chronological order in place
|
|
401
|
+
|
|
402
|
+
Parameters
|
|
403
|
+
----------
|
|
404
|
+
cmp :
|
|
405
|
+
ingored keyword (for compatibility reasons only)
|
|
406
|
+
key :
|
|
407
|
+
ignored keyword (for compatibility reasons only)
|
|
408
|
+
reverse : bool
|
|
409
|
+
If True, performs a reverse sort.
|
|
410
|
+
'''
|
|
411
|
+
list.sort(self, key=self._keyFilename, reverse=reverse)
|
|
412
|
+
|
|
413
|
+
class DBDPatternSelect(object):
|
|
414
|
+
''' Selecting DBD files.
|
|
415
|
+
|
|
416
|
+
A class for selecting dbd files based on a date condition.
|
|
417
|
+
The class opens files and reads the headers only.
|
|
418
|
+
|
|
419
|
+
Parameters
|
|
420
|
+
----------
|
|
421
|
+
date_format : str, optional
|
|
422
|
+
date format used to interpret date strings.
|
|
423
|
+
|
|
424
|
+
Note
|
|
425
|
+
----
|
|
426
|
+
Times are based on the opening time of the file only.
|
|
427
|
+
|
|
428
|
+
'''
|
|
429
|
+
cache = {}
|
|
430
|
+
|
|
431
|
+
def __init__(self, date_format="%d %m %Y", cacheDir=None):
|
|
432
|
+
self.set_date_format(date_format)
|
|
433
|
+
self.cacheDir = cacheDir
|
|
434
|
+
|
|
435
|
+
def set_date_format(self,date_format):
|
|
436
|
+
''' Set date format
|
|
437
|
+
|
|
438
|
+
Sets the date format used to interpret the from_date and until_dates.
|
|
439
|
+
|
|
440
|
+
Parameters
|
|
441
|
+
----------
|
|
442
|
+
date_format: str
|
|
443
|
+
format to interpret date strings. Example "%H %d %m %Y"
|
|
444
|
+
|
|
445
|
+
cachedDir: str or None, optional
|
|
446
|
+
path to CAC file cache directory. If None, the default path is used.
|
|
447
|
+
|
|
448
|
+
'''
|
|
449
|
+
self.date_format=date_format
|
|
450
|
+
|
|
451
|
+
def get_date_format(self):
|
|
452
|
+
''' Returns date format string.
|
|
453
|
+
|
|
454
|
+
Returns
|
|
455
|
+
-------
|
|
456
|
+
str:
|
|
457
|
+
date format string
|
|
458
|
+
'''
|
|
459
|
+
return self.date_format
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def select(self,pattern=None,filenames=[],from_date=None,until_date=None):
|
|
463
|
+
'''Select file names from pattern or list.
|
|
464
|
+
|
|
465
|
+
This method selects the filenames given a filename list or search
|
|
466
|
+
pattern and given time limits.
|
|
467
|
+
|
|
468
|
+
Parameters
|
|
469
|
+
----------
|
|
470
|
+
pattern: str
|
|
471
|
+
search pattern (passed to glob) to find filenames
|
|
472
|
+
|
|
473
|
+
filenames: list of str
|
|
474
|
+
filename list
|
|
475
|
+
|
|
476
|
+
from_date: None or str, optional
|
|
477
|
+
date used as start date criterion. If None, all files are
|
|
478
|
+
included until the until_date.
|
|
479
|
+
|
|
480
|
+
until_date: None or str, optional
|
|
481
|
+
date used aas end date criterion. If None, all files after
|
|
482
|
+
from_date are included.
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
list of filenames that match the criteria
|
|
487
|
+
|
|
488
|
+
Raises:
|
|
489
|
+
ValueError if nor pattern or filenames is given.
|
|
490
|
+
|
|
491
|
+
Note
|
|
492
|
+
----
|
|
493
|
+
Either pattern or filenames should be supplied, and at least one of
|
|
494
|
+
from_date and until_date.
|
|
495
|
+
|
|
496
|
+
'''
|
|
497
|
+
|
|
498
|
+
all_filenames = self.get_filenames(pattern, filenames, self.cacheDir)
|
|
499
|
+
|
|
500
|
+
if not from_date and not until_date:
|
|
501
|
+
# just get all files.
|
|
502
|
+
t0=t1=None
|
|
503
|
+
return all_filenames
|
|
504
|
+
else:
|
|
505
|
+
if from_date:
|
|
506
|
+
t0=strptimeToEpoch(from_date,self.date_format)
|
|
507
|
+
else:
|
|
508
|
+
t0=1
|
|
509
|
+
if until_date:
|
|
510
|
+
t1=strptimeToEpoch(until_date,self.date_format)
|
|
511
|
+
else:
|
|
512
|
+
t1=1e11
|
|
513
|
+
return self._select(all_filenames,t0,t1)
|
|
514
|
+
|
|
515
|
+
def bins(self, pattern=None, filenames=None, binsize=86400, t_start=None, t_end=None):
|
|
516
|
+
'''Return a list of filenames, in time bins
|
|
517
|
+
|
|
518
|
+
The method makes a list of all filenames, matching either
|
|
519
|
+
pattern or filenames and bins these in time windows of width. If
|
|
520
|
+
t_start and t_end are not given, they are computed from the first and
|
|
521
|
+
last timestamps of the files specified, respectively.
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
This method returns a list of tuples, where each tuple
|
|
525
|
+
contains the centred time of the bin, and a list of all
|
|
526
|
+
filenames that fall within this bin.
|
|
527
|
+
|
|
528
|
+
Parameters
|
|
529
|
+
----------
|
|
530
|
+
pattern: str
|
|
531
|
+
search pattern (as used in glob)
|
|
532
|
+
|
|
533
|
+
filenames: list of str
|
|
534
|
+
filename list
|
|
535
|
+
|
|
536
|
+
binsize: float
|
|
537
|
+
binsize of in seconds
|
|
538
|
+
|
|
539
|
+
t_start: None or float
|
|
540
|
+
Timestamp in seconds since 1/1/1970
|
|
541
|
+
|
|
542
|
+
t_end: None or float
|
|
543
|
+
Timestamp in seconds since 1/1/1970
|
|
544
|
+
|
|
545
|
+
Returns
|
|
546
|
+
-------
|
|
547
|
+
list of list of str
|
|
548
|
+
list of filenames, grouped per bin
|
|
549
|
+
|
|
550
|
+
Raises
|
|
551
|
+
------
|
|
552
|
+
ValueError if nor pattern or filenames is given.
|
|
553
|
+
'''
|
|
554
|
+
fns = self.get_filenames(pattern, filenames)
|
|
555
|
+
if not fns:
|
|
556
|
+
raise DbdError(DBD_ERROR_NO_FILES_FOUND,
|
|
557
|
+
f"No files matched search pattern {pattern}.")
|
|
558
|
+
if t_start is None:
|
|
559
|
+
t_start = numpy.min(list(self.cache.keys()))
|
|
560
|
+
if t_end is None:
|
|
561
|
+
t_end = numpy.max(list(self.cache.keys()))
|
|
562
|
+
bin_edges = numpy.arange(t_start, t_end+binsize, binsize)
|
|
563
|
+
bins = [((left+right)/2, self._select(fns, left, right))
|
|
564
|
+
for left, right in zip(bin_edges[0:-1], bin_edges[1:])]
|
|
565
|
+
return bins
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def get_filenames(self, pattern, filenames, cacheDir=None):
|
|
569
|
+
''' Get filenames (sorted) and update CAC cache directory.
|
|
570
|
+
|
|
571
|
+
Parameters
|
|
572
|
+
----------
|
|
573
|
+
pattern : str
|
|
574
|
+
search pattern (as used in glob)
|
|
575
|
+
filenames : list of str
|
|
576
|
+
list of filenames
|
|
577
|
+
|
|
578
|
+
Returns
|
|
579
|
+
-------
|
|
580
|
+
list of str
|
|
581
|
+
sorted list of filenames.
|
|
582
|
+
'''
|
|
583
|
+
if not pattern and not filenames:
|
|
584
|
+
raise ValueError("Expected some pattern to search files for or file list.")
|
|
585
|
+
if pattern:
|
|
586
|
+
all_filenames=DBDList(glob.glob(pattern))
|
|
587
|
+
elif filenames:
|
|
588
|
+
all_filenames=DBDList(filenames)
|
|
589
|
+
else:
|
|
590
|
+
raise ValueError('Supply either pattern or filenames argument.')
|
|
591
|
+
all_filenames.sort()
|
|
592
|
+
self._update_cache(all_filenames, cacheDir)
|
|
593
|
+
return all_filenames
|
|
594
|
+
|
|
595
|
+
def _update_cache(self, fns, cacheDir):
|
|
596
|
+
cached_filenames = DBDList(self.cache.values())
|
|
597
|
+
cached_filenames.sort()
|
|
598
|
+
for fn in fns:
|
|
599
|
+
if fn not in cached_filenames:
|
|
600
|
+
dbd=DBD(fn, cacheDir)
|
|
601
|
+
t_open=dbd.get_fileopen_time()
|
|
602
|
+
self.cache[t_open]=fn
|
|
603
|
+
|
|
604
|
+
def _select(self,all_fns,t0,t1):
|
|
605
|
+
open_times = numpy.array(list(self.cache.keys()))
|
|
606
|
+
open_times = numpy.sort(open_times)
|
|
607
|
+
selected_times = open_times.compress(numpy.logical_and(open_times>=t0,
|
|
608
|
+
open_times<=t1))
|
|
609
|
+
fns = set([self.cache[t] for t in selected_times]).intersection(all_fns)
|
|
610
|
+
fns = DBDList(fns)
|
|
611
|
+
fns.sort()
|
|
612
|
+
return fns
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
class DBDHeader(object):
|
|
620
|
+
|
|
621
|
+
''' Class to read the headers of DBD files. This file is typically used
|
|
622
|
+
by DBD and MultiDBD and not directly.
|
|
623
|
+
'''
|
|
624
|
+
def __init__(self):
|
|
625
|
+
self.keywords={'dbd_label':'string',
|
|
626
|
+
'total_num_sensors':'int',
|
|
627
|
+
'sensor_list_crc':'string',
|
|
628
|
+
'state_bytes_per_cycle':'int',
|
|
629
|
+
'sensors_per_cycle':'int',
|
|
630
|
+
'sensor_list_factored':'int',
|
|
631
|
+
'num_ascii_tags':'int',
|
|
632
|
+
'mission_name':'string',
|
|
633
|
+
'fileopen_time':'string',
|
|
634
|
+
'encoding_ver':'int',
|
|
635
|
+
'full_filename':'string',
|
|
636
|
+
'the8x3_filename':'string'}
|
|
637
|
+
self.info={}
|
|
638
|
+
|
|
639
|
+
@property
|
|
640
|
+
def factored(self):
|
|
641
|
+
try:
|
|
642
|
+
r = self.info['sensor_list_factored']
|
|
643
|
+
except KeyError:
|
|
644
|
+
r = None
|
|
645
|
+
return r
|
|
646
|
+
|
|
647
|
+
def read_header(self, fp, filename=''):
|
|
648
|
+
''' read the header of the file, given by fp '''
|
|
649
|
+
fp.seek(0)
|
|
650
|
+
try:
|
|
651
|
+
result = self.parse(fp.readline())
|
|
652
|
+
except dbdreader.decompress.lz4.block.LZ4BlockError:
|
|
653
|
+
return DBD_ERROR_DECOMPRESSION_ERROR
|
|
654
|
+
except UnicodeDecodeError:
|
|
655
|
+
return DBD_ERROR_INVALID_DBD_FILE
|
|
656
|
+
if not result =='dbd_label':
|
|
657
|
+
return DBD_ERROR_INVALID_DBD_FILE
|
|
658
|
+
n_read=1
|
|
659
|
+
while True:
|
|
660
|
+
self.parse(fp.readline())
|
|
661
|
+
n_read+=1
|
|
662
|
+
if 'num_ascii_tags' in self.info and \
|
|
663
|
+
self.info['num_ascii_tags']==n_read:
|
|
664
|
+
break
|
|
665
|
+
if self.info['encoding_ver']!=ENCODING_VER:
|
|
666
|
+
return DBD_ERROR_INVALID_ENCODING
|
|
667
|
+
return 0
|
|
668
|
+
|
|
669
|
+
def read_cache(self,fp, fpcopy=None):
|
|
670
|
+
''' read cache file '''
|
|
671
|
+
parameter=[]
|
|
672
|
+
all_parameter_names = []
|
|
673
|
+
for i in range(self.info['total_num_sensors']):
|
|
674
|
+
line=fp.readline().decode('ascii')
|
|
675
|
+
if fpcopy!=None:
|
|
676
|
+
fpcopy.write(line)
|
|
677
|
+
parameters={}
|
|
678
|
+
words=line.split()
|
|
679
|
+
j=int(words[3]) # >=0? used : not used
|
|
680
|
+
name=words[5]
|
|
681
|
+
if j!=-1:
|
|
682
|
+
unit=words[6]
|
|
683
|
+
size=int(words[4])
|
|
684
|
+
parameter.append((size,name,unit))
|
|
685
|
+
all_parameter_names.append(name)
|
|
686
|
+
self.info['parameter_list'] = all_parameter_names
|
|
687
|
+
return parameter
|
|
688
|
+
|
|
689
|
+
# private methods
|
|
690
|
+
def parse(self,line):
|
|
691
|
+
# parses a binary datastream. So, first decode it to ascii.
|
|
692
|
+
words=line.decode('ascii').rstrip().split(":")
|
|
693
|
+
param=words[0]
|
|
694
|
+
if param in self.keywords.keys():
|
|
695
|
+
value=":".join(words[1:]).lstrip()
|
|
696
|
+
if self.keywords[param]=='int':
|
|
697
|
+
self.info[param]=int(value)
|
|
698
|
+
else:
|
|
699
|
+
self.info[param]=value
|
|
700
|
+
return param
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
class DBD(object):
|
|
704
|
+
'''Class to read a single DBD type file
|
|
705
|
+
|
|
706
|
+
Parameters
|
|
707
|
+
----------
|
|
708
|
+
|
|
709
|
+
filename: str
|
|
710
|
+
dbd filename
|
|
711
|
+
|
|
712
|
+
cachedDir: str or None, optional
|
|
713
|
+
path to CAC file cache directory. If None, the default path is used.
|
|
714
|
+
|
|
715
|
+
skip_initial_line : bool, default: True
|
|
716
|
+
controls the behaviour of the binary reader: if set to True,
|
|
717
|
+
all first lines of data in the binary files are skipped
|
|
718
|
+
otherwise they are read. Default value is True, as the data in
|
|
719
|
+
the initial file have usually no scienitific merit (random
|
|
720
|
+
value or arbitrarily old); only for debugging purposes one may
|
|
721
|
+
want to have the initial data line read.
|
|
722
|
+
|
|
723
|
+
'''
|
|
724
|
+
|
|
725
|
+
SKIP_INITIAL_LINE = True
|
|
726
|
+
|
|
727
|
+
def __init__(self,filename, cacheDir=None, skip_initial_line=True):
|
|
728
|
+
|
|
729
|
+
self.filename=filename
|
|
730
|
+
self.skip_initial_line = skip_initial_line
|
|
731
|
+
logger.debug('Opening %s', filename)
|
|
732
|
+
if cacheDir==None:
|
|
733
|
+
self.cacheDir=DBDCache.CACHEDIR
|
|
734
|
+
else:
|
|
735
|
+
self.cacheDir=cacheDir
|
|
736
|
+
if dbdreader.decompress.is_compressed(filename):
|
|
737
|
+
with dbdreader.decompress.CompressedFile(filename) as self.fp:
|
|
738
|
+
self.headerInfo,parameterInfo,self.cacheFound, self.cacheID = self._read_header(self.cacheDir)
|
|
739
|
+
else:
|
|
740
|
+
with open(filename,'br') as self.fp:
|
|
741
|
+
self.headerInfo,parameterInfo,self.cacheFound, self.cacheID = self._read_header(self.cacheDir)
|
|
742
|
+
# number of bytes each states section consists of:
|
|
743
|
+
self.n_state_bytes=self.headerInfo['state_bytes_per_cycle']
|
|
744
|
+
# size of variables used
|
|
745
|
+
self.byteSizes=tuple([i[0] for i in parameterInfo])
|
|
746
|
+
# note: python <2.6 can't index tuples.
|
|
747
|
+
self.parameterNames=[i[1] for i in parameterInfo]
|
|
748
|
+
self.parameterUnits=dict((i[1],i[2]) for i in parameterInfo)
|
|
749
|
+
self.timeVariable=self._set_timeVariable()
|
|
750
|
+
if not self.cacheFound:
|
|
751
|
+
mesg = f"\nCache file {self.cacheID} for {self.filename} was not found in the cache directory ({self.cacheDir})."
|
|
752
|
+
data = DbdError.MissingCacheFileData({self.cacheID:[self.filename]}, self.cacheDir)
|
|
753
|
+
raise DbdError(DBD_ERROR_CACHE_NOT_FOUND, mesg=mesg, data=data)
|
|
754
|
+
|
|
755
|
+
def get_mission_name(self):
|
|
756
|
+
''' Returns the mission name such as micro.mi '''
|
|
757
|
+
return self.headerInfo['mission_name'].lower()
|
|
758
|
+
|
|
759
|
+
def get_fileopen_time(self):
|
|
760
|
+
''' Returns the time stamp of opening the file in UTC '''
|
|
761
|
+
return self._get_fileopen_time()
|
|
762
|
+
|
|
763
|
+
def close(self):
|
|
764
|
+
''' Closes a DBD file '''
|
|
765
|
+
return self.fp.close()
|
|
766
|
+
|
|
767
|
+
def get(self,*parameters,decimalLatLon=True,discardBadLatLon=True, return_nans=False, max_values_to_read=-1,
|
|
768
|
+
check_for_invalid_parameters=True):
|
|
769
|
+
'''Returns time and parameter data for requested parameter
|
|
770
|
+
|
|
771
|
+
This method reads the requested parameter, and convert it
|
|
772
|
+
optionally to decimal format if the parameter is latitude-like
|
|
773
|
+
or longitude-like
|
|
774
|
+
|
|
775
|
+
Parameters
|
|
776
|
+
----------
|
|
777
|
+
*parameters: variable length list of str
|
|
778
|
+
parameter name
|
|
779
|
+
|
|
780
|
+
decimalLatLon : bool, optional
|
|
781
|
+
If True (default), latitiude and longitude related parameters are converted to
|
|
782
|
+
decimal format, as opposed to nmea format.
|
|
783
|
+
|
|
784
|
+
discardBadLatLon : bool, optional
|
|
785
|
+
If True (default), bogus latitiude and longitude values are ignored.
|
|
786
|
+
|
|
787
|
+
return_nans : bool, optional
|
|
788
|
+
if True, nans are returned for timestamps the variable was not updated or changed.
|
|
789
|
+
|
|
790
|
+
max_values_to_read : int, optional
|
|
791
|
+
if > 0, reading is stopped after this many values have been read.
|
|
792
|
+
|
|
793
|
+
check_for_invalid_parameters : bool, optional
|
|
794
|
+
if True returns empty arrays for parameters that are marked as invalid, instead of triggering an exception.
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
Returns
|
|
798
|
+
-------
|
|
799
|
+
tuple of (ndarray, ndarray) for each parameter requested.
|
|
800
|
+
time vector (in seconds) and value vector
|
|
801
|
+
|
|
802
|
+
Raises
|
|
803
|
+
------
|
|
804
|
+
DbdError when the requested parameter(s) cannot be read.
|
|
805
|
+
|
|
806
|
+
.. versionchanged:: 0.4.0 Multi parameters can be passed, giving a time,value tuple for each parameter.
|
|
807
|
+
.. versionchanged:: 0.5.5 For a single parameter request, the number of values to be read can be limited.
|
|
808
|
+
|
|
809
|
+
'''
|
|
810
|
+
# It only makes sense to limit the number of parameters read when a single parameter is requested. Check for this.
|
|
811
|
+
if max_values_to_read>0 and len(parameters)!=1:
|
|
812
|
+
raise ValueError("Limiting the values to be read for multiple parameters potentially yields undefined behaviour.\n")
|
|
813
|
+
|
|
814
|
+
timestamps, values = self._get(*parameters, decimalLatLon=decimalLatLon,
|
|
815
|
+
discardBadLatLon=discardBadLatLon, return_nans=return_nans,
|
|
816
|
+
max_values_to_read=max_values_to_read,
|
|
817
|
+
check_for_invalid_parameters=check_for_invalid_parameters)
|
|
818
|
+
r = [(t,v) for t, v in zip(timestamps, values)]
|
|
819
|
+
|
|
820
|
+
if len(parameters)==1:
|
|
821
|
+
return r[0]
|
|
822
|
+
else:
|
|
823
|
+
return r
|
|
824
|
+
|
|
825
|
+
def get_list(self,*parameters,decimalLatLon=True,discardBadLatLon=True,
|
|
826
|
+
return_nans=False):
|
|
827
|
+
''' Returns time and value tuples for a list of requested parameters
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
This method returns time and values tuples for a list of parameters. It
|
|
831
|
+
is basically a short-hand for a looped get() method.
|
|
832
|
+
|
|
833
|
+
Note that each parameter comes with its own time base. No interpolation
|
|
834
|
+
is done. Use get_sync() for that in stead.
|
|
835
|
+
|
|
836
|
+
Parameters
|
|
837
|
+
----------
|
|
838
|
+
*parameters: list of str
|
|
839
|
+
list of parameter names
|
|
840
|
+
|
|
841
|
+
decimalLatLon : bool, optional
|
|
842
|
+
If True (default), latitiude and longitude related parameters are converted to
|
|
843
|
+
decimal format, as opposed to nmea format.
|
|
844
|
+
|
|
845
|
+
discardBadLatLon : bool, optional
|
|
846
|
+
If True (default), bogus latitiude and longitude values are ignored.
|
|
847
|
+
|
|
848
|
+
return_nans : bool
|
|
849
|
+
If True, nan's are returned for those timestamps where no new value is available.
|
|
850
|
+
Default value: False
|
|
851
|
+
|
|
852
|
+
Returns
|
|
853
|
+
-------
|
|
854
|
+
list of (ndarray, ndarray)
|
|
855
|
+
list of tuples of time and value vectors for each parameter requested.
|
|
856
|
+
|
|
857
|
+
.. deprecated:: 0.4.0
|
|
858
|
+
|
|
859
|
+
.. note::
|
|
860
|
+
This function will be removed in a future version. Use .get() instead.
|
|
861
|
+
'''
|
|
862
|
+
logger.info("get_list has been deprecated in version 0.4.0 and may be removed in the future. Use get instead.")
|
|
863
|
+
return self.get(*parameters,decimalLatLon=decimalLatLon ,discardBadLatLon=discardBadLatLon , return_nans=return_nans)
|
|
864
|
+
|
|
865
|
+
def get_xy(self,parameter_x,parameter_y,decimalLatLon=True, discardBadLatLon=True):
|
|
866
|
+
''' Returns values of parameter_x and paramter_y
|
|
867
|
+
|
|
868
|
+
For parameters parameter_x and parameter_y this method returns a tuple
|
|
869
|
+
with the values of both parameters. If necessary, the time base of
|
|
870
|
+
parameter_y is interpolated onto the one of parameter_x.
|
|
871
|
+
|
|
872
|
+
Parameters
|
|
873
|
+
----------
|
|
874
|
+
parameter_x: str
|
|
875
|
+
parameter name of x-parameter
|
|
876
|
+
|
|
877
|
+
parameter_y: str
|
|
878
|
+
parameter name of y-parameter
|
|
879
|
+
|
|
880
|
+
decimalLatLon : bool, optional
|
|
881
|
+
If True (default), latitiude and longitude related parameters are converted to
|
|
882
|
+
decimal format, as opposed to nmea format.
|
|
883
|
+
|
|
884
|
+
discardBadLatLon : bool, optional
|
|
885
|
+
If True (default), bogus latitiude and longitude values are ignored.
|
|
886
|
+
|
|
887
|
+
Returns
|
|
888
|
+
-------
|
|
889
|
+
(ndarray, ndarray)
|
|
890
|
+
tuple of value vectors
|
|
891
|
+
'''
|
|
892
|
+
_, x, y = self._get_sync(parameter_x, parameter_y,
|
|
893
|
+
decimalLatLon=decimalLatLon, discardBadLatLon=discardBadLatLon)
|
|
894
|
+
return x, y
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def get_sync(self, *sync_parameters, decimalLatLon=True, discardBadLatLon=True):
|
|
898
|
+
'''Returns a list of values from parameters, all interpolated to the
|
|
899
|
+
time base of the first paremeter
|
|
900
|
+
|
|
901
|
+
This method is used if a number of parameters should be interpolated
|
|
902
|
+
onto the same time base.
|
|
903
|
+
|
|
904
|
+
Parameters
|
|
905
|
+
----------
|
|
906
|
+
*sync_parameters: variable length list of str
|
|
907
|
+
parameter names. Minimal length is 2. The time base of the first parameter is
|
|
908
|
+
used to interpolate all other parameters onto.
|
|
909
|
+
|
|
910
|
+
decimalLatLon : bool, optional
|
|
911
|
+
If True (default), latitiude and longitude related parameters are converted to
|
|
912
|
+
decimal format, as opposed to nmea format.
|
|
913
|
+
|
|
914
|
+
discardBadLatLon : bool, optional
|
|
915
|
+
If True (default), bogus latitiude and longitude values are ignored.
|
|
916
|
+
|
|
917
|
+
Returns
|
|
918
|
+
-------
|
|
919
|
+
(ndarray, ndarray, ...)
|
|
920
|
+
Time vector (of first parameter), values of first parmaeter, and
|
|
921
|
+
interpolated values of subsequent parameters.
|
|
922
|
+
|
|
923
|
+
Example:
|
|
924
|
+
|
|
925
|
+
get_sync('m_water_pressure','m_water_cond','m_water_temp')
|
|
926
|
+
|
|
927
|
+
Notes
|
|
928
|
+
-----
|
|
929
|
+
.. versionchanged:: 0.4.0
|
|
930
|
+
Calling signature has changed from the sync parameters
|
|
931
|
+
passed on as a list, to passed on as parameters.
|
|
932
|
+
'''
|
|
933
|
+
if len(sync_parameters)<2:
|
|
934
|
+
raise ValueError('Expect at least two parameters.')
|
|
935
|
+
if len(sync_parameters)==2 and (isinstance(sync_parameters[1], list) or isinstance(sync_parameters[1], tuple)):
|
|
936
|
+
# obsolete calling signature.
|
|
937
|
+
logger.info("Calling signature of get_sync() has changed in version 0.4.0.")
|
|
938
|
+
sync_parameters = [sync_parameters[0]] + sync_parameters[1]
|
|
939
|
+
return self._get_sync(*sync_parameters, decimalLatLon=decimalLatLon, discardBadLatLon=discardBadLatLon)
|
|
940
|
+
|
|
941
|
+
def has_parameter(self,parameter):
|
|
942
|
+
''' Check wheter this file contains parameter
|
|
943
|
+
|
|
944
|
+
Parameters
|
|
945
|
+
----------
|
|
946
|
+
parameter: str
|
|
947
|
+
parameter to check
|
|
948
|
+
|
|
949
|
+
Returns
|
|
950
|
+
-------
|
|
951
|
+
bool
|
|
952
|
+
True if parameter is in the list, or False if not
|
|
953
|
+
'''
|
|
954
|
+
return (parameter in self.parameterNames)
|
|
955
|
+
|
|
956
|
+
# Private methods:
|
|
957
|
+
|
|
958
|
+
def _get_fileopen_time(self):
|
|
959
|
+
datestr=self.headerInfo['fileopen_time'].replace("_"," ")
|
|
960
|
+
fmt="%a %b %d %H:%M:%S %Y"
|
|
961
|
+
seconds=strptimeToEpoch(datestr, fmt)
|
|
962
|
+
return seconds
|
|
963
|
+
|
|
964
|
+
def _set_timeVariable(self):
|
|
965
|
+
if 'm_present_time' in self.parameterNames:
|
|
966
|
+
return 'm_present_time'
|
|
967
|
+
else:
|
|
968
|
+
return 'sci_m_present_time'
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _get(self,*parameters,decimalLatLon=True,discardBadLatLon=False, return_nans=False,
|
|
972
|
+
max_values_to_read=-1, check_for_invalid_parameters=True):
|
|
973
|
+
''' returns time and parameter data for requested parameter '''
|
|
974
|
+
invalid_parameters = self._get_valid_parameters(parameters, invert=True, global_scope=True)
|
|
975
|
+
if invalid_parameters and check_for_invalid_parameters:
|
|
976
|
+
# Do not trigger an exception if we allow parameters without data to return empty arrays.
|
|
977
|
+
if len(invalid_parameters)==1:
|
|
978
|
+
mesg = f"Parameter {invalid_parameters[0]} is an unknown glider sensor name. ({self.filename})"
|
|
979
|
+
else:
|
|
980
|
+
mesg = f"Parameters {{{','.join(invalid_parameters)}}} are unknown glider sensor names. ({self.filename})"
|
|
981
|
+
raise DbdError(value=DBD_ERROR_NO_VALID_PARAMETERS, mesg=mesg, data=invalid_parameters)
|
|
982
|
+
|
|
983
|
+
valid_parameters = self._get_valid_parameters(parameters)
|
|
984
|
+
missing_parameters = self._get_valid_parameters(parameters, invert=True)
|
|
985
|
+
number_valid_parameters = len(valid_parameters)
|
|
986
|
+
if not self.timeVariable in self.parameterNames:
|
|
987
|
+
raise DbdError(DBD_ERROR_NO_TIME_VARIABLE)
|
|
988
|
+
|
|
989
|
+
# OK, we have some parameters to return:
|
|
990
|
+
if missing_parameters:
|
|
991
|
+
logger.warning(f"Requested parameters not found: {','.join(missing_parameters)}.")
|
|
992
|
+
|
|
993
|
+
ti=self.parameterNames.index(self.timeVariable)
|
|
994
|
+
idx = [self.parameterNames.index(p) for p in valid_parameters]
|
|
995
|
+
idx_sorted=numpy.sort(idx)
|
|
996
|
+
vi = tuple(idx_sorted)
|
|
997
|
+
self.n_sensors=self.headerInfo['sensors_per_cycle']
|
|
998
|
+
error_no, r=_dbdreader.get(self.n_state_bytes,
|
|
999
|
+
self.n_sensors,
|
|
1000
|
+
self.fp_binary_start,
|
|
1001
|
+
self.byteSizes,
|
|
1002
|
+
self.filename,
|
|
1003
|
+
ti,
|
|
1004
|
+
vi,
|
|
1005
|
+
int(return_nans),
|
|
1006
|
+
int(self.skip_initial_line),
|
|
1007
|
+
max_values_to_read)
|
|
1008
|
+
if error_no:
|
|
1009
|
+
s = dbdreader.decompress.DECOMPRESSION_ERROR_LIST[error_no]
|
|
1010
|
+
raise DbdError(value=DBD_ERROR_READ_ERROR, mesg=f"Decompression of {self.filename} failed with an '{s}' error.", data=error_no)
|
|
1011
|
+
|
|
1012
|
+
# map the contents of vi on timestamps and values, preserving the original order:
|
|
1013
|
+
idx_reorderd = [vi.index(i) for i in idx]
|
|
1014
|
+
# these are for good_parameters:
|
|
1015
|
+
timestamps = [numpy.array(r[i]) for i in idx_reorderd]
|
|
1016
|
+
values = [numpy.array(r[number_valid_parameters+i]) for i in idx_reorderd]
|
|
1017
|
+
# convert to decimal lat lon if applicable:
|
|
1018
|
+
for i, p in enumerate(valid_parameters):
|
|
1019
|
+
if return_nans:
|
|
1020
|
+
idx = numpy.where(numpy.isclose(values[i],1e9))[0]
|
|
1021
|
+
values[i][idx] = numpy.nan
|
|
1022
|
+
if self._is_latlon_parameter(p):
|
|
1023
|
+
if discardBadLatLon and not return_nans: #discards and return nans is not compatible.
|
|
1024
|
+
# p is either a latitude or longitude parameter. Check now which one it is.
|
|
1025
|
+
if "lat" in p:
|
|
1026
|
+
value_limit = 9000 # nmea style
|
|
1027
|
+
else:
|
|
1028
|
+
value_limit = 18000 # nmea style
|
|
1029
|
+
condition = numpy.logical_and(values[i]>=-value_limit, values[i]<=value_limit)
|
|
1030
|
+
timestamps[i], values[i] = numpy.compress(condition, (timestamps[i], values[i]), axis=1)
|
|
1031
|
+
if decimalLatLon:
|
|
1032
|
+
values[i] = toDec(values[i])
|
|
1033
|
+
# if we have any invalid parameters, insert empty arrays in the right places, or full length nan vectors if return_nans is True
|
|
1034
|
+
if return_nans:
|
|
1035
|
+
n_timestamps = timestamps[0].shape[0]
|
|
1036
|
+
def get_empty_array():
|
|
1037
|
+
return numpy.ones(n_timestamps)*numpy.nan
|
|
1038
|
+
else:
|
|
1039
|
+
def get_empty_array():
|
|
1040
|
+
return numpy.array([])
|
|
1041
|
+
for missing_parameter in missing_parameters:
|
|
1042
|
+
idx = parameters.index(missing_parameter)
|
|
1043
|
+
timestamps.insert(idx, get_empty_array())
|
|
1044
|
+
values.insert(idx, get_empty_array())
|
|
1045
|
+
return timestamps, values
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def _get_sync(self,*params, decimalLatLon=True,discardBadLatLon=True):
|
|
1049
|
+
'''
|
|
1050
|
+
x: dbdparameter name
|
|
1051
|
+
|
|
1052
|
+
y: list of dbd parameter names
|
|
1053
|
+
|
|
1054
|
+
returns a list of
|
|
1055
|
+
t, parameter x, parameter y0, parameter y1, ...
|
|
1056
|
+
where the y parameters are synchronized to x.
|
|
1057
|
+
|
|
1058
|
+
if decimalLatLon, then all lat/lon type variables are converted
|
|
1059
|
+
to decimal values prior to interpolation.
|
|
1060
|
+
|
|
1061
|
+
example:
|
|
1062
|
+
|
|
1063
|
+
get_sync('m_water_pressure','m_water_cond','m_water_temp')
|
|
1064
|
+
'''
|
|
1065
|
+
timestamps, values = self._get(*params,decimalLatLon=decimalLatLon,discardBadLatLon=discardBadLatLon)
|
|
1066
|
+
t = timestamps[0]
|
|
1067
|
+
if t.shape[0] == 0:
|
|
1068
|
+
raise DbdError(DBD_ERROR_NO_DATA_TO_INTERPOLATE_TO)
|
|
1069
|
+
|
|
1070
|
+
r = []
|
|
1071
|
+
for i, (_t, _v) in enumerate(zip(timestamps, values)):
|
|
1072
|
+
if i==0:
|
|
1073
|
+
r.append(_t)
|
|
1074
|
+
r.append(_v)
|
|
1075
|
+
else:
|
|
1076
|
+
try:
|
|
1077
|
+
r.append(numpy.interp(t, _t, _v, left=numpy.nan, right=numpy.nan))
|
|
1078
|
+
except ValueError:
|
|
1079
|
+
r.append(t * numpy.nan)
|
|
1080
|
+
logger.info(f"No valid data to interpolate for '{params[i]}'.")
|
|
1081
|
+
|
|
1082
|
+
return tuple(r)
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def _get_valid_parameters(self,parameters, invert=False, global_scope=False):
|
|
1086
|
+
if global_scope:
|
|
1087
|
+
p = self.headerInfo['parameter_list']
|
|
1088
|
+
else:
|
|
1089
|
+
p = self.parameterNames
|
|
1090
|
+
if not invert:
|
|
1091
|
+
validParameters=[i for i in parameters if i in p]
|
|
1092
|
+
else:
|
|
1093
|
+
validParameters=[i for i in parameters if not i in p]
|
|
1094
|
+
return validParameters
|
|
1095
|
+
|
|
1096
|
+
def _is_latlon_parameter(self,x):
|
|
1097
|
+
return x in LATLON_PARAMS
|
|
1098
|
+
|
|
1099
|
+
def _read_header(self, cacheDir):
|
|
1100
|
+
if not os.path.exists(cacheDir):
|
|
1101
|
+
raise DbdError(DBD_ERROR_CACHEDIR_NOT_FOUND, " (%s)"%(cacheDir))
|
|
1102
|
+
dbdheader = DBDHeader()
|
|
1103
|
+
result = dbdheader.read_header(self.fp)
|
|
1104
|
+
if result == DBD_ERROR_INVALID_DBD_FILE:
|
|
1105
|
+
raise DbdError(DBD_ERROR_INVALID_DBD_FILE,
|
|
1106
|
+
f"{self.filename} seems not to be a valid DBD file.")
|
|
1107
|
+
elif result == DBD_ERROR_INVALID_ENCODING:
|
|
1108
|
+
raise DbdError(DBD_ERROR_INVALID_ENCODING,
|
|
1109
|
+
f'{self.filename} has an invalid encoding.')
|
|
1110
|
+
elif result == DBD_ERROR_DECOMPRESSION_ERROR:
|
|
1111
|
+
raise DbdError(DBD_ERROR_DECOMPRESSION_ERROR,
|
|
1112
|
+
f'{self.filename} could not be decompressed.')
|
|
1113
|
+
# determine cache file name
|
|
1114
|
+
cacheID = dbdheader.info['sensor_list_crc'].lower()
|
|
1115
|
+
cacheFilename=os.path.join(cacheDir,cacheID+".cac")
|
|
1116
|
+
cacheFound=True # unless proven otherwise...
|
|
1117
|
+
parameter=[]
|
|
1118
|
+
if dbdheader.factored==1:
|
|
1119
|
+
# read sensorlist from cache
|
|
1120
|
+
if os.path.exists(cacheFilename):
|
|
1121
|
+
fpCache=open(cacheFilename,'br')
|
|
1122
|
+
parameter=dbdheader.read_cache(fpCache)
|
|
1123
|
+
fpCache.close()
|
|
1124
|
+
else:
|
|
1125
|
+
cacheFound=False
|
|
1126
|
+
else: # no need to check for factored==None; the value has been set for sure.
|
|
1127
|
+
# read sensorlist from same file and copy
|
|
1128
|
+
if not os.path.exists(cacheFilename):
|
|
1129
|
+
# only write, when not existing.
|
|
1130
|
+
fpCache=open(cacheFilename,'w')
|
|
1131
|
+
parameter=dbdheader.read_cache(self.fp,fpCache)
|
|
1132
|
+
fpCache.close()
|
|
1133
|
+
else:
|
|
1134
|
+
# keep reading from same file
|
|
1135
|
+
parameter=dbdheader.read_cache(self.fp)
|
|
1136
|
+
self.fp_binary_start=self.fp.tell() # marks the start of the
|
|
1137
|
+
# binary part of the file
|
|
1138
|
+
return (dbdheader.info,parameter,cacheFound, cacheID)
|
|
1139
|
+
|
|
1140
|
+
def _get_by_read_per_byte(self,parameter):
|
|
1141
|
+
''' method that reads the file byte by byte and processes
|
|
1142
|
+
accordingly. As opposed to read the whole file in memory and do the
|
|
1143
|
+
processing then.'''
|
|
1144
|
+
# first n bytes are not used?
|
|
1145
|
+
self.n_sensors=self.headerInfo['sensors_per_cycle']
|
|
1146
|
+
self.fp.seek(0,2) # move to end of file
|
|
1147
|
+
fp_end=self.fp.tell()
|
|
1148
|
+
self.fp.seek(self.fp_binary_start+17)# set file pointer to
|
|
1149
|
+
# start binary block (17
|
|
1150
|
+
# positions are used for
|
|
1151
|
+
# something else, which I
|
|
1152
|
+
# can't figure out
|
|
1153
|
+
paramidx=(self.ti,self.vi)
|
|
1154
|
+
R=dict((i,[]) for i in paramidx)
|
|
1155
|
+
while True:
|
|
1156
|
+
offsets,chunksize=self._read_state_bytes(paramidx)
|
|
1157
|
+
fp=self.fp.tell()
|
|
1158
|
+
if offsets!=None:
|
|
1159
|
+
# we found at least one value we would like to read, otherwise skip directly to the
|
|
1160
|
+
# next state block.
|
|
1161
|
+
for offset,idx in zip(offsets,paramidx):
|
|
1162
|
+
if offset!=-1:
|
|
1163
|
+
self.fp.seek(fp+offset)
|
|
1164
|
+
x=self.fp.read(self.byteSizes[idx])
|
|
1165
|
+
xs=self._convert_bytearray(x)
|
|
1166
|
+
R[idx].append(xs)
|
|
1167
|
+
else:
|
|
1168
|
+
R[idx].append(R[idx][-1])
|
|
1169
|
+
# jump to the next state block
|
|
1170
|
+
if fp+chunksize+1>=fp_end:
|
|
1171
|
+
# jumped beyond the end.
|
|
1172
|
+
break
|
|
1173
|
+
self.fp.seek(fp+chunksize+1)
|
|
1174
|
+
return [R[i] for i in paramidx]
|
|
1175
|
+
|
|
1176
|
+
def _get_by_read_per_chunk(self,parameter):
|
|
1177
|
+
''' method that reads the file chunk by chunk.
|
|
1178
|
+
'''
|
|
1179
|
+
# first n bytes are not used?
|
|
1180
|
+
self.n_sensors=self.headerInfo['sensors_per_cycle']
|
|
1181
|
+
self.fp.seek(0,2) # move to end of file
|
|
1182
|
+
fp_end=self.fp.tell()
|
|
1183
|
+
self.fp.seek(self.fp_binary_start+17)# set file pointer to
|
|
1184
|
+
# start binary block (17
|
|
1185
|
+
# positions are used for
|
|
1186
|
+
# something else, which I
|
|
1187
|
+
# can't figure out
|
|
1188
|
+
paramidx=(self.ti,self.vi)
|
|
1189
|
+
R=dict((i,[]) for i in paramidx)
|
|
1190
|
+
while True:
|
|
1191
|
+
offsets,chunksize=self._read_state_bytes(paramidx)
|
|
1192
|
+
fp=self.fp.tell()
|
|
1193
|
+
if offsets!=None:
|
|
1194
|
+
# we found at least one value we would like to read, otherwise skip directly to the
|
|
1195
|
+
# next state block.
|
|
1196
|
+
chunk=self.fp.read(chunksize+1)
|
|
1197
|
+
for offset,idx in zip(offsets,paramidx):
|
|
1198
|
+
if offset!=-1:
|
|
1199
|
+
s=self.byteSizes[idx]
|
|
1200
|
+
xs=self._convert_bytearray(chunk[offset:offset+s])
|
|
1201
|
+
R[idx].append(xs)
|
|
1202
|
+
else:
|
|
1203
|
+
R[idx].append(R[idx][-1])
|
|
1204
|
+
else:
|
|
1205
|
+
self.fp.seek(fp+chunksize+1)
|
|
1206
|
+
|
|
1207
|
+
if fp+chunksize+1>=fp_end:
|
|
1208
|
+
# jumped beyond the end.
|
|
1209
|
+
break
|
|
1210
|
+
return [R[i] for i in paramidx]
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def _read_state_bytes(self,reqd_variables):
|
|
1214
|
+
''' reads state bytes and returns:
|
|
1215
|
+
offsets, chunksize
|
|
1216
|
+
offsets: list of offsets to read the variables
|
|
1217
|
+
if 0: copy previous
|
|
1218
|
+
if None, chunksize is returned, not all required variables
|
|
1219
|
+
were updated.
|
|
1220
|
+
'''
|
|
1221
|
+
bits_per_byte=8
|
|
1222
|
+
bits_per_field=2
|
|
1223
|
+
mask=3
|
|
1224
|
+
bitshift=bits_per_byte-bits_per_field
|
|
1225
|
+
fields_per_byte=bits_per_byte//bits_per_field
|
|
1226
|
+
offset=0
|
|
1227
|
+
n=0
|
|
1228
|
+
vi=0
|
|
1229
|
+
offsets=[0 for i in range(len(reqd_variables))]
|
|
1230
|
+
state_bytes=self.fp.read(self.n_state_bytes)
|
|
1231
|
+
for sb in state_bytes:
|
|
1232
|
+
for fld in range(fields_per_byte):
|
|
1233
|
+
field=(sb>>bitshift) & mask
|
|
1234
|
+
sb<<=2
|
|
1235
|
+
if field==2:
|
|
1236
|
+
# variable is updated
|
|
1237
|
+
if vi in reqd_variables:
|
|
1238
|
+
# this variable is asked for.
|
|
1239
|
+
# so record its position.
|
|
1240
|
+
offsets[n]=offset
|
|
1241
|
+
n+=1
|
|
1242
|
+
offset+=self.byteSizes[vi]
|
|
1243
|
+
if field==1 and (vi in reqd_variables):
|
|
1244
|
+
# this variable is asked for, but has an old
|
|
1245
|
+
# variable. So not being read
|
|
1246
|
+
offsets[n]=-1
|
|
1247
|
+
n+=1
|
|
1248
|
+
vi+=1 # next variable.
|
|
1249
|
+
if n==len(reqd_variables):
|
|
1250
|
+
return offsets,offset
|
|
1251
|
+
else:
|
|
1252
|
+
return None,offset
|
|
1253
|
+
|
|
1254
|
+
def _convert_bytearray(self,bs):
|
|
1255
|
+
''' converts a byte sequence of length 4 or 8 bytes
|
|
1256
|
+
to a floating point.'''
|
|
1257
|
+
# the byte sequence read should be reversed and then unpacked.
|
|
1258
|
+
# this may be a costly operation...
|
|
1259
|
+
if len(bs)==4:
|
|
1260
|
+
return struct.unpack("f",bs[::-1])[0]
|
|
1261
|
+
else:
|
|
1262
|
+
return struct.unpack("d",bs[::-1])[0]
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
class MultiDBD(object):
|
|
1266
|
+
'''Opens multiple dbd files for reading
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
This class is intended for reading multiple dbd files and treating
|
|
1270
|
+
them as one.
|
|
1271
|
+
|
|
1272
|
+
Parameters
|
|
1273
|
+
----------
|
|
1274
|
+
filenames : list of str or None
|
|
1275
|
+
list of filenames to open
|
|
1276
|
+
pattern : str or None
|
|
1277
|
+
search pattern as passed to glob
|
|
1278
|
+
|
|
1279
|
+
cacheDir: str or None
|
|
1280
|
+
path to directory with CAC cache files (None: the default directory is used)
|
|
1281
|
+
|
|
1282
|
+
complemented_files_only : bool
|
|
1283
|
+
if True, only those files are retained for which both engineering and science
|
|
1284
|
+
data files are available.
|
|
1285
|
+
|
|
1286
|
+
complement_files : bool
|
|
1287
|
+
If True automatically include matching [de]bd files
|
|
1288
|
+
|
|
1289
|
+
banned_missions: list of str
|
|
1290
|
+
List of mission names that should be disregarded.
|
|
1291
|
+
|
|
1292
|
+
missions: list of str
|
|
1293
|
+
List of missions names that should be considered only.
|
|
1294
|
+
|
|
1295
|
+
maxfiles: int
|
|
1296
|
+
maximum number of files to be read, where
|
|
1297
|
+
>0: the first n files are read
|
|
1298
|
+
<0: the last n files are read.
|
|
1299
|
+
|
|
1300
|
+
skip_initial_line: bool (default: True)
|
|
1301
|
+
If True, the first data line in each dbd file (and friends) is not read.
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
Notes
|
|
1306
|
+
-----
|
|
1307
|
+
|
|
1308
|
+
Upon creating the dbd file, when starting a new mission or dive segment, all parameters
|
|
1309
|
+
are written and marked as updated. In reality, most parameters are NOT update, and the
|
|
1310
|
+
value written is the value in memory, which may be several minutes old, or even longer. It
|
|
1311
|
+
has been pointed out to me that a handful parameters, are set only once, before creating the
|
|
1312
|
+
dbd file. Since these parameters are not of interest for normal data processing, the first
|
|
1313
|
+
line of data is skipped by default, but can be read if required.
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
.. versionchanged:: 0.4.0
|
|
1318
|
+
ensure_paired and included_paired keywords have been replaced by complemented_files_only
|
|
1319
|
+
and complement_files, respectively.
|
|
1320
|
+
'''
|
|
1321
|
+
def __init__(self,filenames=None,pattern=None,cacheDir=None,complemented_files_only=False,
|
|
1322
|
+
complement_files=False,banned_missions=[],missions=[],
|
|
1323
|
+
max_files=None, skip_initial_line=True):
|
|
1324
|
+
|
|
1325
|
+
self._ignore_cache=[] # list of files that should be ignored because out of set time limits
|
|
1326
|
+
self._accept_cache=[] # list of files that have data within set time limits
|
|
1327
|
+
self._parameter_names=dict(globally=set(), locally=set())
|
|
1328
|
+
if cacheDir is None:
|
|
1329
|
+
cacheDir=DBDCache.CACHEDIR
|
|
1330
|
+
self.banned_missions=banned_missions
|
|
1331
|
+
self.missions=missions
|
|
1332
|
+
self.mission_list=[]
|
|
1333
|
+
if not filenames and not pattern:
|
|
1334
|
+
raise DbdError(DBD_ERROR_NO_FILE_CRITERIUM_SPECIFIED)
|
|
1335
|
+
fns=DBDList()
|
|
1336
|
+
# A common mistake is to just supply a string for filenames (first argument)
|
|
1337
|
+
# Assume that it was meant as a pattern IF pattern is None.
|
|
1338
|
+
if isinstance(filenames, str):
|
|
1339
|
+
if pattern is None:
|
|
1340
|
+
pattern = filenames # assume filenames should have been pattern and hope for the best.
|
|
1341
|
+
filenames = None
|
|
1342
|
+
else:
|
|
1343
|
+
raise DbdError(DBD_ERROR_INVALID_FILE_CRITERION_SPECIFIED, "I got a string for <filenames> (no list), and a string for <pattern>.")
|
|
1344
|
+
if filenames:
|
|
1345
|
+
fns+=filenames
|
|
1346
|
+
if pattern:
|
|
1347
|
+
fns+=glob.glob(pattern)
|
|
1348
|
+
if len(fns)==0:
|
|
1349
|
+
raise DbdError(DBD_ERROR_NO_FILES_FOUND)
|
|
1350
|
+
fns.sort()
|
|
1351
|
+
if max_files and max_files>0:
|
|
1352
|
+
self.filenames=fns[:max_files]
|
|
1353
|
+
elif max_files and max_files<0:
|
|
1354
|
+
self.filenames=fns[max_files:]
|
|
1355
|
+
else:
|
|
1356
|
+
self.filenames=fns
|
|
1357
|
+
|
|
1358
|
+
if complement_files:
|
|
1359
|
+
self._add_paired_filenames()
|
|
1360
|
+
|
|
1361
|
+
if complemented_files_only:
|
|
1362
|
+
self.pruned_files=self._prune_unmatched(cacheDir)
|
|
1363
|
+
|
|
1364
|
+
self._update_dbd_inventory(cacheDir, skip_initial_line)
|
|
1365
|
+
self.parameterNames=dict((k,self._getParameterList(v)) \
|
|
1366
|
+
for k,v in self.dbds.items())
|
|
1367
|
+
self.parameterUnits=self._getParameterUnits()
|
|
1368
|
+
#
|
|
1369
|
+
self.time_limits_dataset=(None,None)
|
|
1370
|
+
self.time_limits=[None,None]
|
|
1371
|
+
self.set_time_limits()
|
|
1372
|
+
|
|
1373
|
+
##### public methods
|
|
1374
|
+
def get(self, *parameters, decimalLatLon=True, discardBadLatLon=True, return_nans=False, include_source=False,
|
|
1375
|
+
max_values_to_read=-1, continue_on_reading_error=False):
|
|
1376
|
+
'''Returns time and value tuple(s) for requested parameter(s)
|
|
1377
|
+
|
|
1378
|
+
This method returns time and values tuples for a list of parameters.
|
|
1379
|
+
|
|
1380
|
+
Note that each parameter comes with its own time base. No interpolation
|
|
1381
|
+
is done. Use get_sync() for that in stead.
|
|
1382
|
+
|
|
1383
|
+
Parameters
|
|
1384
|
+
----------
|
|
1385
|
+
parameter_list: list of str
|
|
1386
|
+
list of parameter names
|
|
1387
|
+
|
|
1388
|
+
decimalLatLon : bool, optional
|
|
1389
|
+
If True (default), latitiude and longitude related parameters are converted to
|
|
1390
|
+
decimal format, as opposed to nmea format.
|
|
1391
|
+
|
|
1392
|
+
discardBadLatLon : bool, optional
|
|
1393
|
+
If True (default), bogus latitiude and longitude values are ignored.
|
|
1394
|
+
|
|
1395
|
+
return_nans : bool
|
|
1396
|
+
If True, nan's are returned for those timestamps where no new value is available.
|
|
1397
|
+
Default value: False
|
|
1398
|
+
|
|
1399
|
+
include_source : bool, optional
|
|
1400
|
+
If True, a list with a reference for each data point to the DBD object, where the datapoint originated from.
|
|
1401
|
+
If called with a single parameter, a tuple of a Nx2 array with data and a list of N elements with refrences to a DBD object.
|
|
1402
|
+
If called for more parameters, a list of such tuples is returned.
|
|
1403
|
+
|
|
1404
|
+
Default value: False
|
|
1405
|
+
|
|
1406
|
+
max_values_to_read : int, optional
|
|
1407
|
+
if > 1, then reading is stopped after this many values have been read.
|
|
1408
|
+
Default value : -1
|
|
1409
|
+
|
|
1410
|
+
continue_on_reading_error : bool, optional
|
|
1411
|
+
if True, an exception will be raised when a file cannot be read. Otherwise the file will be ignored.
|
|
1412
|
+
|
|
1413
|
+
Returns
|
|
1414
|
+
-------
|
|
1415
|
+
(ndarray, ndarray) or
|
|
1416
|
+
((ndarray, ndarray), list) or
|
|
1417
|
+
[(ndarray, ndarray), (ndarray, ndarray), ...]
|
|
1418
|
+
[((ndarray, ndarray), list), ((ndarray, ndarray), list), ...]
|
|
1419
|
+
for a single parameter, for a single parameter, including source file list, for multiple parameters,
|
|
1420
|
+
for multiple parameters, including source file list, respectively.
|
|
1421
|
+
|
|
1422
|
+
.. versionchanged:: 0.5.5 For a single parameter request, the number of values to be read can be limited.
|
|
1423
|
+
|
|
1424
|
+
.. versionadded:: 0.5.9 Added option (continue_on_reading_error) to control the behaviour when an error is encountered whilst reading a compressed file.
|
|
1425
|
+
|
|
1426
|
+
'''
|
|
1427
|
+
# It only makes sense to limit the number of parameters read when a single parameter is requested. Check for this.
|
|
1428
|
+
if max_values_to_read>0 and len(parameters)!=1:
|
|
1429
|
+
raise ValueError("Limiting the values to be read for multiple parameters potentially yields undefined behaviour.\n")
|
|
1430
|
+
|
|
1431
|
+
eng_variables = []
|
|
1432
|
+
sci_variables = []
|
|
1433
|
+
positions = []
|
|
1434
|
+
invalid_parameters = self._get_valid_parameters(parameters, invert=True, global_scope=True)
|
|
1435
|
+
unavailable_parameters = self._get_valid_parameters(parameters, invert=True, global_scope=False)
|
|
1436
|
+
# invalid parameters are parameters that don't exist in any cache file used by the opened files
|
|
1437
|
+
# unavailable_parameters are parameters that are not stored in any of these files. They are marked F in the cache file.
|
|
1438
|
+
|
|
1439
|
+
if invalid_parameters:
|
|
1440
|
+
if len(invalid_parameters)==1:
|
|
1441
|
+
mesg = f"Parameter {invalid_parameters[0]} is an unknown glider sensor name."
|
|
1442
|
+
else:
|
|
1443
|
+
mesg = f"Parameters {{{','.join(invalid_parameters)}}} are unknown glider sensor names."
|
|
1444
|
+
raise DbdError(value=DBD_ERROR_NO_VALID_PARAMETERS, mesg=mesg, data=invalid_parameters)
|
|
1445
|
+
|
|
1446
|
+
# We don't want to trigger an abort if we ask for a parameter which has no data. Just return empty
|
|
1447
|
+
# arrays. If not desired, uncomment block below:
|
|
1448
|
+
#
|
|
1449
|
+
# if unavailable_parameters:
|
|
1450
|
+
# if len(unavailable_parameters)==1:
|
|
1451
|
+
# mesg = f"Parameter {unavailable_parameters[0]} has no data."
|
|
1452
|
+
# else:
|
|
1453
|
+
# mesg = f"Parameters {{{','.join(unavailable_parameters)}}} hava no data."
|
|
1454
|
+
# raise DbdError(value=DBD_ERROR_NO_DATA, mesg=mesg, data=unavailable_parameters)
|
|
1455
|
+
|
|
1456
|
+
for p in parameters:
|
|
1457
|
+
if p in self.parameterNames['sci']:
|
|
1458
|
+
positions.append(("sci", len(sci_variables)))
|
|
1459
|
+
sci_variables.append(p)
|
|
1460
|
+
elif p in self.parameterNames['eng']:
|
|
1461
|
+
positions.append(("eng", len(eng_variables)))
|
|
1462
|
+
eng_variables.append(p)
|
|
1463
|
+
|
|
1464
|
+
kwds=dict(decimalLatLon=decimalLatLon,
|
|
1465
|
+
discardBadLatLon=discardBadLatLon,
|
|
1466
|
+
return_nans=return_nans, include_source=include_source,
|
|
1467
|
+
max_values_to_read=max_values_to_read,
|
|
1468
|
+
continue_on_reading_error=continue_on_reading_error)
|
|
1469
|
+
|
|
1470
|
+
if len(sci_variables)>=1:
|
|
1471
|
+
r_sci = self._worker("sci", *sci_variables, **kwds)
|
|
1472
|
+
if len(eng_variables)>=1:
|
|
1473
|
+
r_eng = self._worker("eng", *eng_variables, **kwds)
|
|
1474
|
+
r = []
|
|
1475
|
+
for target, idx in positions:
|
|
1476
|
+
if target=='sci':
|
|
1477
|
+
r.append(r_sci[idx])
|
|
1478
|
+
else:
|
|
1479
|
+
r.append(r_eng[idx])
|
|
1480
|
+
for i,p in enumerate(parameters):
|
|
1481
|
+
if p in unavailable_parameters:
|
|
1482
|
+
r.insert(i, (numpy.array([]), numpy.array([])))
|
|
1483
|
+
if len(parameters)==1:
|
|
1484
|
+
return r[0]
|
|
1485
|
+
else:
|
|
1486
|
+
return r
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
def _get_valid_parameters(self,parameters, invert=False, global_scope=False):
|
|
1490
|
+
parameters = set(parameters)
|
|
1491
|
+
if global_scope:
|
|
1492
|
+
p = self._parameter_names['globally']
|
|
1493
|
+
else:
|
|
1494
|
+
p = self._parameter_names['locally']
|
|
1495
|
+
validParameters=p.intersection(parameters)
|
|
1496
|
+
if invert:
|
|
1497
|
+
validParameters=parameters.difference(p.intersection(parameters))
|
|
1498
|
+
return list(validParameters)
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
def get_xy(self,parameter_x,parameter_y,decimalLatLon=True, discardBadLatLon=True,
|
|
1502
|
+
interpolating_function_factory=None):
|
|
1503
|
+
''' Returns values of parameter_x and paramter_y
|
|
1504
|
+
|
|
1505
|
+
For parameters parameter_x and parameter_y this method returns a tuple
|
|
1506
|
+
with the values of both parameters. If necessary, the time base of
|
|
1507
|
+
parameter_y is interpolated onto the one of parameter_x.
|
|
1508
|
+
|
|
1509
|
+
Parameters
|
|
1510
|
+
----------
|
|
1511
|
+
parameter_x: str
|
|
1512
|
+
parameter name of x-parameter
|
|
1513
|
+
|
|
1514
|
+
parameter_y: str
|
|
1515
|
+
parameter name of y-parameter
|
|
1516
|
+
|
|
1517
|
+
decimalLatLon : bool, optional
|
|
1518
|
+
If True (default), latitiude and longitude related parameters are converted to
|
|
1519
|
+
decimal format, as opposed to nmea format.
|
|
1520
|
+
|
|
1521
|
+
discardBadLatLon : bool, optional
|
|
1522
|
+
If True (default), bogus latitiude and longitude values are ignored.
|
|
1523
|
+
|
|
1524
|
+
interpolating_function_factory : function factory, dictionary of function factories, None, optional
|
|
1525
|
+
Specification of a function factory to interpolate data. A dictionary of interpolating_function_factories
|
|
1526
|
+
allows the specification of specific functions for specific parameters. If none is defined, linear interpolation
|
|
1527
|
+
is used.
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
Returns
|
|
1531
|
+
-------
|
|
1532
|
+
(ndarray, ndarray)
|
|
1533
|
+
tuple of value vectors
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
.. versionadded:: 0.5.8
|
|
1537
|
+
keyword option interpolating_function_factory
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
'''
|
|
1541
|
+
_, x, y = self.get_sync(parameter_x, parameter_y, decimalLatLon=decimalLatLon,
|
|
1542
|
+
discardBadLatLon=discardBadLatLon, interpolating_function_factory=interpolating_function_factory)
|
|
1543
|
+
return x, y
|
|
1544
|
+
|
|
1545
|
+
def get_sync(self,*parameters,decimalLatLon=True, discardBadLatLon=True, interpolating_function_factory=None):
|
|
1546
|
+
''' Returns a list of values from parameters, all interpolated to the
|
|
1547
|
+
time base of the first paremeter
|
|
1548
|
+
|
|
1549
|
+
This method is used if a number of parameters should be interpolated
|
|
1550
|
+
onto the same time base.
|
|
1551
|
+
|
|
1552
|
+
Parameters
|
|
1553
|
+
----------
|
|
1554
|
+
*parameters: variable length list of str
|
|
1555
|
+
parameter names. Minimal length is 2. The time base of the first parameter is
|
|
1556
|
+
used to interpolate all other parameters onto.
|
|
1557
|
+
|
|
1558
|
+
decimalLatLon : bool, optional
|
|
1559
|
+
If True (default), latitiude and longitude related parameters are converted to
|
|
1560
|
+
decimal format, as opposed to nmea format.
|
|
1561
|
+
|
|
1562
|
+
discardBadLatLon : bool, optional
|
|
1563
|
+
If True (default), bogus latitiude and longitude values are ignored.
|
|
1564
|
+
|
|
1565
|
+
interpolating_function_factory : function factory, dictionary of function factories, None, optional
|
|
1566
|
+
Specification of a function factory to interpolate data. A dictionary of interpolating_function_factories
|
|
1567
|
+
allows the specification of specific functions for specific parameters. If none is defined, linear interpolation
|
|
1568
|
+
is used.
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
Returns
|
|
1572
|
+
-------
|
|
1573
|
+
(ndarray, ndarray, ...)
|
|
1574
|
+
Time vector (of first parameter), values of first parmaeter, and
|
|
1575
|
+
interpolated values of subsequent parameters.
|
|
1576
|
+
|
|
1577
|
+
Example:
|
|
1578
|
+
|
|
1579
|
+
get_sync('m_water_pressure','m_water_cond','m_water_temp')
|
|
1580
|
+
|
|
1581
|
+
Notes
|
|
1582
|
+
-----
|
|
1583
|
+
.. versionchanged:: 0.4.0
|
|
1584
|
+
Calling signature has changed from the sync parameters
|
|
1585
|
+
passed on as a list, to passed on as parameters.
|
|
1586
|
+
|
|
1587
|
+
.. versionadded:: 0.5.8
|
|
1588
|
+
keyword option interpolating_function_factory
|
|
1589
|
+
|
|
1590
|
+
'''
|
|
1591
|
+
if len(parameters)<2:
|
|
1592
|
+
raise ValueError('Expect at least two parameters.')
|
|
1593
|
+
if len(parameters)==2 and (isinstance(parameters[1], list) or isinstance(parameters[1], tuple)):
|
|
1594
|
+
# obsolete calling signature.
|
|
1595
|
+
logger.info("Calling signature of get_sync() has changed in version 0.4.0.")
|
|
1596
|
+
parameters = [parameters[0]] + parameters[1]
|
|
1597
|
+
tv = self.get(*parameters, decimalLatLon=decimalLatLon, discardBadLatLon=discardBadLatLon,
|
|
1598
|
+
return_nans=False)
|
|
1599
|
+
|
|
1600
|
+
default_interpolating_function_factory = partial(si_interp1d, bounds_error=False, fill_value=numpy.nan)
|
|
1601
|
+
|
|
1602
|
+
t = tv[0][0]
|
|
1603
|
+
r = []
|
|
1604
|
+
for i, (p,(_t, _v)) in enumerate(zip(parameters, tv)):
|
|
1605
|
+
if i==0:
|
|
1606
|
+
r.append(_t)
|
|
1607
|
+
r.append(_v)
|
|
1608
|
+
else:
|
|
1609
|
+
# Create an interpolation function factory
|
|
1610
|
+
logger.debug(f"Checking for ifun factory parameter {i}: {p}")
|
|
1611
|
+
if interpolating_function_factory is None:
|
|
1612
|
+
ifun_factory = default_interpolating_function_factory
|
|
1613
|
+
logger.debug("using default")
|
|
1614
|
+
else:
|
|
1615
|
+
try:
|
|
1616
|
+
ifun_factory = interpolating_function_factory[p]
|
|
1617
|
+
logger.debug(f"Using specific for parameter {p}")
|
|
1618
|
+
except KeyError:
|
|
1619
|
+
ifun_factory = default_interpolating_function_factory
|
|
1620
|
+
logger.debug(f"Using default")
|
|
1621
|
+
except TypeError:
|
|
1622
|
+
ifun_factory = interpolating_function_factory
|
|
1623
|
+
logger.debug(f"custom for all")
|
|
1624
|
+
try:
|
|
1625
|
+
ifun = ifun_factory(_t, _v)
|
|
1626
|
+
except ValueError:
|
|
1627
|
+
r.append(t * numpy.nan)
|
|
1628
|
+
logger.info(f"No valid data to interpolate for '{parameters[i]}'.")
|
|
1629
|
+
else:
|
|
1630
|
+
r.append(ifun(t))
|
|
1631
|
+
return tuple(r)
|
|
1632
|
+
|
|
1633
|
+
|
|
1634
|
+
|
|
1635
|
+
def get_CTD_sync(self, *parameters, decimalLatLon=True, discardBadLatLon=True,
|
|
1636
|
+
interpolating_function_factory=None):
|
|
1637
|
+
'''Returns a list of values from CTD and optionally other parameters,
|
|
1638
|
+
all interpolated to the time base of the CTD timestamp.
|
|
1639
|
+
|
|
1640
|
+
Parameters
|
|
1641
|
+
----------
|
|
1642
|
+
*parameters: variable length list of str
|
|
1643
|
+
names of parameters to be read additionally
|
|
1644
|
+
|
|
1645
|
+
decimalLatLon : bool, optional
|
|
1646
|
+
If True (default), latitiude and longitude related parameters are converted to
|
|
1647
|
+
decimal format, as opposed to nmea format.
|
|
1648
|
+
|
|
1649
|
+
discardBadLatLon : bool, optional
|
|
1650
|
+
If True (default), bogus latitiude and longitude values are ignored.
|
|
1651
|
+
|
|
1652
|
+
interpolating_function_factory : function factory, dictionary of function factories, None, optional
|
|
1653
|
+
Specification of a function factory to interpolate data. A dictionary of interpolating_function_factories
|
|
1654
|
+
allows the specification of specific functions for specific parameters. If none is defined, linear interpolation
|
|
1655
|
+
is used.
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
Returns
|
|
1659
|
+
-------
|
|
1660
|
+
(ndarray, ndarray, ...)
|
|
1661
|
+
Time vector (of first parameter), C, T and P values, and
|
|
1662
|
+
interpolated values of subsequent parameters.
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
Notes
|
|
1666
|
+
-----
|
|
1667
|
+
.. versionadded:: 0.4.0
|
|
1668
|
+
|
|
1669
|
+
.. versionadded:: 0.5.8
|
|
1670
|
+
keyword interpolating_function_factory
|
|
1671
|
+
|
|
1672
|
+
'''
|
|
1673
|
+
CTD_type = self.determine_ctd_type()
|
|
1674
|
+
CTDparameters = [f"sci_{CTD_type}_timestamp", "sci_water_cond",
|
|
1675
|
+
"sci_water_temp", "sci_water_pressure"]
|
|
1676
|
+
offset = len(CTDparameters) + 1 # because of m_present_time is
|
|
1677
|
+
# also returned.
|
|
1678
|
+
tmp = self.get_sync(*CTDparameters, *parameters, decimalLatLon=decimalLatLon, discardBadLatLon=discardBadLatLon,
|
|
1679
|
+
interpolating_function_factory=interpolating_function_factory)
|
|
1680
|
+
# remove all time<=1 timestamps, as there can be nans here too.
|
|
1681
|
+
tmp = numpy.compress(tmp[1]>1, tmp, axis=1)
|
|
1682
|
+
condition = tmp[2]>0 # conductivity > 0
|
|
1683
|
+
if len(parameters):
|
|
1684
|
+
# check for any leading or trailing nans in v, caused by
|
|
1685
|
+
# interpolation:
|
|
1686
|
+
#
|
|
1687
|
+
# collapse v on one vector, and make a condition where the collapsed
|
|
1688
|
+
# vector is nan
|
|
1689
|
+
a = numpy.prod(tmp[offset:], axis=0)
|
|
1690
|
+
condition &= numpy.isfinite(a)
|
|
1691
|
+
if numpy.all(condition==False):
|
|
1692
|
+
raise DbdError(DBD_ERROR_NO_DATA_TO_INTERPOLATE)
|
|
1693
|
+
# ensure monotonicity in time
|
|
1694
|
+
dt = numpy.hstack( ([1], numpy.diff(tmp[1])) )
|
|
1695
|
+
condition &= dt>0
|
|
1696
|
+
_, tctd, C, T, P, *v = numpy.compress(condition, tmp, axis=1)
|
|
1697
|
+
return tuple([tctd, C, T, P] + v)
|
|
1698
|
+
|
|
1699
|
+
def determine_ctd_type(self):
|
|
1700
|
+
'''
|
|
1701
|
+
Determines CTD type installed from the presence of CTD specific name for the time stamp.
|
|
1702
|
+
|
|
1703
|
+
Returns
|
|
1704
|
+
-------
|
|
1705
|
+
string
|
|
1706
|
+
{"ctd41cp", "rbrctd"}
|
|
1707
|
+
|
|
1708
|
+
If unable to get a positive CTD identification, it is assumed the CTD installed is a Seabird
|
|
1709
|
+
CTD, returning "ctd41cp".
|
|
1710
|
+
|
|
1711
|
+
Notes
|
|
1712
|
+
-----
|
|
1713
|
+
.. versionadded:: 0.5.5
|
|
1714
|
+
'''
|
|
1715
|
+
# Gliders can be equipped with a Seabird CTD or an RBR
|
|
1716
|
+
# CTD. The sensor sci_ctd_is_installed or
|
|
1717
|
+
# sci_rbrctd_is_installed is set accordingly. However, we may
|
|
1718
|
+
# read a file for which either parameter is not updated, so it
|
|
1719
|
+
# is not available. Therefore we look at whether the ctd's timestamp is available.
|
|
1720
|
+
ctd_types = ["ctd41cp", "rbrctd"]
|
|
1721
|
+
for ctd_type in ctd_types:
|
|
1722
|
+
is_installed = self._has_ctd_installed(ctd_type)
|
|
1723
|
+
if is_installed:
|
|
1724
|
+
break
|
|
1725
|
+
if is_installed:
|
|
1726
|
+
return ctd_type
|
|
1727
|
+
# Fallback in case neither could be determined, assume seabird
|
|
1728
|
+
# ctd. An exception will be thrown elsewhere.
|
|
1729
|
+
return ctd_types[0]
|
|
1730
|
+
|
|
1731
|
+
def _has_ctd_installed(self, ctd_type):
|
|
1732
|
+
'''
|
|
1733
|
+
Parameters
|
|
1734
|
+
----------
|
|
1735
|
+
ctd_type: string
|
|
1736
|
+
identifier for ctd make.
|
|
1737
|
+
|
|
1738
|
+
Current possible options: "ctd41cp" for Seabird CTD and "ctdrbr" for RBR CTD"
|
|
1739
|
+
|
|
1740
|
+
Returns
|
|
1741
|
+
-------
|
|
1742
|
+
bool
|
|
1743
|
+
Boolean value indicating the ctd type is installed.
|
|
1744
|
+
'''
|
|
1745
|
+
MAX_VALUES_TO_READ=15
|
|
1746
|
+
|
|
1747
|
+
loggerLevel=logger.getEffectiveLevel()
|
|
1748
|
+
if loggerLevel < logging.ERROR:
|
|
1749
|
+
logger.setLevel(logging.ERROR)
|
|
1750
|
+
try:
|
|
1751
|
+
t, tctd = self.get(f"sci_{ctd_type}_timestamp", max_values_to_read=MAX_VALUES_TO_READ)
|
|
1752
|
+
except DbdError as e:
|
|
1753
|
+
logger.setLevel(loggerLevel)
|
|
1754
|
+
if e.value == DBD_ERROR_NO_VALID_PARAMETERS: # If an error is raised, we expect this one
|
|
1755
|
+
result = False
|
|
1756
|
+
else: # else reraise the error.
|
|
1757
|
+
raise(e)
|
|
1758
|
+
else:
|
|
1759
|
+
logger.setLevel(loggerLevel)
|
|
1760
|
+
number_of_timestamps = len(tctd)
|
|
1761
|
+
if number_of_timestamps>=MAX_VALUES_TO_READ:
|
|
1762
|
+
result = True
|
|
1763
|
+
else:
|
|
1764
|
+
result = False
|
|
1765
|
+
return result
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
def set_skip_initial_line(self, skip_initial_line):
|
|
1769
|
+
'''Sets the reading mode of the binary reader to skip the initial data entry or not.
|
|
1770
|
+
|
|
1771
|
+
Parameters
|
|
1772
|
+
----------
|
|
1773
|
+
skip_initial_line : bool
|
|
1774
|
+
Sets the attribute `skip_initial_line` of each DBD
|
|
1775
|
+
instance, controlling the reading of the first data entry
|
|
1776
|
+
of each binary file.
|
|
1777
|
+
'''
|
|
1778
|
+
for i in chain(*self.dbds.values()):
|
|
1779
|
+
i.skip_initial_line = skip_initial_line
|
|
1780
|
+
|
|
1781
|
+
def has_parameter(self,parameter):
|
|
1782
|
+
|
|
1783
|
+
'''Has this file parameter?
|
|
1784
|
+
Returns
|
|
1785
|
+
-------
|
|
1786
|
+
bool
|
|
1787
|
+
True if this instance has found parameter
|
|
1788
|
+
'''
|
|
1789
|
+
return (parameter in self.parameterNames['sci'] or parameter in self.parameterNames['eng'])
|
|
1790
|
+
|
|
1791
|
+
@classmethod
|
|
1792
|
+
def isScienceDataFile(cls,fn):
|
|
1793
|
+
''' Is file a science file?
|
|
1794
|
+
|
|
1795
|
+
Parameters
|
|
1796
|
+
----------
|
|
1797
|
+
fn : str
|
|
1798
|
+
filename
|
|
1799
|
+
|
|
1800
|
+
Returns
|
|
1801
|
+
-------
|
|
1802
|
+
bool
|
|
1803
|
+
True if file fn is a science file
|
|
1804
|
+
'''
|
|
1805
|
+
fn = fn.lower()
|
|
1806
|
+
return fn.endswith("ebd") | fn.endswith("tbd") | fn.endswith("nbd") | fn.endswith("ecd") | fn.endswith("tcd") | fn.endswith("ncd")
|
|
1807
|
+
|
|
1808
|
+
def get_time_range(self,fmt="%d %b %Y %H:%M"):
|
|
1809
|
+
'''Get start and end date of the time range selection set
|
|
1810
|
+
|
|
1811
|
+
Parameters
|
|
1812
|
+
----------
|
|
1813
|
+
fmt: str
|
|
1814
|
+
String that determines how the time string is formatted
|
|
1815
|
+
|
|
1816
|
+
Returns
|
|
1817
|
+
-------
|
|
1818
|
+
(str, str)
|
|
1819
|
+
Tuple with formatted time strings
|
|
1820
|
+
'''
|
|
1821
|
+
return self._get_time_range(self.time_limits,fmt)
|
|
1822
|
+
|
|
1823
|
+
def get_global_time_range(self,fmt="%d %b %Y %H:%M"):
|
|
1824
|
+
''' Returns start and end dates of data set (all files)
|
|
1825
|
+
|
|
1826
|
+
Parameters
|
|
1827
|
+
----------
|
|
1828
|
+
fmt: str
|
|
1829
|
+
String that determines how the time string is formatted.
|
|
1830
|
+
|
|
1831
|
+
Returns
|
|
1832
|
+
-------
|
|
1833
|
+
(str, str)
|
|
1834
|
+
tuple with formatted time strings
|
|
1835
|
+
'''
|
|
1836
|
+
return self._get_time_range(self.time_limits_dataset,fmt)
|
|
1837
|
+
|
|
1838
|
+
def set_time_limits(self,minTimeUTC=None,maxTimeUTC=None):
|
|
1839
|
+
'''Set time limits for data to be returned by get() and friends.
|
|
1840
|
+
|
|
1841
|
+
Parameters
|
|
1842
|
+
----------
|
|
1843
|
+
minTimeUTC: str
|
|
1844
|
+
start time in UTC
|
|
1845
|
+
|
|
1846
|
+
maxTimeUTC: str
|
|
1847
|
+
end time in UTC
|
|
1848
|
+
|
|
1849
|
+
Notes
|
|
1850
|
+
-----
|
|
1851
|
+
{minTimeUTC, maxTimeUTC} are expected in one of these formats:
|
|
1852
|
+
|
|
1853
|
+
"%d %b %Y" 3 Mar 2014
|
|
1854
|
+
|
|
1855
|
+
or
|
|
1856
|
+
|
|
1857
|
+
"%d %b %Y %H:%M" 4 Apr 2014 12:21
|
|
1858
|
+
'''
|
|
1859
|
+
if minTimeUTC:
|
|
1860
|
+
self.time_limits[0]=self._convert_seconds(minTimeUTC)
|
|
1861
|
+
if maxTimeUTC:
|
|
1862
|
+
self.time_limits[1]=self._convert_seconds(maxTimeUTC)
|
|
1863
|
+
self._refresh_cache()
|
|
1864
|
+
|
|
1865
|
+
def close(self):
|
|
1866
|
+
''' Close all open files '''
|
|
1867
|
+
for i in self.dbds['eng']+self.dbds['sci']:
|
|
1868
|
+
i.close()
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
#### private methods
|
|
1872
|
+
|
|
1873
|
+
def _get_matching_fn(self, fn):
|
|
1874
|
+
sci_extensions = ".ebd .tbd .nbd .ecd .tcd .ncd".split()
|
|
1875
|
+
_, extension = os.path.splitext(fn)
|
|
1876
|
+
matchingExtension = list(extension) # make the string mutable.
|
|
1877
|
+
if extension not in sci_extensions:
|
|
1878
|
+
matchingExtension[1] = chr(ord(extension[1])+1)
|
|
1879
|
+
else:
|
|
1880
|
+
matchingExtension[1] = chr(ord(extension[1])-1)
|
|
1881
|
+
matchingExtension = "".join(matchingExtension)
|
|
1882
|
+
matchingFn = fn.replace(extension,matchingExtension)
|
|
1883
|
+
return matchingFn
|
|
1884
|
+
|
|
1885
|
+
def _add_paired_filenames(self):
|
|
1886
|
+
to_add=[]
|
|
1887
|
+
for fn in self.filenames:
|
|
1888
|
+
mfn = self._get_matching_fn(fn)
|
|
1889
|
+
if os.path.exists(mfn):
|
|
1890
|
+
to_add.append(mfn)
|
|
1891
|
+
self.filenames+=to_add
|
|
1892
|
+
|
|
1893
|
+
def _get_matching_dbd(self,fn):
|
|
1894
|
+
'''returns matching dbd object corresponding to fn. If fn is not in the current list
|
|
1895
|
+
of accepted dbds, then None is returned.'''
|
|
1896
|
+
|
|
1897
|
+
if fn not in self.filenames:
|
|
1898
|
+
return None
|
|
1899
|
+
# ok, the file is in the cache, which implies it is in self.dbds too.
|
|
1900
|
+
matchingFn=self._get_matching_fn(fn)
|
|
1901
|
+
if matchingFn in self.filenames:
|
|
1902
|
+
return matchingFn
|
|
1903
|
+
else:
|
|
1904
|
+
return None
|
|
1905
|
+
|
|
1906
|
+
def _prune(self,filelist, cacheDir=None):
|
|
1907
|
+
''' prune all files in filelist.'''
|
|
1908
|
+
for tbr in filelist:
|
|
1909
|
+
self.filenames.remove(tbr)
|
|
1910
|
+
|
|
1911
|
+
def _prune_unmatched(self, cacheDir=None):
|
|
1912
|
+
''' prune all files which don't have a science/engineering partner
|
|
1913
|
+
returns list of removed files.'''
|
|
1914
|
+
to_be_removed=[fn for fn in self.filenames if not self._get_matching_dbd(fn)]
|
|
1915
|
+
self._prune(to_be_removed, cacheDir)
|
|
1916
|
+
return tuple(to_be_removed)
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
def _convert_seconds(self,timestring):
|
|
1920
|
+
t_epoch=None
|
|
1921
|
+
try:
|
|
1922
|
+
t_epoch=strptimeToEpoch(timestring,"%d %b %Y")
|
|
1923
|
+
except:
|
|
1924
|
+
pass
|
|
1925
|
+
try:
|
|
1926
|
+
t_epoch=strptimeToEpoch(timestring,"%d %b %Y %H:%M")
|
|
1927
|
+
except:
|
|
1928
|
+
pass
|
|
1929
|
+
if not t_epoch:
|
|
1930
|
+
raise ValueError('Could not convert time string. Expect a format like "3 Mar" or "3 Mar 12:30".')
|
|
1931
|
+
return t_epoch
|
|
1932
|
+
|
|
1933
|
+
def _refresh_cache(self):
|
|
1934
|
+
''' Internal. Sets global and selected time limits, and a cache with those files
|
|
1935
|
+
that matche the time selection criterion
|
|
1936
|
+
'''
|
|
1937
|
+
self._ignore_cache.clear()
|
|
1938
|
+
self._accept_cache.clear()
|
|
1939
|
+
# min and max times of whole data set
|
|
1940
|
+
time_limits_dataset = [1e10, 0]
|
|
1941
|
+
# min and max times of selected data set (can be None)
|
|
1942
|
+
time_limits = self.time_limits
|
|
1943
|
+
|
|
1944
|
+
# if no time_limits set, use all data.
|
|
1945
|
+
if not time_limits[0]:
|
|
1946
|
+
time_limits[0]=0
|
|
1947
|
+
if not time_limits[1]:
|
|
1948
|
+
time_limits[1]=1e10
|
|
1949
|
+
|
|
1950
|
+
for dbd in self.dbds['eng']+self.dbds['sci']:
|
|
1951
|
+
t=dbd.get_fileopen_time()
|
|
1952
|
+
# set global time limits
|
|
1953
|
+
if t<time_limits_dataset[0]:
|
|
1954
|
+
time_limits_dataset[0]=t
|
|
1955
|
+
if t>time_limits_dataset[1]:
|
|
1956
|
+
time_limits_dataset[1]=t
|
|
1957
|
+
#
|
|
1958
|
+
if t<time_limits[0] or t>time_limits[1]:
|
|
1959
|
+
self._ignore_cache.append(dbd)
|
|
1960
|
+
else:
|
|
1961
|
+
self._accept_cache.append(dbd)
|
|
1962
|
+
# this is a file that matches the selection criterion.
|
|
1963
|
+
if t<time_limits[0]:
|
|
1964
|
+
time_limits[0]=t
|
|
1965
|
+
if t>time_limits[1]:
|
|
1966
|
+
time_limits[1]=t
|
|
1967
|
+
self.time_limits_dataset=tuple(time_limits_dataset)
|
|
1968
|
+
time_limits[0]=max(time_limits[0],time_limits_dataset[0])
|
|
1969
|
+
time_limits[1]=min(time_limits[1],time_limits_dataset[1])
|
|
1970
|
+
|
|
1971
|
+
def _format_time(self,t,fmt):
|
|
1972
|
+
tmp = datetime.datetime.fromtimestamp(t, datetime.UTC)
|
|
1973
|
+
return tmp.strftime(fmt)
|
|
1974
|
+
|
|
1975
|
+
def _get_time_range(self,time_limits,fmt):
|
|
1976
|
+
if fmt=="%s":
|
|
1977
|
+
return time_limits
|
|
1978
|
+
else :
|
|
1979
|
+
return list(map(lambda x: self._format_time(x,fmt), time_limits))
|
|
1980
|
+
|
|
1981
|
+
def _safely_open_dbd_file(self, fn, cacheDir, skip_initial_lines, missing_cacheIDs, check_for_compressed_cac=True):
|
|
1982
|
+
dbd = None
|
|
1983
|
+
try:
|
|
1984
|
+
dbd=DBD(fn, cacheDir, skip_initial_lines)
|
|
1985
|
+
except DbdError as e:
|
|
1986
|
+
#Typically the call in the try block may fail if the
|
|
1987
|
+
# cache file cannot be found because it is not in the
|
|
1988
|
+
# specfied directory or the cache directory cannot be
|
|
1989
|
+
# found. Flesh out these cases and keep a log of
|
|
1990
|
+
# them, we can raise an error later with all the
|
|
1991
|
+
# information on missing cache files and problems
|
|
1992
|
+
# later, so the caller can handle the error
|
|
1993
|
+
# meaningfully.
|
|
1994
|
+
if e.value == DBD_ERROR_CACHEDIR_NOT_FOUND:
|
|
1995
|
+
# if this happens, this will happen for all subsequent files to load.
|
|
1996
|
+
raise DbdError(DBD_ERROR_CACHEDIR_NOT_FOUND,
|
|
1997
|
+
mesg=f"\nCache directory {cacheDir} could not be accessed.",
|
|
1998
|
+
data=DbdError.MissingCacheFileData(None, cacheDir))
|
|
1999
|
+
elif e.value == DBD_ERROR_CACHE_NOT_FOUND:
|
|
2000
|
+
# The cache file could not be found. Let's try if there is a compressed cache file.
|
|
2001
|
+
# if yes, then decompress it and try again.
|
|
2002
|
+
_cacheID = list(e.data.missing_cache_files.keys())[0] # we open a single file, so only one cache file can be missing.
|
|
2003
|
+
missing_cache_filename = os.path.join(cacheDir, _cacheID+'.ccc')
|
|
2004
|
+
if check_for_compressed_cac and os.path.exists(missing_cache_filename):
|
|
2005
|
+
dbdreader.decompress.decompress_file(missing_cache_filename)
|
|
2006
|
+
result = "try_again"
|
|
2007
|
+
else:
|
|
2008
|
+
for k in e.data.missing_cache_files.keys():
|
|
2009
|
+
missing_cacheIDs[k].append(fn)
|
|
2010
|
+
result = "failed"
|
|
2011
|
+
elif e.value == DBD_ERROR_DECOMPRESSION_ERROR:
|
|
2012
|
+
# This particular file could not be decompressed, howeever, it is possible, or even likely that others scheduled
|
|
2013
|
+
# to be read will be decompressed successfully. So, produce a warning so the user knows what happens, and continue.
|
|
2014
|
+
logger.warning('File %s failed to be decompressed successfully.', fn)
|
|
2015
|
+
result = "ignore"
|
|
2016
|
+
else: # some other problem. Just ignore the file but produce a warning.
|
|
2017
|
+
logger.warning('File %s could not be loaded', fn)
|
|
2018
|
+
logger.debug('Exception was %s', e)
|
|
2019
|
+
logger.debug('Exception value was %d', e.value)
|
|
2020
|
+
result = "ignore"
|
|
2021
|
+
else:
|
|
2022
|
+
result = "ok"
|
|
2023
|
+
dbd.close()
|
|
2024
|
+
return dbd, result
|
|
2025
|
+
|
|
2026
|
+
def _update_dbd_inventory(self, cacheDir, skip_initial_lines):
|
|
2027
|
+
self.dbds={'eng':[],'sci':[]}
|
|
2028
|
+
filenames=list(self.filenames)
|
|
2029
|
+
missing_cacheIDs = defaultdict(list)
|
|
2030
|
+
for fn in self.filenames:
|
|
2031
|
+
dbd, result = self._safely_open_dbd_file(fn, cacheDir, skip_initial_lines, missing_cacheIDs)
|
|
2032
|
+
if result == "ignore" or result == "failed":
|
|
2033
|
+
filenames.remove(fn)
|
|
2034
|
+
elif result == "try_again":
|
|
2035
|
+
dbd, result = self._safely_open_dbd_file(fn, cacheDir, skip_initial_lines, missing_cacheIDs, check_for_compressed_cac=False)
|
|
2036
|
+
if result == "ignore" or result == "failed":
|
|
2037
|
+
filenames.remove(fn)
|
|
2038
|
+
if result!="ok":
|
|
2039
|
+
continue
|
|
2040
|
+
mission_name=dbd.get_mission_name()
|
|
2041
|
+
if mission_name in self.banned_missions:
|
|
2042
|
+
filenames.remove(fn)
|
|
2043
|
+
continue
|
|
2044
|
+
if self.missions and mission_name not in self.missions:
|
|
2045
|
+
filenames.remove(fn)
|
|
2046
|
+
continue
|
|
2047
|
+
# so we decided to keep the file.
|
|
2048
|
+
if mission_name not in self.mission_list:
|
|
2049
|
+
self.mission_list.append(mission_name)
|
|
2050
|
+
if self.isScienceDataFile(fn):
|
|
2051
|
+
ft = 'sci'
|
|
2052
|
+
else:
|
|
2053
|
+
ft = 'eng'
|
|
2054
|
+
self.dbds[ft].append(dbd)
|
|
2055
|
+
self._parameter_names['globally'].update(dbd.headerInfo['parameter_list'])
|
|
2056
|
+
self._parameter_names['locally'].update(dbd.parameterNames)
|
|
2057
|
+
|
|
2058
|
+
self.filenames=filenames
|
|
2059
|
+
# At this stage we may have zero or more files, and some could have been removed.
|
|
2060
|
+
# We will raise an error when cache files are missing and when there are no files at all.
|
|
2061
|
+
if missing_cacheIDs:
|
|
2062
|
+
# craft some useful error message
|
|
2063
|
+
mesg = f"\nOne or more cache files could not be found in {cacheDir}:\n"
|
|
2064
|
+
for k, v in missing_cacheIDs.items():
|
|
2065
|
+
mesg+=f"{k} reqd by {v[0]}"
|
|
2066
|
+
if len(v)>1:
|
|
2067
|
+
mesg += f" + {len(v)-1} more files."
|
|
2068
|
+
mesg+="\n"
|
|
2069
|
+
data = DbdError.MissingCacheFileData(dict([(k,v) for k,v in missing_cacheIDs.items()]), cacheDir)
|
|
2070
|
+
raise DbdError(DBD_ERROR_CACHE_NOT_FOUND, mesg=mesg, data=data)
|
|
2071
|
+
if len(self.dbds['sci'])+len(self.dbds['eng'])==0:
|
|
2072
|
+
raise DbdError(DBD_ERROR_ALL_FILES_BANNED, " (Read %d files.)"%(len(self.filenames)))
|
|
2073
|
+
|
|
2074
|
+
|
|
2075
|
+
def _getParameterUnits(self):
|
|
2076
|
+
dbds=self.dbds['eng']
|
|
2077
|
+
units=[]
|
|
2078
|
+
for i in dbds:
|
|
2079
|
+
units+=[j for j in i.parameterUnits.items()]
|
|
2080
|
+
dbds=self.dbds['sci']
|
|
2081
|
+
for i in dbds:
|
|
2082
|
+
units+=[j for j in i.parameterUnits.items()]
|
|
2083
|
+
return dict(i for i in (set(units)))
|
|
2084
|
+
|
|
2085
|
+
def _getParameterList(self,dbds):
|
|
2086
|
+
if len(dbds)==0: # no parameters in here.
|
|
2087
|
+
return []
|
|
2088
|
+
cacheIDs = []
|
|
2089
|
+
for i, dbd in enumerate(dbds):
|
|
2090
|
+
if i==0:
|
|
2091
|
+
parameter_names = dbd.parameterNames.copy()
|
|
2092
|
+
cacheIDs.append(dbd.cacheID)
|
|
2093
|
+
elif not dbd.cacheID in cacheIDs:
|
|
2094
|
+
for pn in dbd.parameterNames:
|
|
2095
|
+
if pn not in parameter_names:
|
|
2096
|
+
parameter_names.append(pn)
|
|
2097
|
+
cacheIDs.append(dbd.cacheID)
|
|
2098
|
+
parameter_names.sort()
|
|
2099
|
+
return parameter_names
|
|
2100
|
+
|
|
2101
|
+
def _worker(self, ft, *p, **kwds):
|
|
2102
|
+
try:
|
|
2103
|
+
include_source = kwds.pop("include_source")
|
|
2104
|
+
except KeyError:
|
|
2105
|
+
include_source = False
|
|
2106
|
+
try:
|
|
2107
|
+
continue_on_reading_error = kwds.pop("continue_on_reading_error")
|
|
2108
|
+
except KeyError:
|
|
2109
|
+
continue_on_reading_error = False
|
|
2110
|
+
|
|
2111
|
+
data = dict([(k,[]) for k in p])
|
|
2112
|
+
srcs = dict([(k,[]) for k in p])
|
|
2113
|
+
error_mesgs = []
|
|
2114
|
+
time_values_read_sofar=0
|
|
2115
|
+
for i in self.dbds[ft]:
|
|
2116
|
+
if i in self._ignore_cache:
|
|
2117
|
+
continue
|
|
2118
|
+
try:
|
|
2119
|
+
t, v = i._get(*p, **kwds)
|
|
2120
|
+
except DbdError as e:
|
|
2121
|
+
# ignore only the no_data_to_interpolate_to error
|
|
2122
|
+
# as the file is probably (close to) empty
|
|
2123
|
+
if e.value==DBD_ERROR_NO_DATA_TO_INTERPOLATE_TO:
|
|
2124
|
+
continue
|
|
2125
|
+
elif e.value==DBD_ERROR_NO_VALID_PARAMETERS:
|
|
2126
|
+
logger.debug("get() call returned an error on invalid parameters.")
|
|
2127
|
+
# set1 is all known parameters:
|
|
2128
|
+
set1 = set([i for i in chain(*self.parameterNames.values())])
|
|
2129
|
+
set2 = set(e.data) # missing parmaeters
|
|
2130
|
+
if set2.intersection(set1) == set2:
|
|
2131
|
+
# all missing parameters in *this* file are
|
|
2132
|
+
# known from at least on other file read.
|
|
2133
|
+
kwds['check_for_invalid_parameters']=False
|
|
2134
|
+
t, v = i._get(*p, **kwds)
|
|
2135
|
+
else:
|
|
2136
|
+
# at least one unknown parameter was aksed for. Reraise the error.
|
|
2137
|
+
raise e
|
|
2138
|
+
elif e.value==DBD_ERROR_READ_ERROR:
|
|
2139
|
+
if continue_on_reading_error:
|
|
2140
|
+
logger.warning(f"Reading from {i.filename} returned an error ({e.data}).")
|
|
2141
|
+
continue
|
|
2142
|
+
else:
|
|
2143
|
+
raise e
|
|
2144
|
+
else:
|
|
2145
|
+
# in all other cases reraise the error..
|
|
2146
|
+
raise e
|
|
2147
|
+
|
|
2148
|
+
# add the data read to the data dictionary.
|
|
2149
|
+
for _p, _t, _v in zip(p, t, v):
|
|
2150
|
+
data[_p].append( (_t, _v) )
|
|
2151
|
+
if include_source:
|
|
2152
|
+
srcs[_p] += [i] * len(_t)
|
|
2153
|
+
# Check if we request only a limited number of
|
|
2154
|
+
# values. Note that the sanity check for not
|
|
2155
|
+
# requesting more than one parameter is made in DBD's
|
|
2156
|
+
# get() method.
|
|
2157
|
+
if kwds["max_values_to_read"]>0:
|
|
2158
|
+
time_values_read_sofar+=len(t[0])
|
|
2159
|
+
if time_values_read_sofar>=kwds["max_values_to_read"]:
|
|
2160
|
+
break
|
|
2161
|
+
|
|
2162
|
+
if not all(data.values()):
|
|
2163
|
+
# nothing has been added, so all files should have returned nothing:
|
|
2164
|
+
raise(DbdError(DBD_ERROR_NO_VALID_PARAMETERS,
|
|
2165
|
+
"\n".join(error_mesgs)))
|
|
2166
|
+
if not include_source:
|
|
2167
|
+
data_arrays = [(numpy.hstack([_d[0] for _d in data[_p]]), numpy.hstack([_d[1] for _d in data[_p]])) for _p in p]
|
|
2168
|
+
else:
|
|
2169
|
+
data_arrays = [((numpy.hstack([_d[0] for _d in data[_p]]), numpy.hstack([_d[1] for _d in data[_p]])), srcs[_p]) for _p in p]
|
|
2170
|
+
return data_arrays
|
|
2171
|
+
|
|
2172
|
+
# Initialises the class
|
|
2173
|
+
DBDCache()
|
|
2174
|
+
|