nomad-schema-plugin-simulation-workflow 1.0.1__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.
@@ -0,0 +1,2601 @@
1
+ #
2
+ # Copyright The NOMAD Authors.
3
+ #
4
+ # This file is part of NOMAD. See https://nomad-lab.eu for further info.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ from typing import List, Dict, Any, Callable
19
+ from itertools import chain
20
+ from collections import namedtuple
21
+ import numpy as np
22
+ from array import array
23
+ from nptyping import Int, NDArray
24
+ from scipy import sparse
25
+ from scipy.stats import linregress
26
+ import networkx
27
+ import MDAnalysis
28
+ from MDAnalysis.core.topology import Topology
29
+ from MDAnalysis.core.universe import Universe
30
+ import MDAnalysis.analysis.rdf as MDA_RDF
31
+ from MDAnalysis.core._get_readers import get_reader_for
32
+
33
+ from nomad.datamodel.data import ArchiveSection
34
+ from nomad.metainfo import (
35
+ SubSection,
36
+ Section,
37
+ Quantity,
38
+ MEnum,
39
+ Reference,
40
+ MSection,
41
+ )
42
+ from nomad.datamodel.hdf5 import HDF5Dataset
43
+ from nomad.datamodel.metainfo.workflow import Link
44
+ from runschema.system import System, AtomsGroup
45
+ from runschema.calculation import (
46
+ RadiusOfGyration as RadiusOfGyrationCalculation,
47
+ RadiusOfGyrationValues as RadiusOfGyrationValuesCalculation,
48
+ )
49
+ from nomad.utils import get_logger
50
+ from nomad.units import ureg
51
+ from nomad import atomutils
52
+ from .general import (
53
+ SimulationWorkflowMethod,
54
+ SimulationWorkflowResults,
55
+ SerialSimulation,
56
+ WORKFLOW_METHOD_NAME,
57
+ WORKFLOW_RESULTS_NAME,
58
+ )
59
+ from .thermodynamics import ThermodynamicsResults
60
+
61
+
62
+ LOGGER = get_logger(__name__)
63
+
64
+
65
+ class BeadGroup(object):
66
+ # see https://github.com/MDAnalysis/mdanalysis/issues/1891#issuecomment-387138110
67
+ # by @richardjgowers with performance improvements
68
+ def __init__(self, atoms, compound='fragments'):
69
+ """Initialize with an AtomGroup instance.
70
+ Will split based on keyword 'compounds' (residues or fragments).
71
+ """
72
+ self._atoms = atoms
73
+ self.compound = compound
74
+ self._nbeads = len(getattr(self._atoms, self.compound))
75
+ # for caching
76
+ self._cache = {}
77
+ self._cache['positions'] = None
78
+ self.__last_frame = None
79
+
80
+ def __len__(self):
81
+ return self._nbeads
82
+
83
+ @property
84
+ def positions(self):
85
+ # cache positions for current frame
86
+ if self.universe.trajectory.frame != self.__last_frame:
87
+ self._cache['positions'] = self._atoms.center_of_mass(
88
+ unwrap=True, compound=self.compound
89
+ )
90
+ self.__last_frame = self.universe.trajectory.frame
91
+ return self._cache['positions']
92
+
93
+ @property # type: ignore
94
+ @MDAnalysis.lib.util.cached('universe')
95
+ def universe(self):
96
+ return self._atoms.universe
97
+
98
+
99
+ def get_bond_list_from_model_contributions(
100
+ sec_run: MSection, method_index: int = -1, model_index: int = -1
101
+ ) -> List[tuple]:
102
+ """
103
+ Generates bond list of tuples using the list of bonded force field interactions stored under run[].method[].force_field.model[].
104
+
105
+ bond_list: List[tuple]
106
+ """
107
+ contributions = []
108
+ if sec_run.m_xpath(
109
+ f'method[{method_index}].force_field.model[{model_index}].contributions'
110
+ ):
111
+ contributions = (
112
+ sec_run.method[method_index].force_field.model[model_index].contributions
113
+ )
114
+ bond_list = []
115
+ for contribution in contributions:
116
+ if contribution.type != 'bond':
117
+ continue
118
+
119
+ atom_indices = contribution.atom_indices
120
+ if (
121
+ contribution.n_interactions
122
+ ): # all bonds have been grouped into one contribution
123
+ bond_list = [tuple(indices) for indices in atom_indices]
124
+ else:
125
+ bond_list.append(tuple(contribution.atom_indices))
126
+
127
+ return bond_list
128
+
129
+
130
+ def create_empty_universe(
131
+ n_atoms: int,
132
+ n_frames: int = 1,
133
+ n_residues: int = 1,
134
+ n_segments: int = 1,
135
+ atom_resindex: NDArray[Int] = None,
136
+ residue_segindex: NDArray[Int] = None,
137
+ flag_trajectory: bool = False,
138
+ flag_velocities: bool = False,
139
+ flag_forces: bool = False,
140
+ timestep: float = None,
141
+ ) -> MDAnalysis.Universe:
142
+ """Create a blank Universe
143
+
144
+ This function was adapted from the function empty() within the MDA class Universe().
145
+ The only difference is that the Universe() class is imported directly here, whereas in the
146
+ original function is is passed as a function argument, since the function there is a classfunction.
147
+
148
+ Useful for building a Universe without requiring existing files,
149
+ for example for system building.
150
+
151
+ If `flag_trajectory` is set to True, a
152
+ :class:`MDAnalysis.coordinates.memory.MemoryReader` will be
153
+ attached to the Universe.
154
+
155
+ Parameters
156
+ ----------
157
+ n_atoms: int
158
+ number of Atoms in the Universe
159
+ n_residues: int, default 1
160
+ number of Residues in the Universe, defaults to 1
161
+ n_segments: int, default 1
162
+ number of Segments in the Universe, defaults to 1
163
+ atom_resindex: array like, optional
164
+ mapping of atoms to residues, e.g. with 6 atoms,
165
+ `atom_resindex=[0, 0, 1, 1, 2, 2]` would put 2 atoms
166
+ into each of 3 residues.
167
+ residue_segindex: array like, optional
168
+ mapping of residues to segments
169
+ flag_trajectory: bool, optional
170
+ if True, attaches a :class:`MDAnalysis.coordinates.memory.MemoryReader`
171
+ allowing coordinates to be set and written. Default is False
172
+ flag_velocities: bool, optional
173
+ include velocities in the :class:`MDAnalysis.coordinates.memory.MemoryReader`
174
+ flag_forces: bool, optional
175
+ include forces in the :class:`MDAnalysis.coordinates.memory.MemoryReader`
176
+
177
+ Returns
178
+ -------
179
+ MDAnalysis.Universe object
180
+
181
+ Examples
182
+ --------
183
+ For example to create a new Universe with 6 atoms in 2 residues, with
184
+ positions for the atoms and a mass attribute:
185
+
186
+ >>> u = mda.Universe.empty(6, 2,
187
+ atom_resindex=np.array([0, 0, 0, 1, 1, 1]),
188
+ flag_trajectory=True,
189
+ )
190
+ >>> u.add_TopologyAttr('masses')
191
+
192
+ .. versionadded:: 0.17.0
193
+ .. versionchanged:: 0.19.0
194
+ The attached Reader when flag_trajectory=True is now a MemoryReader
195
+ .. versionchanged:: 1.0.0
196
+ Universes can now be created with 0 atoms
197
+ """
198
+
199
+ if not n_atoms:
200
+ n_residues = 0
201
+ n_segments = 0
202
+
203
+ if atom_resindex is None:
204
+ LOGGER.warn(
205
+ 'Residues specified but no atom_resindex given. '
206
+ 'All atoms will be placed in first Residue.',
207
+ )
208
+
209
+ if residue_segindex is None:
210
+ LOGGER.warn(
211
+ 'Segments specified but no segment_resindex given. '
212
+ 'All residues will be placed in first Segment',
213
+ )
214
+
215
+ topology = Topology(
216
+ n_atoms,
217
+ n_residues,
218
+ n_segments,
219
+ atom_resindex=atom_resindex,
220
+ residue_segindex=residue_segindex,
221
+ )
222
+
223
+ universe = Universe(topology)
224
+
225
+ if flag_trajectory:
226
+ coords = np.zeros((n_frames, n_atoms, 3), dtype=np.float32)
227
+ vels = np.zeros_like(coords) if flag_velocities else None
228
+ forces = np.zeros_like(coords) if flag_forces else None
229
+
230
+ # grab and attach a MemoryReader
231
+ universe.trajectory = get_reader_for(coords)(
232
+ coords,
233
+ order='fac',
234
+ n_atoms=n_atoms,
235
+ velocities=vels,
236
+ forces=forces,
237
+ dt=timestep,
238
+ )
239
+
240
+ return universe
241
+
242
+
243
+ def archive_to_universe(
244
+ archive,
245
+ system_index: int = 0,
246
+ method_index: int = -1,
247
+ model_index: int = -1,
248
+ ) -> MDAnalysis.Universe:
249
+ """Extract the topology from a provided run section of an archive entry
250
+
251
+ Input:
252
+
253
+ archive_sec_run: section run of an EntryArchive
254
+
255
+ system_index: list index of archive.run[].system to be used for topology extraction
256
+
257
+ method_index: list index of archive.run[].method to be used for atom parameter (charges and masses) extraction
258
+
259
+ model_index: list index of archive.run[].method[].force_field.model for bond list extraction
260
+
261
+ Variables:
262
+
263
+ n_frames (int):
264
+
265
+ n_atoms (int):
266
+
267
+ atom_names (str, shape=(n_atoms)):
268
+
269
+ atom_types (str, shape=(n_atoms)):
270
+
271
+ atom_resindex (str, shape=(n_atoms)):
272
+
273
+ atom_segids (str, shape=(n_atoms)):
274
+
275
+ n_segments (int): Segments correspond to a group of the same type of molecules.
276
+
277
+ n_residues (int): The number of distinct residues (nb - individual molecules are also denoted as a residue).
278
+
279
+ resnames (str, shape=(n_residues)): The name of each residue.
280
+
281
+ residue_segindex (int, shape=(n_residues)): The segment index that each residue belongs to.
282
+
283
+ residue_molnums (int, shape=(n_residues)): The molecule index that each residue belongs to.
284
+
285
+ residue_moltypes (int, shape=(n_residues)): The molecule type of each residue.
286
+
287
+ n_molecules (int):
288
+
289
+ masses (float, shape=(n_atoms)): atom masses, units = amu
290
+
291
+ charges (float, shape=(n_atoms)): atom partial charges, units = e
292
+
293
+ positions (float, shape=(n_frames,n_atoms,3)): atom positions
294
+
295
+ velocities (float, shape=(n_frames,n_atoms,3)): atom velocities
296
+
297
+ dimensions (float, shape=(n_frames,6)): box dimensions (nb - currently assuming a cubic box!)
298
+
299
+ bonds (tuple, shape=([])): list of tuples with the atom indices of each bond
300
+ """
301
+
302
+ try:
303
+ sec_run = archive.run[-1]
304
+ sec_system = sec_run.system
305
+ sec_system_top = sec_run.system[system_index]
306
+ sec_atoms = sec_system_top.atoms
307
+ sec_atoms_group = sec_system_top.atoms_group
308
+ sec_calculation = sec_run.calculation
309
+ sec_method = (
310
+ sec_run.method[method_index] if sec_run.get('method') is not None else None
311
+ )
312
+ except IndexError:
313
+ LOGGER.warning(
314
+ 'Supplied indices or necessary sections do not exist in archive. Cannot build the MDA universe.'
315
+ )
316
+ return None
317
+
318
+ n_atoms = sec_atoms.get('n_atoms')
319
+ if n_atoms is None:
320
+ LOGGER.warning('No atoms found in the archive. Cannot build the MDA universe.')
321
+ return None
322
+
323
+ n_frames = len(sec_system) if sec_system is not None else None
324
+ atom_names = sec_atoms.get('labels')
325
+ model_atom_parameters = sec_method.get('atom_parameters')
326
+ atom_types = (
327
+ [atom.label for atom in model_atom_parameters]
328
+ if model_atom_parameters
329
+ else atom_names
330
+ )
331
+ atom_resindex = np.arange(n_atoms)
332
+ atoms_segindices = np.empty(n_atoms)
333
+ atom_segids = np.array(range(n_atoms), dtype='object')
334
+ molecule_groups = sec_atoms_group
335
+ n_segments = len(molecule_groups)
336
+
337
+ n_residues = 0
338
+ n_molecules = 0
339
+ residue_segindex = []
340
+ resnames = []
341
+ residue_moltypes = []
342
+ residue_min_atom_index = []
343
+ residue_n_atoms = []
344
+ molecule_n_res = []
345
+ for mol_group_ind, mol_group in enumerate(molecule_groups):
346
+ atoms_segindices[mol_group.atom_indices] = mol_group_ind
347
+ atom_segids[mol_group.atom_indices] = mol_group.label
348
+ molecules = mol_group.atoms_group if mol_group.atoms_group is not None else []
349
+ for mol in molecules:
350
+ monomer_groups = mol.atoms_group
351
+ mol_res_counter = 0
352
+ if monomer_groups:
353
+ for mon_group in monomer_groups:
354
+ monomers = mon_group.atoms_group
355
+ for mon in monomers:
356
+ resnames.append(mon.label)
357
+ residue_segindex.append(mol_group_ind)
358
+ residue_moltypes.append(mol.label)
359
+ residue_min_atom_index.append(np.min(mon.atom_indices))
360
+ residue_n_atoms.append(len(mon.atom_indices))
361
+ n_residues += 1
362
+ mol_res_counter += 1
363
+ else: # no monomers => whole molecule is it's own residue
364
+ resnames.append(mol.label)
365
+ residue_segindex.append(mol_group_ind)
366
+ residue_moltypes.append(mol.label)
367
+ residue_min_atom_index.append(np.min(mol.atom_indices))
368
+ residue_n_atoms.append(len(mol.atom_indices))
369
+ n_residues += 1
370
+ mol_res_counter += 1
371
+ molecule_n_res.append(mol_res_counter)
372
+ n_molecules += 1
373
+
374
+ # reorder the residues by atom_indices
375
+ residue_data = np.array(
376
+ [
377
+ [
378
+ residue_min_atom_index[i],
379
+ residue_n_atoms[i],
380
+ residue_segindex[i],
381
+ residue_moltypes[i],
382
+ resnames[i],
383
+ ]
384
+ for i in range(len(residue_min_atom_index))
385
+ ],
386
+ dtype=object,
387
+ )
388
+ residue_data = np.array(sorted(residue_data, key=lambda x: x[0], reverse=False)).T
389
+ residue_n_atoms = residue_data[1].astype(int)
390
+ residue_segindex = residue_data[2].astype(int)
391
+ residue_moltypes = residue_data[3]
392
+ resnames = residue_data[4]
393
+ res_index_counter = 0
394
+ for i_residue, res_n_atoms in enumerate(residue_n_atoms):
395
+ atom_resindex[res_index_counter : res_index_counter + res_n_atoms] = i_residue # type: ignore
396
+ res_index_counter += res_n_atoms
397
+ residue_molnums = np.array(range(n_residues))
398
+ mol_index_counter = 0
399
+ for i_molecule, n_res in enumerate(molecule_n_res):
400
+ residue_molnums[mol_index_counter : mol_index_counter + n_res] = i_molecule
401
+ mol_index_counter += n_res
402
+
403
+ # get the atom masses and charges
404
+
405
+ masses = np.empty(n_atoms)
406
+ charges = np.empty(n_atoms)
407
+ atom_parameters = (
408
+ sec_method.get('atom_parameters') if sec_method is not None else []
409
+ )
410
+ atom_parameters = atom_parameters if atom_parameters is not None else []
411
+
412
+ for atom_ind, atom in enumerate(atom_parameters):
413
+ if atom.get('mass'):
414
+ masses[atom_ind] = ureg.convert(
415
+ atom.mass.magnitude, atom.mass.units, ureg.amu
416
+ )
417
+ if atom.get('charge'):
418
+ charges[atom_ind] = ureg.convert(
419
+ atom.charge.magnitude, atom.charge.units, ureg.e
420
+ )
421
+
422
+ # get the atom positions, velocites, and box dimensions
423
+ positions = np.empty(shape=(n_frames, n_atoms, 3))
424
+ velocities = np.empty(shape=(n_frames, n_atoms, 3))
425
+ dimensions = np.empty(shape=(n_frames, 6))
426
+ for frame_ind, frame in enumerate(sec_system):
427
+ sec_atoms_fr = frame.get('atoms')
428
+ if sec_atoms_fr is not None:
429
+ positions_frame = sec_atoms_fr.positions
430
+ positions[frame_ind] = (
431
+ ureg.convert(
432
+ positions_frame.magnitude, positions_frame.units, ureg.angstrom
433
+ )
434
+ if positions_frame is not None
435
+ else None
436
+ )
437
+ velocities_frame = sec_atoms_fr.velocities
438
+ velocities[frame_ind] = (
439
+ ureg.convert(
440
+ velocities_frame.magnitude,
441
+ velocities_frame.units,
442
+ ureg.angstrom / ureg.picosecond,
443
+ )
444
+ if velocities_frame is not None
445
+ else None
446
+ )
447
+ latt_vec_tmp = sec_atoms_fr.get('lattice_vectors')
448
+ if latt_vec_tmp is not None:
449
+ length_conversion = ureg.convert(
450
+ 1.0, sec_atoms_fr.lattice_vectors.units, ureg.angstrom
451
+ )
452
+ dimensions[frame_ind] = [
453
+ sec_atoms_fr.lattice_vectors.magnitude[0][0] * length_conversion,
454
+ sec_atoms_fr.lattice_vectors.magnitude[1][1] * length_conversion,
455
+ sec_atoms_fr.lattice_vectors.magnitude[2][2] * length_conversion,
456
+ 90,
457
+ 90,
458
+ 90,
459
+ ] # TODO: extend to non-cubic boxes
460
+
461
+ # get the bonds # TODO extend to multiple storage options for interactions
462
+ bonds = sec_atoms.bond_list
463
+ if bonds is None:
464
+ bonds = get_bond_list_from_model_contributions(
465
+ sec_run, method_index=-1, model_index=-1
466
+ )
467
+
468
+ # get the system times
469
+ system_timestep = 1.0 * ureg.picosecond
470
+
471
+ def approx(a, b, rel_tol=1e-09, abs_tol=0.0):
472
+ return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
473
+
474
+ system_times = [calc.time for calc in sec_calculation if calc.system_ref]
475
+ if system_times:
476
+ try:
477
+ method = archive.workflow2.method
478
+ system_timestep = (
479
+ method.integration_timestep * method.coordinate_save_frequency
480
+ )
481
+ except Exception:
482
+ LOGGER.warning(
483
+ 'Cannot find the system times. MDA universe will contain non-physical times and timestep.'
484
+ )
485
+ else:
486
+ time_steps = [
487
+ system_times[i_time] - system_times[i_time - 1]
488
+ for i_time in range(1, len(system_times))
489
+ ]
490
+ if all(approx(time_steps[0], time_step) for time_step in time_steps):
491
+ system_timestep = ureg.convert(
492
+ time_steps[0].magnitude, ureg.second, ureg.picosecond
493
+ )
494
+ else:
495
+ LOGGER.warning(
496
+ 'System times are not equally spaced. Cannot set system times in MDA universe.'
497
+ ' MDA universe will contain non-physical times and timestep.'
498
+ )
499
+
500
+ system_timestep = ureg.convert(
501
+ system_timestep, system_timestep._units, ureg.picoseconds
502
+ )
503
+
504
+ # create the Universe
505
+ metainfo_universe = create_empty_universe(
506
+ n_atoms,
507
+ n_frames=n_frames,
508
+ n_residues=n_residues,
509
+ n_segments=n_segments,
510
+ atom_resindex=atom_resindex,
511
+ residue_segindex=residue_segindex,
512
+ flag_trajectory=True,
513
+ flag_velocities=True,
514
+ timestep=system_timestep.magnitude,
515
+ )
516
+
517
+ # set the positions and velocities
518
+ for frame_ind, frame in enumerate(metainfo_universe.trajectory):
519
+ metainfo_universe.atoms.positions = positions[frame_ind]
520
+ metainfo_universe.atoms.velocities = velocities[frame_ind]
521
+
522
+ # add the atom attributes
523
+ metainfo_universe.add_TopologyAttr('name', atom_names)
524
+ metainfo_universe.add_TopologyAttr('type', atom_types)
525
+ metainfo_universe.add_TopologyAttr('mass', masses)
526
+ metainfo_universe.add_TopologyAttr('charge', charges)
527
+ if n_segments != 0:
528
+ metainfo_universe.add_TopologyAttr('segids', np.unique(atom_segids))
529
+ if n_residues != 0:
530
+ metainfo_universe.add_TopologyAttr('resnames', resnames)
531
+ metainfo_universe.add_TopologyAttr('resids', np.unique(atom_resindex) + 1)
532
+ metainfo_universe.add_TopologyAttr('resnums', np.unique(atom_resindex) + 1)
533
+ if len(residue_molnums) > 0:
534
+ metainfo_universe.add_TopologyAttr('molnums', residue_molnums)
535
+ if len(residue_moltypes) > 0:
536
+ metainfo_universe.add_TopologyAttr('moltypes', residue_moltypes)
537
+
538
+ # add the box dimensions
539
+ for frame_ind, frame in enumerate(metainfo_universe.trajectory):
540
+ metainfo_universe.atoms.dimensions = dimensions[frame_ind]
541
+
542
+ # add the bonds
543
+ if hasattr(metainfo_universe, 'bonds'):
544
+ LOGGER.warning('archive_to_universe() failed, universe already has bonds.')
545
+ return None
546
+ metainfo_universe.add_TopologyAttr('bonds', bonds)
547
+
548
+ return metainfo_universe
549
+
550
+
551
+ def _get_molecular_bead_groups(
552
+ universe: MDAnalysis.Universe, moltypes: List[str] = None
553
+ ) -> Dict[str, BeadGroup]:
554
+ """
555
+ Creates bead groups based on the molecular types as defined by the MDAnalysis universe.
556
+ """
557
+ if moltypes is None:
558
+ atoms_moltypes = getattr(universe.atoms, 'moltypes', [])
559
+ moltypes = np.unique(atoms_moltypes)
560
+ bead_groups = {}
561
+ for moltype in moltypes:
562
+ ags_by_moltype = universe.select_atoms('moltype ' + moltype)
563
+ ags_by_moltype = ags_by_moltype[
564
+ ags_by_moltype.masses > abs(1e-2)
565
+ ] # remove any virtual/massless sites (needed for, e.g., 4-bead water models)
566
+ bead_groups[moltype] = BeadGroup(ags_by_moltype, compound='fragments')
567
+
568
+ return bead_groups
569
+
570
+
571
+ def calc_molecular_rdf(
572
+ universe: MDAnalysis.Universe,
573
+ n_traj_split: int = 10,
574
+ n_prune: int = 1,
575
+ interval_indices=None,
576
+ max_mols: int = 5000,
577
+ ) -> Dict:
578
+ """
579
+ Calculates the radial distribution functions between for each unique pair of
580
+ molecule types as a function of their center of mass distance.
581
+
582
+ interval_indices: 2D array specifying the groups of the n_traj_split intervals to be averaged
583
+ max_mols: the maximum number of molecules per bead group for calculating the rdf, for efficiency purposes.
584
+ """
585
+ # TODO 5k default for max_mols was set after > 50k was giving problems. Should do further testing to see where the appropriate limit should be set.
586
+ if (
587
+ not universe
588
+ or not universe.trajectory
589
+ or universe.trajectory[0].dimensions is None
590
+ ):
591
+ return {}
592
+
593
+ n_frames = universe.trajectory.n_frames
594
+ if n_frames < n_traj_split:
595
+ n_traj_split = 1
596
+ frames_start = np.array([0])
597
+ frames_end = np.array([n_frames])
598
+ n_frames_split = np.array([n_frames])
599
+ interval_indices = [[0]]
600
+ else:
601
+ run_len = int(n_frames / n_traj_split)
602
+ frames_start = np.arange(n_traj_split) * run_len
603
+ frames_end = frames_start + run_len
604
+ frames_end[-1] = n_frames
605
+ n_frames_split = frames_end - frames_start
606
+ if np.sum(n_frames_split) != n_frames:
607
+ LOGGER.error(
608
+ 'Something went wrong with input parameters in calc_molecular_rdf().'
609
+ 'Radial distribution functions will not be calculated.'
610
+ )
611
+ return {}
612
+ if not interval_indices:
613
+ interval_indices = [[i] for i in range(n_traj_split)]
614
+
615
+ bead_groups = _get_molecular_bead_groups(universe)
616
+ if not bead_groups:
617
+ return bead_groups
618
+ moltypes = [moltype for moltype in bead_groups.keys()]
619
+ del_list = [
620
+ i_moltype
621
+ for i_moltype, moltype in enumerate(moltypes)
622
+ if bead_groups[moltype]._nbeads > max_mols
623
+ ]
624
+ moltypes = np.delete(moltypes, del_list).tolist()
625
+
626
+ min_box_dimension = np.min(universe.trajectory[0].dimensions[:3])
627
+ max_rdf_dist = min_box_dimension / 2
628
+ n_bins = 200
629
+ n_smooth = 2
630
+
631
+ rdf_results: Dict[str, Any] = {}
632
+ rdf_results['n_smooth'] = n_smooth
633
+ rdf_results['n_prune'] = n_prune
634
+ rdf_results['type'] = 'molecular'
635
+ rdf_results['types'] = []
636
+ rdf_results['variables_name'] = []
637
+ rdf_results['bins'] = []
638
+ rdf_results['value'] = []
639
+ rdf_results['frame_start'] = []
640
+ rdf_results['frame_end'] = []
641
+ for i, moltype_i in enumerate(moltypes):
642
+ for j, moltype_j in enumerate(moltypes):
643
+ if j > i:
644
+ continue
645
+ elif (
646
+ i == j and bead_groups[moltype_i].positions.shape[0] == 1
647
+ ): # skip if only 1 mol in group
648
+ continue
649
+
650
+ if i == j:
651
+ exclusion_block = (1, 1) # remove self-distance
652
+ else:
653
+ exclusion_block = None
654
+ pair_type = f'{moltype_i}-{moltype_j}'
655
+ rdf_results_interval: Dict[str, Any] = {}
656
+ rdf_results_interval['types'] = []
657
+ rdf_results_interval['variables_name'] = []
658
+ rdf_results_interval['bins'] = []
659
+ rdf_results_interval['value'] = []
660
+ rdf_results_interval['frame_start'] = []
661
+ rdf_results_interval['frame_end'] = []
662
+ for i_interval in range(n_traj_split):
663
+ rdf_results_interval['types'].append(pair_type)
664
+ rdf_results_interval['variables_name'].append(['distance'])
665
+ rdf = MDA_RDF.InterRDF(
666
+ bead_groups[moltype_i],
667
+ bead_groups[moltype_j],
668
+ range=(0, max_rdf_dist),
669
+ exclusion_block=exclusion_block,
670
+ nbins=n_bins,
671
+ ).run(frames_start[i_interval], frames_end[i_interval], n_prune)
672
+ rdf_results_interval['frame_start'].append(frames_start[i_interval])
673
+ rdf_results_interval['frame_end'].append(frames_end[i_interval])
674
+
675
+ rdf_results_interval['bins'].append(
676
+ rdf.results.bins[int(n_smooth / 2) : -int(n_smooth / 2)]
677
+ * ureg.angstrom
678
+ )
679
+ rdf_results_interval['value'].append(
680
+ np.convolve(
681
+ rdf.results.rdf, np.ones((n_smooth,)) / n_smooth, mode='same'
682
+ )[int(n_smooth / 2) : -int(n_smooth / 2)]
683
+ )
684
+
685
+ flag_logging_error = False
686
+ for interval_group in interval_indices:
687
+ split_weights = n_frames_split[np.array(interval_group)] / np.sum(
688
+ n_frames_split[np.array(interval_group)]
689
+ )
690
+ if abs(np.sum(split_weights) - 1.0) > 1e-6:
691
+ flag_logging_error = True
692
+ continue
693
+ rdf_values_avg = (
694
+ split_weights[0] * rdf_results_interval['value'][interval_group[0]]
695
+ )
696
+ for i_interval, interval in enumerate(interval_group[1:]):
697
+ if (
698
+ rdf_results_interval['types'][interval]
699
+ != rdf_results_interval['types'][interval - 1]
700
+ ):
701
+ flag_logging_error = True
702
+ continue
703
+ if (
704
+ rdf_results_interval['variables_name'][interval]
705
+ != rdf_results_interval['variables_name'][interval - 1]
706
+ ):
707
+ flag_logging_error = True
708
+ continue
709
+ if not (
710
+ rdf_results_interval['bins'][interval]
711
+ == rdf_results_interval['bins'][interval - 1]
712
+ ).all():
713
+ flag_logging_error = True
714
+ continue
715
+ rdf_values_avg += (
716
+ split_weights[i_interval + 1]
717
+ * rdf_results_interval['value'][interval]
718
+ )
719
+ if flag_logging_error:
720
+ LOGGER.error(
721
+ 'Something went wrong in calc_molecular_rdf(). Some interval groups were skipped.'
722
+ )
723
+ rdf_results['types'].append(
724
+ rdf_results_interval['types'][interval_group[0]]
725
+ )
726
+ rdf_results['variables_name'].append(
727
+ rdf_results_interval['variables_name'][interval_group[0]]
728
+ )
729
+ rdf_results['bins'].append(
730
+ rdf_results_interval['bins'][interval_group[0]]
731
+ )
732
+ rdf_results['value'].append(rdf_values_avg)
733
+ rdf_results['frame_start'].append(
734
+ int(rdf_results_interval['frame_start'][interval_group[0]])
735
+ )
736
+ rdf_results['frame_end'].append(
737
+ int(rdf_results_interval['frame_end'][interval_group[-1]])
738
+ )
739
+
740
+ return rdf_results
741
+
742
+
743
+ def __log_indices(first: int, last: int, num: int = 100):
744
+ ls = np.logspace(0, np.log10(last - first + 1), num=num)
745
+ return np.unique(np.int_(ls) - 1 + first)
746
+
747
+
748
+ def __correlation(function, positions: List[float]):
749
+ iterator = iter(positions)
750
+ start_frame = next(iterator)
751
+ return map(lambda f: function(start_frame, f), chain([start_frame], iterator))
752
+
753
+
754
+ def _calc_diffusion_constant(
755
+ times: NDArray, values: NDArray, dim: int = 3
756
+ ) -> tuple[float, float]:
757
+ """
758
+ Determines the diffusion constant from a fit of the mean squared displacement
759
+ vs. time according to the Einstein relation.
760
+ """
761
+ linear_model = linregress(times, values)
762
+ slope = linear_model.slope
763
+ error = linear_model.rvalue
764
+ return slope * 1 / (2 * dim), error
765
+
766
+
767
+ def shifted_correlation_average(
768
+ function: Callable,
769
+ times: NDArray,
770
+ positions: NDArray,
771
+ index_distribution: Callable = __log_indices,
772
+ correlation: Callable = __correlation,
773
+ segments: int = 10,
774
+ window: float = 0.5,
775
+ skip: int = 0,
776
+ ) -> tuple[NDArray, NDArray]:
777
+ """
778
+ Code adapted from MDevaluate module: https://github.com/mdevaluate/mdevaluate.git
779
+
780
+ Calculate the time series for a correlation function.
781
+
782
+ The times at which the correlation is calculated are determined automatically by the
783
+ function given as ``index_distribution``. The default is a logarithmic distribution.
784
+
785
+ The function has been edited so that the average is always calculated, i.e., average=True below.
786
+
787
+ Args:
788
+ function: The function that should be correlated
789
+ positions: The coordinates of the simulation data
790
+ index_distribution (opt.):
791
+ A function that returns the indices for which the timeseries
792
+ will be calculated
793
+ correlation (function, opt.):
794
+ The correlation function
795
+ segments (int, opt.):
796
+ The number of segments the time window will be shifted
797
+ window (float, opt.):
798
+ The fraction of the simulation the time series will cover
799
+ skip (float, opt.):
800
+ The fraction of the trajectory that will be skipped at the beginning,
801
+ if this is None the start index of the frames slice will be used,
802
+ which defaults to 0.
803
+ counter (bool, opt.):
804
+ If True, returns length of frames (in general number of particles specified)
805
+ average (bool, opt.):
806
+ If True,
807
+ Returns:
808
+ tuple:
809
+ A list of length N that contains the indices of the frames at which
810
+ the time series was calculated and a numpy array of shape (segments, N)
811
+ that holds the (non-avaraged) correlation data
812
+
813
+ if has_counter == True: adds number of counts to output tupel.
814
+ if average is returned it will be weighted.
815
+
816
+ Example:
817
+ Calculating the mean square displacement of a coordinates object named ``coords``:
818
+
819
+ >>> indices, data = shifted_correlation(msd, coords)
820
+ """
821
+ if window + skip >= 1:
822
+ LOGGER.warn(
823
+ 'Invalid parameters for shifted_correlation(), ' 'resetting to defaults.',
824
+ )
825
+ window = 0.5
826
+ skip = 0
827
+
828
+ start_frames = np.unique(
829
+ np.linspace(
830
+ len(positions) * skip,
831
+ len(positions) * (1 - window),
832
+ num=segments,
833
+ endpoint=False,
834
+ dtype=int,
835
+ )
836
+ )
837
+ num_frames = int(len(positions) * (window))
838
+
839
+ idx = index_distribution(0, num_frames)
840
+
841
+ def correlate(start_frame):
842
+ shifted_idx = idx + start_frame
843
+ return correlation(function, map(positions.__getitem__, shifted_idx))
844
+
845
+ correlation_times = np.array([times[i] for i in idx]) - times[0]
846
+
847
+ result: NDArray
848
+ for i_start_frame, start_frame in enumerate(start_frames):
849
+ if i_start_frame == 0:
850
+ result = np.array(list(correlate(start_frame)))
851
+ else:
852
+ result += np.array(list(correlate(start_frame)))
853
+ result = np.array(result)
854
+ result = result / len(start_frames)
855
+
856
+ return correlation_times, result
857
+
858
+
859
+ def calc_molecular_mean_squared_displacements(
860
+ universe: MDAnalysis.Universe, max_mols: int = 5000
861
+ ) -> Dict:
862
+ """
863
+ Calculates the mean squared displacement for the center of mass of each
864
+ molecule type.
865
+
866
+ max_mols: the maximum number of molecules per bead group for calculating the msd, for efficiency purposes.
867
+ 50k was tested and is very fast and does not seem to have any memory issues.
868
+ """
869
+
870
+ def parse_jumps(
871
+ universe: MDAnalysis.Universe, selection: MDAnalysis.AtomGroup
872
+ ): # TODO Add output declaration
873
+ """
874
+ See __get_nojump_positions().
875
+ """
876
+ __ = universe.trajectory[0]
877
+ prev = np.array(selection.positions)
878
+ box = universe.trajectory[0].dimensions[:3]
879
+ sparse_data = namedtuple('SparseData', ['data', 'row', 'col']) # type: ignore[name-match]
880
+ jump_data = (
881
+ sparse_data(data=array('b'), row=array('l'), col=array('l')),
882
+ sparse_data(data=array('b'), row=array('l'), col=array('l')),
883
+ sparse_data(data=array('b'), row=array('l'), col=array('l')),
884
+ )
885
+
886
+ for i_frame, _ in enumerate(universe.trajectory[1:]):
887
+ curr = np.array(selection.positions)
888
+ delta = ((curr - prev) / box).round().astype(np.int8)
889
+ prev = np.array(curr)
890
+ for d in range(3):
891
+ (col,) = np.where(delta[:, d] != 0)
892
+ jump_data[d].col.extend(col)
893
+ jump_data[d].row.extend([i_frame] * len(col))
894
+ jump_data[d].data.extend(delta[col, d])
895
+
896
+ return jump_data
897
+
898
+ def generate_nojump_matrices(
899
+ universe: MDAnalysis.Universe, selection: MDAnalysis.AtomGroup
900
+ ): # TODO Add output declaration
901
+ """
902
+ See __get_nojump_positions().
903
+ """
904
+ jump_data = parse_jumps(universe, selection)
905
+ n_frames = len(universe.trajectory)
906
+ n_atoms = selection.positions.shape[0]
907
+
908
+ nojump_matrices = tuple(
909
+ sparse.csr_matrix(
910
+ (np.array(m.data), (m.row, m.col)), shape=(n_frames, n_atoms)
911
+ )
912
+ for m in jump_data
913
+ )
914
+ return nojump_matrices
915
+
916
+ def get_nojump_positions(
917
+ universe: MDAnalysis.Universe, selection: MDAnalysis.AtomGroup
918
+ ) -> NDArray:
919
+ """
920
+ Unwraps the positions to create a continuous trajectory without jumps across periodic boundaries.
921
+ """
922
+ nojump_matrices = generate_nojump_matrices(universe, selection)
923
+ box = universe.trajectory[0].dimensions[:3]
924
+
925
+ nojump_positions = []
926
+ for i_frame, __ in enumerate(universe.trajectory):
927
+ delta = (
928
+ np.array(
929
+ np.vstack([m[:i_frame, :].sum(axis=0) for m in nojump_matrices]).T
930
+ )
931
+ * box
932
+ )
933
+ nojump_positions.append(selection.positions - delta)
934
+
935
+ return np.array(nojump_positions)
936
+
937
+ def mean_squared_displacement(start: NDArray, current: NDArray):
938
+ """
939
+ Calculates mean square displacement between current and initial (start) coordinates.
940
+ """
941
+ vec = start - current
942
+ return (vec**2).sum(axis=1).mean()
943
+
944
+ if (
945
+ not universe
946
+ or not universe.trajectory
947
+ or universe.trajectory[0].dimensions is None
948
+ ):
949
+ return {}
950
+
951
+ n_frames = universe.trajectory.n_frames
952
+ if n_frames < 50:
953
+ LOGGER.warn(
954
+ 'At least 50 frames required to calculate molecular'
955
+ ' mean squared displacements, skipping.',
956
+ )
957
+ return {}
958
+
959
+ dt = getattr(universe.trajectory, 'dt')
960
+ if dt is None:
961
+ LOGGER.warn(
962
+ 'Universe is missing time step, cannot calculate molecular'
963
+ ' mean squared displacements, skipping.',
964
+ )
965
+ return {}
966
+ times = np.arange(n_frames) * dt
967
+
968
+ bead_groups = _get_molecular_bead_groups(universe)
969
+ if bead_groups is {}:
970
+ return bead_groups
971
+
972
+ moltypes = [moltype for moltype in bead_groups.keys()]
973
+ del_list = []
974
+ for i_moltype, moltype in enumerate(moltypes):
975
+ if bead_groups[moltype]._nbeads > max_mols:
976
+ if max_mols > 50000:
977
+ LOGGER.warn(
978
+ 'Calculating mean squared displacements for more than 50k molecules.'
979
+ ' Expect long processing times!',
980
+ )
981
+ try:
982
+ # select max_mols nr. of rnd molecules from this moltype
983
+ moltype_indices = np.array(
984
+ [atom._ix for atom in bead_groups[moltype]._atoms]
985
+ )
986
+ molnums = universe.atoms.molnums[moltype_indices]
987
+ molnum_types = np.unique(molnums)
988
+ molnum_types_rnd = np.sort(
989
+ np.random.choice(molnum_types, size=max_mols)
990
+ )
991
+ atom_indices_rnd = np.concatenate(
992
+ [np.where(molnums == molnum)[0] for molnum in molnum_types_rnd]
993
+ )
994
+ selection = ' '.join([str(i) for i in atom_indices_rnd])
995
+ selection = f'index {selection}'
996
+ ags_moltype_rnd = universe.select_atoms(selection)
997
+ bead_groups[moltype] = BeadGroup(ags_moltype_rnd, compound='fragments')
998
+ LOGGER.warn(
999
+ 'Maximum number of molecules for calculating the msd has been reached.'
1000
+ ' Will make a random selection for calculation.'
1001
+ )
1002
+ except Exception:
1003
+ LOGGER.warn(
1004
+ 'Error in selecting random molecules for large group when calculating msd. Skipping this molecule type.'
1005
+ )
1006
+ del_list.append(i_moltype)
1007
+
1008
+ for index in sorted(del_list, reverse=True):
1009
+ del moltypes[index]
1010
+
1011
+ msd_results: Dict[str, Any] = {}
1012
+ msd_results['type'] = 'molecular'
1013
+ msd_results['direction'] = 'xyz'
1014
+ msd_results['value'] = []
1015
+ msd_results['times'] = []
1016
+ msd_results['diffusion_constant'] = []
1017
+ msd_results['error_diffusion_constant'] = []
1018
+ for moltype in moltypes:
1019
+ positions = get_nojump_positions(universe, bead_groups[moltype])
1020
+ results = shifted_correlation_average(
1021
+ mean_squared_displacement, times, positions
1022
+ )
1023
+ if results:
1024
+ msd_results['value'].append(results[1])
1025
+ msd_results['times'].append(results[0])
1026
+ diffusion_constant, error = _calc_diffusion_constant(*results)
1027
+ msd_results['diffusion_constant'].append(diffusion_constant)
1028
+ msd_results['error_diffusion_constant'].append(error)
1029
+
1030
+ msd_results['types'] = moltypes
1031
+ msd_results['times'] = np.array(msd_results['times']) * ureg.picosecond
1032
+ msd_results['value'] = np.array(msd_results['value']) * ureg.angstrom**2
1033
+ msd_results['diffusion_constant'] = (
1034
+ np.array(msd_results['diffusion_constant']) * ureg.angstrom**2 / ureg.picosecond
1035
+ )
1036
+ msd_results['error_diffusion_constant'] = np.array(
1037
+ msd_results['error_diffusion_constant']
1038
+ )
1039
+
1040
+ return msd_results
1041
+
1042
+
1043
+ def calc_radius_of_gyration(
1044
+ universe: MDAnalysis.Universe, molecule_atom_indices: NDArray
1045
+ ) -> Dict:
1046
+ """
1047
+ Calculates the radius of gyration as a function of time for the atoms 'molecule_atom_indices'.
1048
+ """
1049
+ if (
1050
+ not universe
1051
+ or not universe.trajectory
1052
+ or universe.trajectory[0].dimensions is None
1053
+ ):
1054
+ return {}
1055
+ selection = ' '.join([str(i) for i in molecule_atom_indices])
1056
+ selection = f'index {selection}'
1057
+ molecule = universe.select_atoms(selection)
1058
+ rg_results: Dict[str, Any] = {}
1059
+ rg_results['type'] = 'molecular'
1060
+ rg_results['times'] = []
1061
+ rg_results['value'] = []
1062
+ time_unit = hasattr(universe.trajectory.time, 'units')
1063
+ for __ in universe.trajectory:
1064
+ rg_results['times'].append(
1065
+ universe.trajectory.time.magnitude
1066
+ if time_unit
1067
+ else universe.trajectory.time
1068
+ )
1069
+ rg_results['value'].append(molecule.radius_of_gyration())
1070
+ rg_results['n_frames'] = len(rg_results['times'])
1071
+ rg_results['times'] = (
1072
+ np.array(rg_results['times']) * time_unit
1073
+ if time_unit
1074
+ else np.array(rg_results['times'])
1075
+ )
1076
+ rg_results['value'] = np.array(rg_results['value']) * ureg.angstrom
1077
+
1078
+ return rg_results
1079
+
1080
+
1081
+ def calc_molecular_radius_of_gyration(
1082
+ universe: MDAnalysis.Universe, system_topology: MSection
1083
+ ) -> List[Dict]:
1084
+ """
1085
+ Calculates the radius of gyration as a function of time for each polymer in the system.
1086
+ """
1087
+ if not system_topology:
1088
+ return []
1089
+
1090
+ rg_results = []
1091
+ for molgroup in system_topology:
1092
+ for molecule in molgroup.get('atoms_group'):
1093
+ sec_monomer_groups = molecule.get('atoms_group')
1094
+ group_type = sec_monomer_groups[0].type if sec_monomer_groups else None
1095
+ if group_type != 'monomer_group':
1096
+ continue
1097
+ rg_result = calc_radius_of_gyration(universe, molecule.atom_indices)
1098
+ rg_result['label'] = molecule.label + '-index_' + str(molecule.index)
1099
+ rg_result['atomsgroup_ref'] = molecule
1100
+ rg_results.append(rg_result)
1101
+
1102
+ return rg_results
1103
+
1104
+
1105
+ def get_molecules_from_bond_list(
1106
+ n_particles: int,
1107
+ bond_list: List[tuple],
1108
+ particle_types: List[str] = None,
1109
+ particles_typeid: array = None,
1110
+ ) -> List[Dict]:
1111
+ """
1112
+ Returns a list of dictionaries with molecule info from each instance in the list of bonds.
1113
+ """
1114
+ system_graph = networkx.empty_graph(n_particles)
1115
+ system_graph.add_edges_from([(i[0], i[1]) for i in bond_list])
1116
+ molecules = [
1117
+ system_graph.subgraph(c).copy()
1118
+ for c in networkx.connected_components(system_graph)
1119
+ ]
1120
+ molecule_info: List[Dict] = []
1121
+ molecule_dict: Dict = {}
1122
+ for mol in molecules:
1123
+ molecule_dict = {}
1124
+ molecule_dict['indices'] = np.array(mol.nodes())
1125
+ molecule_dict['bonds'] = np.array(mol.edges())
1126
+ molecule_dict['type'] = 'molecule'
1127
+ molecule_dict['is_molecule'] = True
1128
+ if particles_typeid is None and len(particle_types) == n_particles:
1129
+ molecule_dict['names'] = [
1130
+ particle_types[int(x)]
1131
+ for x in sorted(np.array(molecule_dict['indices']))
1132
+ ]
1133
+ if particle_types is not None and particles_typeid is not None:
1134
+ molecule_dict['names'] = [
1135
+ particle_types[particles_typeid[int(x)]]
1136
+ for x in sorted(np.array(molecule_dict['indices']))
1137
+ ]
1138
+ molecule_info.append(molecule_dict)
1139
+ return molecule_info
1140
+
1141
+
1142
+ def is_same_molecule(mol_1: dict, mol_2: dict) -> bool:
1143
+ """
1144
+ Checks whether the 2 input molecule dictionary (see "get_molecules_from_bond_list()" above)
1145
+ represent the same molecule type, i.e., same particle types and corresponding bond connections.
1146
+ """
1147
+
1148
+ def get_bond_list_dict(mol):
1149
+ mol_shift = np.min(mol['indices'])
1150
+ mol_bonds_shift = mol['bonds'] - mol_shift
1151
+ bond_list = [
1152
+ sorted((mol['names'][i], mol['names'][j])) for i, j in mol_bonds_shift
1153
+ ]
1154
+ bond_list_names, bond_list_counts = np.unique(
1155
+ bond_list, axis=0, return_counts=True
1156
+ )
1157
+
1158
+ return {
1159
+ bond[0] + '-' + bond[1]: bond_list_counts[i_bond]
1160
+ for i_bond, bond in enumerate(bond_list_names)
1161
+ }
1162
+
1163
+ if sorted(mol_1['names']) != sorted(mol_2['names']):
1164
+ return False
1165
+
1166
+ bond_list_dict_1 = get_bond_list_dict(mol_1)
1167
+ bond_list_dict_2 = get_bond_list_dict(mol_2)
1168
+
1169
+ if bond_list_dict_1 == bond_list_dict_2:
1170
+ return True
1171
+
1172
+ return False
1173
+
1174
+
1175
+ def get_composition(children_names: List[str]) -> str:
1176
+ """
1177
+ Generates a generalized "chemical formula" based on the provided list `children_names`,
1178
+ with the format X(m)Y(n) for children_names X and Y of quantities m and n, respectively.
1179
+ """
1180
+ children_count_tup = np.unique(children_names, return_counts=True)
1181
+ formula = ''.join([f'{name}({count})' for name, count in zip(*children_count_tup)])
1182
+ return formula
1183
+
1184
+
1185
+ def mda_universe_from_nomad_atoms(system, logger=None):
1186
+ """Returns an instance of mda.Universe from a NOMAD Atoms-section.
1187
+
1188
+ Args:
1189
+ system: The atoms to transform
1190
+
1191
+ Returns:
1192
+ A new mda.Universe created from the given data.
1193
+ """
1194
+ n_atoms = len(system.positions)
1195
+ n_residues = 1
1196
+ atom_resindex = [0] * n_atoms
1197
+ residue_segindex = [0]
1198
+
1199
+ universe = MDAnalysis.Universe.empty(
1200
+ n_atoms,
1201
+ n_residues=n_residues,
1202
+ atom_resindex=atom_resindex,
1203
+ residue_segindex=residue_segindex,
1204
+ trajectory=True,
1205
+ )
1206
+
1207
+ # Add positions
1208
+ universe.atoms.positions = system.positions.to(ureg.angstrom).magnitude
1209
+
1210
+ # Add atom attributes
1211
+ atom_names = system.labels
1212
+ universe.add_TopologyAttr('name', atom_names)
1213
+ universe.add_TopologyAttr('type', atom_names)
1214
+ universe.add_TopologyAttr('element', atom_names)
1215
+
1216
+ # Add the box dimensions
1217
+ if system.lattice_vectors is not None:
1218
+ universe.atoms.dimensions = atomutils.cell_to_cellpar(
1219
+ system.lattice_vectors.to(ureg.angstrom).magnitude, degrees=True
1220
+ )
1221
+
1222
+ return universe
1223
+
1224
+
1225
+ class ThermostatParameters(ArchiveSection):
1226
+ """
1227
+ Section containing the parameters pertaining to the thermostat for a molecular dynamics run.
1228
+ """
1229
+
1230
+ m_def = Section(validate=False)
1231
+
1232
+ thermostat_type = Quantity(
1233
+ type=MEnum(
1234
+ 'andersen',
1235
+ 'berendsen',
1236
+ 'brownian',
1237
+ 'langevin_goga',
1238
+ 'langevin_schneider',
1239
+ 'nose_hoover',
1240
+ 'velocity_rescaling',
1241
+ 'velocity_rescaling_langevin',
1242
+ 'velocity_rescaling_woodcock',
1243
+ 'langevin_leap_frog',
1244
+ ),
1245
+ shape=[],
1246
+ description="""
1247
+ The name of the thermostat used for temperature control. If skipped or an empty string is used, it
1248
+ means no thermostat was applied.
1249
+
1250
+ Allowed values are:
1251
+
1252
+ | Thermostat Name | Description |
1253
+
1254
+ | ---------------------- | ----------------------------------------- |
1255
+
1256
+ | `""` | No thermostat |
1257
+
1258
+ | `"andersen"` | H.C. Andersen, [J. Chem. Phys.
1259
+ **72**, 2384 (1980)](https://doi.org/10.1063/1.439486) |
1260
+
1261
+ | `"berendsen"` | H. J. C. Berendsen, J. P. M. Postma,
1262
+ W. F. van Gunsteren, A. DiNola, and J. R. Haak, [J. Chem. Phys.
1263
+ **81**, 3684 (1984)](https://doi.org/10.1063/1.448118) |
1264
+
1265
+ | `"brownian"` | Brownian Dynamics |
1266
+
1267
+ | `"langevin_goga"` | N. Goga, A. J. Rzepiela, A. H. de Vries,
1268
+ S. J. Marrink, and H. J. C. Berendsen, [J. Chem. Theory Comput. **8**, 3637 (2012)]
1269
+ (https://doi.org/10.1021/ct3000876) |
1270
+
1271
+ | `"langevin_schneider"` | T. Schneider and E. Stoll,
1272
+ [Phys. Rev. B **17**, 1302](https://doi.org/10.1103/PhysRevB.17.1302) |
1273
+
1274
+ | `"nose_hoover"` | S. Nosé, [Mol. Phys. **52**, 255 (1984)]
1275
+ (https://doi.org/10.1080/00268978400101201); W.G. Hoover, [Phys. Rev. A
1276
+ **31**, 1695 (1985) |
1277
+
1278
+ | `"velocity_rescaling"` | G. Bussi, D. Donadio, and M. Parrinello,
1279
+ [J. Chem. Phys. **126**, 014101 (2007)](https://doi.org/10.1063/1.2408420) |
1280
+
1281
+ | `"velocity_rescaling_langevin"` | G. Bussi and M. Parrinello,
1282
+ [Phys. Rev. E **75**, 056707 (2007)](https://doi.org/10.1103/PhysRevE.75.056707) |
1283
+
1284
+ | `"velocity_rescaling_woodcock"` | L. V. Woodcock,
1285
+ [Chem. Phys. Lett. **10**, 257 (1971)](https://doi.org/10.1016/0009-2614(71)80281-6) |
1286
+
1287
+ | `"langevin_leap_frog"` | J.A. Izaguirre, C.R. Sweet, and V.S. Pande
1288
+ [Pac Symp Biocomput. **15**, 240-251 (2010)](https://doi.org/10.1142/9789814295291_0026) |
1289
+ """,
1290
+ )
1291
+
1292
+ reference_temperature = Quantity(
1293
+ type=np.float64,
1294
+ shape=[],
1295
+ unit='kelvin',
1296
+ description="""
1297
+ The target temperature for the simulation. Typically used when temperature_profile is "constant".
1298
+ """,
1299
+ )
1300
+
1301
+ coupling_constant = Quantity(
1302
+ type=np.float64,
1303
+ shape=[],
1304
+ unit='s',
1305
+ description="""
1306
+ The time constant for temperature coupling. Need to describe what this means for the various
1307
+ thermostat options...
1308
+ """,
1309
+ )
1310
+
1311
+ effective_mass = Quantity(
1312
+ type=np.float64,
1313
+ shape=[],
1314
+ unit='kilogram',
1315
+ description="""
1316
+ The effective or fictitious mass of the temperature resevoir.
1317
+ """,
1318
+ )
1319
+
1320
+ temperature_profile = Quantity(
1321
+ type=MEnum('constant', 'linear', 'exponential'),
1322
+ shape=[],
1323
+ description="""
1324
+ Type of temperature control (i.e., annealing) procedure. Can be "constant" (no annealing), "linear", or "exponential".
1325
+ If linear, "temperature_update_delta" specifies the corresponding update parameter.
1326
+ If exponential, "temperature_update_factor" specifies the corresponding update parameter.
1327
+ """,
1328
+ )
1329
+
1330
+ reference_temperature_start = Quantity(
1331
+ type=np.float64,
1332
+ shape=[],
1333
+ unit='kelvin',
1334
+ description="""
1335
+ The initial target temperature for the simulation. Typically used when temperature_profile is "linear" or "exponential".
1336
+ """,
1337
+ )
1338
+
1339
+ reference_temperature_end = Quantity(
1340
+ type=np.float64,
1341
+ shape=[],
1342
+ unit='kelvin',
1343
+ description="""
1344
+ The final target temperature for the simulation. Typically used when temperature_profile is "linear" or "exponential".
1345
+ """,
1346
+ )
1347
+
1348
+ temperature_update_frequency = Quantity(
1349
+ type=int,
1350
+ shape=[],
1351
+ description="""
1352
+ Number of simulation steps between changing the target temperature.
1353
+ """,
1354
+ )
1355
+
1356
+ temperature_update_delta = Quantity(
1357
+ type=np.float64,
1358
+ shape=[],
1359
+ description="""
1360
+ Amount to be added (subtracted if negative) to the current reference_temperature
1361
+ at a frequency of temperature_update_frequency when temperature_profile is "linear".
1362
+ The reference temperature is then replaced by this new value until the next update.
1363
+ """,
1364
+ )
1365
+
1366
+ temperature_update_factor = Quantity(
1367
+ type=np.float64,
1368
+ shape=[],
1369
+ description="""
1370
+ Factor to be multiplied to the current reference_temperature at a frequency of temperature_update_frequency when temperature_profile is exponential.
1371
+ The reference temperature is then replaced by this new value until the next update.
1372
+ """,
1373
+ )
1374
+
1375
+ step_start = Quantity(
1376
+ type=int,
1377
+ shape=[],
1378
+ description="""
1379
+ Trajectory step where this thermostating starts.
1380
+ """,
1381
+ )
1382
+
1383
+ step_end = Quantity(
1384
+ type=int,
1385
+ shape=[],
1386
+ description="""
1387
+ Trajectory step number where this thermostating ends.
1388
+ """,
1389
+ )
1390
+
1391
+
1392
+ class BarostatParameters(ArchiveSection):
1393
+ """
1394
+ Section containing the parameters pertaining to the barostat for a molecular dynamics run.
1395
+ """
1396
+
1397
+ m_def = Section(validate=False)
1398
+
1399
+ barostat_type = Quantity(
1400
+ type=MEnum(
1401
+ 'berendsen',
1402
+ 'martyna_tuckerman_tobias_klein',
1403
+ 'nose_hoover',
1404
+ 'parrinello_rahman',
1405
+ 'stochastic_cell_rescaling',
1406
+ ),
1407
+ shape=[],
1408
+ description="""
1409
+ The name of the barostat used for temperature control. If skipped or an empty string is used, it
1410
+ means no barostat was applied.
1411
+
1412
+ Allowed values are:
1413
+
1414
+ | Barostat Name | Description |
1415
+
1416
+ | ---------------------- | ----------------------------------------- |
1417
+
1418
+ | `""` | No thermostat |
1419
+
1420
+ | `"berendsen"` | H. J. C. Berendsen, J. P. M. Postma,
1421
+ W. F. van Gunsteren, A. DiNola, and J. R. Haak, [J. Chem. Phys.
1422
+ **81**, 3684 (1984)](https://doi.org/10.1063/1.448118) |
1423
+
1424
+ | `"martyna_tuckerman_tobias_klein"` | G.J. Martyna, M.E. Tuckerman, D.J. Tobias, and M.L. Klein,
1425
+ [Mol. Phys. **87**, 1117 (1996)](https://doi.org/10.1080/00268979600100761);
1426
+ M.E. Tuckerman, J. Alejandre, R. López-Rendón, A.L. Jochim, and G.J. Martyna,
1427
+ [J. Phys. A. **59**, 5629 (2006)](https://doi.org/10.1088/0305-4470/39/19/S18)|
1428
+
1429
+ | `"nose_hoover"` | S. Nosé, [Mol. Phys. **52**, 255 (1984)]
1430
+ (https://doi.org/10.1080/00268978400101201); W.G. Hoover, [Phys. Rev. A
1431
+ **31**, 1695 (1985) |
1432
+
1433
+ | `"parrinello_rahman"` | M. Parrinello and A. Rahman,
1434
+ [J. Appl. Phys. **52**, 7182 (1981)](https://doi.org/10.1063/1.328693);
1435
+ S. Nosé and M.L. Klein, [Mol. Phys. **50**, 1055 (1983) |
1436
+
1437
+ | `"stochastic_cell_rescaling"` | M. Bernetti and G. Bussi,
1438
+ [J. Chem. Phys. **153**, 114107 (2020)](https://doi.org/10.1063/1.2408420) |
1439
+ """,
1440
+ )
1441
+
1442
+ coupling_type = Quantity(
1443
+ type=MEnum('isotropic', 'semi_isotropic', 'anisotropic'),
1444
+ shape=[],
1445
+ description="""
1446
+ Describes the symmetry of pressure coupling. Specifics can be inferred from the `coupling constant`
1447
+
1448
+ | Type | Description |
1449
+
1450
+ | ---------------------- | ----------------------------------------- |
1451
+
1452
+ | `isotropic` | Identical coupling in all directions. |
1453
+
1454
+ | `semi_isotropic` | Identical coupling in 2 directions. |
1455
+
1456
+ | `anisotropic` | General case. |
1457
+ """,
1458
+ )
1459
+
1460
+ reference_pressure = Quantity(
1461
+ type=np.float64,
1462
+ shape=[3, 3],
1463
+ unit='pascal',
1464
+ description="""
1465
+ The target pressure for the simulation, stored in a 3x3 matrix, indicating the values for individual directions
1466
+ along the diagonal, and coupling between directions on the off-diagonal. Typically used when pressure_profile is "constant".
1467
+ """,
1468
+ )
1469
+
1470
+ coupling_constant = Quantity(
1471
+ type=np.float64,
1472
+ shape=[3, 3],
1473
+ unit='s',
1474
+ description="""
1475
+ The time constants for pressure coupling, stored in a 3x3 matrix, indicating the values for individual directions
1476
+ along the diagonal, and coupling between directions on the off-diagonal. 0 values along the off-diagonal
1477
+ indicate no-coupling between these directions.
1478
+ """,
1479
+ )
1480
+
1481
+ compressibility = Quantity(
1482
+ type=np.float64,
1483
+ shape=[3, 3],
1484
+ unit='1 / pascal',
1485
+ description="""
1486
+ An estimate of the system's compressibility, used for box rescaling, stored in a 3x3 matrix indicating the values for individual directions
1487
+ along the diagonal, and coupling between directions on the off-diagonal. If None, it may indicate that these values
1488
+ are incorporated into the coupling_constant, or simply that the software used uses a fixed value that is not available in
1489
+ the input/output files.
1490
+ """,
1491
+ )
1492
+
1493
+ pressure_profile = Quantity(
1494
+ type=MEnum('constant', 'linear', 'exponential'),
1495
+ shape=[],
1496
+ description="""
1497
+ Type of pressure control procedure. Can be "constant" (no annealing), "linear", or "exponential".
1498
+ If linear, "pressure_update_delta" specifies the corresponding update parameter.
1499
+ If exponential, "pressure_update_factor" specifies the corresponding update parameter.
1500
+ """,
1501
+ )
1502
+
1503
+ reference_pressure_start = Quantity(
1504
+ type=np.float64,
1505
+ shape=[3, 3],
1506
+ unit='pascal',
1507
+ description="""
1508
+ The initial target pressure for the simulation, stored in a 3x3 matrix, indicating the values for individual directions
1509
+ along the diagonal, and coupling between directions on the off-diagonal. Typically used when pressure_profile is "linear" or "exponential".
1510
+ """,
1511
+ )
1512
+
1513
+ reference_pressure_end = Quantity(
1514
+ type=np.float64,
1515
+ shape=[3, 3],
1516
+ unit='pascal',
1517
+ description="""
1518
+ The final target pressure for the simulation, stored in a 3x3 matrix, indicating the values for individual directions
1519
+ along the diagonal, and coupling between directions on the off-diagonal. Typically used when pressure_profile is "linear" or "exponential".
1520
+ """,
1521
+ )
1522
+
1523
+ pressure_update_frequency = Quantity(
1524
+ type=int,
1525
+ shape=[],
1526
+ description="""
1527
+ Number of simulation steps between changing the target pressure.
1528
+ """,
1529
+ )
1530
+
1531
+ pressure_update_delta = Quantity(
1532
+ type=np.float64,
1533
+ shape=[],
1534
+ description="""
1535
+ Amount to be added (subtracted if negative) to the current reference_pressure
1536
+ at a frequency of pressure_update_frequency when pressure_profile is "linear".
1537
+ The pressure temperature is then replaced by this new value until the next update.
1538
+ """,
1539
+ )
1540
+
1541
+ pressure_update_factor = Quantity(
1542
+ type=np.float64,
1543
+ shape=[],
1544
+ description="""
1545
+ Factor to be multiplied to the current reference_pressure at a frequency of pressure_update_frequency when pressure_profile is exponential.
1546
+ The reference pressure is then replaced by this new value until the next update.
1547
+ """,
1548
+ )
1549
+
1550
+ step_start = Quantity(
1551
+ type=int,
1552
+ shape=[],
1553
+ description="""
1554
+ Trajectory step where this barostating starts.
1555
+ """,
1556
+ )
1557
+
1558
+ step_end = Quantity(
1559
+ type=int,
1560
+ shape=[],
1561
+ description="""
1562
+ Trajectory step number where this barostating ends.
1563
+ """,
1564
+ )
1565
+
1566
+
1567
+ class Lambdas(ArchiveSection):
1568
+ """
1569
+ Section for storing all lambda parameters for free energy perturbation
1570
+ """
1571
+
1572
+ m_def = Section(validate=False)
1573
+
1574
+ type = Quantity(
1575
+ type=MEnum(
1576
+ 'output', 'coulomb', 'vdw', 'bonded', 'restraint', 'mass', 'temperature'
1577
+ ),
1578
+ shape=[],
1579
+ description="""
1580
+ The type of lambda interpolation
1581
+
1582
+ Allowed values are:
1583
+
1584
+ | type | Description |
1585
+
1586
+ | ---------------------- | ----------------------------------------- |
1587
+
1588
+ | `"output"` | Lambdas for the free energy outputs saved.
1589
+ These will also act as a default in case some
1590
+ relevant lambdas are not specified. |
1591
+
1592
+ | `"coulomb"` | Lambdas for interpolating electrostatic interactions. |
1593
+
1594
+ | `"vdw"` | Lambdas for interpolating van der Waals interactions. |
1595
+
1596
+ | `"bonded"` | Lambdas for interpolating all intramolecular interactions. |
1597
+
1598
+ | `"restraint"` | Lambdas for interpolating restraints. |
1599
+
1600
+ | `"mass"` | Lambdas for interpolating masses. |
1601
+
1602
+ | `"temperature"` | Lambdas for interpolating temperature. |
1603
+ """,
1604
+ )
1605
+
1606
+ value = Quantity(
1607
+ type=np.float64,
1608
+ shape=[],
1609
+ description="""
1610
+ List of lambdas.
1611
+ """,
1612
+ )
1613
+
1614
+
1615
+ class FreeEnergyCalculationParameters(ArchiveSection):
1616
+ """
1617
+ Section containing the parameters pertaining to a free energy calculation workflow
1618
+ that interpolates between two system states (defined via the interpolation parameter lambda).
1619
+ The parameters are stored for each molecular dynamics run separately, to be referenced
1620
+ by the overarching workflow.
1621
+ """
1622
+
1623
+ m_def = Section(validate=False)
1624
+
1625
+ type = Quantity(
1626
+ type=MEnum('alchemical', 'umbrella_sampling'),
1627
+ shape=[],
1628
+ description="""
1629
+ Specifies the type of workflow. Allowed values are:
1630
+
1631
+ | kind | Description |
1632
+
1633
+ | ---------------------- | ----------------------------------------- |
1634
+
1635
+ | `"alchemical"` | A non-physical transformation between 2 well-defined systems,
1636
+ typically achieved by smoothly interpolating between Hamiltonians or force fields. |
1637
+
1638
+ | `"umbrella_sampling"` | A sampling of the path between 2 well-defined (sub)states of a system,
1639
+ typically achieved by applying a biasing force to the force field along a
1640
+ specified reaction coordinate.
1641
+ """,
1642
+ )
1643
+
1644
+ lambdas = SubSection(
1645
+ sub_section=Lambdas.m_def,
1646
+ description="""
1647
+ Contains the lists of lambda values defined for the interpolation of the system.
1648
+ """,
1649
+ repeats=True,
1650
+ )
1651
+
1652
+ lambda_index = Quantity(
1653
+ type=int,
1654
+ shape=[],
1655
+ description="""
1656
+ The index of the lambda in `lambdas` corresponding to the state of the current simulation.
1657
+ """,
1658
+ )
1659
+
1660
+ atom_indices = Quantity(
1661
+ type=np.dtype(np.int32),
1662
+ shape=[],
1663
+ description="""
1664
+ List of atom indices involved in the interpolation.
1665
+ """,
1666
+ )
1667
+
1668
+ initial_state_vdw = Quantity(
1669
+ type=bool,
1670
+ shape=[],
1671
+ description="""
1672
+ Specifies whether vdw interactions are on (True) or off (False) in the initial state (i.e., lambda = 0).
1673
+ """,
1674
+ )
1675
+
1676
+ final_state_vdw = Quantity(
1677
+ type=bool,
1678
+ shape=[],
1679
+ description="""
1680
+ Specifies whether vdw interactions are on (True) or off (False) in the final state (i.e., lambda = 0).
1681
+ """,
1682
+ )
1683
+
1684
+ initial_state_coloumb = Quantity(
1685
+ type=bool,
1686
+ shape=[],
1687
+ description="""
1688
+ Specifies whether vdw interactions are on (True) or off (False) in the initial state (i.e., lambda = 0).
1689
+ """,
1690
+ )
1691
+
1692
+ final_state_coloumb = Quantity(
1693
+ type=bool,
1694
+ shape=[],
1695
+ description="""
1696
+ Specifies whether vdw interactions are on (True) or off (False) in the final state (i.e., lambda = 0).
1697
+ """,
1698
+ )
1699
+
1700
+ initial_state_bonded = Quantity(
1701
+ type=bool,
1702
+ shape=[],
1703
+ description="""
1704
+ Specifies whether bonded interactions are on (True) or off (False) in the initial state (i.e., lambda = 0).
1705
+ """,
1706
+ )
1707
+
1708
+ final_state_bonded = Quantity(
1709
+ type=bool,
1710
+ shape=[],
1711
+ description="""
1712
+ Specifies whether bonded interactions are on (True) or off (False) in the final state (i.e., lambda = 0).
1713
+ """,
1714
+ )
1715
+
1716
+
1717
+ class MolecularDynamicsMethod(SimulationWorkflowMethod):
1718
+ thermodynamic_ensemble = Quantity(
1719
+ type=MEnum('NVE', 'NVT', 'NPT', 'NPH'),
1720
+ shape=[],
1721
+ description="""
1722
+ The type of thermodynamic ensemble that was simulated.
1723
+
1724
+ Allowed values are:
1725
+
1726
+ | Thermodynamic Ensemble | Description |
1727
+
1728
+ | ---------------------- | ----------------------------------------- |
1729
+
1730
+ | `"NVE"` | Constant number of particles, volume, and energy |
1731
+
1732
+ | `"NVT"` | Constant number of particles, volume, and temperature |
1733
+
1734
+ | `"NPT"` | Constant number of particles, pressure, and temperature |
1735
+
1736
+ | `"NPH"` | Constant number of particles, pressure, and enthalpy |
1737
+ """,
1738
+ )
1739
+
1740
+ integrator_type = Quantity(
1741
+ type=MEnum(
1742
+ 'brownian',
1743
+ 'conjugant_gradient',
1744
+ 'langevin_goga',
1745
+ 'langevin_schneider',
1746
+ 'leap_frog',
1747
+ 'rRESPA_multitimescale',
1748
+ 'velocity_verlet',
1749
+ 'langevin_leap_frog',
1750
+ ),
1751
+ shape=[],
1752
+ description="""
1753
+ Name of the integrator.
1754
+
1755
+ Allowed values are:
1756
+
1757
+ | Integrator Name | Description |
1758
+
1759
+ | ---------------------- | ----------------------------------------- |
1760
+
1761
+ | `"langevin_goga"` | N. Goga, A. J. Rzepiela, A. H. de Vries,
1762
+ S. J. Marrink, and H. J. C. Berendsen, [J. Chem. Theory Comput. **8**, 3637 (2012)]
1763
+ (https://doi.org/10.1021/ct3000876) |
1764
+
1765
+ | `"langevin_schneider"` | T. Schneider and E. Stoll,
1766
+ [Phys. Rev. B **17**, 1302](https://doi.org/10.1103/PhysRevB.17.1302) |
1767
+
1768
+ | `"leap_frog"` | R.W. Hockney, S.P. Goel, and J. Eastwood,
1769
+ [J. Comp. Phys. **14**, 148 (1974)](https://doi.org/10.1016/0021-9991(74)90010-2) |
1770
+
1771
+ | `"velocity_verlet"` | W.C. Swope, H.C. Andersen, P.H. Berens, and K.R. Wilson,
1772
+ [J. Chem. Phys. **76**, 637 (1982)](https://doi.org/10.1063/1.442716) |
1773
+
1774
+ | `"rRESPA_multitimescale"` | M. Tuckerman, B. J. Berne, and G. J. Martyna
1775
+ [J. Chem. Phys. **97**, 1990 (1992)](https://doi.org/10.1063/1.463137) |
1776
+
1777
+ | `"langevin_leap_frog"` | J.A. Izaguirre, C.R. Sweet, and V.S. Pande
1778
+ [Pac Symp Biocomput. **15**, 240-251 (2010)](https://doi.org/10.1142/9789814295291_0026) |
1779
+ """,
1780
+ )
1781
+
1782
+ integration_timestep = Quantity(
1783
+ type=np.float64,
1784
+ shape=[],
1785
+ unit='s',
1786
+ description="""
1787
+ The timestep at which the numerical integration is performed.
1788
+ """,
1789
+ )
1790
+
1791
+ n_steps = Quantity(
1792
+ type=int,
1793
+ shape=[],
1794
+ description="""
1795
+ Number of timesteps performed.
1796
+ """,
1797
+ )
1798
+
1799
+ coordinate_save_frequency = Quantity(
1800
+ type=int,
1801
+ shape=[],
1802
+ description="""
1803
+ The number of timesteps between saving the coordinates.
1804
+ """,
1805
+ )
1806
+
1807
+ velocity_save_frequency = Quantity(
1808
+ type=int,
1809
+ shape=[],
1810
+ description="""
1811
+ The number of timesteps between saving the velocities.
1812
+ """,
1813
+ )
1814
+
1815
+ force_save_frequency = Quantity(
1816
+ type=int,
1817
+ shape=[],
1818
+ description="""
1819
+ The number of timesteps between saving the forces.
1820
+ """,
1821
+ )
1822
+
1823
+ thermodynamics_save_frequency = Quantity(
1824
+ type=int,
1825
+ shape=[],
1826
+ description="""
1827
+ The number of timesteps between saving the thermodynamic quantities.
1828
+ """,
1829
+ )
1830
+
1831
+ thermostat_parameters = SubSection(
1832
+ sub_section=ThermostatParameters.m_def, repeats=True
1833
+ )
1834
+
1835
+ barostat_parameters = SubSection(sub_section=BarostatParameters.m_def, repeats=True)
1836
+
1837
+ free_energy_calculation_parameters = SubSection(
1838
+ sub_section=FreeEnergyCalculationParameters.m_def, repeats=True
1839
+ )
1840
+
1841
+
1842
+ class Property(ArchiveSection):
1843
+ """
1844
+ Generic parent section for all property types.
1845
+ """
1846
+
1847
+ m_def = Section(validate=False)
1848
+
1849
+ type = Quantity(
1850
+ type=MEnum('molecular', 'atomic'),
1851
+ shape=[],
1852
+ description="""
1853
+ Describes if the observable is calculated at the molecular or atomic level.
1854
+ """,
1855
+ )
1856
+
1857
+ label = Quantity(
1858
+ type=str,
1859
+ shape=[],
1860
+ description="""
1861
+ Name or description of the property.
1862
+ """,
1863
+ )
1864
+
1865
+ error_type = Quantity(
1866
+ type=str,
1867
+ shape=[],
1868
+ description="""
1869
+ Describes the type of error reported for this observable.
1870
+ """,
1871
+ )
1872
+
1873
+
1874
+ class PropertyValues(MSection):
1875
+ """
1876
+ Generic parent section for information regarding the values of a property.
1877
+ """
1878
+
1879
+ m_def = Section(validate=False)
1880
+
1881
+ label = Quantity(
1882
+ type=str,
1883
+ shape=[],
1884
+ description="""
1885
+ Describes the atoms or molecule types involved in determining the property.
1886
+ """,
1887
+ )
1888
+
1889
+ errors = Quantity(
1890
+ type=np.float64,
1891
+ shape=['*'],
1892
+ description="""
1893
+ Error associated with the determination of the property.
1894
+ """,
1895
+ )
1896
+
1897
+
1898
+ class EnsemblePropertyValues(PropertyValues):
1899
+ """
1900
+ Generic section containing information regarding the values of an ensemble property.
1901
+ """
1902
+
1903
+ m_def = Section(validate=False)
1904
+
1905
+ n_bins = Quantity(
1906
+ type=int,
1907
+ shape=[],
1908
+ description="""
1909
+ Number of bins.
1910
+ """,
1911
+ )
1912
+
1913
+ frame_start = Quantity(
1914
+ type=int,
1915
+ shape=[],
1916
+ description="""
1917
+ Trajectory frame number where the ensemble averaging starts.
1918
+ """,
1919
+ )
1920
+
1921
+ frame_end = Quantity(
1922
+ type=int,
1923
+ shape=[],
1924
+ description="""
1925
+ Trajectory frame number where the ensemble averaging ends.
1926
+ """,
1927
+ )
1928
+
1929
+ bins_magnitude = Quantity(
1930
+ type=np.float64,
1931
+ shape=['n_bins'],
1932
+ description="""
1933
+ Values of the variable along which the property is calculated.
1934
+ """,
1935
+ )
1936
+
1937
+ bins_unit = Quantity(
1938
+ type=str,
1939
+ shape=[],
1940
+ description="""
1941
+ Unit of the given bins, using UnitRegistry() notation.
1942
+ """,
1943
+ )
1944
+
1945
+ value_magnitude = Quantity(
1946
+ type=np.float64,
1947
+ shape=['n_bins'],
1948
+ description="""
1949
+ Values of the property.
1950
+ """,
1951
+ )
1952
+
1953
+ value_unit = Quantity(
1954
+ type=str,
1955
+ shape=[],
1956
+ description="""
1957
+ Unit of the property, using UnitRegistry() notation.
1958
+ """,
1959
+ )
1960
+
1961
+
1962
+ class RadialDistributionFunctionValues(EnsemblePropertyValues):
1963
+ """
1964
+ Section containing information regarding the values of
1965
+ radial distribution functions (rdfs).
1966
+ """
1967
+
1968
+ m_def = Section(validate=False)
1969
+
1970
+ bins = Quantity(
1971
+ type=np.float64,
1972
+ shape=['n_bins'],
1973
+ unit='m',
1974
+ description="""
1975
+ Distances along which the rdf was calculated.
1976
+ """,
1977
+ )
1978
+
1979
+ value = Quantity(
1980
+ type=np.float64,
1981
+ shape=['n_bins'],
1982
+ description="""
1983
+ Values of the property.
1984
+ """,
1985
+ )
1986
+
1987
+
1988
+ class EnsembleProperty(Property):
1989
+ """
1990
+ Generic section containing information about a calculation of any static observable
1991
+ from a trajectory (i.e., from an ensemble average).
1992
+ """
1993
+
1994
+ m_def = Section(validate=False)
1995
+
1996
+ n_smooth = Quantity(
1997
+ type=int,
1998
+ shape=[],
1999
+ description="""
2000
+ Number of bins over which the running average was computed for
2001
+ the observable `values'.
2002
+ """,
2003
+ )
2004
+
2005
+ n_variables = Quantity(
2006
+ type=int,
2007
+ shape=[],
2008
+ description="""
2009
+ Number of variables along which the property is determined.
2010
+ """,
2011
+ )
2012
+
2013
+ variables_name = Quantity(
2014
+ type=str,
2015
+ shape=['n_variables'],
2016
+ description="""
2017
+ Name/description of the independent variables along which the observable is defined.
2018
+ """,
2019
+ )
2020
+
2021
+ ensemble_property_values = SubSection(
2022
+ sub_section=EnsemblePropertyValues.m_def, repeats=True
2023
+ )
2024
+
2025
+
2026
+ class RadialDistributionFunction(EnsembleProperty):
2027
+ """
2028
+ Section containing information about the calculation of
2029
+ radial distribution functions (rdfs).
2030
+ """
2031
+
2032
+ m_def = Section(validate=False)
2033
+
2034
+ _rdf_results = None
2035
+
2036
+ radial_distribution_function_values = SubSection(
2037
+ sub_section=RadialDistributionFunctionValues.m_def, repeats=True
2038
+ )
2039
+
2040
+ def normalize(self, archive, logger):
2041
+ super().normalize(archive, logger)
2042
+
2043
+ if self._rdf_results:
2044
+ self.type = self._rdf_results.get('type')
2045
+ self.n_smooth = self._rdf_results.get('n_smooth')
2046
+ self.n_prune = self._rdf_results.get('n_prune')
2047
+ self.n_variables = 1
2048
+ self.variables_name = ['distance']
2049
+ for i_pair, pair_type in enumerate(self._rdf_results.get('types', [])):
2050
+ sec_rdf_values = self.m_create(RadialDistributionFunctionValues)
2051
+ sec_rdf_values.label = str(pair_type)
2052
+ sec_rdf_values.n_bins = len(
2053
+ self._rdf_results.get('bins', [[]] * i_pair)[i_pair]
2054
+ )
2055
+ sec_rdf_values.bins = self._rdf_results.get('bins', [[]] * i_pair)[
2056
+ i_pair
2057
+ ]
2058
+ sec_rdf_values.value = self._rdf_results.get('value', [[]] * i_pair)[
2059
+ i_pair
2060
+ ]
2061
+ sec_rdf_values.frame_start = self._rdf_results.get(
2062
+ 'frame_start', [[]] * i_pair
2063
+ )[i_pair]
2064
+ sec_rdf_values.frame_end = self._rdf_results.get(
2065
+ 'frame_end', [[]] * i_pair
2066
+ )[i_pair]
2067
+
2068
+
2069
+ class TrajectoryProperty(Property):
2070
+ """
2071
+ Generic section containing information about a calculation of any observable
2072
+ defined and stored at each individual frame of a trajectory.
2073
+ """
2074
+
2075
+ m_def = Section(validate=False)
2076
+
2077
+ n_frames = Quantity(
2078
+ type=int,
2079
+ shape=[],
2080
+ description="""
2081
+ Number of frames for which the observable is stored.
2082
+ """,
2083
+ )
2084
+
2085
+ frames = Quantity(
2086
+ type=np.int32,
2087
+ shape=['n_frames'],
2088
+ description="""
2089
+ Frames for which the observable is stored.
2090
+ """,
2091
+ )
2092
+
2093
+ times = Quantity(
2094
+ type=np.float64,
2095
+ shape=['n_frames'],
2096
+ unit='s',
2097
+ description="""
2098
+ Times for which the observable is stored.
2099
+ """,
2100
+ )
2101
+
2102
+ value_magnitude = Quantity(
2103
+ type=np.float64,
2104
+ shape=['n_frames'],
2105
+ description="""
2106
+ Values of the property.
2107
+ """,
2108
+ )
2109
+
2110
+ value_unit = Quantity(
2111
+ type=str,
2112
+ shape=[],
2113
+ description="""
2114
+ Unit of the property, using UnitRegistry() notation.
2115
+ """,
2116
+ )
2117
+
2118
+ errors = Quantity(
2119
+ type=np.float64,
2120
+ shape=['*'],
2121
+ description="""
2122
+ Error associated with the determination of the property.
2123
+ """,
2124
+ )
2125
+
2126
+
2127
+ # TODO Rg + TrajectoryPropery should be removed from workflow. All properties dependent on a single configuration should be store in calculation
2128
+ class RadiusOfGyration(TrajectoryProperty):
2129
+ """
2130
+ Section containing information about the calculation of
2131
+ radius of gyration (Rg).
2132
+ """
2133
+
2134
+ m_def = Section(validate=False)
2135
+
2136
+ _rg_results = None
2137
+
2138
+ atomsgroup_ref = Quantity(
2139
+ type=Reference(AtomsGroup.m_def),
2140
+ shape=[1],
2141
+ description="""
2142
+ References to the atoms_group section containing the molecule for which Rg was calculated.
2143
+ """,
2144
+ )
2145
+
2146
+ value = Quantity(
2147
+ type=np.float64,
2148
+ shape=['n_frames'],
2149
+ unit='m',
2150
+ description="""
2151
+ Values of the property.
2152
+ """,
2153
+ )
2154
+
2155
+ def normalize(self, archive, logger):
2156
+ super().normalize(archive, logger)
2157
+
2158
+ if self._rg_results:
2159
+ self.type = self._rg_results.get('type')
2160
+ self.label = self._rg_results.get('label')
2161
+ # TODO Fix this assignment fails with TypeError
2162
+ try:
2163
+ self.atomsgroup_ref = self._rg_results.get('atomsgroup_ref')
2164
+ except Exception:
2165
+ pass
2166
+ self.n_frames = self._rg_results.get('n_frames')
2167
+ self.times = self._rg_results.get('times')
2168
+ self.value = self._rg_results.get('value')
2169
+
2170
+
2171
+ class FreeEnergyCalculations(TrajectoryProperty):
2172
+ """
2173
+ Section containing information regarding the instantaneous (i.e., for a single configuration)
2174
+ values of free energies calculated via thermodynamic perturbation.
2175
+ The values stored are actually infinitesimal changes in the free energy, determined as derivatives
2176
+ of the Hamiltonian with respect to the coupling parameter (lambda) defining each state for the perturbation.
2177
+ """
2178
+
2179
+ m_def = Section(validate=False)
2180
+
2181
+ method_ref = Quantity(
2182
+ type=Reference(FreeEnergyCalculationParameters.m_def),
2183
+ shape=[],
2184
+ description="""
2185
+ Links the free energy results with the method parameters.
2186
+ """,
2187
+ )
2188
+
2189
+ lambda_index = Quantity(
2190
+ type=int,
2191
+ shape=[],
2192
+ description="""
2193
+ Index of the lambda state for the present simulation within the free energy calculation workflow.
2194
+ I.e., lambda = method_ref.lambdas.values[lambda_index]
2195
+ """,
2196
+ )
2197
+
2198
+ n_states = Quantity(
2199
+ type=int,
2200
+ shape=[],
2201
+ description="""
2202
+ Number of states defined for the interpolation of the system, as indicate in `method_ref`.
2203
+ """,
2204
+ )
2205
+
2206
+ value_unit = Quantity(
2207
+ type=str,
2208
+ shape=[],
2209
+ description="""
2210
+ Unit of the property, using UnitRegistry() notation.
2211
+ In this case, the unit corresponds to all `value` properties stored within this section.
2212
+ """,
2213
+ )
2214
+
2215
+ value_total_energy_magnitude = Quantity(
2216
+ type=HDF5Dataset,
2217
+ shape=[],
2218
+ description="""
2219
+ Value of the total energy for the present lambda state. The expected dimensions are ["n_frames"].
2220
+ This quantity is a reference to the data (file+path), which is stored in an HDF5 file for efficiency.
2221
+ """,
2222
+ )
2223
+
2224
+ value_PV_energy_magnitude = Quantity(
2225
+ type=HDF5Dataset,
2226
+ shape=[],
2227
+ description="""
2228
+ Value of the pressure-volume energy (i.e., P*V) for the present lambda state. The expected dimensions are ["n_frames"].
2229
+ This quantity is a reference to the data (file+path), which is stored in an HDF5 file for efficiency.
2230
+ """,
2231
+ )
2232
+
2233
+ value_total_energy_differences_magnitude = Quantity(
2234
+ type=HDF5Dataset,
2235
+ shape=[],
2236
+ description="""
2237
+ Values correspond to the difference in total energy between each specified lambda state
2238
+ and the reference state, which corresponds to the value of lambda of the current simulation.
2239
+ The expected dimensions are ["n_frames", "n_states"].
2240
+ This quantity is a reference to the data (file+path), which is stored in an HDF5 file for efficiency.
2241
+ """,
2242
+ )
2243
+
2244
+ value_total_energy_derivative_magnitude = Quantity(
2245
+ type=HDF5Dataset,
2246
+ shape=[],
2247
+ description="""
2248
+ Value of the derivative of the total energy with respect to lambda, evaluated for the current
2249
+ lambda state. The expected dimensions are ["n_frames"].
2250
+ This quantity is a reference to the data (file+path), which is stored in an HDF5 file for efficiency.
2251
+ """,
2252
+ )
2253
+
2254
+
2255
+ class DiffusionConstantValues(PropertyValues):
2256
+ """
2257
+ Section containing information regarding the diffusion constants.
2258
+ """
2259
+
2260
+ m_def = Section(validate=False)
2261
+
2262
+ value = Quantity(
2263
+ type=np.float64,
2264
+ shape=[],
2265
+ unit='m^2/s',
2266
+ description="""
2267
+ Values of the diffusion constants.
2268
+ """,
2269
+ )
2270
+
2271
+ error_type = Quantity(
2272
+ type=str,
2273
+ shape=[],
2274
+ description="""
2275
+ Describes the type of error reported for this observable.
2276
+ """,
2277
+ )
2278
+
2279
+
2280
+ class CorrelationFunctionValues(PropertyValues):
2281
+ """
2282
+ Generic section containing information regarding the values of a correlation function.
2283
+ """
2284
+
2285
+ m_def = Section(validate=False)
2286
+
2287
+ n_times = Quantity(
2288
+ type=int,
2289
+ shape=[],
2290
+ description="""
2291
+ Number of times windows for the calculation of the correlation function.
2292
+ """,
2293
+ )
2294
+
2295
+ times = Quantity(
2296
+ type=np.float64,
2297
+ shape=['n_times'],
2298
+ unit='s',
2299
+ description="""
2300
+ Time windows used for the calculation of the correlation function.
2301
+ """,
2302
+ )
2303
+
2304
+ value_magnitude = Quantity(
2305
+ type=np.float64,
2306
+ shape=['n_times'],
2307
+ description="""
2308
+ Values of the property.
2309
+ """,
2310
+ )
2311
+
2312
+ value_unit = Quantity(
2313
+ type=str,
2314
+ shape=[],
2315
+ description="""
2316
+ Unit of the property, using UnitRegistry() notation.
2317
+ """,
2318
+ )
2319
+
2320
+
2321
+ class MeanSquaredDisplacementValues(CorrelationFunctionValues):
2322
+ """
2323
+ Section containing information regarding the values of a mean squared displacements (msds).
2324
+ """
2325
+
2326
+ m_def = Section(validate=False)
2327
+
2328
+ times = Quantity(
2329
+ type=np.float64,
2330
+ shape=['n_times'],
2331
+ unit='s',
2332
+ description="""
2333
+ Time windows used for the calculation of the msds.
2334
+ """,
2335
+ )
2336
+
2337
+ value = Quantity(
2338
+ type=np.float64,
2339
+ shape=['n_times'],
2340
+ unit='m^2',
2341
+ description="""
2342
+ Mean squared displacement values.
2343
+ """,
2344
+ )
2345
+
2346
+ errors = Quantity(
2347
+ type=np.float64,
2348
+ shape=['*'],
2349
+ description="""
2350
+ Error associated with the determination of the msds.
2351
+ """,
2352
+ )
2353
+
2354
+ diffusion_constant = SubSection(
2355
+ sub_section=DiffusionConstantValues.m_def, repeats=False
2356
+ )
2357
+
2358
+
2359
+ class CorrelationFunction(Property):
2360
+ """
2361
+ Generic section containing information about a calculation of any time correlation
2362
+ function from a trajectory.
2363
+ """
2364
+
2365
+ m_def = Section(validate=False)
2366
+
2367
+ direction = Quantity(
2368
+ type=MEnum('x', 'y', 'z', 'xy', 'yz', 'xz', 'xyz'),
2369
+ shape=[],
2370
+ description="""
2371
+ Describes the direction in which the correlation function was calculated.
2372
+ """,
2373
+ )
2374
+
2375
+ correlation_function_values = SubSection(
2376
+ sub_section=CorrelationFunctionValues.m_def, repeats=True
2377
+ )
2378
+
2379
+
2380
+ class MeanSquaredDisplacement(CorrelationFunction):
2381
+ """
2382
+ Section containing information about a calculation of any mean squared displacements (msds).
2383
+ """
2384
+
2385
+ m_def = Section(validate=False)
2386
+
2387
+ _msd_results = None
2388
+
2389
+ mean_squared_displacement_values = SubSection(
2390
+ sub_section=MeanSquaredDisplacementValues.m_def, repeats=True
2391
+ )
2392
+
2393
+ def normalize(self, archive, logger):
2394
+ super().normalize(archive, logger)
2395
+
2396
+ if not self._msd_results:
2397
+ return
2398
+
2399
+ self.type = self._msd_results.get('type')
2400
+ self.direction = self._msd_results.get('direction')
2401
+ for i_type, moltype in enumerate(self._msd_results.get('types', [])):
2402
+ sec_msd_values = self.m_create(MeanSquaredDisplacementValues)
2403
+ sec_msd_values.label = str(moltype)
2404
+ sec_msd_values.n_times = len(
2405
+ self._msd_results.get('times', [[]] * i_type)[i_type]
2406
+ )
2407
+ sec_msd_values.times = (
2408
+ self._msd_results['times'][i_type]
2409
+ if self._msd_results.get('times') is not None
2410
+ else []
2411
+ )
2412
+ sec_msd_values.value = (
2413
+ self._msd_results['value'][i_type]
2414
+ if self._msd_results.get('value') is not None
2415
+ else []
2416
+ )
2417
+ sec_diffusion = sec_msd_values.m_create(DiffusionConstantValues)
2418
+ sec_diffusion.value = (
2419
+ self._msd_results['diffusion_constant'][i_type]
2420
+ if self._msd_results.get('diffusion_constant') is not None
2421
+ else []
2422
+ )
2423
+ sec_diffusion.error_type = 'Pearson correlation coefficient'
2424
+ if self._msd_results.get('error_diffusion_constant') is not None:
2425
+ errors = self._msd_results['error_diffusion_constant'][i_type]
2426
+ sec_diffusion.errors = (
2427
+ list(errors) if isinstance(errors, (list, np.ndarray)) else [errors]
2428
+ )
2429
+
2430
+
2431
+ class MolecularDynamicsResults(ThermodynamicsResults):
2432
+ finished_normally = Quantity(
2433
+ type=bool,
2434
+ shape=[],
2435
+ description="""
2436
+ Indicates if calculation terminated normally.
2437
+ """,
2438
+ )
2439
+
2440
+ n_steps = Quantity(
2441
+ type=np.int32,
2442
+ shape=[],
2443
+ description="""
2444
+ Number of trajectory steps""",
2445
+ )
2446
+
2447
+ trajectory = Quantity(
2448
+ type=Reference(System),
2449
+ shape=['n_steps'],
2450
+ description="""
2451
+ Reference to the system of each step in the trajectory.
2452
+ """,
2453
+ )
2454
+
2455
+ radial_distribution_functions = SubSection(
2456
+ sub_section=RadialDistributionFunction.m_def, repeats=True
2457
+ )
2458
+
2459
+ ensemble_properties = SubSection(sub_section=EnsembleProperty.m_def, repeats=True)
2460
+
2461
+ correlation_functions = SubSection(
2462
+ sub_section=CorrelationFunction.m_def, repeats=True
2463
+ )
2464
+
2465
+ radial_distribution_functions = SubSection(
2466
+ sub_section=RadialDistributionFunction.m_def, repeats=True
2467
+ )
2468
+
2469
+ radius_of_gyration = SubSection(sub_section=RadiusOfGyration, repeats=True)
2470
+
2471
+ mean_squared_displacements = SubSection(
2472
+ sub_section=MeanSquaredDisplacement.m_def, repeats=True
2473
+ )
2474
+
2475
+ free_energy_calculations = SubSection(
2476
+ sub_section=FreeEnergyCalculations.m_def, repeats=True
2477
+ )
2478
+
2479
+ def normalize(self, archive, logger):
2480
+ super().normalize(archive, logger)
2481
+
2482
+ universe = archive_to_universe(archive)
2483
+ if universe is None:
2484
+ return
2485
+
2486
+ # calculate molecular radial distribution functions
2487
+ if not self.radial_distribution_functions:
2488
+ n_traj_split = (
2489
+ 10 # number of intervals to split trajectory into for averaging
2490
+ )
2491
+ interval_indices = [] # 2D array specifying the groups of the n_traj_split intervals to be averaged
2492
+ # first 20% of trajectory
2493
+ interval_indices.append(np.arange(int(n_traj_split * 0.20)))
2494
+ # last 80% of trajectory
2495
+ interval_indices.append(np.arange(n_traj_split)[len(interval_indices[0]) :])
2496
+ # last 60% of trajectory
2497
+ interval_indices.append(
2498
+ np.arange(n_traj_split)[len(interval_indices[0]) * 2 :]
2499
+ )
2500
+ # last 40% of trajectory
2501
+ interval_indices.append(
2502
+ np.arange(n_traj_split)[len(interval_indices[0]) * 3 :]
2503
+ )
2504
+
2505
+ n_prune = int(universe.trajectory.n_frames / len(archive.run[-1].system))
2506
+ rdf_results = calc_molecular_rdf(
2507
+ universe,
2508
+ n_traj_split=n_traj_split,
2509
+ n_prune=n_prune,
2510
+ interval_indices=interval_indices,
2511
+ )
2512
+ if rdf_results:
2513
+ sec_rdfs = RadialDistributionFunction()
2514
+ sec_rdfs._rdf_results = rdf_results
2515
+ sec_rdfs.normalize(archive, logger)
2516
+ self.radial_distribution_functions.append(sec_rdfs)
2517
+
2518
+ # calculate the molecular mean squared displacements
2519
+ if not self.mean_squared_displacements:
2520
+ msd_results = calc_molecular_mean_squared_displacements(universe)
2521
+ if msd_results:
2522
+ sec_msds = MeanSquaredDisplacement()
2523
+ sec_msds._msd_results = msd_results
2524
+ sec_msds.normalize(archive, logger)
2525
+ self.mean_squared_displacements.append(sec_msds)
2526
+
2527
+ # calculate radius of gyration for polymers
2528
+ try:
2529
+ sec_systems = archive.run[-1].system
2530
+ sec_system = sec_systems[0]
2531
+ sec_calc = archive.run[-1].calculation
2532
+ sec_calc = sec_calc if sec_calc is not None else []
2533
+ except Exception:
2534
+ return
2535
+
2536
+ flag_rgs = False
2537
+ for calc in sec_calc:
2538
+ if calc.get('radius_of_gyration'):
2539
+ flag_rgs = True
2540
+ break # TODO Should transfer Rg's to workflow results if they are already supplied in calculation
2541
+
2542
+ if not flag_rgs:
2543
+ sec_rgs_calc = None
2544
+ system_topology = sec_system.atoms_group
2545
+ rg_results = calc_molecular_radius_of_gyration(universe, system_topology)
2546
+ for rg in rg_results:
2547
+ n_frames = rg.get('n_frames')
2548
+ if len(sec_systems) != n_frames:
2549
+ self.logger.warning(
2550
+ 'Mismatch in length of system references in calculation and calculated Rg values.'
2551
+ 'Will not store Rg values under calculation section'
2552
+ )
2553
+ continue
2554
+
2555
+ sec_rgs = RadiusOfGyration()
2556
+ sec_rgs._rg_results = rg
2557
+ sec_rgs.normalize(archive, logger)
2558
+ self.radius_of_gyration.append(sec_rgs)
2559
+
2560
+ for calc in sec_calc:
2561
+ if not calc.system_ref:
2562
+ continue
2563
+ sys_ind = calc.system_ref.m_parent_index
2564
+ sec_rgs_calc = calc.radius_of_gyration
2565
+ if not sec_rgs_calc:
2566
+ sec_rgs_calc = calc.m_create(RadiusOfGyrationCalculation)
2567
+ sec_rgs_calc.kind = rg.get('type')
2568
+ else:
2569
+ sec_rgs_calc = sec_rgs_calc[0]
2570
+ sec_rg_values = sec_rgs_calc.m_create(
2571
+ RadiusOfGyrationValuesCalculation
2572
+ )
2573
+ # TODO Fix this assignment fails with TypeError
2574
+ try:
2575
+ sec_rg_values.atomsgroup_ref = rg.get('atomsgroup_ref')
2576
+ except Exception:
2577
+ pass
2578
+ sec_rg_values.label = rg.get('label')
2579
+ sec_rg_values.value = rg.get('value')[sys_ind]
2580
+
2581
+
2582
+ class MolecularDynamics(SerialSimulation):
2583
+ method = SubSection(sub_section=MolecularDynamicsMethod)
2584
+
2585
+ results = SubSection(sub_section=MolecularDynamicsResults)
2586
+
2587
+ def normalize(self, archive, logger):
2588
+ super().normalize(archive, logger)
2589
+
2590
+ if not self.method:
2591
+ self.method = MolecularDynamicsMethod()
2592
+ self.inputs.append(Link(name=WORKFLOW_METHOD_NAME, section=self.method))
2593
+
2594
+ if not self.results:
2595
+ self.results = MolecularDynamicsResults()
2596
+ self.outputs.append(Link(name=WORKFLOW_RESULTS_NAME, section=self.results))
2597
+
2598
+ if self.results.trajectory is None and self._systems:
2599
+ self.results.trajectory = self._systems
2600
+
2601
+ SimulationWorkflowResults.normalize(self.results, archive, logger)