jetgo 0.1.0__py3-none-any.whl
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.
- jetgo/__init__.py +20 -0
- jetgo/_pythia.py +63 -0
- jetgo/kinematics.py +133 -0
- jetgo/observables/__init__.py +12 -0
- jetgo/observables/base.py +156 -0
- jetgo/observables/classes.py +18 -0
- jetgo/observables/double_differential_jet_cross_section.py +232 -0
- jetgo/observables/eec.py +310 -0
- jetgo/observables/identifier.py +22 -0
- jetgo/simulation/__init__.py +14 -0
- jetgo/simulation/event_generator.py +123 -0
- jetgo/simulation/jet_finder.py +110 -0
- jetgo/simulation/particle_selector.py +115 -0
- jetgo/simulation/simulation_summary.py +103 -0
- jetgo/simulation/simulator.py +377 -0
- jetgo/taggers/__init__.py +11 -0
- jetgo/taggers/base.py +114 -0
- jetgo/taggers/classes.py +17 -0
- jetgo/taggers/descendancy_tracing.py +593 -0
- jetgo/taggers/flavor.py +18 -0
- jetgo/taggers/identifier.py +21 -0
- jetgo-0.1.0.dist-info/METADATA +153 -0
- jetgo-0.1.0.dist-info/RECORD +25 -0
- jetgo-0.1.0.dist-info/WHEEL +4 -0
- jetgo-0.1.0.dist-info/licenses/LICENSE +29 -0
jetgo/__init__.py
ADDED
|
@@ -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"
|
jetgo/_pythia.py
ADDED
|
@@ -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)
|
jetgo/kinematics.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: kinematics.py
|
|
3
|
+
@Author: Maxence Larose
|
|
4
|
+
|
|
5
|
+
@Creation Date: 07/2026
|
|
6
|
+
@Last modification: 09/2026
|
|
7
|
+
|
|
8
|
+
@Description: This file defines the shared kinematic helpers used across the package: the ``delta_r``
|
|
9
|
+
function, so every part of the codebase that needs a ΔR (the EEC observable, a tagger
|
|
10
|
+
matching a jet to a parton, ...) agrees on the same definition, and the encoding of a
|
|
11
|
+
particle's Pythia8 index and charge into a ``PseudoJet``'s user index.
|
|
12
|
+
|
|
13
|
+
FastJet's ``PseudoJet`` carries no particle identity of its own, only a four-momentum and
|
|
14
|
+
a single integer slot, ``user_index()``. This package uses that slot to carry two facts
|
|
15
|
+
about the particle a ``PseudoJet`` was built from: its position in the Pythia8 event
|
|
16
|
+
record, and whether it is electrically charged. The magnitude is the Pythia8 index and
|
|
17
|
+
the sign is the charge, positive for charged and negative for neutral.
|
|
18
|
+
|
|
19
|
+
That encoding works because index 0 in a Pythia8 event record is a reserved placeholder
|
|
20
|
+
and never a real particle, so no real particle has an index whose sign is meaningless.
|
|
21
|
+
``ParticleSelector`` writes the encoding and observables read it back. Anyone writing
|
|
22
|
+
their own ``Observable`` subclass should read it through the functions below rather than
|
|
23
|
+
touching ``user_index()`` directly, since the encoding is an implementation detail that
|
|
24
|
+
these three functions define.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import math
|
|
28
|
+
|
|
29
|
+
import fastjet as fj
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def delta_r(
|
|
33
|
+
a: fj.PseudoJet,
|
|
34
|
+
b: fj.PseudoJet,
|
|
35
|
+
use_pseudorapidity: bool = False
|
|
36
|
+
) -> float:
|
|
37
|
+
"""
|
|
38
|
+
Angular separation ΔR between two four-vectors.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
a : fj.PseudoJet
|
|
43
|
+
First four-vector.
|
|
44
|
+
b : fj.PseudoJet
|
|
45
|
+
Second four-vector.
|
|
46
|
+
use_pseudorapidity : bool, default=False
|
|
47
|
+
If True, ΔR is computed using the pseudorapidity η instead of the (true, mass-dependent) rapidity y, i.e.
|
|
48
|
+
ΔR = sqrt((Δη)² + (Δφ)²) instead of ΔR = sqrt((Δy)² + (Δφ)²).
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
delta_r : float
|
|
53
|
+
ΔR = sqrt((Δy or Δη)² + (Δφ)²), with Δφ correctly wrapped to [0, π].
|
|
54
|
+
"""
|
|
55
|
+
if use_pseudorapidity:
|
|
56
|
+
d_y = a.eta() - b.eta()
|
|
57
|
+
else:
|
|
58
|
+
d_y = a.rap() - b.rap()
|
|
59
|
+
|
|
60
|
+
d_phi = a.delta_phi_to(b)
|
|
61
|
+
|
|
62
|
+
return math.sqrt(d_y ** 2 + d_phi ** 2)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def encode_user_index(
|
|
66
|
+
pythia_event_index: int,
|
|
67
|
+
charged: bool
|
|
68
|
+
) -> int:
|
|
69
|
+
"""
|
|
70
|
+
Encode a particle's Pythia8 event-record index and charge into a single ``PseudoJet`` user index.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
pythia_event_index : int
|
|
75
|
+
Position of the particle in the Pythia8 event record. Must be strictly positive: index 0 is Pythia8's
|
|
76
|
+
reserved placeholder entry and never a real particle.
|
|
77
|
+
charged : bool
|
|
78
|
+
Whether the particle is electrically charged.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
user_index : int
|
|
83
|
+
The index itself if the particle is charged, its negation if it is neutral.
|
|
84
|
+
|
|
85
|
+
Raises
|
|
86
|
+
------
|
|
87
|
+
ValueError
|
|
88
|
+
If ``pythia_event_index`` is not strictly positive, which would make the sign meaningless.
|
|
89
|
+
"""
|
|
90
|
+
if pythia_event_index <= 0:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Expected a strictly positive Pythia8 event-record index, got {pythia_event_index}. Index 0 is "
|
|
93
|
+
f"Pythia8's reserved placeholder and should never belong to a real particle, and the sign of the "
|
|
94
|
+
f"user index is reserved for the particle's charge."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return pythia_event_index if charged else -pythia_event_index
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def is_charged(particle: fj.PseudoJet) -> bool:
|
|
101
|
+
"""
|
|
102
|
+
Whether a particle is electrically charged, read back from its encoded user index.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
particle : fj.PseudoJet
|
|
107
|
+
A particle whose user index was written by ``encode_user_index``, i.e. one produced by
|
|
108
|
+
``ParticleSelector``.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
charged : bool
|
|
113
|
+
True if the particle is charged.
|
|
114
|
+
"""
|
|
115
|
+
return particle.user_index() > 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def pythia_index(particle: fj.PseudoJet) -> int:
|
|
119
|
+
"""
|
|
120
|
+
The particle's position in the Pythia8 event record, read back from its encoded user index.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
particle : fj.PseudoJet
|
|
125
|
+
A particle whose user index was written by ``encode_user_index``, i.e. one produced by
|
|
126
|
+
``ParticleSelector``.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
index : int
|
|
131
|
+
Position of the particle in the Pythia8 event record it was selected from.
|
|
132
|
+
"""
|
|
133
|
+
return abs(particle.user_index())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: __init__.py
|
|
3
|
+
@Author: Maxence Larose
|
|
4
|
+
|
|
5
|
+
@Creation Date: 06/2026
|
|
6
|
+
@Last modification: 06/2026
|
|
7
|
+
|
|
8
|
+
@Description: This file exposes the public observables of the ``observables`` package.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .double_differential_jet_cross_section import DoubleDifferentialJetCrossSection
|
|
12
|
+
from .eec import EEC
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: base.py
|
|
3
|
+
@Author: Maxence Larose
|
|
4
|
+
|
|
5
|
+
@Creation Date: 06/2026
|
|
6
|
+
@Last modification: 07/2026
|
|
7
|
+
|
|
8
|
+
@Description: This file defines the Observable abstract base class. It provides the shared interface and
|
|
9
|
+
common behavior that all physics observables inherit.
|
|
10
|
+
|
|
11
|
+
Observables accumulate a histogram during the simulation, on bin edges fixed when the
|
|
12
|
+
observable is constructed. Those edges are not a property of the physics being
|
|
13
|
+
simulated -- they come from outside it, from whichever measurement or analysis the run
|
|
14
|
+
will be read against -- so every observable takes them as a required argument rather
|
|
15
|
+
than choosing any itself.
|
|
16
|
+
|
|
17
|
+
Saving each contribution unbinned instead would leave that choice open until analysis
|
|
18
|
+
time, which is the more flexible design and was the original one. It does not survive
|
|
19
|
+
contact with the run sizes this project needs: an EEC stores one entry per constituent
|
|
20
|
+
*pair*, which grows quadratically with jet multiplicity, and a million-event window
|
|
21
|
+
costs gigabytes of RAM and JSON to keep values that are only ever histogrammed onto one
|
|
22
|
+
known binning anyway. Binning at fill time makes the cost independent of the run
|
|
23
|
+
length.
|
|
24
|
+
|
|
25
|
+
What remains downstream (see ``finalize``) is the normalization, plus whatever
|
|
26
|
+
re-binning each observable implements on top of the saved edges. Only the
|
|
27
|
+
cross-section does: it sums whole bins onto coarser ones, and its edges are
|
|
28
|
+
deliberately filled fine enough to be a refinement of every binning the analysis asks
|
|
29
|
+
for. The EEC serves only the edges it was filled on.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from abc import ABC, abstractmethod
|
|
33
|
+
from typing import Dict, List
|
|
34
|
+
|
|
35
|
+
import fastjet as fj
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
from .identifier import ObservableIdentifier
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Observable(ABC):
|
|
42
|
+
"""
|
|
43
|
+
Abstract base class for physics observables.
|
|
44
|
+
|
|
45
|
+
Defines the shared interface and delegates observable-specific logic to abstract methods implemented by each
|
|
46
|
+
concrete subclass.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
METADATA_KEY = "metadata"
|
|
50
|
+
VALUES_KEY = "values"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def identifier(self) -> ObservableIdentifier:
|
|
55
|
+
"""
|
|
56
|
+
Return the identifier of the observable. The identifier should match the observable identifier defined on the
|
|
57
|
+
HEP data website.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
identifier : ObservableIdentifier
|
|
62
|
+
Identifier of the observable.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def fill(
|
|
67
|
+
self,
|
|
68
|
+
jets: List[fj.PseudoJet],
|
|
69
|
+
cluster: fj.ClusterSequence
|
|
70
|
+
) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Extract this observable's contributions from one event and accumulate them into its histogram. Called
|
|
73
|
+
once per generated event inside the simulation loop. Subclasses decide which quantities to extract and
|
|
74
|
+
which bin each one lands in.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
jets : List[fj.PseudoJet]
|
|
79
|
+
Reconstructed jets for this event, as returned by the jet finder.
|
|
80
|
+
cluster : fj.ClusterSequence
|
|
81
|
+
Cluster sequence for this event, as returned by the cluster finder.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def scale(
|
|
86
|
+
self,
|
|
87
|
+
scale_factor: float | int
|
|
88
|
+
) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Record the event-normalization scale factor, applied later, at ``finalize`` time.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
scale_factor : float
|
|
95
|
+
Factor by which every bin will eventually be multiplied (``sigmaGen / weightSum`` from the generator).
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def finalize(cls, values, bin_edges: np.ndarray, metadata: Dict) -> np.ndarray:
|
|
101
|
+
"""
|
|
102
|
+
Normalize the histogram saved by ``to_dict`` onto ``bin_edges``.
|
|
103
|
+
|
|
104
|
+
Called downstream (by ``ObservableVisualizer`` or analysis code) -- never during the simulation itself.
|
|
105
|
+
``bin_edges`` cannot reopen the binning decision, which was made when the observable was constructed:
|
|
106
|
+
it is the caller stating which edges it believes it is reading, and each subclass either serves it from
|
|
107
|
+
the edges it was filled on or raises. Implementations must never silently return values on edges other
|
|
108
|
+
than the ones asked for.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
values
|
|
113
|
+
The saved histogram, in whatever shape this observable writes it (see each subclass's ``to_dict``).
|
|
114
|
+
bin_edges : np.ndarray
|
|
115
|
+
The bin edges the result is wanted on.
|
|
116
|
+
metadata : Dict
|
|
117
|
+
This observable's saved metadata (see ``_get_complementary_metadata``).
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
np.ndarray
|
|
122
|
+
Normalized values, one per target bin.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def _get_complementary_metadata(self) -> Dict:
|
|
127
|
+
"""
|
|
128
|
+
Return complementary metadata for this observable.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
complementary_metadata : Dict
|
|
133
|
+
Additional metadata needed for that specific observable.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def _get_binned_values(self):
|
|
138
|
+
"""
|
|
139
|
+
Return this observable's accumulated histogram (see ``fill``), one value per bin of the edges it was
|
|
140
|
+
constructed with.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def to_dict(self) -> Dict:
|
|
144
|
+
"""
|
|
145
|
+
Return this observable's accumulated histogram and metadata as a JSON-serializable dict.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
Dict
|
|
150
|
+
Dictionary with keys ``VALUES_KEY`` (one value per bin) and ``METADATA_KEY`` (observable-specific
|
|
151
|
+
metadata, which always includes the bin edges the values sit on).
|
|
152
|
+
"""
|
|
153
|
+
return {
|
|
154
|
+
self.VALUES_KEY: self._get_binned_values(),
|
|
155
|
+
self.METADATA_KEY: self._get_complementary_metadata(),
|
|
156
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: classes.py
|
|
3
|
+
@Author: Maxence Larose
|
|
4
|
+
|
|
5
|
+
@Creation Date: 08/2026
|
|
6
|
+
@Last modification: 08/2026
|
|
7
|
+
|
|
8
|
+
@Description: Maps each ``ObservableIdentifier`` to the ``Observable`` subclass that implements it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .double_differential_jet_cross_section import DoubleDifferentialJetCrossSection
|
|
12
|
+
from .eec import EEC
|
|
13
|
+
from .identifier import ObservableIdentifier
|
|
14
|
+
|
|
15
|
+
OBSERVABLE_CLASSES = {
|
|
16
|
+
ObservableIdentifier.D2SIG_DPT_DYRAP: DoubleDifferentialJetCrossSection,
|
|
17
|
+
ObservableIdentifier.EEC: EEC,
|
|
18
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: double_differential_jet_cross_section.py
|
|
3
|
+
@Author: Maxence Larose
|
|
4
|
+
|
|
5
|
+
@Creation Date: 06/2026
|
|
6
|
+
@Last modification: 07/2026
|
|
7
|
+
|
|
8
|
+
@Description: This file defines the DoubleDifferentialJetCrossSection observable, the double
|
|
9
|
+
differential inclusive jet cross-section d²σ/dpT dy. Jets are accumulated into a pT
|
|
10
|
+
histogram on the bin edges the observable is constructed with (see ``Observable``);
|
|
11
|
+
unlike the EEC, that grid can still be coarsened downstream, by summing whole bins
|
|
12
|
+
(see ``finalize``).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
import fastjet as fj
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from .base import Observable
|
|
21
|
+
from .identifier import ObservableIdentifier
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DoubleDifferentialJetCrossSection(Observable):
|
|
25
|
+
"""
|
|
26
|
+
Double-differential inclusive jet cross-section d²σ/dpT dy (or d²σ/dpT dη, if jets were selected in
|
|
27
|
+
pseudorapidity instead -- see ``__init__``).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
RAPIDITY_INTERVAL_KEY = "rapidity_interval"
|
|
31
|
+
SCALE_FACTOR_KEY = "scale_factor"
|
|
32
|
+
# The pT grid the values were accumulated on. Always written, and what ``finalize`` re-bins from -- a file
|
|
33
|
+
# without it predates forced binning and holds individual jet pT values instead.
|
|
34
|
+
BIN_EDGES_KEY = "bin_edges"
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
bin_edges: np.ndarray,
|
|
39
|
+
max_abs_rapidity: Optional[float | int] = None,
|
|
40
|
+
max_abs_pseudorapidity: Optional[float | int] = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Constructor for the JetCrossSection class.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
bin_edges : np.ndarray
|
|
48
|
+
The jet pT grid to accumulate the cross-section on. Required, so that the file size stays
|
|
49
|
+
independent of the run length and consistent with the EEC, which has no choice in the matter (see
|
|
50
|
+
``Observable``).
|
|
51
|
+
|
|
52
|
+
The edges must be a *refinement* of every binning the analysis will later ask for: the extractors
|
|
53
|
+
finalize over the single bin ``[pt_min, pt_max]`` while ``PowerIndexExtractor`` uses five equal
|
|
54
|
+
sub-bins, so a grid dividing both reproduces them exactly. ``finalize`` re-bins by summing whole
|
|
55
|
+
bins and raises if the requested edges do not fall on this grid.
|
|
56
|
+
max_abs_rapidity : Optional[float | int]
|
|
57
|
+
Maximum absolute rapidity |y| used in the jet selection. Exactly one of ``max_abs_rapidity``/
|
|
58
|
+
``max_abs_pseudorapidity`` must be given -- whichever cut ``JetFinder`` was actually configured
|
|
59
|
+
with -- since the acceptance interval width (Δy or Δη = 2 * the given value, assuming a symmetric
|
|
60
|
+
acceptance around mid-(pseudo)rapidity) is what the cross-section is normalized by.
|
|
61
|
+
max_abs_pseudorapidity : Optional[float | int]
|
|
62
|
+
Maximum absolute pseudorapidity |η| used in the jet selection, if that was the cut applied instead
|
|
63
|
+
of rapidity (e.g. matching a charged-particle-jet measurement).
|
|
64
|
+
"""
|
|
65
|
+
if (max_abs_rapidity is None) == (max_abs_pseudorapidity is None):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"Exactly one of max_abs_rapidity or max_abs_pseudorapidity must be given, matching whichever "
|
|
68
|
+
"cut JetFinder was actually configured with."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
acceptance = max_abs_rapidity if max_abs_rapidity is not None else max_abs_pseudorapidity
|
|
72
|
+
self._rapidity_interval = 2 * acceptance
|
|
73
|
+
self._scale_factor = 1.0
|
|
74
|
+
self._bin_edges = np.asarray(bin_edges, dtype=float)
|
|
75
|
+
self._counts = np.zeros(len(self._bin_edges) - 1, dtype=float)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def identifier(self) -> ObservableIdentifier:
|
|
79
|
+
"""
|
|
80
|
+
Identifier of the observable.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
identifier : ObservableIdentifier
|
|
85
|
+
Identifier of the observable.
|
|
86
|
+
"""
|
|
87
|
+
return ObservableIdentifier.D2SIG_DPT_DYRAP
|
|
88
|
+
|
|
89
|
+
def scale(
|
|
90
|
+
self,
|
|
91
|
+
scale_factor: float | int
|
|
92
|
+
) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Record the scale factor converting raw jet counts into an absolute cross-section in nb, applied later at
|
|
95
|
+
``finalize`` time.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
scale_factor : float
|
|
100
|
+
Ratio sigmaGen / weightSum returned by the generator.
|
|
101
|
+
"""
|
|
102
|
+
self._scale_factor = scale_factor
|
|
103
|
+
|
|
104
|
+
def fill(
|
|
105
|
+
self,
|
|
106
|
+
jets: List[fj.PseudoJet],
|
|
107
|
+
cluster: fj.ClusterSequence
|
|
108
|
+
) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Accumulate every jet reconstructed in one event into its transverse momentum bin. Jets falling outside
|
|
111
|
+
the grid are dropped, exactly as they would be when histogramming stored pT values after the fact.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
jets : List[fj.PseudoJet]
|
|
116
|
+
Reconstructed jets for this event, as returned by the jet finder after kinematic selections.
|
|
117
|
+
cluster : fj.ClusterSequence
|
|
118
|
+
Cluster sequence for this event. Unused by this observable.
|
|
119
|
+
"""
|
|
120
|
+
for jet in jets:
|
|
121
|
+
index = int(np.searchsorted(self._bin_edges, jet.pt(), side="right")) - 1
|
|
122
|
+
if 0 <= index < len(self._counts):
|
|
123
|
+
self._counts[index] += 1.0
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def finalize(cls, values: List[float | int], bin_edges: np.ndarray, metadata: Dict) -> np.ndarray:
|
|
127
|
+
"""
|
|
128
|
+
Sum the accumulated jet counts onto ``bin_edges`` and normalize into d²σ/dpT dy, by dividing by each
|
|
129
|
+
bin's width in pT and by the total rapidity interval Δy.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
values : List[float]
|
|
134
|
+
The jet count per bin of the grid the observable was filled on.
|
|
135
|
+
bin_edges : np.ndarray
|
|
136
|
+
The bin edges the result is wanted on. They must fall on that grid, so that each requested bin is a
|
|
137
|
+
whole number of grid bins.
|
|
138
|
+
metadata : Dict
|
|
139
|
+
This observable's saved metadata, must contain ``RAPIDITY_INTERVAL_KEY``, ``SCALE_FACTOR_KEY`` and
|
|
140
|
+
``BIN_EDGES_KEY``.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
np.ndarray
|
|
145
|
+
d²σ/dpT dy in nb/GeV, one value per target bin.
|
|
146
|
+
|
|
147
|
+
Raises
|
|
148
|
+
------
|
|
149
|
+
ValueError
|
|
150
|
+
If the requested edges do not lie on the filled grid, so the requested bins cannot be formed by
|
|
151
|
+
summing whole grid bins.
|
|
152
|
+
"""
|
|
153
|
+
bin_edges = np.asarray(bin_edges, dtype=float)
|
|
154
|
+
|
|
155
|
+
if cls.BIN_EDGES_KEY not in metadata:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
"This cross section holds individual jet pT values rather than a pT histogram, which no "
|
|
158
|
+
"version of this observable still produces. Regenerate the simulation, which will accumulate "
|
|
159
|
+
"it directly on a grid the requested edges can be summed from."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
counts = cls._rebin(np.asarray(values, dtype=float),
|
|
163
|
+
np.asarray(metadata[cls.BIN_EDGES_KEY], dtype=float), bin_edges)
|
|
164
|
+
|
|
165
|
+
bin_widths = np.diff(bin_edges)
|
|
166
|
+
rapidity_interval = metadata[cls.RAPIDITY_INTERVAL_KEY]
|
|
167
|
+
scale_factor = metadata[cls.SCALE_FACTOR_KEY]
|
|
168
|
+
|
|
169
|
+
return counts * scale_factor / (bin_widths * rapidity_interval)
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _rebin(counts: np.ndarray, filled_edges: np.ndarray, requested_edges: np.ndarray) -> np.ndarray:
|
|
173
|
+
"""
|
|
174
|
+
Sum a histogram's bins onto coarser edges that lie on its own grid.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
counts : np.ndarray
|
|
179
|
+
Counts on ``filled_edges``.
|
|
180
|
+
filled_edges : np.ndarray
|
|
181
|
+
The grid the observable was filled on.
|
|
182
|
+
requested_edges : np.ndarray
|
|
183
|
+
The coarser edges wanted, each of which must coincide with one of ``filled_edges``.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
np.ndarray
|
|
188
|
+
Counts on ``requested_edges``.
|
|
189
|
+
|
|
190
|
+
Raises
|
|
191
|
+
------
|
|
192
|
+
ValueError
|
|
193
|
+
If any requested edge does not lie on the filled grid.
|
|
194
|
+
"""
|
|
195
|
+
indices = []
|
|
196
|
+
for edge in requested_edges:
|
|
197
|
+
match = np.flatnonzero(np.isclose(filled_edges, edge, rtol=0.0, atol=1e-9))
|
|
198
|
+
if match.size == 0:
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"Requested bin edge {edge:.6g} does not lie on the grid this cross section was filled on "
|
|
201
|
+
f"({len(filled_edges) - 1} bins spanning [{filled_edges[0]:.6g}, {filled_edges[-1]:.6g}]). "
|
|
202
|
+
f"The individual jet pT values are no longer there, so this bin cannot be formed."
|
|
203
|
+
)
|
|
204
|
+
indices.append(int(match[0]))
|
|
205
|
+
|
|
206
|
+
return np.array([counts[lo:hi].sum() for lo, hi in zip(indices[:-1], indices[1:])], dtype=float)
|
|
207
|
+
|
|
208
|
+
def _get_complementary_metadata(self) -> dict:
|
|
209
|
+
"""
|
|
210
|
+
Return complementary metadata.
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
complementary_metadata : dict
|
|
215
|
+
Additional metadata.
|
|
216
|
+
"""
|
|
217
|
+
return {
|
|
218
|
+
self.RAPIDITY_INTERVAL_KEY: self._rapidity_interval,
|
|
219
|
+
self.SCALE_FACTOR_KEY: self._scale_factor,
|
|
220
|
+
self.BIN_EDGES_KEY: self._bin_edges.tolist(),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
def _get_binned_values(self) -> List[float]:
|
|
224
|
+
"""
|
|
225
|
+
Return the jet count accumulated in each pT bin so far.
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
List[float]
|
|
230
|
+
The jet count per pT bin.
|
|
231
|
+
"""
|
|
232
|
+
return self._counts.tolist()
|