Skip to content

Commit 4cdbe03

Browse files
jmcvey3ssolson
authored andcommitted
Improve Reynolds stress ADCP estimation notebook discussion V2 (#326)
* tke updates * Fix shear velocity functions * More detail for tke shear production * Don't rotate heading beyond 360 degrees * Fix some typos in notebook * Rename deprecated function
1 parent 24674f1 commit 4cdbe03

File tree

10 files changed

+750
-691
lines changed

10 files changed

+750
-691
lines changed

examples/adcp_example.ipynb

Lines changed: 584 additions & 516 deletions
Large diffs are not rendered by default.

examples/adv_example.ipynb

Lines changed: 99 additions & 79 deletions
Large diffs are not rendered by default.
-4.63 KB
Binary file not shown.
0 Bytes
Binary file not shown.
-31 Bytes
Binary file not shown.

mhkit/dolfyn/adp/turbulence.py

Lines changed: 33 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def __init__(
128128
self.diff_style = diff_style
129129
self.orientation = orientation
130130

131-
def _diff_func(self, vel, u):
131+
def _diff_func(self, vel, u, orientation):
132132
"""Applies the chosen style of numerical differentiation to velocity data.
133133
134134
This method calculates the derivative of the velocity data 'vel' with respect to the 'range'
@@ -149,15 +149,23 @@ def _diff_func(self, vel, u):
149149
The calculated derivative of the velocity data.
150150
"""
151151

152+
if not orientation:
153+
orientation = self.orientation
154+
sign = 1
155+
if orientation == "down":
156+
sign *= -1
157+
152158
if self.diff_style == "first":
153159
out = _diffz_first(vel[u].values, vel["range"].values)
154-
return out, vel.range[1:]
160+
return sign * out, vel.range[1:]
161+
155162
elif self.diff_style == "centered":
156163
out = _diffz_centered(vel[u].values, vel["range"].values)
157-
return out, vel.range[1:-1]
164+
return sign * out, vel.range[1:-1]
165+
158166
elif self.diff_style == "centered_extended":
159167
out = _diffz_centered_extended(vel[u].values, vel["range"].values)
160-
return out, vel.range
168+
return sign * out, vel.range
161169

162170
def dudz(self, vel, orientation=None):
163171
"""
@@ -182,28 +190,24 @@ def dudz(self, vel, orientation=None):
182190
'true vertical' direction.
183191
"""
184192

185-
if not orientation:
186-
orientation = self.orientation
187-
sign = 1
188-
if orientation == "down":
189-
sign *= -1
190-
191-
dudz, rng = sign * self._diff_func(vel, 0)
193+
dudz, rng = self._diff_func(vel, 0, orientation)
192194
return xr.DataArray(
193195
dudz,
194196
coords=[rng, vel.time],
195197
dims=["range", "time"],
196198
attrs={"units": "s-1", "long_name": "Shear in X-direction"},
197199
)
198200

199-
def dvdz(self, vel):
201+
def dvdz(self, vel, orientation=None):
200202
"""
201203
The shear in the second velocity component.
202204
203205
Parameters
204206
----------
205207
vel : xarray.DataArray
206208
ADCP raw velocity
209+
orientation : str, default=ADPBinner.orientation
210+
Direction ADCP is facing ('up' or 'down')
207211
208212
Returns
209213
-------
@@ -217,22 +221,24 @@ def dvdz(self, vel):
217221
'true vertical' direction.
218222
"""
219223

220-
dvdz, rng = self._diff_func(vel, 1)
224+
dvdz, rng = self._diff_func(vel, 1, orientation)
221225
return xr.DataArray(
222226
dvdz,
223227
coords=[rng, vel.time],
224228
dims=["range", "time"],
225229
attrs={"units": "s-1", "long_name": "Shear in Y-direction"},
226230
)
227231

228-
def dwdz(self, vel):
232+
def dwdz(self, vel, orientation=None):
229233
"""
230234
The shear in the third velocity component.
231235
232236
Parameters
233237
----------
234238
vel : xarray.DataArray
235239
ADCP raw velocity
240+
orientation : str, default=ADPBinner.orientation
241+
Direction ADCP is facing ('up' or 'down')
236242
237243
Returns
238244
-------
@@ -246,7 +252,7 @@ def dwdz(self, vel):
246252
'true vertical' direction.
247253
"""
248254

249-
dwdz, rng = self._diff_func(vel, 2)
255+
dwdz, rng = self._diff_func(vel, 2, orientation)
250256
return xr.DataArray(
251257
dwdz,
252258
coords=[rng, vel.time],
@@ -537,7 +543,7 @@ def _beam_variance(self, ds, time, noise, beam_order, n_beams):
537543
)
538544

539545
# Calculate along-beam velocity prime squared bar
540-
bp2_ = np.empty((n_beams, len(ds.range), len(time))) * np.nan
546+
bp2_ = np.empty((n_beams, len(ds["range"]), len(time))) * np.nan
541547
for i, beam in enumerate(beam_order):
542548
bp2_[i] = np.nanvar(self.reshape(beam_vel[beam]), axis=-1)
543549

@@ -602,7 +608,7 @@ def reynolds_stress_4beam(self, ds, noise=None, orientation=None, beam_angle=Non
602608
np.stack([upwp_ * np.nan, upwp_, vpwp_]).astype("float32"),
603609
coords={
604610
"tau": ["upvp_", "upwp_", "vpwp_"],
605-
"range": ds.range,
611+
"range": ds["range"],
606612
"time": time,
607613
},
608614
attrs={"units": "m2 s-2", "long_name": "Specific Reynolds Stress Vector"},
@@ -646,7 +652,7 @@ def stress_tensor_5beam(
646652
in pitch and roll. u'v'_ cannot be directly calculated by a 5-beam ADCP,
647653
so it is approximated by the covariance of `u` and `v`. The uncertainty
648654
introduced by using this approximation is small if deviations from pitch
649-
and roll are small (< 10 degrees).
655+
and roll are small (<= 5 degrees).
650656
651657
Dewey, R., and S. Stringer. "Reynolds stresses and turbulent kinetic
652658
energy estimates from various ADCP beam configurations: Theory." J. of
@@ -663,7 +669,7 @@ def stress_tensor_5beam(
663669

664670
# Run through warnings
665671
b_angle, noise = self._stress_func_warnings(
666-
ds, beam_angle, noise, tilt_thresh=10
672+
ds, beam_angle, noise, tilt_thresh=5
667673
)
668674

669675
# Fetch beam order
@@ -713,7 +719,7 @@ def stress_tensor_5beam(
713719
np.stack([upup_, vpvp_, wpwp_]).astype("float32"),
714720
coords={
715721
"tke": ["upup_", "vpvp_", "wpwp_"],
716-
"range": ds.range,
722+
"range": ds["range"],
717723
"time": time,
718724
},
719725
attrs={
@@ -752,7 +758,7 @@ def stress_tensor_5beam(
752758
np.stack([upvp_, upwp_, vpwp_]).astype("float32"),
753759
coords={
754760
"tau": ["upvp_", "upwp_", "vpwp_"],
755-
"range": ds.range,
761+
"range": ds["range"],
756762
"time": time,
757763
},
758764
attrs={
@@ -763,49 +769,6 @@ def stress_tensor_5beam(
763769

764770
return tke_vec, stress_vec
765771

766-
def total_turbulent_kinetic_energy(
767-
self, ds, noise=None, orientation=None, beam_angle=None
768-
):
769-
"""
770-
Calculate magnitude of turbulent kinetic energy from 5-beam ADCP.
771-
772-
Parameters
773-
----------
774-
ds : xarray.Dataset
775-
Raw dataset in beam coordinates
776-
noise : int or xarray.DataArray, default=0 (time)
777-
Doppler noise level in units of m/s
778-
orientation : str, default=ds.attrs['orientation']
779-
Direction ADCP is facing ('up' or 'down')
780-
beam_angle : int, default=ds.attrs['beam_angle']
781-
ADCP beam angle in units of degrees
782-
783-
Returns
784-
-------
785-
tke : xarray.DataArray
786-
Turbulent kinetic energy magnitude
787-
788-
Notes
789-
-----
790-
This function is a wrapper around 'calc_stress_5beam' that then
791-
combines the TKE components.
792-
793-
Warning: the integral length scale of turbulence captured by the
794-
ADCP measurements (i.e. the size of turbulent structures) increases
795-
with increasing range from the instrument.
796-
"""
797-
798-
tke_vec = self.stress_tensor_5beam(
799-
ds, noise, orientation, beam_angle, tke_only=True
800-
)
801-
802-
tke = tke_vec.sum("tke") / 2
803-
tke.attrs["units"] = "m2 s-2"
804-
tke.attrs["long_name"] = ("TKE Magnitude",)
805-
tke.attrs["standard_name"] = "specific_turbulent_kinetic_energy_of_sea_water"
806-
807-
return tke.astype("float32")
808-
809772
def check_turbulence_cascade_slope(self, psd, freq_range=[0.2, 0.4]):
810773
"""
811774
This function calculates the slope of the PSD, the power spectra
@@ -1027,11 +990,11 @@ def dissipation_rate_SF(self, vel_raw, r_range=[1, 5]):
1027990
)
1028991

1029992
if "range_b5" in vel_raw.dims:
1030-
rng = vel_raw.range_b5
1031-
time = self.mean(vel_raw.time_b5.values)
993+
rng = vel_raw["range_b5"]
994+
time = self.mean(vel_raw["time_b5"].values)
1032995
else:
1033-
rng = vel_raw.range
1034-
time = self.mean(vel_raw.time.values)
996+
rng = vel_raw["range"]
997+
time = self.mean(vel_raw["time"].values)
1035998

1036999
# bm shape is [range, ensemble time, 'data within ensemble']
10371000
bm = self.demean(vel_raw.values) # take out the ensemble mean
@@ -1139,7 +1102,7 @@ def friction_velocity(self, ds_avg, upwp_, z_inds=slice(1, 5), H=None):
11391102
raise TypeError("`z_inds` must be an instance of `slice(int,int)`.")
11401103

11411104
if not H:
1142-
H = ds_avg.depth.values
1105+
H = ds_avg["depth"].values
11431106
z = ds_avg["range"].values
11441107
upwp_ = upwp_.values
11451108

@@ -1151,6 +1114,6 @@ def friction_velocity(self, ds_avg, upwp_, z_inds=slice(1, 5), H=None):
11511114

11521115
return xr.DataArray(
11531116
u_star.astype("float32"),
1154-
coords={"time": ds_avg.time},
1117+
coords={"time": ds_avg["time"]},
11551118
attrs={"units": "m s-1", "long_name": "Friction Velocity"},
11561119
)

mhkit/dolfyn/adv/motion.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import xarray as xr
33
import warnings
44
import scipy.signal as ss
5-
from scipy.integrate import cumtrapz
5+
from scipy.integrate import cumulative_trapezoid
66

77
from ..rotate import vector as rot
88
from ..rotate.api import _make_model, rotate2
@@ -188,7 +188,7 @@ def calc_velacc(
188188
dat = np.concatenate(
189189
(
190190
np.zeros(list(hp.shape[:-1]) + [1]),
191-
cumtrapz(hp, dx=1 / samp_freq, axis=-1),
191+
cumulative_trapezoid(hp, dx=1 / samp_freq, axis=-1),
192192
),
193193
axis=-1,
194194
)

mhkit/dolfyn/adv/turbulence.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def __call__(self, ds, freq_units="rad/s", window="hann"):
5454
def reynolds_stress(self, veldat, detrend=True):
5555
"""
5656
Calculate the specific Reynolds stresses
57-
(cross-covariances of u,v,w in m^2/s^2)
57+
(covariances of u,v,w in m^2/s^2)
5858
5959
Parameters
6060
----------
@@ -76,7 +76,7 @@ def reynolds_stress(self, veldat, detrend=True):
7676
if not isinstance(veldat, xr.DataArray):
7777
raise TypeError("`veldat` must be an instance of `xarray.DataArray`.")
7878

79-
time = self.mean(veldat.time.values)
79+
time = self.mean(veldat["time"].values)
8080
vel = veldat.values
8181

8282
out = np.empty(self._outshape(vel[:3].shape)[:-1], dtype=np.float32)
@@ -144,7 +144,7 @@ def cross_spectral_density(
144144

145145
fs_in = self._parse_fs(fs)
146146
n_fft = self._parse_nfft_coh(n_fft_coh)
147-
time = self.mean(veldat.time.values)
147+
time = self.mean(veldat["time"].values)
148148
veldat = veldat.values
149149
if len(np.shape(veldat)) != 2:
150150
raise Exception(
@@ -391,7 +391,7 @@ def dissipation_rate_LT83(self, psd, U_mag, freq_range=[6.28, 12.57], noise=None
391391
raise TypeError("`psd` must be an instance of `xarray.DataArray`.")
392392
if len(U_mag.shape) != 1:
393393
raise Exception("U_mag should be 1-dimensional (time)")
394-
if len(psd.time) != len(U_mag.time):
394+
if len(psd["time"]) != len(U_mag["time"]):
395395
raise Exception("`U_mag` should be from ensembled-averaged dataset")
396396
if not hasattr(freq_range, "__iter__") or len(freq_range) != 2:
397397
raise ValueError("`freq_range` must be an iterable of length 2.")
@@ -459,7 +459,7 @@ def dissipation_rate_SF(self, vel_raw, U_mag, fs=None, freq_range=[2.0, 4.0]):
459459

460460
if not isinstance(vel_raw, xr.DataArray):
461461
raise TypeError("`vel_raw` must be an instance of `xarray.DataArray`.")
462-
if len(vel_raw.time) == len(U_mag.time):
462+
if len(vel_raw["time"]) == len(U_mag["time"]):
463463
raise Exception("`U_mag` should be from ensembled-averaged dataset")
464464
if not hasattr(freq_range, "__iter__") or len(freq_range) != 2:
465465
raise ValueError("`freq_range` must be an iterable of length 2.")
@@ -586,8 +586,8 @@ def dissipation_rate_TE01(self, dat_raw, dat_avg, freq_range=[6.28, 12.57]):
586586

587587
# Index data to be used
588588
inds = (freq_range[0] < freq) & (freq < freq_range[1])
589-
psd = dat_avg.psd[..., inds].values
590-
freq = freq[inds].reshape([1] * (dat_avg.psd.ndim - 2) + [sum(inds)])
589+
psd = dat_avg["psd"][..., inds].values
590+
freq = freq[inds].reshape([1] * (dat_avg["psd"].ndim - 2) + [sum(inds)])
591591

592592
# Estimate values
593593
# u & v components (equation 6)
@@ -606,7 +606,7 @@ def dissipation_rate_TE01(self, dat_raw, dat_avg, freq_range=[6.28, 12.57]):
606606

607607
return xr.DataArray(
608608
out.astype("float32"),
609-
coords={"time": dat_avg.psd.time},
609+
coords={"time": dat_avg["psd"]["time"]},
610610
dims="time",
611611
attrs={
612612
"units": "m2 s-3",
@@ -645,7 +645,7 @@ def integral_length_scales(self, a_cov, U_mag, fs=None):
645645

646646
if not isinstance(a_cov, xr.DataArray):
647647
raise TypeError("`a_cov` must be an instance of `xarray.DataArray`.")
648-
if len(a_cov.time) != len(U_mag.time):
648+
if len(a_cov["time"]) != len(U_mag["time"]):
649649
raise Exception("`U_mag` should be from ensembled-averaged dataset")
650650

651651
acov = a_cov.values
@@ -656,7 +656,7 @@ def integral_length_scales(self, a_cov, U_mag, fs=None):
656656

657657
return xr.DataArray(
658658
L_int.astype("float32"),
659-
coords={"dir": a_cov.dir, "time": a_cov.time},
659+
coords={"dir": a_cov["dir"], "time": a_cov["time"]},
660660
attrs={
661661
"units": "m",
662662
"long_name": "Integral Length Scale",

mhkit/dolfyn/rotate/api.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,10 @@ def set_declination(ds, declin, inplace=True):
252252
Rdec,
253253
)
254254
if "heading" in ds:
255-
ds["heading"] += angle
255+
heading = ds["heading"] + angle
256+
heading[heading > 180] -= 360
257+
ds["heading"].values = heading
258+
256259
if rotate2earth:
257260
rotate2(ds, "earth", inplace=True)
258261
if "principal_heading" in ds.attrs:

0 commit comments

Comments
 (0)