Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Remove compute_relative_heading() from this PR
Browse files Browse the repository at this point in the history
b-peri committed Oct 4, 2024
1 parent 96c1cf9 commit 0665c60
Showing 1 changed file with 0 additions and 186 deletions.
186 changes: 0 additions & 186 deletions movement/analysis/kinematics.py
Original file line number Diff line number Diff line change
@@ -420,123 +420,6 @@ def compute_heading(
return heading_array

Check warning on line 420 in movement/analysis/kinematics.py

Codecov / codecov/patch

movement/analysis/kinematics.py#L420

Added line #L420 was not covered by tests


def compute_relative_heading(
data: xr.DataArray,
left_keypoint: str,
right_keypoint: str,
ROI: xr.DataArray | np.ndarray,
camera_view: Literal["top_down", "bottom_up"] = "top_down",
in_radians: bool = False,
) -> xr.DataArray:
"""Compute the 2D heading relative to an ROI.
Relative heading is computed as the signed angle between
the animal's forward vector (see :func:`compute_forward_direction()\
<movement.analysis.kinematics.compute_forward_direction>`)
and the vector pointing from the midpoint between the two provided
left and right keypoints towards a region of interest (ROI).
Parameters
----------
data : xarray.DataArray
The input data representing position. This must contain
the two symmetrical keypoints located on the left and
right sides of the body, respectively.
left_keypoint : str
Name of the left keypoint, e.g., "left_ear"
right_keypoint : str
Name of the right keypoint, e.g., "right_ear"
ROI : xarray.DataArray | np.ndarray
The position array for the region of interest against
which heading will be computed. Position may be provided in the form of
an ``xarray.DataArray`` (containing ``time``, ``space``, and optionally
, ``keypoints`` dimensions) or a ``numpy.ndarray`` with the following
axes: 0: time, 1: space (x, y), and (optionally) 2: keypoints. In both
cases, if the input ROI contains multiple keypoints (e.g. the vertices
of a bounding box), a centroid will be computed and the heading
computed relative to this centroid. For ROIs provided as
``xarray.DataArray``'s, the time dimension must be equal in length to
``data.time``. For ROIs given as ``numpy.ndarray``'s, the time
dimension must either have length 1 (e.g. a fixed coordinate for which
to compute relative heading across a recording) or be equal in length
to ``data.time``. For ``ndarray``'s with a single time point, take care
to ensure the required axes are adhered to (e.g. ``np.array([[0,1]]))``
is a valid ROI, while ``np.array([0,1])`` is not). Note also that the
provided ROI position array may only contain one individual.
camera_view : Literal["top_down", "bottom_up"], optional
The camera viewing angle, used to determine the upwards
direction of the animal. Can be either ``"top_down"`` (where the
upwards direction is [0, 0, -1]), or ``"bottom_up"`` (where the
upwards direction is [0, 0, 1]). If left unspecified, the camera
view is assumed to be ``"top_down"``.
in_radians : bool, optional
If true, the returned heading array is given in radians.
If false, the array is given in degrees. False by default.
Returns
-------
xarray.DataArray
An xarray DataArray containing the computed relative heading
timeseries, with dimensions matching the input data array,
but without the ``keypoints`` and ``space`` dimensions.
"""
# Validate ROI
_validate_roi_for_relative_heading(ROI, data)

# Drop individuals dim if present
if "individuals" in data.dims:
data = data.sel(individuals=data.individuals.values[0], drop=True)

# Compute forward vector
heading_vector = compute_forward_vector(
data, left_keypoint, right_keypoint, camera_view=camera_view
)
forward_x = heading_vector.sel(space="x")
forward_y = heading_vector.sel(space="y")

# Get ROI coordinates
if isinstance(ROI, xr.DataArray):
# If ROI has a keypoints dimension, compute centroid over provided
# points
if "keypoints" in ROI.dims:
ROI_coords = ROI.mean(dim="keypoints")
else:
ROI_coords = ROI
else:
# If ROI has a keypoints axis, compute centroid over provided points
if len(ROI.shape) > 2:
ROI = np.mean(ROI, axis=2)

# If single timepoint, tile ROI array to match dimensions of ``data``
if ROI.shape[0] == 1:
ROI_coords = np.tile(ROI, [len(data.time), 1])
else:
ROI_coords = ROI

# Compute reference vectors from Left-Right-Midpoint to ROI
left_right_midpoint = data.sel(
keypoints=[left_keypoint, right_keypoint]
).mean(dim="keypoints")

reference_vectors = convert_to_unit(ROI_coords - left_right_midpoint)
ref_x = reference_vectors.sel(space="x")
ref_y = reference_vectors.sel(space="y")

# Compute perp dot product to find signed angular difference between
# forward vector and reference vector
rel_heading_array = np.arctan2(
forward_y * ref_x - forward_x * ref_y,
forward_x * ref_x + forward_y * ref_y,
)

# Convert to degrees
if not in_radians:
rel_heading_array = np.rad2deg(rel_heading_array)

return rel_heading_array


def _validate_type_data_array(data: xr.DataArray) -> None:
"""Validate the input data is an xarray DataArray.
@@ -556,72 +439,3 @@ def _validate_type_data_array(data: xr.DataArray) -> None:
TypeError,
f"Input data must be an xarray.DataArray, but got {type(data)}.",
)


def _validate_roi_for_relative_heading(
ROI: xr.DataArray | np.ndarray, data: xr.DataArray
):
"""Validate the ROI has the correct type and dimensions.
Parameters
----------
ROI : xarray.DataArray | numpy.ndarray
The ROI position array to validate.
data : xarray.DataArray
The input data against which to validate the ROI.
Returns
-------
TypeError
If ROI is not an xarray.DataArray or a numpy.ndarray
ValueError
If ROI does not have the correct dimensions
"""
if not isinstance(ROI, (xr.DataArray | np.ndarray)):
raise log_error(
TypeError,
f"ROI must be an xarray.DataArray or a np.ndarray, but got "
f"{type(data)}.",
)
if isinstance(ROI, xr.DataArray):
validate_dims_coords(
ROI,
{
"time": [],
"space": [],
},
)
if not len(ROI.time) == len(data.time):
raise log_error(
ValueError,
"Input data and ROI must have matching time dimensions.",
)
if "individuals" in ROI.dims and len(ROI.individuals) > 1:
raise log_error(
ValueError, "ROI may not contain multiple individuals."
)
else:
if not (
ROI.shape[0] == 1 or ROI.shape[0] == len(data.time)
): # Validate time dim
raise log_error(
ValueError,
"Dimension ``0`` of the ``ROI`` argument must have length 1 or"
" be equal in length to the ``time`` dimension of ``data``. \n"
"\n If passing a single coordinate, make sure that ... (e.g. "
"``np.array([[0,1]])``",
)
if not ROI.shape[1] == 2: # Validate space dimension
raise log_error(
ValueError,
"Dimension ``1`` of the ``ROI`` argument must correspond to "
"coordinates in 2-D space, and may therefore only have size "
f"``2``. Instead, got size ``{ROI.shape[1]}``.",
)
if len(ROI.shape) > 3:
raise log_error(
ValueError,
"ROI may not have more than 3 dimensions (O: time, 1: space, "
"2: keypoints).",
)

0 comments on commit 0665c60

Please sign in to comment.