jetgo 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. jetgo-0.1.0/.gitignore +13 -0
  2. jetgo-0.1.0/LICENSE +29 -0
  3. jetgo-0.1.0/PKG-INFO +153 -0
  4. jetgo-0.1.0/README.md +116 -0
  5. jetgo-0.1.0/examples/README.md +16 -0
  6. jetgo-0.1.0/examples/ex01_minimal_eec.py +63 -0
  7. jetgo-0.1.0/examples/ex02_flavor_tagged_run.py +69 -0
  8. jetgo-0.1.0/examples/ex03_custom_observable.py +102 -0
  9. jetgo-0.1.0/jetgo/__init__.py +20 -0
  10. jetgo-0.1.0/jetgo/_pythia.py +63 -0
  11. jetgo-0.1.0/jetgo/kinematics.py +133 -0
  12. jetgo-0.1.0/jetgo/observables/__init__.py +12 -0
  13. jetgo-0.1.0/jetgo/observables/base.py +156 -0
  14. jetgo-0.1.0/jetgo/observables/classes.py +18 -0
  15. jetgo-0.1.0/jetgo/observables/double_differential_jet_cross_section.py +232 -0
  16. jetgo-0.1.0/jetgo/observables/eec.py +310 -0
  17. jetgo-0.1.0/jetgo/observables/identifier.py +22 -0
  18. jetgo-0.1.0/jetgo/simulation/__init__.py +14 -0
  19. jetgo-0.1.0/jetgo/simulation/event_generator.py +123 -0
  20. jetgo-0.1.0/jetgo/simulation/jet_finder.py +110 -0
  21. jetgo-0.1.0/jetgo/simulation/particle_selector.py +115 -0
  22. jetgo-0.1.0/jetgo/simulation/simulation_summary.py +103 -0
  23. jetgo-0.1.0/jetgo/simulation/simulator.py +377 -0
  24. jetgo-0.1.0/jetgo/taggers/__init__.py +11 -0
  25. jetgo-0.1.0/jetgo/taggers/base.py +114 -0
  26. jetgo-0.1.0/jetgo/taggers/classes.py +17 -0
  27. jetgo-0.1.0/jetgo/taggers/descendancy_tracing.py +593 -0
  28. jetgo-0.1.0/jetgo/taggers/flavor.py +18 -0
  29. jetgo-0.1.0/jetgo/taggers/identifier.py +21 -0
  30. jetgo-0.1.0/pyproject.toml +76 -0
  31. jetgo-0.1.0/tests/conftest.py +137 -0
  32. jetgo-0.1.0/tests/data/generate_golden_run.py +25 -0
  33. jetgo-0.1.0/tests/data/golden_run.json +399 -0
  34. jetgo-0.1.0/tests/test_jet_finder.py +93 -0
  35. jetgo-0.1.0/tests/test_kinematics.py +100 -0
  36. jetgo-0.1.0/tests/test_observables.py +217 -0
  37. jetgo-0.1.0/tests/test_pythia_integration.py +166 -0
  38. jetgo-0.1.0/tests/test_simulator.py +209 -0
