Generalize constraints on MPI rank counts for tests
[alexxy/gromacs.git] / src / programs / mdrun / tests / pmetest.cpp
index 7f73607b93dd8a594914c0e0c3f3a27568cbc5b9..7dadd5d0ea9ca03032a09491bfe0d9801f661966 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * This file is part of the GROMACS molecular simulation package.
  *
- * Copyright (c) 2016,2017, by the GROMACS development team, led by
+ * Copyright (c) 2016,2017,2018,2019,2020,2021, by the GROMACS development team, led by
  * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
  * and including many others, as listed in the AUTHORS file in the
  * top-level source directory and at http://www.gromacs.org.
  */
 /*! \internal \file
  * \brief
- * This implements basic PME sanity test (using single-rank mdrun).
+ * This implements basic PME sanity tests.
  * It runs the input system with PME for several steps (on CPU and GPU, if available),
  * and checks the reciprocal and conserved energies.
- * TODO: implement multi-rank tests as well.
+ * As part of mdrun-test, this will always run single rank PME simulation.
+ * As part of mdrun-mpi-test, this will run same as above when a single rank is requested,
+ * or a simulation with a single separate PME rank ("-npme 1") when multiple ranks are requested.
+ * \todo Extend and generalize this for more multi-rank tests (-npme 0, -npme 2, etc).
+ * \todo Implement death tests (e.g. for PME GPU decomposition).
  *
  * \author Aleksei Iupinov <a.yupinov@gmail.com>
  * \ingroup module_mdrun_integration_tests
  */
 #include "gmxpre.h"
 
-#include "config.h"
-
 #include <map>
 #include <string>
 #include <vector>
 
-#include "gromacs/gpu_utils/gpu_utils.h"
-#include "gromacs/hardware/gpu_hw_info.h"
+#include <gtest/gtest-spi.h>
+
+#include "gromacs/ewald/pme.h"
+#include "gromacs/hardware/detecthardware.h"
+#include "gromacs/hardware/device_management.h"
+#include "gromacs/hardware/hw_info.h"
+#include "gromacs/trajectory/energyframe.h"
+#include "gromacs/utility/basenetwork.h"
 #include "gromacs/utility/cstringutil.h"
+#include "gromacs/utility/gmxmpi.h"
+#include "gromacs/utility/physicalnodecommunicator.h"
 #include "gromacs/utility/stringutil.h"
 
+#include "testutils/mpitest.h"
 #include "testutils/refdata.h"
 
 #include "energyreader.h"
