API reference#

Simulation#

@file: simulator.py @Author: Maxence Larose

@Creation Date: 06/2026 @Last modification: 07/2026

@Description: This file defines the Simulator class, which orchestrates the full jet analysis workflow.

It generates events, selects particles, reconstructs jets, evaluates observables, and saves the resulting histograms to a single JSON file.

class jetgo.simulation.simulator.Simulator(event_generator, jet_finder, particle_selector)[source]#

Bases: object

High-level driver for a jet physics analysis workflow.

This class orchestrates the full event-level analysis pipeline: event generation, particle selection, jet reconstruction, and observable evaluation.

For each event, particles are selected according some acceptance criteria, clustered into jets, and passed to the observable for filling the histogram.

Parameters:
simulate(n_events, observables, output_path, jet_flavor_taggers=None, verbose=False)[source]#

Run the full event loop for one or more observables, optionally split by jet flavor.

Each event is processed sequentially: generation → particle selection → jet finding → observable histogram filling. Every observable in observables is filled with all reconstructed jets, regardless of flavor.

If jet_flavor_taggers is given, each tagger additionally produces a quark-tagged and a gluon-tagged copy of every observable (independent deep copies: same class, same construction parameters, empty histograms), filled only with the jets that tagger labels as a quark or a gluon respectively. Jets a tagger leaves untagged are excluded from both of its copies, but still contribute to the inclusive histogram.

All results are written to a single JSON file: run metadata (number of events, number of jets, and, for every tagger, its number of quark-tagged/gluon-tagged/untagged jets, plus, for every pair of taggers, how often they agree on flavor among jets both of them tag), plus the inclusive histogram of every observable and the quark/gluon histograms for every (observable, tagger) pair, keyed by ObservableIdentifier and JetFlavorTaggerIdentifier respectively.

Parameters:
n_eventsint

Number of events to generate.

observablesObservable or Sequence[Observable]

One observable, or a sequence of observables, whose internal histograms are filled event-by-event with all jets. Each observable must have a distinct identifier, since it is used as that observable’s key in the output file.

output_pathstr or Path

Output JSON file where every result is stored. Created or overwritten.

jet_flavor_taggersOptional[JetFlavorTagger or Sequence[JetFlavorTagger]]

One tagger, or a sequence of taggers, used to label each jet as a quark or a gluon. Each tagger must have a distinct identifier. If None (default), only inclusive histograms are produced.

verbosebool

Verbose mode.

Parameters:
Return type:

None

@file: event_generator.py @Author: Maxence Larose

@Creation Date: 06/2026 @Last modification: 06/2026

@Description: This file defines the EventGenerator class, a wrapper around Pythia8 for generating hard QCD

collision events with configurable beams, center-of-mass energy, and partonic phase space cuts.

class jetgo.simulation.event_generator.EventGenerator(beam_1, beam_2, sqrt_s, pt_min, pt_max, random_seed=0)[source]#

Bases: object

Pythia8 event generator for hard-QCD hadronic collision events.

This class configures Pythia8 for generic beam–beam collisions, restricts the hard-scattering phase space in pT-hat, and provides utilities for event generation and cross-section normalization.

Parameters:
property scale_factor: float#

Compute the event normalization factor for cross-section scaling.

In Pythia, events generated with restricted pT-hat ranges represent a biased Monte Carlo sample. Each event must therefore be reweighted to recover the physical cross-section.

The normalization factor is defined as:

scale_factor = σ_gen / Σw

where:
  • σ_gen is the Pythia estimate of the process cross-section;

  • Σw is the sum of event weights in the generated sample.

Returns:
scale_factor: float

Normalization factor in nanobarns (nb).

print_statistics()[source]#

Print Pythia run statistics, including cross-section estimates, event counts, and generation efficiency.

Return type:

None

@file: particle_selector.py @Author: Maxence Larose

@Creation Date: 06/2026 @Last modification: 07/2026

@Description: This file defines the ParticleSelector class, which selects particles from Pythia8 events for

jet reconstruction. It keeps final-state, visible particles, applies a rapidity acceptance cut, and converts the survivors into FastJet PseudoJet objects.

class jetgo.simulation.particle_selector.ParticleSelector(max_abs_rapidity=None, max_abs_pseudorapidity=None, charged_only=False)[source]#

