westpa 2022.13__cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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.
Files changed (162) hide show
  1. westpa/__init__.py +14 -0
  2. westpa/_version.py +21 -0
  3. westpa/analysis/__init__.py +5 -0
  4. westpa/analysis/core.py +749 -0
  5. westpa/analysis/statistics.py +27 -0
  6. westpa/analysis/trajectories.py +369 -0
  7. westpa/cli/__init__.py +0 -0
  8. westpa/cli/core/__init__.py +0 -0
  9. westpa/cli/core/w_fork.py +152 -0
  10. westpa/cli/core/w_init.py +230 -0
  11. westpa/cli/core/w_run.py +77 -0
  12. westpa/cli/core/w_states.py +212 -0
  13. westpa/cli/core/w_succ.py +99 -0
  14. westpa/cli/core/w_truncate.py +68 -0
  15. westpa/cli/tools/__init__.py +0 -0
  16. westpa/cli/tools/ploterr.py +506 -0
  17. westpa/cli/tools/plothist.py +706 -0
  18. westpa/cli/tools/w_assign.py +597 -0
  19. westpa/cli/tools/w_bins.py +166 -0
  20. westpa/cli/tools/w_crawl.py +119 -0
  21. westpa/cli/tools/w_direct.py +557 -0
  22. westpa/cli/tools/w_dumpsegs.py +94 -0
  23. westpa/cli/tools/w_eddist.py +506 -0
  24. westpa/cli/tools/w_fluxanl.py +376 -0
  25. westpa/cli/tools/w_ipa.py +832 -0
  26. westpa/cli/tools/w_kinavg.py +127 -0
  27. westpa/cli/tools/w_kinetics.py +96 -0
  28. westpa/cli/tools/w_multi_west.py +414 -0
  29. westpa/cli/tools/w_ntop.py +213 -0
  30. westpa/cli/tools/w_pdist.py +515 -0
  31. westpa/cli/tools/w_postanalysis_matrix.py +82 -0
  32. westpa/cli/tools/w_postanalysis_reweight.py +53 -0
  33. westpa/cli/tools/w_red.py +491 -0
  34. westpa/cli/tools/w_reweight.py +780 -0
  35. westpa/cli/tools/w_select.py +226 -0
  36. westpa/cli/tools/w_stateprobs.py +111 -0
  37. westpa/cli/tools/w_timings.py +113 -0
  38. westpa/cli/tools/w_trace.py +599 -0
  39. westpa/core/__init__.py +0 -0
  40. westpa/core/_rc.py +673 -0
  41. westpa/core/binning/__init__.py +55 -0
  42. westpa/core/binning/_assign.c +36018 -0
  43. westpa/core/binning/_assign.cpython-312-aarch64-linux-gnu.so +0 -0
  44. westpa/core/binning/_assign.pyx +370 -0
  45. westpa/core/binning/assign.py +454 -0
  46. westpa/core/binning/binless.py +96 -0
  47. westpa/core/binning/binless_driver.py +54 -0
  48. westpa/core/binning/binless_manager.py +189 -0
  49. westpa/core/binning/bins.py +47 -0
  50. westpa/core/binning/mab.py +506 -0
  51. westpa/core/binning/mab_driver.py +54 -0
  52. westpa/core/binning/mab_manager.py +197 -0
  53. westpa/core/data_manager.py +1761 -0
  54. westpa/core/extloader.py +74 -0
  55. westpa/core/h5io.py +1079 -0
  56. westpa/core/kinetics/__init__.py +24 -0
  57. westpa/core/kinetics/_kinetics.c +45174 -0
  58. westpa/core/kinetics/_kinetics.cpython-312-aarch64-linux-gnu.so +0 -0
  59. westpa/core/kinetics/_kinetics.pyx +815 -0
  60. westpa/core/kinetics/events.py +147 -0
  61. westpa/core/kinetics/matrates.py +156 -0
  62. westpa/core/kinetics/rate_averaging.py +266 -0
  63. westpa/core/progress.py +218 -0
  64. westpa/core/propagators/__init__.py +54 -0
  65. westpa/core/propagators/executable.py +592 -0
  66. westpa/core/propagators/loaders.py +196 -0
  67. westpa/core/reweight/__init__.py +14 -0
  68. westpa/core/reweight/_reweight.c +36899 -0
  69. westpa/core/reweight/_reweight.cpython-312-aarch64-linux-gnu.so +0 -0
  70. westpa/core/reweight/_reweight.pyx +439 -0
  71. westpa/core/reweight/matrix.py +126 -0
  72. westpa/core/segment.py +119 -0
  73. westpa/core/sim_manager.py +839 -0
  74. westpa/core/states.py +359 -0
  75. westpa/core/systems.py +93 -0
  76. westpa/core/textio.py +74 -0
  77. westpa/core/trajectory.py +603 -0
  78. westpa/core/we_driver.py +910 -0
  79. westpa/core/wm_ops.py +43 -0
  80. westpa/core/yamlcfg.py +298 -0
  81. westpa/fasthist/__init__.py +34 -0
  82. westpa/fasthist/_fasthist.c +38755 -0
  83. westpa/fasthist/_fasthist.cpython-312-aarch64-linux-gnu.so +0 -0
  84. westpa/fasthist/_fasthist.pyx +222 -0
  85. westpa/mclib/__init__.py +271 -0
  86. westpa/mclib/__main__.py +28 -0
  87. westpa/mclib/_mclib.c +34610 -0
  88. westpa/mclib/_mclib.cpython-312-aarch64-linux-gnu.so +0 -0
  89. westpa/mclib/_mclib.pyx +226 -0
  90. westpa/oldtools/__init__.py +4 -0
  91. westpa/oldtools/aframe/__init__.py +35 -0
  92. westpa/oldtools/aframe/atool.py +75 -0
  93. westpa/oldtools/aframe/base_mixin.py +26 -0
  94. westpa/oldtools/aframe/binning.py +178 -0
  95. westpa/oldtools/aframe/data_reader.py +560 -0
  96. westpa/oldtools/aframe/iter_range.py +200 -0
  97. westpa/oldtools/aframe/kinetics.py +117 -0
  98. westpa/oldtools/aframe/mcbs.py +153 -0
  99. westpa/oldtools/aframe/output.py +39 -0
  100. westpa/oldtools/aframe/plotting.py +88 -0
  101. westpa/oldtools/aframe/trajwalker.py +126 -0
  102. westpa/oldtools/aframe/transitions.py +469 -0
  103. westpa/oldtools/cmds/__init__.py +0 -0
  104. westpa/oldtools/cmds/w_ttimes.py +361 -0
  105. westpa/oldtools/files.py +34 -0
  106. westpa/oldtools/miscfn.py +23 -0
  107. westpa/oldtools/stats/__init__.py +4 -0
  108. westpa/oldtools/stats/accumulator.py +35 -0
  109. westpa/oldtools/stats/edfs.py +129 -0
  110. westpa/oldtools/stats/mcbs.py +96 -0
  111. westpa/tools/__init__.py +33 -0
  112. westpa/tools/binning.py +472 -0
  113. westpa/tools/core.py +340 -0
  114. westpa/tools/data_reader.py +159 -0
  115. westpa/tools/dtypes.py +31 -0
  116. westpa/tools/iter_range.py +198 -0
  117. westpa/tools/kinetics_tool.py +343 -0
  118. westpa/tools/plot.py +283 -0
  119. westpa/tools/progress.py +17 -0
  120. westpa/tools/selected_segs.py +154 -0
  121. westpa/tools/wipi.py +751 -0
  122. westpa/trajtree/__init__.py +4 -0
  123. westpa/trajtree/_trajtree.c +17829 -0
  124. westpa/trajtree/_trajtree.cpython-312-aarch64-linux-gnu.so +0 -0
  125. westpa/trajtree/_trajtree.pyx +130 -0
  126. westpa/trajtree/trajtree.py +117 -0
  127. westpa/westext/__init__.py +0 -0
  128. westpa/westext/adaptvoronoi/__init__.py +3 -0
  129. westpa/westext/adaptvoronoi/adaptVor_driver.py +214 -0
  130. westpa/westext/hamsm_restarting/__init__.py +3 -0
  131. westpa/westext/hamsm_restarting/example_overrides.py +35 -0
  132. westpa/westext/hamsm_restarting/restart_driver.py +1165 -0
  133. westpa/westext/stringmethod/__init__.py +11 -0
  134. westpa/westext/stringmethod/fourier_fitting.py +69 -0
  135. westpa/westext/stringmethod/string_driver.py +253 -0
  136. westpa/westext/stringmethod/string_method.py +306 -0
  137. westpa/westext/weed/BinCluster.py +180 -0
  138. westpa/westext/weed/ProbAdjustEquil.py +100 -0
  139. westpa/westext/weed/UncertMath.py +247 -0
  140. westpa/westext/weed/__init__.py +10 -0
  141. westpa/westext/weed/weed_driver.py +192 -0
  142. westpa/westext/wess/ProbAdjust.py +101 -0
  143. westpa/westext/wess/__init__.py +6 -0
  144. westpa/westext/wess/wess_driver.py +217 -0
  145. westpa/work_managers/__init__.py +57 -0
  146. westpa/work_managers/core.py +396 -0
  147. westpa/work_managers/environment.py +134 -0
  148. westpa/work_managers/mpi.py +318 -0
  149. westpa/work_managers/processes.py +201 -0
  150. westpa/work_managers/serial.py +28 -0
  151. westpa/work_managers/threads.py +79 -0
  152. westpa/work_managers/zeromq/__init__.py +20 -0
  153. westpa/work_managers/zeromq/core.py +635 -0
  154. westpa/work_managers/zeromq/node.py +131 -0
  155. westpa/work_managers/zeromq/work_manager.py +526 -0
  156. westpa/work_managers/zeromq/worker.py +320 -0
  157. westpa-2022.13.dist-info/METADATA +179 -0
  158. westpa-2022.13.dist-info/RECORD +162 -0
  159. westpa-2022.13.dist-info/WHEEL +7 -0
  160. westpa-2022.13.dist-info/entry_points.txt +30 -0
  161. westpa-2022.13.dist-info/licenses/LICENSE +21 -0
  162. westpa-2022.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,635 @@
