Reparameterize force fields#

The MMForceFieldGenerator class in VeloxChem provides a number of functions to manipulate topology files. As an example, we consider the optimization of the force field parameters for a dihedral angle of a thiophene-based optical ligand named HS-276.

Load the required modules and the equilibrium geometry:

import veloxchem as vlx
import numpy as np

# load B3LYP optimized geometry in the xyz format
molecule = vlx.Molecule.read_xyz_file("../../data/md/hs276.xyz")
molecule.show(atom_indices=True, width=600, height=450)

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

We here consider the dihedral angle between the two thiophene rings, which corresponds to the rotation around bond 21-22.

The dihedral potential curves will be plotted using force field parameters from the general amber forcefield (GAFF) database, as compared to QM results. The force field will then be changed to better fit this potential.

Create the initial force field#

Initialize an MMForceFieldGenerator object and generate initial force field with pre-calculated RESP charges:

resp_chg = np.array([
    0.014967, 0.224409, 0.089489, -0.551618, 0.153050, 0.106847, -0.315042,
    0.165854, 0.206441, -0.425163, 0.225816, -0.039099, 0.179377, 0.002119,
    0.014483, -0.043666, -0.230202, 0.165960, -0.071404, 0.188598, -0.044661,
    0.088786, -0.173586, 0.175730, -0.089376, 0.164515, -0.118573, -0.150541,
    0.736497, -0.570770, -0.273724, -0.144051, 0.112846, 0.112846, 0.112846
])

ff_gen = vlx.MMForceFieldGenerator()
ff_gen.partial_charges = resp_chg
ff_gen.create_topology(molecule)
* Info * Using GAFF (v2.11) parameters.                                                                                   
         Reference: J. Wang, R. M. Wolf, J. W. Caldwell, P. A. Kollman, D. A. Case, J. Comput. Chem. 2004,
         25, 1157-1174.
                                                                                                                          
* Info * Updated bond angle 1-9-10 (ca-ca-cc) to 107.217 deg                                                              
* Info * Updated bond angle 1-14-15 (ca-na-cc) to 126.641 deg                                                             
* Info * Updated bond angle 2-1-14 (ca-ca-na) to 131.687 deg                                                              
* Info * Updated bond angle 7-9-10 (ca-ca-cc) to 135.297 deg                                                              
* Info * Updated bond angle 9-1-14 (ca-ca-na) to 107.608 deg                                                              
* Info * Updated bond angle 14-15-17 (na-cc-cd) to 128.324 deg                                                            
* Info * Updated bond angle 19-21-22 (cd-cc-cc) to 129.220 deg                                                            
* Info * Updated bond angle 21-22-23 (cc-cc-cd) to 128.571 deg                                                            

Reparameterize the force field#

Use the reparameterize_dihedrals method to fit dihedral parameters to QM scan. If you would like to see more output during the fitting, set verbose to True.

ff_gen.reparameterize_dihedrals(rotatable_bond=(21,22),
                                scan_file="../../data/md/27-22-21-16.xyz",
                                visualize=True,
                                verbose=False)
                                          VeloxChem Dihedral Reparameterization                                           
                                         =======================================                                          
                                                                                                                          
* Info * Rotatable bond selected: 21-22                                                                                   
* Info * Dihedrals involved:[[16, 21, 22, 23], [16, 21, 22, 27], [19, 21, 22, 23], [19, 21, 22, 27]]                      
                                                                                                                          
* Info * Reading QM scan from file...                                                                                     
* Info *   ../../data/md/27-22-21-16.xyz                                                                                  
                                                                                                                          
* Info * Performing dihedral scan for MM baseline by excluding the involved dihedral barriers...                          
                                                                                                                          
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[3], line 1
----> 1 ff_gen.reparameterize_dihedrals(rotatable_bond=(21,22),
      2                                 scan_file="../../data/md/27-22-21-16.xyz",
      3                                 visualize=True,
      4                                 verbose=False)

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/veloxchem/mmforcefieldgenerator.py:626, in MMForceFieldGenerator.reparameterize_dihedrals(self, rotatable_bond, scan_results, scan_file, visualize, fit_extrema, initial_validation, show_diff, verbose)
    623 self.ostream.print_blank()
    624 self.ostream.flush()
