PUCHIK 1.0.0__tar.gz

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 (33) hide show
  1. puchik-1.0.0/LICENSE +21 -0
  2. puchik-1.0.0/MANIFEST.in +7 -0
  3. puchik-1.0.0/PKG-INFO +97 -0
  4. puchik-1.0.0/PUCHIK/__init__.py +6 -0
  5. puchik-1.0.0/PUCHIK/_version.py +1 -0
  6. puchik-1.0.0/PUCHIK/cpp/interface.cpp +32 -0
  7. puchik-1.0.0/PUCHIK/grid_project/__init__.py +3 -0
  8. puchik-1.0.0/PUCHIK/grid_project/core/__init__.py +0 -0
  9. puchik-1.0.0/PUCHIK/grid_project/core/densities.py +465 -0
  10. puchik-1.0.0/PUCHIK/grid_project/core/pyutils.py +28 -0
  11. puchik-1.0.0/PUCHIK/grid_project/core/utils.c +15654 -0
  12. puchik-1.0.0/PUCHIK/grid_project/core/utils.cp310-win_amd64.pyd +0 -0
  13. puchik-1.0.0/PUCHIK/grid_project/core/utils.cp311-win_amd64.pyd +0 -0
  14. puchik-1.0.0/PUCHIK/grid_project/core/utils.pyx +92 -0
  15. puchik-1.0.0/PUCHIK/grid_project/settings.py +1 -0
  16. puchik-1.0.0/PUCHIK/grid_project/utilities/__init__.py +0 -0
  17. puchik-1.0.0/PUCHIK/grid_project/utilities/decorators.py +31 -0
  18. puchik-1.0.0/PUCHIK/grid_project/utilities/universal_functions.py +183 -0
  19. puchik-1.0.0/PUCHIK/grid_project/volume/__init__.py +0 -0
  20. puchik-1.0.0/PUCHIK/grid_project/volume/monte_carlo.py +64 -0
  21. puchik-1.0.0/PUCHIK/test/__init__.py +0 -0
  22. puchik-1.0.0/PUCHIK/test/mol_parts.py +16 -0
  23. puchik-1.0.0/PUCHIK/test/test.py +125 -0
  24. puchik-1.0.0/PUCHIK.egg-info/PKG-INFO +97 -0
  25. puchik-1.0.0/PUCHIK.egg-info/SOURCES.txt +32 -0
  26. puchik-1.0.0/PUCHIK.egg-info/dependency_links.txt +1 -0
  27. puchik-1.0.0/PUCHIK.egg-info/requires.txt +3 -0
  28. puchik-1.0.0/PUCHIK.egg-info/top_level.txt +1 -0
  29. puchik-1.0.0/README.md +56 -0
  30. puchik-1.0.0/pyproject.toml +36 -0
  31. puchik-1.0.0/setup.cfg +4 -0
  32. puchik-1.0.0/setup.py +36 -0
  33. puchik-1.0.0/tests/sort.pyx +19 -0
