Skip to content

Commit d675095

Browse files
feat: add method for returning binned spectrum values (#75)
1 parent c3cd678 commit d675095

2 files changed

Lines changed: 311 additions & 1 deletion

File tree

src/waveresponse/_core.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1680,14 +1680,77 @@ def grid(self, freq_hz=False, degrees=False):
16801680

16811681
return freq, dirs, vals
16821682

1683+
def bingrid(self, freq_hz=False, degrees=False, complex_convert="rectangular"):
1684+
"""
1685+
Return a copy of the spectrum's frequency and direction coordinates,
1686+
along with the corresponding binned spectrum values.
1687+
1688+
The spectrum values are interpolated and then integrated over their
1689+
respective direction bins, resulting in total energy per unit frequency.
1690+
This differs from the ``grid`` method, which returns the spectral density
1691+
values directly without bin integration.
1692+
1693+
Parameters
1694+
----------
1695+
freq_hz : bool
1696+
Whether to return frequencies in hertz (Hz). If ``False``, angular
1697+
frequency in radians per second (rad/s) is used.
1698+
degrees : bool
1699+
Whether to return directions in degrees. If ``False``, radians are used.
1700+
complex_convert : str, optional
1701+
How to convert complex number grid values before interpolating. Should
1702+
be 'rectangular' or 'polar'. If 'rectangular' (default), complex values
1703+
are converted to rectangular form (i.e., real and imaginary part) before
1704+
interpolating. If 'polar', the values are instead converted to polar
1705+
form (i.e., amplitude and phase) before interpolating. The values are
1706+
converted back to complex form after interpolation.
1707+
1708+
Returns
1709+
-------
1710+
freq : ndarray, shape (N,)
1711+
1D array of frequency coordinates.
1712+
dirs : ndarray, shape (M,)
1713+
1D array of direction coordinates.
1714+
vals : ndarray, shape (N, M)
1715+
2D array of binned spectrum energy values (energy per unit frequency).
1716+
``N = len(freq)``, ``M = len(dirs)``.
1717+
"""
1718+
1719+
dirs_tmp = np.r_[
1720+
-2.0 * np.pi + self._dirs[-1], self._dirs, 2.0 * np.pi + self._dirs[0]
1721+
]
1722+
1723+
dirs_bin = np.empty(self._dirs.size * 2 + 1, dtype=self._dirs.dtype)
1724+
dirs_bin[0::2] = dirs_tmp[:-1] + np.diff(dirs_tmp) / 2.0 # bin boundaries
1725+
dirs_bin[1::2] = self._dirs # bin centers
1726+
1727+
interp_fun = self._interpolate_function(
1728+
complex_convert=complex_convert, method="linear", bounds_error=True
1729+
)
1730+
1731+
dirsnew, freqnew = np.meshgrid(dirs_bin, self._freq, indexing="ij", sparse=True)
1732+
vals_tmp = interp_fun((dirsnew, freqnew)).T
1733+
1734+
vals_binned = np.column_stack(
1735+
[
1736+
trapezoid(vals_tmp[:, i : i + 3], dirs_bin[i : i + 3], axis=1)
1737+
for i in range(0, 2 * len(self._dirs), 2)
1738+
]
1739+
)
1740+
1741+
if freq_hz:
1742+
vals_binned *= 2.0 * np.pi
1743+
1744+
return self.freq(freq_hz=freq_hz), self.dirs(degrees=degrees), vals_binned
1745+
16831746
def interpolate(
16841747
self,
16851748
freq,
16861749
dirs,
16871750
freq_hz=False,
16881751
degrees=False,
1752+
complex_convert="rectangular",
16891753
fill_value=0.0,
1690-
**kwargs,
16911754
):
16921755
"""
16931756
Interpolate (linear) the spectrum values to match the given frequency and direction
@@ -1706,6 +1769,13 @@ def interpolate(
17061769
If frequency is given in 'Hz'. If ``False``, 'rad/s' is assumed.
17071770
degrees : bool
17081771
If direction is given in 'degrees'. If ``False``, 'radians' is assumed.
1772+
complex_convert : str, optional
1773+
How to convert complex number grid values before interpolating. Should
1774+
be 'rectangular' or 'polar'. If 'rectangular' (default), complex values
1775+
are converted to rectangular form (i.e., real and imaginary part) before
1776+
interpolating. If 'polar', the values are instead converted to polar
1777+
form (i.e., amplitude and phase) before interpolating. The values are
1778+
converted back to complex form after interpolation.
17091779
fill_value : float or None
17101780
The value used for extrapolation (i.e., `freq` outside the bounds of
17111781
the provided grid). If ``None``, values outside the frequency domain
@@ -1723,6 +1793,7 @@ def interpolate(
17231793
dirs,
17241794
freq_hz=freq_hz,
17251795
degrees=degrees,
1796+
complex_convert=complex_convert,
17261797
fill_value=fill_value,
17271798
)
17281799

tests/test_core.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3277,6 +3277,245 @@ def test_grid__hz_deg(self):
32773277
np.testing.assert_array_almost_equal(dirs_out, dirs_expect)
32783278
np.testing.assert_array_almost_equal(vals_out, vals_expect)
32793279

3280+
def test_bingrid(self):
3281+
freq_in = np.array([0.2, 0.4, 0.6, 0.8, 1.0])
3282+
dirs_in = np.array([0.0, 90.0, 180.0])
3283+
vals_in = np.ones((len(freq_in), len(dirs_in)))
3284+
spectrum = DirectionalSpectrum(
3285+
freq_in,
3286+
dirs_in,
3287+
vals_in,
3288+
freq_hz=True,
3289+
degrees=True,
3290+
clockwise=True,
3291+
waves_coming_from=True,
3292+
)
3293+
3294+
freq_out, dirs_out, vals_out = spectrum.bingrid(freq_hz=False, degrees=False)
3295+
3296+
freq_expect = 2.0 * np.pi * freq_in
3297+
dirs_expect = (np.pi / 180.0) * dirs_in
3298+
vals_expect = 1.0 / (2.0 * np.pi * (np.pi / 180.0)) * vals_in
3299+
3300+
vals_expect[:, 0] *= 135.0 * (np.pi / 180.0)
3301+
vals_expect[:, 1] *= 90.0 * (np.pi / 180.0)
3302+
vals_expect[:, 2] *= 135.0 * (np.pi / 180.0)
3303+
3304+
np.testing.assert_array_almost_equal(freq_out, freq_expect)
3305+
np.testing.assert_array_almost_equal(dirs_out, dirs_expect)
3306+
np.testing.assert_array_almost_equal(vals_out, vals_expect)
3307+
3308+
def test_bingrid2(self):
3309+
freq_in = np.array([0.2, 0.4, 0.6, 0.8, 1.0]) * 2.0 * np.pi
3310+
dirs_in = np.radians(np.array([0.0, 90.0, 180.0]))
3311+
vals_in = np.ones((len(freq_in), len(dirs_in)))
3312+
spectrum = DirectionalSpectrum(
3313+
freq_in,
3314+
dirs_in,
3315+
vals_in,
3316+
freq_hz=False,
3317+
degrees=False,
3318+
clockwise=True,
3319+
waves_coming_from=True,
3320+
)
3321+
3322+
freq_out, dirs_out, vals_out = spectrum.bingrid(freq_hz=False, degrees=False)
3323+
3324+
freq_expect = freq_in
3325+
dirs_expect = dirs_in
3326+
vals_expect = vals_in
3327+
3328+
vals_expect[:, 0] *= 135.0 * (np.pi / 180.0)
3329+
vals_expect[:, 1] *= 90.0 * (np.pi / 180.0)
3330+
vals_expect[:, 2] *= 135.0 * (np.pi / 180.0)
3331+
3332+
np.testing.assert_array_almost_equal(freq_out, freq_expect)
3333+
np.testing.assert_array_almost_equal(dirs_out, dirs_expect)
3334+
np.testing.assert_array_almost_equal(vals_out, vals_expect)
3335+
3336+
@pytest.mark.parametrize(
3337+
"dirs_in",
3338+
[
3339+
np.array([0.0, 90.0, 180.0, 270.0]),
3340+
np.array([10.0, 100.0, 190.0, 280.0]),
3341+
np.array([85.0, 175.0, 265.0, 355.0]),
3342+
],
3343+
)
3344+
def test_bingrid_rads_rad(self, dirs_in):
3345+
freq_in = np.arange(0.0, 1, 0.1)
3346+
vals_in = np.column_stack(
3347+
[
3348+
np.zeros_like(freq_in),
3349+
np.ones_like(freq_in),
3350+
4 * np.ones_like(freq_in),
3351+
np.ones_like(freq_in),
3352+
]
3353+
)
3354+
spectrum = DirectionalSpectrum(
3355+
freq_in,
3356+
dirs_in,
3357+
vals_in,
3358+
freq_hz=True,
3359+
degrees=True,
3360+
clockwise=True,
3361+
waves_coming_from=True,
3362+
)
3363+
3364+
freq_out, dirs_out, vals_out = spectrum.bingrid(freq_hz=False, degrees=False)
3365+
3366+
freq_expect = 2.0 * np.pi * freq_in
3367+
dirs_expect = (np.pi / 180.0) * dirs_in
3368+
vals_expect = np.column_stack(
3369+
[
3370+
22.5 * np.ones_like(freq_in),
3371+
112.5 * np.ones_like(freq_in),
3372+
292.5 * np.ones_like(freq_in),
3373+
112.5 * np.ones_like(freq_in),
3374+
]
3375+
) / (2.0 * np.pi)
3376+
3377+
np.testing.assert_array_almost_equal(freq_out, freq_expect)
3378+
np.testing.assert_array_almost_equal(dirs_out, dirs_expect)
3379+
np.testing.assert_array_almost_equal(vals_out, vals_expect)
3380+
3381+
@pytest.mark.parametrize(
3382+
"dirs_in",
3383+
[
3384+
np.array([0.0, 90.0, 180.0, 270.0]),
3385+
np.array([10.0, 100.0, 190.0, 280.0]),
3386+
np.array([85.0, 175.0, 265.0, 355.0]),
3387+
],
3388+
)
3389+
def test_bingrid_hz_rad(self, dirs_in):
3390+
freq_in = np.arange(0.0, 1, 0.1)
3391+
vals_in = np.column_stack(
3392+
[
3393+
np.zeros_like(freq_in),
3394+
np.ones_like(freq_in),
3395+
4 * np.ones_like(freq_in),
3396+
np.ones_like(freq_in),
3397+
]
3398+
)
3399+
3400+
spectrum = DirectionalSpectrum(
3401+
freq_in,
3402+
dirs_in,
3403+
vals_in,
3404+
freq_hz=True,
3405+
degrees=True,
3406+
clockwise=True,
3407+
waves_coming_from=True,
3408+
)
3409+
3410+
freq_out, dirs_out, vals_out = spectrum.bingrid(freq_hz=True, degrees=False)
3411+
3412+
freq_expect = freq_in
3413+
dirs_expect = dirs_in * (np.pi / 180.0)
3414+
vals_expect = np.column_stack(
3415+
[
3416+
22.5 * np.ones_like(freq_in),
3417+
112.5 * np.ones_like(freq_in),
3418+
292.5 * np.ones_like(freq_in),
3419+
112.5 * np.ones_like(freq_in),
3420+
]
3421+
)
3422+
3423+
np.testing.assert_array_almost_equal(freq_out, freq_expect)
3424+
np.testing.assert_array_almost_equal(dirs_out, dirs_expect)
3425+
np.testing.assert_array_almost_equal(vals_out, vals_expect)
3426+
3427+
@pytest.mark.parametrize(
3428+
"dirs_in",
3429+
[
3430+
np.array([0.0, 90.0, 180.0, 270.0]),
3431+
np.array([10.0, 100.0, 190.0, 280.0]),
3432+
np.array([85.0, 175.0, 265.0, 355.0]),
3433+
],
3434+
)
3435+
def test_bingrid_rads_deg(self, dirs_in):
3436+
freq_in = np.arange(0.0, 1, 0.1)
3437+
vals_in = np.column_stack(
3438+
[
3439+
np.zeros_like(freq_in),
3440+
np.ones_like(freq_in),
3441+
4 * np.ones_like(freq_in),
3442+
np.ones_like(freq_in),
3443+
]
3444+
)
3445+
3446+
spectrum = DirectionalSpectrum(
3447+
freq_in,
3448+
dirs_in,
3449+
vals_in,
3450+
freq_hz=True,
3451+
degrees=True,
3452+
clockwise=True,
3453+
waves_coming_from=True,
3454+
)
3455+
3456+
freq_out, dirs_out, vals_out = spectrum.bingrid(freq_hz=False, degrees=True)
3457+
3458+
freq_expect = 2.0 * np.pi * freq_in
3459+
dirs_expect = dirs_in
3460+
vals_expect = np.column_stack(
3461+
[
3462+
22.5 * np.ones_like(freq_in),
3463+
112.5 * np.ones_like(freq_in),
3464+
292.5 * np.ones_like(freq_in),
3465+
112.5 * np.ones_like(freq_in),
3466+
]
3467+
) / (2.0 * np.pi)
3468+
3469+
np.testing.assert_array_almost_equal(freq_out, freq_expect)
3470+
np.testing.assert_array_almost_equal(dirs_out, dirs_expect)
3471+
np.testing.assert_array_almost_equal(vals_out, vals_expect)
3472+
3473+
@pytest.mark.parametrize(
3474+
"dirs_in",
3475+
[
3476+
np.array([0.0, 90.0, 180.0, 270.0]),
3477+
np.array([10.0, 100.0, 190.0, 280.0]),
3478+
np.array([85.0, 175.0, 265.0, 355.0]),
3479+
],
3480+
)
3481+
def test_bingrid_hz_deg(self, dirs_in):
3482+
freq_in = np.arange(0.0, 1, 0.1)
3483+
vals_in = np.column_stack(
3484+
[
3485+
np.zeros_like(freq_in),
3486+
np.ones_like(freq_in),
3487+
4 * np.ones_like(freq_in),
3488+
np.ones_like(freq_in),
3489+
]
3490+
)
3491+
spectrum = DirectionalSpectrum(
3492+
freq_in,
3493+
dirs_in,
3494+
vals_in,
3495+
freq_hz=True,
3496+
degrees=True,
3497+
clockwise=True,
3498+
waves_coming_from=True,
3499+
)
3500+
3501+
freq_out, dirs_out, vals_out = spectrum.bingrid(freq_hz=True, degrees=True)
3502+
3503+
freq_expect = freq_in
3504+
dirs_expect = dirs_in
3505+
dirs_expect = dirs_in
3506+
vals_expect = np.column_stack(
3507+
[
3508+
22.5 * np.ones_like(freq_in),
3509+
112.5 * np.ones_like(freq_in),
3510+
292.5 * np.ones_like(freq_in),
3511+
112.5 * np.ones_like(freq_in),
3512+
]
3513+
)
3514+
3515+
np.testing.assert_array_almost_equal(freq_out, freq_expect)
3516+
np.testing.assert_array_almost_equal(dirs_out, dirs_expect)
3517+
np.testing.assert_array_almost_equal(vals_out, vals_expect)
3518+
32803519
def test_from_spectrum1d(self):
32813520
freq = np.array([0.0, 0.5, 1.0])
32823521
dirs = np.array([0.0, 90.0, 180.0, 270.0])

0 commit comments

Comments
 (0)