diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index d4a2c44..0000000 --- a/.editorconfig +++ /dev/null @@ -1,21 +0,0 @@ -# http://editorconfig.org - -root = true - -[*] -indent_style = space -indent_size = 4 -trim_trailing_whitespace = true -insert_final_newline = true -charset = utf-8 -end_of_line = lf - -[*.bat] -indent_style = tab -end_of_line = crlf - -[LICENSE] -insert_final_newline = false - -[Makefile] -indent_style = tab diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 1a15181..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,15 +0,0 @@ -* mt2 version: -* Python version: -* Operating System: - -### Description - -Describe what you were trying to get done. -Tell us what happened, what went wrong, and what you expected to happen. - -### What I Did - -``` -Paste the command(s) you ran and the output. -If there was a crash, please include the traceback here. -``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b2807c..794101c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,17 @@ name: Build -on: [push, pull_request] +on: + pull_request: + push: + branches: [main] + tags: ["*"] + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true + jobs: test: runs-on: ubuntu-latest @@ -15,22 +21,26 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies + + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Install & activate run: | - python -m pip install --upgrade pip - pip install -U -r requirements_dev.txt - pip install tox tox-gh-actions - - name: Test with tox numpy>=2 - run: tox -e latestnpy - - name: Test with tox numpy<2 - run: tox -e oldestnpy + uv sync --python ${{ matrix.python-version }} + . .venv/bin/activate + echo PATH=$PATH >> $GITHUB_ENV + + - name: Test + run: python -m unittest discover tests + + - name: Test with oldest-supported-numpy + run: | + uv pip install oldest-supported-numpy + python -m unittest discover tests build_wheels: - name: Build wheel for ${{ matrix.os }}-cp${{ matrix.python }}-${{ matrix.arch }} + name: Build wheel for ${{ matrix.os }}-${{ matrix.build }}${{ matrix.python }}-${{ matrix.arch }} runs-on: ${{ matrix.os }} strategy: # Ensure that a wheel builder finishes even if another fails @@ -51,18 +61,20 @@ jobs: - name: Checkout mt2 uses: actions/checkout@v4 - # Used to host cibuildwheel - - name: Setup Python - uses: actions/setup-python@v5 + # Needed within cibuildwheel + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - uses: pypa/cibuildwheel@v2.20.0 env: + CIBW_BUILD_FRONTEND: "build[uv]" CIBW_BUILD: "${{ matrix.build }}${{ matrix.python }}*" CIBW_ARCHS: ${{ matrix.arch }} + CIBW_TEST_COMMAND: python -m unittest discover -t {project} -s {project}/tests - uses: actions/upload-artifact@v4 - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') with: + name: "artifact-${{ matrix.os }}-${{ matrix.build }}-${{ matrix.python }}-${{ matrix.arch }}" path: ./wheelhouse/*.whl build_sdist: @@ -71,20 +83,20 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - name: Install Python - with: - python-version: "3.12" + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Install numpy and setuptools - run: python -m pip install numpy setuptools + - name: Install environment & setuptools + run: | + uv sync + uv pip install setuptools - name: Build sdist - run: python setup.py sdist + run: uv run python setup.py sdist - uses: actions/upload-artifact@v4 - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') with: + name: artifact-source path: dist/*.tar.gz pass: @@ -94,7 +106,7 @@ jobs: - run: echo "All jobs passed" upload_pypi: - needs: [build_wheels, build_sdist, test] + needs: [pass] runs-on: ubuntu-latest # upload to PyPI on every tag starting with 'v' if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') @@ -103,8 +115,8 @@ jobs: steps: - uses: actions/download-artifact@v4 with: - name: artifact path: dist + merge-multiple: true - uses: pypa/gh-action-pypi-publish@release/v1 with: diff --git a/.gitignore b/.gitignore index 89821a8..6aeca48 100644 --- a/.gitignore +++ b/.gitignore @@ -28,55 +28,8 @@ wheels/ .installed.cfg *.egg -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env - # virtualenv .venv -venv/ -ENV/ - -# Rope project settings -.ropeproject - -# mypy -.mypy_cache/ -# IDE settings -.idea/ -.vscode/ +# package manager lock file +uv.lock diff --git a/AUTHORS.rst b/AUTHORS.rst deleted file mode 100644 index dc38a60..0000000 --- a/AUTHORS.rst +++ /dev/null @@ -1,7 +0,0 @@ -======= -Credits -======= - -* Christopher Lester : Original C++ implementation of mT2. -* Rupert Tombs: Current C++ implementation used in this package. -* Thomas Gillam : Python packaging diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index 752838a..0000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,127 +0,0 @@ -.. highlight:: shell - -============ -Contributing -============ - -Contributions are welcome, and they are greatly appreciated! Every little bit -helps, and credit will always be given. - -You can contribute in many ways: - -Types of Contributions ----------------------- - -Report Bugs -~~~~~~~~~~~ - -Report bugs at https://github.com/tpgillam/mt2/issues. - -If you are reporting a bug, please include: - -* Your operating system name and version. -* Any details about your local setup that might be helpful in troubleshooting. -* Detailed steps to reproduce the bug. - -Fix Bugs -~~~~~~~~ - -Look through the GitHub issues for bugs. Anything tagged with "bug" and "help -wanted" is open to whoever wants to implement it. - -Implement Features -~~~~~~~~~~~~~~~~~~ - -Look through the GitHub issues for features. Anything tagged with "enhancement" -and "help wanted" is open to whoever wants to implement it. - -Write Documentation -~~~~~~~~~~~~~~~~~~~ - -mt2 could always use more documentation, whether as part of the -official mt2 docs, in docstrings, or even on the web in blog posts, -articles, and such. - -Submit Feedback -~~~~~~~~~~~~~~~ - -The best way to send feedback is to file an issue at https://github.com/tpgillam/mt2/issues. - -If you are proposing a feature: - -* Explain in detail how it would work. -* Keep the scope as narrow as possible, to make it easier to implement. -* Remember that this is a volunteer-driven project, and that contributions - are welcome :) - -Get Started! ------------- - -Ready to contribute? Here's how to set up `mt2` for local development. - -1. Fork the `mt2` repo on GitHub. -2. Clone your fork locally:: - - $ git clone git@github.com:your_name_here/mt2.git - -3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: - - $ mkvirtualenv mt2 - $ cd mt2/ - $ python setup.py develop - -4. Create a branch for local development:: - - $ git checkout -b name-of-your-bugfix-or-feature - - Now you can make your changes locally. - -5. When you're done making changes, check that your changes pass flake8 and the - tests, including testing other Python versions with tox:: - - $ flake8 mt2 tests - $ python setup.py test or pytest - $ tox - - To get flake8 and tox, just pip install them into your virtualenv. - -6. Commit your changes and push your branch to GitHub:: - - $ git add . - $ git commit -m "Your detailed description of your changes." - $ git push origin name-of-your-bugfix-or-feature - -7. Submit a pull request through the GitHub website. - -Pull Request Guidelines ------------------------ - -Before you submit a pull request, check that it meets these guidelines: - -1. The pull request should include tests. -2. If the pull request adds functionality, the docs should be updated. Put - your new functionality into a function with a docstring, and add the - feature to the list in README.rst. -3. The pull request should work across all supported platforms and Python versions. - Check that the continuous integration tests pass. - -Tips ----- - -To run a subset of tests:: - -$ pytest tests.test_mt2 - - -Deploying ---------- - -A reminder for the maintainers on how to deploy. -Make sure all your changes are committed (including an entry in HISTORY.rst). -Then run:: - -$ bump2version patch # possible: major / minor / patch -$ git push -$ git push --tags - -Travis will then deploy to PyPI if tests pass. diff --git a/HISTORY.rst b/HISTORY.rst index b9a841c..ffaf498 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,7 @@ History ------------------ * Move support to Python 3.9-3.12. Support numpy 2. Thanks to @lgray +* Various build system & package modernisation. 1.2.0 (2021-05-05) ------------------ diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 2e98c08..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,11 +0,0 @@ -include AUTHORS.rst -include CONTRIBUTING.rst -include HISTORY.rst -include LICENSE -include README.rst -include pyproject.toml - -recursive-include src * -recursive-include tests * -recursive-exclude * __pycache__ -recursive-exclude * *.py[co] diff --git a/Makefile b/Makefile index 7fffe8a..0399c58 100644 --- a/Makefile +++ b/Makefile @@ -1,74 +1,29 @@ -.PHONY: clean clean-test clean-pyc clean-build help -.DEFAULT_GOAL := help - -define BROWSER_PYSCRIPT -import os, webbrowser, sys - -from urllib.request import pathname2url - -webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) -endef -export BROWSER_PYSCRIPT - -define PRINT_HELP_PYSCRIPT -import re, sys - -for line in sys.stdin: - match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) - if match: - target, help = match.groups() - print("%-20s %s" % (target, help)) -endef -export PRINT_HELP_PYSCRIPT - -BROWSER := python -c "$$BROWSER_PYSCRIPT" - -help: - @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) - -clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts - -clean-build: ## remove build artifacts - rm -fr build/ - rm -fr dist/ - rm -fr .eggs/ - find . -name '*.egg-info' -exec rm -fr {} + - find . -name '*.egg' -exec rm -f {} + - -clean-pyc: ## remove Python file artifacts - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + +.PHONY: install +install: + uv sync + +.PHONY: clean +clean: clean-build clean-pyc clean-venv + +.PHONY: clean-build +clean-build: + rm -rf build/ + rm -rf dist/ + rm -rf .eggs/ + find . -name '*.egg-info' -delete + find . -name '*.egg' -delete + +.PHONY: clean-pyc +clean-pyc: + find . -name '*.pyc' -delete + find . -name '*.pyo' -delete find . -name '__pycache__' -exec rm -fr {} + -clean-test: ## remove test and coverage artifacts - rm -fr .tox/ - rm -f .coverage - rm -fr htmlcov/ - rm -fr .pytest_cache - -lint: ## check style with flake8 - flake8 mt2 tests - -test: ## run tests quickly with the default Python - pytest - -test-all: ## run tests on every Python version with tox - tox - -coverage: ## check code coverage quickly with the default Python - coverage run --source mt2 -m pytest - coverage report -m - coverage html - $(BROWSER) htmlcov/index.html - -release: dist ## package and upload a release - twine upload dist/* - -dist: clean ## builds source and wheel package - python setup.py sdist - python setup.py bdist_wheel - ls -l dist +.PHONY: clean-venv +clean-venv: + rm -f uv.lock + rm -rf .venv -install: clean ## install the package to the active Python's site-packages - python setup.py install +.PHONY: test +test: install + uv run --locked python -m unittest discover tests diff --git a/README.rst b/README.rst index aa8c867..b5b7acf 100644 --- a/README.rst +++ b/README.rst @@ -5,11 +5,12 @@ mt2 .. image:: https://img.shields.io/pypi/v/mt2.svg :target: https://pypi.python.org/pypi/mt2 +.. image:: https://img.shields.io/pypi/pyversions/mt2.svg + :target: https://pypi.python.org/pypi/mt2 + .. image:: https://github.com/tpgillam/mt2/workflows/Build/badge.svg?branch=master :target: https://github.com/tpgillam/mt2/actions?query=workflow%3ABuild -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black This package may be used to evaluate MT2 in all its variants. This includes both symmetric and asymmetric MT2. @@ -140,3 +141,10 @@ A list of alternative implementations of the MT2 calculation can be found here: https://www.hep.phy.cam.ac.uk/~lester/mt2/#Alternatives In Python, the other wrapper of the same algorithm known to the authors is by Nikolai Hartmann, here: https://gitlab.cern.ch/nihartma/pymt2 + + +Authors +------- +* @kesterlester: Original C++ implementation of mT2. +* @rupt: Current C++ implementation used in this package. +* @tpgillam: Python packaging diff --git a/mt2/__init__.py b/mt2/__init__.py index 62e2a0f..026b10f 100644 --- a/mt2/__init__.py +++ b/mt2/__init__.py @@ -4,8 +4,6 @@ from _mt2 import mt2_lester_ufunc, mt2_tombs_ufunc # pyright: ignore [reportMissingImports] -__author__ = "Thomas Gillam" -__email__ = "tpgillam@googlemail.com" __version__ = "1.2.1" __all__ = ["mt2", "mt2_arxiv", "mt2_ufunc"] diff --git a/pyproject.toml b/pyproject.toml index 1d95a0b..db6faf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,33 @@ -[build-system] -requires = [ - "setuptools", - "wheel", - "numpy>=1.19.3", +[project] +name = "mt2" +version = "1.2.1" +description = "Stransverse mass computation as a numpy ufunc." +authors = [ + { name = "Tom Gillam", email = "tpgillam@googlemail.com" }, + { name = "Rupert Tombs" }, + { name = "Christopher Lester" }, +] +readme = "README.rst" +license = { file = "LICENSE" } +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] +keywords = ["mt2"] +dependencies = ["numpy>=1.19.3"] +requires-python = ">= 3.9" +urls = { Homepage = "https://github.com/tpgillam/mt2" } + +[build-system] +requires = ["setuptools>=61.0", "wheel", "numpy"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.package-data] +mt2 = ["src/*.h", "src/*.cpp"] diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 2137fe4..0000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,12 +0,0 @@ -pip -numpy>=1.19.3 -bump2version -wheel -watchdog -flake8 -tox -coverage -twine - -pytest -pytest-runner diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 31ad82b..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[aliases] -test = pytest diff --git a/setup.py b/setup.py index 392ba58..fa1f817 100644 --- a/setup.py +++ b/setup.py @@ -4,66 +4,24 @@ __version__ = "1.2.1" -with open("README.rst") as readme_file: - readme = readme_file.read() - -with open("HISTORY.rst") as history_file: - history = history_file.read() - -ext_modules = [ - Extension( - "_mt2", - ["src/main.cpp"], - define_macros=[ - # Pass in the version info so we can expose it in the extension. - ("VERSION_INFO", __version__), - # For reasons explained in lester_mt2_bisect_v7.h, we need to manually - # enable some inlining optimisations. - ("ENABLE_INLINING", "1"), - # Copyright printing is disabled here, since we include the necessary - # citation information elsewhere. - ("DISABLE_COPYRIGHT_PRINTING", "1"), - ], - include_dirs=[numpy.get_include()], - language="c++", - extra_compile_args=["-std=c++11"], - ), -] - -setup_requirements = [ - "pytest-runner", -] -test_requirements = [ - "pytest>=3", -] - setup( - author="Thomas Gillam", - author_email="tpgillam@googlemail.com", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Natural Language :: English", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", + ext_modules=[ + Extension( + "_mt2", + ["src/main.cpp"], + define_macros=[ + # Pass in the version info so we can expose it in the extension. + ("VERSION_INFO", __version__), + # For reasons explained in lester_mt2_bisect_v7.h, we need to manually + # enable some inlining optimisations. + ("ENABLE_INLINING", "1"), + # Copyright printing is disabled here, since we include the necessary + # citation information elsewhere. + ("DISABLE_COPYRIGHT_PRINTING", "1"), + ], + include_dirs=[numpy.get_include()], + language="c++", + extra_compile_args=["-std=c++11"], + ), ], - description="Stransverse mass computation as a numpy ufunc.", - install_requires=["numpy>=1.19.3"], - license="MIT license", - long_description=readme + "\n\n" + history, - include_package_data=True, - keywords="mt2", - name="mt2", - packages=["mt2"], - setup_requires=setup_requirements, - test_suite="tests", - tests_require=test_requirements, - url="https://github.com/tpgillam/mt2", - version=__version__, - ext_modules=ext_modules, - zip_safe=False, ) diff --git a/src/main.cpp b/src/main.cpp index 979e2df..6d649cc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #define STRINGIFY(x) #x #define MACRO_STRINGIFY(x) STRINGIFY(x) + static void mt2_lester_ufunc( char **args, // const-correctness was introduced in numpy 1.19, but retain backward compatibility. diff --git a/tests/test_mt2.py b/tests/test_mt2.py index c059855..ff5b5f5 100644 --- a/tests/test_mt2.py +++ b/tests/test_mt2.py @@ -1,98 +1,114 @@ """Tests for `mt2` package.""" + import math import random +import unittest -import pytest +import numpy from mt2 import mt2, mt2_arxiv -def test_simple_example(): - for mt2_impl in (mt2, mt2_arxiv): - computed_val = mt2_impl(100, 410, 20, 150, -210, -300, -200, 280, 100, 100) - assert computed_val == pytest.approx(412.628) - - -def test_near_massless(): - # This test is based on Fig 5 of https://arxiv.org/pdf/1411.4312.pdf - m_vis_a = 0 - px_a = -42.017340486 - py_a = -146.365340528 - - m_vis_b = 0.087252259 - px_b = -9.625614206 - py_b = 145.757295514 - - px_miss = -16.692279406 - py_miss = -14.730240471 - - chi_a = 0 - chi_b = 0 +class TestMt2(unittest.TestCase): + def test_simple_example(self): + for mt2_impl in (mt2, mt2_arxiv): + computed_val = mt2_impl(100, 410, 20, 150, -210, -300, -200, 280, 100, 100) + self.assertAlmostEqual(computed_val, 412.627668458219) - for mt2_impl in (mt2, mt2_arxiv): - computed_val = mt2_impl( - m_vis_a, px_a, py_a, m_vis_b, px_b, py_b, px_miss, py_miss, chi_a, chi_b - ) - assert computed_val == pytest.approx(0.09719971) + def test_near_massless(self): + # This test is based on Fig 5 of https://arxiv.org/pdf/1411.4312.pdf + m_vis_a = 0 + px_a = -42.017340486 + py_a = -146.365340528 + m_vis_b = 0.087252259 + px_b = -9.625614206 + py_b = 145.757295514 -def test_collinear_endpoint_cases(): - random.seed(0) # If the test fails, we want to be able to repeat the test! - for i in range(10000): - m_vis_a = random.uniform(0, 10) - m_vis_b = random.uniform(0, 10) - m_invis_a = random.uniform(0, 10) - m_invis_b = random.uniform(0, 10) + px_miss = -16.692279406 + py_miss = -14.730240471 - # Addition of random positive number prevents possibility of division - # by zero at the locations below marked "MOO" - m_parent = max(m_vis_a + m_invis_a, m_vis_b + m_invis_b) + random.uniform( - 0.1, 10 - ) - p_parent_a = random.uniform(0, 10) - p_parent_b = random.uniform(0, 10) - e_parent_a = math.sqrt(p_parent_a ** 2 + m_parent ** 2) - e_parent_b = math.sqrt(p_parent_b ** 2 + m_parent ** 2) - beta_a = p_parent_a / e_parent_a # MOO - beta_b = p_parent_b / e_parent_b # MOO - gamma_a = 1.0 / math.sqrt(1 - beta_a ** 2) - gamma_b = 1.0 / math.sqrt(1 - beta_b ** 2) - pA = math.sqrt( - (m_parent - m_vis_a - m_invis_a) - * (m_parent + m_vis_a - m_invis_a) - * (m_parent - m_vis_a + m_invis_a) - * (m_parent + m_vis_a + m_invis_a) - ) / (2 * m_parent) - pB = math.sqrt( - (m_parent - m_vis_b - m_invis_b) - * (m_parent + m_vis_b - m_invis_b) - * (m_parent - m_vis_b + m_invis_b) - * (m_parent + m_vis_b + m_invis_b) - ) / (2 * m_parent) - p_vis_a_boosted = gamma_a * (beta_a * math.sqrt(m_vis_a ** 2 + pA ** 2) + pA) - p_vis_b_boosted = gamma_b * (beta_b * math.sqrt(m_vis_b ** 2 + pB ** 2) + pB) - p_invis_a_boosted = gamma_a * ( - beta_a * math.sqrt(m_invis_a ** 2 + pA ** 2) - pA - ) - p_invis_b_boosted = gamma_b * ( - beta_b * math.sqrt(m_invis_b ** 2 + pB ** 2) - pB - ) - p_miss = p_invis_a_boosted + p_invis_b_boosted - theta = random.uniform(0, math.tau) - c = math.cos(theta) - s = math.sin(theta) - px_miss, py_miss = p_miss * c, p_miss * s - ax, ay = p_vis_a_boosted * c, p_vis_a_boosted * s - bx, by = p_vis_b_boosted * c, p_vis_b_boosted * s + chi_a = 0 + chi_b = 0 for mt2_impl in (mt2, mt2_arxiv): - val = mt2_impl( - m_vis_a, ax, ay, m_vis_b, bx, by, px_miss, py_miss, m_invis_a, m_invis_b + computed_val = mt2_impl( + m_vis_a, px_a, py_a, m_vis_b, px_b, py_b, px_miss, py_miss, chi_a, chi_b ) + self.assertAlmostEqual(computed_val, 0.09719971) + + def test_collinear_endpoint_cases(self): + random.seed(0) # If the test fails, we want to be able to repeat the test! + for i in range(10000): + m_vis_a = random.uniform(0, 10) + m_vis_b = random.uniform(0, 10) + m_invis_a = random.uniform(0, 10) + m_invis_b = random.uniform(0, 10) - # passes with rel=1e-12 but sporadically fails with rel=1e-13 - assert val == pytest.approx(m_parent, rel=1e-12), ( - f"WARNING! Expected {m_parent} from collinear event but instead got " - f"{val} for mt2({m_vis_a},{ax},{ay}, {m_vis_b},{bx},{by}, " - f"{px_miss},{py_miss}, {m_invis_a},{m_invis_b}) in test case {i}." + # Addition of random positive number prevents possibility of division + # by zero at the locations below marked "MOO" + m_parent = max(m_vis_a + m_invis_a, m_vis_b + m_invis_b) + random.uniform( + 0.1, 10 + ) + p_parent_a = random.uniform(0, 10) + p_parent_b = random.uniform(0, 10) + e_parent_a = math.sqrt(p_parent_a**2 + m_parent**2) + e_parent_b = math.sqrt(p_parent_b**2 + m_parent**2) + beta_a = p_parent_a / e_parent_a # MOO + beta_b = p_parent_b / e_parent_b # MOO + gamma_a = 1.0 / math.sqrt(1 - beta_a**2) + gamma_b = 1.0 / math.sqrt(1 - beta_b**2) + pA = math.sqrt( + (m_parent - m_vis_a - m_invis_a) + * (m_parent + m_vis_a - m_invis_a) + * (m_parent - m_vis_a + m_invis_a) + * (m_parent + m_vis_a + m_invis_a) + ) / (2 * m_parent) + pB = math.sqrt( + (m_parent - m_vis_b - m_invis_b) + * (m_parent + m_vis_b - m_invis_b) + * (m_parent - m_vis_b + m_invis_b) + * (m_parent + m_vis_b + m_invis_b) + ) / (2 * m_parent) + p_vis_a_boosted = gamma_a * (beta_a * math.sqrt(m_vis_a**2 + pA**2) + pA) + p_vis_b_boosted = gamma_b * (beta_b * math.sqrt(m_vis_b**2 + pB**2) + pB) + p_invis_a_boosted = gamma_a * ( + beta_a * math.sqrt(m_invis_a**2 + pA**2) - pA ) + p_invis_b_boosted = gamma_b * ( + beta_b * math.sqrt(m_invis_b**2 + pB**2) - pB + ) + p_miss = p_invis_a_boosted + p_invis_b_boosted + theta = random.uniform(0, math.tau) + c = math.cos(theta) + s = math.sin(theta) + px_miss, py_miss = p_miss * c, p_miss * s + ax, ay = p_vis_a_boosted * c, p_vis_a_boosted * s + bx, by = p_vis_b_boosted * c, p_vis_b_boosted * s + + for mt2_impl in (mt2, mt2_arxiv): + val = mt2_impl( + m_vis_a, + ax, + ay, + m_vis_b, + bx, + by, + px_miss, + py_miss, + m_invis_a, + m_invis_b, + ) + + # passes with rel=1e-12 but sporadically fails with rel=1e-13 + # update 2024-08-06: fails on MacOS unless tolerance doubled to 2e-12. + numpy.testing.assert_allclose( + val, + m_parent, + rtol=2e-12, + err_msg=( + f"WARNING! Expected {m_parent} from collinear event but instead got " + f"{val} for mt2({m_vis_a},{ax},{ay}, {m_vis_b},{bx},{by}, " + f"{px_miss},{py_miss}, {m_invis_a},{m_invis_b}) in test case {i}." + ), + ) diff --git a/tests/test_mt2_lally.py b/tests/test_mt2_lally.py index 27b991b..6f92e19 100644 --- a/tests/test_mt2_lally.py +++ b/tests/test_mt2_lally.py @@ -1,74 +1,74 @@ """Tests for the variant of MT2 by Colin Lally.""" +import unittest + import numpy -import pytest - -from .common import mt2_lally, mt2_lester - - -def test_simple_example(): - computed_val = mt2_lally(100, 410, 20, 150, -210, -300, -200, 280, 100, 100) - assert computed_val == pytest.approx(412.628) - - -def test_near_massless(): - # This test is based on Fig 5 of https://arxiv.org/pdf/1411.4312.pdf - m_vis_a = 0 - px_a = -42.017340486 - py_a = -146.365340528 - - m_vis_b = 0.087252259 - px_b = -9.625614206 - py_b = 145.757295514 - - px_miss = -16.692279406 - py_miss = -14.730240471 - - chi_a = 0 - chi_b = 0 - - computed_val = mt2_lally( - m_vis_a, px_a, py_a, m_vis_b, px_b, py_b, px_miss, py_miss, chi_a, chi_b - ) - assert computed_val == pytest.approx(0.09719971) - - -@pytest.mark.skip(reason="Currently failing due to inconsistencies") -def test_fuzz(): - batch_size = 100 - num_tests = 1000 - - numpy.random.seed(42) - - def _random_batch(min_, max_): - return numpy.random.uniform(min_, max_, (batch_size,)) - - for _ in range(num_tests): - m_vis_1 = _random_batch(0, 100) - px_vis_1 = _random_batch(-100, 100) - py_vis_1 = _random_batch(-100, 100) - m_vis_2 = _random_batch(0, 100) - px_vis_2 = _random_batch(-100, 100) - py_vis_2 = _random_batch(-100, 100) - px_miss = _random_batch(-100, 100) - py_miss = _random_batch(-100, 100) - m_invis_1 = _random_batch(0, 100) - m_invis_2 = _random_batch(0, 100) - - args = ( - m_vis_1, - px_vis_1, - py_vis_1, - m_vis_2, - px_vis_2, - py_vis_2, - px_miss, - py_miss, - m_invis_1, - m_invis_2, - ) - result_lester = mt2_lester(*args) - result_lally = mt2_lally(*args) +from tests.common import mt2_lally, mt2_lester + + +class TestLally(unittest.TestCase): + def test_simple_example(self): + computed_val = mt2_lally(100, 410, 20, 150, -210, -300, -200, 280, 100, 100) + self.assertAlmostEqual(computed_val, 412.627668458219) + + def test_near_massless(self): + # This test is based on Fig 5 of https://arxiv.org/pdf/1411.4312.pdf + m_vis_a = 0 + px_a = -42.017340486 + py_a = -146.365340528 - numpy.testing.assert_allclose(result_lester, result_lally, rtol=1e-12) + m_vis_b = 0.087252259 + px_b = -9.625614206 + py_b = 145.757295514 + + px_miss = -16.692279406 + py_miss = -14.730240471 + + chi_a = 0 + chi_b = 0 + + computed_val = mt2_lally( + m_vis_a, px_a, py_a, m_vis_b, px_b, py_b, px_miss, py_miss, chi_a, chi_b + ) + self.assertAlmostEqual(computed_val, 0.09719971) + + @unittest.skip(reason="Currently failing due to inconsistencies") + def test_fuzz(self): + batch_size = 100 + num_tests = 1000 + + numpy.random.seed(42) + + def _random_batch(min_, max_): + return numpy.random.uniform(min_, max_, (batch_size,)) + + for _ in range(num_tests): + m_vis_1 = _random_batch(0, 100) + px_vis_1 = _random_batch(-100, 100) + py_vis_1 = _random_batch(-100, 100) + m_vis_2 = _random_batch(0, 100) + px_vis_2 = _random_batch(-100, 100) + py_vis_2 = _random_batch(-100, 100) + px_miss = _random_batch(-100, 100) + py_miss = _random_batch(-100, 100) + m_invis_1 = _random_batch(0, 100) + m_invis_2 = _random_batch(0, 100) + + args = ( + m_vis_1, + px_vis_1, + py_vis_1, + m_vis_2, + px_vis_2, + py_vis_2, + px_miss, + py_miss, + m_invis_1, + m_invis_2, + ) + + result_lester = mt2_lester(*args) + result_lally = mt2_lally(*args) + + numpy.testing.assert_allclose(result_lester, result_lally, rtol=1e-12) diff --git a/tests/test_mt2_tombs.py b/tests/test_mt2_tombs.py index 5fe1933..1806f7d 100644 --- a/tests/test_mt2_tombs.py +++ b/tests/test_mt2_tombs.py @@ -1,97 +1,93 @@ """Tests for the variant of MT2 by Rupert Tombs.""" -from typing import Optional, Union +import unittest import numpy -import pytest - -from .common import mt2_lester, mt2_tombs - - -def test_simple_example(): - computed_val = mt2_tombs(100, 410, 20, 150, -210, -300, -200, 280, 100, 100) - assert computed_val == pytest.approx(412.628) - - -def test_near_massless(): - # This test is based on Fig 5 of https://arxiv.org/pdf/1411.4312.pdf - m_vis_a = 0 - px_a = -42.017340486 - py_a = -146.365340528 - - m_vis_b = 0.087252259 - px_b = -9.625614206 - py_b = 145.757295514 - - px_miss = -16.692279406 - py_miss = -14.730240471 - - chi_a = 0 - chi_b = 0 - - computed_val = mt2_tombs( - m_vis_a, px_a, py_a, m_vis_b, px_b, py_b, px_miss, py_miss, chi_a, chi_b - ) - assert computed_val == pytest.approx(0.09719971) - - -def test_fuzz(): - batch_size = 100 - num_tests = 1000 - - numpy.random.seed(42) - - def _random_batch(min_, max_): - return numpy.random.uniform(min_, max_, (batch_size,)) - - for _ in range(num_tests): - m_vis_1 = _random_batch(0, 100) - px_vis_1 = _random_batch(-100, 100) - py_vis_1 = _random_batch(-100, 100) - m_vis_2 = _random_batch(0, 100) - px_vis_2 = _random_batch(-100, 100) - py_vis_2 = _random_batch(-100, 100) - px_miss = _random_batch(-100, 100) - py_miss = _random_batch(-100, 100) - m_invis_1 = _random_batch(0, 100) - m_invis_2 = _random_batch(0, 100) - - args = ( - m_vis_1, - px_vis_1, - py_vis_1, - m_vis_2, - px_vis_2, - py_vis_2, - px_miss, - py_miss, - m_invis_1, - m_invis_2, - ) - result_lester = mt2_lester(*args) - result_tombs = mt2_tombs(*args) +from tests.common import mt2_lester, mt2_tombs + - numpy.testing.assert_allclose(result_lester, result_tombs, rtol=1e-12) +class TestTombs(unittest.TestCase): + def test_simple_example(self): + computed_val = mt2_tombs(100, 410, 20, 150, -210, -300, -200, 280, 100, 100) + self.assertAlmostEqual(computed_val, 412.627668458219) + def test_near_massless(self): + # This test is based on Fig 5 of https://arxiv.org/pdf/1411.4312.pdf + m_vis_a = 0 + px_a = -42.017340486 + py_a = -146.365340528 -def test_scale_invariance(): - example_args = numpy.array((100, 410, 20, 150, -210, -300, -200, 280, 100, 100)) - example_val = mt2_tombs(*example_args) + m_vis_b = 0.087252259 + px_b = -9.625614206 + py_b = 145.757295514 - # mt2 scales with its arguments; check over some orders of magnitude. - for i in range(-100, 100, 10): - scale = 10.0 ** i - with numpy.errstate(over="ignore"): - # Suppress overflow warnings when performing the evaluation; we're happy - # so long as we match approximately in the test below. - computed_val = mt2_tombs(*(example_args * scale)) - assert computed_val == pytest.approx(example_val * scale) + px_miss = -16.692279406 + py_miss = -14.730240471 + chi_a = 0 + chi_b = 0 -def test_negative_masses(): - # Any negative mass is unphysical. - # These arguments use negative masses to make both initial bounds negative. - # Check that the result is neither positive nor an infinite loop. - computed_val = mt2_tombs(1, 2, 3, 4, 5, 6, 7, 8, -90, -100) - assert not (computed_val > 0) + computed_val = mt2_tombs( + m_vis_a, px_a, py_a, m_vis_b, px_b, py_b, px_miss, py_miss, chi_a, chi_b + ) + self.assertAlmostEqual(computed_val, 0.09719971) + + def test_fuzz(self): + batch_size = 100 + num_tests = 1000 + + numpy.random.seed(42) + + def _random_batch(min_, max_): + return numpy.random.uniform(min_, max_, (batch_size,)) + + for _ in range(num_tests): + m_vis_1 = _random_batch(0, 100) + px_vis_1 = _random_batch(-100, 100) + py_vis_1 = _random_batch(-100, 100) + m_vis_2 = _random_batch(0, 100) + px_vis_2 = _random_batch(-100, 100) + py_vis_2 = _random_batch(-100, 100) + px_miss = _random_batch(-100, 100) + py_miss = _random_batch(-100, 100) + m_invis_1 = _random_batch(0, 100) + m_invis_2 = _random_batch(0, 100) + + args = ( + m_vis_1, + px_vis_1, + py_vis_1, + m_vis_2, + px_vis_2, + py_vis_2, + px_miss, + py_miss, + m_invis_1, + m_invis_2, + ) + + result_lester = mt2_lester(*args) + result_tombs = mt2_tombs(*args) + + numpy.testing.assert_allclose(result_lester, result_tombs, rtol=1e-12) + + def test_scale_invariance(self): + example_args = numpy.array((100, 410, 20, 150, -210, -300, -200, 280, 100, 100)) + example_val = mt2_tombs(*example_args) + + # mt2 scales with its arguments; check over some orders of magnitude. + for i in range(-100, 100, 10): + scale = 10.0**i + with numpy.errstate(over="ignore"): + # Suppress overflow warnings when performing the evaluation; we're happy + # so long as we match approximately in the test below. + computed_val = mt2_tombs(*(example_args * scale)) + numpy.testing.assert_allclose(computed_val, example_val * scale) + + def test_negative_masses(self): + # Any negative mass is unphysical. + # These arguments use negative masses to make both initial bounds negative. + # Check that the result is neither positive nor an infinite loop. + computed_val = mt2_tombs(1, 2, 3, 4, 5, 6, 7, 8, -90, -100) + self.assertLessEqual(computed_val, 0) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 6683f43..0000000 --- a/tox.ini +++ /dev/null @@ -1,41 +0,0 @@ -[tox] -envlist = py39, py310, py311, py312, flake8 - -[gh-actions] -python = - 3.9: py39 - 3.10: py310 - 3.11: py311 - 3.12: py312 - -[testenv:flake8] -basepython = python -deps = flake8 -commands = flake8 mt2 tests - -[testenv:latestnpy] -setenv = - PYTHONPATH = {toxinidir} -deps = - -r{toxinidir}/requirements_dev.txt -; If you want to make tox run the tests with the same versions, create a -; requirements.txt with the pinned versions and uncomment the following line: -; -r{toxinidir}/requirements.txt -commands = - pip install -U pip - pytest --basetemp={envtmpdir} - -[testenv:oldestnpy] -setenv = - PYTHONPATH = {toxinidir} -deps = - -r{toxinidir}/requirements_dev.txt -; If you want to make tox run the tests with the same versions, create a -; requirements.txt with the pinned versions and uncomment the following line: -; -r{toxinidir}/requirements.txt -commands = - pip uninstall -y numpy - pip install -U pip - pip install oldest-supported-numpy - pytest --basetemp={envtmpdir} -