Skip to content

Commit 633a43a

Browse files
authored
Turbulence intensity and instrument noise (#293)
* New TI function, fix noise bug in psd * Update notebook * Fix test file * Fix default
1 parent 61ca58e commit 633a43a

File tree

9 files changed

+377
-211
lines changed

9 files changed

+377
-211
lines changed

examples/adcp_example.ipynb

Lines changed: 209 additions & 170 deletions
Large diffs are not rendered by default.
-86.8 KB
Binary file not shown.
110 KB
Binary file not shown.
3.59 KB
Binary file not shown.

mhkit/dolfyn/adp/turbulence.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,10 @@ def __init__(
111111
n_fft_coh : int
112112
Number of data points to use for coherence and cross-spectra ffts
113113
Default: `n_fft_coh`=`n_fft`
114-
noise : float, list or numpy.ndarray
115-
Instrument's doppler noise in same units as velocity
114+
noise : float or array-like
115+
Instrument noise level in same units as velocity. Typically
116+
found from `adp.turbulence.doppler_noise_level`.
117+
Default: None.
116118
orientation : str, default='up'
117119
Instrument's orientation, either 'up' or 'down'
118120
diff_style : str, default='centered_extended'
@@ -348,9 +350,10 @@ def doppler_noise_level(self, psd, pct_fN=0.8):
348350
N2 = psd.sel(freq=f_range) * psd.freq.sel(freq=f_range)
349351
noise_level = np.sqrt(N2.mean(dim="freq"))
350352

353+
time_coord = psd.dims[0] # no reason this shouldn't be time or time_b5
351354
return xr.DataArray(
352355
noise_level.values.astype("float32"),
353-
dims=["time"],
356+
coords={time_coord: psd.coords[time_coord]},
354357
attrs={
355358
"units": "m s-1",
356359
"long_name": "Doppler Noise Level",
@@ -869,7 +872,7 @@ def check_turbulence_cascade_slope(self, psd, freq_range=[0.2, 0.4]):
869872

870873
return m, b
871874

872-
def dissipation_rate_LT83(self, psd, U_mag, freq_range=[0.2, 0.4]):
875+
def dissipation_rate_LT83(self, psd, U_mag, freq_range=[0.2, 0.4], noise=None):
873876
"""
874877
Calculate the TKE dissipation rate from the velocity spectra.
875878
@@ -882,6 +885,10 @@ def dissipation_rate_LT83(self, psd, U_mag, freq_range=[0.2, 0.4]):
882885
f_range : iterable(2)
883886
The range over which to integrate/average the spectrum, in units
884887
of the psd frequency vector (Hz or rad/s)
888+
noise : float or array-like
889+
Instrument noise level in same units as velocity. Typically
890+
found from `adp.turbulence.doppler_noise_level`.
891+
Default: None.
885892
886893
Returns
887894
-------
@@ -918,6 +925,17 @@ def dissipation_rate_LT83(self, psd, U_mag, freq_range=[0.2, 0.4]):
918925
raise Exception("U_mag should be 1-dimensional (time)")
919926
if not hasattr(freq_range, "__iter__") or len(freq_range) != 2:
920927
raise ValueError("`freq_range` must be an iterable of length 2.")
928+
if noise is not None:
929+
if np.shape(noise)[0] != np.shape(psd)[0]:
930+
raise Exception("Noise should have same first dimension as PSD")
931+
else:
932+
noise = np.array(0)
933+
934+
# Noise subtraction from binner.TimeBinner._psd_base
935+
psd = psd.copy()
936+
if noise is not None:
937+
psd -= noise**2 / (self.fs / 2)
938+
psd = psd.where(psd > 0, np.min(np.abs(psd)) / 100)
921939

922940
freq = psd.freq
923941
idx = np.where((freq_range[0] < freq) & (freq < freq_range[1]))

mhkit/dolfyn/adv/turbulence.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ class ADVBinner(VelBinner):
2424
n_fft_coh : int
2525
Number of data points to use for coherence and cross-spectra fft's.
2626
Optional, default `n_fft_coh` = `n_fft`
27-
noise : float, list or numpy.ndarray
28-
Instrument's doppler noise in same units as velocity
27+
noise : float or array-like
28+
Instrument noise level in same units as velocity. Typically
29+
found from `adv.turbulence.doppler_noise_level`.
30+
Default: None.
2931
"""
3032

3133
def __call__(self, ds, freq_units="rad/s", window="hann"):
@@ -263,7 +265,7 @@ def doppler_noise_level(self, psd, pct_fN=0.8):
263265

264266
return xr.DataArray(
265267
noise_level.values.astype("float32"),
266-
dims=["dir", "time"],
268+
coords={"S": psd["S"], "time": psd["time"]},
267269
attrs={
268270
"units": "m/s",
269271
"long_name": "Doppler Noise Level",
@@ -337,7 +339,7 @@ def check_turbulence_cascade_slope(self, psd, freq_range=[6.28, 12.57]):
337339

338340
return m, b
339341

340-
def dissipation_rate_LT83(self, psd, U_mag, freq_range=[6.28, 12.57]):
342+
def dissipation_rate_LT83(self, psd, U_mag, freq_range=[6.28, 12.57], noise=None):
341343
"""
342344
Calculate the dissipation rate from the PSD
343345
@@ -351,6 +353,10 @@ def dissipation_rate_LT83(self, psd, U_mag, freq_range=[6.28, 12.57]):
351353
The range over which to integrate/average the spectrum, in units
352354
of the psd frequency vector (Hz or rad/s).
353355
Default = [6.28, 12.57] rad/s
356+
noise : float or array-like
357+
Instrument noise level in same units as velocity. Typically
358+
found from `adv.turbulence.calc_doppler_noise`.
359+
Default: None.
354360
355361
Returns
356362
-------
@@ -390,6 +396,18 @@ def dissipation_rate_LT83(self, psd, U_mag, freq_range=[6.28, 12.57]):
390396
if not hasattr(freq_range, "__iter__") or len(freq_range) != 2:
391397
raise ValueError("`freq_range` must be an iterable of length 2.")
392398

399+
if noise is not None:
400+
if np.shape(noise)[0] != 3:
401+
raise Exception("Noise should have same first dimension as velocity")
402+
else:
403+
noise = np.array([0, 0, 0])[:, None, None]
404+
405+
# Noise subtraction from binner.TimeBinner.calc_psd_base
406+
psd = psd.copy()
407+
if noise is not None:
408+
psd -= noise**2 / (self.fs / 2)
409+
psd = psd.where(psd > 0, np.min(np.abs(psd)) / 100)
410+
393411
freq = psd.freq
394412
idx = np.where((freq_range[0] < freq) & (freq < freq_range[1]))
395413
idx = idx[0]

mhkit/dolfyn/binned.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ def _psd_base(
421421

422422
for slc in slice1d_along_axis(dat.shape, -1):
423423
out[slc] = psd_1D(dat[slc], n_fft, fs, window=window, step=step)
424-
if noise != 0:
424+
if np.any(noise):
425425
out -= noise**2 / (fs / 2)
426426
# Make sure all values of the PSD are >0 (but still small):
427427
out[out < 0] = np.min(np.abs(out)) / 100

mhkit/dolfyn/velocity.py

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,60 @@ def autocovariance(self, veldat, n_bin=None):
803803

804804
return da
805805

806+
def turbulence_intensity(self, U_mag, noise=0, thresh=0, detrend=False):
807+
"""
808+
Calculate noise-corrected turbulence intensity.
809+
810+
Parameters
811+
----------
812+
U_mag : xarray.DataArray
813+
Raw horizontal velocity magnitude
814+
noise : numeric
815+
Instrument noise level in same units as velocity. Typically
816+
found from `<adv or adp>.turbulence.doppler_noise_level`.
817+
Default: None.
818+
thresh : numeric
819+
Theshold below which TI will not be calculated
820+
detrend : bool (default: False)
821+
Detrend the velocity data (True), or simply de-mean it
822+
(False), prior to computing TI.
823+
"""
824+
825+
if "xarray" in type(U_mag).__module__:
826+
U = U_mag.values
827+
if "xarray" in type(noise).__module__:
828+
noise = noise.values
829+
830+
if detrend:
831+
up = self.detrend(U)
832+
else:
833+
up = self.demean(U)
834+
835+
# Take RMS and subtract noise
836+
u_rms = np.sqrt(np.nanmean(up**2, axis=-1) - noise**2)
837+
u_mag = self.mean(U)
838+
839+
ti = np.ma.masked_where(u_mag < thresh, u_rms / u_mag)
840+
841+
dims = U_mag.dims
842+
coords = {}
843+
for nm in U_mag.dims:
844+
if "time" in nm:
845+
coords[nm] = self.mean(U_mag[nm].values)
846+
else:
847+
coords[nm] = U_mag[nm].values
848+
849+
return xr.DataArray(
850+
ti.data.astype("float32"),
851+
coords=coords,
852+
dims=dims,
853+
attrs={
854+
"units": "% [0,1]",
855+
"long_name": "Turbulence Intensity",
856+
"comment": f"TI was corrected from a noise level of {noise} m/s",
857+
},
858+
)
859+
806860
def turbulent_kinetic_energy(self, veldat, noise=None, detrend=True):
807861
"""
808862
Calculate the turbulent kinetic energy (TKE) (variances
@@ -814,11 +868,12 @@ def turbulent_kinetic_energy(self, veldat, noise=None, detrend=True):
814868
Velocity data array from ADV or single beam from ADCP.
815869
The last dimension is assumed to be time.
816870
noise : float or array-like
817-
A vector of the noise levels of the velocity data with
818-
the same first dimension as the velocity vector.
871+
Instrument noise level in same units as velocity. Typically
872+
found from `<adv or adp>.turbulence.doppler_noise_level`.
873+
Default: None.
819874
detrend : bool (default: False)
820875
Detrend the velocity data (True), or simply de-mean it
821-
(False), prior to computing tke. Note: the psd routines
876+
(False), prior to computing TKE. Note: the PSD routines
822877
use detrend, so if you want to have the same amount of
823878
variance here as there use ``detrend=True``.
824879
@@ -892,7 +947,7 @@ def power_spectral_density(
892947
freq_units="rad/s",
893948
fs=None,
894949
window="hann",
895-
noise=None,
950+
noise=0,
896951
n_bin=None,
897952
n_fft=None,
898953
n_pad=None,
@@ -913,10 +968,9 @@ def power_spectral_density(
913968
window : string or array
914969
Specify the window function.
915970
Options: 1, None, 'hann', 'hamm'
916-
noise : float or array-like
917-
A vector of the noise levels of the velocity data with
918-
the same first dimension as the velocity vector.
919-
Default = 0.
971+
noise : numeric or array
972+
Instrument noise level in same units as velocity.
973+
Default: 0 (ADCP) or [0, 0, 0] (ADV).
920974
n_bin : int (optional)
921975
The bin-size. Default: from the binner.
922976
n_fft : int (optional)
@@ -938,8 +992,6 @@ def power_spectral_density(
938992
n_fft = self._parse_nfft(n_fft)
939993
if "xarray" in type(veldat).__module__:
940994
vel = veldat.values
941-
if "xarray" in type(noise).__module__:
942-
noise = noise.values
943995
if ("rad" not in freq_units) and ("Hz" not in freq_units):
944996
raise ValueError("`freq_units` should be one of 'Hz' or 'rad/s'")
945997

@@ -964,16 +1016,19 @@ def power_spectral_density(
9641016
).astype("float32")
9651017

9661018
# Spectra, if input is full velocity or a single array
967-
if len(vel.shape) == 2:
968-
if not vel.shape[0] == 3:
969-
raise Exception(
1019+
if len(vel.shape) >= 2:
1020+
if vel.shape[0] != 3:
1021+
raise ValueError(
9701022
"Function can only handle 1D or 3D arrays."
9711023
" If ADCP data, please select a specific depth bin."
9721024
)
973-
if (noise is not None) and (np.shape(noise)[0] != 3):
974-
raise Exception("Noise should have same first dimension as velocity")
1025+
if np.array(noise).any():
1026+
if np.size(noise) != 3:
1027+
raise ValueError("Noise is expected to be an array of 3 scalars")
9751028
else:
1029+
# Reset default to list of 3 zeros
9761030
noise = np.array([0, 0, 0])
1031+
9771032
out = np.empty(
9781033
self._outshape_fft(vel[:3].shape, n_fft=n_fft, n_bin=n_bin),
9791034
dtype=np.float32,
@@ -996,10 +1051,9 @@ def power_spectral_density(
9961051
}
9971052
dims = ["S", "time", "freq"]
9981053
else:
999-
if (noise is not None) and (len(np.shape(noise)) > 1):
1000-
raise Exception("Noise should have same first dimension as velocity")
1001-
else:
1002-
noise = np.array(0)
1054+
if np.array(noise).any() and np.size(noise) > 1:
1055+
raise ValueError("Noise is expected to be a scalar")
1056+
10031057
out = self._psd_base(
10041058
vel,
10051059
fs=fs,

mhkit/tests/dolfyn/test_analysis.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,22 +92,29 @@ def test_adv_turbulence(self):
9292
dat = tv.dat.copy(deep=True)
9393
bnr = avm.ADVBinner(n_bin=20.0, fs=dat.fs)
9494
tdat = bnr(dat)
95-
acov = bnr.autocovariance(dat.vel)
95+
acov = bnr.autocovariance(dat["vel"])
9696

9797
assert_identical(tdat, avm.turbulence_statistics(dat, n_bin=20.0, fs=dat.fs))
9898

99-
tdat["stress_detrend"] = bnr.reynolds_stress(dat.vel)
100-
tdat["stress_demean"] = bnr.reynolds_stress(dat.vel, detrend=False)
99+
tdat["stress_detrend"] = bnr.reynolds_stress(dat["vel"])
100+
tdat["stress_demean"] = bnr.reynolds_stress(dat["vel"], detrend=False)
101101
tdat["csd"] = bnr.cross_spectral_density(
102-
dat.vel, freq_units="rad", window="hamm", n_fft_coh=10
102+
dat["vel"], freq_units="rad", window="hamm", n_fft_coh=10
103103
)
104-
tdat["LT83"] = bnr.dissipation_rate_LT83(tdat.psd, tdat.velds.U_mag)
105-
tdat["SF"] = bnr.dissipation_rate_SF(dat.vel[0], tdat.velds.U_mag)
104+
tdat["LT83"] = bnr.dissipation_rate_LT83(tdat["psd"], tdat.velds.U_mag)
105+
tdat["noise"] = bnr.doppler_noise_level(tdat["psd"], pct_fN=0.8)
106+
tdat["LT83_noise"] = bnr.dissipation_rate_LT83(
107+
tdat["psd"], tdat.velds.U_mag, noise=tdat["noise"]
108+
)
109+
tdat["SF"] = bnr.dissipation_rate_SF(dat["vel"][0], tdat.velds.U_mag)
106110
tdat["TE01"] = bnr.dissipation_rate_TE01(dat, tdat)
107111
tdat["L"] = bnr.integral_length_scales(acov, tdat.velds.U_mag)
108112
slope_check = bnr.check_turbulence_cascade_slope(
109113
tdat["psd"][-1].mean("time"), freq_range=[10, 100]
110114
)
115+
tdat["psd_noise"] = bnr.power_spectral_density(
116+
dat["vel"], freq_units="rad", noise=[0.06, 0.04, 0.01]
117+
)
111118

112119
if make_data:
113120
save(tdat, "vector_data01_bin.nc")
@@ -117,13 +124,22 @@ def test_adv_turbulence(self):
117124
assert_allclose(tdat, load("vector_data01_bin.nc"), atol=1e-6)
118125

119126
def test_adcp_turbulence(self):
120-
dat = tr.dat_sig_i.copy(deep=True)
127+
dat = tr.dat_sig_tide.copy(deep=True)
128+
dat.velds.rotate2("earth")
129+
dat.attrs["principal_heading"] = apm.calc_principal_heading(
130+
dat.vel.mean("range")
131+
)
121132
bnr = apm.ADPBinner(n_bin=20.0, fs=dat.fs, diff_style="centered")
122133
tdat = bnr.bin_average(dat)
123-
tdat["dudz"] = bnr.dudz(tdat.vel)
124-
tdat["dvdz"] = bnr.dvdz(tdat.vel)
125-
tdat["dwdz"] = bnr.dwdz(tdat.vel)
126-
tdat["tau2"] = bnr.shear_squared(tdat.vel)
134+
135+
tdat["dudz"] = bnr.dudz(tdat["vel"])
136+
tdat["dvdz"] = bnr.dvdz(tdat["vel"])
137+
tdat["dwdz"] = bnr.dwdz(tdat["vel"])
138+
tdat["tau2"] = bnr.shear_squared(tdat["vel"])
139+
tdat["I"] = tdat.velds.I
140+
tdat["ti"] = bnr.turbulence_intensity(dat.velds.U_mag, detrend=False)
141+
dat.velds.rotate2("beam")
142+
127143
tdat["psd"] = bnr.power_spectral_density(
128144
dat["vel"].isel(dir=2, range=len(dat.range) // 2), freq_units="Hz"
129145
)
@@ -137,13 +153,22 @@ def test_adcp_turbulence(self):
137153
tdat["tke"] = bnr.total_turbulent_kinetic_energy(
138154
dat, noise=tdat["noise"], orientation="up", beam_angle=25
139155
)
156+
tdat["ti_noise"] = bnr.turbulence_intensity(
157+
dat.velds.U_mag, detrend=False, noise=tdat["noise"]
158+
)
140159
# This is "negative" for this code check
141160
tdat["wpwp"] = bnr.turbulent_kinetic_energy(dat["vel_b5"], noise=tdat["noise"])
142161
tdat["dissipation_rate_LT83"] = bnr.dissipation_rate_LT83(
143162
tdat["psd"],
144163
tdat.velds.U_mag.isel(range=len(dat.range) // 2),
145164
freq_range=[0.2, 0.4],
146165
)
166+
tdat["dissipation_rate_LT83_noise"] = bnr.dissipation_rate_LT83(
167+
tdat["psd"],
168+
tdat.velds.U_mag.isel(range=len(dat.range) // 2),
169+
freq_range=[0.2, 0.4],
170+
noise=tdat["noise"],
171+
)
147172
(
148173
tdat["dissipation_rate_SF"],
149174
tdat["noise_SF"],
@@ -155,10 +180,22 @@ def test_adcp_turbulence(self):
155180
slope_check = bnr.check_turbulence_cascade_slope(
156181
tdat["psd"].mean("time"), freq_range=[0.4, 4]
157182
)
183+
tdat["psd_noise"] = bnr.power_spectral_density(
184+
dat["vel"].isel(dir=2, range=len(dat.range) // 2),
185+
freq_units="Hz",
186+
noise=0.01,
187+
)
158188

159189
if make_data:
160-
save(tdat, "Sig1000_IMU_bin.nc")
190+
save(tdat, "Sig1000_tidal_bin.nc")
161191
return
162192

193+
with pytest.raises(Exception):
194+
bnr.calc_psd(dat["vel"], freq_units="Hz", noise=0.01)
195+
196+
with pytest.raises(Exception):
197+
bnr.calc_psd(dat["vel"][0], freq_units="Hz", noise=0.01)
198+
163199
assert np.round(slope_check[0].values, 4), -1.0682
164-
assert_allclose(tdat, load("Sig1000_IMU_bin.nc"), atol=1e-6)
200+
201+
assert_allclose(tdat, load("Sig1000_tidal_bin.nc"), atol=1e-6)

0 commit comments

Comments
 (0)