main487

Back to index.

// main487.cc is a part of the PYTHIA event generator.
// Copyright (C) 2026 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>
// partly based on code by Marius Utheim

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

// Cosmic ray evolution in the atmosphere, with proton or nucleus beam,
// and with a direct comparison between Angantyr and PythiaCascade.

#include "Pythia8/Pythia.h"
#include "Pythia8/HIInfo.h"
#include "Pythia8Plugins/PythiaCascade.h"
#include "Pythia8Plugins/ProgressLog.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 mn   = 0.93957;
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.5% 14N, 21% 16O and 0.5% 40Ar by number of nuclei.
// Note: often composition by mole is quoted, then with 1% Ar, since
// nitrogen and oxygen form molecules N_2 and O_2 but argon is a noble gas.
constexpr double fracN  = 0.785;
constexpr double fracO  = 0.21;
constexpr double fracAr = 0.005;

// Inform Angantyr about the ions in the atmosphere.
const vector<int> mediumIons = { 1000070140, 1000080160, 1000180400 };
const vector<double> mediumFracs = { fracN, fracO, fracAr };

// 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() {

//========== Basic properties ==========

  // In a run two secarios are compared, in each case with a projectile
  // hitting the atmosphere and inducing a cascade.
  // The comparison mode select the scenario for the incoming projectile:
  // = 1: proton with momentum pPri, Angantyr vs. Cascade;
  // = 2: nucleus with summed momentum A * pPri, with Angantyr,
  //      vs. Z protons and A - Z neutrons each with pPri, with Cascade;
  // = 3: as 2, but use Angantyr in both places;
  // = 4: nucleus with summed momentum A * pPri, with Angantyr,
  //      vs. proton with momentum A * pPri, with Cascade;
  // = 5: as 4, but use Angantyr in both places.
  int comparison = 3;

  // Nuclear properties, in case such a one is selected. Default is iron.
  int Znuc  = 26;
  int Anuc  = 56;

  // Momentum baseline; may be multiplied as above.
  double pPri  = 1e4;

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

  // Minimal kinetic energy for an interaction to be simulated.
  double eKinMin = 0.5;

  // Check, to catch potential infinite loops, or not for upper limit
  // of event record size. The size increases roghly linearly with energy.
  bool checkSize = false;
  int maxSize = 20000 + 3 * int(pPri);

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

  // All figures are collected in fig487-qualifier.pdf, if
  // "python3 plot487.py" is executed after the run. The qualifier
  // is generated from the values of the main event specifications above,
  // "comparison-pPri-nEvent". You may additionally ask to generate
  // five of them as separate figures, fig487a-qualifier.pdf, etc.
  bool separateFigs = true;

