Writing your own tagger#

A tagger answers one question about one jet: was it initiated by a quark or a gluon? Subclass JetFlavorTagger and implement identifier and tag.

import fastjet as fj

from jetgo.taggers.base import JetFlavorTagger
from jetgo.taggers.flavor import JetFlavor


class MyTagger(JetFlavorTagger):

    @property
    def identifier(self) -> str:
        return "my_tagger"

    def tag(self, jet: fj.PseudoJet, event) -> JetFlavor:
        ...
        return JetFlavor.QUARK  # or GLUON, or UNTAGGED

The event handed to tag is the untouched PYTHIA8 record for the event the jet was reconstructed from, so the full particle history is available. Return JetFlavor.UNTAGGED rather than guessing when the answer is not there: untagged jets are excluded from the flavor histograms but still counted inclusively, so the inclusive result stays correct.

The base class gives you _get_flavor_from_pdg_id, which maps a PDG id onto a flavor, and get_diagnostics, which you can override to fold your own counters into the run metadata.

Comparing taggers#

Pass several taggers to one run and each gets its own quark and gluon histograms. The output metadata then also reports, for every pair, how often they agreed on the jets both of them tagged:

simulator.simulate(
    ...,
    jet_flavor_taggers=[DescendancyTracing(delta_r_max=0.4), MyTagger()],
)

Every jet is tagged exactly once per tagger in a single pass, so the comparison is on identical jets rather than on two separate runs. This is the check worth running on any new definition: two strategies recovering the same physical truth should mostly agree, and where they do not is where the definition is doing real work.

The shipped tagger#

DescendancyTracing starts from the outgoing hard partons, those with PYTHIA status 23, and walks their shower descendants forward. Descendants that land within delta_r_max of the jet axis vote for their ancestor’s flavor, weighted by transverse momentum.

The direction matters. Tracing a constituent’s mother list backward is the more obvious construction, but color reconnection rewrites those mother links, so a hadron’s recorded ancestry need not be the shower it physically came from. Descendancy tracing reads only daughterListRecursive from the hard partons outward, which color reconnection does not touch.

Jets whose lineage traces to the beam remnant rather than to the hard scattering have no status-23 ancestor to vote for them. With handle_untagged_jets=True, the default, an explicit fallback finds the relevant beam-remnant partons instead, so no jet is discarded, and the number of times it fired is reported in the run metadata as fallback_call_count.

That path is rare. Across 7,999 jets above 100 GeV in a 5.02 TeV proton-proton run at R = 0.4, the fallback fired for 0.3% of them and left nothing untagged; with handle_untagged_jets=False, those same 0.3% come back as JetFlavor.UNTAGGED instead. Pass False if you would rather see them marked untagged than resolved against the beam remnant.