--> 626 initial_data = self.validate_force_field(0, verbose=verbose)
    628 qm_energies = np.array(initial_data['qm_scan_kJpermol'])
    629 mm_baseline = np.array(initial_data['mm_scan_kJpermol'])

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/veloxchem/mmforcefieldgenerator.py:3229, in MMForceFieldGenerator.validate_force_field(self, i, verbose)
   3227 geom = self.scan_geometries[i]
   3228 angles = self.scan_dih_angles[i]
-> 3229 mm_scan = self.perform_mm_scan(dih, geom, angles, verbose=verbose)
   3230 mm_scan = np.array(mm_scan) - min(mm_scan)
   3232 qm_scan = np.array(self.scan_energies[i]) - min(self.scan_energies[i])

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/veloxchem/mmforcefieldgenerator.py:3271, in MMForceFieldGenerator.perform_mm_scan(self, dihedral, geometries, angles, verbose)
   3263 for i, (geom, angle) in enumerate(zip(geometries, angles)):
   3264     # energy minimization with dihedral constraint
   3265     constraints = [
   3266         'set dihedral {} {} {} {} {}'.format(dihedral[0] + 1,
   3267                                              dihedral[1] + 1,
   3268                                              dihedral[2] + 1,
   3269                                              dihedral[3] + 1, angle)
   3270     ]
-> 3271     pot_energy = self.minimize_mm_energy(geom, constraints)
   3272     # convert to kJ/mol
   3273     pot_energy *= hartree_in_kjpermol()

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/veloxchem/mmforcefieldgenerator.py:3302, in MMForceFieldGenerator.minimize_mm_energy(self, molecule, constraints)
   3300 opt_drv = OptimizationDriver(grad_drv)
   3301 opt_drv.constraints = constraints
-> 3302 opt_results = opt_drv.compute(molecule)
   3303 final_mol = Molecule.read_xyz_string(opt_results['final_geometry'])
   3304 self.ostream.unmute()

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/veloxchem/optimizationdriver.py:380, in OptimizationDriver.compute(self, molecule, *args)
    377 with redirect_stdout(StringIO()) as fg_out, redirect_stderr(
    378         StringIO()) as fg_err:
    379     try:
--> 380         m = geometric.optimize.run_optimizer(
    381             customengine=opt_engine,
    382             coordsys=self.coordsys,
    383             check=self.check_interval,
    384             trust=default_trust,
    385             tmax=default_tmax,
    386             maxiter=self.max_iter,
    387             converge=self.conv_flags(),
    388             constraints=constr_filename,
    389             transition=self.transition,
    390             hessian=self.hessian,
    391             input=optinp_filename)
    392     except geometric.errors.HessianExit:
    393         hessian_exit = True

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/optimize.py:1318, in run_optimizer(**kwargs)
   1316     params.xyzout = prefix+"_optim.xyz"
   1317 if ic == 0:
-> 1318     progress = Optimize(coords, M, IC, engine, dirname, params)
   1319 else:
   1320     progress += Optimize(coords, M, IC, engine, dirname, params, print_info=False)

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/optimize.py:1116, in Optimize(coords, molecule, IC, engine, dirname, params, print_info)
   1088 """
   1089 Optimize the geometry of a molecule. This function used to contain the whole
   1090 optimization loop, which has since been moved to the Optimizer() class;
   (...)   1113     A molecule object for opt trajectory and energies
   1114 """
   1115 optimizer = Optimizer(coords, molecule, IC, engine, dirname, params, print_info)
-> 1116 return optimizer.optimizeGeometry()

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/optimize.py:1035, in Optimizer.optimizeGeometry(self)
   1033 if self.state == OPT_STATE.NEEDS_EVALUATION:
   1034     self.calcEnergyForce()
-> 1035     self.evaluateStep()
   1036 if self.recalcHess:
   1037     # If a Hessian recalculation is needed at this point, we need to
   1038     # call calcEnergyForce() which will compute the cartesian Hessian,
   1039     # convert it to IC, and then store it.
   1040     self.calcEnergyForce()

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/optimize.py:834, in Optimizer.evaluateStep(self)
    832 if self.params.qdata is not None: self.progress.write(self.params.qdata, ftype='qdata')
    833 # Project out the degrees of freedom that are constrained
