mdadash 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mdadash/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """
2
+ mdadash
3
+ Dashboard for tracking and analyzing live MD simulations with streaming.
4
+ """
5
+
6
+ # Add imports here
7
+ from importlib.metadata import version
8
+
9
+ __version__ = version("mdadash")
File without changes
@@ -0,0 +1,3 @@
1
+ def example_function(number: float) -> float:
2
+ """Return `2*number`. This is and example for tests."""
3
+ return 2 * number
File without changes
@@ -0,0 +1,210 @@
1
+ import asyncio
2
+
3
+ import comm
4
+ import MDAnalysis as mda
5
+
6
+
7
+ class CommHandler:
8
+ """Comm Handler
9
+
10
+ This class is responsible for handling all the communication to and from
11
+ this kernel. This is the class that interfaces with the KernelManager on
12
+ the server side.
13
+
14
+ """
15
+
16
+ def __init__(self):
17
+ self._comm = None
18
+ self._handlers = {}
19
+ comm.get_comm_manager().register_target(
20
+ "kernel_comm_handler", self._handle_comm_open
21
+ )
22
+
23
+ def register_handler(self, msg_type: str, handler_func: callable) -> None:
24
+ """Register a handler function for a message type"""
25
+ self._handlers[msg_type] = handler_func
26
+
27
+ def send(self, msg: dict) -> None:
28
+ """Send a message (response) back on the comm
29
+
30
+ Parameters
31
+ ----------
32
+ msg: dict
33
+ A message dictionary
34
+
35
+ """
36
+ if self._comm is not None:
37
+ self._comm.send(msg)
38
+ else:
39
+ raise ValueError("comm is not open yet") # pragma: no cover
40
+
41
+ def _handle_comm_open(self, _comm: comm.base_comm.BaseComm, _msg):
42
+ """Internal: Handler when the comm is opened (comm_open)"""
43
+ self._comm = _comm
44
+ # set the handler for comm messages (comm_msg)
45
+ self._comm.on_msg(self._handle_msg)
46
+
47
+ def _handle_msg(self, msg):
48
+ """Internal: Dispatch the message to the registered handler"""
49
+ content_data = msg["content"]["data"]
50
+ msg_type = content_data["msg_type"]
51
+ if msg_type in self._handlers:
52
+ self._handlers[msg_type](content_data["data"])
53
+ else:
54
+ error_msg = f"{msg_type} does not have a registered handler"
55
+ self.send({"status": "error", "message": error_msg})
56
+ raise ValueError(error_msg)
57
+
58
+
59
+ class UniverseManager:
60
+ """Universe Manager
61
+
62
+ This class is responsible for managing all MDAnalysis universes. It has
63
+ handlers to interact with the MD simulation. These handlers are invoked by
64
+ comm messages sent from the server.
65
+
66
+ This also provides an iterable and indexable access to the individual
67
+ universes.
68
+
69
+ """
70
+
71
+ def __init__(self):
72
+ self._universes = []
73
+ self._iter_loop_task = None
74
+ self._iter_loop_running = False
75
+ self._iter_loop_resumed = asyncio.Event()
76
+ self._iter_loop_resumed.clear()
77
+
78
+ def __iter__(self) -> iter:
79
+ """To support iteration"""
80
+ return iter(self._universes)
81
+
82
+ def __len__(self) -> int:
83
+ """Number of universes"""
84
+ return len(self._universes)
85
+
86
+ def __getitem__(self, index: int):
87
+ """Return universe based on index"""
88
+ # numeric index based array access
89
+ _max = len(self._universes)
90
+ if 0 <= index < _max:
91
+ return self._universes[index]
92
+ raise ValueError(f"Invalid index {index} of {_max} items")
93
+
94
+ def init_n_universes(self, n: int) -> None:
95
+ """Initialize array for n universes
96
+
97
+ Parameters
98
+ ----------
99
+ n: int
100
+ Number of universes to initialize
101
+
102
+ """
103
+ self._universes = [None] * n
104
+
105
+ def connect_to_simulations(self, universe_configs: list[dict]) -> None:
106
+ """Connect to MD simulations
107
+
108
+ Parameters
109
+ ----------
110
+ universe_configs: list[dict]
111
+ A list of configurations for universe(s) creation.
112
+ Each dict has universe related config like topology, trajectory,
113
+ imdclient params, user-defined kwargs etc
114
+
115
+ """
116
+ try:
117
+ for uid, config in enumerate(universe_configs):
118
+ kwargs = {}
119
+ topology = config.get("topology")
120
+ trajectory = config.get("trajectory")
121
+ for key, value in config.items():
122
+ if key in ("topology", "trajectory", "kwargs"):
123
+ continue
124
+ if value is not None:
125
+ kwargs[key] = value
126
+ for name, value in config["kwargs"]:
127
+ if name.strip():
128
+ kwargs[name] = value
129
+ # create universe
130
+ u = mda.Universe(
131
+ topology,
132
+ trajectory,
133
+ **kwargs,
134
+ )
135
+ if uid == 0:
136
+ self._send_tsdata(u)
137
+ self._universes[uid] = u
138
+ # start iter loop for trajectories
139
+ self._iter_loop_resumed.clear()
140
+ self._iter_loop_running = True
141
+ self._iter_loop_task = asyncio.create_task(self._iter_loop())
142
+ comm_handler.send({"status": "ok"})
143
+ except Exception as e: # pylint: disable=broad-exception-caught
144
+ comm_handler.send({"status": "error", "message": str(e)})
145
+
146
+ def _send_tsdata(self, u: mda.Universe):
147
+ """Internal: Send timestep data out"""
148
+ comm_handler.send(
149
+ {
150
+ "tsinfo": {
151
+ "frame": u.trajectory.frame,
152
+ "tsdata": u.trajectory.ts.data,
153
+ }
154
+ }
155
+ )
156
+
157
+ def disconnect_from_simulations(self, _data: dict) -> None:
158
+ """Disconnect from MD simulations"""
159
+ self._iter_loop_running = False
160
+ self._iter_loop_task.cancel()
161
+ for u in self._universes:
162
+ u.trajectory.close()
163
+ comm_handler.send({"status": "ok"})
164
+
165
+ def pause_simulations(self, _data: dict) -> None:
166
+ """Pause MD simulations"""
167
+ self._iter_loop_resumed.clear()
168
+ comm_handler.send({"status": "ok"})
169
+
170
+ def resume_simulations(self, _data: dict) -> None:
171
+ """Resume MD simulations"""
172
+ self._iter_loop_resumed.set()
173
+ comm_handler.send({"status": "ok"})
174
+
175
+ def _trajectory_next(self, u):
176
+ """Internal: Iterate trajectory by 1 frame"""
177
+ return u.trajectory.next()
178
+
179
+ async def _iter_loop(self):
180
+ """Internal: Iteration loop for trajectories"""
181
+ try:
182
+ while self._iter_loop_running:
183
+ await self._iter_loop_resumed.wait()
184
+ for uid, u in enumerate(self._universes):
185
+ try:
186
+ # iterate in thread to not block on a network call here
187
+ await asyncio.to_thread(self._trajectory_next, u)
188
+ if uid == 0:
189
+ self._send_tsdata(u)
190
+ # await asyncio.sleep(0)
191
+ except StopIteration as e: # pragma: no cover
192
+ print(e)
193
+ await asyncio.sleep(0)
194
+ except asyncio.CancelledError:
195
+ pass
196
+
197
+
198
+ def init_n_universes(data: dict) -> None:
199
+ um.init_n_universes(data.get("n"))
200
+
201
+
202
+ um = UniverseManager()
203
+ comm_handler = CommHandler()
204
+ comm_handler.register_handler("init_n_universes", init_n_universes)
205
+ comm_handler.register_handler("connect_to_simulations", um.connect_to_simulations)
206
+ comm_handler.register_handler(
207
+ "disconnect_from_simulations", um.disconnect_from_simulations
208
+ )
209
+ comm_handler.register_handler("pause_simulations", um.pause_simulations)
210
+ comm_handler.register_handler("resume_simulations", um.resume_simulations)
@@ -0,0 +1,305 @@
1
+ import asyncio
2
+ import logging
3
+ import queue
4
+ import sys
5
+ import uuid
6
+
7
+ import socketio
8
+ from jupyter_client import AsyncKernelManager
9
+
10
+ from ..state.manager import StateManager
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ # pylint: disable=too-many-instance-attributes
16
+ class KernelManager:
17
+ """Kernel Manager
18
+
19
+ This class is responsible for managing the AsyncKernelManager (async kernel)
20
+ that runs all the MDAnalysis code. It takes care of starting the async
21
+ kernel, stopping it and communicating with it. It interfaces with the
22
+ CommHandler on the kernel side for messaging.
23
+
24
+ """
25
+
26
+ def __init__(self, sm: StateManager, sio: socketio.AsyncServer):
27
+ self.sm = sm
28
+ self.sio = sio
29
+ self.km = AsyncKernelManager(kernel_name="python3")
30
+ self.kc = None
31
+ self._pending_futures = {}
32
+ self._is_running = False
33
+ self.comm_id = uuid.uuid4().hex
34
+ self.listen_task = None
35
+
36
+ async def start(self) -> None:
37
+ """Start the async kernel"""
38
+ # start the kernel
39
+ await self.km.start_kernel()
40
+ # create a client
41
+ self.kc = self.km.client()
42
+ self.kc.start_channels()
43
+ await self.kc.wait_for_ready()
44
+ # create task to listen on iopub and shell channels
45
+ self.listen_task = asyncio.create_task(self._start_listening())
46
+ # initialize the kernel core
47
+ self.kc.execute("from mdadash.backend.kernel import core")
48
+ # open comms with the kernel
49
+ self._comm_open()
50
+ self._is_running = True
51
+ # initialize n universes in kernel universe manager
52
+ await self.send_message(
53
+ "init_n_universes", {"n": len(self.sm.universe_configs)}
54
+ )
55
+
56
+ async def stop(self) -> None:
57
+ """Stop the async kernel"""
58
+ self._is_running = False
59
+ # wait for listen task to completely exit
60
+ await self.listen_task
61
+ self.kc.stop_channels()
62
+ self.kc = None
63
+ # shutdown kernel gracefully
64
+ await self.km.shutdown_kernel(now=False)
65
+ await self.km.cleanup_resources()
66
+
67
+ async def _start_listening(self):
68
+ """Internal: Create separate listen tasks for iopub and shell"""
69
+ await asyncio.gather(
70
+ self._listen_iopub_channel(),
71
+ self._listen_shell_channel(),
72
+ )
73
+
74
+ async def _emit_tsdata(self, tsinfo):
75
+ """Internal: Emit timestep data"""
76
+ tsdata = tsinfo["tsdata"]
77
+ step = tsdata.get("step", None)
78
+ total_steps = self.sm.universe_configs[0].get("total_steps", None)
79
+ done = (step / total_steps) * 100 if step and total_steps else None
80
+ timestep_info = {
81
+ "frame": tsinfo.get("frame", None),
82
+ "time": tsdata.get("time", None),
83
+ "step": step,
84
+ "done": done,
85
+ "energies": {
86
+ "temperature": tsdata.get("temperature", None),
87
+ "total_energy": tsdata.get("total_energy", None),
88
+ "potential_energy": tsdata.get("potential_energy", None),
89
+ "van_der_walls_energy": tsdata.get("van_der_walls_energy", None),
90
+ "coulomb_energy": tsdata.get("coulomb_energy", None),
91
+ "bonds_energy": tsdata.get("bonds_energy", None),
92
+ "angles_energy": tsdata.get("angles_energy", None),
93
+ "dihedrals_energy": tsdata.get("dihedrals_energy", None),
94
+ "improper_dihedrals_energy": tsdata.get(
95
+ "improper_dihedrals_energy", None
96
+ ),
97
+ },
98
+ }
99
+ await self.sio.emit("timestepInfo", timestep_info)
100
+
101
+ # pylint: disable=too-many-branches
102
+ async def _listen_iopub_channel(self):
103
+ """Internal: Listen on iopub channel"""
104
+ while self._is_running:
105
+ try:
106
+ msg = await self.kc.iopub_channel.get_msg(timeout=0.1)
107
+ msg_type = msg["header"]["msg_type"]
108
+ content = msg["content"]
109
+ parent_id = msg.get("parent_header", {}).get("msg_id")
110
+ # check if a pending future can be resolved with msg
111
+ resolve_future = False
112
+ if parent_id and parent_id in self._pending_futures:
113
+ future = self._pending_futures[parent_id]
114
+ if not future.done():
115
+ resolve_future = True
116
+ # handle different msg_type's
117
+ if msg_type == "comm_msg":
118
+ data = msg["content"]["data"]
119
+ if "tsinfo" in data:
120
+ await self._emit_tsdata(data["tsinfo"])
121
+ elif resolve_future:
122
+ future.set_result(data)
123
+ continue
124
+ elif msg_type == "stream":
125
+ if resolve_future:
126
+ future.set_result(msg["content"]["text"])
127
+ continue
128
+ # redirect kernel stdout and stderr to this server output
129
+ if content["name"] == "stdout" or content["name"] == "stderr":
130
+ output = content["text"]
131
+ file = sys.stdout if content["name"] == "stdout" else sys.stderr
132
+ print(
133
+ f"KERNEL ({content['name']}): {output}", end="", file=file
134
+ )
135
+ elif msg_type == "error":
136
+ # redirect kernel errors to server output
137
+ print(f"KERNEL (error): {content['ename']}: {content['evalue']}")
138
+ if resolve_future:
139
+ future.set_result(content["evalue"])
140
+ continue
141
+ else:
142
+ logger.debug("IOPUB: %s", msg)
143
+ # TODO: handle other message types
144
+ except (asyncio.TimeoutError, queue.Empty):
145
+ continue
146
+
147
+ async def _listen_shell_channel(self):
148
+ """Internal: Listen on shell channel"""
149
+ while self._is_running:
150
+ try:
151
+ msg = await self.kc.shell_channel.get_msg(timeout=0.1)
152
+ # msg_type = msg["header"]["msg_type"]
153
+ # content = msg["content"]
154
+ logger.debug("SHELL: %s", msg)
155
+ except (asyncio.TimeoutError, queue.Empty):
156
+ continue
157
+
158
+ def _comm_open(self):
159
+ """Internal: Open comms with the kernel"""
160
+ content = {
161
+ "comm_id": self.comm_id,
162
+ "target_name": "kernel_comm_handler",
163
+ "data": {"msg_type": "handshake"},
164
+ }
165
+ open_msg = self.kc.session.msg("comm_open", content=content)
166
+ self.kc.shell_channel.send(open_msg)
167
+
168
+ async def send_message(self, msg_type: str, data: dict) -> None:
169
+ """Send message to kernel and don't await a response
170
+
171
+ Parameters
172
+ ----------
173
+ msg_type: str
174
+ A message type string that the kernel has a handler registered for
175
+
176
+ data: dict
177
+ Dict that gets passed to the handler in the kernel
178
+
179
+ """
180
+ content = {
181
+ "comm_id": self.comm_id,
182
+ "target_name": "kernel_comm_handler",
183
+ "data": {"msg_type": msg_type, "data": data},
184
+ }
185
+ data_msg = self.kc.session.msg("comm_msg", content=content)
186
+ self.kc.shell_channel.send(data_msg)
187
+
188
+ async def send_message_await_response(
189
+ self, msg_type: str, data: dict = None, timeout: int = 5
190
+ ) -> dict | None:
191
+ """Send message to kernel and wait for a response (async)
192
+
193
+ Parameters
194
+ ----------
195
+ msg_type: str
196
+ A message type string that the kernel has a handler registered for
197
+
198
+ data: dict
199
+ Dict that gets passed to the handler in the kernel
200
+
201
+ timeout: int
202
+ Timeout in seconds
203
+
204
+ """
205
+ content = {
206
+ "comm_id": self.comm_id,
207
+ "target_name": "kernel_comm_handler",
208
+ "data": {"msg_type": msg_type, "data": data},
209
+ }
210
+ data_msg = self.kc.session.msg("comm_msg", content=content)
211
+ msg_id = data_msg["header"]["msg_id"]
212
+ # add to the _pending_futures to that it gets resolved when the
213
+ # response arrives on the iopub channel
214
+ future = asyncio.get_running_loop().create_future()
215
+ self._pending_futures[msg_id] = future
216
+ self.kc.shell_channel.send(data_msg)
217
+ try:
218
+ return await asyncio.wait_for(future, timeout=timeout)
219
+ except asyncio.TimeoutError as e: # pragma: no cover
220
+ raise TimeoutError("Timed out waiting for kernel response") from e
221
+ finally:
222
+ self._pending_futures.pop(msg_id, None)
223
+
224
+ async def execute_code(self, code: str, timeout: int = 5) -> str:
225
+ """Execute code in the kernel
226
+
227
+ Parameters
228
+ ----------
229
+ code: str
230
+ Code to execute in the kernel
231
+
232
+ timeout: int
233
+ Timeout in seconds
234
+
235
+ """
236
+ msg_id = self.kc.execute(code)
237
+ future = asyncio.get_running_loop().create_future()
238
+ self._pending_futures[msg_id] = future
239
+ try:
240
+ return await asyncio.wait_for(future, timeout=timeout)
241
+ except asyncio.TimeoutError as e: # pragma: no cover
242
+ raise TimeoutError("Timed out waiting for kernel execute response") from e
243
+ finally:
244
+ self._pending_futures.pop(msg_id, None)
245
+
246
+ async def connect_to_simulations(self) -> dict:
247
+ """Connect to the MD simulation
248
+
249
+ Returns
250
+ -------
251
+ response: dict
252
+ Response dict indicating status
253
+
254
+ """
255
+ response = await self.send_message_await_response(
256
+ "connect_to_simulations", self.sm.universe_configs
257
+ )
258
+ if response["status"] == "ok":
259
+ self.sm.running_state["connected"] = True
260
+ self.sm.running_state["running"] = False
261
+ return response
262
+
263
+ async def disconnect_from_simulations(self) -> dict:
264
+ """Disconnect from the MD simulation
265
+
266
+ Returns
267
+ -------
268
+ response: dict
269
+ Response dict indicating status
270
+
271
+ """
272
+ response = await self.send_message_await_response(
273
+ "disconnect_from_simulations", {}
274
+ )
275
+ if response["status"] == "ok":
276
+ self.sm.running_state["connected"] = False
277
+ return response
278
+
279
+ async def pause_simulations(self) -> dict:
280
+ """Pause MD simulations
281
+
282
+ Returns
283
+ -------
284
+ response: dict
285
+ Response dict indicating status
286
+
287
+ """
288
+ response = await self.send_message_await_response("pause_simulations", {})
289
+ if response["status"] == "ok":
290
+ self.sm.running_state["running"] = False
291
+ return response
292
+
293
+ async def resume_simulations(self) -> dict:
294
+ """Resume MD simulations
295
+
296
+ Returns
297
+ -------
298
+ response: dict
299
+ Response dict indicating status
300
+
301
+ """
302
+ response = await self.send_message_await_response("resume_simulations", {})
303
+ if response["status"] == "ok":
304
+ self.sm.running_state["running"] = True
305
+ return response