Skip to content

Commit 7e91cea

Browse files
jmcvey3akeeste
andauthored
Allow clean functions to handle _avg variables (#377)
Makes the ADCP cleaning functions more robust - updated based on latest reader updates for dual profiling instruments. Solution for Issue #373 --------- Co-authored-by: akeeste <[email protected]>
1 parent 638876d commit 7e91cea

File tree

3 files changed

+101
-38
lines changed

3 files changed

+101
-38
lines changed

mhkit/dolfyn/adp/clean.py

Lines changed: 84 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -157,31 +157,39 @@ def water_depth_from_amplitude(ds, thresh=10, nfilt=None) -> None:
157157
"Please manually remove 'depth' if it needs to be recalculated."
158158
)
159159

160+
# Use "avg" velocty if standard isn't available.
161+
# Should not matter which is used.
162+
tag = []
163+
if hasattr(ds, "vel"):
164+
tag += [""]
165+
if hasattr(ds, "vel_avg"):
166+
tag += ["_avg"]
167+
160168
# This finds the maximum of the echo profile:
161-
inds = np.argmax(ds["amp"].values, axis=1)
169+
inds = np.argmax(ds["amp" + tag[0]].values, axis=1)
162170
# This finds the first point that increases (away from the profiler) in
163171
# the echo profile
164-
edf = np.diff(ds["amp"].values.astype(np.int16), axis=1)
172+
edf = np.diff(ds["amp" + tag[0]].values.astype(np.int16), axis=1)
165173
inds2 = (
166-
np.max(
174+
np.nanmax(
167175
(edf < 0)
168-
* np.arange(ds["vel"].shape[1] - 1, dtype=np.uint8)[None, :, None],
176+
* np.arange(ds["vel" + tag[0]].shape[1] - 1, dtype=np.uint8)[None, :, None],
169177
axis=1,
170178
)
171179
+ 1
172180
)
173181

174182
# Calculate the depth of these quantities
175-
d1 = ds["range"].values[inds]
176-
d2 = ds["range"].values[inds2]
183+
d1 = ds["range" + tag[0]].values[inds]
184+
d2 = ds["range" + tag[0]].values[inds2]
177185
# Combine them:
178186
D = np.vstack((d1, d2))
179187
# Take the median value as the estimate of the surface:
180-
d = np.median(D, axis=0)
188+
d = np.nanmedian(D, axis=0)
181189

182190
# Throw out values that do not increase near the surface by *thresh*
183-
for ip in range(ds["vel"].shape[1]):
184-
itmp = np.min(inds[:, ip])
191+
for ip in range(ds["vel" + tag[0]].shape[1]):
192+
itmp = np.nanmin(inds[:, ip])
185193
if (edf[itmp:, :, ip] < thresh).all():
186194
d[ip] = np.nan
187195

@@ -199,7 +207,7 @@ def water_depth_from_amplitude(ds, thresh=10, nfilt=None) -> None:
199207

200208
ds["depth"] = xr.DataArray(
201209
d.astype("float32"),
202-
dims=["time"],
210+
dims=["time" + tag[0]],
203211
attrs={"units": "m", "long_name": long_name, "standard_name": "depth"},
204212
)
205213

@@ -230,7 +238,7 @@ def water_depth_from_pressure(ds, salinity=35) -> None:
230238
ds : xarray.Dataset
231239
The full adcp dataset
232240
salinity: numeric
233-
Water salinity in psu. Default = 35
241+
Water salinity in PSU. Default = 35
234242
235243
Returns
236244
-------
@@ -259,16 +267,26 @@ def water_depth_from_pressure(ds, salinity=35) -> None:
259267
"The variable 'depth' already exists. "
260268
"Please manually remove 'depth' if it needs to be recalculated."
261269
)
262-
if "pressure" not in ds.data_vars:
270+
pressure = [v for v in ds.data_vars if "pressure" in v]
271+
if not pressure:
263272
raise NameError("The variable 'pressure' does not exist.")
264-
elif not ds["pressure"].sum():
265-
raise ValueError("Pressure data not recorded.")
266-
if "temp" not in ds.data_vars:
273+
else:
274+
for p in pressure:
275+
if not ds[p].sum():
276+
pressure.remove(p)
277+
if not pressure:
278+
raise ValueError("Pressure data not recorded.")
279+
temp = [
280+
v
281+
for v in ds.data_vars
282+
if (("temp" in v) and ("clock" not in v) and ("press" not in v))
283+
]
284+
if not temp:
267285
raise NameError("The variable 'temp' does not exist.")
268286

269287
# Density calcation
270-
P = ds["pressure"].values
271-
T = ds["temp"].values # temperature, degC
288+
P = ds[pressure[0]].values # pressure, dbar
289+
T = ds[temp[0]].values # temperature, degC
272290
S = salinity # practical salinity
273291
rho0 = 1027 # kg/m^3
274292
T0 = 10 # degC
@@ -291,7 +309,7 @@ def water_depth_from_pressure(ds, salinity=35) -> None:
291309

