G4BraggIonModel delta-ray production suppressed for low energy alphas in v11 onwards due to cutEnergy

Geant4 Version: v11.3.2 (v11 onwards)
Operating System: Linux Alma9
Compiler/Version: g++ (GCC) 14.2.0
CMake Version: cmake version 3.30.6


Hi,

I’m hoping to get some help with changes to the production of secondaries in v11 of geant4.

The changes in BraggModel from v10 to v11 mean that the production of delta rays is suppressed for alphas below ~2.0 MeV, leading to a two orders of magnitude reduction in the number of secondary electrons produced compared to v10 (specifically comparing to geant4-10-07).

I’m using QGSP_BIC_HP and G4EMPenelopePhysics in my physics lists.

This seems to come from changes to G4BraggModel::ComputeCrossSectionPerElectron which calculates the energy cut as const G4double cutEnergy = std::max(cut, lowestKinEnergy*massRate), where massRate is the ratio of the alpha to proton mass (3.9726), scaling up the lowest kinetic energy and giving a cut floor of 397eV, rather than the defined production cut. This means below ~1.8 MeV no discrete delta-rays are produced by the alpha track.

This can be reproduced in a basic simulation of an alpha in a gaseous nitrogen target with:

void RunAction::BeginOfRunAction(const G4Run* run) {
   
   G4EmCalculator calc; 
   G4Material* mat = G4Material::GetMaterial("N2"); 

   G4cout << "MFP of delta rays produced by alphas in N2" << G4endl;
   std::vector<G4double> energies = { 0.1*MeV, 0.5*MeV, 1.0*MeV, 2.0*MeV, 5.0*MeV};

   for (G4double e : energies) { 
     G4double mfp = calc.GetMeanFreePath(e, "alpha", "ionIoni", mat->GetName());
     G4cout << " E = " <<e/MeV << " MeV MFP = " << mfp/mm << " mm" << G4endl; 
   }
}

This returns:

E = 0.1 MeV MFP = 1.79769e+308 mm
E = 0.5 MeV MFP = 1.79769e+308 mm
E = 1 MeV MFP = 1.79769e+308 mm
E = 2 MeV MFP = 0.354217 mm
E = 5 MeV MFP = 0.116996 mm

Where low energy alphas produce delta rays with infinite mean free path.

I traced this back to the energy cut by adding this in my SteppingAction:

