MDANSE 2.0.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.
- MDANSE/Chemistry/ChemicalSystem.py +581 -0
- MDANSE/Chemistry/Databases.py +818 -0
- MDANSE/Chemistry/__init__.py +22 -0
- MDANSE/Chemistry/atoms.json +17164 -0
- MDANSE/Chemistry/neutron_scattering_lengths_nist.csv +389 -0
- MDANSE/Core/Platform.py +214 -0
- MDANSE/Core/Singleton.py +34 -0
- MDANSE/Core/SubclassFactory.py +203 -0
- MDANSE/Core/__init__.py +15 -0
- MDANSE/Framework/AtomGrouping/grouping.py +380 -0
- MDANSE/Framework/AtomMapping/__init__.py +24 -0
- MDANSE/Framework/AtomMapping/atom_mapping.py +289 -0
- MDANSE/Framework/AtomSelector/__init__.py +15 -0
- MDANSE/Framework/AtomSelector/atom_selection.py +106 -0
- MDANSE/Framework/AtomSelector/general_selection.py +112 -0
- MDANSE/Framework/AtomSelector/group_selection.py +79 -0
- MDANSE/Framework/AtomSelector/molecule_selection.py +48 -0
- MDANSE/Framework/AtomSelector/selection_builder.py +451 -0
- MDANSE/Framework/AtomSelector/selector.py +294 -0
- MDANSE/Framework/AtomSelector/spatial_selection.py +105 -0
- MDANSE/Framework/Configurable.py +396 -0
- MDANSE/Framework/Configurators/AseInputFileConfigurator.py +85 -0
- MDANSE/Framework/Configurators/AtomMappingConfigurator.py +81 -0
- MDANSE/Framework/Configurators/AtomSelectionConfigurator.py +86 -0
- MDANSE/Framework/Configurators/AtomTransmutationConfigurator.py +188 -0
- MDANSE/Framework/Configurators/AxisSelectionConfigurator.py +94 -0
- MDANSE/Framework/Configurators/BooleanConfigurator.py +65 -0
- MDANSE/Framework/Configurators/CorrelationFramesConfigurator.py +79 -0
- MDANSE/Framework/Configurators/DerivativeOrderConfigurator.py +76 -0
- MDANSE/Framework/Configurators/DistHistCutoffConfigurator.py +106 -0
- MDANSE/Framework/Configurators/FileWithAtomDataConfigurator.py +103 -0
- MDANSE/Framework/Configurators/FloatConfigurator.py +87 -0
- MDANSE/Framework/Configurators/FramesConfigurator.py +108 -0
- MDANSE/Framework/Configurators/GridStepConfigurator.py +122 -0
- MDANSE/Framework/Configurators/GroupingLevelConfigurator.py +88 -0
- MDANSE/Framework/Configurators/HDFInputFileConfigurator.py +89 -0
- MDANSE/Framework/Configurators/HDFTrajectoryConfigurator.py +94 -0
- MDANSE/Framework/Configurators/IConfigurator.py +340 -0
- MDANSE/Framework/Configurators/InputFileConfigurator.py +72 -0
- MDANSE/Framework/Configurators/InstrumentResolutionConfigurator.py +113 -0
- MDANSE/Framework/Configurators/IntegerConfigurator.py +99 -0
- MDANSE/Framework/Configurators/InterpolationOrderConfigurator.py +103 -0
- MDANSE/Framework/Configurators/MDAnalysisCoordinateFileConfigurator.py +81 -0
- MDANSE/Framework/Configurators/MDAnalysisTimeStepConfigurator.py +76 -0
- MDANSE/Framework/Configurators/MDAnalysisTopologyFileConfigurator.py +94 -0
- MDANSE/Framework/Configurators/MDMCTrajectoryConfigurator.py +70 -0
- MDANSE/Framework/Configurators/MDTrajTimeStepConfigurator.py +64 -0
- MDANSE/Framework/Configurators/MDTrajTopologyFileConfigurator.py +112 -0
- MDANSE/Framework/Configurators/MDTrajTrajectoryFileConfigurator.py +44 -0
- MDANSE/Framework/Configurators/MockTrajectoryConfigurator.py +74 -0
- MDANSE/Framework/Configurators/MoleculeSelectionConfigurator.py +85 -0
- MDANSE/Framework/Configurators/MultiInputFileConfigurator.py +84 -0
- MDANSE/Framework/Configurators/MultipleChoicesConfigurator.py +81 -0
- MDANSE/Framework/Configurators/OptionalFloatConfigurator.py +94 -0
- MDANSE/Framework/Configurators/OutputFilesConfigurator.py +129 -0
- MDANSE/Framework/Configurators/OutputStructureConfigurator.py +95 -0
- MDANSE/Framework/Configurators/OutputTrajectoryConfigurator.py +114 -0
- MDANSE/Framework/Configurators/PartialChargeConfigurator.py +217 -0
- MDANSE/Framework/Configurators/ProjectionConfigurator.py +96 -0
- MDANSE/Framework/Configurators/QRangeConfigurator.py +35 -0
- MDANSE/Framework/Configurators/QVectorsConfigurator.py +137 -0
- MDANSE/Framework/Configurators/RangeConfigurator.py +136 -0
- MDANSE/Framework/Configurators/RunningModeConfigurator.py +72 -0
- MDANSE/Framework/Configurators/SingleChoiceConfigurator.py +63 -0
- MDANSE/Framework/Configurators/TrajectoryFilterConfigurator.py +133 -0
- MDANSE/Framework/Configurators/UnitCellConfigurator.py +122 -0
- MDANSE/Framework/Configurators/VectorConfigurator.py +89 -0
- MDANSE/Framework/Configurators/WeightsConfigurator.py +140 -0
- MDANSE/Framework/Configurators/__init__.py +124 -0
- MDANSE/Framework/Converters/ASE.py +334 -0
- MDANSE/Framework/Converters/CASTEP.py +164 -0
- MDANSE/Framework/Converters/CHARMM.py +24 -0
- MDANSE/Framework/Converters/CP2K.py +227 -0
- MDANSE/Framework/Converters/Converter.py +82 -0
- MDANSE/Framework/Converters/DCD.py +158 -0
- MDANSE/Framework/Converters/DFTB.py +64 -0
- MDANSE/Framework/Converters/DL_POLY.py +192 -0
- MDANSE/Framework/Converters/Forcite.py +218 -0
- MDANSE/Framework/Converters/Gromacs.py +211 -0
- MDANSE/Framework/Converters/LAMMPS.py +305 -0
- MDANSE/Framework/Converters/MDAnalysis.py +258 -0
- MDANSE/Framework/Converters/MDTraj.py +226 -0
- MDANSE/Framework/Converters/NAMD.py +24 -0
- MDANSE/Framework/Converters/VASP.py +187 -0
- MDANSE/Framework/Converters/XPLOR.py +24 -0
- MDANSE/Framework/Converters/__init__.py +33 -0
- MDANSE/Framework/Formats/FileInMemory.py +70 -0
- MDANSE/Framework/Formats/HDFFormat.py +239 -0
- MDANSE/Framework/Formats/IFormat.py +50 -0
- MDANSE/Framework/Formats/MDAFormat.py +34 -0
- MDANSE/Framework/Formats/MDTFormat.py +34 -0
- MDANSE/Framework/Formats/TextFormat.py +180 -0
- MDANSE/Framework/Formats/__init__.py +23 -0
- MDANSE/Framework/InstrumentResolutions/Gaussian.py +39 -0
- MDANSE/Framework/InstrumentResolutions/IInstrumentResolution.py +58 -0
- MDANSE/Framework/InstrumentResolutions/Ideal.py +35 -0
- MDANSE/Framework/InstrumentResolutions/Lorentzian.py +37 -0
- MDANSE/Framework/InstrumentResolutions/PseudoVoigt.py +49 -0
- MDANSE/Framework/InstrumentResolutions/Square.py +42 -0
- MDANSE/Framework/InstrumentResolutions/Triangular.py +40 -0
- MDANSE/Framework/InstrumentResolutions/__init__.py +24 -0
- MDANSE/Framework/Jobs/AreaPerMolecule.py +167 -0
- MDANSE/Framework/Jobs/AverageStructure.py +178 -0
- MDANSE/Framework/Jobs/CartesianCorrelationFunction.py +226 -0
- MDANSE/Framework/Jobs/CartesianPowerSpectrum.py +172 -0
- MDANSE/Framework/Jobs/CenterOfMassesTrajectory.py +218 -0
- MDANSE/Framework/Jobs/CoordinationNumber.py +227 -0
- MDANSE/Framework/Jobs/CurrentCorrelationFunction.py +693 -0
- MDANSE/Framework/Jobs/Density.py +190 -0
- MDANSE/Framework/Jobs/DensityOfStates.py +57 -0
- MDANSE/Framework/Jobs/DipoleAutoCorrelationFunction.py +167 -0
- MDANSE/Framework/Jobs/DistanceHistogram.py +256 -0
- MDANSE/Framework/Jobs/DynamicCoherentStructureFactor.py +457 -0
- MDANSE/Framework/Jobs/DynamicIncoherentStructureFactor.py +374 -0
- MDANSE/Framework/Jobs/Eccentricity.py +143 -0
- MDANSE/Framework/Jobs/ElasticIncoherentStructureFactor.py +253 -0
- MDANSE/Framework/Jobs/GaussianDynamicIncoherentStructureFactor.py +412 -0
- MDANSE/Framework/Jobs/IJob.py +582 -0
- MDANSE/Framework/Jobs/Infrared.py +257 -0
- MDANSE/Framework/Jobs/JobStatus.py +118 -0
- MDANSE/Framework/Jobs/MeanSquareDisplacement.py +215 -0
- MDANSE/Framework/Jobs/MolecularTrace.py +198 -0
- MDANSE/Framework/Jobs/NeutronDynamicTotalStructureFactor.py +498 -0
- MDANSE/Framework/Jobs/PairDistributionFunction.py +264 -0
- MDANSE/Framework/Jobs/PositionCorrelationFunction.py +49 -0
- MDANSE/Framework/Jobs/PositionPowerSpectrum.py +40 -0
- MDANSE/Framework/Jobs/RadiusOfGyration.py +139 -0
- MDANSE/Framework/Jobs/ReorientationalTimeCorrelationFunction.py +300 -0
- MDANSE/Framework/Jobs/RootMeanSquareDeviation.py +190 -0
- MDANSE/Framework/Jobs/RootMeanSquareFluctuation.py +197 -0
- MDANSE/Framework/Jobs/RotationAutocorrelation.py +211 -0
- MDANSE/Framework/Jobs/ScatteringLengthDensityProfile.py +256 -0
- MDANSE/Framework/Jobs/SolventAccessibleSurface.py +599 -0
- MDANSE/Framework/Jobs/StaticStructureFactor.py +333 -0
- MDANSE/Framework/Jobs/StructureFactorFromScatteringFunction.py +193 -0
- MDANSE/Framework/Jobs/Temperature.py +203 -0
- MDANSE/Framework/Jobs/TrajectoryEditor.py +260 -0
- MDANSE/Framework/Jobs/TrajectoryFilter.py +367 -0
- MDANSE/Framework/Jobs/VanHoveFunctionDistinct.py +740 -0
- MDANSE/Framework/Jobs/VanHoveFunctionSelf.py +376 -0
- MDANSE/Framework/Jobs/VelocityCorrelationFunction.py +83 -0
- MDANSE/Framework/Jobs/Voronoi.py +268 -0
- MDANSE/Framework/Jobs/XRayStaticStructureFactor.py +351 -0
- MDANSE/Framework/Jobs/__init__.py +95 -0
- MDANSE/Framework/OutputVariables/IOutputVariable.py +149 -0
- MDANSE/Framework/OutputVariables/LineOutputVariable.py +22 -0
- MDANSE/Framework/OutputVariables/SurfaceOutputVariable.py +22 -0
- MDANSE/Framework/OutputVariables/VolumeOutputVariable.py +22 -0
- MDANSE/Framework/OutputVariables/__init__.py +20 -0
- MDANSE/Framework/Parsers/ASE.py +67 -0
- MDANSE/Framework/Parsers/CASTEP_MD.py +113 -0
- MDANSE/Framework/Parsers/CP2K_Cell.py +88 -0
- MDANSE/Framework/Parsers/DCDFile.py +190 -0
- MDANSE/Framework/Parsers/DLPoly.py +326 -0
- MDANSE/Framework/Parsers/FortranUnformat.py +98 -0
- MDANSE/Framework/Parsers/LAMMPS.py +822 -0
- MDANSE/Framework/Parsers/LAMMPSConfig.py +759 -0
- MDANSE/Framework/Parsers/Parser.py +73 -0
- MDANSE/Framework/Parsers/XDatCar.py +156 -0
- MDANSE/Framework/Parsers/__init__.py +33 -0
- MDANSE/Framework/Parsers/pdb.py +216 -0
- MDANSE/Framework/Parsers/trj.py +236 -0
- MDANSE/Framework/Parsers/xtd.py +162 -0
- MDANSE/Framework/Parsers/xyz.py +93 -0
- MDANSE/Framework/Projectors/AxialProjector.py +44 -0
- MDANSE/Framework/Projectors/IProjector.py +39 -0
- MDANSE/Framework/Projectors/NullProjector.py +26 -0
- MDANSE/Framework/Projectors/PlanarProjector.py +44 -0
- MDANSE/Framework/Projectors/__init__.py +20 -0
- MDANSE/Framework/QVectors/CircularLatticeQVectors.py +111 -0
- MDANSE/Framework/QVectors/CircularQVectors.py +146 -0
- MDANSE/Framework/QVectors/DispersionLatticeQVectors.py +81 -0
- MDANSE/Framework/QVectors/DispersionQVectors.py +125 -0
- MDANSE/Framework/QVectors/GridQVectors.py +103 -0
- MDANSE/Framework/QVectors/IQVectors.py +272 -0
- MDANSE/Framework/QVectors/LatticeQVectors.py +31 -0
- MDANSE/Framework/QVectors/LinearLatticeQVectors.py +103 -0
- MDANSE/Framework/QVectors/LinearQVectors.py +112 -0
- MDANSE/Framework/QVectors/MillerIndicesQVectors.py +110 -0
- MDANSE/Framework/QVectors/SphericalLatticeQVectors.py +95 -0
- MDANSE/Framework/QVectors/SphericalQVectors.py +113 -0
- MDANSE/Framework/QVectors/__init__.py +35 -0
- MDANSE/Framework/Status.py +123 -0
- MDANSE/Framework/Units.py +1236 -0
- MDANSE/Framework/__init__.py +15 -0
- MDANSE/Framework/units.json +786 -0
- MDANSE/IO/AtomInfo.py +81 -0
- MDANSE/IO/IOUtils.py +386 -0
- MDANSE/IO/__init__.py +15 -0
- MDANSE/MLogging/__init__.py +28 -0
- MDANSE/Mathematics/Arithmetic.py +199 -0
- MDANSE/Mathematics/Geometry.py +144 -0
- MDANSE/Mathematics/LinearAlgebra.py +226 -0
- MDANSE/Mathematics/Signal.py +1170 -0
- MDANSE/Mathematics/__init__.py +15 -0
- MDANSE/MolecularDynamics/Analysis.py +112 -0
- MDANSE/MolecularDynamics/Configuration.py +749 -0
- MDANSE/MolecularDynamics/Connectivity.py +186 -0
- MDANSE/MolecularDynamics/MockTrajectory.py +538 -0
- MDANSE/MolecularDynamics/Trajectory.py +1343 -0
- MDANSE/MolecularDynamics/TrajectoryUtils.py +61 -0
- MDANSE/MolecularDynamics/UnitCell.py +154 -0
- MDANSE/MolecularDynamics/__init__.py +15 -0
- MDANSE/Scripts/__init__.py +15 -0
- MDANSE/Scripts/mdanse.py +475 -0
- MDANSE/Trajectory/FileTrajBase.py +293 -0
- MDANSE/Trajectory/H5MDTrajectory.py +569 -0
- MDANSE/Trajectory/MdanseTrajectory.py +586 -0
- MDANSE/Trajectory/__init__.py +15 -0
- MDANSE/__init__.py +39 -0
- mdanse-2.0.0.dist-info/METADATA +994 -0
- mdanse-2.0.0.dist-info/RECORD +217 -0
- mdanse-2.0.0.dist-info/WHEEL +5 -0
- mdanse-2.0.0.dist-info/entry_points.txt +2 -0
- mdanse-2.0.0.dist-info/licenses/AUTHORS +20 -0
- mdanse-2.0.0.dist-info/licenses/LICENSE +675 -0
- mdanse-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
# This file is part of MDANSE.
|
|
2
|
+
#
|
|
3
|
+
# MDANSE is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
#
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import copy
|
|
19
|
+
import itertools as it
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
from collections.abc import Iterable
|
|
22
|
+
from functools import reduce
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, SupportsInt
|
|
25
|
+
|
|
26
|
+
import h5py
|
|
27
|
+
import networkx as nx
|
|
28
|
+
import numpy as np
|
|
29
|
+
from more_itertools import padded
|
|
30
|
+
from rdkit import Chem
|
|
31
|
+
from rdkit.Chem import rdDetermineBonds
|
|
32
|
+
from rdkit.Geometry import Point3D
|
|
33
|
+
|
|
34
|
+
from MDANSE.Chemistry import ATOMS_DATABASE
|
|
35
|
+
from MDANSE.Framework.Units import measure
|
|
36
|
+
from MDANSE.MLogging import LOG
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def assign_molecules_after_atom_selection(
|
|
40
|
+
selected_indices: Iterable[int], original_cs: ChemicalSystem, new_cs: ChemicalSystem
|
|
41
|
+
):
|
|
42
|
+
"""Create molecule definitions for the output trajectory based on selection.
|
|
43
|
+
|
|
44
|
+
Used by jobs which have both an input and an output trajectory.
|
|
45
|
+
Some molecules may not be completely selected in the input trajectory,
|
|
46
|
+
but they will keep the original molecule label in the output.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
selected_indices : Iterable[int]
|
|
51
|
+
The atom selection of the current analysis job.
|
|
52
|
+
original_cs : ChemicalSystem
|
|
53
|
+
The ChemicalSystem of the input trajectory.
|
|
54
|
+
new_cs : ChemicalSystem
|
|
55
|
+
The ChemicalSystem of the output trajectory.
|
|
56
|
+
"""
|
|
57
|
+
selected_idxs = set(selected_indices)
|
|
58
|
+
indx_map = {j: i for i, j in enumerate(selected_indices)}
|
|
59
|
+
|
|
60
|
+
selected_bonds = [
|
|
61
|
+
(indx_map[i], indx_map[j])
|
|
62
|
+
for i, j in original_cs._bonds
|
|
63
|
+
if i in selected_idxs and j in selected_idxs
|
|
64
|
+
]
|
|
65
|
+
new_cs.add_bonds(selected_bonds)
|
|
66
|
+
|
|
67
|
+
selected_clusters = defaultdict(list)
|
|
68
|
+
for key, vals in original_cs._clusters.items():
|
|
69
|
+
for val in vals:
|
|
70
|
+
new_cluster = set(val) & selected_idxs
|
|
71
|
+
if new_cluster:
|
|
72
|
+
new_cluster = sorted(indx_map[i] for i in new_cluster)
|
|
73
|
+
selected_clusters[key].append(new_cluster)
|
|
74
|
+
|
|
75
|
+
new_cs._clusters = selected_clusters
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ChemicalSystem:
|
|
79
|
+
"""Stores the contents and topology of a trajectory."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, name: str = "", trajectory=None):
|
|
82
|
+
"""Populate the arrays with values from the trajectory.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
name : str, optional
|
|
87
|
+
text label of this system
|
|
88
|
+
trajectory : Trajectory, optional
|
|
89
|
+
instance of the Trajectory class, by default None
|
|
90
|
+
"""
|
|
91
|
+
self.name = str(name)
|
|
92
|
+
self._database = ATOMS_DATABASE
|
|
93
|
+
if trajectory is not None:
|
|
94
|
+
self._database = trajectory
|
|
95
|
+
|
|
96
|
+
self._total_number_of_atoms = 0
|
|
97
|
+
self._atom_types = []
|
|
98
|
+
self._atom_names = None
|
|
99
|
+
self._atom_indices = []
|
|
100
|
+
self._labels = {} # arbitrary tag attached to atoms (e.g. residue name)
|
|
101
|
+
|
|
102
|
+
self._bonds = []
|
|
103
|
+
|
|
104
|
+
self._clusters = {}
|
|
105
|
+
|
|
106
|
+
self.rdkit_mol = Chem.RWMol()
|
|
107
|
+
# rdkit DetermineBondOrders doesn't work with dummy atoms. we
|
|
108
|
+
# avoid adding them to the rdkit_mol but will need to map from
|
|
109
|
+
# the rdkit atom indexes to the ones in MDANSE
|
|
110
|
+
self._rdkit_map = {}
|
|
111
|
+
self._rdkit_map_inv = {}
|
|
112
|
+
self._rdkit_dummy_atms = set()
|
|
113
|
+
|
|
114
|
+
self._unique_elements = set()
|
|
115
|
+
|
|
116
|
+
def __str__(self):
|
|
117
|
+
return (
|
|
118
|
+
f"ChemicalSystem {self.name} consisting of {len(self._atom_types)}"
|
|
119
|
+
" atoms in {len(self._clusters)} molecules"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def initialise_atoms(
|
|
123
|
+
self,
|
|
124
|
+
element_list: list[str],
|
|
125
|
+
name_list: list[str] | None = None,
|
|
126
|
+
):
|
|
127
|
+
"""Assign indices to atoms, save their types and names.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
element_list : list[str]
|
|
132
|
+
list of chemical element labels
|
|
133
|
+
name_list : Optional[list[str]], optional
|
|
134
|
+
list of atom text labels from trajectory, by default None
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
self._atom_indices = [
|
|
138
|
+
self.add_atom(self._database.get_atom_property(symbol, "atomic_number"))
|
|
139
|
+
for symbol in element_list
|
|
140
|
+
]
|
|
141
|
+
self._atom_types = [str(x) for x in element_list]
|
|
142
|
+
self._total_number_of_atoms = len(self._atom_indices)
|
|
143
|
+
self._unique_elements.update(set(element_list))
|
|
144
|
+
if name_list is not None:
|
|
145
|
+
self._atom_names = [str(x) for x in name_list]
|
|
146
|
+
|
|
147
|
+
self._rdkit_dummy_atms = {
|
|
148
|
+
atom.GetIdx()
|
|
149
|
+
for atom in self.rdkit_mol.GetAtoms()
|
|
150
|
+
if atom.GetAtomicNum() == 0
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def add_atom(self, atm_num: int) -> int:
|
|
154
|
+
rdkit_atm = Chem.Atom(atm_num) if atm_num is not None else Chem.Atom(0)
|
|
155
|
+
rdkit_atm.SetNumExplicitHs(0)
|
|
156
|
+
rdkit_atm.SetNoImplicit(True)
|
|
157
|
+
return self.rdkit_mol.AddAtom(rdkit_atm)
|
|
158
|
+
|
|
159
|
+
def add_bonds(self, pair_list: Iterable[tuple[SupportsInt, SupportsInt]]):
|
|
160
|
+
self._bonds.extend(pair_list)
|
|
161
|
+
for pair in pair_list:
|
|
162
|
+
i, j = int(pair[0]), int(pair[1])
|
|
163
|
+
if i not in self._rdkit_dummy_atms and j not in self._rdkit_dummy_atms:
|
|
164
|
+
# Ignore the bonds to any dummy atoms. This means that
|
|
165
|
+
# the SMARTS pattern match will ignore any bonds to
|
|
166
|
+
# dummy atoms. E.g. [OX2] will match oxygen atoms connected
|
|
167
|
+
# to two other real atoms even if it is also connected to
|
|
168
|
+
# dummy atoms.
|
|
169
|
+
self.rdkit_mol.AddBond(i, j, Chem.rdchem.BondType.UNSPECIFIED)
|
|
170
|
+
|
|
171
|
+
def set_bond_orders(
|
|
172
|
+
self, coords: np.ndarray, *, max_iters: int = 1000, max_natms: int = 100
|
|
173
|
+
):
|
|
174
|
+
"""Set the bond types for the bonds in the rdkit_mol.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
coords : np.ndarray
|
|
179
|
+
The coordinates of the system to determine the bond types
|
|
180
|
+
from.
|
|
181
|
+
max_iters: int
|
|
182
|
+
The maximum number of iterations used in the rdkit
|
|
183
|
+
DetermineBondOrders function.
|
|
184
|
+
max_natms : int
|
|
185
|
+
Skips DetermineBondOrders for molecules with a number of atoms
|
|
186
|
+
larger than max_natms.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
uniq_submols = {}
|
|
190
|
+
coord_ang = coords * measure(1.0, "nm").toval("ang")
|
|
191
|
+
|
|
192
|
+
# Calling rdDetermineBonds.DetermineBondOrders for a large system
|
|
193
|
+
# is quite computationally expensive. It's better to call this
|
|
194
|
+
# function for unique molecules and then copy over all the bond
|
|
195
|
+
# types for all others. We also need to remove dummy atom before
|
|
196
|
+
# using rdDetermineBonds.DetermineBondOrders because it can't
|
|
197
|
+
# deal with them.
|
|
198
|
+
for cluster_name in self._clusters:
|
|
199
|
+
for idx, cluster in enumerate(self._clusters[cluster_name]):
|
|
200
|
+
cluster_no_dummies = [
|
|
201
|
+
i for i in cluster if i not in self._rdkit_dummy_atms
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
if len(cluster_no_dummies) > max_natms:
|
|
205
|
+
LOG.warning(
|
|
206
|
+
f"Number of atoms in {cluster_name} with idx {idx} "
|
|
207
|
+
f"is very large - skipping bond type determination. "
|
|
208
|
+
f"SMARTS pattern matching in the atom selection will not "
|
|
209
|
+
f"work as expected. Bond types set to UNSPECIFIED, use "
|
|
210
|
+
f"the bond type wildcard ~ to match bonds in this "
|
|
211
|
+
f"molecule and use general atom symbols e.g. [#6] instead "
|
|
212
|
+
f"of [C] or [c]. See "
|
|
213
|
+
f"https://www.daylight.com/dayhtml/doc/theory/theory.smarts.html "
|
|
214
|
+
f"for more details on SMARTS pattern matching."
|
|
215
|
+
)
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
mapping = {}
|
|
219
|
+
|
|
220
|
+
submolecule = Chem.RWMol()
|
|
221
|
+
for i in cluster_no_dummies:
|
|
222
|
+
new_idx = submolecule.AddAtom(self.rdkit_mol.GetAtomWithIdx(i))
|
|
223
|
+
mapping[i] = new_idx
|
|
224
|
+
bond_idxs = []
|
|
225
|
+
for i, j in it.combinations(cluster_no_dummies, 2):
|
|
226
|
+
bond = self.rdkit_mol.GetBondBetweenAtoms(i, j)
|
|
227
|
+
if bond is None:
|
|
228
|
+
continue
|
|
229
|
+
k = bond.GetBeginAtomIdx()
|
|
230
|
+
m = bond.GetEndAtomIdx()
|
|
231
|
+
submolecule.AddBond(
|
|
232
|
+
mapping[k], mapping[m], Chem.rdchem.BondType.UNSPECIFIED
|
|
233
|
+
)
|
|
234
|
+
bond_idxs.append(bond.GetIdx())
|
|
235
|
+
|
|
236
|
+
# canonical=False should deal molecules in the same
|
|
237
|
+
# cluster_name group which do not have the same atom ordering
|
|
238
|
+
smiles = Chem.MolToSmiles(submolecule, canonical=False)
|
|
239
|
+
|
|
240
|
+
if smiles in uniq_submols:
|
|
241
|
+
submolecule = uniq_submols[smiles]
|
|
242
|
+
else:
|
|
243
|
+
conf = Chem.Conformer(len(cluster_no_dummies))
|
|
244
|
+
for i, j in enumerate(cluster_no_dummies):
|
|
245
|
+
if j not in self._rdkit_dummy_atms:
|
|
246
|
+
x, y, z = coord_ang[j]
|
|
247
|
+
conf.SetAtomPosition(i, Point3D(x, y, z))
|
|
248
|
+
submolecule.AddConformer(conf)
|
|
249
|
+
try:
|
|
250
|
+
LOG.info(
|
|
251
|
+
f"Determining bond orders for molecule "
|
|
252
|
+
f"{cluster_name} with index {idx}"
|
|
253
|
+
)
|
|
254
|
+
rdDetermineBonds.DetermineBondOrders(
|
|
255
|
+
submolecule,
|
|
256
|
+
charge=0,
|
|
257
|
+
maxIterations=max_iters,
|
|
258
|
+
)
|
|
259
|
+
except Exception as e:
|
|
260
|
+
LOG.warning(
|
|
261
|
+
f"Error determining bond orders for molecule "
|
|
262
|
+
f"{cluster_name} with index {idx} from RDKit: {e}. "
|
|
263
|
+
f"SMARTS pattern matching in the atoms selection "
|
|
264
|
+
f"will not work as expected. Bond types set to "
|
|
265
|
+
f"UNSPECIFIED use bond type wildcard ~ to match "
|
|
266
|
+
f"bonds in this molecule."
|
|
267
|
+
)
|
|
268
|
+
uniq_submols[smiles] = submolecule
|
|
269
|
+
|
|
270
|
+
for i, j in enumerate(bond_idxs):
|
|
271
|
+
submol_bond = submolecule.GetBondWithIdx(i)
|
|
272
|
+
mol_bond = self.rdkit_mol.GetBondWithIdx(j)
|
|
273
|
+
bond_type = submol_bond.GetBondType()
|
|
274
|
+
mol_bond.SetBondType(bond_type)
|
|
275
|
+
if bond_type == Chem.rdchem.BondType.AROMATIC:
|
|
276
|
+
mol_bond.GetBeginAtom().SetIsAromatic(True)
|
|
277
|
+
mol_bond.GetEndAtom().SetIsAromatic(True)
|
|
278
|
+
|
|
279
|
+
# determine ring info for the rdkit_mol so the SMART pattern like
|
|
280
|
+
# [cR1] can be used
|
|
281
|
+
Chem.GetSymmSSSR(self.rdkit_mol)
|
|
282
|
+
|
|
283
|
+
def add_labels(self, label_dict: dict[str, list[int]]):
|
|
284
|
+
for key, item in label_dict.items():
|
|
285
|
+
self._labels[key] = self._labels.get(key, []) + item
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def _rename_isotopes(element: str):
|
|
289
|
+
if element[-1].isdigit():
|
|
290
|
+
return f"[{element}]"
|
|
291
|
+
return element
|
|
292
|
+
|
|
293
|
+
def add_clusters(self, group_list: list[list[int]]):
|
|
294
|
+
for group in group_list:
|
|
295
|
+
sorted_group = sorted(set(group))
|
|
296
|
+
|
|
297
|
+
if len(sorted_group) < 2:
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
atom_list = [self._atom_types[index] for index in group]
|
|
301
|
+
unique_atoms, counts = np.unique(atom_list, return_counts=True)
|
|
302
|
+
unique_atoms = map(self._rename_isotopes, unique_atoms)
|
|
303
|
+
name = "_".join(
|
|
304
|
+
f"{atom}{count}"
|
|
305
|
+
for atom, count in zip(unique_atoms, counts, strict=True)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if name not in self._clusters:
|
|
309
|
+
self._clusters[name] = [sorted_group]
|
|
310
|
+
elif sorted_group not in self._clusters[name]:
|
|
311
|
+
self._clusters[name].append(group)
|
|
312
|
+
|
|
313
|
+
def has_substructure_match(self, smarts: str) -> bool:
|
|
314
|
+
"""Check if there is a substructure match.
|
|
315
|
+
|
|
316
|
+
Parameters
|
|
317
|
+
----------
|
|
318
|
+
smarts : str
|
|
319
|
+
SMARTS string.
|
|
320
|
+
|
|
321
|
+
Returns
|
|
322
|
+
-------
|
|
323
|
+
bool
|
|
324
|
+
True if the there is a substructure match.
|
|
325
|
+
|
|
326
|
+
"""
|
|
327
|
+
return self.rdkit_mol.HasSubstructMatch(Chem.MolFromSmarts(smarts))
|
|
328
|
+
|
|
329
|
+
def get_substructure_matches(
|
|
330
|
+
self,
|
|
331
|
+
smarts: str,
|
|
332
|
+
maxmatches: int = 1000000,
|
|
333
|
+
) -> set[int]:
|
|
334
|
+
"""Get the indices which match the smarts string. Note that
|
|
335
|
+
the default bond type in MDANSE is
|
|
336
|
+
Chem.rdchem.BondType.UNSPECIFIED.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
smarts : str
|
|
341
|
+
SMARTS string.
|
|
342
|
+
maxmatches : int
|
|
343
|
+
Maximum number of matches used in the GetSubstructMatches
|
|
344
|
+
rdkit method.
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
set[int]
|
|
349
|
+
An set of matched atom indices.
|
|
350
|
+
"""
|
|
351
|
+
try:
|
|
352
|
+
matches = self.rdkit_mol.GetSubstructMatches(
|
|
353
|
+
Chem.MolFromSmarts(smarts), maxMatches=maxmatches
|
|
354
|
+
)
|
|
355
|
+
except RuntimeError as e:
|
|
356
|
+
LOG.error(f"Unable to run pattern match using {smarts}: {e}")
|
|
357
|
+
return set()
|
|
358
|
+
return {ind for match in matches for ind in match}
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def atom_list(self) -> list[str]:
|
|
362
|
+
"""Return the types of all atoms in the ChemicalSystem."""
|
|
363
|
+
return self._atom_types
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def name_list(self) -> list[str]:
|
|
367
|
+
"""Return the names of all atoms in the ChemicalSystem."""
|
|
368
|
+
if self._atom_names is not None:
|
|
369
|
+
return self._atom_names
|
|
370
|
+
return self._atom_types
|
|
371
|
+
|
|
372
|
+
def atom_property(self, atom_property: str) -> list[Any]:
|
|
373
|
+
"""Return the values of a specific property, for all atoms in the system."""
|
|
374
|
+
lookup = {}
|
|
375
|
+
for atom in self._unique_elements:
|
|
376
|
+
lookup[atom] = self._database.get_atom_property(atom, atom_property)
|
|
377
|
+
return [lookup[atom] for atom in self.atom_list]
|
|
378
|
+
|
|
379
|
+
def grouping_level(self, index: int) -> int:
|
|
380
|
+
"""Temporarily, there is no grouping test.
|
|
381
|
+
|
|
382
|
+
Parameters
|
|
383
|
+
----------
|
|
384
|
+
index : int
|
|
385
|
+
atom index
|
|
386
|
+
|
|
387
|
+
Returns
|
|
388
|
+
-------
|
|
389
|
+
int
|
|
390
|
+
grouping level for the GroupingLevelConfigurator
|
|
391
|
+
|
|
392
|
+
"""
|
|
393
|
+
return 0
|
|
394
|
+
|
|
395
|
+
def copy(self) -> ChemicalSystem:
|
|
396
|
+
"""Return a new instance of ChemicalSystem with the same contents.
|
|
397
|
+
|
|
398
|
+
Returns
|
|
399
|
+
-------
|
|
400
|
+
ChemicalSystem
|
|
401
|
+
A copy of the existing ChemicalSystem.
|
|
402
|
+
|
|
403
|
+
"""
|
|
404
|
+
cs = ChemicalSystem(self.name)
|
|
405
|
+
|
|
406
|
+
for attribute_name, attribute_value in self.__dict__.items():
|
|
407
|
+
if attribute_name in ["rdkit_mol", "_configuration"]:
|
|
408
|
+
continue
|
|
409
|
+
setattr(cs, attribute_name, copy.deepcopy(attribute_value))
|
|
410
|
+
|
|
411
|
+
return cs
|
|
412
|
+
|
|
413
|
+
def find_clusters_from_bonds(self):
|
|
414
|
+
"""Build cluster information based on the saved chemical bonds.
|
|
415
|
+
|
|
416
|
+
Builds graphs and walks them to identify all the atoms that can
|
|
417
|
+
be reached from a starting atom by following bonds.
|
|
418
|
+
"""
|
|
419
|
+
molecules = []
|
|
420
|
+
atom_pool = list(self._atom_indices)
|
|
421
|
+
|
|
422
|
+
total_graph = nx.Graph()
|
|
423
|
+
total_graph.add_nodes_from(atom_pool)
|
|
424
|
+
total_graph.add_edges_from(self._bonds)
|
|
425
|
+
while len(atom_pool) > 0:
|
|
426
|
+
last_atom = atom_pool.pop()
|
|
427
|
+
temp_dict = nx.dfs_successors(total_graph, last_atom)
|
|
428
|
+
others = reduce(list.__add__, temp_dict.values(), [])
|
|
429
|
+
for atom in others:
|
|
430
|
+
atom_pool.remove(atom)
|
|
431
|
+
molecule = [last_atom, *others]
|
|
432
|
+
molecules.append(sorted(molecule))
|
|
433
|
+
self.add_clusters(molecules)
|
|
434
|
+
|
|
435
|
+
def unique_molecules(self) -> list[str]:
|
|
436
|
+
"""Return the list of unique names in the chemical system."""
|
|
437
|
+
return [str(x) for x in self._clusters]
|
|
438
|
+
|
|
439
|
+
def number_of_molecules(self, molecule_name: str) -> int:
|
|
440
|
+
"""Return the number of molecules with the given name in the system."""
|
|
441
|
+
return len(self._clusters[molecule_name])
|
|
442
|
+
|
|
443
|
+
@property
|
|
444
|
+
def number_of_atoms(self) -> int:
|
|
445
|
+
"""The number of non-ghost atoms in the ChemicalSystem."""
|
|
446
|
+
return self._total_number_of_atoms
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def all_indices(self) -> set[int]:
|
|
450
|
+
"""The number of non-ghost atoms in the ChemicalSystem."""
|
|
451
|
+
return set(self._atom_indices)
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def total_number_of_atoms(self) -> int:
|
|
455
|
+
"""The number of all atoms in the ChemicalSystem, including ghost ones."""
|
|
456
|
+
return self._total_number_of_atoms
|
|
457
|
+
|
|
458
|
+
def serialize(self, h5_file: h5py.File) -> None:
|
|
459
|
+
"""Write the current system information into the HDF5 file.
|
|
460
|
+
|
|
461
|
+
Parameters
|
|
462
|
+
----------
|
|
463
|
+
h5_file : h5py.File
|
|
464
|
+
File object of the target trajectory, open for writing.
|
|
465
|
+
|
|
466
|
+
"""
|
|
467
|
+
string_dt = h5py.special_dtype(vlen=str)
|
|
468
|
+
|
|
469
|
+
grp = h5_file.create_group("/composition")
|
|
470
|
+
grp.attrs["name"] = self.name
|
|
471
|
+
|
|
472
|
+
grp.create_dataset("atom_types", data=self._atom_types, dtype=string_dt)
|
|
473
|
+
if self._atom_names is not None:
|
|
474
|
+
grp.create_dataset("atom_names", data=self._atom_names, dtype=string_dt)
|
|
475
|
+
grp.create_dataset("atom_indices", data=self._atom_indices)
|
|
476
|
+
grp.create_dataset("bonds", data=np.array(self._bonds))
|
|
477
|
+
|
|
478
|
+
label_group = grp.create_group("labels")
|
|
479
|
+
for key, value in self._labels.items():
|
|
480
|
+
label_group.create_dataset(key, data=value)
|
|
481
|
+
clusters_group = grp.create_group("clusters")
|
|
482
|
+
for key, vals in self._clusters.items():
|
|
483
|
+
# unable to store array with inhomogeneous row lengths
|
|
484
|
+
# we will pad them with -1, we will ignore these values
|
|
485
|
+
# when the trajectory get loaded up see self.load
|
|
486
|
+
size = max(len(val) for val in vals)
|
|
487
|
+
new_vals = [list(padded(val, fillvalue=-1, n=size)) for val in vals]
|
|
488
|
+
clusters_group.create_dataset(key, data=new_vals)
|
|
489
|
+
|
|
490
|
+
def load(self, trajectory: h5py.File | Path | str):
|
|
491
|
+
"""Read the ChemicalSystem information from the trajectory.
|
|
492
|
+
|
|
493
|
+
Parameters
|
|
494
|
+
----------
|
|
495
|
+
trajectory : str | h5py.File
|
|
496
|
+
Filename or a file object of the trajectory.
|
|
497
|
+
"""
|
|
498
|
+
close_on_end = False
|
|
499
|
+
if isinstance(trajectory, Path | str):
|
|
500
|
+
close_on_end = True
|
|
501
|
+
source = h5py.File(trajectory)
|
|
502
|
+
else:
|
|
503
|
+
source = trajectory
|
|
504
|
+
|
|
505
|
+
assert isinstance(source, h5py.File | dict)
|
|
506
|
+
|
|
507
|
+
if "composition" not in source:
|
|
508
|
+
if close_on_end:
|
|
509
|
+
source.close()
|
|
510
|
+
self.legacy_load(trajectory)
|
|
511
|
+
return
|
|
512
|
+
|
|
513
|
+
self.rdkit_mol = Chem.RWMol()
|
|
514
|
+
|
|
515
|
+
grp = source["/composition"]
|
|
516
|
+
self.name = grp.attrs["name"]
|
|
517
|
+
|
|
518
|
+
atom_types = [binary.decode("utf-8") for binary in grp["atom_types"][:]]
|
|
519
|
+
atom_names = None
|
|
520
|
+
if "atom_names" in grp:
|
|
521
|
+
atom_names = [binary.decode("utf-8") for binary in grp["atom_names"][:]]
|
|
522
|
+
self.initialise_atoms(atom_types, atom_names)
|
|
523
|
+
old_indices = [int(tag) for tag in grp["atom_indices"][:]]
|
|
524
|
+
if not np.allclose(old_indices, self._atom_indices):
|
|
525
|
+
LOG.error("Atoms got re-indexed on loading the trajectory")
|
|
526
|
+
|
|
527
|
+
self.add_bonds([[int(pair[0]), int(pair[1])] for pair in grp["bonds"][:]])
|
|
528
|
+
|
|
529
|
+
self._labels = {
|
|
530
|
+
label: [int(tag) for tag in grp[f"labels/{label}"]]
|
|
531
|
+
for label in map(str, grp["labels"])
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
for cluster in grp["clusters"]:
|
|
535
|
+
self._clusters[str(cluster)] = [
|
|
536
|
+
[int(x) for x in line if int(x) >= 0]
|
|
537
|
+
for line in grp[f"clusters/{cluster}"]
|
|
538
|
+
]
|
|
539
|
+
if close_on_end:
|
|
540
|
+
source.close()
|
|
541
|
+
|
|
542
|
+
def legacy_load(self, trajectory: h5py.File | Path | str):
|
|
543
|
+
"""Read the ChemicalSystem from an old (pre-2025) trajectory.
|
|
544
|
+
Parameters
|
|
545
|
+
----------
|
|
546
|
+
trajectory : Path | str | h5py.File
|
|
547
|
+
Filename or a file object of the trajectory.
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
close_on_end = False
|
|
551
|
+
if isinstance(trajectory, (Path, str)):
|
|
552
|
+
close_on_end = True
|
|
553
|
+
source = h5py.File(trajectory)
|
|
554
|
+
else:
|
|
555
|
+
source = trajectory
|
|
556
|
+
|
|
557
|
+
self.rdkit_mol = Chem.RWMol()
|
|
558
|
+
|
|
559
|
+
grp = source["/chemical_system"]
|
|
560
|
+
self.name = grp.attrs["name"]
|
|
561
|
+
atoms = grp["atoms"]
|
|
562
|
+
element_list = [line[0].decode("utf-8").strip("'") for line in atoms]
|
|
563
|
+
self.initialise_atoms(element_list)
|
|
564
|
+
|
|
565
|
+
bonds = grp["bonds"]
|
|
566
|
+
bond_list = bonds[:]
|
|
567
|
+
self.add_bonds([[int(pair[0]), int(pair[1])] for pair in bond_list])
|
|
568
|
+
|
|
569
|
+
if "atom_clusters" in grp:
|
|
570
|
+
cluster_list = []
|
|
571
|
+
for line in grp["atom_clusters"]:
|
|
572
|
+
indices_string = line[0].decode("utf-8")
|
|
573
|
+
indices_list = [int(x) for x in indices_string.strip("[]").split(",")]
|
|
574
|
+
if indices_list:
|
|
575
|
+
cluster_list.append(indices_list)
|
|
576
|
+
if cluster_list:
|
|
577
|
+
self.add_clusters(cluster_list)
|
|
578
|
+
if close_on_end:
|
|
579
|
+
source.close()
|
|
580
|
+
|
|
581
|
+
self.find_clusters_from_bonds()
|