main487

Back to index.

// main487.cc is a part of the PYTHIA event generator.
// Copyright (C) 2025 Torbjorn Sjostrand.
// PYTHIA is licenced under the GNU GPL v2 or later, see COPYING for details.
// Please respect the MCnet Guidelines, see GUIDELINES for details.

// Author: Torbjorn Sjostrand <torbjorn.sjostrand@fysik.lu.se>
// largely based on code by Marius Utheim

// Keywords:
//            Cosmic ray cascade
//            Switch beam
//            Switch collision energy

// Cosmic ray evolution in the atmosphere, with a direct comparison
// between Angantyr and PythiaCascade. The atmosphere is a mixture of
// nitrogen, oxygen and argon, with exponential density fall-off.
// Hadron-nucleus cross sections are taken from PythiaCascade only,
// since Angantyr ones are not available on the fly.
// Also, since Angantyr does not generate elastic cross sections, an
// arbitrary 0.9 reduction factor is applied to its total cross section.
// By default hadron interactions below 10.5 GeV CM energy are not
// considered, since Angantyr cannot go below 10 GeV. (PythiaCascade
// could go lower, opening for a possible switchover at low energies.)

#include "Pythia8/Pythia.h"
#include "Pythia8Plugins/PythiaCascade.h"

using namespace Pythia8;

//==========================================================================

// Note: when a hadron interacts with a medium particle, the latter is added
// to the event record. This program uses these additional status codes for
// target particles in the medium:
//  181: the first (or only) target nucleon in a collision.
//  182: nucleons from further subcollisions with the same target nucleus.

// Conversion factor between mm and km.
constexpr double KM2MM = 1.e6;

// Conversion factor for kg/m^3 to GeV/(c^2 mm mb)
constexpr double kg_m3_to_GeV_c2mm1mb1 =  5.60958865e26 // kg to GeV/c2
  * 0.001  // m^-1 to mm^-1
  * 1e-31; // m^-2 to mb^-1

// Medium parameters. Assume exponential atmosphere.
constexpr double mp   = 0.93827;
constexpr double mAir = 0.9315;
constexpr double mediumDensity = 1.225 * kg_m3_to_GeV_c2mm1mb1 / mAir;
constexpr double rhoAir = 1.225e-4; // g cm^-2 mm^-1
constexpr double mediumHeight = 100 * KM2MM;
constexpr double H = 10.4 * KM2MM;

// Atmosphere with 78% 14N, 21% 16O and 1% 40Ar by number of nuclei (mol).
constexpr double fracN  = 0.78;
constexpr double fracO  = 0.21;
constexpr double fracAr = 0.01;

// Model parameters.
// The zenith angle of the primary particle (0 = straight down).
constexpr double zenithAngle = 0.;

// Get the medium depth of a particle at the specified height.
double getDepth(double h) {
  return rhoAir * H * exp(-h / H) /  cos(zenithAngle); }

//==========================================================================

