nomad-parser-plugins-atomistic 1.0__py3-none-any.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 (80) hide show
  1. atomisticparsers/__init__.py +400 -0
  2. atomisticparsers/amber/__init__.py +19 -0
  3. atomisticparsers/amber/__main__.py +31 -0
  4. atomisticparsers/amber/metainfo/__init__.py +19 -0
  5. atomisticparsers/amber/metainfo/amber.py +495 -0
  6. atomisticparsers/amber/parser.py +42 -0
  7. atomisticparsers/asap/__init__.py +19 -0
  8. atomisticparsers/asap/__main__.py +31 -0
  9. atomisticparsers/asap/metainfo/__init__.py +19 -0
  10. atomisticparsers/asap/metainfo/asap.py +75 -0
  11. atomisticparsers/asap/parser.py +197 -0
  12. atomisticparsers/bopfox/__init__.py +19 -0
  13. atomisticparsers/bopfox/__main__.py +31 -0
  14. atomisticparsers/bopfox/metainfo/__init__.py +19 -0
  15. atomisticparsers/bopfox/metainfo/bopfox.py +225 -0
  16. atomisticparsers/bopfox/parser.py +808 -0
  17. atomisticparsers/dftbplus/__init__.py +19 -0
  18. atomisticparsers/dftbplus/__main__.py +31 -0
  19. atomisticparsers/dftbplus/metainfo/__init__.py +19 -0
  20. atomisticparsers/dftbplus/metainfo/dftbplus.py +217 -0
  21. atomisticparsers/dftbplus/parser.py +500 -0
  22. atomisticparsers/dlpoly/__init__.py +19 -0
  23. atomisticparsers/dlpoly/__main__.py +31 -0
  24. atomisticparsers/dlpoly/metainfo/__init__.py +19 -0
  25. atomisticparsers/dlpoly/metainfo/dl_poly.py +312 -0
  26. atomisticparsers/dlpoly/parser.py +798 -0
  27. atomisticparsers/gromacs/__init__.py +19 -0
  28. atomisticparsers/gromacs/__main__.py +31 -0
  29. atomisticparsers/gromacs/metainfo/__init__.py +19 -0
  30. atomisticparsers/gromacs/metainfo/gromacs.py +2388 -0
  31. atomisticparsers/gromacs/parser.py +1581 -0
  32. atomisticparsers/gromos/__init__.py +19 -0
  33. atomisticparsers/gromos/__main__.py +31 -0
  34. atomisticparsers/gromos/metainfo/__init__.py +19 -0
  35. atomisticparsers/gromos/metainfo/gromos.py +1995 -0
  36. atomisticparsers/gromos/parser.py +58 -0
  37. atomisticparsers/gulp/__init__.py +19 -0
  38. atomisticparsers/gulp/__main__.py +31 -0
  39. atomisticparsers/gulp/metainfo/__init__.py +19 -0
  40. atomisticparsers/gulp/metainfo/gulp.py +1117 -0
  41. atomisticparsers/gulp/parser.py +1316 -0
  42. atomisticparsers/h5md/__init__.py +19 -0
  43. atomisticparsers/h5md/__main__.py +31 -0
  44. atomisticparsers/h5md/metainfo/__init__.py +19 -0
  45. atomisticparsers/h5md/metainfo/h5md.py +239 -0
  46. atomisticparsers/h5md/parser.py +901 -0
  47. atomisticparsers/lammps/__init__.py +19 -0
  48. atomisticparsers/lammps/__main__.py +31 -0
  49. atomisticparsers/lammps/metainfo/__init__.py +19 -0
  50. atomisticparsers/lammps/metainfo/lammps.py +1417 -0
  51. atomisticparsers/lammps/parser.py +1753 -0
  52. atomisticparsers/libatoms/__init__.py +19 -0
  53. atomisticparsers/libatoms/__main__.py +31 -0
  54. atomisticparsers/libatoms/metainfo/__init__.py +19 -0
  55. atomisticparsers/libatoms/metainfo/lib_atoms.py +251 -0
  56. atomisticparsers/libatoms/parser.py +38 -0
  57. atomisticparsers/namd/__init__.py +19 -0
  58. atomisticparsers/namd/__main__.py +31 -0
  59. atomisticparsers/namd/metainfo/__init__.py +19 -0
  60. atomisticparsers/namd/metainfo/namd.py +1605 -0
  61. atomisticparsers/namd/parser.py +312 -0
  62. atomisticparsers/tinker/__init__.py +19 -0
  63. atomisticparsers/tinker/__main__.py +31 -0
  64. atomisticparsers/tinker/metainfo/__init__.py +18 -0
  65. atomisticparsers/tinker/metainfo/tinker.py +1363 -0
  66. atomisticparsers/tinker/parser.py +685 -0
  67. atomisticparsers/utils/__init__.py +22 -0
  68. atomisticparsers/utils/mdanalysis.py +662 -0
  69. atomisticparsers/utils/parsers.py +226 -0
  70. atomisticparsers/xtb/__init__.py +19 -0
  71. atomisticparsers/xtb/__main__.py +32 -0
  72. atomisticparsers/xtb/metainfo/__init__.py +19 -0
  73. atomisticparsers/xtb/metainfo/xtb.py +256 -0
  74. atomisticparsers/xtb/parser.py +979 -0
  75. nomad_parser_plugins_atomistic-1.0.dist-info/LICENSE +202 -0
  76. nomad_parser_plugins_atomistic-1.0.dist-info/METADATA +327 -0
  77. nomad_parser_plugins_atomistic-1.0.dist-info/RECORD +80 -0
  78. nomad_parser_plugins_atomistic-1.0.dist-info/WHEEL +5 -0
  79. nomad_parser_plugins_atomistic-1.0.dist-info/entry_points.txt +15 -0
  80. nomad_parser_plugins_atomistic-1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,685 @@