Bases: object

An object that is used to apply particle-level selections to a Pythia event and converts the accepted particles into FastJet PseudoJet objects.

The selector retains only final-state, visible particles within a specified rapidity acceptance.

Parameters:
  • max_abs_rapidity (Optional[float | int])

  • max_abs_pseudorapidity (Optional[float | int])

  • charged_only (bool)

select(event)[source]#

Select particles from a Pythia event and convert them to a list of FastJet PseudoJets.

The following selections are applied:
  • Final-state particles only;

  • Visible particles only (interacting via electromagnetic or strong force);

  • Charged particles only, if charged_only was set;

  • |y| < max_abs_rapidity.

The Pythia8 index of each selected particle is encoded into the PseudoJet’s user_index() by jetgo.kinematics.encode_user_index, which defines the convention: its magnitude is the particle’s index in event, and its sign indicates whether the particle is charged (positive) or neutral (negative). Real final-state particles always have a positive index (index 0 in a Pythia8 event is a reserved placeholder, never a real particle), so this encoding is unambiguous and reversible.

Parameters:
eventpythia8.Event

A Pythia-generated event.

Returns:
particlesList[fj.PseudoJet]

Selected particles as FastJet PseudoJets, ready for jet clustering.

Parameters:

event (pythia8mc.Event)

Return type:

List[PseudoJet]

@file: jet_finder.py @Author: Maxence Larose

@Creation Date: 06/2026 @Last modification: 06/2026

@Description: This file defines the JetFinder class, a wrapper around FastJet for jet clustering and jet-level

selections on reconstructed particles.

class jetgo.simulation.jet_finder.JetFinder(radius, pt_min=None, pt_max=None, max_abs_rapidity=None, max_abs_pseudorapidity=None, clustering_algorithm=2, recombination_scheme=0, strategy=1)[source]#

Bases: object

A FastJet-based jet finder for clustering and applying jet-level selections.

Parameters:
get_cluster(particles)[source]#

Cluster particles into a FastJet ClusterSequence.

Parameters:
particlesList[fj.PseudoJet]

Input particles as FastJet PseudoJets.

Returns:
cluster_sequencefj.ClusterSequence

FastJet ClusterSequence.

Parameters:

particles (List[PseudoJet])

Return type:

ClusterSequence

find_jets(cluster_sequence)[source]#

Return jets passing the kinematic selections.

Parameters:
cluster_sequencefj.ClusterSequence

FastJet ClusterSequence.

Returns:
jetsList[fj.PseudoJet]

Reconstructed jets sorted by transverse momentum (highest first), after applying pT and rapidity selections.

Parameters:

cluster_sequence (ClusterSequence)

Return type:

List[PseudoJet]

@file: simulation_summary.py @Author: Maxence Larose

@Creation Date: 07/2026 @Last modification: 07/2026

@Description: This file defines the SimulationSummary dataclass, which accumulates jet counts and

pairwise flavor agreement over a full Simulator run, and knows how to print itself and serialize itself into the metadata saved alongside the histograms.

class jetgo.simulation.simulation_summary.SimulationSummary(n_events, n_jets, tagger_counts, pairwise_agreement)[source]#

Bases: object

Jet counts and pairwise flavor agreement accumulated over a full simulation run, saved as metadata alongside the histograms.

Parameters:
n_events: int#
n_jets: int#
tagger_counts: Dict[JetFlavorTagger, Dict[JetFlavor, int]]#
pairwise_agreement: Dict[Tuple[JetFlavorTagger, JetFlavorTagger], Dict[str, int]]#
print_statistics()[source]#

Print the run’s jet counts, any tagger-specific diagnostics, and, for every pair of taggers, how often they agree on flavor.

Return type:

None

to_dict()[source]#

Build the run metadata entry: number of events, number of jets, and, for every tagger, its counts per flavor plus any tagger-specific diagnostics (see JetFlavorTagger.get_diagnostics), plus every pair of taggers’ flavor agreement.

The pairwise agreement is a sanity check for tagger consistency: two taggers implementing genuinely different strategies should still agree on flavor most of the time for jets they both manage to tag, since the underlying hard-scattering ancestry is the same physical truth regardless of the algorithm used to recover it.

