2 * This file is part of the GROMACS molecular simulation package.
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.
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.
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.
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.
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.
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.
37 * \brief Tests to verify that a simulator that only does some actions
38 * periodically produces the expected results.
40 * \author Mark Abraham <mark.j.abraham@gmail.com>
41 * \ingroup module_mdrun_integration_tests
49 #include "gromacs/trajectory/energyframe.h"
50 #include "gromacs/utility/strconvert.h"
51 #include "gromacs/utility/stringutil.h"
53 #include "testutils/simulationdatabase.h"
54 #include "testutils/testasserts.h"
56 #include "energycomparison.h"
57 #include "energyreader.h"
58 #include "moduletest.h"
67 /*! \brief Mdp parameters that determine the manner of simulation
69 using PropagationParameters = MdpFieldValues;
71 /*! \brief Mdp parameters that should only affect the observations,
72 * not the simulation propagation. */
73 using PeriodicOutputParameters = MdpFieldValues;
75 //! Helper type of output file names for the reference mdrun call
76 struct ReferenceFileNames
78 //! Name of energy file
79 std::string edrFileName_;
82 //! Function type to produce sets of .mdp parameters for testing periodic output
83 using OutputParameterGeneratorFunction = std::vector<PeriodicOutputParameters> (*)();
85 /*! \brief Test fixture base for comparing a simulator with one that
86 * does everything every step
88 * This test ensures that two simulator code paths called via
89 * different mdp options yield identical energy trajectories,
90 * up to some (arbitrary) precision.
92 * These tests are useful to check that periodic actions implemented
93 * in simulators are correct, and that different code paths expected
94 * to yield identical results are equivalent.
96 class PeriodicActionsTest :
97 public MdrunTestFixture,
98 public ::testing::WithParamInterface<std::tuple<PropagationParameters, OutputParameterGeneratorFunction>>
101 // PeriodicActionsTest();
102 //! Run mdrun with given output parameters
103 void doMdrun(const PeriodicOutputParameters& output);
104 //! Generate reference data from mdrun writing everything every step.
105 void prepareReferenceData();
106 //! Names for the output files from the reference mdrun call
107 ReferenceFileNames referenceFileNames_ = { fileManager_.getTemporaryFilePath("reference.edr") };
108 //! Functor for energy comparison
109 EnergyComparison energyComparison_{ EnergyComparison::defaultEnergyTermsToCompare(),
110 MaxNumFrames::compareAllFrames() };
111 //! Names of energies compared by energyComparison_
112 std::vector<std::string> namesOfEnergiesToMatch_ = energyComparison_.getEnergyNames();
115 void PeriodicActionsTest::doMdrun(const PeriodicOutputParameters& output)
117 auto propagation = std::get<0>(GetParam());
119 formatString("Doing %s simulation with %s integrator, %s tcoupling and %s pcoupling\n",
120 propagation["simulationName"].c_str(),
121 propagation["integrator"].c_str(),
122 propagation["tcoupl"].c_str(),
123 propagation["pcoupl"].c_str()));
124 auto mdpFieldValues = prepareMdpFieldValues(propagation["simulationName"],
125 propagation["integrator"],
126 propagation["tcoupl"],
127 propagation["pcoupl"]);
129 // This lambda writes all mdp options in `source` into `target`, overwriting options already
130 // present in `target`. It also filters out non-mdp option entries in the source maps
131 auto overWriteMdpMapValues = [](const MdpFieldValues& source, MdpFieldValues& target) {
132 for (auto const& [key, value] : source)
134 if (key == "simulationName" || key == "maxGromppWarningsTolerated" || key == "description")
136 // Remove non-mdp entries used in propagation and output
142 // Add options in propagation and output to the mdp options
143 overWriteMdpMapValues(propagation, mdpFieldValues);
144 overWriteMdpMapValues(output, mdpFieldValues);
146 // prepare the tpr file
149 caller.append("grompp");
150 caller.addOption("-maxwarn", propagation["maxGromppWarningsTolerated"]);
151 runner_.useTopGroAndNdxFromDatabase(propagation["simulationName"]);
152 runner_.useStringAsMdpFile(prepareMdpFileContents(mdpFieldValues));
153 EXPECT_EQ(0, runner_.callGrompp(caller));
157 CommandLine mdrunCaller;
158 mdrunCaller.append("mdrun");
159 ASSERT_EQ(0, runner_.callMdrun(mdrunCaller));
163 void PeriodicActionsTest::prepareReferenceData()
165 SCOPED_TRACE("Preparing reference data");
167 // Configure the SimulationRunner to write output to files we can
168 // use in comparing each test run.
169 std::swap(runner_.edrFileName_, referenceFileNames_.edrFileName_);
171 // Run the reference simulation with everything output every step
172 PeriodicOutputParameters outputEveryStep = {
173 { "nstenergy", "1" },
176 { "description", "output everything every step" },
178 doMdrun(outputEveryStep);
180 // Restore the standard filenames we'll use for the runs whose behavior we are testing.
181 std::swap(runner_.edrFileName_, referenceFileNames_.edrFileName_);
184 /*! \brief Compare the next frame returned by \c testFrameReader with the
185 * matching frame from \c referenceTestReader using \c comparator.
187 * \returns True when a successful comparison was made
189 template<typename Reader, typename Comparator>
190 bool compareFrames(Reader referenceFrameReader, Reader testFrameReader, const Comparator& comparator)
192 if (!testFrameReader->readNextFrame())
197 auto testFrame = testFrameReader->frame();
198 std::string frameName = testFrame.frameName();
199 SCOPED_TRACE("Found frame from test file named " + frameName);
200 bool foundMatch = false;
203 if (referenceFrameReader->readNextFrame())
205 auto referenceFrame = referenceFrameReader->frame();
206 // Can the reference and test frames be compared, because they have the same name?
207 if (frameName == referenceFrame.frameName())
209 SCOPED_TRACE("Found frame from reference file named " + frameName);
210 comparator(referenceFrame, testFrame);
216 ADD_FAILURE() << "Ran out of reference frames to compare";
223 TEST_P(PeriodicActionsTest, PeriodicActionsAgreeWithReference)
225 auto propagation = std::get<0>(GetParam());
226 SCOPED_TRACE(formatString("Comparing two simulations of '%s' with integrator '%s'",
227 propagation["simulationName"].c_str(),
228 propagation["integrator"].c_str()));
230 prepareReferenceData();
232 auto outputParametersGenerator = std::get<1>(GetParam());
233 for (const PeriodicOutputParameters& output : outputParametersGenerator())
235 SCOPED_TRACE("Comparing to observe " + output.at("description"));
237 // Run the test simulation
240 // Prepare readers for the reference and test output files
241 auto referenceEnergyFrameReader =
242 openEnergyFileToReadTerms(referenceFileNames_.edrFileName_, namesOfEnergiesToMatch_);
243 auto testEnergyFrameReader =
244 openEnergyFileToReadTerms(runner_.edrFileName_, namesOfEnergiesToMatch_);
246 const bool shouldCompareEnergies = fromString<int>(output.at("nstenergy")) > 0;
247 bool shouldContinueComparing = shouldCompareEnergies;
248 while (shouldContinueComparing)
250 if (shouldCompareEnergies)
252 SCOPED_TRACE("Comparing energy frames from reference '" + referenceFileNames_.edrFileName_
253 + "' and test '" + runner_.edrFileName_ + "'");
254 shouldContinueComparing = shouldContinueComparing
255 && compareFrames(referenceEnergyFrameReader.get(),
256 testEnergyFrameReader.get(),
263 /*! \brief Some common choices of periodic output mdp parameters to
264 * simplify defining values for the combinations under test */
265 PeriodicOutputParameters g_basicPeriodicOutputParameters = {
266 { "nstenergy", "0" }, { "nstlog", "0" }, { "nstxout", "0" },
267 { "nstvout", "0" }, { "nstfout", "0" }, { "nstxout-compressed", "0" },
268 { "nstdhdl", "0" }, { "description", "unknown" }
271 /*! \brief Return vector of mdp parameter sets to test
273 * These are constructed to observe the mdp parameter choices that
274 * only affect when output is written agree with those that were
275 * written from a reference run where output was done every step. The
276 * numbers are chosen in the context of the defaults in
277 * prepareDefaultMdpFieldValues().
279 * \todo Test nstlog, nstdhdl, nstxout-compressed */
280 std::vector<PeriodicOutputParameters> outputParameters()
282 std::vector<PeriodicOutputParameters> parameterSets;
284 parameterSets.push_back(g_basicPeriodicOutputParameters);
285 parameterSets.back()["nstenergy"] = "1";
286 parameterSets.back()["description"] = "energies every step works";
288 parameterSets.push_back(g_basicPeriodicOutputParameters);
289 parameterSets.back()["nstenergy"] = "4";
290 parameterSets.back()["description"] = "energies every fourth step works";
292 parameterSets.push_back(g_basicPeriodicOutputParameters);
293 parameterSets.back()["nstxout"] = "4";
294 parameterSets.back()["description"] = "coordinates every fourth step works";
296 parameterSets.push_back(g_basicPeriodicOutputParameters);
297 parameterSets.back()["nstvout"] = "4";
298 parameterSets.back()["description"] = "velocities every fourth step works";
300 parameterSets.push_back(g_basicPeriodicOutputParameters);
301 parameterSets.back()["nstfout"] = "4";
302 parameterSets.back()["description"] = "forces every fourth step works";
304 return parameterSets;
307 //! Returns sets of simple simulation propagators
308 std::vector<PropagationParameters> simplePropagationParameters()
311 { { "simulationName", "argon12" },
312 { "integrator", "md" },
313 { "comm-mode", "linear" },
316 { "nsttcouple", "0" },
318 { "nstpcouple", "0" },
319 { "maxGromppWarningsTolerated", "0" } },
323 /*! \brief Returns sets of simulation propagators including coupling
325 * These are chosen to cover the commonly used space of propagation
326 * algorithms togther with the perdiods between their actions. The
327 * periods tested are chosen to be mutually co-prime and distinct from
328 * the pair search and user output period (4), so that over the
329 * duration of a short simulation many kinds of simulation step
330 * behavior are tested. */
331 std::vector<PropagationParameters> propagationParametersWithCoupling()
333 std::string nsttcouple = "2";
334 std::string nstpcouple = "3";
335 std::string comm_mode = "linear";
336 std::string nstcomm = "5";
338 std::vector<PropagationParameters> parameterSets;
339 for (const std::string& simulationName : { "argon12" })
341 for (const std::string& integrator : { "md", "sd", "md-vv" })
343 for (const std::string& tcoupl : { "no", "v-rescale", "Nose-Hoover" })
345 // SD doesn't support temperature-coupling algorithms,
346 if (integrator == "sd" && tcoupl != "no")
350 for (std::string pcoupl : { "no", "Berendsen", "Parrinello-Rahman", "C-rescale" })
352 // VV supports few algorithm combinations
353 if (integrator == "md-vv")
355 // P-R with VV is called MTTK
356 if (pcoupl == "Parrinello-Rahman")
360 if ((tcoupl == "Nose-Hoover" && pcoupl == "Berendsen")
361 || (tcoupl != "Nose-Hoover" && pcoupl == "MTTK"))
366 if (pcoupl == "MTTK" && simulationName != "argon12")
368 // MTTK does not support constraints
372 int maxGromppWarningsTolerated = 0;
373 if (pcoupl == "Berendsen")
375 ++maxGromppWarningsTolerated;
377 parameterSets.emplace_back(PropagationParameters{
378 { "simulationName", simulationName },
379 { "integrator", integrator },
380 { "comm-mode", comm_mode },
381 { "nstcomm", nstcomm },
382 { "tcoupl", tcoupl },
383 { "nsttcouple", nsttcouple },
384 { "pcoupl", pcoupl },
385 { "nstpcouple", nstpcouple },
386 { "maxGromppWarningsTolerated", toString(maxGromppWarningsTolerated) } });
391 return parameterSets;
394 /*! \brief Returns sets of simulation propagators including coupling
396 * These are chosen to cover the commonly used space of propagation
397 * algorithms on systems with constraints. */
398 std::vector<PropagationParameters> propagationParametersWithConstraints()
400 std::string nsttcouple = "2";
401 std::string nstpcouple = "3";
402 std::string comm_mode = "linear";
403 std::string nstcomm = "5";
405 std::vector<PropagationParameters> parameterSets;
406 for (const std::string& simulationName : { "tip3p5" })
408 for (const std::string& integrator : { "md", "sd", "md-vv" })
410 for (const std::string& tcoupl : { "no", "v-rescale" })
412 // SD doesn't support temperature-coupling algorithms,
413 if (integrator == "sd" && tcoupl != "no")
417 for (std::string pcoupl : { "no", "Parrinello-Rahman", "C-rescale" })
419 // VV supports few algorithm combinations
420 if (integrator == "md-vv")
422 // P-R with VV is called MTTK
423 if (pcoupl == "Parrinello-Rahman")
427 if ((tcoupl == "Nose-Hoover" && pcoupl == "Berendsen")
428 || (tcoupl != "Nose-Hoover" && pcoupl == "MTTK"))
433 if (pcoupl == "MTTK" && simulationName != "argon12")
435 // MTTK does not support constraints
439 int maxGromppWarningsTolerated = 0;
440 parameterSets.emplace_back(PropagationParameters{
441 { "simulationName", simulationName },
442 { "integrator", integrator },
443 { "comm-mode", comm_mode },
444 { "nstcomm", nstcomm },
445 { "tcoupl", tcoupl },
446 { "nsttcouple", nsttcouple },
447 { "pcoupl", pcoupl },
448 { "nstpcouple", nstpcouple },
449 { "maxGromppWarningsTolerated", toString(maxGromppWarningsTolerated) } });
454 return parameterSets;
457 using ::testing::Combine;
458 using ::testing::Values;
459 using ::testing::ValuesIn;
461 // TODO The time for OpenCL kernel compilation means these tests time
462 // out. Once that compilation is cached for the whole process, these
463 // tests can run in such configurations.
465 INSTANTIATE_TEST_CASE_P(BasicPropagators,
467 Combine(ValuesIn(simplePropagationParameters()), Values(outputParameters)));
468 INSTANTIATE_TEST_CASE_P(PropagatorsWithCoupling,
470 Combine(ValuesIn(propagationParametersWithCoupling()), Values(outputParameters)));
471 INSTANTIATE_TEST_CASE_P(PropagatorsWithConstraints,
473 Combine(ValuesIn(propagationParametersWithConstraints()), Values(outputParameters)));
475 INSTANTIATE_TEST_CASE_P(DISABLED_BasicPropagators,
477 Combine(ValuesIn(simplePropagationParameters()), Values(outputParameters)));
478 INSTANTIATE_TEST_CASE_P(DISABLED_PropagatorsWithCoupling,
480 Combine(ValuesIn(propagationParametersWithCoupling()), Values(outputParameters)));
481 INSTANTIATE_TEST_CASE_P(DISABLED_PropagatorsWithConstraints,
483 Combine(ValuesIn(propagationParametersWithConstraints()), Values(outputParameters)));