Skip to content

Commit a864509

Browse files
selipotmilancurcic
andauthored
rotary transform function (#225)
* rotary transform function * Update clouddrift/signal.py Co-authored-by: Milan Curcic <[email protected]> * Update clouddrift/signal.py Co-authored-by: Milan Curcic <[email protected]> * @milancurcic and @philippemiron suggestions --------- Co-authored-by: Milan Curcic <[email protected]>
1 parent 82e5011 commit a864509

File tree

2 files changed

+102
-1
lines changed

2 files changed

+102
-1
lines changed

clouddrift/signal.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def analytic_transform(
1313
x: Union[list, np.ndarray, xr.DataArray, pd.Series],
1414
boundary: Optional[str] = "mirror",
1515
) -> np.ndarray:
16-
"""returns the analytic part of a real-valued signal or of a complex-valued
16+
"""Return the analytic part of a real-valued signal or of a complex-valued
1717
signal. To obtain the anti-analytic part of a complex-valued signal apply analytic_transform
1818
to the conjugate of the input. Analytic_transform removes the mean of the input signals.
1919
@@ -86,3 +86,69 @@ def analytic_transform(
8686
z = z[int(m0 + 1) - 1 : int(2 * m0 + 1) - 1]
8787

8888
return z
89+
90+
91+
def rotary_transform(
92+
u: Union[list, np.ndarray, xr.DataArray, pd.Series],
93+
v: Union[list, np.ndarray, xr.DataArray, pd.Series],
94+
boundary: Optional[str] = "mirror",
95+
) -> Tuple[np.ndarray, np.ndarray]:
96+
"""Return time-domain rotary components time series (zp,zn) from Cartesian components time series (u,v).
97+
Note that zp and zn are both analytic time series which implies that the complex-valued time series
98+
u+1j*v is recovered by zp+np.conj(zn). The mean of the original complex signal is split evenly between
99+
the two rotary components.
100+
101+
If up is the analytic transform of u, and vp the analytic transform of v, then the counterclockwise and
102+
clockwise components are defined by zp = 0.5*(up+1j*vp), zp = 0.5*(up-1j*vp).
103+
See as an example Lilly and Olhede (2010), doi: 10.1109/TSP.2009.2031729.
104+
105+
Parameters
106+
----------
107+
u : np.ndarray
108+
Real-valued signal, first Cartesian component (zonal, east-west)
109+
v : np.ndarray
110+
Real-valued signal, second Cartesian component (meridional, north-south)
111+
boundary : str, optional ["mirror", "zeros", "periodic"] optionally specifies the
112+
boundary condition to be imposed at the edges of the time series for the underlying analytic
113+
transform. Default is "mirror".
114+
115+
Returns
116+
-------
117+
zp : np.ndarray
118+
Time-domain complex-valued positive (counterclockwise) rotary component.
119+
zn : np.ndarray
120+
Time-domain complex-valued negative (clockwise) rotary component.
121+
122+
Examples
123+
--------
124+
125+
To obtain the rotary components of a real-valued signal:
126+
>>> zp, zn = rotary_transform(u,v)
127+
128+
Raises
129+
------
130+
ValueError
131+
If the input arrays do not have the same shape.
132+
133+
See Also
134+
--------
135+
:func:`analytic_transform`
136+
"""
137+
# to implement: input one complex argument instead of two real arguments
138+
# u and v arrays must have the same shape or list length.
139+
if type(u) == list and type(v) == list:
140+
if not len(u) == len(v):
141+
raise ValueError("u and v must have the same length.")
142+
else:
143+
if not u.shape == v.shape:
144+
raise ValueError("u and v must have the same shape.")
145+
146+
muv = np.mean(u) + 1j * np.mean(v)
147+
148+
if muv == xr.DataArray:
149+
muv = muv.to_numpy()
150+
151+
up = analytic_transform(u, boundary=boundary)
152+
vp = analytic_transform(v, boundary=boundary)
153+
154+
return 0.5 * (up + 1j * vp) + 0.5 * muv, 0.5 * (up - 1j * vp) + 0.5 * np.conj(muv)

tests/signal_tests.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from clouddrift.signal import (
22
analytic_transform,
3+
rotary_transform,
34
)
45
import numpy as np
56
import unittest
@@ -52,6 +53,12 @@ def test_analytic_transform_list(self):
5253
self.assertTrue(np.allclose(x - np.mean(x), np.real(z)))
5354

5455

56+
def test_analytic_transform_array(self):
57+
x = np.random.rand(99)
58+
z = analytic_transform(x)
59+
self.assertTrue(np.allclose(x - np.mean(x), np.real(z)))
60+
61+
5562
def test_analytic_transform_pandas(self):
5663
x = pd.Series(data=np.random.rand(99))
5764
z = analytic_transform(x)
@@ -62,3 +69,31 @@ def test_analytic_transform_xarray(self):
6269
x = xr.DataArray(data=np.random.rand(99))
6370
z = analytic_transform(x)
6471
self.assertTrue(np.allclose(x - np.mean(x), np.real(z)))
72+
73+
74+
def test_rotary_transform_array(self):
75+
u = np.random.rand(99)
76+
v = np.random.rand(99)
77+
zp, zn = rotary_transform(u, v)
78+
self.assertTrue(np.allclose(u + 1j * v, zp + np.conj(zn)))
79+
80+
81+
def test_rotary_transform_list(self):
82+
u = list(np.random.rand(99))
83+
v = list(np.random.rand(99))
84+
zp, zn = rotary_transform(u, v)
85+
self.assertTrue(np.allclose(u + 1j * v, zp + np.conj(zn)))
86+
87+
88+
def test_rotary_transform_pandas(self):
89+
u = pd.Series(data=np.random.rand(99))
90+
v = pd.Series(data=np.random.rand(99))
91+
zp, zn = rotary_transform(u, v)
92+
self.assertTrue(np.allclose(u + 1j * v, zp + np.conj(zn)))
93+
94+
95+
def test_rotary_transform_xarray(self):
96+
u = xr.DataArray(data=np.random.rand(99))
97+
v = xr.DataArray(data=np.random.rand(99))
98+
zp, zn = rotary_transform(u, v)
99+
self.assertTrue(np.allclose(u + 1j * v, zp + np.conj(zn)))

0 commit comments

Comments
 (0)