def setUp(self): from parameterize.home import home from moleculekit.molecule import Molecule from parameterize.parameterization.detect import detectEquivalentAtoms from parameterize.parameterization.fftype import fftype molFile = os.path.join(home("test-param"), "glycol.mol2") mol = Molecule(molFile) self.equivalents = detectEquivalentAtoms(mol) _, self.mol = fftype(mol, method="GAFF2")
def setUp(self): from parameterize.home import home from moleculekit.molecule import Molecule molFile = os.path.join(home("test-param"), "H2O2.mol2") mol = Molecule(molFile, guessNE="bonds", guess=("angles", "dihedrals")) self.mol = mol self.esp = ESP() self.esp.molecule = self.mol # Precalculating here for the tests equivalents = detectEquivalentAtoms(self.esp.molecule) self.esp._equivalent_atom_groups = equivalents[0] self.esp._equivalent_group_by_atom = equivalents[2]
def symmetrizeCharges(mol): """ Average the charges of equivalent atoms Parameters ---------- mol: Molecule Molecule to symmetrize the charges Return ------ results: Molecule Copy of the molecule with the symmetrized charges Examples -------- >>> from parameterize.home import home >>> from moleculekit.molecule import Molecule >>> molFile = os.path.join(home('test-charge'), 'H2O.mol2') >>> mol = Molecule(molFile) >>> mol.charge[:] = [0.5, -0.5, 0.0] >>> new_mol = symmetrizeCharges(mol) >>> assert new_mol is not mol >>> new_mol.charge array([ 0.5 , -0.25, -0.25], dtype=float32) """ from parameterize.parameterization.detect import detectEquivalentAtoms if not isinstance(mol, Molecule): raise TypeError('"mol" has to be instance of {}'.format(Molecule)) if mol.numFrames != 1: raise ValueError( '"mol" can have just one frame, but it has {}'.format(mol.numFrames) ) mol = mol.copy() molCharge = np.sum(mol.charge) equivalent_groups, _, _ = detectEquivalentAtoms(mol) for atoms in equivalent_groups: atoms = list(atoms) mol.charge[atoms] = np.mean(mol.charge[atoms]) assert np.isclose(np.sum(mol.charge), molCharge, atol=1e-6) return mol
def inventAtomTypes(mol, fit_dihedrals): """ Duplicate atom types of the dihedral, so its parameters are unique. """ # TODO check symmetry from parameterize.parameterization.detect import detectEquivalentAtoms equivalents = detectEquivalentAtoms(mol) mol = mol.copy() alltypes = list(mol.atomtype) originaltype = {type: type for type in np.unique(mol.atomtype)} # Duplicate the atom types of the dihedral for dih in fit_dihedrals: for d in dih: oldtype = mol.atomtype[d] # if the type is already duplicated if re.match(_ATOM_TYPE_REG_EX, oldtype): continue # Create a new atom type name i = 0 while ("{}x{}".format(oldtype, i)) in alltypes: i += 1 newtype = "{}x{}".format(oldtype, i) alltypes.append(newtype) originaltype[newtype] = oldtype mol.atomtype[d] = newtype # Rename the atom types of the equivalent atoms for index in equivalents[1][d]: if index != d: assert not re.match(_ATOM_TYPE_REG_EX, mol.atomtype[index]) mol.atomtype[index] = newtype equivalent_dihedrals = _getEquivalentDihedrals(mol, equivalents, np.array(dih)) if len(equivalent_dihedrals) > 1: raise ValueError( "Dihedral term still not unique after duplication. Dihedral {} has {} equivalent " "dihedrals: {}".format( dih, len(equivalent_dihedrals), equivalent_dihedrals ) ) return mol, originaltype
def run(self): """ Run ESP charge fitting Return ------ results : dict Dictionary with the fitted charges and fitting loss value """ logger.info("Start RESP charge fitting") self._molecular_charge = self.qm_results[0].charge # Detect equivalent atoms equivalents = detectEquivalentAtoms(self.molecule) self._equivalent_atom_groups = equivalents[0] self._equivalent_group_by_atom = equivalents[2] # Set up heavy atom restrains self._restraint_factors = np.zeros(self.molecule.numAtoms) if self.restraint_factor > 0: for i, element in enumerate(self.molecule.element): if element != "H": self._restraint_factors[i] = self.restraint_factor # Get charge bounds lower_bounds, upper_bounds = self._get_bounds() logger.info("Atom charge boundaries and restraint factor:") for i, name in enumerate(self.molecule.name): lower = lower_bounds[self._equivalent_group_by_atom[i]] upper = upper_bounds[self._equivalent_group_by_atom[i]] factor = self._restraint_factors[i] logger.info( " {:4s}: {:7.3f} {:7.3f} {:10.6f}".format(name, lower, upper, factor) ) # Set up optimizer opt = nlopt.opt(nlopt.LN_COBYLA, self.ngroups) logger.info("Optimizer: {}".format(opt.get_algorithm_name())) opt.set_min_objective(self._objective) opt.add_equality_constraint(self._constraint) logger.info( "Molecular charges constraint: {:.3f}".format(self._molecular_charge) ) opt.set_lower_bounds(lower_bounds) opt.set_upper_bounds(upper_bounds) opt.set_xtol_rel(1.0e-6) opt.set_maxeval(1000 * self.ngroups) opt.set_initial_step(0.001) # Optimize the charges group_charges = opt.optimize(np.zeros(self.ngroups)) status = opt.last_optimize_result() loss = self._objective(group_charges, None) charges = self._map_groups_to_atoms(group_charges) logger.info("Optimizer status: {}".format(status)) logger.info("Final loss: {:.6f}".format(loss)) # Compute RMSD self._restraint_factors[:] = 0 msd = self._objective(group_charges, None) logger.info("Final RMSD: {:.6f} au".format(np.sqrt(msd))) logger.info("Finish RESP charge fitting") return { "charges": charges, "status": status, "loss": loss, "RMSD": np.sqrt(msd), }
def _fit_charges(mol, args, qm, atom_types): from parameterize.charge import ( fitGasteigerCharges, fitChargesWithAntechamber, fitESPCharges, symmetrizeCharges, ) from parameterize.parameterization.util import ( guessBondType, getFixedChargeAtomIndices, getDipole, _qm_method_name, ) from parameterize.parameterization.detect import detectEquivalentAtoms logger.info("=== Atomic charge fitting ===") logger.info("Method: {}".format(args.charge_type)) if args.charge_type == "None": # TODO move to argument validation if len(args.fix_charge) > 0: logger.warning("Flag --fix-charge does not have effect!") logger.info("Atomic charges are taken from {}".format(args.filename)) elif args.charge_type == "Gasteiger": # TODO move to argument validation if len(args.fix_charge) > 0: logger.warning("Flag --fix-charge does not have effect!") # TODO move to _prepare_molecule if np.any(mol.bondtype == "un"): logger.info("Guessing bond types") mol = guessBondType(mol) mol = fitGasteigerCharges(mol, atom_types=atom_types) charge = int(round(np.sum(mol.charge))) if args.charge != charge: logger.warning( f"Molecular charge is {args.charge}, but Gasteiger atomic charges add up to {charge}!" ) args.charge = charge elif args.charge_type == "AM1-BCC": # TODO move to argument validation if len(args.fix_charge) > 0: logger.warning("Flag --fix-charge does not have effect!") mol = fitChargesWithAntechamber(mol, type="bcc", molCharge=args.charge) mol = symmetrizeCharges(mol) elif args.charge_type == "ESP": # Detect equivalent atom groups logger.info("Equivalent atom groups:") atom_groups = [ group for group in detectEquivalentAtoms(mol)[0] if len(group) > 1 ] for atom_group in atom_groups: logger.info(" {}".format(", ".join(mol.name[list(atom_group)]))) # Select the atoms with fixed charges fixed_atom_indices = getFixedChargeAtomIndices(mol, args.fix_charge) # Create an ESP directory espDir = os.path.join(args.outdir, "esp", _qm_method_name(qm)) os.makedirs(espDir, exist_ok=True) charge = int(round(np.sum(mol.charge))) if args.charge != charge: logger.warning( "Molecular charge is set to {}, but atomic charges add up to {}" "".format(args.charge, charge)) if len(args.fix_charge) > 0: raise RuntimeError( "Flag --fix-charge cannot be used when atomic charges are inconsistent with passed " "molecular charge {}".format(args.charge)) mol.charge[:] = args.charge / mol.numAtoms # Set random number generator seed if args.seed: np.random.seed(args.seed) # Fit ESP charges mol, extra = fitESPCharges(mol, qm, espDir, fixed=fixed_atom_indices) # Print QM dipole logger.info( "QM dipole: {:6.3f} {:6.3f} {:6.3f}; total: {:6.3f}".format( *extra["qm_dipole"])) else: raise ValueError() # Print MM dipole mm_dipole = getDipole(mol) logger.info("MM dipole: {:6.3f} {:6.3f} {:6.3f}; total: {:6.3f}".format( *mm_dipole)) # Print the new charges logger.info("Atomic charges:") for name, charge in zip(mol.name, mol.charge): logger.info(" {:4s}: {:6.3f}".format(name, charge)) logger.info("Molecular charge: {:6.3f}".format(np.sum(mol.charge))) return mol