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 ADDED
@@ -0,0 +1,6 @@
1
+ import importlib.metadata
2
+ from . import calcs, helpers, io, proc
3
+
4
+ __all__ = ["io", "proc", "calcs", "helpers"]
5
+ # version is defined in pyproject.toml
6
+ __version__ = importlib.metadata.version("ctdproc")
ctdproc/calcs.py ADDED
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+ import gsw
4
+ import numpy as np
5
+ from scipy import signal
6
+
7
+ from . import helpers
8
+
9
+
10
+ def swcalcs(data):
11
+ """
12
+ Calculate SA, s, CT, theta, sigma, oxygen, depth, z
13
+
14
+ Using gsw package. Variable names for practical salinity
15
+ and potential temperature are kept consistent with the
16
+ Matlab processing package.
17
+
18
+ Salinity is calculated both as absolute salinity (SA)
19
+ and practical salinity (s). Note: Per TEOS-10 instructions,
20
+ the variable to be archived is practical salinity.
21
+
22
+ Temperature is converted to conservative temperature (CT)
23
+ and potential temperature (th). Potential temperature
24
+ is referenced to 0 dbar.
25
+
26
+ Potential density anomaly is referenced to 0 dbar pressure.
27
+ The anomaly is calculated as potential density minus 1000 kg/m^3.
28
+
29
+ Parameters
30
+ ----------
31
+ data : array-like
32
+ CTD time series data structure.
33
+
34
+ Returns
35
+ -------
36
+ data : array-like
37
+ CTD time series data structure.
38
+ """
39
+ # Most derived variables are now calculated in proc.ctd_cleanup2()
40
+ # Not sure why they are calculated two times in the Matlab package.
41
+
42
+ # data = calc_sal(data)
43
+ # data = calc_temp(data)
44
+ # data = calc_sigma(data)
45
+ data = calc_depth(data)
46
+
47
+ # TODO: oxygen
48
+
49
+ return data
50
+
51
+
52
+ def calc_sal(data):
53
+ # Salinity
54
+ SA1, SP1 = calc_allsal(data.c1, data.t1, data.p, data.lon, data.lat)
55
+ SA2, SP2 = calc_allsal(data.c2, data.t2, data.p, data.lon, data.lat)
56
+
57
+ # Absolute salinity
58
+ data["SA1"] = (
59
+ ("time",),
60
+ SA1.data,
61
+ {
62
+ "long_name": "absolute salinity",
63
+ "units": "g kg-1",
64
+ "standard_name": "sea_water_absolute_salinity",
65
+ },
66
+ )
67
+ data["SA2"] = (
68
+ ("time",),
69
+ SA2.data,
70
+ {
71
+ "long_name": "absolute salinity",
72
+ "units": "g kg-1",
73
+ "standard_name": "sea_water_absolute_salinity",
74
+ },
75
+ )
76
+
77
+ # Practical salinity
78
+ data["s1"] = (
79
+ ("time",),
80
+ SP1.data,
81
+ {
82
+ "long_name": "practical salinity",
83
+ "units": "",
84
+ "standard_name": "sea_water_practical_salinity",
85
+ },
86
+ )
87
+ data["s2"] = (
88
+ ("time",),
89
+ SP2.data,
90
+ {
91
+ "long_name": "practical salinity",
92
+ "units": "",
93
+ "standard_name": "sea_water_practical_salinity",
94
+ },
95
+ )
96
+
97
+ return data
98
+
99
+
100
+ def calc_temp(data):
101
+ # Conservative temperature
102
+ for si in ["1", "2"]:
103
+ data["CT{:s}".format(si)] = (
104
+ ("time",),
105
+ gsw.CT_from_t(
106
+ data["s{:s}".format(si)], data["t{:s}".format(si)], data.p
107
+ ).data,
108
+ {
109
+ "long_name": "conservative temperature",
110
+ "units": "degree_C",
111
+ "standard_name": "sea_water_conservative_temperature",
112
+ },
113
+ )
114
+
115
+ # Potential temperature
116
+ for si in ["1", "2"]:
117
+ data["th{:s}".format(si)] = (
118
+ ("time",),
119
+ gsw.pt_from_t(
120
+ data["SA{:s}".format(si)],
121
+ data["t{:s}".format(si)],
122
+ p=data.p,
123
+ p_ref=0,
124
+ ).data,
125
+ {
126
+ "long_name": "potential temperature",
127
+ "units": "degree_C",
128
+ "standard_name": "sea_water_potential_temperature",
129
+ },
130
+ )
131
+
132
+ return data
133
+
134
+
135
+ def calc_sigma(data):
136
+ # Potential density anomaly
137
+ for si in ["1", "2"]:
138
+ data["sg{:s}".format(si)] = (
139
+ ("time",),
140
+ gsw.sigma0(
141
+ data["SA{:s}".format(si)],
142
+ data["CT{:s}".format(si)],
143
+ ).data,
144
+ {
145
+ "long_name": "potential density anomaly",
146
+ "units": "kg m-3",
147
+ "standard_name": "sea_water_sigma_theta",
148
+ "reference_pressure": "0 dbar",
149
+ },
150
+ )
151
+ return data
152
+
153
+
154
+ def calc_depth(data):
155
+ # Depth
156
+ data.coords["depth"] = (
157
+ ("time",),
158
+ -1 * gsw.z_from_p(data.p, data.lat).data,
159
+ {
160
+ "long_name": "depth",
161
+ "units": "m",
162
+ "standard_name": "depth",
163
+ "positive": "down",
164
+ },
165
+ )
166
+ return data
167
+
168
+
169
+ def calc_allsal(c, t, p, lon, lat):
170
+ """
171
+ Calculate absolute and practical salinity.
172
+
173
+ Wrapper for gsw functions. Converts conductivity
174
+ from S/m to mS/cm if output salinity is less than 5.
175
+
176
+ Parameters
177
+ ----------
178
+ c : array-like
179
+ Conductivity. See notes on units above.
180
+ t : array-like
181
+ In-situ temperature (ITS-90), degrees C
182
+ p : array-like
183
+ Sea pressure, dbar
184
+ lon : array-like
185
+ Longitude, -360 to 360 degrees
186
+ lat : array-like
187
+ Latitude, -90 to 90 degrees
188
+ Returns
189
+ -------
190
+ SA : array-like, g/kg
191
+ Absolute Salinity
192
+ SP : array-like
193
+ Practical Salinity
194
+ """
195
+ SP = gsw.SP_from_C(c, t, p)
196
+ if np.nanmean(SP) < 5:
197
+ SP = gsw.SP_from_C(10 * c, t, p)
198
+ SA = gsw.SA_from_SP(SP, p, lon, lat)
199
+ return SA, SP
200
+
201
+
202
+ def wsink(p, Ts, Fs):
203
+ """
204
+ Compute sinking velocity from pressure record.
205
+
206
+ Computes the sinking (or rising) velocity from the pressure signal p
207
+ by first differencing. The pressure signal is smoothed with a low-pass
208
+ filter for differentiation. If the input signal is shorter than the
209
+ smoothing time scale, w is taken as the slope of the linear regression of p.
210
+
211
+ Adapted from wsink.m - Fabian Wolk, Rockland Oceanographic Services Inc.
212
+
213
+ Parameters
214
+ ----------
215
+ p : array-like
216
+ Pressure [dbar]
217
+ Ts : float
218
+ Smoothing time scale [s]
219
+ Fs : float
220
+ Sampling frequency [Hz]
221
+
222
+ Returns
223
+ -------
224
+ w : array-like
225
+ Sinking velocity [dbar/s]
226
+ """
227
+ FORDER = 1
228
+ # low pass filter coefficients
229
+ [b, a] = signal.butter(FORDER, 1 / Ts * 2 / Fs)
230
+ N = p.size
231
+ if N <= Fs * Ts * FORDER:
232
+ pol = np.polyfit(np.array(range(N)), p, 1)
233
+ w = pol[0] * Fs * np.ones(N)
234
+ else:
235
+ # pad the pressure vector left and right
236
+ nPad = int(FORDER * Ts * Fs)
237
+ if nPad > N:
238
+ print(
239
+ "warning: length of pressure vector is smaller than padding length.\n",
240
+ "Filter transients may occur.",
241
+ )
242
+ p = helpers.pad_lr(p, nPad)
243
+ w = np.gradient(Fs * signal.filtfilt(b, a, p))
244
+ w = w[nPad:-nPad]
245
+
246
+ return w
ctdproc/helpers.py ADDED
@@ -0,0 +1,325 @@
1
+ import datetime
2
+ import warnings
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from scipy.interpolate import interp1d
7
+
8
+
9
+ def unique_arrays(*arrays):
10
+ """
11
+ Find unique elements in more than one numpy array.
12
+
13
+ Parameters
14
+ ----------
15
+ arrays : A tuple of np.array.ndarray
16
+
17
+ Returns
18
+ -------
19
+ unique : np.array
20
+ numpy array with unique elements from the input arrays
21
+ """
22
+ return np.unique(np.hstack(arrays))
23
+
24
+
25
+ def findsegments(ibad):
26
+ """
27
+ Find contiguous segments in an array of indices.
28
+
29
+ Parameters
30
+ ----------
31
+ ibad : np.array
32
+ Array with indices
33
+
34
+ Returns
35
+ -------
36
+ istart : np.array
37
+ Segment start indices
38
+ istop : np.array
39
+ Segment stop indices
40
+ seglength : np.array
41
+ Segment length
42
+ """
43
+ dibad = np.diff(ibad)
44
+ jj = np.argwhere(dibad > 1)
45
+
46
+ istart = jj + 1
47
+ istart = np.insert(istart, 0, 0)
48
+ istart = ibad[istart]
49
+
50
+ istop = jj
51
+ istop = np.hstack([np.squeeze(istop), ibad.size - 1])
52
+ istop = ibad[istop]
53
+
54
+ seglength = istop - istart + 1
55
+
56
+ return istart, istop, seglength
57
+
58
+
59
+ def inearby(ibad, inearm, inearp, n):
60
+ """
61
+ Find indices left and right of given indices and add them to
62
+ the index array.
63
+
64
+ Differs from inearby.m as it breaks up ibad into segments and
65
+ then finds nearby indices for each segment.
66
+
67
+ Parameters
68
+ ----------
69
+ ibad : np.array
70
+ Array with indices
71
+ inearm : int
72
+ Number of elements to include on left side
73
+ inearp : int
74
+ Number of elements to include on right side
75
+ n : int
76
+ Maximum index in output array and size of array
77
+ being indexed
78
+
79
+ Returns
80
+ -------
81
+ k : np.array
82
+ New array of indices
83
+ """
84
+ if ibad.size == 0:
85
+ k = np.array([]).astype("int64")
86
+ else:
87
+ istart, istop, seglength = findsegments(ibad)
88
+ new_ind = np.array([]).astype("int64")
89
+ for ia, ib in zip(istart, istop):
90
+ new_ind = np.append(new_ind, np.arange(ia - inearm, ib + inearp + 1))
91
+ k = np.unique(new_ind)
92
+ k = k[((k >= 0) & (k < n))]
93
+ return k
94
+
95
+
96
+ def interpbadsegments(x, ibad):
97
+ """
98
+ Interpolate over segments of bad data.
99
+
100
+ Parameters
101
+ ----------
102
+ x : np.array
103
+ Input data
104
+ ibad : np.array
105
+ Indices of bad data
106
+
107
+ Returns
108
+ -------
109
+ y : np.array
110
+ Interpolated array
111
+ """
112
+
113
+ def start_end_warning(loc):
114
+ warnings.warn(
115
+ message=f"no interpolation at {loc}",
116
+ category=RuntimeWarning,
117
+ )
118
+
119
+ istart, istop, seglen = findsegments(ibad)
120
+ y = x.copy()
121
+ for iia, iis, iilen in zip(istart, istop, seglen):
122
+ i1 = iia - 1
123
+ i2 = range(iia, iis + 1)
124
+ i3 = iis + 1
125
+ if i1 < 0:
126
+ # start_end_warning("start")
127
+ pass
128
+ elif i3 > x.size:
129
+ # start_end_warning("end")
130
+ pass
131
+ else:
132
+ y[i2] = interp1d(np.array([i1, i3]), x[[i1, i3]])(i2)
133
+ return y
134
+
135
+
136
+ def glitchcorrect(x, diffx, prodx, ibefore=0, iafter=0):
137
+ """
138
+ Remove glitches/spikes in array.
139
+
140
+ Adapted from tms_tc_glitchcorrect.m
141
+
142
+ Parameters
143
+ ----------
144
+ x : np.array
145
+ Input array
146
+ diffx : int
147
+ Threshold for differences
148
+ prodx : int
149
+ Threshold for products
150
+ ibefore : int, optional
151
+ Number of elements to interpolate on left side of glitch
152
+ after : int, optional
153
+ Number of elements to interpolate on right side of glitch
154
+
155
+ Returns
156
+ -------
157
+ y : np.array
158
+ Interpolated array
159
+ """
160
+ dx = np.diff(x)
161
+ nx = len(dx)
162
+ y = x.copy()
163
+
164
+ with warnings.catch_warnings():
165
+ # Prevent warning due to nans present in nanmin being printed
166
+ warnings.simplefilter("ignore")
167
+ dmin2 = np.nanmin(
168
+ np.vstack([np.absolute(dx[0:-1]), np.absolute(dx[1:])]), axis=0
169
+ )
170
+ dmin3 = np.nanmin(
171
+ np.vstack([np.absolute(dx[0:-2]), np.absolute(dx[2:])]), axis=0
172
+ )
173
+
174
+ dmul2 = -dx[0:-1] * dx[1:]
175
+ dmul3 = -dx[0:-2] * dx[2:]
176
+
177
+ ii2 = np.squeeze(
178
+ np.argwhere(
179
+ np.greater(dmul2, prodx, where=np.isfinite(dmul2))
180
+ & np.greater(dmin2, diffx, where=np.isfinite(dmin2))
181
+ )
182
+ )
183
+ ii3 = np.squeeze(
184
+ np.argwhere(
185
+ np.greater(dmul3, prodx, where=np.isfinite(dmul3))
186
+ & np.greater(dmin3, diffx, where=np.isfinite(dmin3))
187
+ )
188
+ )
189
+
190
+ ii2 = unique_arrays(ii2, ii2 + 1)
191
+ ii3 = unique_arrays(ii3, ii3 + 1, ii3 + 2)
192
+
193
+ jj2 = inearby(ii2, ibefore, iafter, nx)
194
+ jj3 = inearby(ii3, ibefore, iafter, nx)
195
+
196
+ jj = unique_arrays(jj2, jj3)
197
+
198
+ if jj.size > 0:
199
+ y = interpbadsegments(x, jj)
200
+
201
+ return y
202
+
203
+
204
+ def preen(x, xmin, xmax):
205
+ """
206
+ Eliminate values outside given range and interpolate.
207
+
208
+ Parameters
209
+ ----------
210
+ x : np.array
211
+ Input array
212
+ xmin : float
213
+ Lower limit
214
+ xmax : float
215
+ Upper limit
216
+
217
+ Returns
218
+ -------
219
+ xp : np.array
220
+ Cleaned array
221
+ """
222
+ indexall = np.array(range(0, x.size))
223
+ ii = np.squeeze(np.where(((x < xmin) | (x > xmax) | (np.imag(x) != 0))))
224
+ indexclean = np.delete(indexall, ii)
225
+ x = np.delete(x, ii)
226
+ fint = interp1d(indexclean, x, bounds_error=False, fill_value=np.nan)
227
+ xp = fint(indexall)
228
+ return xp
229
+
230
+
231
+ def atanfit(x, f, Phi, W):
232
+ f = np.arctan(2 * np.pi * f * x[0]) + 2 * np.pi * f * x[1] + Phi
233
+ f = np.matmul(np.matmul(f.transpose(), W ** 4), f)
234
+ return f
235
+
236
+
237
+ def pad_lr(p, nPad):
238
+ """Pad array left and right"""
239
+ p0 = p[0]
240
+ p = p - p0
241
+ p = p0 + np.insert(p, 0, -p[nPad - 1 :: -1])
242
+
243
+ p0 = p[-1]
244
+ p = p - p0
245
+ p = p0 + np.insert(p, -1, -p[: -nPad - 1 : -1])
246
+
247
+ return p
248
+
249
+
250
+ def mtlb2datetime(matlab_datenum, strip_microseconds=False, strip_seconds=False):
251
+ """
252
+ Convert Matlab datenum format to python datetime.
253
+ This version also works for vector input and strips
254
+ milliseconds if desired.
255
+
256
+ Parameters
257
+ ----------
258
+ matlab_datenum : float or np.array
259
+ Matlab time vector.
260
+ strip_microseconds : bool
261
+ Get rid of microseconds (optional)
262
+ strip_seconds : bool
263
+ Get rid of seconds (optional)
264
+
265
+ Returns
266
+ -------
267
+ t : np.datetime64
268
+ Time in numpy's datetime64 format.
269
+ """
270
+
271
+ if np.size(matlab_datenum) == 1:
272
+ day = datetime.datetime.fromordinal(int(matlab_datenum))
273
+ dayfrac = datetime.timedelta(days=matlab_datenum % 1) - datetime.timedelta(
274
+ days=366
275
+ )
276
+ t1 = day + dayfrac
277
+ if strip_microseconds and strip_seconds:
278
+ t1 = datetime.datetime.replace(t1, microsecond=0, second=0)
279
+ elif strip_microseconds:
280
+ t1 = datetime.datetime.replace(t1, microsecond=0)
281
+
282
+ else:
283
+ t1 = np.ones_like(matlab_datenum) * np.nan
284
+ t1 = t1.tolist()
285
+ nonan = np.isfinite(matlab_datenum)
286
+ md = matlab_datenum[nonan]
287
+ day = [datetime.datetime.fromordinal(int(tval)) for tval in md]
288
+ dayfrac = [
289
+ datetime.timedelta(days=tval % 1) - datetime.timedelta(days=366)
290
+ for tval in md
291
+ ]
292
+ tt = [day1 + dayfrac1 for day1, dayfrac1 in zip(day, dayfrac)]
293
+ if strip_microseconds and strip_seconds:
294
+ tt = [
295
+ datetime.datetime.replace(tval, microsecond=0, second=0) for tval in tt
296
+ ]
297
+ elif strip_microseconds:
298
+ tt = [datetime.datetime.replace(tval, microsecond=0) for tval in tt]
299
+ tt = [np.datetime64(ti, "ns") for ti in tt]
300
+ xi = np.where(nonan)[0]
301
+ for i, ii in enumerate(xi):
302
+ t1[ii] = tt[i]
303
+ xi = np.where(~nonan)[0]
304
+ for i in xi:
305
+ t1[i] = np.datetime64("nat")
306
+ t1 = np.array(t1)
307
+
308
+ return t1
309
+
310
+
311
+ def datetime2mtlb(dt):
312
+ pt = pd.to_datetime(dt)
313
+ dt = pt.to_pydatetime()
314
+ mdn = dt + datetime.timedelta(days=366)
315
+ frac_seconds = [
316
+ (dti - datetime.datetime(dti.year, dti.month, dti.day, 0, 0, 0)).seconds
317
+ / (24.0 * 60.0 * 60.0)
318
+ for dti in dt
319
+ ]
320
+ frac_microseconds = [
321
+ dti.microsecond / (24.0 * 60.0 * 60.0 * 1000000.0) for dti in dt
322
+ ]
323
+ out = np.array([mdni.toordinal() for mdni in mdn])
324
+ out = out.astype(float) + frac_seconds + frac_microseconds
325
+ return out