diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..5db58de --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,35 @@ +name: Deploy + +on: + workflow_dispatch: + release: + types: [published] + pull_request: + paths: + - .github/workflows/deploy.yml + +jobs: + deploy: + runs-on: ubuntu-latest + + environment: + name: pypi + url: https://pypi.org/p/numba-stats + permissions: + id-token: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # needed by setuptools_scm + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - run: python -m pip install --upgrade pip build + - run: python -m build + - run: python -m pip install --prefer-binary $(echo dist/*.whl)'[test]' + - run: python -m pytest + + - uses: pypa/gh-action-pypi-publish@release/v1 + if: github.event_name == 'push' && contains(github.event.ref, '/tags/') diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 43c3bc8..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Release - -on: - workflow_dispatch: - release: - types: [published] - -jobs: - upload: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: '3.9' - - # do not remove wheel - - run: python -m pip install --upgrade pip wheel - - run: python -m pip install --prefer-binary -e .[test] - - run: python setup.py sdist bdist_wheel - - run: python -m pip install --force-reinstall dist/*.tar.gz - - run: python -m pytest - - - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{secrets.PYPI_TOKEN}} diff --git a/.github/workflows/ci.yml b/.github/workflows/test.yml similarity index 53% rename from .github/workflows/ci.yml rename to .github/workflows/test.yml index 1cd712e..731aa65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: CI +name: Test on: push: @@ -8,19 +8,25 @@ on: paths-ignore: - '*.md' +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true + +env: + PIP_ONLY_BINARY: ":all:" + jobs: - build: + test: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.12"] + python-version: ["3.9", "3.12"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - run: python -m pip install --upgrade pip - - run: python -m pip install --prefer-binary -e .[test] - - run: python -m pytest + - run: python -m pip install --upgrade pip nox + - run: nox -s test diff --git a/.gitignore b/.gitignore index 240cb9b..d4601f9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ venv *.egg-info .vscode bench/*.svg +.nox diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 24d7289..7c96813 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,6 +40,6 @@ repos: # Python formatting - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 94b7a8a..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include LICENSE pyproject.toml README.md setup.py setup.cfg diff --git a/README.md b/README.md index 45d93ec..b2ac19b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # numba-stats ![](https://img.shields.io/pypi/v/numba-stats.svg) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.13236518.svg)](https://doi.org/10.5281/zenodo.13236518) -We provide `numba`-accelerated implementations of statistical distributions for common probability distributions + +We provide `numba`-accelerated implementations of common probability distributions. * Uniform * (Truncated) Normal @@ -83,6 +85,16 @@ You won't get these errors when you call the numba-stats PDFs outside of a compi but `norm_pdf(1, 2, 3)` (as implemented above) will fail. +## Documentation + +To get documentation, please use `help()` in the Python interpreter. + +Functions with equivalents in `scipy.stats` follow the `scipy` calling conventions exactly, except for distributions starting with `trunc...`, which follow a different convention, since the `scipy` behavior is very impractical. Even so, note that the `scipy` conventions are sometimes a bit unusual, particular in case of the exponential, the log-normal, and the uniform distribution. See the `scipy` docs for details. + +## Citation + +If you use this package in a scientific work, please cite us. You can generate citations in your preferred format on the [Zenodo website](https://doi.org/10.5281/zenodo.13236518). + ## Benchmarks The following benchmarks were produced on an Intel(R) Core(TM) i7-8569U CPU @ 2.80GHz against SciPy-1.10.1. The dotted line on the right-hand figure shows the expected speedup (4x) from parallelization on a CPU with four physical cores. @@ -113,15 +125,9 @@ The `bernstein.density` does not profit from auto-parallelization, on the contra ![](docs/_static/bernstein.density.svg) ![](docs/_static/truncexpon.pdf.plus.norm.pdf.svg) -## Documentation - -To get documentation, please use `help()` in the Python interpreter. - -Functions with equivalents in `scipy.stats` follow the `scipy` calling conventions exactly, except for distributions starting with `trunc...`, which follow a different convention, since the `scipy` behavior is very impractical. Even so, note that the `scipy` conventions are sometimes a bit unusual, particular in case of the exponential, the log-normal, and the uniform distribution. See the `scipy` docs for details. - ## Contributions -**You can help with adding more distributions, patches are very welcome.** Implementing a probability distribution is easy. You need to write it in simple Python that `numba` can understand. Special functions from `scipy.special` can be used after some wrapping, see submodule `numba_stats._special.py` how it is done. +**You can help with adding more distributions, patches are welcome.** Implementing a probability distribution is easy. You need to write it in simple Python that `numba` can understand. Special functions from `scipy.special` can be used after some wrapping, see submodule `numba_stats._special.py` how it is done. ## numba-stats and numba-scipy diff --git a/coverage.sh b/coverage.sh deleted file mode 100755 index 4232093..0000000 --- a/coverage.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -pytest --cov=src/numba_stats --cov-report=html diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..6c9aca4 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,50 @@ +""" +Noxfile to orchestrate tests and computing coverage. + +Pass extra arguments to pytest after -- +""" + +import nox +import sys + +sys.path.append(".") +import python_releases + +nox.needs_version = ">=2024.3.2" +nox.options.default_venv_backend = "uv|virtualenv" + +ENV = { + "COVERAGE_CORE": "sysmon", # faster coverage on Python 3.12 +} + +PYPROJECT = nox.project.load_toml("pyproject.toml") +MINIMUM_PYTHON = PYPROJECT["project"]["requires-python"].strip(">=") +LATEST_PYTHON = str(python_releases.latest()) + +nox.options.sessions = ["test", "maxtest"] + +# running in parallel with pytest-xdist does not make the tests faster + + +@nox.session(reuse_venv=True) +def test(session: nox.Session) -> None: + """Run all tests.""" + session.install("-e.[test]") + session.run("pytest", *session.posargs) + + +@nox.session(python=LATEST_PYTHON, reuse_venv=True) +def maxtest(session: nox.Session) -> None: + """Run the unit and regular tests.""" + session.install("-e.[test]") + session.run("pytest", *session.posargs, env=ENV) + + +# Python-3.12 provides coverage info faster +@nox.session(python="3.12", reuse_venv=True) +def cov(session: nox.Session) -> None: + """Run covage and place in 'htmlcov' directory.""" + session.install("-e.[test]") + session.run("coverage", "run", "-m", "pytest", env=ENV) + session.run("coverage", "html", "-d", "htmlcov") + session.run("coverage", "report", "-m") diff --git a/pyproject.toml b/pyproject.toml index a71f627..9f89f02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,8 +2,47 @@ requires = ["setuptools>=42", "setuptools_scm[toml]>=3.4"] build-backend = "setuptools.build_meta" +[project] +name = "numba-stats" +dynamic = ["version"] +requires-python = ">=3.9" +dependencies = ["numba>=0.53", "numpy>=1.20", "scipy>=1.5"] +authors = [{ name = "Hans Dembinski", email = "hans.dembinski@gmail.com" }] +readme = "README.md" +description = "Numba-accelerated implementations of scipy probability distributions and others used in particle physics" +license = { text = "MIT" } +classifiers = [ + # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Science/Research', + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: Implementation :: CPython', +] + +[project.urls] +repository = "https://github.com/hdembinski/numba-stats" + +[project.optional-dependencies] +test = [ + "pytest>=6", + "pytest-cov>=5", + "pytest-benchmark>=4", + "pydocstyle>=6", + "coverage>=6", +] + +[tool.setuptools.packages.find] +where = ["src"] + [tool.setuptools_scm] -write_to = "src/numba_stats/_version.py" [tool.pytest.ini_options] minversion = "6.0" diff --git a/python_releases.py b/python_releases.py new file mode 100644 index 0000000..2c05763 --- /dev/null +++ b/python_releases.py @@ -0,0 +1,64 @@ +"""Get the latest Python release which is online.""" + +import urllib.request +import re +from html.parser import HTMLParser +import gzip +from packaging.version import Version + + +class PythonVersionParser(HTMLParser): + """Specialized HTMLParser to get Python version number.""" + + def __init__(self): + """Initialize parser state.""" + super().__init__() + self.versions = set() + self.found_version = False + + def handle_starttag(self, tag, attrs): + """Look for the right tag and store result in an attribute.""" + if tag == "a": + for attr in attrs: + if attr[0] == "href" and "/downloads/release/python-" in attr[1]: + self.found_version = True + return + + def handle_data(self, data): + """Extract Python version from entry.""" + if self.found_version: + self.found_version = False + match = re.search(r"Python (\d+\.\d+)", data) + if match: + self.versions.add(Version(match.group(1))) + + +def versions(): + """Get all Python release versions.""" + req = urllib.request.Request("https://www.python.org/downloads/") + req.add_header("Accept-Encoding", "gzip") + + with urllib.request.urlopen(req) as response: + raw = response.read() + if response.info().get("Content-Encoding") == "gzip": + raw = gzip.decompress(raw) + html = raw.decode("utf-8") + + parser = PythonVersionParser() + parser.feed(html) + + return parser.versions + + +def latest(): + """Return version of latest Python release.""" + return max(versions()) + + +def main(): + """Print all discovered release versions.""" + print(" ".join(str(x) for x in sorted(versions()))) + + +if __name__ == "__main__": + main() diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 4a775d7..0000000 --- a/setup.cfg +++ /dev/null @@ -1,37 +0,0 @@ -[metadata] -name = numba-stats -version = attr: numba_stats.__version__ -author = Hans Dembinski -author_email = hans.dembinski@gmail.com -description = Numba-accelerated implementations of common probability distributions -homepage = https://github.com/hdembinski/numba-stats -long_description = file: README.md -long_description_content_type = text/markdown -license = "MIT" -project_urls = - Bug Tracker = https://github.com/hdembinski/numba-stats/issues -license_files = LICENSE.txt - -[options] -packages = numba_stats -package_dir = - =src -python_requires = >=3.7 -install_requires = - numba >= 0.49 - numpy >= 1.18 - scipy >= 1.5 - -[options.extras_require] -test = - pytest - pytest-cov - pytest-benchmark - pydocstyle - -[flake8] -max-line-length = 90 - -[tool:pytest] -testpaths = tests -addopts = --doctest-modules diff --git a/setup.py b/setup.py deleted file mode 100644 index 6ba76e7..0000000 --- a/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -import site -import sys -import setuptools - -# workaround to allow editable install as user -site.ENABLE_USER_SITE = "--user" in sys.argv[1:] - -setuptools.setup() diff --git a/src/numba_stats/__init__.py b/src/numba_stats/__init__.py index 6c02300..51c5acc 100644 --- a/src/numba_stats/__init__.py +++ b/src/numba_stats/__init__.py @@ -1 +1,27 @@ -from ._version import version as __version__ # noqa +""" +We provide numba-accelerated implementations of common probability distributions. + +* Uniform +* (Truncated) Normal +* Log-normal +* Poisson +* (Truncated) Exponential +* Student's t +* Voigtian +* Crystal Ball +* Generalised double-sided Crystal Ball +* Tsallis-Hagedorn, a model for the minimum bias pT distribution +* Q-Gaussian +* Bernstein density (not normalized to unity, use this in extended likelihood fits) +* Cruijff density (not normalized to unity, use this in extended likelihood fits) +* CMS-Shape + +The speed gains are huge, up to a factor of 100 compared to Scipy. + +The distributions are optimized for the use in maximum-likelihood fits, where you query +a distribution at many points with a single set of parameters. +""" + +from importlib.metadata import version + +__version__ = version("numba-stats") diff --git a/src/numba_stats/bernstein.py b/src/numba_stats/bernstein.py index 9f6669d..eb9ac52 100644 --- a/src/numba_stats/bernstein.py +++ b/src/numba_stats/bernstein.py @@ -165,14 +165,13 @@ def _type_check(x, beta, xmin, xmax): def __getattr__(key): # Temporary hack to maintain backward compatibility import warnings - from numpy import VisibleDeprecationWarning if key in ("scaled_pdf", "scaled_cdf"): r = {"scaled_pdf": "density", "scaled_cdf": "integral"} warnings.warn( f"bernstein.{key} is deprecated and will be removed in a future release, " f"use bernstein.{r[key]} instead", - VisibleDeprecationWarning, + FutureWarning, 1, ) return globals()[r[key]] diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_bernstein.py b/tests/test_bernstein.py index b42b825..6fea663 100644 --- a/tests/test_bernstein.py +++ b/tests/test_bernstein.py @@ -61,10 +61,10 @@ def f(): def test_deprecation(): - with pytest.warns(np.VisibleDeprecationWarning): + with pytest.warns(FutureWarning): got = bernstein.scaled_pdf(1, [1, 2], 0, 1) assert_allclose(got, bernstein.density(1, [1, 2], 0, 1)) - with pytest.warns(np.VisibleDeprecationWarning): + with pytest.warns(FutureWarning): got = bernstein.scaled_cdf(1, [1, 2], 0, 1) assert_allclose(got, bernstein.integral(1, [1, 2], 0, 1))