1 from __future__
import print_function
, division
, absolute_import
11 from collections
import OrderedDict
13 from physical_validation
import integrator
, ensemble
, kinetic_energy
14 from physical_validation
.util
.gromacs_interface
import GromacsInterface
15 from physical_validation
.data
.gromacs_parser
import GromacsParser
18 def mkdir_bk(dirname
, verbose
=False, nobackup
=False):
19 if os
.path
.exists(dirname
) and nobackup
:
20 shutil
.rmtree(dirname
)
21 elif os
.path
.exists(dirname
):
23 print('Directory ' + dirname
+ ' exists. Backing it up.')
24 basename
= os
.path
.basename(dirname
)
26 bk_dir
= '#' + basename
+ '_' + str(n
) + '#'
27 while os
.path
.exists(dirname
.replace(basename
, bk_dir
)):
29 bk_dir
= '#' + basename
+ '_' + str(n
) + '#'
30 os
.rename(dirname
, dirname
.replace(basename
, bk_dir
))
34 def file_bk(filename
, verbose
=False):
35 if os
.path
.exists(filename
):
37 print('File ' + filename
+ ' exists. Backing it up.')
38 basename
= os
.path
.basename(filename
)
40 bk_file
= '#' + basename
+ '_' + str(n
) + '#'
41 while os
.path
.exists(filename
.replace(basename
, bk_file
)):
43 bk_file
= '#' + basename
+ '_' + str(n
) + '#'
44 os
.rename(filename
, filename
.replace(basename
, bk_file
))
47 def basic_run_cmds(directory
, grompp_args
=None, mdrun_args
=None):
48 grompp
= '$GROMPPCMD -f system.mdp -p system.top -c system.gro -o system.tpr'
50 for arg
in grompp_args
:
52 mdrun
= '$MDRUNCMD -s system.tpr -deffnm system'
54 for arg
in mdrun_args
:
59 grompp
+ ' && ' + mdrun
,
67 raise NotImplementedError
70 def prepare_parser(cls
, input_dir
, target_dir
, system_name
, nobackup
, args
):
71 raise NotImplementedError
74 def analyze_parser(cls
, gmx_parser
, system_dir
, system_name
, base_data
, verbosity
, args
):
75 raise NotImplementedError
78 def prepare(cls
, input_dir
, target_dir
, system_name
, nobackup
):
79 raise NotImplementedError
82 def analyze(cls
, gmx_parser
, system_dir
, system_name
, base_data
, verbosity
):
83 raise NotImplementedError
86 class IntegratorTest(Test
):
89 parser
= argparse
.ArgumentParser(
90 description
='Tests the integrator convergence.',
91 formatter_class
=argparse
.RawTextHelpFormatter
,
94 parser
.add_argument('-n', '--n_iterations', type=int, default
=2,
95 help='The number of different time steps tested.')
96 parser
.add_argument('-t', '--tolerance', type=float, default
=0.1,
97 help=('The relative tolerance accepted. Default: 0.1.\n'
98 'Example: By default, the test passes if\n'
99 ' 3.6 <= dE(2*dt)/dE(dt) <= 4.4.')
105 def prepare_parser(cls
, input_dir
, target_dir
, system_name
, nobackup
, args
):
106 args
= cls
.parser().parse_args(args
)
107 return cls
.prepare(input_dir
, target_dir
, system_name
, nobackup
,
108 n_iterations
=args
.n_iterations
)
111 def analyze_parser(cls
, gmx_parser
, system_dir
, system_name
, base_data
, verbosity
, args
):
112 args
= cls
.parser().parse_args(args
)
113 return cls
.analyze(gmx_parser
, system_dir
, system_name
, base_data
, verbosity
,
114 tolerance
=args
.tolerance
, n_iterations
=args
.n_iterations
)
117 def prepare(cls
, input_dir
, target_dir
, system_name
, nobackup
, n_iterations
=None):
119 if n_iterations
is None:
120 n_iterations
= cls
.parser().get_default('n_iterations')
122 options
= GromacsInterface
.read_mdp(os
.path
.join(input_dir
, 'system.mdp'))
124 # Check if options relevant to integrator tests are set
125 # Set to standard if not - I am not happy about hardcoding this here...
126 # For future: Find a way to obtain standards from GROMACS build
127 if 'nsteps' not in options
:
128 raise ValueError('System ' + system_name
+ ' has no \'nsteps\' defined in mdp file. ' +
129 'Running integrator test does not make sense.')
130 if 'dt' not in options
:
131 options
['dt'] = str(0.001)
132 if 'nstcalcenergy' not in options
:
133 options
['nstcalcenergy'] = str(100)
134 if 'nstenergy' not in options
:
135 options
['nstenergy'] = str(1000)
136 # if 'nstlist' not in options:
137 # options['nstlist'] = str(10)
139 # Prepare folders for iterations
141 for n
in range(1, n_iterations
+1):
142 current_dir
= os
.path
.join(target_dir
, 'integrator_' + str(n
))
143 mkdir_bk(current_dir
, nobackup
=nobackup
)
144 # update timesteps, length and intervals
145 options
['dt'] = str(float(options
['dt'])*0.5)
146 for key
in ['nsteps', 'nstcalcenergy', 'nstenergy']: # , 'nstlist']:
147 options
[key
] = str(int(options
[key
])*2)
149 GromacsInterface
.write_mdp(options
, os
.path
.join(current_dir
, 'system.mdp'))
150 # copy topology and starting structure
151 for suffix
in ['gro', 'top']:
152 shutil
.copy2(os
.path
.join(input_dir
, 'system.' + suffix
),
154 directories
.append(current_dir
)
159 def analyze(cls
, gmx_parser
, system_dir
, system_name
, base_data
, verbosity
,
160 tolerance
=None, n_iterations
=None):
162 if tolerance
is None:
163 tolerance
= cls
.parser().get_default('tolerance')
164 if n_iterations
is None:
165 n_iterations
= cls
.parser().get_default('n_iterations')
167 # list of results from simulations at different time steps
171 if base_data
['reduced'] is None:
172 current_dir
= os
.path
.join(system_dir
, 'base')
173 base_data
['reduced'] = gmx_parser
.get_simulation_data(
174 mdp
=os
.path
.join(current_dir
, 'mdout.mdp'),
175 top
=os
.path
.join(current_dir
, 'system.top'),
176 edr
=os
.path
.join(current_dir
, 'system.edr'),
177 gro
=os
.path
.join(current_dir
, 'system.gro')
179 results
.append(base_data
['reduced'])
181 # append data at reduced dt
182 for n
in range(1, n_iterations
+1):
183 current_dir
= os
.path
.join(system_dir
, 'integrator_' + str(n
))
184 results
.append(gmx_parser
.get_simulation_data(
185 mdp
=os
.path
.join(current_dir
, 'mdout.mdp'),
186 top
=os
.path
.join(current_dir
, 'system.top'),
187 edr
=os
.path
.join(current_dir
, 'system.edr'),
188 gro
=os
.path
.join(current_dir
, 'system.gro')
192 deviation
= integrator
.convergence(simulations
=results
,
193 verbose
=(verbosity
> 0),
194 convergence_test
='max_deviation')
196 result
= deviation
<= tolerance
198 message
= 'IntegratorTest PASSED (max deviation = {:.2g}, tolerance = {:.2g})'.format(
202 message
= 'IntegratorTest FAILED (max deviation = {:.2g}, tolerance = {:.2g})'.format(
206 return {'test': result
,
208 'tolerance': tolerance
,
212 class EnsembleTest(Test
):
215 parser
= argparse
.ArgumentParser(
216 description
='Tests the validity of the potential energy and / or volume ensemble.',
217 formatter_class
=argparse
.RawTextHelpFormatter
,
220 parser
.add_argument('--dtemp', nargs
='*', type=float, default
=None,
221 help='Ensemble validations are made between two simulations at\n'
222 'different state points.\n'
223 'dtemp determines the temperature difference between base\n'
224 'simulation and the additional point. If more than one\n'
225 'value is given, several tests will be performed.\n'
226 'By also giving dpress, both temperature and pressure can\n'
227 'be displaced simultaneously.')
228 parser
.add_argument('--dpress', nargs
='*', type=float, default
=None,
229 help='Ensemble validations are made between two simulations at\n'
230 'different state points.\n'
231 'dpress determines the pressure difference between base\n'
232 'simulation and the additional point. If more than one\n'
233 'value is given, several tests will be performed.\n'
234 'By also giving dtemp, both temperature and pressure can\n'
235 'be displaced simultaneously.')
236 parser
.add_argument('-t', '--tolerance', type=float, default
=3,
237 help=('The number of standard deviations a result can be off\n'
238 'to be still accepted. Default: 3.'))
243 def prepare_parser(cls
, input_dir
, target_dir
, system_name
, nobackup
, args
):
244 args
= cls
.parser().parse_args(args
)
245 return cls
.prepare(input_dir
, target_dir
, system_name
, nobackup
,
246 dtemp
=args
.dtemp
, dpress
=args
.dpress
)
249 def analyze_parser(cls
, gmx_parser
, system_dir
, system_name
, base_data
, verbosity
, args
):
250 args
= cls
.parser().parse_args(args
)
251 return cls
.analyze(gmx_parser
, system_dir
, system_name
, base_data
, verbosity
,
252 tolerance
=args
.tolerance
, dtemp
=args
.dtemp
, dpress
=args
.dpress
)
255 def prepare(cls
, input_dir
, target_dir
, system_name
, nobackup
, dtemp
=None, dpress
=None):
256 # No standard values (system-dependent!)
257 if not dtemp
and not dpress
:
258 raise ValueError('Ensemble test for system ' + system_name
+
259 ' has no defined temperature or pressure difference.')
260 # Pad arrays - assume 0 difference if other difference is set
262 dtemp
= [0] * len(dpress
)
264 dpress
= [0] * len(dtemp
)
265 if len(dtemp
) < len(dpress
):
266 dtemp
.extend([0] * (len(dpress
) - len(dtemp
)))
267 if len(dpress
) < len(dtemp
):
268 dpress
.extend([0] * (len(dtemp
) - len(dpress
)))
269 # Check if we do any pressure differences
277 options
= GromacsInterface
.read_mdp(os
.path
.join(input_dir
, 'system.mdp'))
279 # Check for target temperature
280 if 'ref-t' in options
:
281 ref_t
= float(options
['ref-t'])
283 elif 'ref_t' in options
:
284 ref_t
= float(options
['ref_t'])
287 raise ValueError('Ensemble test for system ' + system_name
+ ' selected, ' +
288 'but system has no defined temperature.')
290 # Check for target pressure, if relevant
294 elif 'ref-p' in options
:
295 ref_p
= float(options
['ref-p'])
297 elif 'ref_p' in options
:
298 ref_p
= float(options
['ref_p'])
301 raise ValueError('Ensemble test with pressure difference for system ' + system_name
+
302 ' selected, but system has no defined pressure.')
306 for n
, (dt
, dp
) in enumerate(zip(dtemp
, dpress
)):
307 current_dir
= os
.path
.join(target_dir
, 'ensemble_' + str(n
+1))
308 mkdir_bk(current_dir
, nobackup
=nobackup
)
309 # change temperature & pressure
310 options
[ref_t_key
] = str(ref_t
+ dt
)
312 options
[ref_p_key
] = str(ref_p
+ dp
)
314 GromacsInterface
.write_mdp(options
, os
.path
.join(current_dir
, 'system.mdp'))
315 # copy topology and starting structure
316 for suffix
in ['gro', 'top']:
317 shutil
.copy2(os
.path
.join(input_dir
, 'system.' + suffix
),
319 directories
.append(current_dir
)
324 def analyze(cls
, gmx_parser
, system_dir
, system_name
, base_data
, verbosity
,
325 tolerance
=None, dtemp
=None, dpress
=None):
326 # No standard values (system-dependent!)
327 if not dtemp
and not dpress
:
328 raise ValueError('Ensemble test for system ' + system_name
+
329 ' has no defined temperature or pressure difference.')
330 # Pad arrays - assume 0 difference if other difference is set
332 dtemp
= [0] * len(dpress
)
334 dpress
= [0] * len(dtemp
)
335 if len(dtemp
) < len(dpress
):
336 dtemp
.extend([0] * (len(dpress
) - len(dtemp
)))
337 if len(dpress
) < len(dtemp
):
338 dpress
.extend([0] * (len(dtemp
) - len(dpress
)))
340 nsystems
= len(dtemp
)
342 if tolerance
is None:
343 tolerance
= cls
.parser().get_default('tolerance')
346 if base_data
['reduced'] is None:
347 current_dir
= os
.path
.join(system_dir
, 'base')
348 base_data
['reduced'] = gmx_parser
.get_simulation_data(
349 mdp
=os
.path
.join(current_dir
, 'mdout.mdp'),
350 top
=os
.path
.join(current_dir
, 'system.top'),
351 edr
=os
.path
.join(current_dir
, 'system.edr'),
352 gro
=os
.path
.join(current_dir
, 'system.gro')
354 base_result
= base_data
['reduced']
356 # list of results from simulations at different state points
358 for n
in range(nsystems
):
359 current_dir
= os
.path
.join(system_dir
, 'ensemble_' + str(n
+1))
360 results
.append(gmx_parser
.get_simulation_data(
361 mdp
=os
.path
.join(current_dir
, 'mdout.mdp'),
362 top
=os
.path
.join(current_dir
, 'system.top'),
363 edr
=os
.path
.join(current_dir
, 'system.edr'),
364 gro
=os
.path
.join(current_dir
, 'system.gro')
371 for result
, dt
, dp
in zip(results
, dtemp
, dpress
):
372 quantiles
= ensemble
.check(base_result
, result
, verbosity
=verbosity
)
373 # filename=os.path.join(system_dir, system_name + '_ens'))
374 if any(q
> tolerance
or math
.isnan(q
) for q
in quantiles
):
376 if len(quantiles
) == 1:
377 message
+= '\n --dtemp={:.1f} --dpress={:.1f} : FAILED ({:.1f} quantiles off)'.format(
381 message
+= '\n --dtemp={:.1f} --dpress={:.1f} : FAILED ([{:.1f}, {:.1f}] quantiles off)'.format(
382 dt
, dp
, quantiles
[0], quantiles
[1]
385 if len(quantiles
) == 1:
386 message
+= '\n --dtemp={:.1f} --dpress={:.1f} : PASSED ({:.1f} quantiles off)'.format(
390 message
+= '\n --dtemp={:.1f} --dpress={:.1f} : PASSED ([{:.1f}, {:.1f}] quantiles off)'.format(
391 dt
, dp
, quantiles
[0], quantiles
[1]
393 max_quantiles
= max(max_quantiles
, max(quantiles
))
396 message
= ('EnsembleTest PASSED (tolerance: {:.1f} quantiles)'.format(tolerance
) +
399 message
= ('EnsembleTest FAILED (tolerance: {:.1f} quantiles)'.format(tolerance
) +
402 return {'test': passed
,
403 'result': max_quantiles
,
404 'tolerance': tolerance
,
408 class MaxwellBoltzmannTest(Test
):
411 parser
= argparse
.ArgumentParser(
412 description
='Tests the validity of the kinetic energy ensemble.',
413 formatter_class
=argparse
.RawTextHelpFormatter
,
416 parser
.add_argument('-t', '--tolerance', type=float, default
=0.05,
417 help='The alpha value (confidence interval) used in\n'
418 'the statistical test. Default: 0.05.')
423 def prepare_parser(cls
, input_dir
, target_dir
, system_name
, nobackup
, args
):
424 return cls
.prepare(input_dir
, target_dir
, system_name
, nobackup
)
427 def analyze_parser(cls
, gmx_parser
, system_dir
, system_name
, base_data
, verbosity
, args
):
428 args
= cls
.parser().parse_args(args
)
429 return cls
.analyze(gmx_parser
, system_dir
, system_name
, base_data
, verbosity
,
430 alpha
=args
.tolerance
)
433 def prepare(cls
, input_dir
, target_dir
, system_name
, nobackup
):
434 # no additional sims needed, base is enough
435 # could check energy writing settings
439 def analyze(cls
, gmx_parser
, system_dir
, system_name
, base_data
, verbosity
, alpha
=None):
442 alpha
= cls
.parser().get_default('tolerance')
445 if base_data
['reduced'] is None:
446 current_dir
= os
.path
.join(system_dir
, 'base')
447 base_data
['reduced'] = gmx_parser
.get_simulation_data(
448 mdp
=os
.path
.join(current_dir
, 'mdout.mdp'),
449 top
=os
.path
.join(current_dir
, 'system.top'),
450 edr
=os
.path
.join(current_dir
, 'system.edr'),
451 gro
=os
.path
.join(current_dir
, 'system.gro')
453 base_result
= base_data
['reduced']
455 p
= kinetic_energy
.mb_ensemble(base_result
, verbosity
=verbosity
)
456 # filename=os.path.join(system_dir, system_name + '_mb'))
459 message
= 'MaxwellBoltzmannTest PASSED (p = {:g}, alpha = {:f})'.format(p
, alpha
)
461 message
= 'MaxwellBoltzmannTest FAILED (p = {:g}, alpha = {:f})'.format(p
, alpha
)
463 return {'test': p
>= alpha
,
469 class EquipartitionTest(Test
):
472 parser
= argparse
.ArgumentParser(
473 description
='Tests the equipartition of the kinetic energy.',
474 formatter_class
=argparse
.RawTextHelpFormatter
,
477 parser
.add_argument('--distribution', default
=False, action
='store_true',
478 help=('If set, the groups of degrees of freedom will\n'
479 'be separately tested as Maxwell-Boltzmann\n'
480 'distributions. Otherwise, only the difference in\n'
481 'average kinetic energy is checked.'))
482 parser
.add_argument('-t', '--tolerance', type=float, default
=0.1,
483 help=('If --distribution is set:\n'
484 ' The alpha value (confidence interval) used in the\n'
485 ' statistical test. Default: 0.1.\n'
487 ' The maximum deviation allowed between groups.\n'
488 ' Default: 0.1 (10%%)'))
489 parser
.add_argument('--random_groups', type=int, default
=0,
490 help='Number of random division tests attempted.\n'
491 'Default: 0 (random division tests off).')
492 parser
.add_argument('--random_divisions', type=int, default
=2,
493 help='Number of groups the system is randomly divided in.\n'
495 parser
.add_argument('--molec_group', type=int, nargs
='*', default
=None, action
='append',
496 help=('List of molecule indeces defining a group. Useful to\n'
497 'pre-define groups of molecules\n'
498 '(e.g. solute / solvent, liquid mixture species, ...).\n'
499 'If not set, no pre-defined molecule groups will be\ntested.\n'
500 'Note: If the last --molec-group flag is given empty,\n'
501 'the remaining molecules are collected in this group.\n'
502 'This allows, for example, to only specify the solute,\n'
503 'and indicate the solvent by giving an empty flag.'))
508 def prepare(cls
, input_dir
, target_dir
, system_name
, nobackup
):
509 # no additional sims needed, base is enough
510 # could check position, velocity & energy writing settings
514 def prepare_parser(cls
, input_dir
, target_dir
, system_name
, nobackup
, args
):
515 return cls
.prepare(input_dir
, target_dir
, system_name
, nobackup
)
518 def analyze_parser(cls
, gmx_parser
, system_dir
, system_name
, base_data
, verbosity
, args
):
519 args
= cls
.parser().parse_args(args
)
520 return cls
.analyze(gmx_parser
, system_dir
, system_name
, base_data
, verbosity
,
521 tolerance
=args
.tolerance
, distribution
=args
.distribution
,
522 random_groups
=args
.random_groups
, random_divisions
=args
.random_divisions
,
523 molec_group
=args
.molec_group
)
526 def analyze(cls
, gmx_parser
, system_dir
, system_name
, base_data
, verbosity
,
527 tolerance
=None, distribution
=None,
528 random_groups
=None, random_divisions
=None,
532 if tolerance
is None:
533 tolerance
= cls
.parser().get_default('tolerance')
534 if distribution
is None:
535 distribution
= cls
.parser().get_default('distribution')
536 if random_groups
is None:
537 random_groups
= cls
.parser().get_default('random_groups')
538 if random_divisions
is None:
539 random_divisions
= cls
.parser().get_default('random_divisions')
540 if molec_group
is None:
541 molec_group
= cls
.parser().get_default('molec_group')
544 if base_data
['full'] is None:
545 current_dir
= os
.path
.join(system_dir
, 'base')
546 base_data
['full'] = gmx_parser
.get_simulation_data(
547 mdp
=os
.path
.join(current_dir
, 'mdout.mdp'),
548 top
=os
.path
.join(current_dir
, 'system.top'),
549 edr
=os
.path
.join(current_dir
, 'system.edr'),
550 gro
=os
.path
.join(current_dir
, 'system.trr')
552 base_result
= base_data
['full']
554 result
= kinetic_energy
.equipartition(base_result
,
555 dtemp
=tolerance
, alpha
=tolerance
,
556 distribution
=distribution
, molec_groups
=molec_group
,
557 random_divisions
=random_divisions
, random_groups
=random_groups
,
562 test
= res
>= tolerance
564 message
= 'EquipartitionTest PASSED (min p = {:g}, alpha = {:f})'.format(res
, tolerance
)
566 message
= 'EquipartitionTest FAILED (min p = {:g}, alpha = {:f})'.format(res
, tolerance
)
568 dev_min
= min(result
)
569 dev_max
= max(result
)
570 if (1-dev_min
) > (dev_max
-1):
574 test
= res
<= tolerance
576 message
= 'EquipartitionTest PASSED (max dev = {:g}, tolerance = {:f})'.format(res
, tolerance
)
578 message
= 'EquipartitionTest FAILED (max dev = {:g}, tolerance = {:f})'.format(res
, tolerance
)
580 return {'test': test
,
582 'tolerance': tolerance
,
586 class KinConstraintsTest(Test
):
589 parser
= argparse
.ArgumentParser(
590 description
='Tests whether there is kinetic energy in constrained degrees of\nfreedom.',
591 formatter_class
=argparse
.RawTextHelpFormatter
,
594 parser
.add_argument('-t', '--tolerance', type=float, default
=0.1,
595 help='Tolerance - TODO. Default: 0.1.')
600 def prepare(cls
, input_dir
, target_dir
, system_name
, nobackup
):
601 # no additional sims needed, base is enough
602 # could check if there are any constraints in the system
606 def prepare_parser(cls
, input_dir
, target_dir
, system_name
, nobackup
, args
):
607 return cls
.prepare(input_dir
, target_dir
, system_name
, nobackup
)
610 def analyze_parser(cls
, gmx_parser
, system_dir
, system_name
, base_data
, verbosity
, args
):
611 raise NotImplementedError
614 def analyze(cls
, gmx_parser
, system_dir
, system_name
, base_data
, verbosity
):
615 raise NotImplementedError
618 all_tests
= OrderedDict([
619 ('integrator', IntegratorTest
),
620 ('ensemble', EnsembleTest
),
621 ('kin_mb', MaxwellBoltzmannTest
),
622 ('kin_equipartition', EquipartitionTest
),
623 ('kin_constraints', KinConstraintsTest
)
627 def parse_systems(systems_json
, systems_user
, source_path
,
630 # As the order of the systems and the tests
631 # might be meaningful, we need ordered dicts!
632 system_list
= json
.load(systems_json
)
633 system_dict
= OrderedDict()
634 for system
in system_list
:
635 system_name
= system
['name']
636 system_dir
= system
['dir']
637 # do the input files exist? (only relevant if we're not only analyzing)
639 input_dir
= os
.path
.join(source_path
, system_dir
, 'input')
640 if not (os
.path
.isdir(input_dir
) and
641 os
.path
.exists(os
.path
.join(input_dir
, 'system.mdp')) and
642 os
.path
.exists(os
.path
.join(input_dir
, 'system.top')) and
643 os
.path
.exists(os
.path
.join(input_dir
, 'system.gro'))):
644 raise ValueError('System ' + system_name
+ ' in ' +
645 systems_json
.name
+ ': Input files not found')
646 # no need to run systems that we don't test
647 if 'tests' not in system
:
648 raise ValueError('System ' + system_name
+ ' in ' +
649 systems_json
.name
+ ' has no tests defined')
650 test_list
= system
['tests']
651 system
['tests'] = OrderedDict()
652 for test
in test_list
:
653 test_name
= test
['test']
654 if test_name
not in all_tests
:
655 raise ValueError('Test ' + test_name
+ ' in ' +
656 systems_json
.name
+ ' is not a valid test')
657 if test_name
not in system
['tests']:
659 test
['args'] = [test
['args'].split()]
662 system
['tests'][test_name
] = test
665 system
['tests'][test_name
]['args'].append(test
['args'].split())
667 system
['tests'][test_name
]['args'].append([])
669 system_dict
[system_name
] = system
670 # add standard arguments
671 if 'grompp_args' not in system
:
672 system
['grompp_args'] = []
674 system
['grompp_args'] = system
['grompp_args'].split()
675 if 'mdrun_args' not in system
:
676 system
['mdrun_args'] = []
678 system
['mdrun_args'] = system
['mdrun_args'].split()
680 # if user has not chosen otherwise, return full dict of systems
684 # make sure user_dicts matches at least something
685 for user_system
in systems_user
:
686 for system
in system_dict
:
687 if re
.match(user_system
+ '$', system
):
690 raise ValueError('System ' + user_system
+
691 ' used in command line argument is not defined in ' +
694 for system
in list(system_dict
.keys()):
695 # delete systems not selected by user
696 for user_system
in systems_user
:
697 if re
.match(user_system
+ '$', system
):
700 system_dict
.pop(system
)
702 # return reduced dict of systems
707 parser
= argparse
.ArgumentParser(
708 description
='Physical validation suite for GROMACS.',
709 prog
='gmx_physicalvalidation.py',
710 formatter_class
=argparse
.RawTextHelpFormatter
,
711 epilog
='Use --tests for details about the available tests and their arguments.'
713 parser
.add_argument('json', type=open,
714 metavar
='systems.json',
715 help='Json file containing systems and tests to be ran.')
716 parser
.add_argument('--tests', default
=False, action
='store_true',
717 help='Print details about the available tests and their arguments and exit.')
718 parser
.add_argument('-v', '--verbosity', type=int,
719 metavar
='v', default
=0,
720 help='Verbosity level. Default: 0 (quiet).')
721 parser
.add_argument('--gmx', type=str, metavar
='exe', default
=None,
722 help=('GROMACS executable. Default: Trying to use \'gmx\'.\n' +
723 'Note: If -p is used, the GROMACS executable is not needed.'))
724 parser
.add_argument('--bindir', type=str, metavar
='dir', default
=None,
725 help=('GROMACS binary directory.\n' +
726 'If set, trying to use \'bindir/gmx\' instead of plain \'gmx\'\n' +
727 'Note: If --gmx is set, --bindir and --suffix are ignored.'))
728 parser
.add_argument('--suffix', type=str, metavar
='_s', default
=None,
729 help=('Suffix of the GROMACS executable.\n' +
730 'If set, trying to use \'gmx_s\' instead of plain \'gmx\'\n' +
731 'Note: If --gmx is set, --bindir and --suffix are ignored.'))
732 group
= parser
.add_mutually_exclusive_group()
733 group
.add_argument('-p', '--prepare', action
='store_true',
735 help=('Only prepare simulations and output a \'run.sh\' file\n' +
736 'containing the necessary commands to run the systems.\n' +
737 'This allows to separate running simulations from analyzing them,\n' +
738 'useful e.g. to analyze the results on a different machine.\n' +
739 'Default: If none of \'-p\', \'-r\' or \'-a\' is given,\n' +
740 ' the systems are prepared, ran and analyzed in one call.'))
741 group
.add_argument('-r', '--run', action
='store_true',
743 help=('Only prepare and run simulations.\n' +
744 'Default: If none of \'-p\', \'-r\' or \'-a\' is given,\n' +
745 ' the systems are prepared, ran and analyzed in one call.'))
746 group
.add_argument('-a', '--analyze', action
='store_true',
748 help=('Only analyze previously ran simulations.\n' +
749 'This requires that the systems have been prepared using this program.\n' +
750 'Default: If none of \'-p\', \'-r\' or \'-a\' is given,\n' +
751 ' the systems are prepared, ran and analyzed in one call.'))
752 parser
.add_argument('-s', '--system', action
='append', dest
='systems',
754 help=('Specify which system to run.\n' +
755 '\'system\' needs to match a system defined in the json file.\n' +
756 'Several systems can be specified by chaining several \'-s\' arguments.\n' +
757 'Defaults: If no \'-s\' argument is given, all systems and tests\n' +
758 ' defined in the json file are ran.\n' +
759 'Note: \'system\' can be a regular expression matching more than one system.'))
760 parser
.add_argument('--mpicmd', type=str, metavar
='cmd', default
=None,
761 help='MPI command used to invoke run command')
762 parser
.add_argument('--wd', '--working_dir', type=str,
763 metavar
='dir', default
=None,
764 help='Working directory (default: current directory)')
765 parser
.add_argument('--nobackup', default
=False, action
='store_true',
766 help='Do not create backups of files or folders.')
768 if '--tests' in args
:
769 message
= ('Physical validation suite for GROMACS\n'
770 'Available tests and their arguments\n'
771 '=====================================\n\n'
772 'The following tests can be specified in the json file:\n\n')
773 for test
in all_tests
:
774 message
+= ' * ' + test
+ '\n'
776 message
+= '\nAll tests accept additional arguments:\n'
778 for test
, test_cls
in all_tests
.items():
780 test_help
= test_cls
.parser().format_help()
781 test_help
= test_help
.replace(test_cls
.__name
__, test
).replace('usage: ', '')
782 test_help
= test_help
.replace(' -h, --help show this help message and exit\n', '')
783 test_help
= test_help
.replace(' [-h]', '')
784 test_help
= test_help
.replace(' ' * (len(test_cls
.__name
__) + 7), ' ' * len(test
))
786 first_line
= test_help
.split('\n')[0]
788 message
+= test_help
.replace(first_line
, separator
+ '\n' + first_line
)
790 sys
.stderr
.write(message
)
793 args
= parser
.parse_args(args
)
795 # the input files are expected to be located where this script is
796 source_path
= os
.path
.join(os
.path
.dirname(os
.path
.realpath(__file__
)), 'systems')
797 # target directory can be current or user chosen
799 target_path
= os
.getcwd()
801 if not os
.path
.exists(args
.wd
):
803 target_path
= args
.wd
805 # parse simulation stage to perform
806 do_all
= not (args
.prepare
or args
.run
or args
.analyze
)
807 do_prepare
= do_all
or args
.prepare
or args
.run
808 write_script
= args
.prepare
809 do_run
= do_all
or args
.run
810 do_analysis
= do_all
or args
.analyze
812 # get ordered dict of systems from combination of json file and user choices
813 systems
= parse_systems(args
.json
, args
.systems
, source_path
, args
.analyze
)
815 # prepare GROMACS interface
823 gmx
= os
.path
.join(args
.bindir
, gmx
)
826 if do_run
or do_analysis
:
827 gmx_interface
= GromacsInterface(exe
=gmx
)
828 gmx_parser
= GromacsParser(exe
=gmx
)
831 nsystems
= len(systems
)
833 runs
= [] # this will contain all information needed to run the system
834 for system_name
, system
in systems
.items():
836 print('\rPreparing run files for systems... [{:d}/{:d}] '.format(n
, nsystems
), end
='')
837 sys
.stdout
.flush() # py2 compatibility
838 system_dir
= system
['dir']
839 system_dirs
= [] # list of directories with subsystems
840 # prepare the base system
841 input_dir
= os
.path
.join(source_path
, system_dir
, 'input')
842 target_dir
= os
.path
.join(target_path
, system_dir
)
843 mkdir_bk(target_dir
, nobackup
=args
.nobackup
)
844 basedir
= os
.path
.join(target_dir
, 'base')
845 mkdir_bk(basedir
, nobackup
=args
.nobackup
)
846 for suffix
in ['mdp', 'gro', 'top']:
847 shutil
.copy2(os
.path
.join(input_dir
, 'system.' + suffix
),
849 system_dirs
.append(basedir
)
850 # call prepare method of chosen tests
851 for test_name
, test
in system
['tests'].items():
852 for test_args
in test
['args']:
854 all_tests
[test_name
].prepare_parser(input_dir
, target_dir
, system_name
,
855 args
.nobackup
, test_args
)
858 # save run information
859 for d
in system_dirs
:
862 'grompp_args': system
['grompp_args'],
863 'mdrun_args': system
['mdrun_args']
865 # end of loop over systems
869 print('Writing run script... ', end
='')
870 sys
.stdout
.flush() # py2 compatibility
871 script_file
= os
.path
.join(target_path
, 'run_simulations.sh')
872 if not args
.nobackup
:
874 with
open(script_file
, 'w') as f
:
875 f
.write('# This file was created by the physical validation suite for GROMACS.\n')
876 f
.write('\n# Define run variables\n')
877 f
.write('WORKDIR=' + os
.path
.abspath(target_path
) + '\n')
878 f
.write('GROMPPCMD="' + os
.path
.abspath(gmx
) + ' grompp"\n')
879 f
.write('MDRUNCMD="' + os
.path
.abspath(gmx
) + ' mdrun"\n')
880 f
.write('\n# Run systems\n')
881 f
.write('startpath=$PWD\n')
882 f
.write('cd $WORKDIR\n')
884 for cmd
in basic_run_cmds(directory
=os
.path
.relpath(os
.path
.abspath(run
['dir']),
885 os
.path
.abspath(target_path
)),
886 grompp_args
=run
['grompp_args'],
887 mdrun_args
=run
['mdrun_args']):
890 f
.write('cd $startpath\n')
892 print('Run script written to ' + script_file
)
893 print('Adapt script as necessary and run simulations. Make sure to preserve the folder structure!')
894 print('Once all simulations have ran, analyze the results using `make check-phys-analyze` or '
895 'using the `-a` flag of `gmx_physicalvalidation.py`.')
896 # end if write_script
900 # send messages from GROMACS to log
901 gmx_log
= open(os
.path
.join(target_path
, 'physicalvalidation_gmx.log'), 'w')
902 for n
, run
in enumerate(runs
):
903 print('\rRunning (sub)systems... [{:d}/{:d}] '.format(n
+1, nruns
), end
='')
904 sys
.stdout
.flush() # py2 compatibility
905 gmx_interface
.grompp(mdp
='system.mdp',
910 args
=run
['grompp_args'],
913 gmx_interface
.mdrun(tpr
='system.tpr',
916 args
=run
['mdrun_args'],
926 title
= 'GROMACS PHYSICAL VALIDATION RESULTS'
928 indent
= int((width
- len(title
))/2)
930 print(' ' * indent
+ '=' * len(title
))
931 print(' ' * indent
+ title
)
932 print(' ' * indent
+ '=' * len(title
))
935 for system_name
, system
in systems
.items():
936 system_dir
= system
['dir']
937 # save system data if re-used for different test
938 # massively reduces run time of multiple tests
944 target_dir
= os
.path
.join(target_path
, system_dir
)
946 print('Analyzing system ' + system_name
)
948 # call analyze method of chosen tests
949 for test_name
, test
in system
['tests'].items():
950 for test_args
in test
['args']:
952 result
= all_tests
[test_name
].analyze_parser(gmx_parser
, target_dir
,
953 system_name
, system_data
,
954 args
.verbosity
, test_args
)
955 except Exception as err
:
956 print(' ' + all_tests
[test_name
].__name
__ + ' FAILED (Exception in evaluation)')
957 print(' '*2 + type(err
).__name
__ + ': ' + str(err
))
960 for line
in result
['message'].split('\n'):
963 passed
= passed
and result
['test']
964 # end loop over tests
966 # end loop over systems
967 return int(not passed
)
970 # assuming everything is ok if we ended up here
974 if __name__
== "__main__":
975 return_value
= main(sys
.argv
[1:])
976 sys
.exit(return_value
)