1
+ '''
2
+ Created on May 29, 2015
3
+
4
+ @author: mzwier
5
+ '''
6
+
7
+ import collections
8
+ import contextlib
9
+ import errno
10
+ import logging
11
+ import json
12
+ import multiprocessing
13
+ import os
14
+ import re
15
+ import signal
16
+ import socket
17
+ import sys
18
+ import tempfile
19
+ import threading
20
+ import time
21
+ import traceback
22
+ import uuid
23
+
24
+ import zmq
25
+ import numpy as np
26
+
27
+ # Every ten seconds the master requests a status report from workers.
28
+ # This also notifies workers that the master is still alive
29
+ DEFAULT_STATUS_POLL = 10
30
+
31
+ # If we haven't heard from the master or a worker (as appropriate) in these
32
+ # amounts of time, we assume a crash and shut down.
33
+ MASTER_CRASH_TIMEOUT = DEFAULT_STATUS_POLL * 6
34
+ WORKER_CRASH_TIMEOUT = DEFAULT_STATUS_POLL * 3
35
+
36
+ log = logging.getLogger(__name__)
37
+
38
+ signames = {
39
+ val: name for name, val in reversed(sorted(signal.__dict__.items())) if name.startswith('SIG') and not name.startswith('SIG_')
40
+ }
41
+
42
+
43
+ DEFAULT_LINGER = 1
44
+
45
+
46
+ def randport(address='127.0.0.1'):
47
+ '''Select a random unused TCP port number on the given address.'''
48
+ s = socket.socket()
49
+ s.bind((address, 0))
50
+ try:
51
+ port = s.getsockname()[1]
52
+ finally:
53
+ s.close()
54
+ return port
55
+
56
+
57
+ class ZMQWMError(RuntimeError):
58
+ '''Base class for errors related to the ZeroMQ work manager itself'''
59
+
60
+ pass
61
+
62
+
63
+ class ZMQWorkerMissing(ZMQWMError):
64
+ '''Exception representing that a worker processing a task died or disappeared'''
65
+
66
+ pass
67
+
68
+
69
+ class ZMQWMEnvironmentError(ZMQWMError):
70
+ '''Class representing an error in the environment in which the ZeroMQ work manager is running.
71
+ This includes such things as master/worker ID mismatches.'''
72
+
73
+
74
+ class ZMQWMTimeout(ZMQWMEnvironmentError):
75
+ '''A timeout of a sort that indicatess that a master or worker has failed or never started.'''
76
+
77
+
78
+ class Message:
79
+ SHUTDOWN = 'shutdown'
80
+
81
+ ACK = 'ok'
82
+ NAK = 'no'
83
+ IDENTIFY = 'identify' # Two-way identification (a reply must be an IDENTIFY message)
84
+ TASKS_AVAILABLE = 'tasks_available'
85
+ TASK_REQUEST = 'task_request'
86
+
87
+ MASTER_BEACON = 'master_alive'
88
+ RECONFIGURE_TIMEOUT = 'reconfigure_timeout'
89
+
90
+ TASK = 'task'
91
+ RESULT = 'result'
92
+
93
+ idempotent_announcement_messages = {SHUTDOWN, TASKS_AVAILABLE, MASTER_BEACON}
94
+
95
+ def __init__(self, message=None, payload=None, master_id=None, src_id=None):
96
+ if isinstance(message, Message):
97
+ self.message = message.message
98
+ self.payload = message.payload
99
+ self.master_id = message.master_id
100
+ self.src_id = message.src_id
101
+ else:
102
+ self.master_id = master_id
103
+ self.src_id = src_id
104
+ self.message = message
105
+ self.payload = payload
106
+
107
+ def __repr__(self):
108
+ return '<{!s} master_id={master_id!s} src_id={src_id!s} message={message!r} payload={payload!r}>'.format(
109
+ self.__class__.__name__, **self.__dict__
110
+ )
111
+
112
+ @classmethod
113
+ def coalesce_announcements(cls, messages):
114
+ d = collections.OrderedDict()
115
+ for msg in messages:
116
+ if msg.message in cls.idempotent_announcement_messages:
117
+ key = msg.message
118
+ else:
119
+ key = (msg.message, msg.payload)
120
+ d[key] = msg
121
+ coalesced = list(msg.values())
122
+ log.debug('coalesced {} announcements into {}'.format(len(messages), len(coalesced)))
123
+ return coalesced
124
+
125
+
126
+ TIMEOUT_MASTER_BEACON = 'master_beacon'
127
+ TIMEOUT_WORKER_CONTACT = 'worker_contact'
128
+
129
+
130
+ class Task:
131
+ def __init__(self, fn, args, kwargs, task_id=None):
132
+ self.task_id = task_id or uuid.uuid4()
133
+ self.fn = fn
134
+ self.args = args
135
+ self.kwargs = kwargs
136
+
137
+ def __repr__(self):
138
+ try:
139
+ return '<{} {task_id!s} {fn!r} {:d} args {:d} kwargs>'.format(
140
+ self.__class__.__name__, len(self.args), len(self.kwargs), **self.__dict__
141
+ )
142
+ except TypeError:
143
+ # no length
144
+ return '<{} {task_id!s} {fn!r}'.format(self.__class__.__name__, **self.__dict__)
145
+
146
+ def __hash__(self):
147
+ return hash(self.task_id)
148
+
149
+ def execute(self):
150
+ '''Run this task, returning a Result object.'''
151
+ rsl = Result(task_id=self.task_id)
152
+ try:
153
+ rsl.result = self.fn(*self.args, **self.kwargs)
154
+ except BaseException as e:
155
+ rsl.exception = e
156
+ rsl.traceback = traceback.format_exc()
157
+ return rsl
158
+
159
+
160
+ class Result:
161
+ def __init__(self, task_id, result=None, exception=None, traceback=None):
162
+ self.task_id = task_id
163
+ self.result = result
164
+ self.exception = exception
165
+ self.traceback = traceback
166
+
167
+ def __repr__(self):
168
+ return '<{} {task_id!s} ({})>'.format(
169
+ self.__class__.__name__, 'result' if self.exception is None else 'exception', **self.__dict__
170
+ )
171
+
172
+ def __hash__(self):
173
+ return hash(self.task_id)
174
+
175
+
176
+ class PassiveTimer:
177
+ __slots__ = {'started', 'duration'}
178
+
179
+ def __init__(self, duration, started=None):
180
+ if started is None:
181
+ started = time.time()
182
+ self.started = started
183
+ self.duration = duration
184
+
185
+ @property
186
+ def expired(self, at=None):
187
+ at = at or time.time()
188
+ return (at - self.started) > self.duration
189
+
190
+ @property
191
+ def expires_in(self):
192
+ at = time.time()
193
+ return self.started + self.duration - at
194
+
195
+ def reset(self, at=None):
196
+ self.started = at or time.time()
197
+
198
+ start = reset
199
+
200
+
201
+ class PassiveMultiTimer:
202
+ def __init__(self):
203
+ self._identifiers = np.empty((0,), np.object_)
204
+ self._durations = np.empty((0,), float)
205
+ self._started = np.empty((0,), float)
206
+ self._indices = {} # indexes into durations/started, keyed by identifier
207
+
208
+ def add_timer(self, identifier, duration):
209
+ if identifier in self._identifiers:
210
+ raise KeyError('timer {!r} already present'.format(identifier))
211
+
212
+ new_idx = len(self._identifiers)
213
+
214
+ self._durations = np.pad(self._durations, (0, 1), mode='constant', constant_values=duration)
215
+ self._started = np.pad(self._started, (0, 1), mode='constant', constant_values=time.time())
216
+ self._identifiers = np.pad(self._identifiers, (0, 1), mode='constant', constant_values=identifier)
217
+
218
+ self._indices[identifier] = new_idx
219
+
220
+ def remove_timer(self, identifier):
221
+ idx = self._indices.pop(identifier)
222
+ self._durations = np.delete(self._durations, idx)
223
+ self._started = np.delete(self._started, idx)
224
+ self._identifiers = np.delete(self._identifiers, idx)
225
+
226
+ def change_duration(self, identifier, duration):
227
+ idx = self._indices[identifier]
228
+ self._durations[idx] = duration
229
+
230
+ def reset(self, identifier=None, at=None):
231
+ at = at or time.time()
232
+ if identifier is None:
233
+ # reset all timers
234
+ self._started.fill(at)
235
+ else:
236
+ self._started[self._indices[identifier]] = at
237
+
238
+ def expired(self, identifier, at=None):
239
+ at = at or time.time()
240
+ idx = self._indices[identifier]
241
+ return (at - self._started[idx]) > self._durations[idx]
242
+
243
+ def next_expiration(self):
244
+ at = time.time()
245
+ idx = (self._started + self._durations - at).argmin()
246
+ return self._identifiers[idx]
247
+
248
+ def next_expiration_in(self):
249
+ at = time.time()
250
+ idx = (self._started + self._durations - at).argmin()
251
+ next_at = self._started[idx] + self._durations[idx] - at
252
+ return next_at if next_at > 0 else 0
253
+
254
+ def which_expired(self, at=None):
255
+ at = at or time.time()
256
+ expired_indices = (at - self._started) > self._durations
257
+ return self._identifiers[expired_indices]
258
+
259
+
260
+ class ZMQCore:
261
+ # The overall communication topology (socket layout, etc)
262
+ # Cannot be updated without updating configuration files, command-line parameters,
263
+ # etc. (Changes break user scripts.)
264
+ PROTOCOL_MAJOR = 3
265
+
266
+ # The set of messages and replies in use.
267
+ # Cannot be updated without changing existing communications logic. (Changes break
268
+ # the ZMQ WM library.)
269
+ PROTOCOL_MINOR = 0
270
+
271
+ # Minor updates and additions to the protocol.
272
+ # Changes do not break the ZMQ WM library, but only add new
273
+ # functionality/code paths without changing existing code paths.
274
+ PROTOCOL_UPDATE = 0
275
+
276
+ PROTOCOL_VERSION = (PROTOCOL_MAJOR, PROTOCOL_MINOR, PROTOCOL_UPDATE)
277
+
278
+ # The default transport for "internal" (inter-thread/-process) communication
279
+ # IPC should work except on really odd systems with no local storage
280
+ internal_transport = 'ipc'
281
+
282
+ default_comm_mode = 'ipc'
283
+ default_master_heartbeat = 20.0
284
+ default_worker_heartbeat = 20.0
285
+ default_timeout_factor = 5.0
286
+ default_startup_timeout = 120.0
287
+ default_shutdown_timeout = 5.0
288
+
289
+ _ipc_endpoints_to_delete = []
290
+
291
+ @classmethod
292
+ def make_ipc_endpoint(cls):
293
+ (fd, socket_path) = tempfile.mkstemp()
294
+ os.close(fd)
295
+ endpoint = 'ipc://{}'.format(socket_path)
296
+ cls._ipc_endpoints_to_delete.append(endpoint)
297
+ return endpoint
298
+
299
+ @classmethod
300
+ def remove_ipc_endpoints(cls):
301
+ while cls._ipc_endpoints_to_delete:
302
+ endpoint = cls._ipc_endpoints_to_delete.pop()
303
+ assert endpoint.startswith('ipc://')
304
+ socket_path = endpoint[6:]
305
+ try:
306
+ os.unlink(socket_path)
307
+ except OSError as e:
308
+ if e.errno != errno.ENOENT:
309
+ log.debug('could not unlink IPC endpoint {!r}: {}'.format(socket_path, e))
310
+ else:
311
+ log.debug('unlinked IPC endpoint {!r}'.format(socket_path))
312
+
313
+ @classmethod
314
+ def make_tcp_endpoint(cls, address='127.0.0.1'):
315
+ return 'tcp://{}:{}'.format(address, randport(address))
316
+
317
+ @classmethod
318
+ def make_internal_endpoint(cls):
319
+ assert cls.internal_transport in {'ipc', 'tcp'}
320
+ if cls.internal_transport == 'ipc':
321
+ return cls.make_ipc_endpoint()
322
+ else: # cls.internal_transport == 'tcp'
323
+ return cls.make_tcp_endpoint()
324
+
325
+ def __init__(self):
326
+ # Unique identifier of this ZMQ node
327
+ self.node_id = uuid.uuid4()
328
+
329
+ # Identifier of the task distribution network (work manager)
330
+ self.network_id = None
331
+
332
+ # Beacons
333
+ # Workers expect to hear from the master at least every master_beacon_period
334
+ # Master expects to hear from the workers at least every worker_beacon_period
335
+ # If more than {master,worker}_beacon_period*timeout_factor elapses, the
336
+ # master/worker is considered missing.
337
+
338
+ self.worker_beacon_period = self.default_worker_heartbeat
339
+ self.master_beacon_period = self.default_master_heartbeat
340
+ self.timeout_factor = self.default_timeout_factor
341
+
342
+ # These should allow for some fuzz, and should ratchet up as more and
343
+ # more workers become available (maybe order 1 s for 100 workers?) This
344
+ # should also account appropriately for startup delay on difficult
345
+ # systems.
346
+
347
+ # Number of seconds to allow first contact between at least one worker
348
+ # and the master.
349
+ self.startup_timeout = self.default_startup_timeout
350
+
351
+ # A friendlier description for logging
352
+ self.node_description = '{!s} on {!s} at PID {:d}'.format(self.__class__.__name__, socket.gethostname(), os.getpid())
353
+
354
+ self.validation_fail_action = 'exit' # other options are 'raise' and 'warn'
355
+
356
+ self.log = logging.getLogger(__name__ + '.' + self.__class__.__name__ + '.' + str(self.node_id))
357
+
358
+ # ZeroMQ context
359
+ self.context = None
360
+
361
+ # External communication endpoints
362
+ self.rr_endpoint = None
363
+ self.ann_endpoint = None
364
+
365
+ self.inproc_endpoint = 'inproc://{!s}'.format(self.node_id)
366
+
367
+ # Sockets
368
+ self.rr_socket = None
369
+ self.ann_socket = None
370
+
371
+ # This is the main-thread end of this
372
+ self._inproc_socket = None
373
+
374
+ self.master_id = None
375
+
376
+ if os.environ.get('WWMGR_ZMQ_DEBUG_MESSAGES', 'n').upper() in {'Y', 'YES', '1', 'T', 'TRUE'}:
377
+ self._super_debug = True
378
+ else:
379
+ self._super_debug = None
380
+
381
+ def __repr__(self):
382
+ return '<{!s} {!s}>'.format(self.__class__.__name__, self.node_id)
383
+
384
+ def get_identification(self):
385
+ return {
386
+ 'node_id': self.node_id,
387
+ 'master_id': self.master_id,
388
+ 'class': self.__class__.__name__,
389
+ 'description': self.node_description,
390
+ 'hostname': socket.gethostname(),
391
+ 'pid': os.getpid(),
392
+ }
393
+
394
+ def validate_message(self, message):
395
+ '''Validate incoming message. Raises an exception if the message is improperly formatted (TypeError)
396
+ or does not correspond to the appropriate master (ZMQWMEnvironmentError).'''
397
+ try:
398
+ super_validator = super().validate_message
399
+ except AttributeError:
400
+ pass
401
+ else:
402
+ super_validator(message)
403
+
404
+ if not isinstance(message, Message):
405
+ raise TypeError('message is not an instance of core.Message')
406
+ if message.src_id is None:
407
+ raise ZMQWMEnvironmentError('message src_id is not set')
408
+ if self.master_id is not None and message.master_id is not None and message.master_id != self.master_id:
409
+ raise ZMQWMEnvironmentError(
410
+ 'incoming message associated with another master (this={!s}, incoming={!s}'.format(
411
+ self.master_id, message.master_id
412
+ )
413
+ )
414
+
415
+ @contextlib.contextmanager
416
+ def message_validation(self, msg):
417
+ '''A context manager for message validation. The instance variable ``validation_fail_action``
418
+ controls the behavior of this context manager:
419
+ * 'raise': re-raise the exception that indicated failed validation. Useful for development.
420
+ * 'exit' (default): report the error and exit the program.
421
+ * 'warn': report the error and continue.'''
422
+ try:
423
+ yield
424
+ except Exception as e:
425
+ if self.validation_fail_action == 'raise':
426
+ self.log.exception('message validation failed for {!r}'.format(msg))
427
+ raise
428
+ elif self.validation_fail_action == 'exit':
429
+ self.log.error('message validation falied: {!s}'.format(e))
430
+ sys.exit(1)
431
+ elif self.validation_fail_action == 'warn':
432
+ self.log.warning('message validation falied: {!s}'.format(e))
433
+
434
+ def recv_message(self, socket, flags=0, validate=True, timeout=None):
435
+ '''Receive a message object from the given socket, using the given flags.
436
+ Message validation is performed if ``validate`` is true.
437
+ If ``timeout`` is given, then it is the number of milliseconds to wait
438
+ prior to raising a ZMQWMTimeout exception. ``timeout`` is ignored if
439
+ ``flags`` includes ``zmq.NOBLOCK``.'''
440
+
441
+ if timeout is None or flags & zmq.NOBLOCK:
442
+ message = socket.recv_pyobj(flags)
443
+ else:
444
+ poller = zmq.Poller()
445
+ poller.register(socket, zmq.POLLIN)
446
+ try:
447
+ poll_results = dict(poller.poll(timeout=timeout))
448
+ if socket in poll_results:
449
+ message = socket.recv_pyobj(flags)
450
+ else:
451
+ raise ZMQWMTimeout('recv timed out')
452
+ finally:
453
+ poller.unregister(socket)
454
+
455
+ if self._super_debug:
456
+ self.log.debug('received {!r}'.format(message))
457
+ if validate:
458
+ with self.message_validation(message):
459
+ self.validate_message(message)
460
+ return message
461
+
462
+ def recv_all(self, socket, flags=0, validate=True):
463
+ '''Receive all messages currently available from the given socket.'''
464
+ messages = []
465
+ while True:
466
+ try:
467
+ messages.append(self.recv_message(socket, flags | zmq.NOBLOCK, validate))
468
+ except zmq.Again:
469
+ return messages
470
+
471
+ def recv_ack(self, socket, flags=0, validate=True, timeout=None):
472
+ msg = self.recv_message(socket, flags, validate, timeout)
473
+ if validate:
474
+ with self.message_validation(msg):
475
+ assert msg.message in (Message.ACK, Message.NAK)
476
+ return msg
477
+
478
+ def send_message(self, socket, message, payload=None, flags=0):
479
+ '''Send a message object. Subclasses may override this to
480
+ decorate the message with appropriate IDs, then delegate upward to actually send
481
+ the message. ``message`` may either be a pre-constructed ``Message`` object or
482
+ a message identifier, in which (latter) case ``payload`` will become the message payload.
483
+ ``payload`` is ignored if ``message`` is a ``Message`` object.'''
484
+
485
+ message = Message(message, payload)
486
+ if message.master_id is None:
487
+ message.master_id = self.master_id
488
+ message.src_id = self.node_id
489
+
490
+ if self._super_debug:
491
+ self.log.debug('sending {!r}'.format(message))
492
+ socket.send_pyobj(message, flags)
493
+
494
+ def send_reply(self, socket, original_message, reply=Message.ACK, payload=None, flags=0):
495
+ '''Send a reply to ``original_message`` on ``socket``. The reply message
496
+ is a Message object or a message identifier. The reply master_id and worker_id are
497
+ set from ``original_message``, unless master_id is not set, in which case it is
498
+ set from self.master_id.'''
499
+ reply = Message(reply, payload)
500
+ reply.master_id = original_message.master_id or self.master_id
501
+ assert original_message.worker_id is not None # should have been caught by validation prior to this
502
+ reply.worker_id = original_message.worker_id
503
+ self.send_message(socket, reply)
504
+
505
+ def send_ack(self, socket, original_message):
506
+ '''Send an acknowledgement message, which is mostly just to respect REQ/REP
507
+ recv/send patterns.'''
508
+ self.send_message(socket, Message(Message.ACK, master_id=original_message.master_id or self.master_id, src_id=self.node_id))
509
+
510
+ def send_nak(self, socket, original_message):
511
+ '''Send a negative acknowledgement message.'''
512
+ self.send_message(socket, Message(Message.NAK, master_id=original_message.master_id or self.master_id, src_id=self.node_id))
513
+
514
+ def send_inproc_message(self, message, payload=None, flags=0):
515
+ inproc_socket = self.context.socket(zmq.PUB)
516
+ inproc_socket.connect(self.inproc_endpoint)
517
+ # annoying wait for sockets to settle
518
+ time.sleep(0.01)
519
+ self.send_message(inproc_socket, message, payload, flags)
520
+ # used to be a close with linger here, but it was cutting off messages
521
+
522
+ def signal_shutdown(self):
523
+ try:
524
+ self.send_inproc_message(Message.SHUTDOWN)
525
+ except AttributeError:
526
+ # this is expected if self.context has been set to None (i.e. it has already been destroyed)
527
+ pass
528
+ except Exception as e:
529
+ self.log.debug('ignoring exception {!r} in signal_shutdown()'.format(e))
530
+
531
+ def shutdown_handler(self, signal=None, frame=None):
532
+ if signal is None:
533
+ self.log.info('shutting down')
534
+ else:
535
+ self.log.info('shutting down on signal {!s}'.format(signames.get(signal, signal)))
536
+ self.signal_shutdown()
537
+
538
+ def install_signal_handlers(self, signals=None):
539
+ if not signals:
540
+ signals = {signal.SIGINT, signal.SIGQUIT, signal.SIGTERM}
541
+
542
+ for sig in signals:
543
+ signal.signal(sig, self.shutdown_handler)
544
+
545
+ def install_sigint_handler(self):
546
+ self.install_signal_handlers()
547
+
548
+ def startup(self):
549
+ self.context = zmq.Context()
550
+ self.comm_thread = threading.Thread(target=self.comm_loop)
551
+ self.comm_thread.start()
552
+
553
+ # self.install_signal_handlers()
554
+
555
+ def shutdown(self):
556
+ self.shutdown_handler()
557
+
558
+ def join(self):
559
+ while True:
560
+ self.comm_thread.join(0.1)
561
+ if not self.comm_thread.is_alive():
562
+ break
563
+
564
+
565
+ def shutdown_process(process, timeout=1.0):
566
+ process.join(timeout)
567
+ if process.is_alive():
568
+ log.debug('sending SIGINT to process {:d}'.format(process.pid))
569
+ os.kill(process.pid, signal.SIGINT)
570
+ process.join(timeout)
571
+ if process.is_alive():
572
+ log.warning('sending SIGKILL to worker process {:d}'.format(process.pid))
573
+ os.kill(process.pid, signal.SIGKILL)
574
+ process.join()
575
+
576
+ log.debug('process {:d} terminated with code {:d}'.format(process.pid, process.exitcode))
577
+ else:
578
+ log.debug('worker process {:d} terminated gracefully with code {:d}'.format(process.pid, process.exitcode))
579
+ assert not process.is_alive()
580
+
581
+
582
+ class IsNode:
583
+ def __init__(self, n_local_workers=None):
584
+ from westpa.work_managers.zeromq.worker import ZMQWorker
585
+
586
+ if n_local_workers is None:
587
+ n_local_workers = multiprocessing.cpu_count()
588
+
589
+ self.downstream_rr_endpoint = None
590
+ self.downstream_ann_endpoint = None
591
+
592
+ if n_local_workers:
593
+ self.local_ann_endpoint = self.make_internal_endpoint()
594
+ self.local_rr_endpoint = self.make_internal_endpoint()
595
+ self.local_workers = [ZMQWorker(self.local_rr_endpoint, self.local_ann_endpoint) for _n in range(n_local_workers)]
596
+ else:
597
+ self.local_ann_endpoint = None
598
+ self.local_rr_endpoint = None
599
+ self.local_workers = []
600
+
601
+ self.local_worker_processes = [
602
+ multiprocessing.Process(target=worker.startup, args=(n,)) for (n, worker) in enumerate(self.local_workers)
603
+ ]
604
+
605
+ self.host_info_files = []
606
+
607
+ def write_host_info(self, filename=None):
608
+ filename = filename or 'zmq_host_info_{}.json'.format(self.node_id.hex)
609
+ hostname = socket.gethostname()
610
+
611
+ with open(filename, 'wt') as infofile:
612
+ info = {}
613
+ info['rr_endpoint'] = re.sub(r'\*', hostname, self.downstream_rr_endpoint or '')
614
+ info['ann_endpoint'] = re.sub(r'\*', hostname, self.downstream_ann_endpoint or '')
615
+ json.dump(info, infofile)
616
+ self.host_info_files.append(filename)
617
+
618
+ def startup(self):
619
+ for process in self.local_worker_processes:
620
+ process.start()
621
+
622
+ def shutdown(self):
623
+ try:
624
+ shutdown_timeout = self.shutdown_timeout
625
+ except AttributeError:
626
+ shutdown_timeout = 1.0
627
+
628
+ for process in self.local_worker_processes:
629
+ shutdown_process(process, shutdown_timeout)
630
+
631
+ for host_info_file in self.host_info_files:
632
+ try:
633
+ os.unlink(host_info_file)
634
+ except OSError:
635
+ pass