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 "event.h"
0007 
0008 #include <algorithm>
0009 #include <cmath>
0010 
0011 #include <boost/program_options/variables_map.hpp>
0012 #include "nucleus.h"
0013 #include <iostream>
0014 #include <stdexcept>
0015 
0016 namespace trento {
0017 
0018 namespace {
0019 
0020 constexpr double TINY = 1e-12;
0021 
0022 // Generalized mean for p > 0.
0023 // M_p(a, b) = (1/2*(a^p + b^p))^(1/p)
0024 inline double positive_pmean(double p, double a, double b) {
0025   return std::pow(.5*(std::pow(a, p) + std::pow(b, p)), 1./p);
0026 }
0027 
0028 // Generalized mean for p < 0.
0029 // Same as the positive version, except prevents division by zero.
0030 inline double negative_pmean(double p, double a, double b) {
0031   if (a < TINY || b < TINY)
0032     return 0.;
0033   return positive_pmean(p, a, b);
0034 }
0035 
0036 // Generalized mean for p == 0.
0037 inline double geometric_mean(double a, double b) {
0038   return std::sqrt(a*b);
0039 }
0040 
0041 // Get beam rapidity from beam energy sqrt(s)
0042 double beam_rapidity(double beam_energy) {
0043   // Proton mass in GeV
0044   constexpr double mp = 0.938;
0045   return 0.5 * (beam_energy/mp + std::sqrt(std::pow(beam_energy/mp, 2) - 4.));
0046 }
0047 
0048 }  // unnamed namespace
0049 
0050 // Determine the grid parameters like so:
0051 //   1. Read and set step size from the configuration.
0052 //   2. Read grid max from the config, then set the number of steps as
0053 //      nsteps = ceil(2*max/step).
0054 //   3. Set the actual grid max as max = nsteps*step/2.  Hence if the step size
0055 //      does not evenly divide the config max, the actual max will be marginally
0056 //      larger (by at most one step size).
0057 Event::Event(const VarMap& var_map)
0058     : norm_(var_map["normalization"].as<double>()),
0059       beam_energy_(var_map["beam-energy"].as<double>()),
0060       exp_ybeam_(beam_rapidity(beam_energy_)),
0061       mean_coeff_(var_map["mean-coeff"].as<double>()),
0062       std_coeff_(var_map["std-coeff"].as<double>()),
0063       skew_coeff_(var_map["skew-coeff"].as<double>()),
0064       skew_type_(var_map["skew-type"].as<int>()),
0065       dxy_(var_map["xy-step"].as<double>()),
0066       deta_(var_map["eta-step"].as<double>()),
0067       nsteps_(std::ceil(2.*var_map["xy-max"].as<double>()/dxy_)),
0068       neta_(std::ceil(2.*var_map["eta-max"].as<double>()/(deta_+1e-15))),
0069       xymax_(.5*nsteps_*dxy_),
0070       etamax_(var_map["eta-max"].as<double>()),
0071       eta2y_(var_map["jacobian"].as<double>(), etamax_, deta_),
0072       cgf_(),
0073       TA_(boost::extents[nsteps_][nsteps_]),
0074       TB_(boost::extents[nsteps_][nsteps_]),
0075       TR_(boost::extents[nsteps_][nsteps_][1]),
0076       TAB_(boost::extents[nsteps_][nsteps_]),
0077       with_ncoll_(var_map["ncoll"].as<bool>()),
0078       density_(boost::extents[nsteps_][nsteps_][neta_]) {
0079   // Check if the skew parameter is within the applicable range
0080   // For 1: relative skew, skew_coeff_ < 10.
0081   //     2: absolute skew, skew_coeff_ < 3.
0082   try {
0083     if ((skew_type_ == 1 && skew_coeff_ > 10.) || 
0084       (skew_type_ == 2 && skew_coeff_ > 3.) ){
0085       auto info = std::string("Error: skew coefficent too large to be stable.\n")
0086                 + std::string("Requirements: (1) relative skew, skew_coeff < 10\n")
0087                 + std::string("              (2) absolute skew, skew_coeff < 3");
0088       throw std::invalid_argument(info);
0089     }
0090   }
0091   catch (const std::invalid_argument& error){
0092     std::cerr << error.what() << std::endl;
0093     exit(1);
0094   }
0095 
0096   // Choose which version of the generalized mean to use based on the
0097   // configuration. The possibilities are defined above.  See the header for
0098   // more information.
0099   auto p = var_map["reduced-thickness"].as<double>();
0100   if (std::fabs(p) < TINY) {
0101     compute_reduced_thickness_ = [this]() {
0102       compute_reduced_thickness(geometric_mean);
0103     };
0104   } else if (p > 0.) {
0105     compute_reduced_thickness_ = [this, p]() {
0106       compute_reduced_thickness(
0107         [p](double a, double b) { return positive_pmean(p, a, b); });
0108     };
0109   } else {
0110     compute_reduced_thickness_ = [this, p]() {
0111       compute_reduced_thickness(
0112         [p](double a, double b) { return negative_pmean(p, a, b); });
0113     };
0114   }
0115 }
0116 
0117 void Event::compute(const Nucleus& nucleusA, const Nucleus& nucleusB,
0118                     NucleonProfile& profile) {
0119   // Reset npart; compute_nuclear_thickness() increments it.
0120   npart_ = 0;
0121   compute_nuclear_thickness(nucleusA, profile, TA_);
0122   compute_nuclear_thickness(nucleusB, profile, TB_);
0123   compute_reduced_thickness_();
0124   compute_observables();
0125 }
0126 
0127 bool Event::is3D() const {
0128   return etamax_ > TINY;
0129 }
0130 
0131 namespace {
0132 
0133 // Limit a value to a range.
0134 // Used below to constrain grid indices.
0135 template <typename T>
0136 inline const T& clip(const T& value, const T& min, const T& max) {
0137   if (value < min)
0138     return min;
0139   if (value > max)
0140     return max;
0141   return value;
0142 }
0143 
0144 }  // unnamed namespace
0145 
0146 // WK: clear Ncoll density table
0147 void Event::clear_TAB(void){
0148   ncoll_ = 0;
0149   for (int iy = 0; iy < nsteps_; ++iy) {
0150     for (int ix = 0; ix < nsteps_; ++ix) {
0151       TAB_[iy][ix] = 0.;
0152     }
0153   }
0154 }
0155 
0156 // WK: accumulate a Tpp to Ncoll density table
0157 void Event::accumulate_TAB(Nucleon& A, Nucleon& B, NucleonProfile& profile){
0158     ncoll_ ++;
0159     // the loaction of A and B nucleon
0160     double xA = A.x() + xymax_, yA = A.y() + xymax_;
0161     double xB = B.x() + xymax_, yB = B.y() + xymax_;
0162     // impact parameter squared of this binary collision
0163     double bpp_sq = std::pow(xA - xB, 2) + std::pow(yA - yB, 2);
0164     // the mid point of A and B
0165     double x = (xA+xB)/2.;
0166     double y = (yA+yB)/2.;
0167     // the max radius of Tpp 
0168     const double r = profile.radius();
0169     int ixmin = clip(static_cast<int>((x-r)/dxy_), 0, nsteps_-1);
0170     int iymin = clip(static_cast<int>((y-r)/dxy_), 0, nsteps_-1);
0171     int ixmax = clip(static_cast<int>((x+r)/dxy_), 0, nsteps_-1);
0172     int iymax = clip(static_cast<int>((y+r)/dxy_), 0, nsteps_-1);
0173 
0174     // Add Tpp to Ncoll density.
0175     auto norm_Tpp = profile.norm_Tpp(bpp_sq);
0176     for (auto iy = iymin; iy <= iymax; ++iy) {
0177       double dysqA = std::pow(yA - (static_cast<double>(iy)+.5)*dxy_, 2);
0178       double dysqB = std::pow(yB - (static_cast<double>(iy)+.5)*dxy_, 2);
0179       for (auto ix = ixmin; ix <= ixmax; ++ix) {
0180         double dxsqA = std::pow(xA - (static_cast<double>(ix)+.5)*dxy_, 2);
0181         double dxsqB = std::pow(xB - (static_cast<double>(ix)+.5)*dxy_, 2);
0182         // The Ncoll density does not fluctuates, so we use the 
0183         // deterministic_thickness function
0184         // where the Gamma fluctuation are turned off.
0185         // since this binary collision already happened, the binary collision
0186         // density should be normalized to one.
0187         TAB_[iy][ix] += profile.deterministic_thickness(dxsqA + dysqA)
0188                         * profile.deterministic_thickness(dxsqB + dysqB)
0189                         / norm_Tpp;
0190       }
0191     }
0192 }
0193 
0194 void Event::compute_nuclear_thickness(
0195     const Nucleus& nucleus, NucleonProfile& profile, Grid& TX) {
0196   // Construct the thickness grid by looping over participants and adding each
0197   // to a small subgrid within its radius.  Compared to the other possibility
0198   // (grid cells as the outer loop and participants as the inner loop), this
0199   // reduces the number of required distance-squared calculations by a factor of
0200   // ~20 (depending on the nucleon size).  The Event unit test verifies that the
0201   // two methods agree.
0202 
0203   // Wipe grid with zeros.
0204   std::fill(TX.origin(), TX.origin() + TX.num_elements(), 0.);
0205 
0206   const double r = profile.radius();
0207 
0208   // Deposit each participant onto the grid.
0209   for (const auto& nucleon : nucleus) {
0210     if (!nucleon.is_participant())
0211       continue;
0212 
0213     ++npart_;
0214 
0215     // Work in coordinates relative to (-width/2, -width/2).
0216     double x = nucleon.x() + xymax_;
0217     double y = nucleon.y() + xymax_;
0218 
0219     // Determine min & max indices of nucleon subgrid.
0220     int ixmin = clip(static_cast<int>((x-r)/dxy_), 0, nsteps_-1);
0221     int iymin = clip(static_cast<int>((y-r)/dxy_), 0, nsteps_-1);
0222     int ixmax = clip(static_cast<int>((x+r)/dxy_), 0, nsteps_-1);
0223     int iymax = clip(static_cast<int>((y+r)/dxy_), 0, nsteps_-1);
0224 
0225     // Prepare profile for new nucleon.
0226     profile.fluctuate();
0227 
0228     // Add profile to grid.
0229     for (auto iy = iymin; iy <= iymax; ++iy) {
0230       double dysq = std::pow(y - (static_cast<double>(iy)+.5)*dxy_, 2);
0231       for (auto ix = ixmin; ix <= ixmax; ++ix) {
0232         double dxsq = std::pow(x - (static_cast<double>(ix)+.5)*dxy_, 2);
0233         TX[iy][ix] += profile.thickness(dxsq + dysq);
0234       }
0235     }
0236   }
0237 }
0238 
0239 template <typename GenMean>
0240 void Event::compute_reduced_thickness(GenMean gen_mean) {
0241   double sum = 0.;
0242   double ixcm = 0.;
0243   double iycm = 0.;
0244 
0245   for (int iy = 0; iy < nsteps_; ++iy) {
0246     for (int ix = 0; ix < nsteps_; ++ix) {
0247       auto ta = TA_[iy][ix];
0248       auto tb = TB_[iy][ix];
0249       auto t = norm_ * gen_mean(ta, tb);
0250       /// At midrapidity    
0251       TR_[iy][ix][0] = t;
0252       /// If operating in the 3D mode, the 3D density_ array is filled with
0253       /// its value at eta=0 identical to TR_ array
0254       if (is3D()) {
0255         auto mean = mean_coeff_ * mean_function(ta, tb, exp_ybeam_);
0256         auto std = std_coeff_ * std_function(ta, tb);
0257         auto skew = skew_coeff_ * skew_function(ta, tb, skew_type_);
0258         cgf_.calculate_dsdy(mean, std, skew);
0259         auto mid_norm = cgf_.interp_dsdy(0.)*eta2y_.Jacobian(0.);
0260         for (int ieta = 0; ieta < neta_; ++ieta) {
0261           auto eta = -etamax_ + ieta*deta_;
0262           auto rapidity = eta2y_.rapidity(eta);
0263           auto jacobian = eta2y_.Jacobian(eta);
0264           auto rapidity_dist = cgf_.interp_dsdy(rapidity);
0265           density_[iy][ix][ieta] = t * rapidity_dist / mid_norm * jacobian;
0266         }
0267       }
0268 
0269       sum += t;
0270       // Center of mass grid indices.
0271       // No need to multiply by dxy since it would be canceled later.
0272       ixcm += t * static_cast<double>(ix);
0273       iycm += t * static_cast<double>(iy);
0274     }
0275   }
0276 
0277   multiplicity_ = dxy_ * dxy_ * sum;
0278   ixcm_ = ixcm / sum;
0279   iycm_ = iycm / sum;
0280 }
0281 
0282 void Event::compute_observables() {
0283   // Compute eccentricity at mid rapidity
0284 
0285   // Simple helper class for use in the following loop.
0286   struct EccentricityAccumulator {
0287     double re = 0.;  // real part
0288     double im = 0.;  // imaginary part
0289     double wt = 0.;  // weight
0290     double finish() const  // compute final eccentricity
0291     { return std::sqrt(re*re + im*im) / std::fmax(wt, TINY); }
0292     double angle() const  // compute event plane angle
0293     { return atan2(im, re); }
0294   } e2, e3, e4, e5;
0295 
0296   for (int iy = 0; iy < nsteps_; ++iy) {
0297     for (int ix = 0; ix < nsteps_; ++ix) {
0298       const auto& t = TR_[iy][ix][0];
0299       if (t < TINY)
0300         continue;
0301 
0302       // Compute (x, y) relative to the CM and cache powers of x, y, r.
0303       auto x = static_cast<double>(ix) - ixcm_;
0304       auto x2 = x*x;
0305       auto x3 = x2*x;
0306       auto x4 = x2*x2;
0307 
0308       auto y = static_cast<double>(iy) - iycm_;
0309       auto y2 = y*y;
0310       auto y3 = y2*y;
0311       auto y4 = y2*y2;
0312 
0313       auto r2 = x2 + y2;
0314       auto r = std::sqrt(r2);
0315       auto r4 = r2*r2;
0316 
0317       auto xy = x*y;
0318       auto x2y2 = x2*y2;
0319 
0320       // The eccentricity harmonics are weighted averages of r^n*exp(i*n*phi)
0321       // over the entropy profile (reduced thickness).  The naive way to compute
0322       // exp(i*n*phi) at a given (x, y) point is essentially:
0323       //
0324       //   phi = arctan2(y, x)
0325       //   real = cos(n*phi)
0326       //   imag = sin(n*phi)
0327       //
0328       // However this implementation uses three unnecessary trig functions; a
0329       // much faster method is to express the cos and sin directly in terms of x
0330       // and y.  For example, it is trivial to show (by drawing a triangle and
0331       // using rudimentary trig) that
0332       //
0333       //   cos(arctan2(y, x)) = x/r = x/sqrt(x^2 + y^2)
0334       //   sin(arctan2(y, x)) = y/r = x/sqrt(x^2 + y^2)
0335       //
0336       // This is easily generalized to cos and sin of (n*phi) by invoking the
0337       // multiple angle formula, e.g. sin(2x) = 2sin(x)cos(x), and hence
0338       //
0339       //   sin(2*arctan2(y, x)) = 2*sin(arctan2(y, x))*cos(arctan2(y, x))
0340       //                        = 2*x*y / r^2
0341       //
0342       // Which not only eliminates the trig functions, but also naturally
0343       // cancels the r^2 weight.  This cancellation occurs for all n.
0344       //
0345       // The Event unit test verifies that the two methods agree.
0346       e2.re += t * (x2 - y2);
0347       e2.im += t * 2.*xy;
0348       e2.wt += t * r2;
0349 
0350       e3.re += t * (x3 - 3.*x*y2);
0351       e3.im += t * y*(3. - 4.*y2);
0352       e3.wt += t * r2*r;
0353 
0354       e4.re += t * (x4 + y4 - 6.*x2y2);
0355       e4.im += t * 4.*xy*(1. - 2.*y2);
0356       e4.wt += t * r4;
0357 
0358       e5.re += t * x*(x4 - 10.*x2y2 - 5.*y4);
0359       e5.im += t * y*(1. - 12.*x2 + 16.*x4);
0360       e5.wt += t * r4*r;
0361     }
0362   }
0363 
0364   eccentricity_[2] = e2.finish();
0365   eccentricity_[3] = e3.finish();
0366   eccentricity_[4] = e4.finish();
0367   eccentricity_[5] = e5.finish();
0368 
0369   psi_[2] = e2.angle()/2.;
0370   psi_[3] = e3.angle()/3.;
0371   psi_[4] = e4.angle()/4.;
0372   psi_[5] = e5.angle()/5.;
0373 }
0374 
0375 }  // namespace trento