westpa 2022.10__cp312-cp312-macosx_10_9_x86_64.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.

Potentially problematic release.


This version of westpa might be problematic. Click here for more details.

Files changed (150) 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 +746 -0
  5. westpa/analysis/statistics.py +27 -0
  6. westpa/analysis/trajectories.py +360 -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 +59 -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 +596 -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 +547 -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 +378 -0
  25. westpa/cli/tools/w_ipa.py +833 -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 +486 -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_trace.py +599 -0
  38. westpa/core/__init__.py +0 -0
  39. westpa/core/_rc.py +673 -0
  40. westpa/core/binning/__init__.py +55 -0
  41. westpa/core/binning/_assign.cpython-312-darwin.so +0 -0
  42. westpa/core/binning/assign.py +449 -0
  43. westpa/core/binning/binless.py +96 -0
  44. westpa/core/binning/binless_driver.py +54 -0
  45. westpa/core/binning/binless_manager.py +190 -0
  46. westpa/core/binning/bins.py +47 -0
  47. westpa/core/binning/mab.py +427 -0
  48. westpa/core/binning/mab_driver.py +54 -0
  49. westpa/core/binning/mab_manager.py +198 -0
  50. westpa/core/data_manager.py +1694 -0
  51. westpa/core/extloader.py +74 -0
  52. westpa/core/h5io.py +995 -0
  53. westpa/core/kinetics/__init__.py +24 -0
  54. westpa/core/kinetics/_kinetics.cpython-312-darwin.so +0 -0
  55. westpa/core/kinetics/events.py +147 -0
  56. westpa/core/kinetics/matrates.py +156 -0
  57. westpa/core/kinetics/rate_averaging.py +266 -0
  58. westpa/core/progress.py +218 -0
  59. westpa/core/propagators/__init__.py +54 -0
  60. westpa/core/propagators/executable.py +715 -0
  61. westpa/core/reweight/__init__.py +14 -0
  62. westpa/core/reweight/_reweight.cpython-312-darwin.so +0 -0
  63. westpa/core/reweight/matrix.py +126 -0
  64. westpa/core/segment.py +119 -0
  65. westpa/core/sim_manager.py +830 -0
  66. westpa/core/states.py +359 -0
  67. westpa/core/systems.py +93 -0
  68. westpa/core/textio.py +74 -0
  69. westpa/core/trajectory.py +330 -0
  70. westpa/core/we_driver.py +908 -0
  71. westpa/core/wm_ops.py +43 -0
  72. westpa/core/yamlcfg.py +391 -0
  73. westpa/fasthist/__init__.py +34 -0
  74. westpa/fasthist/__main__.py +110 -0
  75. westpa/fasthist/_fasthist.cpython-312-darwin.so +0 -0
  76. westpa/mclib/__init__.py +264 -0
  77. westpa/mclib/__main__.py +28 -0
  78. westpa/mclib/_mclib.cpython-312-darwin.so +0 -0
  79. westpa/oldtools/__init__.py +4 -0
  80. westpa/oldtools/aframe/__init__.py +35 -0
  81. westpa/oldtools/aframe/atool.py +75 -0
  82. westpa/oldtools/aframe/base_mixin.py +26 -0
  83. westpa/oldtools/aframe/binning.py +178 -0
  84. westpa/oldtools/aframe/data_reader.py +560 -0
  85. westpa/oldtools/aframe/iter_range.py +200 -0
  86. westpa/oldtools/aframe/kinetics.py +117 -0
  87. westpa/oldtools/aframe/mcbs.py +146 -0
  88. westpa/oldtools/aframe/output.py +39 -0
  89. westpa/oldtools/aframe/plotting.py +90 -0
  90. westpa/oldtools/aframe/trajwalker.py +126 -0
  91. westpa/oldtools/aframe/transitions.py +469 -0
  92. westpa/oldtools/cmds/__init__.py +0 -0
  93. westpa/oldtools/cmds/w_ttimes.py +358 -0
  94. westpa/oldtools/files.py +34 -0
  95. westpa/oldtools/miscfn.py +23 -0
  96. westpa/oldtools/stats/__init__.py +4 -0
  97. westpa/oldtools/stats/accumulator.py +35 -0
  98. westpa/oldtools/stats/edfs.py +129 -0
  99. westpa/oldtools/stats/mcbs.py +89 -0
  100. westpa/tools/__init__.py +33 -0
  101. westpa/tools/binning.py +472 -0
  102. westpa/tools/core.py +340 -0
  103. westpa/tools/data_reader.py +159 -0
  104. westpa/tools/dtypes.py +31 -0
  105. westpa/tools/iter_range.py +198 -0
  106. westpa/tools/kinetics_tool.py +340 -0
  107. westpa/tools/plot.py +283 -0
  108. westpa/tools/progress.py +17 -0
  109. westpa/tools/selected_segs.py +154 -0
  110. westpa/tools/wipi.py +751 -0
  111. westpa/trajtree/__init__.py +4 -0
  112. westpa/trajtree/_trajtree.cpython-312-darwin.so +0 -0
  113. westpa/trajtree/trajtree.py +117 -0
  114. westpa/westext/__init__.py +0 -0
  115. westpa/westext/adaptvoronoi/__init__.py +3 -0
  116. westpa/westext/adaptvoronoi/adaptVor_driver.py +214 -0
  117. westpa/westext/hamsm_restarting/__init__.py +3 -0
  118. westpa/westext/hamsm_restarting/example_overrides.py +35 -0
  119. westpa/westext/hamsm_restarting/restart_driver.py +1165 -0
  120. westpa/westext/stringmethod/__init__.py +11 -0
  121. westpa/westext/stringmethod/fourier_fitting.py +69 -0
  122. westpa/westext/stringmethod/string_driver.py +253 -0
  123. westpa/westext/stringmethod/string_method.py +306 -0
  124. westpa/westext/weed/BinCluster.py +180 -0
  125. westpa/westext/weed/ProbAdjustEquil.py +100 -0
  126. westpa/westext/weed/UncertMath.py +247 -0
  127. westpa/westext/weed/__init__.py +10 -0
  128. westpa/westext/weed/weed_driver.py +182 -0
  129. westpa/westext/wess/ProbAdjust.py +101 -0
  130. westpa/westext/wess/__init__.py +6 -0
  131. westpa/westext/wess/wess_driver.py +207 -0
  132. westpa/work_managers/__init__.py +57 -0
  133. westpa/work_managers/core.py +396 -0
  134. westpa/work_managers/environment.py +134 -0
  135. westpa/work_managers/mpi.py +318 -0
  136. westpa/work_managers/processes.py +187 -0
  137. westpa/work_managers/serial.py +28 -0
  138. westpa/work_managers/threads.py +79 -0
  139. westpa/work_managers/zeromq/__init__.py +20 -0
  140. westpa/work_managers/zeromq/core.py +641 -0
  141. westpa/work_managers/zeromq/node.py +131 -0
  142. westpa/work_managers/zeromq/work_manager.py +526 -0
  143. westpa/work_managers/zeromq/worker.py +320 -0
  144. westpa-2022.10.dist-info/AUTHORS +22 -0
  145. westpa-2022.10.dist-info/LICENSE +21 -0
  146. westpa-2022.10.dist-info/METADATA +183 -0
  147. westpa-2022.10.dist-info/RECORD +150 -0
  148. westpa-2022.10.dist-info/WHEEL +5 -0
  149. westpa-2022.10.dist-info/entry_points.txt +29 -0
  150. westpa-2022.10.dist-info/top_level.txt +1 -0
