From 42c024f3709792d9b1af28aef2bff8b1ce81531f Mon Sep 17 00:00:00 2001 From: Dhruv Date: Mon, 29 Jan 2024 19:36:09 +0000 Subject: [PATCH 01/13] added from_files() function --- movement/io/load_poses.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/movement/io/load_poses.py b/movement/io/load_poses.py index ff0aa9292..f3d5d4f7d 100644 --- a/movement/io/load_poses.py +++ b/movement/io/load_poses.py @@ -21,6 +21,45 @@ logger = logging.getLogger(__name__) +def from_file( + file_path: Union[Path, str], + source_software: Literal["DeepLabCut", "SLEAP", "LightningPose"], + fps: Optional[float] = None, +) -> xr.Dataset: + """Load pose tracking data from a DeepLabCut (DLC), LightningPose (LP) or + SLEAP output file into an xarray Dataset. + + Parameters + ---------- + file_path : pathlib.Path or str + Path to the file containing the DLC predicted poses, either in .h5 + or .csv format. + source_software : "DeepLabCut", "SLEAP" or "LightningPose" + The source software of the file. + fps : float, optional + The number of frames per second in the video. If None (default), + the `time` coordinates will be in frame numbers. + + Returns + ------- + xarray.Dataset + Dataset containing the pose tracks, confidence scores, and metadata. + + Notes + ----- + Identical to calling any of the functions from_dlc_file(), + from_sleap_file() or from_lp_file(). + + """ + + if source_software == "DeepLabCut": + return from_dlc_file(file_path, fps) + elif source_software == "SLEAP": + return from_sleap_file(file_path, fps) + elif source_software == "SLEAP": + return from_lp_file(file_path, fps) + + def from_dlc_df(df: pd.DataFrame, fps: Optional[float] = None) -> xr.Dataset: """Create an xarray.Dataset from a DeepLabCut-style pandas DataFrame. From aa25647d423c4b550a689d870e68718eeb61cc79 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Mon, 29 Jan 2024 19:43:36 +0000 Subject: [PATCH 02/13] added from_files() function --- movement/io/load_poses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/movement/io/load_poses.py b/movement/io/load_poses.py index f3d5d4f7d..4984d3535 100644 --- a/movement/io/load_poses.py +++ b/movement/io/load_poses.py @@ -56,7 +56,7 @@ def from_file( return from_dlc_file(file_path, fps) elif source_software == "SLEAP": return from_sleap_file(file_path, fps) - elif source_software == "SLEAP": + elif source_software == "LightningPose": return from_lp_file(file_path, fps) From d5b13358a0fdfda29a08466002240f0a00116e22 Mon Sep 17 00:00:00 2001 From: Niko Sirmpilatze Date: Wed, 7 Feb 2024 10:52:40 +0000 Subject: [PATCH 03/13] Use the updated upload_pypi action (#108) * Bump action versions in upload_pypi workflow step * reuse the upload_pypi action instead of custom steps --- .github/workflows/test_and_deploy.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 2f09a8eea..e2c9b5e5f 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -62,11 +62,6 @@ jobs: needs: [build_sdist_wheels] runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v3 + - uses: neuroinformatics-unit/actions/upload_pypi@v2 with: - name: artifact - path: dist - - uses: pypa/gh-action-pypi-publish@v1.5.0 - with: - user: __token__ - password: ${{ secrets.TWINE_API_KEY }} + secret-pypi-key: ${{ secrets.TWINE_API_KEY }} From eb67fae3514748d653be6c814e225acba40f617b Mon Sep 17 00:00:00 2001 From: Dhruv Date: Sat, 10 Feb 2024 16:21:34 +0000 Subject: [PATCH 04/13] adding code review suggestionas and tests --- movement/io/load_poses.py | 18 ++++++++++++++++-- tests/test_unit/test_load_poses.py | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/movement/io/load_poses.py b/movement/io/load_poses.py index 4984d3535..b415c74f4 100644 --- a/movement/io/load_poses.py +++ b/movement/io/load_poses.py @@ -32,8 +32,9 @@ def from_file( Parameters ---------- file_path : pathlib.Path or str - Path to the file containing the DLC predicted poses, either in .h5 - or .csv format. + Path to the file containing predicted poses. The file format must + be among those supported by the from_dlc_file(), from_slp_file() + or from_lp_file() functions. source_software : "DeepLabCut", "SLEAP" or "LightningPose" The source software of the file. fps : float, optional @@ -50,6 +51,15 @@ def from_file( Identical to calling any of the functions from_dlc_file(), from_sleap_file() or from_lp_file(). + See Also + -------- + movement.io.load_poses.from_dlc_file : Load pose tracks directly + from DeepLabCut files. + movement.io.load_poses.from_sleap_file : Load pose tracks directly + from SLEAP files. + movement.io.load_poses.from_lp_file : Load pose tracks directly + from LightningPose files. + """ if source_software == "DeepLabCut": @@ -58,6 +68,10 @@ def from_file( return from_sleap_file(file_path, fps) elif source_software == "LightningPose": return from_lp_file(file_path, fps) + else: + raise ValueError( + "Unsupported source software: {}".format(source_software) + ) def from_dlc_df(df: pd.DataFrame, fps: Optional[float] = None) -> xr.Dataset: diff --git a/tests/test_unit/test_load_poses.py b/tests/test_unit/test_load_poses.py index 37f76f6a8..c5eeec720 100644 --- a/tests/test_unit/test_load_poses.py +++ b/tests/test_unit/test_load_poses.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import h5py import numpy as np import pytest @@ -239,3 +241,25 @@ def test_load_multi_animal_from_lp_file_raises(self): file_path = POSE_DATA_PATHS.get("DLC_two-mice.predictions.csv") with pytest.raises(ValueError): load_poses.from_lp_file(file_path) + + @pytest.mark.parametrize( + "source_software", ["SLEAP", "DeepLabCut", "LightningPose", "Unknown"] + ) + @pytest.mark.parametrize("fps", [None, 30, 60.0]) + def test_from_file_delegates_correctly(self, source_software, fps): + """Test that the from_file() function delegates to the correct + loader function according to the source_software.""" + + software_to_loader = { + "SLEAP": "movement.io.load_poses.from_sleap_file", + "DeepLabCut": "movement.io.load_poses.from_dlc_file", + "LightningPose": "movement.io.load_poses.from_lp_file", + } + + if source_software == "Unknown": + with pytest.raises(ValueError): + load_poses.from_file("some_file", source_software) + else: + with patch(software_to_loader[source_software]) as mock_loader: + load_poses.from_file("some_file", source_software, fps) + mock_loader.assert_called_with("some_file", fps) From 184977b5f351828c369aba6d11359080da5183a2 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Mon, 19 Feb 2024 16:24:21 +0000 Subject: [PATCH 05/13] added log error --- movement/io/load_poses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/movement/io/load_poses.py b/movement/io/load_poses.py index b415c74f4..cda8b1c55 100644 --- a/movement/io/load_poses.py +++ b/movement/io/load_poses.py @@ -69,8 +69,8 @@ def from_file( elif source_software == "LightningPose": return from_lp_file(file_path, fps) else: - raise ValueError( - "Unsupported source software: {}".format(source_software) + raise log_error( + ValueError, f"Unsupported source software: {source_software}" ) From 53a1b2cf7001d26876b9efb925a1a677c47dd83f Mon Sep 17 00:00:00 2001 From: Dhruv Date: Mon, 29 Jan 2024 19:36:09 +0000 Subject: [PATCH 06/13] added from_files() function --- movement/io/load_poses.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/movement/io/load_poses.py b/movement/io/load_poses.py index ff0aa9292..f3d5d4f7d 100644 --- a/movement/io/load_poses.py +++ b/movement/io/load_poses.py @@ -21,6 +21,45 @@ logger = logging.getLogger(__name__) +def from_file( + file_path: Union[Path, str], + source_software: Literal["DeepLabCut", "SLEAP", "LightningPose"], + fps: Optional[float] = None, +) -> xr.Dataset: + """Load pose tracking data from a DeepLabCut (DLC), LightningPose (LP) or + SLEAP output file into an xarray Dataset. + + Parameters + ---------- + file_path : pathlib.Path or str + Path to the file containing the DLC predicted poses, either in .h5 + or .csv format. + source_software : "DeepLabCut", "SLEAP" or "LightningPose" + The source software of the file. + fps : float, optional + The number of frames per second in the video. If None (default), + the `time` coordinates will be in frame numbers. + + Returns + ------- + xarray.Dataset + Dataset containing the pose tracks, confidence scores, and metadata. + + Notes + ----- + Identical to calling any of the functions from_dlc_file(), + from_sleap_file() or from_lp_file(). + + """ + + if source_software == "DeepLabCut": + return from_dlc_file(file_path, fps) + elif source_software == "SLEAP": + return from_sleap_file(file_path, fps) + elif source_software == "SLEAP": + return from_lp_file(file_path, fps) + + def from_dlc_df(df: pd.DataFrame, fps: Optional[float] = None) -> xr.Dataset: """Create an xarray.Dataset from a DeepLabCut-style pandas DataFrame. From 1b13d6cbfda9687b1a44f9ad87b913eb93cbeaf8 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Mon, 29 Jan 2024 19:43:36 +0000 Subject: [PATCH 07/13] added from_files() function --- movement/io/load_poses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/movement/io/load_poses.py b/movement/io/load_poses.py index f3d5d4f7d..4984d3535 100644 --- a/movement/io/load_poses.py +++ b/movement/io/load_poses.py @@ -56,7 +56,7 @@ def from_file( return from_dlc_file(file_path, fps) elif source_software == "SLEAP": return from_sleap_file(file_path, fps) - elif source_software == "SLEAP": + elif source_software == "LightningPose": return from_lp_file(file_path, fps) From 57e3fc3b6b895664ac8d2df8c1957db2bb7afba2 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Sat, 10 Feb 2024 16:21:34 +0000 Subject: [PATCH 08/13] adding code review suggestionas and tests --- movement/io/load_poses.py | 18 ++++++++++++++++-- tests/test_unit/test_load_poses.py | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/movement/io/load_poses.py b/movement/io/load_poses.py index 4984d3535..b415c74f4 100644 --- a/movement/io/load_poses.py +++ b/movement/io/load_poses.py @@ -32,8 +32,9 @@ def from_file( Parameters ---------- file_path : pathlib.Path or str - Path to the file containing the DLC predicted poses, either in .h5 - or .csv format. + Path to the file containing predicted poses. The file format must + be among those supported by the from_dlc_file(), from_slp_file() + or from_lp_file() functions. source_software : "DeepLabCut", "SLEAP" or "LightningPose" The source software of the file. fps : float, optional @@ -50,6 +51,15 @@ def from_file( Identical to calling any of the functions from_dlc_file(), from_sleap_file() or from_lp_file(). + See Also + -------- + movement.io.load_poses.from_dlc_file : Load pose tracks directly + from DeepLabCut files. + movement.io.load_poses.from_sleap_file : Load pose tracks directly + from SLEAP files. + movement.io.load_poses.from_lp_file : Load pose tracks directly + from LightningPose files. + """ if source_software == "DeepLabCut": @@ -58,6 +68,10 @@ def from_file( return from_sleap_file(file_path, fps) elif source_software == "LightningPose": return from_lp_file(file_path, fps) + else: + raise ValueError( + "Unsupported source software: {}".format(source_software) + ) def from_dlc_df(df: pd.DataFrame, fps: Optional[float] = None) -> xr.Dataset: diff --git a/tests/test_unit/test_load_poses.py b/tests/test_unit/test_load_poses.py index 37f76f6a8..c5eeec720 100644 --- a/tests/test_unit/test_load_poses.py +++ b/tests/test_unit/test_load_poses.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import h5py import numpy as np import pytest @@ -239,3 +241,25 @@ def test_load_multi_animal_from_lp_file_raises(self): file_path = POSE_DATA_PATHS.get("DLC_two-mice.predictions.csv") with pytest.raises(ValueError): load_poses.from_lp_file(file_path) + + @pytest.mark.parametrize( + "source_software", ["SLEAP", "DeepLabCut", "LightningPose", "Unknown"] + ) + @pytest.mark.parametrize("fps", [None, 30, 60.0]) + def test_from_file_delegates_correctly(self, source_software, fps): + """Test that the from_file() function delegates to the correct + loader function according to the source_software.""" + + software_to_loader = { + "SLEAP": "movement.io.load_poses.from_sleap_file", + "DeepLabCut": "movement.io.load_poses.from_dlc_file", + "LightningPose": "movement.io.load_poses.from_lp_file", + } + + if source_software == "Unknown": + with pytest.raises(ValueError): + load_poses.from_file("some_file", source_software) + else: + with patch(software_to_loader[source_software]) as mock_loader: + load_poses.from_file("some_file", source_software, fps) + mock_loader.assert_called_with("some_file", fps) From 872ece2745b1f5de3178eb075a000b1548c29b0f Mon Sep 17 00:00:00 2001 From: Dhruv Date: Mon, 19 Feb 2024 16:24:21 +0000 Subject: [PATCH 09/13] added log error --- movement/io/load_poses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/movement/io/load_poses.py b/movement/io/load_poses.py index b415c74f4..cda8b1c55 100644 --- a/movement/io/load_poses.py +++ b/movement/io/load_poses.py @@ -69,8 +69,8 @@ def from_file( elif source_software == "LightningPose": return from_lp_file(file_path, fps) else: - raise ValueError( - "Unsupported source software: {}".format(source_software) + raise log_error( + ValueError, f"Unsupported source software: {source_software}" ) From 149a0c224ccf16d4a50036544a1d62421af49edc Mon Sep 17 00:00:00 2001 From: niksirbi Date: Mon, 19 Feb 2024 21:03:57 +0000 Subject: [PATCH 10/13] formatted docstrign and added to API reference --- docs/source/api_index.rst | 1 + movement/io/load_poses.py | 23 ++++++++--------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/docs/source/api_index.rst b/docs/source/api_index.rst index 293e98b50..78e27563b 100644 --- a/docs/source/api_index.rst +++ b/docs/source/api_index.rst @@ -10,6 +10,7 @@ Input/Output .. autosummary:: :toctree: api + from_file from_sleap_file from_dlc_file from_dlc_df diff --git a/movement/io/load_poses.py b/movement/io/load_poses.py index cda8b1c55..be8422731 100644 --- a/movement/io/load_poses.py +++ b/movement/io/load_poses.py @@ -33,33 +33,26 @@ def from_file( ---------- file_path : pathlib.Path or str Path to the file containing predicted poses. The file format must - be among those supported by the from_dlc_file(), from_slp_file() - or from_lp_file() functions. + be among those supported by the ``from_dlc_file()``, + ``from_slp_file()`` or ``from_lp_file()`` functions, + since one of these functions will be called internally, based on + the value of ``source_software``. source_software : "DeepLabCut", "SLEAP" or "LightningPose" The source software of the file. fps : float, optional The number of frames per second in the video. If None (default), - the `time` coordinates will be in frame numbers. + the ``time`` coordinates will be in frame numbers. Returns ------- xarray.Dataset Dataset containing the pose tracks, confidence scores, and metadata. - Notes - ----- - Identical to calling any of the functions from_dlc_file(), - from_sleap_file() or from_lp_file(). - See Also -------- - movement.io.load_poses.from_dlc_file : Load pose tracks directly - from DeepLabCut files. - movement.io.load_poses.from_sleap_file : Load pose tracks directly - from SLEAP files. - movement.io.load_poses.from_lp_file : Load pose tracks directly - from LightningPose files. - + movement.io.load_poses.from_dlc_file + movement.io.load_poses.from_sleap_file + movement.io.load_poses.from_lp_file """ if source_software == "DeepLabCut": From 053435da03e1a44ad1b2e3a767081201fee805bf Mon Sep 17 00:00:00 2001 From: niksirbi Date: Mon, 19 Feb 2024 21:08:11 +0000 Subject: [PATCH 11/13] added regex matching to ValueError test --- tests/test_unit/test_load_poses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_unit/test_load_poses.py b/tests/test_unit/test_load_poses.py index c5eeec720..e62560569 100644 --- a/tests/test_unit/test_load_poses.py +++ b/tests/test_unit/test_load_poses.py @@ -257,7 +257,7 @@ def test_from_file_delegates_correctly(self, source_software, fps): } if source_software == "Unknown": - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Unsupported source"): load_poses.from_file("some_file", source_software) else: with patch(software_to_loader[source_software]) as mock_loader: From 9e7e18b2d4c81e56b10a43f5f2c842fce09e54a9 Mon Sep 17 00:00:00 2001 From: niksirbi Date: Mon, 19 Feb 2024 21:18:00 +0000 Subject: [PATCH 12/13] documented new funciton in Getting started guide --- docs/source/getting_started.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/source/getting_started.md b/docs/source/getting_started.md index fef1f3487..0ea2e5f6c 100644 --- a/docs/source/getting_started.md +++ b/docs/source/getting_started.md @@ -73,6 +73,11 @@ Then, depending on the source of your data, use one of the following functions: Load from [SLEAP analysis files](sleap:tutorials/analysis) (.h5): ```python ds = load_poses.from_sleap_file("/path/to/file.analysis.h5", fps=30) + +# or equivalently +ds = load_poses.from_file( + "/path/to/file.analysis.h5", source_software="SLEAP", fps=30 +) ``` ::: @@ -86,6 +91,11 @@ ds = load_poses.from_dlc_file("/path/to/file.h5", fps=30) You may also load .csv files (assuming they are formatted as DeepLabCut expects them): ```python ds = load_poses.from_dlc_file("/path/to/file.csv", fps=30) + +# or equivalently +ds = load_poses.from_file( + "/path/to/file.csv", source_software="DeepLabCut", fps=30 +) ``` If you have already imported the data into a pandas DataFrame, you can @@ -103,6 +113,11 @@ ds = load_poses.from_dlc_df(df, fps=30) Load from LightningPose (LP) files (.csv): ```python ds = load_poses.from_lp_file("/path/to/file.analysis.csv", fps=30) + +# or equivalently +ds = load_poses.from_file( + "/path/to/file.analysis.csv", source_software="LightningPose", fps=30 +) ``` ::: From 33e09edce130100e5b0b23313c159a0cb8a4f5b4 Mon Sep 17 00:00:00 2001 From: niksirbi Date: Mon, 19 Feb 2024 21:20:46 +0000 Subject: [PATCH 13/13] use from_file() for fetching sample data --- movement/sample_data.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/movement/sample_data.py b/movement/sample_data.py index 03b59d35e..c5fdc02ed 100644 --- a/movement/sample_data.py +++ b/movement/sample_data.py @@ -179,10 +179,9 @@ def fetch_sample_data( file for file in metadata if file["file_name"] == filename ) - if file_metadata["source_software"] == "SLEAP": - ds = load_poses.from_sleap_file(file_path, fps=file_metadata["fps"]) - elif file_metadata["source_software"] == "DeepLabCut": - ds = load_poses.from_dlc_file(file_path, fps=file_metadata["fps"]) - elif file_metadata["source_software"] == "LightningPose": - ds = load_poses.from_lp_file(file_path, fps=file_metadata["fps"]) + ds = load_poses.from_file( + file_path, + source_software=file_metadata["source_software"], + fps=file_metadata["fps"], + ) return ds