Returns:
dict

Metadata entry, saved under the "metadata" key of the output file.

Return type:

dict

Observables#

@file: base.py @Author: Maxence Larose

@Creation Date: 06/2026 @Last modification: 07/2026

@Description: This file defines the Observable abstract base class. It provides the shared interface and

common behavior that all physics observables inherit.

Observables accumulate a histogram during the simulation, on bin edges fixed when the observable is constructed. Those edges are not a property of the physics being simulated – they come from outside it, from whichever measurement or analysis the run will be read against – so every observable takes them as a required argument rather than choosing any itself.

Saving each contribution unbinned instead would leave that choice open until analysis time, which is the more flexible design and was the original one. It does not survive contact with the run sizes this project needs: an EEC stores one entry per constituent pair, which grows quadratically with jet multiplicity, and a million-event window costs gigabytes of RAM and JSON to keep values that are only ever histogrammed onto one known binning anyway. Binning at fill time makes the cost independent of the run length.

What remains downstream (see finalize) is the normalization, plus whatever re-binning each observable implements on top of the saved edges. Only the cross-section does: it sums whole bins onto coarser ones, and its edges are deliberately filled fine enough to be a refinement of every binning the analysis asks for. The EEC serves only the edges it was filled on.

class jetgo.observables.base.Observable[source]#

Bases: ABC

Abstract base class for physics observables.

Defines the shared interface and delegates observable-specific logic to abstract methods implemented by each concrete subclass.

METADATA_KEY = 'metadata'#
VALUES_KEY = 'values'#
abstract property identifier: ObservableIdentifier#

Return the identifier of the observable. The identifier should match the observable identifier defined on the HEP data website.

Returns:
identifierObservableIdentifier

Identifier of the observable.

abstractmethod fill(jets, cluster)[source]#

Extract this observable’s contributions from one event and accumulate them into its histogram. Called once per generated event inside the simulation loop. Subclasses decide which quantities to extract and which bin each one lands in.

Parameters:
jetsList[fj.PseudoJet]

Reconstructed jets for this event, as returned by the jet finder.

clusterfj.ClusterSequence

Cluster sequence for this event, as returned by the cluster finder.

Parameters:
  • jets (List[PseudoJet])

  • cluster (ClusterSequence)

Return type:

None

abstractmethod scale(scale_factor)[source]#

Record the event-normalization scale factor, applied later, at finalize time.

Parameters:
scale_factorfloat

Factor by which every bin will eventually be multiplied (sigmaGen / weightSum from the generator).

Parameters:

scale_factor (float | int)

Return type:

None

abstractmethod classmethod finalize(values, bin_edges, metadata)[source]#

Normalize the histogram saved by to_dict onto bin_edges.

Called downstream, by whatever plots or analyses the output – never during the simulation itself. bin_edges cannot reopen the binning decision, which was made when the observable was constructed: it is the caller stating which edges it believes it is reading, and each subclass either serves it from the edges it was filled on or raises. Implementations must never silently return values on edges other than the ones asked for.

Parameters:
values

The saved histogram, in whatever shape this observable writes it (see each subclass’s to_dict).

bin_edgesnp.ndarray

The bin edges the result is wanted on.

metadataDict

This observable’s saved metadata (see _get_complementary_metadata).

Returns:
np.ndarray

Normalized values, one per target bin.

Parameters:
Return type:

ndarray

to_dict()[source]#

Return this observable’s accumulated histogram and metadata as a JSON-serializable dict.

Returns:
Dict

Dictionary with keys VALUES_KEY (one value per bin) and METADATA_KEY (observable-specific metadata, which always includes the bin edges the values sit on).

Return type:

Dict

@file: eec.py @Author: Maxence Larose

@Creation Date: 06/2026 @Last modification: 07/2026

@Description: This file defines the EEC observable, the Energy-Energy Correlator as a function of the

angular separation ΔR between pairs of jet constituents. Every constituent pair is accumulated into a ΔR histogram on the bin edges the observable is constructed with – necessarily so, since one entry per pair is quadratic in jet multiplicity and storing them individually is what makes a large run unaffordable (see Observable).

