ctdproc 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ctdproc/__init__.py +6 -0
- ctdproc/calcs.py +246 -0
- ctdproc/helpers.py +325 -0
- ctdproc/io.py +976 -0
- ctdproc/proc.py +770 -0
- ctdproc-0.2.0.dist-info/METADATA +76 -0
- ctdproc-0.2.0.dist-info/RECORD +9 -0
- ctdproc-0.2.0.dist-info/WHEEL +4 -0
- ctdproc-0.2.0.dist-info/licenses/LICENSE +22 -0
ctdproc/io.py
ADDED
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
import errno
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import scipy.io as sio
|
|
10
|
+
import xarray as xr
|
|
11
|
+
import xmltodict
|
|
12
|
+
from munch import Munch, munchify
|
|
13
|
+
from scipy.signal import savgol_filter
|
|
14
|
+
|
|
15
|
+
from .calcs import calc_allsal
|
|
16
|
+
from .helpers import datetime2mtlb, mtlb2datetime
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def CTDx(filename):
|
|
20
|
+
c = CTDHex(filename).to_xarray()
|
|
21
|
+
add_default_proc_params(c)
|
|
22
|
+
return c
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def add_default_proc_params(ds):
|
|
26
|
+
ds.attrs["verbose"] = 1
|
|
27
|
+
ds.attrs["bounds_p"] = [0.0, 6200.0]
|
|
28
|
+
ds.attrs["bounds_t"] = [-2.0, 40.0]
|
|
29
|
+
ds.attrs["bounds_c"] = [2.0, 6.0]
|
|
30
|
+
ds.attrs["bounds_s"] = [20, 38]
|
|
31
|
+
ds.attrs["spike_thresh_t"] = 0.5
|
|
32
|
+
ds.attrs["spike_thresh_s"] = 0.1
|
|
33
|
+
ds.attrs["prod_c"] = 5.0e-7
|
|
34
|
+
ds.attrs["prod_t"] = 1.0e-4
|
|
35
|
+
ds.attrs["prod_s"] = 1.0e-8
|
|
36
|
+
ds.attrs["prod_p"] = 1.0
|
|
37
|
+
ds.attrs["diff_c"] = 1.0e-1
|
|
38
|
+
ds.attrs["diff_t"] = 1.0e-1
|
|
39
|
+
ds.attrs["diff_s"] = 1.0e-3
|
|
40
|
+
ds.attrs["diff_p"] = 2.0
|
|
41
|
+
ds.attrs["wthresh"] = 0.1
|
|
42
|
+
ds.attrs["plot_spectra"] = 0
|
|
43
|
+
ds.attrs["plot_path"] = ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CTDHex(object):
|
|
47
|
+
"""
|
|
48
|
+
Converter for Seabird CTD data in hex format. Initialize with full path to hex file.
|
|
49
|
+
xml config file needs to be located in the same directory.
|
|
50
|
+
|
|
51
|
+
TODO:
|
|
52
|
+
- Add oxygen hysteresis
|
|
53
|
+
- Convert fluorometer voltage
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, filename):
|
|
57
|
+
self.filename = filename
|
|
58
|
+
|
|
59
|
+
self._map_attrs = dict(
|
|
60
|
+
lon=dict(
|
|
61
|
+
name="",
|
|
62
|
+
long_name="longitude",
|
|
63
|
+
standard_name="longitude",
|
|
64
|
+
units="degree_east",
|
|
65
|
+
),
|
|
66
|
+
lat=dict(
|
|
67
|
+
name="",
|
|
68
|
+
long_name="latitude",
|
|
69
|
+
standard_name="latitude",
|
|
70
|
+
units="degree_north",
|
|
71
|
+
),
|
|
72
|
+
t1=dict(
|
|
73
|
+
name="TemperatureSensor1",
|
|
74
|
+
long_name="temperature",
|
|
75
|
+
standard_name="sea_water_temperature",
|
|
76
|
+
units="degree_C",
|
|
77
|
+
),
|
|
78
|
+
t2=dict(
|
|
79
|
+
name="TemperatureSensor2",
|
|
80
|
+
long_name="temperature",
|
|
81
|
+
standard_name="sea_water_temperature",
|
|
82
|
+
units="degree_C",
|
|
83
|
+
),
|
|
84
|
+
c1=dict(
|
|
85
|
+
name="ConductivitySensor1",
|
|
86
|
+
long_name="conductivity",
|
|
87
|
+
standard_name="sea_water_conductivity",
|
|
88
|
+
units="mS cm-1",
|
|
89
|
+
),
|
|
90
|
+
c2=dict(
|
|
91
|
+
name="ConductivitySensor2",
|
|
92
|
+
long_name="conductivity",
|
|
93
|
+
standard_name="sea_water_conductivity",
|
|
94
|
+
units="mS cm-1",
|
|
95
|
+
),
|
|
96
|
+
p=dict(
|
|
97
|
+
name="PressureSensor",
|
|
98
|
+
long_name="pressure",
|
|
99
|
+
standard_name="sea_water_pressure",
|
|
100
|
+
units="dbar",
|
|
101
|
+
positive="down",
|
|
102
|
+
),
|
|
103
|
+
oxygen1=dict(
|
|
104
|
+
name="OxygenSensor1",
|
|
105
|
+
long_name="oxygen",
|
|
106
|
+
standard_name="mass_concentration_of_oxygen_in_sea_water",
|
|
107
|
+
units="ml/L",
|
|
108
|
+
),
|
|
109
|
+
alt=dict(
|
|
110
|
+
name="AltimeterSensor",
|
|
111
|
+
long_name="height above bottom",
|
|
112
|
+
standard_name="height_above_sea_floor",
|
|
113
|
+
units="m",
|
|
114
|
+
),
|
|
115
|
+
spar=dict(name="SPAR_Sensor", long_name="SPAR"),
|
|
116
|
+
fl=dict(name="FluoroSeapointSensor", long_name="fluorescence"),
|
|
117
|
+
par=dict(name="PAR_BiosphericalLicorChelseaSensor", long_name="PAR"),
|
|
118
|
+
trans=dict(name="WET_LabsCStar", long_name="transmissivity"),
|
|
119
|
+
modcount=dict(name="modcount"),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
volt_vars = ["oxygen1", "alt", "spar", "fl", "par", "trans"]
|
|
123
|
+
self._mapnames_volt = {v: self._map_attrs[v] for v in volt_vars}
|
|
124
|
+
|
|
125
|
+
# extract all data and metadata and convert to physical units
|
|
126
|
+
self._extract_physical_data()
|
|
127
|
+
|
|
128
|
+
def _extract_physical_data(self):
|
|
129
|
+
self.read_xml_config()
|
|
130
|
+
self.parse_hex()
|
|
131
|
+
self.physicalunits()
|
|
132
|
+
|
|
133
|
+
def _detect_missing_words(self):
|
|
134
|
+
"""
|
|
135
|
+
Determine location of data entries in hex file line.
|
|
136
|
+
|
|
137
|
+
Sometimes there is no SPAR sensor, in this case one hex word may be
|
|
138
|
+
missing. It lives on voltage channel 9. There may also be words missing
|
|
139
|
+
if less sensors are present.
|
|
140
|
+
|
|
141
|
+
Locating the right data entries also depends on the number of bytes
|
|
142
|
+
written per scan. We read this information from the header.
|
|
143
|
+
|
|
144
|
+
This is still a bit of work in progress.
|
|
145
|
+
|
|
146
|
+
For details, see p.67/68 in manual-11pV2_018.pdf
|
|
147
|
+
"""
|
|
148
|
+
# Read xml config file if this has not happened yet
|
|
149
|
+
if not hasattr(self, "cfgp"):
|
|
150
|
+
self.read_xml_config()
|
|
151
|
+
# Count the number of sensors in the configuration
|
|
152
|
+
n_index = self.cfgp.loc["@index"].values.shape[0]
|
|
153
|
+
# No SPAR sensor
|
|
154
|
+
if (
|
|
155
|
+
"14" not in self.cfgp.loc["@index"].values
|
|
156
|
+
and "SPAR_Sensor" not in self.cfgp.loc["@index"].keys()
|
|
157
|
+
):
|
|
158
|
+
self._hexoffset = -6
|
|
159
|
+
else:
|
|
160
|
+
self._hexoffset = 0
|
|
161
|
+
# Word 8 missing (there is probably a better way to implement this)
|
|
162
|
+
if (
|
|
163
|
+
"14" not in self.cfgp.loc["@index"].values
|
|
164
|
+
and self._bytes_per_scan == 38
|
|
165
|
+
):
|
|
166
|
+
self._hexoffset = -12
|
|
167
|
+
# Less voltage words can lead to a shorter data stream, see manual p. 68
|
|
168
|
+
if "14" in self.cfgp.loc["@index"].values and self._bytes_per_scan == 48:
|
|
169
|
+
self._extra_hexoffset = 8
|
|
170
|
+
elif "14" in self.cfgp.loc["@index"].values and self._bytes_per_scan == 44 and n_index == 11:
|
|
171
|
+
self._extra_hexoffset = 0
|
|
172
|
+
elif "14" in self.cfgp.loc["@index"].values and self._bytes_per_scan == 44 and n_index == 12:
|
|
173
|
+
self._extra_hexoffset = 8
|
|
174
|
+
elif "14" in self.cfgp.loc["@index"].values and self._bytes_per_scan == 45 and n_index == 10:
|
|
175
|
+
self._extra_hexoffset = 0
|
|
176
|
+
elif "14" not in self.cfgp.loc["@index"].values and self._bytes_per_scan == 45:
|
|
177
|
+
self._extra_hexoffset = 8
|
|
178
|
+
elif "14" not in self.cfgp.loc["@index"].values and self._bytes_per_scan == 38:
|
|
179
|
+
self._extra_hexoffset = 8
|
|
180
|
+
else:
|
|
181
|
+
self._extra_hexoffset = 0
|
|
182
|
+
|
|
183
|
+
def parse_hex(self): # noqa: C901
|
|
184
|
+
# Generate data structure for converted data: 5 freq, 8 voltage channels
|
|
185
|
+
tmp = {}
|
|
186
|
+
for i in range(5):
|
|
187
|
+
tmp["f{}".format(i)] = []
|
|
188
|
+
for i in range(8):
|
|
189
|
+
tmp["v{}".format(i)] = []
|
|
190
|
+
tmp["modcount"] = []
|
|
191
|
+
tmp["time"] = []
|
|
192
|
+
tmp["spar"] = []
|
|
193
|
+
tmp["pst"] = []
|
|
194
|
+
tmp["ctdstatus"] = []
|
|
195
|
+
tmp["lon"] = []
|
|
196
|
+
tmp["lat"] = []
|
|
197
|
+
|
|
198
|
+
out = dict(header="")
|
|
199
|
+
|
|
200
|
+
with open(self.filename, "rt") as fin:
|
|
201
|
+
# read header
|
|
202
|
+
for line in fin:
|
|
203
|
+
if len(line) == 1:
|
|
204
|
+
pass
|
|
205
|
+
elif line[0] == "*":
|
|
206
|
+
out["header"] += line
|
|
207
|
+
# if l[0:5] == "* ":
|
|
208
|
+
# i = l.find("=")
|
|
209
|
+
if line[0:17] == "* Number of Bytes":
|
|
210
|
+
i = line.find("=")
|
|
211
|
+
self._bytes_per_scan = int(line[i + 2 :])
|
|
212
|
+
if line[0:25] == "* Number of Voltage Words":
|
|
213
|
+
i = line.find("=")
|
|
214
|
+
self._voltage_words = int(line[i + 2 :])
|
|
215
|
+
if line[0:34] == "* Append System Time to Every Scan":
|
|
216
|
+
self._hex_has_time = True
|
|
217
|
+
else:
|
|
218
|
+
self._hex_has_time = False
|
|
219
|
+
if line[0:15] == "* System UTC = ":
|
|
220
|
+
self.header_time_str = line[15:35]
|
|
221
|
+
else:
|
|
222
|
+
break
|
|
223
|
+
self._detect_missing_words()
|
|
224
|
+
# read data
|
|
225
|
+
for line in fin:
|
|
226
|
+
# parse frequency channels
|
|
227
|
+
for k in tmp.keys():
|
|
228
|
+
if "f" in k:
|
|
229
|
+
i = int(k[1])
|
|
230
|
+
tmp[k].append(self._hexword2freq(line[slice(i * 6, i * 6 + 6)]))
|
|
231
|
+
# parse voltage channels
|
|
232
|
+
for i in range(4):
|
|
233
|
+
v1, v2 = self._hexword2volt(
|
|
234
|
+
line[slice((i + 5) * 6, (i + 5) * 6 + 6)]
|
|
235
|
+
)
|
|
236
|
+
tmp["v{}".format(i * 2)].append(v1)
|
|
237
|
+
tmp["v{}".format(i * 2 + 1)].append(v2)
|
|
238
|
+
# parse other channels
|
|
239
|
+
# spar
|
|
240
|
+
if self._hexoffset == 0:
|
|
241
|
+
tmp["spar"].append(self._hexword2spar(line[slice(57, 60)]))
|
|
242
|
+
# NMEA data
|
|
243
|
+
lat, lon = self._hexword2lonlat(
|
|
244
|
+
line[60 + self._hexoffset : 74 + self._hexoffset]
|
|
245
|
+
)
|
|
246
|
+
tmp["lon"].append(lon)
|
|
247
|
+
tmp["lat"].append(lat)
|
|
248
|
+
# pressure sensor temperature and ctd status
|
|
249
|
+
pst, ctdstatus = self._hexword2pstat(
|
|
250
|
+
line[
|
|
251
|
+
slice(
|
|
252
|
+
74 + self._hexoffset + self._extra_hexoffset,
|
|
253
|
+
78 + self._hexoffset + self._extra_hexoffset,
|
|
254
|
+
)
|
|
255
|
+
]
|
|
256
|
+
)
|
|
257
|
+
tmp["pst"].append(pst)
|
|
258
|
+
tmp["ctdstatus"].append(ctdstatus)
|
|
259
|
+
# modcount
|
|
260
|
+
tmp["modcount"].append(
|
|
261
|
+
int(
|
|
262
|
+
line[
|
|
263
|
+
78
|
|
264
|
+
+ self._hexoffset
|
|
265
|
+
+ self._extra_hexoffset : 80
|
|
266
|
+
+ self._hexoffset
|
|
267
|
+
+ self._extra_hexoffset
|
|
268
|
+
],
|
|
269
|
+
16,
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
# time
|
|
273
|
+
if self._hex_has_time:
|
|
274
|
+
tmp["time"].append(
|
|
275
|
+
int(
|
|
276
|
+
line[
|
|
277
|
+
86
|
|
278
|
+
+ self._hexoffset
|
|
279
|
+
+ self._extra_hexoffset : 88
|
|
280
|
+
+ self._hexoffset
|
|
281
|
+
+ self._extra_hexoffset
|
|
282
|
+
]
|
|
283
|
+
+ line[
|
|
284
|
+
84
|
|
285
|
+
+ self._hexoffset
|
|
286
|
+
+ self._extra_hexoffset : 86
|
|
287
|
+
+ self._hexoffset
|
|
288
|
+
+ self._extra_hexoffset
|
|
289
|
+
]
|
|
290
|
+
+ line[
|
|
291
|
+
82
|
|
292
|
+
+ self._hexoffset
|
|
293
|
+
+ self._extra_hexoffset : 84
|
|
294
|
+
+ self._hexoffset
|
|
295
|
+
+ self._extra_hexoffset
|
|
296
|
+
]
|
|
297
|
+
+ line[
|
|
298
|
+
80
|
|
299
|
+
+ self._hexoffset
|
|
300
|
+
+ self._extra_hexoffset : 82
|
|
301
|
+
+ self._hexoffset
|
|
302
|
+
+ self._extra_hexoffset
|
|
303
|
+
],
|
|
304
|
+
16,
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# generate output array
|
|
309
|
+
# frequency variables are always there
|
|
310
|
+
freqvars = ["t1", "c1", "p", "t2", "c2"]
|
|
311
|
+
for i, k in enumerate(freqvars):
|
|
312
|
+
out[k] = np.array(tmp["f{}".format(i)])
|
|
313
|
+
# voltage variables. see which ones we have
|
|
314
|
+
for k, v in self._mapnames_volt.items():
|
|
315
|
+
if v["name"] in self.cfgp.keys():
|
|
316
|
+
channel = int(self.cfgp[v["name"]]["@index"])
|
|
317
|
+
vchannel = channel - 5
|
|
318
|
+
if vchannel < 8:
|
|
319
|
+
out[k] = np.array(tmp["v{}".format(vchannel)])
|
|
320
|
+
elif vchannel == 9 and k == "spar":
|
|
321
|
+
out["spar"] = np.array(tmp["spar"])
|
|
322
|
+
out["lon"] = np.array(tmp["lon"])
|
|
323
|
+
out["lat"] = np.array(tmp["lat"])
|
|
324
|
+
out["pst"] = np.array(tmp["pst"])
|
|
325
|
+
out["ctdstatus"] = tmp["ctdstatus"]
|
|
326
|
+
out["modcount"] = np.array(tmp["modcount"])
|
|
327
|
+
if self._hex_has_time:
|
|
328
|
+
out["time"] = np.array(tmp["time"])
|
|
329
|
+
else:
|
|
330
|
+
out["time"] = self._generate_time_vector(len(out["t1"]))
|
|
331
|
+
if "spar" in tmp:
|
|
332
|
+
out["spar"] = np.array(tmp["spar"])
|
|
333
|
+
self.dataraw = munchify(out)
|
|
334
|
+
|
|
335
|
+
def _hexword2freq_old(self, hex_str):
|
|
336
|
+
"""
|
|
337
|
+
Convert Seabird hex data to frequency
|
|
338
|
+
each byte is given as two hex digits
|
|
339
|
+
each SB freq word is 3 bytes
|
|
340
|
+
calculates freq from 3 byte word
|
|
341
|
+
|
|
342
|
+
Parameters
|
|
343
|
+
----------
|
|
344
|
+
hex : str
|
|
345
|
+
6 character long hex string
|
|
346
|
+
|
|
347
|
+
Returns
|
|
348
|
+
-------
|
|
349
|
+
f : float
|
|
350
|
+
frequency
|
|
351
|
+
|
|
352
|
+
Notes
|
|
353
|
+
-----
|
|
354
|
+
This is an older version that is a bit slower than the new _hexword2freq.
|
|
355
|
+
"""
|
|
356
|
+
f = (
|
|
357
|
+
int(hex_str[:2], 16) * 256
|
|
358
|
+
+ int(hex_str[2:4], 16)
|
|
359
|
+
+ int(hex_str[4:], 16) / 256
|
|
360
|
+
)
|
|
361
|
+
return f
|
|
362
|
+
|
|
363
|
+
def _hexword2freq(self, hex_str):
|
|
364
|
+
"""
|
|
365
|
+
Convert Seabird hex data to frequency
|
|
366
|
+
each byte is given as two hex digits
|
|
367
|
+
each SB freq word is 3 bytes
|
|
368
|
+
calculates freq from 3 byte word
|
|
369
|
+
|
|
370
|
+
Parameters
|
|
371
|
+
----------
|
|
372
|
+
hex : str
|
|
373
|
+
6 character long hex string
|
|
374
|
+
|
|
375
|
+
Returns
|
|
376
|
+
-------
|
|
377
|
+
f : float
|
|
378
|
+
frequency
|
|
379
|
+
|
|
380
|
+
Notes
|
|
381
|
+
-----
|
|
382
|
+
This is a new version that is a bit faster than _hexword2freq_old.
|
|
383
|
+
See https://github.com/gunnarvoet/ctdproc/issues/1#issuecomment-2439574942
|
|
384
|
+
for details.
|
|
385
|
+
"""
|
|
386
|
+
return int(hex_str, 16) / 256
|
|
387
|
+
|
|
388
|
+
def _hexword2volt_old(self, hex_str):
|
|
389
|
+
"""
|
|
390
|
+
Convert Seabird hex data to voltage
|
|
391
|
+
each byte is given as two hex digits
|
|
392
|
+
each SB voltage is 1.5 words (8 MSB + 4 LSB)
|
|
393
|
+
calculates 2 voltages from 3 byte word
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
hex_str : str
|
|
398
|
+
6 character long hex str
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
v1, v2 : float
|
|
403
|
+
voltages for 2 channels
|
|
404
|
+
|
|
405
|
+
Notes
|
|
406
|
+
-----
|
|
407
|
+
This is an older version that is a bit slower than the new _hexword2volt.
|
|
408
|
+
"""
|
|
409
|
+
byte1 = format(int(hex_str[0:2], 16), "08b")
|
|
410
|
+
byte2 = format(int(hex_str[2:4], 16), "08b")
|
|
411
|
+
byte3 = format(int(hex_str[4:6], 16), "08b")
|
|
412
|
+
|
|
413
|
+
v1 = int(byte1 + byte2[:4], 2)
|
|
414
|
+
v2 = int(byte2[4:] + byte3, 2)
|
|
415
|
+
|
|
416
|
+
v1 = 5 * (1 - v1 / 4095)
|
|
417
|
+
v2 = 5 * (1 - v2 / 4095)
|
|
418
|
+
|
|
419
|
+
return v1, v2
|
|
420
|
+
|
|
421
|
+
def _hexword2volt(self, hex_str):
|
|
422
|
+
"""
|
|
423
|
+
Convert Seabird hex data to voltage
|
|
424
|
+
each byte is given as two hex digits
|
|
425
|
+
each SB voltage is 1.5 words (8 MSB + 4 LSB)
|
|
426
|
+
calculates 2 voltages from 3 byte word
|
|
427
|
+
|
|
428
|
+
Parameters
|
|
429
|
+
----------
|
|
430
|
+
hex_str : str
|
|
431
|
+
6 character long hex str
|
|
432
|
+
|
|
433
|
+
Returns
|
|
434
|
+
-------
|
|
435
|
+
v1, v2 : float
|
|
436
|
+
voltages for 2 channels
|
|
437
|
+
|
|
438
|
+
Notes
|
|
439
|
+
-----
|
|
440
|
+
This is a new version that is a bit faster than _hexword2volt_old.
|
|
441
|
+
See https://github.com/gunnarvoet/ctdproc/issues/1#issuecomment-2455907445
|
|
442
|
+
for details.
|
|
443
|
+
"""
|
|
444
|
+
e = int(hex_str, 16) ^ 0xffffff
|
|
445
|
+
v1 = (e >> 12)/819
|
|
446
|
+
v2 = (e & 0xfff)/819
|
|
447
|
+
return v1, v2
|
|
448
|
+
|
|
449
|
+
def _hexword2lonlat(self, hex_str):
|
|
450
|
+
"""
|
|
451
|
+
Convert Seabird lon/lat data in hex format.
|
|
452
|
+
Each byte is given as two hex digits.
|
|
453
|
+
Last two characters contain pos/neg information.
|
|
454
|
+
More information on p. 43 in manual-11pV2_018.pdf
|
|
455
|
+
|
|
456
|
+
Parameters
|
|
457
|
+
----------
|
|
458
|
+
hex_str : str
|
|
459
|
+
7 byte / character long hex string
|
|
460
|
+
|
|
461
|
+
Returns
|
|
462
|
+
-------
|
|
463
|
+
lon, lat : float
|
|
464
|
+
longitude and latitude
|
|
465
|
+
"""
|
|
466
|
+
b = format(int(hex_str[12:14], 16), "08b")
|
|
467
|
+
# newpos = int(b[7])
|
|
468
|
+
lonneg = int(b[1])
|
|
469
|
+
latneg = int(b[0])
|
|
470
|
+
lat = (
|
|
471
|
+
(-1) ** latneg
|
|
472
|
+
* (
|
|
473
|
+
int(hex_str[:2], 16) * 65536
|
|
474
|
+
+ int(hex_str[2:4], 16) * 256
|
|
475
|
+
+ int(hex_str[4:6], 16)
|
|
476
|
+
)
|
|
477
|
+
/ 5e4
|
|
478
|
+
)
|
|
479
|
+
lon = (
|
|
480
|
+
(-1) ** lonneg
|
|
481
|
+
* (
|
|
482
|
+
int(hex_str[6:8], 16) * 65536
|
|
483
|
+
+ int(hex_str[8:10], 16) * 256
|
|
484
|
+
+ int(hex_str[10:12], 16)
|
|
485
|
+
)
|
|
486
|
+
/ 5e4
|
|
487
|
+
)
|
|
488
|
+
return lat, lon
|
|
489
|
+
|
|
490
|
+
def _hexword2spar(self, hex_str):
|
|
491
|
+
"""
|
|
492
|
+
Convert Seabird spar (Photosynthetically Active Radiation) hex data
|
|
493
|
+
each byte is given as two hex digits
|
|
494
|
+
each SB voltage is 1.5 words (8 MSB + 4 LSB)
|
|
495
|
+
calculates SPAR voltages from 1.5 byte half word
|
|
496
|
+
|
|
497
|
+
Parameters
|
|
498
|
+
----------
|
|
499
|
+
hex_str : str
|
|
500
|
+
3 character long hex string
|
|
501
|
+
|
|
502
|
+
Returns
|
|
503
|
+
-------
|
|
504
|
+
spar : float
|
|
505
|
+
Photosynthetically Active Radiation
|
|
506
|
+
"""
|
|
507
|
+
byte1 = format(int(hex_str[0], 16), "04b")
|
|
508
|
+
byte2 = format(int(hex_str[1:3], 16), "08b")
|
|
509
|
+
spar = int(byte1 + byte2, 2) / 819
|
|
510
|
+
return spar
|
|
511
|
+
|
|
512
|
+
def _hexword2pstat(self, hex_str):
|
|
513
|
+
"""
|
|
514
|
+
Convert Seabird pressure sensor temperature and ctd status from hex data.
|
|
515
|
+
Each byte is given as two hex digits.
|
|
516
|
+
12 bit number from 0-4095 represents P sensor temperature .
|
|
517
|
+
4 bit CTD status:
|
|
518
|
+
bit 0 = pump status = 1/0 = on/off
|
|
519
|
+
bit 1 = bottom contact = 1/0 = no contact/contact
|
|
520
|
+
bit 2 = water sampler confirm = 1/0 = deck unit detetcts/does not signal
|
|
521
|
+
bit 3 = CTD modem carrier detects/does not detect deck unit = 1/0
|
|
522
|
+
|
|
523
|
+
Parameters
|
|
524
|
+
----------
|
|
525
|
+
hex_str : str
|
|
526
|
+
4 character long hex string
|
|
527
|
+
|
|
528
|
+
Returns
|
|
529
|
+
-------
|
|
530
|
+
pst : int
|
|
531
|
+
Pressure sensor temperature
|
|
532
|
+
ctdstatus : binary
|
|
533
|
+
CTD status (see above)
|
|
534
|
+
"""
|
|
535
|
+
byte1 = format(int(hex_str[0:2], 16), "08b")
|
|
536
|
+
byte2 = format(int(hex_str[2:4], 16), "08b")
|
|
537
|
+
pst = int(byte1 + byte2[:4], 2)
|
|
538
|
+
ctdstatus = byte2[4:]
|
|
539
|
+
return pst, ctdstatus
|
|
540
|
+
|
|
541
|
+
def _find_xmlconfig(self):
|
|
542
|
+
"""Generate path to xml config file for current hex file.
|
|
543
|
+
Config file needs to be in the same directory as the hex file."""
|
|
544
|
+
pp = Path(self.filename)
|
|
545
|
+
name = pp.stem
|
|
546
|
+
# try upper case filename
|
|
547
|
+
xmlfile = name.upper() + ".XMLCON"
|
|
548
|
+
p = pp.parent
|
|
549
|
+
self.xmlfile = p.joinpath(xmlfile)
|
|
550
|
+
# use os.listdir to find the actual case of the filename if the upper
|
|
551
|
+
# case did not work.
|
|
552
|
+
if self.xmlfile.name not in os.listdir(os.path.dirname(self.xmlfile)):
|
|
553
|
+
xmlfile = name.lower() + ".XMLCON"
|
|
554
|
+
self.xmlfile = p.joinpath(xmlfile)
|
|
555
|
+
|
|
556
|
+
def read_xml_config(self):
|
|
557
|
+
"""Read xml config file."""
|
|
558
|
+
self._find_xmlconfig()
|
|
559
|
+
try:
|
|
560
|
+
with open(self.xmlfile) as fd:
|
|
561
|
+
tmp = xmltodict.parse(fd.read())
|
|
562
|
+
except OSError as e:
|
|
563
|
+
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), e.filename)
|
|
564
|
+
sa = tmp["SBE_InstrumentConfiguration"]["Instrument"]["SensorArray"]["Sensor"]
|
|
565
|
+
# parse only valid sensors
|
|
566
|
+
cfg = {}
|
|
567
|
+
ti = 0
|
|
568
|
+
ci = 0
|
|
569
|
+
oi = 0
|
|
570
|
+
for si in sa:
|
|
571
|
+
keys = list(si.keys())
|
|
572
|
+
for k in keys:
|
|
573
|
+
if "@" not in k and k != "NotInUse":
|
|
574
|
+
if k == "TemperatureSensor":
|
|
575
|
+
ti += 1
|
|
576
|
+
kstr = "{}{}".format(k, ti)
|
|
577
|
+
elif k == "ConductivitySensor":
|
|
578
|
+
ci += 1
|
|
579
|
+
kstr = "{}{}".format(k, ci)
|
|
580
|
+
elif k == "OxygenSensor":
|
|
581
|
+
oi += 1
|
|
582
|
+
kstr = "{}{}".format(k, oi)
|
|
583
|
+
else:
|
|
584
|
+
kstr = k
|
|
585
|
+
cfg[kstr] = si.copy()
|
|
586
|
+
cfg[kstr]["cal"] = munchify(cfg[kstr][k])
|
|
587
|
+
del cfg[kstr][k]
|
|
588
|
+
self.cfgp = pd.DataFrame(cfg)
|
|
589
|
+
self._xml_coeffs_to_float()
|
|
590
|
+
|
|
591
|
+
def _xml_coeffs_to_float(self):
|
|
592
|
+
# Convert calibration coefficients to floats.
|
|
593
|
+
keep_strings = [
|
|
594
|
+
"@SensorID",
|
|
595
|
+
"SerialNumber",
|
|
596
|
+
"CalibrationDate",
|
|
597
|
+
"UseG_J",
|
|
598
|
+
]
|
|
599
|
+
for k in self.cfgp.keys():
|
|
600
|
+
for ki in self.cfgp[k].cal.keys():
|
|
601
|
+
if isinstance(self.cfgp[k]["cal"][ki], str):
|
|
602
|
+
if ki not in keep_strings:
|
|
603
|
+
self.cfgp[k]["cal"][ki] = float(self.cfgp[k]["cal"][ki])
|
|
604
|
+
elif isinstance(self.cfgp[k]["cal"][ki], list):
|
|
605
|
+
for i, li in enumerate(self.cfgp[k]["cal"][ki]):
|
|
606
|
+
for kli in li.keys():
|
|
607
|
+
self.cfgp[k]["cal"][ki][i][kli] = float(
|
|
608
|
+
self.cfgp[k]["cal"][ki][i][kli]
|
|
609
|
+
)
|
|
610
|
+
# We can't have None values in the xarray.Dataset later on
|
|
611
|
+
# or otherwise it won't properly write to netcdf. Therefore,
|
|
612
|
+
# convert any None items to 'N/A'
|
|
613
|
+
for ki, v in self.cfgp[k].cal.items():
|
|
614
|
+
if v is None:
|
|
615
|
+
self.cfgp[k].cal[ki] = "N/A"
|
|
616
|
+
|
|
617
|
+
def physicalunits(self):
|
|
618
|
+
# pressure
|
|
619
|
+
self._p_atm = 10.1353 # why not 10.1325 dbar?
|
|
620
|
+
self.data = Munch()
|
|
621
|
+
self.data.lon = self.dataraw.lon
|
|
622
|
+
self.data.lat = self.dataraw.lat
|
|
623
|
+
self.data.p = (
|
|
624
|
+
self._freq2pressure(
|
|
625
|
+
self.dataraw.p, self.dataraw.pst, self.cfgp.PressureSensor.cal
|
|
626
|
+
)
|
|
627
|
+
- self._p_atm
|
|
628
|
+
)
|
|
629
|
+
self.data.t1 = self._freq2temp(
|
|
630
|
+
self.dataraw.t1, self.cfgp.TemperatureSensor1.cal
|
|
631
|
+
)
|
|
632
|
+
self.data.t2 = self._freq2temp(
|
|
633
|
+
self.dataraw.t2, self.cfgp.TemperatureSensor2.cal
|
|
634
|
+
)
|
|
635
|
+
ccal1 = self.cfgp.ConductivitySensor1.cal.Coefficients[1]
|
|
636
|
+
self.data.c1 = self._freq2cond(
|
|
637
|
+
self.dataraw.c1, self.data.t1, self.data.p, ccal1
|
|
638
|
+
)
|
|
639
|
+
ccal2 = self.cfgp.ConductivitySensor2.cal.Coefficients[1]
|
|
640
|
+
self.data.c2 = self._freq2cond(
|
|
641
|
+
self.dataraw.c2, self.data.t2, self.data.p, ccal2
|
|
642
|
+
)
|
|
643
|
+
if hasattr(self.dataraw, "alt"):
|
|
644
|
+
self.data.alt = self._volt2alt(
|
|
645
|
+
self.dataraw.alt, self.cfgp.AltimeterSensor.cal
|
|
646
|
+
)
|
|
647
|
+
if hasattr(self.dataraw, "fl"):
|
|
648
|
+
# couldn't find any good calibration coefficients in the con file I looked at.
|
|
649
|
+
self.data.fl = self.dataraw.fl
|
|
650
|
+
if hasattr(self.dataraw, "trans"):
|
|
651
|
+
self.data.trans = self._volt2trans(
|
|
652
|
+
self.dataraw.trans, self.cfgp[self._mapnames_volt["trans"]["name"]].cal
|
|
653
|
+
)
|
|
654
|
+
if hasattr(self.dataraw, "par"):
|
|
655
|
+
self.data.par = self._volt2par(
|
|
656
|
+
self.dataraw.par, self.cfgp[self._mapnames_volt["par"]["name"]].cal
|
|
657
|
+
)
|
|
658
|
+
if hasattr(self.data, 'c1') and hasattr(self.data, 't1') and hasattr(self.data, 'p'):
|
|
659
|
+
if hasattr(self.data, 'lon') and hasattr(self.data, 'lat'):
|
|
660
|
+
lon, lat = self.data.lon, self.data.lat
|
|
661
|
+
else:
|
|
662
|
+
print("Warning: No position data, using default lon=0, lat=0")
|
|
663
|
+
lon, lat = 0.0, 0.0
|
|
664
|
+
SA, SP = calc_allsal(self.data.c1, self.data.t1, self.data.p, lon, lat)
|
|
665
|
+
self.data.sal = SP
|
|
666
|
+
self.data.SA = SA
|
|
667
|
+
# Ensure attributes dictionary exists
|
|
668
|
+
if not hasattr(self, '_map_attrs'):
|
|
669
|
+
self._map_attrs = {}
|
|
670
|
+
self._map_attrs["sal"] = {
|
|
671
|
+
"name": "sal",
|
|
672
|
+
"long_name": "Practical Salinity",
|
|
673
|
+
"units": "PSU",
|
|
674
|
+
}
|
|
675
|
+
self._map_attrs["SA"] = {
|
|
676
|
+
"name": "SA",
|
|
677
|
+
"long_name": "Absolute Salinity",
|
|
678
|
+
"units": "g/kg",
|
|
679
|
+
}
|
|
680
|
+
if hasattr(self.dataraw, "oxygen1"):
|
|
681
|
+
if hasattr(self.data, 'time'):
|
|
682
|
+
time = self.data.time
|
|
683
|
+
elif hasattr(self.data, 'scan'):
|
|
684
|
+
time = self.data.scan
|
|
685
|
+
else:
|
|
686
|
+
time = np.arange(len(self.dataraw.oxygen1))
|
|
687
|
+
self.data.oxygen1 = self._volt2oxygen(
|
|
688
|
+
self.dataraw.oxygen1,
|
|
689
|
+
self.data.t1,
|
|
690
|
+
self.data.sal,
|
|
691
|
+
self.data.p,
|
|
692
|
+
time,
|
|
693
|
+
self.cfgp[self._mapnames_volt["oxygen1"]["name"]].cal,
|
|
694
|
+
min_pressure=1.0
|
|
695
|
+
)
|
|
696
|
+
self.data.modcount = self.dataraw.modcount
|
|
697
|
+
self._check_modcount_errors(self.data.modcount)
|
|
698
|
+
self.data.dtnum = self.sbetime_to_mattime(self.dataraw.time)
|
|
699
|
+
self.data.time = self.mattime_to_datetime64(self.data.dtnum)
|
|
700
|
+
|
|
701
|
+
def _calculate_oxsol(self, temp, salinity):
|
|
702
|
+
"""
|
|
703
|
+
Calculate oxygen solubility using Garcia and Gordon (1992).
|
|
704
|
+
Returns oxygen solubility in ml/L.
|
|
705
|
+
"""
|
|
706
|
+
T_scaled = np.log((298.15 - temp) / (273.15 + temp))
|
|
707
|
+
|
|
708
|
+
# Garcia and Gordon (1992) coefficients
|
|
709
|
+
A0 = 2.00907
|
|
710
|
+
A1 = 3.22014
|
|
711
|
+
A2 = 4.05010
|
|
712
|
+
A3 = 4.94457
|
|
713
|
+
A4 = -0.256847
|
|
714
|
+
A5 = 3.88767
|
|
715
|
+
B0 = -0.00624523
|
|
716
|
+
B1 = -0.00737614
|
|
717
|
+
B2 = -0.0103410
|
|
718
|
+
B3 = -0.00817083
|
|
719
|
+
C0 = -0.000000488682
|
|
720
|
+
|
|
721
|
+
oxsol = np.exp(
|
|
722
|
+
A0 + A1*T_scaled + A2*T_scaled**2 + A3*T_scaled**3 +
|
|
723
|
+
A4*T_scaled**4 + A5*T_scaled**5 +
|
|
724
|
+
salinity * (B0 + B1*T_scaled + B2*T_scaled**2 + B3*T_scaled**3) +
|
|
725
|
+
C0 * salinity**2
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
return oxsol
|
|
729
|
+
|
|
730
|
+
def _calculate_tau(self, temp, pressure, cal):
|
|
731
|
+
"""Calculate sensor time constant tau(T,P)."""
|
|
732
|
+
D0 = float(cal.D0)
|
|
733
|
+
D1 = float(cal.D1)
|
|
734
|
+
D2 = float(cal.D2)
|
|
735
|
+
tau20 = float(cal.Tau20)
|
|
736
|
+
|
|
737
|
+
tau = tau20 * D0 * np.exp(D1*pressure + D2*(temp - 20))
|
|
738
|
+
return tau
|
|
739
|
+
|
|
740
|
+
def _calculate_dvdt_window(self, volt, time_seconds, window_seconds=2.0):
|
|
741
|
+
"""
|
|
742
|
+
Calculate dV/dt over a window as specified in SBE documentation.
|
|
743
|
+
If there are fewer than 2 points in the window (2s by default), dV/dt is set to zero.
|
|
744
|
+
"""
|
|
745
|
+
# Convert the desired window length (in seconds) to samples
|
|
746
|
+
dt = np.mean(np.diff(time_seconds))
|
|
747
|
+
window_length = int(window_seconds / dt)
|
|
748
|
+
|
|
749
|
+
# window_length must be odd and >= polyorder + 2
|
|
750
|
+
if window_length % 2 == 0:
|
|
751
|
+
window_length += 1
|
|
752
|
+
window_length = max(window_length, 5)
|
|
753
|
+
|
|
754
|
+
# Compute the first derivative directly
|
|
755
|
+
dVdt = savgol_filter(volt, window_length=window_length, polyorder=1, deriv=1, delta=dt)
|
|
756
|
+
|
|
757
|
+
return dVdt
|
|
758
|
+
|
|
759
|
+
def _freq2pressure(self, freq, tc, pcal):
|
|
760
|
+
"""Calculates pressure given frequency pressure temperature compensation
|
|
761
|
+
and pressure calibration structure pcal"""
|
|
762
|
+
psi2dbar = 0.689476
|
|
763
|
+
|
|
764
|
+
Td = pcal.AD590M * tc + pcal.AD590B
|
|
765
|
+
|
|
766
|
+
c = pcal.C1 + Td * (pcal.C2 + Td * pcal.C3)
|
|
767
|
+
d = pcal.D1 + Td * pcal.D2
|
|
768
|
+
t0 = pcal.T1 + Td * (pcal.T2 + Td * (pcal.T3 + Td * (pcal.T4 + Td * pcal.T5)))
|
|
769
|
+
t0f = 1e-6 * t0 * freq
|
|
770
|
+
fact = 1 - (t0f * t0f)
|
|
771
|
+
pres = psi2dbar * (c * fact * (1 - d * fact))
|
|
772
|
+
pres = pcal.Slope * pres + pcal.Offset
|
|
773
|
+
return pres
|
|
774
|
+
|
|
775
|
+
def _freq2temp(self, freq, tcal):
|
|
776
|
+
"""Calculate temperature given frequency and
|
|
777
|
+
temperature calibration structure tcal
|
|
778
|
+
D. Rudnick 01/06/05"""
|
|
779
|
+
|
|
780
|
+
logf0f = np.log(tcal.F0 / freq)
|
|
781
|
+
temp = (
|
|
782
|
+
1 / (tcal.G + logf0f * (tcal.H + logf0f * (tcal.I + logf0f * tcal.J)))
|
|
783
|
+
) - 273.15
|
|
784
|
+
return temp
|
|
785
|
+
|
|
786
|
+
def _freq2cond(self, freq, temp, pres, ccal):
|
|
787
|
+
"""Calculates conductivity given frequency, temperature,
|
|
788
|
+
pressure and conductivity calibration structure ccal.
|
|
789
|
+
D. Rudnick 01/06/05"""
|
|
790
|
+
|
|
791
|
+
ff = freq / 1000
|
|
792
|
+
cond = (ccal.G + ff * ff * (ccal.H + ff * (ccal.I + ff * ccal.J))) / (
|
|
793
|
+
10 * (1 + ccal.CTcor * temp + ccal.CPcor * pres)
|
|
794
|
+
)
|
|
795
|
+
return cond
|
|
796
|
+
|
|
797
|
+
def _volt2alt(self, volt, acal):
|
|
798
|
+
"""Calculate altimeter data from voltage."""
|
|
799
|
+
alt = volt * acal.ScaleFactor + acal.Offset
|
|
800
|
+
return alt
|
|
801
|
+
|
|
802
|
+
def _volt2trans(self, volt, transcal):
|
|
803
|
+
"""Calculate transmissometer data from voltage."""
|
|
804
|
+
trans = volt * transcal.M + transcal.B
|
|
805
|
+
return trans
|
|
806
|
+
|
|
807
|
+
def _volt2par(self, volt, cal):
|
|
808
|
+
"""Calculate PAR data from voltage."""
|
|
809
|
+
par = (
|
|
810
|
+
cal.Multiplier
|
|
811
|
+
* (10**9 * 10 ** ((volt - cal.B) / cal.M))
|
|
812
|
+
/ cal.CalibrationConstant
|
|
813
|
+
) + cal.Offset
|
|
814
|
+
return par
|
|
815
|
+
|
|
816
|
+
def _volt2oxygen(self, volt, temp, salinity, pressure, time, ocal,
|
|
817
|
+
use_tau_correction=True, min_pressure=1.0):
|
|
818
|
+
"""
|
|
819
|
+
Convert oxygen voltage to ml/L using SBE43 equation.
|
|
820
|
+
|
|
821
|
+
Parameters
|
|
822
|
+
----------
|
|
823
|
+
use_tau_correction : bool
|
|
824
|
+
Apply tau*dV/dt correction. Default is True.
|
|
825
|
+
min_pressure : float
|
|
826
|
+
Minimum pressure (dbar) below which oxygen is set to NaN (default 1.0).
|
|
827
|
+
"""
|
|
828
|
+
equation_index = int(ocal.Use2007Equation) if hasattr(ocal, 'Use2007Equation') else 1
|
|
829
|
+
cal = ocal.CalibrationCoefficients[equation_index]
|
|
830
|
+
|
|
831
|
+
Soc = float(cal.Soc)
|
|
832
|
+
Voffset = float(cal.offset)
|
|
833
|
+
A = float(cal.A)
|
|
834
|
+
B = float(cal.B)
|
|
835
|
+
C = float(cal.C)
|
|
836
|
+
E = float(cal.E)
|
|
837
|
+
|
|
838
|
+
# Convert time to numeric seconds
|
|
839
|
+
if hasattr(time, 'dtype') and np.issubdtype(time.dtype, np.datetime64):
|
|
840
|
+
time_seconds = (time - time[0]) / np.timedelta64(1, 's')
|
|
841
|
+
elif hasattr(time, 'dtype') and np.issubdtype(time.dtype, np.timedelta64):
|
|
842
|
+
time_seconds = time / np.timedelta64(1, 's')
|
|
843
|
+
else:
|
|
844
|
+
time_seconds = np.asarray(time, dtype=float)
|
|
845
|
+
|
|
846
|
+
# Calculate tau*dV/dt correction if requested (for moorings)
|
|
847
|
+
if use_tau_correction:
|
|
848
|
+
dVdt = self._calculate_dvdt_window(volt, time_seconds, window_seconds=2.0)
|
|
849
|
+
tau = self._calculate_tau(temp, pressure, cal)
|
|
850
|
+
tau_correction = tau * dVdt
|
|
851
|
+
else:
|
|
852
|
+
print(" Skipping tau*dV/dt correction...")
|
|
853
|
+
tau_correction = 0.0
|
|
854
|
+
|
|
855
|
+
# Calculate oxygen solubility
|
|
856
|
+
oxsol = self._calculate_oxsol(temp, salinity)
|
|
857
|
+
|
|
858
|
+
# Convert temperature to Kelvin
|
|
859
|
+
K = temp + 273.15
|
|
860
|
+
|
|
861
|
+
# SBE43 equation
|
|
862
|
+
oxygen = Soc * (volt + Voffset + tau_correction) * \
|
|
863
|
+
oxsol * \
|
|
864
|
+
(1.0 + A*temp + B*temp**2 + C*temp**3) * \
|
|
865
|
+
np.exp(E * pressure / K)
|
|
866
|
+
|
|
867
|
+
# Quality control
|
|
868
|
+
oxygen = np.where(pressure < min_pressure, np.nan, oxygen)
|
|
869
|
+
oxygen = np.where(salinity < 1.0, np.nan, oxygen)
|
|
870
|
+
oxygen = np.where(oxygen < 0, np.nan, oxygen)
|
|
871
|
+
oxygen = np.where(oxygen > 15, np.nan, oxygen)
|
|
872
|
+
|
|
873
|
+
return oxygen
|
|
874
|
+
|
|
875
|
+
def _check_modcount_errors(self, modcount):
|
|
876
|
+
"""Check for modcount errors."""
|
|
877
|
+
dmc = np.diff(modcount)
|
|
878
|
+
mmc = np.mod(dmc, 256)
|
|
879
|
+
fmc = np.squeeze(np.where(mmc - 1))
|
|
880
|
+
if np.any(fmc):
|
|
881
|
+
print("Warning: {} bad modcounts".format(len(dmc[mmc != 1])))
|
|
882
|
+
print("Warning: {} missing scans".format(np.sum(mmc[fmc])))
|
|
883
|
+
|
|
884
|
+
def sbetime_to_mattime(self, dt):
|
|
885
|
+
"""Convert SBE time format to matlab time format."""
|
|
886
|
+
dtnum = dt / 24 / 3600 + 719529
|
|
887
|
+
return dtnum
|
|
888
|
+
|
|
889
|
+
def mattime_to_sbetime(self, dt):
|
|
890
|
+
"""Convert matlab time format to SBE time format."""
|
|
891
|
+
dtnum = (dt - 719529) * 24 * 3600
|
|
892
|
+
return dtnum
|
|
893
|
+
|
|
894
|
+
def mattime_to_datetime64(self, dtnum):
|
|
895
|
+
"""Convert Matlab time format to numpy datetime64 time format."""
|
|
896
|
+
dt64 = mtlb2datetime(dtnum)
|
|
897
|
+
return dt64
|
|
898
|
+
|
|
899
|
+
def _generate_time_vector(self, length):
|
|
900
|
+
"""generate time vector in SBE time format from a start time string"""
|
|
901
|
+
pt = pd.to_datetime(self.header_time_str)
|
|
902
|
+
# generate a 24Hz time vector
|
|
903
|
+
pr = pd.date_range(
|
|
904
|
+
pt,
|
|
905
|
+
periods=length,
|
|
906
|
+
freq="{}ns".format(np.int64(np.round(1 / 24 * 1e9))),
|
|
907
|
+
)
|
|
908
|
+
# t = pr.to_numpy()
|
|
909
|
+
mattime = datetime2mtlb(pr.to_numpy())
|
|
910
|
+
sbetime = self.mattime_to_sbetime(mattime)
|
|
911
|
+
return sbetime
|
|
912
|
+
|
|
913
|
+
def to_mat(self, matname):
|
|
914
|
+
"""Save data in Matlab format."""
|
|
915
|
+
ctdout = self.data.copy()
|
|
916
|
+
ctdout.pop("time")
|
|
917
|
+
# ctdout.pop('matlabtime')
|
|
918
|
+
# ctdout['den'] = ctdout['pden']
|
|
919
|
+
# ctdout.pop('pden')
|
|
920
|
+
# ctdout['time'] = ctd['matlabtime']
|
|
921
|
+
sio.savemat(matname, ctdout, format="5")
|
|
922
|
+
|
|
923
|
+
def to_xarray(self):
|
|
924
|
+
"""Convert data into xarray.Dataset.
|
|
925
|
+
|
|
926
|
+
Returns
|
|
927
|
+
-------
|
|
928
|
+
ds : xarray.Dataset
|
|
929
|
+
CTD time series in Dataset format.
|
|
930
|
+
"""
|
|
931
|
+
dsout = self.data.copy()
|
|
932
|
+
dsout.pop("time")
|
|
933
|
+
dsout.pop("dtnum")
|
|
934
|
+
datavars = dsout.keys()
|
|
935
|
+
ds_data = {var: (["time"], self.data[var]) for var in datavars}
|
|
936
|
+
|
|
937
|
+
ds = xr.Dataset(
|
|
938
|
+
data_vars=dict(ds_data),
|
|
939
|
+
coords={
|
|
940
|
+
"time": (
|
|
941
|
+
["time"],
|
|
942
|
+
self.data["time"],
|
|
943
|
+
{
|
|
944
|
+
"long_name": "time",
|
|
945
|
+
"standard_name": "time",
|
|
946
|
+
},
|
|
947
|
+
)
|
|
948
|
+
},
|
|
949
|
+
)
|
|
950
|
+
# set attributes
|
|
951
|
+
for k in ds.data_vars:
|
|
952
|
+
v = self._map_attrs[k]
|
|
953
|
+
name = v.pop("name")
|
|
954
|
+
ds[k].attrs = {**ds[k].attrs, **v}
|
|
955
|
+
if name not in self.cfgp:
|
|
956
|
+
continue
|
|
957
|
+
ds[k].attrs["serial_number"] = self.cfgp[name].cal["SerialNumber"]
|
|
958
|
+
ds[k].attrs["calibration_date"] = self.cfgp[name].cal["CalibrationDate"]
|
|
959
|
+
|
|
960
|
+
return ds
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def prof_to_mat(matname, datad, datau):
|
|
964
|
+
out = dict(
|
|
965
|
+
datad=datad,
|
|
966
|
+
datau=datau,
|
|
967
|
+
)
|
|
968
|
+
# a few adjustments before saving as .mat file
|
|
969
|
+
for k, v in out.items():
|
|
970
|
+
# matlab time as new variable
|
|
971
|
+
# out[k]['datenum'] = (['time'], datetime2mtlb(v.time.data))
|
|
972
|
+
# drop datetime64
|
|
973
|
+
out[k].drop("time")
|
|
974
|
+
# make coordinates variables so they are saved
|
|
975
|
+
out[k] = v.reset_coords()
|
|
976
|
+
sio.savemat(matname, out, format="5")
|