Skip to content

Commit

Permalink
Merge branch 'neuroinformatics-unit:main' into dhruv
Browse files Browse the repository at this point in the history
  • Loading branch information
DhruvSkyy authored Oct 13, 2023
2 parents b6b7a6f + 1d4aba5 commit 364157a
Show file tree
Hide file tree
Showing 14 changed files with 715 additions and 486 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ repos:
- id: requirements-txt-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.287
rev: v0.0.291
hooks:
- id: ruff
- repo: https://github.com/psf/black
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ The version number is automatically determined from the latest tag on the _main_
## Contributing documentation

The documentation is hosted via [GitHub pages](https://pages.github.com/) at
[neuroinformatics-unit.github.io/movement](https://neuroinformatics-unit.github.io/movement/).
[movement.neuroinformatics.dev](https://movement.neuroinformatics.dev).
Its source files are located in the `docs` folder of this repository.
They are written in either [reStructuredText](https://docutils.sourceforge.io/rst.html) or
[markdown](https://myst-parser.readthedocs.io/en/stable/syntax/typography.html).
Expand Down Expand Up @@ -185,7 +185,7 @@ my_new_file
### Updating the API reference
If your PR introduces new public-facing functions, classes, or methods,
make sure to add them to the `docs/source/api_index.rst` page, so that they are
included in the [API reference](https://neuroinformatics-unit.github.io/movement/api_index.html),
included in the [API reference](https://movement.neuroinformatics.dev/api_index.html),
e.g.:

```rst
Expand All @@ -204,7 +204,7 @@ that follow the [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html

### Updating the examples
We use [sphinx-gallery](https://sphinx-gallery.github.io/stable/index.html)
to create the [examples](https://neuroinformatics-unit.github.io/movement/auto_examples/index.html).
to create the [examples](https://movement.neuroinformatics.dev/auto_examples/index.html).
To add new examples, you will need to create a new `.py` file in `examples/`.
The file should be structured as specified in the relevant
[sphinx-gallery documentation](https://sphinx-gallery.github.io/stable/syntax.html).
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v0.json)](https://github.com/charliermarsh/ruff)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://neuroinformatics.zulipchat.com/#narrow/stream/406001-Movement/topic/Welcome!)

# movement

Kinematic analysis of animal 🐝 🦀 🐀 🐒 body movements for neuroscience and ethology research 🔬.

- Read the [documentation](https://neuroinformatics-unit.github.io/movement/) for more information.
- Read the [documentation](https://movement.neuroinformatics.dev) for more information.
- If you wish to contribute, please read the [contributing guide](./CONTRIBUTING.md).
- Join us on [zulip](https://neuroinformatics.zulipchat.com/#narrow/stream/406001-Movement/topic/Welcome!) to chat with the team. We welcome your questions and suggestions.

## Status
> **Warning**
> - 🏗️ The package is currently in early development. Stay tuned ⌛
> - It is not sufficiently tested to be used for scientific analysis
> - The interface is subject to changes. [Open an issue](https://github.com/neuroinformatics-unit/movement/issues) if you have suggestions.
> - The interface is subject to changes.
## Aims
* Load pose tracks from pose estimation software packages (e.g. [DeepLabCut](http://www.mackenziemathislab.org/deeplabcut) or [SLEAP](https://sleap.ai/))
Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
# The default is the URL of the GitHub pages
# https://www.sphinx-doc.org/en/master/usage/extensions/githubpages.html
github_user = "neuroinformatics-unit"
html_baseurl = "https://neuroinformatics-unit.github.io/movement/"
html_baseurl = "https://movement.neuroinformatics.dev"

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
Expand Down
2 changes: 1 addition & 1 deletion docs/source/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pip install --upgrade movement

:::{tab-item} Developers
To get the latest development version, clone the
[GitHub repository](https://neuroinformatics-unit.github.io/movement/)
[GitHub repository](https://github.com/neuroinformatics-unit/movement/)
and then run from inside the repository:

```sh
Expand Down
6 changes: 5 additions & 1 deletion docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ Index of all functions, classes, and methods.
:::
::::

:::{admonition} Chat with us!
We welcome your questions and suggestions. Join us on [zulip](https://neuroinformatics.zulipchat.com/#narrow/stream/406001-Movement/topic/Welcome!) to chat with the team.
:::

## Status
:::{warning}
- 🏗️ The package is currently in early development. Stay tuned ⌛
- It is not sufficiently tested to be used for scientific analysis
- The interface is subject to changes. [Open an issue](https://github.com/neuroinformatics-unit/movement/issues) if you have suggestions.
- The interface is subject to changes
:::


Expand Down
49 changes: 26 additions & 23 deletions movement/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,34 +31,37 @@ def configure_logging(

# Set the log directory and file path
log_directory.mkdir(parents=True, exist_ok=True)
log_file = log_directory / f"{logger_name}.log"

# If a logger with the given name is already configured
if logger_name in logging.root.manager.loggerDict:
logger = logging.getLogger(logger_name)
handlers = logger.handlers[:]
# If the log file path has changed
if log_file.as_posix() != handlers[0].baseFilename: # type: ignore
# remove the handlers to allow for reconfiguration
for handler in handlers:
logger.removeHandler(handler)
else:
# otherwise, do nothing
return
log_file = (log_directory / f"{logger_name}.log").as_posix()

# Check if logger with the given name is already configured
logger_configured = logger_name in logging.root.manager.loggerDict

logger = logging.getLogger(logger_name)
logger.setLevel(log_level)

# Create a rotating file handler
max_log_size = 5 * 1024 * 1024 # 5 MB
handler = RotatingFileHandler(log_file, maxBytes=max_log_size)
# Logger needs to be (re)configured if unconfigured or
# if configured but the log file path has changed
configure_logger = (
not logger_configured
or log_file != logger.handlers[0].baseFilename # type: ignore
)

if configure_logger:
if logger_configured:
# remove the handlers to allow for reconfiguration
logger.handlers.clear()

logger.setLevel(log_level)

# Create a rotating file handler
max_log_size = 5 * 1024 * 1024 # 5 MB
handler = RotatingFileHandler(log_file, maxBytes=max_log_size)

# Create a formatter and set it to the handler
formatter = logging.Formatter(FORMAT)
handler.setFormatter(formatter)
# Create a formatter and set it to the handler
formatter = logging.Formatter(FORMAT)
handler.setFormatter(formatter)

# Add the handler to the logger
logger.addHandler(handler)
# Add the handler to the logger
logger.addHandler(handler)


def log_error(error, message: str, logger_name: str = "movement"):
Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ classifiers = [
]

[project.urls]
homepage = "https://github.com/neuroinformatics-unit/movement"
bug_tracker = "https://github.com/neuroinformatics-unit/movement/issues"
documentation = "https://github.com/neuroinformatics-unit/movement"
source_code = "https://github.com/neuroinformatics-unit/movement"
user_support = "https://github.com/neuroinformatics-unit/movement/issues"
"Homepage" = "https://github.com/neuroinformatics-unit/movement"
"Bug Tracker" = "https://github.com/neuroinformatics-unit/movement/issues"
"Documentation" = "https://movement.neuroinformatics.dev/"
"Source Code" = "https://github.com/neuroinformatics-unit/movement"
"User Support" = "https://neuroinformatics.zulipchat.com/#narrow/stream/406001-Movement"

[project.optional-dependencies]
dev = [
Expand Down
196 changes: 196 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
import logging
import os
import stat

import h5py
import numpy as np
import pandas as pd
import pytest
import xarray as xr

from movement.datasets import fetch_pose_data_path
from movement.io import PosesAccessor
from movement.logging import configure_logging


def pytest_configure():
"""Perform initial configuration for pytest.
Fetches pose data file paths as a dictionary for tests."""
pytest.POSE_DATA = {
file_name: fetch_pose_data_path(file_name)
for file_name in [
"DLC_single-wasp.predictions.h5",
"DLC_single-wasp.predictions.csv",
"DLC_two-mice.predictions.csv",
"SLEAP_single-mouse_EPM.analysis.h5",
"SLEAP_single-mouse_EPM.predictions.slp",
"SLEAP_three-mice_Aeon_proofread.analysis.h5",
"SLEAP_three-mice_Aeon_proofread.predictions.slp",
]
}


@pytest.fixture(autouse=True)
def setup_logging(tmp_path):
"""Set up logging for the test module.
Expand All @@ -14,3 +39,174 @@ def setup_logging(tmp_path):
logger_name="movement",
log_directory=(tmp_path / ".movement"),
)


@pytest.fixture
def unreadable_file(tmp_path):
"""Return a dictionary containing the file path and
expected permission for an unreadable h5 file."""
file_path = tmp_path / "unreadable.h5"
with open(file_path, "w") as f:
f.write("unreadable data")
os.chmod(f.name, not stat.S_IRUSR)
yield {
"file_path": file_path,
"expected_permission": "r",
}
os.chmod(f.name, stat.S_IRUSR)


@pytest.fixture
def unwriteable_file(tmp_path):
"""Return a dictionary containing the file path and
expected permission for an unwriteable h5 file."""
unwriteable_dir = tmp_path / "no_write"
unwriteable_dir.mkdir()
os.chmod(unwriteable_dir, not stat.S_IWUSR)
file_path = unwriteable_dir / "unwriteable.h5"
yield {
"file_path": file_path,
"expected_permission": "w",
}
os.chmod(unwriteable_dir, stat.S_IWUSR)


@pytest.fixture
def wrong_ext_file(tmp_path):
"""Return a dictionary containing the file path,
expected permission, and expected suffix for a file
with an incorrect extension.
"""
file_path = tmp_path / "wrong_extension.txt"
with open(file_path, "w") as f:
f.write("")
return {
"file_path": file_path,
"expected_permission": "r",
"expected_suffix": ["h5", "csv"],
}


@pytest.fixture
def nonexistent_file(tmp_path):
"""Return a dictionary containing the file path and
expected permission for a nonexistent file."""
file_path = tmp_path / "nonexistent.h5"
return {
"file_path": file_path,
"expected_permission": "r",
}


@pytest.fixture
def directory(tmp_path): # used in save_poses, validators
"""Return a dictionary containing the file path and
expected permission for a directory."""
file_path = tmp_path / "directory"
file_path.mkdir()
return {
"file_path": file_path,
"expected_permission": "r",
}


@pytest.fixture
def h5_file_no_dataframe(tmp_path):
"""Return a dictionary containing the file path and
expected datasets for an h5 file with no dataframe."""
file_path = tmp_path / "no_dataframe.h5"
with h5py.File(file_path, "w") as f:
f.create_dataset("data_in_list", data=[1, 2, 3])
return {
"file_path": file_path,
"expected_datasets": ["dataframe"],
}


@pytest.fixture
def fake_h5_file(tmp_path): # used in save_poses, validators
"""Return a dictionary containing the file path,
expected exception, and expected datasets for
a file with .h5 extension that is not in HDF5 format.
"""
file_path = tmp_path / "fake.h5"
with open(file_path, "w") as f:
f.write("")
return {
"file_path": file_path,
"expected_datasets": ["dataframe"],
"expected_permission": "w",
}


@pytest.fixture
def invalid_single_animal_csv_file(tmp_path):
"""Return the file path for a fake single-animal csv file."""
file_path = tmp_path / "fake_single_animal.csv"
with open(file_path, "w") as f:
f.write("scorer,columns\nsome,columns\ncoords,columns\n")
f.write("1,2")
return file_path


@pytest.fixture
def invalid_multi_animal_csv_file(tmp_path):
"""Return the file path for a fake multi-animal csv file."""
file_path = tmp_path / "fake_multi_animal.csv"
with open(file_path, "w") as f:
f.write(
"scorer,columns\nindividuals,columns\nbodyparts,columns\nsome,columns\n"
)
f.write("1,2")
return file_path


@pytest.fixture
def dlc_style_df():
"""Return a valid DLC-style DataFrame."""
return pd.read_hdf(pytest.POSE_DATA.get("DLC_single-wasp.predictions.h5"))


@pytest.fixture
def valid_tracks_array():
"""Return a function that generate different kinds
of valid tracks array."""

def _valid_tracks_array(array_type):
"""Return a valid tracks array."""
if array_type == "single_keypoint_array":
return np.zeros((10, 2, 1, 2))
elif array_type == "single_track_array":
return np.zeros((10, 1, 2, 2))
else: # "multi_track_array":
return np.zeros((10, 2, 2, 2))

return _valid_tracks_array


@pytest.fixture
def valid_pose_dataset(valid_tracks_array):
"""Return a valid pose tracks dataset."""
dim_names = PosesAccessor.dim_names
tracks_array = valid_tracks_array("multi_track_array")
return xr.Dataset(
data_vars={
"pose_tracks": xr.DataArray(tracks_array, dims=dim_names),
"confidence": xr.DataArray(
tracks_array[..., 0],
dims=dim_names[:-1],
),
},
coords={
"time": np.arange(tracks_array.shape[0]),
"individuals": ["ind1", "ind2"],
"keypoints": ["key1", "key2"],
"space": ["x", "y"],
},
attrs={
"fps": None,
"time_unit": "frames",
"source_software": "SLEAP",
"source_file": "test.h5",
},
)
Loading

0 comments on commit 364157a

Please sign in to comment.