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,628 @@
1
+ import numpy as np
2
+
3
+
4
+ def compute_bond_force(xi, xj, bond_length, force_constant,
5
+ flag='force', derivative=None):
6
+ """
7
+ Computes forces or derivatives related to a bond.
8
+ For harmonic bonds: V(r) = 1/2 * k * (r - r_0)^2
9
+
10
+ Args:
11
+ xi, xj: Coordinates of the two atoms
12
+ bond_length: Equilibrium bond length (r_0)
13
+ force_constant: Force constant (k)
14
+ flag: 'force' or 'derivative'
15
+ derivative: 'force_constant' or 'bond_length'
16
+
17
+ Returns:
18
+ tuple: (fi, fj) Computed forces or derivatives
19
+ """
20
+ # Compute distance vector and length
21
+ R_ij = xj - xi
22
+ r_ij = get_magnitude(R_ij)
23
+
24
+ # Check for zero length vector
25
+ if r_ij == 0:
26
+ raise ValueError("Zero length bond vector encountered")
27
+
28
+ # Compute unit vector
29
+ R_ij_unit = R_ij / r_ij
30
+
31
+ # Compute force derivatives with respect to atomic positions
32
+ # These are the derivatives of r with respect to atomic positions
33
+ dr_dri = -R_ij_unit # Derivative of r with respect to ri
34
+ dr_drj = R_ij_unit # Derivative of r with respect to rj
35
+
36
+ if flag == 'force':
37
+ # Force is negative gradient of potential: F = -k * (r - r_0) * dr/dr
38
+ F_i = -force_constant * (r_ij - bond_length) * dr_dri
39
+ F_j = -force_constant * (r_ij - bond_length) * dr_drj
40
+ return F_i, F_j
41
+
42
+ elif flag == 'derivative':
43
+ if derivative == 'force_constant':
44
+ # Derivative of force with respect to force constant
45
+ # dF/dk = -(r - r_0) * dr/dr
46
+ dk_i = -(r_ij - bond_length) * dr_dri
47
+ dk_j = -(r_ij - bond_length) * dr_drj
48
+ return dk_i, dk_j
49
+
50
+ elif derivative == 'bond_length':
51
+ # Derivative of force with respect to equilibrium bond length
52
+ # dF/dr_0 = k * dr/dr
53
+ dr0_i = force_constant * dr_dri
54
+ dr0_j = force_constant * dr_drj
55
+ return dr0_i, dr0_j
56
+
57
+ def compute_angle_force(xi, xj, xk, angle, force_constant, flag='force', derivative=None):
58
+ """
59
+ Computes forces or derivatives related to an angle.
60
+ For harmonic angles: V(theta) = 1/2 * k * (theta - theta_0)^2
61
+
62
+ Args:
63
+ xi, xj, xk: Coordinates of the three atoms
64
+ angle: Equilibrium angle (theta_0)
65
+ force_constant: Force constant (k)
66
+ flag: 'force' or 'derivative'
67
+ derivative: 'force_constant' or 'angle'
68
+
69
+ Returns:
70
+ tuple: (fi, fj, fk) Computed forces or derivatives
71
+ """
72
+ # Compute distance vectors and distances
73
+ R_ij = xj - xi
74
+ R_kj = xj - xk
75
+ r_ij = np.linalg.norm(R_ij)
76
+ r_kj = np.linalg.norm(R_kj)
77
+
78
+ # Check for zero length vectors
79
+ if r_ij == 0 or r_kj == 0:
80
+ raise ValueError("Zero length vector encountered in angle force computation")
81
+
82
+ # Compute angle
83
+ dot_r_ij_kj = np.dot(R_ij, R_kj)
84
+ prod_r_ij_kj = r_ij * r_kj
85
+ cos_theta = np.clip(dot_r_ij_kj/prod_r_ij_kj, -1.0, 1.0)
86
+ theta = np.arccos(cos_theta)
87
+
88
+ # Compute sin(theta) safely
89
+ sin_theta = np.sqrt(1.0 - cos_theta**2)
90
+ if sin_theta < 1e-10: # Near linear angles
91
+ raise ValueError("Near linear angle encountered")
92
+
93
+ # Compute force derivatives with respect to atomic positions
94
+ # These are the derivatives of theta with respect to atomic positions
95
+ f_i = 1/(r_ij * sin_theta) * (R_kj/r_kj - cos_theta * R_ij/r_ij)
96
+ f_k = 1/(r_kj * sin_theta) * (R_ij/r_ij - cos_theta * R_kj/r_kj)
97
+ f_j = -f_i - f_k # Force on central atom
98
+
99
+ if flag == 'force':
100
+ # Force is negative gradient of potential: F = -k * (theta - theta_0) * dtheta/dr
101
+ F_i = -force_constant * (theta - angle) * f_i
102
+ F_k = -force_constant * (theta - angle) * f_k
103
+ F_j = -force_constant * (theta - angle) * f_j
104
+ return F_i, F_j, F_k
105
+
106
+ elif flag == 'derivative':
107
+ if derivative == 'force_constant':
108
+ # Derivative of force with respect to force constant
109
+ # dF/dk = -(theta - theta_0) * dtheta/dr
110
+ dk_i = -(theta - angle) * f_i
111
+ dk_k = -(theta - angle) * f_k
112
+ dk_j = -(theta - angle) * f_j
113
+ return dk_i, dk_j, dk_k
114
+
115
+ elif derivative == 'angle':
116
+ # Derivative of force with respect to equilibrium angle
117
+ # dF/dtheta_0 = k * dtheta/dr
118
+ dtheta_i = force_constant * f_i
119
+ dtheta_k = force_constant * f_k
120
+ dtheta_j = force_constant * f_j
121
+ return dtheta_i, dtheta_j, dtheta_k
122
+
123
+
124
+ def get_magnitude(vector):
125
+ return np.linalg.norm(vector)
126
+
127
+ def distance_vector(R1, R2):
128
+ r = R1 - R2
129
+ d = get_magnitude(r)
130
+ return d, r
131
+
132
+ def compute_RB_dihedral_force(xi, xj, xk, xl,
133
+ force_constant, flag='force', derivative=None):
134
+ """
135
+ Computes forces or derivatives related to a Ryckaert-Bellemans (RB) dihedral angle.
136
+
137
+ The RB potential is defined as:
138
+ V(φ) = C0 + C1*cos(φ-180°) + C2*cos²(φ-180°) + C3*cos³(φ-180°) + C4*cos⁴(φ-180°) + C5*cos⁵(φ-180°)
139
+
140
+ where φ is the dihedral angle and C0-C5 are the force constants.
141
+
142
+ Args:
143
+ xi, xj, xk, xl: Coordinates of the four atoms defining the dihedral angle
144
+ force_constant: Array of force constants [C0, C1, C2, C3, C4, C5]
145
+ flag: 'force' or 'derivative'
146
+ derivative: 'C0', 'C1', 'C2', 'C3', 'C4', or 'C5' for parameter derivatives
147
+
148
+ Returns:
149
+ tuple: (fi, fj, fk, fl) Computed forces or derivatives for each atom
150
+ """
151
+
152
+ # Compute distance vectors and distances
153
+ R_ij = xj - xi
154
+ R_kj = xj - xk
155
+ R_kl = xl - xk
156
+
157
+ r_kj = np.linalg.norm(R_kj)
158
+
159
+ # Projection of vectors on the plane orthogonal to R_kj
160
+ R_im = R_ij - np.dot(R_ij, R_kj) * R_kj / r_kj**2
161
+ R_ln = -R_kl + np.dot(R_kl, R_kj) * R_kj / r_kj**2
162
+
163
+ # Magnitudes of the projections
164
+ r_im = np.linalg.norm(R_im)
165
+ r_ln = np.linalg.norm(R_ln)
166
+
167
+ # Compute cross product and sign of the dihedral angle
168
+ cosphi = np.dot(R_im, R_ln) / (r_im * r_ln)
169
+ cosphi = np.clip(cosphi, -1.0, 1.0) # Ensure cosphi is in valid range
170
+
171
+ # For RB potential, we need cos(phi-180°) = -cos(phi)
172
+ cosphi_rb = -cosphi
173
+
174
+ # Calculate forces
175
+ di = (R_ln/r_ln - R_im/r_im * cosphi) * 1/r_im
176
+ dl = (R_im/r_im - R_ln/r_ln * cosphi) * 1/r_ln
177
+ dj = (np.dot(R_ij, R_kj) / r_kj**2 - 1.0) * di - (np.dot(R_kl, R_kj) / r_kj**2) * dl
178
+ dk = -di - dj - dl
179
+
180
+ if flag == 'force':
181
+ # RB potential: V(phi) = C0 + C1*cos(phi-180°) + C2*cos²(phi-180°) + C3*cos³(phi-180°) + C4*cos⁴(phi-180°) + C5*cos⁵(phi-180°)
182
+ # Force factor is the negative derivative of V with respect to cos(phi-180°)
183
+ factor = -(force_constant[1] # C1
184
+ + force_constant[2] * 2 * cosphi_rb # C2
185
+ + force_constant[3] * 3 * cosphi_rb**2 # C3
186
+ + force_constant[4] * 4 * cosphi_rb**3 # C4
187
+ + force_constant[5] * 5 * cosphi_rb**4) # C5
188
+
189
+ fi = factor * di
190
+ fj = factor * dj
191
+ fk = factor * dk
192
+ fl = factor * dl
193
+ return fi, fj, fk, fl
194
+
195
+ elif flag=='derivative':
196
+ # For derivatives, we need dF/dC_n where F is the force
197
+ # F = -d/d(cos(phi-180°)) of V * di
198
+ # So dF/dC_n = -d/d(cos(phi-180°)) of (C_n * cos^n(phi-180°)) * di
199
+ if derivative == 'C0':
200
+ factor = 0.0 # C0 doesn't contribute to forces
201
+ elif derivative == 'C1':
202
+ factor = -1.0 # -d/d(cos(phi-180°)) of C1*cos(phi-180°)
203
+ elif derivative == 'C2':
204
+ factor = -2 * cosphi_rb # -d/d(cos(phi-180°)) of C2*cos²(phi-180°)
205
+ elif derivative == 'C3':
206
+ factor = -3 * cosphi_rb**2 # -d/d(cos(phi-180°)) of C3*cos³(phi-180°)
207
+ elif derivative == 'C4':
208
+ factor = -4 * cosphi_rb**3 # -d/d(cos(phi-180°)) of C4*cos⁴(phi-180°)
209
+ elif derivative == 'C5':
210
+ factor = -5 * cosphi_rb**4 # -d/d(cos(phi-180°)) of C5*cos⁵(phi-180°)
211
+ else:
212
+ raise ValueError(f'Unknown derivative parameter: {derivative}')
213
+
214
+ # The derivatives of the forces with respect to the coefficients
215
+ fi = factor * di
216
+ fj = factor * dj
217
+ fk = factor * dk
218
+ fl = factor * dl
219
+ return fi, fj, fk, fl
220
+
221
+ def compute_dihedral_force(xi, xj, xk, xl, phase, force_constant, m, flag='force', derivative=None):
222
+ """
223
+ Computes forces or derivatives related to a dihedral angle.
224
+ For periodic dihedrals (types 1, 4, 9): V(phi) = k[1 + cos(n*phi - phi_0)]
225
+
226
+ Args:
227
+ xi, xj, xk, xl: Coordinates of the four atoms
228
+ phase: Phase shift (phi_0)
229
+ force_constant: Force constant (k)
230
+ m: Multiplicity (n)
231
+ flag: 'force' or 'derivative'
232
+ derivative: Not used for periodic dihedrals
233
+
234
+ Returns:
235
+ tuple: (fi, fj, fk, fl) Computed forces or derivatives
236
+ """
237
+ # Compute distance vectors and distances
238
+ R_ij = xj - xi
239
+ R_kj = xj - xk
240
+ R_kl = xl - xk
241
+
242
+ r_kj = np.linalg.norm(R_kj)
243
+
244
+ R_mj = np.cross(R_ij, R_kj)
245
+ R_nk = np.cross(R_kj, R_kl)
246
+ r_mj = np.linalg.norm(R_mj)
247
+ r_nk = np.linalg.norm(R_nk)
248
+
249
+ # Projection of vectors on the plane orthogonal to R_kj
250
+ R_im = R_ij - np.dot(R_ij, R_kj) * R_kj / r_kj**2
251
+ R_ln = -R_kl + np.dot(R_kl, R_kj) * R_kj / r_kj**2
252
+
253
+ # Magnitudes of the projections
254
+ r_im = np.linalg.norm(R_im)
255
+ r_ln = np.linalg.norm(R_ln)
256
+
257
+ # Compute cross product and sign of the dihedral angle
258
+ cosphi = np.dot(R_im, R_ln) / (r_im * r_ln)
259
+ cosphi = np.clip(cosphi, -1.0, 1.0) # Ensure cosphi is in valid range
260
+ phi = np.sign(np.dot(R_ij, R_nk))*np.arccos(cosphi)
261
+ sinphi = np.sin(m*phi - phase)
262
+
263
+ # Calculate force derivatives with respect to atomic positions
264
+ di = m * sinphi * r_kj * R_mj / r_mj**2
265
+ dl = - m * sinphi * r_kj * R_nk / r_nk**2
266
+ dj = (np.dot(R_ij, R_kj) / r_kj**2 - 1.0) * di - (np.dot(R_kl, R_kj) / r_kj**2) * dl
267
+ dk = -di - dj - dl
268
+
269
+ if flag == 'force':
270
+ # Force is negative gradient of potential
271
+ F_i = -force_constant * di
272
+ F_l = -force_constant * dl
273
+ F_j = -force_constant * dj
274
+ F_k = -force_constant * dk
275
+ return F_i, F_j, F_k, F_l
276
+
277
+ elif flag=='derivative':
278
+ # For periodic dihedrals, we only need derivative with respect to force constant
279
+ # dF/dk = -di, -dj, -dk, -dl
280
+ return -di, -dj, -dk, -dl
281
+
282
+ def get_jac_indx(atom_idx):
283
+ return 3*atom_idx
284
+
285
+ def compute_bonds_forces(bonds, qm_coordinates,
286
+ ff_optimize,
287
+ bond2params, flag='force'):
288
+
289
+ function_map = {
290
+ 1: compute_bond_force,
291
+ }
292
+ if flag == 'force':
293
+ forces = np.zeros(qm_coordinates.shape)
294
+ for bond in bonds:
295
+ idxs = bond2params.get(bond['index'])
296
+ b = bond['parameters'][0]
297
+ kb = bond['parameters'][1]
298
+ if idxs[0] is not None:
299
+ b = ff_optimize[idxs[0]]
300
+ if idxs[1] is not None:
301
+ kb = ff_optimize[idxs[1]]
302
+
303
+ # Get atom coordinates using global indices
304
+ i_coords = qm_coordinates[bond['atoms'][0]]
305
+ j_coords = qm_coordinates[bond['atoms'][1]]
306
+
307
+ fi, fj = function_map.get(bond['function'])(i_coords, j_coords,
308
+ b, kb,
309
+ flag=flag)
310
+
311
+ # Add forces to the correct atoms
312
+ forces[bond['atoms'][0]] += fi
313
+ forces[bond['atoms'][1]] += fj
314
+
315
+ return forces
316
+ elif flag == 'derivative':
317
+ jac_mat = np.zeros((3*qm_coordinates.shape[0],ff_optimize.shape[0]))
318
+
319
+ for bond in bonds:
320
+ if not bond['optimize']:
321
+ continue
322
+
323
+ idxs = bond2params.get(bond['index'])
324
+ b = bond['parameters'][0]
325
+ kb = bond['parameters'][1]
326
+
327
+ if idxs[0] is not None:
328
+ b = ff_optimize[idxs[0]]
329
+ if idxs[1] is not None:
330
+ kb = ff_optimize[idxs[1]]
331
+
332
+ idx0 = get_jac_indx(bond['atoms'][0])
333
+ idx1 = get_jac_indx(bond['atoms'][1])
334
+
335
+ if idxs[0] is not None:
336
+ bi, bj = function_map.get(bond['function'])(qm_coordinates[bond['atoms'][0]],
337
+ qm_coordinates[bond['atoms'][1]],
338
+ b, kb,
339
+ flag=flag, derivative='bond_length')
340
+
341
+ jac_mat[idx0:idx0+3, idxs[0]] += bi
342
+ jac_mat[idx1:idx1+3, idxs[0]] += bj
343
+
344
+ if idxs[1] is not None:
345
+ ki, kj = function_map.get(bond['function'])(qm_coordinates[bond['atoms'][0]],
346
+ qm_coordinates[bond['atoms'][1]],
347
+ b, kb,
348
+ flag=flag, derivative='force_constant')
349
+
350
+ jac_mat[idx0:idx0+3, idxs[1]] += ki
351
+ jac_mat[idx1:idx1+3, idxs[1]] += kj
352
+
353
+ return jac_mat
354
+
355
+
356
+
357
+ def compute_angles_forces(angles, qm_coordinates,
358
+ ff_optimize,
359
+ bond2params, flag='force'):
360
+
361
+ function_map = {
362
+ 1: compute_angle_force,
363
+ }
364
+ if flag == 'force':
365
+
366
+ forces = np.zeros(qm_coordinates.shape)
367
+
368
+ for angle in angles:
369
+ idxs = bond2params.get(angle['index'])
370
+ theta = angle['parameters'][0]
371
+ cth = angle['parameters'][1]
372
+ if idxs[0] is not None:
373
+ theta = ff_optimize[idxs[0]]
374
+ if idxs[1] is not None:
375
+ cth = ff_optimize[idxs[1]]
376
+ fi, fj, fk = function_map.get(angle['function'])(qm_coordinates[angle['atoms'][0]],
377
+ qm_coordinates[angle['atoms'][1]],
378
+ qm_coordinates[angle['atoms'][2]],
379
+ theta, cth,
380
+ flag=flag)
381
+
382
+ forces[angle['atoms'][0]] += fi
383
+ forces[angle['atoms'][1]] += fj
384
+ forces[angle['atoms'][2]] += fk
385
+
386
+ return forces
387
+
388
+ elif flag == 'derivative':
389
+ jac_mat = np.zeros((3*qm_coordinates.shape[0],ff_optimize.shape[0]))
390
+
391
+ for angle in angles:
392
+
393
+ if not angle['optimize']:
394
+ continue
395
+ idxs = bond2params.get(angle['index'])
396
+ theta = angle['parameters'][0]
397
+ cth = angle['parameters'][1]
398
+
399
+ if idxs[0] is not None:
400
+ theta = ff_optimize[idxs[0]]
401
+ if idxs[1] is not None:
402
+ cth = ff_optimize[idxs[1]]
403
+
404
+ idx0 = get_jac_indx(angle['atoms'][0])
405
+ idx1 = get_jac_indx(angle['atoms'][1])
406
+ idx2 = get_jac_indx(angle['atoms'][2])
407
+
408
+ if idxs[0] is not None:
409
+ agi, agj, agk = function_map.get(angle['function'])(qm_coordinates[angle['atoms'][0]],
410
+ qm_coordinates[angle['atoms'][1]],
411
+ qm_coordinates[angle['atoms'][2]],
412
+ theta, cth,
413
+ flag='derivative',
414
+ derivative='angle')
415
+
416
+
417
+ jac_mat[idx0: idx0 +3, idxs[0]] += agi
418
+ jac_mat[idx1 : idx1 +3, idxs[0]] += agj
419
+ jac_mat[idx2 : idx2 +3, idxs[0]] += agk
420
+
421
+ if idxs[1] is not None:
422
+ ki, kj, kk = function_map.get(angle['function'])(qm_coordinates[angle['atoms'][0]],
423
+ qm_coordinates[angle['atoms'][1]],
424
+ qm_coordinates[angle['atoms'][2]],
425
+ theta, cth,
426
+ flag='derivative',
427
+ derivative='force_constant')
428
+
429
+ jac_mat[idx0: idx0 +3, idxs[1]] += ki
430
+ jac_mat[idx1 : idx1 +3, idxs[1]] += kj
431
+ jac_mat[idx2 : idx2 +3, idxs[1]] += kk
432
+
433
+ return jac_mat
434
+
435
+
436
+
437
+ def compute_dihedrals_forces(dihedrals, qm_coordinates,
438
+ ff_optimize,
439
+ bond2params, flag='force'):
440
+
441
+ function_map = {
442
+ 4: compute_dihedral_force,
443
+ 3: compute_RB_dihedral_force
444
+ }
445
+
446
+
447
+ if flag == 'force':
448
+ forces = np.zeros(qm_coordinates.shape)
449
+ for dihedral in dihedrals:
450
+ if dihedral['function'] in [1, 4, 9]:
451
+ kd = dihedral['parameters'][1]
452
+ idxs = bond2params.get(dihedral['index'])
453
+ if idxs[1] is not None:
454
+ kd = ff_optimize[idxs[1]]
455
+ fi, fj, fk, fl= compute_dihedral_force(qm_coordinates[dihedral['atoms'][0]],
456
+ qm_coordinates[dihedral['atoms'][1]],
457
+ qm_coordinates[dihedral['atoms'][2]],
458
+ qm_coordinates[dihedral['atoms'][3]],
459
+ dihedral['parameters'][0], kd,
460
+ dihedral['parameters'][2], flag=flag)
461
+ forces[dihedral['atoms'][0]] += fi
462
+ forces[dihedral['atoms'][1]] += fj
463
+ forces[dihedral['atoms'][2]] += fk
464
+ forces[dihedral['atoms'][3]] += fl
465
+
466
+ elif dihedral['function'] == 3:
467
+ C_params = []
468
+ idxs = bond2params.get(dihedral['index'])
469
+ for i, c in enumerate(idxs):
470
+ if c is not None:
471
+ C_params.append(ff_optimize[c])
472
+ else:
473
+ C_params.append(dihedral['parameters'][i])
474
+ fi, fj, fk, fl= compute_RB_dihedral_force(qm_coordinates[dihedral['atoms'][0]],
475
+ qm_coordinates[dihedral['atoms'][1]],
476
+ qm_coordinates[dihedral['atoms'][2]],
477
+ qm_coordinates[dihedral['atoms'][3]],
478
+ C_params,flag=flag)
479
+
480
+ forces[dihedral['atoms'][0]] += fi
481
+ forces[dihedral['atoms'][1]] += fj
482
+ forces[dihedral['atoms'][2]] += fk
483
+ forces[dihedral['atoms'][3]] += fl
484
+
485
+ return forces
486
+
487
+ elif flag == 'derivative':
488
+ jac_mat = np.zeros((3*qm_coordinates.shape[0],ff_optimize.shape[0]))
489
+ for dihedral in dihedrals:
490
+ if not dihedral['optimize']:
491
+ continue
492
+ idxs = bond2params.get(dihedral['index'])
493
+
494
+ if dihedral['function'] in [1, 4, 9]:
495
+ # Only compute derivatives if the force constant is being optimized
496
+ if idxs[1] is not None:
497
+ ki, kj, kk, kl= compute_dihedral_force(qm_coordinates[dihedral['atoms'][0]],
498
+ qm_coordinates[dihedral['atoms'][1]],
499
+ qm_coordinates[dihedral['atoms'][2]],
500
+ qm_coordinates[dihedral['atoms'][3]],
501
+ dihedral['parameters'][0], ff_optimize[idxs[1]],
502
+ dihedral['parameters'][2], flag='derivative')
503
+ idx0 = get_jac_indx(dihedral['atoms'][0])
504
+ idx1 = get_jac_indx(dihedral['atoms'][1])
505
+ idx2 = get_jac_indx(dihedral['atoms'][2])
506
+ idx3 = get_jac_indx(dihedral['atoms'][3])
507
+
508
+ jac_mat[idx0: idx0 +3, idxs[1]] += ki
509
+ jac_mat[idx1 : idx1 +3, idxs[1]] += kj
510
+ jac_mat[idx2 : idx2 +3, idxs[1]] += kk
511
+ jac_mat[idx3 : idx3 +3, idxs[1]] += kl
512
+
513
+ elif dihedral['function'] == 3:
514
+ if not dihedral['optimize']:
515
+ continue
516
+ C_params = []
517
+ idxs = bond2params.get(dihedral['index'])
518
+ for i, c in enumerate(idxs):
519
+ if c is not None:
520
+ C_params.append(ff_optimize[c])
521
+ else:
522
+ C_params.append(dihedral['parameters'][i])
523
+
524
+ for i, c in enumerate(idxs):
525
+ if c is None:
526
+ continue
527
+ ki, kj, kk, kl= compute_RB_dihedral_force(qm_coordinates[dihedral['atoms'][0]],
528
+ qm_coordinates[dihedral['atoms'][1]],
529
+ qm_coordinates[dihedral['atoms'][2]],
530
+ qm_coordinates[dihedral['atoms'][3]],
531
+ C_params, flag='derivative',derivative=f'C{i}')
532
+
533
+ idx0 = get_jac_indx(dihedral['atoms'][0])
534
+ idx1 = get_jac_indx(dihedral['atoms'][1])
535
+ idx2 = get_jac_indx(dihedral['atoms'][2])
536
+ idx3 = get_jac_indx(dihedral['atoms'][3])
537
+ jac_mat[idx0: idx0 +3, c] += ki
538
+ jac_mat[idx1 : idx1 +3, c] += kj
539
+ jac_mat[idx2 : idx2 +3, c] += kk
540
+ jac_mat[idx3 : idx3 +3, c] += kl
541
+
542
+ return jac_mat
543
+
544
+ def compute_bonded_forces(ff_optimize,
545
+ qm_coordinates,
546
+ bonds,
547
+ angles,
548
+ dihedrals,
549
+ bond2params,
550
+ qm_atoms_count=None):
551
+ """
552
+ Compute bonded forces for all interactions in the QM region.
553
+
554
+ Args:
555
+ ff_optimize (numpy.ndarray): Array of optimized force field parameters
556
+ qm_coordinates (numpy.ndarray): Array of coordinates (QM only or QM+MM)
557
+ bonds (list): List of bond dictionaries
558
+ angles (list): List of angle dictionaries
559
+ dihedrals (list): List of dihedral dictionaries
560
+ bond2params (dict): Mapping of interaction indices to parameter indices
561
+ qm_atoms_count (int, optional): Number of QM atoms if coordinates include MM atoms
562
+
563
+ Returns:
564
+ numpy.ndarray: Array of forces for QM atoms only
565
+ """
566
+ # Initialize forces array for all atoms in coordinates
567
+ forces = np.zeros(qm_coordinates.shape)
568
+
569
+ # Process bonds
570
+ b_forces = compute_bonds_forces(bonds, qm_coordinates,
571
+ ff_optimize,
572
+ bond2params, 'force')
573
+ forces += b_forces
574
+
575
+ # Process angles
576
+ a_forces = compute_angles_forces(angles, qm_coordinates,
577
+ ff_optimize,
578
+ bond2params, 'force')
579
+ forces += a_forces
580
+
581
+ # Process dihedrals
582
+ d_forces = compute_dihedrals_forces(dihedrals, qm_coordinates,
583
+ ff_optimize,
584
+ bond2params, 'force')
585
+ forces += d_forces
586
+
587
+ # If qm_atoms_count is provided, return only QM atom forces
588
+ if qm_atoms_count is not None:
589
+ return forces[:qm_atoms_count]
590
+
591
+ return forces
592
+
593
+
594
+ def jacobian_ff(ff_optimize,
595
+ qm_coordinates,
596
+ bond2params,
597
+ bonds, angles, dihedrals,
598
+ qm_atoms_count=None):
599
+
600
+ """
601
+ ff_optimize: b1, k1, b2, k2, ...
602
+ """
603
+
604
+ number_of_parameters = ff_optimize.shape[0]
605
+ # If qm_atoms_count is provided, compute Jacobian only for QM atoms
606
+ if qm_atoms_count is not None:
607
+ number_of_residuals = 3 * qm_atoms_count
608
+ else:
609
+ number_of_residuals = 3 * qm_coordinates.shape[0]
610
+
611
+ # compute the bonded forces
612
+ jacobian_matrix = np.zeros((number_of_residuals,number_of_parameters))
613
+
614
+ # Compute Jacobian for all atoms in coordinates
615
+ full_jacobian = np.zeros((3 * qm_coordinates.shape[0], number_of_parameters))
616
+
617
+ full_jacobian += compute_bonds_forces(bonds, qm_coordinates,
618
+ ff_optimize, bond2params, flag='derivative')
619
+ full_jacobian += compute_angles_forces(angles, qm_coordinates,
620
+ ff_optimize, bond2params, flag='derivative')
621
+ full_jacobian += compute_dihedrals_forces(dihedrals, qm_coordinates,
622
+ ff_optimize, bond2params, flag='derivative')
623
+
624
+ # If qm_atoms_count is provided, return only QM atom Jacobian
625
+ if qm_atoms_count is not None:
626
+ return full_jacobian[:3*qm_atoms_count, :]
627
+
628
+ return full_jacobian