-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into smg/review-vector-tests
Showing
32 changed files
with
1,169 additions
and
506 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ | |
channels: | ||
- conda-forge | ||
dependencies: | ||
- python=3.10 | ||
- python=3.11 | ||
- pytables | ||
- pip: | ||
- movement |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,68 +1,62 @@ | ||
(target-installation)= | ||
# Installation | ||
|
||
## Create a conda environment | ||
|
||
## Install the package | ||
:::{admonition} Use a conda environment | ||
:class: note | ||
We recommend you install movement inside a [conda](conda:) | ||
or [mamba](mamba:) environment, to avoid dependency conflicts with other packages. | ||
In the following we assume you have `conda` installed, | ||
but the same commands will also work with `mamba`/`micromamba`. | ||
To avoid dependency conflicts with other packages, it is best practice to install Python packages within a virtual environment. | ||
We recommend using [conda](conda:) or [mamba](mamba:) to create and manage this environment, as they simplify the installation process. | ||
The following instructions assume that you have conda installed, but the same commands will also work with `mamba`/`micromamba`. | ||
::: | ||
|
||
First, create and activate an environment with some prerequisites. | ||
You can call your environment whatever you like, we've used `movement-env`. | ||
### Users | ||
To install movement in a new environment, follow one of the options below. | ||
We will use `movement-env` as the environment name, but you can choose any name you prefer. | ||
|
||
::::{tab-set} | ||
:::{tab-item} Conda | ||
Create and activate an environment with movement installed: | ||
```sh | ||
conda create -n movement-env -c conda-forge python=3.11 pytables | ||
conda create -n movement-env -c conda-forge movement | ||
conda activate movement-env | ||
``` | ||
|
||
## Install the package | ||
|
||
Then install the `movement` package as described below. | ||
|
||
::::{tab-set} | ||
|
||
:::{tab-item} Users | ||
To get the latest release from PyPI: | ||
|
||
::: | ||
:::{tab-item} Pip | ||
Create and activate an environment with some prerequisites: | ||
```sh | ||
pip install movement | ||
conda create -n movement-env -c conda-forge python=3.11 pytables | ||
conda activate movement-env | ||
``` | ||
If you have an older version of `movement` installed in the same environment, | ||
you can update to the latest version with: | ||
|
||
Install the latest movement release from PyPI: | ||
```sh | ||
pip install --upgrade movement | ||
pip install movement | ||
``` | ||
::: | ||
:::: | ||
|
||
:::{tab-item} Developers | ||
To get the latest development version, clone the | ||
[GitHub repository](movement-github:) | ||
and then run from inside the repository: | ||
### Developers | ||
If you are a developer looking to contribute to movement, please refer to our [contributing guide](target-contributing) for detailed setup instructions and guidelines. | ||
|
||
## Check the installation | ||
To verify that the installation was successful, run (with `movement-env` activated): | ||
```sh | ||
pip install -e .[dev] # works on most shells | ||
pip install -e '.[dev]' # works on zsh (the default shell on macOS) | ||
movement info | ||
``` | ||
You should see a printout including the version numbers of movement | ||
and some of its dependencies. | ||
|
||
This will install the package in editable mode, including all `dev` dependencies. | ||
Please see the [contributing guide](target-contributing) for more information. | ||
::: | ||
|
||
:::: | ||
|
||
## Check the installation | ||
|
||
To verify that the installation was successful, you can run the following | ||
command (with the `movement-env` activated): | ||
## Update the package | ||
To update movement to the latest version, we recommend installing it in a new environment, | ||
as this prevents potential compatibility issues caused by changes in dependency versions. | ||
|
||
To uninstall an existing environment named `movement-env`: | ||
```sh | ||
movement info | ||
conda env remove -n movement-env | ||
``` | ||
|
||
You should see a printout including the version numbers of `movement` | ||
and some of its dependencies. | ||
:::{tip} | ||
If you are unsure about the environment name, you can get a list of the environments on your system with: | ||
```sh | ||
conda env list | ||
``` | ||
::: | ||
Once the environment has been removed, you can create a new one following the [installation instructions](#install-the-package) above. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,112 +1,184 @@ | ||
from contextlib import nullcontext as does_not_raise | ||
|
||
import numpy as np | ||
import pytest | ||
import xarray as xr | ||
|
||
from movement.analysis import kinematics | ||
|
||
|
||
class TestKinematics: | ||
"""Test suite for the kinematics module.""" | ||
|
||
@pytest.fixture | ||
def expected_dataarray(self, valid_poses_dataset): | ||
"""Return a function to generate the expected dataarray | ||
for different kinematic properties. | ||
""" | ||
|
||
def _expected_dataarray(property): | ||
"""Return an xarray.DataArray with default values and | ||
the expected dimensions and coordinates. | ||
""" | ||
# Expected x,y values for velocity | ||
x_vals = np.array( | ||
[1.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 17.0] | ||
) | ||
y_vals = np.full((10, 2, 2, 1), 4.0) | ||
if property == "acceleration": | ||
x_vals = np.array( | ||
[1.0, 1.5, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 1.5, 1.0] | ||
) | ||
y_vals = np.full((10, 2, 2, 1), 0) | ||
elif property == "displacement": | ||
x_vals = np.array( | ||
[0.0, 1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0, 17.0] | ||
) | ||
y_vals[0] = 0 | ||
|
||
x_vals = x_vals.reshape(-1, 1, 1, 1) | ||
# Repeat the x_vals to match the shape of the position | ||
x_vals = np.tile(x_vals, (1, 2, 2, 1)) | ||
return xr.DataArray( | ||
np.concatenate( | ||
[x_vals, y_vals], | ||
axis=-1, | ||
), | ||
dims=valid_poses_dataset.dims, | ||
coords=valid_poses_dataset.coords, | ||
) | ||
|
||
return _expected_dataarray | ||
|
||
kinematic_test_params = [ | ||
("valid_poses_dataset", does_not_raise()), | ||
("valid_poses_dataset_with_nan", does_not_raise()), | ||
("missing_dim_poses_dataset", pytest.raises(AttributeError)), | ||
] | ||
@pytest.mark.parametrize( | ||
"valid_dataset_uniform_linear_motion", | ||
[ | ||
"valid_poses_dataset_uniform_linear_motion", | ||
"valid_bboxes_dataset", | ||
], | ||
) | ||
@pytest.mark.parametrize( | ||
"kinematic_variable, expected_kinematics", | ||
[ | ||
( | ||
"displacement", | ||
[ | ||
np.vstack([np.zeros((1, 2)), np.ones((9, 2))]), # Individual 0 | ||
np.multiply( | ||
np.vstack([np.zeros((1, 2)), np.ones((9, 2))]), | ||
np.array([1, -1]), | ||
), # Individual 1 | ||
], | ||
), | ||
( | ||
"velocity", | ||
[ | ||
np.ones((10, 2)), # Individual 0 | ||
np.multiply( | ||
np.ones((10, 2)), np.array([1, -1]) | ||
), # Individual 1 | ||
], | ||
), | ||
( | ||
"acceleration", | ||
[ | ||
np.zeros((10, 2)), # Individual 0 | ||
np.zeros((10, 2)), # Individual 1 | ||
], | ||
), | ||
], | ||
) | ||
def test_kinematics_uniform_linear_motion( | ||
valid_dataset_uniform_linear_motion, | ||
kinematic_variable, | ||
expected_kinematics, # 2D: n_frames, n_space_dims | ||
request, | ||
): | ||
"""Test computed kinematics for a uniform linear motion case. | ||
Uniform linear motion means the individuals move along a line | ||
at constant velocity. | ||
We consider 2 individuals ("id_0" and "id_1"), | ||
tracked for 10 frames, along x and y: | ||
- id_0 moves along x=y line from the origin | ||
- id_1 moves along x=-y line from the origin | ||
- they both move one unit (pixel) along each axis in each frame | ||
If the dataset is a poses dataset, we consider 3 keypoints per individual | ||
(centroid, left, right), that are always in front of the centroid keypoint | ||
at 45deg from the trajectory. | ||
""" | ||
# Compute kinematic array from input dataset | ||
position = request.getfixturevalue( | ||
valid_dataset_uniform_linear_motion | ||
).position | ||
kinematic_array = getattr(kinematics, f"compute_{kinematic_variable}")( | ||
position | ||
) | ||
|
||
@pytest.mark.parametrize("ds, expected_exception", kinematic_test_params) | ||
def test_displacement( | ||
self, ds, expected_exception, expected_dataarray, request | ||
): | ||
"""Test displacement computation.""" | ||
ds = request.getfixturevalue(ds) | ||
with expected_exception: | ||
result = kinematics.compute_displacement(ds.position) | ||
expected = expected_dataarray("displacement") | ||
if ds.position.isnull().any(): | ||
expected.loc[ | ||
{"individuals": "ind1", "time": [3, 4, 7, 8, 9]} | ||
] = np.nan | ||
xr.testing.assert_allclose(result, expected) | ||
|
||
@pytest.mark.parametrize("ds, expected_exception", kinematic_test_params) | ||
def test_velocity( | ||
self, ds, expected_exception, expected_dataarray, request | ||
): | ||
"""Test velocity computation.""" | ||
ds = request.getfixturevalue(ds) | ||
with expected_exception: | ||
result = kinematics.compute_velocity(ds.position) | ||
expected = expected_dataarray("velocity") | ||
if ds.position.isnull().any(): | ||
expected.loc[ | ||
{"individuals": "ind1", "time": [2, 4, 6, 7, 8, 9]} | ||
] = np.nan | ||
xr.testing.assert_allclose(result, expected) | ||
|
||
@pytest.mark.parametrize("ds, expected_exception", kinematic_test_params) | ||
def test_acceleration( | ||
self, ds, expected_exception, expected_dataarray, request | ||
): | ||
"""Test acceleration computation.""" | ||
ds = request.getfixturevalue(ds) | ||
with expected_exception: | ||
result = kinematics.compute_acceleration(ds.position) | ||
expected = expected_dataarray("acceleration") | ||
if ds.position.isnull().any(): | ||
expected.loc[ | ||
{"individuals": "ind1", "time": [1, 3, 5, 6, 7, 8, 9]} | ||
] = np.nan | ||
xr.testing.assert_allclose(result, expected) | ||
|
||
@pytest.mark.parametrize("order", [0, -1, 1.0, "1"]) | ||
def test_approximate_derivative_with_invalid_order(self, order): | ||
"""Test that an error is raised when the order is non-positive.""" | ||
data = np.arange(10) | ||
expected_exception = ( | ||
ValueError if isinstance(order, int) else TypeError | ||
# Build expected data array from the expected numpy array | ||
expected_array = xr.DataArray( | ||
np.stack(expected_kinematics, axis=1), | ||
# Stack along the "individuals" axis | ||
dims=["time", "individuals", "space"], | ||
) | ||
if "keypoints" in position.coords: | ||
expected_array = expected_array.expand_dims( | ||
{"keypoints": position.coords["keypoints"].size} | ||
) | ||
with pytest.raises(expected_exception): | ||
kinematics._compute_approximate_derivative(data, order=order) | ||
expected_array = expected_array.transpose( | ||
"time", "individuals", "keypoints", "space" | ||
) | ||
|
||
# Compare the values of the kinematic_array against the expected_array | ||
np.testing.assert_allclose(kinematic_array.values, expected_array.values) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"valid_dataset_with_nan", | ||
[ | ||
"valid_poses_dataset_with_nan", | ||
"valid_bboxes_dataset_with_nan", | ||
], | ||
) | ||
@pytest.mark.parametrize( | ||
"kinematic_variable, expected_nans_per_individual", | ||
[ | ||
("displacement", [5, 0]), # individual 0, individual 1 | ||
("velocity", [6, 0]), | ||
("acceleration", [7, 0]), | ||
], | ||
) | ||
def test_kinematics_with_dataset_with_nans( | ||
valid_dataset_with_nan, | ||
kinematic_variable, | ||
expected_nans_per_individual, | ||
helpers, | ||
request, | ||
): | ||
"""Test kinematics computation for a dataset with nans. | ||
We test that the kinematics can be computed and that the number | ||
of nan values in the kinematic array is as expected. | ||
""" | ||
# compute kinematic array | ||
valid_dataset = request.getfixturevalue(valid_dataset_with_nan) | ||
position = valid_dataset.position | ||
kinematic_array = getattr(kinematics, f"compute_{kinematic_variable}")( | ||
position | ||
) | ||
|
||
# compute n nans in kinematic array per individual | ||
n_nans_kinematics_per_indiv = [ | ||
helpers.count_nans(kinematic_array.isel(individuals=i)) | ||
for i in range(valid_dataset.sizes["individuals"]) | ||
] | ||
|
||
# expected nans per individual adjusted for space and keypoints dimensions | ||
expected_nans_adjusted = [ | ||
n | ||
* valid_dataset.sizes["space"] | ||
* valid_dataset.sizes.get("keypoints", 1) | ||
for n in expected_nans_per_individual | ||
] | ||
# check number of nans per individual is as expected in kinematic array | ||
np.testing.assert_array_equal( | ||
n_nans_kinematics_per_indiv, expected_nans_adjusted | ||
) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"invalid_dataset, expected_exception", | ||
[ | ||
("not_a_dataset", pytest.raises(AttributeError)), | ||
("empty_dataset", pytest.raises(AttributeError)), | ||
("missing_var_poses_dataset", pytest.raises(AttributeError)), | ||
("missing_var_bboxes_dataset", pytest.raises(AttributeError)), | ||
("missing_dim_poses_dataset", pytest.raises(ValueError)), | ||
("missing_dim_bboxes_dataset", pytest.raises(ValueError)), | ||
], | ||
) | ||
@pytest.mark.parametrize( | ||
"kinematic_variable", | ||
[ | ||
"displacement", | ||
"velocity", | ||
"acceleration", | ||
], | ||
) | ||
def test_kinematics_with_invalid_dataset( | ||
invalid_dataset, | ||
expected_exception, | ||
kinematic_variable, | ||
request, | ||
): | ||
"""Test kinematics computation with an invalid dataset.""" | ||
with expected_exception: | ||
position = request.getfixturevalue(invalid_dataset).position | ||
getattr(kinematics, f"compute_{kinematic_variable}")(position) | ||
|
||
|
||
@pytest.mark.parametrize("order", [0, -1, 1.0, "1"]) | ||
def test_approximate_derivative_with_invalid_order(order): | ||
"""Test that an error is raised when the order is non-positive.""" | ||
data = np.arange(10) | ||
expected_exception = ValueError if isinstance(order, int) else TypeError | ||
with pytest.raises(expected_exception): | ||
kinematics._compute_approximate_time_derivative(data, order=order) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters