diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..53df731 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: tests + +on: + - push + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + pyver: ["3.8", "3.9", "3.10", "3.11"] + steps: + - uses: actions/checkout@v3 + - uses: mamba-org/setup-micromamba@v1 + with: + micromamba-version: '1.3.1-0' + environment-name: test-env + create-args: >- + python=${{ matrix.pyver }} + pytest>=6.2.5 + init-shell: bash + cache-environment: true + post-cleanup: 'all' + + - name: Install graphicle + shell: bash -el {0} + run: pip install . + + - name: Run tests + shell: bash -el {0} + run: pytest diff --git a/README.md b/README.md deleted file mode 100644 index 3e02164..0000000 --- a/README.md +++ /dev/null @@ -1,179 +0,0 @@ -# graphicle - -[![PyPI version](https://img.shields.io/pypi/v/graphicle.svg)](https://pypi.org/project/graphicle/) -[![Documentation](https://readthedocs.org/projects/graphicle/badge/?version=latest)](https://graphicle.readthedocs.io) -[![License](https://img.shields.io/pypi/l/graphicle)](https://raw.githubusercontent.com/jacanchaplais/graphicle/main/LICENSE.txt) -[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) - -Utilities for representing high energy physics data as graphs / networks. - - -## Installation -``` -pip install graphicle -``` - -# Features - -Object oriented interface to track-level particle data for collider physics, -with routines for constructing and performing calculations over -graph-structured data. - -Provides data structures for: -* 4-momenta -* PDG codes -* Particle status codes -* Color codes -* Helicity / spin polarisation data -* COO adjacency lists (for graph-structured data) - -```python3 ->>> import graphicle as gcl - -# query pdg records ->>> pdgs = gcl.PdgArray([1, 3, 6, -6, 25, 2212]) ->>> pdgs.name -['d', 's', 't', 't~', 'H0', 'p'], dtype=object) ->>> pdgs.charge -array([-0.33333333, -0.33333333, 0.66666667, -0.66666667, 0. , - 1. ]) - -# extract information from momentum data ->>> pmu_data -array([( 1.95057378e-02, 3.12923088e-02, 3.53556064e-01, 3.55473730e-01), - ( 2.60116947e+01, -3.63466398e+00, -3.33718718e+00, 2.64755711e+01), - ( 5.91884324e-05, -7.62144267e-06, -6.76385314e-06, 6.00591927e-05), - ( 2.82881807e+01, 4.32224823e+00, 2.14691072e+02, 2.16589841e+02), - (-8.73280642e-02, -6.48540201e-02, 3.73744945e-01, 6.28679140e-01), - ( 1.06204871e-01, 5.78888984e-01, -1.44899819e+02, 1.44901081e+02)], - dtype=[('x', '>> pmu = gcl.MomentumArray(pmu_data) ->>> pmu.pt -array([3.68738715e-02, 2.62644064e+01, 5.96771055e-05, 2.86164812e+01, - 1.08776076e-01, 5.88550704e-01]) ->>> pmu.mass -array([-7.45058060e-09, 5.11000489e-04, 9.09494702e-13, 5.10991478e-04, - 4.93680000e-01, 1.39570000e-01]) ->>> pmu.eta -array([ 2.95639434, -0.12672178, -0.11309956, 2.71277683, 1.94796328, - -6.1992861 ]) ->>> pmu.phi -array([ 1.01339184, -0.138833 , -0.12806107, 0.15162078, -2.5028134 , - 1.38935084]) - -# calculate the inter-particle distances ->>> pmu.delta_R(pmu) -array([[0. , 3.2913868 , 3.27485993, 0.89554388, 2.94501476, - 9.16339617], - [3.2913868 , 0. , 0.01736661, 2.85431528, 3.14526968, - 6.26189934], - [3.27485993, 0.01736661, 0. , 2.83968296, 3.14442819, - 6.27249595], - [0.89554388, 2.85431528, 2.83968296, 0. , 2.76241933, - 8.99760198], - [2.94501476, 3.14526968, 3.14442819, 2.76241933, 0. , - 8.4908571 ], - [9.16339617, 6.26189934, 6.27249595, 8.99760198, 8.4908571 , - 0. ]]) -``` - -Graphicle really shines with its composite data structures. These can be used -to filter and query heterogeneous particle data records simultaneously, either -using user provided boolean masks, or `MaskArray` instances produced with -routines in the `select` module. -Additionally, routines in the `calculate` and `transform` modules take -composite data structures to standardise useful calculations which blends -multiple particle data records. - -To see an example, let's generate a collision event using Pythia, wrapped -with `showerpipe`. - - -```python3 ->>> from showerpipe.generator import PythiaGenerator -... -... lhe_path = "https://zenodo.org/record/6034610/files/unweighted_events.lhe.gz" -... gen = PythiaGenerator("pythia-settings.cmnd", lhe_path) ->>> for event in gen: -... graph = gcl.Graphicle.from_event(event) -... break - ->>> graph.pdg -PdgArray(data=array([2212, 2212, 21, ..., 22, 22, 22], dtype=int32)) ->>> graph.edges -array([( 0, -1), ( 0, -2), ( -6, -3), ..., (-635, 1211), - (-636, 1212), (-636, 1213)], dtype=[('in', '>> W_mask = gcl.select.hard_descendants(graph, {24}) ->>> W_mask -MaskGroup(mask_arrays=["W+", "W-"], agg_op=OR) -# filter data record to get final state W+ boson descendants ->>> Wp_desc = graph[W_mask["W+"] & graph.final] ->>> Wp_desc.pdg -PdgArray(data=array([ 321, -211, -211, 321, -211, -321, 211, 211, -13, 14, 22, - 22, 211, -211, 22, 22, 22, 22, 22, 211, -211, 22, - 22, 22, 22, 130, 22, 22], dtype=int32)) ->>> Wp_desc -Graphicle(particles=ParticleSet( -PdgArray(data=array([ 321, -211, -211, 321, -211, -321, 211, 211, -13, 14, 22, - 22, 211, -211, 22, 22, 22, 22, 22, 211, -211, 22, - 22, 22, 22, 130, 22, 22], dtype=int32)), -MomentumArray(data=array([(-1.41648688e+00, -2.6653416 , -2.25487483e-01, 3.06676466e+00), - ( 5.26078595e-01, 0.11325339, -1.85115863e+00, 1.93283550e+00), - ( 2.92112800e+00, 2.19611382, -9.04351574e+00, 9.75502749e+00), - ( 1.70197168e+01, 9.65578074, -4.51506419e+01, 4.92110663e+01), - (-5.70145778e-01, -1.02762625, 1.35915720e-01, 1.19123247e+00), - (-1.70566595e-01, 0.02598637, -1.34183423e-01, 5.39901276e-01), - (-1.80439204e-01, -0.51409054, 1.82537117e-01, 5.91309546e-01), - ( 1.63182285e-01, 0.13788241, -3.17043212e-01, 4.06984277e-01), - (-2.45719652e+00, -4.10607321, 3.31426006e-01, 4.79777648e+00), - (-1.08820465e+00, -1.84333164, -1.69547133e-01, 2.14727900e+00), - (-4.92718715e-01, -0.87998859, 1.11984849e-01, 1.01473753e+00), - ( 8.90383374e-03, -0.01019132, 4.32869417e-04, 1.35398920e-02), - (-6.11110402e-01, -0.74064239, 5.47809445e-02, 9.71847628e-01), - (-2.13853648e-01, -0.34188095, -1.89837677e-01, 4.67048281e-01), - (-3.57251890e-01, -0.42033772, -1.39634796e-01, 5.69043576e-01), - (-2.41744268e-01, 0.16830106, -1.53611666e-02, 2.94960174e-01), - (-8.27775995e-01, -0.4279882 , 1.03575995e-01, 9.37611318e-01), - (-3.44298782e-05, 0.14091286, -4.51929191e-02, 1.47982551e-01), - ( 6.20276481e-02, 0.12552564, -1.96113732e-01, 2.40966203e-01), - ( 6.32168629e+00, 4.5683574 , -1.69888394e+01, 1.86942171e+01), - ( 8.77035615e-01, 0.4961944 , -2.38422385e+00, 2.59218122e+00), - (-1.12781117e+00, -1.41626175, -6.02316244e-02, 1.81145887e+00), - (-1.52146265e+00, -1.67738354, -3.45502640e-02, 2.26487480e+00), - ( 1.82715744e+00, 0.28701504, -3.76239153e+00, 4.19243031e+00), - ( 4.77818092e-01, 0.02881935, -8.63039360e-01, 9.86903046e-01), - (-3.03560171e+00, -2.76703663, 9.57894838e-02, 4.13861822e+00), - ( 8.99971241e-01, 0.6677899 , -2.26276823e+00, 2.52507657e+00), - ( 1.42885287e+00, 0.86196369, -3.46387012e+00, 3.84486646e+00)], - dtype=[('x', '>> gcl.calculate.combined_mass(Wp_desc.pmu) -80.419002446 -``` - -More information on the API is available in the -[documentation](https://graphicle.readthedocs.io) diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..7ef120b --- /dev/null +++ b/README.rst @@ -0,0 +1,192 @@ +graphicle +========= + +|PyPI version| |Tests| |Documentation| |License| |pre-commit| |Code style: +black| + +Utilities for representing high energy physics data as graphs / +networks. + +Installation +------------ + +.. code:: bash + + pip install graphicle + +Features +======== + +Object oriented interface to track-level particle data for collider +physics, with routines for constructing and performing calculations over +graph-structured data. + +Provides data structures for: + +* 4-momenta +* PDG codes +* Particle status codes +* Color codes +* Helicity / spin polarisation data +* COO adjacency lists (for graph-structured data) + +.. code:: python3 + + >>> import graphicle as gcl + + # query pdg records + >>> pdgs = gcl.PdgArray([1, 3, 6, -6, 25, 2212]) + >>> pdgs.name + ['d', 's', 't', 't~', 'H0', 'p'], dtype=object) + >>> pdgs.charge + array([-0.33333333, -0.33333333, 0.66666667, -0.66666667, 0. , + 1. ]) + + # extract information from momentum data + >>> pmu_data + array([( 1.95057378e-02, 3.12923088e-02, 3.53556064e-01, 3.55473730e-01), + ( 2.60116947e+01, -3.63466398e+00, -3.33718718e+00, 2.64755711e+01), + ( 5.91884324e-05, -7.62144267e-06, -6.76385314e-06, 6.00591927e-05), + ( 2.82881807e+01, 4.32224823e+00, 2.14691072e+02, 2.16589841e+02), + (-8.73280642e-02, -6.48540201e-02, 3.73744945e-01, 6.28679140e-01), + ( 1.06204871e-01, 5.78888984e-01, -1.44899819e+02, 1.44901081e+02)], + dtype=[('x', '>> pmu = gcl.MomentumArray(pmu_data) + >>> pmu.pt + array([3.68738715e-02, 2.62644064e+01, 5.96771055e-05, 2.86164812e+01, + 1.08776076e-01, 5.88550704e-01]) + >>> pmu.mass + array([-7.45058060e-09, 5.11000489e-04, 9.09494702e-13, 5.10991478e-04, + 4.93680000e-01, 1.39570000e-01]) + >>> pmu.eta + array([ 2.95639434, -0.12672178, -0.11309956, 2.71277683, 1.94796328, + -6.1992861 ]) + >>> pmu.phi + array([ 1.01339184, -0.138833 , -0.12806107, 0.15162078, -2.5028134 , + 1.38935084]) + + # calculate the inter-particle distances + >>> pmu.delta_R(pmu) + array([[0. , 3.2913868 , 3.27485993, 0.89554388, 2.94501476, + 9.16339617], + [3.2913868 , 0. , 0.01736661, 2.85431528, 3.14526968, + 6.26189934], + [3.27485993, 0.01736661, 0. , 2.83968296, 3.14442819, + 6.27249595], + [0.89554388, 2.85431528, 2.83968296, 0. , 2.76241933, + 8.99760198], + [2.94501476, 3.14526968, 3.14442819, 2.76241933, 0. , + 8.4908571 ], + [9.16339617, 6.26189934, 6.27249595, 8.99760198, 8.4908571 , + 0. ]]) + +Graphicle really shines with its composite data structures. These can be +used to filter and query heterogeneous particle data records +simultaneously, either using user provided boolean masks, or +``MaskArray`` instances produced with routines in the ``select`` module. +Additionally, routines in the ``calculate`` and ``transform`` modules +take composite data structures to standardise useful calculations which +blends multiple particle data records. + +To see an example, let’s generate a collision event using Pythia, +wrapped with ``showerpipe``. + +.. code:: python3 + + >>> from showerpipe.generator import PythiaGenerator + ... + ... lhe_path = "https://zenodo.org/record/6034610/files/unweighted_events.lhe.gz" + ... gen = PythiaGenerator("pythia-settings.cmnd", lhe_path) + >>> for event in gen: + ... graph = gcl.Graphicle.from_event(event) + ... break + + >>> graph.pdg + PdgArray(data=array([2212, 2212, 21, ..., 22, 22, 22], dtype=int32)) + >>> graph.edges + array([( 0, -1), ( 0, -2), ( -6, -3), ..., (-635, 1211), + (-636, 1212), (-636, 1213)], dtype=[('in', '>> W_mask = gcl.select.hard_descendants(graph, {24}) + >>> W_mask + MaskGroup(mask_arrays=["W+", "W-"], agg_op=OR) + # filter data record to get final state W+ boson descendants + >>> Wp_desc = graph[W_mask["W+"] & graph.final] + >>> Wp_desc.pdg + PdgArray(data=array([ 321, -211, -211, 321, -211, -321, 211, 211, -13, 14, 22, + 22, 211, -211, 22, 22, 22, 22, 22, 211, -211, 22, + 22, 22, 22, 130, 22, 22], dtype=int32)) + >>> Wp_desc + Graphicle(particles=ParticleSet( + PdgArray(data=array([ 321, -211, -211, 321, -211, -321, 211, 211, -13, 14, 22, + 22, 211, -211, 22, 22, 22, 22, 22, 211, -211, 22, + 22, 22, 22, 130, 22, 22], dtype=int32)), + MomentumArray(data=array([(-1.41648688e+00, -2.6653416 , -2.25487483e-01, 3.06676466e+00), + ( 5.26078595e-01, 0.11325339, -1.85115863e+00, 1.93283550e+00), + ( 2.92112800e+00, 2.19611382, -9.04351574e+00, 9.75502749e+00), + ( 1.70197168e+01, 9.65578074, -4.51506419e+01, 4.92110663e+01), + (-5.70145778e-01, -1.02762625, 1.35915720e-01, 1.19123247e+00), + (-1.70566595e-01, 0.02598637, -1.34183423e-01, 5.39901276e-01), + (-1.80439204e-01, -0.51409054, 1.82537117e-01, 5.91309546e-01), + ( 1.63182285e-01, 0.13788241, -3.17043212e-01, 4.06984277e-01), + (-2.45719652e+00, -4.10607321, 3.31426006e-01, 4.79777648e+00), + (-1.08820465e+00, -1.84333164, -1.69547133e-01, 2.14727900e+00), + (-4.92718715e-01, -0.87998859, 1.11984849e-01, 1.01473753e+00), + ( 8.90383374e-03, -0.01019132, 4.32869417e-04, 1.35398920e-02), + (-6.11110402e-01, -0.74064239, 5.47809445e-02, 9.71847628e-01), + (-2.13853648e-01, -0.34188095, -1.89837677e-01, 4.67048281e-01), + (-3.57251890e-01, -0.42033772, -1.39634796e-01, 5.69043576e-01), + (-2.41744268e-01, 0.16830106, -1.53611666e-02, 2.94960174e-01), + (-8.27775995e-01, -0.4279882 , 1.03575995e-01, 9.37611318e-01), + (-3.44298782e-05, 0.14091286, -4.51929191e-02, 1.47982551e-01), + ( 6.20276481e-02, 0.12552564, -1.96113732e-01, 2.40966203e-01), + ( 6.32168629e+00, 4.5683574 , -1.69888394e+01, 1.86942171e+01), + ( 8.77035615e-01, 0.4961944 , -2.38422385e+00, 2.59218122e+00), + (-1.12781117e+00, -1.41626175, -6.02316244e-02, 1.81145887e+00), + (-1.52146265e+00, -1.67738354, -3.45502640e-02, 2.26487480e+00), + ( 1.82715744e+00, 0.28701504, -3.76239153e+00, 4.19243031e+00), + ( 4.77818092e-01, 0.02881935, -8.63039360e-01, 9.86903046e-01), + (-3.03560171e+00, -2.76703663, 9.57894838e-02, 4.13861822e+00), + ( 8.99971241e-01, 0.6677899 , -2.26276823e+00, 2.52507657e+00), + ( 1.42885287e+00, 0.86196369, -3.46387012e+00, 3.84486646e+00)], + dtype=[('x', '>> gcl.calculate.combined_mass(Wp_desc.pmu) + 80.419002446 + +More information on the API is available in the +`documentation `__ + +.. |PyPI version| image:: https://img.shields.io/pypi/v/graphicle.svg + :target: https://pypi.org/project/graphicle/ +.. |Tests| image:: https://github.com/jacanchaplais/graphicle/actions/workflows/tests.yml/badge.svg +.. |Documentation| image:: https://readthedocs.org/projects/graphicle/badge/?version=latest + :target: https://graphicle.readthedocs.io +.. |License| image:: https://img.shields.io/pypi/l/graphicle + :target: https://raw.githubusercontent.com/jacanchaplais/graphicle/main/LICENSE.txt +.. |pre-commit| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit + :target: https://github.com/pre-commit/pre-commit +.. |Code style: black| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black diff --git a/docs/source/index.rst b/docs/source/index.rst index 7e86e93..0ac8a35 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -17,4 +17,4 @@ included on this page, too. API Reference -.. mdinclude:: ../../README.md +.. include:: ../../README.rst diff --git a/pyproject.toml b/pyproject.toml index 84ea30a..25f5d5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,15 +5,40 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "graphicle/_version.py" +[project] +name = "graphicle" +dynamic = ["version"] +authors = [{name = "Jacan Chaplais"}] +maintainers = [{name = "Jacan Chaplais"}] +description = "Encode particle physics data onto graph structures." +readme = {file = "README.rst", content-type = "text/x-rst"} +requires-python = ">=3.8" +license = {file = "LICENSE.txt"} +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent" +] +dependencies = [ + "attrs", + "numpy >=1.21", + "scipy >=1.8.0", + "numba", + "mcpid", + "typicle >=0.1.4", + "networkx", + "pyjet ==1.9.0", + "rich", + "deprecation", +] + +[project.urls] +repository = "https://github.com/jacanchaplais/graphicle" +documentation = "https://graphicle.readthedocs.io" + [tool.black] line-length = 79 -[tool.pytest.ini_options] -addopts = "--cov=graphicle" -testpaths = [ - "tests", -] - [tool.mypy] mypy_path = "graphicle" check_untyped_defs = true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 02cb08f..0000000 --- a/setup.cfg +++ /dev/null @@ -1,45 +0,0 @@ -[metadata] -name = graphicle -author = Jacan Chaplais -author_email = jacanchaplais@gmail.com -description = Encode particle physics data onto graph structures. -long_description = file: README.md -long_description_content_type = text/markdown -project_urls = - Documentation = https://graphicle.readthedocs.io -url = https://github.com/jacanchaplais/graphicle -license = BSD 3-Clause License -classifiers = - Programming Language :: Python :: 3 - License :: OSI Approved :: BSD License - Operating System :: OS Independent - -[options] -packages = graphicle -python_requires = >=3.8 -install_requires = - attrs - numpy >=1.21 - scipy >=1.8.0 - numba - mcpid - typicle >=0.1.4 - networkx - pyjet ==1.9.0 - rich - deprecation - -[options.extras_require] -dev = - pre-commit ==2.19.0 - flake8 ==3.9.2 - tox ==3.24.3 - pytest ==6.2.5 - pytest-cov ==2.12.1 - mypy ==0.910 - -[options.package_data] -graphicle = py.typed - -[flake8] -max-line-length = 79 diff --git a/tests/test_data.py b/tests/test_data.py index b291f01..ca00a48 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1,13 +1,68 @@ +import cmath +import dataclasses as dc +import math + import numpy as np -import typicle -import graphicle +import graphicle as gcl + +ZERO_TOL = 1.0e-10 # absolute tolerance for detecting zero-values + + +@dc.dataclass +class MomentumExample: + """Dataclass for representing the four-momentum of a particle, based + on its cylindrical beamline coordinates. + + Parameters + ---------- + pt, phi, pz, energy : float + Components of the four-momentum in cylindrical coordinates. + Note that ``phi`` is given in units of pi. + """ + + pt: float = 3.0 + phi: float = 0.75 + pz: float = 4.0 + energy: float = 5.0 + @property + def _polar(self) -> complex: + return self.pt * cmath.exp(complex(0, self.phi * math.pi)) -types_ = typicle.Types() + @property + def px(self) -> float: + """x-component of four momentum.""" + return self._polar.real + + @property + def py(self) -> float: + """y-component of four momentum.""" + return self._polar.imag + + def to_momentum_array(self) -> gcl.MomentumArray: + """Formats the components in a ``MomentumArray``.""" + return gcl.MomentumArray([(self.px, self.py, self.pz, self.energy)]) def test_pdgs(): - pdg_vals = np.arange(1, 7, dtype=types_.int) - pdgs = graphicle.PdgArray(pdg_vals) - assert list(pdgs.name) == ["d", "u", "s", "c", "b", "t"] + pdg_vals = np.arange(1, 7, dtype=np.int32) + pdgs = gcl.PdgArray(pdg_vals) + assert pdgs.name.tolist() == ["d", "u", "s", "c", "b", "t"] + + +def test_pmu_coords() -> None: + momentum = MomentumExample() + pmu = momentum.to_momentum_array() + assert math.isclose(pmu.pt.item(), momentum.pt) + assert math.isclose(pmu.phi.item(), momentum.phi * math.pi) + assert math.isclose(pmu.mass.item(), 0.0, abs_tol=ZERO_TOL) + assert math.isclose(pmu.theta.item(), math.atan(momentum.pt / momentum.pz)) + + +def test_pmu_transform() -> None: + momentum = MomentumExample() + pmu = momentum.to_momentum_array() + zero_transverse = pmu.shift_phi(math.pi) + pmu + assert math.isclose(0.0, zero_transverse.pt.item(), abs_tol=ZERO_TOL) + assert math.isclose(zero_transverse.mass.item(), 6.0)