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,910 @@
1
+ import logging
2
+ import math
3
+ import operator
4
+
5
+ import numpy as np
6
+ from numpy.random import Generator, MT19937
7
+
8
+ import westpa
9
+ from .segment import Segment
10
+ from .states import InitialState
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ def _group_walkers_identity(we_driver, ibin, **kwargs):
16
+ log.debug('using we_driver._group_walkers_identity')
17
+ bin_set = we_driver.next_iter_binning[ibin]
18
+ list_bins = [set()]
19
+ for i in bin_set:
20
+ list_bins[0].add(i)
21
+ return list_bins
22
+
23
+
24
+ class ConsistencyError(RuntimeError):
25
+ pass
26
+
27
+
28
+ class AccuracyError(RuntimeError):
29
+ pass
30
+
31
+
32
+ class NewWeightEntry:
33
+ NW_SOURCE_RECYCLED = 0
34
+
35
+ def __init__(
36
+ self,
37
+ source_type,
38
+ weight,
39
+ prev_seg_id=None,
40
+ prev_init_pcoord=None,
41
+ prev_final_pcoord=None,
42
+ new_init_pcoord=None,
43
+ target_state_id=None,
44
+ initial_state_id=None,
45
+ ):
46
+ self.source_type = source_type
47
+ self.weight = weight
48
+ self.prev_seg_id = prev_seg_id
49
+ self.prev_init_pcoord = np.asarray(prev_init_pcoord) if prev_init_pcoord is not None else None
50
+ self.prev_final_pcoord = np.asarray(prev_final_pcoord) if prev_final_pcoord is not None else None
51
+ self.new_init_pcoord = np.asarray(new_init_pcoord) if new_init_pcoord is not None else None
52
+ self.target_state_id = target_state_id
53
+ self.initial_state_id = initial_state_id
54
+
55
+ def __repr__(self):
56
+ return '<{} object at 0x{:x}: weight={self.weight:g} target_state_id={self.target_state_id} prev_final_pcoord={self.prev_final_pcoord}>'.format(
57
+ self.__class__.__name__, id(self), self=self
58
+ )
59
+
60
+
61
+ class WEDriver:
62
+ '''A class implemented Huber & Kim's weighted ensemble algorithm over Segment objects.
63
+ This class handles all binning, recycling, and preparation of new Segment objects for the
64
+ next iteration. Binning is accomplished using system.bin_mapper, and per-bin target counts
65
+ are from system.bin_target_counts.
66
+
67
+ The workflow is as follows:
68
+
69
+ 1) Call `new_iteration()` every new iteration, providing any recycling targets that are
70
+ in force and any available initial states for recycling.
71
+ 2) Call `assign()` to assign segments to bins based on their initial and end points. This
72
+ returns the number of walkers that were recycled.
73
+ 3) Call `run_we()`, optionally providing a set of initial states that will be used to
74
+ recycle walkers.
75
+
76
+ Note the presence of flux_matrix, transition_matrix,
77
+ current_iter_segments, next_iter_segments, recycling_segments,
78
+ initial_binning, final_binning, next_iter_binning, and new_weights (to be documented soon).
79
+ '''
80
+
81
+ weight_split_threshold = 2.0
82
+ weight_merge_cutoff = 1.0
83
+ largest_allowed_weight = 1.0
84
+ smallest_allowed_weight = 1e-310
85
+
86
+ def __init__(self, rc=None, system=None):
87
+ self.rc = rc or westpa.rc
88
+ self.system = system or self.rc.get_system_driver()
89
+
90
+ # Whether to adjust counts to exactly match target count
91
+ self.do_adjust_counts = True
92
+
93
+ # bin mapper and per-bin target counts (see new_iteration for initialization)
94
+ self.bin_mapper = None
95
+ self.bin_target_counts = None
96
+
97
+ # Mapping of bin index to target state
98
+ self.target_states = None
99
+
100
+ # binning on initial points
101
+ self.initial_binning = None
102
+
103
+ # binning on final points (pre-WE)
104
+ self.final_binning = None
105
+
106
+ # binning on initial points for next iteration
107
+ self.next_iter_binning = None
108
+
109
+ # Flux and rate matrices for the current iteration
110
+ self.flux_matrix = None
111
+ self.transition_matrix = None
112
+
113
+ # Information on new weights (e.g. from recycling) for the next iteration
114
+ self.new_weights = None
115
+
116
+ # Set of initial states passed to run_we() that are actually used for
117
+ # recycling targets
118
+ self.used_initial_states = None
119
+
120
+ self.avail_initial_states = None
121
+
122
+ self.rng = Generator(MT19937())
123
+
124
+ # Make property for subgrouping function.
125
+ self.subgroup_function = _group_walkers_identity
126
+ self.subgroup_function_kwargs = {}
127
+
128
+ self.process_config()
129
+ self.check_threshold_configs()
130
+
131
+ def process_config(self):
132
+ config = self.rc.config
133
+
134
+ config.require_type_if_present(['west', 'we', 'adjust_counts'], bool)
135
+
136
+ config.require_type_if_present(['west', 'we', 'thresholds'], bool)
137
+
138
+ self.do_adjust_counts = config.get(['west', 'we', 'adjust_counts'], True)
139
+ log.info('Adjust counts to exactly match target_counts: {}'.format(self.do_adjust_counts))
140
+
141
+ self.do_thresholds = config.get(['west', 'we', 'thresholds'], True)
142
+ log.info('Obey abolute weight thresholds: {}'.format(self.do_thresholds))
143
+
144
+ self.weight_split_threshold = config.get(['west', 'we', 'weight_split_threshold'], self.weight_split_threshold)
145
+ log.info('Split threshold: {}'.format(self.weight_split_threshold))
146
+
147
+ self.weight_merge_cutoff = config.get(['west', 'we', 'weight_merge_cutoff'], self.weight_merge_cutoff)
148
+ log.info('Merge cutoff: {}'.format(self.weight_merge_cutoff))
149
+
150
+ self.largest_allowed_weight = config.get(['west', 'we', 'largest_allowed_weight'], self.largest_allowed_weight)
151
+ log.info('Largest allowed weight: {}'.format(self.largest_allowed_weight))
152
+
153
+ self.smallest_allowed_weight = config.get(['west', 'we', 'smallest_allowed_weight'], self.smallest_allowed_weight)
154
+ log.info('Smallest allowed_weight: {}'.format(self.smallest_allowed_weight))
155
+
156
+ @property
157
+ def next_iter_segments(self):
158
+ '''Newly-created segments for the next iteration'''
159
+ if self.next_iter_binning is None:
160
+ raise RuntimeError('cannot access next iteration segments before running WE')
161
+
162
+ for _bin in self.next_iter_binning:
163
+ for walker in _bin:
164
+ yield walker
165
+
166
+ @property
167
+ def current_iter_segments(self):
168
+ '''Segments for the current iteration'''
169
+ for _bin in self.final_binning:
170
+ for walker in _bin:
171
+ yield walker
172
+
173
+ @property
174
+ def next_iter_assignments(self):
175
+ '''Bin assignments (indices) for initial points of next iteration.'''
176
+ if self.next_iter_binning is None:
177
+ raise RuntimeError('cannot access next iteration segments before running WE')
178
+
179
+ for ibin, _bin in enumerate(self.next_iter_binning):
180
+ for _walker in _bin:
181
+ yield ibin
182
+
183
+ @property
184
+ def current_iter_assignments(self):
185
+ '''Bin assignments (indices) for endpoints of current iteration.'''
186
+ for ibin, _bin in enumerate(self.final_binning):
187
+ for walker in _bin:
188
+ yield ibin
189
+
190
+ @property
191
+ def recycling_segments(self):
192
+ '''Segments designated for recycling'''
193
+ if len(self.target_states):
194
+ for ibin, tstate in self.target_states.items():
195
+ for segment in self.final_binning[ibin]:
196
+ yield segment
197
+ else:
198
+ return
199
+
200
+ @property
201
+ def n_recycled_segs(self):
202
+ '''Number of segments recycled this iteration'''
203
+ count = 0
204
+ for _segment in self.recycling_segments:
205
+ count += 1
206
+ return count
207
+
208
+ @property
209
+ def n_istates_needed(self):
210
+ '''Number of initial states needed to support recycling for this iteration'''
211
+ n_istates_avail = len(self.avail_initial_states)
212
+ return max(0, self.n_recycled_segs - n_istates_avail)
213
+
214
+ def check_threshold_configs(self):
215
+ '''Check to see if weight thresholds parameters are valid'''
216
+ if (not np.issubdtype(type(self.largest_allowed_weight), np.floating)) or (
217
+ not np.issubdtype(type(self.smallest_allowed_weight), np.floating)
218
+ ):
219
+ try:
220
+ # Trying to self correct
221
+ self.largest_allowed_weight = float(self.largest_allowed_weight)
222
+ self.smallest_allowed_weight = float(self.smallest_allowed_weight)
223
+ except ValueError:
224
+ # Generate error saying thresholds are invalid
225
+ raise ValueError("Invalid weight thresholds specified. Please check your west.cfg.")
226
+
227
+ if np.isclose(self.largest_allowed_weight, self.smallest_allowed_weight):
228
+ raise ValueError("Weight threshold bounds cannot be identical.")
229
+ elif self.largest_allowed_weight < self.smallest_allowed_weight:
230
+ self.smallest_allowed_weight, self.largest_allowed_weight = self.largest_allowed_weight, self.smallest_allowed_weight
231
+ log.warning('Swapped largest allowed weight with smallest allowed weight to fulfill inequality (largest > smallest).')
232
+
233
+ def clear(self):
234
+ '''Explicitly delete all Segment-related state.'''
235
+
236
+ del self.initial_binning, self.final_binning, self.next_iter_binning
237
+ del self.flux_matrix, self.transition_matrix
238
+ del self.new_weights, self.used_initial_states, self.avail_initial_states
239
+
240
+ self.initial_binning = None
241
+ self.final_binning = None
242
+ self.next_iter_binning = None
243
+ self.flux_matrix = None
244
+ self.transition_matrix = None
245
+ self.avail_initial_states = None
246
+ self.used_initial_states = None
247
+ self.new_weights = None
248
+
249
+ def new_iteration(self, initial_states=None, target_states=None, new_weights=None, bin_mapper=None, bin_target_counts=None):
250
+ '''Prepare for a new iteration. ``initial_states`` is a sequence of all InitialState objects valid
251
+ for use in to generating new segments for the *next* iteration (after the one being begun with the call to
252
+ new_iteration); that is, these are states available to recycle to. Target states which generate recycling events
253
+ are specified in ``target_states``, a sequence of TargetState objects. Both ``initial_states``
254
+ and ``target_states`` may be empty as required.
255
+
256
+ The optional ``new_weights`` is a sequence of NewWeightEntry objects which will
257
+ be used to construct the initial flux matrix.
258
+
259
+ The given ``bin_mapper`` will be used for assignment, and ``bin_target_counts`` used for splitting/merging
260
+ target counts; each will be obtained from the system object if omitted or None.
261
+ '''
262
+
263
+ self.clear()
264
+
265
+ new_weights = new_weights or []
266
+ if initial_states is None:
267
+ initial_states = initial_states or []
268
+
269
+ # update mapper, in case it has changed on the system driver and has not been overridden
270
+ if bin_mapper is not None:
271
+ self.bin_mapper = bin_mapper
272
+ else:
273
+ self.bin_mapper = self.system.bin_mapper
274
+
275
+ if bin_target_counts is not None:
276
+ self.bin_target_counts = bin_target_counts
277
+ else:
278
+ self.bin_target_counts = np.array(self.system.bin_target_counts).copy()
279
+ nbins = self.bin_mapper.nbins
280
+ log.debug('mapper is {!r}, handling {:d} bins'.format(self.bin_mapper, nbins))
281
+
282
+ self.initial_binning = self.bin_mapper.construct_bins()
283
+ self.final_binning = self.bin_mapper.construct_bins()
284
+ self.next_iter_binning = None
285
+
286
+ flux_matrix = self.flux_matrix = np.zeros((nbins, nbins), dtype=np.float64)
287
+ transition_matrix = self.transition_matrix = np.zeros((nbins, nbins), np.uint)
288
+
289
+ # map target state specifications to bins
290
+ target_states = target_states or []
291
+ self.target_states = {}
292
+ for tstate in target_states:
293
+ tstate_assignment = self.bin_mapper.assign([tstate.pcoord])[0]
294
+ self.target_states[tstate_assignment] = tstate
295
+ log.debug('target state {!r} mapped to bin {}'.format(tstate, tstate_assignment))
296
+ self.bin_target_counts[tstate_assignment] = 0
297
+
298
+ # loop over recycled segments, adding entries to the flux matrix appropriately
299
+ if new_weights:
300
+ init_pcoords = np.empty((len(new_weights), self.system.pcoord_ndim), dtype=self.system.pcoord_dtype)
301
+ prev_init_pcoords = np.empty((len(new_weights), self.system.pcoord_ndim), dtype=self.system.pcoord_dtype)
302
+
303
+ for ientry, entry in enumerate(new_weights):
304
+ init_pcoords[ientry] = entry.new_init_pcoord
305
+ prev_init_pcoords[ientry] = entry.prev_init_pcoord
306
+
307
+ init_assignments = self.bin_mapper.assign(init_pcoords)
308
+ prev_init_assignments = self.bin_mapper.assign(prev_init_pcoords)
309
+
310
+ for entry, i, j in zip(new_weights, prev_init_assignments, init_assignments):
311
+ flux_matrix[i, j] += entry.weight
312
+ transition_matrix[i, j] += 1
313
+
314
+ del init_pcoords, prev_init_pcoords, init_assignments, prev_init_assignments
315
+
316
+ self.avail_initial_states = {state.state_id: state for state in initial_states}
317
+ self.used_initial_states = {}
318
+
319
+ def add_initial_states(self, initial_states):
320
+ '''Add newly-prepared initial states to the pool available for recycling.'''
321
+ for state in initial_states:
322
+ self.avail_initial_states[state.state_id] = state
323
+
324
+ @property
325
+ def all_initial_states(self):
326
+ '''Return an iterator over all initial states (available or used)'''
327
+ for state in self.avail_initial_states.values():
328
+ yield state
329
+ for state in self.used_initial_states.values():
330
+ yield state
331
+
332
+ def assign(self, segments, initializing=False):
333
+ '''Assign segments to initial and final bins, and update the (internal) lists of used and available
334
+ initial states. If ``initializing`` is True, then the "final" bin assignments will
335
+ be identical to the initial bin assignments, a condition required for seeding a new iteration from
336
+ pre-existing segments.'''
337
+
338
+ # collect initial and final coordinates into one place
339
+ all_pcoords = np.empty((2, len(segments), self.system.pcoord_ndim), dtype=self.system.pcoord_dtype)
340
+
341
+ for iseg, segment in enumerate(segments):
342
+ all_pcoords[0, iseg] = segment.pcoord[0, :]
343
+ all_pcoords[1, iseg] = segment.pcoord[-1, :]
344
+
345
+ # assign based on initial and final progress coordinates
346
+ initial_assignments = self.bin_mapper.assign(all_pcoords[0, :, :])
347
+ if initializing:
348
+ final_assignments = initial_assignments
349
+ else:
350
+ final_assignments = self.bin_mapper.assign(all_pcoords[1, :, :])
351
+
352
+ initial_binning = self.initial_binning
353
+ final_binning = self.final_binning
354
+ flux_matrix = self.flux_matrix
355
+ transition_matrix = self.transition_matrix
356
+ for segment, iidx, fidx in zip(segments, initial_assignments, final_assignments):
357
+ initial_binning[iidx].add(segment)
358
+ final_binning[fidx].add(segment)
359
+ flux_matrix[iidx, fidx] += segment.weight
360
+ transition_matrix[iidx, fidx] += 1
361
+
362
+ n_recycled_total = self.n_recycled_segs
363
+ n_new_states = n_recycled_total - len(self.avail_initial_states)
364
+
365
+ log.debug(
366
+ '{} walkers scheduled for recycling, {} initial states available'.format(
367
+ n_recycled_total, len(self.avail_initial_states)
368
+ )
369
+ )
370
+
371
+ if n_new_states > 0:
372
+ return n_new_states
373
+ else:
374
+ return 0
375
+
376
+ def _recycle_walkers(self):
377
+ '''Recycle walkers'''
378
+
379
+ # recall that every walker we deal with is already a new segment in the subsequent iteration,
380
+ # so to recycle, we actually move the appropriate Segment from the target bin to the initial state bin
381
+
382
+ self.new_weights = []
383
+
384
+ n_recycled_walkers = len(list(self.recycling_segments))
385
+ if not n_recycled_walkers:
386
+ return
387
+ elif n_recycled_walkers > len(self.avail_initial_states):
388
+ raise ConsistencyError(
389
+ 'need {} initial states for recycling, but only {} present'.format(
390
+ n_recycled_walkers, len(self.avail_initial_states)
391
+ )
392
+ )
393
+
394
+ used_istate_ids = set()
395
+ istateiter = iter(self.avail_initial_states.values())
396
+ for ibin, target_state in self.target_states.items():
397
+ target_bin = self.next_iter_binning[ibin]
398
+ for segment in set(target_bin):
399
+ initial_state = next(istateiter)
400
+ istate_assignment = self.bin_mapper.assign([initial_state.pcoord])[0]
401
+ parent = self._parent_map[segment.parent_id]
402
+ parent.endpoint_type = Segment.SEG_ENDPOINT_RECYCLED
403
+
404
+ if log.isEnabledFor(logging.DEBUG):
405
+ log.debug(
406
+ 'recycling {!r} from target state {!r} to initial state {!r}'.format(segment, target_state, initial_state)
407
+ )
408
+ log.debug('parent is {!r}'.format(parent))
409
+
410
+ segment.parent_id = -(initial_state.state_id + 1)
411
+ segment.pcoord[0] = initial_state.pcoord
412
+
413
+ self.new_weights.append(
414
+ NewWeightEntry(
415
+ source_type=NewWeightEntry.NW_SOURCE_RECYCLED,
416
+ weight=parent.weight,
417
+ prev_seg_id=parent.seg_id,
418
+ # the .copy() is crucial, otherwise the slice of pcoords will
419
+ # keep the parent segments' pcoord data alive unnecessarily long
420
+ prev_init_pcoord=parent.pcoord[0].copy(),
421
+ prev_final_pcoord=parent.pcoord[-1].copy(),
422
+ new_init_pcoord=initial_state.pcoord.copy(),
423
+ target_state_id=target_state.state_id,
424
+ initial_state_id=initial_state.state_id,
425
+ )
426
+ )
427
+
428
+ if log.isEnabledFor(logging.DEBUG):
429
+ log.debug('new weight entry is {!r}'.format(self.new_weights[-1]))
430
+
431
+ self.next_iter_binning[istate_assignment].add(segment)
432
+
433
+ initial_state.iter_used = segment.n_iter
434
+ log.debug('marking initial state {!r} as used'.format(initial_state))
435
+ used_istate_ids.add(initial_state.state_id)
436
+ target_bin.remove(segment)
437
+
438
+ assert len(target_bin) == 0
439
+
440
+ # Transfer newly-assigned states from "available" to "used"
441
+ for state_id in used_istate_ids:
442
+ self.used_initial_states[state_id] = self.avail_initial_states.pop(state_id)
443
+
444
+ def _split_walker(self, segment, m, bin):
445
+ '''Split the walker ``segment`` (in ``bin``) into ``m`` walkers'''
446
+ new_segments = []
447
+ for _inew in range(0, m):
448
+ new_segment = Segment(
449
+ n_iter=segment.n_iter, # previously incremented
450
+ weight=segment.weight / m,
451
+ parent_id=segment.parent_id,
452
+ wtg_parent_ids=set(segment.wtg_parent_ids),
453
+ pcoord=segment.pcoord.copy(),
454
+ status=Segment.SEG_STATUS_PREPARED,
455
+ )
456
+ new_segment.pcoord[0, :] = segment.pcoord[0, :]
457
+ new_segments.append(new_segment)
458
+
459
+ if log.isEnabledFor(logging.DEBUG):
460
+ log.debug('splitting {!r} into {:d}:\n {!r}'.format(segment, m, new_segments))
461
+
462
+ return new_segments
463
+
464
+ def _merge_walkers(self, segments, cumul_weight, bin):
465
+ '''Merge the given ``segments`` in ``bin``, previously sorted by weight, into one conglomerate segment.
466
+ ``cumul_weight`` is the cumulative sum of the weights of the ``segments``; this may be None to calculate here.'''
467
+
468
+ if cumul_weight is None:
469
+ cumul_weight = np.add.accumulate([segment.weight for segment in segments])
470
+
471
+ glom = Segment(
472
+ n_iter=segments[0].n_iter, # assumed correct (and equal among all segments)
473
+ weight=cumul_weight[len(segments) - 1],
474
+ status=Segment.SEG_STATUS_PREPARED,
475
+ pcoord=self.system.new_pcoord_array(),
476
+ )
477
+
478
+ # Select the history to use
479
+ # The following takes a random number in the interval 0 <= x < glom.weight, then
480
+ # sees where this value falls among the (sorted) weights of the segments being merged;
481
+ # this ensures that a walker with (e.g.) twice the weight of its brethren has twice the
482
+ # probability of having its history selected for continuation
483
+ iparent = np.digitize((self.rng.uniform(0, glom.weight),), cumul_weight)[0]
484
+ gparent_seg = segments[iparent]
485
+
486
+ # Inherit history from this segment ("gparent" stands for "glom parent", as opposed to historical
487
+ # parent).
488
+ glom.parent_id = gparent_seg.parent_id
489
+ glom.pcoord[0, :] = gparent_seg.pcoord[0, :]
490
+
491
+ # Weight comes from all segments being merged, and therefore all their
492
+ # parent segments
493
+ glom.wtg_parent_ids = set()
494
+ for segment in segments:
495
+ glom.wtg_parent_ids |= segment.wtg_parent_ids
496
+
497
+ # The historical parent of gparent is continued; all others are marked as merged
498
+ for segment in segments:
499
+ if segment is gparent_seg:
500
+ # we must ignore initial states here...
501
+ if segment.parent_id >= 0:
502
+ self._parent_map[segment.parent_id].endpoint_type = Segment.SEG_ENDPOINT_CONTINUES
503
+ else:
504
+ # and "unuse" an initial state here (recall that initial states are in 1:1 correspondence
505
+ # with the segments they initiate), except when a previously-split particle is being
506
+ # merged
507
+ if segment.parent_id >= 0:
508
+ self._parent_map[segment.parent_id].endpoint_type = Segment.SEG_ENDPOINT_MERGED
509
+ else:
510
+ if segment.initial_state_id in {segment.initial_state_id for segment in bin}:
511
+ log.debug('initial state in use by other walker; not removing')
512
+ else:
513
+ initial_state = self.used_initial_states.pop(segment.initial_state_id)
514
+ log.debug('freeing initial state {!r} for future use (merged)'.format(initial_state))
515
+ self.avail_initial_states[initial_state.state_id] = initial_state
516
+ initial_state.iter_used = None
517
+
518
+ if log.isEnabledFor(logging.DEBUG):
519
+ log.debug('merging ({:d}) {!r} into 1:\n {!r}'.format(len(segments), segments, glom))
520
+
521
+ return glom, gparent_seg
522
+
523
+ def _split_by_weight(self, bin, target_count, ideal_weight):
524
+ '''Split overweight particles'''
525
+
526
+ segments = np.array(sorted(bin, key=operator.attrgetter('weight')), dtype=np.object_)
527
+ weights = np.array(list(map(operator.attrgetter('weight'), segments)))
528
+
529
+ if len(bin) > 0:
530
+ assert target_count > 0
531
+
532
+ to_split = segments[weights > self.weight_split_threshold * ideal_weight]
533
+
534
+ for segment in to_split:
535
+ m = math.ceil(segment.weight / ideal_weight)
536
+ bin.remove(segment)
537
+ new_segments_list = self._split_walker(segment, m, bin)
538
+ bin.update(new_segments_list)
539
+
540
+ def _merge_by_weight(self, bin, target_count, ideal_weight):
541
+ '''Merge underweight particles'''
542
+
543
+ while True:
544
+ segments = np.array(sorted(bin, key=operator.attrgetter('weight')), dtype=np.object_)
545
+ weights = np.array(list(map(operator.attrgetter('weight'), segments)))
546
+ cumul_weight = np.add.accumulate(weights)
547
+
548
+ to_merge = segments[cumul_weight <= ideal_weight * self.weight_merge_cutoff]
549
+ if len(to_merge) < 2:
550
+ return
551
+ bin.difference_update(to_merge)
552
+ new_segment, parent = self._merge_walkers(to_merge, cumul_weight, bin)
553
+ bin.add(new_segment)
554
+
555
+ def _adjust_count(self, bin, subgroups, target_count):
556
+ weight_getter = operator.attrgetter('weight')
557
+ # Order subgroups by the sum of their weights.
558
+ if len(subgroups) > target_count:
559
+ sorted_subgroups = [set()]
560
+ for i in bin:
561
+ sorted_subgroups[0].add(i)
562
+ else:
563
+ sorted_subgroups = sorted(subgroups, key=lambda gp: sum(seg.weight for seg in gp))
564
+ # Loops over the groups, splitting/merging until the proper count has been reached. This way, no trajectories are accidentally destroyed.
565
+
566
+ # split
567
+ while len(bin) < target_count:
568
+ for i in sorted_subgroups:
569
+ log.debug('adjusting counts by splitting')
570
+ # always split the highest probability walker into two
571
+ segments = sorted(i, key=weight_getter)
572
+ bin.remove(segments[-1])
573
+ i.remove(segments[-1])
574
+ new_segments_list = self._split_walker(segments[-1], 2, bin)
575
+ i.update(new_segments_list)
576
+ bin.update(new_segments_list)
577
+
578
+ if len(bin) == target_count:
579
+ break
580
+
581
+ # merge
582
+ while len(bin) > target_count:
583
+ sorted_subgroups.reverse()
584
+ # Adjust to go from lowest weight group to highest to merge
585
+ for i in sorted_subgroups:
586
+ # Ensures that there are least two walkers to merge
587
+ if len(i) > 1:
588
+ log.debug('adjusting counts by merging')
589
+ # always merge the two lowest-probability walkers
590
+ segments = sorted(i, key=weight_getter)
591
+ bin.difference_update(segments[:2])
592
+ i.difference_update(segments[:2])
593
+ merged_segment, parent = self._merge_walkers(segments[:2], cumul_weight=None, bin=bin)
594
+ i.add(merged_segment)
595
+ bin.add(merged_segment)
596
+
597
+ # As long as we're changing the merge_walkers and split_walkers, adjust them so that they don't update the bin within the function
598
+ # and instead update the bin here. Assuming nothing else relies on those. Make sure with grin.
599
+ # in bash, "find . -name \*.py | xargs fgrep -n '_merge_walkers'"
600
+ if len(bin) == target_count:
601
+ break
602
+
603
+ def _merge_by_threshold(self, bin, subgroup):
604
+ # merge to satisfy weight thresholds
605
+ # this gets rid of weights that are too small
606
+ while True:
607
+ segments = np.array(sorted(subgroup, key=operator.attrgetter('weight')), dtype=np.object_)
608
+ weights = np.array(list(map(operator.attrgetter('weight'), segments)))
609
+ cumul_weight = np.add.accumulate(weights)
610
+
611
+ to_merge = segments[weights < self.smallest_allowed_weight]
612
+ if len(to_merge) < 2:
613
+ return
614
+ bin.difference_update(to_merge)
615
+ subgroup.difference_update(to_merge)
616
+ new_segment, parent = self._merge_walkers(to_merge, cumul_weight, bin)
617
+ bin.add(new_segment)
618
+ subgroup.add(new_segment)
619
+
620
+ def _split_by_threshold(self, bin, subgroup):
621
+ # split to satisfy weight thresholds
622
+ # this splits walkers that are too big
623
+ segments = np.array(sorted(subgroup, key=operator.attrgetter('weight')), dtype=np.object_)
624
+ weights = np.array(list(map(operator.attrgetter('weight'), segments)))
625
+
626
+ to_split = segments[weights > self.largest_allowed_weight]
627
+ for segment in to_split:
628
+ m = math.ceil(segment.weight / self.largest_allowed_weight)
629
+ bin.remove(segment)
630
+ subgroup.remove(segment)
631
+ new_segments_list = self._split_walker(segment, m, bin)
632
+ bin.update(new_segments_list)
633
+ subgroup.update(new_segments_list)
634
+
635
+ def _check_pre(self):
636
+ for ibin, _bin in enumerate(self.next_iter_binning):
637
+ if self.bin_target_counts[ibin] == 0 and len(_bin) > 0:
638
+ raise ConsistencyError('bin {:d} has target count of 0 but contains {:d} walkers'.format(ibin, len(_bin)))
639
+
640
+ def _check_post(self):
641
+ for segment in self.next_iter_segments:
642
+ if segment.weight == 0:
643
+ raise ConsistencyError('segment {!r} has weight of zero')
644
+
645
+ def _prep_we(self):
646
+ '''Prepare internal state for WE recycle/split/merge.'''
647
+ self._parent_map = {}
648
+ self.next_iter_binning = self.bin_mapper.construct_bins()
649
+
650
+ def _run_we(self):
651
+ '''Run recycle/split/merge. Do not call this function directly; instead, use
652
+ populate_initial(), rebin_current(), or construct_next().'''
653
+ self._recycle_walkers()
654
+
655
+ # sanity check
656
+ self._check_pre()
657
+
658
+ # Regardless of current particle count, always split overweight particles and merge underweight particles
659
+ # Then and only then adjust for correct particle count
660
+ total_number_of_subgroups = 0
661
+ total_number_of_particles = 0
662
+ for ibin, bin in enumerate(self.next_iter_binning):
663
+ if len(bin) == 0:
664
+ continue
665
+
666
+ # Splits the bin into subgroups as defined by the called function
667
+ target_count = self.bin_target_counts[ibin]
668
+ subgroups = self.subgroup_function(self, ibin, **self.subgroup_function_kwargs)
669
+ total_number_of_subgroups += len(subgroups)
670
+ # Clear the bin
671
+ segments = np.array(sorted(bin, key=operator.attrgetter('weight')), dtype=np.object_)
672
+ weights = np.array(list(map(operator.attrgetter('weight'), segments)))
673
+ ideal_weight = weights.sum() / target_count
674
+ bin.clear()
675
+ # Determines to see whether we have more sub bins than we have target walkers in a bin (or equal to), and then uses
676
+ # different logic to deal with those cases. Should devolve to the Huber/Kim algorithm in the case of few subgroups.
677
+ if len(subgroups) >= target_count:
678
+ for i in subgroups:
679
+ # Merges all members of set i. Checks to see whether there are any to merge.
680
+ if len(i) > 1:
681
+ (segment, parent) = self._merge_walkers(
682
+ list(i),
683
+ np.add.accumulate(np.array(list(map(operator.attrgetter('weight'), i)))),
684
+ i,
685
+ )
686
+ i.clear()
687
+ i.add(segment)
688
+ # Add all members of the set i to the bin. This keeps the bins in sync for the adjustment step.
689
+ bin.update(i)
690
+
691
+ if len(subgroups) > target_count:
692
+ self._adjust_count(bin, subgroups, target_count)
693
+
694
+ if len(subgroups) < target_count:
695
+ for i in subgroups:
696
+ self._split_by_weight(i, target_count, ideal_weight)
697
+ self._merge_by_weight(i, target_count, ideal_weight)
698
+ # Same logic here.
699
+ bin.update(i)
700
+ if self.do_adjust_counts:
701
+ # A modified adjustment routine is necessary to ensure we don't unnecessarily destroy trajectory pathways.
702
+ self._adjust_count(bin, subgroups, target_count)
703
+ if self.do_thresholds:
704
+ for i in subgroups:
705
+ self._split_by_threshold(bin, i)
706
+ self._merge_by_threshold(bin, i)
707
+ for iseg in bin:
708
+ if iseg.weight > self.largest_allowed_weight or iseg.weight < self.smallest_allowed_weight:
709
+ log.warning(
710
+ f'Unable to fulfill threshold conditions for {iseg}. The given threshold range is likely too small.'
711
+ )
712
+ total_number_of_particles += len(bin)
713
+ log.debug('Total number of subgroups: {!r}'.format(total_number_of_subgroups))
714
+
715
+ self._check_post()
716
+
717
+ self.new_weights = self.new_weights or []
718
+
719
+ log.debug('used initial states: {!r}'.format(self.used_initial_states))
720
+ log.debug('available initial states: {!r}'.format(self.avail_initial_states))
721
+
722
+ def populate_initial(self, initial_states, weights, system=None):
723
+ '''Create walkers for a new weighted ensemble simulation.
724
+
725
+ One segment is created for each provided initial state, then binned and split/merged
726
+ as necessary. After this function is called, next_iter_segments will yield the new
727
+ segments to create, used_initial_states will contain data about which of the
728
+ provided initial states were used, and avail_initial_states will contain data about
729
+ which initial states were unused (because their corresponding walkers were merged
730
+ out of existence).
731
+ '''
732
+
733
+ # This has to be down here to avoid an import race
734
+ from westpa.core.data_manager import weight_dtype
735
+
736
+ EPS = np.finfo(weight_dtype).eps
737
+
738
+ system = system or westpa.rc.get_system_driver()
739
+ self.new_iteration(
740
+ initial_states=[], target_states=[], bin_mapper=system.bin_mapper, bin_target_counts=system.bin_target_counts
741
+ )
742
+
743
+ # Create dummy segments
744
+ segments = []
745
+ for seg_id, (initial_state, weight) in enumerate(zip(initial_states, weights)):
746
+ dummy_segment = Segment(
747
+ n_iter=0,
748
+ seg_id=seg_id,
749
+ parent_id=-(initial_state.state_id + 1),
750
+ weight=weight,
751
+ wtg_parent_ids=set([-(initial_state.state_id + 1)]),
752
+ pcoord=system.new_pcoord_array(),
753
+ status=Segment.SEG_STATUS_PREPARED,
754
+ )
755
+ dummy_segment.pcoord[[0, -1]] = initial_state.pcoord
756
+ segments.append(dummy_segment)
757
+
758
+ # Adjust weights, if necessary
759
+ tprob = sum(weights)
760
+ if abs(1.0 - tprob) > len(weights) * EPS:
761
+ pscale = 1.0 / tprob
762
+ log.warning('Weights of initial segments do not sum to unity; scaling by {:g}'.format(pscale))
763
+ for segment in segments:
764
+ segment.weight *= pscale
765
+
766
+ self.assign(segments, initializing=True)
767
+ self.construct_next()
768
+
769
+ # We now have properly-constructed initial segments, except for parent information,
770
+ # and we need to mark initial states as used or unused
771
+ istates_by_id = {state.state_id: state for state in initial_states}
772
+ dummysegs_by_id = self._parent_map
773
+
774
+ # Don't add start states to the list of available initial states.
775
+ # They're only meant to be used in the first iteration, so nothing should ever be recycled into them.
776
+ # Thus, they're not available.
777
+ self.avail_initial_states = {
778
+ k: v for (k, v) in istates_by_id.items() if not v.istate_type == InitialState.ISTATE_TYPE_START
779
+ }
780
+
781
+ for state in self.avail_initial_states.keys():
782
+ if self.avail_initial_states[state].istate_type == InitialState.ISTATE_TYPE_START:
783
+ self.avail_initial_states.pop(state)
784
+
785
+ self.used_initial_states = {}
786
+ for segment in self.next_iter_segments:
787
+ segment.parent_id = dummysegs_by_id[segment.parent_id].parent_id
788
+ segment.wtg_parent_ids = set([segment.parent_id])
789
+ assert segment.initpoint_type == Segment.SEG_INITPOINT_NEWTRAJ
790
+ istate = istates_by_id[segment.initial_state_id]
791
+ try:
792
+ self.used_initial_states[istate.state_id] = self.avail_initial_states.pop(istate.state_id)
793
+ except KeyError:
794
+ # Shared by more than one segment, and already marked as used
795
+ pass
796
+
797
+ for used_istate in self.used_initial_states.values():
798
+ used_istate.iter_used = 1
799
+
800
+ def rebin_current(self, parent_segments):
801
+ '''Reconstruct walkers for the current iteration based on (presumably) new binning.
802
+ The previous iteration's segments must be provided (as ``parent_segments``) in order
803
+ to update endpoint types appropriately.'''
804
+
805
+ self._prep_we()
806
+ self._parent_map = {segment.seg_id: segment for segment in parent_segments}
807
+
808
+ # Create new segments for the next iteration
809
+ # We assume that everything is going to continue without being touched by recycling or WE, and
810
+ # adjust later
811
+ new_pcoord_array = self.system.new_pcoord_array
812
+ n_iter = None
813
+
814
+ for ibin, _bin in enumerate(self.final_binning):
815
+ for segment in _bin:
816
+ if n_iter is None:
817
+ n_iter = segment.n_iter
818
+ else:
819
+ assert segment.n_iter == n_iter
820
+
821
+ new_segment = Segment(
822
+ n_iter=segment.n_iter,
823
+ parent_id=segment.parent_id,
824
+ weight=segment.weight,
825
+ wtg_parent_ids=set(segment.wtg_parent_ids or []),
826
+ pcoord=new_pcoord_array(),
827
+ status=Segment.SEG_STATUS_PREPARED,
828
+ )
829
+ new_segment.pcoord[0] = segment.pcoord[0]
830
+ self.next_iter_binning[ibin].add(new_segment)
831
+
832
+ self._run_we()
833
+
834
+ def construct_next(self):
835
+ '''Construct walkers for the next iteration, by running weighted ensemble recycling
836
+ and bin/split/merge on the segments previously assigned to bins using ``assign``.
837
+ Enough unused initial states must be present in ``self.avail_initial_states`` for every recycled
838
+ walker to be assigned an initial state.
839
+
840
+ After this function completes, ``self.flux_matrix`` contains a valid flux matrix for this
841
+ iteration (including any contributions from recycling from the previous iteration), and
842
+ ``self.next_iter_segments`` contains a list of segments ready for the next iteration,
843
+ with appropriate values set for weight, endpoint type, parent walkers, and so on.
844
+ '''
845
+
846
+ self._prep_we()
847
+
848
+ # Create new segments for the next iteration
849
+ # We assume that everything is going to continue without being touched by recycling or WE, and
850
+ # adjust later
851
+ new_pcoord_array = self.system.new_pcoord_array
852
+ n_iter = None
853
+
854
+ for ibin, _bin in enumerate(self.final_binning):
855
+ for segment in _bin:
856
+ if n_iter is None:
857
+ n_iter = segment.n_iter
858
+ else:
859
+ assert segment.n_iter == n_iter
860
+
861
+ segment.endpoint_type = Segment.SEG_ENDPOINT_CONTINUES
862
+ new_segment = Segment(
863
+ n_iter=segment.n_iter + 1,
864
+ parent_id=segment.seg_id,
865
+ weight=segment.weight,
866
+ wtg_parent_ids=[segment.seg_id],
867
+ pcoord=new_pcoord_array(),
868
+ status=Segment.SEG_STATUS_PREPARED,
869
+ )
870
+ new_segment.pcoord[0] = segment.pcoord[-1]
871
+ self.next_iter_binning[ibin].add(new_segment)
872
+
873
+ # Store a link to the parent segment, so we can update its endpoint status as we need,
874
+ # based on its ID
875
+ self._parent_map[segment.seg_id] = segment
876
+
877
+ self._run_we()
878
+
879
+ log.debug('used initial states: {!r}'.format(self.used_initial_states))
880
+ log.debug('available initial states: {!r}'.format(self.avail_initial_states))
881
+
882
+ def _log_bin_stats(self, bin, heading=None, level=logging.DEBUG):
883
+ if log.isEnabledFor(level):
884
+ weights = sorted(np.array(list(map(operator.attrgetter('weight'), bin))))
885
+ bin_label = getattr(bin, 'label', None) or ''
886
+ log_fmt = '\n '.join(
887
+ [
888
+ '',
889
+ 'stats for bin {bin_label!r} {heading}',
890
+ ' count: {bin.count:d}, target count: {bin.target_count:d}',
891
+ ' total weight: {bin.weight:{weight_spec}}, ideal weight: {ideal_weight:{weight_spec}}',
892
+ ' mean weight: {mean_weight:{weight_spec}}, stdev weight: {stdev_weight:{weight_spec}}',
893
+ ' min weight: {min_weight:{weight_spec}}, med weight : {median_weight:{weight_spec}}'
894
+ + ', max weight: {max_weight:{weight_spec}}',
895
+ ]
896
+ )
897
+ log_msg = log_fmt.format(
898
+ log_fmt,
899
+ weight_spec='<12.6e',
900
+ bin_label=bin_label,
901
+ heading=heading,
902
+ bin=bin,
903
+ ideal_weight=bin.weight / bin.target_count,
904
+ mean_weight=weights.mean(),
905
+ stdev_weight=weights.std(),
906
+ min_weight=weights[0],
907
+ median_weight=np.median(weights),
908
+ max_weight=weights[-1],
909
+ )
910
+ log.log(level, log_msg)