Skip to content

Commit 82facb0

Browse files
committed
Merge branch 'develop' of https://github.com/MHKiT-Software/MHKiT-Python into D3D_updates
2 parents 861d9d1 + 6f806c8 commit 82facb0

7 files changed

Lines changed: 76 additions & 66 deletions

File tree

mhkit/acoustics/analysis.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@
5050
from mhkit.dolfyn.time import epoch2dt64, dt642epoch
5151

5252

53+
def _check_numeric(value, name: str):
54+
if np.issubdtype(type(value), np.ndarray):
55+
value = value.item()
56+
if not (
57+
isinstance(value, (int, float))
58+
or np.issubdtype(type(value), np.integer)
59+
or np.issubdtype(type(value), np.floating)
60+
):
61+
raise TypeError(f"{name} must be a numeric type (int or float).")
62+
63+
5364
def _fmax_warning(
5465
fn: Union[int, float, np.ndarray], fmax: Union[int, float, np.ndarray]
5566
) -> Union[int, float, np.ndarray]:
@@ -69,11 +80,6 @@ def _fmax_warning(
6980
The adjusted maximum frequency limit, ensuring it does not exceed the Nyquist frequency.
7081
"""
7182

72-
if not isinstance(fn, (int, float, np.ndarray)):
73-
raise TypeError("'fn' must be a numeric type (int or float).")
74-
if not isinstance(fmax, (int, float, np.ndarray)):
75-
raise TypeError("'fmax' must be a numeric type (int or float).")
76-
7783
if fmax > fn:
7884
warnings.warn(
7985
f"`fmax` = {fmax} is greater than the Nyquist frequency. Setting"
@@ -120,10 +126,8 @@ def minimum_frequency(
120126
if not np.issubdtype(water_depth.dtype, np.number):
121127
raise TypeError("'water_depth' must be a numeric type or array of numerics.")
122128

123-
if not isinstance(c, (int, float)):
124-
raise TypeError("'c' must be a numeric type (int or float).")
125-
if not isinstance(c_seabed, (int, float)):
126-
raise TypeError("'c_seabed' must be a numeric type (int or float).")
129+
_check_numeric(c, "c")
130+
_check_numeric(c_seabed, "c_seabed")
127131

128132
if np.any(water_depth <= 0):
129133
raise ValueError("All elements of 'water_depth' must be positive numbers.")
@@ -143,6 +147,7 @@ def sound_pressure_spectral_density(
143147
pressure: xr.DataArray,
144148
fs: Union[int, float],
145149
bin_length: Union[int, float] = 1,
150+
fft_length: Optional[Union[int, float]] = None,
146151
rms: bool = True,
147152
) -> xr.DataArray:
148153
"""
@@ -167,6 +172,8 @@ def sound_pressure_spectral_density(
167172
Data collection sampling rate [Hz]
168173
bin_length: int or float
169174
Length of time in seconds to create FFTs. Default: 1.
175+
fft_length: int or float, optional
176+
Length of FFT to use. If None, uses bin_length * fs. Default: None.
170177
rms: bool
171178
If True, calculates the mean-squared SPSD. Set to False to
172179
calculate standard SPSD. Default: True.
@@ -180,20 +187,23 @@ def sound_pressure_spectral_density(
180187
# Type checks
181188
if not isinstance(pressure, xr.DataArray):
182189
raise TypeError("'pressure' must be an xarray.DataArray.")
183-
if not isinstance(fs, (int, float)):
184-
raise TypeError("'fs' must be a numeric type (int or float).")
185-
if not isinstance(bin_length, (int, float)):
186-
raise TypeError("'bin_length' must be a numeric type (int or float).")
190+
_check_numeric(fs, "fs")
191+
_check_numeric(bin_length, "bin_length")
187192

188193
# Ensure that 'pressure' has a 'time' coordinate
189194
if "time" not in pressure.dims:
190195
raise ValueError("'pressure' must be indexed by 'time' dimension.")
191196

192197
# window length of each time series
193198
nbin = bin_length * fs
199+
if fft_length is not None:
200+
_check_numeric(fft_length, "fft_length")
201+
nfft = fft_length
202+
else:
203+
nfft = nbin
194204

195205
# Use dolfyn PSD functionality
196-
binner = VelBinner(n_bin=nbin, fs=fs, n_fft=nbin)
206+
binner = VelBinner(n_bin=nbin, fs=fs, n_fft=nfft)
197207
# Always 50% overlap if numbers reshape perfectly
198208
# Mean square sound pressure
199209
psd = binner.power_spectral_density(pressure, freq_units="Hz")
@@ -221,9 +231,9 @@ def sound_pressure_spectral_density(
221231
"units": pressure.units + "^2/Hz",
222232
"long_name": long_name,
223233
"fs": fs,
224-
"nbin": str(bin_length) + " s",
234+
"bin_length": bin_length,
225235
"overlap": "50%",
226-
"nfft": nbin,
236+
"n_fft": nfft,
227237
},
228238
)
229239

@@ -234,6 +244,7 @@ def apply_calibration(
234244
spsd: xr.DataArray,
235245
sensitivity_curve: xr.DataArray,
236246
fill_value: Union[float, int, np.ndarray],
247+
interp_method: str = "linear",
237248
) -> xr.DataArray:
238249
"""
239250
Applies custom calibration to spectral density values.
@@ -248,6 +259,9 @@ def apply_calibration(
248259
fill_value: float or int
249260
Value with which to fill missing values from the calibration curve,
250261
in units of dB rel 1 V^2/uPa^2.
262+
interp_method: str
263+
Interpolation method to use when interpolating the calibration curve
264+
to the frequencies in 'spsd'. Default is 'linear'.
251265
252266
Returns
253267
-------
@@ -259,8 +273,7 @@ def apply_calibration(
259273
raise TypeError("'spsd' must be an xarray.DataArray.")
260274
if not isinstance(sensitivity_curve, xr.DataArray):
261275
raise TypeError("'sensitivity_curve' must be an xarray.DataArray.")
262-
if not isinstance(fill_value, (int, float, np.ndarray)):
263-
raise TypeError("'fill_value' must be a numeric type (int or float).")
276+
_check_numeric(fill_value, "fill_value")
264277

265278
# Ensure 'freq' dimension exists in 'spsd'
266279
if "freq" not in spsd.dims:
@@ -295,7 +308,7 @@ def apply_calibration(
295308
freq = sensitivity_curve.dims[0]
296309
# Interpolate calibration curve to desired value
297310
calibration = sensitivity_curve.interp(
298-
{freq: spsd_calibrated["freq"]}, method="linear"
311+
{freq: spsd_calibrated["freq"]}, method=interp_method
299312
)
300313
# Fill missing with provided value
301314
calibration = calibration.fillna(fill_value)
@@ -340,6 +353,7 @@ def sound_pressure_spectral_density_level(spsd: xr.DataArray) -> xr.DataArray:
340353
attrs={
341354
"units": "dB re 1 uPa^2/Hz",
342355
"long_name": "Sound Pressure Spectral Density Level",
356+
"time_resolution": spsd.attrs["bin_length"],
343357
},
344358
)
345359

@@ -571,8 +585,8 @@ def band_aggregate(
571585
for val in octave:
572586
if not isinstance(val, int) or (val <= 0):
573587
raise TypeError("'octave' must contain positive integers.")
574-
if not isinstance(fmin, int) or (fmin <= 0):
575-
raise TypeError("'fmin' must be a positive integer.")
588+
_check_numeric(fmin, "fmin")
589+
_check_numeric(fmax, "fmax")
576590
if fmax <= fmin: # also checks that fmax is positive
577591
raise ValueError("'fmax' must be greater than 'fmin'.")
578592

mhkit/acoustics/graphics.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import xarray as xr
2525
import matplotlib.pyplot as plt
2626

27-
from .analysis import _fmax_warning
27+
from .analysis import _fmax_warning, _check_numeric
2828

2929

3030
def plot_spectrogram(
@@ -61,8 +61,9 @@ def plot_spectrogram(
6161
Figure plot axis
6262
"""
6363

64-
if not isinstance(fmin, (int, float)) or not isinstance(fmax, (int, float)):
65-
raise TypeError("fmin and fmax must be numeric types.")
64+
# Type checks
65+
_check_numeric(fmin, "fmin")
66+
_check_numeric(fmax, "fmax")
6667
if fmin >= fmax:
6768
raise ValueError("fmin must be less than fmax.")
6869

@@ -128,8 +129,8 @@ def plot_spectra(
128129
Figure plot axis
129130
"""
130131

131-
if not isinstance(fmin, (int, float)) or not isinstance(fmax, (int, float)):
132-
raise TypeError("fmin and fmax must be numeric types.")
132+
_check_numeric(fmin, "fmin")
133+
_check_numeric(fmax, "fmax")
133134
if fmin >= fmax:
134135
raise ValueError("fmin must be less than fmax.")
135136

mhkit/acoustics/io.py

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
import xarray as xr
5050
from scipy.io import wavfile
5151

52+
from mhkit.acoustics.analysis import _check_numeric
53+
5254

5355
def _read_wav_metadata(f: BinaryIO) -> dict:
5456
"""
@@ -132,10 +134,9 @@ def _calculate_voltage_and_time(
132134
raise TypeError("Raw audio data 'raw' must be a numpy.ndarray.")
133135
if not isinstance(bits_per_sample, int):
134136
raise TypeError("'bits_per_sample' must be an integer.")
135-
if not isinstance(peak_voltage, (int, float)):
136-
raise TypeError("'peak_voltage' must be numeric (int or float).")
137137
if not isinstance(start_time, (str, np.datetime64)):
138138
raise TypeError("'start_time' must be a string or np.datetime64.")
139+
_check_numeric(peak_voltage, "peak_voltage")
139140

140141
length = raw.shape[0] // fs # length of recording in seconds
141142

@@ -156,7 +157,7 @@ def _calculate_voltage_and_time(
156157
raw_voltage = raw.astype(float) / max_count * peak_voltage
157158

158159
# Get time
159-
end_time = np.datetime64(start_time) + np.timedelta64(length * 1000, "ms")
160+
end_time = np.datetime64(start_time) + np.timedelta64(length * 1000000000, "ns")
160161
time = pd.date_range(start_time, end_time, raw.size + 1)
161162

162163
return raw_voltage, time, max_count
@@ -196,15 +197,12 @@ def read_hydrophone(
196197

197198
if not isinstance(filename, (str, Path)):
198199
raise TypeError("Filename must be a string or a pathlib.Path object.")
199-
if not isinstance(peak_voltage, (int, float)):
200-
raise TypeError("'peak_voltage' must be numeric (int or float).")
201-
if sensitivity is not None and not isinstance(sensitivity, (int, float)):
202-
raise TypeError("'sensitivity' must be numeric (int, float) or None.")
203-
if not isinstance(gain, (int, float)):
204-
raise TypeError("'gain' must be numeric (int or float).")
200+
if sensitivity is not None:
201+
_check_numeric(sensitivity, "sensitivity")
202+
_check_numeric(peak_voltage, "peak_voltage")
203+
_check_numeric(gain, "gain")
205204
if not isinstance(start_time, (str, np.datetime64)):
206205
raise TypeError("'start_time' must be a string or np.datetime64")
207-
208206
if (sensitivity is not None) and (sensitivity > 0):
209207
raise ValueError(
210208
"Hydrophone calibrated sensitivity should be entered as a negative number."
@@ -225,9 +223,9 @@ def read_hydrophone(
225223
# If sensitivity is provided, convert to sound pressure
226224
if sensitivity is not None:
227225
# Subtract gain
228-
# Hydrophone with sensitivity of -177 dB and gain of -3 dB = sensitivity of -174 dB
226+
# Hydrophone with sensitivity of -177 dB and gain of 3 dB = sensitivity of -174 dB
229227
if gain:
230-
sensitivity -= gain
228+
sensitivity += gain
231229
# Convert calibration from dB rel 1 V/uPa into ratio
232230
sensitivity = 10 ** (sensitivity / 20) # V/uPa
233231

@@ -511,25 +509,22 @@ def export_audio(
511509
-------
512510
None
513511
"""
512+
514513
if not isinstance(filename, str):
515514
raise TypeError("'filename' must be a string.")
516-
517515
if not isinstance(pressure, xr.DataArray):
518516
raise TypeError("'pressure' must be an xarray.DataArray.")
519-
520517
if not hasattr(pressure, "values") or not isinstance(pressure.values, np.ndarray):
521518
raise TypeError("'pressure.values' must be a numpy.ndarray.")
522-
523-
if not hasattr(pressure, "sensitivity") or not isinstance(
524-
pressure.sensitivity, (int, float)
525-
):
526-
raise TypeError("'pressure.sensitivity' must be a numeric type (int or float).")
527-
528-
if not hasattr(pressure, "fs") or not isinstance(pressure.fs, (int, float)):
529-
raise TypeError("'pressure.fs' must be a numeric type (int or float).")
530-
531-
if not isinstance(gain, (int, float)):
532-
raise TypeError("'gain' must be a numeric type (int or float).")
519+
if hasattr(pressure, "sensitivity"):
520+
_check_numeric(pressure.sensitivity, "pressure.sensitivity")
521+
else:
522+
raise AttributeError("'pressure' must have a 'sensitivity' attribute.")
523+
if hasattr(pressure, "fs"):
524+
_check_numeric(pressure.fs, "pressure.fs")
525+
else:
526+
raise AttributeError("'pressure' must have a 'fs' attribute.")
527+
_check_numeric(gain, "gain")
533528

534529
# Convert from Pascals to UPa
535530
upa = pressure.values.T * 1e6

mhkit/acoustics/sel.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,7 @@ def sound_exposure_level(
134134
exposure = np.trapezoid(band * w, band["freq"])
135135

136136
# Sound exposure level (L_{E,p}) = (L_{p,rms} + 10log10(t))
137-
sel = 10 * np.log10(exposure / reference) + 10 * np.log10(
138-
spsd.attrs["nfft"] / spsd.attrs["fs"] # n_points / (n_points/s)
139-
)
137+
sel = 10 * np.log10(exposure / reference) + 10 * np.log10(spsd.attrs["bin_length"])
140138

141139
out = xr.DataArray(
142140
sel.astype(np.float32),
@@ -145,7 +143,7 @@ def sound_exposure_level(
145143
"units": "dB re 1 uPa^2 s",
146144
"long_name": long_name,
147145
"weighting_group": group,
148-
"integration_time": spsd.attrs["nbin"],
146+
"integration_time": spsd.attrs["bin_length"],
149147
"freq_band_min": fmin,
150148
"freq_band_max": fmax,
151149
},

mhkit/acoustics/spl.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import numpy as np
2020
import xarray as xr
2121

22-
from .analysis import _fmax_warning, _create_frequency_bands
22+
from .analysis import _check_numeric, _fmax_warning, _create_frequency_bands
2323

2424

2525
def _argument_check(spsd, fmin, fmax):
@@ -45,10 +45,8 @@ def _argument_check(spsd, fmin, fmax):
4545
# Type checks
4646
if not isinstance(spsd, xr.DataArray):
4747
raise TypeError("'spsd' must be an xarray.DataArray.")
48-
if not isinstance(fmin, int):
49-
raise TypeError("'fmin' must be an integer.")
50-
if not isinstance(fmax, int):
51-
raise TypeError("'fmax' must be an integer.")
48+
_check_numeric(fmin, "fmin")
49+
_check_numeric(fmax, "fmax")
5250

5351
# Ensure 'freq' and 'time' dimensions are present
5452
if ("freq" not in spsd.dims) or ("time" not in spsd.dims):
@@ -59,9 +57,9 @@ def _argument_check(spsd, fmin, fmax):
5957
raise ValueError(
6058
"'spsd' must have 'fs' (sampling frequency) in its attributes."
6159
)
62-
if "nfft" not in spsd.attrs:
60+
if "n_fft" not in spsd.attrs:
6361
raise ValueError(
64-
"'spsd' must have 'nfft' (sampling frequency) in its attributes."
62+
"'spsd' must have 'n_fft' (number of points in each FFT) in its attributes."
6563
)
6664

6765
# Value checks
@@ -119,6 +117,7 @@ def sound_pressure_level(
119117
attrs={
120118
"units": "dB re 1 uPa",
121119
"long_name": "Sound Pressure Level",
120+
"time_resolution": spsd.attrs["bin_length"],
122121
"freq_band_min": fmin,
123122
"freq_band_max": fmax,
124123
},
@@ -235,6 +234,7 @@ def third_octave_sound_pressure_level(
235234
{
236235
"units": "dB re 1 uPa",
237236
"long_name": "Third Octave Sound Pressure Level",
237+
"time_resolution": spsd.attrs["bin_length"],
238238
}
239239
)
240240

@@ -272,6 +272,7 @@ def decidecade_sound_pressure_level(
272272
{
273273
"units": "dB re 1 uPa",
274274
"long_name": "Decidecade Sound Pressure Level",
275+
"time_resolution": spsd.attrs["bin_length"],
275276
}
276277
)
277278

mhkit/tests/acoustics/test_analysis.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ def test_sound_pressure_spectral_density(self):
5252
self.assertIn("freq", spsd.dims)
5353
self.assertIn("time", spsd.dims)
5454
self.assertEqual(spsd.attrs["units"], "Pa^2/Hz")
55-
self.assertEqual(spsd.attrs["nbin"], f"{bin_length} s")
55+
self.assertEqual(spsd.attrs["bin_length"], bin_length)
56+
self.assertEqual(spsd.attrs["n_fft"], bin_length * fs)
5657

5758
# Calculate expected number of segments
5859
overlap = 0.0

mhkit/tests/acoustics/test_io.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ def test_calculate_voltage_and_time(self):
5353
np.testing.assert_allclose(raw_voltage, expected_raw_voltage, atol=1e-6)
5454

5555
# Expected time array
56-
time_interval = pd.Timedelta(seconds=1 / fs)
57-
expected_time = pd.date_range(
58-
start=start_time, periods=len(raw) + 1, freq=time_interval
56+
end_time = np.datetime64(start_time) + np.timedelta64(
57+
raw.size * 1000000000, "ns"
5958
)
59+
expected_time = pd.date_range(start_time, end_time, raw.size + 1)
6060
pd.testing.assert_index_equal(time, expected_time)
6161

6262
def test_read_iclisten_metadata(self):

0 commit comments

Comments
 (0)