@@ -0,0 +1,55 @@
1
+ from . import _assign
2
+ from . import assign, bins
3
+
4
+ from .assign import (
5
+ NopMapper,
6
+ FuncBinMapper,
7
+ PiecewiseBinMapper,
8
+ RectilinearBinMapper,
9
+ RecursiveBinMapper,
10
+ VectorizingFuncBinMapper,
11
+ VoronoiBinMapper,
12
+ )
13
+
14
+ from .mab import map_mab, MABBinMapper
15
+ from .binless import map_binless, BinlessMapper
16
+
17
+ from .mab_driver import MABDriver
18
+ from .mab_manager import MABSimManager
19
+ from .binless_manager import BinlessSimManager
20
+ from .binless_driver import BinlessDriver
21
+
22
+ from ._assign import accumulate_labeled_populations, assign_and_label, accumulate_state_populations_from_labeled
23
+ from ._assign import assignments_list_to_table
24
+
25
+ from .assign import coord_dtype, index_dtype
26
+ from .bins import Bin
27
+
28
+
29
+ __all__ = [
30
+ '_assign',
31
+ 'assign',
32
+ 'bins',
33
+ 'NopMapper',
34
+ 'FuncBinMapper',
35
+ 'PiecewiseBinMapper',
36
+ 'RectilinearBinMapper',
37
+ 'RecursiveBinMapper',
38
+ 'VectorizingFuncBinMapper',
39
+ 'VoronoiBinMapper',
40
+ 'map_mab',
41
+ 'map_binless',
42
+ 'MABBinMapper',
43
+ 'BinlessMapper',
44
+ 'MABDriver',
45
+ 'MABSimManager',
46
+ 'BinlessDriver',
47
+ 'BinlessSimManager',
48
+ 'accumulate_labeled_populations',
49
+ 'assign_and_label',
50
+ 'accumulate_state_populations_from_labeled',
51
+ 'assignments_list_to_table',
52
+ 'coord_dtype',
53
+ 'index_dtype',
54
+ 'Bin',
55
+ ]
@@ -0,0 +1,449 @@
1
+ '''
2
+ Bin assignment for WEST simulations. This module defines "bin mappers" which take
3
+ vectors of coordinates (or rather, coordinate tuples), and assign each a definite
4
+ integer value identifying a bin. Critical portions are implemented in a Cython
5
+ extension module.
6
+
7
+ A number of pre-defined bin mappers are available here:
8
+
9
+ * :class:`RectilinearBinMapper`, for bins divided by N-dimensional grids
10
+ * :class:`FuncBinMapper`, for functions which directly calculate bin assignments
11
+ for a number of coordinate values. This is best used with C/Cython/Numba
12
+ functions, or intellegently-tuned numpy-based Python functions.
13
+ * :class:`VectorizingFuncBinMapper`, for functions which calculate a bin
14
+ assignment for a single coordinate value. This is best used for arbitrary
15
+ Python functions.
16
+ * :class:`PiecewiseBinMapper`, for using a set of boolean-valued functions, one
17
+ per bin, to determine assignments. This is likely to be much slower than a
18
+ `FuncBinMapper` or `VectorizingFuncBinMapper` equipped with an appropriate
19
+ function, and its use is discouraged.
20
+
21
+ One "super-mapper" is available, for assembling more complex bin spaces from
22
+ simpler components:
23
+
24
+ * :class:`RecursiveBinMapper`, for nesting one set of bins within another.
25
+
26
+ Users are also free to implement their own mappers. A bin mapper must implement, at
27
+ least, an ``assign(coords, mask=None, output=None)`` method, which is responsible
28
+ for mapping each of the vector of coordinate tuples ``coords`` to an integer
29
+ (np.uint16) indicating a what bin that coordinate tuple falls into. The optional
30
+ ``mask`` (a numpy bool array) specifies that some coordinates are to be skipped; this is used,
31
+ for instance, by the recursive (nested) bin mapper to minimize the number of calculations
32
+ required to definitively assign a coordinate tuple to a bin. Similarly, the optional
33
+ ``output`` must be an integer (uint16) array of the same length as ``coords``, into which
34
+ assignments are written. The ``assign()`` function must return a reference to ``output``.
35
+ (This is used to avoid allocating many temporary output arrays in complex binning
36
+ scenarios.)
37
+
38
+ A user-defined bin mapper must also make an ``nbins`` property available, containing
39
+ the total number of bins within the mapper.
40
+
41
+ '''
42
+
43
+ import hashlib
44
+ import logging
45
+ import pickle
46
+
47
+ import numpy as np
48
+
49
+ from .bins import Bin
50
+ from ._assign import output_map, apply_down, apply_down_argmin_across, rectilinear_assign
51
+
52
+ # All bin numbers are 16-bit unsigned ints, with one element (65525) reserved to
53
+ # indicate unknown or unassigned points. This allows up to 65,536 bins, making
54
+ # rate and flux matrices up to 32 GB (2**32 elements * 8 bytes). If you need more
55
+ # bins, change index_dtype here and index_dtype and index_t in _assign.pyx.
56
+ index_dtype = np.uint16
57
+ UNKNOWN_INDEX = 65535
58
+
59
+ # All coordinates are currently 32-bit floats. If you need 64-bit, change
60
+ # coord_dtype here and coord_t in _assign.pyx.
61
+ coord_dtype = np.float32
62
+
63
+
64
+ log = logging.getLogger(__name__)
65
+
66
+
67
+ class BinMapper:
68
+ hashfunc = hashlib.sha256
69
+
70
+ def __init__(self):
71
+ self.labels = None
72
+ self.nbins = 0
73
+
74
+ def construct_bins(self, type_=Bin):
75
+ '''Construct and return an array of bins of type ``type``'''
76
+ return np.array([type_() for _i in range(self.nbins)], dtype=np.object_)
77
+
78
+ def pickle_and_hash(self):
79
+ '''Pickle this mapper and calculate a hash of the result (thus identifying the
80
+ contents of the pickled data), returning a tuple ``(pickled_data, hash)``.
81
+ This will raise PickleError if this mapper cannot be pickled, in which case
82
+ code that would otherwise rely on detecting a topology change must assume
83
+ a topology change happened, even if one did not.
84
+ '''
85
+
86
+ pkldat = pickle.dumps(self, pickle.HIGHEST_PROTOCOL)
87
+ hash = self.hashfunc(pkldat)
88
+ return (pkldat, hash.hexdigest())
89
+
90
+ def __repr__(self):
91
+ return '<{} at 0x{:x} with {:d} bins>'.format(self.__class__.__name__, id(self), self.nbins or 0)
92
+
93
+
94
+ class NopMapper(BinMapper):
95
+ '''Put everything into one bin.'''
96
+
97
+ def __init__(self):
98
+ super().__init__()
99
+ self.nbins = 1
100
+ self.labels = ['nop']
101
+
102
+ def assign(self, coords, mask=None, output=None):
103
+ if output is None:
104
+ output = np.zeros((len(coords),), dtype=index_dtype)
105
+
106
+ if mask is None:
107
+ mask = np.ones((len(coords),), dtype=np.bool_)
108
+ else:
109
+ mask = np.require(mask, dtype=np.bool_)
110
+
111
+ output[mask] = 0
112
+
113
+
114
+ class RectilinearBinMapper(BinMapper):
115
+ '''Bin into a rectangular grid based on tuples of float values'''
116
+
117
+ def __init__(self, boundaries):
118
+ super().__init__()
119
+ self._boundaries = None
120
+ self._boundlens = None
121
+ self.ndim = 0
122
+ self.nbins = 0
123
+
124
+ # the setter function below handles all of the required wrangling
125
+ self.boundaries = boundaries
126
+
127
+ @property
128
+ def boundaries(self):
129
+ return self._boundaries
130
+
131
+ @boundaries.setter
132
+ def boundaries(self, boundaries):
133
+ del self._boundaries, self.labels
134
+ self._boundaries = []
135
+ self.labels = labels = []
136
+ for boundset in boundaries:
137
+ boundarray = np.asarray(boundset, dtype=coord_dtype, order='C')
138
+ db = np.diff(boundarray)
139
+ if (db <= 0).any():
140
+ raise ValueError('boundary set must be strictly monotonically increasing')
141
+ self._boundaries.append(boundarray)
142
+ self._boundlens = np.array([len(boundset) for boundset in self._boundaries], dtype=index_dtype)
143
+ self.ndim = len(self._boundaries)
144
+ self.nbins = np.multiply.accumulate([1] + [len(bounds) - 1 for bounds in self._boundaries])[-1]
145
+
146
+ _boundaries = self._boundaries
147
+ binspace_shape = tuple(self._boundlens[:] - 1)
148
+ for index in np.ndindex(binspace_shape):
149
+ bounds = [(_boundaries[idim][index[idim]], boundaries[idim][index[idim] + 1]) for idim in range(len(_boundaries))]
150
+ labels.append(repr(bounds))
151
+
152
+ def assign(self, coords, mask=None, output=None):
153
+ try:
154
+ passed_coord_dtype = coords.dtype
155
+ except AttributeError:
156
+ coords = np.require(coords, dtype=coord_dtype)
157
+ else:
158
+ if passed_coord_dtype != coord_dtype:
159
+ coords = np.require(coords, dtype=coord_dtype)
160
+
161
+ if coords.ndim != 2:
162
+ raise TypeError('coords must be 2-dimensional')
163
+
164
+ if mask is None:
165
+ mask = np.ones((len(coords),), dtype=np.bool_)
166
+ elif len(mask) != len(coords):
167
+ raise TypeError('mask [shape {}] has different length than coords [shape {}]'.format(mask.shape, coords.shape))
168
+
169
+ if output is None:
170
+ output = np.empty((len(coords),), dtype=index_dtype)
171
+ elif len(output) != len(coords):
172
+ raise TypeError('output has different length than coords')
173
+
174
+ rectilinear_assign(coords, mask, output, self.boundaries, self._boundlens)
175
+
176
+ return output
177
+
178
+
179
+ class PiecewiseBinMapper(BinMapper):
180
+ '''Binning using a set of functions returing boolean values; if the Nth function
181
+ returns True for a coordinate tuple, then that coordinate is in the Nth bin.'''
182
+
183
+ def __init__(self, functions):
184
+ self.functions = functions
185
+ self.nbins = len(functions)
186
+ self.index_dtype = np.min_scalar_type(self.nbins)
187
+ self.labels = [repr(func) for func in functions]
188
+
189
+ def assign(self, coords, mask=None, output=None):
190
+ if output is None:
191
+ output = np.zeros((len(coords),), dtype=index_dtype)
192
+
193
+ if mask is None:
194
+ mask = np.ones((len(coords),), dtype=np.bool_)
195
+ else:
196
+ mask = np.require(mask, dtype=np.bool_)
197
+
198
+ coord_subset = coords[mask]
199
+ fnvals = np.empty((len(coord_subset), len(self.functions)), dtype=index_dtype)
200
+ for ifn, fn in enumerate(self.functions):
201
+ rsl = np.apply_along_axis(fn, 0, coord_subset)
202
+ if rsl.ndim > 1:
203
+ # this should work like a squeeze, unless the function returned something truly
204
+ # stupid (e.g., a 3d array with at least two dimensions greater than 1), in which
205
+ # case a broadcast error will occur
206
+ fnvals[:, ifn] = rsl.flat
207
+ else:
208
+ fnvals[:, ifn] = rsl
209
+ amask = np.require(fnvals.argmax(axis=1), dtype=index_dtype)
210
+ output[mask] = amask
211
+ return output
212
+
213
+
214
+ class FuncBinMapper(BinMapper):
215
+ '''Binning using a custom function which must iterate over input coordinate
216
+ sets itself.'''
217
+
218
+ def __init__(self, func, nbins, args=None, kwargs=None):
219
+ self.func = func
220
+ self.nbins = nbins
221
+ self.args = args or ()
222
+ self.kwargs = kwargs or {}
223
+ self.labels = ['{!r} bin {:d}'.format(func, ibin) for ibin in range(nbins)]
224
+
225
+ def assign(self, coords, mask=None, output=None):
226
+ try:
227
+ passed_coord_dtype = coords.dtype
228
+ except AttributeError:
229
+ coords = np.require(coords, dtype=coord_dtype)
230
+ else:
231
+ if passed_coord_dtype != coord_dtype:
232
+ coords = np.require(coords, dtype=coord_dtype)
233
+
234
+ if coords.ndim != 2:
235
+ raise TypeError('coords must be 2-dimensional')
236
+ if mask is None:
237
+ mask = np.ones((len(coords),), dtype=np.bool_)
238
+ elif len(mask) != len(coords):
239
+ raise TypeError('mask [shape {}] has different length than coords [shape {}]'.format(mask.shape, coords.shape))
240
+
241
+ if output is None:
242
+ output = np.empty((len(coords),), dtype=index_dtype)
243
+ elif len(output) != len(coords):
244
+ raise TypeError('output has different length than coords')
245
+
246
+ self.func(coords, mask, output, *self.args, **self.kwargs)
247
+
248
+ return output
249
+
250
+
251
+ class VectorizingFuncBinMapper(BinMapper):
252
+ '''Binning using a custom function which is evaluated once for each (unmasked)
253
+ coordinate tuple provided.'''
254
+
255
+ def __init__(self, func, nbins, args=None, kwargs=None):
256
+ self.func = func
257
+ self.args = args or ()
258
+ self.kwargs = kwargs or {}
259
+ self.nbins = nbins
260
+ self.index_dtype = np.min_scalar_type(self.nbins)
261
+ self.labels = ['{!r} bin {:d}'.format(func, ibin) for ibin in range(nbins)]
262
+
263
+ def assign(self, coords, mask=None, output=None):
264
+ try:
265
+ passed_coord_dtype = coords.dtype
266
+ except AttributeError:
267
+ coords = np.require(coords, dtype=coord_dtype)
268
+ else:
269
+ if passed_coord_dtype != coord_dtype:
270
+ coords = np.require(coords, dtype=coord_dtype)
271
+
272
+ if coords.ndim != 2:
273
+ raise TypeError('coords must be 2-dimensional')
274
+ if mask is None:
275
+ mask = np.ones((len(coords),), dtype=np.bool_)
276
+ elif len(mask) != len(coords):
277
+ raise TypeError('mask [shape {}] has different length than coords [shape {}]'.format(mask.shape, coords.shape))
278
+
279
+ if output is None:
280
+ output = np.empty((len(coords),), dtype=index_dtype)
281
+ elif len(output) != len(coords):
282
+ raise TypeError('output has different length than coords')
283
+
284
+ apply_down(self.func, self.args, self.kwargs, coords, mask, output)
285
+
286
+ return output
287
+
288
+
289
+ class VoronoiBinMapper(BinMapper):
290
+ '''A one-dimensional mapper which assigns a multidimensional pcoord to the
291
+ closest center based on a distance metric. Both the list of centers and the
292
+ distance function must be supplied.'''
293
+
294
+ def __init__(self, dfunc, centers, dfargs=None, dfkwargs=None):
295
+ self.dfunc = dfunc
296
+ self.dfargs = dfargs or ()
297
+ self.dfkwargs = dfkwargs or {}
298
+ self.centers = np.asarray(centers)
299
+ self.nbins = self.centers.shape[0]
300
+ self.ndim = self.centers.shape[1]
301
+ self.labels = ['center={!r}'.format(center) for center in self.centers]
302
+
303
+ # Sanity check: does the distance map the centers to themselves?
304
+ check = self.assign(self.centers)
305
+ if (check != np.arange(len(self.centers))).any():
306
+ raise TypeError('dfunc does not map centers to themselves')
307
+
308
+ def assign(self, coords, mask=None, output=None):
309
+ try:
310
+ passed_coord_dtype = coords.dtype
311
+ except AttributeError:
312
+ coords = np.require(coords, dtype=coord_dtype)
313
+ else:
314
+ if passed_coord_dtype != coord_dtype:
315
+ coords = np.require(coords, dtype=coord_dtype)
316
+
317
+ if coords.ndim != 2:
318
+ raise TypeError('coords must be 2-dimensional')
319
+ if mask is None:
320
+ mask = np.ones((len(coords),), dtype=np.bool_)
321
+ elif len(mask) != len(coords):
322
+ raise TypeError('mask [shape {}] has different length than coords [shape {}]'.format(mask.shape, coords.shape))
323
+
324
+ if output is None:
325
+ output = np.empty((len(coords),), dtype=index_dtype)
326
+ elif len(output) != len(coords):
327
+ raise TypeError('output has different length than coords')
328
+
329
+ apply_down_argmin_across(self.dfunc, (self.centers,) + self.dfargs, self.dfkwargs, self.nbins, coords, mask, output)
330
+
331
+ return output
332
+
333
+
334
+ class RecursiveBinMapper(BinMapper):
335
+ '''Nest mappers one within another.'''
336
+
337
+ def __init__(self, base_mapper, start_index=0):
338
+ self.base_mapper = base_mapper
339
+ self.nbins = base_mapper.nbins
340
+
341
+ # Targets for recursion
342
+ self._recursion_targets = {}
343
+
344
+ # Which bins must we recurse into?
345
+ self._recursion_map = np.zeros((self.base_mapper.nbins,), dtype=np.bool_)
346
+
347
+ self.start_index = start_index
348
+
349
+ @property
350
+ def labels(self):
351
+ for ilabel in range(self.base_mapper.nbins):
352
+ if self._recursion_map[ilabel]:
353
+ for label in self._recursion_targets[ilabel].labels:
354
+ yield label
355
+ else:
356
+ yield self.base_mapper.labels[ilabel]
357
+
358
+ @property
359
+ def start_index(self):
360
+ return self._start_index
361
+
362
+ @start_index.setter
363
+ def start_index(self, new_index):
364
+ self._start_index = new_index
365
+ not_recursed = ~self._recursion_map
366
+ n_not_recursed = not_recursed.sum()
367
+ if n_not_recursed == self.nbins:
368
+ self._output_map = np.arange(self._start_index, self._start_index + self.nbins, dtype=index_dtype)
369
+ elif n_not_recursed > 0:
370
+ # This looks like uninitialized access, but self._output_map is always set during __init__
371
+ # (by self.start_index = 0, or whatever value was passed in), so this modifies the existing
372
+ # set chosen above
373
+ self._output_map[not_recursed] = np.arange(self._start_index, self._start_index + n_not_recursed, dtype=index_dtype)
374
+ else:
375
+ # No un-replaced bins
376
+ self._output_map = None
377
+
378
+ n_own_bins = self.base_mapper.nbins - self._recursion_map.sum()
379
+ startindex = self.start_index + n_own_bins
380
+ for mapper in self._recursion_targets.values():
381
+ mapper.start_index = startindex
382
+ startindex += mapper.nbins
383
+
384
+ def add_mapper(self, mapper, replaces_bin_at):
385
+ '''Replace the bin containing the coordinate tuple ``replaces_bin_at`` with the
386
+ specified ``mapper``.'''
387
+
388
+ replaces_bin_at = np.require(replaces_bin_at, dtype=coord_dtype)
389
+ if replaces_bin_at.ndim < 1:
390
+ replaces_bin_at.shape = (1, 1)
391
+ elif replaces_bin_at.ndim < 2:
392
+ replaces_bin_at.shape = (1, replaces_bin_at.shape[0])
393
+ elif replaces_bin_at.ndim > 2 or replaces_bin_at.shape[1] > 1:
394
+ raise TypeError('a single coordinate vector is required')
395
+
396
+ self.nbins += mapper.nbins - 1
397
+
398
+ ibin = self.base_mapper.assign(replaces_bin_at)[0]
399
+ log.debug('replacing bin {!r} containing {!r} with {!r}'.format(ibin, replaces_bin_at, mapper))
400
+ if self._recursion_map[ibin]:
401
+ # recursively add; this doesn't change anything for us except our
402
+ # total bin count, which has been accounted for above
403
+ self._recursion_targets[ibin].add_mapper(mapper, replaces_bin_at[0])
404
+ else:
405
+ # replace a bin on our mapper
406
+ self._recursion_map[ibin] = True
407
+ mapper = RecursiveBinMapper(mapper)
408
+ self._recursion_targets[ibin] = mapper
409
+
410
+ # we have updated our list of recursed bins, so set our own start index to trigger a recursive
411
+ # reassignment of mappers' output values
412
+ # Note that we're reordering the recursion targets based on outer bin numbers (dict keys) first,
413
+ # so the order the mappers were added no longer matters...
414
+ self._recursion_targets = {k: self._recursion_targets[k] for k in sorted(self._recursion_targets)}
415
+ self.start_index = self.start_index
416
+
417
+ def assign(self, coords, mask=None, output=None):
418
+ if mask is None:
419
+ mask = np.ones((len(coords),), dtype=np.bool_)
420
+
421
+ if output is None:
422
+ output = np.empty((len(coords),), dtype=index_dtype)
423
+
424
+ # mapping mask -- which output values come from our base
425
+ # region set and therefore must be remapped
426
+ mmask = np.zeros((len(coords),), dtype=np.bool_)
427
+
428
+ # Assign based on this mapper
429
+ self.base_mapper.assign(coords, mask, output)
430
+
431
+ # Which coordinates do we need to reassign, because they landed in
432
+ # bins with embedded mappers?
433
+ rmasks = {}
434
+ for rindex, mapper in self._recursion_targets.items():
435
+ omask = output == rindex
436
+ mmask |= omask
437
+ rmasks[rindex] = omask
438
+
439
+ # remap output from our (base) mapper
440
+ # omap may be None if every bin has a recursive mapper in it
441
+ omap = self._output_map
442
+ if omap is not None:
443
+ output_map(output, omap, mask & ~mmask)
444
+
445
+ # do any recursive assignments necessary
446
+ for rindex, mapper in self._recursion_targets.items():
447
+ mapper.assign(coords, mask & rmasks[rindex], output)
448
+
449
+ return output
@@ -0,0 +1,96 @@
1
+ import numpy as np
2
+ from westpa.core.binning import FuncBinMapper
3
+ from westpa.core.extloader import get_object
4
+ import logging
5
+
6
+ log = logging.getLogger(__name__)
7
+
8
+
9
+ def map_binless(coords, mask, output, *args, **kwargs):
10
+ '''Adaptively groups walkers according to a user-defined grouping function
11
+ that is defined externally. Very general implementation but limited to
12
+ only a two dimensional progress coordinate (for now).'''
13
+
14
+ n_groups = kwargs.get("n_groups")
15
+ n_dims = kwargs.get("n_dims")
16
+ group_function = get_object(kwargs.get("group_function"))
17
+ log.debug(f'binless arguments: {kwargs}')
18
+ try:
19
+ group_function_kwargs = kwargs.get('group_function_kwargs')['group_arguments']
20
+ except KeyError:
21
+ group_function_kwargs = {}
22
+ ndim = n_dims
23
+
24
+ if not np.any(mask):
25
+ return output
26
+
27
+ allcoords = np.copy(coords)
28
+ allmask = np.copy(mask)
29
+
30
+ isfinal = None
31
+ splitting = False
32
+
33
+ # the segments should be sent in by the driver as half initial segments and half final segments
34
+ # allcoords contains all segments
35
+ # coords should contain ONLY final segments
36
+ if coords.shape[1] > ndim:
37
+ if coords.shape[1] > ndim + 1:
38
+ isfinal = allcoords[:, ndim + 1].astype(np.bool_)
39
+ else:
40
+ isfinal = np.ones(coords.shape[0], dtype=np.bool_)
41
+ coords = coords[isfinal, :ndim]
42
+ mask = mask[isfinal]
43
+ splitting = True
44
+
45
+ # in case where there is no final segments but initial ones in range
46
+ if not np.any(mask):
47
+ coords = allcoords[:, :ndim]
48
+ mask = allmask
49
+ splitting = False
50
+
51
+ # filter the list of coordinates (which contains coordinates outside of the binless region)
52
+ # to obtain only the ones we want to cluster
53
+ # this is done with all dimensions at once
54
+ binless_coords = coords[mask]
55
+ nsegs_binless = len(binless_coords)
56
+
57
+ # we need to make sure that the number of segments in the binless region is greater than
58
+ # the number of clusters we request
59
+ # if only one segment in the binless region, assign it to a single cluster
60
+ if nsegs_binless == 1:
61
+ clusters = [0]
62
+
63
+ # if there are more than one segment in the binless region but still the total is less than
64
+ # our target number, adjust our target number to be the number of segments in the binless
65
+ # region minus one
66
+ elif nsegs_binless < n_groups:
67
+ clusters = group_function(binless_coords, nsegs_binless, splitting, **group_function_kwargs)
68
+ # if there are enough segments in the binless region, proceed as planned
69
+ elif nsegs_binless >= n_groups:
70
+ clusters = group_function(binless_coords, n_groups, splitting, **group_function_kwargs)
71
+
72
+ # this is a good place to say this... output is a list which matches the length of allcoords
73
+ # allcoords is a collection of all initial and final segment coords for that iteration
74
+ # we first filtered those to only contain the final data points, since those are the ones we care
75
+ # about clustering
76
+ # we then filtered to only have the coords in the binless region, since, again, those are what we care about
77
+ # we then assigned each to a cluster which is essentially a slitting index
78
+ # all that's left is to find where each binless segment is in the output and insert the cluster index there
79
+ for idx, val in enumerate(binless_coords):
80
+ if ndim > 1:
81
+ mask2 = np.logical_and(allcoords[:, 0] == val[0], allcoords[:, 1] == val[1])
82
+ else:
83
+ mask2 = allcoords[:, 0] == val[0]
84
+ output[mask2] = clusters[idx]
85
+
86
+ return output
87
+
88
+
89
+ class BinlessMapper(FuncBinMapper):
90
+ '''Adaptively group walkers according to a user-defined grouping
91
+ function that is defined externally.'''
92
+
93
+ def __init__(self, ngroups, ndims, group_function, **group_function_kwargs):
94
+ kwargs = dict(n_groups=ngroups, n_dims=ndims, group_function=group_function, group_function_kwargs=group_function_kwargs)
95
+ n_total_groups = ngroups
96
+ super().__init__(map_binless, n_total_groups, kwargs=kwargs)
@@ -0,0 +1,54 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+ from westpa.core.we_driver import WEDriver
5
+
6
+ log = logging.getLogger(__name__)
7
+
8
+
9
+ class BinlessDriver(WEDriver):
10
+ def assign(self, segments, initializing=False):
11
+ '''Assign segments to initial and final bins, and update the (internal) lists of used and available
12
+ initial states. This function is adapted to the MAB scheme, so that the inital and final segments are
13
+ sent to the bin mapper at the same time, otherwise the inital and final bin boundaries can be inconsistent.'''
14
+
15
+ log.debug("BinlessDriver in use.")
16
+ # collect initial and final coordinates into one place
17
+ n_segments = len(segments)
18
+ all_pcoords = np.empty((n_segments * 2, self.system.pcoord_ndim + 2), dtype=self.system.pcoord_dtype)
19
+
20
+ for iseg, segment in enumerate(segments):
21
+ all_pcoords[iseg] = np.append(segment.pcoord[0, :], [segment.weight, 0.0])
22
+ all_pcoords[n_segments + iseg] = np.append(segment.pcoord[-1, :], [segment.weight, 1.0])
23
+
24
+ # assign based on initial and final progress coordinates
25
+ assignments = self.bin_mapper.assign(all_pcoords)
26
+ initial_assignments = assignments[:n_segments]
27
+ if initializing:
28
+ final_assignments = initial_assignments
29
+ else:
30
+ final_assignments = assignments[n_segments:]
31
+
32
+ initial_binning = self.initial_binning
33
+ final_binning = self.final_binning
34
+ flux_matrix = self.flux_matrix
35
+ transition_matrix = self.transition_matrix
36
+ for segment, iidx, fidx in zip(segments, initial_assignments, final_assignments):
37
+ initial_binning[iidx].add(segment)
38
+ final_binning[fidx].add(segment)
39
+ flux_matrix[iidx, fidx] += segment.weight
40
+ transition_matrix[iidx, fidx] += 1
41
+
42
+ n_recycled_total = self.n_recycled_segs
43
+ n_new_states = n_recycled_total - len(self.avail_initial_states)
44
+
45
+ log.debug(
46
+ '{} walkers scheduled for recycling, {} initial states available'.format(
47
+ n_recycled_total, len(self.avail_initial_states)
48
+ )
49
+ )
50
+
51
+ if n_new_states > 0:
52
+ return n_new_states
53
+ else:
54
+ return 0