From a391afdf55b8c936e0048f3a5cec6f77da72baa3 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 28 Nov 2024 11:50:37 +0100 Subject: [PATCH] Modernize package and prepare release --- .github/workflows/full_tests.yml | 26 ++++++++ .github/workflows/publish-to-pypi.yml | 28 +++++++++ .pre-commit-config.yaml | 36 +++++++++++ head_direction/__init__.py | 1 - pyproject.toml | 59 +++++++++++++++++ setup.py | 31 +-------- src/head_direction/__init__.py | 4 ++ .../head_direction}/head.py | 13 ++-- .../head_direction}/tools.py | 8 +-- {head_direction/tests => tests}/test_head.py | 63 +++++++++---------- 10 files changed, 198 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/full_tests.yml create mode 100644 .github/workflows/publish-to-pypi.yml create mode 100644 .pre-commit-config.yaml delete mode 100644 head_direction/__init__.py create mode 100644 pyproject.toml create mode 100644 src/head_direction/__init__.py rename {head_direction => src/head_direction}/head.py (92%) rename {head_direction => src/head_direction}/tools.py (90%) rename {head_direction/tests => tests}/test_head.py (50%) diff --git a/.github/workflows/full_tests.yml b/.github/workflows/full_tests.yml new file mode 100644 index 0000000..4906926 --- /dev/null +++ b/.github/workflows/full_tests.yml @@ -0,0 +1,26 @@ +name: Test on Ubuntu + +on: + pull_request: + branches: [dev] + types: [synchronize, opened, reopened] + + +jobs: + build-and-test: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install package + run: | + python -m pip install --upgrade pip + pip install .[test] + - name: Pytest + run: | + pytest -v diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml new file mode 100644 index 0000000..7d5c266 --- /dev/null +++ b/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,28 @@ +name: Release to PyPI + +on: + push: + tags: + - '*' +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install Tools + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine build + - name: Package and Upload + env: + STACKMANAGER_VERSION: ${{ github.event.release.tag_name }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + python -m build --sdist --wheel + twine upload dist/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7be81c7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: fix-encoding-pragma + exclude: tests/test_data + - id: trailing-whitespace + exclude: tests/test_data + - id: end-of-file-fixer + exclude: tests/test_data + - id: check-docstring-first + - id: debug-statements + - id: check-toml + - id: check-yaml + exclude: tests/test_data + - id: requirements-txt-fixer + - id: detect-private-key + - id: check-merge-conflict + + - repo: https://github.com/psf/black + rev: 24.4.2 + hooks: + - id: black + exclude: tests/test_data + - id: black-jupyter + + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + args: ["--profile", "black"] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.4 + hooks: + - id: ruff diff --git a/head_direction/__init__.py b/head_direction/__init__.py deleted file mode 100644 index 26fa6fc..0000000 --- a/head_direction/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .head import (head_direction_rate, head_direction_score, head_direction) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5c90c49 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,59 @@ +[project] +name = "head_direction" +version = "0.1.0" +authors = [ + { name = "Mikkel Lepperod", email = "mikkel@simula.no" }, + { name = "Alessio Buccino", email = "alessiop.buccino@gmail.com" }, +] + +description = "Compute spatial maps for neural data." +readme = "README.md" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +dependencies = [ + "numpy<2", + "scipy", + "astropy", + "pycircstat", + "pandas", + "elephant", + "matplotlib", + "nose" +] + +[project.urls] +homepage = "https://github.com/CINPLA/head-directopm" +repository = "https://github.com/CINPLA/head-direction" + +[build-system] +requires = ["setuptools>=62.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +where = ["src"] +include = ["head_direction*"] +namespaces = false + +[project.optional-dependencies] +dev = ["pre-commit", "black[jupyter]", "isort", "ruff"] +test = ["pytest", "pytest-cov", "pytest-dependency", "mountainsort5"] +docs = ["sphinx-gallery", "sphinx_rtd_theme"] +full = [ + "head_direction[dev]", + "head_direction[test]", + "head_direction[docs]", +] + +[tool.coverage.run] +omit = ["tests/*"] + +[tool.black] +line-length = 120 diff --git a/setup.py b/setup.py index 43551a6..7a58127 100644 --- a/setup.py +++ b/setup.py @@ -1,31 +1,6 @@ # -*- coding: utf-8 -*- -from setuptools import setup -import os -from setuptools import setup, find_packages +import setuptools -long_description = open("README.md").read() - -install_requires = [ - 'numpy>=1.9', - 'scipy', - 'astropy', - 'pandas>=0.14.1', - 'elephant', - 'matplotlib'] -extras_require = { - 'testing': ['pytest'], - 'docs': ['numpydoc>=0.5', - 'sphinx>=1.2.2', - 'sphinx_rtd_theme'] -} - -setup( - name="head_direction", - install_requires=install_requires, - tests_require=install_requires, - extras_require=extras_require, - packages=find_packages(), - include_package_data=True, - version='0.1', -) +if __name__ == "__main__": + setuptools.setup() diff --git a/src/head_direction/__init__.py b/src/head_direction/__init__.py new file mode 100644 index 0000000..88f0122 --- /dev/null +++ b/src/head_direction/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +import head + +__all__ = ["head"] diff --git a/head_direction/head.py b/src/head_direction/head.py similarity index 92% rename from head_direction/head.py rename to src/head_direction/head.py index e2011c1..cbe4390 100644 --- a/head_direction/head.py +++ b/src/head_direction/head.py @@ -1,9 +1,10 @@ +# -*- coding: utf-8 -*- import numpy as np + from .tools import moving_average -def head_direction_rate(spike_train, head_angles, t, - n_bins=36, avg_window=4): +def head_direction_rate(spike_train, head_angles, t, n_bins=36, avg_window=4): """ Calculeate firing rate at head direction in binned head angles for time t. Moving average filter is applied on firing rate @@ -37,7 +38,7 @@ def head_direction_rate(spike_train, head_angles, t, spikes_in_ang, _ = np.histogram(head_angles, weights=spikes_in_bin, bins=ang_bins) time_in_ang, _ = np.histogram(head_angles, weights=time_in_bin, bins=ang_bins) - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): rate_in_ang = np.divide(spikes_in_ang, time_in_ang) rate_in_ang = moving_average(rate_in_ang, avg_window) return ang_bins[:-1], rate_in_ang @@ -59,8 +60,8 @@ def head_direction_score(head_angle_bins, rate): out : float, float mean angle, mean vector length """ - import math import pycircstat as pc + nanIndices = np.where(np.isnan(rate)) head_angle_bins = np.delete(head_angle_bins, nanIndices) mean_ang = pc.mean(head_angle_bins, w=rate) @@ -69,7 +70,7 @@ def head_direction_score(head_angle_bins, rate): return mean_ang, mean_vec_len -def head_direction(x1, y1, x2, y2, t, filt=2.): +def head_direction(x1, y1, x2, y2, t, filt=2.0): """ Calculeate head direction in angles or radians for time t @@ -98,7 +99,7 @@ def head_direction(x1, y1, x2, y2, t, filt=2.): r = np.linalg.norm(dr, axis=0) r_mean = np.mean(r) r_std = np.std(r) - mask = (r > r_mean - filt*r_std) + mask = r > r_mean - filt * r_std x1 = x1[mask] y1 = y1[mask] x2 = x2[mask] diff --git a/head_direction/tools.py b/src/head_direction/tools.py similarity index 90% rename from head_direction/tools.py rename to src/head_direction/tools.py index 4af2b35..4dd7936 100644 --- a/head_direction/tools.py +++ b/src/head_direction/tools.py @@ -1,9 +1,9 @@ +# -*- coding: utf-8 -*- import numpy as np -import scipy.signal as sig def unit_vector(v): - """ Return unit vector of v + """Return unit vector of v modified from David Wolever, https://stackoverflow.com/questions/2827393/angles -between-two-n-dimensional-vectors-in-python @@ -47,11 +47,11 @@ def moving_average(vector, N): if N * 2 > len(vector): raise ValueError('Window must be at least half of "len(vector)"') vector = np.concatenate((vector[-N:], vector, vector[:N])) - return np.convolve(vector, np.ones((N,)) / N, mode='same')[N:-N] + return np.convolve(vector, np.ones((N,)) / N, mode="same")[N:-N] def angle_between_vectors(v1, v2): - """ Returns the angle in radians between vectors 'v1' and 'v2' + """Returns the angle in radians between vectors 'v1' and 'v2' modified from David Wolever, https://stackoverflow.com/questions/2827393/angles -between-two-n-dimensional-vectors-in-python diff --git a/head_direction/tests/test_head.py b/tests/test_head.py similarity index 50% rename from head_direction/tests/test_head.py rename to tests/test_head.py index 23dbdfc..fab19cf 100644 --- a/head_direction/tests/test_head.py +++ b/tests/test_head.py @@ -1,75 +1,74 @@ -import pytest +# -*- coding: utf-8 -*- import numpy as np -import math + +from head_direction.head import ( + head_direction, + head_direction_rate, + head_direction_score, +) + def test_head_direction_45(): - from head_direction.head import head_direction - x1 = np.linspace(.01,1,10) + x1 = np.linspace(0.01, 1, 10) y1 = x1 - x2 = x1 + .01 # 1cm between - y2 = x1 - .01 + x2 = x1 + 0.01 # 1cm between + y2 = x1 - 0.01 t = x1 a, t = head_direction(x1, y1, x2, y2, t) assert np.allclose(a, np.pi / 4) def test_head_direction_135(): - from head_direction.head import head_direction - x1 = np.linspace(.01,1,10)[::-1] + x1 = np.linspace(0.01, 1, 10)[::-1] y1 = x1[::-1] - x2 = x1 - .01 # 1cm between - y2 = y1 - .01 + x2 = x1 - 0.01 # 1cm between + y2 = y1 - 0.01 t = x1 a, t = head_direction(x1, y1, x2, y2, t) assert np.allclose(a, np.pi - np.pi / 4) def test_head_direction_225(): - from head_direction.head import head_direction - x1 = np.linspace(.01,1,10)[::-1] + x1 = np.linspace(0.01, 1, 10)[::-1] y1 = x1 - x2 = x1 - .01 # 1cm between - y2 = y1 + .01 + x2 = x1 - 0.01 # 1cm between + y2 = y1 + 0.01 t = x1 a, t = head_direction(x1, y1, x2, y2, t) assert np.allclose(a, np.pi + np.pi / 4) def test_head_direction_reverse_315(): - from head_direction.head import head_direction - x1 = np.linspace(.01,1,10) + x1 = np.linspace(0.01, 1, 10) y1 = x1[::-1] - x2 = x1 + .01 # 1cm between - y2 = y1 + .01 + x2 = x1 + 0.01 # 1cm between + y2 = y1 + 0.01 t = x1 a, t = head_direction(x1, y1, x2, y2, t) assert np.allclose(a, 2 * np.pi - np.pi / 4) def test_head_rate(): - from head_direction.head import head_direction, head_direction_rate - x1 = np.linspace(.01,1,10) + x1 = np.linspace(0.01, 1, 10) y1 = x1 - x2 = x1 + .01 # 1cm between - y2 = x1 - .01 - t = np.linspace(0,1,10) + x2 = x1 + 0.01 # 1cm between + y2 = x1 - 0.01 + t = np.linspace(0, 1, 10) a, t = head_direction(x1, y1, x2, y2, t) - sptr = np.linspace(0,1,100) + sptr = np.linspace(0, 1, 100) bins, rate = head_direction_rate(sptr, a, t, n_bins=8, avg_window=1) assert bins[1] == np.pi / 4 - assert abs(rate[1] - 100) < .5 + assert abs(rate[1] - 100) < 0.5 def test_head_score(): - from head_direction.head import ( - head_direction, head_direction_rate, head_direction_score) - x1 = np.linspace(.01,1,10) + x1 = np.linspace(0.01, 1, 10) y1 = x1 - x2 = x1 + .01 # 1cm between - y2 = x1 - .01 - t = np.linspace(0,1,10) + x2 = x1 + 0.01 # 1cm between + y2 = x1 - 0.01 + t = np.linspace(0, 1, 10) a, t = head_direction(x1, y1, x2, y2, t) - sptr = np.linspace(0,1,100) + sptr = np.linspace(0, 1, 100) bins, rate = head_direction_rate(sptr, a, t, n_bins=100, avg_window=2) ang, score = head_direction_score(bins, rate) assert abs(score - 1) < 0.001