Restore gmxapi._gmxapi.add_mdmodule() Python functionality.
Some functionality was orphaned and forgotten in the last-minute changes to core gmxapi infrastructure as the Python package was merged to GROMACS 2020. Issue #3145 remains to be addressed, but we should restore some of the gmxapi 0.0.7 functionality that was lost in gmxapi 0.1. A logic error was introduced as `gmxapi::System` was deprecated, in which `IRestraintPotential` providers added to the system are not attached to the simulation that is launched. This was possible because of some confusing redundancy that was introduced, in which a container was duplicated in the `gmxapi::System` and `gmxpy::PyContext` proxy, but roles were not completely updated. ## Background A working updated protocol is demonstrated in the C++ tests, illustrated in the following sequence diagram. ```plantuml 'https://plantuml.com/sequence-diagram 'autonumber autoactivate on client -> libgmxapi: gmxapi::fromTprFile() libgmxapi -> "gmxapi::System" **: create return "gmxapi::System" client -> libgmxapi: gmxapi::createContext() libgmxapi -> "gmxapi::Context" **: create return "gmxapi::Context" client -> "gmxapi::Context": setMDArgs() return client -> plugin: create plugin -> restraint **: create return restraint client -> "gmxapi::System": System.launch(gmxapi::Context) "gmxapi::System" -> "gmxapi::Context": launch(systemimpl.workflow_) return session loop foreach member in systemIml.spec_->getModules() "gmxapi::System" -> libgmxapi: addSessionRestraint(session, member) note right spec is empty! end note return end return "gmxapi::Session" client -> libgmxapi: gmxapi::addSessionRestraint(session, restraint) libgmxapi -> sessionImpl: addRestraint(restraint) sessionImpl -> "gmx::MdRunner": addPotential(restraint) return return success return success client -> "gmxapi::Session": run() "gmxapi::Session" -> "gmx::MdRunner": run() return return ``` It took an extra few years to enable automated testing of Python code in the GROMACS CI pipelines, during which time development and test coverage languished. The broken protocol is illustrated in the following sequence diagram. ```plantuml @startuml 'https://plantuml.com/sequence-diagram 'autonumber autoactivate on context.py -> _gmxapi: Context() _gmxapi -> "gmxpy::PyContext" **: create "gmxpy::PyContext" -> "gmxapi::Context" **: gmxapi::createContext() return ... context.py -> plugin: _builder.add_subscriber(mdbuilder) deactivate context.py -> plugin: _builder.build(graph) plugin -> restraint **: create plugin -> context.py: mdbuilder.potential.append(restraint) deactivate deactivate plugin ... context.py -> context.py: mdbuilder.build(graph) 'in MD builder-> launch context.py -> _gmxapi: from_tpr() _gmxapi -> "gmxapi::System" **: create return system context.py -> _gmxapi: setMDArgs() _gmxapi -> "gmxpy::PyContext": setMDArgs() "gmxpy::PyContext" -> "gmxapi::Context": setMDArgs() deactivate deactivate deactivate _gmxapi context.py -> _gmxapi: add_mdmodule(plugin) _gmxapi -> "gmxpy::PyContext": addMDModule(restraint) "gmxpy::PyContext" -> "gmxpy::PyContext": getSpec() return this->workNodes_ "gmxpy::PyContext" -> restraint: bind(pycontext.workspec) restraint -> "gmxpy::PyContext": gmxapi::MDWorkSpec::addModule(module) note right restraint added to PyContext member. end note deactivate deactivate deactivate deactivate context.py -> _gmxapi: system.launch(context) _gmxapi -> "gmxpy::PyContext": get() return gmxapi::Context _gmxapi -> "gmxapi::System": System.launch(gmxapi::Context) "gmxapi::System" -> "gmxapi::Context": launch(systemimpl.workflow_) return session loop foreach member in systemIml.spec_->getModules() "gmxapi::System" -> gmxapi: addSessionRestraint(session, member) deactivate note right spec is empty! end note end return gmxapi::Session return _gmxapi.MDSession return ' in session launch context.py -> _gmxapi: _gmxapi.MDSession.run() _gmxapi -> "gmxapi::Session": run() ... @enduml ``` ## Proposal * Make minimal changes to the gmxapi interfaces related to `gmxapi::System` and `gmxapi::Context` at this time. * Update the `gmxapi._gmxapi.add_mdmodule()` function in the Python bindings to allow the target of the plugin `bind()` behavior to target the correct instance of the container. * Add a test to `python_packaging/sample_restraint` to confirm that IRestraintPotental code is actually called in GROMACS 2020+ / gmxapi 0.1+. * Follow up in issue #4079 to migrate to using a `SimulationInput` and expanded `SimulationContext` interface, and remove `gmxapi::System`, `gmxapi::Workflow`, and `gmxapi::WorkSpec` The proposed patch for GROMACS 2020, 2021, and 2022 is illustrated in the following sequence diagram. ```plantuml @startuml 'https://plantuml.com/sequence-diagram 'autonumber autoactivate on context.py -> _gmxapi: Context() _gmxapi -> "gmxpy::PyContext" **: create "gmxpy::PyContext" -> "gmxapi::Context" **: gmxapi::createContext() return ... context.py -> plugin: _builder.add_subscriber(mdbuilder) deactivate context.py -> plugin: _builder.build(graph) plugin -> restraint **: create plugin -> context.py: mdbuilder.potential.append(restraint) deactivate deactivate plugin ... context.py -> context.py: mdbuilder.build(graph) 'in MD builder-> launch context.py -> _gmxapi: from_tpr() _gmxapi -> "gmxapi::System" **: create return system context.py -> _gmxapi: setMDArgs() _gmxapi -> "gmxpy::PyContext": setMDArgs() "gmxpy::PyContext" -> "gmxapi::Context": setMDArgs() deactivate deactivate deactivate _gmxapi context.py -> _gmxapi: add_mdmodule(plugin) _gmxapi -> "gmxpy::PyContext": addMDModule(restraint) "gmxpy::PyContext" -> "gmxpy::PyContext": getSpec() return this->workNodes_ "gmxpy::PyContext" -> restraint: bind(pycontext.workspec) restraint -> "gmxpy::PyContext": gmxapi::MDWorkSpec::addModule(module) note right restraint added to PyContext member. end note deactivate deactivate deactivate deactivate context.py -> _gmxapi: system.launch(context) _gmxapi -> gmxapi: getWork(system) note right We now only use gmxapi::System to hold the TPR filename. We can replace gmxapi::System with gmxapi::SimulationInput. end note gmxapi -> "gmxapi::System": systemimpl.workflow_ return return gmxapi::Workflow _gmxapi -> "gmxpy::PyContext": launch(work) "gmxpy::PyContext" -> gmxapi: launchSession(context, work) gmxapi -> "gmxapi::Context": launch(work) note right "gmxapi::Context" Convert filename to SimulationInput. (Should be done before entering ContextImpl::launch()) end note return return "gmxapi::Session" "gmxpy::PyContext" -> gmxapi: this->workNodes_->getModules() return restraints loop foreach restraint "gmxpy::PyContext" -> gmxapi: addSessionRestraint(session, restraint) deactivate end return gmxapi::Session return _gmxapi::MDSession return session ' in session launch context.py -> _gmxapi: _gmxapi.MDSession.run() _gmxapi -> "gmxapi::Session": run() ... @enduml ``` ## Relates to See #3145 and #4079
issue