ctdproc 0.2.0__tar.gz → 0.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctdproc
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: CTD data processing
5
5
  Author: Gunnar Voet
6
6
  Author-email: Gunnar Voet <gvoet@ucsd.edu>
@@ -22,10 +22,10 @@ Requires-Dist: scipy>=1.13.1
22
22
  Requires-Dist: xarray>=2024.7.0
23
23
  Requires-Dist: xmltodict>=1.0.2
24
24
  Requires-Python: >=3.9
25
- Project-URL: Bug Tracker, https://github.com/gunnarvoet/ctdproc/issues
26
- Project-URL: Changelog, https://github.com/gunnarvoet/ctdproc/blob/main/HISTORY.rst
27
- Project-URL: Documentation, https://ctdproc.readthedocs.io
28
25
  Project-URL: Homepage, https://github.com/gunnarvoet/ctdproc
26
+ Project-URL: Documentation, https://ctdproc.readthedocs.io
27
+ Project-URL: Changelog, https://github.com/gunnarvoet/ctdproc/blob/main/HISTORY.rst
28
+ Project-URL: Bug Tracker, https://github.com/gunnarvoet/ctdproc/issues
29
29
  Description-Content-Type: text/x-rst
30
30
 
31
31
  =======
@@ -0,0 +1,78 @@
1
+ [project]
2
+ name = "ctdproc"
3
+ version = "0.3.0"
4
+ description = "CTD data processing"
5
+ readme = "README.rst"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.9"
9
+ dependencies = [
10
+ "gsw>=3.6.20",
11
+ "matplotlib>=3.9.4",
12
+ "munch>=4.0.0",
13
+ "netcdf4>=1.7.2",
14
+ "numpy>=2.0.2",
15
+ "pandas>=2.3.3",
16
+ "scipy>=1.13.1",
17
+ "xarray>=2024.7.0",
18
+ "xmltodict>=1.0.2",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Intended Audience :: Science/Research",
23
+ "Topic :: Scientific/Engineering :: Physics",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Natural Language :: English",
26
+ "Programming Language :: Python :: 3",
27
+ ]
28
+
29
+ [[project.authors]]
30
+ name = "Gunnar Voet"
31
+ email = "gvoet@ucsd.edu"
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/gunnarvoet/ctdproc"
35
+ Documentation = "https://ctdproc.readthedocs.io"
36
+ Changelog = "https://github.com/gunnarvoet/ctdproc/blob/main/HISTORY.rst"
37
+ "Bug Tracker" = "https://github.com/gunnarvoet/ctdproc/issues"
38
+
39
+ [build-system]
40
+ requires = ["uv_build>=0.8.22,<0.10.0"]
41
+ build-backend = "uv_build"
42
+
43
+ [dependency-groups]
44
+ dev = [
45
+ "pytest>=8.4.2",
46
+ "ruff>=0.14.2",
47
+ { include-group = "docs" },
48
+ ]
49
+ docs = [
50
+ "sphinx>=7.4.7",
51
+ "sphinx-issues>=5.0.1",
52
+ "sphinx-rtd-theme>=3.0.2",
53
+ ]
54
+
55
+ [tool.pytest.ini_options]
56
+ minversion = "6.0"
57
+ addopts = [
58
+ "--strict-markers",
59
+ "--import-mode=importlib",
60
+ ]
61
+ testpaths = ["tests"]
62
+ norecursedirs = [
63
+ ".git",
64
+ ".venv",
65
+ "dist",
66
+ "build",
67
+ "__pycache__",
68
+ ]
69
+ xfail_strict = true
70
+
71
+ [tool.ruff]
72
+ exclude = [".venv"]
73
+
74
+ [[tool.uv.index]]
75
+ name = "testpypi"
76
+ url = "https://test.pypi.org/simple/"
77
+ publish-url = "https://test.pypi.org/legacy/"
78
+ explicit = true
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ctdproc"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "CTD data processing"
5
5
  readme = "README.rst"
6
6
  authors = [ { name = "Gunnar Voet", email = "gvoet@ucsd.edu" }, ]
