Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-11-03 08:57:23

0001 // This file is part of the ACTS project.
0002 //
0003 // Copyright (C) 2016 CERN for the benefit of the ACTS project
0004 //
0005 // This Source Code Form is subject to the terms of the Mozilla Public
0006 // License, v. 2.0. If a copy of the MPL was not distributed with this
0007 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
0008 
0009 #include "Acts/Definitions/ParticleData.hpp"
0010 #include "Acts/Definitions/PdgParticle.hpp"
0011 
0012 #include <algorithm>
0013 #include <array>
0014 #include <cmath>
0015 #include <ostream>
0016 #include <ranges>
0017 #include <utility>
0018 #include <vector>
0019 
0020 // This file is based on the following files, with modifications:
0021 //
0022 // https://gitlab.cern.ch/atlas/athena/-/blob/b0898f93585c4eec97550a9e0a16f5b6e6b6b973/Generators/TruthUtils/TruthUtils/AtlasPID.h
0023 //
0024 // These files are licensed under Apache License 2.0
0025 
0026 namespace Acts {
0027 namespace {
0028 
0029 class DecodedPID : public std::pair<int, std::vector<int>> {
0030  public:
0031   explicit DecodedPID(const int& p) {
0032     this->first = p;
0033     this->second.reserve(10);
0034     int ap = std::abs(p);
0035     for (; ap != 0; ap /= 10) {
0036       this->second.push_back(ap % 10);
0037     }
0038     std::ranges::reverse(this->second);
0039   }
0040   inline DecodedPID shift(const std::size_t n) const {
0041     return DecodedPID(this->first %
0042                       static_cast<int>(std::pow(10, ndigits() - n)));
0043   }
0044   inline const int& operator()(const std::size_t n) const {
0045     return this->second.at(n);
0046   }
0047   inline const int& last() const { return this->second.back(); }
0048   inline const int& pid() const { return this->first; }
0049   inline int max_digit(const int m, const int n) const {
0050     return *std::max_element(second.rbegin() + m, second.rbegin() + n);
0051   }
0052   inline int min_digit(const int m, const int n) const {
0053     return *std::min_element(second.rbegin() + m, second.rbegin() + n);
0054   }
0055   inline std::size_t ndigits() const { return this->second.size(); }
0056 };
0057 
0058 const int TABLESIZE = 100;
0059 const std::array<int, TABLESIZE> triple_charge = {
0060     +0, -1, +2, -1, +2, -1, +2, -1, +2, +0, +0, -3, +0, -3, +0, -3, +0,
0061     -3, +0, +0, +0, +0, +0, +0, +3, +0, +0, +0, +0, +0, +0, +0, +0, +0,
0062     +3, +0, +0, +3, +6, +0, +0, +0, -1, +0, +0, +0, +0, +0, +0, +0, +0,
0063     +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0,
0064     +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0,
0065     +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0, +0};
0066 
0067 const int DQUARK = 1;
0068 const int UQUARK = 2;
0069 const int SQUARK = 3;
0070 const int CQUARK = 4;
0071 const int BQUARK = 5;
0072 const int TQUARK = 6;
0073 
0074 const int ELECTRON = 11;
0075 const int MUON = 13;
0076 const int TAU = 15;
0077 
0078 const int GLUON = 21;
0079 // APID: 9 rather than 21 is used to denote a gluon/gluino in composite states.
0080 // (From PDG 11g)
0081 const int COMPOSITEGLUON = 9;
0082 const int PHOTON = 22;
0083 const int Z0BOSON = 23;
0084 const int ZPRIME = 32;     // Z′/Z^0_2
0085 const int ZDBLPRIME = 33;  // Z′′/Z^0_3
0086 const int LEPTOQUARK = 42;
0087 
0088 /// PDG Ids for Mavtop madgraph UFO model found under DarkX. The
0089 /// mavtop is a vector-like top partner with coupling to a dark photon.
0090 /// Theory paper: https://arxiv.org/abs/1904.05893
0091 /// Pheno paper: https://arxiv.org/pdf/2112.08425
0092 const int MAVTOP = 60001;
0093 
0094 const int K0L = 130;
0095 
0096 const int K0S = 310;
0097 const int K0 = 311;
0098 const int PROTON = 2212;
0099 
0100 /// PDG rule 10:
0101 /// Codes 81–100 are reserved for generator-specific pseudoparticles and
0102 /// concepts. Codes 901–930, 1901–1930, 2901–2930, and 3901–3930 are for
0103 /// additional components of Standard Modelparton distribution functions, where
0104 /// the latter three ranges are intended to distinguish left/right/ longitudinal
0105 /// components. Codes 998 and 999 are reserved for GEANT tracking pur-poses.
0106 const int GEANTINOPLUS = 998;
0107 const int GEANTINO0 = 999;
0108 
0109 /// PDG rule 2:
0110 /// Quarks and leptons are numbered consecutively starting from 1 and 11
0111 /// respectively; to dothis they are first ordered by family and within
0112 /// families by weak isospin.
0113 /// APID: the fourth generation quarks are quarks.
0114 bool isQuark(int p) {
0115   return p != 0 && (std::abs(p) <= 8 || std::abs(p) == MAVTOP);
0116 }
0117 
0118 bool isSMQuark(int p) {
0119   return p != 0 && std::abs(p) <= TQUARK;
0120 }
0121 
0122 /// PDG rule 4
0123 /// Diquarks have 4-digit numbers with nq1 >= nq2 and nq3 = 0
0124 /// APID: the diquarks with fourth generation are not diquarks
0125 bool isDiquark(const DecodedPID& p) {
0126   if (p.ndigits() == 4 && p(0) >= p(1) && p(2) == 0 && p.last() % 2 == 1 &&
0127       p.max_digit(2, 4) <= TQUARK) {
0128     return true;
0129   }
0130   return false;
0131 }
0132 
0133 /// Table 43.1
0134 ///  PDG rule 5a:
0135 ///  The numbers specifying the meson's quark content conform to the convention
0136 ///  nq1= 0 and nq2 >= nq3. The special case K0L is the sole exception to this
0137 ///  rule. PDG rule 5C: The special numbers 310 and 130 are given to the K0S and
0138 ///  K0L respectively. APID: The special code K0 is used when a generator uses
0139 ///  K0S/K0L
0140 bool isMeson(const DecodedPID& p) {
0141   if (p.ndigits() < 3) {
0142     return false;
0143   }
0144   if (p.ndigits() == 7 && (p(0) == 1 || p(0) == 2)) {
0145     return false;  // APID don't match SUSY particles
0146   }
0147   if (std::abs(p.pid()) == K0S) {
0148     return true;
0149   }
0150   if (std::abs(p.pid()) == K0L) {
0151     return true;
0152   }
0153   if (std::abs(p.pid()) == K0) {
0154     return true;
0155   }
0156   if (p.last() % 2 != 1) {
0157     return false;
0158   }
0159   if (p.max_digit(1, 3) >= 6) {
0160     return false;
0161   }
0162   if (p.max_digit(1, 3) == 0) {
0163     return false;
0164   }
0165   if (p.ndigits() > 3 && *(p.second.rbegin() + 3) != 0) {
0166     return false;
0167   }
0168 
0169   if (p.ndigits() == 3 && p(0) == p(1) && p.pid() < 0) {
0170     return false;
0171   }
0172   if (p.ndigits() == 5 && p(2) == p(3) && p.pid() < 0) {
0173     return false;
0174   }
0175   if (p.ndigits() == 7 && p(4) == p(5) && p.pid() < 0) {
0176     return false;
0177   }
0178 
0179   if (p.ndigits() == 3 && p(0) >= p(1) && p(1) != 0) {
0180     return true;
0181   }
0182   if (p.ndigits() == 5 && p(2) >= p(3) && p(3) != 0 && p(0) == 1 && p(1) == 0) {
0183     return true;
0184   }
0185   if (p.ndigits() == 5 && p(2) >= p(3) && p(3) != 0 && p(0) == 2 && p(1) == 0 &&
0186       p.last() > 1) {
0187     return true;
0188   }
0189   if (p.ndigits() == 5 && p(2) >= p(3) && p(3) != 0 && p(0) == 3 && p(1) == 0 &&
0190       p.last() > 1) {
0191     return true;
0192   }
0193 
0194   if (p.ndigits() == 6 && p(3) >= p(4) && p(4) != 0 && p.last() % 2 == 1) {
0195     return true;
0196   }
0197 
0198   if (p.ndigits() == 7 && p(0) == 9 && p(1) == 0 && p(4) >= p(5) && p(5) != 0) {
0199     return true;
0200   }
0201 
0202   return false;
0203 }
0204 
0205 /// Table 43.2
0206 bool isBaryon(const DecodedPID& p) {
0207   if (p.ndigits() < 4) {
0208     return false;
0209   }
0210   if (p.max_digit(1, 4) >= 6) {
0211     return false;
0212   }
0213   if (p.min_digit(1, 4) == 0) {
0214     return false;
0215   }
0216   if (p.ndigits() == 4 &&
0217       (p.last() == 2 || p.last() == 4 || p.last() == 6 || p.last() == 8)) {
0218     return true;
0219   }
0220 
0221   if (p.ndigits() == 5 && p(0) == 1 && (p.last() == 2 || p.last() == 4)) {
0222     return true;
0223   }
0224   if (p.ndigits() == 5 && p(0) == 3 && (p.last() == 2 || p.last() == 4)) {
0225     return true;
0226   }
0227 
0228   if (p.ndigits() == 6) {
0229     if (p(0) == 1 && p(1) == 0 && p.last() == 2) {
0230       return true;
0231     }
0232     if (p(0) == 1 && p(1) == 1 && p.last() == 2) {
0233       return true;
0234     }
0235     if (p(0) == 1 && p(1) == 2 && p.last() == 4) {
0236       return true;
0237     }
0238 
0239     if (p(0) == 2 && p(1) == 0 && p.last() == 2) {
0240       return true;
0241     }
0242     if (p(0) == 2 && p(1) == 0 && p.last() == 4) {
0243       return true;
0244     }
0245     if (p(0) == 2 && p(1) == 1 && p.last() == 2) {
0246       return true;
0247     }
0248 
0249     if (p(0) == 1 && p(1) == 0 && p.last() == 4) {
0250       return true;
0251     }
0252     if (p(0) == 1 && p(1) == 0 && p.last() == 6) {
0253       return true;
0254     }
0255     if (p(0) == 2 && p(1) == 0 && p.last() == 6) {
0256       return true;
0257     }
0258     if (p(0) == 2 && p(1) == 0 && p.last() == 8) {
0259       return true;
0260     }
0261   }
0262 
0263   if (p.ndigits() == 5) {
0264     if (p(0) == 2 && p.last() == 2) {
0265       return true;
0266     }
0267     if (p(0) == 2 && p.last() == 4) {
0268       return true;
0269     }
0270     if (p(0) == 2 && p.last() == 6) {
0271       return true;
0272     }
0273     if (p(0) == 5 && p.last() == 2) {
0274       return true;
0275     }
0276     if (p(0) == 1 && p.last() == 6) {
0277       return true;
0278     }
0279     if (p(0) == 4 && p.last() == 2) {
0280       return true;
0281     }
0282   }
0283   return false;
0284 }
0285 
0286 /// PDG rule 14
0287 /// The 9-digit tetra-quark codes are±1nrnLnq1nq20nq3nq4nJ. For the
0288 /// particleq1q2is a diquarkand
0289 /// ̄q3 ̄q4an antidiquark, sorted such thatnq1≥nq2,nq3≥nq4,nq1≥nq3,
0290 /// andnq2≥nq4ifnq1=nq3.
0291 /// For the antiparticle, given with a negative sign, ̄q1 ̄q2is an antidiquark
0292 /// andq3q4a diquark,
0293 /// with the same sorting except that eithernq1> nq3ornq2> nq4(so
0294 /// thatflavour-diagonal states are particles). Thenr,nL, andnJnumbers have the
0295 /// same meaningas for ordinary hadrons.
0296 bool isTetraquark(const DecodedPID& p) {
0297   return (p.ndigits() == 9 && p(0) == 1 && p(5) == 0 &&
0298           p.max_digit(1, 3) <= 6 && p.min_digit(1, 3) > 0 &&
0299           p.max_digit(1 + 3, 3 + 3) <= 6 && p.min_digit(1 + 3, 3 + 3) > 0 &&
0300           (p(3) >= p(4) && p(6) >= p(7)) &&
0301           ((p(3) > p(6)) || (p(3) == p(6) && (p(4) >= p(7)))));
0302 }
0303 
0304 /// PDG rule 15
0305 /// The 9-digit penta-quark codes are±1nrnLnq1nq2nq3nq4nq5nJ, sorted such
0306 /// thatnq1≥nq2≥nq3≥nq4. In the particle the first four are quarks and the fifth
0307 /// an antiquark while t
0308 /// heopposite holds in the antiparticle, which is given with a negative sign.
0309 /// Thenr,nL, andnJnumbers have the same meaning as for ordinary hadrons.
0310 bool isPentaquark(const DecodedPID& p) {
0311   return (p.ndigits() == 9 && p(0) == 1 && p.max_digit(1, 6) <= 6 &&
0312           p.min_digit(1, 6) > 0 &&
0313           (p(3) >= p(4) && p(4) >= p(5) && p(5) >= p(6)));
0314 }
0315 
0316 // APID Mesons, Baryons, Tetraquarks and Pentaquarks are Hadrons
0317 bool isHadron(const DecodedPID& p) {
0318   return isMeson(p) || isBaryon(p) || isTetraquark(p) || isPentaquark(p);
0319 }
0320 
0321 /// PDG rule 9:
0322 /// Two-digit numbers in the range 21–30 are provided for the Standard
0323 /// Model gauge and Higgs bosons.
0324 /// PDG rule 11b:
0325 /// The graviton and the boson content of a two-Higgs-doublet scenario
0326 /// and of additional SU(2)×U(1) groups are found in the range 31–40.
0327 
0328 bool isGluon(int p) {
0329   return p == GLUON;
0330 }
0331 
0332 bool isPhoton(int p) {
0333   return p == PHOTON;
0334 }
0335 
0336 bool isZ(int p) {
0337   return p == Z0BOSON;
0338 }
0339 
0340 /// PDG rule 11c:
0341 /// "One-of-a-kind" exotic particles are assigned numbers in the range
0342 /// 41–80. The subrange 61-80 can be used for new heavier fermions in
0343 /// generic models, where partners to the SM fermions would have codes
0344 /// offset by 60. If required, however, other assignments could be
0345 /// made.
0346 bool isLeptoQuark(int p) {
0347   return std::abs(p) == LEPTOQUARK;
0348 }
0349 
0350 /// Main Table
0351 /// for MC internal use 81–100,901–930,998-999,1901–1930,2901–2930, and
0352 /// 3901–3930
0353 bool isGenSpecific(int p) {
0354   if (p >= 81 && p <= 100) {
0355     return true;
0356   }
0357   if (p >= 901 && p <= 930) {
0358     return true;
0359   }
0360   if (p >= 998 && p <= 999) {
0361     return true;
0362   }
0363   if (p >= 1901 && p <= 1930) {
0364     return true;
0365   }
0366   if (p >= 2901 && p <= 2930) {
0367     return true;
0368   }
0369   if (p >= 3901 && p <= 3930) {
0370     return true;
0371   }
0372   return false;
0373 }
0374 
0375 bool isGeantino(int p) {
0376   return (std::abs(p) == GEANTINO0 || std::abs(p) == GEANTINOPLUS);
0377 }
0378 
0379 /// APID: Definition of Glueballs: SM glueballs 99X (X=1,5), 999Y (Y=3,7)
0380 bool isGlueball(const DecodedPID& p) {
0381   if (p.ndigits() > 4) {
0382     return false;  // APID avoid classifying R-Glueballs as SM Glueballs
0383   }
0384   return ((p.ndigits() == 3 && p(0) == COMPOSITEGLUON &&
0385            p(1) == COMPOSITEGLUON && (p(2) == 1 || p(2) == 5)) ||
0386           (p.ndigits() == 4 && p(0) == COMPOSITEGLUON &&
0387            p(1) == COMPOSITEGLUON && p(2) == COMPOSITEGLUON &&
0388            (p(3) == 3 || p(3) == 7)));
0389 }
0390 
0391 /// PDG rule 11d
0392 /// Fundamental supersymmetric particles are identified by adding a nonzero n to
0393 /// the particle number. The superpartner of a boson or a left-handed fermion
0394 /// has n = 1 while the superpartner of a right-handed fermion has n = 2. When
0395 /// mixing occurs, such as between the winos and charged Higgsinos to give
0396 /// charginos, or between left and right sfermions, the lighter physical state
0397 /// is given the smaller basis state number.
0398 bool isSUSY(const DecodedPID& p) {
0399   return (p.ndigits() == 7 && (p(0) == 1 || p(0) == 2) &&
0400           !isGenSpecific(p.shift(2).pid()));
0401 }
0402 
0403 /// PDG rule 11g:
0404 /// Within several scenarios of new physics, it is possible to have colored
0405 /// particles sufficiently long-lived for color-singlet hadronic states to form
0406 /// around them. In the context of supersymmetric scenarios, these states are
0407 /// called R-hadrons, since they carry odd R- parity. R-hadron codes, defined
0408 /// here, should be viewed as templates for corresponding codes also in other
0409 /// scenarios, for any long-lived particle that is either an unflavored color
0410 /// octet or a flavored color triplet. The R-hadron code is obtained by combining
0411 /// the SUSY particle code with a code for the light degrees of freedom, with as
0412 /// many intermediate zeros removed from the former as required to make place
0413 /// for the latter at the end. (To exemplify, a sparticle n00000n˜q combined
0414 /// with quarks q1 and q2 obtains code n00n˜qnq1 nq2 nJ .) Specifically, the
0415 /// new-particle spin decouples in the limit of large masses, so that the final
0416 /// nJ digit is defined by the spin state of the light-quark system alone. An
0417 /// appropriate number of nq digits is used to define the ordinary-quark content.
0418 /// As usual, 9 rather than 21 is used to denote a gluon/gluino in composite
0419 /// states. The sign of the hadron agrees with that of the constituent new
0420 /// particle (a color triplet) where there is a distinct new antiparticle, and
0421 /// else is defined as for normal hadrons. Particle names are R with the flavor
0422 /// content as lower index.
0423 
0424 /// APID: Definition of R-Glueballs: 100099X (X=1,3), 100999Y (Y=1,5)
0425 /// APID: NB In the current numbering scheme, some states with 2
0426 /// gluinos + gluon or 2 gluons + gluino could have degenerate
0427 /// PDG_IDs.
0428 bool isRGlueball(const DecodedPID& p) {
0429   if (p.ndigits() != 7 || p(0) != 1) {
0430     return false;
0431   }
0432   auto pp = p.shift(1);
0433   return ((pp.ndigits() == 3 && pp(0) == COMPOSITEGLUON &&
0434            pp(1) == COMPOSITEGLUON && (pp(2) == 1 || pp(2) == 3)) ||
0435           (pp.ndigits() == 4 && pp(0) == COMPOSITEGLUON &&
0436            pp(1) == COMPOSITEGLUON && pp(2) == COMPOSITEGLUON &&
0437            (pp(3) == 1 || pp(3) == 5)));
0438 }
0439 
0440 // APID Define R-Mesons as gluino-quark-antiquark and squark-antiquark bound
0441 // states (ignore 4th generation squarks/quarks) NB Current models only allow
0442 // gluino-quark-antiquark, stop-antiquark and sbottom-antiquark states
0443 bool isRMeson(const DecodedPID& p) {
0444   if (!(p.ndigits() == 7 && (p(0) == 1 || p(0) == 2))) {
0445     return false;
0446   }
0447   auto pp = p.shift(1);
0448   return (
0449       // Handle ~gluino-quark-antiquark states
0450       (pp.ndigits() == 4 && pp(0) == COMPOSITEGLUON &&
0451        pp.max_digit(1, 3) < COMPOSITEGLUON && pp(2) <= pp(1) &&
0452        isSMQuark(pp(1)) && isSMQuark(pp(2)) &&
0453        (pp.last() == 1 || pp.last() == 3)) ||
0454       // Handle squark-antiquark states (previously called Smeson/mesoninos)
0455       (pp.ndigits() == 3 && pp.max_digit(1, 3) < COMPOSITEGLUON &&
0456        pp(1) <= pp(0) && isSMQuark(pp(0)) && isSMQuark(pp(1)) &&
0457        pp.last() == 2));
0458 }
0459 
0460 // APID Define R-Baryons as gluino-quark-quark-quark and squark-quark-quark
0461 // bound states (ignore 4th generation squarks/quarks) NB Current models only
0462 // allow gluino-quark-quark-quark, stop-quark-quark and sbottom-quark-quark
0463 // states
0464 bool isRBaryon(const DecodedPID& p) {
0465   if (!(p.ndigits() == 7 && (p(0) == 1 || p(0) == 2))) {
0466     return false;
0467   }
0468   auto pp = p.shift(1);
0469   return (
0470       // Handle ~gluino-quark-quark-quark states
0471       (pp.ndigits() == 5 && pp(0) == COMPOSITEGLUON &&
0472        pp.max_digit(1, 4) < COMPOSITEGLUON && pp(2) <= pp(1) &&
0473        pp(3) <= pp(2) && isSMQuark(pp(1)) && isSMQuark(pp(2)) &&
0474        isSMQuark(pp(3)) && (pp.last() == 2 || pp.last() == 4)) ||
0475       // Handle squark-quark-quark states (previously called Sbaryons)
0476       (pp.ndigits() == 4 && pp.max_digit(1, 4) < COMPOSITEGLUON &&
0477        pp(1) <= pp(0) && pp(2) <= pp(1) && isSMQuark(pp(0)) &&
0478        isSMQuark(pp(1)) && isSMQuark(pp(2)) &&
0479        (pp.last() == 1 || pp.last() == 3)));
0480 }
0481 
0482 /// PDG rule 11i
0483 /// Magnetic monopoles and dyons are assumed to have one unit of Dirac monopole
0484 /// charge and a variable integer number nq1nq2 nq3 units of electric charge.
0485 /// Codes 411nq1nq2 nq3 0 are then used when the magnetic and electrical charge
0486 /// sign agree and 412nq1nq2 nq3 0 when they disagree, with the overall sign of
0487 /// the particle set by the magnetic charge. For now no spin information is
0488 /// provided.
0489 bool isMonopole(const DecodedPID& p) {
0490   return (p.ndigits() == 7 && p(0) == 4 && p(1) == 1 &&
0491           (p(2) == 1 || p(2) == 2) && p(6) == 0);
0492 }
0493 
0494 /// In addition, there is a need to identify "Q-ball" and similar very exotic
0495 /// (multi-charged) particles which may have large, non-integer charge. These
0496 /// particles are assigned the ad-hoc numbering +/-100XXXY0, where the charge is
0497 /// XXX.Y. or +/-200XXYY0, where the charge is XX/YY. The case of +/-200XXYY0 is
0498 /// legacy, see https://gitlab.cern.ch/atlas/athena/-/merge_requests/25862 Note
0499 /// that no other quantum numbers besides the charge are considered for these
0500 /// generic multi-charged particles (e.g. isSUSY() is false for them). Such a
0501 /// model was used in previous Run-1 (1301.5272,1504.04188) and Run-2
0502 /// (1812.03673,2303.13613) ATLAS searches.
0503 bool isGenericMultichargedParticle(const DecodedPID& p) {
0504   return (p.ndigits() == 8 && (p(0) == 1 || p(0) == 2) && p(1) == 0 &&
0505           p(2) == 0 && p(7) == 0);
0506 }
0507 
0508 /// PDG rule 16
0509 /// Nuclear codes are given as 10-digit numbers ±10LZZZAAAI.
0510 /// For a (hyper)nucleus consisting of n_p protons, n_n neutrons and
0511 /// n_Λ Λ's:
0512 /// A = n_p + n_n + n_Λ gives the total baryon number,
0513 /// Z = n_p gives the total charge,
0514 /// L = n_Λ gives the total number of strange quarks.
0515 /// I gives the isomer level, with I= 0 corresponding to the ground
0516 /// state and I > 0 to excitations, see
0517 /// [http://www.nndc.bnl.gov/amdc/web/nubase en.html], where states
0518 /// denoted m, n, p ,q translate to I= 1–4. As examples, the deuteron
0519 /// is 1000010020 and 235U is 1000922350. To avoid ambiguities,
0520 /// nuclear codes should not be applied to a single hadron, like p, n or
0521 /// Λ^0, where quark-contents-based codes already exist.
0522 bool isNucleus(const DecodedPID& p) {
0523   if (std::abs(p.pid()) == PROTON) {
0524     return true;
0525   }
0526   return (p.ndigits() == 10 && p(0) == 1 && p(1) == 0);
0527 }
0528 
0529 int numberOfProtons(const DecodedPID& p) {
0530   if (std::abs(p.pid()) == PROTON) {
0531     return (p.pid() > 0) ? 1 : -1;
0532   }
0533   if (isNucleus(p)) {
0534     const int result = p(5) + 10 * p(4) + 100 * p(3);
0535     return (p.pid() > 0) ? result : -result;
0536   }
0537   return 0;
0538 }
0539 
0540 inline int leadingQuark(const DecodedPID& p) {
0541   if (isQuark(p.pid())) {
0542     return std::abs(p.pid());
0543   }
0544   if (isMeson(p)) {
0545     return p.max_digit(1, 3);
0546   }
0547   if (isDiquark(p)) {
0548     return p.max_digit(2, 4);
0549   }
0550   if (isBaryon(p)) {
0551     return p.max_digit(1, 4);
0552   }
0553   if (isTetraquark(p)) {
0554     return p.max_digit(1, 5);
0555   }
0556   if (isPentaquark(p)) {
0557     return p.max_digit(1, 6);
0558   }
0559   if (isSUSY(p)) {  // APID SUSY case
0560     auto pp = p.shift(1);
0561     if (pp.ndigits() == 1) {
0562       return 0;
0563     }  // Handle squarks
0564     if (pp.ndigits() == 3) {
0565       pp = DecodedPID(pp(1));
0566     }  // Handle ~q qbar pairs
0567     if (pp.ndigits() > 3) {
0568       pp = pp.shift(1);
0569     }  // Drop gluinos and squarks
0570     return leadingQuark(pp);
0571   }
0572   return 0;
0573 }
0574 
0575 bool isLightMeson(const DecodedPID& p) {
0576   auto lq = leadingQuark(p);
0577   return (lq == DQUARK || lq == UQUARK || lq == SQUARK) && isMeson(p);
0578 }
0579 
0580 bool isStrangeMeson(const DecodedPID& p) {
0581   return leadingQuark(p) == SQUARK && isMeson(p);
0582 }
0583 
0584 bool isCharmMeson(const DecodedPID& p) {
0585   return leadingQuark(p) == CQUARK && isMeson(p);
0586 }
0587 
0588 bool isBottomMeson(const DecodedPID& p) {
0589   return leadingQuark(p) == BQUARK && isMeson(p);
0590 }
0591 
0592 inline bool isCCbarMeson(const DecodedPID& p) {
0593   return leadingQuark(p) == CQUARK && isMeson(p) &&
0594          (*(p.second.rbegin() + 2)) == CQUARK &&
0595          (*(p.second.rbegin() + 1)) == CQUARK;
0596 }
0597 
0598 inline bool isBBbarMeson(const DecodedPID& p) {
0599   return leadingQuark(p) == BQUARK && isMeson(p) &&
0600          (*(p.second.rbegin() + 2)) == BQUARK &&
0601          (*(p.second.rbegin() + 1)) == BQUARK;
0602 }
0603 
0604 bool isLightBaryon(const DecodedPID& p) {
0605   auto lq = leadingQuark(p);
0606   return (lq == DQUARK || lq == UQUARK || lq == SQUARK) && isBaryon(p);
0607 }
0608 
0609 bool isStrangeBaryon(const DecodedPID& p) {
0610   return leadingQuark(p) == SQUARK && isBaryon(p);
0611 }
0612 
0613 bool isCharmBaryon(const DecodedPID& p) {
0614   return leadingQuark(p) == CQUARK && isBaryon(p);
0615 }
0616 
0617 bool isBottomBaryon(const DecodedPID& p) {
0618   return leadingQuark(p) == BQUARK && isBaryon(p);
0619 }
0620 
0621 double fractionalCharge(const DecodedPID& p);
0622 int charge3(const DecodedPID& p);
0623 
0624 double charge(const DecodedPID& p) {
0625   if (isGenericMultichargedParticle(
0626           p)) {  // BSM multi-charged particles might have a fractional charge
0627                  // that's not a multiple of 1/3
0628     return fractionalCharge(p);
0629   } else {
0630     return 1.0 * charge3(p) / 3.0;
0631   }
0632 }
0633 
0634 int charge3(const DecodedPID& p) {
0635   auto ap = std::abs(p.pid());
0636   if (ap < TABLESIZE) {
0637     return p.pid() > 0 ? triple_charge.at(ap) : -triple_charge.at(ap);
0638   }
0639   if (ap == K0) {
0640     return 0;
0641   }
0642   if (ap == GEANTINO0) {
0643     return 0;
0644   }
0645   if (ap == GEANTINOPLUS) {
0646     return p.pid() > 0 ? 3 : -3;
0647   }
0648   if (ap == MAVTOP) {
0649     return p.pid() > 0 ? 2 : -2;
0650   }
0651   std::size_t nq = 0;
0652   int sign = 1;
0653   int signmult = 1;
0654   int result = 0;
0655   bool classified = false;
0656   if (!classified && isMeson(p)) {
0657     classified = true;
0658     nq = 2;
0659     if ((*(p.second.rbegin() + 2)) == 2 || (*(p.second.rbegin() + 2)) == 4) {
0660       sign = -1;
0661     }
0662     signmult = -1;
0663   }
0664   if (!classified && isDiquark(p)) {
0665     return triple_charge.at(p(0)) + triple_charge.at(p(1));
0666   }
0667   if (!classified && isBaryon(p)) {
0668     classified = true;
0669     nq = 3;
0670   }
0671   if (!classified && isTetraquark(p)) {
0672     return triple_charge.at(p(3)) + triple_charge.at(p(4)) -
0673            triple_charge.at(p(6)) - triple_charge.at(p(7));
0674   }
0675   if (!classified && isPentaquark(p)) {
0676     return triple_charge.at(p(3)) + triple_charge.at(p(4)) +
0677            triple_charge.at(p(5)) + triple_charge.at(p(6)) -
0678            triple_charge.at(p(7));
0679   }
0680   if (!classified && isNucleus(p)) {
0681     return 3 * numberOfProtons(p);
0682   }
0683   if (!classified && isSUSY(p)) {
0684     nq = 0;
0685     auto pp = p.shift(1);
0686     if (pp.ndigits() < 3) {
0687       return charge3(pp);
0688     }  // super-partners of fundamental particles
0689     if (pp(0) == COMPOSITEGLUON) {
0690       if (pp(1) == COMPOSITEGLUON) {
0691         return 0;
0692       }  // R-Glueballs
0693       if (pp.ndigits() == 4 || pp.ndigits() == 5) {
0694         pp = pp.shift(1);  // Remove gluino
0695       }
0696     }
0697     if (pp.ndigits() == 3) {
0698       classified = true;
0699       nq = 2;
0700       if (p.last() % 2 == 0) {
0701         sign = -1;
0702       }
0703       signmult = -1;
0704     }  // states with squark-antiquark or quark-anti-quark
0705     if (pp.ndigits() == 4) {
0706       classified = true;
0707       nq = 3;
0708     }  // states with squark-quark-quark or quark-quark-quark
0709   }
0710   if (!classified && isMonopole(p)) {
0711     /// Codes 411nq1nq2 nq3 0  are then used when the magnetic and electrical
0712     /// charge sign agree and 412nq1nq2 nq3 0
0713     ///  when they disagree, with the overall sign of the particle set by the
0714     ///  magnetic charge.
0715     result = 3 * (p(3) * 100 + p(4) * 10 + p(5));
0716     return ((p.pid() > 0 && p(2) == 1) || (p.pid() < 0 && p(2) == 2)) ? result
0717                                                                       : -result;
0718   }
0719   if (!classified && isGenericMultichargedParticle(p)) {
0720     double abs_charge = 0.0;
0721     if (p(0) == 1) {
0722       abs_charge = p(3) * 100. + p(4) * 10. + p(5) * 1 +
0723                    p(6) * 0.1;  // multi-charged particle PDG ID is
0724                                 // +/-100XXXY0, where the charge is XXX.Y
0725     }
0726     if (p(0) == 2) {
0727       abs_charge = (p(3) * 10. + p(4)) /
0728                    (p(5) * 10.0 + p(6));  // multi-charged particle PDG ID is
0729       // +/-200XXYY0, where the charge is XX/YY
0730     }
0731     int abs_threecharge = static_cast<int>(std::round(
0732         abs_charge *
0733         3.));  // the multi-charged particles might have a fractional charge
0734                // that's not a multiple of 1/3, in that case round to the
0735                // closest multiple of 1/3 for charge3 and threecharge
0736     return p.pid() > 0 ? abs_threecharge : -1 * abs_threecharge;
0737   }
0738   for (auto r = p.second.rbegin() + 1; r != p.second.rbegin() + 1 + nq; ++r) {
0739     result += triple_charge.at(*r) * sign;
0740     sign *= signmult;
0741   }
0742   return p.pid() > 0 ? result : -result;
0743 }
0744 double fractionalCharge(const DecodedPID& p) {
0745   if (!isGenericMultichargedParticle(p)) {
0746     return 1.0 * charge3(p) /
0747            3.0;  // this method is written for multi-charged particles, still
0748                  // make sure other cases are handled properly
0749   }
0750   double abs_charge = 0;
0751   if (p(0) == 1) {
0752     abs_charge = p(3) * 100. + p(4) * 10. + p(5) * 1 +
0753                  p(6) * 0.1;  // multi-charged particle PDG ID is +/-100XXXY0,
0754   }
0755   // where the charge is XXX.Y
0756   if (p(0) == 2) {
0757     abs_charge =
0758         (p(3) * 10. + p(4)) /
0759         (p(5) * 10.0 + p(6));  // multi-charged particle PDG ID is +/-200XXYY0,
0760   }
0761   // where the charge is XX/YY
0762   return p.pid() > 0 ? abs_charge : -1 * abs_charge;
0763 }
0764 
0765 // APID: Including Z' and Z'' as EM interacting.
0766 bool isEMInteracting(const DecodedPID& p) {
0767   return (isPhoton(p.pid()) || isZ(p.pid()) || p.pid() == ZPRIME ||
0768           p.pid() == ZDBLPRIME ||
0769           std::abs(charge(p)) > std::numeric_limits<double>::epsilon() ||
0770           isMonopole(p));
0771 }
0772 
0773 bool isRHadron(const DecodedPID& p) {
0774   return (isRBaryon(p) || isRMeson(p) || isRGlueball(p));
0775 }
0776 
0777 bool isStrongInteracting(const DecodedPID& p) {
0778   return (isGluon(p.pid()) || isQuark(p.pid()) || isDiquark(p) ||
0779           isGlueball(p) || isLeptoQuark(p.pid()) || isHadron(p) ||
0780           isRHadron(p));
0781 }  // APID: Glueballs and R-Hadrons are also strong-interacting
0782 
0783 }  // namespace
0784 
0785 // https://gitlab.cern.ch/atlas/athena/-/blob/b0898f93585c4eec97550a9e0a16f5b6e6b6b973/Generators/TruthUtils/TruthUtils/AtlasPID.h#L325
0786 bool ParticleIdHelper::isHadron(PdgParticle pdg) {
0787   DecodedPID p(pdg);
0788   return isMeson(p) || isBaryon(p) || isTetraquark(p) || isPentaquark(p);
0789 }
0790 
0791 // https://gitlab.cern.ch/atlas/athena/-/blob/b0898f93585c4eec97550a9e0a16f5b6e6b6b973/Generators/TruthUtils/TruthUtils/AtlasPID.h#L180
0792 bool ParticleIdHelper::isLepton(PdgParticle pdg) {
0793   auto sp = std::abs(pdg);
0794   return sp >= 11 && sp <= 18;
0795 }
0796 
0797 bool ParticleIdHelper::isMuon(PdgParticle pdg) {
0798   return std::abs(pdg) == MUON;
0799 }
0800 
0801 bool ParticleIdHelper::isElectron(PdgParticle pdg) {
0802   return std::abs(pdg) == ELECTRON;
0803 }
0804 
0805 bool ParticleIdHelper::isPhoton(PdgParticle pdg) {
0806   return std::abs(pdg) == PHOTON;
0807 }
0808 
0809 bool ParticleIdHelper::isTau(PdgParticle pdg) {
0810   return std::abs(pdg) == TAU;
0811 }
0812 
0813 HadronType ParticleIdHelper::hadronType(PdgParticle pdg) {
0814   DecodedPID p(pdg);
0815 
0816   using enum HadronType;
0817 
0818   if (isBBbarMeson(p)) {
0819     return BBbarMeson;
0820   }
0821   if (isCCbarMeson(p)) {
0822     return CCbarMeson;
0823   }
0824   if (isBottomMeson(p)) {
0825     return BottomMeson;
0826   }
0827   if (isCharmMeson(p)) {
0828     return CharmedMeson;
0829   }
0830   if (isBottomBaryon(p)) {
0831     return BottomBaryon;
0832   }
0833   if (isCharmBaryon(p)) {
0834     return CharmedBaryon;
0835   }
0836   if (isStrangeBaryon(p)) {
0837     return StrangeBaryon;
0838   }
0839   if (isLightBaryon(p)) {
0840     return LightBaryon;
0841   }
0842   if (isStrangeMeson(p)) {
0843     return StrangeMeson;
0844   }
0845   if (isLightMeson(p)) {
0846     return LightMeson;
0847   }
0848 
0849   return Unknown;
0850 }
0851 
0852 // https://gitlab.cern.ch/atlas/athena/-/blob/b0898f93585c4eec97550a9e0a16f5b6e6b6b973/Generators/TruthUtils/TruthUtils/AtlasPID.h#L159
0853 bool ParticleIdHelper::isQuark(PdgParticle pdg) {
0854   return pdg != 0 && (std::abs(pdg) <= 8 || std::abs(pdg) == MAVTOP);
0855 }
0856 
0857 // https://gitlab.cern.ch/atlas/athena/-/blob/b0898f93585c4eec97550a9e0a16f5b6e6b6b973/Generators/TruthUtils/TruthUtils/HepMCHelpers.h#L33
0858 bool ParticleIdHelper::isInteracting(PdgParticle pdg) {
0859   DecodedPID p(pdg);
0860   return isStrongInteracting(p) || isEMInteracting(p) || isGeantino(pdg);
0861 }
0862 
0863 std::ostream& operator<<(std::ostream& os, HadronType hadron) {
0864   switch (hadron) {
0865     using enum HadronType;
0866     case Hadron:
0867       return os << "Hadron";
0868     case BBbarMeson:
0869       return os << "BBbarMeson";
0870     case CCbarMeson:
0871       return os << "CCbarMeson";
0872     case BottomMeson:
0873       return os << "BottomMeson";
0874     case BottomBaryon:
0875       return os << "BottomBaryon";
0876     case CharmedMeson:
0877       return os << "CharmedMeson";
0878     case CharmedBaryon:
0879       return os << "CharmedBaryon";
0880     case StrangeMeson:
0881       return os << "StrangeMeson";
0882     case StrangeBaryon:
0883       return os << "StrangeBaryon";
0884     case LightMeson:
0885       return os << "LightMeson";
0886     case LightBaryon:
0887       return os << "LightBaryon";
0888     case Unknown:
0889       return os << "Unknown";
0890   }
0891   return os;
0892 }
0893 }  // namespace Acts