5a9d0f274a32374e0d70599ce1a66a66a5fbbd75
[alexxy/gromacs.git] / api / gmxapi / cpp / session.cpp
1 /*
2  * This file is part of the GROMACS molecular simulation package.
3  *
4  * Copyright (c) 2018,2020, 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
36 #include "gmxapi/session.h"
37
38 #include <memory>
39
40 #include "gromacs/mdlib/sighandler.h"
41 #include "gromacs/mdrunutility/logging.h"
42 #include "gromacs/restraint/restraintpotential.h"
43 #include "gromacs/utility/gmxassert.h"
44 #include "gromacs/utility/basenetwork.h"
45 #include "gromacs/utility/init.h"
46
47 #include "gmxapi/context.h"
48 #include "gmxapi/exceptions.h"
49 #include "gmxapi/status.h"
50 #include "gmxapi/md/mdmodule.h"
51
52 #include "createsession.h"
53 #include "mdsignals.h"
54 #include "session_impl.h"
55 #include "sessionresources.h"
56
57 namespace gmxapi
58 {
59
60 /*!
61  * \brief Provide RAII management of communications resource state.
62  *
63  * To acquire an MpiContextManager is to have assurance that any external MPI
64  * environment is ready to use. When the MpiContextManager is released or
65  * goes out of scope, the destructor finalizes the resources.
66  *
67  * Note that thread-MPI chooses the number of ranks and constructs its
68  * MPI communicator internally, so does not and is unlikely to ever
69  * participate here.
70  *
71  * \todo There is no resource for logging or reporting errors during initialization
72  * \todo Remove this class by managing the MPI context with mpi4py and so
73  *       configuring the SimulationContext externally
74  *
75  * \ingroup gmxapi
76  */
77 class MpiContextManager
78 {
79 public:
80     MpiContextManager()
81     {
82         gmx::init(nullptr, nullptr);
83         GMX_RELEASE_ASSERT(!GMX_LIB_MPI || gmx_mpi_initialized(),
84                            "MPI should be initialized before reaching this point.");
85     };
86
87     ~MpiContextManager()
88     {
89         // This is always safe to call. It is a no-op if
90         // thread-MPI, and if the constructor completed then the
91         // MPI library is initialized with reference counting.
92         gmx::finalize();
93     }
94
95     /*!
96      * \brief Exclusive ownership of a scoped context means copying is impossible.
97      *
98      * \{
99      */
100     MpiContextManager(const MpiContextManager&) = delete;
101     MpiContextManager& operator=(const MpiContextManager&) = delete;
102     //! \}
103
104     /*!
105      * \brief Move semantics are trivial.
106      *
107      * \{
108      */
109     MpiContextManager(MpiContextManager&&) noexcept = default;
110     MpiContextManager& operator=(MpiContextManager&&) noexcept = default;
111     //! \}
112 };
113
114 SignalManager::SignalManager(gmx::StopHandlerBuilder* stopHandlerBuilder) :
115     state_(std::make_shared<gmx::StopSignal>(gmx::StopSignal::noSignal))
116 {
117
118     /*!
119      * \brief Signal issuer managed by this object.
120      *
121      * Created and bound when the runner is built. Subsequently, client
122      * stop signals are proxied to the simulation through the session
123      * resources. The MD internal signal handler calls this functor
124      * during the MD loop to see if the simulation should be stopped.
125      * Thus, this should execute within a very small fraction of an MD
126      * step and not require any synchronization.
127      */
128     auto currentState     = state_;
129     auto stopSignalIssuer = [currentState]() { return *currentState; };
130     stopHandlerBuilder->registerStopCondition(stopSignalIssuer);
131 }
132
133 //! \cond
134 SignalManager::~SignalManager() = default;
135 //! \endcond
136
137 bool SessionImpl::isOpen() const noexcept
138 {
139     // Currently, an active session is equivalent to an active Mdrunner.
140     return bool(runner_);
141 }
142
143 Status SessionImpl::close()
144 {
145     // Assume unsuccessful until proven otherwise.
146     auto successful = Status(false);
147
148     // When the Session is closed, we need to know that the MD output has been finalized.
149     runner_.reset();
150     logFilePtr_.reset();
151
152     // \todo provide meaningful result.
153     // We should be checking that resources were properly shut down, but
154     // there isn't currently a way to do that.
155     successful = true;
156     return successful;
157 }
158
159 Status SessionImpl::run() noexcept
160 {
161     // Status is failure until proven otherwise.
162     auto successful = Status(false);
163     GMX_ASSERT(runner_, "SessionImpl invariant implies valid Mdrunner handle.");
164     auto rc = runner_->mdrunner();
165     if (rc == 0)
166     {
167         successful = true;
168     }
169     return successful;
170 }
171
172 std::unique_ptr<SessionImpl> SessionImpl::create(std::shared_ptr<ContextImpl> context,
173                                                  gmx::MdrunnerBuilder&&       runnerBuilder,
174                                                  gmx::SimulationContext&&     simulationContext,
175                                                  gmx::LogFilePtr              logFilehandle)
176 {
177     // We should be able to get a communicator (or subcommunicator) through the
178     // Context.
179     return std::make_unique<SessionImpl>(std::move(context), std::move(runnerBuilder),
180                                          std::move(simulationContext), std::move(logFilehandle));
181 }
182
183 SessionImpl::SessionImpl(std::shared_ptr<ContextImpl> context,
184                          gmx::MdrunnerBuilder&&       runnerBuilder,
185                          gmx::SimulationContext&&     simulationContext,
186                          gmx::LogFilePtr              fplog) :
187     context_(std::move(context)),
188     mpiContextManager_(std::make_unique<MpiContextManager>()),
189     simulationContext_(std::move(simulationContext)),
190     logFilePtr_(std::move(fplog))
191 {
192     GMX_ASSERT(context_, "SessionImpl invariant implies valid ContextImpl handle.");
193     GMX_ASSERT(mpiContextManager_, "SessionImpl invariant implies valid MpiContextManager guard.");
194
195     // \todo Session objects can have logic specialized for the runtime environment.
196
197     auto stopHandlerBuilder = std::make_unique<gmx::StopHandlerBuilder>();
198     signalManager_          = std::make_unique<SignalManager>(stopHandlerBuilder.get());
199     GMX_ASSERT(signalManager_, "SessionImpl invariant includes a valid SignalManager.");
200
201     runnerBuilder.addStopHandlerBuilder(std::move(stopHandlerBuilder));
202     runner_ = std::make_unique<gmx::Mdrunner>(runnerBuilder.build());
203     GMX_ASSERT(runner_, "SessionImpl invariant implies valid Mdrunner handle.");
204
205     // For the libgromacs context, a session should explicitly reset global variables that could
206     // have been set in a previous simulation during the same process.
207     gmx_reset_stop_condition();
208 }
209
210 std::shared_ptr<Session> createSession(std::shared_ptr<ContextImpl> context,
211                                        gmx::MdrunnerBuilder&&       runnerBuilder,
212                                        gmx::SimulationContext&&     simulationContext,
213                                        gmx::LogFilePtr              logFilehandle)
214 {
215     auto newSession      = SessionImpl::create(std::move(context), std::move(runnerBuilder),
216                                           std::move(simulationContext), std::move(logFilehandle));
217     auto launchedSession = std::make_shared<Session>(std::move(newSession));
218     return launchedSession;
219 }
220
221 Status SessionImpl::addRestraint(std::shared_ptr<gmxapi::MDModule> module)
222 {
223     GMX_ASSERT(runner_, "SessionImpl invariant implies valid Mdrunner handle.");
224     Status status{ false };
225
226     if (module != nullptr)
227     {
228         const auto& name = module->name();
229         if (restraints_.find(name) == restraints_.end())
230         {
231             auto restraint = module->getRestraint();
232             if (restraint != nullptr)
233             {
234                 restraints_.emplace(std::make_pair(name, restraint));
235                 auto sessionResources = createResources(module);
236                 if (!sessionResources)
237                 {
238                     status = false;
239                 }
240                 else
241                 {
242                     runner_->addPotential(restraint, module->name());
243                     status = true;
244                 }
245             }
246         }
247     }
248     return status;
249 }
250
251
252 SignalManager* SessionImpl::getSignalManager()
253 {
254     SignalManager* ptr = nullptr;
255     if (isOpen())
256     {
257         ptr = signalManager_.get();
258     }
259     return ptr;
260 }
261
262 gmx::Mdrunner* SessionImpl::getRunner()
263 {
264     gmx::Mdrunner* runner = nullptr;
265     if (runner_)
266     {
267         runner = runner_.get();
268     }
269     return runner;
270 }
271
272 gmxapi::SessionResources* SessionImpl::getResources(const std::string& name) const noexcept
273 {
274     gmxapi::SessionResources* resources = nullptr;
275     try
276     {
277         resources = resources_.at(name).get();
278     }
279     catch (const std::out_of_range& e)
280     {
281         // named operation does not have any resources registered.
282     }
283
284     return resources;
285 }
286
287 gmxapi::SessionResources* SessionImpl::createResources(std::shared_ptr<gmxapi::MDModule> module) noexcept
288 {
289     // check if resources already exist for this module
290     // If not, create resources and return handle.
291     // Return nullptr for any failure.
292     gmxapi::SessionResources* resources = nullptr;
293     const auto&               name      = module->name();
294     if (resources_.find(name) == resources_.end())
295     {
296         auto resourcesInstance = std::make_unique<SessionResources>(this, name);
297         resources_.emplace(std::make_pair(name, std::move(resourcesInstance)));
298         resources = resources_.at(name).get();
299         // To do: This should be more dynamic.
300         getSignalManager()->addSignaller(name);
301         if (restraints_.find(name) != restraints_.end())
302         {
303             auto restraintRef = restraints_.at(name);
304             auto restraint    = restraintRef.lock();
305             if (restraint)
306             {
307                 restraint->bindSession(resources);
308             }
309         }
310     };
311     return resources;
312 }
313
314 Session::Session(std::unique_ptr<SessionImpl> impl) noexcept
315 {
316     if (impl != nullptr)
317     {
318         impl_ = std::move(impl);
319     }
320     GMX_ASSERT(impl_->isOpen(), "SessionImpl invariant implies valid Mdrunner handle.");
321 }
322
323 Status Session::run() noexcept
324 {
325     GMX_ASSERT(impl_, "Session invariant implies valid implementation object handle.");
326
327     const Status status = impl_->run();
328     return status;
329 }
330
331 Status Session::close()
332 {
333     GMX_ASSERT(impl_, "Session invariant implies valid implementation object handle.");
334
335     auto status = Status(false);
336     if (isOpen())
337     {
338         // \todo catch exceptions when we know what they might be
339         status = impl_->close();
340     }
341
342     return status;
343 }
344
345 Session::~Session()
346 {
347     GMX_ASSERT(impl_, "Session invariant implies valid implementation object handle.");
348     if (isOpen())
349     {
350         try
351         {
352             impl_->close();
353         }
354         catch (const std::exception&)
355         {
356             // \todo find some exception-safe things to do with this via the Context interface.
357         }
358     }
359 }
360
361 bool Session::isOpen() const noexcept
362 {
363     GMX_ASSERT(impl_, "Session invariant implies valid implementation object handle.");
364     const auto result = impl_->isOpen();
365     return result;
366 }
367
368 Status addSessionRestraint(Session* session, std::shared_ptr<gmxapi::MDModule> restraint)
369 {
370     auto status = gmxapi::Status(false);
371
372     if (session != nullptr && restraint != nullptr)
373     {
374         // \todo Improve the external / library API facets
375         // so the public API does not need to offer raw pointers.
376         auto sessionImpl = session->getRaw();
377
378         GMX_RELEASE_ASSERT(sessionImpl,
379                            "Session invariant implies valid implementation object handle.");
380         // GMX_ASSERT alone is not strong enough to convince linters not to warn of possible nullptr.
381         if (sessionImpl)
382         {
383             status = sessionImpl->addRestraint(std::move(restraint));
384         }
385     }
386     return status;
387 }
388
389 //! \cond internal
390 SessionImpl* Session::getRaw() const noexcept
391 {
392     return impl_.get();
393 }
394 //! \endcond
395
396 std::shared_ptr<Session> launchSession(Context* context, const Workflow& work) noexcept
397 {
398     auto session = context->launch(work);
399     return session;
400 }
401
402 SessionImpl::~SessionImpl() = default;
403
404 SessionResources::SessionResources(gmxapi::SessionImpl* session, std::string name) :
405     sessionImpl_(session),
406     name_(std::move(name))
407 {
408 }
409
410 SessionResources::~SessionResources() = default;
411
412 std::string SessionResources::name() const
413 {
414     return name_;
415 }
416
417 Signal SessionResources::getMdrunnerSignal(md::signals signal)
418 {
419     //// while there is only one choice...
420     if (signal != md::signals::STOP)
421     {
422         throw gmxapi::NotImplementedError("This signaller only handles stop signals.");
423     }
424
425     // Get a signalling proxy for the caller.
426     auto signalManager = sessionImpl_->getSignalManager();
427     if (signalManager == nullptr)
428     {
429         throw gmxapi::ProtocolError(
430                 "Client requested access to a signaller that is not available.");
431     }
432     auto functor = signalManager->getSignal(name_, signal);
433
434     return functor;
435 }
436
437 } // end namespace gmxapi