LCOV - code coverage report
Current view: top level - src/module - NuclearDecay.cpp (source / functions) Coverage Total Hit
Test: coverage.info.cleaned Lines: 82.1 % 168 138
Test Date: 2026-06-18 09:49:19 Functions: 84.6 % 13 11

            Line data    Source code
       1              : #include "crpropa/module/NuclearDecay.h"
       2              : #include "crpropa/Units.h"
       3              : #include "crpropa/ParticleID.h"
       4              : #include "crpropa/ParticleMass.h"
       5              : #include "crpropa/Random.h"
       6              : 
       7              : #include <fstream>
       8              : #include <limits>
       9              : #include <cmath>
      10              : #include <stdexcept>
      11              : 
      12              : #include "kiss/logger.h"
      13              : 
      14              : namespace crpropa {
      15              : 
      16            9 : NuclearDecay::NuclearDecay(bool electrons, bool photons, bool neutrinos, double l) {
      17            9 :         haveElectrons = electrons;
      18            9 :         havePhotons = photons;
      19            9 :         haveNeutrinos = neutrinos;
      20            9 :         limit = l;
      21            9 :         setDescription("NuclearDecay");
      22              : 
      23              :         // load decay table
      24           18 :         std::string filename = getDataPath("nuclear_decay.txt");
      25            9 :         std::ifstream infile(filename.c_str());
      26            9 :         if (!infile.good())
      27              :                 throw std::runtime_error(
      28            0 :                                 "crpropa::NuclearDecay: could not open file " + filename);
      29              : 
      30            9 :         decayTable.resize(27 * 31);
      31              :         std::string line;
      32         8550 :         while (std::getline(infile,line)) {
      33         8541 :                 std::stringstream stream(line);
      34         8541 :                 if (stream.peek() == '#')
      35              :                         continue;
      36              :                 DecayMode decay;
      37              :                 int Z, N;
      38              :                 double lifetime;
      39         8523 :                 stream >> Z >> N >> decay.channel >> lifetime;
      40         8523 :                 decay.rate = 1. / lifetime / c_light; // decay rate in [1/m]
      41              :                 std::vector<double> gamma;
      42              :                 double val;
      43        34227 :                 while (stream >> val)
      44        25704 :                         gamma.push_back(val);
      45        21375 :                 for (int i = 0; i < gamma.size(); i += 2) {
      46        12852 :                         decay.energy.push_back(gamma[i] * keV);
      47        12852 :                         decay.intensity.push_back(gamma[i+1]);
      48              :                 }
      49         8523 :                 if (infile)
      50         8523 :                         decayTable[Z * 31 + N].push_back(decay);
      51        17064 :         }
      52            9 :         infile.close();
      53           18 : }
      54              : 
      55            2 : void NuclearDecay::setHaveElectrons(bool b) {
      56            2 :         haveElectrons = b;
      57            2 : }
      58              : 
      59            1 : void NuclearDecay::setHavePhotons(bool b) {
      60            1 :         havePhotons = b;
      61            1 : }
      62              : 
      63            1 : void NuclearDecay::setHaveNeutrinos(bool b) {
      64            1 :         haveNeutrinos = b;
      65            1 : }
      66              : 
      67            0 : void NuclearDecay::setLimit(double l) {
      68            0 :         limit = l;
      69            0 : }
      70              : 
      71          439 : void NuclearDecay::process(Candidate *candidate) const {
      72              :         // the loop should be processed at least once for limiting the next step
      73          439 :         double step = candidate->getCurrentStep();
      74          439 :         double z = candidate->getRedshift();
      75              :         do {
      76              :                 // check if nucleus
      77          444 :                 int id = candidate->current.getId();
      78          444 :                 if (not (isNucleus(id)))
      79              :                         return;
      80              : 
      81          443 :                 int A = massNumber(id);
      82          443 :                 int Z = chargeNumber(id);
      83          443 :                 int N = A - Z;
      84              : 
      85              :                 // check if particle can decay
      86          443 :                 const std::vector<DecayMode> &decays = decayTable[Z * 31 + N];
      87          443 :                 if (decays.size() == 0)
      88              :                         return;
      89              : 
      90              :                 // find interaction mode with minimum random decay distance
      91           12 :                 Random &random = Random::instance();
      92              :                 double randDistance = std::numeric_limits<double>::max();
      93              :                 int channel;
      94              :                 double totalRate = 0;
      95              : 
      96           24 :                 for (size_t i = 0; i < decays.size(); i++) {
      97           12 :                         double rate = decays[i].rate;
      98           12 :                         rate /= candidate->current.getLorentzFactor();  // relativistic time dilation
      99           12 :                         rate /= (1 + z);  // rate per light travel distance -> rate per comoving distance
     100           12 :                         totalRate += rate;
     101           12 :                         double d = -log(random.rand()) / rate;
     102           12 :                         if (d > randDistance)
     103            0 :                                 continue;
     104              :                         randDistance = d;
     105           12 :                         channel = decays[i].channel;
     106              :                 }
     107              : 
     108              :                 // check if interaction doesn't happen
     109           12 :                 if (step < randDistance) {
     110              :                         // limit next step to a fraction of the mean free path
     111            7 :                         candidate->limitNextStep(limit / totalRate);
     112            7 :                         return;
     113              :                 }
     114              : 
     115              :                 // interact and repeat with remaining step
     116            5 :                 performInteraction(candidate, channel);
     117            5 :                 step -= randDistance;
     118            5 :         } while (step > 0);
     119              : }
     120              : 
     121          964 : void NuclearDecay::performInteraction(Candidate *candidate, int channel) const {
     122              :         // interpret decay channel
     123              :         int nBetaMinus = digit(channel, 10000);
     124              :         int nBetaPlus = digit(channel, 1000);
     125              :         int nAlpha = digit(channel, 100);
     126              :         int nProton = digit(channel, 10);
     127              :         int nNeutron = digit(channel, 1);
     128              : 
     129              :         // perform decays
     130          964 :         if (havePhotons)
     131           11 :                 gammaEmission(candidate,channel);
     132         1276 :         for (size_t i = 0; i < nBetaMinus; i++)
     133          312 :                 betaDecay(candidate, false);
     134         1131 :         for (size_t i = 0; i < nBetaPlus; i++)
     135          167 :                 betaDecay(candidate, true);
     136          988 :         for (size_t i = 0; i < nAlpha; i++)
     137           24 :                 nucleonEmission(candidate, 4, 2);
     138         1299 :         for (size_t i = 0; i < nProton; i++)
     139          335 :                 nucleonEmission(candidate, 1, 1);
     140         1304 :         for (size_t i = 0; i < nNeutron; i++)
     141          340 :                 nucleonEmission(candidate, 1, 0);
     142          964 : }
     143              : 
     144           11 : void NuclearDecay::gammaEmission(Candidate *candidate, int channel) const {
     145           11 :         int id = candidate->current.getId();
     146           11 :         int Z = chargeNumber(id);
     147           11 :         int N = massNumber(id) - Z;
     148              : 
     149              :         // get photon energies and emission probabilities for decay channel
     150           11 :         const std::vector<DecayMode> &decays = decayTable[Z * 31 + N];
     151              :         size_t idecay = decays.size();
     152           21 :         while (idecay-- != 0) {
     153           21 :                 if (decays[idecay].channel == channel)
     154              :                         break;
     155              :         }
     156              : 
     157              :         const std::vector<double> &energy = decays[idecay].energy;
     158              :         const std::vector<double> &intensity = decays[idecay].intensity;
     159              : 
     160              :         // check if photon emission available
     161           11 :         if (energy.size() == 0)
     162            0 :                 return;
     163              : 
     164           11 :         Random &random = Random::instance();
     165           11 :         Vector3d pos = random.randomInterpolatedPosition(candidate->previous.getPosition(), candidate->current.getPosition());
     166              : 
     167           26 :         for (int i = 0; i < energy.size(); ++i) {
     168              :                 // check if photon of specific energy is emitted
     169           15 :                 if (random.rand() > intensity[i])
     170            7 :                         continue;
     171              :                 // create secondary photon; boost to lab frame
     172            8 :                 double cosTheta = 2 * random.rand() - 1;
     173            8 :                 double E = energy[i] * candidate->current.getLorentzFactor() * (1. - cosTheta);
     174            8 :                 candidate->addSecondary(22, E, pos, 1., interactionTag);
     175              :         }
     176              : }
     177              : 
     178          479 : void NuclearDecay::betaDecay(Candidate *candidate, bool isBetaPlus) const {
     179          479 :         double gamma = candidate->current.getLorentzFactor();
     180          479 :         int id = candidate->current.getId();
     181          479 :         int A = massNumber(id);
     182          479 :         int Z = chargeNumber(id);
     183              : 
     184              :         // beta- decay
     185              :         int electronId = 11; // electron
     186              :         int neutrinoId = -12; // anti-electron neutrino
     187              :         int dZ = 1;
     188              :         // beta+ decay
     189          479 :         if (isBetaPlus) {
     190              :                 electronId = -11; // positron
     191              :                 neutrinoId = 12; // electron neutrino
     192              :                 dZ = -1;
     193              :         }
     194              : 
     195              :         // update candidate, nuclear recoil negligible
     196              :         try
     197              :         {
     198          479 :                 candidate->current.setId(nucleusId(A, Z + dZ));
     199              :         }
     200            0 :         catch (std::runtime_error &e)
     201              :         {
     202            0 :                 KISS_LOG_ERROR<< "Something went wrong in the NuclearDecay\n" << "Please report this error on https://github.com/CRPropa/CRPropa3/issues including your simulation setup and the following random seed:\n" << Random::instance().getSeed_base64();
     203            0 :                 throw;
     204            0 :         }
     205              : 
     206          479 :         candidate->current.setLorentzFactor(gamma);
     207              : 
     208          479 :         if (not (haveElectrons or haveNeutrinos))
     209          467 :                 return;
     210              : 
     211              :         // Q-value of the decay, subtract total energy of emitted photons
     212           12 :         double m1 = nuclearMass(A, Z);
     213           12 :         double m2 = nuclearMass(A, Z+dZ);
     214           12 :         double Q = (m1 - m2 - mass_electron) * c_squared;
     215              : 
     216              :         // generate cdf of electron energy, neglecting Coulomb correction
     217              :         // see Basdevant, Fundamentals in Nuclear Physics, eq. (4.92)
     218              :         // This leads to deviations from theoretical expectations at low 
     219              :         // primary energies.
     220              :         std::vector<double> energies;
     221              :         std::vector<double> densities; // cdf(E), unnormalized
     222              : 
     223           12 :         energies.reserve(51);
     224           12 :         densities.reserve(51);
     225              : 
     226              :         double me = mass_electron * c_squared;
     227           12 :         double cdf = 0;
     228          624 :         for (int i = 0; i <= 50; i++) {
     229          612 :                 double E = me + i / 50. * Q;
     230          612 :                 cdf += E * sqrt(E * E - me * me) * pow(Q + me - E, 2);
     231          612 :                 energies.push_back(E);
     232          612 :                 densities.push_back(cdf);
     233              :         }
     234              : 
     235              :         // draw random electron energy and angle
     236              :         // assumption of ultra-relativistic particles 
     237              :         // leads to deviations from theoretical predictions
     238              :         // is not problematic for usual CRPropa energies E>~TeV
     239           12 :         Random &random = Random::instance();
     240           12 :         double E = interpolate(random.rand() * cdf, densities, energies);
     241           12 :         double p = sqrt(E * E - me * me);  // p*c
     242           12 :         double cosTheta = 2 * random.rand() - 1;
     243              : 
     244              :         // boost to lab frame
     245           12 :         double Ee = gamma * (E - p * cosTheta);
     246           12 :         double Enu = gamma * (Q + me - E) * (1 + cosTheta);  // pnu*c ~ Enu
     247              : 
     248           12 :         Vector3d pos = random.randomInterpolatedPosition(candidate->previous.getPosition(), candidate->current.getPosition());
     249           12 :         if (haveElectrons)
     250           12 :                 candidate->addSecondary(electronId, Ee, pos, 1., interactionTag);
     251           12 :         if (haveNeutrinos)
     252           10 :                 candidate->addSecondary(neutrinoId, Enu, pos, 1., interactionTag);
     253           12 : }
     254              : 
     255          699 : void NuclearDecay::nucleonEmission(Candidate *candidate, int dA, int dZ) const {
     256          699 :         Random &random = Random::instance();
     257          699 :         int id = candidate->current.getId();
     258          699 :         int A = massNumber(id);
     259          699 :         int Z = chargeNumber(id);
     260          699 :         double EpA = candidate->current.getEnergy() / double(A);
     261              : 
     262              :         try
     263              :         {
     264          699 :                 candidate->current.setId(nucleusId(A - dA, Z - dZ));
     265              :         }
     266            0 :         catch (std::runtime_error &e)
     267              :         {
     268            0 :                 KISS_LOG_ERROR<< "Something went wrong in the NuclearDecay\n" << "Please report this error on https://github.com/CRPropa/CRPropa3/issues including your simulation setup and the following random seed:\n" << Random::instance().getSeed_base64();
     269            0 :                 throw;
     270            0 :         }
     271              : 
     272          699 :         candidate->current.setEnergy(EpA * (A - dA));
     273          699 :         Vector3d pos = random.randomInterpolatedPosition(candidate->previous.getPosition(),candidate->current.getPosition());
     274              : 
     275              :         try
     276              :         {
     277          699 :                 candidate->addSecondary(nucleusId(dA, dZ), EpA * dA, pos, 1., interactionTag);
     278              :         }
     279            0 :         catch (std::runtime_error &e)
     280              :         {
     281            0 :                 KISS_LOG_ERROR<< "Something went wrong in the NuclearDecay\n" << "Please report this error on https://github.com/CRPropa/CRPropa3/issues including your simulation setup and the following random seed:\n" << Random::instance().getSeed_base64();
     282            0 :                 throw;
     283            0 :         }
     284              : 
     285          699 : }
     286              : 
     287            0 : double NuclearDecay::meanFreePath(int id, double gamma) {
     288            0 :         if (not (isNucleus(id)))
     289              :                 return std::numeric_limits<double>::max();
     290              : 
     291            0 :         int A = massNumber(id);
     292            0 :         int Z = chargeNumber(id);
     293            0 :         int N = A - Z;
     294              : 
     295              :         // check if particle can decay
     296            0 :         const std::vector<DecayMode> &decays = decayTable[Z * 31 + N];
     297            0 :         if (decays.size() == 0)
     298              :                 return std::numeric_limits<double>::max();
     299              : 
     300              :         double totalRate = 0;
     301              : 
     302            0 :         for (size_t i = 0; i < decays.size(); i++) {
     303            0 :                 double rate = decays[i].rate;
     304            0 :                 rate /= gamma;
     305            0 :                 totalRate += rate;
     306              :         }
     307              : 
     308            0 :         return 1. / totalRate;
     309              : }
     310              : 
     311            1 : void NuclearDecay::setInteractionTag(std::string tag) {
     312            1 :         interactionTag = tag;
     313            1 : }
     314              : 
     315            2 : std::string NuclearDecay::getInteractionTag() const {
     316            2 :         return interactionTag;
     317              : }
     318              : 
     319              : } // namespace crpropa
        

Generated by: LCOV version 2.0-1