Skip to content

Commit 08285e9

Browse files
niksirbivtushar06
authored andcommitted
Update logo, favicon, and overview images (neuroinformatics-unit#632)
* update logo and favicon images * updated overview image
1 parent 8f38215 commit 08285e9

File tree

11 files changed

+161
-2
lines changed

11 files changed

+161
-2
lines changed
34 KB
Loading
611 Bytes
Loading
34.5 KB
Loading
-40.2 KB
Binary file not shown.
-43 KB
Binary file not shown.
90.9 KB
Loading

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
124124
html_theme = "pydata_sphinx_theme"
125125
html_title = "movement"
126-
html_logo = "_static/movement_logo_transparent_no-text.png"
126+
html_logo = "_static/movement_favicon_512px.png"
127127
html_favicon = "_static/movement_favicon_64px.png"
128128
# Customize the theme
129129
html_theme_options = {

movement/kinematics/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
compute_speed,
99
compute_time_derivative,
1010
compute_velocity,
11+
detect_u_turns,
1112
)
1213
from movement.kinematics.orientation import (
1314
compute_forward_vector,
@@ -26,4 +27,5 @@
2627
"compute_forward_vector",
2728
"compute_head_direction_vector",
2829
"compute_forward_vector_angle",
30+
"detect_u_turns",
2931
]

movement/kinematics/kinematics.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
import warnings
1414
from typing import Literal
1515

16+
import numpy as np
1617
import xarray as xr
1718

19+
from movement.kinematics.orientation import compute_forward_vector
1820
from movement.utils.logging import logger
1921
from movement.utils.reports import report_nan_values
20-
from movement.utils.vector import compute_norm
22+
from movement.utils.vector import compute_norm, compute_signed_angle_2d
2123
from movement.validators.arrays import validate_dims_coords
2224

2325

@@ -379,3 +381,68 @@ def _compute_scaled_path_length(
379381
valid_proportion = valid_segments / (data.sizes["time"] - 1)
380382
# return scaled path length
381383
return compute_norm(displacement).sum(dim="time") / valid_proportion
384+
385+
386+
def detect_u_turns(
387+
data: xr.DataArray,
388+
use_direction: Literal["forward_vector", "displacement"] = "displacement",
389+
u_turn_threshold: float = np.pi * 5 / 6, # 150 degrees in radians
390+
camera_view: Literal["top_down", "bottom_up"] = "bottom_up",
391+
) -> xr.DataArray:
392+
"""Detect U-turn behavior in a trajectory.
393+
394+
This function computes the directional change between consecutive time
395+
frames and accumulates the rotation angles. If the accumulated angle
396+
exceeds a specified threshold, a U-turn is detected.
397+
398+
Parameters
399+
----------
400+
data : xarray.DataArray
401+
The trajectory data, which must contain the 'time' and 'space' (x, y).
402+
use_direction : Literal["forward_vector", "displacement"], optional
403+
Method to compute direction vectors, default is `"displacement"`:
404+
- `"forward_vector"`: Computes the forward direction vector.
405+
- `"displacement"`: Computes displacement vectors.
406+
u_turn_threshold : float, optional
407+
The angle threshold (in radians) to detect U-turn. Default is (`5π/6`).
408+
camera_view : Literal["top_down", "bottom_up"], optional
409+
Specifies the camera perspective used for computing direction vectors.
410+
411+
Returns
412+
-------
413+
xarray.DataArray
414+
A boolean scalar DataArray indicating whether a U-turn has occurred.
415+
416+
"""
417+
# Compute direction vectors
418+
if use_direction == "forward_vector":
419+
direction_vectors = compute_forward_vector(
420+
data, "left_ear", "right_ear", camera_view=camera_view
421+
)
422+
elif use_direction == "displacement":
423+
if "keypoints" in data.dims:
424+
raise ValueError(
425+
"Displacement expects single keypoint data "
426+
"and must not include the 'keypoints' dimension."
427+
)
428+
direction_vectors = compute_displacement(data)
429+
else:
430+
raise ValueError(
431+
"The parameter `use_direction` must be one of `forward_vector` "
432+
f" or `displacement`, but got {use_direction}."
433+
)
434+
435+
# Compute angle between vectors
436+
angles = compute_signed_angle_2d(
437+
direction_vectors.shift(time=1), direction_vectors
438+
)
439+
angles = angles.fillna(0)
440+
441+
# Accumulate angles over time and compute range
442+
cumulative_rotation = angles.cumsum(dim="time")
443+
rotation_range = cumulative_rotation.max(
444+
dim="time"
445+
) - cumulative_rotation.min(dim="time")
446+
447+
# Return scalar boolean as xarray.DataArray
448+
return xr.DataArray(rotation_range >= u_turn_threshold)

tests/test_unit/test_kinematics/test_kinematics.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,71 @@ def test_path_length_nan_warn_threshold(
318318
position, nan_warn_threshold=nan_warn_threshold
319319
)
320320
assert result.name == "path_length"
321+
322+
323+
@pytest.fixture
324+
def valid_data_array_for_u_turn_detection():
325+
"""Return a position data array for an individual with 3 keypoints
326+
(left ear, right ear, and nose), tracked for 4 frames, in x-y space.
327+
"""
328+
time = [0, 1, 2, 3]
329+
keypoints = ["left_ear", "right_ear", "nose"]
330+
space = ["x", "y"]
331+
332+
ds = xr.DataArray(
333+
[
334+
[[-1, 0], [1, 0], [0, 1]], # time 0
335+
[[0, 2], [0, 0], [1, 1]], # time 1
336+
[[2, 1], [0, 1], [1, 0]], # time 2
337+
[[1, -1], [1, 1], [0, 0]], # time 3
338+
],
339+
dims=["time", "keypoints", "space"],
340+
coords={
341+
"time": time,
342+
"keypoints": keypoints,
343+
"space": space,
344+
},
345+
)
346+
return ds
347+
348+
349+
def test_detect_u_turns(valid_data_array_for_u_turn_detection):
350+
"""Test that U-turn detection works correctly using a mock dataset."""
351+
# Forward vector method
352+
u_turn_forward_vector = kinematics.detect_u_turns(
353+
valid_data_array_for_u_turn_detection, use_direction="forward_vector"
354+
)
355+
assert u_turn_forward_vector.item() is True
356+
357+
# Displacement method (nose-only)
358+
nose_data = valid_data_array_for_u_turn_detection.sel(
359+
keypoints="nose"
360+
).drop_vars("keypoints")
361+
u_turn_displacement = kinematics.detect_u_turns(
362+
nose_data, use_direction="displacement"
363+
)
364+
assert u_turn_displacement.item() is True
365+
366+
# Stricter threshold - displacement should return False
367+
strict_displacement = kinematics.detect_u_turns(
368+
nose_data, use_direction="displacement", u_turn_threshold=np.pi * 7 / 6
369+
)
370+
assert strict_displacement.item() is False
371+
372+
# Stricter threshold - forward vector still returns True
373+
strict_forward_vector = kinematics.detect_u_turns(
374+
valid_data_array_for_u_turn_detection,
375+
use_direction="forward_vector",
376+
u_turn_threshold=np.pi * 7 / 6,
377+
)
378+
assert strict_forward_vector.item() is True
379+
380+
# Invalid use_direction check
381+
with pytest.raises(
382+
ValueError,
383+
match="must be one of `forward_vector`.*but got invalid_direction",
384+
):
385+
kinematics.detect_u_turns(
386+
valid_data_array_for_u_turn_detection,
387+
use_direction="invalid_direction",
388+
)

0 commit comments

Comments
 (0)