Each pair is weighted by (z_i * z_j)^n, where z_i = pT_i / pT_jet is the constituent’s momentum fraction of its own jet – not by the raw (pT_i * pT_j)^n, so that a pair’s weight means the same thing regardless of the absolute pT of the jet it came from. That is what lets a per-jet-normalized EEC remain well defined across jets of different pT within one window. ΔR is computed manually from the rapidity (or, optionally, the pseudorapidity) and the azimuthal angle of each constituent.

class jetgo.observables.eec.EEC(bin_edges, pt_min=0, energy_weight=1, charged_only=False, use_pseudorapidity=False)[source]#

Bases: Observable

Energy-Energy Correlator (EEC) as a function of angular separation ΔR.

Defined as (Eq. 1):

EEC(ΔR) = (1 / W_pairs) * (1 / ΔR_bin) * Σ_{jets} Σ_{i<j ∈ jet} (z_i * z_j)^n

where z_i = pT_i / pT_jet is constituent i’s momentum fraction of its own jet, and W_pairs is the total sum of all pair weights,

W_pairs = Σ_{jets} Σ_{i<j ∈ jet} (z_i * z_j)^n,

such that the observable is normalized to unity after integration over ΔR. Optionally, only charged constituents and/or constituents above a minimum transverse momentum threshold can be included.

Parameters:
W_PAIRS_KEY = 'w_pairs'#
N_JETS_KEY = 'n_jets'#
BIN_EDGES_KEY = 'bin_edges'#
ORDERED_PAIR_FACTOR = 2#
property identifier: ObservableIdentifier#

Identifier of the observable.

Returns:
identifierObservableIdentifier

Identifier of the observable.

fill(jets, cluster)[source]#

Accumulate every constituent pair of every reconstructed jet from one event into the ΔR histogram.

For each jet, constituents are first filtered according to the minimum constituent pT requirement and, optionally, the charged-particle selection. All unique constituent pairs (i < j) are then considered and added to their ΔR bin, along with the total pair weight W_pairs accumulated for later normalization.

Parameters:
jetsList[fj.PseudoJet]

Reconstructed jets for this event.

clusterfj.ClusterSequence

Cluster sequence for this event. Unused by this observable.

Parameters:
  • jets (List[PseudoJet])

  • cluster (ClusterSequence)

Return type:

None

scale(scale_factor)[source]#

The EEC is normalized by the total pair weight W_pairs and is therefore independent of the event normalization. Consequently, this method intentionally performs no operation.

Parameters:
scale_factorfloat

Multiplicative scale factor. Ignored.

Parameters:

scale_factor (float)

Return type:

None

classmethod finalize(values, bin_edges, metadata)[source]#

Normalize the accumulated ΔR histogram into EEC(ΔR), by dividing by the total pair weight W_pairs and by each bin’s width in ΔR.

Parameters:
valuesList[float]

The accumulated pair weight per ΔR bin.

bin_edgesnp.ndarray

The bin edges the result is wanted on, which must be the ones the observable was filled with.

metadataDict

This observable’s saved metadata, must contain W_PAIRS_KEY and BIN_EDGES_KEY.

Returns:
np.ndarray

EEC(ΔR), one value per bin.

Raises:
ValueError

If the requested edges are not the ones the observable was filled on. Summing whole bins onto a coarser grid would be exact here as it is for the cross-section, but nothing asks for it – the EEC is always read on the measurement’s own edges – so anything else is refused rather than approximated.

Parameters:
Return type:

ndarray

classmethod normalization_denominator(metadata, per_jet_normalized)[source]#

The quantity this convention divides by to turn raw binned pair weight into an EEC.

This is what a flavor-summed EEC must be recombined with. finalize divides by the sample’s total pair weight and to_per_jet_normalized by its jet count, so for two flavors

EEC_inclusive = (D_q * s_q + D_g * s_g) / (D_q + D_g)

with D the value returned here – and not with the flavor cross sections, which is only the same thing when both flavors carry equal pair weight per jet. They do not: gluon jets carry about 11% more at R = 0.4, so weighting by cross section mis-mixes the two shapes by a coherent tilt of a couple of percent across ΔR. In the per-jet convention D is the jet count, which is proportional to the cross section, so that convention is unaffected.

Parameters:
metadataDict

This observable’s saved metadata, must contain W_PAIRS_KEY and N_JETS_KEY.

