Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2025-08-05 08:19:07

0001 // TRENTO: Reduced Thickness Event-by-event Nuclear Topology
0002 // Copyright 2015 Jonah E. Bernhard, J. Scott Moreland
0003 // TRENTO3D: Three-dimensional extension of TRENTO by Weiyao Ke
0004 // MIT License
0005 
0006 #include "nucleus.h"
0007 
0008 #include <algorithm>
0009 #include <cmath>
0010 #include <memory>
0011 #include <stdexcept>
0012 #include <string>
0013 #include <utility>
0014 
0015 #include <boost/math/constants/constants.hpp>
0016 #ifdef TRENTO_HDF5
0017 // include multi_array for use with ManualNucleus
0018 #ifdef NDEBUG
0019 #define BOOST_DISABLE_ASSERTS
0020 #endif
0021 #include <boost/multi_array.hpp>
0022 #endif
0023 
0024 #include "hdf5_utils.h"
0025 #include "random.h"
0026 
0027 namespace trento {
0028 
0029 double correct_a(double a, double w) {
0030    constexpr auto c = 0.61;  // correction coefficient
0031    constexpr auto a_min = 0.01;  // min. value (prevent div. by zero, etc.)
0032    return std::sqrt(std::fmax(a*a - c*c*w*w, a_min*a_min));
0033 }
0034 
0035 NucleusPtr Nucleus::create(const std::string& species, double nucleon_width, double nucleon_dmin) {
0036   // W-S params ref. in header
0037   // XXX: remember to add new species to the help output in main() and the readme
0038   if (species == "p")
0039     return NucleusPtr{new Proton{}};
0040   else if (species == "d")
0041     return NucleusPtr{new Deuteron{}};
0042   else if (species == "Cu")
0043     return NucleusPtr{new WoodsSaxonNucleus{
0044        63, 4.20, 0.596, nucleon_dmin
0045     }};
0046   else if (species == "Cu2")
0047     return NucleusPtr{new DeformedWoodsSaxonNucleus{
0048        63, 4.20, 0.596, 0.162, -0.006, nucleon_dmin
0049     }};
0050   else if (species == "Xe")
0051     return NucleusPtr{new WoodsSaxonNucleus{
0052       129, 5.36, 0.590, nucleon_dmin
0053     }};
0054   else if (species == "Au")
0055     return NucleusPtr{new WoodsSaxonNucleus{
0056       197, 6.38, 0.535, nucleon_dmin
0057     }};
0058   else if (species == "Au2")
0059     return NucleusPtr{new DeformedWoodsSaxonNucleus{
0060       197, 6.38, 0.535, -0.131, -0.031, nucleon_dmin
0061     }};
0062   else if (species == "Pb")
0063     return NucleusPtr{new WoodsSaxonNucleus{
0064       208, 6.62, 0.546, nucleon_dmin
0065     }};
0066   else if (species == "U")
0067     return NucleusPtr{new DeformedWoodsSaxonNucleus{
0068       238, 6.81, 0.600, 0.280, 0.093, nucleon_dmin
0069     }};
0070   else if (species == "U2")
0071     return NucleusPtr{new DeformedWoodsSaxonNucleus{
0072       238, 6.86, 0.420, 0.265, 0.000, nucleon_dmin
0073     }};
0074   else if (species == "U3")
0075     return NucleusPtr{new DeformedWoodsSaxonNucleus{
0076       238, 6.67, 0.440, 0.280, 0.093, nucleon_dmin
0077     }};
0078   // Read nuclear configurations from HDF5.
0079   else if (hdf5::filename_is_hdf5(species)) {
0080 #ifdef TRENTO_HDF5
0081     return ManualNucleus::create(species);
0082 #else
0083     throw std::invalid_argument{"HDF5 output was not compiled"};
0084 #endif  // TRENTO_HDF5
0085   }
0086   else
0087     throw std::invalid_argument{"unknown projectile species: " + species};
0088 }
0089 
0090 Nucleus::Nucleus(std::size_t A) : nucleons_(A), offset_(0) {}
0091 
0092 void Nucleus::sample_nucleons(double offset) {
0093   offset_ = offset;
0094   sample_nucleons_impl();
0095 }
0096 
0097 void Nucleus::set_nucleon_position(
0098     iterator nucleon, double x, double y, double z) {
0099   nucleon->set_position(x + offset_, y, z);
0100 }
0101 
0102 Proton::Proton() : Nucleus(1) {}
0103 
0104 /// Always zero.
0105 double Proton::radius() const {
0106   return 0.;
0107 }
0108 
0109 /// Always place the nucleon at the origin.
0110 void Proton::sample_nucleons_impl() {
0111   set_nucleon_position(begin(), 0., 0., 0.);
0112 }
0113 
0114 // Without loss of generality, let the internal a_ parameter be the minimum of
0115 // the given (a, b) and the internal b_ be the maximum.
0116 Deuteron::Deuteron(double a, double b)
0117     : Nucleus(2),
0118       a_(std::fmin(a, b)),
0119       b_(std::fmax(a, b))
0120 {}
0121 
0122 double Deuteron::radius() const {
0123   // The quantile function for the exponential distribution exp(-2*a*r) is
0124   // -log(1-q)/(2a).  Return the 99% quantile.
0125   return -std::log(.01)/(2*a_);
0126 }
0127 
0128 void Deuteron::sample_nucleons_impl() {
0129   // Sample the inter-nucleon radius using rejection sampling with an envelope
0130   // function.  The Hulthén wavefunction including the r^2 Jacobian expands to
0131   // three exponential terms:  exp(-2*a*r) + exp(-2*b*r) - 2*exp(-(a+b)*r).
0132   // This does not have a closed-form inverse CDF, however we can easily sample
0133   // exponential numbers from the term that falls off the slowest, i.e.
0134   // exp(-2*min(a,b)*r).  In the ctor initializer list the "a" parameter is
0135   // always set to the minimum, so we should sample from exp(-2*a*r).
0136   double r, prob;
0137   do {
0138     // Sample a uniform random number, u = exp(-2*a*r).
0139     auto u = random::canonical<double>();
0140     // Invert to find the actual radius.
0141     r = -std::log(u) / (2*a_);
0142     // The acceptance probability is now the radial wavefunction over the
0143     // envelope function, both evaluated at the proposal radius r.
0144     // Conveniently, the envelope evaluated at r is just the uniform random
0145     // number u.
0146     prob = std::pow(std::exp(-a_*r) - std::exp(-b_*r), 2) / u;
0147   } while (prob < random::canonical<double>());
0148 
0149   // Now sample spherical rotation angles.
0150   auto cos_theta = random::cos_theta<double>();
0151   auto phi = random::phi<double>();
0152 
0153   // And compute the Cartesian coordinates of one nucleon.
0154   auto r_sin_theta = r * std::sqrt(1. - cos_theta*cos_theta);
0155   auto x = r_sin_theta * std::cos(phi);
0156   auto y = r_sin_theta * std::sin(phi);
0157   auto z = r * cos_theta;
0158 
0159   // Place the first nucleon at the sampled coordinates.
0160   set_nucleon_position(begin(), x, y, z);
0161   // Place the second nucleon opposite to the first.
0162   set_nucleon_position(std::next(begin()), -x, -y, -z);
0163 }
0164 
0165 MinDistNucleus::MinDistNucleus(std::size_t A, double dmin)
0166     : Nucleus(A),
0167       dminsq_(dmin*dmin)
0168 {}
0169 
0170 bool MinDistNucleus::is_too_close(const_iterator nucleon) const {
0171   if (dminsq_ < 1e-10)
0172     return false;
0173   for (const_iterator nucleon2 = begin(); nucleon2 != nucleon; ++nucleon2) {
0174     auto dx = nucleon->x() - nucleon2->x();
0175     auto dy = nucleon->y() - nucleon2->y();
0176     auto dz = nucleon->z() - nucleon2->z();
0177     if (dx*dx + dy*dy + dz*dz < dminsq_)
0178       return true;
0179   }
0180   return false;
0181 }
0182 
0183 // Extend the W-S dist out to R + 10a; for typical values of (R, a), the
0184 // probability of sampling a nucleon beyond this radius is O(10^-5).
0185 WoodsSaxonNucleus::WoodsSaxonNucleus(
0186     std::size_t A, double R, double a, double dmin)
0187     : MinDistNucleus(A, dmin),
0188       R_(R),
0189       a_(a),
0190       woods_saxon_dist_(1000, 0., R + 10.*a,
0191         [R, a](double r) { return r*r/(1.+std::exp((r-R)/a)); })
0192 {}
0193 
0194 /// Return something a bit smaller than the true maximum radius.  The
0195 /// Woods-Saxon distribution falls off very rapidly (exponentially), and since
0196 /// this radius determines the impact parameter range, the true maximum radius
0197 /// would cause far too many events with zero participants.
0198 double WoodsSaxonNucleus::radius() const {
0199   return R_ + 3.*a_;
0200 }
0201 
0202 /// Sample Woods-Saxon nucleon positions.
0203 void WoodsSaxonNucleus::sample_nucleons_impl() {
0204   // When placing nucleons with a minimum distance criterion, resample spherical
0205   // angles until the nucleon is not too close to a previously sampled nucleon,
0206   // but do not resample radius -- this could modify the Woods-Saxon dist.
0207 
0208   // Because of the r^2 Jacobian, there is less available space at smaller
0209   // radii.  Therefore, pre-sample all radii first, sort them, and then place
0210   // nucleons starting with the smallest radius and working outwards.  This
0211   // dramatically reduces the chance that a nucleon cannot be placed.
0212   std::vector<double> radii(size());
0213   for (auto&& r : radii)
0214     r = woods_saxon_dist_(random::engine);
0215   std::sort(radii.begin(), radii.end());
0216 
0217   // Place each nucleon at a pre-sampled radius.
0218   auto r_iter = radii.cbegin();
0219   for (iterator nucleon = begin(); nucleon != end(); ++nucleon) {
0220     // Get radius and advance iterator.
0221     auto& r = *r_iter++;
0222 
0223     // Sample angles until the minimum distance criterion is satisfied.
0224     auto ntries = 0;
0225     do {
0226       // Sample isotropic spherical angles.
0227       auto cos_theta = random::cos_theta<double>();
0228       auto phi = random::phi<double>();
0229 
0230       // Convert to Cartesian coordinates.
0231       auto r_sin_theta = r * std::sqrt(1. - cos_theta*cos_theta);
0232       auto x = r_sin_theta * std::cos(phi);
0233       auto y = r_sin_theta * std::sin(phi);
0234       auto z = r * cos_theta;
0235 
0236       set_nucleon_position(nucleon, x, y, z);
0237 
0238       // Retry sampling a reasonable number of times.  If a nucleon cannot be
0239       // placed, give up and leave it at its last sampled position.  Some
0240       // approximate numbers for Pb nuclei:
0241       //
0242       //   dmin = 0.5 fm, < 0.001% of nucleons cannot be placed
0243       //          1.0 fm, ~0.005%
0244       //          1.5 fm, ~0.1%
0245       //          1.73 fm, ~1%
0246     } while (++ntries < 1000 && is_too_close(nucleon));
0247   }
0248   // XXX: re-center nucleon positions?
0249 }
0250 
0251 // Set rmax like the non-deformed case (R + 10a), but for the maximum
0252 // "effective" radius.  The numerical coefficients for beta2 and beta4 are the
0253 // approximate values of Y20 and Y40 at theta = 0.
0254 DeformedWoodsSaxonNucleus::DeformedWoodsSaxonNucleus(
0255     std::size_t A, double R, double a, double beta2, double beta4, double dmin)
0256     : MinDistNucleus(A, dmin),
0257       R_(R),
0258       a_(a),
0259       beta2_(beta2),
0260       beta4_(beta4),
0261       rmax_(R*(1. + .63*std::fabs(beta2) + .85*std::fabs(beta4)) + 10.*a)
0262 {}
0263 
0264 /// Return something a bit smaller than the true maximum radius.  The
0265 /// Woods-Saxon distribution falls off very rapidly (exponentially), and since
0266 /// this radius determines the impact parameter range, the true maximum radius
0267 /// would cause far too many events with zero participants.
0268 double DeformedWoodsSaxonNucleus::radius() const {
0269   return rmax_ - 7.*a_;
0270 }
0271 
0272 double DeformedWoodsSaxonNucleus::deformed_woods_saxon_dist(
0273     double r, double cos_theta) const {
0274   auto cos_theta_sq = cos_theta*cos_theta;
0275 
0276   // spherical harmonics
0277   using math::double_constants::one_div_root_pi;
0278   auto Y20 = std::sqrt(5)/4. * one_div_root_pi * (3.*cos_theta_sq - 1.);
0279   auto Y40 = 3./16. * one_div_root_pi *
0280              (35.*cos_theta_sq*cos_theta_sq - 30.*cos_theta_sq + 3.);
0281 
0282   // "effective" radius
0283   auto Reff = R_ * (1. + beta2_*Y20 + beta4_*Y40);
0284 
0285   return 1. / (1. + std::exp((r - Reff) / a_));
0286 }
0287 
0288 /// Sample deformed Woods-Saxon nucleon positions.
0289 void DeformedWoodsSaxonNucleus::sample_nucleons_impl() {
0290   // The deformed W-S distribution is defined so the symmetry axis is aligned
0291   // with the Z axis, so e.g. the long axis of uranium coincides with Z.
0292   //
0293   // After sampling positions, they must be randomly rotated.  In general this
0294   // requires three Euler rotations, but in this case we only need two
0295   // because there is no use in rotating about the nuclear symmetry axis.
0296   //
0297   // The two rotations are:
0298   //  - a polar "tilt", i.e. rotation about the X axis
0299   //  - an azimuthal "spin", i.e. rotation about the original Z axis
0300 
0301   // "tilt" angle
0302   const auto cos_a = random::cos_theta<double>();
0303   const auto sin_a = std::sqrt(1. - cos_a*cos_a);
0304 
0305   // "spin" angle
0306   const auto angle_b = random::phi<double>();
0307   const auto cos_b = std::cos(angle_b);
0308   const auto sin_b = std::sin(angle_b);
0309 
0310   // Pre-sample and sort (r, cos_theta) points from the deformed W-S dist.
0311   // See comments in WoodsSaxonNucleus (above) for rationale.
0312   struct Sample {
0313     double r, cos_theta;
0314   };
0315 
0316   std::vector<Sample> samples(size());
0317 
0318   for (auto&& sample : samples) {
0319     // Sample (r, cos_theta) using a standard rejection method.
0320     // Remember to include the phase-space factors.
0321     do {
0322       sample.r = rmax_ * std::cbrt(random::canonical<double>());
0323       sample.cos_theta = random::cos_theta<double>();
0324     } while (
0325       random::canonical<double>() >
0326       deformed_woods_saxon_dist(sample.r, sample.cos_theta)
0327     );
0328   }
0329 
0330   // Sort by radius.  Could also sort by e.g. the perpendicular distance from
0331   // the z-axis, or by descending W-S density.  Empirically, radius leads to the
0332   // smallest failure rate.
0333   std::sort(
0334     samples.begin(), samples.end(),
0335     [](const Sample& a, const Sample& b) {
0336       return a.r < b.r;
0337     }
0338   );
0339 
0340   // Place each nucleon at a pre-sampled (r, cos_theta).
0341   auto sample = samples.cbegin();
0342   for (iterator nucleon = begin(); nucleon != end(); ++nucleon, ++sample) {
0343     auto& r = sample->r;
0344     auto& cos_theta = sample->cos_theta;
0345 
0346     auto r_sin_theta = r * std::sqrt(1. - cos_theta*cos_theta);
0347     auto z = r * cos_theta;
0348 
0349     // Sample azimuthal angle until the minimum distance criterion is satisfied.
0350     auto ntries = 0;
0351     do {
0352       // Choose azimuthal angle.
0353       auto phi = random::phi<double>();
0354 
0355       // Convert to Cartesian coordinates.
0356       auto x = r_sin_theta * std::cos(phi);
0357       auto y = r_sin_theta * std::sin(phi);
0358 
0359       // Rotate.
0360       // The rotation formula was derived by composing the "tilt" and "spin"
0361       // rotations described above.
0362       auto x_rot = x*cos_b - y*cos_a*sin_b + z*sin_a*sin_b;
0363       auto y_rot = x*sin_b + y*cos_a*cos_b - z*sin_a*cos_b;
0364       auto z_rot =           y*sin_a       + z*cos_a;
0365 
0366       set_nucleon_position(nucleon, x_rot, y_rot, z_rot);
0367 
0368       // In addition to resampling phi, flip the z-coordinate each time.  This
0369       // works because the deformed WS dist is symmetric in z.  Effectively
0370       // doubles the available space for the nucleon.
0371       z *= -1;
0372 
0373       // Retry a reasonable number of times.  Unfortunately the failure rate is
0374       // worse than non-deformed sampling because there is less freedom to place
0375       // each nucleon.  Some approximate numbers for U nuclei:
0376       //
0377       //   dmin = 0.5 fm, < 0.001% of nucleons cannot be placed
0378       //          1.0 fm, ~0.03%
0379       //          1.3 fm, ~0.3%
0380       //          1.5 fm, ~1.2%
0381     } while (++ntries < 1000 && is_too_close(nucleon));
0382   }
0383 }
0384 
0385 #ifdef TRENTO_HDF5
0386 
0387 namespace {
0388 
0389 // Read a slice of an HDF5 dataset into a boost::multi_array.
0390 template <typename T, std::size_t FileDims, std::size_t MemDims>
0391 boost::multi_array<T, MemDims>
0392 read_dataset(
0393     const H5::DataSet& dataset,
0394     const std::array<hsize_t, FileDims>& count,
0395     const std::array<hsize_t, FileDims>& start,
0396     const std::array<hsize_t, MemDims>& shape) {
0397   boost::multi_array<T, MemDims> array{shape};
0398   auto filespace = dataset.getSpace();
0399   filespace.selectHyperslab(H5S_SELECT_SET, count.data(), start.data());
0400   auto memspace = hdf5::make_dataspace(shape);
0401 
0402   dataset.read(array.data(), hdf5::type<T>(), memspace, filespace);
0403 
0404   return array;
0405 }
0406 
0407 }  // unnamed namespace
0408 
0409 std::unique_ptr<ManualNucleus> ManualNucleus::create(const std::string& path) {
0410   auto file = hdf5::try_open_file(path);
0411 
0412   // Check that there is a single dataset in the file.
0413   // Might relax this constraint in the future.
0414   if (file.getNumObjs() != 1)
0415     throw std::invalid_argument{
0416       "file '" + path + "' must contain exactly one object"
0417     };
0418 
0419   auto name = file.getObjnameByIdx(0);
0420 #if H5_VERSION_GE(1, 8, 13)
0421   if (file.childObjType(name) != H5O_TYPE_DATASET)  // added v1.8.13
0422 #else
0423   if (file.getObjTypeByIdx(0) != H5G_DATASET)  // deprecated fall back
0424 #endif
0425     throw std::invalid_argument{
0426       "object '" + name + "' in file '" + path + "' is not a dataset"
0427     };
0428 
0429   // Make dataset object in a unique_ptr for eventual passing to ctor.
0430   auto dataset = std::unique_ptr<H5::DataSet>{
0431     new H5::DataSet{file.openDataSet(name)}
0432   };
0433 
0434   // Verify that the dataset has the correct dimensionality and shape.
0435   std::array<hsize_t, 3> shape;
0436   auto ndim = dataset->getSpace().getSimpleExtentDims(shape.data());
0437 
0438   if (ndim != 3)
0439     throw std::invalid_argument{
0440       "dataset '" + name + "' in file '" + path + "' has " +
0441       std::to_string(ndim) + " dimensions (need 3)"
0442     };
0443 
0444   if (shape[2] != 3)
0445     throw std::invalid_argument{
0446       "dataset '" + name + "' in file '" + path + "' has " +
0447       std::to_string(shape[2]) + " columns (need 3)"
0448     };
0449 
0450   // Deduce number of configs and number of nucleons (A) from the shape.
0451   const auto& nconfigs = shape[0];
0452   const auto& A = shape[1];
0453 
0454   // Estimate the max radius from at least 500 nucleon positions.
0455   auto n = std::min(500/A + 1, nconfigs);
0456   std::array<hsize_t, 3> count = {n, A, 3};
0457   std::array<hsize_t, 3> start = {0, 0, 0};
0458   std::array<hsize_t, 2> shape_n = {n*A, 3};
0459   auto positions = read_dataset<float>(*dataset, count, start, shape_n);
0460 
0461   auto rmax_sq = 0.;
0462 
0463   for (const auto& position : positions) {
0464     auto& x = position[0];
0465     auto& y = position[1];
0466     auto& z = position[2];
0467     auto r_sq = x*x + y*y + z*z;
0468     if (r_sq > rmax_sq)
0469       rmax_sq = r_sq;
0470   }
0471 
0472   auto rmax = std::sqrt(rmax_sq);
0473 
0474   return std::unique_ptr<ManualNucleus>{
0475     new ManualNucleus{std::move(dataset), nconfigs, A, rmax}
0476   };
0477 }
0478 
0479 ManualNucleus::ManualNucleus(std::unique_ptr<H5::DataSet> dataset,
0480                              std::size_t nconfigs, std::size_t A, double rmax)
0481     : Nucleus(A),
0482       dataset_(std::move(dataset)),
0483       rmax_(rmax),
0484       index_dist_(0, nconfigs - 1)
0485 {}
0486 
0487 ManualNucleus::~ManualNucleus() = default;
0488 
0489 double ManualNucleus::radius() const {
0490   return rmax_;
0491 }
0492 
0493 void ManualNucleus::sample_nucleons_impl() {
0494   // Sample Euler rotation angles.
0495   // First is an azimuthal spin about the Z axis.
0496   const auto angle_1 = random::phi<double>();
0497   const auto c1 = std::cos(angle_1);
0498   const auto s1 = std::sin(angle_1);
0499   // Then a polar tilt about the original X axis, uniform in cos(theta).
0500   const auto c2 = random::cos_theta<double>();
0501   const auto s2 = std::sqrt(1. - c2*c2);
0502   // Finally another azimuthal spin about the original Z axis.
0503   const auto angle_3 = random::phi<double>();
0504   const auto c3 = std::cos(angle_3);
0505   const auto s3 = std::sin(angle_3);
0506 
0507   // Choose and read a random config from the dataset.
0508   std::array<hsize_t, 3> count = {1, size(), 3};
0509   std::array<hsize_t, 3> start = {index_dist_(random::engine), 0, 0};
0510   std::array<hsize_t, 2> shape = {size(), 3};
0511   const auto positions = read_dataset<float>(*dataset_, count, start, shape);
0512 
0513   // Loop over positions and nucleons.
0514   auto positions_iter = positions.begin();
0515   for (iterator nucleon = begin(); nucleon != end(); ++nucleon) {
0516     // Extract position vector and increment iterator.
0517     auto position = *positions_iter++;
0518     auto& x = position[0];
0519     auto& y = position[1];
0520     auto& z = position[2];
0521 
0522     // Rotate.
0523     auto x_rot = x*(c1*c3 - c2*s1*s3) - y*(c3*s1 + c1*c2*s3) + z*s2*s3;
0524     auto y_rot = x*(c1*s3 + c2*c3*s1) - y*(s1*s3 - c1*c2*c3) - z*c3*s2;
0525     auto z_rot = x*s1*s2              + y*c1*s2              + z*c2;
0526 
0527     set_nucleon_position(nucleon, x_rot, y_rot, z_rot);
0528   }
0529 }
0530 
0531 #endif  // TRENTO_HDF5
0532 
0533 }  // namespace trento