Skip to content

Commit 7d61af5

Browse files
committed
feat(polarization): add in_degrees parameter + unit test
1 parent 96ef674 commit 7d61af5

2 files changed

Lines changed: 52 additions & 9 deletions

File tree

movement/kinematics/collective.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ def compute_polarization(
1919
body_axis_keypoints: tuple[Hashable, Hashable] | None = None,
2020
displacement_frames: int = 1,
2121
return_angle: bool = False,
22+
in_degrees: bool = False,
2223
) -> xr.DataArray | tuple[xr.DataArray, xr.DataArray]:
2324
r"""Compute polarization (group alignment) of individuals.
2425
25-
Polarization measures how aligned the heading directions of individuals
26-
are. A value of 1 indicates perfect alignment, while a value near 0
27-
indicates weak or canceling alignment.
26+
Polarization measures how aligned individuals' direction vectors are,
27+
supporting two modes: **orientation polarization** (body-axis mode) for
28+
body orientation alignment, and **heading polarization** (displacement
29+
mode) for movement direction alignment. A value of 1 indicates perfect
30+
alignment, while a value near 0 indicates weak or canceling alignment.
2831
2932
The polarization is computed as
3033
@@ -56,9 +59,13 @@ def compute_polarization(
5659
``body_axis_keypoints`` is not provided. Must be a positive integer.
5760
This parameter is ignored when ``body_axis_keypoints`` is provided.
5861
return_angle : bool, default=False
59-
If True, also return the mean angle in radians. Returns the mean
60-
body orientation angle when using ``body_axis_keypoints``, or the
61-
mean heading angle when using displacement-based polarization.
62+
If True, also return the mean angle. Returns the mean body
63+
orientation angle when using ``body_axis_keypoints``, or the mean
64+
movement direction angle when using displacement-based polarization.
65+
in_degrees : bool, default=False
66+
If True, the mean angle is returned in degrees. Otherwise, the
67+
angle is returned in radians. Only relevant when
68+
``return_angle=True``.
6269
6370
Returns
6471
-------
@@ -103,21 +110,29 @@ def compute_polarization(
103110
104111
>>> polarization = compute_polarization(ds.position)
105112
106-
Return orientation polarization with mean body angle:
113+
Return orientation polarization with mean body orientation angle:
107114
108115
>>> polarization, mean_angle = compute_polarization(
109116
... ds.position,
110117
... body_axis_keypoints=("tail_base", "neck"),
111118
... return_angle=True,
112119
... )
113120
114-
Return heading polarization with mean movement angle:
121+
Return heading polarization with mean movement direction angle (radians):
115122
116123
>>> polarization, mean_angle = compute_polarization(
117124
... ds.position.sel(keypoints="thorax"),
118125
... return_angle=True,
119126
... )
120127
128+
Return heading polarization with mean movement direction angle (degrees):
129+
130+
>>> polarization, mean_angle = compute_polarization(
131+
... ds.position.sel(keypoints="thorax"),
132+
... return_angle=True,
133+
... in_degrees=True,
134+
... )
135+
121136
If multiple keypoints exist, first is used; also return mean angle:
122137
123138
>>> polarization, mean_angle = compute_polarization(
@@ -170,7 +185,10 @@ def compute_polarization(
170185
vector_sum.sel(space="x"),
171186
),
172187
np.nan,
173-
).rename("mean_angle")
188+
)
189+
if in_degrees:
190+
mean_angle = np.rad2deg(mean_angle)
191+
mean_angle = mean_angle.rename("mean_angle")
174192

175193
return polarization, mean_angle
176194

tests/test_unit/test_kinematics/test_collective.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,3 +1101,28 @@ def test_mean_angle_wraparound_near_pi_is_handled_correctly(self):
11011101
np.pi,
11021102
atol=1e-6,
11031103
)
1104+
1105+
def test_in_degrees_true_returns_degrees(self):
1106+
"""in_degrees=True returns angle in degrees."""
1107+
# Two individuals moving in +y direction
1108+
data = np.array(
1109+
[
1110+
[[0, 0], [0, 0]],
1111+
[[0, 0], [1, 1]],
1112+
[[0, 0], [2, 2]],
1113+
],
1114+
dtype=float,
1115+
)
1116+
_, mean_angle_rad = kinematics.compute_polarization(
1117+
_make_position_dataarray(data),
1118+
return_angle=True,
1119+
in_degrees=False,
1120+
)
1121+
_, mean_angle_deg = kinematics.compute_polarization(
1122+
_make_position_dataarray(data),
1123+
return_angle=True,
1124+
in_degrees=True,
1125+
)
1126+
# +y direction = 90 degrees = pi/2 radians
1127+
assert np.allclose(mean_angle_rad.values[1:], np.pi / 2, atol=1e-10)
1128+
assert np.allclose(mean_angle_deg.values[1:], 90.0, atol=1e-10)

0 commit comments

Comments
 (0)