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.
@@ -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