Skip to content

Commit 4c8cccc

Browse files
ENH: add RAO phase conventions (#55)
1 parent d7c9946 commit 4c8cccc

3 files changed

Lines changed: 136 additions & 5 deletions

File tree

docs/user_guide/concepts_utils/rao.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Alternatively, you can construct an :class:`~waveresponse.RAO` object using ampl
5151
amp,
5252
phase,
5353
phase_degrees=False,
54+
phase_leading=True,
5455
freq_hz=True,
5556
degrees=True,
5657
)

src/waveresponse/_core.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,18 @@ def polar_to_complex(amp, phase, phase_degrees=False):
4949
"""
5050
Convert polar coordinates (i.e., amplitude and phase) to complex numbers.
5151
52+
Given as:
53+
54+
``A * exp(j * phi)``
55+
56+
where, ``A`` is the amplitude and ``phi`` is the phase.
57+
5258
Parameters
5359
----------
54-
complex_vals : array-like
55-
Complex number values.
60+
amp : array-like
61+
Amplitude values.
62+
phase : array-like
63+
Phase angle values.
5664
phase_degrees : bool
5765
Whether the phase angles are given in 'degrees'. If ``False``, 'radians'
5866
is assumed.
@@ -71,7 +79,7 @@ def polar_to_complex(amp, phase, phase_degrees=False):
7179
if amp.shape != phase.shape:
7280
raise ValueError()
7381

74-
return amp * (np.cos(phase) + 1j * np.sin(phase))
82+
return amp * np.exp(1j * phase)
7583

7684

7785
def _check_is_similar(*grids, exact_type=True):
@@ -963,6 +971,7 @@ def __init__(
963971
waves_coming_from=waves_coming_from,
964972
)
965973
self._phase_degrees = False
974+
self._phase_leading = True
966975

967976
@classmethod
968977
def from_amp_phase(
@@ -972,6 +981,7 @@ def from_amp_phase(
972981
amp,
973982
phase,
974983
phase_degrees=False,
984+
phase_leading=True,
975985
freq_hz=True,
976986
degrees=True,
977987
clockwise=True,
@@ -999,6 +1009,13 @@ def from_amp_phase(
9991009
phase_degrees : bool
10001010
If the RAO phase values are given in 'degrees'. If ``False``, 'radians'
10011011
is assumed.
1012+
phase_leading : bool
1013+
Whether the phase values follow the 'leading' convention (``True``)
1014+
or the 'lagging' convention (``False``). Mathematically, an RAO with
1015+
phase lead convention is expressed as a complex number of the form
1016+
``A * exp(j * phi)``, where ``A`` represents the amplitude and ``phi``
1017+
represents the phase angle. Whereas an RAO with phase lag convention
1018+
is expressed as ``A * exp(-j * phi)``.
10021019
freq_hz : bool
10031020
If frequency is given in 'Hz'. If ``False``, 'rad/s' is assumed.
10041021
degrees : bool
@@ -1025,6 +1042,9 @@ def from_amp_phase(
10251042
that are folded about a symmetry plane.
10261043
10271044
"""
1045+
if not phase_leading:
1046+
phase = -np.asarray_chkfinite(phase)
1047+
10281048
rao_complex = polar_to_complex(amp, phase, phase_degrees=phase_degrees)
10291049

