efbfbf: upgrade to Bugs Everywhere Directory v1.5
[sawsim.git] / pysawsim / sawsim.py
1 # Copyright (C) 2010  W. Trevor King <wking@drexel.edu>
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
15 #
16 # The author may be contacted at <wking@drexel.edu> on the Internet, or
17 # write to Trevor King, Drudge's University, Physics Dept., 3141 Chestnut St.,
18 # Philadelphia PA 19104, USA.
19
20
21 """`Seminar` for running `sawsim` and parsing the results.
22 """
23
24 from __future__ import with_statement
25
26 try:
27     from collections import namedtuple
28 except ImportError:  # work around Python < 2.6
29     from ._collections import namedtuple
30 from distutils.spawn import find_executable
31 import hashlib
32 from optparse import Option
33 import os
34 import os.path
35 from random import shuffle
36 import shutil
37 from uuid import uuid4
38
39 from . import __version__
40 from . import PYSAWSIM_LOG_LEVEL_MSG as _PYSAWSIM_LOG_LEVEL_MSG
41 from .manager import MANAGERS, get_manager, InvokeJob
42
43
44 _multiprocess_can_split_ = True
45 """Allow nosetests to split tests between processes.
46 """
47
48 SAWSIM = find_executable('sawsim')
49 if SAWSIM == None:
50     SAWSIM = os.path.join('bin', 'sawsim')
51
52 CACHE_DIR = os.path.expanduser(os.path.join('~', '.sawsim-cache'))
53 DEFAULT_PARAM_STRING = (
54     '-s cantilever,hooke,0.05 -N1 '
55     '-s folded,null -N8 '
56     "-s 'unfolded,wlc,{0.39e-9,28e-9}' "
57     "-k 'folded,unfolded,bell,{3.3e-4,0.25e-9}' "
58     '-q folded -v 1e-6')
59
60
61 # `Event` instances represent domain state transitions.
62 Event = namedtuple(
63     typename='Event',
64     field_names=['force', 'initial_state', 'final_state'])
65
66
67 class SawsimRunner (object):
68     """
69     >>> m = get_manager()()
70     >>> sr = SawsimRunner(manager=m)
71     >>> for run in sr(param_string=DEFAULT_PARAM_STRING, N=2):
72     ...     print 'New run'
73     ...     for i,event in enumerate(run):
74     ...         print i, event  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
75     New run
76     0 Event(force=..., initial_state='folded', final_state='unfolded')
77     1 Event(force=..., initial_state='folded', final_state='unfolded')
78     2 Event(force=..., initial_state='folded', final_state='unfolded')
79     3 Event(force=..., initial_state='folded', final_state='unfolded')
80     4 Event(force=..., initial_state='folded', final_state='unfolded')
81     5 Event(force=..., initial_state='folded', final_state='unfolded')
82     6 Event(force=..., initial_state='folded', final_state='unfolded')
83     7 Event(force=..., initial_state='folded', final_state='unfolded')
84     New run
85     0 Event(force=..., initial_state='folded', final_state='unfolded')
86     1 Event(force=..., initial_state='folded', final_state='unfolded')
87     2 Event(force=..., initial_state='folded', final_state='unfolded')
88     3 Event(force=..., initial_state='folded', final_state='unfolded')
89     4 Event(force=..., initial_state='folded', final_state='unfolded')
90     5 Event(force=..., initial_state='folded', final_state='unfolded')
91     6 Event(force=..., initial_state='folded', final_state='unfolded')
92     7 Event(force=..., initial_state='folded', final_state='unfolded')
93     >>> m.teardown()
94     """
95
96     optparse_options = [
97         Option('-s','--sawsim', dest='sawsim',
98                metavar='PATH',
99                help='Set sawsim binary (%default).',
100                default=SAWSIM),
101         Option('-p','--params', dest='param_string',
102                metavar='PARAMS',
103                help='Initial params for fitting (%default).',
104                default=DEFAULT_PARAM_STRING),
105         Option('-N', '--number-of-runs', dest='N',
106                metavar='INT', type='int',
107                help='Number of sawsim runs at each point in parameter space (%default).',
108                default=400),
109         Option('-m', '--manager', dest='manager',
110                metavar='STRING',
111                help='Job manager name (one of %s) (default: auto-select).'
112                % (', '.join(MANAGERS)),
113                default=None),
114         Option('-C','--use-cache', dest='use_cache',
115                help='Use cached simulations if they exist (vs. running new simulations) (%default)',
116                default=False, action='store_true'),
117         Option('--clean-cache', dest='clean_cache',
118                help='Remove previously cached simulations if they exist (%default)',
119                default=False, action='store_true'),
120         Option('-d','--cache-dir', dest='cache_dir',
121                metavar='STRING',
122                help='Cache directory for sawsim unfolding forces (%default).',
123                default=CACHE_DIR),
124     ]
125
126     def __init__(self, sawsim=None, cache_dir=None,
127                  use_cache=False, clean_cache=False,
128                  manager=None):
129         if sawsim == None:
130             sawsim = SAWSIM
131         self._sawsim = sawsim
132         if cache_dir == None:
133             cache_dir = CACHE_DIR
134         self._cache_dir = cache_dir
135         self._use_cache = use_cache
136         self._clean_cache = clean_cache
137         self._manager = manager
138         self._local_manager = False
139         self._headline = None
140
141     def initialize_from_options(self, options):
142         self._sawsim = options.sawsim
143         self._cache_dir = options.cache_dir
144         self._use_cache = options.use_cache
145         self._clean_cache = options.clean_cache
146         self._manager = get_manager(options.manager)()
147         self._local_manager = True
148         call_params = {}
149         for param in ['param_string', 'N']:
150             try:
151                 call_params[param] = getattr(options, param)
152             except AttributeError:
153                 pass
154         return call_params
155
156     def teardown(self):
157         if self._local_manager == True:
158             self._manager.teardown()
159
160     def __call__(self, param_string, N):
161         """Run `N` simulations and yield `Event` generators for each run.
162
163         Use the `JobManager` instance `manager` for asynchronous job
164         execution.
165
166         If `_use_cache` is `True`, store an array of unfolding forces
167         in `cache_dir` for each simulated pull.  If the cached forces
168         are already present for `param_string`, do not redo the
169         simulation unless `_clean_cache` is `True`.
170         """
171         count = N
172         if self._use_cache == True:
173             d = self._param_cache_dir(param_string)
174             if os.path.exists(d):
175                 if self._clean_cache == True:
176                     shutil.rmtree(d)
177                     self._make_cache(param_string)
178                 else:
179                     for data in self._load_cached_data(param_string):
180                         yield data
181                         count -= 1
182                         if count == 0:
183                             return
184             else:
185                 self._make_cache(param_string)
186
187         jobs = {}
188         for i in range(count):
189             jobs[i] = self._manager.async_invoke(InvokeJob(
190                     target='%s %s' % (self._sawsim, param_string)))
191         complete_jobs = self._manager.wait(
192             [job.id for job in jobs.itervalues()])
193         for i,job in jobs.iteritems():
194             j = complete_jobs[job.id]
195             assert j.status == 0, j.data['error']
196             if self._use_cache == True:
197                 self._cache_run(d, j.data['stdout'])
198             yield self.parse(j.data['stdout'])
199         del(jobs)
200         del(complete_jobs)
201
202     def _param_cache_dir(self, param_string):
203         """
204         >>> s = SawsimRunner()
205         >>> s._param_cache_dir(DEFAULT_PARAM_STRING)  # doctest: +ELLIPSIS
206         '/.../.sawsim-cache/...'
207         """
208         return os.path.join(
209             self._cache_dir, hashlib.sha256(param_string).hexdigest())
210
211     def _make_cache(self, param_string):
212         cache_dir = self._param_cache_dir(param_string)
213         os.makedirs(cache_dir)
214         with open(os.path.join(cache_dir, 'param_string'), 'w') as f:
215             f.write('# version: %s\n%s\n' % (__version__, param_string))
216
217     def _load_cached_data(self, param_string):
218         pcd = self._param_cache_dir(param_string)
219         filenames = os.listdir(pcd)
220         shuffle(filenames)
221         for filename in filenames:
222             if not filename.endswith('.dat'):
223                 continue
224             with open(os.path.join(pcd, filename), 'r') as f:
225                 yield self.parse(f.read())
226
227     def _cache_run(self, cache_dir, stdout):
228         simulation_path = os.path.join(cache_dir, '%s.dat' % uuid4())
229         with open(simulation_path, 'w') as f:
230             f.write(stdout)
231
232     def parse(self, text):
233         """Parse the output of a `sawsim` run.
234     
235         >>> text = '''#Force (N)\\tinitial state\\tFinal state
236         ... 2.90301e-10\\tfolded\\tunfolded
237         ... 2.83948e-10\\tfolded\\tunfolded
238         ... 2.83674e-10\\tfolded\\tunfolded
239         ... 2.48384e-10\\tfolded\\tunfolded
240         ... 2.43033e-10\\tfolded\\tunfolded
241         ... 2.77589e-10\\tfolded\\tunfolded
242         ... 2.85343e-10\\tfolded\\tunfolded
243         ... 2.67796e-10\\tfolded\\tunfolded
244         ... '''
245         >>> sr = SawsimRunner()
246         >>> events = list(sr.parse(text))
247         >>> len(events)
248         8
249         >>> events[0]  # doctest: +ELLIPSIS
250         Event(force=2.9030...e-10, initial_state='folded', final_state='unfolded')
251         >>> sr._headline
252         ['Force (N)', 'initial state', 'Final state']
253         """
254         for line in text.splitlines():
255             line = line.strip()
256             if len(line) == 0:
257                 continue
258             elif line.startswith('#'):
259                 if self._headline == None:
260                     self._headline = line[len('#'):].split('\t')
261                 continue
262             fields = line.split('\t')
263             if len(fields) != 3:
264                 raise ValueError(fields)
265             force,initial_state,final_state = fields
266             yield Event(float(force), initial_state, final_state)
267
268
269 def main(argv=None):
270     """
271     >>> try:
272     ...     main(['--help'])
273     ... except SystemExit, e:
274     ...     pass  # doctest: +ELLIPSIS, +REPORT_UDIFF
275     Usage: ... [options]
276     <BLANKLINE>
277     Options:
278       -h, --help            show this help message and exit
279       -s PATH, --sawsim=PATH
280                             Set sawsim binary (...sawsim).
281       ...
282     >>> print e
283     0
284     >>> main(['-N', '2'])
285     ... # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
286     #Force (N)  Initial state  Final state
287     ...         folded         unfolded
288     ...         folded         unfolded
289     ...         folded         unfolded
290     ...         folded         unfolded
291     ...
292     ...         folded         unfolded
293     """
294     from optparse import OptionParser
295     import sys
296
297     if argv == None:
298         argv = sys.argv[1:]
299
300     sr = SawsimRunner()
301
302     usage = '%prog [options]'
303     epilog = '\n'.join([
304             'Python wrapper around `sawsim`.  Distribute `N` runs using',
305             'one of the possible job "managers".  Also supports caching',
306             'results to speed future runs.',
307             _PYSAWSIM_LOG_LEVEL_MSG,
308             ])
309     parser = OptionParser(usage, epilog=epilog)
310     parser.format_epilog = lambda formatter: epilog+'\n'
311     for option in sr.optparse_options:
312         parser.add_option(option)
313     
314     options,args = parser.parse_args(argv)
315
316     try:
317         sr_call_params = sr.initialize_from_options(options)
318     
319         first_run = True
320         for run in sr(**sr_call_params):
321             if first_run == True:
322                 first_run = False
323                 run = list(run)  # force iterator evaluation
324                 if sr._headline != None:
325                     print '#%s' % '\t'.join(sr._headline)
326             for event in run:
327                 print '\t'.join([str(x) for x in event])
328     finally:
329         sr.teardown()