void SteppingAction::UserSteppingAction(const G4Step* step) {
  G4Track *track = step->GetTrack();
  static G4bool printed = false;
  if (!printed && track->GetParticleDefinition()->GetParticleName() == "alpha") {
    printed = true;
    G4ProcessManager* pm = G4Alpha::Alpha()->GetProcessManager();
    for (G4int i = 0; i < pm->GetProcessList()->size(); i++) {
      G4VProcess* proc = (*pm->GetProcessList())[i];
      if (proc->GetProcessName() == "ionIoni") {
        G4VEnergyLossProcess* ioni = dynamic_cast<G4VEnergyLossProcess*>(proc);
        G4BraggIonModel* bim = dynamic_cast<G4BraggIonModel*>(ioni->GetModelByIndex(0));
        if (bim) {
          G4Material* mat = track->GetMaterial();
          G4double KE = 1.0*MeV;

          G4double mass     = G4Alpha::Alpha()->GetPDGMass();
          G4double ratio    = CLHEP::electron_mass_c2 / mass;
          G4double q          = G4Alpha::Alpha()->GetPDGCharge()/CLHEP::eplus;
          G4double chargeSq   = q * q;
          G4double lowestKE   = bim->LowEnergyLimit();
          G4double massRate = mass / CLHEP::proton_mass_c2;

          G4double tau        = KE / mass;
          G4double tmax       = 2.0*CLHEP::electron_mass_c2*tau*(tau + 2.0) /
            (1.0 + 2.0*(tau + 1.0)*ratio + ratio*ratio);
          G4double cutEnergy  = std::max(1*eV, lowestKE * massRate);
          G4double maxEnergy  = std::min(tmax, 1*MeV);

          G4double beta2      = KE*(KE + 2.0*mass) / ((KE+mass)*(KE+mass));

          G4cout << "----- ComputeCrossSectionPerElectron -----" << G4endl;
          G4cout << "mass = " << mass/MeV << " MeV" << G4endl;
          G4cout << "ratio = " << ratio << G4endl;
          G4cout << "chargeSquare = " << chargeSq << G4endl;
          G4cout << "massRate = " << massRate << G4endl;
          G4cout << "lowestKE = " << lowestKE/MeV << " MeV" << G4endl;
          G4cout << "tau = " << tau << G4endl;
          G4cout << "tmax = " << tmax/eV << " eV"  << G4endl;
          G4cout << "cutEnergy = " << cutEnergy/eV << " eV"  << G4endl;
          G4cout << "maxEnergy = " << maxEnergy/eV << " eV"  << G4endl;
          G4cout << "beta2 = " << beta2 << G4endl;

          if (cutEnergy < maxEnergy) {
            G4double cross = (maxEnergy - cutEnergy)/(cutEnergy*maxEnergy)
              - beta2*std::log(maxEnergy/cutEnergy)/tmax;
            cross *= CLHEP::twopi_mc2_rcl2 * chargeSq / beta2;
            G4cout << "  xsec (manual) = " << cross/cm2 << " cm2" << G4endl;
          }
       }
   }
}

which returns:

mass = 3727.38 MeV
ratio = 0.000137093
chargeSquare = 4
massRate = 3.9726
lowestKE = 0.0001 MeV
tau = 0.000268285
tmax = 548.297 eV
cutEnergy = 397.26 eV
maxEnergy = 548.297 eV
beta2 = 0.000536354
xsec (manual) = 1.31785e-18 cm2

where the energy cut is 397.26 eV rather than the 20 eV I set in my PhysicsList. Calling G4double xsec = bim->ComputeCrossSectionPerAtom(G4Alpha::Alpha(), 1*MeV, 7.0, 0.0, 1*eV, 1*MeV); directly also gives 0 cross-section below 2 MeV.

This is particularly significant in gaseous detector simulations where I need to accurately count the number of secondary electrons produced. Is there something I should be calling differently to track the electrons produced by my low energy alphas?

Thanks!

Lex

Dear Lex,

thanks a lot for your report and for your analysis. I went back to the history of G4BraggModel.cc and apparently the line that you mention was introduced on 2024-11-08, as a result of a bug fix by @civanch : “fixed problem of 4.12 MeV mu+ range reported in the Forum #12312”, i.e. this post. I just made a quick check and looked for the first occurrence of the line you mentioned, but didn’t verify if this line changed the previous logic. I will give a closer look. If you have the chance to have multiple Geant4 versions installed, it would be great to confirm that this is (or it is not) the source of the change.

Ciao,

Luciano

Hello again,

thanks to @dkonst, the origin of the change was located to five years ago

27 August 21: V.Ivanchenko  (emstand-V10-07-21)

G4BraggModel, G4BraggIonModel - set lowest energy to 0.25 keV,
added protection to limit cut value by the lowest energy

and it is definitely intended. I guess we need the help by @civanch to reconstruct the origin and the motivation of this change. Consistently, the restricted cross section should be zero in the cases in which the maximum tranferrable energy is lower than the effective cut: this is what you actually observed. I am only puzzled by the calculation of cutEnergy in your SteppingAction: according to the code, the limit should be aboutlowestKinEnergy * massRate`and hence 250*3.97 = 1 keV, not 397 eV.

The 20 eV theshold that you would intend to put is not feasible in the current model and always capped to about 1 keV for He. The energy of the electron is released as a local energy deposit. Yet, if you just care about the number of electrons produced in the gas (and you don’t need to track them individually), you can just use the total energy deposit AlongStep (i.e. by all sub-threshold electrons) and divide it by some average energy required for single ionization.

Ciao,

Luciano

Hi Luciano and Lex,

Thank you for this discussion, I’m actually running into the exact same problem except for a completely different application. I’ll summarize my findings and I’d like some advice on what are the correct settings to use.

Initially my simulations were showing the 1 keV cut-off for secondaries generated by alphas mentioned by Luciano (lowestKinEnergy * massRate). This was causing a sharp drop-off at 1 keV in the secondary electron spectrum (top plot)

After digging into the source code, I found the lowestKinEnergy parameter and modified it to 2.5 eV just to see what would happen. Instead of the spectrum smoothly going down to 10 eV, I actually saw that the drop-off happened at around ~300 eV (for Gallium) and about 130 eV (for Silicon). I believe there is a second limit at around the mean excitation energy (middle plot).

To remove this lower energy limit, I modified the xmin parameter in SampleSecondaries to allow it to generate down to 10 eV, which produces the following plot (bottom plot).

Sorry, I’ve stacked all the plots into one screenshot.

However, what I think I’m essentially doing is just changing the sampling function range down to 10 eV which is just redistributing the secondary electrons to lower energy. The total number of secondaries stays roughly the same (which I’m not sure if this is physically correct or not). The xmin parameter also exists in ComputeCrossSectionPerElectron and ComputeDEDXPerVolume (as the cutEnergy parameter), however, I’m hesitant to modify those since it looks like it changes the actual energy loss and cross-sections. Any advice or comments would be appreciated!

Thanks,
Brian

Hi @bxyzhu

thanks a lot for the follow-up. Indeed, I believe that the xmin should be changed consistently in ComputeCrossSectionPerElectron and ComputeDEDXPerVolume, otherwise there is going to be a double-counting (or an underestimate) of the total energy loss. If you shift secondaries to lower energy, I expect (physically) that there should be more of them produced. I guess that in the current case you are just changing the sampling function of secondaries, but without affecting the restricted cross section (and this the reason why you get approximately the same number of secondaries, with a globally lower energy loss).

Ciao, thanks for the discussion

Luciano