74ccf9aeb41779388eba378739c2e34be9c0e135
[alexxy/gromacs.git] / src / gromacs / math / gausstransform.cpp
1 /*
2  * This file is part of the GROMACS molecular simulation package.
3  *
4  * Copyright (c) 2019,2020,2021, by the GROMACS development team, led by
5  * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
6  * and including many others, as listed in the AUTHORS file in the
7  * top-level source directory and at http://www.gromacs.org.
8  *
9  * GROMACS is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU Lesser General Public License
11  * as published by the Free Software Foundation; either version 2.1
12  * of the License, or (at your option) any later version.
13  *
14  * GROMACS is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17  * Lesser General Public License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with GROMACS; if not, see
21  * http://www.gnu.org/licenses, or write to the Free Software Foundation,
22  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
23  *
24  * If you want to redistribute modifications to GROMACS, please
25  * consider that scientific software is very special. Version
26  * control is crucial - bugs must be traceable. We will be happy to
27  * consider code for inclusion in the official distribution, but
28  * derived work must not be called official GROMACS. Details are found
29  * in the README & COPYING files - if they are missing, get the
30  * official version at http://www.gromacs.org.
31  *
32  * To help us fund GROMACS development, we humbly ask that you cite
33  * the research papers on the package. Check out http://www.gromacs.org.
34  */
35 /*! \internal \file
36  * \brief
37  * Implements Gaussian function evaluations on lattices and related functionality
38  *
39  * \author Christian Blau <blau@kth.se>
40  *
41  * \ingroup module_math
42  */
43 #include "gmxpre.h"
44
45 #include "gausstransform.h"
46
47 #include <cmath>
48
49 #include <algorithm>
50 #include <array>
51
52 #include "gromacs/math/functions.h"
53 #include "gromacs/math/multidimarray.h"
54 #include "gromacs/math/units.h"
55 #include "gromacs/math/utilities.h"
56
57 namespace gmx
58 {
59
60 /********************************************************************
61  * GaussianOn1DLattice::Impl
62  */
63
64 class GaussianOn1DLattice::Impl
65 {
66 public:
67     Impl(int numGridPointsForSpreadingHalfWidth, real sigma);
68     ~Impl()                 = default;
69     Impl(const Impl& other) = default;
70     Impl& operator=(const Impl& other) = default;
71
72     /*! \brief evaluate Gaussian function at all lattice points
73      * \param[in] amplitude the amplitude of the Gaussian
74      * \param[in] dx distance from the center
75      */
76     void spread(double amplitude, real dx);
77     //! Largest distance in number of gridpoints from 0
78     int numGridPointsForSpreadingHalfWidth_;
79     /*! \brief Avoid overflow for E2^offset and underflow for E3(i).
80      *
81      * Occurs when sigma is much smaller than numGridPointsForSpreadingHalfWidth_.
82      *
83      * E2^offset smaller than maximum float requires
84      * \f$exp(dx / (2*square(sigma))^numGridPointsForSpreadingHalfWidth_ \leq max_float \f$
85      * The maximum expected distance of the Gaussian center to the next lattice point is dx = 0.5,
86      * thus the maximum spread distance here is \f$4 * sigma^2 * \log(\mathrm{maxfloat})\f$ .
87      *
88      * E3(i) larger than minmium float requires
89      * exp(i^2 / 2*(sigma)^2) > min_float
90      * Thus the maximum spread distance here is \f$\sigma \sqrt(-2\log(\mathrm{minfloat}))\f$
91      */
92     int maxEvaluatedSpreadDistance_;
93     //! Width of the Gaussian function
94     double sigma_;
95     //! The result of the spreading calculation
96     std::vector<float> spreadingResult_;
97     //! Pre-calculated exp(-gridIndex^2/2 * (sigma^2)) named as in Greengard2004
98     std::vector<float> e3_;
99     /*! \brief Equal to std::floor(std::log(std::numeric_limits<float>::max())).
100      * Above expression is not constexpr and a const variable would implicitly delete default copy
101      * assignment. Therefore resorting to setting number manually.
102      */
103     static constexpr double c_logMaxFloat = 88.72284;
104     static constexpr double c_logMinFloat = -87.33654;
105 };
106
107 GaussianOn1DLattice::Impl::Impl(int numGridPointsForSpreadingHalfWidth, real sigma) :
108     numGridPointsForSpreadingHalfWidth_(numGridPointsForSpreadingHalfWidth),
109     sigma_(sigma),
110     spreadingResult_(2 * numGridPointsForSpreadingHalfWidth + 1)
111 {
112     maxEvaluatedSpreadDistance_ =
113             std::min(numGridPointsForSpreadingHalfWidth_,
114                      static_cast<int>(std::floor(4 * square(sigma) * c_logMaxFloat)) - 1);
115     maxEvaluatedSpreadDistance_ =
116             std::min(maxEvaluatedSpreadDistance_,
117                      static_cast<int>(std::floor(sigma * sqrt(-2.0 * c_logMinFloat))) - 1);
118
119     std::generate_n(
120             std::back_inserter(e3_), maxEvaluatedSpreadDistance_ + 1, [sigma, latticeIndex = 0]() mutable {
121                 return std::exp(-0.5 * square(latticeIndex++ / sigma));
122             });
123
124     std::fill(std::begin(spreadingResult_), std::end(spreadingResult_), 0.);
125 };
126
127 void GaussianOn1DLattice::Impl::spread(double amplitude, real dx)
128 {
129     /* The spreading routine implements the fast gaussian gridding as in
130      *
131      * Leslie Greengard and June-Yub Lee,
132      * "Accelerating the Nonuniform Fast Fourier Transform"
133      * SIAM REV 2004 Vol. 46, No. 3, pp. 443-454 DOI. 10.1137/S003614450343200X
134      *
135      * Following the naming conventions for e1, e2 and e3, nu = 1, m = numGridPointsForSpreadingHalfWidth_.
136      *
137      * Speed up is achieved by factorization of the exponential that is evaluted
138      * at regular lattice points i, where the distance from the
139      * Gaussian center is \f$x-i\f$:
140      *
141      * \f[
142      *      a * \exp(-(x^2-2*i*x+ i^2)/(2*\sigma^2)) =
143      *      a * \exp(-x^2/2*\sigma^2) * \exp(x/\sigma^2)^i * \exp(i/2*sigma^2) =
144      *      e_1(x) * e_2(x)^i * e_3(i)
145      * \f]
146      *
147      * Requiring only two exp evaluations per spreading operation.
148      *
149      */
150     const double e1 = amplitude * exp(-0.5 * dx * dx / square(sigma_)) / (sqrt(2 * M_PI) * sigma_);
151     spreadingResult_[numGridPointsForSpreadingHalfWidth_] = e1;
152
153     const double e2 = exp(dx / square(sigma_));
154
155     double e2pow = e2; //< powers of e2, e2^offset
156
157     // Move outwards from mid-point, using e2pow value for both points simultaneously
158     //      o    o    o<----O---->o    o    o
159     for (int offset = 1; offset < maxEvaluatedSpreadDistance_; offset++)
160     {
161         const double e1_3                                              = e1 * e3_[offset];
162         spreadingResult_[numGridPointsForSpreadingHalfWidth_ + offset] = e1_3 * e2pow;
163         spreadingResult_[numGridPointsForSpreadingHalfWidth_ - offset] = e1_3 / e2pow;
164         e2pow *= e2;
165     }
166     // separate statement for gridpoints at the end of the range avoids
167     // overflow for large sigma and saves one e2 multiplication operation
168     spreadingResult_[numGridPointsForSpreadingHalfWidth_ - maxEvaluatedSpreadDistance_] =
169             (e1 / e2pow) * e3_[maxEvaluatedSpreadDistance_];
170     spreadingResult_[numGridPointsForSpreadingHalfWidth_ + maxEvaluatedSpreadDistance_] =
171             (e1 * e2pow) * e3_[maxEvaluatedSpreadDistance_];
172 }
173
174 /********************************************************************
175  * GaussianOn1DLattice
176  */
177
178 GaussianOn1DLattice::GaussianOn1DLattice(int numGridPointsForSpreadingHalfWidth_, real sigma) :
179     impl_(new Impl(numGridPointsForSpreadingHalfWidth_, sigma))
180 {
181 }
182
183 GaussianOn1DLattice::~GaussianOn1DLattice() {}
184
185 void GaussianOn1DLattice::spread(double amplitude, real dx)
186 {
187     impl_->spread(amplitude, dx);
188 }
189
190 ArrayRef<const float> GaussianOn1DLattice::view()
191 {
192     return impl_->spreadingResult_;
193 }
194
195 GaussianOn1DLattice::GaussianOn1DLattice(const GaussianOn1DLattice& other) :
196     impl_(new Impl(*other.impl_))
197 {
198 }
199
200 GaussianOn1DLattice& GaussianOn1DLattice::operator=(const GaussianOn1DLattice& other)
201 {
202     *impl_ = *other.impl_;
203     return *this;
204 }
205
206 GaussianOn1DLattice::GaussianOn1DLattice(GaussianOn1DLattice&&) noexcept = default;
207
208 GaussianOn1DLattice& GaussianOn1DLattice::operator=(GaussianOn1DLattice&&) noexcept = default;
209
210 namespace
211 {
212
213 //! rounds real-valued coordinate to the closest integer values
214 IVec closestIntegerPoint(const RVec& coordinate)
215 {
216     return { roundToInt(coordinate[XX]), roundToInt(coordinate[YY]), roundToInt(coordinate[ZZ]) };
217 }
218
219 /*! \brief Substracts a range from a three-dimensional integer coordinate and ensures
220  * the resulting coordinate is within a lattice.
221  * \param[in] index point in lattice
222  * \param[in] range to be shifted
223  * \returns Shifted index or zero if shifted index is smaller than zero.
224  */
225 IVec rangeBeginWithinLattice(const IVec& index, const IVec& range)
226 {
227     return elementWiseMax({ 0, 0, 0 }, index - range);
228 }
229
230 /*! \brief Adds a range from a three-dimensional integer coordinate and ensures
231  * the resulting coordinate is within a lattice.
232  * \param[in] index point in lattice
233  * \param[in] extents extent of the lattice
234  * \param[in] range to be shifted
235  * \returns Shifted index or the lattice extent if shifted index is larger than the extent
236  */
237 IVec rangeEndWithinLattice(const IVec& index, const dynamicExtents3D& extents, const IVec& range)
238 {
239     IVec extentAsIvec(static_cast<int>(extents.extent(ZZ)),
240                       static_cast<int>(extents.extent(YY)),
241                       static_cast<int>(extents.extent(XX)));
242     return elementWiseMin(extentAsIvec, index + range);
243 }
244
245
246 } // namespace
247
248 /********************************************************************
249  * OuterProductEvaluator
250  */
251
252 mdspan<const float, dynamic_extent, dynamic_extent> OuterProductEvaluator::
253                                                     operator()(ArrayRef<const float> x, ArrayRef<const float> y)
254 {
255     data_.resize(ssize(x), ssize(y));
256     for (gmx::index xIndex = 0; xIndex < ssize(x); ++xIndex)
257     {
258         const auto xValue = x[xIndex];
259         std::transform(std::begin(y), std::end(y), begin(data_.asView()[xIndex]), [xValue](float yValue) {
260             return xValue * yValue;
261         });
262     }
263     return data_.asConstView();
264 }
265
266 /********************************************************************
267  * IntegerBox
268  */
269
270 IntegerBox::IntegerBox(const IVec& begin, const IVec& end) : begin_{ begin }, end_{ end } {}
271
272 const IVec& IntegerBox::begin() const
273 {
274     return begin_;
275 }
276 const IVec& IntegerBox::end() const
277 {
278     return end_;
279 }
280
281 bool IntegerBox::empty() const
282 {
283     return !((begin_[XX] < end_[XX]) && (begin_[YY] < end_[YY]) && (begin_[ZZ] < end_[ZZ]));
284 }
285
286 IntegerBox spreadRangeWithinLattice(const IVec& center, dynamicExtents3D extent, IVec range)
287 {
288     const IVec begin = rangeBeginWithinLattice(center, range);
289     const IVec end   = rangeEndWithinLattice(center, extent, range);
290     return { begin, end };
291 }
292 /********************************************************************
293  * GaussianSpreadKernel
294  */
295
296 IVec GaussianSpreadKernelParameters::Shape::latticeSpreadRange() const
297 {
298     DVec range(std::ceil(sigma_[XX] * spreadWidthMultiplesOfSigma_),
299                std::ceil(sigma_[YY] * spreadWidthMultiplesOfSigma_),
300                std::ceil(sigma_[ZZ] * spreadWidthMultiplesOfSigma_));
301     return range.toIVec();
302 }
303
304 /********************************************************************
305  * GaussTransform3D::Impl
306  */
307
308 /*! \internal \brief
309  * Private implementation class for GaussTransform3D.
310  */
311 class GaussTransform3D::Impl
312 {
313 public:
314     //! Construct from extent and spreading width and range
315     Impl(const dynamicExtents3D& extent, const GaussianSpreadKernelParameters::Shape& kernelShapeParameters);
316     ~Impl() = default;
317     //! Copy constructor
318     Impl(const Impl& other) = default;
319     //! Copy assignment
320     Impl& operator=(const Impl& other) = default;
321     //! Add another gaussian
322     void add(const GaussianSpreadKernelParameters::PositionAndAmplitude& localParamters);
323     //! The width of the Gaussian in lattice spacing units
324     BasicVector<double> sigma_;
325     //! The spread range in lattice points
326     IVec spreadRange_;
327     //! The result of the Gauss transform
328     MultiDimArray<std::vector<float>, dynamicExtents3D> data_;
329     //! The outer product of a Gaussian along the z and y dimension
330     OuterProductEvaluator outerProductZY_;
331     //! The three one-dimensional Gaussians, whose outer product is added to the Gauss transform
332     std::array<GaussianOn1DLattice, DIM> gauss1d_;
333 };
334
335 GaussTransform3D::Impl::Impl(const dynamicExtents3D&                      extent,
336                              const GaussianSpreadKernelParameters::Shape& kernelShapeParameters) :
337     sigma_{ kernelShapeParameters.sigma_ },
338     spreadRange_{ kernelShapeParameters.latticeSpreadRange() },
339     data_{ extent },
340     gauss1d_({ GaussianOn1DLattice(spreadRange_[XX], sigma_[XX]),
341                GaussianOn1DLattice(spreadRange_[YY], sigma_[YY]),
342                GaussianOn1DLattice(spreadRange_[ZZ], sigma_[ZZ]) })
343 {
344 }
345
346 void GaussTransform3D::Impl::add(const GaussianSpreadKernelParameters::PositionAndAmplitude& localParameters)
347 {
348     const IVec closestLatticePoint = closestIntegerPoint(localParameters.coordinate_);
349     const auto spreadRange =
350             spreadRangeWithinLattice(closestLatticePoint, data_.asView().extents(), spreadRange_);
351
352     // do nothing if the added Gaussian will never reach the lattice
353     if (spreadRange.empty())
354     {
355         return;
356     }
357
358     for (int dimension = XX; dimension <= ZZ; ++dimension)
359     {
360         // multiply with amplitude so that Gauss3D = (amplitude * Gauss_x) * Gauss_y * Gauss_z
361         const float gauss1DAmplitude = dimension > XX ? 1.0 : localParameters.amplitude_;
362         gauss1d_[dimension].spread(
363                 gauss1DAmplitude, localParameters.coordinate_[dimension] - closestLatticePoint[dimension]);
364     }
365
366     const auto spreadZY         = outerProductZY_(gauss1d_[ZZ].view(), gauss1d_[YY].view());
367     const auto spreadX          = gauss1d_[XX].view();
368     const IVec spreadGridOffset = spreadRange_ - closestLatticePoint;
369
370     // \todo optimize these loops if performance critical
371     // The looping strategy uses that the last, x-dimension is contiguous in the memory layout
372     for (int zLatticeIndex = spreadRange.begin()[ZZ]; zLatticeIndex < spreadRange.end()[ZZ]; ++zLatticeIndex)
373     {
374         const auto zSlice = data_.asView()[zLatticeIndex];
375
376         for (int yLatticeIndex = spreadRange.begin()[YY]; yLatticeIndex < spreadRange.end()[YY]; ++yLatticeIndex)
377         {
378             const auto  ySlice      = zSlice[yLatticeIndex];
379             const float zyPrefactor = spreadZY(zLatticeIndex + spreadGridOffset[ZZ],
380                                                yLatticeIndex + spreadGridOffset[YY]);
381
382             for (int xLatticeIndex = spreadRange.begin()[XX]; xLatticeIndex < spreadRange.end()[XX];
383                  ++xLatticeIndex)
384             {
385                 const float xPrefactor = spreadX[xLatticeIndex + spreadGridOffset[XX]];
386                 ySlice[xLatticeIndex] += zyPrefactor * xPrefactor;
387             }
388         }
389     }
390 }
391
392 /********************************************************************
393  * GaussTransform3D
394  */
395
396 GaussTransform3D::GaussTransform3D(const dynamicExtents3D&                      extent,
397                                    const GaussianSpreadKernelParameters::Shape& kernelShapeParameters) :
398     impl_(new Impl(extent, kernelShapeParameters))
399 {
400 }
401
402 void GaussTransform3D::add(const GaussianSpreadKernelParameters::PositionAndAmplitude& localParameters)
403 {
404     impl_->add(localParameters);
405 }
406
407 void GaussTransform3D::setZero()
408 {
409     std::fill(begin(impl_->data_), end(impl_->data_), 0.);
410 }
411
412 basic_mdspan<float, dynamicExtents3D> GaussTransform3D::view()
413 {
414     return impl_->data_.asView();
415 }
416
417 basic_mdspan<const float, dynamicExtents3D> GaussTransform3D::constView() const
418 {
419     return impl_->data_.asConstView();
420 }
421
422 GaussTransform3D::~GaussTransform3D() {}
423
424 GaussTransform3D::GaussTransform3D(const GaussTransform3D& other) : impl_(new Impl(*other.impl_)) {}
425
426 GaussTransform3D& GaussTransform3D::operator=(const GaussTransform3D& other)
427 {
428     *impl_ = *other.impl_;
429     return *this;
430 }
431
432 GaussTransform3D::GaussTransform3D(GaussTransform3D&&) noexcept = default;
433
434 GaussTransform3D& GaussTransform3D::operator=(GaussTransform3D&&) noexcept = default;
435
436 } // namespace gmx