per_jet_normalizedbool

Whether the shape is per-jet normalized rather than normalized to unity.

Returns:
float

The flavor’s EEC normalization denominator.

Parameters:
  • metadata (Dict)

  • per_jet_normalized (bool)

Return type:

float

classmethod to_per_jet_normalized(shape, metadata)[source]#

Convert a unity-normalized EEC shape (as returned by finalize) into the per-jet normalized Σ_EEC convention that measurements are typically published in – i.e. divided by the number of jets rather than by the total pair weight, and summed over ordered constituent pairs rather than unordered ones (see ORDERED_PAIR_FACTOR).

Parameters:
shapenp.ndarray

Unity-normalized EEC(ΔR), one value per bin, as returned by finalize.

metadataDict

This observable’s saved metadata, must contain W_PAIRS_KEY and N_JETS_KEY.

Returns:
np.ndarray

Σ_EEC(ΔR) per jet, one value per bin.

Parameters:
Return type:

ndarray

@file: double_differential_jet_cross_section.py @Author: Maxence Larose

@Creation Date: 06/2026 @Last modification: 07/2026

@Description: This file defines the DoubleDifferentialJetCrossSection observable, the double

differential inclusive jet cross-section d²σ/dpT dy. Jets are accumulated into a pT histogram on the bin edges the observable is constructed with (see Observable); unlike the EEC, that grid can still be coarsened downstream, by summing whole bins (see finalize).

class jetgo.observables.double_differential_jet_cross_section.DoubleDifferentialJetCrossSection(bin_edges, max_abs_rapidity=None, max_abs_pseudorapidity=None)[source]#

Bases: Observable

Double-differential inclusive jet cross-section d²σ/dpT dy (or d²σ/dpT dη, if jets were selected in pseudorapidity instead – see __init__).

Parameters:
RAPIDITY_INTERVAL_KEY = 'rapidity_interval'#
SCALE_FACTOR_KEY = 'scale_factor'#
BIN_EDGES_KEY = 'bin_edges'#
property identifier: ObservableIdentifier#

Identifier of the observable.

Returns:
identifierObservableIdentifier

Identifier of the observable.

scale(scale_factor)[source]#

Record the scale factor converting raw jet counts into an absolute cross-section in nb, applied later at finalize time.

Parameters:
scale_factorfloat

Ratio sigmaGen / weightSum returned by the generator.

Parameters:

scale_factor (float | int)

Return type:

None

fill(jets, cluster)[source]#

Accumulate every jet reconstructed in one event into its transverse momentum bin. Jets falling outside the grid are dropped, exactly as they would be when histogramming stored pT values after the fact.

Parameters:
jetsList[fj.PseudoJet]

Reconstructed jets for this event, as returned by the jet finder after kinematic selections.

clusterfj.ClusterSequence

Cluster sequence for this event. Unused by this observable.

Parameters:
  • jets (List[PseudoJet])

  • cluster (ClusterSequence)

Return type:

None

classmethod finalize(values, bin_edges, metadata)[source]#

Sum the accumulated jet counts onto bin_edges and normalize into d²σ/dpT dy, by dividing by each bin’s width in pT and by the total rapidity interval Δy.

Parameters:
valuesList[float]

The jet count per bin of the grid the observable was filled on.

bin_edgesnp.ndarray

The bin edges the result is wanted on. They must fall on that grid, so that each requested bin is a whole number of grid bins.

metadataDict

This observable’s saved metadata, must contain RAPIDITY_INTERVAL_KEY, SCALE_FACTOR_KEY and BIN_EDGES_KEY.

Returns:
np.ndarray

d²σ/dpT dy in nb/GeV, one value per target bin.

Raises:
ValueError

If the requested edges do not lie on the filled grid, so the requested bins cannot be formed by summing whole grid bins.

Parameters:
Return type:

ndarray

@file: identifier.py @Author: Maxence Larose

@Creation Date: 06/2026 @Last modification: 06/2026

@Description: This file defines the ObservableIdentifier enumeration. It provides a strongly-typed set of

identifiers used to reference HEPData observables in a consistent and error-safe way across the codebase.

class jetgo.observables.identifier.ObservableIdentifier(*values)[source]#

Bases: StrEnum

