Analysis Software
Documentation for sPHENIX simulation software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
nucleus.cxx
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file nucleus.cxx
1 // TRENTO: Reduced Thickness Event-by-event Nuclear Topology
2 // Copyright 2015 Jonah E. Bernhard, J. Scott Moreland
3 // TRENTO3D: Three-dimensional extension of TRENTO by Weiyao Ke
4 // MIT License
5 
6 #include "nucleus.h"
7 
8 #include <algorithm>
9 #include <cmath>
10 #include <memory>
11 #include <stdexcept>
12 #include <string>
13 #include <utility>
14 
15 #include <boost/math/constants/constants.hpp>
16 #ifdef TRENTO_HDF5
17 // include multi_array for use with ManualNucleus
18 #ifdef NDEBUG
19 #define BOOST_DISABLE_ASSERTS
20 #endif
21 #include <boost/multi_array.hpp>
22 #endif
23 
24 #include "hdf5_utils.h"
25 #include "random.h"
26 
27 namespace trento {
28 
29 double correct_a(double a, double w) {
30  constexpr auto c = 0.61; // correction coefficient
31  constexpr auto a_min = 0.01; // min. value (prevent div. by zero, etc.)
32  return std::sqrt(std::fmax(a*a - c*c*w*w, a_min*a_min));
33 }
34 
35 NucleusPtr Nucleus::create(const std::string& species, double nucleon_width, double nucleon_dmin) {
36  // W-S params ref. in header
37  // XXX: remember to add new species to the help output in main() and the readme
38  if (species == "p")
39  return NucleusPtr{new Proton{}};
40  else if (species == "d")
41  return NucleusPtr{new Deuteron{}};
42  else if (species == "Cu")
43  return NucleusPtr{new WoodsSaxonNucleus{
44  63, 4.20, 0.596, nucleon_dmin
45  }};
46  else if (species == "Cu2")
48  63, 4.20, 0.596, 0.162, -0.006, nucleon_dmin
49  }};
50  else if (species == "Xe")
51  return NucleusPtr{new WoodsSaxonNucleus{
52  129, 5.36, 0.590, nucleon_dmin
53  }};
54  else if (species == "Au")
55  return NucleusPtr{new WoodsSaxonNucleus{
56  197, 6.38, 0.535, nucleon_dmin
57  }};
58  else if (species == "Au2")
60  197, 6.38, 0.535, -0.131, -0.031, nucleon_dmin
61  }};
62  else if (species == "Pb")
63  return NucleusPtr{new WoodsSaxonNucleus{
64  208, 6.62, 0.546, nucleon_dmin
65  }};
66  else if (species == "U")
68  238, 6.81, 0.600, 0.280, 0.093, nucleon_dmin
69  }};
70  else if (species == "U2")
72  238, 6.86, 0.420, 0.265, 0.000, nucleon_dmin
73  }};
74  else if (species == "U3")
76  238, 6.67, 0.440, 0.280, 0.093, nucleon_dmin
77  }};
78  // Read nuclear configurations from HDF5.
79  else if (hdf5::filename_is_hdf5(species)) {
80 #ifdef TRENTO_HDF5
81  return ManualNucleus::create(species);
82 #else
83  throw std::invalid_argument{"HDF5 output was not compiled"};
84 #endif // TRENTO_HDF5
85  }
86  else
87  throw std::invalid_argument{"unknown projectile species: " + species};
88 }
89 
90 Nucleus::Nucleus(std::size_t A) : nucleons_(A), offset_(0) {}
91 
93  offset_ = offset;
95 }
96 
98  iterator nucleon, double x, double y, double z) {
99  nucleon->set_position(x + offset_, y, z);
100 }
101 
103 
105 double Proton::radius() const {
106  return 0.;
107 }
108 
111  set_nucleon_position(begin(), 0., 0., 0.);
112 }
113 
114 // Without loss of generality, let the internal a_ parameter be the minimum of
115 // the given (a, b) and the internal b_ be the maximum.
116 Deuteron::Deuteron(double a, double b)
117  : Nucleus(2),
118  a_(std::fmin(a, b)),
119  b_(std::fmax(a, b))
120 {}
121 
122 double Deuteron::radius() const {
123  // The quantile function for the exponential distribution exp(-2*a*r) is
124  // -log(1-q)/(2a). Return the 99% quantile.
125  return -std::log(.01)/(2*a_);
126 }
127 
129  // Sample the inter-nucleon radius using rejection sampling with an envelope
130  // function. The Hulthén wavefunction including the r^2 Jacobian expands to
131  // three exponential terms: exp(-2*a*r) + exp(-2*b*r) - 2*exp(-(a+b)*r).
132  // This does not have a closed-form inverse CDF, however we can easily sample
133  // exponential numbers from the term that falls off the slowest, i.e.
134  // exp(-2*min(a,b)*r). In the ctor initializer list the "a" parameter is
135  // always set to the minimum, so we should sample from exp(-2*a*r).
136  double r, prob;
137  do {
138  // Sample a uniform random number, u = exp(-2*a*r).
139  auto u = random::canonical<double>();
140  // Invert to find the actual radius.
141  r = -std::log(u) / (2*a_);
142  // The acceptance probability is now the radial wavefunction over the
143  // envelope function, both evaluated at the proposal radius r.
144  // Conveniently, the envelope evaluated at r is just the uniform random
145  // number u.
146  prob = std::pow(std::exp(-a_*r) - std::exp(-b_*r), 2) / u;
147  } while (prob < random::canonical<double>());
148 
149  // Now sample spherical rotation angles.
150  auto cos_theta = random::cos_theta<double>();
151  auto phi = random::phi<double>();
152 
153  // And compute the Cartesian coordinates of one nucleon.
154  auto r_sin_theta = r * std::sqrt(1. - cos_theta*cos_theta);
155  auto x = r_sin_theta * std::cos(phi);
156  auto y = r_sin_theta * std::sin(phi);
157  auto z = r * cos_theta;
158 
159  // Place the first nucleon at the sampled coordinates.
161  // Place the second nucleon opposite to the first.
163 }
164 
165 MinDistNucleus::MinDistNucleus(std::size_t A, double dmin)
166  : Nucleus(A),
167  dminsq_(dmin*dmin)
168 {}
169 
171  if (dminsq_ < 1e-10)
172  return false;
173  for (const_iterator nucleon2 = begin(); nucleon2 != nucleon; ++nucleon2) {
174  auto dx = nucleon->x() - nucleon2->x();
175  auto dy = nucleon->y() - nucleon2->y();
176  auto dz = nucleon->z() - nucleon2->z();
177  if (dx*dx + dy*dy + dz*dz < dminsq_)
178  return true;
179  }
180  return false;
181 }
182 
183 // Extend the W-S dist out to R + 10a; for typical values of (R, a), the
184 // probability of sampling a nucleon beyond this radius is O(10^-5).
186  std::size_t A, double R, double a, double dmin)
187  : MinDistNucleus(A, dmin),
188  R_(R),
189  a_(a),
190  woods_saxon_dist_(1000, 0., R + 10.*a,
191  [R, a](double r) { return r*r/(1.+std::exp((r-R)/a)); })
192 {}
193 
198 double WoodsSaxonNucleus::radius() const {
199  return R_ + 3.*a_;
200 }
201 
204  // When placing nucleons with a minimum distance criterion, resample spherical
205  // angles until the nucleon is not too close to a previously sampled nucleon,
206  // but do not resample radius -- this could modify the Woods-Saxon dist.
207 
208  // Because of the r^2 Jacobian, there is less available space at smaller
209  // radii. Therefore, pre-sample all radii first, sort them, and then place
210  // nucleons starting with the smallest radius and working outwards. This
211  // dramatically reduces the chance that a nucleon cannot be placed.
212  std::vector<double> radii(size());
213  for (auto&& r : radii)
215  std::sort(radii.begin(), radii.end());
216 
217  // Place each nucleon at a pre-sampled radius.
218  auto r_iter = radii.cbegin();
219  for (iterator nucleon = begin(); nucleon != end(); ++nucleon) {
220  // Get radius and advance iterator.
221  auto& r = *r_iter++;
222 
223  // Sample angles until the minimum distance criterion is satisfied.
224  auto ntries = 0;
225  do {
226  // Sample isotropic spherical angles.
227  auto cos_theta = random::cos_theta<double>();
228  auto phi = random::phi<double>();
229 
230  // Convert to Cartesian coordinates.
231  auto r_sin_theta = r * std::sqrt(1. - cos_theta*cos_theta);
232  auto x = r_sin_theta * std::cos(phi);
233  auto y = r_sin_theta * std::sin(phi);
234  auto z = r * cos_theta;
235 
236  set_nucleon_position(nucleon, x, y, z);
237 
238  // Retry sampling a reasonable number of times. If a nucleon cannot be
239  // placed, give up and leave it at its last sampled position. Some
240  // approximate numbers for Pb nuclei:
241  //
242  // dmin = 0.5 fm, < 0.001% of nucleons cannot be placed
243  // 1.0 fm, ~0.005%
244  // 1.5 fm, ~0.1%
245  // 1.73 fm, ~1%
246  } while (++ntries < 1000 && is_too_close(nucleon));
247  }
248  // XXX: re-center nucleon positions?
249 }
250 
251 // Set rmax like the non-deformed case (R + 10a), but for the maximum
252 // "effective" radius. The numerical coefficients for beta2 and beta4 are the
253 // approximate values of Y20 and Y40 at theta = 0.
255  std::size_t A, double R, double a, double beta2, double beta4, double dmin)
256  : MinDistNucleus(A, dmin),
257  R_(R),
258  a_(a),
259  beta2_(beta2),
260  beta4_(beta4),
261  rmax_(R*(1. + .63*std::fabs(beta2) + .85*std::fabs(beta4)) + 10.*a)
262 {}
263 
269  return rmax_ - 7.*a_;
270 }
271 
273  double r, double cos_theta) const {
274  auto cos_theta_sq = cos_theta*cos_theta;
275 
276  // spherical harmonics
277  using math::double_constants::one_div_root_pi;
278  auto Y20 = std::sqrt(5)/4. * one_div_root_pi * (3.*cos_theta_sq - 1.);
279  auto Y40 = 3./16. * one_div_root_pi *
280  (35.*cos_theta_sq*cos_theta_sq - 30.*cos_theta_sq + 3.);
281 
282  // "effective" radius
283  auto Reff = R_ * (1. + beta2_*Y20 + beta4_*Y40);
284 
285  return 1. / (1. + std::exp((r - Reff) / a_));
286 }
287 
290  // The deformed W-S distribution is defined so the symmetry axis is aligned
291  // with the Z axis, so e.g. the long axis of uranium coincides with Z.
292  //
293  // After sampling positions, they must be randomly rotated. In general this
294  // requires three Euler rotations, but in this case we only need two
295  // because there is no use in rotating about the nuclear symmetry axis.
296  //
297  // The two rotations are:
298  // - a polar "tilt", i.e. rotation about the X axis
299  // - an azimuthal "spin", i.e. rotation about the original Z axis
300 
301  // "tilt" angle
302  const auto cos_a = random::cos_theta<double>();
303  const auto sin_a = std::sqrt(1. - cos_a*cos_a);
304 
305  // "spin" angle
306  const auto angle_b = random::phi<double>();
307  const auto cos_b = std::cos(angle_b);
308  const auto sin_b = std::sin(angle_b);
309 
310  // Pre-sample and sort (r, cos_theta) points from the deformed W-S dist.
311  // See comments in WoodsSaxonNucleus (above) for rationale.
312  struct Sample {
313  double r, cos_theta;
314  };
315 
316  std::vector<Sample> samples(size());
317 
318  for (auto&& sample : samples) {
319  // Sample (r, cos_theta) using a standard rejection method.
320  // Remember to include the phase-space factors.
321  do {
322  sample.r = rmax_ * std::cbrt(random::canonical<double>());
323  sample.cos_theta = random::cos_theta<double>();
324  } while (
325  random::canonical<double>() >
326  deformed_woods_saxon_dist(sample.r, sample.cos_theta)
327  );
328  }
329 
330  // Sort by radius. Could also sort by e.g. the perpendicular distance from
331  // the z-axis, or by descending W-S density. Empirically, radius leads to the
332  // smallest failure rate.
333  std::sort(
334  samples.begin(), samples.end(),
335  [](const Sample& a, const Sample& b) {
336  return a.r < b.r;
337  }
338  );
339 
340  // Place each nucleon at a pre-sampled (r, cos_theta).
341  auto sample = samples.cbegin();
342  for (iterator nucleon = begin(); nucleon != end(); ++nucleon, ++sample) {
343  auto& r = sample->r;
344  auto& cos_theta = sample->cos_theta;
345 
346  auto r_sin_theta = r * std::sqrt(1. - cos_theta*cos_theta);
347  auto z = r * cos_theta;
348 
349  // Sample azimuthal angle until the minimum distance criterion is satisfied.
350  auto ntries = 0;
351  do {
352  // Choose azimuthal angle.
353  auto phi = random::phi<double>();
354 
355  // Convert to Cartesian coordinates.
356  auto x = r_sin_theta * std::cos(phi);
357  auto y = r_sin_theta * std::sin(phi);
358 
359  // Rotate.
360  // The rotation formula was derived by composing the "tilt" and "spin"
361  // rotations described above.
362  auto x_rot = x*cos_b - y*cos_a*sin_b + z*sin_a*sin_b;
363  auto y_rot = x*sin_b + y*cos_a*cos_b - z*sin_a*cos_b;
364  auto z_rot = y*sin_a + z*cos_a;
365 
366  set_nucleon_position(nucleon, x_rot, y_rot, z_rot);
367 
368  // In addition to resampling phi, flip the z-coordinate each time. This
369  // works because the deformed WS dist is symmetric in z. Effectively
370  // doubles the available space for the nucleon.
371  z *= -1;
372 
373  // Retry a reasonable number of times. Unfortunately the failure rate is
374  // worse than non-deformed sampling because there is less freedom to place
375  // each nucleon. Some approximate numbers for U nuclei:
376  //
377  // dmin = 0.5 fm, < 0.001% of nucleons cannot be placed
378  // 1.0 fm, ~0.03%
379  // 1.3 fm, ~0.3%
380  // 1.5 fm, ~1.2%
381  } while (++ntries < 1000 && is_too_close(nucleon));
382  }
383 }
384 
385 #ifdef TRENTO_HDF5
386 
387 namespace {
388 
389 // Read a slice of an HDF5 dataset into a boost::multi_array.
390 template <typename T, std::size_t FileDims, std::size_t MemDims>
391 boost::multi_array<T, MemDims>
392 read_dataset(
393  const H5::DataSet& dataset,
394  const std::array<hsize_t, FileDims>& count,
395  const std::array<hsize_t, FileDims>& start,
396  const std::array<hsize_t, MemDims>& shape) {
397  boost::multi_array<T, MemDims> array{shape};
398  auto filespace = dataset.getSpace();
399  filespace.selectHyperslab(H5S_SELECT_SET, count.data(), start.data());
400  auto memspace = hdf5::make_dataspace(shape);
401 
402  dataset.read(array.data(), hdf5::type<T>(), memspace, filespace);
403 
404  return array;
405 }
406 
407 } // unnamed namespace
408 
409 std::unique_ptr<ManualNucleus> ManualNucleus::create(const std::string& path) {
410  auto file = hdf5::try_open_file(path);
411 
412  // Check that there is a single dataset in the file.
413  // Might relax this constraint in the future.
414  if (file.getNumObjs() != 1)
415  throw std::invalid_argument{
416  "file '" + path + "' must contain exactly one object"
417  };
418 
419  auto name = file.getObjnameByIdx(0);
420 #if H5_VERSION_GE(1, 8, 13)
421  if (file.childObjType(name) != H5O_TYPE_DATASET) // added v1.8.13
422 #else
423  if (file.getObjTypeByIdx(0) != H5G_DATASET) // deprecated fall back
424 #endif
425  throw std::invalid_argument{
426  "object '" + name + "' in file '" + path + "' is not a dataset"
427  };
428 
429  // Make dataset object in a unique_ptr for eventual passing to ctor.
430  auto dataset = std::unique_ptr<H5::DataSet>{
431  new H5::DataSet{file.openDataSet(name)}
432  };
433 
434  // Verify that the dataset has the correct dimensionality and shape.
435  std::array<hsize_t, 3> shape;
436  auto ndim = dataset->getSpace().getSimpleExtentDims(shape.data());
437 
438  if (ndim != 3)
439  throw std::invalid_argument{
440  "dataset '" + name + "' in file '" + path + "' has " +
441  std::to_string(ndim) + " dimensions (need 3)"
442  };
443 
444  if (shape[2] != 3)
445  throw std::invalid_argument{
446  "dataset '" + name + "' in file '" + path + "' has " +
447  std::to_string(shape[2]) + " columns (need 3)"
448  };
449 
450  // Deduce number of configs and number of nucleons (A) from the shape.
451  const auto& nconfigs = shape[0];
452  const auto& A = shape[1];
453 
454  // Estimate the max radius from at least 500 nucleon positions.
455  auto n = std::min(500/A + 1, nconfigs);
456  std::array<hsize_t, 3> count = {n, A, 3};
457  std::array<hsize_t, 3> start = {0, 0, 0};
458  std::array<hsize_t, 2> shape_n = {n*A, 3};
459  auto positions = read_dataset<float>(*dataset, count, start, shape_n);
460 
461  auto rmax_sq = 0.;
462 
463  for (const auto& position : positions) {
464  auto& x = position[0];
465  auto& y = position[1];
466  auto& z = position[2];
467  auto r_sq = x*x + y*y + z*z;
468  if (r_sq > rmax_sq)
469  rmax_sq = r_sq;
470  }
471 
472  auto rmax = std::sqrt(rmax_sq);
473 
474  return std::unique_ptr<ManualNucleus>{
475  new ManualNucleus{std::move(dataset), nconfigs, A, rmax}
476  };
477 }
478 
479 ManualNucleus::ManualNucleus(std::unique_ptr<H5::DataSet> dataset,
480  std::size_t nconfigs, std::size_t A, double rmax)
481  : Nucleus(A),
482  dataset_(std::move(dataset)),
483  rmax_(rmax),
484  index_dist_(0, nconfigs - 1)
485 {}
486 
487 ManualNucleus::~ManualNucleus() = default;
488 
489 double ManualNucleus::radius() const {
490  return rmax_;
491 }
492 
493 void ManualNucleus::sample_nucleons_impl() {
494  // Sample Euler rotation angles.
495  // First is an azimuthal spin about the Z axis.
496  const auto angle_1 = random::phi<double>();
497  const auto c1 = std::cos(angle_1);
498  const auto s1 = std::sin(angle_1);
499  // Then a polar tilt about the original X axis, uniform in cos(theta).
500  const auto c2 = random::cos_theta<double>();
501  const auto s2 = std::sqrt(1. - c2*c2);
502  // Finally another azimuthal spin about the original Z axis.
503  const auto angle_3 = random::phi<double>();
504  const auto c3 = std::cos(angle_3);
505  const auto s3 = std::sin(angle_3);
506 
507  // Choose and read a random config from the dataset.
508  std::array<hsize_t, 3> count = {1, size(), 3};
509  std::array<hsize_t, 3> start = {index_dist_(random::engine), 0, 0};
510  std::array<hsize_t, 2> shape = {size(), 3};
511  const auto positions = read_dataset<float>(*dataset_, count, start, shape);
512 
513  // Loop over positions and nucleons.
514  auto positions_iter = positions.begin();
515  for (iterator nucleon = begin(); nucleon != end(); ++nucleon) {
516  // Extract position vector and increment iterator.
517  auto position = *positions_iter++;
518  auto& x = position[0];
519  auto& y = position[1];
520  auto& z = position[2];
521 
522  // Rotate.
523  auto x_rot = x*(c1*c3 - c2*s1*s3) - y*(c3*s1 + c1*c2*s3) + z*s2*s3;
524  auto y_rot = x*(c1*s3 + c2*c3*s1) - y*(s1*s3 - c1*c2*c3) - z*c3*s2;
525  auto z_rot = x*s1*s2 + y*c1*s2 + z*c2;
526 
527  set_nucleon_position(nucleon, x_rot, y_rot, z_rot);
528  }
529 }
530 
531 #endif // TRENTO_HDF5
532 
533 } // namespace trento