Skip to content

Commit 03af6ed

Browse files
feat: add direction binned Grid-like class BinGrid (#73)
1 parent 9295e31 commit 03af6ed

3 files changed

Lines changed: 540 additions & 61 deletions

File tree

src/waveresponse/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from ._core import (
22
RAO,
3+
BinGrid,
34
CosineFullSpreading,
45
CosineHalfSpreading,
56
DirectionalSpectrum,
@@ -37,6 +38,7 @@
3738
"CosineHalfSpreading",
3839
"DirectionalSpectrum",
3940
"Grid",
41+
"BinGrid",
4042
"JONSWAP",
4143
"ModifiedPiersonMoskowitz",
4244
"OchiHubble",

src/waveresponse/_core.py

Lines changed: 185 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,47 @@ def rotate(self, angle, degrees=False):
538538
new._dirs, new._vals = _sort(dirs_new, new._vals)
539539
return new
540540

541+
def _interpolate_function(self, complex_convert="rectangular", **kw):
542+
"""
543+
Interpolation function based on ``scipy.interpolate.RegularGridInterpolator``.
544+
"""
545+
xp = np.concatenate(
546+
(self._dirs[-1:] - 2 * np.pi, self._dirs, self._dirs[:1] + 2.0 * np.pi)
547+
)
548+
549+
yp = self._freq
550+
zp = np.concatenate(
551+
(
552+
self._vals[:, -1:],
553+
self._vals,
554+
self._vals[:, :1],
555+
),
556+
axis=1,
557+
)
558+
559+
if np.all(np.isreal(zp)):
560+
return RGI((xp, yp), zp.T, **kw)
561+
elif complex_convert.lower() == "polar":
562+
amp, phase = complex_to_polar(zp, phase_degrees=False)
563+
phase_complex = np.cos(phase) + 1j * np.sin(phase)
564+
interp_amp = RGI((xp, yp), amp.T, **kw)
565+
interp_phase = RGI((xp, yp), phase_complex.T, **kw)
566+
return lambda *args, **kwargs: (
567+
polar_to_complex(
568+
interp_amp(*args, **kwargs),
569+
np.angle(interp_phase(*args, **kwargs)),
570+
phase_degrees=False,
571+
)
572+
)
573+
elif complex_convert.lower() == "rectangular":
574+
interp_real = RGI((xp, yp), np.real(zp.T), **kw)
575+
interp_imag = RGI((xp, yp), np.imag(zp.T), **kw)
576+
return lambda *args, **kwargs: (
577+
interp_real(*args, **kwargs) + 1j * interp_imag(*args, **kwargs)
578+
)
579+
else:
580+
raise ValueError("Unknown 'complex_convert' type")
581+
541582
def __mul__(self, other):
542583
"""
543584
Multiply values (element-wise).
@@ -658,7 +699,9 @@ def imag(self):
658699

659700
class Grid(_BaseGrid):
660701
"""
661-
Two-dimentional frequency/(wave)direction grid.
702+
A two-dimensional grid with values as a function of frequency and wave direction.
703+
The values are assumed to represent a continuous field. I.e., the values can
704+
be interpolated in the frequency and direction domain.
662705
663706
Parameters
664707
----------
@@ -686,47 +729,6 @@ class Grid(_BaseGrid):
686729
def __repr__(self):
687730
return "Grid"
688731

689-
def _interpolate_function(self, complex_convert="rectangular", **kw):
690-
"""
691-
Interpolation function based on ``scipy.interpolate.RegularGridInterpolator``.
692-
"""
693-
xp = np.concatenate(
694-
(self._dirs[-1:] - 2 * np.pi, self._dirs, self._dirs[:1] + 2.0 * np.pi)
695-
)
696-
697-
yp = self._freq
698-
zp = np.concatenate(
699-
(
700-
self._vals[:, -1:],
701-
self._vals,
702-
self._vals[:, :1],
703-
),
704-
axis=1,
705-
)
706-
707-
if np.all(np.isreal(zp)):
708-
return RGI((xp, yp), zp.T, **kw)
709-
elif complex_convert.lower() == "polar":
710-
amp, phase = complex_to_polar(zp, phase_degrees=False)
711-
phase_complex = np.cos(phase) + 1j * np.sin(phase)
712-
interp_amp = RGI((xp, yp), amp.T, **kw)
713-
interp_phase = RGI((xp, yp), phase_complex.T, **kw)
714-
return lambda *args, **kwargs: (
715-
polar_to_complex(
716-
interp_amp(*args, **kwargs),
717-
np.angle(interp_phase(*args, **kwargs)),
718-
phase_degrees=False,
719-
)
720-
)
721-
elif complex_convert.lower() == "rectangular":
722-
interp_real = RGI((xp, yp), np.real(zp.T), **kw)
723-
interp_imag = RGI((xp, yp), np.imag(zp.T), **kw)
724-
return lambda *args, **kwargs: (
725-
interp_real(*args, **kwargs) + 1j * interp_imag(*args, **kwargs)
726-
)
727-
else:
728-
raise ValueError("Unknown 'complex_convert' type")
729-
730732
def interpolate(
731733
self,
732734
freq,
@@ -861,6 +863,147 @@ def reshape(
861863
return new
862864

863865

866+
class BinGrid(_BaseGrid):
867+
"""
868+
A two-dimensional grid with values as a function of frequency and wave direction.
869+
The values are assumed to represent a continuous field along the frequency axis,
870+
but are treated as bins along the direction axis. I.e., the values can only be
871+
interpolated in the frequency domain.
872+
873+
Parameters
874+
----------
875+
freq : array-like
876+
1-D array of grid frequency coordinates. Positive and monotonically increasing.
877+
dirs : array-like
878+
1-D array of grid direction coordinates. Positive and monotonically increasing.
879+
Must cover the directional range [0, 360) degrees (or [0, 2 * numpy.pi) radians).
880+
vals : array-like (N, M)
881+
Values associated with the grid. Should be a 2-D array of shape (N, M),
882+
such that ``N=len(freq)`` and ``M=len(dirs)``.
883+
freq_hz : bool
884+
If frequency is given in 'Hz'. If ``False``, 'rad/s' is assumed.
885+
degrees : bool
886+
If direction is given in 'degrees'. If ``False``, 'radians' is assumed.
887+
clockwise : bool
888+
If positive directions are defined to be 'clockwise' (``True``) or 'counterclockwise'
889+
(``False``). Clockwise means that the directions follow the right-hand rule
890+
with an axis pointing downwards.
891+
waves_coming_from : bool
892+
If waves are 'coming from' the given directions. If ``False``, 'going towards'
893+
convention is assumed.
894+
"""
895+
896+
def __repr__(self):
897+
return "BinGrid"
898+
899+
def interpolate(
900+
self,
901+
freq,
902+
freq_hz=False,
903+
complex_convert="rectangular",
904+
fill_value=0.0,
905+
):
906+
"""
907+
Interpolate (linear) the grid values to match the given frequency coordinates.
908+
909+
A 'fill value' is used for extrapolation (i.e. `freq` outside the bounds
910+
of the provided 2-D grid). Directions are treated as periodic.
911+
912+
Parameters
913+
----------
914+
freq : array-like
915+
1-D array of grid frequency coordinates. Positive and monotonically increasing.
916+
freq_hz : bool
917+
If frequency is given in 'Hz'. If ``False``, 'rad/s' is assumed.
918+
complex_convert : str, optional
919+
How to convert complex number grid values before interpolating. Should
920+
be 'rectangular' or 'polar'. If 'rectangular' (default), complex values
921+
are converted to rectangular form (i.e., real and imaginary part) before
922+
interpolating. If 'polar', the values are instead converted to polar
923+
form (i.e., amplitude and phase) before interpolating. The values are
924+
converted back to complex form after interpolation.
925+
fill_value : float or None
926+
The value used for extrapolation (i.e., `freq` outside the bounds of
927+
the provided grid). If ``None``, values outside the frequency domain
928+
are extrapolated via nearest-neighbor extrapolation. Note that directions
929+
are treated as periodic (and will not need extrapolation).
930+
931+
Returns
932+
-------
933+
array :
934+
Interpolated grid values.
935+
"""
936+
freq = np.asarray_chkfinite(freq).reshape(-1)
937+
938+
if freq_hz:
939+
freq = 2.0 * np.pi * freq
940+
941+
self._check_freq(freq)
942+
943+
interp_fun = self._interpolate_function(
944+
complex_convert=complex_convert,
945+
method="linear",
946+
bounds_error=False,
947+
fill_value=fill_value,
948+
)
949+
950+
dirsnew, freqnew = np.meshgrid(self._dirs, freq, indexing="ij", sparse=True)
951+
return interp_fun((dirsnew, freqnew)).T
952+
953+
def reshape(
954+
self,
955+
freq,
956+
freq_hz=False,
957+
complex_convert="rectangular",
958+
fill_value=0.0,
959+
):
960+
"""
961+
Reshape the grid to match the given frequency coordinates. Grid
962+
values will be interpolated (linear).
963+
964+
Parameters
965+
----------
966+
freq : array-like
967+
1-D array of new grid frequency coordinates. Positive and monotonically
968+
increasing.
969+
freq_hz : bool
970+
If frequency is given in 'Hz'. If ``False``, 'rad/s' is assumed.
971+
complex_convert : str, optional
972+
How to convert complex number grid values before interpolating. Should
973+
be 'rectangular' or 'polar'. If 'rectangular' (default), complex values
974+
are converted to rectangular form (i.e., real and imaginary part) before
975+
interpolating. If 'polar', the values are instead converted to polar
976+
form (i.e., amplitude and phase) before interpolating. The values are
977+
converted back to complex form after interpolation.
978+
fill_value : float or None
979+
The value used for extrapolation (i.e., `freq` outside the bounds of
980+
the provided grid). If ``None``, values outside the frequency domain
981+
are extrapolated via nearest-neighbor extrapolation. Note that directions
982+
are treated as periodic (and will not need extrapolation).
983+
984+
Returns
985+
-------
986+
obj :
987+
A copy of the object where the underlying coordinate system is reshaped.
988+
"""
989+
freq_new = np.asarray_chkfinite(freq).copy()
990+
991+
if freq_hz:
992+
freq_new = 2.0 * np.pi * freq_new
993+
994+
self._check_freq(freq_new)
995+
996+
vals_new = self.interpolate(
997+
freq_new,
998+
freq_hz=False,
999+
complex_convert=complex_convert,
1000+
fill_value=fill_value,
1001+
)
1002+
new = self.copy()
1003+
new._freq, new._vals = freq_new, vals_new
1004+
return new
1005+
1006+
8641007
class DisableComplexMixin:
8651008
@property
8661009
def imag(self):

0 commit comments

Comments
 (0)