Observable identifiers used by HEPData.

D2SIG_DPT_DYRAP = 'D2SIG/DPT/DYRAP'#
EEC = 'Energy-energy correlator'#

Taggers#

@file: base.py @Author: Maxence Larose

@Creation Date: 06/2026 @Last modification: 07/2026

@Description: This file defines the JetFlavorTagger abstract base class. It provides the shared interface

and common PDG-id-to-flavor mapping used by all concrete jet flavor tagging strategies.

class jetgo.taggers.base.JetFlavorTagger[source]#

Bases: ABC

Abstract interface for jet quark/gluon tagging strategies.

Implementations label a reconstructed jet as originating from a quark or a gluon (or leave it untagged) using information from the full Pythia8 event record for the event the jet was reconstructed from.

HARD_PROCESS_STATUS = 23#
QUARK_PDG_IDS = frozenset({1, 2, 3, 4, 5, 6})#
GLUON_PDG_ID = 21#
abstract property identifier: JetFlavorTaggerIdentifier#

Return the identifier of this tagging strategy. Used as a key to distinguish this tagger’s results when several taggers are saved to the same output file.

Returns:
identifierJetFlavorTaggerIdentifier

Identifier of the tagging strategy.

get_diagnostics()[source]#

Return tagger-specific diagnostic counters accumulated since construction, to be included in the run’s saved metadata and printed summary alongside the quark/gluon/untagged counts. Empty by default; a subclass that tracks something extra (e.g. how often it fell back to a secondary strategy) should override this.

Returns:
dict

Extra diagnostic counters, or an empty dict if this tagger doesn’t track any.

Return type:

dict

abstractmethod tag(jet, event)[source]#

Label a jet as originating from a quark or a gluon.

Parameters:
jetfj.PseudoJet

Reconstructed jet to tag.

eventpythia8.Event

Full Pythia8 event record for the same event the jet was reconstructed from. Must be the untouched event yielded by the generator, since it needs to still contain the original hard-scattering partons and, for ancestry-based strategies, the full particle history.

Returns:
flavorJetFlavor

JetFlavor.QUARK or JetFlavor.GLUON if the jet was successfully tagged, JetFlavor.UNTAGGED otherwise.

Parameters:
  • jet (PseudoJet)

  • event (pythia8.Event)

Return type:

JetFlavor

@file: descendancy_tracing.py @Author: Maxence Larose

@Creation Date: 07/2026 @Last modification: 07/2026

@Description: This file defines the DescendancyTracing tagger, which tags a jet via a pT-weighted

average vote cast by each hard-scattering parton, or one of its descendants, that reaches geometrically close to the jet axis. Jets left untagged by that search can optionally fall back to every quark/gluon daughter of the relevant beam-remnant parton (found by tracing the jet’s own constituents backward), letting them all vote the same way, to catch jets that genuinely originate from the beam remnant rather than from the two outgoing hard-scattering partons.

class jetgo.taggers.descendancy_tracing.FlavorDecisionRule(*values)[source]#

Bases: StrEnum

How DescendancyTracing picks the winning flavor out of a set of pT-weighted votes (see DescendancyTracing._decide_flavor).

AVERAGE_PT = 'average_pt'#
CLOSEST_PT_TO_JET = 'closest_pt_to_jet'#
class jetgo.taggers.descendancy_tracing.DescendancyTracing(delta_r_max, min_generations=5, max_generations=100, use_pseudorapidity=False, handle_untagged_jets=True, decision_rule=FlavorDecisionRule.AVERAGE_PT)[source]#

Bases: JetFlavorTagger

Tags a jet with a pT-weighted average vote cast by each hard-scattering parton, or one of its descendants, that reaches geometrically close to the jet axis.

For each hard-scattering (|status| == 23) parton, the parton itself and its shower are walked forward generation by generation, starting with the parton itself (generation 0), then its daughters (generation 1), then daughters of daughters, and so on. At each generation, every particle in it casts a vote (weighted by its pT) for its parton’s flavor, but only if it satisfies both:

  • It is within delta_r_max of the jet axis;

  • It is one of the jet’s constituents, or an ancestor of one.