@@ -67,104 +78,196 @@ namespace test
 namespace
 {
 
-//! A basic PME runner
+/*! \brief A basic PME runner
+ *
+ * \todo Consider also using GpuTest class. */
 class PmeTest : public MdrunTestFixture
 {
-    public:
-        //! Before any test is run, work out whether any compatible GPUs exist.
-        static void SetUpTestCase();
-        //! Store whether any compatible GPUs exist.
-        static bool s_hasCompatibleCudaGpus;
+public:
+    //! Convenience typedef
+    using RunModesList = std::map<std::string, std::vector<const char*>>;
+    //! Runs the test with the given inputs
+    void runTest(const RunModesList& runModes);
 };
 
-bool PmeTest::s_hasCompatibleCudaGpus = false;
-
-void PmeTest::SetUpTestCase()
-{
-    gmx_gpu_info_t gpuInfo {};
-    char           detection_error[STRLEN];
-    GMX_UNUSED_VALUE(detection_error); //TODO
-    // It would be nicer to do this detection once and have mdrun
-    // re-use it, but this is OK. Note that this also caters for when
-    // there is no GPU support in the build.
-    if (GMX_GPU == GMX_GPU_CUDA &&
-        (detect_gpus(&gpuInfo, detection_error) >= 0) &&
-        gpuInfo.n_dev_compatible > 0)
-    {
-        s_hasCompatibleCudaGpus = true;
-    }
-    free_gpu_info(&gpuInfo);
-}
-
-TEST_F(PmeTest, ReproducesEnergies)
+void PmeTest::runTest(const RunModesList& runModes)
 {
-    const int   nsteps     = 20;
-    std::string theMdpFile = formatString("coulombtype     = PME\n"
-                                          "nstcalcenergy   = 1\n"
-                                          "nstenergy       = 1\n"
-                                          "pme-order       = 4\n"
-                                          "nsteps          = %d\n",
-                                          nsteps);
-
-    runner_.useStringAsMdpFile(theMdpFile);
-
     const std::string inputFile = "spc-and-methanol";
-    runner_.useTopGroAndNdxFromDatabase(inputFile.c_str());
+    runner_.useTopGroAndNdxFromDatabase(inputFile);
+
+    // With single rank we can and will always test PP+PME as part of mdrun-test.
+    // With multiple ranks we can additionally test a single PME-only rank within mdrun-mpi-test.
+    const bool parallelRun    = (getNumberOfTestMpiRanks() > 1);
+    const bool useSeparatePme = parallelRun;
 
     EXPECT_EQ(0, runner_.callGrompp());
 
-    //TODO test all proper/improper combinations in more thorough way?
-    std::map < std::string, std::vector < const char *>> runModes;
-    runModes["PmeOnCpu"] = {"-pme", "cpu"};
-    runModes["PmeAuto"]  = {"-pme", "auto"};
-    // TODO uncomment this when functionality gets activated.
-    //runModes["PmeOnGpuFftOnCpu"] = {"-pme", "gpu", "-pmefft", "cpu"};
-    runModes["PmeOnGpuFftOnGpu"] = {"-pme", "gpu", "-pmefft", "gpu"};
-    runModes["PmeOnGpuFftAuto"]  = {"-pme", "gpu", "-pmefft", "auto"};
     TestReferenceData    refData;
     TestReferenceChecker rootChecker(refData.rootChecker());
+    const bool           thisRankChecks = (gmx_node_rank() == 0);
+    if (!thisRankChecks)
+    {
+        EXPECT_NONFATAL_FAILURE(rootChecker.checkUnusedEntries(), ""); // skip checks on other ranks
+    }
+
+    auto hardwareInfo_ =
+            gmx_detect_hardware(PhysicalNodeCommunicator(MPI_COMM_WORLD, gmx_physicalnode_id_hash()));
 
-    for (const auto &mode : runModes)
+    for (const automode : runModes)
     {
+        SCOPED_TRACE("mdrun " + joinStrings(mode.second, " "));
         auto modeTargetsGpus = (mode.first.find("Gpu") != std::string::npos);
-        if (modeTargetsGpus && !s_hasCompatibleCudaGpus)
+        if (modeTargetsGpus && getCompatibleDevices(hardwareInfo_->deviceInfoList).empty())
         {
             // This run mode will cause a fatal error from mdrun when
             // it can't find GPUs, which is not something we're trying
             // to test here.
             continue;
         }
+        auto modeTargetsPmeOnGpus = (mode.first.find("PmeOnGpu") != std::string::npos);
+        if (modeTargetsPmeOnGpus
+            && !(pme_gpu_supports_build(nullptr) && pme_gpu_supports_hardware(*hardwareInfo_, nullptr)))
+        {
+            // This run mode will cause a fatal error from mdrun when
+            // it finds an unsuitable device, which is not something
+            // we're trying to test here.
+            continue;
+        }
 
-        runner_.edrFileName_ = fileManager_.getTemporaryFilePath(inputFile + "_" + mode.first + ".edr");
+        runner_.edrFileName_ =
+                fileManager_.getTemporaryFilePath(inputFile + "_" + mode.first + ".edr");
 
         CommandLine commandLine(mode.second);
-        commandLine.append("-notunepme"); // for reciprocal energy reproducibility
+
+        const bool usePmeTuning = (mode.first.find("Tune") != std::string::npos);
+        if (usePmeTuning)
+        {
+            commandLine.append("-tunepme");
+            commandLine.addOption("-nstlist", 1); // a new grid every step
+        }
+        else
+        {
+            commandLine.append("-notunepme"); // for reciprocal energy reproducibility
+        }
+        if (useSeparatePme)
+        {
+            commandLine.addOption("-npme", 1);
+        }
+
         ASSERT_EQ(0, runner_.callMdrun(commandLine));
 
-        auto energyReader      = openEnergyFileToReadFields(runner_.edrFileName_, {"Coul. recip.", "Total Energy", "Kinetic En."});
-        auto conservedChecker  = rootChecker.checkCompound("Energy", "Conserved");
-        auto reciprocalChecker = rootChecker.checkCompound("Energy", "Reciprocal");
-        for (int i = 0; i <= nsteps; i++)
+        if (thisRankChecks)
         {
-            EnergyFrame frame            = energyReader->frame();
-            std::string stepNum          = gmx::formatString("%d", i);
-            const real  conservedEnergy  = frame.at("Total Energy");
-            const real  reciprocalEnergy = frame.at("Coul. recip.");
-            if (i == 0)
+            auto energyReader = openEnergyFileToReadTerms(
+                    runner_.edrFileName_, { "Coul. recip.", "Total Energy", "Kinetic En." });
+            auto conservedChecker  = rootChecker.checkCompound("Energy", "Conserved");
+            auto reciprocalChecker = rootChecker.checkCompound("Energy", "Reciprocal");
+            bool firstIteration    = true;
+            while (energyReader->readNextFrame())
             {
-                // use first step values as references for tolerance
-                const real startingKineticEnergy = frame.at("Kinetic En.");
-                const auto conservedTolerance    = relativeToleranceAsFloatingPoint(startingKineticEnergy, 2e-5);
-                const auto reciprocalTolerance   = relativeToleranceAsFloatingPoint(reciprocalEnergy, 2e-5);
-                reciprocalChecker.setDefaultTolerance(reciprocalTolerance);
-                conservedChecker.setDefaultTolerance(conservedTolerance);
+                const EnergyFrame& frame            = energyReader->frame();
+                const std::string  stepName         = frame.frameName();
+                const real         conservedEnergy  = frame.at("Total Energy");
+                const real         reciprocalEnergy = frame.at("Coul. recip.");
+                if (firstIteration)
+                {
+                    // use first step values as references for tolerance
+                    const real startingKineticEnergy = frame.at("Kinetic En.");
+                    const auto conservedTolerance =
+                            relativeToleranceAsFloatingPoint(startingKineticEnergy, 2e-5);
+                    const auto reciprocalTolerance =
+                            relativeToleranceAsFloatingPoint(reciprocalEnergy, 3e-5);
+                    reciprocalChecker.setDefaultTolerance(reciprocalTolerance);
+                    conservedChecker.setDefaultTolerance(conservedTolerance);
+                    firstIteration = false;
+                }
+                conservedChecker.checkReal(conservedEnergy, stepName.c_str());
+                if (!usePmeTuning) // with PME tuning come differing grids and differing reciprocal energy
+                {
+                    reciprocalChecker.checkReal(reciprocalEnergy, stepName.c_str());
+                }
             }
-            conservedChecker.checkReal(conservedEnergy, stepNum.c_str());
-            reciprocalChecker.checkReal(reciprocalEnergy, stepNum.c_str());
         }
     }
 }
 
+TEST_F(PmeTest, ReproducesEnergies)
+{
+    const int         nsteps     = 20;
+    const std::string theMdpFile = formatString(
+            "coulombtype     = PME\n"
+            "nstcalcenergy   = 1\n"
+            "nstenergy       = 1\n"
+            "pme-order       = 4\n"
+            "nsteps          = %d\n",
+            nsteps);
+
+    runner_.useStringAsMdpFile(theMdpFile);
+
+    // TODO test all proper/improper combinations in more thorough way?
+    RunModesList runModes;
+    runModes["PmeOnCpu"]         = { "-pme", "cpu" };
+    runModes["PmeAuto"]          = { "-pme", "auto" };
+    runModes["PmeOnGpuFftOnCpu"] = { "-pme", "gpu", "-pmefft", "cpu" };
+    runModes["PmeOnGpuFftOnGpu"] = { "-pme", "gpu", "-pmefft", "gpu" };
+    runModes["PmeOnGpuFftAuto"]  = { "-pme", "gpu", "-pmefft", "auto" };
+    // same manual modes but marked for PME tuning
+    runModes["PmeOnCpuTune"]         = { "-pme", "cpu" };
+    runModes["PmeOnGpuFftOnCpuTune"] = { "-pme", "gpu", "-pmefft", "cpu" };
+    runModes["PmeOnGpuFftOnGpuTune"] = { "-pme", "gpu", "-pmefft", "gpu" };
+
+    runTest(runModes);
 }
+
+TEST_F(PmeTest, ScalesTheBox)
+{
+    const int         nsteps     = 0;
+    const std::string theMdpFile = formatString(
+            "coulombtype     = PME\n"
+            "nstcalcenergy   = 1\n"
+            "nstenergy       = 1\n"
+            "pme-order       = 4\n"
+            "pbc             = xyz\n"
+            "nsteps          = %d\n",
+            nsteps);
+
+    runner_.useStringAsMdpFile(theMdpFile);
+
+    RunModesList runModes;
+    runModes["PmeOnCpu"]         = { "-pme", "cpu" };
+    runModes["PmeOnGpuFftOnCpu"] = { "-pme", "gpu", "-pmefft", "cpu" };
+    runModes["PmeOnGpuFftOnGpu"] = { "-pme", "gpu", "-pmefft", "gpu" };
+
+    runTest(runModes);
 }
+
+TEST_F(PmeTest, ScalesTheBoxWithWalls)
+{
+    const int         nsteps     = 0;
+    const std::string theMdpFile = formatString(
+            "coulombtype     = PME\n"
+            "nstcalcenergy   = 1\n"
+            "nstenergy       = 1\n"
+            "pme-order       = 4\n"
+            "pbc             = xy\n"
+            "nwall           = 2\n"
+            "ewald-geometry  = 3dc\n"
+            "wall_atomtype   = CMet H\n"
+            "wall_density    = 9 9.0\n"
+            "wall-ewald-zfac = 5\n"
+            "nsteps          = %d\n",
+            nsteps);
+
+    runner_.useStringAsMdpFile(theMdpFile);
+
+    RunModesList runModes;
+    runModes["PmeOnCpu"]         = { "-pme", "cpu" };
+    runModes["PmeOnGpuFftOnCpu"] = { "-pme", "gpu", "-pmefft", "cpu" };
+    runModes["PmeOnGpuFftOnGpu"] = { "-pme", "gpu", "-pmefft", "gpu" };
+
+    runTest(runModes);
 }
+
+} // namespace
+} // namespace test
+} // namespace gmx