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.
Files changed (50) hide show
  1. _dbdreader.cp313-win_amd64.pyd +0 -0
  2. dbdreader/__init__.py +12 -0
  3. dbdreader/data/01600000.dcd +0 -0
  4. dbdreader/data/01600000.ebd +0 -0
  5. dbdreader/data/01600000.ecd +0 -0
  6. dbdreader/data/01600000.mcg +0 -0
  7. dbdreader/data/01600000.mlg +1904 -0
  8. dbdreader/data/01600001.dbd +0 -0
  9. dbdreader/data/01600001.dcd +0 -0
  10. dbdreader/data/01600001.ebd +0 -0
  11. dbdreader/data/01600001.ecd +0 -0
  12. dbdreader/data/02380107.ecd +0 -0
  13. dbdreader/data/02380108.ecd +0 -0
  14. dbdreader/data/02450133.tcd +0 -0
  15. dbdreader/data/02450137.tcd +0 -0
  16. dbdreader/data/amadeus-2014-203-00-000.SBD +0 -0
  17. dbdreader/data/amadeus-2014-203-00-000.TBD +0 -0
  18. dbdreader/data/amadeus-2014-204-05-000.dbd +0 -0
  19. dbdreader/data/amadeus-2014-204-05-000.ebd +0 -0
  20. dbdreader/data/amadeus-2014-204-05-000.sbd +0 -0
  21. dbdreader/data/amadeus-2014-204-05-000.tbd +0 -0
  22. dbdreader/data/amadeus-2014-204-05-001.sbd +0 -0
  23. dbdreader/data/amadeus-2014-204-05-001.tbd +0 -0
  24. dbdreader/data/amadeus-2014-204-05-002.sbd +0 -0
  25. dbdreader/data/amadeus-2014-204-05-002.tbd +0 -0
  26. dbdreader/data/ammonite-2008-028-01-000.mbd +2091 -1
  27. dbdreader/data/dbd2asc_output.txt +25 -0
  28. dbdreader/data/electa-2023-143-00-050.sbd +0 -0
  29. dbdreader/data/electa-2023-143-00-050.tbd +0 -0
  30. dbdreader/data/empty-2014-204-05-000.dbd +0 -0
  31. dbdreader/data/hal_1002-2024-183-4-4.sbd +0 -0
  32. dbdreader/data/hal_1002-2024-183-4-4.tbd +0 -0
  33. dbdreader/data/hal_1002-2024-183-4-6.tbd +0 -0
  34. dbdreader/data/invalid_encoding-2014-204-05-000.dbd +0 -0
  35. dbdreader/data/sebastian-2014-204-05-000.dbd +0 -0
  36. dbdreader/data/sebastian-2014-204-05-000.ebd +0 -0
  37. dbdreader/data/sebastian-2014-204-05-001.dbd +0 -0
  38. dbdreader/data/sebastian-2014-204-05-001.ebd +0 -0
  39. dbdreader/data/unit_887-2021-321-3-0.sbd +0 -0
  40. dbdreader/data/unit_887-2021-321-3-0.tbd +0 -0
  41. dbdreader/dbdreader.py +2174 -0
  42. dbdreader/decompress.py +247 -0
  43. dbdreader/scripts.py +279 -0
  44. dbdreader-0.6.0.dev1.dist-info/METADATA +272 -0
  45. dbdreader-0.6.0.dev1.dist-info/RECORD +50 -0
  46. dbdreader-0.6.0.dev1.dist-info/WHEEL +5 -0
  47. dbdreader-0.6.0.dev1.dist-info/entry_points.txt +3 -0
  48. dbdreader-0.6.0.dev1.dist-info/licenses/COPYING +340 -0
  49. dbdreader-0.6.0.dev1.dist-info/licenses/LICENSE +674 -0
  50. 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
+