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.
- puchik-1.0.0/LICENSE +21 -0
- puchik-1.0.0/MANIFEST.in +7 -0
- puchik-1.0.0/PKG-INFO +97 -0
- puchik-1.0.0/PUCHIK/__init__.py +6 -0
- puchik-1.0.0/PUCHIK/_version.py +1 -0
- puchik-1.0.0/PUCHIK/cpp/interface.cpp +32 -0
- puchik-1.0.0/PUCHIK/grid_project/__init__.py +3 -0
- puchik-1.0.0/PUCHIK/grid_project/core/__init__.py +0 -0
- puchik-1.0.0/PUCHIK/grid_project/core/densities.py +465 -0
- puchik-1.0.0/PUCHIK/grid_project/core/pyutils.py +28 -0
- puchik-1.0.0/PUCHIK/grid_project/core/utils.c +15654 -0
- puchik-1.0.0/PUCHIK/grid_project/core/utils.cp310-win_amd64.pyd +0 -0
- puchik-1.0.0/PUCHIK/grid_project/core/utils.cp311-win_amd64.pyd +0 -0
- puchik-1.0.0/PUCHIK/grid_project/core/utils.pyx +92 -0
- puchik-1.0.0/PUCHIK/grid_project/settings.py +1 -0
- puchik-1.0.0/PUCHIK/grid_project/utilities/__init__.py +0 -0
- puchik-1.0.0/PUCHIK/grid_project/utilities/decorators.py +31 -0
- puchik-1.0.0/PUCHIK/grid_project/utilities/universal_functions.py +183 -0
- puchik-1.0.0/PUCHIK/grid_project/volume/__init__.py +0 -0
- puchik-1.0.0/PUCHIK/grid_project/volume/monte_carlo.py +64 -0
- puchik-1.0.0/PUCHIK/test/__init__.py +0 -0
- puchik-1.0.0/PUCHIK/test/mol_parts.py +16 -0
- puchik-1.0.0/PUCHIK/test/test.py +125 -0
- puchik-1.0.0/PUCHIK.egg-info/PKG-INFO +97 -0
- puchik-1.0.0/PUCHIK.egg-info/SOURCES.txt +32 -0
- puchik-1.0.0/PUCHIK.egg-info/dependency_links.txt +1 -0
- puchik-1.0.0/PUCHIK.egg-info/requires.txt +3 -0
- puchik-1.0.0/PUCHIK.egg-info/top_level.txt +1 -0
- puchik-1.0.0/README.md +56 -0
- puchik-1.0.0/pyproject.toml +36 -0
- puchik-1.0.0/setup.cfg +4 -0
- puchik-1.0.0/setup.py +36 -0
- 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.
|
puchik-1.0.0/MANIFEST.in
ADDED
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
|
+

|
|
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
|
+

|
|
@@ -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
|
+
}
|
|
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
|