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,22 @@
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
+ from .mdanalysis import MDAnalysisParser
20
+ from .parsers import MDParser
21
+
22
+ MOL = 6.022140857e23
@@ -0,0 +1,662 @@
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 numpy as np
21
+ import os
22
+
23
+ try:
24
+ import MDAnalysis
25
+ import MDAnalysis.analysis.rdf as MDA_RDF
26
+ from MDAnalysis.topology.guessers import guess_atom_element
27
+ except Exception:
28
+ MDAnalysis = None
29
+ from typing import Any, Dict
30
+ from nptyping import NDArray
31
+ from collections import namedtuple
32
+ from array import array
33
+ from scipy import sparse
34
+ from scipy.stats import linregress
35
+
36
+ from nomad.units import ureg
37
+ from nomad.parsing.file_parser import FileParser
38
+ from simulationworkflowschema.molecular_dynamics import BeadGroup, shifted_correlation_average
39
+
40
+
41
+ MOL = 6.022140857e23
42
+
43
+
44
+ class MDAnalysisParser(FileParser):
45
+ def __init__(self, *args, **kwargs):
46
+ super().__init__()
47
+ self._args = args
48
+ self._kwargs = kwargs
49
+ self._atomsgroup_info = None
50
+
51
+ @property
52
+ def auxilliary_files(self):
53
+ return self._args
54
+
55
+ @auxilliary_files.setter
56
+ def auxilliary_files(self, value):
57
+ self._file_handler = None
58
+ self._args = [value] if isinstance(value, str) else value
59
+
60
+ @property
61
+ def options(self):
62
+ return self._kwargs
63
+
64
+ @options.setter
65
+ def options(self, value):
66
+ self._file_handler = None
67
+ self._kwargs = value
68
+
69
+ @property
70
+ def universe(self):
71
+ if self._file_handler is None:
72
+ try:
73
+ self._file_handler = MDAnalysis.Universe(
74
+ self.mainfile, *self.auxilliary_files, **self.options
75
+ )
76
+ except Exception as e:
77
+ self.logger.error('Error creating MDAnalysis universe.', exc_info=e)
78
+ return self._file_handler
79
+
80
+ @property
81
+ def bead_groups(self):
82
+ atoms_moltypes = self.get('atoms_info', {}).get('moltypes', [])
83
+ moltypes = np.unique(atoms_moltypes)
84
+ bead_groups = {}
85
+ compound = 'fragments'
86
+ for moltype in moltypes:
87
+ if hasattr(self.universe.atoms, 'moltypes'):
88
+ ags_by_moltype = self.universe.select_atoms('moltype ' + moltype)
89
+ else: # this is easier than adding something to the universe
90
+ selection = ' '.join(
91
+ [str(i) for i in np.where(atoms_moltypes == moltype)[0]]
92
+ )
93
+ selection = f'index {selection}'
94
+ ags_by_moltype = self.universe.select_atoms(selection)
95
+ ags_by_moltype = ags_by_moltype[
96
+ ags_by_moltype.masses > abs(1e-2)
97
+ ] # remove any virtual/massless sites (needed for, e.g., 4-bead water models)
98
+ bead_groups[moltype] = BeadGroup(ags_by_moltype, compound=compound)
99
+
100
+ return bead_groups
101
+
102
+ def parse(self, quantity_key: str = None, **kwargs):
103
+ if self._results is None:
104
+ self._results: Dict[str, Any] = dict()
105
+
106
+ if not self.universe:
107
+ return
108
+
109
+ atoms = list(self.universe.atoms)
110
+
111
+ name_map = {'mass': 'masses'}
112
+ unit_map = {'mass': ureg.amu, 'charge': ureg.elementary_charge}
113
+ self._results['atoms_info'] = dict()
114
+ for key in [
115
+ 'name',
116
+ 'charge',
117
+ 'mass',
118
+ 'resid',
119
+ 'resname',
120
+ 'molnum',
121
+ 'moltype',
122
+ 'type',
123
+ 'segid',
124
+ 'element',
125
+ ]:
126
+ try:
127
+ value = [getattr(atom, key) for atom in atoms]
128
+ except Exception:
129
+ continue
130
+ value = value * unit_map.get(key, 1) if value is not None else value
131
+ self._results['atoms_info'][name_map.get(key, f'{key}s')] = value
132
+
133
+ # if atom name is not identified, set it to 'X'
134
+ if self._results['atoms_info'].get('names') is None:
135
+ self._results['atoms_info']['names'] = ['X'] * self.universe.atoms.n_atoms
136
+ self._results['n_atoms'] = self.universe.atoms.n_atoms
137
+ self._results['n_frames'] = len(self.universe.trajectory)
138
+
139
+ # make substitutions based on available atom info
140
+ if self._results['atoms_info'].get('moltypes') is None:
141
+ if hasattr(self.universe.atoms, 'fragments'):
142
+ self._results['atoms_info']['moltypes'] = self.get_fragtypes()
143
+
144
+ if self._results['atoms_info'].get('molnums') is None:
145
+ try:
146
+ value = getattr(self.universe.atoms, 'fragindices')
147
+ self._results['atoms_info']['molnums'] = value
148
+ except Exception:
149
+ pass
150
+
151
+ if self._results['atoms_info'].get('resnames') is None:
152
+ try:
153
+ self._results['atoms_info']['resnames'] = self._results['atoms_info'][
154
+ 'resids'
155
+ ]
156
+ except Exception:
157
+ pass
158
+
159
+ if self._results['atoms_info'].get('names') is None:
160
+ try:
161
+ self._results['atoms_info']['names'] = self._results['atoms_info'][
162
+ 'types'
163
+ ]
164
+ except Exception:
165
+ pass
166
+
167
+ if self._results['atoms_info'].get('elements') is None:
168
+ try:
169
+ self._results['atoms_info']['elements'] = self._results['atoms_info'][
170
+ 'names'
171
+ ]
172
+ except Exception:
173
+ pass
174
+
175
+ def get_fragtypes(self):
176
+ # TODO put description otherwise, make private or put under parse method
177
+ """ """
178
+ atoms_fragtypes = np.empty(self.universe.atoms.types.shape, dtype=str)
179
+ ctr_fragtype = 0
180
+ atoms_fragtypes[self.universe.atoms.fragments[0].ix] = ctr_fragtype
181
+ frag_unique_atomtypes = [
182
+ self.universe.atoms.types[self.universe.atoms.fragments[0].ix]
183
+ ]
184
+ ctr_fragtype += 1
185
+ for i_frag in range(1, self.universe.atoms.n_fragments):
186
+ types_i_frag = self.universe.atoms.types[
187
+ self.universe.atoms.fragments[i_frag].ix
188
+ ]
189
+ flag_fragtype_exists = False
190
+ for j_frag in range(len(frag_unique_atomtypes) - 1, -1, -1):
191
+ types_j_frag = frag_unique_atomtypes[j_frag]
192
+ if len(types_i_frag) != len(types_j_frag):
193
+ continue
194
+ elif np.all(types_i_frag == types_j_frag):
195
+ atoms_fragtypes[self.universe.atoms.fragments[i_frag].ix] = j_frag
196
+ flag_fragtype_exists = True
197
+ if not flag_fragtype_exists:
198
+ atoms_fragtypes[self.universe.atoms.fragments[i_frag].ix] = ctr_fragtype
199
+ frag_unique_atomtypes.append(
200
+ self.universe.atoms.types[self.universe.atoms.fragments[i_frag].ix]
201
+ )
202
+ ctr_fragtype += 1
203
+ return atoms_fragtypes
204
+
205
+ def calc_molecular_rdf(
206
+ self,
207
+ n_traj_split=10,
208
+ n_prune=1,
209
+ interval_indices=None,
210
+ n_bins=200,
211
+ n_smooth=2,
212
+ max_mols=5000,
213
+ ):
214
+ """
215
+ Calculates the radial distribution functions between for each unique pair of
216
+ molecule types as a function of their center of mass distance.
217
+
218
+ interval_indices: 2D array specifying the groups of the n_traj_split intervals to be averaged
219
+ max_mols: the maximum number of molecules per bead group for calculating the rdf, for efficiency purposes.
220
+ 5k was set after > 50k was giving problems. Should do further testing to see where the appropriate limit should be set.
221
+ """
222
+
223
+ def get_rdf_avg(rdf_results_tmp, rdf_results, interval_indices, n_frames_split):
224
+ split_weights = n_frames_split[np.array(interval_indices)] / np.sum(
225
+ n_frames_split[np.array(interval_indices)]
226
+ )
227
+ assert abs(np.sum(split_weights) - 1.0) < 1e-6
228
+ rdf_values_avg = (
229
+ split_weights[0] * rdf_results_tmp['value'][interval_indices[0]]
230
+ )
231
+ for i_interval, interval in enumerate(interval_indices[1:]):
232
+ assert (
233
+ rdf_results_tmp['types'][interval]
234
+ == rdf_results_tmp['types'][interval - 1]
235
+ )
236
+ assert (
237
+ rdf_results_tmp['variables_name'][interval]
238
+ == rdf_results_tmp['variables_name'][interval - 1]
239
+ )
240
+ assert (
241
+ rdf_results_tmp['bins'][interval]
242
+ == rdf_results_tmp['bins'][interval - 1]
243
+ ).all()
244
+ rdf_values_avg += (
245
+ split_weights[i_interval + 1] * rdf_results_tmp['value'][interval]
246
+ )
247
+ rdf_results['types'].append(rdf_results_tmp['types'][interval_indices[0]])
248
+ rdf_results['variables_name'].append(
249
+ rdf_results_tmp['variables_name'][interval_indices[0]]
250
+ )
251
+ rdf_results['bins'].append(rdf_results_tmp['bins'][interval_indices[0]])
252
+ rdf_results['value'].append(rdf_values_avg)
253
+ rdf_results['frame_start'].append(
254
+ int(rdf_results_tmp['frame_start'][interval_indices[0]])
255
+ )
256
+ rdf_results['frame_end'].append(
257
+ int(rdf_results_tmp['frame_end'][interval_indices[-1]])
258
+ )
259
+
260
+ if self.universe is None:
261
+ return
262
+ trajectory = self.universe.trajectory[0] if self.universe.trajectory else None
263
+ dimensions = getattr(trajectory, 'dimensions', None) if trajectory else None
264
+ if dimensions is None:
265
+ return
266
+
267
+ n_frames = self.universe.trajectory.n_frames
268
+ if n_frames < n_traj_split:
269
+ n_traj_split = 1
270
+ frames_start = np.array([0])
271
+ frames_end = np.array([n_frames])
272
+ n_frames_split = np.array([n_frames])
273
+ interval_indices = [[0]]
274
+ else:
275
+ run_len = int(n_frames / n_traj_split)
276
+ frames_start = np.arange(n_traj_split) * run_len
277
+ frames_end = frames_start + run_len
278
+ frames_end[-1] = n_frames
279
+ n_frames_split = frames_end - frames_start
280
+ assert np.sum(n_frames_split) == n_frames
281
+ if not interval_indices:
282
+ interval_indices = [[i] for i in range(n_traj_split)]
283
+
284
+ bead_groups = self.bead_groups
285
+ if bead_groups is {}:
286
+ return bead_groups
287
+ moltypes = [moltype for moltype in bead_groups.keys()]
288
+ del_list = []
289
+ for i_moltype, moltype in enumerate(moltypes):
290
+ if bead_groups[moltype]._nbeads > max_mols:
291
+ del_list.append(i_moltype)
292
+ self.logger.warning(
293
+ 'The number of molecules of exceeds the maximum of for calculating the rdf. Skipping this molecule type.'
294
+ )
295
+ moltypes = np.delete(moltypes, del_list)
296
+
297
+ min_box_dimension = np.min(self.universe.trajectory[0].dimensions[:3])
298
+ max_rdf_dist = min_box_dimension / 2
299
+
300
+ rdf_results = {}
301
+ rdf_results['n_smooth'] = n_smooth
302
+ rdf_results['types'] = []
303
+ rdf_results['variables_name'] = []
304
+ rdf_results['bins'] = []
305
+ rdf_results['value'] = []
306
+ rdf_results['frame_start'] = []
307
+ rdf_results['frame_end'] = []
308
+ for i, moltype_i in enumerate(moltypes):
309
+ for j, moltype_j in enumerate(moltypes):
310
+ if j > i:
311
+ continue
312
+ elif (
313
+ i == j and bead_groups[moltype_i].positions.shape[0] == 1
314
+ ): # skip if only 1 mol in group
315
+ continue
316
+
317
+ if i == j:
318
+ exclusion_block = (1, 1) # remove self-distance
319
+ else:
320
+ exclusion_block = None
321
+ pair_type = moltype_i + '-' + moltype_j
322
+ rdf_results_tmp = {}
323
+ rdf_results_tmp['types'] = []
324
+ rdf_results_tmp['variables_name'] = []
325
+ rdf_results_tmp['bins'] = []
326
+ rdf_results_tmp['value'] = []
327
+ rdf_results_tmp['frame_start'] = []
328
+ rdf_results_tmp['frame_end'] = []
329
+ for i_interval in range(n_traj_split):
330
+ rdf_results_tmp['types'].append(pair_type)
331
+ rdf_results_tmp['variables_name'].append(['distance'])
332
+ rdf = MDA_RDF.InterRDF(
333
+ bead_groups[moltype_i],
334
+ bead_groups[moltype_j],
335
+ range=(0, max_rdf_dist),
336
+ exclusion_block=exclusion_block,
337
+ nbins=n_bins,
338
+ ).run(frames_start[i_interval], frames_end[i_interval], n_prune)
339
+ rdf_results_tmp['frame_start'].append(frames_start[i_interval])
340
+ rdf_results_tmp['frame_end'].append(frames_end[i_interval])
341
+
342
+ rdf_results_tmp['bins'].append(
343
+ rdf.results.bins[int(n_smooth / 2) : -int(n_smooth / 2)]
344
+ * ureg.angstrom
345
+ )
346
+ rdf_results_tmp['value'].append(
347
+ np.convolve(
348
+ rdf.results.rdf,
349
+ np.ones((n_smooth,)) / n_smooth,
350
+ mode='same',
351
+ )[int(n_smooth / 2) : -int(n_smooth / 2)]
352
+ )
353
+
354
+ for interval_group in interval_indices:
355
+ get_rdf_avg(
356
+ rdf_results_tmp, rdf_results, interval_group, n_frames_split
357
+ )
358
+
359
+ return rdf_results
360
+
361
+ @property
362
+ def with_trajectory(self):
363
+ """
364
+ True if trajectory is present.
365
+ """
366
+ return (
367
+ self.universe.trajectory is not None and len(self.universe.trajectory) > 0
368
+ )
369
+
370
+ def get_frame(self, frame_index):
371
+ """
372
+ Returns the frame in the trajectory with index frame_index.
373
+ """
374
+ try:
375
+ return self.universe.trajectory[frame_index]
376
+ except Exception as e:
377
+ self.logger.warning('Error accessing frame.', exc_info=e)
378
+
379
+ def get_n_atoms(self, frame_index):
380
+ """
381
+ Returns the number of atoms of the frame with index frame_index.
382
+ """
383
+ frame = self.get_frame(frame_index)
384
+ return len(frame) if frame is not None else None
385
+
386
+ def get_atom_labels(self, frame_index):
387
+ """
388
+ Returns the number of atoms of the frame with index frame_index.
389
+ """
390
+ # MDAnalysis assumes no change in atom configuration
391
+ return [
392
+ guess_atom_element(name).title()
393
+ for name in self.get('atoms_info', {}).get('names', [])
394
+ ]
395
+
396
+ def get_time(self, frame_index):
397
+ """
398
+ Returns the elapsed simulated physical time since the start of the simulation for index frame_index.
399
+ """
400
+ frame = self.get_frame(frame_index)
401
+ return frame.time * ureg.picosecond if frame is not None else None
402
+
403
+ def get_step(self, frame_index):
404
+ """
405
+ Returns the step of the frame with index frame_index.
406
+ """
407
+ frame = self.get_frame(frame_index)
408
+ dt = frame.dt if frame.dt else getattr(self.universe.trajectory, 'dt')
409
+ if not dt:
410
+ return
411
+ if frame:
412
+ return round(frame.time / dt)
413
+
414
+ def get_lattice_vectors(self, frame_index):
415
+ """
416
+ Returns the lattice vectors of the frame with index frame_index.
417
+ """
418
+ lattice_vectors = self.get_frame(frame_index).triclinic_dimensions
419
+ return lattice_vectors * ureg.angstrom if lattice_vectors is not None else None
420
+
421
+ def get_pbc(self, frame_index):
422
+ """
423
+ Returns the lattice periodicity of the frame with index frame_index.
424
+ """
425
+ lattice_vectors = self.get_lattice_vectors(frame_index)
426
+ return [True] * 3 if lattice_vectors is not None else [False] * 3
427
+
428
+ def get_positions(self, frame_index):
429
+ """
430
+ Returns the positions of the atoms of the frame with index frame_index.
431
+ """
432
+ frame = self.get_frame(frame_index)
433
+ return frame.positions * ureg.angstrom if frame.has_positions else None
434
+
435
+ def get_velocities(self, frame_index):
436
+ """
437
+ Returns the velocities of the atoms of the frame with index frame_index.
438
+ """
439
+ frame = self.get_frame(frame_index)
440
+ return (
441
+ frame.velocities * ureg.angstrom / ureg.ps if frame.has_velocities else None
442
+ )
443
+
444
+ def get_forces(self, frame_index):
445
+ """
446
+ Returns the forces on the atoms of the frame with index frame_index.
447
+ """
448
+ frame = self.get_frame(frame_index)
449
+ return (
450
+ frame.forces * ureg.kJ / (MOL * ureg.angstrom) if frame.has_forces else None
451
+ )
452
+
453
+ def get_interactions(self):
454
+ interactions = self.get('interactions', None)
455
+ if interactions is not None:
456
+ return interactions
457
+
458
+ interaction_types = ['angles', 'bonds', 'dihedrals', 'impropers']
459
+ interactions = []
460
+ for interaction_type in interaction_types:
461
+ try:
462
+ interaction = getattr(self.universe, interaction_type)
463
+ except Exception:
464
+ continue
465
+
466
+ for inter in interaction:
467
+ atom_labels = None
468
+ try:
469
+ atom_labels = [
470
+ self.universe.atoms[ind].type for ind in inter.indices
471
+ ]
472
+ except Exception:
473
+ self.logger.warning('Could not assign atom labels to interactions.')
474
+ interactions.append(
475
+ dict(
476
+ atom_labels=atom_labels,
477
+ # parameters=float(inter.value()), ## This is not the parameter but rather the value of the interaction order parameter for a single frame
478
+ # TODO implement functions to get parameters for individual parsers
479
+ atom_indices=inter.indices,
480
+ type=inter.btype,
481
+ )
482
+ )
483
+
484
+ self._results['interactions'] = interactions
485
+
486
+ return interactions
487
+
488
+ def __calc_diffusion_constant(self, times: NDArray, values: NDArray, dim: int = 3):
489
+ """
490
+ Determines the diffusion constant from a fit of the mean squared displacement
491
+ vs. time according to the Einstein relation.
492
+ """
493
+ linear_model = linregress(times, values)
494
+ slope = linear_model.slope
495
+ error = linear_model.rvalue
496
+ return slope * 1 / (2 * dim), error
497
+
498
+ def calc_molecular_mean_squared_displacements(self, max_mols=5000):
499
+ """
500
+ Calculates the mean squared displacement for the center of mass of each
501
+ molecule type.
502
+
503
+ max_mols: the maximum number of molecules per bead group for calculating the msd, for efficiency purposes.
504
+ 1M is arbitrary, 50k was tested and is very fast and does not seem to have any memory issues.
505
+ """
506
+
507
+ def mean_squared_displacement(start: np.ndarray, current: np.ndarray):
508
+ """
509
+ Calculates mean square displacement between current and initial (start) coordinates.
510
+ """
511
+ vec = start - current
512
+ return (vec**2).sum(axis=1).mean()
513
+
514
+ if self.universe is None:
515
+ return
516
+ trajectory = self.universe.trajectory[0] if self.universe.trajectory else None
517
+ dimensions = getattr(trajectory, 'dimensions', None) if trajectory else None
518
+ if dimensions is None:
519
+ return
520
+
521
+ n_frames = self.universe.trajectory.n_frames
522
+ if n_frames < 50:
523
+ self.logger.warning(
524
+ 'At least 50 frames required to calculate molecular' # noqa: PLE1205
525
+ ' mean squared displacements, skipping.',
526
+ )
527
+ return
528
+
529
+ dt = getattr(self.universe.trajectory, 'dt')
530
+ if dt is None:
531
+ return
532
+ times = np.arange(n_frames) * dt
533
+
534
+ bead_groups = self.bead_groups
535
+ if bead_groups is {}:
536
+ return bead_groups
537
+
538
+ moltypes = [moltype for moltype in bead_groups.keys()]
539
+ del_list = []
540
+ for i_moltype, moltype in enumerate(moltypes):
541
+ if bead_groups[moltype]._nbeads > max_mols:
542
+ try:
543
+ # select max_mols nr. of rnd molecules from this moltype
544
+ moltype_indices = np.array(
545
+ [atom._ix for atom in bead_groups[moltype]._atoms]
546
+ )
547
+ molnums = self.universe.atoms.molnums[moltype_indices]
548
+ molnum_types = np.unique(molnums)
549
+ molnum_types_rnd = np.sort(
550
+ np.random.choice(molnum_types, size=max_mols)
551
+ )
552
+ atom_indices_rnd = moltype_indices[
553
+ np.concatenate(
554
+ [
555
+ np.where(molnums == molnum)[0]
556
+ for molnum in molnum_types_rnd
557
+ ]
558
+ )
559
+ ]
560
+ selection = ' '.join([str(i) for i in atom_indices_rnd])
561
+ selection = f'index {selection}'
562
+ ags_moltype_rnd = self.universe.select_atoms(selection)
563
+ bead_groups[moltype] = BeadGroup(
564
+ ags_moltype_rnd, compound='fragments'
565
+ )
566
+ self.logger.warning(
567
+ 'Maximum number of molecules for calculating the msd has been reached.'
568
+ ' Will make a random selection for calculation.'
569
+ )
570
+ except Exception:
571
+ self.logger.warning(
572
+ 'Tried to select random molecules for large group when calculating msd, but something went wrong. Skipping this molecule type.'
573
+ )
574
+ del_list.append(i_moltype)
575
+ moltypes = np.delete(moltypes, del_list)
576
+
577
+ msd_results = {}
578
+ msd_results['value'] = []
579
+ msd_results['times'] = []
580
+ msd_results['diffusion_constant'] = []
581
+ msd_results['error_diffusion_constant'] = []
582
+ for moltype in moltypes:
583
+ positions = self.get_nojump_positions(bead_groups[moltype])
584
+ results = shifted_correlation_average(
585
+ mean_squared_displacement, times, positions
586
+ )
587
+ msd_results['value'].append(results[1])
588
+ msd_results['times'].append(results[0])
589
+ diffusion_constant, error = self.__calc_diffusion_constant(*results)
590
+ msd_results['diffusion_constant'].append(diffusion_constant)
591
+ msd_results['error_diffusion_constant'].append(error)
592
+
593
+ msd_results['types'] = moltypes
594
+ msd_results['times'] = np.array(msd_results['times']) * ureg.picosecond
595
+ msd_results['value'] = np.array(msd_results['value']) * ureg.angstrom**2
596
+ msd_results['diffusion_constant'] = (
597
+ np.array(msd_results['diffusion_constant'])
598
+ * ureg.angstrom**2
599
+ / ureg.picosecond
600
+ )
601
+ msd_results['error_diffusion_constant'] = np.array(
602
+ msd_results['error_diffusion_constant']
603
+ )
604
+
605
+ return msd_results
606
+
607
+ def parse_jumps(self, selection):
608
+ __ = self.universe.trajectory[0]
609
+ prev = np.array(selection.positions)
610
+ box = self.universe.trajectory[0].dimensions[:3]
611
+ sparse_data = namedtuple('SparseData', ['data', 'row', 'col'])
612
+ jump_data = (
613
+ sparse_data(data=array('b'), row=array('l'), col=array('l')),
614
+ sparse_data(data=array('b'), row=array('l'), col=array('l')),
615
+ sparse_data(data=array('b'), row=array('l'), col=array('l')),
616
+ )
617
+
618
+ for i_frame, _ in enumerate(self.universe.trajectory[1:]):
619
+ curr = np.array(selection.positions)
620
+ delta = ((curr - prev) / box).round().astype(np.int8)
621
+ prev = np.array(curr)
622
+ for d in range(3):
623
+ (col,) = np.where(delta[:, d] != 0)
624
+ jump_data[d].col.extend(col)
625
+ jump_data[d].row.extend([i_frame] * len(col))
626
+ jump_data[d].data.extend(delta[col, d])
627
+
628
+ return jump_data
629
+
630
+ def generate_nojump_matrices(self, selection):
631
+ jump_data = self.parse_jumps(selection)
632
+ N = len(self.universe.trajectory)
633
+ M = selection.positions.shape[0]
634
+
635
+ nojump_matrices = tuple(
636
+ sparse.csr_matrix((np.array(m.data), (m.row, m.col)), shape=(N, M))
637
+ for m in jump_data
638
+ )
639
+ return nojump_matrices
640
+
641
+ def get_nojump_positions(self, selection):
642
+ nojump_matrices = self.generate_nojump_matrices(selection)
643
+ box = self.universe.trajectory[0].dimensions[:3]
644
+
645
+ nojump_positions = []
646
+ for i_frame, __ in enumerate(self.universe.trajectory):
647
+ delta = (
648
+ np.array(
649
+ np.vstack([m[:i_frame, :].sum(axis=0) for m in nojump_matrices]).T
650
+ )
651
+ * box
652
+ )
653
+ nojump_positions.append(selection.positions - delta)
654
+
655
+ return np.array(nojump_positions)
656
+
657
+ def clean(self):
658
+ for name in os.listdir(self.maindir):
659
+ if name.startswith('.') and (
660
+ name.endswith('.lock') or name.endswith('.npz')
661
+ ):
662
+ os.remove(os.path.join(self.maindir, name))