Establishing that second condition is a two-step process, and neither step ever uses a mother pointer:

  1. Forward check (_valid_descendants_of): does the hard parton have any descendant that is a jet constituent at all, via daughterListRecursive()? If not, this parton contributes nothing, no matter how far the search below expands.

  2. Forward ancestor check (_is_ancestor_of_seed_constituent): every candidate the generation-by-generation search below actually visits – not the hard parton’s entire shower up front, since with the default min_generations the search almost always settles well before reaching final-state hadronization, so most of that shower is never visited at all – is checked, one at a time, the same way: does its own daughterListRecursive() reach one of the constituents confirmed in step 1? If so, it is recognized as “on the path” and may cast a vote, even though it isn’t a jet constituent itself. This is needed because the search usually finds a candidate long before it reaches the constituent itself.

    Mother pointers (mother1()/mother2()) are deliberately not used for either step. PYTHIA8’s color reconnection can rearrange which partons end up color-connected into the same hadronizing string, so a hadron’s recorded mother1()/mother2() can point somewhere entirely disconnected from its true shower-emission history, even when a forward path from the hard parton to that hadron genuinely exists. Both steps here rely only on daughterListRecursive(), which is unaffected by this and remains exhaustive.

At least min_generations generations are always explored for every hard parton (not just until the first parton to find a vote), so that partons whose shower happens to resolve close to the jet earlier don’t cut the search short for a parton that would only be identified deeper in its own shower. If no parton has collected any vote after min_generations generations, the search keeps expanding, for every parton simultaneously, one generation at a time, until at least one vote is found or every parton’s shower is fully exhausted.

The jet is tagged according to decision_rule (see _decide_flavor): by default, whichever flavor has the higher average pT among its own collected votes (not the sum, so a flavor with fewer but harder matching particles can outrank one with more but softer ones). If neither flavor collects any vote, the jet is left untagged by this search.

If handle_untagged_jets is True (the default), a jet still untagged at that point gets one more chance: some jets genuinely originate from a beam-remnant parton (spawned to conserve color/quantum numbers once the actual hard-scattering parton is removed from the incoming proton) rather than from either of the two outgoing hard-scattering partons, so no |status| == 23 parton was ever going to claim them. For such a jet, every constituent is traced backward through mother1() to its earliest quark/gluon ancestor (see _trace_to_earliest_ancestor), and that ancestor’s own mother – typically the incoming beam proton itself – is used to enumerate every one of its quark/gluon daughters (see _find_fallback_hard_partons).

Rather than picking a single substitute parton, every one of these candidates is run through the exact same generation-by-generation forward search as a real hard parton, so the same pT-weighted-average vote decides the flavor – this mirrors how the primary search above never commits to a single hard parton in advance either; it considers every |status| == 23 parton in the event and lets them all vote. Committing to one substitute would be unprincipled here anyway: these beam-remnant partons are typically pieces of a single jointly-fragmenting color system, so more than one of them can legitimately claim to have contributed to the same jet. If no candidate collects any vote, or no constituent’s lineage passes through a quark or a gluon at all, the jet remains untagged.

Parameters:
MAX_ANCESTRY_STEPS = 500#
get_diagnostics()[source]#

Return how many times the fallback (see handle_untagged_jets) was invoked, i.e. how many jets the normal hard-parton search alone left untagged.

Returns:
dict

{"fallback_call_count": <int>}.

Return type:

dict

property identifier: JetFlavorTaggerIdentifier#

Identifier of the tagging strategy.

Returns:
identifierJetFlavorTaggerIdentifier

Identifier of the tagging strategy.

tag(jet, event)[source]#

Label a jet as originating from a quark or a gluon.

Parameters:
jetfj.PseudoJet

Reconstructed jet to tag.

eventpythia8.Event

Full Pythia8 event record for the same event the jet was reconstructed from. Must be the untouched event yielded by the generator, since it needs to still contain the original hard-scattering partons and their full shower history.

Returns:
flavorJetFlavor

JetFlavor.QUARK or JetFlavor.GLUON if the jet was successfully tagged, JetFlavor.UNTAGGED otherwise.

Parameters:
  • jet (PseudoJet)

  • event (pythia8.Event)

Return type:

JetFlavor

@file: flavor.py @Author: Maxence Larose

@Creation Date: 06/2026 @Last modification: 07/2026

