Implement PME solve in SYCL
[alexxy/gromacs.git] / src / gromacs / ewald / tests / pmesolvetest.cpp
1 /*
2  * This file is part of the GROMACS molecular simulation package.
3  *
4  * Copyright (c) 2016,2017,2018,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 PME solving tests.
38  *
39  * \author Aleksei Iupinov <a.yupinov@gmail.com>
40  * \ingroup module_ewald
41  */
42
43 #include "gmxpre.h"
44
45 #include <string>
46
47 #include <gmock/gmock.h>
48
49 #include "gromacs/mdtypes/inputrec.h"
50 #include "gromacs/utility/stringutil.h"
51
52 #include "testutils/refdata.h"
53 #include "testutils/test_hardware_environment.h"
54 #include "testutils/testasserts.h"
55
56 #include "pmetestcommon.h"
57
58 namespace gmx
59 {
60 namespace test
61 {
62 namespace
63 {
64 /*! \brief Convenience typedef of the test input parameters - unit cell box, complex grid dimensions, complex grid values,
65  * electrostatic constant epsilon_r, Ewald splitting parameters ewaldcoeff_q and ewaldcoeff_lj, solver type
66  * Output: transformed local grid (Fourier space); optionally reciprocal energy and virial matrix.
67  * TODO:
68  * Implement and test Lorentz-Berthelot
69  */
70 typedef std::tuple<Matrix3x3, IVec, SparseComplexGridValuesInput, double, double, double, PmeSolveAlgorithm> SolveInputParameters;
71
72 //! Test fixture
73 class PmeSolveTest : public ::testing::TestWithParam<SolveInputParameters>
74 {
75 public:
76     PmeSolveTest() = default;
77
78     //! Sets the programs once
79     static void SetUpTestSuite()
80     {
81         s_pmeTestHardwareContexts    = createPmeTestHardwareContextList();
82         g_allowPmeWithSyclForTesting = true; // We support PmeSolve with SYCL
83     }
84
85     static void TearDownTestSuite()
86     {
87         // Revert the value back.
88         g_allowPmeWithSyclForTesting = false;
89     }
90
91     //! The test
92     static void runTest()
93     {
94         /* Getting the input */
95         Matrix3x3                    box;
96         IVec                         gridSize;
97         SparseComplexGridValuesInput nonZeroGridValues;
98         double                       epsilon_r;
99         double                       ewaldCoeff_q;
100         double                       ewaldCoeff_lj;
101         PmeSolveAlgorithm            method;
102         std::tie(box, gridSize, nonZeroGridValues, epsilon_r, ewaldCoeff_q, ewaldCoeff_lj, method) =
103                 GetParam();
104
105         /* Storing the input where it's needed, running the test */
106         t_inputrec inputRec;
107         inputRec.nkx         = gridSize[XX];
108         inputRec.nky         = gridSize[YY];
109         inputRec.nkz         = gridSize[ZZ];
110         inputRec.pme_order   = 4;
111         inputRec.coulombtype = CoulombInteractionType::Pme;
112         inputRec.epsilon_r   = epsilon_r;
113         switch (method)
114         {
115             case PmeSolveAlgorithm::Coulomb: break;
116
117             case PmeSolveAlgorithm::LennardJones: inputRec.vdwtype = VanDerWaalsType::Pme; break;
118
119             default: GMX_THROW(InternalError("Unknown PME solver"));
120         }
121
122         TestReferenceData refData;
123         for (const auto& pmeTestHardwareContext : s_pmeTestHardwareContexts)
124         {
125             pmeTestHardwareContext->activate();
126             CodePath   codePath       = pmeTestHardwareContext->codePath();
127             const bool supportedInput = pmeSupportsInputForMode(
128                     *getTestHardwareEnvironment()->hwinfo(), &inputRec, codePath);
129             if (!supportedInput)
130             {
131                 /* Testing the failure for the unsupported input */
132                 EXPECT_THROW_GMX(
133                         pmeInitWrapper(&inputRec, codePath, nullptr, nullptr, nullptr, box, ewaldCoeff_q, ewaldCoeff_lj),
134                         NotImplementedError);
135                 continue;
136             }
137
138             std::map<GridOrdering, std::string> gridOrderingsToTest = { { GridOrdering::YZX,
139                                                                           "YZX" } };
140             if (codePath == CodePath::GPU)
141             {
142                 gridOrderingsToTest[GridOrdering::XYZ] = "XYZ";
143             }
144             for (const auto& gridOrdering : gridOrderingsToTest)
145             {
146                 for (bool computeEnergyAndVirial : { false, true })
147                 {
148                     /* Describing the test*/
149                     SCOPED_TRACE(formatString(
150                             "Testing solving (%s, %s, %s energy/virial) on %s for PME grid "
151                             "size %d %d %d, Ewald coefficients %g %g",
152                             (method == PmeSolveAlgorithm::LennardJones) ? "Lennard-Jones" : "Coulomb",
153                             gridOrdering.second.c_str(),
154                             computeEnergyAndVirial ? "with" : "without",
155                             pmeTestHardwareContext->description().c_str(),
156                             gridSize[XX],
157                             gridSize[YY],
158                             gridSize[ZZ],
159                             ewaldCoeff_q,
160                             ewaldCoeff_lj));
161
162                     /* Running the test */
163                     PmeSafePointer pmeSafe = pmeInitWrapper(&inputRec,
164                                                             codePath,
165                                                             pmeTestHardwareContext->deviceContext(),
166                                                             pmeTestHardwareContext->deviceStream(),
167                                                             pmeTestHardwareContext->pmeGpuProgram(),
168                                                             box,
169                                                             ewaldCoeff_q,
170                                                             ewaldCoeff_lj);
171                     pmeSetComplexGrid(pmeSafe.get(), codePath, gridOrdering.first, nonZeroGridValues);
172                     const real cellVolume = box[0] * box[4] * box[8];
173                     // FIXME - this is box[XX][XX] * box[YY][YY] * box[ZZ][ZZ], should be stored in the PME structure
174                     pmePerformSolve(pmeSafe.get(), codePath, method, cellVolume, gridOrdering.first, computeEnergyAndVirial);
175                     pmeFinalizeTest(pmeSafe.get(), codePath);
176
177                     /* Check the outputs */
178                     TestReferenceChecker checker(refData.rootChecker());
179
180                     SparseComplexGridValuesOutput nonZeroGridValuesOutput =
181                             pmeGetComplexGrid(pmeSafe.get(), codePath, gridOrdering.first);
182                     /* Transformed grid */
183                     TestReferenceChecker gridValuesChecker(
184                             checker.checkCompound("NonZeroGridValues", "ComplexSpaceGrid"));
185
186                     real gridValuesMagnitude = 1.0;
187                     for (const auto& point : nonZeroGridValuesOutput)
188                     {
189                         gridValuesMagnitude = std::max(std::fabs(point.second.re), gridValuesMagnitude);
190                         gridValuesMagnitude = std::max(std::fabs(point.second.im), gridValuesMagnitude);
191                     }
192                     // Spline moduli participate 3 times in the computation; 2 is an additional factor for SIMD exp() precision
193                     uint64_t gridUlpToleranceFactor = DIM * 2;
194                     if (method == PmeSolveAlgorithm::LennardJones)
195                     {
196                         // Lennard Jones is more complex and also uses erfc(), relax more
197                         gridUlpToleranceFactor *= 2;
198                     }
199                     const uint64_t splineModuliDoublePrecisionUlps =
200                             getSplineModuliDoublePrecisionUlps(inputRec.pme_order + 1);
201                     auto gridTolerance = relativeToleranceAsPrecisionDependentUlp(
202                             gridValuesMagnitude,
203                             gridUlpToleranceFactor * c_splineModuliSinglePrecisionUlps,
204                             gridUlpToleranceFactor * splineModuliDoublePrecisionUlps);
205                     gridValuesChecker.setDefaultTolerance(gridTolerance);
206
207                     for (const auto& point : nonZeroGridValuesOutput)
208                     {
209                         // we want an additional safeguard for denormal numbers as they cause an exception in string conversion;
210                         // however, using GMX_REAL_MIN causes an "unused item warning" for single precision builds
211                         if (fabs(point.second.re) >= GMX_FLOAT_MIN)
212                         {
213                             gridValuesChecker.checkReal(point.second.re, (point.first + " re").c_str());
214                         }
215                         if (fabs(point.second.im) >= GMX_FLOAT_MIN)
216                         {
217                             gridValuesChecker.checkReal(point.second.im, (point.first + " im").c_str());
218                         }
219                     }
220
221                     if (computeEnergyAndVirial)
222                     {
223                         // Extract the energy and virial
224                         const auto output =
225                                 pmeGetReciprocalEnergyAndVirial(pmeSafe.get(), codePath, method);
226                         const auto& energy = (method == PmeSolveAlgorithm::Coulomb)
227                                                      ? output.coulombEnergy_
228                                                      : output.lennardJonesEnergy_;
229                         const auto& virial = (method == PmeSolveAlgorithm::Coulomb)
230                                                      ? output.coulombVirial_
231                                                      : output.lennardJonesVirial_;
232
233                         // These quantities are computed based on the grid values, so must have
234                         // checking relative tolerances at least as large. Virial needs more flops
235                         // than energy, so needs a larger tolerance.
236
237                         /* Energy */
238                         double energyMagnitude = 10.0;
239                         // TODO This factor is arbitrary, do a proper error-propagation analysis
240                         uint64_t energyUlpToleranceFactor = gridUlpToleranceFactor * 2;
241                         auto     energyTolerance = relativeToleranceAsPrecisionDependentUlp(
242                                 energyMagnitude,
243                                 energyUlpToleranceFactor * c_splineModuliSinglePrecisionUlps,
244                                 energyUlpToleranceFactor * splineModuliDoublePrecisionUlps);
245                         TestReferenceChecker energyChecker(checker);
246                         energyChecker.setDefaultTolerance(energyTolerance);
247                         energyChecker.checkReal(energy, "Energy");
248
249                         /* Virial */
250                         double virialMagnitude = 1000.0;
251                         // TODO This factor is arbitrary, do a proper error-propagation analysis
252                         uint64_t virialUlpToleranceFactor = energyUlpToleranceFactor * 2;
253                         auto     virialTolerance = relativeToleranceAsPrecisionDependentUlp(
254                                 virialMagnitude,
255                                 virialUlpToleranceFactor * c_splineModuliSinglePrecisionUlps,
256                                 virialUlpToleranceFactor * splineModuliDoublePrecisionUlps);
257                         TestReferenceChecker virialChecker(
258                                 checker.checkCompound("Matrix", "Virial"));
259                         virialChecker.setDefaultTolerance(virialTolerance);
260                         for (int i = 0; i < DIM; i++)
261                         {
262                             for (int j = 0; j <= i; j++)
263                             {
264                                 std::string valueId = formatString("Cell %d %d", i, j);
265                                 virialChecker.checkReal(virial[i][j], valueId.c_str());
266                             }
267                         }
268                     }
269                 }
270             }
271         }
272     }
273
274     static std::vector<std::unique_ptr<PmeTestHardwareContext>> s_pmeTestHardwareContexts;
275 };
276
277 std::vector<std::unique_ptr<PmeTestHardwareContext>> PmeSolveTest::s_pmeTestHardwareContexts;
278
279 /*! \brief Test for PME solving */
280 TEST_P(PmeSolveTest, ReproducesOutputs)
281 {
282     EXPECT_NO_THROW_GMX(runTest());
283 }
284
285 /* Valid input instances */
286
287 //! A couple of valid inputs for boxes.
288 std::vector<Matrix3x3> const c_sampleBoxes{
289     // normal box
290     Matrix3x3{ { 8.0F, 0.0F, 0.0F, 0.0F, 3.4F, 0.0F, 0.0F, 0.0F, 2.0F } },
291     // triclinic box
292     Matrix3x3{ { 7.0F, 0.0F, 0.0F, 0.0F, 4.1F, 0.0F, 3.5F, 2.0F, 12.2F } },
293 };
294
295 //! A couple of valid inputs for grid sizes
296 std::vector<IVec> const c_sampleGridSizes{ IVec{ 16, 12, 28 }, IVec{ 9, 7, 23 } };
297
298 //! Moved out from instantiations for readability
299 const auto c_inputBoxes = ::testing::ValuesIn(c_sampleBoxes);
300 //! Moved out from instantiations for readability
301 const auto c_inputGridSizes = ::testing::ValuesIn(c_sampleGridSizes);
302
303 //! 2 sample complex grids - only non-zero values have to be listed
304 std::vector<SparseComplexGridValuesInput> const c_sampleGrids{
305     SparseComplexGridValuesInput{
306             { IVec{ 0, 0, 0 }, t_complex{ 3.5F, 6.7F } },
307             { IVec{ 7, 0, 0 }, t_complex{ -2.5F, -0.7F } },
308             { IVec{ 3, 5, 7 }, t_complex{ -0.006F, 1e-8F } },
309             { IVec{ 3, 1, 2 }, t_complex{ 0.6F, 7.9F } },
310             { IVec{ 6, 2, 4 }, t_complex{ 30.1F, 2.45F } },
311     },
312     SparseComplexGridValuesInput{
313             { IVec{ 0, 4, 0 }, t_complex{ 0.0F, 0.3F } },
314             { IVec{ 4, 2, 7 }, t_complex{ 13.76F, -40.0F } },
315             { IVec{ 0, 6, 7 }, t_complex{ 3.6F, 0.0F } },
316             { IVec{ 2, 5, 10 }, t_complex{ 3.6F, 10.65F } },
317     }
318 };
319
320 //! Moved out from instantiations for readability
321 const auto c_inputGrids = ::testing::ValuesIn(c_sampleGrids);
322 //! Moved out from instantiations for readability
323 const auto c_inputEpsilon_r = ::testing::Values(1.2);
324 //! Moved out from instantiations for readability
325 const auto c_inputEwaldCoeff_q = ::testing::Values(2.0);
326 //! Moved out from instantiations for readability
327 const auto c_inputEwaldCoeff_lj = ::testing::Values(0.7);
328 //! Moved out from instantiations for readability
329 const auto c_inputMethods = ::testing::Values(PmeSolveAlgorithm::Coulomb, PmeSolveAlgorithm::LennardJones);
330
331 //! Instantiation of the PME solving test
332 INSTANTIATE_TEST_SUITE_P(SaneInput,
333                          PmeSolveTest,
334                          ::testing::Combine(c_inputBoxes,
335                                             c_inputGridSizes,
336                                             c_inputGrids,
337                                             c_inputEpsilon_r,
338                                             c_inputEwaldCoeff_q,
339                                             c_inputEwaldCoeff_lj,
340                                             c_inputMethods));
341
342 //! A few more instances to check that different ewaldCoeff_q actually affects results of the Coulomb solver
343 INSTANTIATE_TEST_SUITE_P(DifferentEwaldCoeffQ,
344                          PmeSolveTest,
345                          ::testing::Combine(c_inputBoxes,
346                                             c_inputGridSizes,
347                                             c_inputGrids,
348                                             c_inputEpsilon_r,
349                                             ::testing::Values(0.4),
350                                             c_inputEwaldCoeff_lj,
351                                             ::testing::Values(PmeSolveAlgorithm::Coulomb)));
352
353 //! A few more instances to check that different ewaldCoeff_lj actually affects results of the Lennard-Jones solver.
354 //! The value has to be approximately larger than 1 / (box dimensions) to have a meaningful output grid.
355 //! Previous value of 0.3 caused one of the grid cells to be less or greater than GMX_FLOAT_MIN, depending on the architecture.
356 INSTANTIATE_TEST_SUITE_P(DifferentEwaldCoeffLJ,
357                          PmeSolveTest,
358                          ::testing::Combine(c_inputBoxes,
359                                             c_inputGridSizes,
360                                             c_inputGrids,
361                                             c_inputEpsilon_r,
362                                             c_inputEwaldCoeff_q,
363                                             ::testing::Values(2.35),
364                                             ::testing::Values(PmeSolveAlgorithm::LennardJones)));
365
366 //! A few more instances to check that different epsilon_r actually affects results of all solvers
367 INSTANTIATE_TEST_SUITE_P(DifferentEpsilonR,
368                          PmeSolveTest,
369                          ::testing::Combine(c_inputBoxes,
370                                             c_inputGridSizes,
371                                             c_inputGrids,
372                                             testing::Values(1.9),
373                                             c_inputEwaldCoeff_q,
374                                             c_inputEwaldCoeff_lj,
375                                             c_inputMethods));
376
377 } // namespace
378 } // namespace test
379 } // namespace gmx