diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..49d7f52 Binary files /dev/null and b/.coverage differ diff --git a/.gitignore b/.gitignore index 403f8af..c27b58e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ /develop/ /deploy/docs/ /deploy/dist/ +/deploy/coverage/ +/deploy/coverage.sqlite /dist/ **/__pycache__ **/.pytest_cache diff --git a/ReadMe.md b/ReadMe.md index e5d44c9..abbfa5b 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,4 +1,5 @@ -Kernel density estimation via diffusion in 1d and 2d. +# KDE-diffusion +*Kernel density estimation via diffusion in 1d and 2d* Provides the fast, adaptive kernel density estimator based on linear diffusion processes for one-dimensional and two-dimensional input data @@ -9,15 +10,14 @@ in Python, with added test coverage. Find the full [documentation on Read-the-Docs][docs]. -[![citation](https://zenodo.org/badge/263433787.svg)](https://zenodo.org/badge/latestdoi/263433787) -[![license](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) -[![release](https://img.shields.io/pypi/v/kde-diffusion.svg)](https://pypi.python.org/pypi/kde-diffusion) -[![downloads](https://pepy.tech/badge/kde-diffusion)](https://pepy.tech/project/kde-diffusion) -[![documentation](https://readthedocs.org/projects/kde-diffusion/badge/?version=latest)](https://kde-diffusion.readthedocs.io/en/latest/?badge=latest) - [paper]: https://dx.doi.org/10.1214/10-AOS799 [kde1d]: https://mathworks.com/matlabcentral/fileexchange/14034 [kde2d]: https://mathworks.com/matlabcentral/fileexchange/17204 [docs]: https://kde-diffusion.readthedocs.io - +[![citation](https://zenodo.org/badge/263433787.svg)](https://zenodo.org/badge/latestdoi/263433787) +[![license](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) +[![release](https://img.shields.io/pypi/v/kde-diffusion.svg)](https://pypi.python.org/pypi/kde-diffusion) +[![downloads](https://pepy.tech/badge/kde-diffusion)](https://pepy.tech/project/kde-diffusion) +![coverage](tests/coverage.svg?raw=true) +[![documentation](https://readthedocs.org/projects/kde-diffusion/badge/?version=latest)](https://kde-diffusion.readthedocs.io/en/latest/?badge=latest) diff --git a/deploy/build.py b/deploy/build.py index c6e8b90..5ac82ed 100644 --- a/deploy/build.py +++ b/deploy/build.py @@ -1,4 +1,4 @@ -"""Builds documentation and installation package.""" +"""Builds the installation package.""" __license__ = 'MIT' from subprocess import run @@ -6,11 +6,6 @@ from shutil import rmtree root = Path(__file__).resolve().parent.parent - -process = run(['sphinx-build', 'docs', 'deploy/docs'], cwd=root) -if process.returncode: - raise RuntimeError('Error while building documentation.') - process = run(['flit', 'build', '--format', 'wheel'], cwd=root) if process.returncode: raise RuntimeError('Error while building wheel.') diff --git a/deploy/clean.py b/deploy/clean.py index 03b9d3f..f51710e 100644 --- a/deploy/clean.py +++ b/deploy/clean.py @@ -4,13 +4,13 @@ from pathlib import Path from shutil import rmtree -root = Path(__file__).absolute().parent.parent +root = Path(__file__).resolve().parent.parent for folder in root.rglob('__pycache__'): - rmtree(folder) + rmtree(folder, ignore_errors=True) for folder in root.rglob('.pytest_cache'): rmtree(folder) -for folder in (root/'deploy'/'dist', root/'deploy'/'docs'): - rmtree(folder, ignore_errors=True) +for folder in ('docs', 'dist', 'coverage'): + rmtree(root/'deploy'/folder, ignore_errors=True) diff --git a/deploy/coverage.py b/deploy/coverage.py new file mode 100644 index 0000000..6a80118 --- /dev/null +++ b/deploy/coverage.py @@ -0,0 +1,24 @@ +"""Measures code coverage by test suite.""" +__license__ = 'MIT' + + +from subprocess import run +from pathlib import Path + + +here = Path(__file__).resolve().parent +root = here.parent +file = here/'coverage.sqlite' + +run(['pytest', '--cov'], cwd=root) + +print('Rendering coverage report.') +folder = (here/'coverage').relative_to(root) +run(['coverage', 'html', f'--directory={folder}'], cwd=root) + +badge = root/'tests'/file.with_suffix('.svg').name +if badge.exists(): + print('Coverage badge already exists.') +else: + print('Rendering coverage badge.') + run(['coverage-badge', '-f', '-o', str(badge)], cwd=root) diff --git a/deploy/notes.txt b/deploy/notes.txt deleted file mode 100644 index a67cf3f..0000000 --- a/deploy/notes.txt +++ /dev/null @@ -1,14 +0,0 @@ -Releases -‾‾‾‾‾‾‾‾ - -Steps to take when releasing a new version: -• Bump version number and enter current date in `__init__.py`. -• Add a dedicated commit for the version bump. -• Tag the commit with the version number, for example: `git tag -a v0.9.0`. -• Enter the change log relative to the previous release as an annotation. -• Push the commit (but not the tag): `git push origin main`. -• Check that documentation built successfully on Read-the-Docs. -• Publish to PyPI by running `deploy/publish.py`. -• Check that meta information is correct on PyPI. -• Then push the tag: `git push --tags`. -• Publish the new release on GitHub with the change log. diff --git a/deploy/publish.py b/deploy/publish.py index fd6dd01..e9e1db9 100644 --- a/deploy/publish.py +++ b/deploy/publish.py @@ -5,7 +5,6 @@ from pathlib import Path root = Path(__file__).resolve().parent.parent - process = run(['flit', 'publish', '--format', 'wheel'], cwd=root) if process.returncode: - raise RuntimeError('Error while publishing to PyPI.') + raise RuntimeError('Error while publishing on PyPI.') diff --git a/deploy/release.md b/deploy/release.md new file mode 100644 index 0000000..2310328 --- /dev/null +++ b/deploy/release.md @@ -0,0 +1,11 @@ +Steps to take when releasing a new version: +* Bump version number and enter current date in `__init__.py`. +* Add a dedicated commit for the version bump. +* Tag the commit with the version number, for example: `git tag -a v1.0.3`. +* Enter the release notes as an annotation. +* Push the commit (but not the tag): `git push origin main`. +* Check that documentation built successfully on Read-the-Docs. +* Publish on PyPI by running `deploy/publish.py`. +* Check that meta information is correct on PyPI. +* Then push the tag: `git push --tags`. +* Create a new release on GitHub and add the release notes. diff --git a/deploy/render.py b/deploy/render.py new file mode 100644 index 0000000..56c3428 --- /dev/null +++ b/deploy/render.py @@ -0,0 +1,10 @@ +"""Renders the documentation.""" +__license__ = 'MIT' + +from subprocess import run +from pathlib import Path + +root = Path(__file__).resolve().parent.parent +process = run(['sphinx-build', 'docs', 'deploy/docs'], cwd=root) +if process.returncode: + raise RuntimeError('Error while rendering documentation.') diff --git a/pyproject.toml b/pyproject.toml index e213a8b..1af2b1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,8 +22,8 @@ classifiers = [ 'License :: OSI Approved :: MIT License'] [tool.flit.metadata.requires-extra] -test = ['pyTest'] -doc = ['Sphinx', 'Sphinx-RTD-theme', 'reCommonMark'] +doc = ['Sphinx', 'Sphinx-RTD-theme', 'MyST-parser', 'CommonMark'] +test = ['pyTest', 'pyTest-cov', 'coverage-badge', 'flake8'] [tool.flit.metadata.urls] Documentation = 'https://kde-diffusion.readthedocs.io' diff --git a/tests/coverage.svg b/tests/coverage.svg new file mode 100644 index 0000000..ef08b50 --- /dev/null +++ b/tests/coverage.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + coverage + coverage + 100% + 100% + + diff --git a/tests/test_1d.py b/tests/test_1d.py index dd256a5..3a2a17b 100644 --- a/tests/test_1d.py +++ b/tests/test_1d.py @@ -27,7 +27,7 @@ def setup_module(): # Tests # ######################################## -def test_density(): +def test_reference(): x = reference['x'] N = reference['N'] assert N == len(x) @@ -40,11 +40,27 @@ def test_density(): assert isclose(bandwidth, reference['bandwidth']).all() +def test_arguments(): + (density, grid, bandwidth) = kde1d([-2, -1, 0, +1, +2]*20, 4) + assert len(grid) == 4 + assert isclose(grid.min(), -2.4) + assert isclose(grid.max(), +1.2) + (density, grid, bandwidth) = kde1d([-2, -1, 0, +1, +2]*20, 4, 2) + assert isclose(grid.min(), -2) + assert isclose(grid.max(), +1) + try: + kde1d([-2, -1, 0, +1, +2]*10, 4) + raised_error = False + except ValueError: + raised_error = True + assert raised_error + + ######################################## # Main # ######################################## if __name__ == '__main__': - # Runs if test script is executed directly, and not via pytest. setup_module() - test_density() + test_reference() + test_arguments() diff --git a/tests/test_2d.py b/tests/test_2d.py index 57c2021..709a0d7 100644 --- a/tests/test_2d.py +++ b/tests/test_2d.py @@ -27,7 +27,7 @@ def setup_module(): # Test # ######################################## -def test_density(): +def test_reference(): x = reference['x'] y = reference['y'] N = reference['N'] @@ -47,11 +47,49 @@ def test_density(): assert isclose(bandwidth, reference['bandwidth']).all() +def test_arguments(): + samples = [-2, -1, 0, +1, +2] + (density, grid, bandwith) = kde2d(samples*5, samples*5, 16) + assert len(grid[0]) == 16 + assert len(grid[1]) == 16 + assert isclose(grid[0].min(), -3.0) + assert isclose(grid[0].max(), +2.625) + assert isclose(grid[1].min(), -3.0) + assert isclose(grid[1].max(), +2.625) + (density, grid, bandwidth) = kde2d(samples*5, samples*5, 16, (2, None)) + assert isclose(grid[0].min(), -2) + assert isclose(grid[0].max(), +1.75) + assert isclose(grid[1].min(), -3) + assert isclose(grid[1].max(), +2.625) + (density, grid, bandwidth) = kde2d(samples*5, samples*5, 16, (None, 2)) + assert isclose(grid[0].min(), -3) + assert isclose(grid[0].max(), +2.625) + assert isclose(grid[1].min(), -2) + assert isclose(grid[1].max(), +1.75) + (density, grid, bandwidth) = kde2d(samples*5, samples*5, 16, 2) + assert isclose(grid[0].min(), -2) + assert isclose(grid[0].max(), +1.75) + assert isclose(grid[1].min(), -2) + assert isclose(grid[1].max(), +1.75) + try: + kde2d(samples, samples*2, 16) + raised_error = False + except ValueError: + raised_error = True + assert raised_error + try: + kde2d(samples, samples, 16) + raised_error = False + except ValueError: + raised_error = True + assert raised_error + + ######################################## # Main # ######################################## if __name__ == '__main__': - # Runs if test script is executed directly, and not via pytest. setup_module() - test_density() + test_reference() + test_arguments() diff --git a/tox.ini b/tox.ini index f2b92c3..f27c2c9 100644 --- a/tox.ini +++ b/tox.ini @@ -18,3 +18,16 @@ ignore = E741 per-file-ignores = kde_diffusion/__init__.py:F401 + + +[pytest] +addopts = --verbose +testpaths = + tests +filterwarnings = + ignore::UserWarning + + +[coverage:run] +source = kde_diffusion/ +data_file = deploy/coverage.sqlite