EGSnrc C++ class library  Report PIRS-898 (2021)
Iwan Kawrakow, Ernesto Mainegra-Hing, Frederic Tessier, Reid Townson and Blake Walters
estar_formulaCalcs.cpp
1 /*
2 ###############################################################################
3 #
4 # EGSnrc estar
5 # Copyright (C) 2026 National Research Council Canada
6 #
7 # This file is part of EGSnrc.
8 #
9 # EGSnrc is free software: you can redistribute it and/or modify it under
10 # the terms of the GNU Affero General Public License as published by the
11 # Free Software Foundation, either version 3 of the License, or (at your
12 # option) any later version.
13 #
14 # EGSnrc is distributed in the hope that it will be useful, but WITHOUT ANY
15 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
17 # more details.
18 #
19 # You should have received a copy of the GNU Affero General Public License
20 # along with EGSnrc. If not, see <http://www.gnu.org/licenses/>.
21 #
22 ###############################################################################
23 #
24 # Author: Sehmimul Hoque, 2022
25 #
26 # Contributors: Martin J. Berger
27 # Johnathan S. Coursey
28 # Reid Townson
29 # Ernesto Mainegra-Hing
30 #
31 # Based on the original ESTAR code by Martin J. Berger,
32 # National Institute of Standards and Technology (NIST). Including
33 # modifications by Johnathan S. Coursey.
34 #
35 ###############################################################################
36 */
37 
38 #include <iostream>
39 #include <iomanip>
40 #include <vector>
41 #include <math.h>
42 #include "estar_formulaCalcs.h"
43 #include "estar_dataParser.h"
44 #include "estar_dataTables.h"
45 #include "egs_functions.h"
46 
47 // The objective of this module is to determine whether to call
48 // fcalc() or mixtureCalculation()
49 
50 namespace {
51 /*
52  The class contains information on pre-processing compound formula
53 */
54 class compFormulaPreprocess {
55 public:
56  struct RestructureCompound {
57  int finalNumOfElems; // the number of different elements present in the compound
58  string finalElemArray[100]; // array containing the elements present
59  float finalNumAtoms[100]; // array containing the number of atoms of each element present
60  };
61 
62  // Here I am constructing a function which takes in the elemArray and massFraction arrays
63  // and returns a string with the whole formula of the compound.
64  // for example if inputElemArray = {H,H,O} and inputNumAtomArray = {2,2,1},
65  // then the function returns an object of RestructureCompound where:
66  // * finalElemArray = {H,O}
67  // * finalNumAtoms = {2,1}
68  // * finalNumOfElems = 2
69  RestructureCompound compRes(string *inputElemArray, float *inputNumAtomArray, int NEP) {
70  // NEP is the length of inputElemArray and inputNumAtomArray
71  int j = 0;
72  int perTableLength = 100; // we work with elements from atomic number 1-100
73  int elemPresent[perTableLength]; // elemPresent[i-1] is set to 1 if element with Z=i is
74  // present at least once in the compound
75  float numAtomsArray[perTableLength]; // numAtomsArray[i-1] stores the number of atoms of Z=i
76  // present in the compound
77  string tempElemArray[perTableLength];
78  while (j < perTableLength) {
79  // initialize elemPresent to contain zeros
80  elemPresent[j] = 0;
81  j = j + 1;
82  }
83  // Now get the corresponding atomic number array
84  vector<int> atomicNumArray(NEP);
85  int i = 0;
86  while (i < NEP) {
87  auto it = atomic_number.find(inputElemArray[i]);
88  if (it == atomic_number.end()) {
89  egsFatal("estar::compRes: Unrecognised element symbol '%s' at index %d.\n"
90  "Check the formula input.\n", inputElemArray[i].c_str(), i);
91  }
92  atomicNumArray[i] = it->second;
93 
94  i = i + 1;
95  }
96  i = 0;
97  int zIndex; // zIndex = atomic number -1
98  int numDiffAtoms = 0;
99  while (i < NEP) {
100  zIndex = atomicNumArray[i] - 1; // define this for simplicity
101 
102  if (zIndex < 0 || zIndex >= perTableLength) {
103  egsFatal("estar::compRes: Atomic number index %d is out of bounds [0, %d).\n"
104  "Element '%s' may not be in the periodic table.\n",
105  zIndex, perTableLength, inputElemArray[i].c_str());
106  }
107 
108  if (elemPresent[zIndex] == 0) {
109  elemPresent[zIndex] = 1;
110  numAtomsArray[zIndex] = inputNumAtomArray[i];
111  tempElemArray[zIndex] = inputElemArray[i]; // we use tempElemArray for simplicity
112  numDiffAtoms = numDiffAtoms + 1;
113  }
114  else {
115  // elemPresent[zIndex] is 1
116  numAtomsArray[zIndex] = numAtomsArray[zIndex] + inputNumAtomArray[i];
117  }
118  i = i + 1;
119  }
120  int k = 0;
121  RestructureCompound compForm;
122  compForm.finalNumOfElems = numDiffAtoms;
123  for (int m=0; m < perTableLength; ++m) {
124  if (elemPresent[m] == 1) {
125  compForm.finalElemArray[k] = tempElemArray[m];
126  compForm.finalNumAtoms[k] = numAtomsArray[m];
127  k = k + 1;
128  }
129  }
130  return compForm;
131  }
132 
133  // This function produces the chemical formula of a compound from the
134  // elementrray and numofAtoms array
135  string getCompFormula(string *elementArray, float *numOfAtoms, int NEP, int mediaNum) {
136  vector<int> numberOfAtoms(NEP);
137  vector<string> numberOfAtomsStr(NEP);
138  string compoundFormula = "";
139  for (int i=0; i < NEP; ++i) {
140  numberOfAtoms[i] = static_cast<int>(numOfAtoms[i]); // convert float to int
141  numberOfAtomsStr[i] = to_string(numberOfAtoms[i]); // convert int to string
142  }
143 
144  // now we produce the final string
145  for (int i=0; i < NEP; ++i) {
146  compoundFormula = compoundFormula + elementArray[i] + numberOfAtomsStr[i];
147  }
148 
149  egsInformation("\nestar::getCompFormula: Medium %d is a compound of %d elements with formula: %s\n", mediaNum, NEP, compoundFormula.c_str());
150 
151  return compoundFormula;
152  }
153 };
154 }
155 
156 
157 /*
158  This is a simple function which runs either fcalc or mixtureCalculation
159  depending on whether the substance is a compound/element or whether it is a mixture respectively
160 */
161 formula_calc getDataFromFormulae(int knmat, double rho, string *elementArray, double *massFraction, float *numOfAtoms, int NEP, int mediaNum) {
162  formula_calc fc;
163  string formula;
164  string formulaCompound;
165  if (knmat == 0) { // Element
166  formula = elementArray[0];
167  fc = fcalc(knmat, rho, formula);
168 
169  egsInformation("\nestar::getDataFromFormulae: Medium %d treated as element.\n", mediaNum);
170 
171  return fc;
172  }
173  else if (knmat == 1) { // Compound
174  compFormulaPreprocess compObject; // Pre-processing needed only if material is a compound
175  compFormulaPreprocess::RestructureCompound rc = compObject.compRes(elementArray, numOfAtoms, NEP);
176  string compFormula = compObject.getCompFormula(rc.finalElemArray, rc.finalNumAtoms, rc.finalNumOfElems, mediaNum);
177  fc = fcalc(knmat, rho, compFormula);
178 
179  egsInformation("\nestar::getDataFromFormulae: Medium %d treated as compound.\n", mediaNum);
180 
181  return fc;
182  }
183  else { // Mixture
184  fc = mixtureCalculation(rho, elementArray, massFraction, NEP);
185 
186  egsInformation("\nestar::getDataFromFormulae: Medium %d treated as mixture.\n", mediaNum);
187 
188  return fc;
189  }
190 }
191 
192 /*
193  The purpose of this module is to get a formula_calc object
194  containing relevant data for a single chemical formula like:
195  Na, Cl, NaCl2, H2O etc.
196  For mixtures please refer to mixformula.cpp
197 */
198 
199 // The function takes in the element name as a string and simply returns
200 // the atomic number by using the atomic_number dictionary
201 int atom_num(string elem_name) {
202  auto it = atomic_number.find(elem_name);
203  if (it == atomic_number.end()) {
204  egsFatal("estar::atom_num: Unrecognised element symbol '%s'.\n"
205  "Check the formula input, they must be characters not integers.\n", elem_name.c_str());
206  }
207  return it->second;
208 }
209 
210 // formula_calc is a structure we defined in the module formulaStruct.cpp.
211 // The input of fcalc are :
212 // * knmat := type of material (0-> element; 1->compound; 2:->mixture)
213 // * rho := density of material
214 // * elemName := is the name of the element/compound and is a string
215 // The return output is a structure formula_calc which contains some
216 // parameters (including i-value) which will be used to find the density corrections
217 // The parameters inside the function below.
218 formula_calc fcalc(int knmat, double rho, string elemName) {
219  int numElemsPerTable = 100; // as 100 elements are present in our periodic table
220  formula_calc fc; // we define fc to be an object of struct formula_calc
221  parseformula pf = parse(elemName);
222 
223  // mmax is the number of different types of elements present in the substance.
224  // for example if elemName == H2O, mmax will be 2.
225  // if elemName == H2OMgClH, mmax will be 4.
226  int mmax = pf.elem_types;
227 
228  if (mmax > numElemsPerTable) {
229  egsFatal("estar::fcalc: Parsed element type count %d exceeds maximum of %d.\n"
230  "Formula '%s' may be malformed.\n",
231  mmax, numElemsPerTable, elemName.c_str());
232  }
233 
234  int atomic_number_element;
235  fc.mmax = pf.elem_types;
236  double nz[numElemsPerTable]; // initialize array with numElemsPerTable elements
237  int i = 0;
238  while (i < mmax) {
239  fc.jz[i] = atom_num(pf.str_arr[i]); // for each element we get the atomic number
240  atomic_number_element = fc.jz[i];
241 
242  /* below we have nz[i] = pf.num_arr[i]. Now pf.num_arr[i] produces
243  the number of each atom present in the element/compound.
244  * For example: for H20, nz[0] will be 2 while nz[1] will be 1.
245  * For example: for Cl2, nz[0] will be 2.
246  */
247  nz[i] = pf.num_arr[i];
248  i = i + 1;
249  }
250 
251  double asum = 0.0;
252  int jm;
253  /* After the while loop below runs, we get asum.
254  The final asum we get (at end of while loop) is the:
255  sum of ATOMIC_MASS_OF_ELEMENT_i * NUMBER_OF_ATOMS_WITH_ATOMIC_NUMBER_i
256  * for example, for H2O,
257  asum = 1.007940 * 2 + 32.0660 * 1
258  */
259  int m = 0;
260  while (m < mmax) {
261  jm = fc.jz[m];
262  asum = asum+atb[jm-1]*nz[m];
263  m = m + 1;
264  }
265 
266  if (asum == 0.0) {
267  egsFatal("estar::fcalc: Total atomic mass sum is zero for formula '%s'.\n"
268  "Check that atom counts are non-zero.\n", elemName.c_str());
269  }
270 
271  /* After the while loop below runs, we get fc.wt.
272  The final fc.wt we get (at end of while loop) is the:
273  normalized sum of ATOMIC_MASS_OF_ELEMENT_i * NUMBER_OF_ATOMS_WITH_ATOMIC_NUMBER_i
274  * for example, for H2O,
275  fc.wt[0] = (1.007940 * 2)/(1.007940 * 2 + 32.0660 * 1)
276  fc.wt[1] = (32.0660 * 1)/(1.007940 * 2 + 32.0660 * 1)
277  Thus fc.wt gives the weight by mass of each element present in the compound/element
278  */
279  m = 0;
280  while (m < mmax) {
281  jm = fc.jz[m];
282  fc.wt[m] = atb[jm-1]*(nz[m]/asum);
283  m = m + 1;
284  }
285 
286  fc.zav = 0.0;
287  double potl = 0.0;
288  double potm = 0.0;
289  double za;
290 
291  // g/cm^3 threshold below which a material is treated as a gas for I-value selection
292  const double rhocut = 0.1;
293 
294  m = 0;
295 
296  while (m < mmax) {
297  jm = fc.jz[m];
298 
299  za = fc.jz[m]/atb[jm-1]; // ratio of atomic number to atomic mass
300  fc.zav = fc.zav + fc.wt[m]*za; // This Z/A is the same as the Z/A in equation 4 (Sternheimer 1948)
301  // You can simple replace fc.wt[m] and za with their definitions
302  // to arrive at the formula:
303  // fc.zav = (total number of electrons)/(sum of atomic weights of constituent atoms)
304  // as given by Sternheimer 1948 just below equation 4.
305 
306  if (knmat >= 1) { // This ensures only elements do not get boosted
307  if (jm>=10) {
308  // ---
309  // This code snippet contains modifications made by Ernesto
310  if (mmax>1) {
311  potm = 1.13*poth[jm-1]; // The 1.13 factor arises from the 'Others' condition in ICRU 37 - table 5.1
312  }
313  else {
314  potm = poth[jm-1];
315  }
316  // ---
317  }
318  else {
319  if (rho <= rhocut) { // using <= gives correct output in ESTAR. However < was used in ESTAR
320  // Please see Rhocut Error section in my report
321  // Now when rho <= rhocut, the code assumes the material is in gaseous form
322  // and thus uses potgas.
323  potm = potgas[jm-1];
324  }
325  else {
326  potm = potcon[jm-1];// Now when rho > rhocut, the code assumes the material is in solid/liquid form
327  // and thus uses potcon.
328  }
329  }
330  }
331  else {
332  potm = poth[jm-1];
333  }
334 
335  if (potm <= 0.0) {
336  egsFatal("estar::fcalc: I-value potm=%g is non-positive for Z=%d.\n"
337  "Cannot take log. Check poth/potgas/potcon tables.\n", potm, jm);
338  }
339 
340  potl = potl + fc.wt[m]*za*log(potm); // This equation represents equation 5.3 of ICRU 37.
341  // fc.wt[m] is w[m] and za is Z[i]/A[i] of ICRU 37
342  m = m + 1;
343  }
344  // fc.pot is the I-Value
345  if (fc.zav == 0.0) {
346  egsFatal("estar::fcalc: Mean Z/A (zav) is zero for formula '%s'.\n"
347  "Cannot compute I-value.\n", elemName.c_str());
348  }
349  fc.pot = exp(potl/fc.zav); // we remove the log in equation 5.3 (ICRU 37) and divide by <Z/a> TO GET THE I-value
350  // Note that fc.zav in the code is is <Z/a> of ICRU 37 equation 5.3
351  return fc;
352 };
353 
354 // This processes the input data and puts them in a mixtureData structure object
355 mixtureData getEgsMediaData(string *elementArray, double *massFraction, int NEP) {
356  mixtureData md;
357  md.ncomp = NEP;
358  int ncomp = NEP;
359  if (ncomp <= 0) {
360  egsFatal("estar::getEgsMediaData: Number of components must be > 0, got %d.\n", ncomp);
361  }
362  for (int i=0; i < ncomp; ++i) {
363  md.frm[i] = elementArray[i];
364  }
365 
366  double sumf = 0;
367  for (int i=0; i < ncomp; ++i) {
368  if (massFraction[i] <= 0) {
369  egsFatal("estar::getEgsMediaData: Mass fraction for component %d is %g.\n"
370  "Mass fractions must be > 0.\n", i, massFraction[i]);
371  }
372  md.frac[i] = massFraction[i];
373  sumf = sumf + md.frac[i];
374  }
375  // normalize
376  for (int i=0; i < ncomp; ++i) {
377  md.frac[i] = md.frac[i]/sumf;
378  }
379  return md;
380 }
381 
382 // Here we find the I-value of the whole mixture
383 // we return a formula_calc structure object where
384 // the object has the relevant properties of the mixture which are:
385 // 1. I-value of the mixture
386 // 2. fractional weight of each element used
387 // 3. atomic number of the different elements present
388 formula_calc mixtureCalculation(double rho, string *elementArray, double *massFraction, int NEP) {
389 
390  mixtureData md = getEgsMediaData(elementArray, massFraction, NEP);
391  int numComp = md.ncomp;
392  vector<string> formulaArray(numComp); // array containing all the formula
393  vector<double> fractionArray(numComp); // array contaning all the weights
394  for (int i = 0; i < numComp; i++) {
395  formulaArray[i] = md.frm[i];
396  fractionArray[i] = md.frac[i];
397  };
398  int num_elems = 100; // we work with elements from atomic number 1-100
399  bool lh[num_elems];
400  double wate[num_elems];
401  for (int j=0; j < num_elems; ++j) {
402  // lh and wate to contain zeros
403  lh[j] = 0;
404  wate[j] = 0.0;
405  }
406  int atmoicNumIndex;
407  vector<double> zavArray(numComp); // array containing Z/A of each component
408  vector<double> potArray(numComp); // array containing I-Value of each component
409 
410  for (int i = 0; i < numComp; i++) {
411  formula_calc fc; // this object is redefined for every different formula used in the mixture
412  fc = fcalc(2, rho, formulaArray[i]);
413  for (int j = 0; j < fc.mmax; j++) {
414  atmoicNumIndex = fc.jz[j] - 1; // define this for simplicity
415  /*
416  lh is used to denote whether a particular element was not encountered before
417  or whether it was part of some other compounds used in the mixture
418  */
419  if (lh[atmoicNumIndex] == 0) {
420  lh[atmoicNumIndex] = 1;
421  wate[atmoicNumIndex] = md.frac[i]*fc.wt[j]; // if we encounter a new element, we do this
422  }
423  else {
424  wate[atmoicNumIndex] = wate[atmoicNumIndex] + md.frac[i]*fc.wt[j]; // if we encounter
425  // an element which was part of a previous compound we add
426  // the previous weight to md.frac[i]*fc.wt[j]
427  }
428  }
429  zavArray[i] = fc.zav;
430  potArray[i] = fc.pot;
431 
432  }
433  formula_calc ffc; // this object contains the final data we want to return
434  int index = 0;
435  for (int k = 0; k < num_elems; k++) {
436  if (lh[k]) {
437  /*
438  if lh[k] == 1, this means the element with atomic number k+1 is
439  part of some compound of the mixture or is in elemental form in the mixture
440  */
441  // here we just put the atomic numbers and weights of the elements present in the mixture in
442  // arrays.
443  ffc.jz[index] = k+1;
444  ffc.wt[index] = wate[k];
445 
446  index = index + 1;
447  }
448  }
449 
450  int numDiffElemsUsed = index; // this is the number of differents used.
451  // * example: if a mixture is made with NaCl and H2O, numDiffElemsUsed will be 4
452  ffc.mmax = numDiffElemsUsed;
453  ffc.zav = 0.0;
454  double potl = 0.0;
455  for (int i = 0; i < numComp; i++) {
456  ffc.zav = ffc.zav + fractionArray[i]*zavArray[i]; // --------------------------(i)
457  potl = potl+fractionArray[i]*zavArray[i]*log(potArray[i]); // -----------------(ii)
458  }
459  if (ffc.zav == 0.0) {
460  egsFatal("estar::mixtureCalculation: Mixture mean Z/A is zero.\n"
461  "Check that mass fractions and element Z/A values are non-zero.\n");
462  }
463  ffc.pot = exp(potl/ffc.zav); // --------------------------------------------------(iii)
464  // equations i,ii and iii are used to find the I-value of the mixture from equation 5.3 of ICRU 37
465  return ffc;
466 }
467 
Global egspp functions header file.
EGS_InfoFunction EGS_EXPORT egsInformation
Always use this function for reporting the progress of a simulation and any other type of information...
EGS_InfoFunction EGS_EXPORT egsFatal
Always use this function for reporting fatal errors.
Holds the parsed representation of a chemical formula.