westpa 2022.12__cp313-cp313-macosx_10_13_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 (149) 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 +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 +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 +376 -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 +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_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-313-darwin.so +0 -0
  42. westpa/core/binning/assign.py +455 -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 +506 -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-313-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 +719 -0
  61. westpa/core/reweight/__init__.py +14 -0
  62. westpa/core/reweight/_reweight.cpython-313-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 +835 -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 +910 -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/_fasthist.cpython-313-darwin.so +0 -0
  75. westpa/mclib/__init__.py +271 -0
  76. westpa/mclib/__main__.py +28 -0
  77. westpa/mclib/_mclib.cpython-313-darwin.so +0 -0
  78. westpa/oldtools/__init__.py +4 -0
  79. westpa/oldtools/aframe/__init__.py +35 -0
  80. westpa/oldtools/aframe/atool.py +75 -0
  81. westpa/oldtools/aframe/base_mixin.py +26 -0
  82. westpa/oldtools/aframe/binning.py +178 -0
  83. westpa/oldtools/aframe/data_reader.py +560 -0
  84. westpa/oldtools/aframe/iter_range.py +200 -0
  85. westpa/oldtools/aframe/kinetics.py +117 -0
  86. westpa/oldtools/aframe/mcbs.py +153 -0
  87. westpa/oldtools/aframe/output.py +39 -0
  88. westpa/oldtools/aframe/plotting.py +90 -0
  89. westpa/oldtools/aframe/trajwalker.py +126 -0
  90. westpa/oldtools/aframe/transitions.py +469 -0
  91. westpa/oldtools/cmds/__init__.py +0 -0
  92. westpa/oldtools/cmds/w_ttimes.py +361 -0
  93. westpa/oldtools/files.py +34 -0
  94. westpa/oldtools/miscfn.py +23 -0
  95. westpa/oldtools/stats/__init__.py +4 -0
  96. westpa/oldtools/stats/accumulator.py +35 -0
  97. westpa/oldtools/stats/edfs.py +129 -0
  98. westpa/oldtools/stats/mcbs.py +96 -0
  99. westpa/tools/__init__.py +33 -0
  100. westpa/tools/binning.py +472 -0
  101. westpa/tools/core.py +340 -0
  102. westpa/tools/data_reader.py +159 -0
  103. westpa/tools/dtypes.py +31 -0
  104. westpa/tools/iter_range.py +198 -0
  105. westpa/tools/kinetics_tool.py +340 -0
  106. westpa/tools/plot.py +283 -0
  107. westpa/tools/progress.py +17 -0
  108. westpa/tools/selected_segs.py +154 -0
  109. westpa/tools/wipi.py +751 -0
  110. westpa/trajtree/__init__.py +4 -0
  111. westpa/trajtree/_trajtree.cpython-313-darwin.so +0 -0
  112. westpa/trajtree/trajtree.py +117 -0
  113. westpa/westext/__init__.py +0 -0
  114. westpa/westext/adaptvoronoi/__init__.py +3 -0
  115. westpa/westext/adaptvoronoi/adaptVor_driver.py +214 -0
  116. westpa/westext/hamsm_restarting/__init__.py +3 -0
  117. westpa/westext/hamsm_restarting/example_overrides.py +35 -0
  118. westpa/westext/hamsm_restarting/restart_driver.py +1165 -0
  119. westpa/westext/stringmethod/__init__.py +11 -0
  120. westpa/westext/stringmethod/fourier_fitting.py +69 -0
  121. westpa/westext/stringmethod/string_driver.py +253 -0
  122. westpa/westext/stringmethod/string_method.py +306 -0
  123. westpa/westext/weed/BinCluster.py +180 -0
  124. westpa/westext/weed/ProbAdjustEquil.py +100 -0
  125. westpa/westext/weed/UncertMath.py +247 -0
  126. westpa/westext/weed/__init__.py +10 -0
  127. westpa/westext/weed/weed_driver.py +192 -0
  128. westpa/westext/wess/ProbAdjust.py +101 -0
  129. westpa/westext/wess/__init__.py +6 -0
  130. westpa/westext/wess/wess_driver.py +217 -0
  131. westpa/work_managers/__init__.py +57 -0
  132. westpa/work_managers/core.py +396 -0
  133. westpa/work_managers/environment.py +134 -0
  134. westpa/work_managers/mpi.py +318 -0
  135. westpa/work_managers/processes.py +187 -0
  136. westpa/work_managers/serial.py +28 -0
  137. westpa/work_managers/threads.py +79 -0
  138. westpa/work_managers/zeromq/__init__.py +20 -0
  139. westpa/work_managers/zeromq/core.py +641 -0
  140. westpa/work_managers/zeromq/node.py +131 -0
  141. westpa/work_managers/zeromq/work_manager.py +526 -0
  142. westpa/work_managers/zeromq/worker.py +320 -0
  143. westpa-2022.12.dist-info/AUTHORS +22 -0
  144. westpa-2022.12.dist-info/LICENSE +21 -0
  145. westpa-2022.12.dist-info/METADATA +193 -0
  146. westpa-2022.12.dist-info/RECORD +149 -0
  147. westpa-2022.12.dist-info/WHEEL +6 -0
  148. westpa-2022.12.dist-info/entry_points.txt +29 -0
  149. westpa-2022.12.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,455 @@
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 = []
136
+ for boundset in boundaries:
137
+ boundarray = np.ascontiguousarray(boundset, dtype=coord_dtype)
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
+ label = (
150
+ '['
151
+ + ', '.join(
152
+ f'({boundarray[index[idim]]!s}, {boundarray[index[idim] + 1]!s})' for idim, boundarray in enumerate(_boundaries)
153
+ )
154
+ + ']'
155
+ )
156
+ self.labels.append(label)
157
+
158
+ def assign(self, coords, mask=None, output=None):
159
+ try:
160
+ passed_coord_dtype = coords.dtype
161
+ except AttributeError:
162
+ coords = np.require(coords, dtype=coord_dtype)
163
+ else:
164
+ if passed_coord_dtype != coord_dtype:
165
+ coords = np.require(coords, dtype=coord_dtype)
166
+
167
+ if coords.ndim != 2:
168
+ raise TypeError('coords must be 2-dimensional')
169
+
170
+ if mask is None:
171
+ mask = np.ones((len(coords),), dtype=np.bool_)
172
+ elif len(mask) != len(coords):
173
+ raise TypeError('mask [shape {}] has different length than coords [shape {}]'.format(mask.shape, coords.shape))
174
+
175
+ if output is None:
176
+ output = np.empty((len(coords),), dtype=index_dtype)
177
+ elif len(output) != len(coords):
178
+ raise TypeError('output has different length than coords')
179
+
180
+ rectilinear_assign(coords, mask, output, self.boundaries, self._boundlens)
181
+
182
+ return output
183
+
184
+
185
+ class PiecewiseBinMapper(BinMapper):
186
+ '''Binning using a set of functions returing boolean values; if the Nth function
187
+ returns True for a coordinate tuple, then that coordinate is in the Nth bin.'''
188
+
189
+ def __init__(self, functions):
190
+ self.functions = functions
191
+ self.nbins = len(functions)
192
+ self.index_dtype = np.min_scalar_type(self.nbins)
193
+ self.labels = [str(func) for func in functions]
194
+
195
+ def assign(self, coords, mask=None, output=None):
196
+ if output is None:
197
+ output = np.zeros((len(coords),), dtype=index_dtype)
198
+
199
+ if mask is None:
200
+ mask = np.ones((len(coords),), dtype=np.bool_)
201
+ else:
202
+ mask = np.require(mask, dtype=np.bool_)
203
+
204
+ coord_subset = coords[mask]
205
+ fnvals = np.empty((len(coord_subset), len(self.functions)), dtype=index_dtype)
206
+ for ifn, fn in enumerate(self.functions):
207
+ rsl = np.apply_along_axis(fn, 0, coord_subset)
208
+ if rsl.ndim > 1:
209
+ # this should work like a squeeze, unless the function returned something truly
210
+ # stupid (e.g., a 3d array with at least two dimensions greater than 1), in which
211
+ # case a broadcast error will occur
212
+ fnvals[:, ifn] = rsl.flat
213
+ else:
214
+ fnvals[:, ifn] = rsl
215
+ amask = np.require(fnvals.argmax(axis=1), dtype=index_dtype)
216
+ output[mask] = amask
217
+ return output
218
+
219
+
220
+ class FuncBinMapper(BinMapper):
221
+ '''Binning using a custom function which must iterate over input coordinate
222
+ sets itself.'''
223
+
224
+ def __init__(self, func, nbins, args=None, kwargs=None):
225
+ self.func = func
226
+ self.nbins = nbins
227
+ self.args = args or ()
228
+ self.kwargs = kwargs or {}
229
+ self.labels = ['{!r} bin {:d}'.format(func, ibin) for ibin in range(nbins)]
230
+
231
+ def assign(self, coords, mask=None, output=None):
232
+ try:
233
+ passed_coord_dtype = coords.dtype
234
+ except AttributeError:
235
+ coords = np.require(coords, dtype=coord_dtype)
236
+ else:
237
+ if passed_coord_dtype != coord_dtype:
238
+ coords = np.require(coords, dtype=coord_dtype)
239
+
240
+ if coords.ndim != 2:
241
+ raise TypeError('coords must be 2-dimensional')
242
+ if mask is None:
243
+ mask = np.ones((len(coords),), dtype=np.bool_)
244
+ elif len(mask) != len(coords):
245
+ raise TypeError('mask [shape {}] has different length than coords [shape {}]'.format(mask.shape, coords.shape))
246
+
247
+ if output is None:
248
+ output = np.empty((len(coords),), dtype=index_dtype)
249
+ elif len(output) != len(coords):
250
+ raise TypeError('output has different length than coords')
251
+
252
+ self.func(coords, mask, output, *self.args, **self.kwargs)
253
+
254
+ return output
255
+
256
+
257
+ class VectorizingFuncBinMapper(BinMapper):
258
+ '''Binning using a custom function which is evaluated once for each (unmasked)
259
+ coordinate tuple provided.'''
260
+
261
+ def __init__(self, func, nbins, args=None, kwargs=None):
262
+ self.func = func
263
+ self.args = args or ()
264
+ self.kwargs = kwargs or {}
265
+ self.nbins = nbins
266
+ self.index_dtype = np.min_scalar_type(self.nbins)
267
+ self.labels = ['{!r} bin {:d}'.format(func, ibin) for ibin in range(nbins)]
268
+
269
+ def assign(self, coords, mask=None, output=None):
270
+ try:
271
+ passed_coord_dtype = coords.dtype
272
+ except AttributeError:
273
+ coords = np.require(coords, dtype=coord_dtype)
274
+ else:
275
+ if passed_coord_dtype != coord_dtype:
276
+ coords = np.require(coords, dtype=coord_dtype)
277
+
278
+ if coords.ndim != 2:
279
+ raise TypeError('coords must be 2-dimensional')
280
+ if mask is None:
281
+ mask = np.ones((len(coords),), dtype=np.bool_)
282
+ elif len(mask) != len(coords):
283
+ raise TypeError('mask [shape {}] has different length than coords [shape {}]'.format(mask.shape, coords.shape))
284
+
285
+ if output is None:
286
+ output = np.empty((len(coords),), dtype=index_dtype)
287
+ elif len(output) != len(coords):
288
+ raise TypeError('output has different length than coords')
289
+
290
+ apply_down(self.func, self.args, self.kwargs, coords, mask, output)
291
+
292
+ return output
293
+
294
+
295
+ class VoronoiBinMapper(BinMapper):
296
+ '''A one-dimensional mapper which assigns a multidimensional pcoord to the
297
+ closest center based on a distance metric. Both the list of centers and the
298
+ distance function must be supplied.'''
299
+
300
+ def __init__(self, dfunc, centers, dfargs=None, dfkwargs=None):
301
+ self.dfunc = dfunc
302
+ self.dfargs = dfargs or ()
303
+ self.dfkwargs = dfkwargs or {}
304
+ self.centers = np.asarray(centers)
305
+ self.nbins = self.centers.shape[0]
306
+ self.ndim = self.centers.shape[1]
307
+ self.labels = ['center={!r}'.format(center) for center in self.centers]
308
+
309
+ # Sanity check: does the distance map the centers to themselves?
310
+ check = self.assign(self.centers)
311
+ if (check != np.arange(len(self.centers))).any():
312
+ raise TypeError('dfunc does not map centers to themselves')
313
+
314
+ def assign(self, coords, mask=None, output=None):
315
+ try:
316
+ passed_coord_dtype = coords.dtype
317
+ except AttributeError:
318
+ coords = np.require(coords, dtype=coord_dtype)
319
+ else:
320
+ if passed_coord_dtype != coord_dtype:
321
+ coords = np.require(coords, dtype=coord_dtype)
322
+
323
+ if coords.ndim != 2:
324
+ raise TypeError('coords must be 2-dimensional')
325
+ if mask is None:
326
+ mask = np.ones((len(coords),), dtype=np.bool_)
327
+ elif len(mask) != len(coords):
328
+ raise TypeError('mask [shape {}] has different length than coords [shape {}]'.format(mask.shape, coords.shape))
329
+
330
+ if output is None:
331
+ output = np.empty((len(coords),), dtype=index_dtype)
332
+ elif len(output) != len(coords):
333
+ raise TypeError('output has different length than coords')
334
+
335
+ apply_down_argmin_across(self.dfunc, (self.centers,) + self.dfargs, self.dfkwargs, self.nbins, coords, mask, output)
336
+
337
+ return output
338
+
339
+
340
+ class RecursiveBinMapper(BinMapper):
341
+ '''Nest mappers one within another.'''
342
+
343
+ def __init__(self, base_mapper, start_index=0):
344
+ self.base_mapper = base_mapper
345
+ self.nbins = base_mapper.nbins
346
+
347
+ # Targets for recursion
348
+ self._recursion_targets = {}
349
+
350
+ # Which bins must we recurse into?
351
+ self._recursion_map = np.zeros((self.base_mapper.nbins,), dtype=np.bool_)
352
+
353
+ self.start_index = start_index
354
+
355
+ @property
356
+ def labels(self):
357
+ for ilabel in range(self.base_mapper.nbins):
358
+ if self._recursion_map[ilabel]:
359
+ for label in self._recursion_targets[ilabel].labels:
360
+ yield label
361
+ else:
362
+ yield self.base_mapper.labels[ilabel]
363
+
364
+ @property
365
+ def start_index(self):
366
+ return self._start_index
367
+
368
+ @start_index.setter
369
+ def start_index(self, new_index):
370
+ self._start_index = new_index
371
+ not_recursed = ~self._recursion_map
372
+ n_not_recursed = not_recursed.sum()
373
+ if n_not_recursed == self.nbins:
374
+ self._output_map = np.arange(self._start_index, self._start_index + self.nbins, dtype=index_dtype)
375
+ elif n_not_recursed > 0:
376
+ # This looks like uninitialized access, but self._output_map is always set during __init__
377
+ # (by self.start_index = 0, or whatever value was passed in), so this modifies the existing
378
+ # set chosen above
379
+ self._output_map[not_recursed] = np.arange(self._start_index, self._start_index + n_not_recursed, dtype=index_dtype)
380
+ else:
381
+ # No un-replaced bins
382
+ self._output_map = None
383
+
384
+ n_own_bins = self.base_mapper.nbins - self._recursion_map.sum()
385
+ startindex = self.start_index + n_own_bins
386
+ for mapper in self._recursion_targets.values():
387
+ mapper.start_index = startindex
388
+ startindex += mapper.nbins
389
+
390
+ def add_mapper(self, mapper, replaces_bin_at):
391
+ '''Replace the bin containing the coordinate tuple ``replaces_bin_at`` with the
392
+ specified ``mapper``.'''
393
+
394
+ replaces_bin_at = np.require(replaces_bin_at, dtype=coord_dtype)
395
+ if replaces_bin_at.ndim < 1:
396
+ replaces_bin_at.shape = (1, 1)
397
+ elif replaces_bin_at.ndim < 2:
398
+ replaces_bin_at.shape = (1, replaces_bin_at.shape[0])
399
+ elif replaces_bin_at.ndim > 2 or replaces_bin_at.shape[1] > 1:
400
+ raise TypeError('a single coordinate vector is required')
401
+
402
+ self.nbins += mapper.nbins - 1
403
+
404
+ ibin = self.base_mapper.assign(replaces_bin_at)[0]
405
+ log.debug('replacing bin {!r} containing {!r} with {!r}'.format(ibin, replaces_bin_at, mapper))
406
+ if self._recursion_map[ibin]:
407
+ # recursively add; this doesn't change anything for us except our
408
+ # total bin count, which has been accounted for above
409
+ self._recursion_targets[ibin].add_mapper(mapper, replaces_bin_at[0])
410
+ else:
411
+ # replace a bin on our mapper
412
+ self._recursion_map[ibin] = True
413
+ mapper = RecursiveBinMapper(mapper)
414
+ self._recursion_targets[ibin] = mapper
415
+
416
+ # we have updated our list of recursed bins, so set our own start index to trigger a recursive
417
+ # reassignment of mappers' output values
418
+ # Note that we're reordering the recursion targets based on outer bin numbers (dict keys) first,
419
+ # so the order the mappers were added no longer matters...
420
+ self._recursion_targets = {k: self._recursion_targets[k] for k in sorted(self._recursion_targets)}
421
+ self.start_index = self.start_index
422
+
423
+ def assign(self, coords, mask=None, output=None):
424
+ if mask is None:
425
+ mask = np.ones((len(coords),), dtype=np.bool_)
426
+
427
+ if output is None:
428
+ output = np.empty((len(coords),), dtype=index_dtype)
429
+
430
+ # mapping mask -- which output values come from our base
431
+ # region set and therefore must be remapped
432
+ mmask = np.zeros((len(coords),), dtype=np.bool_)
433
+
434
+ # Assign based on this mapper
435
+ self.base_mapper.assign(coords, mask, output)
436
+
437
+ # Which coordinates do we need to reassign, because they landed in
438
+ # bins with embedded mappers?
439
+ rmasks = {}
440
+ for rindex, mapper in self._recursion_targets.items():
441
+ omask = output == rindex
442
+ mmask |= omask
443
+ rmasks[rindex] = omask
444
+
445
+ # remap output from our (base) mapper
446
+ # omap may be None if every bin has a recursive mapper in it
447
+ omap = self._output_map
448
+ if omap is not None:
449
+ output_map(output, omap, mask & ~mmask)
450
+
451
+ # do any recursive assignments necessary
452
+ for rindex, mapper in self._recursion_targets.items():
453
+ mapper.assign(coords, mask & rmasks[rindex], output)
454
+
455
+ 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