1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
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.
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.
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/>.
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.
21 """`Seminar` for running `sawsim` and parsing the results.
24 from __future__ import with_statement
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
32 from optparse import Option
35 from random import shuffle
37 from uuid import uuid4
39 from . import __version__
40 from . import PYSAWSIM_LOG_LEVEL_MSG as _PYSAWSIM_LOG_LEVEL_MSG
41 from .manager import MANAGERS, get_manager, InvokeJob
44 _multiprocess_can_split_ = True
45 """Allow nosetests to split tests between processes.
48 SAWSIM = find_executable('sawsim')
50 SAWSIM = os.path.join('bin', 'sawsim')
52 CACHE_DIR = os.path.expanduser(os.path.join('~', '.sawsim-cache'))
53 DEFAULT_PARAM_STRING = (
54 '-s cantilever,hooke,0.05 -N1 '
56 "-s 'unfolded,wlc,{0.39e-9,28e-9}' "
57 "-k 'folded,unfolded,bell,{3.3e-4,0.25e-9}' "
61 # `Event` instances represent domain state transitions.
64 field_names=['force', 'initial_state', 'final_state'])
67 class SawsimRunner (object):
69 >>> m = get_manager()()
70 >>> sr = SawsimRunner(manager=m)
71 >>> for run in sr(param_string=DEFAULT_PARAM_STRING, N=2):
73 ... for i,event in enumerate(run):
74 ... print i, event # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
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')
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')
97 Option('-s','--sawsim', dest='sawsim',
99 help='Set sawsim binary (%default).',
101 Option('-p','--params', dest='param_string',
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).',
109 Option('-m', '--manager', dest='manager',
111 help='Job manager name (one of %s) (default: auto-select).'
112 % (', '.join(MANAGERS)),
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',
122 help='Cache directory for sawsim unfolding forces (%default).',
126 def __init__(self, sawsim=None, cache_dir=None,
127 use_cache=False, clean_cache=False,
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
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
149 for param in ['param_string', 'N']:
151 call_params[param] = getattr(options, param)
152 except AttributeError:
157 if self._local_manager == True:
158 self._manager.teardown()
160 def __call__(self, param_string, N):
161 """Run `N` simulations and yield `Event` generators for each run.
163 Use the `JobManager` instance `manager` for asynchronous job
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`.
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:
177 self._make_cache(param_string)
179 for data in self._load_cached_data(param_string):
185 self._make_cache(param_string)
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'])
202 def _param_cache_dir(self, param_string):
204 >>> s = SawsimRunner()
205 >>> s._param_cache_dir(DEFAULT_PARAM_STRING) # doctest: +ELLIPSIS
206 '/.../.sawsim-cache/...'
209 self._cache_dir, hashlib.sha256(param_string).hexdigest())
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))
217 def _load_cached_data(self, param_string):
218 pcd = self._param_cache_dir(param_string)
219 filenames = os.listdir(pcd)
221 for filename in filenames:
222 if not filename.endswith('.dat'):
224 with open(os.path.join(pcd, filename), 'r') as f:
225 yield self.parse(f.read())
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:
232 def parse(self, text):
233 """Parse the output of a `sawsim` run.
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
245 >>> sr = SawsimRunner()
246 >>> events = list(sr.parse(text))
249 >>> events[0] # doctest: +ELLIPSIS
250 Event(force=2.9030...e-10, initial_state='folded', final_state='unfolded')
252 ['Force (N)', 'initial state', 'Final state']
254 for line in text.splitlines():
258 elif line.startswith('#'):
259 if self._headline == None:
260 self._headline = line[len('#'):].split('\t')
262 fields = line.split('\t')
264 raise ValueError(fields)
265 force,initial_state,final_state = fields
266 yield Event(float(force), initial_state, final_state)
273 ... except SystemExit, e:
274 ... pass # doctest: +ELLIPSIS, +REPORT_UDIFF
278 -h, --help show this help message and exit
279 -s PATH, --sawsim=PATH
280 Set sawsim binary (...sawsim).
284 >>> main(['-N', '2'])
285 ... # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
286 #Force (N) Initial state Final state
294 from optparse import OptionParser
302 usage = '%prog [options]'
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,
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)
314 options,args = parser.parse_args(argv)
317 sr_call_params = sr.initialize_from_options(options)
320 for run in sr(**sr_call_params):
321 if first_run == True:
323 run = list(run) # force iterator evaluation
324 if sr._headline != None:
325 print '#%s' % '\t'.join(sr._headline)
327 print '\t'.join([str(x) for x in event])