@@ -41,6 +41,9 @@ build-backend = "uv_build"
41
41
  dev = [
42
42
  "pytest>=8.4.2",
43
43
  "ruff>=0.14.2",
44
+ {include-group = "docs"},
45
+ ]
46
+ docs = [
44
47
  "sphinx>=7.4.7",
45
48
  "sphinx-issues>=5.0.1",
46
49
  "sphinx-rtd-theme>=3.0.2",
@@ -2,9 +2,15 @@
2
2
  # coding: utf-8
3
3
  import gsw
4
4
  import numpy as np
5
- from scipy import signal
6
5
 
7
- from . import helpers
6
+ __all__ = [
7
+ "swcalcs",
8
+ "calc_sal",
9
+ "calc_temp",
10
+ "calc_sigma",
11
+ "calc_depth",
12
+ "calc_allsal",
13
+ ]
8
14
 
9
15
 
10
16
  def swcalcs(data):
@@ -50,6 +56,25 @@ def swcalcs(data):
50
56
 
51
57
 
52
58
  def calc_sal(data):
59
+ """
60
+ Add absolute and practical salinity for both sensor pairs.
61
+
62
+ Adds variables ``SA1``, ``SA2`` (absolute salinity, g/kg) and ``s1``,
63
+ ``s2`` (practical salinity) to the dataset, computed from
64
+ conductivity, temperature, pressure, and the cast position via
65
+ :func:`calc_allsal`.
66
+
67
+ Parameters
68
+ ----------
69
+ data : xarray.Dataset
70
+ CTD time series with ``c1``, ``c2``, ``t1``, ``t2``, ``p``,
71
+ ``lon``, ``lat``.
72
+
73
+ Returns
74
+ -------
75
+ xarray.Dataset
76
+ Input augmented with ``SA1``, ``SA2``, ``s1``, ``s2``.
77
+ """
53
78
  # Salinity
54
79
  SA1, SP1 = calc_allsal(data.c1, data.t1, data.p, data.lon, data.lat)
55
80
  SA2, SP2 = calc_allsal(data.c2, data.t2, data.p, data.lon, data.lat)
@@ -98,13 +123,29 @@ def calc_sal(data):
98
123
 
99
124
 
100
125
  def calc_temp(data):
126
+ """
127
+ Add conservative and potential temperature for both sensor pairs.
128
+
129
+ Adds variables ``CT1``, ``CT2`` (conservative temperature, deg C) and
130
+ ``th1``, ``th2`` (potential temperature, deg C, referenced to 0 dbar)
131
+ to the dataset.
132
+
133
+ Parameters
134
+ ----------
135
+ data : xarray.Dataset
136
+ CTD time series with ``SA1``/``SA2``, ``t1``/``t2``, and ``p``. Run
137
+ :func:`calc_sal` first.
138
+
139
+ Returns
140
+ -------
141
+ xarray.Dataset
142
+ Input augmented with ``CT1``, ``CT2``, ``th1``, ``th2``.
143
+ """
101
144
  # Conservative temperature
102
145
  for si in ["1", "2"]:
103
146
  data["CT{:s}".format(si)] = (
104
147
  ("time",),
105
- gsw.CT_from_t(
106
- data["s{:s}".format(si)], data["t{:s}".format(si)], data.p
107
- ).data,
148
+ gsw.CT_from_t(data[f"SA{si}"], data[f"t{si}"], data.p).data,
108
149
  {
109
150
  "long_name": "conservative temperature",
110
151
  "units": "degree_C",
@@ -133,6 +174,23 @@ def calc_temp(data):
133
174
 
134
175
 
135
176
  def calc_sigma(data):
177
+ """
178
+ Add potential density anomaly for both sensor pairs.
179
+
180
+ Adds variables ``sg1``, ``sg2`` (potential density anomaly, kg/m^3,
181
+ referenced to 0 dbar) to the dataset.
182
+
183
+ Parameters
184
+ ----------
185
+ data : xarray.Dataset
186
+ CTD time series with ``SA1``/``SA2`` and ``CT1``/``CT2``. Run
187
+ :func:`calc_sal` and :func:`calc_temp` first.
188
+
189
+ Returns
190
+ -------
191
+ xarray.Dataset
192
+ Input augmented with ``sg1``, ``sg2``.
193
+ """
136
194
  # Potential density anomaly
137
195
  for si in ["1", "2"]:
138
196
  data["sg{:s}".format(si)] = (
@@ -152,6 +210,21 @@ def calc_sigma(data):
152
210
 
153
211
 
154
212
  def calc_depth(data):
213
+ """
214
+ Add a ``depth`` coordinate (m, positive down) computed from pressure.
215
+
216
+ Uses :func:`gsw.z_from_p` with latitude.
217
+
218
+ Parameters
219
+ ----------
220
+ data : xarray.Dataset
221
+ CTD time series with ``p`` and ``lat``.
222
+
223
+ Returns
224
+ -------
225
+ xarray.Dataset
226
+ Input with a new ``depth`` coordinate.
227
+ """
155
228
  # Depth
156
229
  data.coords["depth"] = (
157
230
  ("time",),
@@ -197,50 +270,3 @@ def calc_allsal(c, t, p, lon, lat):
197
270
  SP = gsw.SP_from_C(10 * c, t, p)
198
271
  SA = gsw.SA_from_SP(SP, p, lon, lat)
199
272
  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
@@ -5,6 +5,17 @@ import numpy as np
5
5
  import pandas as pd
6
6
  from scipy.interpolate import interp1d
7
7
 
8
+ __all__ = [
9
+ "findsegments",
10
+ "inearby",
11
+ "interpbadsegments",
12
+ "glitchcorrect",
13
+ "preen",
14
+ "unique_arrays",
15
+ "mtlb2datetime",
16
+ "datetime2mtlb",
17
+ ]
18
+
8
19
 
9
20
  def unique_arrays(*arrays):
10
21
  """
@@ -125,14 +136,29 @@ def interpbadsegments(x, ibad):
125
136
  if i1 < 0:
126
137
  # start_end_warning("start")
127
138
  pass
128
- elif i3 > x.size:
139
+ elif i3 >= x.size:
129
140
  # start_end_warning("end")
130
141
  pass
142
+ elif np.isnan(x[i1]) or np.isnan(x[i3]):
143
+ # a NaN neighbor, such as a sample removed by despiking, would
144
+ # turn the whole segment into NaN; leave the segment as it is
145
+ pass
131
146
  else:
132
147
  y[i2] = interp1d(np.array([i1, i3]), x[[i1, i3]])(i2)
133
148
  return y
134
149
 
135
150
 
151
+ def _greater(a, b):
152
+ """``a > b`` where ``a`` is finite, False elsewhere.
153
+
154
+ ``np.greater`` with ``where=`` and no ``out=`` leaves the masked entries
155
+ uninitialized, so a comparison against NaN came back True or False at
156
+ random and processing was not repeatable.
157
+ """
158
+ a = np.asarray(a)
159
+ return np.greater(a, b, out=np.zeros(a.shape, dtype=bool), where=np.isfinite(a))
160
+
161
+
136
162
  def glitchcorrect(x, diffx, prodx, ibefore=0, iafter=0):
137
163
  """
138
164
  Remove glitches/spikes in array.
@@ -174,18 +200,8 @@ def glitchcorrect(x, diffx, prodx, ibefore=0, iafter=0):
174
200
  dmul2 = -dx[0:-1] * dx[1:]
175
201
  dmul3 = -dx[0:-2] * dx[2:]
176
202
 
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
- )
203
+ ii2 = np.squeeze(np.argwhere(_greater(dmul2, prodx) & _greater(dmin2, diffx)))
204
+ ii3 = np.squeeze(np.argwhere(_greater(dmul3, prodx) & _greater(dmin3, diffx)))
189
205
 
190
206
  ii2 = unique_arrays(ii2, ii2 + 1)
191
207
  ii3 = unique_arrays(ii3, ii3 + 1, ii3 + 2)
@@ -228,25 +244,6 @@ def preen(x, xmin, xmax):
228
244
  return xp
229
245
 
230
246
 
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
247
  def mtlb2datetime(matlab_datenum, strip_microseconds=False, strip_seconds=False):
251
248
  """
252
249
  Convert Matlab datenum format to python datetime.
@@ -309,6 +306,19 @@ def mtlb2datetime(matlab_datenum, strip_microseconds=False, strip_seconds=False)
309
306
 
310
307
 
311
308
  def datetime2mtlb(dt):
309
+ """
310
+ Convert a numpy datetime64 array to MATLAB datenum.
311
+
312
+ Parameters
313
+ ----------
314
+ dt : array-like of numpy.datetime64
315
+ Input timestamps.
316
+
317
+ Returns
318
+ -------
319
+ numpy.ndarray of float
320
+ MATLAB datenum values (days since the MATLAB epoch).
321
+ """
312
322
  pt = pd.to_datetime(dt)
313
323
  dt = pt.to_pydatetime()
314
324
  mdn = dt + datetime.timedelta(days=366)