EGSnrc C++ class library  Report PIRS-898 (2021)
Iwan Kawrakow, Ernesto Mainegra-Hing, Frederic Tessier, Reid Townson and Blake Walters
estar.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 <vector>
40 #include <string>
41 #include <sstream>
42 #include <algorithm>
43 #include <iomanip>
44 #include <fstream>
45 #include <assert.h>
46 #include <cmath>
47 
48 #include "estar.h"
49 #include "estar_dataTables.h"
50 #include "egs_functions.h"
51 
52 #if defined(WIN32) || defined(_WIN32)
53 # ifdef BUILD_DLL
54 # define ESTAR_EXPORT __declspec(dllexport)
55 # else
56 # define ESTAR_EXPORT __declspec(dllimport)
57 # endif
58 #else
59 # define ESTAR_EXPORT
60 #endif
61 
62 /*
63  The purpose of this module is to process the arrays and data received from
64  pegs4_routine.mortran and then to call the main function (estarCalculation)
65  which is given in estarMainCalc.cpp.
66  estarCalculation -> calculates the density correction factors
67 
68  Note that more pre-processing is done for compounds in formula_calculation.cpp
69 */
70 
71 /*
72  This class is used to process the formula array from fortran
73  to make the formula array readable by c++
74 */
75 namespace {
76 class GetElements {
77 public:
78  struct GetElementsStruct {
79  // this array contains all elements present in a medium
80  string elemArrayStrut[100];
81  };
82  void tokenize(std::string const &str, const char delim,
83  std::vector<std::string> &out) {
84  // Construct a stream from the string
85  std::stringstream ss(str);
86 
87  std::string s;
88  while (std::getline(ss, s, delim)) {
89  if (s.empty()) {
90  continue;
91  }
92  out.push_back(s);
93  }
94  }
95  // this function below parses fortran array to produce an array
96  // which can be used in our C++ estar.
97  // This is needed as arrays returned by fortran cannot be read by C++
98  // without this pre-processing.
99  //
100  // Returns true on success, false if the component list is inconsistent
101  // with NEP.
102  void getElemArray(char *formulaStr, int NEP, GetElementsStruct &GElem) {
103  const char delim = ' ';
104  std::vector<std::string> components;
105  tokenize(string(formulaStr), delim, components);
106 
107  if (components.size() < (size_t)NEP) {
108  egsFatal("\nestar::getElemArray: List of elements is inconsistent with the number expected. Expected %d elements but only found %d.\n", NEP, components.size());
109  }
110 
111  int k = 0;
112  while (k < NEP) {
113  if (components[k].size() > 1) {
114  components[k][0] = static_cast<char>(std::toupper(static_cast<unsigned char>(components[k][0])));
115  components[k][1] = static_cast<char>(std::tolower(static_cast<unsigned char>(components[k][1])));
116  }
117  GElem.elemArrayStrut[k] = components[k];
118  k = k + 1;
119  }
120  }
121 };
122 }
123 
124 /*
125  The function below takes inputs from pegs4_routines.mortran and then
126  calculates the density correction factors. The density correction factors are stored in the
127  variable densityCorr. This array is then passed to pegs4_routine where it is read.
128 
129  Now we explain the meaning of the variables:
130 
131  1. formulaStr : is a FORTRAN array containing chemical symbols of the
132  elements present.
133 
134  2. massFraction : is a FORTRAN array containing mass fractions (weights) of the
135  elements present.
136 
137  3. numOfAtoms : is a FORTRAN array containing number atoms of each of the
138  elements present in the material.
139 
140  4. mediaDensity : is the user defined medium density for a particular medium
141 
142  5. densityCorr : is the array containing the density correction factors which are computed
143  by the ESTAR integrated in EGSnrc
144 
145  6. enGrid : is the standard energy grid
146 
147  7. NEP : is the number of elements mentioned in the MEDIA DEFINITION block of
148  the egsinp file.
149  * example: if we have -> elements: H,O,C
150  then NEP will be 3
151 
152  8. iscomp : is a binary variable to denote if the substance is a compound or not.
153  iscomp = 1 means the material is a compound
154  iscomp = 0 means the material is not a compound
155 
156  9. meanIval : is the ivalue of the material. If the user defines ivalue = a in egsinp file,
157  then, meanIval = a. However, if the user does not define ivalue, then meanIval is the
158  ivalue that is computed by ESTAR.
159 
160  10. ipotval : is used to determine if ivalue has been defined by the user. If the user
161  does not define a custom ivalue, then ipotval = -1 and so ESTAR computes
162  the ivalue. However if the user
163  defines ivalue = a in egsinp file, then ipotval = meanIval = a.
164 
165  11. mediaID : This is the id of the medium. This depends on the order in which
166  the media have been listed in the media input block of an egsinp file.
167  * For example: Say we have -
168  :start media input:
169  media = air, water
170  set medium = 1
171  :stop media input:
172  Then, air will have mediaID = 1 and water will have mediaID = 2.
173 
174 */
175 
176 #define egsEstar F77_OBJ_(egs_estar,EGS_ESTAR)
177 
178 extern __extc__ ESTAR_EXPORT int egsEstar(char *formulaStr,
179  float *massFraction,
180  float *numOfAtoms,
181  float *mediaDensity,
182  double *densityCorr,
183  double *enGrid,
184  int *NEP,
185  int *ISCOMP,
186  float *meanIval,
187  float *ipotval,
188  int *mediaID,
189  char *outputFilename
190  ) {
191 
192  // Validate all pointers before dereferencing
193  if (!formulaStr) {
194  egsFatal("estar::egsEstar: formulaStr pointer is null for medium.\n");
195  }
196  if (!massFraction) {
197  egsFatal("estar::egsEstar: massFraction pointer is null.\n");
198  }
199  if (!numOfAtoms) {
200  egsFatal("estar::egsEstar: numOfAtoms pointer is null.\n");
201  }
202  if (!mediaDensity) {
203  egsFatal("estar::egsEstar: mediaDensity pointer is null.\n");
204  }
205  if (!densityCorr) {
206  egsFatal("estar::egsEstar: densityCorr pointer is null.\n");
207  }
208  if (!enGrid) {
209  egsFatal("estar::egsEstar: enGrid pointer is null.\n");
210  }
211  if (!NEP) {
212  egsFatal("estar::egsEstar: NEP pointer is null.\n");
213  }
214  if (!ISCOMP) {
215  egsFatal("estar::egsEstar: ISCOMP pointer is null.\n");
216  }
217  if (!meanIval) {
218  egsFatal("estar::egsEstar: meanIval pointer is null.\n");
219  }
220  if (!ipotval) {
221  egsFatal("estar::egsEstar: ipotval pointer is null.\n");
222  }
223  if (!mediaID) {
224  egsFatal("estar::egsEstar: mediaID pointer is null.\n");
225  }
226  if (!outputFilename) {
227  egsFatal("estar::egsEstar: outputFilename pointer is null.\n");
228  }
229 
230  // Validate the values pointed to
231  if (*NEP <= 0 || *NEP > 100) {
232  egsFatal("estar::egsEstar: NEP=%d is out of valid range [1, 100].\n", *NEP);
233  }
234  if (*mediaID <= 0) {
235  egsFatal("estar::egsEstar: mediaID=%d is invalid, must be > 0.\n", *mediaID);
236  }
237  if (*ISCOMP != 0 && *ISCOMP != 1) {
238  egsFatal("estar::egsEstar: ISCOMP=%d is invalid, must be 0 or 1.\n", *ISCOMP);
239  }
240  if (*mediaDensity <= 0.0f) {
241  egsFatal("estar::egsEstar: mediaDensity=%g is invalid, must be > 0.\n",
242  *mediaDensity);
243  }
244 
245  egsInformation("\n-------------------------\n"
246  "== MEDIUM %d BLOCK FOR ESTAR ==\n", *mediaID);
247 
248  // The lines below process formula_str to make the array readable by estar c++
249  GetElements elemObject;
250  GetElements::GetElementsStruct GeElems;
251  int nepInt = *NEP;
252  int isCompInt = *ISCOMP;
253  int mediaNum = *mediaID; // this is the media id
254 
255  elemObject.getElemArray(formulaStr, nepInt, GeElems);
256 
257  // Use std::vector instead of a VLA (variable-length arrays are a GCC
258  // extension, not valid standard C++14).
259  vector<string> estarFormulaArrayInput(nepInt);
260  vector<double> estarWeightArrayInput(nepInt);
261 
262  int i = 0;
263  while (i < nepInt) {
264  estarFormulaArrayInput[i] = GeElems.elemArrayStrut[i];
265  estarWeightArrayInput[i] = massFraction[i];
266 
267  egsInformation("estar::egsEstar: Formula is %s with fraction %g\n",
268  estarFormulaArrayInput[i].c_str(), estarWeightArrayInput[i]);
269 
270  i = i + 1;
271  }
272 
273  double mediumDensity = *mediaDensity;
274 
275  // Call the main estar calculation.
276  // Check the return value and propagate any error back to the Fortran caller
277  // so failures are not silently swallowed.
278  int result = estarCalculation(isCompInt, nepInt, mediumDensity,
279  estarFormulaArrayInput.data(),
280  estarWeightArrayInput.data(),
281  numOfAtoms, densityCorr, enGrid,
282  meanIval, ipotval, mediaNum,
283  string(outputFilename));
284  if (result != 0) {
285  egsFatal("estar::egsEstar: estarCalculation failed for medium %d with error code %d.\n",
286  mediaNum, result);
287  return result;
288  }
289 
290  egsInformation("-------------------------\n");
291 
292  return 0;
293 }
294 
295 #define egsCompoundsToElements F77_OBJ_(egs_compoundstoelements,EGS_COMPOUNDSTOELEMENTS)
296 
297 extern __extc__ ESTAR_EXPORT int egsCompoundsToElements(char *formulaStr,
298  double *massFraction,
299  float *mediaDensity,
300  char *elementStr,
301  double *rhoz,
302  double *zelem,
303  int *ncomp,
304  int *NEP
305  ) {
306 
307  // Validate all pointers before dereferencing
308  if (!formulaStr) {
309  egsFatal("estar::egsCompoundsToElements: formulaStr pointer is null.\n");
310  }
311  if (!massFraction) {
312  egsFatal("estar::egsCompoundsToElements: massFraction pointer is null.\n");
313  }
314  if (!mediaDensity) {
315  egsFatal("estar::egsCompoundsToElements: mediaDensity pointer is null.\n");
316  }
317  if (!elementStr) {
318  egsFatal("estar::egsCompoundsToElements: elementStr pointer is null.\n");
319  }
320  if (!rhoz) {
321  egsFatal("estar::egsCompoundsToElements: rhoz pointer is null.\n");
322  }
323  if (!zelem) {
324  egsFatal("estar::egsCompoundsToElements: zelem pointer is null.\n");
325  }
326  if (!ncomp) {
327  egsFatal("estar::egsCompoundsToElements: ncomp pointer is null.\n");
328  }
329  if (!NEP) {
330  egsFatal("estar::egsCompoundsToElements: NEP pointer is null.\n");
331  }
332 
333  // Validate the values pointed to
334  if (*ncomp <= 0 || *ncomp > 100) {
335  egsFatal("estar::egsCompoundsToElements: ncomp=%d is out of valid range "
336  "[1, 100].\n", *ncomp);
337  }
338  if (*mediaDensity <= 0) {
339  egsFatal("estar::egsCompoundsToElements: mediaDensity=%f must be > 0.\n", *mediaDensity);
340  }
341 
342  // In egsnrc.macros, the max number of elements per medium is $MXEL=50
343  // And these arrays are 50 characters long, so we can restrict to that size
344  const size_t MAX_ELEMENT_STR_SIZE = 50 * 50; // 50 elements * 50 chars each
345 
346  GetElements elemObject;
347  GetElements::GetElementsStruct GeElems;
348  int numCompounds = *ncomp;
349 
350  elemObject.getElemArray(formulaStr, numCompounds, GeElems);
351 
352  // Use std::vector instead of VLAs (variable-length arrays are a GCC
353  // extension, not valid standard C++14).
354  vector<string> estarFormulaArrayInput(numCompounds);
355  vector<double> estarWeightArrayInput(numCompounds);
356 
357  for (size_t i=0; i < (size_t)numCompounds; ++i) {
358  estarFormulaArrayInput[i] = GeElems.elemArrayStrut[i];
359  estarWeightArrayInput[i] = massFraction[i];
360  }
361 
362  float rho = *mediaDensity;
363  formula_calc fc = mixtureCalculation(rho,
364  estarFormulaArrayInput.data(),
365  estarWeightArrayInput.data(),
366  numCompounds);
367 
368  // Set NEP to actually be the number of elements now, instead of the number of compounds
369  NEP[0] = fc.mmax;
370 
371  // Fortran-side arrays (RHOZ, ZELEM, ASYM) hold at most $MXEL = 50 elements
372  if (fc.mmax > 50) {
373  egsFatal("estar::egsCompoundsToElements: medium has %d distinct elements, "
374  "but EGSnrc supports at most 50 ($MXEL).\n", fc.mmax);
375  }
376 
377  size_t charPos = 0;
378  for (size_t i=0; i != (size_t)fc.mmax; ++i) {
379 
380  // For each Z value we have, look up the element string
381  // They are already sorted by increasing Z
382  for (auto it = atomic_number.begin(); it != atomic_number.end(); ++it) {
383  if (it->second == fc.jz[i]) {
384  if (charPos >= MAX_ELEMENT_STR_SIZE) {
385  egsFatal("estar::egsCompoundsToElements: elementStr buffer overflow at "
386  "charPos=%zu. Buffer may be too small.\n", charPos);
387  }
388 
389  // For each character in the element string
390  for (auto &ch : it->first) {
391  elementStr[charPos++] = ch;
392  }
393 
394  // Pad out to 50 characters with spaces
395  for (auto j=it->first.length(); j < 50; j++) {
396  elementStr[charPos++] = ' ';
397  }
398 
399  break;
400  }
401  }
402 
403  // Set the mass fraction for the element, for the whole mixture
404  rhoz[i] = fc.wt[i];
405  zelem[i] = fc.jz[i];
406  }
407 
408  return 0;
409 }
410 
411 string getFileNameWithoutExtension(const string &s) {
412  char sep = '/';
413 #ifdef _WIN32
414  sep = '\\';
415 #endif
416 
417  size_t i = s.rfind(sep, s.length());
418  if (i != string::npos) {
419  string filename = s.substr(i+1, s.length() - i);
420  size_t lastindex = filename.find_last_of(".");
421  string rawname = filename.substr(0, lastindex);
422  return (rawname);
423  }
424  return ("");
425 }
426 
427 // Output a density correction file
428 void outputDensityFile(float mediaDensity, double *densityCorr, double *enGrid, float *meanIval, formula_calc fc, string outputFilename) {
429  size_t firstSpace = outputFilename.find_first_of(" \n\r\t");
430  if (firstSpace != string::npos) {
431  outputFilename = outputFilename.erase(firstSpace);
432  }
433  if (outputFilename.empty()) {
434  return;
435  }
436 
437  egsInformation("estar::outputDensityFile: Writing density correction file '%s'.\n",
438  outputFilename.c_str());
439 
440  std::ofstream f(outputFilename.c_str());
441 
442  if (!f.is_open()) {
443  egsFatal("estar::outputDensityFile: Could not open output file '%s'.\n",
444  outputFilename.c_str());
445  return;
446  }
447 
448  f << getFileNameWithoutExtension(outputFilename).c_str() << endl;
449  f << setprecision(8);
450  f << "113 " << *meanIval << " " << mediaDensity << " " << fc.mmax << endl;
451 
452  // Output the atomic numbers and mass fractions
453  for (int k=0; k < fc.mmax; ++k) {
454  f << fc.jz[k] << " " << fc.wt[k];
455 
456  // Put a new line every 6 elements, or after the last element
457  if ((k+1) % 6 == 0 || k+1 == fc.mmax) {
458  f << endl;
459  }
460  else {
461  f << " ";
462  }
463  }
464 
465  // Set formatting for the density data
466  f << scientific << showpoint;
467 
468  // Output the energy grid and density effect corrections
469  for (int i = 0; i < 113; i++) {
470  f << setprecision(2);
471  f << enGrid[i];
472  f << setprecision(3);
473  f << "," << densityCorr[i];
474 
475  // Put a new line every 4 values, or after the last value
476  if ((i+1) % 4 == 0 || i+1 == 113) {
477  f << endl;
478  }
479  else {
480  f << " ";
481  }
482  }
483 
484  f.close();
485 }
486 
487 /*
488  This function computes the density correction factors by using the processed input arrays and variables
489  from estarCalc.cpp. The density correction factors are stored in the variable densityCorr.
490 */
491 
492 int estarCalculation(int isCompound, int NEP, float mediaDensity, string *elementArray, double *massFraction,
493  float *numOfAtoms, double *densityCorr, double *enGrid, float *meanIval, float *ipotval, int mediaNum, string outputFilename) {
494  //------------------------------------------------//
495  int knmat;
496 
497  if (isCompound == 1) { // 1 means compound and 0 means not compound
498  knmat = 1;
499  }
500  else if (NEP == 1) {
501  knmat = 0; // substance is an element
502  }
503  else {
504  knmat = 2; // substance is a mixture
505  }
506 
507  // HelperFunctions hf; // Removed: hf was declared but never used (ycut call is commented out)
508 
509  formula_calc fc;
510  double rho;
511  rho = mediaDensity;
512  if (rho <= 0) {
513  egsFatal("estar::estarCalculation: Density must be greater than 0, got %g "
514  "for medium %d.\n", rho, mediaNum);
515  return 9;
516  };
517 
518  // Here we pass the processed data from the .egsinp file
519  // in getDataFromFormulae which computes the ivalue and other relevant quantities
520  fc = getDataFromFormulae(knmat, rho, elementArray, massFraction, numOfAtoms, NEP, mediaNum);
521 
522  if (*ipotval >= 0) {
523  fc.pot = *ipotval;
524 
525  egsInformation("\nestar::estarCalculation: For medium %d I-value (eV) given "
526  "in egsinp file is %g.\n", mediaNum, fc.pot);
527  }
528  else {
529  egsInformation("\nestar::estarCalculation: For medium %d I-value (eV) not "
530  "provided in egsinp file. I-value calculated by ESTAR is %g.\n",
531  mediaNum, fc.pot);
532  }
533  *meanIval = fc.pot;
534 
535  // Removed: int p = 0; was declared here but never used at this scope;
536  // it was shadowed by loop variable p in later for-loops.
537 
538  //------------------------------------------------//
539  int lkmax = 113; // number of elements in the energy grid ->sizeof(er)/sizeof(*er)
540 
541  //================================================//
542  // define q here. Further information about q is given in 2.1 of report.
543  long double qfac = exp(log(10)/50);
544 
545  double qbeg = 1e-04;
546  int lmax = 1101;
547  vector<long double> q(lmax);
548  q[0] = qbeg;
549 
550  /*
551  The q[] array we find below will help us to find an approximate solution of l^2
552  in eqation 2 of Sternheimer 1984.
553  Then using the approximate solution, we can find the exact value of the density factor using
554  equation 1.
555  */
556  for (int i = 1; i < lmax; i++) {
557  q[i] = q[i-1]*qfac;
558  };
559  //================================================//
560 
561 
562  //-----------------------//------------------------------//
563  /*
564  In this snippet we find f (the oscillator strength for each oscillator)
565  and some other parameters
566  */
567  int mmax = fc.mmax; // this is the number of different types of elements present in the compound/mixture
568 
569  // Guard against mmax exceeding the fixed array sizes below.
570  // at[] and g[] are sized 50; if mmax exceeds this we would overflow.
571  if (mmax <= 0 || mmax > 50) {
572  egsFatal("estar::estarCalculation: mmax=%d is out of valid range [1, 50] "
573  "for medium %d.\n", mmax, mediaNum);
574  return 9;
575  }
576 
577  double at[50]; // mass number
578  double g[50]; // This is the weight*atmic_number/atomic_mass
579  double a; // mass number (defined for convenience)
580  int jz; // atomic number
581 
582  // atb is indexed as atb[jz-1]. Determine its size for bounds checking.
583  const int ATB_SIZE = sizeof(atb) / sizeof(atb[0]);
584 
585  for (int i = 0; i < mmax; i++) {
586  jz = fc.jz[i];
587 
588  // Guard against an out-of-bounds index into atb[] before we use it.
589  if (jz <= 0 || jz > ATB_SIZE) {
590  egsFatal("estar::estarCalculation: Atomic number Z=%d at index %d is out of "
591  "valid range [1, %d] for medium %d.\n", jz, i, ATB_SIZE, mediaNum);
592  return 9;
593  }
594 
595  at[i] = atb[jz-1];
596  double z = fc.jz[i];
597  a = at[i];
598 
599  // Guard against divide-by-zero: atomic mass should never be zero for a
600  // real element. A zero here means the atb table entry is missing or corrupt.
601  if (a == 0.0) {
602  egsFatal("estar::estarCalculation: Atomic mass is zero for Z=%d at index %d "
603  "for medium %d. Check atb table entry.\n", jz, i, mediaNum);
604  return 9;
605  }
606 
607  g[i] = fc.wt[i]*(z/a); // This is the weight*atmic_number/atomic_mass
608  }
609 
610  double zav = fc.zav;
611 
612  // Guard against sqrt of non-positive value in hom, and against zav==0
613  // which would also make phil and cbar undefined.
614  if (zav <= 0.0) {
615  egsFatal("estar::estarCalculation: zav=%g is non-positive for medium %d. "
616  "Cannot compute hom.\n", zav, mediaNum);
617  return 9;
618  }
619 
620  const double plasmaFeqCoeff = 28.81593;
621  double hom = plasmaFeqCoeff*sqrt(rho*zav); // this is equation 4 of Sternheimer 1984
622  double phil = 2.0*log(fc.pot/hom); // this is equation 7 of Sternheimer 1984 with a slight modification.
623  // Please refer to the report (2.2) to understand the modification.
624 
625  for (int i = 0; i < mmax; i++) {
626  // Guard against divide-by-zero: zav was already checked above so this
627  // is just a safeguard in case something changed it unexpectedly.
628  if (zav == 0.0) {
629  egsFatal("estar::estarCalculation: zav is zero when normalising g[%d] "
630  "for medium %d.\n", i, mediaNum);
631  return 9;
632  }
633  g[i] = g[i]/zav;
634  };
635 
636  int nbas = 0; // index used to compute the oscillator strength for each oscillator
637  int nmax;
638  int record;
639  double sum;
640 
641  // f[] and en[] are sized 1000. nbas accumulates across elements; guard below.
642  vector<double> f(1000);
643  vector<double> en(1000);
644 
645  // parseData() reads/parses data from disk. Moved outside the loop so it is
646  // only called once rather than once per element.
647  ElementOscillatorData ds = parseData();
648 
649  for (int m = 0; m < mmax; m++) {
650  int iz = fc.jz[m]; // atomic number
651  record = iz-1; // this is the index used for simplicity (atomic number - 1)
652  nmax = ds.nmax[record];
653 
654  if (iz == 6 && mmax==1) { // elemental carbon is dealt differently
655  ds.nc[record][nmax -1] = 1;
656  ds.nc[record][nmax] = -1;
657  ds.bd[record][nmax] = ds.bd[record][nmax-1];
658  nmax = nmax + 1;
659  };
660 
661  if (ds.nc[record][nmax-1] <0) {
662  // This condition is true ONLY when the element is a metallic conductor.
663  // * example: Please look at elementData.h. More information is given in the report (Integration of ESTAR in EGSnrc) on
664  // where to find the nc section. You will see that for metalllic conductors the last number of nc[] is
665  // negative of number of electrons in last subshell. This is how we know the element is a conductor.
666  // Otherwise it is treated as a non-conductor. Once we know the element is a conductor,
667  // we make the negative number positive with the code below
668  ds.nc[record][nmax-1] = - ds.nc[record][nmax-1];
669  if (mmax<=1) {
670  /*
671  It was discussed in Sternheimer 1984 (just below equation 8) that when the substance is a metallic conductor,
672  there is a term ouside the summation in the right hand side of equation 8. Now the code below ensures that we
673  have the correct term outside the summation for metallic conductors.
674  */
675  ds.bd[record][nmax-1] = 0.0;
676  }
677  }
678 
679  int nsum = 0;
680  for (int p = 0; p < nmax; p++) {
681  // This is just the sum of the number of electrons present
682  // and is the same as the atomic number
683  nsum = nsum + ds.nc[record][p];
684  };
685  sum = nsum;
686 
687  // Guard against nbas + nmax overflowing f[] and en[] (both sized 1000).
688  if (nbas + nmax > 1000) {
689  egsFatal("estar::estarCalculation: Total oscillator count nbas+nmax=%d "
690  "exceeds maximum of 1000 at element index %d for medium %d.\n",
691  nbas+nmax, m, mediaNum);
692  return 9;
693  }
694 
695  // Guard against divide-by-zero in f[nn] calculation.
696  if (sum == 0.0) {
697  egsFatal("estar::estarCalculation: Electron sum is zero for element index %d "
698  "(Z=%d) in medium %d. Cannot compute oscillator strengths.\n",
699  m, iz, mediaNum);
700  return 9;
701  }
702 
703  int nn;
704  /*
705  In the loop below, we compute the oscillator strength for each oscillator.
706  Now this calculation is a bit different depending on whether the substance is an element/anything else.
707  When the substance is an element, f is computed using just the formula given in Sternheimer 1984.
708  However when the substance is a compound/mixture, the calculation is a bit different and it is not given in Sternheimer 1984.
709  I could not find this calculation in any other source
710  */
711  for (int n = 0; n < nmax; n++) {
712  nn = n + nbas;
713  f[nn] = ds.nc[record][n]*g[m]/sum;
714  en[nn] = ds.bd[record][n]; // we redefine for convenience
715 
716  };
717  nbas = nbas + nmax;
718  };
719  //-----------------------//------------------------------//
720 
721  nmax = nbas; // nmax is the total number of dispersion oscillators present
722 
723  //--------------------------------------------------------------------//
724  /*
725  The code in this snippet is used to construct some parameters of Sternheimer equation 8.
726  They will also be used to construct some other equations.
727  */
728  vector<double> alf(1000);
729  vector<double> eps(1000);
730  for (int n = 0; n < nmax; n++) {
731  alf[n] = 2.0/3.0; // This is the 2/3 factor in equation 5 Sternheimer 1984
732  };
733 
734  if (en[nmax-1]<=0) { // when en[nmax-1]==0 is true, it means the substance is a conductor.
735  alf[nmax-1] = 1.0; // when it is a conductor, this code ensures there is this
736  // term outside the loop according to equation 8
737  };
738 
739  for (int n = 0; n < nmax; n++) {
740  eps[n] = (en[n]/hom) * (en[n]/hom); // This is square of equation 3 (Sternheimer 1984) without
741  // the (adjustment factor)^2 which we will find
742  };
743  //--------------------------------------------------------------------//
744  // The variables defined above are used to solve equation 8 of Sternheimer 1984
745 
746  //=============================//====================================//
747  /*
748  The code snippet here is used to solve equation 8 of Sternheimer 1984 using
749  Newton's Method to solve for root (adjustment_factor^2 of the paper).
750  Note that the equation constructed here is a bit different from equation 8.
751  The differences are explained in the report (2.1).
752  */
753  double root; //root is the adjustment_factor^2 we want to find
754  double fun; // this stores the function value
755  double der; // der is derivative of fun with respect to root
756  double trm; // this is one term in the function
757  root = 1.0;
758  double droot = 1; // initialization to ensure loop runs at least once
759 
760  const int MAX_NEWTON_ITER = 1000;
761  int newtonIter = 0;
762 
763  while (std::abs(droot)-0.00001 > 0) {
764  fun = -phil;
765  der = 0.0;
766  for (int n = 0; n < nmax; n++) {
767  trm = root*eps[n] + alf[n]*f[n];
768 
769  // Guard against log of non-positive and divide-by-zero in der.
770  if (trm <= 0.0) {
771  egsFatal("estar::estarCalculation: Non-positive trm=%g at oscillator n=%d "
772  "during Newton's method for medium %d. log and division are undefined.\n",
773  trm, n, mediaNum);
774  return 9;
775  }
776 
777  fun = fun +f[n]*log(trm);
778  der = der + f[n]*eps[n]/trm; // der is derivative of fun with respect to root
779  };
780 
781  // Guard against zero derivative (would cause divide-by-zero in droot).
782  if (der == 0.0) {
783  egsFatal("estar::estarCalculation: Zero derivative in Newton's method at "
784  "iteration %d (root=%g) for medium %d. Cannot continue.\n",
785  newtonIter, root, mediaNum);
786  return 9;
787  }
788 
789  droot = fun/der;
790 
791  root = root - droot;
792 
793  if (++newtonIter >= MAX_NEWTON_ITER) {
794  egsFatal("estar::estarCalculation: Newton's method failed to converge after "
795  "%d iterations for medium %d.\n", MAX_NEWTON_ITER, mediaNum);
796  return 9;
797  }
798  };
799  //=============================//====================================//
800 
801  // Guard against sqrt of a negative root (Newton's method could converge
802  // to a negative value if the initial guess is poor or input is bad).
803  if (root < 0.0) {
804  egsFatal("estar::estarCalculation: Newton's method converged to negative "
805  "root=%g for medium %d. Cannot compute adjustment factor.\n",
806  root, mediaNum);
807  return 9;
808  }
809 
810  //double factor = sqrt(root); // this is the adjustment factor
811 
812 
813  for (int n = 0; n < nmax; n++) {
814  eps[n] = root*eps[n]; // after we find the adjustment factor we can write down the square of
815  // equation 3 of Sternheimer 1984 explicitly
816  };
817 
818  // ========================================================== //
819  /*
820  In this snippet we mainly find d, which is equation 1 of Sternheimer 1984
821  with l^2 being replaced by q[n]. However the formulation of d[n] is slightly different
822  from the formulation in the paper. This has been described in detail in section 2.2 of the
823  report. Furthermore, I have discussed about yql and yq in 2.1 of the report.
824  The idea behind using d[n] is discussed at the end of 2.2.
825  */
826  vector<double> yq(lmax);
827  vector<double> yql(lmax);
828  vector<double> d(lmax);
829  double arg;
830 
831  for (int n = 0; n < lmax; n++) {
832  sum = 0.0;
833  for (int m = 0; m < nmax; m++) {
834  double denom = eps[m] + q[n];
835  if (denom == 0.0) {
836  egsFatal("estar::estarCalculation: eps[%d] + q[%d] is zero for medium %d.\n"
837  "Check oscillator energies and q grid.\n", m, n, mediaNum);
838  }
839  sum = sum + f[m]/denom;
840  };
841 
842  // Guard against divide-by-zero when computing yq[n].
843  if (sum == 0.0) {
844  egsFatal("estar::estarCalculation: Zero sum when computing yq[%d] for "
845  "medium %d. Cannot compute 1/sum.\n", n, mediaNum);
846  return 9;
847  }
848 
849  yq[n] = 1/sum;
850 
851  if (yq[n] <= 0.0) {
852  egsFatal("estar::estarCalculation: yq[%d]=%g is non-positive for medium %d. "
853  "Cannot compute log.\n", n, yq[n], mediaNum);
854  }
855 
856  yql[n] = log(yq[n]);
857  sum = 0.0;
858  for (int m = 0; m < nmax; m++) {
859  double denom = eps[m] + alf[m] * f[m];
860 
861  // Guard against divide-by-zero inside the log argument.
862  if (denom == 0.0) {
863  egsFatal("estar::estarCalculation: Zero denominator in arg calculation at "
864  "n=%d, m=%d for medium %d.\n", n, m, mediaNum);
865  return 9;
866  }
867 
868  arg = 1 + q[n]/denom;
869  sum = sum + f[m]*log(arg);
870  };
871  d[n] = sum - q[n]/(yq[n] + 1.0);
872  };
873 
874  // ========================================================== //
875 
876 
877  double rmass = 0.510999906; // this is the rest mass of an electron
878 
879  // we call scof to get a,b,c,d which will be used to find density effect parameters with bspol
880  // please see 2.1 of the report for more details.
881  scof sf2 = fscof(lmax, yql, d);
882 
883  //---------------------------------------------//
884  // The following code is used to obtain
885  // density corrections. Some details on what is happening here is givn in
886  // section 2.1 of the notes.
887  double tau;
888  double y;
889  double delta;
890  double yl;
891  vector<double> dlt(lkmax);
892  double tol = 0.000000001; // Newton's method convergence tolerance for bisection method
893  double xroot;
894  double nb_density; // density factor from bisection method
895  /*
896  solver = 1 := use approximation in estar
897  solver = 2 := use bisection method (section 2.2 of report)
898  */
899  int solver = 2;
900  bspol bp;
901 
902  for (int i = 0; i < lkmax; i++) {
903  tau = energy_grid[i]/rmass;
904  y = tau*(tau+2.0);
905  delta = 0.0;
906  nb_density = 0.0;
907  // y must not exceed yq[lmax-1]
908  // section 2.3 of the report gives more detail about the range error.
909  if (y>=yq[0]) {
910  if (y-yq[lmax-1] <= 0) {
911  yl = log(y);
912  bp = fbspol(yl, yql, sf2.a, sf2.b, sf2.c, sf2.d);
913  if (solver == 1) {
914  delta = bp.density_corr;
915  }
916  else if (solver == 2) {
917  xroot = bisec(q[bp.lb_index], q[bp.ub_index], tol, tau, f, eps, nmax);
918  double yqn = 0;
919  sum = 0.0;
920  for (int m = 0; m < nmax; m++) {
921  double denom = eps[m] + alf[m] * f[m];
922 
923  // Guard against divide-by-zero inside bisection solver arg.
924  if (denom == 0.0) {
925  egsFatal("estar::estarCalculation: Zero denominator in bisection arg at "
926  "energy index %d, m=%d for medium %d.\n", i, m, mediaNum);
927  return 9;
928  }
929 
930  arg = 1 + xroot/denom;
931  sum = sum + f[m]*log(arg);
932  yqn = yqn + f[m]/(eps[m] + xroot);
933  };
934 
935  // Guard against divide-by-zero when inverting yqn.
936  if (yqn == 0.0) {
937  egsFatal("estar::estarCalculation: yqn is zero at energy index %d for "
938  "medium %d. Cannot compute 1/yqn.\n", i, mediaNum);
939  return 9;
940  }
941 
942  yqn = 1/yqn;
943  nb_density = sum - xroot/(yqn + 1.0);
944  delta = nb_density;
945  }
946  else {
947  egsFatal("estar::estarCalculation: Invalid solver option %d for medium %d. "
948  "Must be 1 or 2.\n", solver, mediaNum);
949  return 9;
950  }
951 
952  }
953  else {
954  egsFatal("estar::estarCalculation: Energy energy_grid[%d]=%g is too high and out of "
955  "range for medium %d.\n", i, energy_grid[i], mediaNum);
956  return 9;
957  }
958  }
959  dlt[i] = delta;
960  };
961  //---------------------------------------------//
962 
963 
964  for (int i = 0; i < lkmax; i++) {
965 
966  densityCorr[i] = dlt[i];
967  enGrid[i] = energy_grid[i];
968  }
969 
970  egsInformation("\nestar::estarCalculation: Density correction factors have been "
971  "calculated by ESTAR for medium %d.\n", mediaNum);
972 
973  // output a density correction file
974  outputDensityFile(mediaDensity, densityCorr, enGrid, meanIval, fc, outputFilename);
975 
976  return 0;
977 
978 };
979 
980 bspol fbspol(double s, const std::vector<double> &x, const std::vector<double> &a,
981  const std::vector<double> &b, const std::vector<double> &c,
982  const std::vector<double> &d) {
983  int n = static_cast<int>(x.size());
984 
985  bspol bp;
986  int idir;
987  int mlb;
988  int mub;
989  int mu;
990  int ml;
991  int mav;
992  double k;
993  double g;
994 
995  if (n < 2) {
996  egsFatal("estar::fbspol: n=%d is too small, need at least 2 points.\n", n);
997  }
998 
999  if (x[0] <= x[n-1]) {
1000  idir =0;
1001  mlb =0;
1002  mub = n;
1003  }
1004  else {
1005  idir =1;
1006  mlb =n;
1007  mub = 0;
1008  }
1009 
1010  if (s > x[mub+idir-1]) {
1011  egsWarning("estar::fbspol: s=%g is beyond the upper grid boundary %g. "
1012  "Extrapolating using end spline segment — result may be unreliable.\n",
1013  s, x[mub+idir-1]);
1014  mu = mub + 2*idir - 1;
1015  }
1016  else if (s < x[mlb+1-idir-1]) {
1017  egsWarning("estar::fbspol: s=%g is below the lower grid boundary %g. "
1018  "Extrapolating using end spline segment — result may be unreliable.\n",
1019  s, x[mlb+1-idir-1]);
1020  mu = mlb - 2*idir + 1;
1021  }
1022  else {
1023  // s is within the grid — standard binary search
1024  ml = mlb;
1025  mu = mub;
1026  do {
1027  mav = (ml + mu) / 2;
1028  if (s < x[mav]) {
1029  mu = mav;
1030  }
1031  else {
1032  ml = mav;
1033  }
1034  }
1035  while (std::abs(mu - ml) > 1);
1036  mu = mu + idir - 1;
1037  }
1038  mu = mu + 1;
1039 
1040  // an exact hit on the top grid point lands past the last segment via
1041  // either branch above; clamp so ub_index stays a valid index
1042  if (mu > n - 1) {
1043  mu = n - 1;
1044  }
1045 
1046  // s lies between x[mu-1] and x[mu]
1047  k = s - x[mu-1];
1048  /*
1049  g gives you the density correction factor
1050  by using the variables from scof - a,b,c,d
1051  */
1052  g = ((d[mu-1]*k+c[mu-1])*k+b[mu-1])*k+a[mu-1];
1053 
1054  // putting in the structure
1055  bp.lb_index = mu-1;
1056  bp.ub_index = mu;
1057  bp.density_corr = g;
1058  return bp;
1059 
1060 }
1061 
1062 scof fscof(int nmax, const vector<double> &x, const vector<double> &f) {
1063 
1064  if (nmax < 2) {
1065  egsFatal("estar::fscof: fscof requires at least 2 points, got nmax=%d\n", nmax);
1066  }
1067 
1068  scof sf(nmax);
1069 
1070  int m2 = nmax-1;
1071  double s = 0.0;
1072  double r;
1073  for (int m = 0; m < m2; m++) {
1074  sf.d[m] = x[m+1] - x[m];
1075  if (sf.d[m] == 0.0) {
1076  egsFatal("estar::fscof: Zero interval at m=%d. "
1077  "Knot positions x[%d] and x[%d] may be identical.\n", m, m, m+1);
1078  }
1079  r = (f[m+1] - f[m])/sf.d[m];
1080  sf.c[m] = r - s;
1081  s = r;
1082  };
1083 
1084  s = 0.0;
1085  r = 0.0;
1086  sf.c[0] = 0.0;
1087  sf.c[nmax-1] = 0.0;
1088 
1089  for (int m = 1; m < m2; m++) {
1090  sf.c[m] = sf.c[m] + r*sf.c[m-1];
1091  sf.b[m] = (x[m-1] - x[m+1])*2 - r*s;
1092  if (sf.b[m] == 0.0) {
1093  egsFatal("estar::fscof: Zero pivot sf.b[%d] in spline solve. "
1094  "Knot data may be degenerate.\n", m);
1095  }
1096 
1097  s = sf.d[m];
1098  r = s/sf.b[m];
1099  };
1100 
1101  int mr = m2 - 1;
1102  for (int m = 1; m < m2; m++) {
1103  sf.c[mr] = (sf.d[mr] * sf.c[mr+1] - sf.c[mr])/sf.b[mr];
1104  mr = mr - 1;
1105  };
1106 
1107  for (int m = 0; m < m2; m++) {
1108  s = sf.d[m];
1109  r = sf.c[m+1] - sf.c[m];
1110  sf.d[m] = r/s;
1111  sf.c[m] = sf.c[m]*3.0;
1112  sf.b[m] = (f[m+1]-f[m])/s - (sf.c[m]+r)*s;
1113  sf.a[m] = f[m];
1114  };
1115 
1116  return sf;
1117 }
1118 
1119 double objective_function(double tau, const vector<double> &f,
1120  const vector<double> &eps, int nmax, double x) {
1121  double arg = tau * (tau + 2.0);
1122  if (arg <= 0.0) {
1123  egsFatal("estar::objective_function: tau*(tau+2)=%g is non-positive "
1124  "for tau=%g. Cannot compute log.\n", arg, tau);
1125  }
1126  double yl = log(arg);
1127 
1128  double yql = 0;
1129  for (int i = 0; i < nmax; i++) {
1130  double denom = eps[i] + x;
1131  if (denom == 0.0) {
1132  egsFatal("estar::objective_function: eps[%d]=%g and x=%g sum to "
1133  "zero. Cannot divide. Check oscillator energies and "
1134  "bisection bounds.\n", i, eps[i], x);
1135  }
1136  yql = yql + f[i]/denom;
1137  }
1138 
1139  // Guard against divide-by-zero before log
1140  if (yql <= 0.0) {
1141  egsFatal("estar::objective_function: yql is non-positive at x=%g. "
1142  "Cannot compute log.\n", x);
1143  }
1144  return yl - log(1.0/yql);
1145 }
1146 
1147 // now we have to write the bisection algorithm
1148 double bisec(double lowerbound, double upperbound, double tolerance,
1149  double tau, const vector<double> &f, const vector<double> &eps,
1150  int nmax) {
1151  if (tolerance <= 0) {
1152  egsFatal("estar::bisec: bisection tolerance must be positive, got %g\n", tolerance);
1153  }
1154  if (nmax > static_cast<int>(f.size()) || nmax > static_cast<int>(eps.size())) {
1155  egsFatal("estar::bisec: nmax=%d exceeds vector size.\n", nmax);
1156  }
1157 
1158  double fLower = objective_function(tau, f, eps, nmax, lowerbound);
1159  double fUpper = objective_function(tau, f, eps, nmax, upperbound);
1160 
1161  if ((fLower > 0) == (fUpper > 0)) {
1162  egsFatal("estar::bisec: Bounds [%g, %g] do not bracket a root for tau=%g.\n",
1163  lowerbound, upperbound, tau);
1164  return lowerbound; // signal failure; caller should check
1165  }
1166 
1167  double x_mid = lowerbound;
1168  double del = 1.0;
1169  while (del > tolerance) {
1170  x_mid = (lowerbound + upperbound) / 2.0;
1171  double fMid = objective_function(tau, f, eps, nmax, x_mid);
1172  if ((fMid > 0) == (fLower > 0)) {
1173  lowerbound = x_mid;
1174  fLower = fMid;
1175  }
1176  else {
1177  upperbound = x_mid;
1178  }
1179  del = std::abs(fMid);
1180  }
1181  return x_mid;
1182 }
Global egspp functions header file.
Core ESTAR density correction calculation interface.
bspol fbspol(double s, const std::vector< double > &x, const std::vector< double > &a, const std::vector< double > &b, const std::vector< double > &c, const std::vector< double > &d)
Evaluate the cubic spline interpolant at a given point.
Definition: estar.cpp:980
double objective_function(double tau, const vector< double > &f, const vector< double > &eps, int nmax, double x)
Objective function for the bisection solver.
Definition: estar.cpp:1119
scof fscof(int nmax, const vector< double > &x, const vector< double > &f)
Compute natural cubic spline coefficients.
Definition: estar.cpp:1062
int estarCalculation(int isCompound, int NEP, float mediaDensity, std::string *elementArray, double *massFraction, float *numOfAtoms, double *densityCorr, double *enGrid, float *meanIval, float *ipotval, int mediaNum, std::string outputFilename)
Compute density correction factors for a medium.
double bisec(double lowerbound, double upperbound, double tolerance, double tau, const std::vector< double > &f, const std::vector< double > &eps, int nmax)
Find the root of objective_function() by bisection.
void outputDensityFile(float mediaDensity, double *densityCorr, double *enGrid, float *meanIval, formula_calc fc, std::string outputFilename)
Output a density correction 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.
EGS_InfoFunction EGS_EXPORT egsWarning
Always use this function for reporting warnings.
Holds the dispersion oscillator data for all 100 elements.
int nc[100][26]
Number of electrons in each subshell for each element.
double bd[100][26]
Absorption edge energies (in units of hbar) for each oscillator.
int nmax[100]
Number of dispersion oscillators for each element.
Result struct returned by fbspol().
Definition: estar.h:144
double density_corr
Definition: estar.h:145
int lb_index
Definition: estar.h:147
int ub_index
Definition: estar.h:149
Spline coefficient struct returned by fscof().
Definition: estar.h:189
std::vector< double > d
Definition: estar.h:193
std::vector< double > a
Definition: estar.h:190
std::vector< double > c
Definition: estar.h:192
std::vector< double > b
Definition: estar.h:191