Improve robustness of StopSignalClient test.
[alexxy/gromacs.git] / src / api / cpp / tests / stopsignaler.cpp
1 /*
2  * This file is part of the GROMACS molecular simulation package.
3  *
4  * Copyright (c) 2018, 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 #include <memory>
36 #include <optional>
37
38 #include "gromacs/math/functions.h"
39 #include "gromacs/math/vectypes.h"
40 #include "gromacs/restraint/restraintpotential.h"
41 #include "gromacs/utility.h"
42
43 #include "gmxapi/context.h"
44 #include "gmxapi/md.h"
45 #include "gmxapi/session.h"
46 #include "gmxapi/session/resources.h"
47 #include "gmxapi/status.h"
48 #include "gmxapi/system.h"
49 #include "gmxapi/md/mdmodule.h"
50 #include "gmxapi/md/mdsignals.h"
51
52 #include "testingconfiguration.h"
53
54 namespace gmxapi
55 {
56
57 namespace testing
58 {
59
60 namespace
61 {
62
63 /*!
64  * \brief Restraint that can optionally issue an immediate stop signal.
65  */
66 class StopSignalIssuer : public gmx::IRestraintPotential
67 {
68 public:
69     /*!
70      * \brief Construct a restraint that does nothing.
71      */
72     StopSignalIssuer() : StopSignalIssuer(false) {}
73
74     /*!
75      * \brief Choose whether or not to issue stop signal when called.
76      *
77      * \param sendStopSignal If true, issue stop signal at every opportunity.
78      */
79     explicit StopSignalIssuer(bool sendStopSignal) : sendStopSignal_{ sendStopSignal } {}
80
81     /*! \cond Implement IRestraintPotential */
82     gmx::PotentialPointData evaluate(gmx::Vector /* r_site */, gmx::Vector /*  r_ref */, double t) override
83     {
84         // Note that evaluate gets called once for each site,
85         // which is twice per time step for a pair restraint.
86         // The following initialization logic is not atomic, but it is sufficient.
87         if (!isInitialized())
88         {
89             // Force is also calculated for initial step.
90             simulationStartTime_ = t;
91         }
92         lastSimulationTime_ = t;
93
94         if (sendStopSignal_)
95         {
96             auto signalSender = gmxapi::getMdrunnerSignal(resources_, gmxapi::md::signals::STOP);
97             signalSender();
98         }
99
100         return { { 0., 0., 0. }, 0. };
101     }
102
103     std::vector<int> sites() const override { return { { 0, 1 } }; }
104
105     void bindSession(gmxapi::SessionResources* resources) override { resources_ = resources; }
106     //! \endcond
107
108     /*!
109      * \brief Note simulation start time when called on the zeroeth step.
110      */
111     double simulationStartTime_ = 0.;
112
113     /*!
114      * \brief Record the simulation time at the last step active.
115      */
116     std::optional<double> lastSimulationTime_;
117
118     /*!
119      * \brief Whether restraint was ever used
120      */
121     bool isInitialized() const { return lastSimulationTime_.has_value(); }
122
123 private:
124     /*!
125      * \brief Handle through which to get signalling resources.
126      */
127     gmxapi::SessionResources* resources_ = nullptr;
128
129     /*!
130      * \brief Whether to issue stop signal when called.
131      */
132     bool sendStopSignal_ = false;
133 };
134
135 /*!
136  * \brief Wrap a StopSignalIssuer for testing purposes.
137  */
138 class SimpleSignalingClient : public gmxapi::MDModule
139 {
140 public:
141     /*! \cond
142      * Implement gmxapi::MDModule interface.
143      */
144     SimpleSignalingClient() : restraint_(std::make_shared<StopSignalIssuer>()) {}
145
146     explicit SimpleSignalingClient(bool sendStopSignal) :
147         restraint_(std::make_shared<StopSignalIssuer>(sendStopSignal))
148     {
149     }
150
151     const char* name() const override { return "SimpleSignalingClient"; }
152
153     std::shared_ptr<gmx::IRestraintPotential> getRestraint() override { return restraint_; }
154     //! \endcond
155
156     /*!
157      * \brief Last time this restraint was active, minus the simulation start time.
158      *
159      * \return Time elapsed since start.
160      */
161     double timeElapsedSinceStart() const
162     {
163         if (!restraint_->isInitialized())
164         {
165             GMX_THROW(gmx::InternalError("timeElapsedSinceStart called before restraint was used"));
166         }
167         return restraint_->lastSimulationTime_.value() - restraint_->simulationStartTime_;
168     }
169
170 private:
171     //! restraint to provide to client or MD simulator.
172     std::shared_ptr<StopSignalIssuer> restraint_;
173 };
174
175 /*!
176  * \brief Check that we can bind to and use the stop signaler.
177  */
178 TEST_F(GmxApiTest, ApiRunnerStopSignalClient)
179 {
180     const int nsteps = 4;
181     makeTprFile(nsteps);
182
183     // Check assumptions about basic simulation behavior.
184     {
185         const int nstlist = 1;
186
187         auto system  = gmxapi::fromTprFile(runner_.tprFileName_);
188         auto context = std::make_shared<gmxapi::Context>(gmxapi::createContext());
189
190         gmxapi::MDArgs args = makeMdArgs();
191         args.emplace_back("-nstlist");
192         args.emplace_back(std::to_string(nstlist));
193
194         context->setMDArgs(args);
195
196         auto restraint = std::make_shared<SimpleSignalingClient>();
197
198         auto session = system.launch(context);
199         EXPECT_TRUE(session);
200
201         gmxapi::addSessionRestraint(session.get(), restraint);
202         EXPECT_THROW(restraint->timeElapsedSinceStart(), gmx::InternalError);
203
204         gmxapi::Status status;
205         ASSERT_NO_THROW(status = session->run());
206         EXPECT_TRUE(status.success());
207         EXPECT_EQ(nsteps, gmx::roundToInt(restraint->timeElapsedSinceStart() / getTestStepSize()));
208
209         status = session->close();
210         EXPECT_TRUE(status.success());
211     }
212
213     // Make sure that stop signal shortens simulation.
214     {
215         /* StopHandler promises to stop a simulation at the next NS step after the signal got communicated.
216          * We don't know the communication interval, but we know that it is at most nstlist. We cannot assume
217          * that the signal gets communicated on the step it is set, even if that step is a communication step.
218          * As the signal is set on the first step, we know that the restraint will be called at
219          * most 2*nstlist + 1 times.
220          * Since the time elapsed after the first step is 0, however, we expect the elapsed time
221          * divided by the step size to be at most 2*nstlist.
222          */
223
224         const int           nstlist  = 1;
225         constexpr const int maxsteps = nstlist * 2 + 1;
226         // This test is meaningless if the the simulation ends early without a signal.
227         static_assert(
228                 maxsteps < nsteps,
229                 "Simulation is already scheduled to end before it can receive a stop signal.");
230
231         auto system  = gmxapi::fromTprFile(runner_.tprFileName_);
232         auto context = std::make_shared<gmxapi::Context>(gmxapi::createContext());
233
234         gmxapi::MDArgs args = makeMdArgs();
235         args.emplace_back("-nstlist");
236         args.emplace_back(std::to_string(nstlist));
237         // TODO (Ref #3256) use api functionality to extend simulation instead
238         args.emplace_back("-nsteps");
239         args.emplace_back(std::to_string(nsteps));
240
241         context->setMDArgs(args);
242
243         const bool issueImmediateStopSignal = true;
244         auto       restraint = std::make_shared<SimpleSignalingClient>(issueImmediateStopSignal);
245
246         auto session = system.launch(context);
247         EXPECT_TRUE(session);
248
249         gmxapi::addSessionRestraint(session.get(), restraint);
250         EXPECT_THROW(restraint->timeElapsedSinceStart(), gmx::InternalError);
251
252         gmxapi::Status status;
253         ASSERT_NO_THROW(status = session->run());
254         EXPECT_TRUE(status.success());
255
256         const int steps_just_run =
257                 gmx::roundToInt(restraint->timeElapsedSinceStart() / getTestStepSize());
258         EXPECT_LT(steps_just_run, maxsteps);
259
260         status = session->close();
261         EXPECT_TRUE(status.success());
262     }
263 }
264
265 } // end anonymous namespace
266
267 } // end namespace testing
268
269 } // end namespace gmxapi