//========== Initialization ==========

  // Scenario beam names.
  vector<string> legendA = {"Angantyr(p)", "Angantyr(A)", "Angantyr(A)",
    "Angantyr(A)", "Angantyr(A)"};
  vector<string> legendB = {"Cascade(p)", "Cascade(A*p/n)", "Angantyr(A*p/n)",
    "Cascade(p)", "Angantyr(p)"};
  vector<string> legend = { legendA[comparison - 1], legendB[comparison - 1]};

  // Book histograms.
  const int nCases = 2;
  double depthMax = rhoAir * H / cos(zenithAngle);
  Hist nInt[nCases], diffHadA[nCases], diffHadS[nCases], rateMuonP[nCases],
    rateMuonD[nCases], diffMuonA[nCases], diffMuonS[nCases], prodnueA[nCases],
    prodnueS[nCases], prodnumuA[nCases], prodnumuS[nCases], eventSize[nCases],
    nucleonEnergy[nCases], finalNucleonEnergy[nCases], eKinColl[nCases],
    eDepthNuc[nCases], eDepthHad[nCases], eDepthEM[nCases], eDepthMu[nCases],
    eDepthNu[nCases], eMaxEMnow[nCases], eMaxEM[nCases], eAvgEM[nCases];
  for (int iC = 0; iC < nCases; ++iC) {
    nInt[iC].book("depth of interactions",             100, 0., depthMax);
    diffHadA[iC].book("hadron production-decay depth", 100, 0., depthMax);
    diffHadS[iC].book("hadron production-decay depth", 100, 0., depthMax);
    rateMuonD[iC].book("muon decay depth",             100, 0., depthMax);
    rateMuonP[iC].book("muon production depth",        100, 0., depthMax);
    diffMuonA[iC].book("muon production-decay depth",  100, 0., depthMax);
    diffMuonS[iC].book("muon production-decay depth",  100, 0., depthMax);
    prodnueA[iC].book("nu_e production depth",         100, 0., depthMax);
    prodnueS[iC].book("nu_e production depth",         100, 0., depthMax);
    prodnumuA[iC].book("nu_mu production depth",       100, 0., depthMax);
    prodnumuS[iC].book("nu_mu production depth",       100, 0., depthMax);
    eDepthNuc[iC].book("Nuclear energy at depth",      100, 0., depthMax);
    eDepthHad[iC].book("Hadronic energy at depth",     100, 0., depthMax);
    eDepthEM[iC].book("Electromag.energy at depth",    100, 0., depthMax);
    eDepthMu[iC].book("Muon energy at depth",          100, 0., depthMax);
    eDepthNu[iC].book("Neutrino energy at depth",      100, 0., depthMax);
    eventSize[iC].book("event size",                    80, 10., 1e9, true);
    nucleonEnergy[iC].book("Nucleon Energy",            50, 0.0, 10.0);
    finalNucleonEnergy[iC].book("Final nucleon energy", 50, 0.0, 10.0);
    eKinColl[iC].book("kinetic energy of collisions",   50, 0.0, 10.0);
    eMaxEMnow[iC].book("Electromag.maximum at depth",  100, 0., 0.5*depthMax);
    eMaxEM[iC].book("Electromag.maximum at depth",     100, 0., 0.5*depthMax);
    eAvgEM[iC].book("Electromag.maximum at depth",     100, 0., 0.5*depthMax);
  }

  // Final particle composition info.
  map<int, int> finalAng, finalCas;

  // 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;

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

  // Scale up for incoming nucleus.
  int idnuc = 1000000000 + 10000 * Znuc + 10 * Anuc;
  double mnuc = pythiaMain.particleData.m0( idnuc);
  Vec4 p0nuc( Anuc * pxPri, Anuc * pyPri, -Anuc * pzPri,
    sqrt( pow2(Anuc * pPri) + pow2(mnuc) ) );

  // Scale up p for comparison with A.
  Vec4 p0pA( Anuc * pxPri, Anuc * pyPri, -Anuc * pzPri,
    sqrt( pow2(Anuc * pPri) + pow2(mp) ) );

  // Commonly reused variables.
  bool   canDecayNow, mustDecayNow;
  int    idNow, idTarg, Ztarg, Atarg;
  double mNow, eNow, sigmaN, sigmaO, sigmaAr, sigmaPick, sigmaNow,
         zNow, dzds, logR, zNext;
  Vec4   pNow, vDec, vInt, dirNow, pSumAll[nCases];
  clock_t timeBeg, timeEnd;
  double timeSec[nCases];

  // Pythia object for performing individual Angantyr collisions with
  // the new strategy also for low energy collisions.
  Pythia pythiaAng;

  // Enable Angantyr. Reuse deault MPI and cross sections
  // initialization files. (If alternative parameters are needed these
  // files can be generate by running main424.)
  pythiaAng.readString("include = setups/AngantyrCascade.cmnd");
  // Set up for fixed-target collisions.
  pythiaAng.readString("Beams:frameType = 3");
  pythiaAng.settings.parm("Beams:pzA", -pPri);
  pythiaAng.readString("Beams:pzB = 0.");
  // Decays to be done by pythiaMain except for extremely short-lived hadrons.
  pythiaAng.readString("HadronLevel:Decay = on");
  pythiaAng.settings.flag("ParticleDecays:limitTau0", true);
  pythiaAng.settings.parm("ParticleDecays:tau0Max", 1.0e-10);
  // Reduce printout and relax energy-momentum conservation.
  pythiaAng.readString("Print:quiet = on");
  pythiaAng.readString("Check:epTolErr = 0.1");
  // Switch on the new new cascade mode.
  pythiaAng.readString("HeavyIon:eCMLowEnergy = 20");
  pythiaAng.settings.mvec("Angantyr:cascadeMediumIons", mediumIons);
  pythiaAng.readString("SoftQCD:all = on");
  pythiaAng.readString("LowEnergyQCD:all = on");

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

  // Normalization factor to get cross section per nucleon.
  double sigmaNorm = 0.0;
  for ( size_t i = 0; i < mediumIons.size(); ++i )
    sigmaNorm += mediumFracs[i]*((mediumIons[i]/10)%1000);

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

  // See PythiaCascade.h for full list and explanation of options.
  // Only the first value deviates from default.
  double eKinMinCas         = eKinMin;
  double enhanceSDtargetCas = 0.5;
  string initFileCas        = "setups/InitDefaultMPI.cmnd";

  // Return error if PythiaCas initialization fails.
  if (!pythiaCas.init( eKinMinCas, enhanceSDtargetCas, initFileCas)) return 1;

  // Begin loop over two beam options.
  for (int iCase = 0; iCase < nCases; ++iCase) {
    timeBeg = clock();
    cout << "Starting " << legend[iCase] << " run..." << endl;
    ProgressLog logger(nEvent);

//========== Event generation ==========

    // Begin event loop. Clear main event record.
    for (int iEvent = 0; iEvent < nEvent; logger(++iEvent)) {
      eventMain.clear();

      // Insert p projectile in event record.
      if (comparison == 1) {
        eventMain.append(90,  -11, 0, 0, 1, 1, 0, 0, p0p, mp);
        eventMain.append(2212, 12, 0, 0, 0, 0, 0, 0, p0p, mp);

      // Insert a nuclear projectile projectile.
      } else if (iCase == 0) {
        eventMain.append(90,   -11, 0, 0, 1, 1, 0, 0, p0nuc, mnuc);
        eventMain.append(idnuc, 12, 0, 0, 0, 0, 0, 0, p0nuc, mnuc);

      // Insert A separate proton/neutron projectiles.
      } else if (comparison == 2 || comparison == 3) {
        eventMain.append(90,  -11, 0, 0, 1, 1, 0, 0,
          Znuc * p0p + (Anuc - Znuc) * p0n, Znuc * mp + (Anuc - Znuc) * mn);
        for (int iProj = 1; iProj <= Anuc; ++iProj) {
          if (iProj <= Znuc)
               eventMain.append(2212, 12, 0, 0, 0, 0, 0, 0, p0p, mp);
          else eventMain.append(2112, 12, 0, 0, 0, 0, 0, 0, p0n, mn);
        }

      // Insert a p with A times higher momentum.
      } else {
        eventMain.append(90,  -11, 0, 0, 1, 1, 0, 0, p0pA, mp);
        eventMain.append(2212, 12, 0, 0, 0, 0, 0, 0, p0pA, mp);
      }

      // Set initial location of initiator, where z is distance above ground.
      double yProd = -mediumHeight * tan(zenithAngle);
      double zProd = mediumHeight;
      for (int iProj = 0; iProj < eventMain.size(); ++iProj) {
        eventMain[iProj].yProd(yProd);
        eventMain[iProj].zProd(zProd);
      }

      // 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;

        // For nuclear projectile rescale to single nucleon
        if ( abs(idNow) > 1000000000 ) {
          int ANow = (abs(idNow)/10)%1000;
          eNow /= ANow;
          pNow /= ANow;
          mNow /= ANow;
        }

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

        // Non-hadronic and low-energy hadrons should not interact with medium.
        // Decay non-hadronic or low-energy ones if decay happens above ground.
        if ( (!hadNow.isHadron() && idNow < 1000000000)
          || eNow - mNow < eKinMin) {
          if (canDecayNow) mustDecayNow = true;
          else continue;
        }

        // Get hadron-nucleus cross sections and pick interaction vertex.
        if (!mustDecayNow) {

          // Begin Angantyr handling.
          if (iCase == 0 ||
             (iCase == 1 && ((comparison == 3) || (comparison == 5)))) {

            // First let Angantyr know about the beam and its energy.
            // The first round of setBeamIDs and setKinematics with a
            // light hadron helps avoid vanishing inelastic cross section.
            pythiaAng.setBeamIDs(211, 0);
            pythiaAng.setKinematics(pNow, Vec4(0., 0., 0., 0.));
            if ( !pythiaAng.setBeamIDs(idNow, 0)
              || !pythiaAng.setKinematics(pNow, Vec4(0., 0., 0., 0.)) ) {
              if (canDecayNow) mustDecayNow = true;
              else continue;
            }

            if (!mustDecayNow) {
              // Get hadron-ion cross sections, corrected by fractions,
              // and normalised to cross section per nucleon.
              // (Atmosphere medium density is nucleons per volume.)
              vector<double> crossSections
                = pythiaAng.info.hiInfo->mediumXSecs();
              sigmaNow = 0.0;
              for ( size_t i = 0; i < mediumFracs.size(); ++i )
                sigmaNow += (crossSections[i] *= mediumFracs[i]/sigmaNorm);

              // Get info about projetile
              dirNow = pNow / pNow.pAbs();
              zNow  = hadNow.zProd();
              dzds  = hadNow.pz() / hadNow.pAbs();
              vInt = hadNow.vProd();

              // Accept/reject loop to handle overestimated cross sections
              // in Angantyr.
              idTarg = 0;
              while ( !idTarg ) {

                // Calculate potential interaction vertex.
                logR  = log(pythiaAng.rndm.flat());
                zNext = -H * log( exp(-zNow / H)
                      + dzds / (H * sigmaNow * mediumDensity) * logR );
                vInt += (zNext - zNow) * dirNow / dzds;
                zNow  = zNext;

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

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

                // Now try to pick a nucleus according to the relative
                // overestimated cross section and do a collision.
                // Accept/reject to get the correct cross section.
                idTarg = mediumIons[pythiaAng.rndm.pick(crossSections)];
                if (!pythiaAng.setBeamIDs(idNow, idTarg) || !pythiaAng.next())
                  idTarg = 0;
              }
            }

            // Prepare for next step.
            if ( !idTarg && !mustDecayNow ) continue;
            Ztarg = (idTarg / 10000) % 100;
            Atarg = (idTarg / 10) % 1000;

          // Begin PythiaCascade handling. Skip if vanishing cross section.
          } else {
            if (!pythiaCas.sigmaSetuphN( idNow, pNow, mNow)) {
              if (canDecayNow) mustDecayNow = true;
              else continue;
            }

            if (!mustDecayNow) {
              // 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.);

              // 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 (canDecayNow && vDec.pz() > vInt.pz()) mustDecayNow = true;
            }
          }
        }

        // Handling of decay or interaction with Angantyr.
        if (iCase == 0 ||
           (iCase == 1 && ((comparison == 3) || (comparison == 5)))) {

          // 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 {
            Event & eventTmp = pythiaAng.event;

            // 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 < eventTmp.size(); ++iSub) {
              if (eventTmp[iSub].isFinal()) {
                int idNew = eventTmp[iSub].idAbs();
                if ( idNew/100000000 == 10 && idNew%10 == 9 ) {
                  int ZNew = (idNew/10000)%1000;
                  pythiaMain.particleData.addParticle
                    (idNew, "NucRem", 0, 3*ZNew, 0, eventTmp[iSub].mCalc());
                  pythiaMain.particleData.particleDataEntryPtr(idNew)->
                    setHasChanged(false);
                }
                int iNew = eventMain.append(eventTmp[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);

            // Fill histogram.
            eKinColl[iCase].fill(eNow - mNow);
          }

        // 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,
          // including invariant lifetime.
          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);

          // Fill histogram.
          if (!mustDecayNow) eKinColl[iCase].fill(eNow - mNow);

        }

        // Stop generation if event record is extremely long.
        if (checkSize && eventMain.size() > maxSize) {
          cout << "\n Error: the event record has now reached a size of "
               << eventMain.size() << "," << endl
               << "            which is bigger than the maxSize value "
               << maxSize << "." << endl
               << " Likely there are unfragmented partons and/or undecayed "
               << "particles." << endl
               << " Feel free to increase the maxSize value when relevant."
               << endl;
          break;
        }

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

//========== Event analysis ==========

      // Begin analysis: loop over all particles.
      eventSize[iCase].fill(eventMain.size());
      eMaxEMnow[iCase].null();
      Vec4 pSumEvt;
      int nNucInit = (iCase == 1 && ((comparison == 2) || (comparison == 3)))
        ? Anuc : 1;
      for (Particle& h : eventMain) {

        // Tag higher-energy particles. Production and decay depth X.
        bool highKin = (h.e() - h.m() > eKinMinHist);
        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);
          continue;
        }

        // Restrict to downwards-moving particles.
        if ( (h.status() == -12 && h.index() > nNucInit) || h.pz() > -0.01)
          continue;
        if (h.isFinal()) pSumEvt += h.p();

        // Kinetic energy of nucleons.
        if ( h.id() == 2212 || h.id() == 2112 ) {
          nucleonEnergy[iCase].fill(h.e() - h.m());
          if ( h.isFinal() )
            finalNucleonEnergy[iCase].fill(h.e() - h.m());
        }

        // Track depths where particles are created/destroyed ...

        // ... nuclei.
        if (h.id() > 1000000000) {
          if (h.isFinal()) {
            eDepthNuc[iCase].fill(depthProd, h.pz());
          } else {
            eDepthNuc[iCase].fill(min(depthProd, depthDec), h.pz());
            eDepthNuc[iCase].fill(max(depthProd, depthDec), -h.pz());
         }

        // ... hadrons.
        } else if (h.isHadron()) {
          if (h.isFinal()) {
            diffHadA[iCase].fill(depthProd, 1.);
            if (highKin) diffHadS[iCase].fill(depthProd, 1.);
            eDepthHad[iCase].fill(depthProd, h.pz());
          } else {
            diffHadA[iCase].fill(min(depthProd, depthDec), 1.);
            if (highKin) diffHadS[iCase].fill(min(depthProd, depthDec), 1.);
            eDepthHad[iCase].fill(min(depthProd, depthDec), h.pz());
            diffHadA[iCase].fill(max(depthProd, depthDec), -1.);
            if (highKin) diffHadS[iCase].fill(max(depthProd, depthDec), -1.);
            eDepthHad[iCase].fill(max(depthProd, depthDec), -h.pz());
          }

        // ... electrons and photons.
        } else if (h.idAbs() == 11 || h.idAbs() == 22) {
          if (h.isFinal()) {
            eDepthEM[iCase].fill(depthProd, h.pz());
            eMaxEMnow[iCase].fill(depthProd, h.e());
          } else {
            eDepthEM[iCase].fill(min(depthProd, depthDec), h.pz());
            eDepthEM[iCase].fill(max(depthProd, depthDec), -h.pz());
            eMaxEMnow[iCase].fill(min(depthProd, depthDec), h.e());
            eMaxEMnow[iCase].fill(max(depthProd, depthDec), -h.e());
          }

        // ... muons.
        } else if (h.idAbs() == 13) {
          if (h.isFinal()) {
            rateMuonP[iCase].fill(depthProd, 1.);
            diffMuonA[iCase].fill(depthProd, 1.);
            if (highKin) diffMuonS[iCase].fill(depthProd, 1.);
            eDepthMu[iCase].fill(depthProd, h.pz());
          } else {
            rateMuonP[iCase].fill(min(depthProd, depthDec), 1.);
            rateMuonD[iCase].fill(max(depthProd, depthDec), 1.);
            diffMuonA[iCase].fill(min(depthProd, depthDec), 1.);
            if (highKin) diffMuonS[iCase].fill(min(depthProd, depthDec), 1.);
            eDepthMu[iCase].fill(min(depthProd, depthDec), h.pz());
            diffMuonA[iCase].fill(max(depthProd, depthDec), -1.);
            if (highKin) diffMuonS[iCase].fill(max(depthProd, depthDec), -1.);
            eDepthMu[iCase].fill(max(depthProd, depthDec), -h.pz());
          }

        // ... neutrinos: electron and muon kinds.
        } else if (h.idAbs() == 12) {
          prodnueA[iCase].fill(depthProd, 1.);
          if (highKin) prodnueS[iCase].fill(depthProd, 1.);
          eDepthNu[iCase].fill(depthProd, h.pz());
        } else if (h.idAbs() == 14) {
          prodnumuA[iCase].fill(depthProd, 1.);
          if (highKin) prodnumuS[iCase].fill(depthProd, 1.);
          eDepthNu[iCase].fill(depthProd, h.pz());
        }

        // Particle composition, neglecting upwards moving ones.
        if (h.isFinal() && h.pz() < 0.) {
          if (iCase == 0) finalAng[h.idAbs()] += 1;
          if (iCase == 1) finalCas[h.idAbs()] += 1;
        }
      }

      // Find maximum of EM energy production.
      int binMax   = 0;
      double pzMax = 0.;
      for (int bin = 1; bin <= 100; ++bin) {
        double pzNow = eMaxEMnow[iCase].getBinContent(bin);
        if (pzNow > pzMax) {
          binMax = bin;
          pzMax  = pzNow;
        }
      }
      double depthMaxEM = eMaxEMnow[iCase].getBinCenter(binMax);
      eMaxEM[iCase].fill( depthMaxEM, 1.);
      double depthAvgEM = eMaxEMnow[iCase].getXMean();
      eAvgEM[iCase].fill( depthAvgEM, 1.);

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

