crisp-ase 1.0.0.post0.dev0__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.

Potentially problematic release.


This version of crisp-ase might be problematic. Click here for more details.

@@ -0,0 +1,716 @@
1
+ """
2
+ CRISP/data_analysis/h_bond.py
3
+
4
+ This script performs hydrogen bond analysis on molecular dynamics trajectory data.
5
+ """
6
+ from ase.io import read
7
+ import numpy as np
8
+ import csv
9
+ from joblib import Parallel, delayed
10
+ import argparse
11
+ import os
12
+ from typing import Union, List, Optional, Tuple, Any
13
+ import matplotlib.pyplot as plt
14
+ from ase.data import vdw_radii, atomic_numbers, chemical_symbols
15
+ import seaborn as sns
16
+ import itertools
17
+ import pandas as pd
18
+ import networkx as nx
19
+ import plotly.graph_objects as go
20
+ import plotly.io as pio
21
+
22
+ pio.renderers.default = 'svg'
23
+ pio.renderers.default = 'notebook'
24
+
25
+
26
+ def indices(atoms, ind: Union[str, List[Union[int, str]]]) -> np.ndarray:
27
+ """
28
+ Extract atom indices from an ASE Atoms object based on the input specifier.
29
+
30
+ Parameters
31
+ ----------
32
+ atoms : ase.Atoms
33
+ ASE Atoms object containing atomic structure
34
+ ind : Union[str, List[Union[int, str]]]
35
+ Index specifier, can be:
36
+ - "all" or None: all atoms
37
+ - string ending with ".npy": load indices from NumPy file
38
+ - integer or list of integers: direct atom indices
39
+ - string or list of strings: chemical symbols to select
40
+
41
+ Returns
42
+ -------
43
+ np.ndarray
44
+ Array of selected indices
45
+
46
+ Raises
47
+ ------
48
+ ValueError
49
+ If the index type is invalid
50
+ """
51
+ if ind == "all" or ind is None:
52
+ return np.arange(len(atoms))
53
+
54
+ if isinstance(ind, str) and ind.endswith(".npy"):
55
+ return np.load(ind, allow_pickle=True)
56
+
57
+ if not isinstance(ind, list):
58
+ ind = [ind]
59
+
60
+ if any(isinstance(item, int) for item in ind):
61
+ return np.array(ind)
62
+
63
+ if any(isinstance(item, str) for item in ind):
64
+ idx = []
65
+ for symbol in ind:
66
+ idx.append(np.where(np.array(atoms.get_chemical_symbols()) == symbol)[0])
67
+ return np.concatenate(idx)
68
+
69
+ raise ValueError("Invalid index type")
70
+
71
+
72
+ def count_hydrogen_bonds(atoms, acceptor_atoms=["N","O","F"], angle_cutoff=120, h_bond_cutoff=2.4, bond_cutoff=1.6, mic=True, single_h_bond=False):
73
+
74
+ indices_hydrogen = indices(atoms, "H")
75
+ indices_acceptor = indices(atoms, acceptor_atoms)
76
+
77
+ dm = atoms.get_all_distances(mic=mic)
78
+ np.fill_diagonal(dm, np.inf)
79
+
80
+ sub_dm = dm[indices_hydrogen, :][:, indices_acceptor]
81
+
82
+ hb_hyd = indices_hydrogen[np.where(sub_dm < h_bond_cutoff)[0]]
83
+ hb_acc = indices_acceptor[np.where(sub_dm < h_bond_cutoff)[1]]
84
+
85
+ distances = sub_dm[np.where(sub_dm < h_bond_cutoff)]
86
+
87
+ hydrogen_dict = {}
88
+
89
+ for hydrogen, acceptor, distance in zip(hb_hyd, hb_acc, distances):
90
+ if hydrogen not in hydrogen_dict:
91
+ hydrogen_dict[hydrogen] = []
92
+ hydrogen_dict[hydrogen].append([acceptor, distance])
93
+
94
+ hydrogen_dict = {hydrogen: sorted(acceptors, key=lambda x: x[1]) for hydrogen, acceptors in hydrogen_dict.items()}
95
+
96
+ for hydrogen, bonds in hydrogen_dict.items():
97
+ if len(bonds) > 0 and bonds[0][1] < bond_cutoff:
98
+ filtered_bonds = [bonds[0]]
99
+ for acceptor_h_bond in bonds[1:]:
100
+ angle = atoms.get_angle(bonds[0][0], hydrogen, acceptor_h_bond[0], mic=mic)
101
+ if angle >= angle_cutoff:
102
+ acceptor_h_bond.append(angle)
103
+ filtered_bonds.append(acceptor_h_bond)
104
+ hydrogen_dict[hydrogen] = filtered_bonds
105
+ else:
106
+ hydrogen_dict[hydrogen] = []
107
+
108
+ for idx in indices_hydrogen:
109
+ if idx not in hydrogen_dict:
110
+ hydrogen_dict[idx] = []
111
+
112
+ if single_h_bond:
113
+ num_hydrogen_bonds = sum(1 for bonds in hydrogen_dict.values() if len(bonds) > 1)
114
+ else:
115
+ num_hydrogen_bonds = sum(len(bonds[1:]) for bonds in hydrogen_dict.values())
116
+
117
+ return hydrogen_dict, num_hydrogen_bonds
118
+
119
+
120
+ def aggregate_data(data, index_map, N):
121
+ """
122
+ Aggregates hydrogen bond data to create correlation matrix and network graph.
123
+
124
+ Parameters
125
+ ----------
126
+ data : pandas.DataFrame
127
+ DataFrame containing hydrogen bond data
128
+ index_map : dict
129
+ Mapping from atom indices to array indices
130
+ N : int
131
+ Number of atoms to include in correlation matrix
132
+
133
+ Returns
134
+ -------
135
+ Tuple[np.ndarray, nx.Graph, List]
136
+ Correlation matrix, NetworkX graph, and list of all pairs
137
+ """
138
+ # Aggregates hydrogen bond data
139
+ node_frequency = {node: 0 for node in index_map.keys()}
140
+ edge_weight = {}
141
+ all_pairs = []
142
+
143
+ for frame, group in data.groupby('Frame'):
144
+ pairs = group[['Donor', 'Acceptor']].values
145
+ all_pairs.extend(pairs)
146
+
147
+ for donor, acceptor in pairs:
148
+ if donor in index_map and acceptor in index_map:
149
+ node_frequency[donor] += 1
150
+ node_frequency[acceptor] += 1
151
+ edge = tuple(sorted([donor, acceptor]))
152
+ edge_weight[edge] = edge_weight.get(edge, 0) + 1
153
+
154
+ G = nx.Graph()
155
+
156
+ for node, freq in node_frequency.items():
157
+ G.add_node(node, size=freq)
158
+
159
+ for (donor, acceptor), weight in edge_weight.items():
160
+ G.add_edge(donor, acceptor, weight=weight)
161
+
162
+ corr_matrix = np.zeros((N, N), dtype=int)
163
+ for (donor, acceptor), weight in edge_weight.items():
164
+ donor_idx = index_map[donor]
165
+ acceptor_idx = index_map[acceptor]
166
+ corr_matrix[donor_idx, acceptor_idx] = weight
167
+ corr_matrix[acceptor_idx, donor_idx] = weight
168
+
169
+ return corr_matrix, G, all_pairs
170
+
171
+
172
+ def process_frame(data, donor_acceptor_indices, frame_index, index_map, N):
173
+ """
174
+ Processes data for a specific frame to create correlation matrix and network graph.
175
+
176
+ Parameters
177
+ ----------
178
+ data : pandas.DataFrame
179
+ DataFrame containing hydrogen bond data
180
+ donor_acceptor_indices : np.ndarray
181
+ Array of donor/acceptor atom indices
182
+ frame_index : int
183
+ Frame index to process
184
+ index_map : dict
185
+ Mapping from atom indices to array indices
186
+ N : int
187
+ Number of atoms to include in correlation matrix
188
+
189
+ Returns
190
+ -------
191
+ Tuple[np.ndarray, nx.Graph, List]
192
+ Correlation matrix, NetworkX graph, and list of all pairs
193
+ """
194
+ frame_data = data[data['Frame'] == frame_index]
195
+
196
+ has_distance = 'Distance' in frame_data.columns
197
+
198
+ pairs = frame_data[['Donor', 'Acceptor']].values
199
+ distances = frame_data['Distance'].values if has_distance else [1.0] * len(pairs)
200
+
201
+ corr_matrix = np.zeros((N, N), dtype=float)
202
+
203
+ for i, (donor, acceptor) in enumerate(pairs):
204
+ if donor in index_map and acceptor in index_map:
205
+ donor_idx = index_map[donor]
206
+ acceptor_idx = index_map[acceptor]
207
+ distance = float(distances[i]) if has_distance and distances[i] != "N/A" else 0
208
+
209
+ # Stores the distance in the correlation matrix
210
+ if corr_matrix[donor_idx, acceptor_idx] == 0:
211
+ corr_matrix[donor_idx, acceptor_idx] = distance
212
+ corr_matrix[acceptor_idx, donor_idx] = distance
213
+ else:
214
+ # If multiple bonds exist, uses the average distance
215
+ corr_matrix[donor_idx, acceptor_idx] = (corr_matrix[donor_idx, acceptor_idx] + distance) / 2
216
+ corr_matrix[acceptor_idx, donor_idx] = (corr_matrix[acceptor_idx, donor_idx] + distance) / 2
217
+
218
+ G = nx.Graph()
219
+ for i, donor_idx in enumerate(range(len(donor_acceptor_indices))):
220
+ for j, acceptor_idx in enumerate(range(len(donor_acceptor_indices))):
221
+ if corr_matrix[i, j] > 0:
222
+ G.add_edge(donor_acceptor_indices[i], donor_acceptor_indices[j],
223
+ weight=1, # default weight of 1 for edge thickness
224
+ distance=corr_matrix[i, j]) # Store the actual distance
225
+
226
+ return corr_matrix, G, pairs
227
+
228
+
229
+ def visualize_hydrogen_bonds_matrix(corr_matrix, donor_acceptor_indices=None, frame_index=None, average=False, output_dir=None):
230
+ """
231
+ Visualizes the hydrogen bond correlation matrix using Matplotlib.
232
+
233
+ Parameters
234
+ ----------
235
+ corr_matrix : np.ndarray
236
+ Correlation matrix of hydrogen bonds
237
+ donor_acceptor_indices : np.ndarray, optional
238
+ Array of donor/acceptor atom indices
239
+ frame_index : int, optional
240
+ Frame index for title
241
+ average : bool, optional
242
+ Whether this is an average across frames
243
+ output_dir : str, optional
244
+ Directory to save output file
245
+
246
+ Returns
247
+ -------
248
+ None
249
+ """
250
+ plt.figure(figsize=(10, 8))
251
+
252
+ if np.issubdtype(corr_matrix.dtype, np.integer):
253
+ fmt = "d" # Integer format
254
+ else:
255
+ fmt = ".2f"
256
+
257
+ sns.heatmap(corr_matrix, annot=True, fmt=fmt, cmap='viridis',
258
+ xticklabels=donor_acceptor_indices, yticklabels=donor_acceptor_indices)
259
+
260
+ if average:
261
+ plt.title("Average Hydrogen Bond Correlation Matrix Across All Frames")
262
+ filename = "hbond_correlation_matrix_average.png"
263
+ else:
264
+ plt.title(f"Hydrogen Bond Correlation Matrix for Frame {frame_index}")
265
+ filename = f"hbond_correlation_matrix_frame_{frame_index}.png"
266
+
267
+ plt.xlabel("Atom Index")
268
+ plt.ylabel("Atom Index")
269
+
270
+ if output_dir:
271
+ plt.savefig(os.path.join(output_dir, filename), bbox_inches='tight')
272
+ plt.show()
273
+ plt.close()
274
+ print(f"Correlation matrix saved as '{os.path.join(output_dir, filename)}'")
275
+ else:
276
+ plt.show()
277
+
278
+
279
+ def visualize_hydrogen_bonds_plotly(G, donor_acceptor_indices=None, frame_index=None, average=False, output_dir=None):
280
+ """
281
+ Visualizes the hydrogen bond network using Plotly.
282
+
283
+ Parameters
284
+ ----------
285
+ G : nx.Graph
286
+ NetworkX graph of hydrogen bonds
287
+ donor_acceptor_indices : np.ndarray, optional
288
+ Array of donor/acceptor atom indices
289
+ frame_index : int, optional
290
+ Frame index for title
291
+ average : bool, optional
292
+ Whether this is an average across frames
293
+ output_dir : str, optional
294
+ Directory to save output file
295
+
296
+ Returns
297
+ -------
298
+ None
299
+ """
300
+ seed = 42
301
+ pos = nx.spring_layout(G, seed=seed)
302
+
303
+ node_size = [G.nodes[node].get('size', 20) for node in G.nodes()]
304
+ node_color = [G.degree(node) for node in G.nodes()]
305
+ edge_width = [G[u][v].get('weight', 1) * 0.5 for u, v in G.edges()]
306
+
307
+ edge_distances = []
308
+ for u, v in G.edges():
309
+ distance = G[u][v].get('distance', None)
310
+ if distance is not None and distance != "N/A":
311
+ edge_distances.append(f"{distance:.2f} Å")
312
+ else:
313
+ edge_distances.append("N/A")
314
+
315
+ x_nodes = [pos[node][0] for node in G.nodes()]
316
+ y_nodes = [pos[node][1] for node in G.nodes()]
317
+
318
+ edge_trace = []
319
+ for i, (u, v) in enumerate(G.edges()):
320
+ x0, y0 = pos[u]
321
+ x1, y1 = pos[v]
322
+
323
+ edge_info = f'Bond {u}-{v}: {edge_distances[i]}'
324
+
325
+ edge_trace.append(go.Scatter(
326
+ x=[x0, x1],
327
+ y=[y0, y1],
328
+ mode='lines',
329
+ line=dict(width=edge_width[i], color='Magenta'),
330
+ name=edge_info,
331
+ hoverinfo='text',
332
+ text=edge_info
333
+ ))
334
+
335
+ node_trace = go.Scatter(
336
+ x=x_nodes, y=y_nodes, mode='markers',
337
+ marker=dict(
338
+ size=node_size,
339
+ color=node_color,
340
+ colorscale='Viridis',
341
+ colorbar=dict(
342
+ thickness=15,
343
+ title=dict(text='Node Connections', side='right'),
344
+ xanchor='left'
345
+ )
346
+ ),
347
+ text=[str(node) for node in G.nodes()],
348
+ textposition='top center',
349
+ name='Donor/Acceptor Atoms'
350
+ )
351
+
352
+ title = "Average Hydrogen Bond Network Across All Frames" if average else f"Hydrogen Bond Network for Frame {frame_index}"
353
+
354
+ fig = go.Figure(
355
+ data=edge_trace + [node_trace],
356
+ layout=go.Layout(
357
+ title=dict(text=f'<br>{title}', font=dict(size=16)),
358
+ showlegend=False,
359
+ hovermode='closest',
360
+ margin=dict(b=20, l=5, r=5, t=40),
361
+ annotations=[dict(
362
+ text="Network graph visualization of hydrogen bonds",
363
+ showarrow=False,
364
+ xref="paper",
365
+ yref="paper",
366
+ x=0.005,
367
+ y=-0.002
368
+ )],
369
+ xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
370
+ yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)
371
+ )
372
+ )
373
+
374
+ if output_dir:
375
+ filename = "hbond_network_average.html" if average else f"hbond_network_frame_{frame_index}.html"
376
+ fig.write_html(os.path.join(output_dir, filename))
377
+ print(f"Figure saved as '{os.path.join(output_dir, filename)}'")
378
+ else:
379
+ fig.show()
380
+
381
+
382
+ def visualize_hydrogen_bonds_matrix_plotly(corr_matrix, donor_acceptor_indices=None, frame_index=None, average=False, output_dir=None):
383
+ """
384
+ Visualizes the hydrogen bond correlation matrix using Plotly.
385
+
386
+ Parameters
387
+ ----------
388
+ corr_matrix : np.ndarray
389
+ Correlation matrix of hydrogen bonds
390
+ donor_acceptor_indices : np.ndarray, optional
391
+ Array of donor/acceptor atom indices
392
+ frame_index : int, optional
393
+ Frame index for title
394
+ average : bool, optional
395
+ Whether this is an average across frames
396
+ output_dir : str, optional
397
+ Directory to save output file
398
+
399
+ Returns
400
+ -------
401
+ None
402
+ """
403
+ # Format hover text based on matrix values
404
+ hover_text = [[f"Donor: {donor_acceptor_indices[i]}<br>Acceptor: {donor_acceptor_indices[j]}<br>Value: {val:.2f}"
405
+ if isinstance(val, float) else f"Donor: {donor_acceptor_indices[i]}<br>Acceptor: {donor_acceptor_indices[j]}<br>Value: {val}"
406
+ for j, val in enumerate(row)] for i, row in enumerate(corr_matrix)]
407
+
408
+ fig = go.Figure(data=go.Heatmap(
409
+ z=corr_matrix,
410
+ x=[str(idx) for idx in donor_acceptor_indices],
411
+ y=[str(idx) for idx in donor_acceptor_indices],
412
+ colorscale='Viridis',
413
+ text=hover_text,
414
+ hoverinfo='text',
415
+ colorbar=dict(title='No. of H-Bond')
416
+ ))
417
+
418
+ title = "Average Hydrogen Bond Correlation Matrix Across All Frames" if average else f"Hydrogen Bond Correlation Matrix for Frame {frame_index}"
419
+
420
+ fig.update_layout(
421
+ title=dict(text=title, font=dict(size=16)),
422
+ xaxis=dict(title='Atom Index', tickfont=dict(size=10)),
423
+ yaxis=dict(title='Atom Index', tickfont=dict(size=10)),
424
+ width=900,
425
+ height=800,
426
+ autosize=True
427
+ )
428
+
429
+ if output_dir:
430
+ filename = "hbond_correlation_matrix_average.html" if average else f"hbond_correlation_matrix_frame_{frame_index}.html"
431
+ fig.write_html(os.path.join(output_dir, filename))
432
+ print(f"Interactive correlation matrix saved as '{os.path.join(output_dir, filename)}'")
433
+ else:
434
+ fig.show()
435
+
436
+
437
+ def visualize_hydrogen_bonds(csv_file, indices_path, frame_index=None, average=False, output_dir=None):
438
+ """
439
+ Visualizes hydrogen bonds from a CSV file containing donor-acceptor pairs.
440
+
441
+ Parameters
442
+ ----------
443
+ csv_file : str
444
+ Path to CSV file with hydrogen bond data
445
+ indices_path : str
446
+ Path to NumPy array file with donor/acceptor indices
447
+ frame_index : int, optional
448
+ Frame index to visualize (required if average=False)
449
+ average : bool, optional
450
+ Whether to visualize average across all frames
451
+ output_dir : str, optional
452
+ Directory to save output files
453
+
454
+ Returns
455
+ -------
456
+ None
457
+ """
458
+ data = pd.read_csv(csv_file)
459
+ donor_acceptor_indices = np.load(indices_path)
460
+ index_map = {idx: i for i, idx in enumerate(donor_acceptor_indices)}
461
+ N = len(donor_acceptor_indices)
462
+
463
+ if average:
464
+ corr_matrix, G, pairs = aggregate_data(data, index_map, N)
465
+ visualize_hydrogen_bonds_matrix_plotly(corr_matrix, donor_acceptor_indices=donor_acceptor_indices, average=True, output_dir=output_dir)
466
+ visualize_hydrogen_bonds_plotly(G, average=True, output_dir=output_dir)
467
+ else:
468
+ if frame_index is None:
469
+ raise ValueError("frame_index must be provided when average=False")
470
+
471
+ corr_matrix, G, pairs = process_frame(data, donor_acceptor_indices, frame_index, index_map, N)
472
+ visualize_hydrogen_bonds_matrix_plotly(corr_matrix, donor_acceptor_indices=donor_acceptor_indices, frame_index=frame_index, output_dir=output_dir)
473
+ visualize_hydrogen_bonds_plotly(G, frame_index=frame_index, output_dir=output_dir)
474
+
475
+
476
+ def hydrogen_bonds(traj_path, frame_skip=10, acceptor_atoms=["N","O","F"], angle_cutoff=120,
477
+ h_bond_cutoff=2.4, bond_cutoff=1.6, mic=True, single_h_bond=False,
478
+ output_dir="./", time_step=None, plot_count=False, plot_heatmap=False,
479
+ plot_graph_frame=True, plot_graph_average=False, indices_path=None,
480
+ graph_frame_index=0):
481
+ """
482
+ Analyze hydrogen bonds in a molecular dynamics trajectory.
483
+
484
+ Parameters
485
+ ----------
486
+ traj_path : str
487
+ Path to trajectory file
488
+ frame_skip : int, optional
489
+ Number of frames to skip (default: 10)
490
+ acceptor_atoms : List[str], optional
491
+ List of element symbols that can be acceptors (default: ["N","O","F"])
492
+ angle_cutoff : float, optional
493
+ Minimum angle in degrees for hydrogen bond (default: 120)
494
+ h_bond_cutoff : float, optional
495
+ Maximum distance in Å for hydrogen bond (default: 2.4)
496
+ bond_cutoff : float, optional
497
+ Maximum distance in Å for covalent bond (default: 1.6)
498
+ mic : bool, optional
499
+ Whether to use minimum image convention (default: True)
500
+ single_h_bond : bool, optional
501
+ Whether to count only first hydrogen bond per atom (default: False)
502
+ output_dir : str, optional
503
+ Directory to save output files (default: "./")
504
+ time_step : float, optional
505
+ Simulation time step for plotting (default: None)
506
+ plot_count : bool, optional
507
+ Whether to plot hydrogen bond count (default: False)
508
+ plot_heatmap : bool, optional
509
+ Whether to plot 2D histogram (default: False)
510
+ plot_graph_frame : bool, optional
511
+ Whether to plot interactive hydrogen bond network graph for specific frame (default: True)
512
+ plot_graph_average : bool, optional
513
+ Whether to plot interactive average hydrogen bond network graph (default: False)
514
+ indices_path : str, optional
515
+ Path to NumPy array with donor/acceptor atom indices for graph plotting.
516
+ If None, the unique indices will be automatically extracted and saved (default: None)
517
+ graph_frame_index : int, optional
518
+ Frame index to use for graph visualization (default: 0)
519
+
520
+ Returns
521
+ -------
522
+ List[int]
523
+ List of hydrogen bond counts per frame
524
+ """
525
+ os.makedirs(output_dir, exist_ok=True)
526
+
527
+ output_filename = os.path.join(output_dir, f'hydrogen_bonds_{frame_skip}skips.csv')
528
+ total_bonds_filename = os.path.join(output_dir, f'total_hydrogen_bonds_per_frame_{frame_skip}skips.csv')
529
+
530
+ trajectory = read(traj_path, index=f"::{frame_skip}")
531
+
532
+ all_data = Parallel(n_jobs=-1)(
533
+ delayed(count_hydrogen_bonds)(
534
+ atoms, acceptor_atoms=acceptor_atoms, angle_cutoff=angle_cutoff, h_bond_cutoff=h_bond_cutoff,
535
+ bond_cutoff=bond_cutoff, mic=True, single_h_bond=single_h_bond
536
+ ) for i, atoms in enumerate(trajectory)
537
+ )
538
+
539
+ h_bonds_per_frame = [num_bonds for _, num_bonds in all_data]
540
+ frame_dict_list = [frame_dict for frame_dict, _ in all_data]
541
+ data_dict = {i*frame_skip: d for i, d in enumerate(frame_dict_list)}
542
+
543
+ # Donor-acceptor distances for each frame
544
+ donor_acceptor_distances = {}
545
+ for frame_idx, frame in enumerate(list(data_dict.keys())):
546
+ frame_atoms = trajectory[frame_idx]
547
+ dm = frame_atoms.get_all_distances(mic=mic)
548
+
549
+ donor_acceptor_distances[frame] = {}
550
+
551
+ for hydrogen in data_dict[frame]:
552
+ if len(data_dict[frame][hydrogen]) > 1:
553
+ donor = data_dict[frame][hydrogen][0][0]
554
+ donor_acceptor_distances[frame][hydrogen] = {}
555
+
556
+ for sublist in data_dict[frame][hydrogen][1:]:
557
+ acceptor = sublist[0]
558
+ # Get direct donor-acceptor distance (e.g., O-O distance in water)
559
+ donor_acceptor_dist = dm[donor, acceptor]
560
+ donor_acceptor_distances[frame][hydrogen][acceptor] = donor_acceptor_dist
561
+
562
+ with open(output_filename, 'w', newline='') as csvfile:
563
+ fieldnames = [
564
+ 'Frame', 'Hydrogen', 'Donor', 'Acceptor(s)',
565
+ 'Donor-Hydrogen Distance', 'Hydrogen-Acceptor(s) Distance(s)',
566
+ 'Donor-Acceptor(s) Distance(s)', 'Angle(A-H-D)'
567
+ ]
568
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
569
+ writer.writeheader()
570
+ for frame in list(data_dict.keys()):
571
+ for hydrogen in data_dict[frame]:
572
+ donor = data_dict[frame][hydrogen][0][0] if len(data_dict[frame][hydrogen]) > 0 else ""
573
+ donor_hydrogen_dist = data_dict[frame][hydrogen][0][1] if len(data_dict[frame][hydrogen]) > 0 else ""
574
+
575
+ if len(data_dict[frame][hydrogen]) > 1:
576
+ acceptors = [sublist[0] for sublist in data_dict[frame][hydrogen][1:]]
577
+ acceptors_hydrogen_dist = [sublist[1] for sublist in data_dict[frame][hydrogen][1:]]
578
+ angles = [sublist[2] for sublist in data_dict[frame][hydrogen][1:]]
579
+
580
+ # Get donor-acceptor distances
581
+ donor_acceptor_dist = [donor_acceptor_distances[frame][hydrogen].get(acc, "N/A") for acc in acceptors]
582
+ else:
583
+ acceptors = ""
584
+ acceptors_hydrogen_dist = ""
585
+ donor_acceptor_dist = ""
586
+ angles = ""
587
+
588
+ row = {
589
+ 'Frame': frame,
590
+ 'Hydrogen': hydrogen,
591
+ 'Donor': donor,
592
+ 'Acceptor(s)': acceptors,
593
+ 'Donor-Hydrogen Distance': donor_hydrogen_dist,
594
+ 'Hydrogen-Acceptor(s) Distance(s)': acceptors_hydrogen_dist,
595
+ 'Donor-Acceptor(s) Distance(s)': donor_acceptor_dist,
596
+ 'Angle(A-H-D)': angles,
597
+ }
598
+
599
+ writer.writerow(row)
600
+
601
+
602
+ with open(total_bonds_filename, 'w', newline='') as f:
603
+ writer = csv.writer(f)
604
+ writer.writerow(['Frame', 'Total Hydrogen Bonds'])
605
+ for frame_idx, num_bonds in enumerate(h_bonds_per_frame):
606
+ writer.writerow([frame_idx * frame_skip, num_bonds])
607
+
608
+ if time_step is not None and plot_count:
609
+ plot_hydrogen_count(h_bonds_per_frame, frame_skip, time_step, output_dir)
610
+
611
+ if plot_heatmap:
612
+ plot_2Dheatmap(data_dict, output_dir)
613
+
614
+ if plot_graph_frame or plot_graph_average:
615
+ network_csv = os.path.join(output_dir, "hydrogen_bonds_network.csv")
616
+
617
+ unique_atoms = set()
618
+
619
+ with open(network_csv, 'w', newline='') as csvfile:
620
+ writer = csv.writer(csvfile)
621
+ writer.writerow(['Frame', 'Donor', 'Acceptor', 'Distance'])
622
+ for frame in list(data_dict.keys()):
623
+ for hydrogen in data_dict[frame]:
624
+ if len(data_dict[frame][hydrogen]) > 1:
625
+ donor = data_dict[frame][hydrogen][0][0]
626
+ unique_atoms.add(donor) # Added donor to unique atoms
627
+
628
+ for i, sublist in enumerate(data_dict[frame][hydrogen][1:]):
629
+ acceptor = sublist[0]
630
+ unique_atoms.add(acceptor) # Added acceptor to unique atoms
631
+
632
+ # Include the donor-acceptor distance in the network file
633
+ distance = donor_acceptor_distances[frame][hydrogen].get(acceptor, "N/A")
634
+ writer.writerow([frame, donor, acceptor, distance])
635
+
636
+ if indices_path is None:
637
+ indices_path = os.path.join(output_dir, "donor_acceptor_indices.npy")
638
+ unique_atoms_array = np.array(sorted(list(unique_atoms)), dtype=int)
639
+ np.save(indices_path, unique_atoms_array)
640
+ print(f"Generated and saved {len(unique_atoms_array)} unique donor/acceptor atom indices to {indices_path}")
641
+
642
+ if plot_graph_frame:
643
+ print(f"Generating hydrogen bond network visualizations for frame {graph_frame_index}...")
644
+ visualize_hydrogen_bonds(network_csv, indices_path,
645
+ frame_index=graph_frame_index, average=False,
646
+ output_dir=output_dir)
647
+
648
+ if plot_graph_average:
649
+ print("Generating average hydrogen bond network visualization...")
650
+ visualize_hydrogen_bonds(network_csv, indices_path,
651
+ average=True, output_dir=output_dir)
652
+
653
+ return h_bonds_per_frame
654
+
655
+
656
+ def plot_hydrogen_count(h_bonds_per_frame, frame_skip, time_step, output_dir):
657
+ """
658
+ Plot hydrogen bond count over time.
659
+
660
+ Parameters
661
+ ----------
662
+ h_bonds_per_frame : List[int]
663
+ Number of hydrogen bonds per frame
664
+ frame_skip : int
665
+ Number of frames skipped in trajectory analysis
666
+ time_step : float
667
+ Time step between frames in fs
668
+ output_dir : str
669
+ Directory to save output file
670
+
671
+ Returns
672
+ -------
673
+ None
674
+ """
675
+ x = np.arange(len(h_bonds_per_frame)) * time_step * frame_skip / 1000
676
+
677
+ fig, ax1 = plt.subplots(figsize=(8, 6))
678
+ ax1.plot(x, h_bonds_per_frame, '-', color="blue", label="H-bond count")
679
+ ax1.axhline(np.mean(h_bonds_per_frame), linestyle="--", color="blue", label=f"Mean: {np.mean(h_bonds_per_frame):.2f}")
680
+ ax1.set_xlabel("Time [ps]", fontsize=12)
681
+ ax1.set_ylabel("Count", fontsize=12)
682
+ plt.title("Hydrogen bond count", fontsize=14)
683
+ fig.legend(loc="center right", bbox_to_anchor=(1.1, 0.5))
684
+ filename = os.path.join(output_dir, "h_bond_count.png")
685
+ plt.savefig(filename, bbox_inches="tight")
686
+ plt.show()
687
+ plt.close()
688
+ print(f"Hydrogen bond count plot saved to '{filename}'")
689
+
690
+
691
+ def plot_2Dheatmap(data_dict, output_dir):
692
+ angles_list = []
693
+ dist_list = []
694
+
695
+ for frame in list(data_dict.keys()):
696
+ for hydrogen in data_dict[frame]:
697
+ if len(data_dict[frame][hydrogen]) > 1:
698
+ dist_list.append([sublist[1] for sublist in data_dict[frame][hydrogen][1:]])
699
+ angles_list.append([sublist[2] for sublist in data_dict[frame][hydrogen][1:]])
700
+
701
+ angles_list = list(itertools.chain(*angles_list))
702
+ dist_list = list(itertools.chain(*dist_list))
703
+
704
+ hb = plt.hist2d(angles_list, dist_list, bins=30, cmap="viridis")
705
+
706
+ plt.colorbar(hb[3], label="Count")
707
+
708
+ plt.xlabel("Donor-Hydrogen-Acceptor Angle [°]",fontsize=12)
709
+ plt.ylabel("Acceptor-Hydrogen Distance [Å]",fontsize=12)
710
+ plt.title("2D Histogram of H-bonds parameters",fontsize=14)
711
+
712
+ filename = os.path.join(output_dir, "h_bond_structure.png")
713
+ plt.savefig(filename, bbox_inches="tight")
714
+ plt.show()
715
+ plt.close()
716
+ print(f"H-bond structure 2D histogram saved to '{filename}'")