--> 834 rms_gradient, max_gradient = self.calcGradNorm()
    835 rms_displacement, max_displacement = calc_drms_dmax(self.X, self.Xprev)
    836 rms_displacement_noalign, max_displacement_noalign = calc_drms_dmax(self.X, self.Xprev, align=False)

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/optimize.py:256, in Optimizer.calcGradNorm(self)
    255 def calcGradNorm(self):
--> 256     gradxc = self.IC.calcGradProj(self.X, self.gradx) if self.IC.haveConstraints() else self.gradx.copy()
    257     if self.IC.rigid:
    258         mol = deepcopy(self.molecule)

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/internal.py:3197, in DelocalizedInternalCoordinates.calcGradProj(self, xyz, gradx)
   3195     return gradx
   3196 q0 = self.calculate(xyz)
-> 3197 Ginv = self.GInverse(xyz)
   3198 Bmat = self.wilsonB(xyz)
   3199 # Internal coordinate gradient
   3200 # Gq = np.matrix(Ginv)*np.matrix(Bmat)*np.matrix(gradx).T

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/internal.py:3674, in DelocalizedInternalCoordinates.GInverse(self, xyz)
   3673 def GInverse(self, xyz):
-> 3674     return self.GInverse_SVD(xyz)

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/internal.py:1848, in InternalCoordinates.GInverse_SVD(self, xyz, sqrt, invMW)
   1846 while True:
   1847     try:
-> 1848         G = self.GMatrix(xyz, invMW)
   1849         time_G = click()
   1850         U, S, VT = np.linalg.svd(G)

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/internal.py:1837, in InternalCoordinates.GMatrix(self, xyz, invMW)
   1832 def GMatrix(self, xyz, invMW=False):
   1833     """
   1834     Given Cartesian coordinates xyz, return the G-matrix
   1835     given by G = BuBt where u is an arbitrary matrix (default to identity)
   1836     """
-> 1837     Bmat = self.wilsonB(xyz, invMW)
   1838     BuBt = np.dot(Bmat,Bmat.T)
   1839     return BuBt

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/internal.py:1820, in InternalCoordinates.wilsonB(self, xyz, invMW)
   1818     return ans
   1819 WilsonB = []
-> 1820 Der = self.derivatives(xyz)
   1821 for i in range(Der.shape[0]):
   1822     WilsonB.append(Der[i].flatten())

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/internal.py:3656, in DelocalizedInternalCoordinates.derivatives(self, coords)
   3654 def derivatives(self, coords):
   3655     """ Obtain the change of the DLCs with respect to the Cartesian coordinates. """
-> 3656     PrimDers = self.Prims.derivatives(coords)
   3657     # The following code does the same as "tensordot"
   3658     # print PrimDers.shape
   3659     # print self.Vecs.shape
   (...)   3663     #         Answer[i, :, :] += self.Vecs[j, i] * PrimDers[j, :, :]
   3664     # print Answer.shape
   3665     Answer1 = np.tensordot(self.Vecs, PrimDers, axes=(0, 0))

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/internal.py:2702, in PrimitiveInternalCoordinates.derivatives(self, xyz)
   2701 def derivatives(self, xyz):
-> 2702     self.calculate(xyz)
   2703     answer = []
   2704     for Internal in self.Internals:

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/internal.py:2683, in PrimitiveInternalCoordinates.calculate(self, xyz)
   2681 answer = []
   2682 for Internal in self.Internals:
-> 2683     answer.append(Internal.value(xyz))
   2684 return np.array(answer)

File ~/miniconda3/envs/echem/lib/python3.12/site-packages/geometric/internal.py:1398, in Dihedral.value(self, xyz)
   1397 def value(self, xyz):
-> 1398     xyz = xyz.reshape(-1,3)
   1399     a = self.a
   1400     b = self.b

KeyboardInterrupt: 

It is not uncommon that the existing dihedral terms in the force field are not sufficient to reproduce the QM potential. Here we can try adding more dihedral terms to the force field, and then redo the fitting. In this particular case we would like to lower the MM energy at 0\(^{\circ}\) so that it matches the QM energy better. This corresponds to adding a dihedral term with periodicity 1 and phase 180\(^{\circ}\). The barrier can be set to 0 since it will be fitted.

ff_gen.add_dihedral((16, 21, 22, 27), barrier=0.0, phase=180.0, periodicity=1)
* Info * Added dihedral 16-21-22-27                                                                                       