1
+ #
2
+ # Copyright The NOMAD Authors.
3
+ #
4
+ # This file is part of NOMAD.
5
+ # See https://nomad-lab.eu for further info.
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ import os
21
+ import numpy as np
22
+ import fnmatch
23
+ from typing import List
24
+ import re
25
+
26
+ from nomad.parsing.file_parser.text_parser import Quantity, TextParser
27
+ from nomad.units import ureg
28
+ from runschema.run import Run, Program
29
+ from runschema.system import System, Atoms
30
+ from runschema.method import Method, ForceField, Model, AtomParameters
31
+ from runschema.calculation import (
32
+ Calculation,
33
+ Energy,
34
+ EnergyEntry,
35
+ VibrationalFrequencies,
36
+ )
37
+ from simulationworkflowschema import GeometryOptimization, GeometryOptimizationMethod
38
+ from atomisticparsers.utils import MDAnalysisParser, MDParser
39
+ from .metainfo.tinker import x_tinker_section_control_parameters
40
+
41
+ re_n = r'[\n\r]'
42
+ re_f = r'[-+]?\d+\.\d*(?:[DdEe][-+]\d+)?'
43
+ mol = (1 * ureg.mol).to('particle').magnitude
44
+
45
+
46
+ class KeyParser(TextParser):
47
+ def __init__(self):
48
+ super().__init__()
49
+
50
+ def init_quantities(self):
51
+ def to_key_val(val_in):
52
+ val = val_in.strip().split()
53
+ if len(val) == 1:
54
+ return [val[0], True]
55
+ elif len(val) == 2:
56
+ val[1] = float(val[1]) if re.match(re_f, val[1]) else val[1]
57
+ return val
58
+ else:
59
+ return [val[0], val[1:]]
60
+
61
+ self._quantities = [
62
+ Quantity(
63
+ 'key_val',
64
+ rf'([a-z\-]+) *([^#]*?) *{re_n}',
65
+ str_operation=to_key_val,
66
+ repeats=True,
67
+ convert=False,
68
+ )
69
+ ]
70
+
71
+
72
+ class RunParser(TextParser):
73
+ def __init__(self):
74
+ super().__init__()
75
+
76
+ def init_quantities(self):
77
+ def to_argument(val_in):
78
+ val_in = val_in.strip().split()
79
+ argument = dict()
80
+ if '-k' in val_in:
81
+ argument['key'] = val_in.pop(val_in.index('-k') + 1)
82
+ val_in.remove('-k')
83
+ argument['name'] = val_in.pop(0)
84
+ argument['parameters'] = val_in
85
+ return argument
86
+
87
+ self._quantities = [
88
+ Quantity(
89
+ 'molecular_dynamics',
90
+ r'dynamic +(\S+ +\-*k* *\S+.+)',
91
+ repeats=True,
92
+ str_operation=to_argument,
93
+ ),
94
+ Quantity(
95
+ 'geometry_optimization',
96
+ r'minimize +(\S+ +\-*k* *\S+.+)',
97
+ repeats=True,
98
+ str_operation=to_argument,
99
+ ),
100
+ Quantity(
101
+ 'single_point',
102
+ r'vibrate +(\S+ +\-*k* *\S+.+)',
103
+ repeats=True,
104
+ str_operation=to_argument,
105
+ ),
106
+ ]
107
+
108
+
109
+ class OutParser(TextParser):
110
+ def __init__(self):
111
+ super().__init__()
112
+
113
+ def init_quantities(self):
114
+ iteration_quantity = Quantity(
115
+ 'iteration',
116
+ rf'Iter.+([\s\S]+?){re_n} *{re_n}',
117
+ sub_parser=TextParser(
118
+ quantities=[
119
+ Quantity(
120
+ 'step',
121
+ rf' +\d+ +({re_f} +{re_f} +[\d\.DE\+\- ]+)',
122
+ str_operation=lambda x: [
123
+ float(v)
124
+ for v in x.replace('D+', 'E+').replace('d+', 'e+').split()
125
+ ],
126
+ repeats=True,
127
+ dtype=np.dtype(np.float64),
128
+ )
129
+ ]
130
+ ),
131
+ )
132
+
133
+ calculation_quantities = [
134
+ Quantity('program_version', r'Version ([\d\.]+)', dtype=str),
135
+ Quantity(
136
+ 'vibrate',
137
+ r'(Eigenvalues of the Hessian Matrix[\s\S]+?)\Z',
138
+ sub_parser=TextParser(
139
+ quantities=[
140
+ Quantity(
141
+ 'eigenvalues',
142
+ r'Eigenvalues of the Hessian Matrix :\s+([\d\-\s\.]+)',
143
+ dtype=np.dtype(np.float64),
144
+ ),
145
+ Quantity(
146
+ 'frequencies',
147
+ r'Vibrational Frequencies \(cm\-1\) :([\d\-\s\.]+)',
148
+ dtype=np.dtype(np.float64),
149
+ ),
150
+ ]
151
+ ),
152
+ ),
153
+ Quantity(
154
+ 'minimize',
155
+ r'(.+?Optimization :[\s\S]+?Final Gradient Norm.+)',
156
+ sub_parser=TextParser(
157
+ quantities=[
158
+ iteration_quantity,
159
+ Quantity(
160
+ 'method', r' +(.+) Optimization', flatten=False, dtype=str
161
+ ),
162
+ Quantity(
163
+ 'x_tiner_final_function_value',
164
+ rf'Final Function Value : +({re_f})',
165
+ dtype=np.float64,
166
+ ),
167
+ Quantity(
168
+ 'x_tinker_final_rms_gradient',
169
+ rf'Final RMS Gradient : +({re_f})',
170
+ dtype=np.float64,
171
+ ),
172
+ Quantity(
173
+ 'x_tinker_final_gradient_norm',
174
+ rf'Final Gradient Norm : +({re_f})',
175
+ dtype=np.float64,
176
+ ),
177
+ ]
178
+ ),
179
+ ),
180
+ Quantity(
181
+ 'dynamic',
182
+ r'(Molecular Dynamics[\s\S]+?)(?:\#\#\#\#\#|\Z)',
183
+ sub_parser=TextParser(
184
+ quantities=[
185
+ iteration_quantity,
186
+ Quantity(
187
+ 'instantaneous_values',
188
+ r'(Instantaneous Values for Frame Saved at[\s\S]+?Coordinate File.+)',
189
+ repeats=True,
190
+ sub_parser=TextParser(
191
+ quantities=[
192
+ Quantity(
193
+ 'step', r'(\d+) Dynamics Steps', dtype=np.int32
194
+ ),
195
+ Quantity(
196
+ 'time',
197
+ rf'Current Time +({re_f}) Picosecond',
198
+ dtype=np.float64,
199
+ unit=ureg.ps,
200
+ ),
201
+ Quantity(
202
+ 'potential',
203
+ rf'Current Potential +({re_f}) Kcal/mole',
204
+ dtype=np.float64,
205
+ unit=ureg.J * 4184.0 / mol,
206
+ ),
207
+ Quantity(
208
+ 'kinetic',
209
+ rf'Current Kinetic +({re_f}) Kcal/mole',
210
+ dtype=np.float64,
211
+ unit=ureg.J * 4184.0 / mol,
212
+ ),
213
+ Quantity(
214
+ 'lattice_lengths',
215
+ rf'Lattice Lengths +({re_f} +{re_f} +{re_f})',
216
+ dtype=np.dtype(np.float64),
217
+ ),
218
+ Quantity(
219
+ 'lattice_angles',
220
+ rf'Lattice Angles +({re_f} +{re_f} +{re_f})',
221
+ dtype=np.dtype(np.float64),
222
+ ),
223
+ Quantity(
224
+ 'frame', r'Frame Number +(\d+)', dtype=np.int32
225
+ ),
226
+ Quantity(
227
+ 'coordinate_file',
228
+ r'Coordinate File +(\S+)',
229
+ dtype=str,
230
+ ),
231
+ ]
232
+ ),
233
+ ),
234
+ Quantity(
235
+ 'average_values',
236
+ r'(Average Values for the Last[\s\S]+?Density.+)',
237
+ repeats=True,
238
+ sub_parser=TextParser(
239
+ quantities=[
240
+ Quantity(
241
+ 'step', r'(\d+) Dynamics Steps', dtype=np.int32
242
+ ),
243
+ Quantity(
244
+ 'time',
245
+ rf'Simulation Time +({re_f}) Picosecond',
246
+ dtype=np.float64,
247
+ unit=ureg.ps,
248
+ ),
249
+ Quantity(
250
+ 'energy_total',
251
+ rf'Total Energy +({re_f}) Kcal/mole',
252
+ dtype=np.float64,
253
+ unit=ureg.J * 4184.0 / mol,
254
+ ),
255
+ Quantity(
256
+ 'potential',
257
+ rf'Potential Energy +({re_f}) Kcal/mole',
258
+ dtype=np.float64,
259
+ unit=ureg.J * 4184.0 / mol,
260
+ ),
261
+ Quantity(
262
+ 'kinetic',
263
+ rf'Kinetic Energy +({re_f}) Kcal/mole',
264
+ dtype=np.float64,
265
+ unit=ureg.J * 4184.0 / mol,
266
+ ),
267
+ Quantity(
268
+ 'temperature',
269
+ rf'Temperature +({re_f}) Kelvin',
270
+ dtype=np.float64,
271
+ unit=ureg.kelvin,
272
+ ),
273
+ Quantity(
274
+ 'pressure',
275
+ rf'Pressure +({re_f}) Atmosphere',
276
+ dtype=np.float64,
277
+ unit=ureg.atm,
278
+ ),
279
+ Quantity(
280
+ 'density',
281
+ rf'Density +({re_f}) Grams/cc',
282
+ dtype=np.float64,
283
+ unit=ureg.g / ureg.cm**3,
284
+ ),
285
+ ]
286
+ ),
287
+ ),
288
+ ]
289
+ ),
290
+ ),
291
+ ]
292
+
293
+ self._quantities = [
294
+ Quantity(
295
+ 'run',
296
+ r'Software Tools for Molecular Design([\s\S]+?)(?:TINKER \-\-\-|\Z)',
297
+ repeats=True,
298
+ sub_parser=TextParser(quantities=calculation_quantities),
299
+ )
300
+ ]
301
+
302
+
303
+ class TinkerParser(MDParser):
304
+ def __init__(self):
305
+ self.out_parser = OutParser()
306
+ self.traj_parser = MDAnalysisParser()
307
+ self.key_parser = KeyParser()
308
+ self.run_parser = RunParser()
309
+ self._run_types = {
310
+ 'vibrate': 'single_point',
311
+ 'minimize': 'geometry_optimization',
312
+ 'dynamic': 'molecular_dynamics',
313
+ }
314
+ super().__init__()
315
+
316
+ def _get_tinker_file(self, ext):
317
+ if ext not in self._files:
318
+ return
319
+ current = self._files[ext]['current']
320
+ # advance to the next file
321
+ files = self._files[ext]['files']
322
+ try:
323
+ self._files[ext]['current'] = files[files.index(current) + 1]
324
+ except Exception:
325
+ # the last file has been reached in this case
326
+ pass
327
+ return os.path.join(self.maindir, current)
328
+
329
+ def parse_system(self, index, filename):
330
+ self.traj_parser.mainfile = filename
331
+ self.traj_parser.options = dict(
332
+ topology_format='ARC' if filename.endswith('.arc') else 'TXYZ'
333
+ )
334
+
335
+ if self.traj_parser.universe is None:
336
+ return
337
+
338
+ sec_system = System()
339
+ self.archive.run[-1].system.append(sec_system)
340
+ trajectory = self.traj_parser.universe.trajectory[index]
341
+ sec_system.atoms = Atoms(
342
+ positions=trajectory.positions * ureg.angstrom,
343
+ labels=[atom.name for atom in list(self.traj_parser.universe.atoms)],
344
+ )
345
+ if trajectory.triclinic_dimensions is not None:
346
+ sec_system.atoms.lattice_vectors = (
347
+ trajectory.triclinic_dimensions * ureg.angstrom
348
+ )
349
+ sec_system.atoms.periodic = [True, True, True]
350
+ if trajectory.has_velocities:
351
+ sec_system.atoms.velocities = trajectory.velocities * (
352
+ ureg.angstrom / ureg.ps
353
+ )
354
+
355
+ return sec_system
356
+
357
+ def parse_method(self):
358
+ sec_method = Method()
359
+ self.archive.run[-1].method.append(sec_method)
360
+
361
+ parameters = self.archive.run[-1].x_tinker_control_parameters
362
+ if parameters.get('parameters') is not None:
363
+ sec_method.force_field = ForceField(
364
+ model=[Model(name=parameters['parameters'])]
365
+ )
366
+ # TODO read the prm file
367
+
368
+ property_map = {
369
+ 'name': 'label',
370
+ 'type': 'x_tinker_atom_type',
371
+ 'resid': 'x_tinker_atom_resid',
372
+ }
373
+ if self.traj_parser.universe is not None:
374
+ for atom in list(self.traj_parser.universe.atoms):
375
+ sec_atom = AtomParameters()
376
+ sec_method.atom_parameters.append(sec_atom)
377
+ for key in ['charge', 'mass', 'name', 'type', 'resid']:
378
+ if hasattr(atom, key):
379
+ setattr(
380
+ sec_atom, property_map.get(key, key), getattr(atom, key)
381
+ )
382
+
383
+ # TODO add interaction parameters
384
+
385
+ def resolve_workflow_type(self, run):
386
+ for program in run.keys():
387
+ if program in self._run_types and run.get(program) is not None:
388
+ return self._run_types[program]
389
+
390
+ def parse_workflow(self, program, run):
391
+ def resolve_ensemble_type():
392
+ parameters = self.archive.run[-1].x_tinker_control_parameters
393
+ if parameters is None:
394
+ return
395
+
396
+ thermostat, barostat = (
397
+ parameters.get('thermostat', ''),
398
+ parameters.get('barostat', ''),
399
+ )
400
+ thermostats = ['berendsen', 'andersen', 'bussi']
401
+ # TODO verify this
402
+ if barostat.lower() in ['berendsen', 'montecarlo']:
403
+ ensemble_type = 'NPT'
404
+ elif not barostat and thermostat in thermostats:
405
+ ensemble_type = 'NVT'
406
+ elif not barostat and not thermostat:
407
+ ensemble_type = 'NVE'
408
+ else:
409
+ ensemble_type = None
410
+
411
+ return ensemble_type
412
+
413
+ # TODO handle multiple workflow sections
414
+ parameters = list(program.get('parameters', []))
415
+ # so we cover the case when optional parameters are missing
416
+ parameters.extend([None] * 6)
417
+ workflow_type = self.resolve_workflow_type(run)
418
+ if workflow_type == 'molecular_dynamics':
419
+ control_parameters = self.archive.run[-1].x_tinker_control_parameters
420
+ # TODO verify this! I am sure it is wrong but tinker documentation does not specify clearly
421
+ ensemble_types = ['NVE', 'NVT', 'NPT', None, None]
422
+ ensemble = (
423
+ ensemble_types[int(parameters[3]) - 1]
424
+ if parameters[3] is not None
425
+ else resolve_ensemble_type()
426
+ )
427
+ self.parse_md_workflow(
428
+ dict(
429
+ method=dict(
430
+ thermodynamic_ensemble=ensemble,
431
+ integration_timestep=parameters[1] * ureg.fs
432
+ if parameters[1]
433
+ else parameters[1],
434
+ ),
435
+ x_tinker_barostat_tau=control_parameters.get('tau-pressure'),
436
+ x_tinker_barostat_type=control_parameters.get('barostat'),
437
+ x_tinker_integrator_type=control_parameters.get('integrator'),
438
+ x_tinker_number_of_steps_requested=parameters[0],
439
+ x_tinker_integrator_dt=parameters[1] * ureg.ps
440
+ if parameters[1]
441
+ else parameters,
442
+ x_tinker_thermostat_target_temperature=parameters[4] * ureg.kelvin
443
+ if parameters[4]
444
+ else parameters[4],
445
+ x_tinker_barostat_target_pressure=parameters[5] * ureg.atmosphere
446
+ if parameters[5]
447
+ else parameters[5],
448
+ x_tinker_thermostat_tau=control_parameters.get('tau-temperature'),
449
+ x_tinker_thermostat_type=control_parameters.get('thermostat'),
450
+ )
451
+ )
452
+
453
+ elif workflow_type == 'geometry_optimization':
454
+ workflow = GeometryOptimization(method=GeometryOptimizationMethod())
455
+ workflow.method.method = run.minimize.get('method')
456
+ workflow.x_tinker_convergence_tolerance_rms_gradient = parameters[0]
457
+ for key, val in run.minimize.items():
458
+ if key.startswith('x_tinker'):
459
+ setattr(workflow, key, val)
460
+
461
+ self.archive.workflow2 = workflow
462
+
463
+ def write_to_archive(self) -> None:
464
+ self.out_parser.mainfile = self.mainfile
465
+ self.out_parser.logger = self.logger
466
+ self.maindir = os.path.dirname(self.mainfile)
467
+ self._base_name = os.path.basename(self.mainfile).rsplit('.', 1)[0]
468
+ files = os.listdir(self.maindir)
469
+ # required to track the current files
470
+ self._files = dict()
471
+ for ext in ['xyz', 'arc', 'key']:
472
+ matches = fnmatch.filter(files, f'{self._base_name}.{ext}*')
473
+ if not matches:
474
+ matches = fnmatch.filter(files, f'tinker.{ext}*')
475
+ matches.sort(
476
+ key=lambda x: int(x.rsplit('_', 1)[-1]) if x[-1].isdecimal() else 0
477
+ )
478
+ self._files[ext] = dict(
479
+ files=matches, current=matches[0] if matches else ''
480
+ )
481
+
482
+ self.maindir = os.path.dirname(self.mainfile)
483
+
484
+ workflows: List[str] = []
485
+
486
+ def get_reference_filename(program):
487
+ # resolves the filename as provided in the cli command for the program
488
+ filename = program.get('name', '')
489
+ if '.xyz' not in filename:
490
+ filename = self._files['xyz']['current']
491
+ # succeding files start from the index of the file for geometry_opt and md
492
+ filename = filename if '.xyz' in filename else f'{filename}.xyz'
493
+ if workflows[-1] in ['geometry_optimization', 'molecular_dynamics']:
494
+ files = self._files['xyz']['files']
495
+ try:
496
+ self._files['xyz']['current'] = files[files.index(filename) + 1]
497
+ except Exception:
498
+ pass
499
+ return os.path.join(self.maindir, filename)
500
+
501
+ # necesarry to extract program parameters from the cli command in basename.run
502
+ self.run_parser.mainfile = os.path.join(self.maindir, f'{self._base_name}.run')
503
+
504
+ # TODO put runs in separate archives
505
+ for run in self.out_parser.get('run', []):
506
+ sec_run = Run()
507
+ self.archive.run.append(sec_run)
508
+ sec_run.program = Program(name='tinker', version=run.get('program_version'))
509
+
510
+ workflow_type = self.resolve_workflow_type(run)
511
+ workflows.append(workflow_type)
512
+ # get parameters of the program from the cli command, the key file and the
513
+ # initial structure file can also be specied as an argument so we need to
514
+ # extract the information here
515
+ # program can be executed for an arbitrary number of times so we need to
516
+ # resolve the index of the appropriate command
517
+ n_workflow = (
518
+ len([workflow for workflow in workflows if workflow == workflow_type])
519
+ - 1
520
+ )
521
+ program = self.run_parser.get(workflow_type, [])
522
+ program = program[n_workflow] if len(program) > n_workflow else dict()
523
+ if run.vibrate is not None:
524
+ # reference structure
525
+ sec_system = self.parse_system(0, get_reference_filename(program))
526
+ sec_scc = Calculation()
527
+ sec_run.calculation.append(sec_scc)
528
+ sec_vibrations = VibrationalFrequencies()
529
+ sec_scc.vibrational_frequencies.append(sec_vibrations)
530
+ sec_vibrations.value = [
531
+ run.vibrate.frequencies[n]
532
+ for n in range(len(run.vibrate.get('frequencies', [])))
533
+ if n % 2 == 1
534
+ ] * (1 / ureg.cm)
535
+ sec_vibrations.x_tinker_eigenvalues = [
536
+ run.vibrate.eigenvalues[n]
537
+ for n in range(len(run.vibrate.get('eigenvalues', [])))
538
+ if n % 2 == 1
539
+ ]
540
+ sec_scc.system_ref = sec_system
541
+
542
+ if run.minimize is not None:
543
+ # initial structure
544
+ initial_system = self.parse_system(0, get_reference_filename(program))
545
+
546
+ # optimized structure
547
+ sec_system = self.parse_system(0, self._get_tinker_file('xyz'))
548
+
549
+ for n, step in enumerate(
550
+ run.minimize.get('iteration', {}).get('step', [])
551
+ ):
552
+ sec_scc = Calculation()
553
+ sec_run.calculation.append(sec_scc)
554
+ sec_scc.energy = Energy(
555
+ total=EnergyEntry(
556
+ value=step[0]
557
+ * len(sec_system.atoms.positions)
558
+ * ureg.J
559
+ * 4184.0
560
+ / mol
561
+ )
562
+ )
563
+ if n == 0:
564
+ sec_scc.system_ref = initial_system
565
+ # only the optimized structure is printed, corresponding to the last scc
566
+ sec_scc.system_ref = sec_system
567
+
568
+ if run.dynamic is not None:
569
+ self.traj_parser.mainfile = self._get_tinker_file('arc')
570
+ self.traj_parser.options = dict(topology_format='ARC')
571
+ average_values = {
572
+ value.step: value for value in run.dynamic.get('average_values', [])
573
+ }
574
+ # set up md parser
575
+ # TODO this is not efficient as we need to load traj file to know if it exists
576
+ traj_steps, thermo_steps, n_atoms, filenames = [], [], [], []
577
+ for nframe, value in enumerate(
578
+ run.dynamic.get('instantaneous_values', [])
579
+ ):
580
+ filename = os.path.join(
581
+ self.maindir, value.get('coordinate_file', '')
582
+ )
583
+ index = nframe if filename.endswith('.arc') else 0
584
+ filenames.append((filename, index))
585
+ thermo_steps.append(value.step)
586
+ self.traj_parser.mainfile = filename
587
+ self.traj_parser.options = dict(
588
+ topology_format='ARC' if filename.endswith('.arc') else 'TXYZ'
589
+ )
590
+ trajectory = self.traj_parser.universe.trajectory[index]
591
+ if trajectory is not None:
592
+ traj_steps.append(value.step)
593
+ n_atoms.append(len(trajectory))
594
+ self.n_atoms = max(n_atoms)
595
+ self.trajectory_steps = traj_steps
596
+ self.thermodynamics_steps = thermo_steps
597
+
598
+ for n_frame, step in enumerate(self.thermodynamics_steps):
599
+ self.traj_parser.mainfile = filenames[n_frame][0]
600
+ trajectory = self.traj_parser.universe.trajectory[
601
+ filenames[n_frame][1]
602
+ ]
603
+ if step in self.trajectory_steps:
604
+ # sampled trajectory
605
+ positions = trajectory.positions * ureg.angstrom
606
+ labels = [
607
+ atom.name for atom in list(self.traj_parser.universe.atoms)
608
+ ]
609
+ lattice_vectors, periodic, velocities = None, None, None
610
+ if trajectory.triclinic_dimensions is not None:
611
+ lattice_vectors = (
612
+ trajectory.triclinic_dimensions * ureg.angstrom
613
+ )
614
+ periodic = [True, True, True]
615
+ if trajectory.has_velocities:
616
+ velocities = trajectory.velocities * (
617
+ ureg.angstrom / ureg.ps
618
+ )
619
+ self.parse_trajectory_step(
620
+ dict(
621
+ atoms=dict(
622
+ positions=positions,
623
+ labels=labels,
624
+ lattice_vectors=lattice_vectors,
625
+ periodic=periodic,
626
+ velocities=velocities,
627
+ )
628
+ )
629
+ )
630
+ # thermo properties
631
+ instantaneous = run.dynamic.instantaneous_values[n_frame]
632
+ n_atoms_step = n_atoms[traj_steps.index(step)]
633
+ energy = dict(
634
+ total=dict(
635
+ value=(instantaneous.potential + instantaneous.kinetic)
636
+ * n_atoms_step,
637
+ kinetic=instantaneous.kinetic * n_atoms_step,
638
+ potential=instantaneous.potential * n_atoms_step,
639
+ )
640
+ )
641
+ data = dict(energy=energy, step=instantaneous.step)
642
+ average = average_values.get(instantaneous.step)
643
+ if average:
644
+ data['temperature'] = average.temperature
645
+ data['pressure'] = average.pressure
646
+ if trajectory is not None and trajectory.has_forces:
647
+ data['forces'] = dict(
648
+ total=dict(
649
+ value=trajectory.forces * (ureg.kJ / ureg.angstrom)
650
+ )
651
+ )
652
+ self.parse_thermodynamics_step(data)
653
+
654
+ # TODO add support for other tinker programs
655
+
656
+ # control parameters
657
+ # a key file can be specified optionally via the cli we assign this instead to get the parameters
658
+ self.key_parser.mainfile = os.path.join(
659
+ self.maindir, program.get('key', '')
660
+ )
661
+ if self.key_parser.mainfile is None:
662
+ # if the key file is not specified in cli, read from either basename.key
663
+ # or tinker.key
664
+ self.key_parser.mainfile = self._get_tinker_file('key')
665
+
666
+ parameters = {
667
+ key.lower(): val for key, val in self.key_parser.get('key_val', [])
668
+ }
669
+ sec_run.x_tinker_control_parameters = parameters
670
+ # TODO should this be removed and only have a dictionary of control parameters
671
+ sec_control = x_tinker_section_control_parameters()
672
+ sec_run.x_tinker_section_control_parameters.append(sec_control)
673
+ for key, val in parameters.items():
674
+ key = key.replace('-', '_')
675
+ setattr(
676
+ sec_control,
677
+ f'x_tinker_inout_control_{key}',
678
+ val if isinstance(val, bool) else str(val),
679
+ )
680
+
681
+ self.parse_method()
682
+
683
+ self.parse_workflow(program, run)
684
+
685
+ self.traj_parser.close()