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/proc.py
ADDED
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
import gsw
|
|
4
|
+
import matplotlib as mpl
|
|
5
|
+
import numpy as np
|
|
6
|
+
import xarray as xr
|
|
7
|
+
from scipy import fft, optimize, signal, stats
|
|
8
|
+
|
|
9
|
+
from . import calcs, helpers
|
|
10
|
+
|
|
11
|
+
# We are running into trouble if the default backend is not installed
|
|
12
|
+
# in the current python environment. Try to import pyplot the default
|
|
13
|
+
# way first. If this fails, set backend to agg which should always work.
|
|
14
|
+
try:
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
except ImportError:
|
|
17
|
+
mpl.use("agg")
|
|
18
|
+
import matplotlib.pyplot as plt
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run_all(ds):
|
|
22
|
+
"""
|
|
23
|
+
Run all standard processing steps on raw CTD time series.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
data : xarray.Dataset
|
|
28
|
+
CTD time series data structure
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
datad : xarray.Dataset
|
|
33
|
+
Downcast time series
|
|
34
|
+
datau : xarray.Dataset
|
|
35
|
+
Upcast time series
|
|
36
|
+
"""
|
|
37
|
+
ds = cleanup(ds)
|
|
38
|
+
|
|
39
|
+
ds_updown = split_updn(ds)
|
|
40
|
+
|
|
41
|
+
for v, d in ds_updown.items():
|
|
42
|
+
add_tcfit_default(d)
|
|
43
|
+
d = d.dropna("time", how="any")
|
|
44
|
+
d = phase_correct(d)
|
|
45
|
+
d = calcs.swcalcs(d)
|
|
46
|
+
d = rmloops(d)
|
|
47
|
+
d = cleanup_ud(d)
|
|
48
|
+
ds_updown[v] = d
|
|
49
|
+
|
|
50
|
+
return ds_updown
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def plot_profile(ds, var_list):
|
|
54
|
+
for i, v in enumerate(var_list, start=1):
|
|
55
|
+
plt.subplot(1, len(var_list), i)
|
|
56
|
+
if "time" in ds.dims:
|
|
57
|
+
ds[v].plot(x="time")
|
|
58
|
+
elif "depth" in ds.dims:
|
|
59
|
+
ds[v].plot(y="depth")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def plot_TS(ds):
|
|
63
|
+
plt.plot(ds["t1"], ds["s1"], alpha=0.1)
|
|
64
|
+
plt.plot(ds["t2"], ds["s2"], alpha=0.1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def add_tcfit_default(ds):
|
|
68
|
+
"""
|
|
69
|
+
Get default values for tc fit range depending on depth of cast.
|
|
70
|
+
|
|
71
|
+
Range for tc fit is 200dbar to maximum pressure if the cast is
|
|
72
|
+
shallower than 1000dbar, 500dbar to max pressure otherwise.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
ds : xarray.Dataset
|
|
77
|
+
CTD time series data structure
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
tcfit : tuple
|
|
82
|
+
Upper and lower limit for tc fit in phase_correct.
|
|
83
|
+
"""
|
|
84
|
+
if ds.p.max() > 1000:
|
|
85
|
+
tcfit = [500, ds.p.max().data]
|
|
86
|
+
elif ds.p.max() > 300:
|
|
87
|
+
tcfit = [200, ds.p.max().data]
|
|
88
|
+
else:
|
|
89
|
+
tcfit = [50, ds.p.max().data]
|
|
90
|
+
ds.attrs["tcfit"] = tcfit
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def cleanup(ds):
|
|
94
|
+
"""
|
|
95
|
+
Clean up CTD raw time series.
|
|
96
|
+
|
|
97
|
+
- despike pressure
|
|
98
|
+
- eliminate data near surface
|
|
99
|
+
- remove spikes in other data
|
|
100
|
+
- remove smaller T, C, glitches
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
# despike pressure
|
|
104
|
+
ds["p"].data = helpers.glitchcorrect(
|
|
105
|
+
ds.p.data, ds.attrs["diff_p"], ds.attrs["prod_p"]
|
|
106
|
+
)
|
|
107
|
+
ipmax = np.argmax(ds.p.data)
|
|
108
|
+
|
|
109
|
+
# eliminate near-surface data
|
|
110
|
+
ptop = 1
|
|
111
|
+
fdeep = np.squeeze(np.where(ds.p.data > ptop))
|
|
112
|
+
ideepstart, ideepstop, ideeplen = helpers.findsegments(fdeep)
|
|
113
|
+
ii = np.max(ideepstart[ideepstart < ipmax])
|
|
114
|
+
jj = np.min(ideepstop[ideepstop >= ipmax])
|
|
115
|
+
ds = ds.isel(time=range(ii, jj + 1))
|
|
116
|
+
|
|
117
|
+
# remove spikes in temperature
|
|
118
|
+
ib = np.squeeze(
|
|
119
|
+
np.where(np.absolute(np.diff(ds.t1.data)) > ds.attrs["spike_thresh_t"])
|
|
120
|
+
)
|
|
121
|
+
ds.t1[ib] = np.nan
|
|
122
|
+
ib = np.squeeze(
|
|
123
|
+
np.where(np.absolute(np.diff(ds.t2.data)) > ds.attrs["spike_thresh_t"])
|
|
124
|
+
)
|
|
125
|
+
ds.t2[ib] = np.nan
|
|
126
|
+
|
|
127
|
+
# remove out of range values
|
|
128
|
+
ds = preen_ctd(ds)
|
|
129
|
+
# no trans, fl ***
|
|
130
|
+
|
|
131
|
+
# remove nans at start
|
|
132
|
+
fnan1 = np.squeeze(np.where(np.isnan(ds.c1.data)))
|
|
133
|
+
fnan2 = np.squeeze(np.where(np.isnan(ds.c2.data)))
|
|
134
|
+
if np.array_equiv(fnan1, fnan2) is False:
|
|
135
|
+
print("warning: NaNs index different in data.c1 and data.c2")
|
|
136
|
+
if fnan1.size > 0:
|
|
137
|
+
istart, istop, ilen = helpers.findsegments(fnan1)
|
|
138
|
+
if istart[0] != 0 | istart.size != 1:
|
|
139
|
+
print("warning: more NaNs")
|
|
140
|
+
ds = ds.isel(time=range(istop[0] + 1, ds.time.size))
|
|
141
|
+
|
|
142
|
+
return ds
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def cleanup_ud(ds):
|
|
146
|
+
"""More cleaning and calculation of derived variables."""
|
|
147
|
+
|
|
148
|
+
# remove spikes in temperature
|
|
149
|
+
for v in ["t1", "t2"]:
|
|
150
|
+
ds[v] = despike(ds[v], ds.attrs["spike_thresh_t"])
|
|
151
|
+
|
|
152
|
+
# despike T, C
|
|
153
|
+
ibefore = 1
|
|
154
|
+
iafter = 1
|
|
155
|
+
for v in ["c1", "c2", "t1", "t2"]:
|
|
156
|
+
ds[v].data = helpers.glitchcorrect(
|
|
157
|
+
ds[v], ds.attrs[f"diff_{v[0]}"], ds.attrs[f"prod_{v[0]}"], ibefore, iafter
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Calculate salinity (both absolute and practical)
|
|
161
|
+
ds = calcs.calc_sal(ds)
|
|
162
|
+
|
|
163
|
+
# despike s
|
|
164
|
+
ibefore = 2
|
|
165
|
+
iafter = 2
|
|
166
|
+
for v in ["s1", "s2", "SA1", "SA2"]:
|
|
167
|
+
# remove spikes
|
|
168
|
+
ds[v] = despike(ds[v], ds.attrs["spike_thresh_s"])
|
|
169
|
+
# remove out of bounds data
|
|
170
|
+
ds[v] = remove_out_of_bounds(
|
|
171
|
+
ds[v], bmin=ds.attrs["bounds_s"][0], bmax=ds.attrs["bounds_s"][1]
|
|
172
|
+
)
|
|
173
|
+
ds[v].data = helpers.glitchcorrect(
|
|
174
|
+
ds[v], ds.attrs["diff_s"], ds.attrs["prod_s"], ibefore, iafter
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# calculate potential/conservative temperature, potential density anomaly
|
|
178
|
+
ds = calcs.calc_temp(ds)
|
|
179
|
+
ds = calcs.calc_sigma(ds)
|
|
180
|
+
|
|
181
|
+
return ds
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def despike(da, spike_threshold):
|
|
185
|
+
"""Set spikes to NaN."""
|
|
186
|
+
absdiff = np.absolute(np.diff(da.data))
|
|
187
|
+
# Using np.greater instead of the > operator as we can use the where option
|
|
188
|
+
# and avoid the warning when nans are compared to a number. It broadcasts
|
|
189
|
+
# to the original array size.
|
|
190
|
+
ib = np.squeeze(
|
|
191
|
+
np.where(np.greater(absdiff, spike_threshold, where=np.isfinite(absdiff)))
|
|
192
|
+
)
|
|
193
|
+
da[ib] = np.nan
|
|
194
|
+
|
|
195
|
+
return da
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def remove_out_of_bounds(da, bmin, bmax):
|
|
199
|
+
"""Remove out of bounds data."""
|
|
200
|
+
|
|
201
|
+
da = da.where((da<=bmax) & (da>=bmin))
|
|
202
|
+
|
|
203
|
+
return da
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def preen_ctd(ds):
|
|
207
|
+
"""Remove spikes in p, t1, t2, c1, c2.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
data : xarray.Dataset
|
|
212
|
+
Dataset with CTD time series.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
data : xarray.Dataset
|
|
217
|
+
Cleaned dataset.
|
|
218
|
+
"""
|
|
219
|
+
for v in ["p", "t1", "t2", "c1", "c2"]:
|
|
220
|
+
ds[v] = ds[v].where(
|
|
221
|
+
xr.zeros_like(ds[v]),
|
|
222
|
+
helpers.preen(
|
|
223
|
+
ds[v].data, ds.attrs[f"bounds_{v[0]}"][0], ds.attrs[f"bounds_{v[0]}"][1]
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# TODO: remove spikes in oxygen, trans, fl in volts.
|
|
228
|
+
return ds
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def phase_correct(ds):
|
|
232
|
+
"""
|
|
233
|
+
Bring temperature and conductivity in phase.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
ds : dtype
|
|
238
|
+
description
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
ds : dtype
|
|
243
|
+
description
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
# remove spikes
|
|
247
|
+
# TODO: bring this back in. however, the function fails later on if there
|
|
248
|
+
# are nan's present. Could interpolate over anything that is just a few data points
|
|
249
|
+
# for field in ["t1", "t2", "c1", "c2"]:
|
|
250
|
+
# ib = np.squeeze(np.where(np.absolute(np.diff(data[field].data)) > 0.5))
|
|
251
|
+
# data[field][ib] = np.nan
|
|
252
|
+
|
|
253
|
+
# ---Spectral Analysis of Raw Data---
|
|
254
|
+
# 24Hz data
|
|
255
|
+
dt = 1 / 24
|
|
256
|
+
# number of points per segment
|
|
257
|
+
N = 2**9
|
|
258
|
+
|
|
259
|
+
# only data within tcfit range.
|
|
260
|
+
ii = np.squeeze(
|
|
261
|
+
np.argwhere(
|
|
262
|
+
(ds.p.data > ds.attrs["tcfit"][0]) & (ds.p.data < ds.attrs["tcfit"][1])
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
i1 = ii[0]
|
|
266
|
+
i2 = ii[-1]
|
|
267
|
+
n = i2 - i1 + 1
|
|
268
|
+
n = (np.floor(n / N) * N).astype("int64")
|
|
269
|
+
# Truncate to be multiple of N elements long
|
|
270
|
+
i2 = (i1 + n).astype("int64")
|
|
271
|
+
# number of segments = dof/2
|
|
272
|
+
m = (n / N).astype("int64")
|
|
273
|
+
# Number of degrees of freedom (power of 2)
|
|
274
|
+
dof = 2 * m
|
|
275
|
+
# Frequency resolution at dof degrees of freedom.
|
|
276
|
+
df = 1 / (N * dt)
|
|
277
|
+
|
|
278
|
+
# fft of each segment (row). Data are detrended, then windowed.
|
|
279
|
+
window = signal.windows.triang(N) * np.ones((m, N))
|
|
280
|
+
At1 = fft.fft(
|
|
281
|
+
signal.detrend(np.reshape(ds.t1.data[i1:i2], newshape=(m, N))) * window
|
|
282
|
+
)
|
|
283
|
+
At2 = fft.fft(
|
|
284
|
+
signal.detrend(np.reshape(ds.t2.data[i1:i2], newshape=(m, N))) * window
|
|
285
|
+
)
|
|
286
|
+
Ac1 = fft.fft(
|
|
287
|
+
signal.detrend(np.reshape(ds.c1.data[i1:i2], newshape=(m, N))) * window
|
|
288
|
+
)
|
|
289
|
+
Ac2 = fft.fft(
|
|
290
|
+
signal.detrend(np.reshape(ds.c2.data[i1:i2], newshape=(m, N))) * window
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Positive frequencies only
|
|
294
|
+
At1 = At1[:, 0 : int(N / 2)]
|
|
295
|
+
At2 = At2[:, 0 : int(N / 2)]
|
|
296
|
+
Ac1 = Ac1[:, 0 : int(N / 2)]
|
|
297
|
+
Ac2 = Ac2[:, 0 : int(N / 2)]
|
|
298
|
+
|
|
299
|
+
# Frequency
|
|
300
|
+
f = fft.ifftshift(np.linspace(-N / 2, N / 2 - 1, N) / N / dt)
|
|
301
|
+
f = f[: int(N / 2)]
|
|
302
|
+
fold = f
|
|
303
|
+
|
|
304
|
+
# Spectral Estimates. Note: In Matlab, At1*conj(At1) is not complex anymore.
|
|
305
|
+
# Here, it is still a complex number but the imaginary part is zero.
|
|
306
|
+
# We keep only the real part to stay consistent.
|
|
307
|
+
Et1 = 2 * np.real(np.nanmean(At1 * np.conj(At1) / df / N**2, axis=0))
|
|
308
|
+
Et2 = 2 * np.real(np.nanmean(At2 * np.conj(At2) / df / N**2, axis=0))
|
|
309
|
+
Ec1 = 2 * np.real(np.nanmean(Ac1 * np.conj(Ac1) / df / N**2, axis=0))
|
|
310
|
+
Ec2 = 2 * np.real(np.nanmean(Ac2 * np.conj(Ac2) / df / N**2, axis=0))
|
|
311
|
+
|
|
312
|
+
# Cross Spectral Estimates
|
|
313
|
+
Ct1c1 = 2 * np.nanmean(At1 * np.conj(Ac1) / df / N**2, axis=0)
|
|
314
|
+
Ct2c2 = 2 * np.nanmean(At2 * np.conj(Ac2) / df / N**2, axis=0)
|
|
315
|
+
|
|
316
|
+
# Squared Coherence Estimates
|
|
317
|
+
Coht1c1 = np.real(Ct1c1 * np.conj(Ct1c1) / (Et1 * Ec1))
|
|
318
|
+
Coht2c2 = np.real(Ct2c2 * np.conj(Ct2c2) / (Et2 * Ec2))
|
|
319
|
+
|
|
320
|
+
# Cross-spectral Phase Estimates
|
|
321
|
+
Phit1c1 = np.arctan2(np.imag(Ct1c1), np.real(Ct1c1))
|
|
322
|
+
Phit2c2 = np.arctan2(np.imag(Ct2c2), np.real(Ct2c2))
|
|
323
|
+
|
|
324
|
+
# ---Determine tau and L---
|
|
325
|
+
# tau is the thermistor time constant (sec)
|
|
326
|
+
# L is the lag of t behind c due to sensor separation (sec)
|
|
327
|
+
# Matrix of weights based on squared coherence.
|
|
328
|
+
W1 = np.diag(Coht1c1)
|
|
329
|
+
W2 = np.diag(Coht2c2)
|
|
330
|
+
|
|
331
|
+
x1 = optimize.fmin(
|
|
332
|
+
func=helpers.atanfit, x0=[0, 0], args=(f, Phit1c1, W1), disp=False
|
|
333
|
+
)
|
|
334
|
+
x2 = optimize.fmin(
|
|
335
|
+
func=helpers.atanfit, x0=[0, 0], args=(f, Phit2c2, W2), disp=False
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
tau1 = x1[0]
|
|
339
|
+
tau2 = x2[0]
|
|
340
|
+
L1 = x1[1]
|
|
341
|
+
L2 = x2[1]
|
|
342
|
+
|
|
343
|
+
print("1: tau = {:1.4f}s, lag = {:1.4f}s".format(tau1, L1))
|
|
344
|
+
print("2: tau = {:1.4f}s, lag = {:1.4f}s".format(tau2, L2))
|
|
345
|
+
|
|
346
|
+
# ---Apply Phase Correction and LP Filter---
|
|
347
|
+
ii = np.squeeze(np.argwhere(ds.p.data > 1))
|
|
348
|
+
i1 = ii[0]
|
|
349
|
+
i2 = ii[-1]
|
|
350
|
+
n = i2 - i1 + 1
|
|
351
|
+
n = (np.floor(n / N) * N).astype("int64")
|
|
352
|
+
# Truncate to be multiple of N elements long
|
|
353
|
+
i2 = (i1 + n).astype("int64")
|
|
354
|
+
# number of segments = dof/2
|
|
355
|
+
m = (n / N).astype("int64")
|
|
356
|
+
|
|
357
|
+
# Number of degrees of freedom (power of 2)
|
|
358
|
+
dof = 2 * m
|
|
359
|
+
# Frequency resolution at dof degrees of freedom.
|
|
360
|
+
df = 1 / (N * dt)
|
|
361
|
+
|
|
362
|
+
# Transfer function
|
|
363
|
+
f = fft.ifftshift(np.linspace(-N / 2, N / 2 - 1, N) / N / dt)
|
|
364
|
+
H1 = (1 + 1j * 2 * np.pi * f * tau1) * np.exp(1j * 2 * np.pi * f * L1)
|
|
365
|
+
H2 = (1 + 1j * 2 * np.pi * f * tau2) * np.exp(1j * 2 * np.pi * f * L2)
|
|
366
|
+
|
|
367
|
+
# Low Pass Filter
|
|
368
|
+
f0 = 6 # Cutoff frequency
|
|
369
|
+
LP = 1 / (1 + (f / f0) ** 6)
|
|
370
|
+
|
|
371
|
+
# Restructure data with overlapping segments.
|
|
372
|
+
# Staggered segments
|
|
373
|
+
vars = [
|
|
374
|
+
"t1",
|
|
375
|
+
"t2",
|
|
376
|
+
"c1",
|
|
377
|
+
"c2",
|
|
378
|
+
"p",
|
|
379
|
+
"trans",
|
|
380
|
+
"fl",
|
|
381
|
+
"par",
|
|
382
|
+
"alt",
|
|
383
|
+
"oxygen1",
|
|
384
|
+
"oxygen2",
|
|
385
|
+
"ph",
|
|
386
|
+
]
|
|
387
|
+
vard = {}
|
|
388
|
+
for v in vars:
|
|
389
|
+
if v in ds:
|
|
390
|
+
vard[v] = np.zeros((2 * m - 1, N))
|
|
391
|
+
vard[v][: 2 * m - 1 : 2, :] = np.reshape(ds[v].data[i1:i2], newshape=(m, N))
|
|
392
|
+
vard[v][1::2, :] = np.reshape(
|
|
393
|
+
ds[v].data[i1 + int(N / 2) : i2 - int(N / 2)],
|
|
394
|
+
newshape=(m - 1, N),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
time = ds.time[i1:i2]
|
|
398
|
+
lon = ds.lon[i1:i2]
|
|
399
|
+
lat = ds.lat[i1:i2]
|
|
400
|
+
|
|
401
|
+
# FFTs of staggered segments (each row)
|
|
402
|
+
Ad = {}
|
|
403
|
+
for v in vars:
|
|
404
|
+
if v in ds:
|
|
405
|
+
Ad[v] = fft.fft(vard[v])
|
|
406
|
+
|
|
407
|
+
# Corrected Fourier transforms of temperature.
|
|
408
|
+
Ad["t1"] = Ad["t1"] * ((H1 * LP) * np.ones((2 * m - 1, 1)))
|
|
409
|
+
Ad["t2"] = Ad["t2"] * ((H2 * LP) * np.ones((2 * m - 1, 1)))
|
|
410
|
+
|
|
411
|
+
# LP filter remaining variables
|
|
412
|
+
vars2 = [
|
|
413
|
+
"c1",
|
|
414
|
+
"c2",
|
|
415
|
+
"p",
|
|
416
|
+
"trans",
|
|
417
|
+
"fl",
|
|
418
|
+
"par",
|
|
419
|
+
"alt",
|
|
420
|
+
"oxygen1",
|
|
421
|
+
"oxygen2",
|
|
422
|
+
"ph",
|
|
423
|
+
]
|
|
424
|
+
for v in vars2:
|
|
425
|
+
if v in ds:
|
|
426
|
+
Ad[v] = Ad[v] * (LP * np.ones((2 * m - 1, 1)))
|
|
427
|
+
|
|
428
|
+
# Inverse transforms of corrected temperature
|
|
429
|
+
# and low passed other variables
|
|
430
|
+
Adi = {}
|
|
431
|
+
for v in vars:
|
|
432
|
+
if v in ds:
|
|
433
|
+
Adi[v] = np.real(fft.ifft(Ad[v]))
|
|
434
|
+
Adi[v] = np.squeeze(
|
|
435
|
+
np.reshape(Adi[v][:, int(N / 4) : (3 * int(N / 4))], newshape=(1, -1))
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
time = time[int(N / 4) : -int(N / 4)]
|
|
439
|
+
lon = lon[int(N / 4) : -int(N / 4)]
|
|
440
|
+
lat = lat[int(N / 4) : -int(N / 4)]
|
|
441
|
+
|
|
442
|
+
# Generate output structure. Copy attributes over.
|
|
443
|
+
out = xr.Dataset(coords={"time": time})
|
|
444
|
+
out.attrs = ds.attrs
|
|
445
|
+
out["lon"] = lon
|
|
446
|
+
out["lat"] = lat
|
|
447
|
+
for v in vars:
|
|
448
|
+
if v in ds:
|
|
449
|
+
out[v] = xr.DataArray(Adi[v], coords=(out.time,))
|
|
450
|
+
out[v].attrs = ds[v].attrs
|
|
451
|
+
out.assign_attrs(dict(tau1=tau1, tau2=tau2, L1=L1, L2=L2))
|
|
452
|
+
|
|
453
|
+
# ---Recalculate and replot spectra, coherence and phase---
|
|
454
|
+
t1 = Adi["t1"][int(N / 4) : -int(N / 4)] # Now N elements shorter
|
|
455
|
+
t2 = Adi["t2"][int(N / 4) : -int(N / 4)]
|
|
456
|
+
c1 = Adi["c1"][int(N / 4) : -int(N / 4)]
|
|
457
|
+
c2 = Adi["c2"][int(N / 4) : -int(N / 4)]
|
|
458
|
+
# p = Adi["p"][int(N / 4) : -int(N / 4)]
|
|
459
|
+
|
|
460
|
+
m = (i2 - N) / N # number of segments = dof/2
|
|
461
|
+
m = np.floor(m).astype("int64")
|
|
462
|
+
dof = 2 * m # Number of degrees of freedom (power of 2)
|
|
463
|
+
df = 1 / (N * dt) # Frequency resolution at dof degrees of freedom.
|
|
464
|
+
|
|
465
|
+
window = signal.windows.triang(N) * np.ones((m, N))
|
|
466
|
+
At1 = fft.fft(signal.detrend(np.reshape(t1, newshape=(m, N))) * window)
|
|
467
|
+
At2 = fft.fft(signal.detrend(np.reshape(t2, newshape=(m, N))) * window)
|
|
468
|
+
Ac1 = fft.fft(signal.detrend(np.reshape(c1, newshape=(m, N))) * window)
|
|
469
|
+
Ac2 = fft.fft(signal.detrend(np.reshape(c2, newshape=(m, N))) * window)
|
|
470
|
+
|
|
471
|
+
# Positive frequencies only
|
|
472
|
+
At1 = At1[:, 0 : int(N / 2)]
|
|
473
|
+
At2 = At2[:, 0 : int(N / 2)]
|
|
474
|
+
Ac1 = Ac1[:, 0 : int(N / 2)]
|
|
475
|
+
Ac2 = Ac2[:, 0 : int(N / 2)]
|
|
476
|
+
fn = f[0 : int(N / 2)]
|
|
477
|
+
|
|
478
|
+
Et1n = 2 * np.nanmean(np.absolute(At1[:, : int(N / 2)]) ** 2, 0) / df / N**2
|
|
479
|
+
Et2n = 2 * np.nanmean(np.absolute(At2[:, : int(N / 2)]) ** 2, 0) / df / N**2
|
|
480
|
+
Ec1n = 2 * np.nanmean(np.absolute(Ac1[:, : int(N / 2)]) ** 2, 0) / df / N**2
|
|
481
|
+
Ec2n = 2 * np.nanmean(np.absolute(Ac2[:, : int(N / 2)]) ** 2, 0) / df / N**2
|
|
482
|
+
|
|
483
|
+
# Cross Spectral Estimates
|
|
484
|
+
Ct1c1n = 2 * np.nanmean(At1 * np.conj(Ac1) / df / N**2, axis=0)
|
|
485
|
+
Ct2c2n = 2 * np.nanmean(At2 * np.conj(Ac2) / df / N**2, axis=0)
|
|
486
|
+
|
|
487
|
+
# Squared Coherence Estimates
|
|
488
|
+
Coht1c1n = np.real(Ct1c1n * np.conj(Ct1c1n) / (Et1n * Ec1n))
|
|
489
|
+
Coht2c2n = np.real(Ct2c2n * np.conj(Ct2c2n) / (Et2n * Ec2n))
|
|
490
|
+
# 95% confidence bound
|
|
491
|
+
# epsCoht1c1n = np.sqrt(2) * (1 - Coht1c1n) / np.sqrt(Coht1c1n) / np.sqrt(m)
|
|
492
|
+
# epsCoht2c2n = np.sqrt(2) * (1 - Coht2c2n) / np.sqrt(Coht2c2n) / np.sqrt(m)
|
|
493
|
+
# 95% significance level for coherence from Gille notes
|
|
494
|
+
betan = 1 - 0.05 ** (1 / (m - 1))
|
|
495
|
+
|
|
496
|
+
# Cross-spectral Phase Estimates
|
|
497
|
+
Phit1c1n = np.arctan2(np.imag(Ct1c1n), np.real(Ct1c1n))
|
|
498
|
+
Phit2c2n = np.arctan2(np.imag(Ct2c2n), np.real(Ct2c2n))
|
|
499
|
+
# 95% error bound
|
|
500
|
+
# epsPhit1c1n = np.arcsin(
|
|
501
|
+
# stats.t.ppf(0.05, dof) * np.sqrt((1 - Coht1c1n) / (dof * Coht1c1n))
|
|
502
|
+
# )
|
|
503
|
+
# epsPhit1c2n = np.arcsin(
|
|
504
|
+
# stats.t.ppf(0.05, dof) * np.sqrt((1 - Coht2c2n) / (dof * Coht2c2n))
|
|
505
|
+
# )
|
|
506
|
+
|
|
507
|
+
if ds.attrs["plot_spectra"]:
|
|
508
|
+
fig, ax = plt.subplots(
|
|
509
|
+
nrows=2, ncols=2, figsize=(9, 7), constrained_layout=True
|
|
510
|
+
)
|
|
511
|
+
ax0, ax1, ax2, ax3 = ax.flatten()
|
|
512
|
+
|
|
513
|
+
ax0.plot(fold, Et1, label="1 uncorrected", color="0.5")
|
|
514
|
+
ax0.plot(fold, Et2, label="2 uncorrected", color="0.8")
|
|
515
|
+
ax0.plot(fn, Et1n, label="sensor 1")
|
|
516
|
+
ax0.plot(fn, Et2n, label="sensor 2")
|
|
517
|
+
ax0.set(
|
|
518
|
+
yscale="log",
|
|
519
|
+
xscale="log",
|
|
520
|
+
xlabel="frequency [Hz]",
|
|
521
|
+
ylabel=r"spectral density [$^{\circ}$C$^2$/Hz]",
|
|
522
|
+
title="temperature spectra",
|
|
523
|
+
)
|
|
524
|
+
ax0.plot(
|
|
525
|
+
[fn[50], fn[50]],
|
|
526
|
+
[
|
|
527
|
+
dof * Et1n[100] / stats.distributions.chi2.ppf(0.05 / 2, dof),
|
|
528
|
+
dof * Et1n[100] / stats.distributions.chi2.ppf(1 - 0.05 / 2, dof),
|
|
529
|
+
],
|
|
530
|
+
"k",
|
|
531
|
+
)
|
|
532
|
+
ax0.legend()
|
|
533
|
+
|
|
534
|
+
ax1.plot(fold, Ec1, label="1 uncorrected", color="0.5")
|
|
535
|
+
ax1.plot(fold, Ec2, label="2 uncorrected", color="0.8")
|
|
536
|
+
ax1.plot(fn, Ec1n, label="1")
|
|
537
|
+
ax1.plot(fn, Ec2n, label="2")
|
|
538
|
+
ax1.set(
|
|
539
|
+
yscale="log",
|
|
540
|
+
xscale="log",
|
|
541
|
+
xlabel="frequency [Hz]",
|
|
542
|
+
ylabel=r"spectral density [mmho$^2$/cm$^2$/Hz]",
|
|
543
|
+
title="conductivity spectra",
|
|
544
|
+
)
|
|
545
|
+
ax1.plot(
|
|
546
|
+
[fn[50], fn[50]],
|
|
547
|
+
[
|
|
548
|
+
dof * Ec1n[100] / stats.distributions.chi2.ppf(0.05 / 2, dof),
|
|
549
|
+
dof * Ec1n[100] / stats.distributions.chi2.ppf(1 - 0.05 / 2, dof),
|
|
550
|
+
],
|
|
551
|
+
"k",
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Coherence between Temperature and Conductivity
|
|
555
|
+
ax2.plot(fold, Coht1c1, color="0.5")
|
|
556
|
+
ax2.plot(fold, Coht2c2, color="0.8")
|
|
557
|
+
ax2.plot(fn, Coht1c1n)
|
|
558
|
+
ax2.plot(fn, Coht2c2n)
|
|
559
|
+
|
|
560
|
+
# ax.plot(fn, Coht1c1 / (1 + 2 * epsCoht1c1), color="b", linewidth=0.5, alpha=0.2)
|
|
561
|
+
# ax.plot(fn, Coht1c1 / (1 - 2 * epsCoht1c1), color="b", linewidth=0.5, alpha=0.2)
|
|
562
|
+
ax2.plot(fn, betan * np.ones(fn.size), "k--")
|
|
563
|
+
ax2.set(
|
|
564
|
+
xlabel="frequency [Hz]",
|
|
565
|
+
ylabel="squared coherence",
|
|
566
|
+
ylim=(-0.1, 1.1),
|
|
567
|
+
title="t/c coherence",
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Phase between Temperature and Conductivity
|
|
571
|
+
ax3.plot(fold, Phit1c1, color="0.5")
|
|
572
|
+
ax3.plot(fold, Phit2c2, color="0.8")
|
|
573
|
+
ax3.plot(fn, Phit1c1n)
|
|
574
|
+
ax3.plot(fn, Phit2c2n)
|
|
575
|
+
ax3.set(
|
|
576
|
+
xlabel="frequency [Hz]",
|
|
577
|
+
ylabel="phase [rad]",
|
|
578
|
+
ylim=[-4, 4],
|
|
579
|
+
title="t/c phase",
|
|
580
|
+
# xscale="log",
|
|
581
|
+
)
|
|
582
|
+
ax3.plot(
|
|
583
|
+
fold,
|
|
584
|
+
-np.arctan(2 * np.pi * fold * x1[0]) - 2 * np.pi * fold * x1[1],
|
|
585
|
+
"k--",
|
|
586
|
+
)
|
|
587
|
+
ax3.plot(
|
|
588
|
+
fold,
|
|
589
|
+
-np.arctan(2 * np.pi * fold * x2[0]) - 2 * np.pi * fold * x2[1],
|
|
590
|
+
"k--",
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
if ds.attrs["plot_path"] != "":
|
|
594
|
+
plt.savefig(ds.attrs["plot_path"], dpi=200)
|
|
595
|
+
|
|
596
|
+
return out
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def rmloops(ds):
|
|
600
|
+
"""
|
|
601
|
+
Eliminate depth loops in CTD data based on sinking velocity.
|
|
602
|
+
|
|
603
|
+
All data variables are set to NaN within depth loops.
|
|
604
|
+
|
|
605
|
+
Parameters
|
|
606
|
+
----------
|
|
607
|
+
ds : xarray.Dataset
|
|
608
|
+
CTD time series dataset
|
|
609
|
+
|
|
610
|
+
Returns
|
|
611
|
+
-------
|
|
612
|
+
ds : xarray.Dataset
|
|
613
|
+
CTD time series dataset
|
|
614
|
+
"""
|
|
615
|
+
|
|
616
|
+
assert "wthresh" in ds.attrs
|
|
617
|
+
|
|
618
|
+
tsmooth = 0.25 # seconds
|
|
619
|
+
fs = 24 # Hz
|
|
620
|
+
w = calcs.wsink(ds.p.data, tsmooth, fs) # down/up +/-ve
|
|
621
|
+
iloop = np.array([])
|
|
622
|
+
|
|
623
|
+
# up or downcast?
|
|
624
|
+
pol = np.polyfit(np.array(range(ds.p.size)), ds.p, 1)
|
|
625
|
+
dn = 1 if pol[0] > 0 else 0
|
|
626
|
+
|
|
627
|
+
if dn:
|
|
628
|
+
# downcast
|
|
629
|
+
flp = np.squeeze(np.argwhere(w < ds.attrs["wthresh"]))
|
|
630
|
+
if flp.size >= 1:
|
|
631
|
+
ia, ib, ilen = helpers.findsegments(flp)
|
|
632
|
+
if ia[0] == ib[0] == 0:
|
|
633
|
+
ia = np.delete(ia, 0)
|
|
634
|
+
ib = np.delete(ib, 0)
|
|
635
|
+
for start, stop in zip(ia, ib):
|
|
636
|
+
pmi = np.argmax(ds.p[:stop].data)
|
|
637
|
+
pm = ds.p[pmi].data
|
|
638
|
+
tmp = np.squeeze(np.where(ds.p[pmi:] < pm))
|
|
639
|
+
iloop = np.append(iloop, pmi + 1)
|
|
640
|
+
iloop = np.append(iloop, pmi + tmp - 1)
|
|
641
|
+
iloop = np.unique(iloop)
|
|
642
|
+
iloop = iloop.astype("int64")
|
|
643
|
+
else:
|
|
644
|
+
iloop = np.array([])
|
|
645
|
+
else:
|
|
646
|
+
# upcast
|
|
647
|
+
flp = np.squeeze(np.argwhere(w > -ds.attrs["wthresh"]))
|
|
648
|
+
if flp.size >= 1:
|
|
649
|
+
ia, ib, ilen = helpers.findsegments(flp)
|
|
650
|
+
if ia[0] == ib[0] == 0:
|
|
651
|
+
ia = np.delete(ia, 0)
|
|
652
|
+
ib = np.delete(ib, 0)
|
|
653
|
+
for start, stop in zip(ia, ib):
|
|
654
|
+
pmi = np.argmax(ds.p[:stop].data)
|
|
655
|
+
pm = ds.p[pmi].data
|
|
656
|
+
tmp = np.squeeze(np.where(ds.p[:pmi] < pm))
|
|
657
|
+
iloop = np.append(iloop, pmi)
|
|
658
|
+
iloop = np.append(iloop, pmi - tmp)
|
|
659
|
+
iloop = np.unique(iloop)
|
|
660
|
+
iloop = iloop.astype("int64")
|
|
661
|
+
else:
|
|
662
|
+
iloop = np.array([])
|
|
663
|
+
|
|
664
|
+
# get data variable names
|
|
665
|
+
varnames = [k for k, v in ds.data_vars.items()]
|
|
666
|
+
for rmitem in ["lon", "lat", "z", "depth"]:
|
|
667
|
+
if rmitem in varnames:
|
|
668
|
+
varnames.remove(rmitem)
|
|
669
|
+
# set loops to NaN
|
|
670
|
+
for varn in varnames:
|
|
671
|
+
ds[varn][iloop] = np.nan
|
|
672
|
+
|
|
673
|
+
return ds
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def bincast(ds, dz, zmin, zmax):
|
|
677
|
+
"""
|
|
678
|
+
Depth-bin CTD time series.
|
|
679
|
+
|
|
680
|
+
Parameters
|
|
681
|
+
----------
|
|
682
|
+
ds : xr.Dataset
|
|
683
|
+
CTD time series
|
|
684
|
+
dz : float
|
|
685
|
+
Bin size [m]
|
|
686
|
+
zmin : float
|
|
687
|
+
Minimum bin depth center [m]
|
|
688
|
+
zmax : float
|
|
689
|
+
Maximum bin depth center [m]
|
|
690
|
+
|
|
691
|
+
Returns
|
|
692
|
+
-------
|
|
693
|
+
ds : xr.Dataset
|
|
694
|
+
Depth-binned CTD profile
|
|
695
|
+
"""
|
|
696
|
+
dz2 = dz / 2
|
|
697
|
+
zbin = np.arange(zmin - dz2, zmax + dz + dz2, dz)
|
|
698
|
+
zbinlabel = np.arange(zmin, zmax + dz, dz)
|
|
699
|
+
|
|
700
|
+
# prepare dataset
|
|
701
|
+
tmp = ds.swap_dims({"time": "depth"})
|
|
702
|
+
tmp = tmp.reset_coords()
|
|
703
|
+
|
|
704
|
+
# need to bin time separately, not sure why
|
|
705
|
+
btime = tmp.time.groupby_bins(
|
|
706
|
+
"depth", bins=zbin, labels=zbinlabel, right=True, include_lowest=True
|
|
707
|
+
).mean()
|
|
708
|
+
# bin all variables
|
|
709
|
+
out = tmp.groupby_bins(
|
|
710
|
+
"depth", bins=zbin, labels=zbinlabel, right=True, include_lowest=True
|
|
711
|
+
).mean()
|
|
712
|
+
|
|
713
|
+
# organize
|
|
714
|
+
out.coords["time"] = btime
|
|
715
|
+
out = out.set_coords(["lon", "lat"])
|
|
716
|
+
out = out.rename({"depth_bins": "depth"})
|
|
717
|
+
|
|
718
|
+
# copy attributes
|
|
719
|
+
# get data variable names
|
|
720
|
+
varnames = [k for k, v in ds.data_vars.items()]
|
|
721
|
+
for vari in varnames:
|
|
722
|
+
out[vari].attrs = ds[vari].attrs
|
|
723
|
+
out["depth"].attrs = {
|
|
724
|
+
"long_name": "depth",
|
|
725
|
+
"units": "m",
|
|
726
|
+
"standard_name": "depth",
|
|
727
|
+
"positive": "down",
|
|
728
|
+
}
|
|
729
|
+
out.attrs = ds.attrs
|
|
730
|
+
|
|
731
|
+
# recalculate pressure from depth bins
|
|
732
|
+
out["p"] = (("depth",), gsw.p_from_z(-1 * out.depth.data, out.lat.data))
|
|
733
|
+
out.p.attrs = {
|
|
734
|
+
"long_name": "pressure",
|
|
735
|
+
"units": "dbar",
|
|
736
|
+
"standard_name": "sea_water_pressure",
|
|
737
|
+
"positive": "down",
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return out
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def split_updn(ds):
|
|
744
|
+
"""
|
|
745
|
+
Separate into down/up-casts and apply corrections.
|
|
746
|
+
|
|
747
|
+
## TODO: (should this really be here?)
|
|
748
|
+
- tc lag correction
|
|
749
|
+
- thermal mass correction
|
|
750
|
+
- lowpass-filter T, C, oxygen
|
|
751
|
+
|
|
752
|
+
Parameters
|
|
753
|
+
----------
|
|
754
|
+
data : xarray.Dataset
|
|
755
|
+
CTD time series
|
|
756
|
+
|
|
757
|
+
Returns
|
|
758
|
+
-------
|
|
759
|
+
datad : xarray.Dataset
|
|
760
|
+
CTD time series for downcast
|
|
761
|
+
datau : xarray.Dataset
|
|
762
|
+
CTD time series for upcast
|
|
763
|
+
"""
|
|
764
|
+
n = ds.p.size
|
|
765
|
+
ipmax = np.argmax(ds.p.data)
|
|
766
|
+
|
|
767
|
+
datad = ds.isel(time=range(0, ipmax))
|
|
768
|
+
datau = ds.isel(time=range(ipmax, n))
|
|
769
|
+
|
|
770
|
+
return {"down": datad, "up": datau}
|