From c35a511c845c178938817108db1013624f1ea587 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Tue, 14 Feb 2023 00:05:31 -0600 Subject: [PATCH] Modern packaging Restructure the package to use modern Python packaging methods. - Drop support for Python < 3.7 - Use pyproject.toml - Use cibuildwheel to build wheels for many linux distros - Introduce automation with GitHub actions for builds - Use setuptools_scm to automate versioning - Assume Cython is available when preparing a source distribution --- .github/workflows/build_wheels.yml | 25 +++ .gitignore | 5 +- MANIFEST.in | 2 +- Makefile | 59 +++--- conftest.py | 6 - dev_requirements.txt | 1 - dist_requirements.txt | 4 - pyemd/__about__.py | 20 -- pyproject.toml | 35 +++- pytest.ini | 6 +- requirements.txt | 4 - setup.py | 104 +++------- {pyemd => src/pyemd}/__init__.py | 6 +- {pyemd => src/pyemd}/emd.pyx | 0 {pyemd => src/pyemd}/lib/EMD_DEFS.hpp | 0 {pyemd => src/pyemd}/lib/emd_hat.hpp | 0 {pyemd => src/pyemd}/lib/emd_hat_impl.hpp | 0 .../lib/emd_hat_signatures_interface.hpp | 0 {pyemd => src/pyemd}/lib/flow_utils.hpp | 0 {pyemd => src/pyemd}/lib/min_cost_flow.hpp | 0 test/test_pyemd.py | 192 +++++++++--------- test_requirements.txt | 2 - 22 files changed, 226 insertions(+), 245 deletions(-) create mode 100644 .github/workflows/build_wheels.yml delete mode 100644 conftest.py delete mode 100644 dev_requirements.txt delete mode 100644 dist_requirements.txt delete mode 100644 pyemd/__about__.py delete mode 100644 requirements.txt rename {pyemd => src/pyemd}/__init__.py (95%) rename {pyemd => src/pyemd}/emd.pyx (100%) rename {pyemd => src/pyemd}/lib/EMD_DEFS.hpp (100%) rename {pyemd => src/pyemd}/lib/emd_hat.hpp (100%) rename {pyemd => src/pyemd}/lib/emd_hat_impl.hpp (100%) rename {pyemd => src/pyemd}/lib/emd_hat_signatures_interface.hpp (100%) rename {pyemd => src/pyemd}/lib/flow_utils.hpp (100%) rename {pyemd => src/pyemd}/lib/min_cost_flow.hpp (100%) delete mode 100644 test_requirements.txt diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml new file mode 100644 index 0000000..668973d --- /dev/null +++ b/.github/workflows/build_wheels.yml @@ -0,0 +1,25 @@ +name: Build + +on: [push, pull_request] + +jobs: + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-20.04, windows-2019, macos-11] + + steps: + - uses: actions/checkout@v3 + + - name: Build wheels + uses: pypa/cibuildwheel@v2.12.0 + with: + package-dir: . + output-dir: wheelhouse + config-file: "{package}/pyproject.toml" + + - uses: actions/upload-artifact@v3 + with: + path: ./wheelhouse/*.whl diff --git a/.gitignore b/.gitignore index 710f5b5..7fa6859 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ __pycache__ .gitconfig .cache +.pytest_cache .tox .env .ropeproject @@ -8,7 +9,9 @@ __pycache__ *.pyc MANIFEST *.egg* +src/pyemd/emd.cpp +src/pyemd/_version.py build dist -pyemd/emd.cpp +wheelhouse ignore diff --git a/MANIFEST.in b/MANIFEST.in index 966751d..09593c5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -graft pyemd +graft src/pyemd graft test include README.rst diff --git a/Makefile b/Makefile index 80d8457..cbcb750 100644 --- a/Makefile +++ b/Makefile @@ -1,39 +1,44 @@ -.PHONY: default test build clean upload-dist test-dist sign-dist check-dist build-dist clean-dist +.PHONY: default clean develop test dist-clean build-local build dist-upload dist-test-upload dist-sign dist-check -src = pyemd -dist_dir = dist +src = src/pyemd +test = test +dist = wheelhouse +readme = README.rst -default: build +default: develop -test: build - py.test +clean: + rm -rf $(shell find . -name '__pycache__') + rm -rf $(shell find . -name '*.so') + rm -rf .eggs + rm -rf pyemd.egg-info + rm -rf dist + rm -rf build -build: clean +develop: clean python -m pip install -e . - # setup.py build_ext -b . -clean: - rm -f pyemd/*.so - rm -rf **/__pycache__ - rm -rf build - rm -rf pyemd.egg-info +test: develop + py.test -upload-dist: sign-dist - twine upload $(dist_dir)/* +build-local: clean + python -m build -test-dist: check-dist - twine upload --repository testpypi $(dist_dir)/* +dist-clean: + rm -rf $(dist) -sign-dist: check-dist - gpg --detach-sign -a dist/*.tar.gz - gpg --detach-sign -a dist/*.whl +build: dist-clean + cibuildwheel --platform linux --config-file pyproject.toml --output-dir $(dist) -check-dist: build-dist - twine check --strict dist/* +dist-upload: dist-sign + twine upload $(dist)/* -build-dist: clean-dist - python -m build - # python -m setup.py sdist bdist_wheel --dist-dir=$(dist_dir) +dist-test-upload: dist-check + twine upload --repository testpypi $(dist)/* + +dist-sign: dist-check + gpg --detach-sign -a $(dist)/*.tar.gz + gpg --detach-sign -a $(dist)/*.whl -clean-dist: - rm -r $(dist_dir) +dist-check: dist-build + twine check --strict $(dist)/* diff --git a/conftest.py b/conftest.py deleted file mode 100644 index 24326d9..0000000 --- a/conftest.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# conftest.py - - -collect_ignore = ["setup.py", "build", "dist"] diff --git a/dev_requirements.txt b/dev_requirements.txt deleted file mode 100644 index 142a744..0000000 --- a/dev_requirements.txt +++ /dev/null @@ -1 +0,0 @@ -Cython >=0.20.2 diff --git a/dist_requirements.txt b/dist_requirements.txt deleted file mode 100644 index 8409743..0000000 --- a/dist_requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -docutils -pygments -twine -wheel diff --git a/pyemd/__about__.py b/pyemd/__about__.py deleted file mode 100644 index 48408e8..0000000 --- a/pyemd/__about__.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# __about__.py - -"""PyEMD metadata""" - -__title__ = 'pyemd' -__version__ = '0.5.1' -__description__ = ("A Python wrapper for Ofir Pele and Michael Werman's " - "implementation of the Earth Mover's Distance.") -__author__ = 'Will Mayner' -__author_email__ = 'wmayner@gmail.com' -__author_website__ = 'http://willmayner.com' -__license__ = 'MIT' -__copyright__ = 'Copyright (c) 2014-2017 Will Mayner' -__url__ = 'https://github.com/wmayner/pyemd' - -__all__ = ['__title__', '__version__', '__description__', '__author__', - '__author_email__', '__author_website__', '__license__', - '__copyright__', '__url__'] diff --git a/pyproject.toml b/pyproject.toml index 44c8c55..cff1c25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,24 +1,43 @@ [build-system] -requires = ["setuptools>=61.0", "cython", "wheel"] +requires = [ + "cython", + "oldest-supported-numpy", + "setuptools >= 45", + "setuptools_scm", + "wheel", +] build-backend = "setuptools.build_meta" [project] name = "pyemd" -version = "0.5.1" -authors = [{ name = "Will Mayner", email = "wmayner@gmail.com" }] -description = "A Python wrapper for Ofir Pele and Michael Werman's implementation of the Earth Mover's Distance." -readme = "README.md" license = { file = "LICENSE" } +description = "A Python wrapper for Ofir Pele and Michael Werman's implementation of the Earth Mover's Distance." +authors = [{ name = "Will Mayner", email = "wmayner@gmail.com" }] +requires-python = ">=3.7" +dependencies = ["numpy >= 1.9.0"] +readme = "README.rst" classifiers = [ - 'Development Status :: 3 - Alpha', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Natural Language :: English', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', ] -dynamic = ['dependencies'] +dynamic = ['version'] + +[project.optional-dependencies] +test = ['pytest'] +dist = ['cibuildwheel', 'setuptools_scm'] [project.urls] "Homepage" = "https://github.com/wmayner/pyemd" "Bug Tracker" = "https://github.com/wmayner/pyemd/issues" + +[tool.setuptools_scm] +write_to = "src/pyemd/_version.py" + +[tool.cibuildwheel] +skip = "cp36*" +build-verbosity = 2 +test-requires = ["pytest"] +test-command = "py.test {project}" diff --git a/pytest.ini b/pytest.ini index 1a744be..0254159 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,8 +4,12 @@ addopts = --tb=auto --doctest-glob='*.rst' --doctest-modules -vv + --ignore setup.py norecursedirs = + src dist build + wheelhouse + ignore + .git .tox - .eggs diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e0a2d12..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ --e . --r test_requirements.txt --r dev_requirements.txt --r dist_requirements.txt diff --git a/setup.py b/setup.py index 138b6b6..3fe6fd4 100644 --- a/setup.py +++ b/setup.py @@ -1,39 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import io import os import platform import sys -from warnings import warn +from distutils.sysconfig import get_config_var from setuptools import Extension, setup from setuptools.command.build_ext import build_ext as _build_ext -from setuptools.command.sdist import sdist as _sdist -from distutils.sysconfig import get_config_var -from distutils.version import LooseVersion + +from packaging.version import Version, parse as parse_version def is_platform_mac(): return sys.platform == "darwin" -# Alias ModuleNotFound for Python <= 3.5 -if sys.version_info < (3, 6): - ModuleNotFoundError = ImportError - # For macOS, ensure extensions are built for macOS 10.9 when compiling on a # 10.9 system or above, overriding distutils behaviour which is to target # the version that Python was built for. This may be overridden by setting # MACOSX_DEPLOYMENT_TARGET before calling setup.py -if is_platform_mac(): - import packaging - - if "MACOSX_DEPLOYMENT_TARGET" not in os.environ: - current_system = LooseVersion(platform.mac_ver()[0]) - python_target = LooseVersion(get_config_var("MACOSX_DEPLOYMENT_TARGET")) - if python_target < "10.9" and current_system >= "10.9": - os.environ["MACOSX_DEPLOYMENT_TARGET"] = "10.9" +if is_platform_mac() and "MACOSX_DEPLOYMENT_TARGET" not in os.environ: + current_system = parse_version(platform.mac_ver()[0]) + python_target = parse_version(get_config_var("MACOSX_DEPLOYMENT_TARGET")) + mac_deployment_target = Version("10.9") + if ( + python_target < mac_deployment_target + and current_system >= mac_deployment_target + ): + os.environ["MACOSX_DEPLOYMENT_TARGET"] = str(mac_deployment_target) try: from Cython.Build import cythonize as _cythonize @@ -43,10 +38,10 @@ def is_platform_mac(): USE_CYTHON = False -def cythonize(extensions, **_ignore): +def cythonize(extensions, **kwargs): # Attempt to use Cython if USE_CYTHON: - return _cythonize(extensions) + return _cythonize(extensions, **kwargs) # Cython is not available for extension in extensions: sources = [] @@ -63,25 +58,15 @@ def cythonize(extensions, **_ignore): return extensions -EXTENSIONS = [Extension("pyemd.emd", sources=["pyemd/emd.pyx"], language="c++")] - -EXT_MODULES = cythonize(EXTENSIONS) - +EXTENSIONS = [ + Extension( + "pyemd.emd", + sources=["src/pyemd/emd.pyx"], + language="c++", + ) +] -class sdist(_sdist): - def run(self): - # Ensure the compiled Cython files in the distribution are up-to-date - if USE_CYTHON: - _cythonize(EXTENSIONS) - else: - warn( - "\n\n\033[91m\033[1m WARNING: " - "IF YOU A PREPARING A DISTRIBUTION: Cython is not available! " - "The cythonized `*.cpp` files may be out of date. Please " - "install Cython and run `sdist` again." - "\033[0m\n" - ) - _sdist.run(self) +EXT_MODULES = cythonize(EXTENSIONS, language_level=3) # See https://stackoverflow.com/a/21621689/1085344 @@ -96,46 +81,21 @@ def finalize_options(self): self.include_dirs.append(numpy.get_include()) -CMDCLASS = {"sdist": sdist, "build_ext": build_ext} - +CMDCLASS = {"build_ext": build_ext} -with io.open("README.rst", encoding="utf-8") as f: - README = f.read() - -ABOUT = {} -with open("./pyemd/__about__.py") as f: - exec(f.read(), ABOUT) - - -REQUIRES = ["packaging"] - -NUMPY_REQUIREMENT = [ - "numpy >=1.9.0, <1.20.0; python_version<='3.6'", - "numpy >=1.9.0, <2.0.0; python_version>'3.6'", -] - -# Copied from scipy's installer, to solve the same issues they saw: - -# Figure out whether to add ``*_requires = ['numpy']``. -# We don't want to do that unconditionally, because we risk updating -# an installed numpy which fails too often. Just if it's not installed, we -# may give it a try. See scipy gh-3379. -try: - import numpy -except ImportError: # We do not have numpy installed - REQUIRES += NUMPY_REQUIREMENT -else: - # If we're building a wheel, assume there already exist numpy wheels - # for this platform, so it is safe to add numpy to build requirements. - # See scipy gh-5184. - if "bdist_wheel" in sys.argv[1:]: - REQUIRES += NUMPY_REQUIREMENT +SETUP_REQUIRES = ["setuptools_scm", "packaging"] setup( + name="pyemd", packages=["pyemd", "pyemd.lib"], - install_requires=REQUIRES, - setup_requires=REQUIRES, + package_dir={ + "pyemd": "src/pyemd", + "pyemd.lib": "src/pyemd/lib", + }, + include_package_data=True, ext_modules=EXT_MODULES, cmdclass=CMDCLASS, + setup_requires=SETUP_REQUIRES, + use_scm_version=True, ) diff --git a/pyemd/__init__.py b/src/pyemd/__init__.py similarity index 95% rename from pyemd/__init__.py rename to src/pyemd/__init__.py index b0245c0..713ecb0 100644 --- a/pyemd/__init__.py +++ b/src/pyemd/__init__.py @@ -71,5 +71,9 @@ :license: See the LICENSE file. """ -from .__about__ import * from .emd import emd, emd_with_flow, emd_samples + +try: + from ._version import version as __version__ +except ImportError: + __version__ = "unknown version" diff --git a/pyemd/emd.pyx b/src/pyemd/emd.pyx similarity index 100% rename from pyemd/emd.pyx rename to src/pyemd/emd.pyx diff --git a/pyemd/lib/EMD_DEFS.hpp b/src/pyemd/lib/EMD_DEFS.hpp similarity index 100% rename from pyemd/lib/EMD_DEFS.hpp rename to src/pyemd/lib/EMD_DEFS.hpp diff --git a/pyemd/lib/emd_hat.hpp b/src/pyemd/lib/emd_hat.hpp similarity index 100% rename from pyemd/lib/emd_hat.hpp rename to src/pyemd/lib/emd_hat.hpp diff --git a/pyemd/lib/emd_hat_impl.hpp b/src/pyemd/lib/emd_hat_impl.hpp similarity index 100% rename from pyemd/lib/emd_hat_impl.hpp rename to src/pyemd/lib/emd_hat_impl.hpp diff --git a/pyemd/lib/emd_hat_signatures_interface.hpp b/src/pyemd/lib/emd_hat_signatures_interface.hpp similarity index 100% rename from pyemd/lib/emd_hat_signatures_interface.hpp rename to src/pyemd/lib/emd_hat_signatures_interface.hpp diff --git a/pyemd/lib/flow_utils.hpp b/src/pyemd/lib/flow_utils.hpp similarity index 100% rename from pyemd/lib/flow_utils.hpp rename to src/pyemd/lib/flow_utils.hpp diff --git a/pyemd/lib/min_cost_flow.hpp b/src/pyemd/lib/min_cost_flow.hpp similarity index 100% rename from pyemd/lib/min_cost_flow.hpp rename to src/pyemd/lib/min_cost_flow.hpp diff --git a/test/test_pyemd.py b/test/test_pyemd.py index 5c784d6..b3f211b 100644 --- a/test/test_pyemd.py +++ b/test/test_pyemd.py @@ -8,6 +8,7 @@ from pyemd import emd, emd_samples, emd_with_flow + EMD_PRECISION = 5 FLOW_PRECISION = 4 @@ -30,60 +31,52 @@ def emd_flow_assert(got, expected): def test_emd_1(): first_signature = np.array([0.0, 1.0]) second_signature = np.array([5.0, 3.0]) - distance_matrix = np.array([[0.0, 0.5], - [0.5, 0.0]]) - emd_assert( - emd(first_signature, second_signature, distance_matrix), - 3.5 - ) + distance_matrix = np.array([[0.0, 0.5], [0.5, 0.0]]) + emd_assert(emd(first_signature, second_signature, distance_matrix), 3.5) def test_emd_2(): first_signature = np.array([1.0, 1.0]) second_signature = np.array([1.0, 1.0]) - distance_matrix = np.array([[0.0, 1.0], - [1.0, 0.0]]) - emd_assert( - emd(first_signature, second_signature, distance_matrix), - 0.0 - ) + distance_matrix = np.array([[0.0, 1.0], [1.0, 0.0]]) + emd_assert(emd(first_signature, second_signature, distance_matrix), 0.0) def test_emd_3(): first_signature = np.array([6.0, 1.0]) second_signature = np.array([1.0, 7.0]) - distance_matrix = np.array([[0.0, 0.0], - [0.0, 0.0]]) - emd_assert( - emd(first_signature, second_signature, distance_matrix), - 0.0 - ) + distance_matrix = np.array([[0.0, 0.0], [0.0, 0.0]]) + emd_assert(emd(first_signature, second_signature, distance_matrix), 0.0) def test_emd_4(): first_signature = np.array([1.0, 2.0, 1.0, 2.0]) second_signature = np.array([2.0, 1.0, 2.0, 1.0]) - distance_matrix = np.array([[0.0, 1.0, 1.0, 2.0], - [1.0, 0.0, 2.0, 1.0], - [1.0, 2.0, 0.0, 1.0], - [2.0, 1.0, 1.0, 0.0]]) - emd_assert( - emd(first_signature, second_signature, distance_matrix), - 2.0 + distance_matrix = np.array( + [ + [0.0, 1.0, 1.0, 2.0], + [1.0, 0.0, 2.0, 1.0], + [1.0, 2.0, 0.0, 1.0], + [2.0, 1.0, 1.0, 0.0], + ] ) + emd_assert(emd(first_signature, second_signature, distance_matrix), 2.0) def test_emd_extra_mass_penalty(): first_signature = np.array([0.0, 2.0, 1.0, 2.0]) second_signature = np.array([2.0, 1.0, 2.0, 1.0]) - distance_matrix = np.array([[0.0, 1.0, 1.0, 2.0], - [1.0, 0.0, 2.0, 1.0], - [1.0, 2.0, 0.0, 1.0], - [2.0, 1.0, 1.0, 0.0]]) + distance_matrix = np.array( + [ + [0.0, 1.0, 1.0, 2.0], + [1.0, 0.0, 2.0, 1.0], + [1.0, 2.0, 0.0, 1.0], + [2.0, 1.0, 1.0, 0.0], + ] + ) emd_assert( - emd(first_signature, second_signature, distance_matrix, - extra_mass_penalty=2.5), - 4.5 + emd(first_signature, second_signature, distance_matrix, extra_mass_penalty=2.5), + 4.5, ) @@ -93,8 +86,7 @@ def test_emd_extra_mass_penalty(): def test_emd_validate_larger_signatures_1(): first_signature = np.array([0.0, 1.0, 2.0]) second_signature = np.array([5.0, 3.0, 3.0]) - distance_matrix = np.array([[0.0, 0.5], - [0.5, 0.0]]) + distance_matrix = np.array([[0.0, 0.5], [0.5, 0.0]]) with pytest.raises(ValueError): emd(first_signature, second_signature, distance_matrix) @@ -102,8 +94,7 @@ def test_emd_validate_larger_signatures_1(): def test_emd_validate_larger_signatures_2(): first_signature = np.array([0.0, 1.0, 2.0]) second_signature = np.array([5.0, 3.0]) - distance_matrix = np.array([[0.0, 0.5], - [0.5, 0.0]]) + distance_matrix = np.array([[0.0, 0.5], [0.5, 0.0]]) with pytest.raises(ValueError): emd_with_flow(first_signature, second_signature, distance_matrix) @@ -119,9 +110,7 @@ def test_emd_validate_larger_signatures_3(): def test_emd_validate_different_signature_dims(): first_signature = np.array([0.0, 1.0]) second_signature = np.array([5.0, 3.0, 3.0]) - distance_matrix = np.array([[0.0, 0.5, 0.0], - [0.5, 0.0, 0.0], - [0.5, 0.0, 0.0]]) + distance_matrix = np.array([[0.0, 0.5, 0.0], [0.5, 0.0, 0.0], [0.5, 0.0, 0.0]]) with pytest.raises(ValueError): emd(first_signature, second_signature, distance_matrix) @@ -129,8 +118,7 @@ def test_emd_validate_different_signature_dims(): def test_emd_validate_symmetric_distance_matrix(): first_signature = np.array([0.0, 1.0]) second_signature = np.array([5.0, 3.0]) - distance_matrix = np.array([[0.0, 0.5, 3.0], - [0.5, 0.0]], dtype=object) + distance_matrix = np.array([[0.0, 0.5, 3.0], [0.5, 0.0]], dtype=object) with pytest.raises(ValueError): emd(first_signature, second_signature, distance_matrix) @@ -142,93 +130,102 @@ def test_emd_validate_symmetric_distance_matrix(): def test_emd_with_flow_1(): first_signature = np.array([0.0, 1.0]) second_signature = np.array([5.0, 3.0]) - distance_matrix = np.array([[0.0, 0.5], - [0.5, 0.0]]) + distance_matrix = np.array([[0.0, 0.5], [0.5, 0.0]]) emd_flow_assert( emd_with_flow(first_signature, second_signature, distance_matrix), - (3.5, [[0.0, 0.0], - [0.0, 1.0]]) + (3.5, [[0.0, 0.0], [0.0, 1.0]]), ) def test_emd_with_flow_2(): first_signature = np.array([1.0, 1.0]) second_signature = np.array([1.0, 1.0]) - distance_matrix = np.array([[0.0, 1.0], - [1.0, 0.0]]) + distance_matrix = np.array([[0.0, 1.0], [1.0, 0.0]]) emd_flow_assert( emd_with_flow(first_signature, second_signature, distance_matrix), - (0.0, [[1.0, 0.0], - [0.0, 1.0]]) + (0.0, [[1.0, 0.0], [0.0, 1.0]]), ) def test_emd_with_flow_3(): first_signature = np.array([6.0, 1.0]) second_signature = np.array([1.0, 7.0]) - distance_matrix = np.array([[0.0, 0.0], - [0.0, 0.0]]) + distance_matrix = np.array([[0.0, 0.0], [0.0, 0.0]]) emd_flow_assert( emd_with_flow(first_signature, second_signature, distance_matrix), - (0.0, [[1.0, 5.0], - [0.0, 1.0]]) + (0.0, [[1.0, 5.0], [0.0, 1.0]]), ) def test_emd_with_flow_4(): first_signature = np.array([1.0, 7.0]) second_signature = np.array([6.0, 1.0]) - distance_matrix = np.array([[0.0, 0.0], - [0.0, 0.0]]) + distance_matrix = np.array([[0.0, 0.0], [0.0, 0.0]]) emd_flow_assert( emd_with_flow(first_signature, second_signature, distance_matrix), - (0.0, [[1.0, 0.0], - [5.0, 1.0]]) + (0.0, [[1.0, 0.0], [5.0, 1.0]]), ) def test_emd_with_flow_5(): first_signature = np.array([3.0, 5.0]) second_signature = np.array([6.0, 2.0]) - distance_matrix = np.array([[0.0, 0.0], - [0.0, 0.0]]) + distance_matrix = np.array([[0.0, 0.0], [0.0, 0.0]]) emd_flow_assert( emd_with_flow(first_signature, second_signature, distance_matrix), - (0.0, [[3.0, 0.0], - [3.0, 2.0]]) + (0.0, [[3.0, 0.0], [3.0, 2.0]]), ) def test_emd_with_flow_6(): first_signature = np.array([1.0, 2.0, 1.0, 2.0]) second_signature = np.array([2.0, 1.0, 2.0, 1.0]) - distance_matrix = np.array([[0.0, 1.0, 1.0, 2.0], - [1.0, 0.0, 2.0, 1.0], - [1.0, 2.0, 0.0, 1.0], - [2.0, 1.0, 1.0, 0.0]]) + distance_matrix = np.array( + [ + [0.0, 1.0, 1.0, 2.0], + [1.0, 0.0, 2.0, 1.0], + [1.0, 2.0, 0.0, 1.0], + [2.0, 1.0, 1.0, 0.0], + ] + ) emd_flow_assert( emd_with_flow(first_signature, second_signature, distance_matrix), - (2.0, [[1.0, 0.0, 0.0, 0.0], - [1.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 1.0, 1.0]]) + ( + 2.0, + [ + [1.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 1.0, 1.0], + ], + ), ) def test_emd_with_flow_extra_mass_penalty(): first_signature = np.array([0.0, 2.0, 1.0, 2.0]) second_signature = np.array([2.0, 1.0, 2.0, 1.0]) - distance_matrix = np.array([[0.0, 1.0, 1.0, 2.0], - [1.0, 0.0, 2.0, 1.0], - [1.0, 2.0, 0.0, 1.0], - [2.0, 1.0, 1.0, 0.0]]) + distance_matrix = np.array( + [ + [0.0, 1.0, 1.0, 2.0], + [1.0, 0.0, 2.0, 1.0], + [1.0, 2.0, 0.0, 1.0], + [2.0, 1.0, 1.0, 0.0], + ] + ) emd_flow_assert( - emd_with_flow(first_signature, second_signature, distance_matrix, - extra_mass_penalty=2.5), - (4.5, [[0.0, 0.0, 0.0, 0.0], - [1.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 1.0, 1.0]]) + emd_with_flow( + first_signature, second_signature, distance_matrix, extra_mass_penalty=2.5 + ), + ( + 4.5, + [ + [0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 1.0, 1.0], + ], + ), ) @@ -238,8 +235,7 @@ def test_emd_with_flow_extra_mass_penalty(): def test_emd_with_flow_validate_larger_signatures_1(): first_signature = np.array([0.0, 1.0, 2.0]) second_signature = np.array([5.0, 3.0, 3.0]) - distance_matrix = np.array([[0.0, 0.5], - [0.5, 0.0]]) + distance_matrix = np.array([[0.0, 0.5], [0.5, 0.0]]) with pytest.raises(ValueError): emd_with_flow(first_signature, second_signature, distance_matrix) @@ -247,8 +243,7 @@ def test_emd_with_flow_validate_larger_signatures_1(): def test_emd_with_flow_validate_larger_signatures_2(): first_signature = np.array([0.0, 1.0, 2.0]) second_signature = np.array([5.0, 3.0]) - distance_matrix = np.array([[0.0, 0.5], - [0.5, 0.0]]) + distance_matrix = np.array([[0.0, 0.5], [0.5, 0.0]]) with pytest.raises(ValueError): emd(first_signature, second_signature, distance_matrix) @@ -256,8 +251,7 @@ def test_emd_with_flow_validate_larger_signatures_2(): def test_emd_with_flow_validate_larger_signatures_3(): first_signature = np.array([0.0, 1.0]) second_signature = np.array([5.0, 3.0, 3.0]) - distance_matrix = np.array([[0.0, 0.5], - [0.5, 0.0]]) + distance_matrix = np.array([[0.0, 0.5], [0.5, 0.0]]) with pytest.raises(ValueError): emd_with_flow(first_signature, second_signature, distance_matrix) @@ -265,9 +259,7 @@ def test_emd_with_flow_validate_larger_signatures_3(): def test_emd_with_flow_validate_different_signature_dims(): first_signature = np.array([0.0, 1.0]) second_signature = np.array([5.0, 3.0, 3.0]) - distance_matrix = np.array([[0.0, 0.5, 0.0], - [0.5, 0.0, 0.0], - [0.5, 0.0, 0.0]]) + distance_matrix = np.array([[0.0, 0.5, 0.0], [0.5, 0.0, 0.0], [0.5, 0.0, 0.0]]) with pytest.raises(ValueError): emd_with_flow(first_signature, second_signature, distance_matrix) @@ -275,8 +267,7 @@ def test_emd_with_flow_validate_different_signature_dims(): def test_emd_with_flow_validate_square_distance_matrix(): first_signature = np.array([0.0, 1.0]) second_signature = np.array([5.0, 3.0]) - distance_matrix = np.array([[0.0, 0.5, 3.0], - [0.5, 0.0]], dtype=object) + distance_matrix = np.array([[0.0, 0.5, 3.0], [0.5, 0.0]], dtype=object) with pytest.raises(ValueError): emd_with_flow(first_signature, second_signature, distance_matrix) @@ -312,6 +303,7 @@ def test_emd_samples_1_not_normalized(): def test_emd_samples_1_custom_distance(): def dist(x): return np.array([[0.0 if i == j else 1.0 for i in x] for j in x]) + first_array = [1, 2, 3, 4] second_array = [2, 3, 4, 5] emd_assert(emd_samples(first_array, second_array, distance=dist), 0.25) @@ -320,16 +312,20 @@ def dist(x): def test_emd_samples_all_kwargs(): # Regression only; not checked by hand def dist(x): - return [[(i - j)**3 for i in range(len(x))] for j in range(len(x))] + return [[(i - j) ** 3 for i in range(len(x))] for j in range(len(x))] + first_array = [1, 2, 3, 4, 5] second_array = [2, 3, 4, 5] emd_assert( - emd_samples(first_array, second_array, - bins=30, - normalized=False, - range=(-5, 15), - distance=dist), - 24389.0 + emd_samples( + first_array, + second_array, + bins=30, + normalized=False, + range=(-5, 15), + distance=dist, + ), + 24389.0, ) @@ -370,6 +366,7 @@ def test_emd_samples_validate_empty(): def test_emd_samples_validate_distance_matrix_square(): def dist(x): return [[1, 2, 3]] + first_array = [1, 2, 3] second_array = [1, 2, 3] with pytest.raises(ValueError): @@ -379,6 +376,7 @@ def dist(x): def test_emd_samples_validate_distance_matrix_size(): def dist(x): return [[0, 1], [1, 0]] + first_array = [1, 2, 3, 4] second_array = [1, 2, 3, 4] with pytest.raises(ValueError): diff --git a/test_requirements.txt b/test_requirements.txt deleted file mode 100644 index 5bbae8f..0000000 --- a/test_requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest -tox