mimicpy 0.2.0__py3-none-any.whl → 0.3.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.
Files changed (53) hide show
  1. mimicpy/__init__.py +1 -1
  2. mimicpy/__main__.py +726 -2
  3. mimicpy/_authors.py +2 -2
  4. mimicpy/_version.py +2 -2
  5. mimicpy/coords/__init__.py +1 -1
  6. mimicpy/coords/base.py +1 -1
  7. mimicpy/coords/cpmdgeo.py +1 -1
  8. mimicpy/coords/gro.py +1 -1
  9. mimicpy/coords/pdb.py +1 -1
  10. mimicpy/core/__init__.py +1 -1
  11. mimicpy/core/prepare.py +3 -3
  12. mimicpy/core/selector.py +1 -1
  13. mimicpy/force_matching/__init__.py +34 -0
  14. mimicpy/force_matching/bonded_forces.py +628 -0
  15. mimicpy/force_matching/compare_top.py +809 -0
  16. mimicpy/force_matching/dresp.py +435 -0
  17. mimicpy/force_matching/nonbonded_forces.py +32 -0
  18. mimicpy/force_matching/opt_ff.py +2114 -0
  19. mimicpy/force_matching/qm_region.py +1960 -0
  20. mimicpy/plugins/__main_installer__.py +76 -0
  21. mimicpy/{__main_vmd__.py → plugins/__main_vmd__.py} +2 -2
  22. mimicpy/plugins/pymol.py +56 -0
  23. mimicpy/plugins/vmd.tcl +78 -0
  24. mimicpy/scripts/__init__.py +1 -1
  25. mimicpy/scripts/cpmd.py +1 -1
  26. mimicpy/scripts/fm_input.py +265 -0
  27. mimicpy/scripts/fmdata.py +120 -0
  28. mimicpy/scripts/mdp.py +1 -1
  29. mimicpy/scripts/ndx.py +1 -1
  30. mimicpy/scripts/script.py +1 -1
  31. mimicpy/topology/__init__.py +1 -1
  32. mimicpy/topology/itp.py +603 -35
  33. mimicpy/topology/mpt.py +1 -1
  34. mimicpy/topology/top.py +254 -15
  35. mimicpy/topology/topol_dict.py +233 -4
  36. mimicpy/utils/__init__.py +1 -1
  37. mimicpy/utils/atomic_numbers.py +1 -1
  38. mimicpy/utils/constants.py +17 -3
  39. mimicpy/utils/elements.py +1 -1
  40. mimicpy/utils/errors.py +1 -1
  41. mimicpy/utils/file_handler.py +1 -1
  42. mimicpy/utils/strings.py +1 -1
  43. mimicpy-0.3.0.dist-info/METADATA +156 -0
  44. mimicpy-0.3.0.dist-info/RECORD +50 -0
  45. {mimicpy-0.2.0.dist-info → mimicpy-0.3.0.dist-info}/WHEEL +1 -1
  46. mimicpy-0.3.0.dist-info/entry_points.txt +4 -0
  47. mimicpy-0.2.0.dist-info/METADATA +0 -86
  48. mimicpy-0.2.0.dist-info/RECORD +0 -38
  49. mimicpy-0.2.0.dist-info/entry_points.txt +0 -3
  50. {mimicpy-0.2.0.dist-info → mimicpy-0.3.0.dist-info/licenses}/COPYING +0 -0
  51. {mimicpy-0.2.0.dist-info → mimicpy-0.3.0.dist-info/licenses}/COPYING.LESSER +0 -0
  52. {mimicpy-0.2.0.dist-info → mimicpy-0.3.0.dist-info}/top_level.txt +0 -0
  53. {mimicpy-0.2.0.dist-info → mimicpy-0.3.0.dist-info}/zip-safe +0 -0