After adding the dihedral term we can redo the fitting. This time we can skip the initial validation step by setting initial_validation to False, since we already know how the MM potential energy curve looks. We can also set show_diff to True so that the difference between QM and MM energies will be plotted after the fitting.

ff_gen.reparameterize_dihedrals(rotatable_bond=(21,22),
                                scan_file="../../data/md/27-22-21-16.xyz",
                                visualize=True,
                                verbose=False,
                                initial_validation=False,
                                show_diff=True)
                                          VeloxChem Dihedral Reparameterization                                           
                                          =====================================                                           
                                                                                                                          
* Info * Rotatable bond selected: 21-22                                                                                   
* Info * Dihedrals involved:[[16, 21, 22, 23], [16, 21, 22, 27], [19, 21, 22, 23], [19, 21, 22, 27]]                      
* Info * Reading QM scan from file...                                                                                     
* Info *   ../../data/md/27-22-21-16.xyz                                                                                  
                                                                                                                          
* Info * Performing dihedral scan for MM baseline by excluding the involved dihedral barriers...                          
                                                                                                                          
* Info *   Target dihedral angle: 16-21-22-27                                                                             
                                                                                                                          
* Info * Dihedral barriers [1.85974413 1.85974413 0.         1.85974413 1.85974413] will be used as initial guess.        
* Info * Fitting the dihedral parameters...                                                                               
* Info * Optimizing dihedral via least squares fitting...                                                                 
* Info * New fitted barriers: [1.98197174 1.98197174 1.97103413 1.98197174 1.98197174]                                    
* Info * Validating the fitted force field...                                                                             
                                                                                                                          
* Info * Summary of validation                                                                                            
* Info * ---------------------                                                                                            
* Info * Maximum difference: 2.368 kJ/mol                                                                                 
* Info * Standard deviation: 0.969 kJ/mol                                                                                 
                                                                                                                          
../../_images/d40f171a5f3da714d5ed49a97be8021746399060c2eaf509bc0e570f27742e28.png
* Info * Dihedral MM parameters have been reparameterized and updated in the topology.                                    

Now we get better agreement between QM and MM energies. Since the difference between QM and MM is also plotted, we can tell that the MM energies can be brought closer to QM energies by adding one more dihedral term with periodicity 4 and phase 0\(^{\circ}\).

ff_gen.add_dihedral((16, 21, 22, 27), barrier=0.0, phase=0.0, periodicity=4)
* Info * Added dihedral 16-21-22-27                                                                                       
ff_gen.reparameterize_dihedrals(rotatable_bond=(21,22),
                                scan_file="../../data/md/27-22-21-16.xyz",
                                visualize=True,
                                verbose=False,
                                initial_validation=False)
                                          VeloxChem Dihedral Reparameterization                                           
                                          =====================================                                           
                                                                                                                          
* Info * Rotatable bond selected: 21-22                                                                                   
* Info * Dihedrals involved:[[16, 21, 22, 23], [16, 21, 22, 27], [19, 21, 22, 23], [19, 21, 22, 27]]                      
* Info * Reading QM scan from file...                                                                                     
* Info *   ../../data/md/27-22-21-16.xyz                                                                                  
                                                                                                                          
* Info * Performing dihedral scan for MM baseline by excluding the involved dihedral barriers...                          
                                                                                                                          
* Info *   Target dihedral angle: 16-21-22-27                                                                             
                                                                                                                          
* Info * Dihedral barriers [1.98197174 1.98197174 1.97103413 0.         1.98197174 1.98197174] will be used as initial guess.
* Info * Fitting the dihedral parameters...                                                                               
* Info * Optimizing dihedral via least squares fitting...                                                                 
* Info * New fitted barriers: [2.00263461 2.00263461 1.90578785 1.23978392 2.00263461 2.00263461]                         
* Info * Validating the fitted force field...                                                                             
                                                                                                                          
* Info * Summary of validation                                                                                            
* Info * ---------------------                                                                                            
* Info * Maximum difference: 0.344 kJ/mol                                                                                 
* Info * Standard deviation: 0.197 kJ/mol                                                                                 
                                                                                                                          
../../_images/17b4bfcb882b635126a252911d633dcf34cd1c9d16da60153478a3532a3ed0f3.png
* Info * Dihedral MM parameters have been reparameterized and updated in the topology.                                    

Save the force field to file#

Use write_gromacs_files or write_openmm_files to write the force field to files.

ff_gen.write_gromacs_files('hs276')