@Description: This file defines the JetFlavor enumeration, used to label a reconstructed jet as

originating from a quark, a gluon, or as untagged.

class jetgo.taggers.flavor.JetFlavor(*values)[source]#

Bases: StrEnum

GLUON = 'gluon'#
QUARK = 'quark'#
UNTAGGED = 'untagged'#

@file: identifier.py @Author: Maxence Larose

@Creation Date: 07/2026 @Last modification: 07/2026

@Description: This file defines the JetFlavorTaggerIdentifier enumeration. It provides a strongly-typed set

of identifiers used to reference jet flavor tagging strategies in a consistent and error-safe way across the codebase, e.g. as keys when saving results to disk.

class jetgo.taggers.identifier.JetFlavorTaggerIdentifier(*values)[source]#

Bases: StrEnum

Identifiers for jet flavor tagging strategies.

DESCENDANCY_TRACING = 'descendancy_tracing'#

Kinematics#

@file: kinematics.py @Author: Maxence Larose

@Creation Date: 07/2026 @Last modification: 09/2026

@Description: This file defines the shared kinematic helpers used across the package: the delta_r

function, so every part of the codebase that needs a ΔR (the EEC observable, a tagger matching a jet to a parton, …) agrees on the same definition, and the encoding of a particle’s Pythia8 index and charge into a PseudoJet’s user index.

FastJet’s PseudoJet carries no particle identity of its own, only a four-momentum and a single integer slot, user_index(). This package uses that slot to carry two facts about the particle a PseudoJet was built from: its position in the Pythia8 event record, and whether it is electrically charged. The magnitude is the Pythia8 index and the sign is the charge, positive for charged and negative for neutral.

That encoding works because index 0 in a Pythia8 event record is a reserved placeholder and never a real particle, so no real particle has an index whose sign is meaningless. ParticleSelector writes the encoding and observables read it back. Anyone writing their own Observable subclass should read it through the functions below rather than touching user_index() directly, since the encoding is an implementation detail that these three functions define.

jetgo.kinematics.delta_r(a, b, use_pseudorapidity=False)[source]#

Angular separation ΔR between two four-vectors.

Parameters:
afj.PseudoJet

First four-vector.

bfj.PseudoJet

Second four-vector.

use_pseudorapiditybool, default=False

If True, ΔR is computed using the pseudorapidity η instead of the (true, mass-dependent) rapidity y, i.e. ΔR = sqrt((Δη)² + (Δφ)²) instead of ΔR = sqrt((Δy)² + (Δφ)²).

Returns:
delta_rfloat

ΔR = sqrt((Δy or Δη)² + (Δφ)²), with Δφ correctly wrapped to [0, π].

Parameters:
  • a (PseudoJet)

  • b (PseudoJet)

  • use_pseudorapidity (bool)

Return type:

float

jetgo.kinematics.encode_user_index(pythia_event_index, charged)[source]#

Encode a particle’s Pythia8 event-record index and charge into a single PseudoJet user index.

Parameters:
pythia_event_indexint

Position of the particle in the Pythia8 event record. Must be strictly positive: index 0 is Pythia8’s reserved placeholder entry and never a real particle.

chargedbool

Whether the particle is electrically charged.

Returns:
user_indexint

The index itself if the particle is charged, its negation if it is neutral.

Raises:
ValueError

If pythia_event_index is not strictly positive, which would make the sign meaningless.

Parameters:
  • pythia_event_index (int)

  • charged (bool)

Return type:

int

jetgo.kinematics.is_charged(particle)[source]#

Whether a particle is electrically charged, read back from its encoded user index.

Parameters:
particlefj.PseudoJet

A particle whose user index was written by encode_user_index, i.e. one produced by ParticleSelector.

Returns:
chargedbool

True if the particle is charged.

Parameters:

particle (PseudoJet)

Return type:

bool

jetgo.kinematics.pythia_index(particle)[source]#

The particle’s position in the Pythia8 event record, read back from its encoded user index.

Parameters:
particlefj.PseudoJet

A particle whose user index was written by encode_user_index, i.e. one produced by ParticleSelector.

Returns:
indexint

Position of the particle in the Pythia8 event record it was selected from.

Parameters:

particle (PseudoJet)

Return type:

int