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.
Files changed (217) hide show
  1. MDANSE/Chemistry/ChemicalSystem.py +581 -0
  2. MDANSE/Chemistry/Databases.py +818 -0
  3. MDANSE/Chemistry/__init__.py +22 -0
  4. MDANSE/Chemistry/atoms.json +17164 -0
  5. MDANSE/Chemistry/neutron_scattering_lengths_nist.csv +389 -0
  6. MDANSE/Core/Platform.py +214 -0
  7. MDANSE/Core/Singleton.py +34 -0
  8. MDANSE/Core/SubclassFactory.py +203 -0
  9. MDANSE/Core/__init__.py +15 -0
  10. MDANSE/Framework/AtomGrouping/grouping.py +380 -0
  11. MDANSE/Framework/AtomMapping/__init__.py +24 -0
  12. MDANSE/Framework/AtomMapping/atom_mapping.py +289 -0
  13. MDANSE/Framework/AtomSelector/__init__.py +15 -0
  14. MDANSE/Framework/AtomSelector/atom_selection.py +106 -0
  15. MDANSE/Framework/AtomSelector/general_selection.py +112 -0
  16. MDANSE/Framework/AtomSelector/group_selection.py +79 -0
  17. MDANSE/Framework/AtomSelector/molecule_selection.py +48 -0
  18. MDANSE/Framework/AtomSelector/selection_builder.py +451 -0
  19. MDANSE/Framework/AtomSelector/selector.py +294 -0
  20. MDANSE/Framework/AtomSelector/spatial_selection.py +105 -0
  21. MDANSE/Framework/Configurable.py +396 -0
  22. MDANSE/Framework/Configurators/AseInputFileConfigurator.py +85 -0
  23. MDANSE/Framework/Configurators/AtomMappingConfigurator.py +81 -0
  24. MDANSE/Framework/Configurators/AtomSelectionConfigurator.py +86 -0
  25. MDANSE/Framework/Configurators/AtomTransmutationConfigurator.py +188 -0
  26. MDANSE/Framework/Configurators/AxisSelectionConfigurator.py +94 -0
  27. MDANSE/Framework/Configurators/BooleanConfigurator.py +65 -0
  28. MDANSE/Framework/Configurators/CorrelationFramesConfigurator.py +79 -0
  29. MDANSE/Framework/Configurators/DerivativeOrderConfigurator.py +76 -0
  30. MDANSE/Framework/Configurators/DistHistCutoffConfigurator.py +106 -0
  31. MDANSE/Framework/Configurators/FileWithAtomDataConfigurator.py +103 -0
  32. MDANSE/Framework/Configurators/FloatConfigurator.py +87 -0
  33. MDANSE/Framework/Configurators/FramesConfigurator.py +108 -0
  34. MDANSE/Framework/Configurators/GridStepConfigurator.py +122 -0
  35. MDANSE/Framework/Configurators/GroupingLevelConfigurator.py +88 -0
  36. MDANSE/Framework/Configurators/HDFInputFileConfigurator.py +89 -0
  37. MDANSE/Framework/Configurators/HDFTrajectoryConfigurator.py +94 -0
  38. MDANSE/Framework/Configurators/IConfigurator.py +340 -0
  39. MDANSE/Framework/Configurators/InputFileConfigurator.py +72 -0
  40. MDANSE/Framework/Configurators/InstrumentResolutionConfigurator.py +113 -0
  41. MDANSE/Framework/Configurators/IntegerConfigurator.py +99 -0
  42. MDANSE/Framework/Configurators/InterpolationOrderConfigurator.py +103 -0
  43. MDANSE/Framework/Configurators/MDAnalysisCoordinateFileConfigurator.py +81 -0
  44. MDANSE/Framework/Configurators/MDAnalysisTimeStepConfigurator.py +76 -0
  45. MDANSE/Framework/Configurators/MDAnalysisTopologyFileConfigurator.py +94 -0
  46. MDANSE/Framework/Configurators/MDMCTrajectoryConfigurator.py +70 -0
  47. MDANSE/Framework/Configurators/MDTrajTimeStepConfigurator.py +64 -0
  48. MDANSE/Framework/Configurators/MDTrajTopologyFileConfigurator.py +112 -0
  49. MDANSE/Framework/Configurators/MDTrajTrajectoryFileConfigurator.py +44 -0
  50. MDANSE/Framework/Configurators/MockTrajectoryConfigurator.py +74 -0
  51. MDANSE/Framework/Configurators/MoleculeSelectionConfigurator.py +85 -0
  52. MDANSE/Framework/Configurators/MultiInputFileConfigurator.py +84 -0
  53. MDANSE/Framework/Configurators/MultipleChoicesConfigurator.py +81 -0
  54. MDANSE/Framework/Configurators/OptionalFloatConfigurator.py +94 -0
  55. MDANSE/Framework/Configurators/OutputFilesConfigurator.py +129 -0
  56. MDANSE/Framework/Configurators/OutputStructureConfigurator.py +95 -0
  57. MDANSE/Framework/Configurators/OutputTrajectoryConfigurator.py +114 -0
  58. MDANSE/Framework/Configurators/PartialChargeConfigurator.py +217 -0
  59. MDANSE/Framework/Configurators/ProjectionConfigurator.py +96 -0
  60. MDANSE/Framework/Configurators/QRangeConfigurator.py +35 -0
  61. MDANSE/Framework/Configurators/QVectorsConfigurator.py +137 -0
  62. MDANSE/Framework/Configurators/RangeConfigurator.py +136 -0
  63. MDANSE/Framework/Configurators/RunningModeConfigurator.py +72 -0
  64. MDANSE/Framework/Configurators/SingleChoiceConfigurator.py +63 -0
  65. MDANSE/Framework/Configurators/TrajectoryFilterConfigurator.py +133 -0
  66. MDANSE/Framework/Configurators/UnitCellConfigurator.py +122 -0
  67. MDANSE/Framework/Configurators/VectorConfigurator.py +89 -0
  68. MDANSE/Framework/Configurators/WeightsConfigurator.py +140 -0
  69. MDANSE/Framework/Configurators/__init__.py +124 -0
  70. MDANSE/Framework/Converters/ASE.py +334 -0
  71. MDANSE/Framework/Converters/CASTEP.py +164 -0
  72. MDANSE/Framework/Converters/CHARMM.py +24 -0
  73. MDANSE/Framework/Converters/CP2K.py +227 -0
  74. MDANSE/Framework/Converters/Converter.py +82 -0
  75. MDANSE/Framework/Converters/DCD.py +158 -0
  76. MDANSE/Framework/Converters/DFTB.py +64 -0
  77. MDANSE/Framework/Converters/DL_POLY.py +192 -0
  78. MDANSE/Framework/Converters/Forcite.py +218 -0
  79. MDANSE/Framework/Converters/Gromacs.py +211 -0
  80. MDANSE/Framework/Converters/LAMMPS.py +305 -0
  81. MDANSE/Framework/Converters/MDAnalysis.py +258 -0
  82. MDANSE/Framework/Converters/MDTraj.py +226 -0
  83. MDANSE/Framework/Converters/NAMD.py +24 -0
  84. MDANSE/Framework/Converters/VASP.py +187 -0
  85. MDANSE/Framework/Converters/XPLOR.py +24 -0
  86. MDANSE/Framework/Converters/__init__.py +33 -0
  87. MDANSE/Framework/Formats/FileInMemory.py +70 -0
  88. MDANSE/Framework/Formats/HDFFormat.py +239 -0
  89. MDANSE/Framework/Formats/IFormat.py +50 -0
  90. MDANSE/Framework/Formats/MDAFormat.py +34 -0
  91. MDANSE/Framework/Formats/MDTFormat.py +34 -0
  92. MDANSE/Framework/Formats/TextFormat.py +180 -0
  93. MDANSE/Framework/Formats/__init__.py +23 -0
  94. MDANSE/Framework/InstrumentResolutions/Gaussian.py +39 -0
  95. MDANSE/Framework/InstrumentResolutions/IInstrumentResolution.py +58 -0
  96. MDANSE/Framework/InstrumentResolutions/Ideal.py +35 -0
  97. MDANSE/Framework/InstrumentResolutions/Lorentzian.py +37 -0
  98. MDANSE/Framework/InstrumentResolutions/PseudoVoigt.py +49 -0
  99. MDANSE/Framework/InstrumentResolutions/Square.py +42 -0
  100. MDANSE/Framework/InstrumentResolutions/Triangular.py +40 -0
  101. MDANSE/Framework/InstrumentResolutions/__init__.py +24 -0
  102. MDANSE/Framework/Jobs/AreaPerMolecule.py +167 -0
  103. MDANSE/Framework/Jobs/AverageStructure.py +178 -0
  104. MDANSE/Framework/Jobs/CartesianCorrelationFunction.py +226 -0
  105. MDANSE/Framework/Jobs/CartesianPowerSpectrum.py +172 -0
  106. MDANSE/Framework/Jobs/CenterOfMassesTrajectory.py +218 -0
  107. MDANSE/Framework/Jobs/CoordinationNumber.py +227 -0
  108. MDANSE/Framework/Jobs/CurrentCorrelationFunction.py +693 -0
  109. MDANSE/Framework/Jobs/Density.py +190 -0
  110. MDANSE/Framework/Jobs/DensityOfStates.py +57 -0
  111. MDANSE/Framework/Jobs/DipoleAutoCorrelationFunction.py +167 -0
  112. MDANSE/Framework/Jobs/DistanceHistogram.py +256 -0
  113. MDANSE/Framework/Jobs/DynamicCoherentStructureFactor.py +457 -0
  114. MDANSE/Framework/Jobs/DynamicIncoherentStructureFactor.py +374 -0
  115. MDANSE/Framework/Jobs/Eccentricity.py +143 -0
  116. MDANSE/Framework/Jobs/ElasticIncoherentStructureFactor.py +253 -0
  117. MDANSE/Framework/Jobs/GaussianDynamicIncoherentStructureFactor.py +412 -0
  118. MDANSE/Framework/Jobs/IJob.py +582 -0
  119. MDANSE/Framework/Jobs/Infrared.py +257 -0
  120. MDANSE/Framework/Jobs/JobStatus.py +118 -0
  121. MDANSE/Framework/Jobs/MeanSquareDisplacement.py +215 -0
  122. MDANSE/Framework/Jobs/MolecularTrace.py +198 -0
  123. MDANSE/Framework/Jobs/NeutronDynamicTotalStructureFactor.py +498 -0
  124. MDANSE/Framework/Jobs/PairDistributionFunction.py +264 -0
  125. MDANSE/Framework/Jobs/PositionCorrelationFunction.py +49 -0
  126. MDANSE/Framework/Jobs/PositionPowerSpectrum.py +40 -0
  127. MDANSE/Framework/Jobs/RadiusOfGyration.py +139 -0
  128. MDANSE/Framework/Jobs/ReorientationalTimeCorrelationFunction.py +300 -0
  129. MDANSE/Framework/Jobs/RootMeanSquareDeviation.py +190 -0
  130. MDANSE/Framework/Jobs/RootMeanSquareFluctuation.py +197 -0
  131. MDANSE/Framework/Jobs/RotationAutocorrelation.py +211 -0
  132. MDANSE/Framework/Jobs/ScatteringLengthDensityProfile.py +256 -0
  133. MDANSE/Framework/Jobs/SolventAccessibleSurface.py +599 -0
  134. MDANSE/Framework/Jobs/StaticStructureFactor.py +333 -0
  135. MDANSE/Framework/Jobs/StructureFactorFromScatteringFunction.py +193 -0
  136. MDANSE/Framework/Jobs/Temperature.py +203 -0
  137. MDANSE/Framework/Jobs/TrajectoryEditor.py +260 -0
  138. MDANSE/Framework/Jobs/TrajectoryFilter.py +367 -0
  139. MDANSE/Framework/Jobs/VanHoveFunctionDistinct.py +740 -0
  140. MDANSE/Framework/Jobs/VanHoveFunctionSelf.py +376 -0
  141. MDANSE/Framework/Jobs/VelocityCorrelationFunction.py +83 -0
  142. MDANSE/Framework/Jobs/Voronoi.py +268 -0
  143. MDANSE/Framework/Jobs/XRayStaticStructureFactor.py +351 -0
  144. MDANSE/Framework/Jobs/__init__.py +95 -0
  145. MDANSE/Framework/OutputVariables/IOutputVariable.py +149 -0
  146. MDANSE/Framework/OutputVariables/LineOutputVariable.py +22 -0
  147. MDANSE/Framework/OutputVariables/SurfaceOutputVariable.py +22 -0
  148. MDANSE/Framework/OutputVariables/VolumeOutputVariable.py +22 -0
  149. MDANSE/Framework/OutputVariables/__init__.py +20 -0
  150. MDANSE/Framework/Parsers/ASE.py +67 -0
  151. MDANSE/Framework/Parsers/CASTEP_MD.py +113 -0
  152. MDANSE/Framework/Parsers/CP2K_Cell.py +88 -0
  153. MDANSE/Framework/Parsers/DCDFile.py +190 -0
  154. MDANSE/Framework/Parsers/DLPoly.py +326 -0
  155. MDANSE/Framework/Parsers/FortranUnformat.py +98 -0
  156. MDANSE/Framework/Parsers/LAMMPS.py +822 -0
  157. MDANSE/Framework/Parsers/LAMMPSConfig.py +759 -0
  158. MDANSE/Framework/Parsers/Parser.py +73 -0
  159. MDANSE/Framework/Parsers/XDatCar.py +156 -0
  160. MDANSE/Framework/Parsers/__init__.py +33 -0
  161. MDANSE/Framework/Parsers/pdb.py +216 -0
  162. MDANSE/Framework/Parsers/trj.py +236 -0
  163. MDANSE/Framework/Parsers/xtd.py +162 -0
  164. MDANSE/Framework/Parsers/xyz.py +93 -0
  165. MDANSE/Framework/Projectors/AxialProjector.py +44 -0
  166. MDANSE/Framework/Projectors/IProjector.py +39 -0
  167. MDANSE/Framework/Projectors/NullProjector.py +26 -0
  168. MDANSE/Framework/Projectors/PlanarProjector.py +44 -0
  169. MDANSE/Framework/Projectors/__init__.py +20 -0
  170. MDANSE/Framework/QVectors/CircularLatticeQVectors.py +111 -0
  171. MDANSE/Framework/QVectors/CircularQVectors.py +146 -0
  172. MDANSE/Framework/QVectors/DispersionLatticeQVectors.py +81 -0
  173. MDANSE/Framework/QVectors/DispersionQVectors.py +125 -0
  174. MDANSE/Framework/QVectors/GridQVectors.py +103 -0
  175. MDANSE/Framework/QVectors/IQVectors.py +272 -0
  176. MDANSE/Framework/QVectors/LatticeQVectors.py +31 -0
  177. MDANSE/Framework/QVectors/LinearLatticeQVectors.py +103 -0
  178. MDANSE/Framework/QVectors/LinearQVectors.py +112 -0
  179. MDANSE/Framework/QVectors/MillerIndicesQVectors.py +110 -0
  180. MDANSE/Framework/QVectors/SphericalLatticeQVectors.py +95 -0
  181. MDANSE/Framework/QVectors/SphericalQVectors.py +113 -0
  182. MDANSE/Framework/QVectors/__init__.py +35 -0
  183. MDANSE/Framework/Status.py +123 -0
  184. MDANSE/Framework/Units.py +1236 -0
  185. MDANSE/Framework/__init__.py +15 -0
  186. MDANSE/Framework/units.json +786 -0
  187. MDANSE/IO/AtomInfo.py +81 -0
  188. MDANSE/IO/IOUtils.py +386 -0
  189. MDANSE/IO/__init__.py +15 -0
  190. MDANSE/MLogging/__init__.py +28 -0
  191. MDANSE/Mathematics/Arithmetic.py +199 -0
  192. MDANSE/Mathematics/Geometry.py +144 -0
  193. MDANSE/Mathematics/LinearAlgebra.py +226 -0
  194. MDANSE/Mathematics/Signal.py +1170 -0
  195. MDANSE/Mathematics/__init__.py +15 -0
  196. MDANSE/MolecularDynamics/Analysis.py +112 -0
  197. MDANSE/MolecularDynamics/Configuration.py +749 -0
  198. MDANSE/MolecularDynamics/Connectivity.py +186 -0
  199. MDANSE/MolecularDynamics/MockTrajectory.py +538 -0
  200. MDANSE/MolecularDynamics/Trajectory.py +1343 -0
  201. MDANSE/MolecularDynamics/TrajectoryUtils.py +61 -0
  202. MDANSE/MolecularDynamics/UnitCell.py +154 -0
  203. MDANSE/MolecularDynamics/__init__.py +15 -0
  204. MDANSE/Scripts/__init__.py +15 -0
  205. MDANSE/Scripts/mdanse.py +475 -0
  206. MDANSE/Trajectory/FileTrajBase.py +293 -0
  207. MDANSE/Trajectory/H5MDTrajectory.py +569 -0
  208. MDANSE/Trajectory/MdanseTrajectory.py +586 -0
  209. MDANSE/Trajectory/__init__.py +15 -0
  210. MDANSE/__init__.py +39 -0
  211. mdanse-2.0.0.dist-info/METADATA +994 -0
  212. mdanse-2.0.0.dist-info/RECORD +217 -0
  213. mdanse-2.0.0.dist-info/WHEEL +5 -0
  214. mdanse-2.0.0.dist-info/entry_points.txt +2 -0
  215. mdanse-2.0.0.dist-info/licenses/AUTHORS +20 -0
  216. mdanse-2.0.0.dist-info/licenses/LICENSE +675 -0
  217. 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()