faunus
scatter.h
1 #pragma once
2 
3 #include <fstream>
4 #include <algorithm>
5 #include <cmath>
6 #include "atomdata.h"
7 
8 namespace Faunus {
9 
12 namespace Scatter {
13 
14 enum Algorithm
15 {
16  SIMD,
17  EIGEN,
18  GENERIC
19 };
20 
23 template <std::floating_point T = float> class FormFactorSphere
24 {
25  private:
26  T j1(T x) const
27  { // spherical Bessel function
28  T xinv = 1 / x;
29  return xinv * (sin(x) * xinv - cos(x));
30  }
31 
32  public:
39  template <class Tparticle> T operator()(T q, const Tparticle& a) const
40  {
41  assert(q > 0 && a.radius > 0 && "Particle radius and q must be positive");
42  T qR = q * a.radius;
43  qR = 3. / (qR * qR * qR) * (sin(qR) - qR * cos(qR));
44  return qR * qR;
45  }
46 };
47 
51 template <std::floating_point T = float> struct FormFactorUnity
52 {
53  template <class Tparticle> [[nodiscard]] constexpr T operator()(T, const Tparticle&) const
54  {
55  return T{1};
56  }
57 };
58 
65 struct Scatterer
66 {
67  Point pos;
68  int id = 0;
69 };
70 
77 template <typename T>
78 constexpr const auto& getPosition(const T& scatterer)
79 {
80  if constexpr (requires { scatterer.pos; }) {
81  return scatterer.pos;
82  }
83  else {
84  return scatterer;
85  }
86 }
87 
95 template <std::floating_point T = float> struct FormFactorAtomicConstant
96 {
97  template <class Tscatterer> [[nodiscard]] T operator()(T, const Tscatterer& scatterer) const
98  {
99  if (scatterer.id < 0) {
100  return T{1};
101  }
102  assert(static_cast<size_t>(scatterer.id) < atoms.size());
103  return static_cast<T>(atoms[scatterer.id].scattering_f0);
104  }
105 };
106 
122 #pragma GCC diagnostic push
123 #pragma GCC diagnostic ignored "-Wdouble-promotion"
124 
125 template <class Tformfactor, std::floating_point T = float> class DebyeFormula
126 {
127  static constexpr T r_cutoff_infty =
128  1e9; //<! a cutoff distance in angstrom considered to be infinity
129  T q_mesh_min, q_mesh_max,
130  q_mesh_step; //<! q_mesh parameters in inverse angstrom; used for inline lambda-functions
131 
136 #pragma omp declare simd uniform(this) linear(m : 1)
137 
138  inline T q_mesh(int m) { return q_mesh_min + m * q_mesh_step; }
139 
146  void init_mesh(T q_min, T q_max, T q_step)
147  {
148  if (q_step <= 0 || q_min <= 0 || q_max <= 0 || q_min > q_max ||
149  q_step / q_max < 4 * std::numeric_limits<T>::epsilon()) {
150  throw std::range_error("DebyeFormula: Invalid mesh parameters for q");
151  }
152  q_mesh_min = q_min < T(1e-6) ? q_step : q_min; // ensure that q > 0
153  q_mesh_max = q_max;
154  q_mesh_step = q_step;
155  try {
156  // resolution of the 1D mesh approximation of the scattering vector magnitude q
157  const int q_resolution = numeric_cast<int>(1.0 + std::floor((q_max - q_min) / q_step));
158  intensity.resize(q_resolution, 0.0);
159  sampling.resize(q_resolution, 0.0);
160  }
161  catch (std::overflow_error& e) {
162  throw std::range_error("DebyeFormula: Too many samples");
163  }
164  }
165 
166  T r_cutoff;
167  Tformfactor form_factor;
168  std::vector<T> intensity;
169  std::vector<T> sampling;
170 
171  public:
172  DebyeFormula(T q_min, T q_max, T q_step, T r_cutoff)
173  : r_cutoff(r_cutoff)
174  {
175  init_mesh(q_min, q_max, q_step);
176  };
177 
178  DebyeFormula(T q_min, T q_max, T q_step)
179  : DebyeFormula(r_cutoff_infty, q_min, q_max, q_step) {};
180 
181  explicit DebyeFormula(const json& j)
182  : DebyeFormula(j.at("qmin").get<double>(), j.at("qmax").get<double>(),
183  j.at("dq").get<double>(), j.value("cutoff", r_cutoff_infty)) {};
184 
200  template <class Tpvec> void sample(const Tpvec& p, const T weight = 1, const T volume = -1)
201  {
202  const int N = (int)p.size(); // number of particles
203  const int M = (int)intensity.size(); // number of mesh points
204  std::vector<T> intensity_sum(M, 0.0);
205 
206 // Allow parallelization with a hand-written reduction of intensity_sum at the end.
207 // https://gcc.gnu.org/gcc-9/porting_to.html#ompdatasharing
208 // #pragma omp parallel default(none) shared(N, M) shared(geo, r_cutoff, p) shared(intensity_sum)
209 #pragma omp parallel default(shared) shared(intensity_sum)
210  {
211  std::vector<T> intensity_sum_private(M, 0.0); // a temporal private intensity_sum
212 #pragma omp for schedule(dynamic)
213  for (int i = 0; i < N - 1; ++i) {
214  for (int j = i + 1; j < N; ++j) {
215  T r = T(Faunus::Geometry::Sphere::sqdist(
216  getPosition(p[i]), getPosition(p[j]))); // the square root follows
217  if (r < r_cutoff * r_cutoff) {
218  r = std::sqrt(r);
219  // Black magic: The q_mesh function must be inlineable otherwise the loop
220  // cannot be unrolled using advanced SIMD instructions leading to a huge
221  // performance penalty (a factor of 4). The unrolled loop uses a different
222  // sin implementation, which may be spotted when profiling.
223  // TODO: Optimize also for other compilers than GCC by using a vector math
224  // library, e.g.,
225  // TODO: https://github.com/vectorclass/version2
226  // #pragma GCC unroll 16 // for diagnostics, GCC issues warning when cannot
227  // unroll
228  for (int m = 0; m < M; ++m) {
229  const T q = q_mesh(m);
230  intensity_sum_private[m] += form_factor(q, p[i]) *
231  form_factor(q, p[j]) * std::sin(q * r) /
232  (q * r);
233  }
234  }
235  }
236  }
237 // reduce intensity_sum_private into intensity_sum
238 #pragma omp critical
239  std::transform(intensity_sum.begin(), intensity_sum.end(),
240  intensity_sum_private.begin(), intensity_sum.begin(), std::plus<T>());
241  }
242 
243 // https://gcc.gnu.org/gcc-9/porting_to.html#ompdatasharing
244 // #pragma omp parallel for default(none) shared(N, M, weight, volume) shared(p, r_cutoff,
245 // intensity_sum) shared(sampling, intensity)
246 #pragma omp parallel for shared(sampling, intensity)
247  for (int m = 0; m < M; ++m) {
248  const T q = q_mesh(m);
249  T intensity_self_sum = 0;
250  for (int i = 0; i < N; ++i) {
251  intensity_self_sum += std::pow(form_factor(q, p[i]), 2);
252  }
253  T intensity_corr = 0;
254  if (r_cutoff < r_cutoff_infty && volume > 0) {
255  intensity_corr = 4 * pc::pi * N / (volume * std::pow(q, 3)) *
256  (q * r_cutoff * std::cos(q * r_cutoff) - std::sin(q * r_cutoff));
257  }
258  sampling[m] += weight;
259  if (intensity_self_sum != T{0}) {
260  intensity[m] +=
261  ((2 * intensity_sum[m] + intensity_self_sum) / intensity_self_sum +
262  intensity_corr) *
263  weight;
264  }
265  }
266  }
267 
271  auto getQMeshParameters() { return std::make_tuple(q_mesh_min, q_mesh_max, q_mesh_step); }
272 
277  {
278  std::map<T, T> averaged_intensity;
279  for (size_t m = 0; m < intensity.size(); ++m) {
280  const T average = intensity[m] / (sampling[m] != T(0.0) ? sampling[m] : T(1.0));
281  averaged_intensity.emplace(q_mesh(m), average);
282  }
283  return averaged_intensity;
284  }
285 };
286 
287 #pragma GCC diagnostic pop
288 
294 template <std::floating_point T> class SamplingPolicy
295 {
296  public:
298  {
299  T value;
300  T weight;
301  };
302 
303  typedef std::map<T, sampled_value> TSampledValueMap;
304 
305  private:
306  TSampledValueMap samples;
307  const T precision = 10000.0;
308 
309  public:
310  std::map<T, T> getSampling() const
311  {
312  std::map<T, T> average;
313  for (auto [key, sample] : samples) {
314  average.emplace(key, sample.value / sample.weight);
315  }
316  return average;
317  }
318 
324  void addSampling(T key_approx, T value, T weight = 1.0)
325  {
326  const T key =
327  std::round(key_approx * precision) / precision; // round |q| for better binning
328  samples[key].value += value * weight;
329  samples[key].weight += weight;
330  }
331 };
332 
341 template <std::floating_point T>
342 std::map<T, T> averageByMagnitude(const std::vector<std::pair<T, T>>& pairs,
343  T precision = T{10000})
344 {
345  struct Accumulator
346  {
347  T sum = T{0};
348  int count = 0;
349  };
350  std::map<T, Accumulator> bins;
351  for (const auto& [key, value] : pairs) {
352  const T rounded = std::round(key * precision) / precision;
353  bins[rounded].sum += value;
354  bins[rounded].count++;
355  }
356  std::map<T, T> result;
357  for (const auto& [key, acc] : bins) {
358  result[key] = acc.sum / static_cast<T>(acc.count);
359  }
360  return result;
361 }
362 
376 template <class Tformfactor = FormFactorUnity<double>, typename T = double, Algorithm method = SIMD,
377  typename TSamplingPolicy = SamplingPolicy<T>>
378 class StructureFactorPBC : private TSamplingPolicy
379 {
381  const std::vector<Point> directions = {
382  {1, 0, 0}, {0, 1, 0}, {0, 0, 1}, // 3 permutations
383  {1, 1, 0}, {0, 1, 1}, {1, 0, 1}, {-1, 1, 0}, {-1, 0, 1}, {0, -1, 1}, // 6 permutations
384  {1, 1, 1}, {-1, 1, 1}, {1, -1, 1}, {1, 1, -1} // 4 permutations
385  };
386 
387  const int p_max;
388  Tformfactor form_factor;
389  using TSamplingPolicy::addSampling;
390 
391  public:
392  StructureFactorPBC(int q_multiplier)
393  : p_max(q_multiplier)
394  {
395  }
396 
402  template <typename Tscatterers>
403  void sample(const Tscatterers& scatterers, const Point& boxlength)
404  {
405  const auto n = directions.size() * static_cast<size_t>(p_max);
406  std::vector<std::pair<T, T>> q_intensity(n);
407 
408 #pragma omp parallel for collapse(2) default(shared)
409  for (size_t i = 0; i < directions.size(); ++i) {
410  for (int p = 1; p <= p_max; ++p) {
411  const Point q =
412  2.0 * pc::pi * p * directions[i].cwiseQuotient(boxlength);
413  q_intensity[i * static_cast<size_t>(p_max) + static_cast<size_t>(p - 1)] =
414  {static_cast<T>(q.norm()), calculateIntensity(scatterers, q)};
415  }
416  }
417 
418  for (const auto& [q, intensity] : averageByMagnitude(q_intensity)) {
419  addSampling(q, intensity);
420  }
421  }
422 
423  template <typename Tscatterers>
424  T calculateIntensity(const Tscatterers& scatterers, const Point& q) const
425  {
426  T sum_cos = 0.0;
427  T sum_sin = 0.0;
428  T sum_f_squared = 0.0;
429  const auto q_norm = q.norm();
430  for (const auto& scatterer : scatterers) {
431  const auto& pos = getPosition(scatterer);
432  const auto f = form_factor(q_norm, scatterer);
433  const auto qr = static_cast<T>(q.dot(pos));
434  sum_cos += f * cos(qr);
435  sum_sin += f * sin(qr);
436  sum_f_squared += f * f;
437  }
438  if (sum_f_squared == T{0}) {
439  return T{0};
440  }
441  return std::norm(std::complex<T>(sum_cos, sum_sin)) / sum_f_squared;
442  }
443 
444  int getQMultiplier() { return p_max; }
445 
446  using TSamplingPolicy::getSampling;
447 };
448 
456 template <class Tformfactor = FormFactorUnity<float>, std::floating_point T = float,
457  typename TSamplingPolicy = SamplingPolicy<T>>
458 class StructureFactorIPBC : private TSamplingPolicy
459 {
462  std::vector<Point> directions = {{1, 0, 0}, {1, 1, 0}, {1, 1, 1}};
463 
464  int p_max;
465  Tformfactor form_factor;
466  using TSamplingPolicy::addSampling;
467 
468  public:
469  explicit StructureFactorIPBC(int q_multiplier)
470  : p_max(q_multiplier)
471  {
472  }
473 
474  template <typename Tscatterers>
475  void sample(const Tscatterers& scatterers, const Point& boxlength)
476  {
477  const auto n = directions.size() * static_cast<size_t>(p_max);
478  std::vector<std::pair<T, T>> q_intensity(n);
479 
480 // https://gcc.gnu.org/gcc-9/porting_to.html#ompdatasharing
481 // #pragma omp parallel for collapse(2) default(none) shared(directions, p_max, scatterers,
482 // boxlength)
483 #pragma omp parallel for collapse(2) default(shared)
484  for (size_t i = 0; i < directions.size(); ++i) {
485  for (int p = 1; p <= p_max; ++p) { // loop over multiples of q
486  const Point q =
487  2.0 * pc::pi * p * directions[i].cwiseQuotient(boxlength); // scattering vector
488  T sum_f_cos = 0;
489  T sum_f_squared = 0;
490  const auto q_norm = q.norm();
491  for (const auto& scatterer : scatterers) {
492  const auto& r = getPosition(scatterer);
493  const auto f = form_factor(q_norm, scatterer);
494  // if q[i] == 0 then its cosine == 1 hence we can avoid cosine computation for
495  // performance reasons
496  T product = std::cos(T(q[0] * r[0]));
497  if (q[1] != 0)
498  product *= std::cos(T(q[1] * r[1]));
499  if (q[2] != 0)
500  product *= std::cos(T(q[2] * r[2]));
501  sum_f_cos += f * product;
502  sum_f_squared += f * f;
503  }
504  const T ipbc_factor =
505  std::pow(2, directions[i].count()); // 2 ^ number of non-zero elements
506  T intensity = T{0};
507  if (sum_f_squared != T{0}) {
508  intensity = (sum_f_cos * sum_f_cos) / sum_f_squared * ipbc_factor;
509  }
510  q_intensity[i * static_cast<size_t>(p_max) + static_cast<size_t>(p - 1)] =
511  {static_cast<T>(q_norm), intensity};
512  }
513  }
514 
515  for (const auto& [q, intensity] : averageByMagnitude(q_intensity)) {
516  addSampling(q, intensity);
517  }
518  }
519 
520  int getQMultiplier() { return p_max; }
521 
522  using TSamplingPolicy::getSampling;
523 };
524 
525 } // namespace Scatter
526 } // namespace Faunus
Calculate scattering intensity using explicit q averaging in isotropic periodic boundary conditions (...
Definition: scatter.h:458
nlohmann::json json
JSON object.
Definition: json_support.h:10
Eigen::Vector3d Point
3D vector used for positions, velocities, forces etc.
Definition: coordinates.h:7
Unity form factor (q independent).
Definition: scatter.h:51
double T
floating point size
Definition: units.h:73
void sample(const Tpvec &p, const T weight=1, const T volume=-1)
Sample I(q) and add to average.
Definition: scatter.h:200
std::vector< Faunus::AtomData > atoms
Global instance of atom list.
Definition: atomdata.cpp:242
A policy for collecting samples.
Definition: scatter.h:294
std::map< T, T > averageByMagnitude(const std::vector< std::pair< T, T >> &pairs, T precision=T{10000})
Average values with equivalent keys (same rounded magnitude).
Definition: scatter.h:342
auto getQMeshParameters()
Definition: scatter.h:271
Calculate scattering intensity, I(q), on a mesh using the Debye formula.
Definition: scatter.h:125
auto getIntensity()
Definition: scatter.h:276
Cell list class templates.
Definition: actions.cpp:11
T operator()(T q, const Tparticle &a) const
Definition: scatter.h:39
void addSampling(T key_approx, T value, T weight=1.0)
Definition: scatter.h:324
TOut numeric_cast(const TIn number)
Convert floating point number to integral number.
Definition: auxiliary.h:30
Atom-specific constant form factor (q independent).
Definition: scatter.h:95
void sample(const Tscatterers &scatterers, const Point &boxlength)
https://gcc.gnu.org/gcc-9/porting_to.html#ompdatasharing #pragma omp parallel for collapse(2) default...
Definition: scatter.h:403
constexpr const auto & getPosition(const T &scatterer)
Helper to extract position from a scatterer or point.
Definition: scatter.h:78
Form factor, F(q), for a hard sphere of radius R.
Definition: scatter.h:23
Lightweight scatterer holding position and atom type id.
Definition: scatter.h:65
Calculate scattering intensity using explicit q averaging.
Definition: scatter.h:378