MBN-tools 1.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.
- MBN_tools/__init__.py +66 -0
- MBN_tools/analysis.py +227 -0
- MBN_tools/core.py +286 -0
- MBN_tools/crystallography.py +163 -0
- MBN_tools/visualise.py +175 -0
- mbn_tools-1.0.0.dist-info/METADATA +90 -0
- mbn_tools-1.0.0.dist-info/RECORD +10 -0
- mbn_tools-1.0.0.dist-info/WHEEL +5 -0
- mbn_tools-1.0.0.dist-info/licenses/LICENSE.txt +7 -0
- mbn_tools-1.0.0.dist-info/top_level.txt +1 -0
MBN_tools/__init__.py
ADDED
|
@@ -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__
|
MBN_tools/analysis.py
ADDED
|
@@ -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
|
+
|
MBN_tools/core.py
ADDED
|
@@ -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
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MBN_tools.crystallography
|
|
3
|
+
|
|
4
|
+
A collection of crystallography-related utility functions for analysing
|
|
5
|
+
atomic structures produced by MBN Explorer simulations.
|
|
6
|
+
|
|
7
|
+
This module provides tools for:
|
|
8
|
+
- Grouping atoms relative to crystalline planes based on Miller indices.
|
|
9
|
+
- Translating atomic coordinates onto specified planes.
|
|
10
|
+
- Removing atoms within an exclusion region at the boundaries of a simulation box.
|
|
11
|
+
|
|
12
|
+
Dependencies:
|
|
13
|
+
- numpy
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
import MBN_tools as MBN
|
|
17
|
+
from MBN_tools import crystallography
|
|
18
|
+
|
|
19
|
+
# Read coordinates from .xyz file
|
|
20
|
+
xyz_data = MBN.read_xyz('xyz_file.xyz')
|
|
21
|
+
atom_coords = xyz_data['coordinates']
|
|
22
|
+
|
|
23
|
+
# Remove all atoms within a certain distance from simulation box edge
|
|
24
|
+
filtered_atoms, removed_atoms = crystallography.remove_atoms_exclusion_region(atom_coords, box_size=30.0, shell_size=5.0)
|
|
25
|
+
|
|
26
|
+
# Finds centre coordinate of all 110 crystalline planes
|
|
27
|
+
planes = crystallography.group_atoms_by_plane(filtered_atoms, (1, 1, 0), tolerance=0.5)
|
|
28
|
+
|
|
29
|
+
# Translate the cebtre point of planes to a the 001 normal
|
|
30
|
+
for atoms, centre in planes:
|
|
31
|
+
coords.append(centre)
|
|
32
|
+
|
|
33
|
+
translated_coords = crystallography.translate_to_plane(coords, np.array([0, 0, 1]))
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
from typing import List, Tuple
|
|
38
|
+
|
|
39
|
+
def group_atoms_by_plane(structured_array: np.ndarray,
|
|
40
|
+
hkl: Tuple[int, int, int],
|
|
41
|
+
tolerance: float = 0.6) -> List[Tuple[np.ndarray, np.ndarray]]:
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
Group atoms by their position relative to a particular crystalline plane and calculate the centre of each plane,
|
|
45
|
+
using a tolerance-based approach to more accurately group atoms to planes.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
structured_array (numpy.ndarray): The structured array containing atom types and coordinates.
|
|
49
|
+
hkl (tuple): The Miller indices (h, k, l) defining the crystalline plane.
|
|
50
|
+
tolerance (float): Tolerance value for grouping atoms to planes (in the same units as lattice constant and coordinates).
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
list: A list of tuples where each tuple contains an array of atoms on that plane and their centre coordinates.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# Unpack the Miller indices
|
|
57
|
+
h, k, l = hkl
|
|
58
|
+
|
|
59
|
+
# Define the plane normal
|
|
60
|
+
plane_normal = np.array([h, k, l])
|
|
61
|
+
|
|
62
|
+
# Normalize the plane normal
|
|
63
|
+
norm_plane_normal = np.linalg.norm(plane_normal)
|
|
64
|
+
plane_normal = plane_normal / norm_plane_normal
|
|
65
|
+
|
|
66
|
+
# Project the atomic coordinates onto the plane normal to find their positions along this direction
|
|
67
|
+
projected_positions = np.dot(structured_array['coordinates'], plane_normal)
|
|
68
|
+
|
|
69
|
+
# Initialize a list to store groups of atoms for each plane
|
|
70
|
+
planes = []
|
|
71
|
+
|
|
72
|
+
# Sort the projected positions to process them in order
|
|
73
|
+
sorted_indices = np.argsort(projected_positions)
|
|
74
|
+
sorted_positions = projected_positions[sorted_indices]
|
|
75
|
+
sorted_atoms = structured_array[sorted_indices]
|
|
76
|
+
|
|
77
|
+
# Initialize the first plane
|
|
78
|
+
current_plane_d = sorted_positions[0]
|
|
79
|
+
current_plane_atoms = []
|
|
80
|
+
|
|
81
|
+
# Group atoms into planes based on their position
|
|
82
|
+
for pos, atom in zip(sorted_positions, sorted_atoms):
|
|
83
|
+
# If the atom is within the tolerance of the current plane, add it to the plane
|
|
84
|
+
if abs(pos - current_plane_d) <= tolerance:
|
|
85
|
+
current_plane_atoms.append(atom)
|
|
86
|
+
else:
|
|
87
|
+
# Finalize the current plane and calculate its centre
|
|
88
|
+
if current_plane_atoms:
|
|
89
|
+
plane_atoms_array = np.array(current_plane_atoms, dtype=structured_array.dtype)
|
|
90
|
+
centre_coordinates = np.nanmean(plane_atoms_array['coordinates'], axis=0)
|
|
91
|
+
planes.append((plane_atoms_array, centre_coordinates))
|
|
92
|
+
|
|
93
|
+
# Start a new plane
|
|
94
|
+
current_plane_d = pos
|
|
95
|
+
current_plane_atoms = [atom]
|
|
96
|
+
|
|
97
|
+
# Finalize the last plane
|
|
98
|
+
if current_plane_atoms:
|
|
99
|
+
plane_atoms_array = np.array(current_plane_atoms, dtype=structured_array.dtype)
|
|
100
|
+
centre_coordinates = np.nanmean(plane_atoms_array['coordinates'], axis=0)
|
|
101
|
+
planes.append((plane_atoms_array, centre_coordinates))
|
|
102
|
+
|
|
103
|
+
return planes
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def translate_to_plane(coordinates: np.ndarray,
|
|
107
|
+
normal_vector: np.ndarray,
|
|
108
|
+
d: float = 0) -> np.ndarray:
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
Translate coordinates so that they lie on the specified plane.
|
|
112
|
+
|
|
113
|
+
Parameters:
|
|
114
|
+
- coordinates (numpy.ndarray): The atomic coordinates array, shape (N, 3).
|
|
115
|
+
- normal_vector (numpy.ndarray): The normal vector of the plane.
|
|
116
|
+
- d (float): The constant d in the plane equation ax + by + cz + d = 0. Default is 0.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
- translated_coordinates (numpy.ndarray): Coordinates translated to lie on the plane.
|
|
120
|
+
"""
|
|
121
|
+
# Normalize the normal vector
|
|
122
|
+
normal_vector = normal_vector / np.linalg.norm(normal_vector)
|
|
123
|
+
|
|
124
|
+
# Calculate the distance from each coordinate to the plane
|
|
125
|
+
distances = np.dot(coordinates, normal_vector) + d
|
|
126
|
+
|
|
127
|
+
# Translate each coordinate by the distance along the normal vector
|
|
128
|
+
translated_coordinates = coordinates - np.outer(distances, normal_vector)
|
|
129
|
+
|
|
130
|
+
return translated_coordinates
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def remove_atoms_exclusion_region(structured_array: np.ndarray,
|
|
134
|
+
box_size: float,
|
|
135
|
+
shell_size: float) -> Tuple[np.ndarray, np.ndarray]:
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
Remove atoms that are within a specified boundary distance from the edges of a simulation box.
|
|
139
|
+
|
|
140
|
+
Parameters:
|
|
141
|
+
structured_array (numpy structured array): A structured array containing atom data, including coordinates.
|
|
142
|
+
box_size (float): The size of the simulation box (assumed cubic).
|
|
143
|
+
shell_size (float): The distance from the edge of the box within which atoms should be removed.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
numpy structured array: A filtered structured array with atoms near the edges removed.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
# Calculate the center of the crystal (assuming cubic box)
|
|
150
|
+
centre = box_size / 2.0
|
|
151
|
+
|
|
152
|
+
# Get the coordinates of all atoms
|
|
153
|
+
coordinates = structured_array['coordinates']
|
|
154
|
+
|
|
155
|
+
condition_remove = np.where(np.any(np.abs(coordinates)>centre - shell_size, axis=1))[0]
|
|
156
|
+
|
|
157
|
+
# Atoms outside the boundary
|
|
158
|
+
removed_atoms = structured_array[condition_remove]
|
|
159
|
+
|
|
160
|
+
# Atoms inside the boundary
|
|
161
|
+
filtered_atoms = structured_array[~np.any(np.abs(coordinates)>centre - shell_size, axis=1)]
|
|
162
|
+
|
|
163
|
+
return filtered_atoms, removed_atoms
|
MBN_tools/visualise.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MBN_tools.visualise
|
|
3
|
+
|
|
4
|
+
A collection of 3D visualisation utilities for MBN Explorer simulations using VisPy.
|
|
5
|
+
|
|
6
|
+
This submodule provides tools to:
|
|
7
|
+
- Draw transparent simulation boxes with wireframe edges.
|
|
8
|
+
- Render arbitrary 3D planes with configurable orientation and border.
|
|
9
|
+
- Plot atom positions as markers with customisable appearance.
|
|
10
|
+
|
|
11
|
+
Dependencies:
|
|
12
|
+
- numpy
|
|
13
|
+
- vispy
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
import MBN_tools as MBN
|
|
17
|
+
from MBN_tools import visualise
|
|
18
|
+
import vispy.scene
|
|
19
|
+
|
|
20
|
+
# Create a canvas and a 3D view
|
|
21
|
+
canvas = vispy.scene.SceneCanvas(keys='interactive', show=True, bgcolor='white')
|
|
22
|
+
view = canvas.central_widget.add_view()
|
|
23
|
+
|
|
24
|
+
# Read coordinates from .xyz file
|
|
25
|
+
xyz_data = MBN.read_xyz('xyz_file.xyz')
|
|
26
|
+
atom_coords = xyz_data['coordinates']
|
|
27
|
+
|
|
28
|
+
# Draw a simulation box, a plane, and some atoms
|
|
29
|
+
visualisation.draw_simulation_box(30, 30, 30, parent=view.scene)
|
|
30
|
+
visualisation.draw_plane(centre=(0,0,0), size_x=10, size_y=10, normal=(0,0,1), parent=view.scene)
|
|
31
|
+
visualisation.draw_atoms(atom_coords, face_color=(1,0,0,1), symbol='o', size=12, parent=view.scene)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import numpy as np
|
|
35
|
+
import vispy.scene
|
|
36
|
+
from vispy.scene import visuals
|
|
37
|
+
from typing import Tuple, Optional, Union, Any
|
|
38
|
+
|
|
39
|
+
ColorType = Union[Tuple[float, float, float, float], str]
|
|
40
|
+
|
|
41
|
+
def draw_simulation_box(size_x: float, size_y: float, size_z: float,
|
|
42
|
+
edge_color: ColorType = (0, 0, 0, 1),
|
|
43
|
+
edge_width: int = 2,
|
|
44
|
+
parent: Optional[Any] = None) -> vispy.scene.visuals.Line:
|
|
45
|
+
"""
|
|
46
|
+
Draw a transparent 3D box (rectangular cuboid) with visible wireframe edges.
|
|
47
|
+
|
|
48
|
+
Parameters:
|
|
49
|
+
- size_x (float): Size of the box in the x-direction.
|
|
50
|
+
- size_y (float): Size of the box in the y-direction.
|
|
51
|
+
- size_z (float): Size of the box in the z-direction.
|
|
52
|
+
- edge_color (tuple): RGBA color of the box edges. Default is black.
|
|
53
|
+
- edge_width (int): Width of the box edges. Default is 2.
|
|
54
|
+
- parent (Node, optional): Parent VisPy node to which this box will be attached.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
- vispy.scene.visuals.Line: The wireframe visual representing the box.
|
|
58
|
+
"""
|
|
59
|
+
# Define the 8 vertices of the box
|
|
60
|
+
vertices = np.array([
|
|
61
|
+
[-size_x / 2, -size_y / 2, -size_z / 2],
|
|
62
|
+
[ size_x / 2, -size_y / 2, -size_z / 2],
|
|
63
|
+
[ size_x / 2, size_y / 2, -size_z / 2],
|
|
64
|
+
[-size_x / 2, size_y / 2, -size_z / 2],
|
|
65
|
+
[-size_x / 2, -size_y / 2, size_z / 2],
|
|
66
|
+
[ size_x / 2, -size_y / 2, size_z / 2],
|
|
67
|
+
[ size_x / 2, size_y / 2, size_z / 2],
|
|
68
|
+
[-size_x / 2, size_y / 2, size_z / 2],
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
# Define the 12 edges of the box
|
|
72
|
+
edges = np.array([
|
|
73
|
+
[0, 1], [1, 2], [2, 3], [3, 0], # bottom face
|
|
74
|
+
[4, 5], [5, 6], [6, 7], [7, 4], # top face
|
|
75
|
+
[0, 4], [1, 5], [2, 6], [3, 7] # vertical edges
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
# Create the box edges (wireframe only)
|
|
79
|
+
box_edges = vispy.scene.visuals.Line(pos=vertices, connect=edges, color=edge_color,
|
|
80
|
+
parent=parent, width=edge_width)
|
|
81
|
+
|
|
82
|
+
# Set the box to be transparent by not adding a face visual
|
|
83
|
+
# If you were using a filled mesh, you would set `face_color` to be transparent
|
|
84
|
+
|
|
85
|
+
return box_edges
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def draw_plane(centre: Tuple[float, float, float],
|
|
89
|
+
size_x: float, size_y: float,
|
|
90
|
+
normal: Tuple[float, float, float],
|
|
91
|
+
color: ColorType = (1, 1, 1, 1),
|
|
92
|
+
edge_color: ColorType = (0, 0, 0, 1),
|
|
93
|
+
parent: Optional[Any] = None) -> Tuple[vispy.scene.visuals.Mesh, vispy.scene.visuals.Line]:
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
Draw a plane in 3D space with adjustable size in each direction, a specified normal vector, and a border.
|
|
97
|
+
|
|
98
|
+
Parameters:
|
|
99
|
+
- centre: A tuple (x, y, z) specifying the centre of the plane.
|
|
100
|
+
- size_x: The size of the plane along the first basis vector direction.
|
|
101
|
+
- size_y: The size of the plane along the second basis vector direction.
|
|
102
|
+
- normal: A 3-element tuple specifying the normal vector of the plane.
|
|
103
|
+
- color: The color of the plane with RGBA format (default is white).
|
|
104
|
+
- edge_color: The color of the edges with RGBA format (default is black).
|
|
105
|
+
- parent: The parent node to which this plane will be added (default is None).
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
- A tuple containing the Mesh visual of the plane and the Line visual of the border.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
# Normalize the normal vector
|
|
112
|
+
normal = np.array(normal) / np.linalg.norm(normal)
|
|
113
|
+
|
|
114
|
+
# Create an arbitrary vector that is not parallel to the normal
|
|
115
|
+
if np.allclose(normal, [0, 0, 1]):
|
|
116
|
+
basis1 = np.array([1, 0, 0])
|
|
117
|
+
else:
|
|
118
|
+
basis1 = np.cross(normal, [0, 0, 1])
|
|
119
|
+
basis1 /= np.linalg.norm(basis1) # Normalize basis1
|
|
120
|
+
|
|
121
|
+
# Create the second basis vector
|
|
122
|
+
basis2 = np.cross(normal, basis1)
|
|
123
|
+
|
|
124
|
+
# Create the vertices of the plane using the local basis vectors
|
|
125
|
+
half_size_x = size_x / 2
|
|
126
|
+
half_size_y = size_y / 2
|
|
127
|
+
vertices = np.array([
|
|
128
|
+
-half_size_x * basis1 - half_size_y * basis2,
|
|
129
|
+
half_size_x * basis1 - half_size_y * basis2,
|
|
130
|
+
half_size_x * basis1 + half_size_y * basis2,
|
|
131
|
+
-half_size_x * basis1 + half_size_y * basis2,
|
|
132
|
+
]) + np.array(centre)
|
|
133
|
+
|
|
134
|
+
# Define the indices that connect the vertices to form two triangles
|
|
135
|
+
indices = np.array([0, 1, 2, 0, 2, 3])
|
|
136
|
+
|
|
137
|
+
# Create the plane as a Mesh visual
|
|
138
|
+
plane = vispy.scene.visuals.Mesh(vertices=vertices, faces=indices.reshape((-1, 3)),
|
|
139
|
+
color=color, parent=parent)
|
|
140
|
+
|
|
141
|
+
# Create the edges of the plane as a Line visual
|
|
142
|
+
edges = vispy.scene.visuals.Line(pos=np.append(vertices, [vertices[0]], axis=0), # Close the loop
|
|
143
|
+
color=edge_color, width=2, parent=parent, connect='strip')
|
|
144
|
+
|
|
145
|
+
return plane, edges
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def draw_atoms(atom_coordinates: np.ndarray,
|
|
149
|
+
face_color: ColorType = (1, 1, 1, 1),
|
|
150
|
+
edge_color: ColorType = (0, 0, 0, 1),
|
|
151
|
+
symbol: str = 'o',
|
|
152
|
+
edge_width: float = 1,
|
|
153
|
+
size: float = 10,
|
|
154
|
+
parent: Optional[Any] = None) -> vispy.scene.visuals.Markers:
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
Draw a collection of atoms as markers in 3D space.
|
|
158
|
+
|
|
159
|
+
Parameters:
|
|
160
|
+
- atom_coordinates (numpy.ndarray): Nx3 array of atomic coordinates.
|
|
161
|
+
- face_color (tuple): RGBA color of marker faces. Default is white.
|
|
162
|
+
- edge_color (tuple): RGBA color of marker edges. Default is black.
|
|
163
|
+
- symbol (str): Marker shape (e.g. 'o' for circle, 's' for square).
|
|
164
|
+
- edge_width (float): Width of marker edges.
|
|
165
|
+
- size (float): Size of the markers.
|
|
166
|
+
- parent (Node, optional): Parent VisPy node to attach the markers to.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
- vispy.scene.visuals.Markers: The visual representing the atoms.
|
|
170
|
+
"""
|
|
171
|
+
atoms = visuals.Markers(parent=parent)
|
|
172
|
+
atoms.set_data(atom_coordinates, face_color=face_color, edge_color=edge_color,
|
|
173
|
+
edge_width=edge_width, size=size, symbol=symbol)
|
|
174
|
+
|
|
175
|
+
return atoms
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: MBN_tools
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Useful tools for use with MBN Explorer simulations, files, and their analysis
|
|
5
|
+
Author-email: Matthew Dickers <mattdickers@gmail.com>
|
|
6
|
+
License: Copyright 2024, MBN-tools Developers.
|
|
7
|
+
|
|
8
|
+
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:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
Project-URL: Homepage, https://github.com/mattdickers/MBN-tools
|
|
15
|
+
Project-URL: Source, https://github.com/mattdickers/MBN-tools
|
|
16
|
+
Keywords: MBN Explorer,MBN Studio,Molecular Dynamics,MD,Data Analysis
|
|
17
|
+
Classifier: Development Status :: 4 - Beta
|
|
18
|
+
Classifier: Intended Audience :: Science/Research
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE.txt
|
|
29
|
+
Requires-Dist: numpy>=1.26
|
|
30
|
+
Requires-Dist: scipy>=1.12
|
|
31
|
+
Requires-Dist: mdtraj
|
|
32
|
+
Requires-Dist: vispy
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# MBN Tools
|
|
37
|
+
|
|
38
|
+
**MBN Tools** provides a set of utilities for running, processing, and analysing simulations with [MBN Explorer](https://mbnresearch.com/get-mbn-explorer-software).
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **Run MBN Explorer simulations**
|
|
43
|
+
Run MBN Explorer simulation tasks directly from Python.
|
|
44
|
+
|
|
45
|
+
- **File handling utilities**
|
|
46
|
+
Create, read, modify, and convert simulation input/output files including `.task`, `.xyz`, `.dcd`, and`.pdb`.
|
|
47
|
+
|
|
48
|
+
- **Trajectory analysis**
|
|
49
|
+
Analyze simulation outputs, including radial distribution functions (RDF), mean squared displacement (MSD), RMSD, and more.
|
|
50
|
+
|
|
51
|
+
- **Crystallography utilities**
|
|
52
|
+
Group atoms by crystallographic planes, translate coordinates to planes, and remove atoms near simulation boundaries.
|
|
53
|
+
|
|
54
|
+
- **3D visualisation**
|
|
55
|
+
Render atoms, planes, and simulation boxes in 3D using `VisPy` with customisable colours, sizes, and markers.
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install MBN-tools
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
```python
|
|
65
|
+
import MBN_tools as MBN
|
|
66
|
+
from MBN_tools import analysis, crystallography, visualise
|
|
67
|
+
|
|
68
|
+
# Run an MBN Explorer simulation
|
|
69
|
+
stdout, stderr = MBN.run_task("example.task", "/path/to/MBN_Explorer")
|
|
70
|
+
|
|
71
|
+
# Read an XYZ file
|
|
72
|
+
xyz_data = MBN.read_xyz("simulation.xyz")
|
|
73
|
+
|
|
74
|
+
# Compute RMSD
|
|
75
|
+
rmsd_values = analysis.calculate_rmsd(xyz_data, box_size=[50, 50, 100])
|
|
76
|
+
|
|
77
|
+
# Group atoms by a crystalline plane
|
|
78
|
+
planes = crystallography.group_atoms_by_plane(xyz_data, hkl=(1,1,0), tolerance=0.5)
|
|
79
|
+
|
|
80
|
+
# Visualize atoms in 3D
|
|
81
|
+
canvas = visualise.create_canvas()
|
|
82
|
+
visualise.draw_atoms(xyz_data['coordinates'], parent=canvas.central_widget.add_view())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Dependencies
|
|
86
|
+
|
|
87
|
+
- `numpy >= 1.26`
|
|
88
|
+
- `scipy >= 1.12`
|
|
89
|
+
- `mdtraj`
|
|
90
|
+
- `vispy`
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
MBN_tools/__init__.py,sha256=b6kr80R_sZ7qJf-s4Ygwb8s_yaAEn5jeEOUMAxupP-c,1173
|
|
2
|
+
MBN_tools/analysis.py,sha256=K-USyY7H85CtnJwhP0gUiMKaxuE-oySixDg6RpUBoSA,7458
|
|
3
|
+
MBN_tools/core.py,sha256=Ebr_u_iNV5CM4MvEchJeQcQRBP8O8tVcQNC4rN1LiJ0,10673
|
|
4
|
+
MBN_tools/crystallography.py,sha256=xu58IEgoa8MmvANGFcjORG-YhmHL6VkneVbZRc9fiZM,6615
|
|
5
|
+
MBN_tools/visualise.py,sha256=mNKy1xxLnwR46vDwzqZQs3QF0cmcipOaaNNVWGLt7-4,7073
|
|
6
|
+
mbn_tools-1.0.0.dist-info/licenses/LICENSE.txt,sha256=rObxqiGzkOrl9qyG_7-Lpt1yWi5V0F7a8gj3foRt8Bw,1070
|
|
7
|
+
mbn_tools-1.0.0.dist-info/METADATA,sha256=nffm8U6FufJj6psQrWrwYj3xDho052PkeBqHWe7C49g,3917
|
|
8
|
+
mbn_tools-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
+
mbn_tools-1.0.0.dist-info/top_level.txt,sha256=rD-wUhF3PVoN6Sie6ZUp0H-tzEuTwaG0ZzHgDdfjMk4,10
|
|
10
|
+
mbn_tools-1.0.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
MBN_tools
|