10301050
rao = cls(
@@ -1037,6 +1057,7 @@ def from_amp_phase(
10371057
waves_coming_from=waves_coming_from,
10381058
)
10391059
rao._phase_degrees = phase_degrees
1060+
rao._phase_leading = phase_leading
10401061
return rao
10411062

10421063
def differentiate(self, n=1):
@@ -1057,7 +1078,9 @@ def differentiate(self, n=1):
10571078
new._vals = new._vals * ((1j * new._freq.reshape(-1, 1)) ** n)
10581079
return new
10591080

1060-
def to_amp_phase(self, phase_degrees=None, freq_hz=None, degrees=None):
1081+
def to_amp_phase(
1082+
self, phase_degrees=None, phase_leading=None, freq_hz=None, degrees=None
1083+
):
10611084
"""
10621085
Return the RAO as amplitude and phase values.
10631086
@@ -1066,6 +1089,15 @@ def to_amp_phase(self, phase_degrees=None, freq_hz=None, degrees=None):
10661089
phase_degrees : bool
10671090
If phase values should be returned in 'degrees'. If ``False``, 'radians'
10681091
is used. Defaults to original units used during initialization or ``False``.
1092+
phase_leading : bool
1093+
Whether the phase values should follow the 'leading' convention (``True``)
1094+
or the 'lagging' convention (``False``). If ``None``, it defaults to
1095+
the convention given during initialization, or the lagging convention
1096+
if no convention was specified during initialization. Mathematically,
1097+
an RAO with phase lead convention is expressed as a complex number of
1098+
the form ``A * exp(j * phi)``, where ``A`` represents the amplitude and
1099+
``phi`` represents the phase angle. Whereas an RAO with phase lag convention
1100+
is expressed as ``A * exp(-j * phi)``.
10691101
freq_hz : bool
10701102
If frequencies should be returned in 'Hz'. If ``False``, 'rad/s' is used.
10711103
Defaults to original units used during initialization.
@@ -1092,9 +1124,19 @@ def to_amp_phase(self, phase_degrees=None, freq_hz=None, degrees=None):
10921124
degrees = self._degrees
10931125
if phase_degrees is None:
10941126
phase_degrees = self._phase_degrees
1127+
if phase_leading is None:
1128+
phase_leading = self._phase_leading
10951129

10961130
freq, dirs, vals = self.grid(freq_hz=freq_hz, degrees=degrees)
1097-
vals_amp, vals_phase = complex_to_polar(vals, phase_degrees=phase_degrees)
1131+
vals_amp, vals_phase = complex_to_polar(vals, phase_degrees=False)
1132+
1133+
if not phase_leading:
1134+
vals_phase = -vals_phase
1135+
vals_phase = np.where(np.isclose(vals_phase, -np.pi), np.pi, vals_phase)
1136+
1137+
if phase_degrees:
1138+
vals_phase = (180.0 / np.pi) * vals_phase
1139+
10981140
return freq, dirs, vals_amp, vals_phase
10991141

11001142
def __repr__(self):

tests/test_core.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2018,6 +2018,7 @@ def test__init__(self):
20182018
assert rao._freq_hz is True
20192019
assert rao._degrees is True
20202020
assert rao._phase_degrees is False
2021+
assert rao._phase_leading is True
20212022

20222023
def test_from_grid(self):
20232024
freq = np.linspace(0, 1.0, 10)
@@ -2069,6 +2070,7 @@ def test_from_amp_phase_rad(self):
20692070
amp_in,
20702071
phase_in,
20712072
phase_degrees=False,
2073+
phase_leading=True,
20722074
freq_hz=True,
20732075
degrees=True,
20742076
clockwise=True,
@@ -2087,6 +2089,7 @@ def test_from_amp_phase_rad(self):
20872089
assert rao._freq_hz is True
20882090
assert rao._degrees is True
20892091
assert rao._phase_degrees is False
2092+
assert rao._phase_leading is True
20902093

20912094
def test_from_amp_phase_deg(self):
20922095
freq_in = np.array([0, 1, 2])
@@ -2131,6 +2134,44 @@ def test_from_amp_phase_deg(self):
21312134
assert rao._freq_hz is True
21322135
assert rao._degrees is True
21332136
assert rao._phase_degrees is True
2137+
assert rao._phase_leading is True
2138+
2139+
def test_from_amp_phase_lagging(self):
2140+
freq_in = np.array([0, 1, 2])
2141+
dirs_in = np.array([0, 45, 90, 135])
2142+
amp_in = np.array(
2143+
[
2144+
[1, 2, 3, 4],
2145+
[1, 2, 3, 4],
2146+
[1, 2, 3, 4],
2147+
]
2148+
)
2149+
phase_in = np.array(
2150+
[
2151+
[0.1, 0.2, 0.3, 0.4],
2152+
[0.1, 0.2, 0.3, 0.4],
2153+
[0.1, 0.2, 0.3, 0.4],
2154+
]
2155+
)
2156+
rao = RAO.from_amp_phase(
2157+
freq_in,
2158+
dirs_in,
2159+
amp_in,
2160+
phase_in,
2161+
phase_degrees=False,
2162+
phase_leading=False,
2163+
freq_hz=True,
2164+
degrees=True,
2165+
clockwise=True,
2166+
waves_coming_from=True,
2167+
)
2168+
2169+
vals_expect = amp_in * np.exp(-1j * phase_in)
2170+
2171+
np.testing.assert_array_almost_equal(rao._freq, 2.0 * np.pi * freq_in)
2172+
np.testing.assert_array_almost_equal(rao._dirs, (np.pi / 180.0) * dirs_in)
2173+
np.testing.assert_array_almost_equal(rao._vals, vals_expect)
2174+
assert rao._phase_leading is False
21342175

21352176
def test__mul__(self, rao):
21362177
rao_squared = rao * rao
@@ -2480,6 +2521,53 @@ def test_to_amp_phase_None(self):
24802521
np.testing.assert_array_almost_equal(amp_out, amp_expect)
24812522
np.testing.assert_array_almost_equal(phase_out, phase_expect)
24822523

2524+
def test_to_amp_phase_lagging(self):
2525+
freq_in = np.array([0, 1, 2])
2526+
dirs_in = np.array([0, 1, 2])
2527+
vals_in = np.array(
2528+
[
2529+
[1.0 + 0.0j, 0.0 + 1.0j, -1.0 + 0.0j],
2530+
[1.0 + 0.0j, 0.0 + 1.0j, -1.0 + 0.0j],
2531+
[1.0 + 0.0j, 0.0 + 1.0j, -1.0 + 0.0j],
2532+
]
2533+
)
2534+
2535+
rao = RAO(
2536+
freq_in,
2537+
dirs_in,
2538+
vals_in,
2539+
freq_hz=True,
2540+
degrees=True,
2541+
clockwise=True,
2542+
waves_coming_from=True,
2543+
)
2544+
2545+
freq_out, dirs_out, amp_out, phase_out = rao.to_amp_phase(
2546+
freq_hz=True, degrees=True, phase_degrees=True, phase_leading=False
2547+
)
2548+
2549+
freq_expect = np.array([0, 1, 2])
2550+
dirs_expect = np.array([0, 1, 2])
2551+
amp_expect = np.array(
2552+
[
2553+
[1.0, 1.0, 1.0],
2554+
[1.0, 1.0, 1.0],
2555+
[1.0, 1.0, 1.0],
2556+
]
2557+
)
2558+
phase_expect = np.array(
2559+
[
2560+
[0.0, -90.0, 180.0],
2561+
[0.0, -90.0, 180.0],
2562+
[0.0, -90.0, 180.0],
2563+
]
2564+
)
2565+
2566+
np.testing.assert_array_almost_equal(freq_out, freq_expect)
2567+
np.testing.assert_array_almost_equal(dirs_out, dirs_expect)
2568+
np.testing.assert_array_almost_equal(amp_out, amp_expect)
2569+
np.testing.assert_array_almost_equal(phase_out, phase_expect)
2570+
24832571
def test__repr__(self, rao):
24842572
assert str(rao) == "RAO"
24852573

0 commit comments

Comments
 (0)