jetgo-0.1.0/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .coverage
5
+ htmlcov/
6
+ build/
7
+ dist/
8
+ *.egg-info/
9
+ /venv
10
+ /.venv
11
+ /.idea
12
+ /.claude
13
+ /docs/_build
jetgo-0.1.0/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Maxence Larose
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
jetgo-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.5
2
+ Name: jetgo
3
+ Version: 0.1.0
4
+ Summary: Jet Event Toolkit for Generating Observables: generate collider events, find jets, tag their generator-level flavor, and save the observable data.
5
+ Project-URL: Homepage, https://github.com/MaxenceLarose/jetgo
6
+ Project-URL: Documentation, https://maxencelarose.github.io/jetgo
7
+ Project-URL: Repository, https://github.com/MaxenceLarose/jetgo
8
+ Project-URL: Issues, https://github.com/MaxenceLarose/jetgo/issues
9
+ Author-email: Maxence Larose <maxence.larose@stonybrook.edu>
10
+ Maintainer-email: Maxence Larose <maxence.larose@stonybrook.edu>
11
+ License-Expression: BSD-3-Clause
12
+ License-File: LICENSE
13
+ Keywords: energy-energy-correlator,fastjet,high-energy-physics,jet-flavor,jets,monte-carlo,pythia8,qcd
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: BSD License
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Scientific/Engineering :: Physics
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: fastjet>=3.4
26
+ Requires-Dist: numpy>=1.24
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest-cov>=4.1; extra == 'dev'
29
+ Requires-Dist: pytest>=7.4; extra == 'dev'
30
+ Provides-Extra: docs
31
+ Requires-Dist: numpydoc>=1.6; extra == 'docs'
32
+ Requires-Dist: pydata-sphinx-theme>=0.15; extra == 'docs'
33
+ Requires-Dist: sphinx>=7.2; extra == 'docs'
34
+ Provides-Extra: pythia
35
+ Requires-Dist: pythia8mc>=8.3; extra == 'pythia'
36
+ Description-Content-Type: text/markdown
37
+
38
+ <p align="center">
39
+ <img src="https://raw.githubusercontent.com/MaxenceLarose/jetgo/main/images/jetgo_banner.png" alt="jetgo" width="820">
40
+ </p>
41
+
42
+ <p align="center">
43
+ <i>The simplest way to generate collider events and measure their jet observables.</i>
44
+ </p>
45
+
46
+ <p align="center">
47
+ <img src="https://img.shields.io/badge/python-3.11+-blue.svg?logo=python&logoColor=white" alt="Python 3.11+">
48
+ <img src="https://img.shields.io/badge/PYTHIA-8.3+-orange.svg" alt="PYTHIA 8.3+">
49
+ <img src="https://img.shields.io/badge/FastJet-3.5+-9cf.svg" alt="FastJet 3.5+">
50
+ <a href="https://github.com/MaxenceLarose/jetgo/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-BSD--3--Clause-green.svg" alt="BSD-3-Clause license"></a>
51
+ <a href="https://pypi.org/project/jetgo/"><img src="https://img.shields.io/pypi/dm/jetgo?label=downloads&color=blue" alt="Downloads"></a>
52
+ </p>
53
+
54
+ `jetgo` generates collider events, clusters the particles into jets, tags each jet with the color
55
+ charge of the parton that initiated it, and writes the observables out.
56
+
57
+ You define the simulator, the observables and the tagger. `jetgo` does the rest.
58
+
59
+ # Installation
60
+
61
+ ```bash
62
+ pip install jetgo[pythia]
63
+ ```
64
+
65
+ PYTHIA8 is an optional dependency, because its wheels are Linux-only. Plain `pip install jetgo`
66
+ works everywhere and gives you every observable and tagger; only event generation needs the extra.
67
+
68
+ # Quick usage preview
69
+
70
+ ```python
71
+ import numpy as np
72
+
73
+ from jetgo.observables import EEC
74
+ from jetgo.simulation import EventGenerator, JetFinder, ParticleSelector, Simulator
75
+ from jetgo.taggers import DescendancyTracing
76
+
77
+ event_generator = EventGenerator(
78
+ beam_1=2212,
79
+ beam_2=2212,
80
+ sqrt_s=5020,
81
+ pt_min=80,
82
+ pt_max=200,
83
+ random_seed=42
84
+ )
85
+
86
+ particle_selector = ParticleSelector(
87
+ max_abs_pseudorapidity=2.4
88
+ )
89
+
90
+ jet_finder = JetFinder(
91
+ radius=0.4,
92
+ pt_min=120,
93
+ pt_max=140,
94
+ max_abs_pseudorapidity=1.6
95
+ )
96
+
97
+ observable = EEC(
98
+ bin_edges=np.geomspace(0.005, 0.8, 31),
99
+ charged_only=True,
100
+ use_pseudorapidity=True
101
+ )
102
+
103
+ jet_flavor_tagger = DescendancyTracing(
104
+ delta_r_max=0.4,
105
+ use_pseudorapidity=True
106
+ )
107
+
108
+ simulator = Simulator(
109
+ event_generator=event_generator,
110
+ particle_selector=particle_selector,
111
+ jet_finder=jet_finder
112
+ )
113
+
114
+ simulator.simulate(
115
+ n_events=100_000,
116
+ observables=observable,
117
+ jet_flavor_taggers=jet_flavor_tagger,
118
+ output_path="jet_eec.json"
119
+ )
120
+ ```
121
+
122
+ That run writes one file holding the inclusive energy-energy correlator, its quark-tagged and
123
+ gluon-tagged counterparts, and the run metadata.
124
+
125
+ # Documentation
126
+
127
+ Full documentation lives at [maxencelarose.github.io/jetgo](https://maxencelarose.github.io/jetgo),
128
+ including how to write your own observable or your own flavor tagger. Runnable scripts are in
129
+ [`examples/`](examples).
130
+
131
+ # License
132
+
133
+ This project is licensed under the terms of the [BSD 3-Clause License](LICENSE).
134
+
135
+ # Citation
136
+
137
+ If you use `jetgo` in your work, please cite the paper it was written for:
138
+
139
+ ```bibtex
140
+ @article{Bossi:2026colors,
141
+ author = {Bossi, Hannah and Larose, Maxence and Mehtar-Tani, Yacine},
142
+ title = {The Colors of Jet Quenching},
143
+ year = {2026},
144
+ eprint = {2609.05609},
145
+ archivePrefix = {arXiv},
146
+ primaryClass = {hep-ph},
147
+ url = {https://arxiv.org/abs/2609.05609}
148
+ }
149
+ ```
150
+
151
+ # Contact
152
+
153
+ Maxence Larose — [maxence.larose@stonybrook.edu](mailto:maxence.larose@stonybrook.edu)
jetgo-0.1.0/README.md ADDED
@@ -0,0 +1,116 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/MaxenceLarose/jetgo/main/images/jetgo_banner.png" alt="jetgo" width="820">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <i>The simplest way to generate collider events and measure their jet observables.</i>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <img src="https://img.shields.io/badge/python-3.11+-blue.svg?logo=python&logoColor=white" alt="Python 3.11+">
11
+ <img src="https://img.shields.io/badge/PYTHIA-8.3+-orange.svg" alt="PYTHIA 8.3+">
12
+ <img src="https://img.shields.io/badge/FastJet-3.5+-9cf.svg" alt="FastJet 3.5+">
13
+ <a href="https://github.com/MaxenceLarose/jetgo/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-BSD--3--Clause-green.svg" alt="BSD-3-Clause license"></a>
14
+ <a href="https://pypi.org/project/jetgo/"><img src="https://img.shields.io/pypi/dm/jetgo?label=downloads&color=blue" alt="Downloads"></a>
15
+ </p>
16
+
17
+ `jetgo` generates collider events, clusters the particles into jets, tags each jet with the color
18
+ charge of the parton that initiated it, and writes the observables out.
19
+
20
+ You define the simulator, the observables and the tagger. `jetgo` does the rest.
21
+
22
+ # Installation
23
+
24
+ ```bash
25
+ pip install jetgo[pythia]
26
+ ```
27
+
28
+ PYTHIA8 is an optional dependency, because its wheels are Linux-only. Plain `pip install jetgo`
29
+ works everywhere and gives you every observable and tagger; only event generation needs the extra.
30
+
31
+ # Quick usage preview
32
+
33
+ ```python
34
+ import numpy as np
35
+
36
+ from jetgo.observables import EEC
37
+ from jetgo.simulation import EventGenerator, JetFinder, ParticleSelector, Simulator
38
+ from jetgo.taggers import DescendancyTracing
39
+
40
+ event_generator = EventGenerator(
41
+ beam_1=2212,
42
+ beam_2=2212,
43
+ sqrt_s=5020,
44
+ pt_min=80,
45
+ pt_max=200,
46
+ random_seed=42
47
+ )
48
+
49
+ particle_selector = ParticleSelector(
50
+ max_abs_pseudorapidity=2.4
51
+ )
52
+
53
+ jet_finder = JetFinder(
54
+ radius=0.4,
55
+ pt_min=120,
56
+ pt_max=140,
57
+ max_abs_pseudorapidity=1.6
58
+ )
59
+
60
+ observable = EEC(
61
+ bin_edges=np.geomspace(0.005, 0.8, 31),
62
+ charged_only=True,
63
+ use_pseudorapidity=True
64
+ )
65
+
66
+ jet_flavor_tagger = DescendancyTracing(
67
+ delta_r_max=0.4,
68
+ use_pseudorapidity=True
69
+ )
70
+
71
+ simulator = Simulator(
72
+ event_generator=event_generator,
73
+ particle_selector=particle_selector,
74
+ jet_finder=jet_finder
75
+ )
76
+
77
+ simulator.simulate(
78
+ n_events=100_000,
79
+ observables=observable,
80
+ jet_flavor_taggers=jet_flavor_tagger,
81
+ output_path="jet_eec.json"
82
+ )
83
+ ```
84
+
85
+ That run writes one file holding the inclusive energy-energy correlator, its quark-tagged and
86
+ gluon-tagged counterparts, and the run metadata.
87
+
88
+ # Documentation
89
+
90
+ Full documentation lives at [maxencelarose.github.io/jetgo](https://maxencelarose.github.io/jetgo),
91
+ including how to write your own observable or your own flavor tagger. Runnable scripts are in
92
+ [`examples/`](examples).
93
+
94
+ # License
95
+
96
+ This project is licensed under the terms of the [BSD 3-Clause License](LICENSE).
97
+
98
+ # Citation
99
+
100
+ If you use `jetgo` in your work, please cite the paper it was written for:
101
+
102
+ ```bibtex
103
+ @article{Bossi:2026colors,
104
+ author = {Bossi, Hannah and Larose, Maxence and Mehtar-Tani, Yacine},
105
+ title = {The Colors of Jet Quenching},
106
+ year = {2026},
107
+ eprint = {2609.05609},
108
+ archivePrefix = {arXiv},
109
+ primaryClass = {hep-ph},
110
+ url = {https://arxiv.org/abs/2609.05609}
111
+ }
112
+ ```
113
+
114
+ # Contact
115
+
116
+ Maxence Larose — [maxence.larose@stonybrook.edu](mailto:maxence.larose@stonybrook.edu)
@@ -0,0 +1,16 @@
1
+ # Examples
2
+
3
+ Each script is standalone and runnable. Install the package with the Pythia8 extra first:
4
+
5
+ ```bash
6
+ pip install jetgo[pythia]
7
+ ```
8
+
9
+ | script | what it shows |
10
+ |---|---|
11
+ | `ex01_minimal_eec.py` | the smallest useful run: generate events, cluster jets, fill one observable, save |
12
+ | `ex02_flavor_tagged_run.py` | the same run split into quark-tagged and gluon-tagged jets, and how to read the result back |
13
+ | `ex03_custom_observable.py` | writing your own `Observable` subclass and running it through the simulator |
14
+
15
+ They are deliberately small, a few thousand events each, so they finish in under a minute. Real
16
+ runs are the same code with a larger `n_events`.
@@ -0,0 +1,63 @@
1
+ """
2
+ @Title: Minimal energy-energy correlator run.
3
+
4
+ @Description: The smallest useful jetgo run. Generate hard-QCD proton-proton events, keep the
5
+ charged final-state particles inside a tracker-like acceptance, cluster them into
6
+ anti-kT jets, accumulate the energy-energy correlator, and write everything to one
7
+ JSON file.
8
+ """
9
+
10
+ import numpy as np
11
+
12
+ from jetgo.observables import EEC
13
+ from jetgo.simulation import EventGenerator, JetFinder, ParticleSelector, Simulator
14
+
15
+ if __name__ == "__main__":
16
+ # ---------------------------------------------------------------------------------------------------- #
17
+ # The three pieces #
18
+ # ---------------------------------------------------------------------------------------------------- #
19
+ event_generator = EventGenerator(
20
+ beam_1=2212, # proton
21
+ beam_2=2212, # proton
22
+ sqrt_s=5020, # GeV
23
+ pt_min=80, # partonic p̂T window, in GeV
24
+ pt_max=200,
25
+ random_seed=42, # anything non-zero: 0 lets Pythia8 pick its own seed, and the run stops
26
+ ) # being reproducible
27
+
28
+ jet_finder = JetFinder(
29
+ radius=0.4,
30
+ pt_min=120,
31
+ pt_max=140,
32
+ max_abs_pseudorapidity=1.6,
33
+ )
34
+
35
+ particle_selector = ParticleSelector(
36
+ max_abs_pseudorapidity=2.4,
37
+ )
38
+
39
+ # ---------------------------------------------------------------------------------------------------- #
40
+ # The run #
41
+ # ---------------------------------------------------------------------------------------------------- #
42
+ simulator = Simulator(
43
+ event_generator=event_generator,
44
+ jet_finder=jet_finder,
45
+ particle_selector=particle_selector,
46
+ )
47
+
48
+ # The bin edges are required. Observables histogram at fill time, so the file size does not grow with
49
+ # the run length, and the edges have to be the ones the result will be read on.
50
+ observable = EEC(
51
+ bin_edges=np.geomspace(0.005, 0.8, 31),
52
+ pt_min=1,
53
+ energy_weight=1,
54
+ charged_only=True,
55
+ use_pseudorapidity=True,
56
+ )
57
+
58
+ simulator.simulate(
59
+ n_events=5_000,
60
+ observables=observable,
61
+ output_path="jet_eec.json",
62
+ verbose=True,
63
+ )
@@ -0,0 +1,69 @@
1
+ """
2
+ @Title: Splitting a run by generator-level jet flavor.
3
+
4
+ @Description: The same run as ex01, with a flavor tagger attached. Every observable is filled three
5
+ times over: once with all jets, once with the jets the tagger calls quark-initiated,
6
+ and once with the ones it calls gluon-initiated. The second half of the script reads
7
+ the file back and turns the saved histograms into normalized observables.
8
+ """
9
+
10
+ import json
11
+
12
+ import numpy as np
13
+
14
+ from jetgo.observables import DoubleDifferentialJetCrossSection, EEC
15
+ from jetgo.simulation import EventGenerator, JetFinder, ParticleSelector, Simulator
16
+ from jetgo.taggers import DescendancyTracing
17
+
18
+ OUTPUT_PATH = "flavor_tagged_run.json"
19
+ EEC_BIN_EDGES = np.geomspace(0.005, 0.8, 31)
20
+ CROSS_SECTION_BIN_EDGES = np.linspace(120.0, 140.0, 21)
21
+
22
+ if __name__ == "__main__":
23
+ # ---------------------------------------------------------------------------------------------------- #
24
+ # The run #
25
+ # ---------------------------------------------------------------------------------------------------- #
26
+ simulator = Simulator(
27
+ event_generator=EventGenerator(
28
+ beam_1=2212, beam_2=2212, sqrt_s=5020, pt_min=80, pt_max=200, random_seed=42
29
+ ),
30
+ jet_finder=JetFinder(radius=0.4, pt_min=120, pt_max=140, max_abs_pseudorapidity=1.6),
31
+ particle_selector=ParticleSelector(max_abs_pseudorapidity=2.4),
32
+ )
33
+
34
+ simulator.simulate(
35
+ n_events=5_000,
36
+ observables=[
37
+ EEC(bin_edges=EEC_BIN_EDGES, pt_min=1, charged_only=True, use_pseudorapidity=True),
38
+ DoubleDifferentialJetCrossSection(
39
+ bin_edges=CROSS_SECTION_BIN_EDGES, max_abs_pseudorapidity=1.6
40
+ ),
41
+ ],
42
+ # Pass a list here to run several taggers at once. The output metadata then also reports, for every
43
+ # pair, how often they agreed on the jets both of them tagged.
44
+ jet_flavor_taggers=DescendancyTracing(delta_r_max=0.4, use_pseudorapidity=True),
45
+ output_path=OUTPUT_PATH,
46
+ verbose=True,
47
+ )
48
+
49
+ # ---------------------------------------------------------------------------------------------------- #
50
+ # Reading it back #
51
+ # ---------------------------------------------------------------------------------------------------- #
52
+ result = json.loads(open(OUTPUT_PATH).read())
53
+
54
+ counts = result["metadata"]["flavor_taggers"]["descendancy_tracing"]
55
+ print(f"quark-tagged jets: {counts['n_quark']}")
56
+ print(f"gluon-tagged jets: {counts['n_gluon']}")
57
+ print(f"untagged jets: {counts['n_untagged']}")
58
+
59
+ # finalize turns a saved histogram into the normalized observable. It is a classmethod, and it runs here,
60
+ # never during the simulation itself.
61
+ histograms = result["Energy-energy correlator"]["histograms"]
62
+ for label, saved in (
63
+ ("inclusive", histograms["inclusive"]),
64
+ ("quark", histograms["descendancy_tracing"]["quark"]),
65
+ ("gluon", histograms["descendancy_tracing"]["gluon"]),
66
+ ):
67
+ shape = EEC.finalize(saved["values"], EEC_BIN_EDGES, saved["metadata"])
68
+ peak = EEC_BIN_EDGES[:-1][int(np.argmax(shape))]
69
+ print(f"{label:>10} EEC peaks near ΔR = {peak:.3f}")
@@ -0,0 +1,102 @@
1
+ """
2
+ @Title: Writing your own observable.
3
+
4
+ @Description: Observables are the main extension point of jetgo. This example implements the jet
5
+ constituent multiplicity, a quantity that separates quark and gluon jets, and runs it
6
+ through the simulator exactly like a shipped observable.
7
+
8
+ The contract is six methods. ``fill`` receives one event's jets and adds them to a
9
+ histogram whose edges were fixed at construction; ``finalize`` is the classmethod that
10
+ turns the saved histogram into the normalized quantity, downstream.
11
+ """
12
+
13
+ import json
14
+ from typing import Dict, List
15
+
16
+ import fastjet as fj
17
+ import numpy as np
18
+
19
+ from jetgo.observables.base import Observable
20
+ from jetgo.simulation import EventGenerator, JetFinder, ParticleSelector, Simulator
21
+ from jetgo.taggers import DescendancyTracing
22
+
23
+ BIN_EDGES = np.arange(0.5, 60.5, 2.0)
24
+ OUTPUT_PATH = "constituent_multiplicity.json"
25
+
26
+
27
+ class ConstituentMultiplicity(Observable):
28
+ """
29
+ Distribution of the number of constituents per jet, normalized to unit area.
30
+ """
31
+
32
+ N_JETS_KEY = "n_jets"
33
+ BIN_EDGES_KEY = "bin_edges"
34
+
35
+ def __init__(self, bin_edges: np.ndarray, pt_min: float = 1.0) -> None:
36
+ self._bin_edges = np.asarray(bin_edges, dtype=float)
37
+ self._counts = np.zeros(len(self._bin_edges) - 1, dtype=float)
38
+ self._pt_min = pt_min
39
+ self._n_jets = 0
40
+
41
+ @property
42
+ def identifier(self) -> str:
43
+ # Shipped observables use an ObservableIdentifier member, whose value is the HEPData keyword. Any
44
+ # hashable, unique value works: the simulator only uses it as this observable's key in the output.
45
+ return "constituent_multiplicity"
46
+
47
+ def fill(self, jets: List[fj.PseudoJet], cluster: fj.ClusterSequence) -> None:
48
+ for jet in jets:
49
+ self._n_jets += 1
50
+ multiplicity = sum(1 for c in jet.constituents() if c.pt() > self._pt_min)
51
+
52
+ index = int(np.searchsorted(self._bin_edges, multiplicity, side="right")) - 1
53
+ if 0 <= index < len(self._counts):
54
+ self._counts[index] += 1.0
55
+
56
+ def scale(self, scale_factor: float) -> None:
57
+ # Self-normalized, like the EEC: the generator's cross-section factor cancels in the shape.
58
+ pass
59
+
60
+ @classmethod
61
+ def finalize(cls, values, bin_edges: np.ndarray, metadata: Dict) -> np.ndarray:
62
+ counts = np.asarray(values, dtype=float)
63
+ return counts / counts.sum() / np.diff(np.asarray(bin_edges, dtype=float))
64
+
65
+ def _get_complementary_metadata(self) -> Dict:
66
+ return {self.N_JETS_KEY: self._n_jets, self.BIN_EDGES_KEY: self._bin_edges.tolist()}
67
+
68
+ def _get_binned_values(self) -> List[float]:
69
+ return self._counts.tolist()
70
+
71
+
72
+ if __name__ == "__main__":
73
+ simulator = Simulator(
74
+ event_generator=EventGenerator(
75
+ beam_1=2212, beam_2=2212, sqrt_s=5020, pt_min=80, pt_max=200, random_seed=42
76
+ ),
77
+ jet_finder=JetFinder(radius=0.4, pt_min=120, pt_max=140, max_abs_pseudorapidity=1.6),
78
+ particle_selector=ParticleSelector(max_abs_pseudorapidity=2.4),
79
+ )
80
+
81
+ simulator.simulate(
82
+ n_events=5_000,
83
+ observables=ConstituentMultiplicity(bin_edges=BIN_EDGES),
84
+ jet_flavor_taggers=DescendancyTracing(delta_r_max=0.4, use_pseudorapidity=True),
85
+ output_path=OUTPUT_PATH,
86
+ verbose=True,
87
+ )
88
+
89
+ histograms = json.loads(open(OUTPUT_PATH).read())["constituent_multiplicity"]["histograms"]
90
+ centers = 0.5 * (BIN_EDGES[:-1] + BIN_EDGES[1:])
91
+
92
+ for label, saved in (
93
+ ("quark", histograms["descendancy_tracing"]["quark"]),
94
+ ("gluon", histograms["descendancy_tracing"]["gluon"]),
95
+ ):
96
+ shape = ConstituentMultiplicity.finalize(saved["values"], BIN_EDGES, saved["metadata"])
97
+ mean = float(np.sum(centers * shape * np.diff(BIN_EDGES)))
98
+ print(f"{label:>6} jets: mean constituent multiplicity = {mean:.1f}")
99
+
100
+ print("Gluon jets carry more constituents than quark jets: they radiate more, in the ratio of the")
101
+ print("color charges C_A/C_F. The measured ratio is well below that naive 9/4, since hadronization")
102
+ print("and the finite jet energy both dilute it.")
@@ -0,0 +1,20 @@
1
+ """
2
+ @file: __init__.py
3
+ @Author: Maxence Larose
4
+
5
+ @Creation Date: 09/2026
6
+ @Last modification: 09/2026
7
+
8
+ @Description: This file defines package-level metadata (version, author, license, maintainer) for the
9
+ ``jetgo`` package. It deliberately exposes no classes: the public API lives in the
10
+ subpackages, so that importing ``jetgo`` never pulls in Pythia8 or FastJet.
11
+ """
12
+
13
+ __version__ = "0.1.0"
14
+
15
+ __author__ = "Maxence Larose"
16
+ __credits__ = ["Maxence Larose"]
17
+ __license__ = "BSD-3-Clause"
18
+ __maintainer__ = "Maxence Larose"
19
+ __email__ = "maxence.larose@stonybrook.edu"
20
+ __status__ = "Production"
@@ -0,0 +1,63 @@
1
+ """
2
+ @file: _pythia.py
3
+ @Author: Maxence Larose
4
+
5
+ @Creation Date: 06/2026
6
+ @Last modification: 09/2026
7
+
8
+ @Description: This file resolves and re-exports the Pythia8 module, accounting for the different module
9
+ name used by the pip-installed ``pythia8mc`` package versus a self-compiled ``pythia8``
10
+ build.
11
+
12
+ Pythia8 is an optional dependency of this package: only event generation and particle
13
+ selection need it at runtime, so observables and taggers import without it. Importing
14
+ this module on a machine without either build raises with an actionable message rather
15
+ than a bare ModuleNotFoundError from somewhere deep in the import graph.
16
+ """
17
+
18
+ import importlib
19
+
20
+ # The module is named pythia8 when users compile it themselves, and pythia8mc when installed via pip.
21
+ _CANDIDATE_MODULE_NAMES = ("pythia8mc", "pythia8")
22
+
23
+ _NOT_INSTALLED_MESSAGE = (
24
+ "Pythia8 is required for event generation but is not installed. Install it with "
25
+ "`pip install jetgo[pythia]`, which pulls in the `pythia8mc` wheel, or compile Pythia8 yourself with its "
26
+ "Python interface enabled so that `import pythia8` works. Note that the `pythia8mc` wheels are Linux-only; "
27
+ "on another platform, a self-compiled build is the only option. The rest of jetgo, including every "
28
+ "observable and tagger, imports and runs without Pythia8."
29
+ )
30
+
31
+
32
+ class _Pythia8NotInstalled:
33
+ """
34
+ Stands in for the Pythia8 module when neither build is installed.
35
+
36
+ Importing a module must not fail merely because an optional dependency is absent, or nothing that
37
+ merely *mentions* Pythia8 could be imported either, including the modules whose only use of it is a
38
+ type annotation. Touching anything on this stand-in raises instead, so the error arrives when event
39
+ generation is actually attempted and names the way to fix it.
40
+ """
41
+
42
+ def __getattr__(self, name: str):
43
+ raise ModuleNotFoundError(_NOT_INSTALLED_MESSAGE)
44
+
45
+ def __bool__(self) -> bool:
46
+ return False
47
+
48
+
49
+ def _resolve():
50
+ for name in _CANDIDATE_MODULE_NAMES:
51
+ try:
52
+ return importlib.import_module(name)
53
+ except ModuleNotFoundError:
54
+ continue
55
+
56
+ return _Pythia8NotInstalled()
57
+
58
+
59
+ pythia8 = _resolve()
60
+
61
+ #: Whether a working Pythia8 build was found. Lets callers branch on availability without having to
62
+ #: import Pythia8 themselves, and lets the test suite skip what genuinely needs a generator.
63
+ PYTHIA8_AVAILABLE = not isinstance(pythia8, _Pythia8NotInstalled)