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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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