//========== Final statistics and histograms ==========

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

  // Print particle production statistics.
  cout << "\n Final particle composition in " << legend[0] << ": " << endl;
  for (const auto& entry : finalAng)
    cout << setw(12) << entry.first << setw(12) << entry.second << endl;
  cout << "\n Final particle composition in " << legend[1] << ": " << endl;
  for (const auto& entry : finalCas)
    cout << setw(12) << entry.first << setw(12) << entry.second << 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();

  // Integrate and normalize histograms.
  for (int iC = 0; iC < nCases; ++iC) {

    // Integrate relevant histograms.
    diffHadA[iC].makeCumulative();
    diffHadS[iC].makeCumulative();
    diffMuonA[iC].makeCumulative();
    diffMuonS[iC].makeCumulative();
    prodnueA[iC].makeCumulative();
    prodnueS[iC].makeCumulative();
    prodnumuA[iC].makeCumulative();
    prodnumuS[iC].makeCumulative();
    eDepthNuc[iC].makeCumulative();
    eDepthHad[iC].makeCumulative();
    eDepthEM[iC].makeCumulative();
    eDepthMu[iC].makeCumulative();
    eDepthNu[iC].makeCumulative();

    // Normalize histograms.
    nInt[iC].normalizeSpectrum(nEvent);
    rateMuonP[iC].normalizeSpectrum(nEvent);
    rateMuonD[iC].normalizeSpectrum(nEvent);
    diffHadA[iC]  /= nEvent;
    diffHadS[iC]  /= nEvent;
    diffMuonA[iC] /= nEvent;
    diffMuonS[iC] /= nEvent;
    prodnueA[iC]  /= nEvent;
    prodnueS[iC]  /= nEvent;
    prodnumuA[iC] /= nEvent;
    prodnumuS[iC] /= nEvent;
    eDepthNuc[iC] /= pSumAll[iC].pz();
    eDepthHad[iC] /= pSumAll[iC].pz();
    eDepthEM[iC]  /= pSumAll[iC].pz();
    eDepthMu[iC]  /= pSumAll[iC].pz();
    eDepthNu[iC]  /= pSumAll[iC].pz();
    nucleonEnergy[iC]      /= nEvent;
    finalNucleonEnergy[iC] /= nEvent;
    eMaxEM[iC] /= nEvent;
    eAvgEM[iC] /= nEvent;
  }

  // Plot histograms. Symbols and (parts of) legends.
  HistPlot plt("plot487");
  vector<string> col = { "black", "r", "b", "g"};
  vector<string> ecol = { "blue", "magenta", "b", "g"};
  stringstream eKinstream;
  eKinstream << " $E_{\\mathrm{kin}} > $" << fixed << setprecision(1)
    << eKinMinHist << " GeV";
  string eKinstring = eKinstream.str();
  stringstream qualifierstream;
  qualifierstream << "-" << comparison << "-" << scientific << setprecision(0)
    << pPri << "-" << nEvent;
  string qualifier = qualifierstream.str();

  // Common vertical scale. Rescale from per-nucleon to full energy.
  double scaleLo = min( 2. * sqrt(pPri / 1e6), 2. * pPri / 1e6);
  double scaleHi = (pPri < 0.5e6) ? 2. * pPri / 1e6 : pPri / 1e6;
  if (comparison > 1) {
    scaleLo *= Anuc;
    scaleHi *= Anuc;
  }

  // Nuclear interaction rate.
  plt.frame("fig487" + qualifier, "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.05, 100. * scaleHi, true);

  // Current hadron number.
  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(diffHadA[iC], "-,"+col[iC], legend[iC] + " all");
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(diffHadS[iC], "--,"+col[iC], legend[iC] + eKinstring);
  plt.plot( 0., depthMax, 5. * scaleLo, 100000. * scaleHi, true);

  // Muon production and decay.
  plt.frame("", "Rate of muon production and decay at depth ",
    "$X$ (g/cm$^2$)", "$(1/n_{ev})  dn_{\\mu}/dX$", 6.4, 4.8);
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(rateMuonP[iC], "-,"+col[iC], legend[iC] + " production");
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(rateMuonD[iC], "--,"+col[iC], legend[iC] + " decay");
  plt.plot( 0., depthMax, 0.2 * scaleLo, 200. * scaleHi, true);

  // Net muon number.
  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(diffMuonA[iC], "-,"+col[iC], legend[iC] + " all");
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(diffMuonS[iC], "--,"+col[iC], legend[iC] + eKinstring);
  plt.plot( 0., depthMax, 5. * scaleLo, 20000. * scaleHi, true);

  // Net nu_e number.
  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(prodnueA[iC], "-,"+col[iC], legend[iC] + " all");
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(prodnueS[iC], "--,"+col[iC], legend[iC] + eKinstring);
  plt.plot( 0., depthMax, 0.2 * scaleLo, 50000. * scaleHi, true);

  // Net nu_mu number.
  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(prodnumuA[iC], "-,"+col[iC], legend[iC] + " all");
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(prodnumuS[iC], "--,"+col[iC], legend[iC] + eKinstring);
  plt.plot( 0., depthMax, 5. * scaleLo, 100000. * scaleHi, true);

  // Current nuclei/hadron/EM/mu/nu momentum fractions of the whole.
  plt.frame("", "Momentum fractions at depth ",
    "$X$ (g/cm$^2$)", "$(1/n_{ev}) \\int_0^{X} dpz_{had}$", 6.4, 4.8);
  for (int iC = 0; iC < nCases; ++iC) {
    if (comparison > 1 && iC == 0)
      plt.add(eDepthNuc[iC], "-,"+ecol[iC], legend[iC] + " nuclei");
    plt.add(eDepthHad[iC], "-,"+col[iC],   legend[iC] + " hadrons");
    plt.add(eDepthEM[iC], "--,"+col[iC],  legend[iC] + " electrons+photons");
    plt.add(eDepthMu[iC], "-.,"+col[iC],  legend[iC] + " muons");
    plt.add(eDepthNu[iC], ".,"+col[iC],   legend[iC] + " neutrinos");
  }
  plt.plot(0., depthMax, 1e-3, 1., true, false);

  // Event record size.
  plt.frame("", "Size of the event record ",
    "$n_{\\mathrm{size}}$", "Number of events", 6.4, 4.8);
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(eventSize[iC], "h,"+col[iC], legend[iC]);
  plt.plot();

  // Nucleon energy.
  plt.frame("", "Energy of produced nucleons ",
    "$E_{nuc}$", "dN/dE_{nuc}", 6.4, 4.8);
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(nucleonEnergy[iC], "h,"+col[iC], legend[iC]);
  plt.plot(true);
  plt.frame("", "Energy of final nucleons ",
    "$E_{nuc}$", "dN/dE_{nuc}", 6.4, 4.8);
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(finalNucleonEnergy[iC], "h,"+col[iC], legend[iC]);
  plt.plot(true);

  // Kinetic energy of interacting particles.
   plt.frame("", "Kinetic energy of colliding hadrons ",
    "$E_{had}$", "$dN/dE_{had}$", 6.4, 4.8);
  for (int iC = 0; iC < nCases; ++iC)
    plt.add(eKinColl[iC], "h,"+col[iC], legend[iC]);
  plt.plot();

  if (comparison > 1) {
    // Depth of maximum EM energy production.
    plt.frame("", "Depth of maximum EM energy production ",
      "$X_{\\mathrm{max}}$ (g/cm$^2$)", "$P(X_{\\mathrm{max}})$", 6.4, 4.8);
    for (int iC = 0; iC < nCases; ++iC)
      plt.add(eMaxEM[iC], "h,"+col[iC], legend[iC]);
    plt.plot();
    plt.frame("", "Depth of average EM energy production ",
      "$X_{\\mathrm{avg}}$ (g/cm$^2$)", "$P(X_{\\mathrm{avg}})$", 6.4, 4.8);
    for (int iC = 0; iC < nCases; ++iC)
      plt.add(eAvgEM[iC], "h,"+col[iC], legend[iC]);
    plt.plot();
  }

  // From now detached figures, e.g. for inclusion in article.
  if (separateFigs) {

    // Nuclear interaction rate.
    plt.frame("fig487a" + qualifier, "", "$X$ (g/cm$^2$)",
    "$(1/n_{\\mathrm{ev}}) \\mathrm{d}n_{\\mathrm{int}}/dX$", 4.8, 4.8);
    for (int iC = 0; iC < nCases; ++iC)
      plt.add(nInt[iC], "-,"+col[iC], legend[iC]);
    plt.plot( 0., depthMax, 0.05, 100. * scaleHi, true);

    // Current hadron number.
    plt.frame("fig487b" + qualifier, "", "$X$ (g/cm$^2$)",
    "$(1/n_{\\mathrm{ev}}) \\int_0^{X} \\mathrm{d}n_{\\mathrm{had}}$",
      4.8, 4.8);
    for (int iC = 0; iC < nCases; ++iC)
      plt.add(diffHadA[iC], "-,"+col[iC], legend[iC] + " all");
    for (int iC = 0; iC < nCases; ++iC)
      plt.add(diffHadS[iC], "--,"+col[iC], legend[iC] + eKinstring);
    plt.plot( 0., depthMax, 5. * scaleLo, 100000. * scaleHi, true);

    // Muon production and decay.
    plt.frame("fig487c" + qualifier, "", "$X$ (g/cm$^2$)",
      "$(1/n_{\\mathrm{ev}}) \\int_0^{X} \\mathrm{d}n_{\\mu}$", 4.8, 4.8);
    for (int iC = 0; iC < nCases; ++iC)
      plt.add(rateMuonP[iC], "-,"+col[iC], legend[iC] + " production");
    for (int iC = 0; iC < nCases; ++iC)
      plt.add(rateMuonD[iC], "--,"+col[iC], legend[iC] + " decay");
    plt.plot( 0., depthMax, 0.2 * scaleLo, 200. * scaleHi, true);

    // Net muon number.
    plt.frame("fig487d" + qualifier, "", "$X$ (g/cm$^2$)",
      "$(1/n_{\\mathrm{ev}}) \\int_0^{X} \\mathrm{d}n_{\\mu}$", 4.8, 4.8);
    for (int iC = 0; iC < nCases; ++iC)
      plt.add(diffMuonA[iC], "-,"+col[iC], legend[iC] + " all");
    for (int iC = 0; iC < nCases; ++iC)
      plt.add(diffMuonS[iC], "--,"+col[iC], legend[iC] + eKinstring);
    plt.plot( 0., depthMax, 5. * scaleLo, 20000. * scaleHi, true);

    // Current nuclei/hadron/EM/mu/nu momentum fraction of the whole.
    plt.frame("fig487e" + qualifier, "", "$X$ (g/cm$^2$)",
      "$p_{z,\\mathrm{kind}}(X) / p_{z,\\mathrm{tot}}$", 4.8, 4.8);
    for (int iC = 0; iC < nCases; ++iC) {
      if (comparison > 1 && iC == 0)
        plt.add(eDepthNuc[iC], "-,"+ecol[iC], legend[iC] + " nuclei");
      plt.add(eDepthHad[iC], "-,"+col[iC],  legend[iC] + " hadrons");
      plt.add(eDepthEM[iC], "--,"+col[iC],  legend[iC] + " electrons+photons");
      plt.add(eDepthMu[iC], "-.,"+col[iC],  legend[iC] + " muons");
      plt.add(eDepthNu[iC], ".,"+col[iC],   legend[iC] + " neutrinos");
    }
    plt.plot(0., depthMax, 1e-3, 1., true, false);

    // Depth of maximum EM energy production.
    plt.frame("fig487f" + qualifier, "",
      "$X_{\\mathrm{avg}}$ (g/cm$^2$)", "$P(X_{\\mathrm{avg}})$", 4.8, 4.8);
    for (int iC = 0; iC < nCases; ++iC)
      plt.add(eAvgEM[iC], "h,"+col[iC], legend[iC]);
    plt.plot();
  }
}