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.
- mimicpy/__init__.py +1 -1
- mimicpy/__main__.py +726 -2
- mimicpy/_authors.py +2 -2
- mimicpy/_version.py +2 -2
- mimicpy/coords/__init__.py +1 -1
- mimicpy/coords/base.py +1 -1
- mimicpy/coords/cpmdgeo.py +1 -1
- mimicpy/coords/gro.py +1 -1
- mimicpy/coords/pdb.py +1 -1
- mimicpy/core/__init__.py +1 -1
- mimicpy/core/prepare.py +3 -3
- mimicpy/core/selector.py +1 -1
- mimicpy/force_matching/__init__.py +34 -0
- mimicpy/force_matching/bonded_forces.py +628 -0
- mimicpy/force_matching/compare_top.py +809 -0
- mimicpy/force_matching/dresp.py +435 -0
- mimicpy/force_matching/nonbonded_forces.py +32 -0
- mimicpy/force_matching/opt_ff.py +2114 -0
- mimicpy/force_matching/qm_region.py +1960 -0
- mimicpy/plugins/__main_installer__.py +76 -0
- mimicpy/{__main_vmd__.py → plugins/__main_vmd__.py} +2 -2
- mimicpy/plugins/pymol.py +56 -0
- mimicpy/plugins/vmd.tcl +78 -0
- mimicpy/scripts/__init__.py +1 -1
- mimicpy/scripts/cpmd.py +1 -1
- mimicpy/scripts/fm_input.py +265 -0
- mimicpy/scripts/fmdata.py +120 -0
- mimicpy/scripts/mdp.py +1 -1
- mimicpy/scripts/ndx.py +1 -1
- mimicpy/scripts/script.py +1 -1
- mimicpy/topology/__init__.py +1 -1
- mimicpy/topology/itp.py +603 -35
- mimicpy/topology/mpt.py +1 -1
- mimicpy/topology/top.py +254 -15
- mimicpy/topology/topol_dict.py +233 -4
- mimicpy/utils/__init__.py +1 -1
- mimicpy/utils/atomic_numbers.py +1 -1
- mimicpy/utils/constants.py +17 -3
- mimicpy/utils/elements.py +1 -1
- mimicpy/utils/errors.py +1 -1
- mimicpy/utils/file_handler.py +1 -1
- mimicpy/utils/strings.py +1 -1
- mimicpy-0.3.0.dist-info/METADATA +156 -0
- mimicpy-0.3.0.dist-info/RECORD +50 -0
- {mimicpy-0.2.0.dist-info → mimicpy-0.3.0.dist-info}/WHEEL +1 -1
- mimicpy-0.3.0.dist-info/entry_points.txt +4 -0
- mimicpy-0.2.0.dist-info/METADATA +0 -86
- mimicpy-0.2.0.dist-info/RECORD +0 -38
- mimicpy-0.2.0.dist-info/entry_points.txt +0 -3
- {mimicpy-0.2.0.dist-info → mimicpy-0.3.0.dist-info/licenses}/COPYING +0 -0
- {mimicpy-0.2.0.dist-info → mimicpy-0.3.0.dist-info/licenses}/COPYING.LESSER +0 -0
- {mimicpy-0.2.0.dist-info → mimicpy-0.3.0.dist-info}/top_level.txt +0 -0
- {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()
|