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,839 @@
1
+ import logging
2
+ import math
3
+ import operator
4
+ import time
5
+ from datetime import timedelta
6
+ from pickle import PickleError
7
+ from itertools import zip_longest
8
+ from collections import Counter
9
+
10
+ import numpy as np
11
+ from numpy.random import Generator, MT19937
12
+
13
+ import westpa
14
+ from .data_manager import weight_dtype
15
+ from .segment import Segment
16
+ from .states import InitialState
17
+ from . import extloader
18
+ from . import wm_ops
19
+
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ EPS = np.finfo(weight_dtype).eps
24
+
25
+
26
+ def grouper(n, iterable, fillvalue=None):
27
+ "Collect data into fixed-length chunks or blocks"
28
+ # grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx
29
+ args = [iter(iterable)] * n
30
+ return zip_longest(fillvalue=fillvalue, *args)
31
+
32
+
33
+ class PropagationError(RuntimeError):
34
+ pass
35
+
36
+
37
+ class WESimManager:
38
+ def process_config(self):
39
+ config = self.rc.config
40
+ for entry, type_ in [('gen_istates', bool), ('block_size', int), ('save_transition_matrices', bool)]:
41
+ config.require_type_if_present(['west', 'propagation', entry], type_)
42
+
43
+ self.do_gen_istates = config.get(['west', 'propagation', 'gen_istates'], False)
44
+ self.propagator_block_size = config.get(['west', 'propagation', 'block_size'], 1)
45
+ self.save_transition_matrices = config.get(['west', 'propagation', 'save_transition_matrices'], False)
46
+ self.max_run_walltime = config.get(['west', 'propagation', 'max_run_wallclock'], default=None)
47
+ self.max_total_iterations = config.get(['west', 'propagation', 'max_total_iterations'], default=None)
48
+
49
+ def __init__(self, rc=None):
50
+ self.rc = rc or westpa.rc
51
+ self.work_manager = self.rc.get_work_manager()
52
+ self.data_manager = self.rc.get_data_manager()
53
+ self.we_driver = self.rc.get_we_driver()
54
+ self.system = self.rc.get_system_driver()
55
+
56
+ # A table of function -> list of (priority, name, callback) tuples
57
+ self._callback_table = {}
58
+ self._valid_callbacks = {
59
+ self.prepare_run,
60
+ self.finalize_run,
61
+ self.prepare_iteration,
62
+ self.finalize_iteration,
63
+ self.pre_propagation,
64
+ self.post_propagation,
65
+ self.pre_we,
66
+ self.post_we,
67
+ self.prepare_new_iteration,
68
+ }
69
+ self._callbacks_by_name = {fn.__name__: fn for fn in self._valid_callbacks}
70
+ self.n_propagated = 0
71
+
72
+ # config items
73
+ self.do_gen_istates = False
74
+ self.propagator_block_size = 1
75
+ self.save_transition_matrices = False
76
+ self.max_run_walltime = None
77
+ self.max_total_iterations = None
78
+ self.process_config()
79
+
80
+ # Per-iteration variables
81
+ self.n_iter = None # current iteration
82
+
83
+ # Basis and initial states for this iteration, in case the propagator needs them
84
+ self.current_iter_bstates = None # BasisStates valid at this iteration
85
+ self.current_iter_istates = None # InitialStates used in this iteration
86
+
87
+ # Basis states for next iteration
88
+ self.next_iter_bstates = None # BasisStates valid for the next iteration
89
+ self.next_iter_bstate_cprobs = None # Cumulative probabilities for basis states, used for selection
90
+
91
+ # Tracking of this iteration's segments
92
+ self.segments = None # Mapping of seg_id to segment for all segments in this iteration
93
+ self.completed_segments = None # Mapping of seg_id to segment for all completed segments in this iteration
94
+ self.incomplete_segments = None # Mapping of seg_id to segment for all incomplete segments in this iteration
95
+
96
+ # Tracking of binning
97
+ self.bin_mapper_hash = None # Hash of bin mapper from most recently-run WE, for use by post-WE analysis plugins
98
+
99
+ # Pseudo Random Number Generator
100
+ self.rng = Generator(MT19937())
101
+
102
+ def register_callback(self, hook, function, priority=0):
103
+ '''Registers a callback to execute during the given ``hook`` into the simulation loop. The optional
104
+ priority is used to order when the function is called relative to other registered callbacks.'''
105
+
106
+ if hook not in self._valid_callbacks:
107
+ try:
108
+ hook = self._callbacks_by_name[hook]
109
+ except KeyError:
110
+ raise KeyError('invalid hook {!r}'.format(hook))
111
+
112
+ # It's possible to register a callback that's a duplicate function, but at a different place in memory.
113
+ # For example, if you launch a run without clearing state from a previous run.
114
+ # More details on this are available in https://github.com/westpa/westpa/issues/182 but the below code
115
+ # handles specifically the problem that causes in plugin loading.
116
+ try:
117
+ # Before checking for set membership of (priority, function.__name__, function), just check
118
+ # function hash for collisions in this hook.
119
+ hook_function_hash = [hash(callback[2]) for callback in self._callback_table[hook]]
120
+ except KeyError:
121
+ # If there's no entry in self._callback_table for this hook, then there definitely aren't any collisions
122
+ # because no plugins are registered to it yet in the first place.
123
+ pass
124
+ else:
125
+ # If there are plugins registered to this hook, check for duplicate hash, which will definitely have the same name, module, function.
126
+ try:
127
+ if hash(function) in hook_function_hash:
128
+ log.info('{!r} has already been loaded, skipping'.format(function))
129
+ return
130
+ except KeyError:
131
+ pass
132
+
133
+ try:
134
+ self._callback_table[hook].add((priority, function.__name__, function))
135
+ except KeyError:
136
+ self._callback_table[hook] = set([(priority, function.__name__, function)])
137
+
138
+ # Raise warning if there are multiple callback with same priority.
139
+ for priority, count in Counter([callback[0] for callback in self._callback_table[hook]]).items():
140
+ if count > 1:
141
+ log.warning(
142
+ f'{count} callbacks in {hook} have identical priority {priority}. The order of callback execution is not guaranteed.'
143
+ )
144
+ log.warning(f'{hook}: {self._callback_table[hook]}')
145
+
146
+ log.debug('registered callback {!r} for hook {!r}'.format(function, hook))
147
+
148
+ def invoke_callbacks(self, hook, *args, **kwargs):
149
+ callbacks = self._callback_table.get(hook, [])
150
+ # Sort by priority, function name, then module name
151
+ sorted_callbacks = sorted(callbacks, key=lambda x: (x[0], x[1], x[2].__module__))
152
+ for priority, name, fn in sorted_callbacks:
153
+ log.debug('invoking callback {!r} for hook {!r}'.format(fn, hook))
154
+ fn(*args, **kwargs)
155
+
156
+ def load_plugins(self, plugins=None):
157
+ if plugins is None:
158
+ plugins = []
159
+
160
+ try:
161
+ plugins_config = westpa.rc.config['west', 'plugins']
162
+ except KeyError:
163
+ plugins_config = []
164
+
165
+ if plugins_config is None:
166
+ plugins_config = []
167
+
168
+ plugins += plugins_config
169
+
170
+ for plugin_config in plugins:
171
+ plugin_name = plugin_config['plugin']
172
+ if plugin_config.get('enabled', True):
173
+ log.info('loading plugin {!r}'.format(plugin_name))
174
+ plugin = extloader.get_object(plugin_name)(self, plugin_config)
175
+ log.debug('loaded plugin {!r}'.format(plugin))
176
+
177
+ def report_bin_statistics(self, bins, target_states, save_summary=False):
178
+ segments = list(self.segments.values())
179
+ bin_counts = np.fromiter(map(len, bins), dtype=np.int_, count=len(bins))
180
+ target_counts = self.we_driver.bin_target_counts
181
+
182
+ # Do not include bins with target count zero (e.g. sinks, never-filled bins) in the (non)empty bins statistics
183
+ n_active_bins = len(target_counts[target_counts != 0])
184
+
185
+ if target_states:
186
+ n_active_bins -= len(target_states)
187
+
188
+ seg_probs = np.fromiter(map(operator.attrgetter('weight'), segments), dtype=weight_dtype, count=len(segments))
189
+ bin_probs = np.fromiter(map(operator.attrgetter('weight'), bins), dtype=weight_dtype, count=len(bins))
190
+ norm = seg_probs.sum()
191
+
192
+ assert abs(1 - norm) < EPS * (len(segments) + n_active_bins)
193
+
194
+ min_seg_prob = seg_probs[seg_probs != 0].min()
195
+ max_seg_prob = seg_probs.max()
196
+ seg_drange = math.log(max_seg_prob / min_seg_prob)
197
+ min_bin_prob = bin_probs[bin_probs != 0].min()
198
+ max_bin_prob = bin_probs.max()
199
+ bin_drange = math.log(max_bin_prob / min_bin_prob)
200
+ n_pop = len(bin_counts[bin_counts != 0])
201
+
202
+ self.rc.pstatus('{:d} of {:d} ({:%}) active bins are populated'.format(n_pop, n_active_bins, n_pop / n_active_bins))
203
+ self.rc.pstatus('per-bin minimum non-zero probability: {:g}'.format(min_bin_prob))
204
+ self.rc.pstatus('per-bin maximum probability: {:g}'.format(max_bin_prob))
205
+ self.rc.pstatus('per-bin probability dynamic range (kT): {:g}'.format(bin_drange))
206
+ self.rc.pstatus('per-segment minimum non-zero probability: {:g}'.format(min_seg_prob))
207
+ self.rc.pstatus('per-segment maximum non-zero probability: {:g}'.format(max_seg_prob))
208
+ self.rc.pstatus('per-segment probability dynamic range (kT): {:g}'.format(seg_drange))
209
+ self.rc.pstatus('norm = {:g}, error in norm = {:g} ({:.2g}*epsilon)'.format(norm, (norm - 1), (norm - 1) / EPS))
210
+ self.rc.pflush()
211
+
212
+ if min_seg_prob < 1e-100:
213
+ log.warning(
214
+ '\nMinimum segment weight is < 1e-100 and might not be physically relevant. Please reconsider your progress coordinate or binning scheme.'
215
+ )
216
+
217
+ if save_summary:
218
+ iter_summary = self.data_manager.get_iter_summary()
219
+ iter_summary['n_particles'] = len(segments)
220
+ iter_summary['norm'] = norm
221
+ iter_summary['min_bin_prob'] = min_bin_prob
222
+ iter_summary['max_bin_prob'] = max_bin_prob
223
+ iter_summary['min_seg_prob'] = min_seg_prob
224
+ iter_summary['max_seg_prob'] = max_seg_prob
225
+ if np.isnan(iter_summary['cputime']):
226
+ iter_summary['cputime'] = 0.0
227
+ if np.isnan(iter_summary['walltime']):
228
+ iter_summary['walltime'] = 0.0
229
+ self.data_manager.update_iter_summary(iter_summary)
230
+
231
+ def get_bstate_pcoords(self, basis_states, label='basis'):
232
+ '''For each of the given ``basis_states``, calculate progress coordinate values
233
+ as necessary. The HDF5 file is not updated. The BasisState objects are explicitly
234
+ copied from the futures in order to retain auxdata/restart files (under BasisState.data)
235
+ from certain work managers (e.g., the ``processes`` work manager.)'''
236
+
237
+ self.rc.pstatus('Calculating progress coordinate values for {} states.'.format(label))
238
+ futures = [self.work_manager.submit(wm_ops.get_pcoord, args=(basis_state,)) for basis_state in basis_states]
239
+ fmap = {future: i for (i, future) in enumerate(futures)}
240
+ for future in self.work_manager.as_completed(futures):
241
+ basis_states[fmap[future]] = future.get_result()
242
+
243
+ def report_basis_states(self, basis_states, label='basis'):
244
+ pstatus = self.rc.pstatus
245
+ pstatus('{:d} {} state(s) present'.format(len(basis_states), label), end='')
246
+ if self.rc.verbose_mode:
247
+ pstatus(':')
248
+ pstatus(
249
+ '{:6s} {:12s} {:20s} {:20s} {}'.format(
250
+ 'ID', 'Label', 'Probability', 'Aux Reference', 'Progress Coordinate'
251
+ )
252
+ )
253
+ for basis_state in basis_states:
254
+ pstatus(
255
+ '{:<6d} {:12s} {:<20.14g} {:20s} {}'.format(
256
+ basis_state.state_id,
257
+ basis_state.label,
258
+ basis_state.probability,
259
+ basis_state.auxref or '',
260
+ ', '.join(map(str, basis_state.pcoord)),
261
+ )
262
+ )
263
+ pstatus()
264
+ self.rc.pflush()
265
+
266
+ def report_target_states(self, target_states):
267
+ pstatus = self.rc.pstatus
268
+ pstatus('{:d} target state(s) present'.format(len(target_states)), end='')
269
+ if self.rc.verbose_mode and target_states:
270
+ pstatus(':')
271
+ pstatus('{:6s} {:12s} {}'.format('ID', 'Label', 'Progress Coordinate'))
272
+ for target_state in target_states:
273
+ pstatus(
274
+ '{:<6d} {:12s} {}'.format(
275
+ target_state.state_id, target_state.label, ','.join(map(str, target_state.pcoord))
276
+ )
277
+ )
278
+ pstatus()
279
+ self.rc.pflush()
280
+
281
+ def initialize_simulation(self, basis_states, target_states, start_states, segs_per_state=1, suppress_we=False):
282
+ '''Initialize a new weighted ensemble simulation, taking ``segs_per_state`` initial
283
+ states from each of the given ``basis_states``.
284
+
285
+ ``w_init`` is the forward-facing version of this function'''
286
+
287
+ data_manager = self.data_manager
288
+ work_manager = self.work_manager
289
+ pstatus = self.rc.pstatus
290
+ system = self.system
291
+
292
+ pstatus('Creating HDF5 file {!r}'.format(self.data_manager.we_h5filename))
293
+ data_manager.prepare_backing()
294
+
295
+ # Process target states
296
+ data_manager.save_target_states(target_states)
297
+ self.report_target_states(target_states)
298
+
299
+ # Process basis states
300
+ self.get_bstate_pcoords(basis_states)
301
+ self.data_manager.create_ibstate_group(basis_states)
302
+ self.data_manager.create_ibstate_iter_h5file(basis_states)
303
+ self.report_basis_states(basis_states)
304
+
305
+ # Process start states
306
+ # Unlike the above, does not create an ibstate group.
307
+ # Should it? I don't think so, if needed it can be traced back through basis_auxref
308
+
309
+ # Here, we are trying to assign a state_id to the start state to be initialized, without actually
310
+ # saving it to the ibstates records in any of the h5 files. It might actually be a problem
311
+ # when tracing trajectories with westpa.analysis (especially with HDF5 framework) since it would
312
+ # try to look for a basis state id > len(basis_state).
313
+ # Since start states are only used while initializing and iteration 1, it's ok to not save it to save space. If necessary,
314
+ # the structure can be traced directly to the parent file using the standard basis state logic referencing
315
+ # west[iterations/iter_00000001/ibstates/istate_index/basis_auxref] of that istate.
316
+
317
+ if len(start_states) > 0 and start_states[0].state_id is None:
318
+ last_id = basis_states[-1].state_id
319
+ for start_state in start_states:
320
+ start_state.state_id = last_id + 1
321
+ last_id += 1
322
+
323
+ self.get_bstate_pcoords(start_states, label='start')
324
+ self.report_basis_states(start_states, label='start')
325
+
326
+ pstatus('Preparing initial states')
327
+ initial_states = []
328
+ weights = []
329
+ if self.do_gen_istates:
330
+ istate_type = InitialState.ISTATE_TYPE_GENERATED
331
+ else:
332
+ istate_type = InitialState.ISTATE_TYPE_BASIS
333
+
334
+ for basis_state in basis_states:
335
+ for _iseg in range(segs_per_state):
336
+ initial_state = data_manager.create_initial_states(1, 1)[0]
337
+ initial_state.basis_state_id = basis_state.state_id
338
+ initial_state.basis_state = basis_state
339
+ initial_state.istate_type = istate_type
340
+ weights.append(basis_state.probability / segs_per_state)
341
+ initial_states.append(initial_state)
342
+
343
+ for start_state in start_states:
344
+ for _iseg in range(segs_per_state):
345
+ initial_state = data_manager.create_initial_states(1, 1)[0]
346
+ initial_state.basis_state_id = start_state.state_id
347
+ initial_state.basis_state = start_state
348
+ initial_state.basis_auxref = start_state.auxref
349
+
350
+ # Start states are assigned their own type, so they can be identified later
351
+ initial_state.istate_type = InitialState.ISTATE_TYPE_START
352
+ weights.append(start_state.probability / segs_per_state)
353
+ initial_state.iter_used = 1
354
+ initial_states.append(initial_state)
355
+
356
+ if self.do_gen_istates:
357
+ futures = [
358
+ work_manager.submit(wm_ops.gen_istate, args=(initial_state.basis_state, initial_state))
359
+ for initial_state in initial_states
360
+ ]
361
+ for future in work_manager.as_completed(futures):
362
+ rbstate, ristate = future.get_result()
363
+ initial_states[ristate.state_id].pcoord = ristate.pcoord
364
+ else:
365
+ for initial_state in initial_states:
366
+ basis_state = initial_state.basis_state
367
+ initial_state.pcoord = basis_state.pcoord
368
+ initial_state.istate_status = InitialState.ISTATE_STATUS_PREPARED
369
+
370
+ for initial_state in initial_states:
371
+ log.debug('initial state created: {!r}'.format(initial_state))
372
+
373
+ # save list of initial states just generated
374
+ # some of these may not be used, depending on how WE shakes out
375
+ data_manager.update_initial_states(initial_states, n_iter=1)
376
+
377
+ if not suppress_we:
378
+ self.we_driver.populate_initial(initial_states, weights, system)
379
+ segments = list(self.we_driver.next_iter_segments)
380
+ binning = self.we_driver.next_iter_binning
381
+ else:
382
+ segments = list(self.we_driver.current_iter_segments)
383
+ binning = self.we_driver.final_binning
384
+
385
+ bin_occupancies = np.fromiter(map(len, binning), dtype=np.uint, count=self.we_driver.bin_mapper.nbins)
386
+ target_occupancies = np.require(self.we_driver.bin_target_counts, dtype=np.uint)
387
+
388
+ # total_bins/replicas defined here to remove target state bin from "active" bins
389
+ total_bins = len(bin_occupancies) - len(target_states)
390
+ total_replicas = int(sum(target_occupancies)) - int(self.we_driver.bin_target_counts[-1]) * len(target_states)
391
+
392
+ # Make sure we have
393
+ for segment in segments:
394
+ segment.n_iter = 1
395
+ segment.status = Segment.SEG_STATUS_PREPARED
396
+ assert segment.parent_id < 0
397
+ assert initial_states[segment.initial_state_id].iter_used == 1
398
+
399
+ data_manager.prepare_iteration(1, segments)
400
+ data_manager.update_initial_states(initial_states, n_iter=1)
401
+
402
+ if self.rc.verbose_mode:
403
+ pstatus('\nSegments generated:')
404
+ for segment in segments:
405
+ pstatus('{!r}'.format(segment))
406
+
407
+ pstatus(
408
+ '''
409
+ Total bins: {total_bins:d}
410
+ Initial replicas: {init_replicas:d} in {occ_bins:d} bins, total weight = {weight:g}
411
+ Total target replicas: {total_replicas:d}
412
+ '''.format(
413
+ total_bins=total_bins,
414
+ init_replicas=int(sum(bin_occupancies)),
415
+ occ_bins=len(bin_occupancies[bin_occupancies > 0]),
416
+ weight=float(sum(segment.weight for segment in segments)),
417
+ total_replicas=total_replicas,
418
+ )
419
+ )
420
+
421
+ total_prob = float(sum(segment.weight for segment in segments))
422
+ pstatus(f'1-prob: {1 - total_prob:.4e}')
423
+
424
+ target_counts = self.we_driver.bin_target_counts
425
+ # Do not include bins with target count zero (e.g. sinks, never-filled bins) in the (non)empty bins statistics
426
+ n_active_bins = len(target_counts[target_counts != 0])
427
+ seg_probs = np.fromiter(map(operator.attrgetter('weight'), segments), dtype=weight_dtype, count=len(segments))
428
+ norm = seg_probs.sum()
429
+
430
+ if not abs(1 - norm) < EPS * (len(segments) + n_active_bins):
431
+ pstatus("Normalization check failed at w_init, explicitly renormalizing")
432
+ for segment in segments:
433
+ segment.weight /= norm
434
+
435
+ # Send the segments over to the data manager to commit to disk
436
+ data_manager.current_iteration = 1
437
+
438
+ # Save BinMapper stuff for iteration 1
439
+ try:
440
+ pickled, hashed = self.we_driver.bin_mapper.pickle_and_hash()
441
+ except PickleError:
442
+ pickled = hashed = ''
443
+ data_manager.save_iter_binning(data_manager.current_iteration, hashed, pickled, target_counts)
444
+
445
+ # Report statistics
446
+ pstatus('Simulation prepared.')
447
+ self.segments = {segment.seg_id: segment for segment in segments}
448
+ self.report_bin_statistics(binning, target_states, save_summary=True)
449
+ data_manager.flush_backing()
450
+ data_manager.close_backing()
451
+
452
+ def prepare_iteration(self):
453
+ log.debug('beginning iteration {:d}'.format(self.n_iter))
454
+
455
+ # the WE driver needs a list of all target states for this iteration
456
+ # along with information about any new weights introduced (e.g. by recycling)
457
+ target_states = self.data_manager.get_target_states(self.n_iter)
458
+ new_weights = self.data_manager.get_new_weight_data(self.n_iter)
459
+
460
+ self.we_driver.new_iteration(target_states=target_states, new_weights=new_weights)
461
+
462
+ # Get basis states used in this iteration
463
+ self.current_iter_bstates = self.data_manager.get_basis_states(self.n_iter)
464
+
465
+ # Get the segments for this iteration and separate into complete and incomplete
466
+ if self.segments is None:
467
+ segments = self.segments = {segment.seg_id: segment for segment in self.data_manager.get_segments()}
468
+ log.debug('loaded {:d} segments'.format(len(segments)))
469
+ else:
470
+ segments = self.segments
471
+ log.debug('using {:d} pre-existing segments'.format(len(segments)))
472
+
473
+ completed_segments = self.completed_segments = {}
474
+ incomplete_segments = self.incomplete_segments = {}
475
+ for segment in segments.values():
476
+ if segment.status == Segment.SEG_STATUS_COMPLETE:
477
+ completed_segments[segment.seg_id] = segment
478
+ else:
479
+ incomplete_segments[segment.seg_id] = segment
480
+ log.debug('{:d} segments are complete; {:d} are incomplete'.format(len(completed_segments), len(incomplete_segments)))
481
+
482
+ if len(incomplete_segments) == len(segments):
483
+ # Starting a new iteration
484
+ self.rc.pstatus('Beginning iteration {:d}'.format(self.n_iter))
485
+ elif incomplete_segments:
486
+ self.rc.pstatus('Continuing iteration {:d}'.format(self.n_iter))
487
+ self.rc.pstatus(
488
+ '{:d} segments remain in iteration {:d} ({:d} total)'.format(len(incomplete_segments), self.n_iter, len(segments))
489
+ )
490
+
491
+ # Get the initial states active for this iteration (so that the propagator has them if necessary)
492
+ self.current_iter_istates = {
493
+ state.state_id: state for state in self.data_manager.get_segment_initial_states(list(segments.values()))
494
+ }
495
+ log.debug('This iteration uses {:d} initial states'.format(len(self.current_iter_istates)))
496
+
497
+ # Assign this iteration's segments' initial points to bins and report on bin population
498
+ initial_pcoords = self.system.new_pcoord_array(len(segments))
499
+ initial_binning = self.system.bin_mapper.construct_bins()
500
+ for iseg, segment in enumerate(segments.values()):
501
+ initial_pcoords[iseg] = segment.pcoord[0]
502
+ initial_assignments = self.system.bin_mapper.assign(initial_pcoords)
503
+ for segment, assignment in zip(iter(segments.values()), initial_assignments):
504
+ initial_binning[assignment].add(segment)
505
+ self.report_bin_statistics(initial_binning, [], save_summary=True)
506
+ del initial_pcoords, initial_binning
507
+
508
+ self.rc.pstatus('Waiting for segments to complete...')
509
+
510
+ # Let the WE driver assign completed segments
511
+ if completed_segments:
512
+ self.we_driver.assign(list(completed_segments.values()))
513
+
514
+ # load restart data
515
+ self.data_manager.prepare_segment_restarts(
516
+ incomplete_segments.values(), self.current_iter_bstates, self.current_iter_istates
517
+ )
518
+
519
+ # Get the basis states and initial states for the next iteration, necessary for doing on-the-fly recycling
520
+ self.next_iter_bstates = self.data_manager.get_basis_states(self.n_iter + 1)
521
+ self.next_iter_bstate_cprobs = np.add.accumulate([bstate.probability for bstate in self.next_iter_bstates])
522
+
523
+ self.we_driver.avail_initial_states = {
524
+ istate.state_id: istate for istate in self.data_manager.get_unused_initial_states(n_iter=self.n_iter + 1)
525
+ }
526
+ log.debug('{:d} unused initial states found'.format(len(self.we_driver.avail_initial_states)))
527
+
528
+ # Invoke callbacks
529
+ self.invoke_callbacks(self.prepare_iteration)
530
+
531
+ # dispatch and immediately wait on result for prep_iter
532
+ log.debug('dispatching propagator prep_iter to work manager')
533
+ self.work_manager.submit(wm_ops.prep_iter, args=(self.n_iter, segments)).get_result()
534
+
535
+ def finalize_iteration(self):
536
+ '''Clean up after an iteration and prepare for the next.'''
537
+ log.debug('finalizing iteration {:d}'.format(self.n_iter))
538
+
539
+ self.invoke_callbacks(self.finalize_iteration)
540
+
541
+ # dispatch and immediately wait on result for post_iter
542
+ log.debug('dispatching propagator post_iter to work manager')
543
+ self.work_manager.submit(wm_ops.post_iter, args=(self.n_iter, list(self.segments.values()))).get_result()
544
+
545
+ # Move existing segments into place as new segments
546
+ del self.segments
547
+ self.segments = {segment.seg_id: segment for segment in self.we_driver.next_iter_segments}
548
+
549
+ self.rc.pstatus("Iteration completed successfully")
550
+
551
+ def get_istate_futures(self):
552
+ '''Add ``n_states`` initial states to the internal list of initial states assigned to
553
+ recycled particles. Spare states are used if available, otherwise new states are created.
554
+ If created new initial states requires generation, then a set of futures is returned
555
+ representing work manager tasks corresponding to the necessary generation work.'''
556
+
557
+ n_recycled = self.we_driver.n_recycled_segs
558
+ n_istates_needed = self.we_driver.n_istates_needed
559
+
560
+ log.debug('{:d} unused initial states available'.format(len(self.we_driver.avail_initial_states)))
561
+ log.debug('{:d} new initial states required for recycling {:d} walkers'.format(n_istates_needed, n_recycled))
562
+
563
+ futures = set()
564
+ updated_states = []
565
+ for _i in range(n_istates_needed):
566
+ # Select a basis state according to its weight
567
+ ibstate = np.digitize([self.rng.random()], self.next_iter_bstate_cprobs)
568
+ basis_state = self.next_iter_bstates[ibstate[0]]
569
+ initial_state = self.data_manager.create_initial_states(1, n_iter=self.n_iter + 1)[0]
570
+ initial_state.iter_created = self.n_iter
571
+ initial_state.basis_state_id = basis_state.state_id
572
+ initial_state.istate_status = InitialState.ISTATE_STATUS_PENDING
573
+
574
+ if self.do_gen_istates:
575
+ log.debug('generating new initial state from basis state {!r}'.format(basis_state))
576
+ initial_state.istate_type = InitialState.ISTATE_TYPE_GENERATED
577
+ futures.add(self.work_manager.submit(wm_ops.gen_istate, args=(basis_state, initial_state)))
578
+ else:
579
+ log.debug('using basis state {!r} directly'.format(basis_state))
580
+ initial_state.istate_type = InitialState.ISTATE_TYPE_BASIS
581
+ initial_state.pcoord = basis_state.pcoord.copy()
582
+ initial_state.istate_status = InitialState.ISTATE_STATUS_PREPARED
583
+ self.we_driver.avail_initial_states[initial_state.state_id] = initial_state
584
+ updated_states.append(initial_state)
585
+ self.data_manager.update_initial_states(updated_states, n_iter=self.n_iter + 1)
586
+ return futures
587
+
588
+ def propagate(self):
589
+ segments = list(self.incomplete_segments.values())
590
+ log.debug('iteration {:d}: propagating {:d} segments'.format(self.n_iter, len(segments)))
591
+
592
+ # all futures dispatched for this iteration
593
+ futures = set()
594
+ segment_futures = set()
595
+
596
+ # Immediately dispatch any necessary initial state generation
597
+ istate_gen_futures = self.get_istate_futures()
598
+ futures.update(istate_gen_futures)
599
+
600
+ # Dispatch propagation tasks using work manager
601
+ for segment_block in grouper(self.propagator_block_size, segments):
602
+ segment_block = [_f for _f in segment_block if _f]
603
+ pbstates, pistates = westpa.core.states.pare_basis_initial_states(
604
+ self.current_iter_bstates, list(self.current_iter_istates.values()), segment_block
605
+ )
606
+ future = self.work_manager.submit(wm_ops.propagate, args=(pbstates, pistates, segment_block))
607
+ futures.add(future)
608
+ segment_futures.add(future)
609
+
610
+ while futures and self.work_manager.running:
611
+ future = self.work_manager.wait_any(futures)
612
+ futures.remove(future)
613
+
614
+ if future in segment_futures:
615
+ segment_futures.remove(future)
616
+ incoming = future.get_result()
617
+ self.n_propagated += 1
618
+
619
+ self.segments.update({segment.seg_id: segment for segment in incoming})
620
+ self.completed_segments.update({segment.seg_id: segment for segment in incoming})
621
+
622
+ self.we_driver.assign(incoming)
623
+ new_istate_futures = self.get_istate_futures()
624
+ istate_gen_futures.update(new_istate_futures)
625
+ futures.update(new_istate_futures)
626
+
627
+ with self.data_manager.expiring_flushing_lock():
628
+ self.data_manager.update_segments(self.n_iter, incoming)
629
+
630
+ elif future in istate_gen_futures:
631
+ istate_gen_futures.remove(future)
632
+ _basis_state, initial_state = future.get_result()
633
+ log.debug('received newly-prepared initial state {!r}'.format(initial_state))
634
+ initial_state.istate_status = InitialState.ISTATE_STATUS_PREPARED
635
+ with self.data_manager.expiring_flushing_lock():
636
+ self.data_manager.update_initial_states([initial_state], n_iter=self.n_iter + 1)
637
+ self.we_driver.avail_initial_states[initial_state.state_id] = initial_state
638
+ else:
639
+ log.error('unknown future {!r} received from work manager'.format(future))
640
+ raise AssertionError('untracked future {!r}'.format(future))
641
+
642
+ log.debug('done with propagation')
643
+ self.save_bin_data()
644
+ self.data_manager.flush_backing()
645
+
646
+ def save_bin_data(self):
647
+ '''Calculate and write flux and transition count matrices to HDF5. Population and rate matrices
648
+ are likely useless at the single-tau level and are no longer written.'''
649
+ # save_bin_data(self, populations, n_trans, fluxes, rates, n_iter=None)
650
+
651
+ if self.save_transition_matrices:
652
+ with self.data_manager.expiring_flushing_lock():
653
+ iter_group = self.data_manager.get_iter_group(self.n_iter)
654
+ for key in ['bin_ntrans', 'bin_fluxes']:
655
+ try:
656
+ del iter_group[key]
657
+ except KeyError:
658
+ pass
659
+ iter_group['bin_ntrans'] = self.we_driver.transition_matrix
660
+ iter_group['bin_fluxes'] = self.we_driver.flux_matrix
661
+
662
+ def check_propagation(self):
663
+ '''Check for failures in propagation or initial state generation, and raise an exception
664
+ if any are found.'''
665
+
666
+ failed_segments = [segment for segment in self.segments.values() if segment.status != Segment.SEG_STATUS_COMPLETE]
667
+
668
+ if failed_segments:
669
+ failed_ids = ' \n'.join(str(segment.seg_id) for segment in failed_segments)
670
+ log.error('propagation failed for {:d} segment(s):\n{}'.format(len(failed_segments), failed_ids))
671
+ raise PropagationError('propagation failed for {:d} segments'.format(len(failed_segments)))
672
+ else:
673
+ log.debug('propagation complete for iteration {:d}'.format(self.n_iter))
674
+
675
+ failed_istates = [
676
+ istate
677
+ for istate in self.we_driver.used_initial_states.values()
678
+ if istate.istate_status != InitialState.ISTATE_STATUS_PREPARED
679
+ ]
680
+ log.debug('{!r}'.format(failed_istates))
681
+ if failed_istates:
682
+ failed_ids = ' \n'.join(str(istate.state_id) for istate in failed_istates)
683
+ log.error('initial state generation failed for {:d} states:\n{}'.format(len(failed_istates), failed_ids))
684
+ raise PropagationError('initial state generation failed for {:d} states'.format(len(failed_istates)))
685
+ else:
686
+ log.debug('initial state generation complete for iteration {:d}'.format(self.n_iter))
687
+
688
+ def run_we(self):
689
+ '''Run the weighted ensemble algorithm based on the binning in self.final_bins and
690
+ the recycled particles in self.to_recycle, creating and committing the next iteration's
691
+ segments to storage as well.'''
692
+
693
+ # The WE driver now does almost everything; we just have to record the
694
+ # mapper used for binning this iteration, and update initial states
695
+ # that have been used
696
+
697
+ try:
698
+ pickled, hashed = self.we_driver.bin_mapper.pickle_and_hash()
699
+ except PickleError:
700
+ pickled = hashed = ''
701
+
702
+ self.bin_mapper_hash = hashed
703
+ self.we_driver.construct_next()
704
+
705
+ if self.we_driver.used_initial_states:
706
+ for initial_state in self.we_driver.used_initial_states.values():
707
+ initial_state.iter_used = self.n_iter + 1
708
+ self.data_manager.update_initial_states(list(self.we_driver.used_initial_states.values()))
709
+
710
+ self.data_manager.update_segments(self.n_iter, list(self.segments.values()))
711
+
712
+ self.data_manager.require_iter_group(self.n_iter + 1)
713
+ self.data_manager.save_iter_binning(self.n_iter + 1, hashed, pickled, self.we_driver.bin_target_counts)
714
+
715
+ # Report on recycling
716
+ recycling_events = {}
717
+ for nw in self.we_driver.new_weights:
718
+ try:
719
+ recycling_events[nw.target_state_id].append(nw.weight)
720
+ except KeyError:
721
+ recycling_events[nw.target_state_id] = list([nw.weight])
722
+
723
+ tstates_by_id = {state.state_id: state for state in self.we_driver.target_states.values()}
724
+
725
+ for tstate_id, weights in recycling_events.items():
726
+ tstate = tstates_by_id[tstate_id]
727
+ self.rc.pstatus(
728
+ 'Recycled {:g} probability ({:d} walkers) from target state {!r}'.format(sum(weights), len(weights), tstate.label)
729
+ )
730
+
731
+ def prepare_new_iteration(self):
732
+ '''Commit data for the coming iteration to the HDF5 file.'''
733
+ self.invoke_callbacks(self.prepare_new_iteration)
734
+
735
+ if self.rc.debug_mode:
736
+ self.rc.pstatus('\nSegments generated:')
737
+ for segment in self.we_driver.next_iter_segments:
738
+ self.rc.pstatus('{!r} pcoord[0]={!r}'.format(segment, segment.pcoord[0]))
739
+
740
+ self.data_manager.prepare_iteration(self.n_iter + 1, list(self.we_driver.next_iter_segments))
741
+ self.data_manager.save_new_weight_data(self.n_iter + 1, self.we_driver.new_weights)
742
+
743
+ def run(self):
744
+ run_starttime = time.time()
745
+ max_walltime = self.max_run_walltime
746
+ if max_walltime:
747
+ run_killtime = run_starttime + max_walltime
748
+ self.rc.pstatus('Maximum wallclock time: %s' % timedelta(seconds=max_walltime or 0))
749
+ else:
750
+ run_killtime = None
751
+
752
+ self.n_iter = self.data_manager.current_iteration
753
+ max_iter = self.max_total_iterations or self.n_iter + 1
754
+
755
+ iter_elapsed = 0
756
+ while self.n_iter <= max_iter:
757
+ if max_walltime and time.time() + 1.1 * iter_elapsed >= run_killtime:
758
+ self.rc.pstatus('Iteration {:d} would require more than the allotted time. Ending run.'.format(self.n_iter))
759
+ return
760
+
761
+ try:
762
+ iter_start_time = time.time()
763
+
764
+ self.rc.pstatus('\n%s' % time.asctime())
765
+ self.rc.pstatus('Iteration %d (%d requested)' % (self.n_iter, max_iter))
766
+
767
+ self.prepare_iteration()
768
+ self.rc.pflush()
769
+
770
+ self.pre_propagation()
771
+ self.propagate()
772
+ self.rc.pflush()
773
+ self.check_propagation()
774
+ self.rc.pflush()
775
+ self.post_propagation()
776
+
777
+ cputime = sum(segment.cputime for segment in self.segments.values())
778
+
779
+ self.rc.pflush()
780
+ self.pre_we()
781
+ self.run_we()
782
+ self.post_we()
783
+ self.rc.pflush()
784
+
785
+ self.prepare_new_iteration()
786
+
787
+ self.finalize_iteration()
788
+
789
+ iter_elapsed = time.time() - iter_start_time
790
+ iter_summary = self.data_manager.get_iter_summary()
791
+ iter_summary['walltime'] += iter_elapsed
792
+ iter_summary['cputime'] = cputime
793
+ self.data_manager.update_iter_summary(iter_summary)
794
+
795
+ self.n_iter += 1
796
+ self.data_manager.current_iteration += 1
797
+
798
+ try:
799
+ # This may give NaN if starting a truncated simulation
800
+ walltime = timedelta(seconds=float(iter_summary['walltime']))
801
+ except ValueError:
802
+ walltime = 0.0
803
+
804
+ try:
805
+ cputime = timedelta(seconds=float(iter_summary['cputime']))
806
+ except ValueError:
807
+ cputime = 0.0
808
+
809
+ self.rc.pstatus('Iteration wallclock: {0!s}, cputime: {1!s}\n'.format(walltime, cputime))
810
+ self.rc.pflush()
811
+ finally:
812
+ self.data_manager.flush_backing()
813
+
814
+ self.rc.pstatus('\n%s' % time.asctime())
815
+ self.rc.pstatus('WEST run complete.')
816
+
817
+ def prepare_run(self):
818
+ '''Prepare a new run.'''
819
+ self.data_manager.prepare_run()
820
+ self.system.prepare_run()
821
+ self.invoke_callbacks(self.prepare_run)
822
+
823
+ def finalize_run(self):
824
+ '''Perform cleanup at the normal end of a run'''
825
+ self.invoke_callbacks(self.finalize_run)
826
+ self.system.finalize_run()
827
+ self.data_manager.finalize_run()
828
+
829
+ def pre_propagation(self):
830
+ self.invoke_callbacks(self.pre_propagation)
831
+
832
+ def post_propagation(self):
833
+ self.invoke_callbacks(self.post_propagation)
834
+
835
+ def pre_we(self):
836
+ self.invoke_callbacks(self.pre_we)
837
+
838
+ def post_we(self):
839
+ self.invoke_callbacks(self.post_we)