@@ -0,0 +1,809 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+ from .qm_region import QMRegion
4
+ from ..utils.constants import au_to_nm, kb_au2gmx, au_kjm
5
+
6
+
7
+ def compare_qm_parameters(top1_file, top2_file, top3_file=None, coord_file=None, qm_selection=None, dir='.', labels=['Original FF', 'Force-matched FF', ' QM/MM']):
8
+ """
9
+ Compare charges and bonded parameters of QM atoms between two or three topologies
10
+
11
+ Args:
12
+ top1_file (str): Path to first topology file
13
+ top2_file (str): Path to second topology file
14
+ top3_file (str, optional): Path to third topology file (for bond lengths and angles only)
15
+ coord_file (str): Path to coordinate file (gro/pdb)
16
+ qm_selection (str): Selection string for QM atoms
17
+ dir (str): Directory to save plots
18
+ labels (list, optional): List of labels for the topologies (default: ['Original FF', 'Force-matched FF', 'QM/MM'])
19
+ """
20
+ if labels is None:
21
+ labels = ['Topology 1', 'Topology 2', 'Topology 3']
22
+ if len(labels) < 2:
23
+ raise ValueError('labels must be a list of at least two strings')
24
+ if top3_file is not None and len(labels) < 3:
25
+ labels.append('Topology 3')
26
+
27
+ # Create QM regions for topologies
28
+ qm1 = QMRegion(top1_file, coord_file)
29
+ qm2 = QMRegion(top2_file, coord_file)
30
+
31
+ # Get QM atoms from both topologies
32
+ qm1.setup_qm_region(qm_selection) # Add all QM atoms
33
+ qm2.setup_qm_region(qm_selection) # Add all QM atoms
34
+
35
+ # Get QM atoms dataframes
36
+ qm_atoms1 = qm1.qm_atoms
37
+ qm_atoms2 = qm2.qm_atoms
38
+
39
+ # Initialize third topology if provided
40
+ qm3 = None
41
+ qm_atoms3 = None
42
+ qm_interactions3 = None
43
+ if top3_file is not None:
44
+ qm3 = QMRegion(top3_file, coord_file)
45
+ qm3.setup_qm_region(qm_selection)
46
+ qm_atoms3 = qm3.qm_atoms
47
+ qm_interactions3 = qm3.extract_qm_interactions()
48
+
49
+ # Compare charges
50
+ print("\nComparing QM atom charges:")
51
+ print("-" * 50)
52
+
53
+ # Create atom labels with residue information
54
+ atom_labels = [f"{row['name']}"
55
+ for _, row in qm_atoms1.iterrows()]
56
+
57
+ num_atoms = len(atom_labels)
58
+
59
+
60
+
61
+ # For large numbers, use subplot-based bar plots for charges
62
+ # Calculate subplot layout - stack vertically
63
+ atoms_per_plot = 20 # Number of atoms per subplot
64
+ num_plots = (num_atoms + atoms_per_plot - 1) // atoms_per_plot # Ceiling division
65
+
66
+ # Stack plots vertically (1 column, multiple rows)
67
+ cols = 1 # Single column
68
+ rows = num_plots
69
+
70
+ # Create figure for charge comparison
71
+ fig1, axes1 = plt.subplots(rows, cols, figsize=(12, 4*rows))
72
+ fig1.suptitle('Charge Comparison Between Topologies', fontsize=16, y=0.98)
73
+
74
+ # Flatten axes if needed
75
+ if rows == 1:
76
+ axes1 = [axes1] if cols == 1 else axes1
77
+ else:
78
+ axes1 = axes1.flatten()
79
+
80
+ # Plot charges in subplots
81
+ for plot_idx in range(num_plots):
82
+ start_idx = plot_idx * atoms_per_plot
83
+ end_idx = min(start_idx + atoms_per_plot, num_atoms)
84
+
85
+ ax = axes1[plot_idx]
86
+ x = np.arange(end_idx - start_idx)
87
+ width = 0.35
88
+
89
+ # Get data for this subplot
90
+ charge1_subset = qm_atoms1['charge'].iloc[start_idx:end_idx]
91
+ charge2_subset = qm_atoms2['charge'].iloc[start_idx:end_idx]
92
+ atom_labels_subset = atom_labels[start_idx:end_idx]
93
+
94
+ # Create bars
95
+ ax.bar(x - width/2, charge1_subset, width, label=labels[0], alpha=0.7, color='blue')
96
+ ax.bar(x + width/2, charge2_subset, width, label=labels[1], alpha=0.7, color='red')
97
+
98
+ ax.set_xlabel('Atoms')
99
+ ax.set_ylabel('Charge')
100
+ ax.set_title(f'Atoms {start_idx+1}-{end_idx}')
101
+ ax.set_xticks(x)
102
+ # Use actual atom labels
103
+ ax.set_xticklabels(atom_labels_subset, rotation=45, ha='right', fontsize=8)
104
+ ax.legend(fontsize=8)
105
+ ax.grid(True, alpha=0.3)
106
+
107
+ # Hide unused subplots
108
+ for i in range(num_plots, len(axes1)):
109
+ axes1[i].set_visible(False)
110
+
111
+ plt.tight_layout()
112
+ plt.subplots_adjust(top=0.95) # Adjust for suptitle
113
+ plt.savefig(f'{dir}/charge_comparison_subplots.png', dpi=300, bbox_inches='tight')
114
+ plt.close()
115
+
116
+ # Create figure for charge differences
117
+ fig2, axes2 = plt.subplots(rows, cols, figsize=(12, 4*rows))
118
+ fig2.suptitle('Charge Differences Between Topologies', fontsize=16, y=0.98)
119
+
120
+ # Flatten axes if needed
121
+ if rows == 1:
122
+ axes2 = [axes2] if cols == 1 else axes2
123
+ else:
124
+ axes2 = axes2.flatten()
125
+
126
+ # Plot charge differences in subplots
127
+ for plot_idx in range(num_plots):
128
+ start_idx = plot_idx * atoms_per_plot
129
+ end_idx = min(start_idx + atoms_per_plot, num_atoms)
130
+
131
+ ax = axes2[plot_idx]
132
+ x = np.arange(end_idx - start_idx)
133
+ width = 0.7
134
+
135
+ # Get data for this subplot
136
+ charge1_subset = qm_atoms1['charge'].iloc[start_idx:end_idx]
137
+ charge2_subset = qm_atoms2['charge'].iloc[start_idx:end_idx]
138
+ atom_labels_subset = atom_labels[start_idx:end_idx]
139
+
140
+ # Calculate charge differences
141
+ charge_diff_subset = ((charge1_subset - charge2_subset) / charge1_subset) * 100
142
+
143
+ # Create bars
144
+ ax.bar(x, charge_diff_subset, width, color='red', alpha=0.7)
145
+
146
+ # Add horizontal line at y=0
147
+ ax.axhline(y=0, color='black', linestyle='-', alpha=0.3)
148
+
149
+ ax.set_xlabel('Atoms')
150
+ ax.set_ylabel('Charge Difference (%)')
151
+ ax.set_title(f'Atoms {start_idx+1}-{end_idx}')
152
+ ax.set_xticks(x)
153
+ # Use actual atom labels
154
+ ax.set_xticklabels(atom_labels_subset, rotation=45, ha='right', fontsize=8)
155
+ ax.grid(True, alpha=0.3)
156
+
157
+ # Hide unused subplots
158
+ for i in range(num_plots, len(axes2)):
159
+ axes2[i].set_visible(False)
160
+
161
+ plt.tight_layout()
162
+ plt.subplots_adjust(top=0.95) # Adjust for suptitle
163
+ plt.savefig(f'{dir}/charge_differences_subplots.png', dpi=300, bbox_inches='tight')
164
+ plt.close()
165
+
166
+
167
+ # Compare bonded parameters
168
+ print("\nComparing QM atom bonded parameters:")
169
+ print("-" * 50)
170
+
171
+ # Extract QM interactions
172
+ qm_interactions1 = qm1.extract_qm_interactions()
173
+ qm_interactions2 = qm2.extract_qm_interactions()
174
+
175
+ # Helper function to get atom labels for interactions
176
+ def get_interaction_label(atoms, qm_atoms):
177
+ labels_ = []
178
+ for atom_idx in atoms:
179
+ atom = qm_atoms.iloc[atom_idx] # Convert to 1-based index
180
+ labels_.append(f"{atom['name']}")
181
+ return " - ".join(labels_)
182
+
183
+ # Compare bonds
184
+ # Create bond labels and parameters
185
+ bond_labels = []
186
+ r1_values = []
187
+ r2_values = []
188
+ r3_values = []
189
+ k1_values = []
190
+ k2_values = []
191
+
192
+ for bond1 in qm_interactions1['bonds']:
193
+ if bond1['involves_mm']:
194
+ continue
195
+ atoms = tuple(sorted(bond1['atoms']))
196
+ # Find matching bond in topology 2
197
+ for bond2 in qm_interactions2['bonds']:
198
+ if bond2['involves_mm']:
199
+ continue
200
+ if tuple(sorted(bond2['atoms'])) == atoms:
201
+ bond_labels.append(get_interaction_label(atoms, qm_atoms1))
202
+ # Convert atomic units to GROMACS units
203
+ r1_values.append(bond1['parameters'][0] * au_to_nm) # Convert from au to nm
204
+ r2_values.append(bond2['parameters'][0] * au_to_nm) # Convert from au to nm
205
+ k1_values.append(bond1['parameters'][1] * kb_au2gmx) # Convert from au to kJ/mol/nm^2
206
+ k2_values.append(bond2['parameters'][1] * kb_au2gmx) # Convert from au to kJ/mol/nm^2
207
+
208
+ # Add third topology bond length if available
209
+ if qm_interactions3 is not None:
210
+ r3_found = False
211
+ for bond3 in qm_interactions3['bonds']:
212
+ if tuple(sorted(bond3['atoms'])) == atoms:
213
+ r3_values.append(bond3['parameters'][0] * au_to_nm) # Convert from au to nm
214
+ r3_found = True
215
+ break
216
+ if not r3_found:
217
+ r3_values.append(None) # No matching bond found
218
+ else:
219
+ r3_values.append(None)
220
+ break
221
+
222
+ # Plot bond parameters with summary statistics for large numbers
223
+ num_bonds = len(bond_labels)
224
+ if num_bonds > 0:
225
+
226
+ # For large numbers, use subplot-based bar plots
227
+ # Calculate subplot layout - stack vertically
228
+ bonds_per_plot = 20 # Number of bonds per subplot
229
+ num_plots = (num_bonds + bonds_per_plot - 1) // bonds_per_plot # Ceiling division
230
+
231
+ # Stack plots vertically (1 column, multiple rows)
232
+ cols = 1 # Single column
233
+ rows = num_plots
234
+
235
+ # Create figure for bond length comparison
236
+ fig1, axes1 = plt.subplots(rows, cols, figsize=(12, 4*rows))
237
+ fig1.suptitle('Bond Length Comparison', fontsize=16, y=0.98)
238
+
239
+ # Flatten axes if needed
240
+ if rows == 1:
241
+ axes1 = [axes1] if cols == 1 else axes1
242
+ else:
243
+ axes1 = axes1.flatten()
244
+
245
+ # Plot bond lengths in subplots
246
+ for plot_idx in range(num_plots):
247
+ start_idx = plot_idx * bonds_per_plot
248
+ end_idx = min(start_idx + bonds_per_plot, num_bonds)
249
+
250
+ ax = axes1[plot_idx]
251
+ x = np.arange(end_idx - start_idx)
252
+ width = 0.35
253
+
254
+ # Get data for this subplot
255
+ r1_subset = r1_values[start_idx:end_idx]
256
+ r2_subset = r2_values[start_idx:end_idx]
257
+ bond_labels_subset = bond_labels[start_idx:end_idx]
258
+
259
+ # Create bars
260
+ bar_width = 0.25 if qm_interactions3 is not None else 0.35
261
+ ax.bar(x - bar_width, r1_subset, bar_width, label=labels[0], alpha=0.7, color='blue')
262
+ ax.bar(x, r2_subset, bar_width, label=labels[1], alpha=0.7, color='red')
263
+
264
+ if qm_interactions3 is not None:
265
+ # Filter out None values for third topology
266
+ r3_subset = [r3_values[i] for i in range(start_idx, end_idx) if r3_values[i] is not None]
267
+ if len(r3_subset) > 0:
268
+ # Only plot if we have valid third topology data
269
+ valid_indices = [i for i in range(start_idx, end_idx) if r3_values[i] is not None]
270
+ valid_x = [x[i - start_idx] for i in valid_indices]
271
+ ax.bar(valid_x + bar_width, r3_subset, bar_width, label=labels[2], alpha=0.7, color='green')
272
+
273
+ ax.set_xlabel('Bonds')
274
+ ax.set_ylabel('Bond Length (nm)')
275
+ ax.set_title(f'Bonds {start_idx+1}-{end_idx}')
276
+ ax.set_xticks(x)
277
+ # Use actual atom labels for bonds
278
+ ax.set_xticklabels(bond_labels_subset, rotation=45, ha='right', fontsize=8)
279
+ ax.legend(fontsize=8)
280
+ ax.grid(True, alpha=0.3)
281
+
282
+ # Hide unused subplots
283
+ for i in range(num_plots, len(axes1)):
284
+ axes1[i].set_visible(False)
285
+
286
+ plt.tight_layout()
287
+ plt.subplots_adjust(top=0.95) # Adjust for suptitle
288
+ plt.savefig(f'{dir}/bond_length_subplots.png', dpi=300, bbox_inches='tight')
289
+ plt.close()
290
+
291
+ # Create figure for bond force constant comparison
292
+ fig2, axes2 = plt.subplots(rows, cols, figsize=(12, 4*rows))
293
+ fig2.suptitle('Bond Force Constant Comparison', fontsize=16, y=0.98)
294
+
295
+ # Flatten axes if needed
296
+ if rows == 1:
297
+ axes2 = [axes2] if cols == 1 else axes2
298
+ else:
299
+ axes2 = axes2.flatten()
300
+
301
+ # Plot force constants in subplots
302
+ for plot_idx in range(num_plots):
303
+ start_idx = plot_idx * bonds_per_plot
304
+ end_idx = min(start_idx + bonds_per_plot, num_bonds)
305
+
306
+ ax = axes2[plot_idx]
307
+ x = np.arange(end_idx - start_idx)
308
+ width = 0.35
309
+
310
+ # Get data for this subplot
311
+ k1_subset = k1_values[start_idx:end_idx]
312
+ k2_subset = k2_values[start_idx:end_idx]
313
+ bond_labels_subset = bond_labels[start_idx:end_idx]
314
+
315
+ # Create bars
316
+ ax.bar(x - width/2, k1_subset, width, label=labels[0], alpha=0.7, color='blue')
317
+ ax.bar(x + width/2, k2_subset, width, label=labels[1], alpha=0.7, color='red')
318
+
319
+ ax.set_xlabel('Bonds')
320
+ ax.set_ylabel('Force Constant (kJ/mol/nm²)')
321
+ ax.set_title(f'Bonds {start_idx+1}-{end_idx}')
322
+ ax.set_xticks(x)
323
+ # Use actual atom labels for bonds
324
+ ax.set_xticklabels(bond_labels_subset, rotation=45, ha='right', fontsize=8)
325
+ ax.legend(fontsize=8)
326
+ ax.grid(True, alpha=0.3)
327
+
328
+ # Hide unused subplots
329
+ for i in range(num_plots, len(axes2)):
330
+ axes2[i].set_visible(False)
331
+
332
+ plt.tight_layout()
333
+ plt.subplots_adjust(top=0.95) # Adjust for suptitle
334
+ plt.savefig(f'{dir}/bond_force_subplots.png', dpi=300, bbox_inches='tight')
335
+ plt.close()
336
+
337
+
338
+ # Compare angles
339
+ # Create angle labels and parameters
340
+ angle_labels = []
341
+ theta1_values = []
342
+ theta2_values = []
343
+ theta3_values = []
344
+ k1_values = []
345
+ k2_values = []
346
+
347
+ for angle1 in qm_interactions1['angles']:
348
+ if angle1['involves_mm']:
349
+ continue
350
+ atoms = tuple(sorted(angle1['atoms']))
351
+ # Find matching angle in topology 2
352
+ for angle2 in qm_interactions2['angles']:
353
+ if angle2['involves_mm']:
354
+ continue
355
+ if tuple(sorted(angle2['atoms'])) == atoms:
356
+ angle_labels.append(get_interaction_label(atoms, qm_atoms1))
357
+ # Convert atomic units to GROMACS units
358
+ theta1_values.append(np.rad2deg(angle1['parameters'][0])) # theta in degrees (already converted)
359
+ theta2_values.append(np.rad2deg(angle2['parameters'][0])) # theta in degrees (already converted)
360
+ k1_values.append(angle1['parameters'][1] * au_kjm) # Convert from au to kJ/mol/rad^2
361
+ k2_values.append(angle2['parameters'][1] * au_kjm) # Convert from au to kJ/mol/rad^2
362
+
363
+ # Add third topology angle value if available
364
+ if qm_interactions3 is not None:
365
+ theta3_found = False
366
+ for angle3 in qm_interactions3['angles']:
367
+ if tuple(sorted(angle3['atoms'])) == atoms:
368
+ theta3_values.append(np.rad2deg(angle3['parameters'][0])) # theta in degrees
369
+ theta3_found = True
370
+ break
371
+ if not theta3_found:
372
+ theta3_values.append(None) # No matching angle found
373
+ else:
374
+ theta3_values.append(None)
375
+ break
376
+
377
+ # Plot angle parameters with summary statistics for large numbers
378
+ num_angles = len(angle_labels)
379
+ if num_angles > 0:
380
+
381
+ # For large numbers, use subplot-based bar plots for angles
382
+ # Calculate subplot layout - stack vertically
383
+ angles_per_plot = 20 # Number of angles per subplot
384
+ num_plots = (num_angles + angles_per_plot - 1) // angles_per_plot # Ceiling division
385
+
386
+ # Stack plots vertically (1 column, multiple rows)
387
+ cols = 1 # Single column
388
+ rows = num_plots
389
+
390
+ # Create figure for angle value comparison
391
+ fig1, axes1 = plt.subplots(rows, cols, figsize=(12, 4*rows))
392
+ fig1.suptitle('Angle Value Comparison', fontsize=16, y=0.98)
393
+
394
+ # Flatten axes if needed
395
+ if rows == 1:
396
+ axes1 = [axes1] if cols == 1 else axes1
397
+ else:
398
+ axes1 = axes1.flatten()
399
+
400
+ # Plot angle values in subplots
401
+ for plot_idx in range(num_plots):
402
+ start_idx = plot_idx * angles_per_plot
403
+ end_idx = min(start_idx + angles_per_plot, num_angles)
404
+
405
+ ax = axes1[plot_idx]
406
+ x = np.arange(end_idx - start_idx)
407
+ width = 0.35
408
+
409
+ # Get data for this subplot
410
+ theta1_subset = theta1_values[start_idx:end_idx]
411
+ theta2_subset = theta2_values[start_idx:end_idx]
412
+ angle_labels_subset = angle_labels[start_idx:end_idx]
413
+
414
+ # Create bars
415
+ bar_width = 0.25 if qm_interactions3 is not None else 0.35
416
+ ax.bar(x - bar_width, theta1_subset, bar_width, label=labels[0], alpha=0.7, color='blue')
417
+ ax.bar(x, theta2_subset, bar_width, label=labels[1], alpha=0.7, color='red')
418
+
419
+ if qm_interactions3 is not None:
420
+ # Filter out None values for third topology
421
+ theta3_subset = [theta3_values[i] for i in range(start_idx, end_idx) if theta3_values[i] is not None]
422
+ if len(theta3_subset) > 0:
423
+ # Only plot if we have valid third topology data
424
+ valid_indices = [i for i in range(start_idx, end_idx) if theta3_values[i] is not None]
425
+ valid_x = [x[i - start_idx] for i in valid_indices]
426
+ ax.bar(valid_x + bar_width, theta3_subset, bar_width, label=labels[2], alpha=0.7, color='green')
427
+
428
+ ax.set_xlabel('Angles')
429
+ ax.set_ylabel('Angle (degrees)')
430
+ ax.set_title(f'Angles {start_idx+1}-{end_idx}')
431
+ ax.set_xticks(x)
432
+ # Use actual atom labels for angles
433
+ ax.set_xticklabels(angle_labels_subset, rotation=45, ha='right', fontsize=8)
434
+ ax.legend(fontsize=8)
435
+ ax.grid(True, alpha=0.3)
436
+
437
+ # Hide unused subplots
438
+ for i in range(num_plots, len(axes1)):
439
+ axes1[i].set_visible(False)
440
+
441
+ plt.tight_layout()
442
+ plt.subplots_adjust(top=0.95) # Adjust for suptitle
443
+ plt.savefig(f'{dir}/angle_value_subplots.png', dpi=300, bbox_inches='tight')
444
+ plt.close()
445
+
446
+ # Create figure for angle force constant comparison
447
+ fig2, axes2 = plt.subplots(rows, cols, figsize=(12, 4*rows))
448
+ fig2.suptitle('Angle Force Constant Comparison', fontsize=16, y=0.98)
449
+
450
+ # Flatten axes if needed
451
+ if rows == 1:
452
+ axes2 = [axes2] if cols == 1 else axes2
453
+ else:
454
+ axes2 = axes2.flatten()
455
+
456
+ # Plot force constants in subplots
457
+ for plot_idx in range(num_plots):
458
+ start_idx = plot_idx * angles_per_plot
459
+ end_idx = min(start_idx + angles_per_plot, num_angles)
460
+
461
+ ax = axes2[plot_idx]
462
+ x = np.arange(end_idx - start_idx)
463
+ width = 0.35
464
+
465
+ # Get data for this subplot
466
+ k1_subset = k1_values[start_idx:end_idx]
467
+ k2_subset = k2_values[start_idx:end_idx]
468
+ angle_labels_subset = angle_labels[start_idx:end_idx]
469
+
470
+ # Create bars
471
+ ax.bar(x - width/2, k1_subset, width, label=labels[0], alpha=0.7, color='blue')
472
+ ax.bar(x + width/2, k2_subset, width, label=labels[1], alpha=0.7, color='red')
473
+
474
+ ax.set_xlabel('Angles')
475
+ ax.set_ylabel('Force Constant (kJ/mol/rad²)')
476
+ ax.set_title(f'Angles {start_idx+1}-{end_idx}')
477
+ ax.set_xticks(x)
478
+ # Use actual atom labels for angles
479
+ ax.set_xticklabels(angle_labels_subset, rotation=45, ha='right', fontsize=8)
480
+ ax.legend(fontsize=8)
481
+ ax.grid(True, alpha=0.3)
482
+
483
+ # Hide unused subplots
484
+ for i in range(num_plots, len(axes2)):
485
+ axes2[i].set_visible(False)
486
+
487
+ plt.tight_layout()
488
+ plt.subplots_adjust(top=0.95) # Adjust for suptitle
489
+ plt.savefig(f'{dir}/angle_force_subplots.png', dpi=300, bbox_inches='tight')
490
+ plt.close()
491
+
492
+ # Compare dihedrals
493
+ # Create separate figures for each dihedral function type
494
+ dihedral_labels = []
495
+ dihedral_params1 = []
496
+ dihedral_params2 = []
497
+ dihedral_funcs = []
498
+
499
+ for dihedral1 in qm_interactions1['dihedrals']:
500
+ if dihedral1['involves_mm']:
501
+ continue
502
+ atoms = tuple(dihedral1['atoms'])
503
+ for dihedral2 in qm_interactions2['dihedrals']:
504
+ if dihedral2['involves_mm']:
505
+ continue
506
+ if tuple(dihedral2['atoms']) == atoms:
507
+
508
+ dihedral_labels.append(get_interaction_label(atoms, qm_atoms1))
509
+ dihedral_funcs.append(dihedral1['function'])
510
+
511
+ # Store parameters based on function type with unit conversion
512
+ if dihedral1['function'] in [1, 4, 9]: # Format 1
513
+ if dihedral1['parameters'][2] == dihedral2['parameters'][2]:
514
+ params1 = {
515
+ 'phi0': np.rad2deg(dihedral1['parameters'][0]), # phi0 in degrees
516
+ 'k': dihedral1['parameters'][1] * au_kjm, # Convert from au to kJ/mol
517
+ 'n': dihedral1['parameters'][2] # multiplicity
518
+ }
519
+ params2 = {
520
+ 'phi0': np.rad2deg(dihedral2['parameters'][0]),
521
+ 'k': dihedral2['parameters'][1] * au_kjm,
522
+ 'n': dihedral2['parameters'][2]
523
+ }
524
+ else:
525
+ dihedral_labels.pop()
526
+ dihedral_funcs.pop()
527
+ break
528
+
529
+ elif dihedral1['function'] == 2: # Format 2
530
+ params1 = {
531
+ 'xi': dihedral1['parameters'][0], # xi (dimensionless)
532
+ 'k': dihedral1['parameters'][1] * au_kjm # Convert from au to kJ/mol
533
+ }
534
+ params2 = {
535
+ 'xi': dihedral2['parameters'][0],
536
+ 'k': dihedral2['parameters'][1] * au_kjm
537
+ }
538
+ elif dihedral1['function'] == 3: # Format 3
539
+ params1 = {
540
+ 'C0': dihedral1['parameters'][0] * au_kjm, # C0-C5 in kJ/mol
541
+ 'C1': dihedral1['parameters'][1] * au_kjm,
542
+ 'C2': dihedral1['parameters'][2] * au_kjm,
543
+ 'C3': dihedral1['parameters'][3] * au_kjm,
544
+ 'C4': dihedral1['parameters'][4] * au_kjm,
545
+ 'C5': dihedral1['parameters'][5] * au_kjm
546
+ }
547
+ params2 = {
548
+ 'C0': dihedral2['parameters'][0] * au_kjm,
549
+ 'C1': dihedral2['parameters'][1] * au_kjm,
550
+ 'C2': dihedral2['parameters'][2] * au_kjm,
551
+ 'C3': dihedral2['parameters'][3] * au_kjm,
552
+ 'C4': dihedral2['parameters'][4] * au_kjm,
553
+ 'C5': dihedral2['parameters'][5] * au_kjm
554
+ }
555
+
556
+ dihedral_params1.append(params1)
557
+ dihedral_params2.append(params2)
558
+ break
559
+
560
+ # Plot dihedral parameters based on function type
561
+ num_dihedrals = len(dihedral_labels)
562
+ if num_dihedrals > 0:
563
+ # Plot dihedral parameters based on function type
564
+ for func in set(dihedral_funcs):
565
+ func_dihedrals = [(i, label, p1, p2) for i, (label, f, p1, p2) in enumerate(zip(dihedral_labels, dihedral_funcs, dihedral_params1, dihedral_params2)) if f == func]
566
+
567
+
568
+ # For large numbers, use subplot-based bar plots for dihedrals
569
+ # Calculate subplot layout - stack vertically
570
+ dihedrals_per_plot = 20 # Number of dihedrals per subplot
571
+ num_plots = (len(func_dihedrals) + dihedrals_per_plot - 1) // dihedrals_per_plot # Ceiling division
572
+
573
+ # Stack plots vertically (1 column, multiple rows)
574
+ cols = 1 # Single column
575
+ rows = num_plots
576
+
577
+ if func in [1, 4, 9]: # Format 1
578
+ # Create figure for phase angle comparison
579
+ fig1, axes1 = plt.subplots(rows, cols, figsize=(12, 4*rows))
580
+ fig1.suptitle(f'Dihedral Phase Angle Comparison (Function {func})', fontsize=16, y=0.98)
581
+
582
+ # Flatten axes if needed
583
+ if rows == 1:
584
+ axes1 = [axes1] if cols == 1 else axes1
585
+ else:
586
+ axes1 = axes1.flatten()
587
+
588
+ # Plot phase angles in subplots
589
+ for plot_idx in range(num_plots):
590
+ start_idx = plot_idx * dihedrals_per_plot
591
+ end_idx = min(start_idx + dihedrals_per_plot, len(func_dihedrals))
592
+
593
+ ax = axes1[plot_idx]
594
+ x = np.arange(end_idx - start_idx)
595
+ width = 0.35
596
+
597
+ # Get data for this subplot
598
+ phi0_1_subset = [p1['phi0'] for _, _, p1, _ in func_dihedrals[start_idx:end_idx]]
599
+ phi0_2_subset = [p2['phi0'] for _, _, _, p2 in func_dihedrals[start_idx:end_idx]]
600
+ dihedral_labels_subset = [label for _, label, _, _ in func_dihedrals[start_idx:end_idx]]
601
+
602
+ # Create bars
603
+ ax.bar(x - width/2, phi0_1_subset, width, label=labels[0], alpha=0.7, color='blue')
604
+ ax.bar(x + width/2, phi0_2_subset, width, label=labels[1], alpha=0.7, color='red')
605
+
606
+ ax.set_xlabel('Dihedrals')
607
+ ax.set_ylabel('Phase Angle (degrees)')
608
+ ax.set_title(f'Dihedrals {start_idx+1}-{end_idx}')
609
+ ax.set_xticks(x)
610
+ # Use actual atom labels for dihedrals
611
+ ax.set_xticklabels(dihedral_labels_subset, rotation=45, ha='right', fontsize=8)
612
+ ax.legend(fontsize=8)
613
+ ax.grid(True, alpha=0.3)
614
+
615
+ # Hide unused subplots
616
+ for i in range(num_plots, len(axes1)):
617
+ axes1[i].set_visible(False)
618
+
619
+ plt.tight_layout()
620
+ plt.subplots_adjust(top=0.95) # Adjust for suptitle
621
+ plt.savefig(f'{dir}/dihedral_phase_subplots_func{func}.png', dpi=300, bbox_inches='tight')
622
+ plt.close()
623
+
624
+ # Create figure for force constant comparison
625
+ fig2, axes2 = plt.subplots(rows, cols, figsize=(12, 4*rows))
626
+ fig2.suptitle(f'Dihedral Force Constant Comparison (Function {func})', fontsize=16, y=0.98)
627
+
628
+ # Flatten axes if needed
629
+ if rows == 1:
630
+ axes2 = [axes2] if cols == 1 else axes2
631
+ else:
632
+ axes2 = axes2.flatten()
633
+
634
+ # Plot force constants in subplots
635
+ for plot_idx in range(num_plots):
636
+ start_idx = plot_idx * dihedrals_per_plot
637
+ end_idx = min(start_idx + dihedrals_per_plot, len(func_dihedrals))
638
+
639
+ ax = axes2[plot_idx]
640
+ x = np.arange(end_idx - start_idx)
641
+ width = 0.35
642
+
643
+ # Get data for this subplot
644
+ k_1_subset = [p1['k'] for _, _, p1, _ in func_dihedrals[start_idx:end_idx]]
645
+ k_2_subset = [p2['k'] for _, _, _, p2 in func_dihedrals[start_idx:end_idx]]
646
+ dihedral_labels_subset = [label for _, label, _, _ in func_dihedrals[start_idx:end_idx]]
647
+
648
+ # Create bars
649
+ ax.bar(x - width/2, k_1_subset, width, label=labels[0], alpha=0.7, color='blue')
650
+ ax.bar(x + width/2, k_2_subset, width, label=labels[1], alpha=0.7, color='red')
651
+
652
+ ax.set_xlabel('Dihedrals')
653
+ ax.set_ylabel('Force Constant (kJ/mol)')
654
+ ax.set_title(f'Dihedrals {start_idx+1}-{end_idx}')
655
+ ax.set_xticks(x)
656
+ # Use actual atom labels for dihedrals
657
+ ax.set_xticklabels(dihedral_labels_subset, rotation=45, ha='right', fontsize=8)
658
+ ax.legend(fontsize=8)
659
+ ax.grid(True, alpha=0.3)
660
+
661
+ # Hide unused subplots
662
+ for i in range(num_plots, len(axes2)):
663
+ axes2[i].set_visible(False)
664
+
665
+ plt.tight_layout()
666
+ plt.subplots_adjust(top=0.95) # Adjust for suptitle
667
+ plt.savefig(f'{dir}/dihedral_force_subplots_func{func}.png', dpi=300, bbox_inches='tight')
668
+ plt.close()
669
+
670
+ elif func == 2: # Format 2
671
+ # Create figure for xi comparison
672
+ fig1, axes1 = plt.subplots(rows, cols, figsize=(12, 4*rows))
673
+ fig1.suptitle('Improper Dihedral Xi Comparison', fontsize=16, y=0.98)
674
+
675
+ # Flatten axes if needed
676
+ if rows == 1:
677
+ axes1 = [axes1] if cols == 1 else axes1
678
+ else:
679
+ axes1 = axes1.flatten()
680
+
681
+ # Plot xi values in subplots
682
+ for plot_idx in range(num_plots):
683
+ start_idx = plot_idx * dihedrals_per_plot
684
+ end_idx = min(start_idx + dihedrals_per_plot, len(func_dihedrals))
685
+
686
+ ax = axes1[plot_idx]
687
+ x = np.arange(end_idx - start_idx)
688
+ width = 0.35
689
+
690
+ # Get data for this subplot
691
+ xi_1_subset = [p1['xi'] for _, _, p1, _ in func_dihedrals[start_idx:end_idx]]
692
+ xi_2_subset = [p2['xi'] for _, _, _, p2 in func_dihedrals[start_idx:end_idx]]
693
+ dihedral_labels_subset = [label for _, label, _, _ in func_dihedrals[start_idx:end_idx]]
694
+
695
+ # Create bars
696
+ ax.bar(x - width/2, xi_1_subset, width, label=labels[0], alpha=0.7, color='blue')
697
+ ax.bar(x + width/2, xi_2_subset, width, label=labels[1], alpha=0.7, color='red')
698
+
699
+ ax.set_xlabel('Dihedrals')
700
+ ax.set_ylabel('Xi')
701
+ ax.set_title(f'Dihedrals {start_idx+1}-{end_idx}')
702
+ ax.set_xticks(x)
703
+ # Use actual atom labels for dihedrals
704
+ ax.set_xticklabels(dihedral_labels_subset, rotation=45, ha='right', fontsize=8)
705
+ ax.legend(fontsize=8)
706
+ ax.grid(True, alpha=0.3)
707
+
708
+ # Hide unused subplots
709
+ for i in range(num_plots, len(axes1)):
710
+ axes1[i].set_visible(False)
711
+
712
+ plt.tight_layout()
713
+ plt.subplots_adjust(top=0.95) # Adjust for suptitle
714
+ plt.savefig(f'{dir}/dihedral_xi_subplots.png', dpi=300, bbox_inches='tight')
715
+ plt.close()
716
+
717
+ # Create figure for force constant comparison
718
+ fig2, axes2 = plt.subplots(rows, cols, figsize=(12, 4*rows))
719
+ fig2.suptitle('Improper Dihedral Force Constant Comparison', fontsize=16, y=0.98)
720
+
721
+ # Flatten axes if needed
722
+ if rows == 1:
723
+ axes2 = [axes2] if cols == 1 else axes2
724
+ else:
725
+ axes2 = axes2.flatten()
726
+
727
+ # Plot force constants in subplots
728
+ for plot_idx in range(num_plots):
729
+ start_idx = plot_idx * dihedrals_per_plot
730
+ end_idx = min(start_idx + dihedrals_per_plot, len(func_dihedrals))
731
+
732
+ ax = axes2[plot_idx]
733
+ x = np.arange(end_idx - start_idx)
734
+ width = 0.35
735
+
736
+ # Get data for this subplot
737
+ k_1_subset = [p1['k'] for _, _, p1, _ in func_dihedrals[start_idx:end_idx]]
738
+ k_2_subset = [p2['k'] for _, _, _, p2 in func_dihedrals[start_idx:end_idx]]
739
+ dihedral_labels_subset = [label for _, label, _, _ in func_dihedrals[start_idx:end_idx]]
740
+
741
+ # Create bars
742
+ ax.bar(x - width/2, k_1_subset, width, label=labels[0], alpha=0.7, color='blue')
743
+ ax.bar(x + width/2, k_2_subset, width, label=labels[1], alpha=0.7, color='red')
744
+
745
+ ax.set_xlabel('Dihedrals')
746
+ ax.set_ylabel('Force Constant (kJ/mol)')
747
+ ax.set_title(f'Dihedrals {start_idx+1}-{end_idx}')
748
+ ax.set_xticks(x)
749
+ # Use actual atom labels for dihedrals
750
+ ax.set_xticklabels(dihedral_labels_subset, rotation=45, ha='right', fontsize=8)
751
+ ax.legend(fontsize=8)
752
+ ax.grid(True, alpha=0.3)
753
+
754
+ # Hide unused subplots
755
+ for i in range(num_plots, len(axes2)):
756
+ axes2[i].set_visible(False)
757
+
758
+ plt.tight_layout()
759
+ plt.subplots_adjust(top=0.95) # Adjust for suptitle
760
+ plt.savefig(f'{dir}/dihedral_force_improper_subplots.png', dpi=300, bbox_inches='tight')
761
+ plt.close()
762
+
763
+ elif func == 3: # Format 3
764
+ # Create subplots for each coefficient
765
+ for i in range(6):
766
+ fig, axes = plt.subplots(rows, cols, figsize=(12, 4*rows))
767
+ fig.suptitle(f'Ryckaert-Bellemans Coefficient C{i} Comparison', fontsize=16, y=0.98)
768
+
769
+ # Flatten axes if needed
770
+ if rows == 1:
771
+ axes = [axes] if cols == 1 else axes
772
+ else:
773
+ axes = axes.flatten()
774
+
775
+ # Plot coefficient in subplots
776
+ for plot_idx in range(num_plots):
777
+ start_idx = plot_idx * dihedrals_per_plot
778
+ end_idx = min(start_idx + dihedrals_per_plot, len(func_dihedrals))
779
+
780
+ ax = axes[plot_idx]
781
+ x = np.arange(end_idx - start_idx)
782
+ width = 0.35
783
+
784
+ # Get data for this subplot
785
+ c_1_subset = [p1[f'C{i}'] for _, _, p1, _ in func_dihedrals[start_idx:end_idx]]
786
+ c_2_subset = [p2[f'C{i}'] for _, _, _, p2 in func_dihedrals[start_idx:end_idx]]
787
+ dihedral_labels_subset = [label for _, label, _, _ in func_dihedrals[start_idx:end_idx]]
788
+
789
+ # Create bars
790
+ ax.bar(x - width/2, c_1_subset, width, label=labels[0], alpha=0.7, color='blue')
791
+ ax.bar(x + width/2, c_2_subset, width, label=labels[1], alpha=0.7, color='red')
792
+
793
+ ax.set_xlabel('Dihedrals')
794
+ ax.set_ylabel(f'Coefficient C{i} (kJ/mol)')
795
+ ax.set_title(f'Dihedrals {start_idx+1}-{end_idx}')
796
+ ax.set_xticks(x)
797
+ # Use actual atom labels for dihedrals
798
+ ax.set_xticklabels(dihedral_labels_subset, rotation=45, ha='right', fontsize=8)
799
+ ax.legend(fontsize=8)
800
+ ax.grid(True, alpha=0.3)
801
+
802
+ # Hide unused subplots
803
+ for j in range(num_plots, len(axes)):
804
+ axes[j].set_visible(False)
805
+
806
+ plt.tight_layout()
807
+ plt.subplots_adjust(top=0.95) # Adjust for suptitle
808
+ plt.savefig(f'{dir}/dihedral_c{i}_subplots.png', dpi=300, bbox_inches='tight')
809
+ plt.close()