Analysis Software
Documentation for sPHENIX simulation software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
test_nucleus.cxx
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file test_nucleus.cxx
1 // TRENTO: Reduced Thickness Event-by-event Nuclear Topology
2 // Copyright 2015 Jonah E. Bernhard, J. Scott Moreland
3 // MIT License
4 
5 #include "../src/nucleus.h"
6 
7 #include <algorithm>
8 #include <cmath>
9 #include <iomanip>
10 #include <iterator>
11 #include <map>
12 
13 #include "catch.hpp"
14 #include "util.h"
15 
16 #include "../src/hdf5_utils.h"
17 #include "../src/random.h"
18 
19 using namespace trento;
20 
21 TEST_CASE( "proton" ) {
22  auto nucleus = Nucleus::create("p");
23 
24  CHECK( dynamic_cast<Proton*>(nucleus.get()) != nullptr );
25 
26  // proton contains one nucleon
27  CHECK( std::distance(nucleus->begin(), nucleus->end()) == 1 );
28  CHECK( std::distance(nucleus->cbegin(), nucleus->cend()) == 1 );
29 
30  // and has zero radius
31  CHECK( nucleus->radius() == 0. );
32 
33  // sample position with random offset
34  double offset = random::canonical<>();
35  nucleus->sample_nucleons(offset);
36  auto&& proton = *(nucleus->begin());
37 
38  // check correct position
39  CHECK( proton.x() == offset );
40  CHECK( proton.y() == 0. );
41  CHECK( proton.z() == 0. );
42 
43  // not a participant initially
44  CHECK( !proton.is_participant() );
45 }
46 
47 TEST_CASE( "deuteron" ) {
48  auto nucleus = Nucleus::create("d");
49 
50  CHECK( dynamic_cast<Deuteron*>(nucleus.get()) != nullptr );
51 
52  CHECK( std::distance(nucleus->begin(), nucleus->end()) == 2 );
53  CHECK( std::distance(nucleus->cbegin(), nucleus->cend()) == 2 );
54 
55  nucleus->sample_nucleons(0);
56 
57  CHECK( nucleus->cbegin()->x() == -std::next(nucleus->cbegin())->x() );
58  CHECK( nucleus->cbegin()->y() == -std::next(nucleus->cbegin())->y() );
59  CHECK( nucleus->cbegin()->z() == -std::next(nucleus->cbegin())->z() );
60 
61  CHECK( nucleus->radius() == Approx(-std::log(.01)/(2*0.457)) );
62 }
63 
64 TEST_CASE( "lead nucleus" ) {
65  auto nucleus = Nucleus::create("Pb");
66 
67  CHECK( dynamic_cast<WoodsSaxonNucleus*>(nucleus.get()) != nullptr );
68 
69  constexpr int A = 208;
70  CHECK( std::distance(nucleus->begin(), nucleus->end()) == A );
71  CHECK( std::distance(nucleus->cbegin(), nucleus->cend()) == A );
72 
73  CHECK( nucleus->radius() > 6. );
74 
75  double offset = nucleus->radius() * random::canonical<>();
76  nucleus->sample_nucleons(offset);
77 
78  // average nucleon position
79  double x = 0., y = 0., z = 0.;
80  for (const auto& nucleon : *nucleus) {
81  x += nucleon.x();
82  y += nucleon.y();
83  z += nucleon.z();
84  }
85  x /= A;
86  y /= A;
87  z /= A;
88  auto tolerance = .7;
89  CHECK( std::abs(x - offset) < tolerance );
90  CHECK( std::abs(y) < tolerance );
91  CHECK( std::abs(z) < tolerance );
92 
93  // no initial participants
94  bool initial_participants = std::any_of(
95  nucleus->cbegin(), nucleus->cend(),
96  [](decltype(*nucleus->cbegin())& n) {
97  return n.is_participant();
98  });
99  CHECK( !initial_participants );
100 }
101 
102 TEST_CASE( "copper nucleus" ) {
103  auto nucleus = Nucleus::create("Cu");
104  auto def_nucleus = Nucleus::create("Cu2");
105 
106  CHECK( dynamic_cast<WoodsSaxonNucleus*>(nucleus.get()) != nullptr );
107  CHECK( dynamic_cast<DeformedWoodsSaxonNucleus*>(def_nucleus.get()) != nullptr );
108 
109  constexpr int A = 63;
110  CHECK( std::distance(nucleus->begin(), nucleus->end()) == A );
111  CHECK( std::distance(def_nucleus->cbegin(), def_nucleus->cend()) == A );
112 
113  CHECK( nucleus->radius() > 4. );
114  CHECK( def_nucleus->radius() > nucleus->radius() );
115 }
116 
117 TEST_CASE( "gold nucleus" ) {
118  auto nucleus = Nucleus::create("Au");
119  auto def_nucleus = Nucleus::create("Au2");
120 
121  CHECK( dynamic_cast<WoodsSaxonNucleus*>(nucleus.get()) != nullptr );
122  CHECK( dynamic_cast<DeformedWoodsSaxonNucleus*>(def_nucleus.get()) != nullptr );
123 
124  constexpr int A = 197;
125  CHECK( std::distance(nucleus->begin(), nucleus->end()) == A );
126  CHECK( std::distance(def_nucleus->cbegin(), def_nucleus->cend()) == A );
127 
128  CHECK( nucleus->radius() > 6. );
129  CHECK( def_nucleus->radius() > nucleus->radius() );
130 }
131 
132 TEST_CASE( "uranium nucleus" ) {
133  auto nucleus = Nucleus::create("U");
134 
135  CHECK( dynamic_cast<DeformedWoodsSaxonNucleus*>(nucleus.get()) != nullptr );
136 
137  constexpr int A = 238;
138  CHECK( std::distance(nucleus->begin(), nucleus->end()) == A );
139  CHECK( std::distance(nucleus->cbegin(), nucleus->cend()) == A );
140 
141  CHECK( nucleus->radius() > 8. );
142 }
143 
144 #ifdef TRENTO_HDF5
145 
146 TEST_CASE( "manual nucleus" ) {
147  std::vector<float> positions = {
148  0., 0., 0.,
149  1., 0., 0.,
150  -1., 0., 0.,
151  3., 0., 0.,
152  0., 2., 0.,
153  0., -2., 0.,
154  };
155 
156  const auto A = positions.size() / 3;
157 
158  temporary_path temp{".hdf5"};
159 
160  {
161  auto dataspace = hdf5::make_dataspace(std::array<hsize_t, 3>{1, A, 3});
162  const auto& datatype = hdf5::type<decltype(positions)::value_type>();
163 
164  H5::H5File file{temp.path.string(), H5F_ACC_EXCL};
165 
166  auto dataset = file.createDataSet("test", datatype, dataspace);
167  dataset.write(positions.data(), datatype);
168  }
169 
170  auto nucleus = Nucleus::create(temp.path.string());
171 
172  CHECK( dynamic_cast<ManualNucleus*>(nucleus.get()) != nullptr );
173 
174  CHECK( std::distance(nucleus->begin(), nucleus->end()) == A );
175  CHECK( std::distance(nucleus->cbegin(), nucleus->cend()) == A );
176 
177  CHECK( nucleus->radius() == Approx(3.) );
178 
179  nucleus->sample_nucleons(0.);
180 
181  auto nucleon = nucleus->cbegin();
182 
183  CHECK( nucleon->x() == Approx(0.) );
184  CHECK( nucleon->y() == Approx(0.) );
185  CHECK( nucleon->z() == Approx(0.) );
186 
187  std::advance(nucleon, 1);
188  CHECK( nucleon->x() == Approx(-std::next(nucleon)->x()) );
189  CHECK( nucleon->y() == Approx(-std::next(nucleon)->y()) );
190  CHECK( nucleon->z() == Approx(-std::next(nucleon)->z()) );
191 
192  CHECK( nucleon->x() == Approx(std::next(nucleon, 2)->x()/3) );
193  CHECK( nucleon->y() == Approx(std::next(nucleon, 2)->y()/3) );
194  CHECK( nucleon->z() == Approx(std::next(nucleon, 2)->z()/3) );
195 
196  std::advance(nucleon, 3);
197  CHECK( nucleon->x() == Approx(-std::next(nucleon)->x()) );
198  CHECK( nucleon->y() == Approx(-std::next(nucleon)->y()) );
199  CHECK( nucleon->z() == Approx(-std::next(nucleon)->z()) );
200 
201  CHECK_THROWS_AS( Nucleus::create("nonexistent.hdf"), std::invalid_argument );
202 }
203 
204 #endif // TRENTO_HDF5
205 
206 TEST_CASE( "woods-saxon sampling" ) {
207  int A = 200;
208  double R = 6., a = .5;
209  NucleusPtr nucleus{new WoodsSaxonNucleus{static_cast<std::size_t>(A), R, a}};
210 
211  // Test Woods-Saxon sampling.
212  // This is honestly not a great test; while it does prove that the code
213  // basically works as intended, it does not rigorously show that the generated
214  // numbers are actually Woods-Saxon distributed. Personally I am much more
215  // convinced by plotting a histogram and visually comparing it to the smooth
216  // curve. The script 'plot-woods-saxon.py' in the 'woods-saxon' subdirectory
217  // does this.
218 
219  // sample a bunch of nuclei and bin all the nucleon positions
220  auto nevents = 5000;
221  auto nsamples = nevents * A;
222  std::map<int, int> hist{};
223  for (auto i = 0; i < nevents; ++i) {
224  nucleus->sample_nucleons(0.);
225  for (const auto& nucleon : *nucleus) {
226  auto x = nucleon.x();
227  auto y = nucleon.y();
228  auto z = nucleon.z();
229  auto r = std::sqrt(x*x + y*y + z*z);
230  ++hist[static_cast<int>(r)];
231  }
232  }
233 
234  // integrate the W-S dist from rmin to rmax
235  auto integrate_woods_saxon = [R, a](double rmin, double rmax) -> double {
236  auto nbins = static_cast<int>((rmax - rmin)/.001);
237  auto dr = (rmax - rmin)/nbins;
238  double result = 0.;
239  for (auto n = 0; n <= nbins; ++n) {
240  auto r = rmin + n*dr;
241  auto f = r*r/(1. + std::exp((r - R)/a));
242  if (n == 0 || n == nbins)
243  f /= 2;
244  result += f;
245  }
246  return result * dr;
247  };
248 
249  double ws_norm = integrate_woods_saxon(0, hist.size());
250 
251  // check all histogram bins agree with numerical integration
252  auto all_bins_correct = true;
253  std::ostringstream bin_output{};
254  bin_output << std::fixed << std::boolalpha
255  << "rmin rmax prob cprob ratio pass\n";
256  for (const auto& bin : hist) {
257  auto rmin = bin.first;
258  auto rmax = rmin + 1;
259  auto prob = static_cast<double>(bin.second) / nsamples;
260  auto correct_prob = integrate_woods_saxon(rmin, rmax) / ws_norm;
261  bool within_tol = prob == Approx(correct_prob).epsilon(.1).margin(1e-4);
262  if (!within_tol)
263  all_bins_correct = false;
264  bin_output << std::setw(4) << rmin << ' '
265  << std::setw(4) << rmax << " "
266  << prob << " "
267  << correct_prob << " "
268  << prob/correct_prob << " "
269  << within_tol << '\n';
270  }
271  INFO( bin_output.str() );
272  CHECK( all_bins_correct );
273 }
274 
275 TEST_CASE( "deformed woods-saxon sampling" ) {
276  // This is tough to test because the sampled positions are randomly rotated
277  // and the z-coordinate is discarded. However, the authors have visually
278  // checked the results by histogramming the samples (not done here, but easy
279  // to reproduce).
280 
281  // As a not-very-stringent test, check that the mean ellipticity of a deformed
282  // W-S nucleus is significantly larger than a symmetric nucleus.
283 
284  std::size_t A = 200;
285  double R = 6., a = .5, beta2 = .3, beta4 = .1;
286 
287  auto nucleus_sym = WoodsSaxonNucleus{A, R, a};
288  auto nucleus_def = DeformedWoodsSaxonNucleus{A, R, a, beta2, beta4};
289 
290  auto mean_ecc2 = [](Nucleus& nucleus) {
291  double e2 = 0.;
292  int nevents = 500;
293 
294  for (int n = 0; n < nevents; ++n) {
295  nucleus.sample_nucleons(0.);
296  double numer = 0.;
297  double denom = 0.;
298  for (const auto& nucleon : nucleus) {
299  double x2 = std::pow(nucleon.x(), 2);
300  double y2 = std::pow(nucleon.y(), 2);
301  numer += x2 - y2;
302  denom += x2 + y2;
303  }
304 
305  e2 += std::fabs(numer) / denom;
306  }
307 
308  return e2 / nevents;
309  };
310 
311  CHECK( mean_ecc2(nucleus_def) > 2.*mean_ecc2(nucleus_sym) );
312 }
313 
314 TEST_CASE( "nuclear radius" ) {
315  constexpr auto R = 5., a = .5;
316  constexpr auto radius = R + 3*a;
317 
318  WoodsSaxonNucleus nucleus{100, R, a};
319  DeformedWoodsSaxonNucleus dummy_def_nucleus{100, R, a, 0., 0.};
320 
321  CHECK( nucleus.radius() == Approx(radius) );
322  CHECK( dummy_def_nucleus.radius() == Approx(radius) );
323 
324  DeformedWoodsSaxonNucleus def_nucleus{100, R, a, .2, .1};
325  CHECK( def_nucleus.radius() > radius );
326 }
327 
328 TEST_CASE( "minimum distance" ) {
329  for (const auto& species : {"Pb", "U"}) {
330  for (auto repeat = 0; repeat < 10; ++repeat) {
331  const auto target_dmin = .2 + .4*random::canonical<>();
332  auto nucleus = Nucleus::create("Pb", target_dmin);
333  nucleus->sample_nucleons(10*random::canonical<>());
334  auto dminsq = 100.;
335  for (auto n1 = nucleus->cbegin(); n1 != nucleus->cend(); ++n1) {
336  for (auto n2 = n1 + 1; n2 != nucleus->cend(); ++n2) {
337  auto dx = n1->x() - n2->x();
338  auto dy = n1->y() - n2->y();
339  auto dz = n1->z() - n2->z();
340  dminsq = std::fmin(dx*dx + dy*dy + dz*dz, dminsq);
341  }
342  }
343  auto dmin = std::sqrt(dminsq);
344  INFO( "species " << species << " repeat " << repeat );
345  CHECK( dmin >= target_dmin );
346  }
347  }
348 }
349 
350 TEST_CASE( "unknown nucleus species" ) {
351  CHECK_THROWS_AS( Nucleus::create("hello"), std::invalid_argument );
352 }