292310
ds["water_density"] = xr.DataArray(
293311
rho.astype("float32"),
294-
dims=["time"],
312+
dims=[ds[pressure[0]].dims[0]],
295313
attrs={
296314
"units": "kg m-3",
297315
"long_name": "Water Density",
@@ -301,7 +319,7 @@ def water_depth_from_pressure(ds, salinity=35) -> None:
301319
)
302320
ds["depth"] = xr.DataArray(
303321
d.astype("float32"),
304-
dims=["time"],
322+
dims=[ds[pressure[0]].dims[0]],
305323
attrs={"units": "m", "long_name": long_name, "standard_name": "depth"},
306324
)
307325

@@ -319,7 +337,7 @@ def nan_beyond_surface(*args, **kwargs):
319337

320338

321339
def remove_surface_interference(
322-
ds, val=np.nan, beam_angle=None, inplace=False
340+
ds, val=np.nan, beam_angle=None, cell_size=None, inplace=False
323341
) -> Optional[xr.Dataset]:
324342
"""
325343
Mask the values of 3D data (vel, amp, corr, echo) that are beyond the surface.
@@ -332,6 +350,8 @@ def remove_surface_interference(
332350
Specifies the value to set the bad values to. Default is `numpy.nan`
333351
beam_angle : int
334352
ADCP beam inclination angle in degrees. Default = dataset.attrs['beam_angle']
353+
cell_size : float
354+
ADCP beam cellsize in meters. Default = dataset.attrs['cell_size']
335355
inplace : bool
336356
When True the existing data object is modified. When False
337357
a copy is returned. Default = False
@@ -352,51 +372,78 @@ def remove_surface_interference(
352372
raise KeyError(
353373
"Depth variable 'depth' does not exist in input dataset."
354374
"Please calculate 'depth' using the function 'water_depth_from_pressure'"
355-
"or 'water_depth_from_amplitude."
375+
"or 'water_depth_from_amplitude, or it can be found from the 'dist_bt'"
376+
"(bottom track) or 'dist_alt' (altimeter) variables, if available."
356377
)
357378

358379
if beam_angle is None:
359380
if hasattr(ds, "beam_angle"):
360381
beam_angle = np.deg2rad(ds.attrs["beam_angle"])
361382
else:
362-
raise Exception(
383+
raise KeyError(
363384
"'beam_angle` not found in dataset attributes. "
364385
"Please supply the ADCP's beam angle."
365386
)
366387
else:
367388
beam_angle = np.deg2rad(beam_angle)
368389

390+
if cell_size is None:
391+
# Fetch cell size
392+
cell_sizes = [
393+
a
394+
for a in ds.attrs
395+
if (
396+
("cell_size" in a)
397+
and ("_bt" not in a)
398+
and ("_alt" not in a)
399+
and ("wave" not in a)
400+
)
401+
]
402+
if cell_sizes:
403+
cs = cell_sizes[0]
404+
else:
405+
raise KeyError(
406+
"'cell_size` not found in dataset attributes. "
407+
"Please supply the ADCP's cell size."
408+
)
409+
else:
410+
cs = [cell_size]
411+
369412
if not inplace:
370413
ds = ds.copy(deep=True)
371414

372415
# Get all variables with 'range' coordinate
373416
profile_vars = [h for h in ds.keys() if any(s for s in ds[h].dims if "range" in s)]
374417

375-
# Surface interference distance
376418
# Apply range_offset if available
377419
range_offset = __check_for_range_offset(ds)
378420
if range_offset:
379421
range_limit = (
380-
(ds["depth"] - range_offset) * np.cos(beam_angle) - ds.attrs["cell_size"]
422+
(ds["depth"] - range_offset) * np.cos(beam_angle) - ds.attrs[cs]
381423
) + range_offset
382424
else:
383-
range_limit = ds["depth"] * np.cos(beam_angle) - ds.attrs["cell_size"]
384-
385-
bds = ds["range"] > range_limit
425+
range_limit = ds["depth"] * np.cos(beam_angle) - ds.attrs[cs]
386426

387427
# Echosounder data needs only be trimmed at water surface
388428
if "echo" in profile_vars:
389429
mask_echo = ds["range_echo"] > ds["depth"]
390430
ds["echo"].values[..., mask_echo] = val
391431
profile_vars.remove("echo")
392432

393-
# Correct rest of "range" data for surface interference
433+
# Correct profile measurements for surface interference
394434
for var in profile_vars:
435+
# Use correct coordinate tag
436+
if "_" in var and ("gd" not in var):
437+
tag = "_" + "_".join(var.split("_")[1:])
438+
else:
439+
tag = ""
440+
mask = ds["range" + tag] > range_limit
441+
# Remove values
395442
a = ds[var].values
396443
try: # float dtype
397-
a[..., bds] = val
444+
a[..., mask] = val
398445
except: # int dtype
399-
a[..., bds] = 0
446+
a[..., mask] = 0
400447
ds[var].values = a
401448

402449
if not inplace:
@@ -434,10 +481,13 @@ def correlation_filter(ds, thresh=50, inplace=False) -> Optional[xr.Dataset]:
434481
ds = ds.copy(deep=True)
435482

436483
# 4 or 5 beam
484+
tag = []
485+
if hasattr(ds, "vel"):
486+
tag += [""]
437487
if hasattr(ds, "vel_b5"):
438-
tag = ["", "_b5"]
439-
else:
440-
tag = [""]
488+
tag += ["_b5"]
489+
if hasattr(ds, "vel_avg"):
490+
tag += ["_avg"]
441491

442492
# copy original ref frame
443493
coord_sys_orig = ds.coord_sys
@@ -456,7 +506,7 @@ def correlation_filter(ds, thresh=50, inplace=False) -> Optional[xr.Dataset]:
456506
ds[var + tg].attrs["Comments"] = (
457507
"Filtered of data with a correlation value below "
458508
+ str(thresh)
459-
+ ds.corr.units
509+
+ ds["corr" + tg].units
460510
)
461511

462512
rotate2(ds, coord_sys_orig, inplace=True)

mhkit/dolfyn/velocity.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,11 @@ def __repr__(
177177
self,
178178
):
179179
time_string = "{:.2f} {} (started: {})"
180-
if "time" not in self or dt642epoch(self["time"][0]) < 1:
180+
time = "time" if "time" in self else "time_avg"
181+
if time not in self or dt642epoch(self[time][0]) < 1:
181182
time_string = "-->No Time Information!<--"
182183
else:
183-
tm = self["time"][[0, -1]].values
184+
tm = self[time][[0, -1]].values
184185
dt = dt642date(tm[0])[0]
185186
delta = (dt642epoch(tm[-1]) - dt642epoch(tm[0])) / (3600 * 24) # days
186187
if delta > 1:
@@ -202,7 +203,7 @@ def __repr__(
202203
time_string = "-->Error in time info<--"
203204

204205
p = self.ds.attrs
205-
t_shape = self["time"].shape
206+
t_shape = self[time].shape
206207
if len(t_shape) > 1:
207208
shape_string = "({} bins, {} pings @ {}Hz)".format(
208209
t_shape[0], t_shape, p.get("fs")

mhkit/tests/dolfyn/test_clean.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def test_clean_upADCP(self):
6262
td_awac = tp.dat_awac.copy(deep=True)
6363
td_sig = tp.dat_sig_tide.copy(deep=True)
6464
td_rdi = tp.dat_rdi.copy(deep=True)
65+
td_dual = tp.dat_sig_dp2.copy(deep=True)
6566

6667
apm.clean.water_depth_from_pressure(td_awac, salinity=30)
6768
apm.clean.remove_surface_interference(td_awac, beam_angle=20, inplace=True)
@@ -71,6 +72,11 @@ def test_clean_upADCP(self):
7172
apm.clean.remove_surface_interference(td_sig, inplace=True)
7273
td_sig = apm.clean.correlation_filter(td_sig, thresh=50)
7374

75+
apm.clean.set_range_offset(td_dual, 0.6)
76+
apm.clean.water_depth_from_pressure(td_dual, salinity=31)
77+
apm.clean.remove_surface_interference(td_dual, inplace=True)
78+
td_dual = apm.clean.correlation_filter(td_dual, thresh=50)
79+
7480
# Depth should already be found for this RDI file, but it's bad
7581
td_rdi["pressure"] /= 10 # set to something reasonable
7682
td_rdi = td_rdi.drop_vars("depth")
@@ -89,6 +95,7 @@ def test_clean_upADCP(self):
8995

9096
def test_clean_downADCP(self):
9197
td = tp.dat_sig_ie.copy(deep=True)
98+
td_dual = tp.dat_sig_dp2.copy(deep=True)
9299

93100
# First remove bad data
94101
td["vel"] = apm.clean.val_exceeds_thresh(td.vel, thresh=3)
@@ -100,7 +107,12 @@ def test_clean_downADCP(self):
100107
# Then clean below seabed
101108
apm.clean.set_range_offset(td, 0.5)
102109
apm.clean.water_depth_from_amplitude(td, thresh=10, nfilt=3)
103-
td = apm.clean.remove_surface_interference(td)
110+
td = apm.clean.remove_surface_interference(td, inplace=False)
111+
112+
# Technically up-facing but a good check
113+
apm.clean.set_range_offset(td_dual, 0.6)
114+
apm.clean.water_depth_from_amplitude(td_dual, thresh=10, nfilt=3)
115+
td_dual = apm.clean.remove_surface_interference(td_dual, inplace=False)
104116

105117
if make_data:
106118
save(td, "Sig500_Echo_clean.nc")

0 commit comments

Comments
 (0)