puchik-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Hrachya Ishkhanyan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ global-include *.pyx
2
+ global-include *.pyd
3
+ global-include *.pxd
4
+ global-include *.c
5
+ global-include *.h
6
+ global-include *.cpp
7
+ global-include *.so
puchik-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.1
2
+ Name: PUCHIK
3
+ Version: 1.0.0
4
+ Summary: Python Utility for Characterizing Heterogeneous Interfaces and Kinetics creates a grid from an molecular dynamics structure file. The system in question can have an arbitrary structure. Volume and species densities can then calculated.
5
+ Home-page: https://github.com/hrachishkhanyan/grid_project
6
+ Author: H. Ishkhanyan
7
+ Author-email: Hrachya Ishkhanyan <hrachya.ishkhanyan@kcl.ac.uk>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2024 Hrachya Ishkhanyan
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ Project-URL: Homepage, https://github.com/hrachishkhanyan/grid_project
30
+ Project-URL: Bug Tracker, https://github.com/hrachishkhanyan/grid_project/issues
31
+ Classifier: Programming Language :: Python :: 3
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Operating System :: OS Independent
34
+ Provides: PUCHIK
35
+ Requires-Python: >=3.10
36
+ Description-Content-Type: text/markdown
37
+ License-File: LICENSE
38
+ Requires-Dist: MDAnalysis
39
+ Requires-Dist: scipy>=1.14.0
40
+ Requires-Dist: PyGEL3D
41
+
42
+ # PUCHIK
43
+
44
+ Python Utility for Characterizing Heterogeneous Interfaces and Kinetics (PUCHIK), is a tool for analyzing molecular dynamics trajectories. It allows constructing an interface between two phases, enabling to calculate intrinsic density profiles, volumes, etc.
45
+
46
+ The interface construction works for spherical and rod-like nanoparticles equally well, making it a great tool to work with nanoparticles of almost every shape.
47
+
48
+ ![image](https://drive.google.com/uc?export=view&id=1YTiM2OxzkGO0GcbC5WvFffBdZN9-e_6D)
49
+
50
+ ## Installation
51
+
52
+ You can install the PUCHIK package using pip:
53
+
54
+ ```
55
+ pip install PUCHIK
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ The main class in this package is the "Mesh" class. To set up a mesh, import it from PUCHIK:
61
+
62
+ ```python
63
+ from PUCHIK import Mesh
64
+ ```
65
+
66
+ You should provide it with a topology and optionally a trajectory files. AICON uses MDAnalysis Readers to open a trajectory. You can find the supported formats [here](https://docs.mdanalysis.org/stable/documentation_pages/coordinates/init.html).
67
+
68
+ ```python
69
+ trj = '<path_to_trajectory>'
70
+ top = '<path_to_topology>'
71
+ m = Mesh(trj, top)
72
+ ```
73
+
74
+ Lastly, select the atom groups you want to consider, atom groups that comprise the interface, and run the **calculate_density** method:
75
+
76
+ ```python
77
+ m.select_atoms('all') # Consider every atom in the system
78
+ m.select_structure('<resname_of_the_nanoparticle>') # resname of the nanoparticle
79
+ # m.select_structure(['<resname_1>', '<resname_2>']) # use a list if it's a mix of residues
80
+
81
+ density_selection = 'resname TIP3'
82
+ m.calculate_density(density_selection)
83
+ ```
84
+
85
+ Note that **calculate_density** uses every CPU core. You can specify the number of cores you want to use with the keyword argument **cpu_count**.
86
+
87
+ A more customized usage of the **calculate_density** method can be:
88
+
89
+ ```python
90
+ m.calculate_density(density_selection, start=10, end=1000, skip=2, norm_bin_count=12)
91
+ ```
92
+
93
+ This version will start the calculation at the 10th frame and finish it at frame 1000 considering every 2nd frame. **norm_bin_count** specifies the number of divisions of the simulation box in each dimension to create a grid.
94
+
95
+ An example figure which shows the number density of different residues relative to the distance to the interface:
96
+
97
+ ![image](https://drive.google.com/uc?export=view&id=1swRuoD-rs01SA-4jqPLjwxuuv9UlUGkm)
@@ -0,0 +1,6 @@
1
+ __all__ = ['Mesh']
2
+
3
+ from .grid_project.core.densities import Mesh
4
+ from ._version import __version__
5
+
6
+
@@ -0,0 +1 @@
1
+ __version__ = '1.0.0'
@@ -0,0 +1,32 @@
1
+ #include <iostream>
2
+ #include <vector>
3
+ #include <fstream>
4
+ #include <string.h>
5
+ #include <string>
6
+
7
+ using namespace std;
8
+
9
+ string split(string s, char delim) {
10
+
11
+ }
12
+
13
+ int main(int argc, char const *argv[])
14
+ {
15
+ fstream file;
16
+ file.open("C:\\Users\\hrach\\PycharmProjects\\md_grid_project\\cpp\\test.txt", ios::in);
17
+
18
+
19
+ if (file.is_open()) {
20
+ string line;
21
+
22
+ while (getline(file, line)) {
23
+ char str[80];
24
+ strcpy(str, line.c_str());
25
+ strtok(str, " ");
26
+
27
+ cout << str << "\n";
28
+ }
29
+ }
30
+ file.close();
31
+ return 0;
32
+ }
@@ -0,0 +1,3 @@
1
+ # from grid_project.core.densities import Mesh
2
+ #
3
+ # __all__ = ['Mesh']
File without changes
@@ -0,0 +1,465 @@
1
+ # from sys import argv # for benchmarking only
2
+ import logging
3
+ # import warnings
4
+ from functools import partial
5
+ from multiprocessing import cpu_count
6
+
7
+ from MDAnalysis.analysis.distances import self_distance_array
8
+ from MDAnalysis.transformations.wrap import wrap
9
+ import MDAnalysis as mda
10
+ import numpy as np
11
+
12
+ from scipy.spatial import ConvexHull
13
+ # from tqdm import tqdm
14
+ from tqdm.contrib.concurrent import process_map
15
+
16
+ # Local imports
17
+ from ..utilities.decorators import logger #, timer
18
+ from ..utilities.universal_functions import extract_hull # , _is_inside
19
+ from ..volume.monte_carlo import monte_carlo_volume
20
+ from ..settings import DEBUG
21
+ from .utils import find_distance
22
+
23
+ logging.basicConfig(format='%(message)s')
24
+ np.seterr(invalid='ignore', divide='ignore')
25
+
26
+ """
27
+ Grid method for analyzing complex shaped structures
28
+ """
29
+ CPU_COUNT = cpu_count()
30
+ UNITS = ('nm', 'a')
31
+ TQDM_BAR_FORMAT = '\033[37m{percentage:3.0f}%|{bar:30}\033[37m|[Estimated time remaining: {remaining}]\033[0m'
32
+
33
+
34
+ class Mesh:
35
+ """
36
+ Class creates to create a mesh of points representing different molecule
37
+ types of the system in a grid
38
+
39
+ Attributes:
40
+ traj (str): Path to any trajectory format supported by MDAnalysis package
41
+ top (str): Path to any topology format supported by MDAnalysis package. Defaults to None
42
+ rescale (int): Rescales the system down n times. Defaults to 1
43
+ """
44
+
45
+ def __init__(self, traj, top=None, rescale=1):
46
+ self.grid_matrix = None
47
+ self.u: mda.Universe = mda.Universe(top, traj) if top else mda.Universe(traj)
48
+ self.ag = None
49
+ self.dim = None
50
+ self.mesh = None
51
+ self.rescale = rescale
52
+ self.interface_rescale = 1 # this is for calculating a rescaled interface then upscaling it
53
+ self.length = self.u.trajectory.n_frames
54
+ self.unique_resnames = None
55
+ self.main_structure = []
56
+ self.main_structure_resnames = ''
57
+
58
+ self.interface_borders = None # defined in calculate_interface method
59
+ self.current_frame = 0
60
+
61
+ def select_atoms(self, sel):
62
+ """
63
+ Method for selecting the atoms using MDAnalysis selections
64
+
65
+ Args:
66
+ sel (str): selection string
67
+
68
+ """
69
+ self.ag = self.u.select_atoms(sel)
70
+ self.unique_resnames = np.unique(self.ag.resnames)
71
+ print('Wrapping trajectory...')
72
+ transform = wrap(self.ag)
73
+ self.u.trajectory.add_transformations(transform)
74
+
75
+ def select_structure(self, *res_names,
76
+ auto=False): # TODO I think this can be determined automatically by clustering
77
+ """
78
+ Use this method to select the structure for density calculations. Enter 1 or more resnames
79
+ :param res_names: Resname(s) of the main structure
80
+ :param auto: Determine automatically if True
81
+ :return: None
82
+ """
83
+ self.main_structure_resnames = f'resname {" ".join(res_names)} and not type H'
84
+ self.main_structure = np.where(np.in1d(self.unique_resnames, res_names))[0]
85
+
86
+ def _get_int_dim(self):
87
+ """
88
+ Utility function to get box dimensions
89
+
90
+ Returns:
91
+ Dimensions of the box as an int
92
+ """
93
+ self.u.trajectory[self.current_frame]
94
+ return int(np.ceil(self.u.dimensions[0]))
95
+
96
+ @logger(DEBUG)
97
+ def calculate_volume(self, number=100_000, units='nm', method='mc', rescale=None):
98
+ """
99
+ Returns the volume of the selected structure
100
+
101
+ Args:
102
+ number (int): Number of points to generate for volume estimation
103
+ units (str): Measure unit of the returned value
104
+ method (str): Method of calculation. 'mc' for Monte Carlo estimation,
105
+ 'riemann' for Riemann sum method
106
+ rescale (int): Rescale factor
107
+
108
+ Returns:
109
+ float: Volume of the structure
110
+ """
111
+
112
+ if units not in UNITS:
113
+ raise ValueError('units should be either \'nm\' or \'a\'')
114
+
115
+ rescale = rescale if rescale is not None else self.find_min_dist()
116
+
117
+ # vol = self._monte_carlo_volume(number, rescale) if method == 'mc' else None
118
+ vol = monte_carlo_volume(self._get_int_dim(), self.grid_matrix, number, rescale) if method == 'mc' else None
119
+
120
+ # scale back up and convert from Angstrom to nm if units == 'nm'
121
+ # return vol * self.find_min_dist() ** 3 / 1000 if units == 'nm' else vol * self.find_min_dist() ** 3
122
+ return vol / 1000
123
+
124
+ # return vol * rescale ** 3 / 1000 if units == 'nm' else vol * rescale ** 3
125
+
126
+ @staticmethod
127
+ def make_grid(pbc_dim: int, dim=1, d4=None) -> np.ndarray:
128
+ """
129
+ Returns a 4D matrix
130
+
131
+ Args:
132
+ pbc_dim (int): Dimensions of the box
133
+ dim (int): Dimensions of the box
134
+ d4 (int): Returns an 4-D matrix if d4 is given. 4th dimension contains d4 elements
135
+ """
136
+
137
+ x = y = z = pbc_dim // dim + 1
138
+ grid_matrix = np.zeros((x, y, z)) if d4 is None else np.zeros((x, y, z, d4))
139
+
140
+ return grid_matrix
141
+
142
+ @staticmethod
143
+ def check_cube(x: float, y: float, z: float) -> tuple:
144
+ """
145
+ Find to which cube does the atom belong to
146
+ Args:
147
+ x (float): x coordinate
148
+ y (float): y coordinate
149
+ z (float): z coordinate
150
+ rescale (int): rescale factor
151
+
152
+ Returns:
153
+ tuple: Coordinates of the node inside the grid where the point belongs
154
+ """
155
+
156
+ # n_x = round(x / rescale_coef)
157
+ # n_y = round(y / rescale_coef)
158
+ # n_z = round(z / rescale_coef)
159
+ n_x = int(x)
160
+ n_y = int(y)
161
+ n_z = int(z)
162
+
163
+ return n_x, n_y, n_z
164
+
165
+ @staticmethod
166
+ def make_coordinates(mesh, keep_numbers=False):
167
+ """
168
+ Converts the mesh to coordinates
169
+ Args:
170
+ mesh (np.ndarray): Mesh to convert into 3D coordinates
171
+ keep_numbers (bool): Resulting tuples will also contain the number of particles at that coordinate if True
172
+
173
+ Returns:
174
+ np.ndarray: Ndarray of tuples representing coordinates of each of the points in the mesh
175
+ """
176
+
177
+ coords = []
178
+ for i, mat in enumerate(mesh):
179
+ for j, col in enumerate(mat):
180
+ for k, elem in enumerate(col):
181
+ if elem > 0:
182
+ coords.append((i, j, k)) if not keep_numbers else coords.append((i, j, k, mesh[i, j, k]))
183
+
184
+ return np.array(coords, dtype=int)
185
+
186
+ def find_min_dist(self):
187
+ """
188
+ Estimate rescale factor.
189
+ Get rid of this.
190
+ Returns:
191
+
192
+ """
193
+ return int(np.ceil(self_distance_array(self.ag.positions).min()))
194
+
195
+ def _calc_density(self, mol_type, grid_dim, min_distance_coeff):
196
+ """ Not sure what's this for. May delete it later """
197
+ density_matrix = self.make_grid(grid_dim, dim=min_distance_coeff, d4=False)
198
+ for atom in self.ag:
199
+ x, y, z = self.check_cube(*atom.position, rescale=min_distance_coeff)
200
+ if atom.type == mol_type:
201
+ density_matrix[x, y, z] += 1
202
+
203
+ return density_matrix
204
+
205
+ def _calc_mesh(self, grid_dim, selection, diff=False):
206
+ """
207
+ Calculates the mesh according the atom positions in the box
208
+
209
+ Args:
210
+ grid_dim (int): Box dimensions
211
+ rescale: rescale factor
212
+ diff: Is True if we are calculating a mesh for other than the main structure
213
+
214
+ Returns:
215
+ np.ndarray: The grid
216
+ """
217
+ self.u.trajectory[self.current_frame]
218
+ atom_group = self.u.select_atoms(selection)
219
+
220
+ grid_matrix = self.make_grid(grid_dim, d4=len(self.unique_resnames))
221
+ for atom in atom_group:
222
+ x, y, z = self.check_cube(*atom.position)
223
+ res_number = 0 if not diff else np.where(self.unique_resnames == atom.resname)
224
+ grid_matrix[x, y, z, res_number] += 1
225
+
226
+ return grid_matrix
227
+
228
+ # @logger(DEBUG)
229
+ def calculate_mesh(self, selection=None, main_structure=False):
230
+ """
231
+ Calculates the mesh using _calc_mesh private method
232
+ Args:
233
+ selection: Selection for atom group to calculate mesh
234
+ rescale: rescale factor
235
+ main_structure (bool): use as the main structure if true (e.g. densities are calculated relative to this)
236
+ Returns:
237
+ np.ndarray: Returns the grid matrix
238
+ """
239
+ # find closest atoms and rescale positions according to this
240
+ # get one dimension
241
+
242
+ # print(atom_group.universe.trajectory.frame)
243
+ # self.u.trajectory[atom_group.universe.trajectory.frame]
244
+ # define the matrices
245
+
246
+ grid_matrix = self._calc_mesh(self._get_int_dim(), selection, main_structure) # !TODO _get_int_dim փոխի
247
+
248
+ if main_structure: # if selection is None, then it's the main structure
249
+ self.grid_matrix = grid_matrix
250
+
251
+ return grid_matrix
252
+
253
+ # @logger(DEBUG)
254
+ def calculate_interface(self, ratio=0.4, inverse=False):
255
+ """
256
+ Extract the interface from the grid TODO better way needed
257
+ Args:
258
+ inverse (bool): Return everything except for the structure if True
259
+ :param ratio: ratio of moltype/water at a certain point
260
+ Returns:
261
+ np.ndarray: interface matrix
262
+
263
+ """
264
+
265
+ interface = self.grid_matrix.copy()
266
+
267
+ if inverse:
268
+ interface[self.grid_matrix[:, :, :, 1] / self.grid_matrix[:, :, :, 0] >= ratio] = 0
269
+ return interface[:, :, :, 0]
270
+
271
+ # The sum(axis=3) is for taking into account the whole structure, which could be constructed of different
272
+ # types of molecules
273
+ interface[(0 < self.grid_matrix[:, :, :, self.main_structure].sum(axis=3) / self.grid_matrix[:, :, :, 0]) & (
274
+ self.grid_matrix[:, :, :, self.main_structure].sum(axis=3) / self.grid_matrix[:, :, :, 0] < ratio)] = 0
275
+
276
+ interface = interface[:, :, :, self.main_structure].sum(axis=3)
277
+ # extracted, self.interface_borders = extract_interface(interface, self.interface_rescale)
278
+ interface_hull = extract_hull(interface, 14)
279
+ transposed = extract_hull(interface.T).T # This is done for filling gaps in the other side
280
+ interface_hull += transposed
281
+ return interface_hull
282
+
283
+ def _calculate_density_grid(self, coords, bin_count):
284
+ # Works on a cubic box. !TODO Generalize later
285
+ self.u.trajectory[self.current_frame] # Set the frame to the current frame. Must be a better way...
286
+
287
+ coords = np.array(coords)
288
+ density_grid = np.zeros((bin_count, bin_count, bin_count))
289
+
290
+ edges, step = np.linspace(0, self._get_int_dim(), bin_count + 1, retstep=True)
291
+ grid_cell_volume = step ** 3
292
+
293
+ # y_edges = np.linspace(0, self._get_int_dim(), bin_count + 1)
294
+ # z_edges = np.linspace(0, self._get_int_dim(), bin_count + 1)
295
+
296
+ for x, y, z in coords:
297
+ x_idx = np.digitize(x, edges) - 1
298
+ y_idx = np.digitize(y, edges) - 1
299
+ z_idx = np.digitize(z, edges) - 1
300
+
301
+ density_grid[x_idx, y_idx, z_idx] += 1
302
+
303
+ density_grid /= grid_cell_volume
304
+
305
+ return density_grid
306
+
307
+ def _grid_centers(self, hull, bin_count):
308
+ self.u.trajectory[self.current_frame]
309
+
310
+ edges, step = np.linspace(0, self._get_int_dim(), bin_count + 1, retstep=True)
311
+ x_centers = (edges[:-1] + edges[1:]) / 2
312
+ y_centers = (edges[:-1] + edges[1:]) / 2
313
+ z_centers = (edges[:-1] + edges[1:]) / 2
314
+ x_grid, y_grid, z_grid = np.meshgrid(x_centers, y_centers, z_centers, indexing='ij')
315
+
316
+ return np.vstack([x_grid.ravel(), y_grid.ravel(), z_grid.ravel()]).T
317
+
318
+ def _normalize_density(self, coords, bin_count):
319
+ density_grid = self._calculate_density_grid(coords, bin_count)
320
+ density_grid = density_grid.flatten()
321
+
322
+ return density_grid
323
+
324
+ def _extract_from_mesh(self, mol_type):
325
+ if mol_type not in self.unique_resnames:
326
+ raise ValueError(
327
+ f'Molecule type "{mol_type}" is not present in the system. Available types: {self.unique_resnames}'
328
+ )
329
+
330
+ mol_index = np.where(self.unique_resnames == mol_type)
331
+
332
+ return self.grid_matrix[:, :, :, mol_index]
333
+
334
+ def _calc_dens_mp(self, frame_num, selection, norm_bin_count):
335
+ """
336
+ Calculates the density of selection from interface. Multiprocessing version
337
+
338
+ Args:
339
+ frame_num (int): Number of the frame
340
+ selection (str): Selection of the atom group density of which is to be calculated
341
+ ratio (float): Ratio moltype/water !TODO for testing. Remove later
342
+ Returns:
343
+ tuple: Density array and corresponding distances
344
+ """
345
+ self.current_frame = frame_num
346
+
347
+ mesh_coords = []
348
+
349
+ mesh = self.calculate_mesh(selection=self.main_structure_resnames, main_structure=True)[:, :, :,
350
+ self.main_structure]
351
+
352
+ for index, struct in enumerate(self.main_structure):
353
+ mesh_coords.extend(self.make_coordinates(mesh[:, :, :, index]))
354
+ mesh_coordinates = np.array(mesh_coords)
355
+
356
+ selection_coords = self.u.select_atoms(selection).positions # self.make_coordinates(selection_mesh)
357
+
358
+ try:
359
+ hull = ConvexHull(mesh_coordinates) # , qhull_options='Q0')
360
+ except IndexError as _:
361
+ logging.warning(
362
+ f'Cannot construct the hull at frame {self.current_frame}: one of your selections might be empty')
363
+ return
364
+
365
+ grid_centers = self._grid_centers(hull, bin_count=norm_bin_count)
366
+
367
+ distances = np.array(
368
+ find_distance(hull, grid_centers)) # Calculate distances from the interface to each grid cell
369
+ densities = self._normalize_density(selection_coords,
370
+ bin_count=norm_bin_count) # Calculate the density of each cell
371
+
372
+ indices = np.argsort(distances)
373
+ distances = distances[indices]
374
+ densities = densities[indices]
375
+
376
+ return distances, densities
377
+
378
+ # @timer
379
+ def calculate_density(self, selection=None, start=0, skip=1, end=None,
380
+ norm_bin_count=20, cpu_count=CPU_COUNT):
381
+ """
382
+ Calculates density of selection from the interface
383
+ :param end: Final frame
384
+ :param norm_bin_count: Bin count for normalization
385
+ :param cpu_count: Number of cores to use
386
+ :param selection: MDAnalysis selection of ag
387
+ :param interface_selection: Selection of what is considered as interface
388
+ :param start: Starting frame
389
+ :param skip: Skip every n-th frame
390
+ :return:
391
+ """
392
+ n_frames = self.u.trajectory.n_frames if end is None else end
393
+
394
+ dens_per_frame = partial(self._calc_dens_mp,
395
+ selection=selection,
396
+ norm_bin_count=norm_bin_count) # _calc_dens_mp function with filled selection using partial
397
+ frame_range = range(start, n_frames, skip)
398
+
399
+ # with Pool(cpu_count) as worker_pool:
400
+ # res = worker_pool.map(dens_per_frame, frame_range)
401
+ print(f'Running density calculation for the following atom group: {selection}')
402
+ res = process_map(dens_per_frame, frame_range,
403
+ max_workers=cpu_count,
404
+ # ascii=" >=",
405
+ # colour="YELLOW",
406
+ bar_format=TQDM_BAR_FORMAT
407
+ )
408
+
409
+ res = np.array(res)
410
+
411
+ distances = res[:, 0]
412
+ densities = res[:, 1]
413
+
414
+ distances, densities = self._process_result(res)
415
+
416
+ # Simply taking the mean might not be the best option
417
+ # distances = distances.mean(axis=0)
418
+ # densities = densities.mean(axis=0)
419
+
420
+ return distances, densities
421
+
422
+ def interface(self, data=None):
423
+ mesh = self.calculate_interface() if data is None else data
424
+ res = mesh.copy()
425
+
426
+ for i, plane in enumerate(res):
427
+ for j, row in enumerate(plane):
428
+ for k, point in enumerate(row):
429
+ if point > 0:
430
+ if (mesh[i, j - 1, k] != 0 and mesh[i, j + 1, k] != 0
431
+ and mesh[i, j, k - 1] != 0 and mesh[i, j, k + 1] != 0
432
+ and mesh[i - 1, j, k] != 0 and mesh[i + 1, j, k] != 0):
433
+ res[i, j, k] = 0
434
+ return res
435
+
436
+ @staticmethod
437
+ def _process_result(res):
438
+ """ Helper method to correctly calculate the average of the result """
439
+ distances = res[:, 0]
440
+ densities = res[:, 1]
441
+ dim_1, dim_2 = distances.shape
442
+ offset = 50 # an offset to shift the distances to correct positions
443
+
444
+ offset_distances = np.zeros((dim_1, dim_2 + offset))
445
+ offset_densities = np.zeros((dim_1, dim_2 + offset))
446
+ for i, arr in enumerate(distances):
447
+ minim = abs(int(arr[0]))
448
+
449
+ offset_distances[i, offset - minim:dim_2 + (offset - minim)] = arr
450
+ offset_densities[i, offset - minim:dim_2 + (offset - minim)] = densities[i]
451
+
452
+ # Trim zeros
453
+ global_min = abs(int(distances.min()))
454
+
455
+ final_distances = offset_distances[:, offset - global_min:dim_2 + (offset - global_min)]
456
+ final_densities = offset_densities[:, offset - global_min:dim_2 + (offset - global_min)]
457
+
458
+ final_distances = final_distances.mean(axis=0, where=final_distances != 0)
459
+ final_densities = final_densities.mean(axis=0, where=final_distances != 0)
460
+
461
+ return final_distances, final_densities
462
+
463
+
464
+ if __name__ == '__main__':
465
+ pass
@@ -0,0 +1,28 @@
1
+ from pygel3d import hmesh
2
+
3
+
4
+ def find_distance_2(hull, points):
5
+ # Construct PyGEL Manifold from the convex hull
6
+ m = hmesh.Manifold()
7
+ for s in hull.simplices:
8
+ m.add_face(hull.points[s])
9
+
10
+ dist = hmesh.MeshDistance(m)
11
+ res = []
12
+ for p in points:
13
+ # Get the distance to the point
14
+ # But don't trust its sign, because of possible
15
+ # wrong orientation of mesh face
16
+ d = dist.signed_distance(p)
17
+
18
+ # Correct the sign with ray inside test
19
+ if dist.ray_inside_test(p):
20
+ if d > 0:
21
+ d *= -1
22
+ else:
23
+ if d < 0:
24
+ d *= -1
25
+
26
+ res.append((d, p))
27
+
28
+ return res