int main() {

  // Energy of primary incident proton (in GeV).
  double pPri = 1e6;

  // Number of events per case. Only do a few since each shower is so big.
  int nEvent = 100;

  // Minimal collision CM-frame energies allowed in the shower evolution,
  // restricted by Angantyr for these comnpƄarisons.
  double eCMMin = 10.5;

  // Minimal kinetic energy for histogramming of particles.
  double eKinMin = 10.;

  // Set maximum size on the event record, to limit runaway code.
  int maxSize = 2000000;

  // Book histograms.
  const int nCases = 2;
  double depthMax = rhoAir * H / cos(zenithAngle);
  Hist nInt[nCases], diffHad[nCases], diffMuon[nCases], prodnue[nCases],
       prodnumu[nCases];
  for (int iC = 0; iC < nCases; ++iC) {
    nInt[iC].book("depth of interactions",            100, 0., depthMax);
    diffHad[iC].book("hadron production-decay depth", 100, 0., depthMax);
    diffMuon[iC].book("muon production-decay depth",  100, 0., depthMax);
    prodnue[iC].book("nu_e production depth",         100, 0., depthMax);
    prodnumu[iC].book("nu_mu production depth",       100, 0., depthMax);
  }

  // Four-momentum of incoming initiator.
  double pxPri = 0.;
  double pyPri = pPri * sin(zenithAngle);
  double pzPri = pPri * cos(zenithAngle);
  Vec4 p0(pxPri, pyPri, -pzPri, sqrt(pow2(pPri) + pow2(mp)));

  // Commonly reused variables.
  bool   canDecayNow, mustDecayNow;
  int    idNow, idTarg, Ztarg, Atarg, angCount[3] = {0,0,0};
  double mNow, eNow, sigmaN, sigmaO, sigmaAr, sigmaPick, sigmaNow,
         zNow, dzds, logR, zNext, eCMNow;
  Vec4   pNow, vDec, vInt, dirNow;
  clock_t timeBeg, timeEnd;
  double timeSec[2];

  // Pythia object for storing and parsing the full atmospheric cascade.
  // Also for handling Angantyr particle decays.
  Pythia pythiaMain;
  Event& eventMain = pythiaMain.event;
  // Prepare to do decays but no hard processes.
  pythiaMain.readString("ProcessLevel:all = off");
  pythiaMain.readString("211:mayDecay = on");
  pythiaMain.readString("13:mayDecay  = on");
  pythiaMain.readString("321:mayDecay = on");
  pythiaMain.readString("130:mayDecay = on");

  // Return error if pythiaMain initialization fails.
  if (!pythiaMain.init()) return 1;

  // Pythia object for performing individual Angantyr collisions.
  Pythia pythiaAng;
  Event& eventAng = pythiaAng.event;
  // Enable Angantyr. Reuse MPI and cross sections initialization files.
  // If they don't exist, you can generate them by running main424.
  pythiaAng.readFile("main424.cmnd");
  pythiaAng.readString("HeavyIon:mode = 2");
  // Set up for fixed-target collisions.
  pythiaAng.readString("Beams:frameType = 3");
  pythiaAng.settings.parm("Beams:pzA", -pPri);
  pythiaAng.readString("Beams:pzB = 0.");
  // Unit weight needed in cascade.
  pythiaAng.readString("HeavyIon:forceUnitWeight = on");
  // Decays to be done by pythiaMain.
  pythiaAng.readString("HadronLevel:Decay = off");
  // Reduce printout and relax energy-momentum conservation.
  pythiaAng.readString("Print:quiet = on");
  pythiaAng.readString("Check:epTolErr = 0.1");

  // Return error if pythiaAng initialization fails.
  if (!pythiaAng.init()) return 1;

  // PythiaCas object for performing individual PythiaCascade collisions.
  PythiaCascade pythiaCas;

  // Return error if PythiaCas initialization fails.
  // See PythiaCascade.h for explanation of reuseMPI options. In this run
  // it is sensible but not necessary to use same data file as for Angantyr.
  bool listFinal   = false;
  bool rapidDecays = false;
  int reuseMPI     = -1;
  string reuseFile = (reuseMPI < 0) ? "main424.cmnd" : "pythiaCascade.mpi";
  if ( !pythiaCas.init( pPri + mp, listFinal, rapidDecays, 0.,
    reuseMPI, reuseFile) ) return 1;

  // Begin loop over Angantyr (iCase = 0) and PythiaCas (iCase = 1).
  for (int iCase = 0; iCase < 2; ++iCase) {
    timeBeg = clock();

    // Begin event loop.
    for (int iEvent = 0; iEvent < nEvent; ++iEvent) {

      // Insert primary particle in cleared main event record.
      eventMain.clear();
      eventMain.append(90,  -11, 0, 0, 1, 1, 0, 0, p0, mp);
      eventMain.append(2212, 12, 0, 0, 0, 0, 0, 0, p0, mp);

      // Set initial location of initiator, where z is distance above ground.
      double heightNow = mediumHeight;
      eventMain[0].yProd(-heightNow * tan(zenithAngle));
      eventMain[0].zProd(heightNow);
      eventMain[1].yProd(-heightNow * tan(zenithAngle));
      eventMain[1].zProd(heightNow);

      // Loop over particles (usually hadrons) in the main event record.
      for (int iHad = 1; iHad < eventMain.size(); ++iHad) {
        Particle& hadNow = eventMain[iHad];

        // Skip already fragmented/decayed or upwards-moving particles.
        if (!hadNow.isFinal() || hadNow.pz() > 0.) continue;

        //  Projectile properties. Invariant mass  with a p/n nucleon at rest.
        idNow        = hadNow.id();
        pNow         = hadNow.p();
        mNow         = hadNow.m();
        eNow         = hadNow.e();
        mustDecayNow = false;
        eCMNow       = (pNow + Vec4(0, 0, 0, mp)).mCalc();

        // Find decay vertex for unstable hadrons. (Below ground if no decay.)
        vDec = hadNow.canDecay() ? hadNow.vDec() : Vec4( 0., 0., -1., 0.);
        canDecayNow = hadNow.canDecay() && vDec.pz() > 0.;

        // Low energy hadrons should not interact with medium.
        // Decay non-hadrons or low-energy ones if decay happens above ground.
        if (!hadNow.isHadron() || eCMNow < eCMMin) {
          if (canDecayNow) mustDecayNow = true;
          else continue;
        }

        // Get hadron-nucleus cross sections and pick interaction vertex.
        // This part is temporary until Angantyr cross sections are reliable.
        if (!mustDecayNow) {

          // Set up hadron-nucleon cross section. Fail if vanishing.
          if (!pythiaCas.sigmaSetuphN( idNow, pNow, mNow)) {
            if (canDecayNow) mustDecayNow = true;
            else continue;

          } else {
            // Get hadron-ion cross sections, corrected by fractions.
            sigmaN  = fracN  * pythiaCas.sigmahA(14);
            sigmaO  = fracO  * pythiaCas.sigmahA(16);
            sigmaAr = fracAr * pythiaCas.sigmahA(40);

            // Pick nucleus by relative cross sections.
            sigmaPick = (sigmaN + sigmaO + sigmaAr) * pythiaCas.rndm().flat();
            if (sigmaPick < sigmaN)               idTarg = 1000070140;
            else if (sigmaPick < sigmaN + sigmaO) idTarg = 1000080160;
            else                                  idTarg = 1000180400;
            Ztarg = (idTarg / 10000) * 100;
            Atarg = (idTarg / 10) % 1000;

            // Atmosphere medium density is nucleons per volume (not nuclei),
            // so need cross section per nucleon.
            sigmaNow = (sigmaN + sigmaO + sigmaAr)
                     / (fracN * 14. + fracO * 16. + fracAr * 40.);

            // Ad hoc Angantyr cross section reduction from excluding elastic
            // collisions handing in Angantyr, while included in PythiaCascade.
            if (iCase == 0) sigmaNow *= 0.9;

            // Calculate potential interaction vertex.
            dirNow = pNow / pNow.pAbs();
            zNow  = hadNow.zProd();
            dzds  = hadNow.pz() / hadNow.pAbs();
            logR  = log(pythiaCas.rndm().flat());
            zNext = -H * log( exp(-zNow / H)
                  + dzds / (H * sigmaNow * mediumDensity) * logR );
            vInt  = hadNow.vProd() + (zNext - zNow) * dirNow / dzds;

            // Done if hadron reaches surface before both interaction
            // and decay.
            if (vInt.pz() < 0. && !canDecayNow) continue;

            // Do decay if it happens first.
            if (vDec.pz() > vInt.pz()) mustDecayNow = true;
          }
        }

        // Handling of decay or interaction with Angantyr.
        if (iCase == 0) {

          // Perform the decay if it happens first.
          if (mustDecayNow) {
            // Decay incoming particle. Skip particle on a failure, which
            // could lead to unintended punch-through of a particle.
            if (!pythiaMain.moreDecays(iHad)) continue;

          // Perform interaction if it happens first.
          } else {
            if ( pythiaAng.setBeamIDs(idNow, idTarg)
            && pythiaAng.setKinematics(pNow, Vec4(0., 0., 0., 0.)) ) {
            } else {
              ++angCount[0];
              continue;
            }

            // Simulate interaction. Skip particle if failure, which
            // could lead to unintended punch-through of a particle.
            if (!pythiaAng.next()) {
              ++angCount[1];
              continue;
            }
            ++angCount[2];

            // Append target.
            int iTarg = eventMain.append(idTarg, -181, iHad, iHad, 0, 0, 0, 0,
                0., 0., 0., mp, mp);
            eventMain[iTarg].vProdAdd(vInt);

            // Copy final-state particles.
            int sizeOld = eventMain.size();
            for (int iSub = 3; iSub < eventAng.size(); ++iSub) {
              if (eventAng[iSub].isFinal()) {
                int iNew = eventMain.append(eventAng[iSub]);
                eventMain[iNew].mothers(iHad, iTarg);
                eventMain[iNew].vProdAdd(vInt);
              }
            }

            // Update daughters of the interacting particles.
            int sizeNew = eventMain.size();
            eventMain[iTarg].daughters(sizeOld, sizeNew - 1);
            eventMain[iHad].daughters(sizeOld, sizeNew - 1);
            eventMain[iHad].statusNeg();
            eventMain[iHad].tau(( vInt.e() - eventMain[iHad].tProd() ) * mNow
              / eNow);
          }

        // Handling of decay or interaction with PythiaCascade.
        } else {

          // Do decay or collision. Empty event means failure, so skip,
          // which could lead to unintended punch-through of a particle.
          int sizeSave    = eventMain.size();
          Event& eventTmp = (mustDecayNow)
                          ? pythiaCas.nextDecay(idNow, pNow, mNow, vDec)
                          : pythiaCas.nextColl( Ztarg, Atarg, vInt);
          if (eventTmp.size() == 0) continue;

          // Update properties of incoming hadron.
          hadNow.statusNeg();
          hadNow.daughters( sizeSave, sizeSave);
          double dTau = ( (mustDecayNow ? vDec.e() : vInt.e())
            - hadNow.tProd() ) * mNow / eNow;
          hadNow.tau( dTau);

          // Append new event to existing and link history.
          eventMain += eventTmp;
          eventMain[sizeSave].mothers( iHad, iHad);
        }

        // Stop generation if event record extremely long.
        if (eventMain.size() > maxSize) break;

      // End of loop over interactions + decays inside a single cascade.
      }

      // Begin analysis. Loop over all particles to find interaction depths.
      for (Particle& h : eventMain) {
        if (h.status() == -12) continue;
        double depthProd = getDepth(h.zProd());
        double depthDec  = getDepth(h.isFinal() ? 0. : h.zDec());

        // If particle came from the medium, record the interaction depth.
        if (h.status() == -181) {
          nInt[iCase].fill(depthProd);
        }
        // Otherwise, track depths where particles are created/destroyed.
        else if (h.e() - h.m() > eKinMin) {
          if (h.isHadron()) {
            if (h.isFinal())
              diffHad[iCase].fill(depthProd, 1.);
            else {
              diffHad[iCase].fill(min(depthProd, depthDec), 1.);
              diffHad[iCase].fill(max(depthProd, depthDec), -1.);
            }
          }
          else if (h.idAbs() == 13) {
            if (h.isFinal())
              diffMuon[iCase].fill(depthProd, 1.);
            else {
              diffMuon[iCase].fill(min(depthProd, depthDec), 1.);
              diffMuon[iCase].fill(max(depthProd, depthDec), -1.);
            }
          }
          else if (h.idAbs() == 12)
            prodnue[iCase].fill(depthProd, 1.);
          else if (h.idAbs() == 14)
            prodnumu[iCase].fill(depthProd, 1.);
        }
      }

    // End loops over events and over Angantyr/PythiaCascade cases.
    }
    timeEnd = clock();
    timeSec[iCase] = double(timeEnd - timeBeg) / double(CLOCKS_PER_SEC);
  }

  // Print time.
  cout << " \n Runtime for Angantyr events is " << fixed
       << setprecision(3) << timeSec[0] << " s" << endl
       << " Runtime for PythiaCascade events is " << timeSec[1] << " s"
       << endl;

  // Print Angantyr errors.
  cout << "\n Angantyr failed setup collision = " << angCount[0] << endl
       << " Angantyr failed performing collision = " << angCount[1] << endl
       << " Angantyr successful collision = " << angCount[2] << endl;

  // Print statistics, mainly for errors.
  cout << "\n Statistics from PythiaMain: " << endl;
  pythiaMain.stat();
  cout << "\n Statistics from PythiaAng: " << endl;
  pythiaAng.stat();
  cout << "\n Statistics from PythiaCas: " << endl;
  pythiaCas.stat();

  // Book histograms.
  Hist nHad[nCases], nMuon[nCases], nnue[nCases], nnumu[nCases];
  for (int iC = 0; iC < nCases; ++iC) {
    nHad[iC] .book("", 100, 0., depthMax);
    nMuon[iC].book("", 100, 0., depthMax);
    nnue[iC] .book("", 100, 0., depthMax);
    nnumu[iC].book("", 100, 0., depthMax);

    // Integrate production minus depletion to find particle number by depth.
    double nHadSum = 0., nMuonSum = 0., nnueSum = 0., nnumuSum = 0.;
    for (int i = 1; i <= 100; ++i) {
      double depthNow = depthMax * (i - 0.5) / 100.;
      if (depthNow > getDepth(0.)) break;
      nHadSum  += diffHad[iC] .getBinContent(i);
      nMuonSum += diffMuon[iC].getBinContent(i);
      nnueSum  += prodnue[iC] .getBinContent(i);
      nnumuSum += prodnumu[iC].getBinContent(i);
      nHad[iC] .fill(depthNow, nHadSum );
      nMuon[iC].fill(depthNow, nMuonSum);
      nnue[iC] .fill(depthNow, nnueSum );
      nnumu[iC].fill(depthNow, nnumuSum);
    }

    // Normalize histograms.
    nInt[iC].normalizeSpectrum(nEvent);
    nHad[iC]  /= nEvent;
    nMuon[iC] /= nEvent;
    nnue[iC]  /= nEvent;
    nnumu[iC] /= nEvent;
  }

  // Normalize and plot histograms.
  HistPlot plt("plot487");
  vector<string> col = { "r", "b" };
  vector<string> legend = {  "Angantyr", "PythiaCascade"};
  plt.frame("fig487", "Atmospheric depth of nuclear interactions",
    "$X$ (g/cm$^2$)", "$(1/n_{ev}) dn_{int}/dX$", 6.4, 4.8);
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(nInt[iC], "-,"+col[iC], legend[iC]);
  plt.plot( 0., depthMax, 0.02, 20., true);
  plt.frame("", "Number of hadrons at depth",
    "$X$ (g/cm$^2$)", "$(1/n_{ev}) \\int_0^{X} dn_{had}$", 6.4, 4.8);
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(nHad[iC], "-,"+col[iC], legend[iC]);
  plt.plot( 0., depthMax, 5., 5000., true);
  plt.frame("", "Number of muons at depth",
    "$X$ (g/cm$^2$)", "$(1/n_{ev}) \\int_0^{X} dn_{\\mu}$", 6.4, 4.8);
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(nMuon[iC], "-,"+col[iC], legend[iC]);
  plt.plot( 0., depthMax, 5., 5000., true);
  plt.frame("", "Number of e neutrinos at depth",
    "$X$ (g/cm$^2$)", "$(1/n_{ev}) \\int_0^{X} dn_{\\nu_e}$", 6.4, 4.8);
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(nnue[iC], "-,"+col[iC], legend[iC]);
  plt.plot( 0., depthMax, 0.2, 200., true);
  plt.frame("", "Number of mu neutrinos at depth",
    "$X$ (g/cm$^2$)", "$(1/n_{ev}) \\int_0^{X} dn_{\\nu_\\mu}$", 6.4, 4.8);
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(nnumu[iC], "-,"+col[iC], legend[iC]);
  plt.plot( 0., depthMax, 2., 2000., true);

}