MBN-tools 0.1__tar.gz → 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.
- mbn_tools-1.0.0/LICENSE.txt +7 -0
- mbn_tools-1.0.0/MBN_tools/__init__.py +66 -0
- mbn_tools-1.0.0/MBN_tools/analysis.py +227 -0
- mbn_tools-1.0.0/MBN_tools/core.py +286 -0
- mbn_tools-1.0.0/MBN_tools/crystallography.py +163 -0
- mbn_tools-1.0.0/MBN_tools/visualise.py +175 -0
- mbn_tools-1.0.0/MBN_tools.egg-info/PKG-INFO +90 -0
- mbn_tools-1.0.0/MBN_tools.egg-info/SOURCES.txt +13 -0
- mbn_tools-1.0.0/MBN_tools.egg-info/dependency_links.txt +1 -0
- mbn_tools-1.0.0/MBN_tools.egg-info/requires.txt +4 -0
- mbn_tools-1.0.0/MBN_tools.egg-info/top_level.txt +1 -0
- mbn_tools-1.0.0/PKG-INFO +90 -0
- mbn_tools-1.0.0/README.md +56 -0
- mbn_tools-1.0.0/pyproject.toml +40 -0
- mbn_tools-1.0.0/setup.cfg +4 -0
- MBN_tools-0.1/MBN_tools/MBN_tools.py +0 -119
- MBN_tools-0.1/MBN_tools/__init__.py +0 -0
- MBN_tools-0.1/PKG-INFO +0 -16
- MBN_tools-0.1/setup.cfg +0 -2
- MBN_tools-0.1/setup.py +0 -23
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2024, MBN-tools Developers.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MBN Tools
|
|
3
|
+
|
|
4
|
+
A collection of functions for use with the MBN Explorer software (https://mbnresearch.com/get-mbn-explorer-software).
|
|
5
|
+
|
|
6
|
+
This module includes functions for:
|
|
7
|
+
- Running MBN Explorer simulations.
|
|
8
|
+
- File manipulation.
|
|
9
|
+
- Data analysis.
|
|
10
|
+
- Visualisation.
|
|
11
|
+
|
|
12
|
+
Dependencies:
|
|
13
|
+
- numpy
|
|
14
|
+
- scipy
|
|
15
|
+
- mdtraj
|
|
16
|
+
- vispy
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
import MBN_tools as MBN
|
|
20
|
+
|
|
21
|
+
# Run MBN Explorer simulation
|
|
22
|
+
stdout, stderr = MBN.run_MBN('task_file.task', '/path/to/MBN_Explorer')
|
|
23
|
+
|
|
24
|
+
# Read XYZ file
|
|
25
|
+
xyz_data = MBN.read_xyz('xyz_file.xyz')
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
from .core import (
|
|
30
|
+
create_structured_array,
|
|
31
|
+
run_task,
|
|
32
|
+
read_task,
|
|
33
|
+
write_task,
|
|
34
|
+
read_xyz,
|
|
35
|
+
write_xyz,
|
|
36
|
+
read_trajectory,
|
|
37
|
+
xyz_to_pdb,
|
|
38
|
+
xyz_to_input
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
from . import analysis
|
|
42
|
+
from . import crystallography
|
|
43
|
+
from . import visualise
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# Core functions
|
|
48
|
+
'create_structured_array',
|
|
49
|
+
'run_task',
|
|
50
|
+
'read_task',
|
|
51
|
+
'write_task',
|
|
52
|
+
'read_xyz',
|
|
53
|
+
'write_xyz',
|
|
54
|
+
'read_trajectory',
|
|
55
|
+
'xyz_to_pdb',
|
|
56
|
+
'xyz_to_input',
|
|
57
|
+
|
|
58
|
+
# Submodules
|
|
59
|
+
'analysis',
|
|
60
|
+
'crystallography',
|
|
61
|
+
'visualise',
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def __dir__():
|
|
66
|
+
return __all__
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analysis submodule of MBN_tools.
|
|
3
|
+
Provides utility functions for MBN Explorer trajectory file analysis.
|
|
4
|
+
|
|
5
|
+
Functions:
|
|
6
|
+
- calculate_rdf
|
|
7
|
+
- calculate_msd
|
|
8
|
+
- calculate_rmsd
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
import MBN_tools as MBN
|
|
12
|
+
from MBN_tools import analysis
|
|
13
|
+
|
|
14
|
+
xyz_data = MBN.read_xyz('xyz_file.xyz')
|
|
15
|
+
RMSD = analysis.calculate_rmsd(xyz_data, box_size=[50, 50, 100], directions='xyz')
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import warnings
|
|
19
|
+
import numpy as np
|
|
20
|
+
from scipy.spatial import KDTree
|
|
21
|
+
from typing import Optional, Tuple
|
|
22
|
+
|
|
23
|
+
def calculate_rdf(coordinates: np.ndarray, step: float, r_max: float, frame: int = 0, box_size: Optional[np.ndarray] = None, select_atoms: Optional[str] = None) -> Tuple[np.ndarray, np.ndarray]:
|
|
24
|
+
"""
|
|
25
|
+
Calculate the radial distribution function (RDF) from coordinates.
|
|
26
|
+
|
|
27
|
+
Parameters:
|
|
28
|
+
coordinates (numpy.ndarray): A structured array of coordinates.
|
|
29
|
+
step (float): The width of the bins for RDF calculation.
|
|
30
|
+
r_max (float): The maximum distance for RDF calculation.
|
|
31
|
+
frame (int, optional): The simulation frame to use if specified. Defaults to the first frame.
|
|
32
|
+
box_size (numpy.ndarray, optional): Dimensions of the simulation box. If None, it will be calculated from coordinates.
|
|
33
|
+
select_atoms (str, optional): Atom type to select. Default is None.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
tuple: (r_values, rdf) where r_values is an array of bin centers and rdf is the radial distribution function.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
coordinates = coordinates[frame]
|
|
40
|
+
if box_size is None:
|
|
41
|
+
# Calculate box dimensions from given coors
|
|
42
|
+
mins = np.min(coordinates['coordinates'], axis=0)
|
|
43
|
+
maxs = np.max(coordinates['coordinates'], axis=0)
|
|
44
|
+
box_size = maxs - mins
|
|
45
|
+
warnings.warn('Using calculated box size. This is unadvisable and may produce incorrect results. For best results define a box size manually.')
|
|
46
|
+
|
|
47
|
+
if select_atoms:
|
|
48
|
+
# Reduce coordinates to selection of atom types
|
|
49
|
+
coordinates = coordinates[np.isin(coordinates['atoms'], select_atoms)]
|
|
50
|
+
if len(coordinates) == 0:
|
|
51
|
+
raise ValueError('No atoms of the selected type(s).')
|
|
52
|
+
|
|
53
|
+
num_atoms = len(coordinates)
|
|
54
|
+
num_bins = int(r_max / step)
|
|
55
|
+
|
|
56
|
+
rdf = np.zeros(num_bins)
|
|
57
|
+
|
|
58
|
+
# Apply PBC for coordinates
|
|
59
|
+
pbc_coordinates = coordinates['coordinates'] % box_size
|
|
60
|
+
|
|
61
|
+
# Create KD-tree
|
|
62
|
+
tree = KDTree(pbc_coordinates)
|
|
63
|
+
|
|
64
|
+
# Average density rho for cuboidal box
|
|
65
|
+
box_volume = np.prod(box_size)
|
|
66
|
+
density = num_atoms / box_volume
|
|
67
|
+
|
|
68
|
+
# Find pairs within max_distance using KD-tree
|
|
69
|
+
pairs = tree.query_pairs(r=r_max, output_type='ndarray')
|
|
70
|
+
|
|
71
|
+
# Calculate distances and bin them
|
|
72
|
+
distances = np.linalg.norm(pbc_coordinates[pairs[:, 0]] - pbc_coordinates[pairs[:, 1]], axis=1)
|
|
73
|
+
hist, bin_edges = np.histogram(distances, bins=num_bins, range=(0, r_max))
|
|
74
|
+
|
|
75
|
+
r_values = (bin_edges[:-1] + bin_edges[1:]) / 2 # Mid points of bins
|
|
76
|
+
|
|
77
|
+
# Convert histogram to RDF
|
|
78
|
+
for i_bin in range(num_bins):
|
|
79
|
+
r = r_values[i_bin]
|
|
80
|
+
shell_volume = 4 * np.pi * r**2 * step
|
|
81
|
+
rdf[i_bin] = hist[i_bin] / (shell_volume * density * num_atoms)
|
|
82
|
+
|
|
83
|
+
return r_values, rdf
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def calculate_msd(structured_array, box_size, directions='xyz'):
|
|
87
|
+
"""
|
|
88
|
+
Calculate the Mean Squared Displacement (MSD).
|
|
89
|
+
|
|
90
|
+
Parameters:
|
|
91
|
+
structured_array (np.ndarray): A structured array of coordinates.
|
|
92
|
+
box_size (float or list): Size of the simulation box. A single float assumes a cubic box,
|
|
93
|
+
and a list of three values specifies [Lx, Ly, Lz] for non-cubic boxes.
|
|
94
|
+
directions (str): Directions to include in the MSD calculation ('x', 'y', 'z', or combinations like 'xyz', 'xy', etc.).
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
np.ndarray: MSD values as a function of time.
|
|
98
|
+
"""
|
|
99
|
+
# Extract coordinates
|
|
100
|
+
coordinates = structured_array['coordinates'] # Shape: (n_timesteps, n_atoms, 3)
|
|
101
|
+
n_timesteps, n_atoms, _ = coordinates.shape
|
|
102
|
+
|
|
103
|
+
# Handle box size
|
|
104
|
+
if isinstance(box_size, (int, float)):
|
|
105
|
+
box_size = np.array([box_size] * 3) # Convert to cubic box
|
|
106
|
+
else:
|
|
107
|
+
box_size = np.array(box_size) # Ensure array format
|
|
108
|
+
|
|
109
|
+
if box_size.shape != (3,):
|
|
110
|
+
raise ValueError("Box size must be a single value for cubic boxes or a list of three values for non-cubic boxes.")
|
|
111
|
+
|
|
112
|
+
# Calculate displacements
|
|
113
|
+
displacements = np.zeros_like(coordinates)
|
|
114
|
+
for t in range(1, n_timesteps):
|
|
115
|
+
step_displacement = coordinates[t] - coordinates[t - 1]
|
|
116
|
+
# Apply minimum image convention for PBC
|
|
117
|
+
step_displacement -= box_size * np.round(step_displacement / box_size)
|
|
118
|
+
displacements[t] = displacements[t - 1] + step_displacement
|
|
119
|
+
|
|
120
|
+
# Initial positions
|
|
121
|
+
initial_positions = displacements[0] # Shape: (n_atoms, 3)
|
|
122
|
+
|
|
123
|
+
# Total displacements
|
|
124
|
+
total_displacements = displacements - initial_positions # Shape: (n_timesteps, n_atoms, 3)
|
|
125
|
+
|
|
126
|
+
# Filter for specified directions
|
|
127
|
+
direction_indices = {'x': 0, 'y': 1, 'z': 2}
|
|
128
|
+
selected_indices = [direction_indices[dim] for dim in directions]
|
|
129
|
+
selected_displacements = total_displacements[..., selected_indices] # Select desired dimensions
|
|
130
|
+
|
|
131
|
+
# Squared displacements
|
|
132
|
+
squared_displacements = np.sum(selected_displacements**2, axis=-1) # Sum over selected dimensions
|
|
133
|
+
|
|
134
|
+
# Mean squared displacement (MSD) over all atoms
|
|
135
|
+
msd = np.mean(squared_displacements, axis=1) # Average over particles
|
|
136
|
+
|
|
137
|
+
return msd
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def calculate_rmsd(structured_array, box_size, directions='xyz'):
|
|
141
|
+
"""
|
|
142
|
+
Calculate the Root Mean Squared Displacement (RMSD),
|
|
143
|
+
handling periodic boundary conditions (PBC).
|
|
144
|
+
|
|
145
|
+
Parameters:
|
|
146
|
+
structured_array (np.ndarray): A structured array of coordinates.
|
|
147
|
+
box_size (float or list): Size of the simulation box. A single float assumes a cubic box,
|
|
148
|
+
and a list of three values specifies [Lx, Ly, Lz] for non-cubic boxes.
|
|
149
|
+
directions (str): Directions to include in the displacement calculation ('x', 'y', 'z', or combinations like 'xyz', 'xy', etc.).
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
np.ndarray: RMSD values as a function of time.
|
|
153
|
+
"""
|
|
154
|
+
# Compute MSD first
|
|
155
|
+
msd = calculate_msd(structured_array, box_size, directions=directions)
|
|
156
|
+
|
|
157
|
+
# Compute RMSD
|
|
158
|
+
rmsd = np.sqrt(msd)
|
|
159
|
+
|
|
160
|
+
return rmsd
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def melting_temperature_calculation() -> None:
|
|
164
|
+
"""
|
|
165
|
+
Placeholder function for melting temperature calculation.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
None
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def diffusion_analysis() -> None:
|
|
175
|
+
"""
|
|
176
|
+
Placeholder function for diffusion analysis.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
None
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def kinetic_energy_distribution() -> None:
|
|
186
|
+
"""
|
|
187
|
+
Placeholder function for kinetic energy distribution analysis.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
None
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def temperature_fluctuation_calculation() -> None:
|
|
197
|
+
"""
|
|
198
|
+
Placeholder function for temperature fluctuation calculation.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
None
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def heat_capacity_calculation() -> None:
|
|
208
|
+
"""
|
|
209
|
+
Placeholder function for heat capacity calculation.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
None
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def spectral_statistics_analyser() -> None:
|
|
219
|
+
"""
|
|
220
|
+
Placeholder function for spectral statistics analysis.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
None
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
pass
|
|
227
|
+
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MBN_tools package
|
|
3
|
+
|
|
4
|
+
Core tools for working with MBN Explorer IO files and simulations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import warnings
|
|
9
|
+
from typing import Union, Optional, List, Dict, Tuple
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_structured_array(atoms: List[str], frames: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray:
|
|
13
|
+
"""
|
|
14
|
+
Create a single 3D structured numpy array from a list of atom types and a nested list/array of coordinates for multiple frames.
|
|
15
|
+
|
|
16
|
+
Parameters:
|
|
17
|
+
atoms (list of str): A list of atom type labels.
|
|
18
|
+
frames (numpy array or list): A list or array of 3D coordinates for multiple frames
|
|
19
|
+
(shape: MxNx3, where M is the number of frames, and N is the number of atoms).
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
numpy.ndarray: A 3D structured array with 'atoms' and 'coordinates' fields, one for each frame.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
frames = np.array(frames)
|
|
26
|
+
|
|
27
|
+
dtype = [('atoms', 'U10'), ('coordinates', 'f8', 3)]
|
|
28
|
+
|
|
29
|
+
structured_array = np.array(
|
|
30
|
+
[[(atom, coord) for atom, coord in zip(atoms, frame)] for frame in frames],
|
|
31
|
+
dtype=dtype
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return structured_array
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_task(task_file: str, MBN_path: str, show_output: bool = False) -> Tuple[str, str]:
|
|
38
|
+
"""
|
|
39
|
+
Run an MBN Explorer simulation using a specified Task file and return the standard output and error.
|
|
40
|
+
|
|
41
|
+
Parameters:
|
|
42
|
+
task_file (str): Path to the Task file.
|
|
43
|
+
MBN_path (str): Path to the MBN Explorer executable.
|
|
44
|
+
show_output (bool, optional): If True, prints the simulation output to the screen. Default is False.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
tuple: (stdout, stderr) where stdout and stderr are strings containing the standard output and error of the simulation.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
import subprocess
|
|
51
|
+
result = subprocess.run(MBN_path + ' -t ' + task_file, capture_output=True, text=True, creationflags=0x08000000)
|
|
52
|
+
|
|
53
|
+
if show_output:
|
|
54
|
+
print('Output:')
|
|
55
|
+
if len(result.stdout) > 0:
|
|
56
|
+
print(result.stdout)
|
|
57
|
+
else:
|
|
58
|
+
print(result.stderr)
|
|
59
|
+
|
|
60
|
+
return result.stdout, result.stderr
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def read_task(task_file: str, flatten: bool = False) -> Dict[str, Dict[str, str]]:
|
|
64
|
+
"""
|
|
65
|
+
Read a Task file and split it into a dictionary of options and their parameters.
|
|
66
|
+
|
|
67
|
+
Parameters:
|
|
68
|
+
task_file (str): Path to the Task file.
|
|
69
|
+
flatten (bool, optional): If True, flattens the nested dictionary into a single dictionary. Default is False.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
dict: A dictionary of Task file options. If flatten=True, a flattened dictionary ignoring Task file sections.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
file_options = {}
|
|
76
|
+
with open(task_file) as f:
|
|
77
|
+
data = f.read().split('\n')
|
|
78
|
+
|
|
79
|
+
section_options = {}
|
|
80
|
+
for line in data:
|
|
81
|
+
try:
|
|
82
|
+
if line[0] ==';' and len(line)>1:
|
|
83
|
+
if len(section_options)>0:
|
|
84
|
+
file_options[section] = section_options
|
|
85
|
+
section_options = {}
|
|
86
|
+
section = line.replace(';','').strip()
|
|
87
|
+
if '=' in line:
|
|
88
|
+
line=line.split(' = ')
|
|
89
|
+
section_options[line[0].strip()] = str(line[1].strip())
|
|
90
|
+
except IndexError:
|
|
91
|
+
pass
|
|
92
|
+
file_options[section] = section_options
|
|
93
|
+
|
|
94
|
+
if flatten:
|
|
95
|
+
file_options_flat = {}
|
|
96
|
+
for key in file_options.keys():
|
|
97
|
+
file_options_flat.update(file_options[key])
|
|
98
|
+
return file_options_flat
|
|
99
|
+
else:
|
|
100
|
+
return file_options
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def write_task(task_file: str, file_options: Dict[str, Dict[str, str]]) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Write a dictionary of Task file options to a Task file.
|
|
106
|
+
|
|
107
|
+
Parameters:
|
|
108
|
+
task_file (str): Path to the Task file.
|
|
109
|
+
file_options (dict): A dictionary of Task file options, where the keys are section headers and the values are dictionaries of parameters.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
None: Writes the Task file.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
with open(task_file, 'w') as f:
|
|
116
|
+
f.write(';\n')
|
|
117
|
+
for key in file_options:
|
|
118
|
+
f.write('\n; '+key+'\n')
|
|
119
|
+
for sub_key in file_options[key]:
|
|
120
|
+
if sub_key == 'Random':
|
|
121
|
+
f.write('\n'+'{:<31}'.format(sub_key)+'= '+file_options[key][sub_key]+'\n')
|
|
122
|
+
else:
|
|
123
|
+
f.write('{:<31}'.format(sub_key)+'= '+file_options[key][sub_key]+'\n')
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def read_xyz(xyz_file: str) -> np.ndarray:
|
|
127
|
+
"""
|
|
128
|
+
Read an XYZ file and return its contents as a structured array.
|
|
129
|
+
|
|
130
|
+
Parameters:
|
|
131
|
+
xyz_file (str): Path to the XYZ file.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
numpy.ndarray: A structured array with fields 'atoms' and 'coordinates'.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
with open(xyz_file, 'r') as f:
|
|
138
|
+
data = [i.split() for i in f.read().split('\n')[2:-1]]
|
|
139
|
+
atoms = [i[0] for i in data]
|
|
140
|
+
coordinates = [[[float(i[1]), float(i[2]), float(i[3])] for i in data]]
|
|
141
|
+
|
|
142
|
+
xyz_data = create_structured_array(atoms, coordinates)
|
|
143
|
+
|
|
144
|
+
return xyz_data
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def write_xyz(coords: np.ndarray, xyz_file: str, frame: int = 0) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Write coordinates to an XYZ file from a structured array.
|
|
150
|
+
|
|
151
|
+
Parameters:
|
|
152
|
+
coords (numpy.ndarray): A structured array of coordinates.
|
|
153
|
+
xyz_file (str): Path to the XYZ file.
|
|
154
|
+
frame (int, optional): The simulation frame to use if multiple are specified. Defaults to the first frame.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
None: Writes the XYZ file.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
with open(xyz_file, 'w') as f:
|
|
161
|
+
f.write(str(len(coords[frame]))+'\n')
|
|
162
|
+
f.write('Type name\t\t\tPosition X\t\t\tPosition Y\t\t\tPosition Z\n')
|
|
163
|
+
for line in coords[frame]:
|
|
164
|
+
f.write(line['atoms']+'\t\t\t\t'+'\t\t'.join(['{:.8e}'.format(float(x)) for x in line['coordinates']])+'\n')
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def read_trajectory(dcd_file: str, xyz_file: str, frame: Optional[int] = None) -> np.ndarray:
|
|
168
|
+
"""
|
|
169
|
+
Read coordinates from a DCD trajectory file and match them with atom labels from an XYZ file.
|
|
170
|
+
|
|
171
|
+
Parameters:
|
|
172
|
+
dcd_file (str): Path to the DCD file.
|
|
173
|
+
xyz_file (str): Path to the XYZ file containing atom labels.
|
|
174
|
+
frame (int, optional): The specific frame to return. If None, returns all frames.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
numpy.ndarray: A structured array of coordinates for the specified frame with fields 'atoms' and 'coordinates'.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
import mdtraj.formats as md
|
|
181
|
+
|
|
182
|
+
with md.DCDTrajectoryFile(dcd_file) as f:
|
|
183
|
+
xyz, cell_lengths, cell_angles = f.read()
|
|
184
|
+
#coords = (xyz)
|
|
185
|
+
|
|
186
|
+
atoms = read_xyz(xyz_file)[0]['atoms']
|
|
187
|
+
|
|
188
|
+
coords = create_structured_array(atoms, (xyz))
|
|
189
|
+
|
|
190
|
+
if frame:
|
|
191
|
+
return coords[frame]
|
|
192
|
+
else:
|
|
193
|
+
return coords
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def xyz_to_pdb(xyz_file: str, pdb_file: str) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Update coordinates in a PDB file using new coordinates from an XYZ file.
|
|
199
|
+
|
|
200
|
+
Parameters:
|
|
201
|
+
xyz_file (str): Path to the XYZ file containing new coordinates.
|
|
202
|
+
pdb_file (str): Path to the PDB file with old coordinates.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
None: Writes a new PDB file with updated coordinates prefixed by 'new_'.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
with open(xyz_file) as xyz:
|
|
209
|
+
lines = xyz.readlines()
|
|
210
|
+
xyz_list = [line.strip().split() for line in lines[2:]]
|
|
211
|
+
|
|
212
|
+
with open(pdb_file) as pdb:
|
|
213
|
+
lines = pdb.readlines()
|
|
214
|
+
head = []
|
|
215
|
+
atoms = []
|
|
216
|
+
foot = []
|
|
217
|
+
atoms_done = False
|
|
218
|
+
for line in lines:
|
|
219
|
+
if line.startswith('ATOM'):
|
|
220
|
+
atoms.append(line.strip().split())
|
|
221
|
+
elif line.startswith('END'):
|
|
222
|
+
atoms_done = True
|
|
223
|
+
foot.append(line)
|
|
224
|
+
elif atoms_done:
|
|
225
|
+
foot.append(line)
|
|
226
|
+
else:
|
|
227
|
+
head.append(line)
|
|
228
|
+
|
|
229
|
+
with open('new_' + pdb_file, 'w') as new_pdb:
|
|
230
|
+
for line in head:
|
|
231
|
+
new_pdb.write(line)
|
|
232
|
+
for idx, atom in enumerate(atoms):
|
|
233
|
+
updated_atom = "{ATOM}{atom_num} {atom_name}{alt_loc_ind}{res_name} {chain_id}{res_seq_num}{res_code} {x_coord}{y_coord}{z_coord}{occ}{temp} {seg_id}{element}{charge}\n".format( #The values on the lines below can be edited to comply with other PDB types
|
|
234
|
+
ATOM=atom[0].ljust(6),
|
|
235
|
+
atom_num=atom[1].rjust(5),
|
|
236
|
+
atom_name=atom[2].ljust(4),
|
|
237
|
+
alt_loc_ind=' ',
|
|
238
|
+
res_name=atom[3].rjust(3),
|
|
239
|
+
chain_id=' ',
|
|
240
|
+
res_seq_num=atom[4].rjust(4),
|
|
241
|
+
res_code=' ',
|
|
242
|
+
x_coord=str('%3.3f' % (float(xyz_list[idx][1]))).rjust(8),
|
|
243
|
+
y_coord=str('%3.3f' % (float(xyz_list[idx][2]))).rjust(8),
|
|
244
|
+
z_coord=str('%3.3f' % (float(xyz_list[idx][3]))).rjust(8),
|
|
245
|
+
occ=str('%1.2f' % (float(atom[8]))).rjust(6),
|
|
246
|
+
temp=str('%1.2f' % (float(atom[9]))).rjust(6),
|
|
247
|
+
seg_id=atom[10].ljust(4),
|
|
248
|
+
element=atom[11].rjust(2),
|
|
249
|
+
charge=' '
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
new_pdb.write(updated_atom)
|
|
253
|
+
for line in foot:
|
|
254
|
+
new_pdb.write(line)
|
|
255
|
+
#TODO: Switch to new strucutred array method
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def xyz_to_input(xyz: Union[str, np.ndarray], input_file: str, charge: Optional[str] = None, fixed: Optional[Union[int, List[int]]] = None, frame: int = 0) -> None:
|
|
259
|
+
"""
|
|
260
|
+
Convert an XYZ file to an MBN Explorer input file.
|
|
261
|
+
|
|
262
|
+
Parameters:
|
|
263
|
+
xyz (str or numpy.ndarray): Path to an XYZ file or a structured array of coordinates.
|
|
264
|
+
input_file (str): Path to the output MBN Explorer input file.
|
|
265
|
+
charge (str, optional): Charge of the atoms, if specified. Default is None.
|
|
266
|
+
fixed (int or list of int, optional): Index or indices of fixed blocks. Default is None.
|
|
267
|
+
frame (int, optional): The simulation frame to use if multiple are specified. Defaults to the first frame.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
None: Writes the input file for MBN Explorer.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
if type(xyz) == str:
|
|
274
|
+
coords = read_xyz(xyz)[0]
|
|
275
|
+
elif type(xyz) == np.ndarray:
|
|
276
|
+
coords = xyz[0]
|
|
277
|
+
|
|
278
|
+
with open(input_file, 'w') as f:
|
|
279
|
+
for i, line in enumerate(coords):
|
|
280
|
+
if i==fixed:
|
|
281
|
+
f.write('<*\n')
|
|
282
|
+
f.write(line['atoms']+(':'+charge if charge else '')+'\t\t\t'+'\t\t'.join(['{:.8e}'.format(float(x)) for x in line['coordinates']])+'\n')
|
|
283
|
+
if fixed:
|
|
284
|
+
f.write('>')
|
|
285
|
+
#TODO: Add dictionary of charges
|
|
286
|
+
#TODO: Change fixed to be lists. First item is start index, second is end index. Also list of lists. Single length list corresponds to fixed block after given index to end of file
|