Skip to content

Commit efcdffe

Browse files
authored
Nortek Dual Profile Dataset Rotation (#414)
Discovered this problem working on PacWave data, where the second dataset, which contains instrument-averaged velocity measurements, errors out when trying to use dolfyn's 'rotate' functionality. This PR ensures the second profile from Nortek instrument can be rotated like that of the first. This is to fix a somewhat hacky fix from several months back, and doesn't require the user to make additional changes to the 2nd dataset beyond that what dolfyn already does.
1 parent aefc1d6 commit efcdffe

10 files changed

Lines changed: 320 additions & 248 deletions

File tree

examples/adcp_example.ipynb

Lines changed: 214 additions & 183 deletions
Large diffs are not rendered by default.
31 Bytes
Binary file not shown.
-806 Bytes
Binary file not shown.
-1.22 KB
Binary file not shown.
16 Bytes
Binary file not shown.

mhkit/dolfyn/adp/clean.py

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ def water_depth_from_amplitude(ds, thresh=10, nfilt=None) -> None:
205205
else:
206206
long_name = "Instrument Depth"
207207

208-
ds["depth"] = xr.DataArray(
208+
ds["depth" + tag[0]] = xr.DataArray(
209209
d.astype("float32"),
210210
dims=["time" + tag[0]],
211211
attrs={"units": "m", "long_name": long_name, "standard_name": "depth"},
@@ -307,7 +307,13 @@ def water_depth_from_pressure(ds, salinity=35) -> None:
307307
else:
308308
long_name = "Instrument Depth"
309309

310-
ds["water_density"] = xr.DataArray(
310+
# Use correct coordinate tag
311+
if "_" in pressure[0]:
312+
tag = "_" + pressure[0].split("_")[-1]
313+
else:
314+
tag = ""
315+
316+
ds["water_density" + tag] = xr.DataArray(
311317
rho.astype("float32"),
312318
dims=[ds[pressure[0]].dims[0]],
313319
attrs={
@@ -317,7 +323,7 @@ def water_depth_from_pressure(ds, salinity=35) -> None:
317323
"description": "Water density from linear approximation of sea water equation of state",
318324
},
319325
)
320-
ds["depth"] = xr.DataArray(
326+
ds["depth" + tag] = xr.DataArray(
321327
d.astype("float32"),
322328
dims=[ds[pressure[0]].dims[0]],
323329
attrs={"units": "m", "long_name": long_name, "standard_name": "depth"},
@@ -368,7 +374,7 @@ def remove_surface_interference(
368374
`distance > range * cos(beam angle) - cell size`
369375
"""
370376

371-
if "depth" not in ds.data_vars:
377+
if ("depth" not in ds.data_vars) and ("depth_avg" not in ds.data_vars):
372378
raise KeyError(
373379
"Depth variable 'depth' does not exist in input dataset."
374380
"Please calculate 'depth' using the function 'water_depth_from_pressure'"
@@ -388,27 +394,23 @@ def remove_surface_interference(
388394
beam_angle = np.deg2rad(beam_angle)
389395

390396
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:
397+
# Fetch cell size (usually 'cell_size' or 'cell_size_avg')
398+
cell_sizes = []
399+
if hasattr(ds, "cell_size"):
400+
cell_sizes.append("cell_size")
401+
if hasattr(ds, "cell_size_avg"):
402+
cell_sizes.append("cell_size_avg")
403+
if not cell_sizes:
405404
raise KeyError(
406405
"'cell_size` not found in dataset attributes. "
407406
"Please supply the ADCP's cell size."
408407
)
409408
else:
410409
cs = [cell_size]
411410

411+
# Depth variable(s)
412+
depths = [cs.replace("cell_size", "depth") for cs in cell_sizes]
413+
412414
if not inplace:
413415
ds = ds.copy(deep=True)
414416

@@ -417,34 +419,50 @@ def remove_surface_interference(
417419

418420
# Apply range_offset if available
419421
range_offset = __check_for_range_offset(ds)
420-
if range_offset:
421-
range_limit = (
422-
(ds["depth"] - range_offset) * np.cos(beam_angle) - ds.attrs[cs]
423-
) + range_offset
424-
else:
425-
range_limit = ds["depth"] * np.cos(beam_angle) - ds.attrs[cs]
426-
427-
# Echosounder data needs only be trimmed at water surface
428-
if "echo" in profile_vars:
429-
mask_echo = ds["range_echo"] > ds["depth"]
430-
ds["echo"].values[..., mask_echo] = val
431-
profile_vars.remove("echo")
432-
433-
# Correct profile measurements for surface interference
434-
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:])
422+
for depth, cs in zip(depths, cell_sizes):
423+
if range_offset:
424+
range_limit = (
425+
(ds[depth] - range_offset) * np.cos(beam_angle) - ds.attrs[cs]
426+
) + range_offset
427+
else:
428+
range_limit = ds[depth] * np.cos(beam_angle) - ds.attrs[cs]
429+
430+
# No good way to do this
431+
if "_avg" not in depth:
432+
# Echosounder data needs only be trimmed at water surface
433+
if "echo" in profile_vars:
434+
mask_echo = ds["range_echo"] > ds["depth"]
435+
ds["echo"].values[..., mask_echo] = val
436+
profile_vars.remove("echo")
437+
438+
# Correct profile measurements for surface interference
439+
for var in profile_vars:
440+
if "avg" in var:
441+
continue
442+
# Use correct coordinate tag
443+
if "_" in var and ("gd" not in var):
444+
tag = "_" + "_".join(var.split("_")[1:])
445+
else:
446+
tag = ""
447+
mask = ds["range" + tag] > range_limit
448+
# Remove values
449+
a = ds[var].values
450+
try: # float dtype
451+
a[..., mask] = val
452+
except: # int dtype
453+
a[..., mask] = 0
454+
ds[var].values = a
438455
else:
439-
tag = ""
440-
mask = ds["range" + tag] > range_limit
441-
# Remove values
442-
a = ds[var].values
443-
try: # float dtype
444-
a[..., mask] = val
445-
except: # int dtype
446-
a[..., mask] = 0
447-
ds[var].values = a
456+
for var in profile_vars:
457+
if "avg" in var:
458+
mask = ds["range_avg"] > range_limit
459+
# Remove values
460+
a = ds[var].values
461+
try: # float dtype
462+
a[..., mask] = val
463+
except: # int dtype
464+
a[..., mask] = 0
465+
ds[var].values = a
448466

449467
if not inplace:
450468
return ds

mhkit/dolfyn/io/nortek2.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,14 @@ def read_signature(
113113
ds = _create_dataset(out)
114114
ds = _set_coords(ds, ref_frame=ds.coord_sys)
115115

116-
if "orientmat" not in ds:
116+
if ("orientmat" not in ds) and ("heading" in ds):
117117
ds["orientmat"] = _euler2orient(
118118
ds["time"], ds["heading"], ds["pitch"], ds["roll"]
119119
)
120+
elif ("orientmat_avg" not in ds) and ("heading_avg" in ds):
121+
ds["orientmat_avg"] = _euler2orient(
122+
ds["time_avg"], ds["heading_avg"], ds["pitch_avg"], ds["roll_avg"]
123+
)
120124

121125
if declin is not None:
122126
set_declination(ds, declin, inplace=True)
@@ -729,13 +733,6 @@ def _reduce(data):
729733
dc["range_avg"] = (np.arange(dv["vel_avg"].shape[1]) + 1) * da[
730734
"cell_size_avg"
731735
] + da["blank_dist_avg"]
732-
# Check to see if orientmat_avg was written (only for instruments with an AHRS)
733-
if ("orientmat" not in dv) and ("orientmat_avg" in dv):
734-
dv["orientmat"] = dv.pop("orientmat_avg")
735-
elif "heading" not in dv: # need these to write orientation matrix
736-
dv["heading"] = dv.pop("heading_avg")
737-
dv["pitch"] = dv.pop("pitch_avg")
738-
dv["roll"] = dv.pop("roll_avg")
739736
tmat = da["filehead_config"]["XFAVG"]
740737
da["duty_cycle_interval"] = dci = da["filehead_config"]["PLAN"]["MIAVG"]
741738
da["duty_cycle_n_burst"] = da["filehead_config"]["AVG"]["NPING"]
@@ -822,10 +819,11 @@ def split_dp_datasets(ds):
822819
ds2.attrs["rotate_vars"] = rotate_vars2
823820
# Set orientation matricies
824821
ds2["beam2inst_orientmat"] = ds["beam2inst_orientmat"]
825-
if ds.attrs["has_imu"]:
826-
ds2 = ds2.rename({"orientmat_avg": "orientmat"})
827-
else:
828-
ds2["orientmat"] = ds["orientmat"]
822+
# IMU versions have 'orientmat_avg' variable, but non-IMU versions do not
823+
if not ds.attrs["has_imu"]:
824+
ds2["orientmat_avg"] = _euler2orient(
825+
ds2["time_avg"], ds2["heading_avg"], ds2["pitch_avg"], ds2["roll_avg"]
826+
)
829827
# Set original coordinate system
830828
cy = ds2.attrs["coord_sys_axes_avg"]
831829
ds2.attrs["coord_sys"] = {"XYZ": "inst", "ENU": "earth", "beam": "beam"}[cy]

mhkit/dolfyn/rotate/api.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,28 @@ def set_declination(ds, declin, inplace=True):
246246
else:
247247
rotate2earth = False
248248

249-
ds["orientmat"].values = np.einsum(
250-
"kj...,ij->ki...",
251-
ds["orientmat"].values,
252-
Rdec,
253-
).astype(np.float32)
249+
# Should only be one of these:
250+
if "orientmat" in ds:
251+
ds["orientmat"].values = np.einsum(
252+
"kj...,ij->ki...",
253+
ds["orientmat"].values,
254+
Rdec,
255+
).astype(np.float32)
256+
elif "orientmat_avg" in ds:
257+
ds["orientmat_avg"].values = np.einsum(
258+
"kj...,ij->ki...",
259+
ds["orientmat_avg"].values,
260+
Rdec,
261+
).astype(np.float32)
262+
254263
if "heading" in ds:
255264
heading = ds["heading"] + angle
256265
heading[heading > 180] -= 360
257266
ds["heading"].values = heading
267+
elif "heading_avg" in ds:
268+
heading = ds["heading_avg"] + angle
269+
heading[heading > 180] -= 360
270+
ds["heading_avg"].values = heading
258271

259272
if rotate2earth:
260273
rotate2(ds, "earth", inplace=True)

mhkit/dolfyn/rotate/signature.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,22 @@ def _inst2earth(adcpo, reverse=False, rotate_vars=None, force=False):
5757

5858
if "orientmat" in adcpo:
5959
omat = adcpo["orientmat"]
60-
else:
60+
elif "orientmat_avg" in adcpo:
61+
omat = adcpo["orientmat_avg"]
62+
elif "time" in adcpo:
6163
omat = _euler2orient(
6264
adcpo["time"],
6365
adcpo["heading"].values,
6466
adcpo["pitch"].values,
6567
adcpo["roll"].values,
6668
)
69+
elif "time_avg" in adcpo:
70+
omat = _euler2orient(
71+
adcpo["time_avg"],
72+
adcpo["heading_avg"].values,
73+
adcpo["pitch_avg"].values,
74+
adcpo["roll_avg"].values,
75+
)
6776

6877
# Take the transpose of the orientation to get the inst->earth rotation
6978
# matrix.

mhkit/tests/dolfyn/test_rotate_adp.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,15 @@ def test_rotate_earth2inst(self):
118118
rotate2(td_sig, "inst", inplace=True)
119119
td_sig_i = load("Sig1000_IMU_rotate_inst2earth.nc")
120120
rotate2(td_sig_i, "inst", inplace=True)
121-
td_sig_avg = load("Sig100_avg")
121+
# Just check that these run without error
122+
td_sig_avg = load("Sig100_avg.nc")
122123
rotate2(td_sig_avg, "inst", inplace=True)
124+
td_sig_dp1_ice = load("Sig500_dp_ice2.nc")
125+
rotate2(td_sig_dp1_ice, "inst", inplace=True)
123126

124127
cd_rdi = load("RDI_test01_rotate_beam2inst.nc")
125128
cd_wr2 = tr.dat_wr2
126-
# ship and inst are considered equivalent in dolfy
129+
# ship and inst are considered equivalent in dolfyn
127130
cd_wr2.attrs["coord_sys"] = "inst"
128131
cd_awac = load("AWAC_test01_earth2inst.nc")
129132
cd_sig = load("BenchFile01_rotate_beam2inst.nc")

0 commit comments

Comments
 (0)