def gradient(self, X): ''' Compute the gradient of the strain w.r.t. Cartesian coordinates of the system. For the ic that needs to be constrained, a Lagrange multiplier is included. ''' #initialize return value grad = np.zeros((len(X),)) #compute strain gradient gstrain = np.zeros(self.coords0.shape) self.update_pos(self.coords0 + X[:self.ndof].reshape((-1,3))) self.value = self.compute(gpos=gstrain) #compute constraint gradient gconstraint = np.zeros(self.coords0.shape) self.constraint.update_pos(self.coords0 + X[:self.ndof].reshape((-1,3))) self.constrain_value = self.constraint.compute(gpos=gconstraint) + 1.0 #construct gradient grad[:self.ndof] = gstrain.reshape((-1,)) + X[self.ndof]*gconstraint.reshape((-1,)) grad[self.ndof] = self.constrain_value - self.constrain_target #cartesian penalty, i.e. extra penalty for deviation w.r.t. cartesian equilibrium coords indices = np.array([[3*i,3*i+1,3*i+2] for i in range(self.ndof//3) if i not in self.cons_ic_atindexes]).ravel() if len(indices)>0: grad[indices] += X[indices]/(self.ndof*self.cart_penalty**2) with log.section('PTGEN', 4, timer='PT Generate'): log.dump(' Gradient: rms = %.3e max = %.3e cnstr = %.3e' %(np.sqrt((grad[:self.ndof]**2).mean()), max(grad[:self.ndof]), grad[self.ndof])) return grad
def __init__(self, system, ai, settings, ffrefs=[]): ''' **Arguments** system a Yaff `System` instance defining the system ai a `Reference` instance corresponding to the ab initio input data settings a `Settings` instance defining all QuickFF settings **Optional Arguments** ffrefs a list of `Reference` instances defining the a-priori force field contributions. ''' with log.section('INIT', 1, timer='Initializing'): log.dump('Initializing program') self.settings = settings self.system = system self.ai = ai self.ffrefs = ffrefs self.valence = ValenceFF(system, settings) self.perturbation = RelaxedStrain(system, self.valence, settings) self.trajectories = None self.print_system()
def do_eq_setrv(self, tasks, logger_level=3): ''' Set the rest values to their respective AI equilibrium values. ''' with log.section('EQSET', 2, timer='Equil Set RV'): self.reset_system() log.dump('Setting rest values to AI equilibrium values for tasks %s' %' '.join(tasks)) for term in self.valence.terms: vterm = self.valence.vlist.vtab[term.index] if np.array([task in term.tasks for task in tasks]).any(): if term.kind==3:#cross term ic0 = self.valence.iclist.ictab[vterm['ic0']] ic1 = self.valence.iclist.ictab[vterm['ic1']] self.valence.set_params(term.index, rv0=ic0['value'], rv1=ic1['value']) elif term.kind==4 and term.ics[0].kind==4:#Cosine of DihedAngle ic = self.valence.iclist.ictab[vterm['ic0']] m = self.valence.get_params(term.index, only='m') rv = ic['value']%(360.0*deg/m) with log.section('EQSET', 4, timer='Equil Set RV'): log.dump('Set rest value of %s(%s) (eq=%.3f deg) to %.3f deg' %( term.basename, '.'.join([str(at) for at in term.get_atoms()]), ic['value']/deg, rv/deg )) self.valence.set_params(term.index, rv0=rv) else: rv = self.valence.iclist.ictab[vterm['ic0']]['value'] self.valence.set_params(term.index, rv0=rv) self.valence.dump_logger(print_level=logger_level) self.average_pars()
def do_bendclin(self, thresshold=5*deg): ''' No Harmonic bend can have a rest value equal to are large than 180 deg - thresshold. If a master (or its slaves) has such a rest value, convert master and all slaves to BendCLin (which corresponds to a chebychev1 potential with sign=+1): 0.5*K*[1+cos(theta)] ''' for master in self.valence.iter_masters(label='BendAHarm'): indices = [master.index] for slave in master.slaves: indices.append(slave) found = False for index in indices: rv = self.valence.get_params(index, only='rv') if rv>=180.0*deg-thresshold: found = True break if found: log.dump('%s has rest value > 180-%.0f deg, converted to BendCheby1' %(master.basename, thresshold/deg)) for index in indices: term = self.valence.terms[index] self.valence.modify_term( index, Chebychev1, [BendCos(*term.get_atoms())], term.basename.replace('BendAHarm', 'BendCheby1'), ['HC_FC_DIAG'], ['kjmol', 'au'] ) self.valence.set_params(index, fc=0.0, sign=1.0) for traj in self.trajectories: if traj.term.index==index: traj.rv = None traj.fc = None traj.active = False
def do_sqoopdist_to_oopdist(self, thresshold=1e-4*angstrom): ''' Transform a SqOopdist term with a rest value that has been set to zero, to a term Oopdist (harmonic in Oopdist instead of square of Oopdist) with a rest value of 0.0 A. ''' for master in self.valence.iter_masters(label='SqOopdist'): indices = [master.index] for slave in master.slaves: indices.append(slave) found = False for index in indices: rv = self.valence.get_params(index, only='rv') if rv<=thresshold: found = True break if found: log.dump('%s has rest value <= %.0f A^2, converted to Oopdist with d0=0' %(master.basename, thresshold/angstrom)) for index in indices: term = self.valence.terms[index] self.valence.modify_term( index, Harmonic, [OopDist(*term.get_atoms())], term.basename.replace('SqOopdist', 'Oopdist'), ['HC_FC_DIAG'], ['kjmol/A**2', 'A'] ) self.valence.set_params(index, fc=0.0, rv0=0.0)
def do_pt_estimate(self, do_valence=False, energy_noise=None, logger_level=3): ''' Estimate force constants and rest values from the perturbation trajectories **Optional Arguments** do_valence if set to True, the current valence force field will be used to estimate the contribution of all other valence terms. ''' with log.section('PTEST', 2, timer='PT Estimate'): self.reset_system() message = 'Estimating FF parameters from perturbation trajectories' if do_valence: message += ' with valence reference' log.dump(message) #compute fc and rv from trajectory only = self.settings.only_traj for traj in self.trajectories: if traj is None: continue if not (only is None or only=='PT_ALL' or only=='pt_all'): if isinstance(only, str): only = [only] basename = self.valence.terms[traj.term.master].basename if basename not in only: continue self.perturbation.estimate(traj, self.ai, ffrefs=self.ffrefs, do_valence=do_valence, energy_noise=energy_noise) #set force field parameters to computed fc and rv for traj in self.trajectories: if traj is None: continue if not (only is None or only=='PT_ALL' or only=='pt_all'): if isinstance(only, str): only = [only] basename = self.valence.terms[traj.term.master].basename if basename not in only: continue self.valence.set_params(traj.term.index, fc=traj.fc, rv0=traj.rv) #output self.valence.dump_logger(print_level=logger_level)
def reset_system(self): ''' routine to reset the system coords to the ai equilbrium ''' log.dump('Resetting system coordinates to ab initio ref') self.system.pos = self.ai.coords0.copy() self.valence.dlist.forward() self.valence.iclist.forward()
def do_squarebend(self, thresshold=20*deg): ''' Identify bend patterns in which 4 atoms of type A surround a central atom of type B with A-B-A angles of 90/180 degrees. A simple harmonic pattern will not be adequate since a rest value of 90 and 180 degrees is possible for the same A-B-A term. Therefore, a cosine term with multiplicity of 4 is used (which corresponds to a chebychev4 potential with sign=-1): V = K/2*[1-cos(4*theta)] To identify the patterns, it is assumed that the rest values have already been estimated from the perturbation trajectories. For each master and slave of a BENDAHARM term, its rest value is computed and checked if it lies either the interval [90-thresshold,90+thresshold] or [180-thresshold,180]. If this is the case, the new cosine term is used. **Optional arguments** thresshold the (half) the width of the interval around 180 deg (90 degrees) to check if a square BA4 ''' for master in self.valence.iter_masters(label='BendAHarm'): rvs = np.zeros([len(master.slaves)+1], float) rvs[0] = self.valence.get_params(master.index, only='rv') for i, islave in enumerate(master.slaves): rvs[1+i] = self.valence.get_params(islave, only='rv') n90 = 0 n180 = 0 nother = 0 for i, rv in enumerate(rvs): if 90*deg-thresshold<=rv and rv<=90*deg+thresshold: n90 += 1 elif 180*deg-thresshold<=rv and rv<=180*deg+thresshold: n180 += 1 else: nother += 1 if n90>0 and n180>0: log.dump('%s has rest values around 90 deg and 180 deg, converted to BendCheby4' %master.basename) #modify master and slaves indices = [master.index] for slave in master.slaves: indices.append(slave) for index in indices: term = self.valence.terms[index] self.valence.modify_term( index, Chebychev4, [BendCos(*term.get_atoms())], term.basename.replace('BendAHarm', 'BendCheby4'), ['HC_FC_DIAG'], ['kjmol', 'au'] ) self.valence.set_params(index, sign=-1) for traj in self.trajectories: if traj.term.index==index: traj.active = False traj.fc = None traj.rv = None
def write_trajectories(self): ''' Write perturbation trajectories to XYZ files. ''' only = self.settings.only_traj if not isinstance(only, list): only = [only] with log.section('XYZ', 3, timer='PT dump XYZ'): for trajectory in self.trajectories: if trajectory is None: continue for pattern in only: if pattern=='PT_ALL' or pattern in trajectory.term.basename: log.dump('Writing XYZ trajectory for %s' %trajectory.term.basename) trajectory.to_xyz()
def plot_trajectories(self, do_valence=False, suffix=''): ''' Plot energy contributions along perturbation trajectories and ''' only = self.settings.only_traj if not isinstance(only, list): only = [only] with log.section('PLOT', 3, timer='PT plot energy'): valence = None if do_valence: valence=self.valence for trajectory in self.trajectories: if trajectory is None: continue for pattern in only: if pattern=='PT_ALL' or pattern in trajectory.term.basename: log.dump('Plotting trajectory for %s' %trajectory.term.basename) trajectory.plot(self.ai, ffrefs=self.ffrefs, valence=valence, suffix=suffix)
def average_pars(self): ''' Average force field parameters over master and slaves. ''' log.dump('Averaging force field parameters over master and slaves') for master in self.valence.iter_masters(): npars = len(self.valence.get_params(master.index)) pars = np.zeros([len(master.slaves)+1, npars], float) pars[0,:] = np.array(self.valence.get_params(master.index)) for i, islave in enumerate(master.slaves): pars[1+i,:] = np.array(self.valence.get_params(islave)) if master.kind in [0,2,11,12]:#harmonic,fues,MM3Quartic,MM3Bend fc, rv = pars.mean(axis=0) self.valence.set_params(master.index, fc=fc, rv0=rv) for islave in master.slaves: self.valence.set_params(islave, fc=fc, rv0=rv) elif master.kind==1: a0, a1, a2, a3 = pars.mean(axis=0) self.valence.set_params(master.index, a0=a0, a1=a1, a2=a2, a3=a3) for islave in master.slaves: self.valence.set_params(islave, a0=a0, a1=a1, a2=a2, a3=a3) elif master.kind==3:#cross fc, rv0, rv1 = pars.mean(axis=0) self.valence.set_params(master.index, fc=fc, rv0=rv0, rv1=rv1) for islave in master.slaves: self.valence.set_params(islave, fc=fc, rv0=rv0, rv1=rv1) elif master.kind==4:#cosine assert pars[:,0].std()<1e-6, 'dihedral multiplicity not unique' m, fc, rv = pars.mean(axis=0) self.valence.set_params(master.index, fc=fc, rv0=rv, m=m) for islave in master.slaves: self.valence.set_params(islave, fc=fc, rv0=rv, m=m) elif master.kind in [5, 6, 7, 8, 9]:#chebychev assert pars.shape[1]==2 fc = pars[:,0].mean() self.valence.set_params(master.index, fc=fc) for islave in master.slaves: self.valence.set_params(islave, fc=fc) else: raise NotImplementedError
def do_pt_generate(self): ''' Generate perturbation trajectories. ''' with log.section('PTGEN', 2, timer='PT Generate'): #read if an existing file was specified through fn_traj fn_traj = self.settings.fn_traj if fn_traj is not None and os.path.isfile(fn_traj): self.trajectories = pickle.load(open(fn_traj, 'rb')) log.dump('Trajectories read from file %s' %fn_traj) self.update_trajectory_terms() newname = 'updated_'+fn_traj.split('/')[-1] pickle.dump(self.trajectories, open(newname, 'wb')) return #configure self.reset_system() only = self.settings.only_traj if only is None or only=='PT_ALL' or only=='pt_all': do_terms = [term for term in self.valence.terms if term.kind in [0,2,11,12]] else: if isinstance(only, str): only = [only] do_terms = [] for pattern in only: for term in self.valence.iter_terms(pattern): if term.kind in [0,2,11,12]: do_terms.append(term) trajectories = self.perturbation.prepare(do_terms) #compute log.dump('Constructing trajectories') self.trajectories = paracontext.map(self.perturbation.generate, [traj for traj in trajectories if traj.active]) #write the trajectories to the non-existing file fn_traj if fn_traj is not None: assert not os.path.isfile(fn_traj) pickle.dump(self.trajectories, open(fn_traj, 'wb')) log.dump('Trajectories stored to file %s' %fn_traj)
def update_trajectory_terms(self): ''' Routine to make ``self.valence.terms`` and the term attribute of each trajectory in ``self.trajectories`` consistent again. This is usefull if the trajectory were read from a file and the ``valenceFF`` instance was modified. ''' log.dump('Updating terms of trajectories to current valenceFF terms') with log.section('PTUPD', 3): #update the terms in the trajectories to match the terms in #self.valence for traj in self.trajectories: found = False for term in self.valence.iter_terms(): if traj.term.get_atoms()==term.get_atoms(): if found: raise ValueError('Found two terms for trajectory %s with atom indices %s' %(traj.term.basename, str(traj.term.get_atoms()))) traj.term = term if 'PT_ALL' not in term.tasks: log.dump('PT_ALL not in tasks of %s-%i, deactivated PT' %(term.basename, term.index)) traj.active = False found = True if not found: log.warning('No term found for trajectory %s with atom indices %s, deactivating trajectory' %(traj.term.basename, str(traj.term.get_atoms()))) traj.active = False #check if every term with task PT_ALL has a trajectory associated #with it. It a trajectory is missing, generate it. for term in self.valence.iter_terms(): if 'PT_ALL' not in term.tasks: continue found = False for traj in self.trajectories: if term.get_atoms()==traj.term.get_atoms(): if found: raise ValueError('Found two trajectories for term %s with atom indices %s' %(term.basename, str(term.get_atoms()))) found =True if not found: log.warning('No trajectory found for term %s with atom indices %s. Generating it now.' %(term.basename, str(term.get_atoms()))) trajectory = self.perturbation.prepare([term])[term.index] self.perturbation.generate(trajectory) self.trajectories.append(trajectory)
def print_system(self): ''' dump overview of atoms (and associated parameters) in the system ''' with log.section('SYS', 3, timer='Initializing'): log.dump('Atomic configuration of the system:') log.dump('') log.dump(' index | x [A] | y [A] | z [A] | ffatype | q | R [A] ') log.dump('---------------------------------------------------------------------') for i in range(len(self.system.numbers)): x, y, z = self.system.pos[i,0], self.system.pos[i,1], self.system.pos[i,2] if self.system.charges is not None: q = self.system.charges[i] else: q = np.nan if self.system.radii is not None: R = self.system.radii[i] else: R = np.nan log.dump(' %4i | % 7.3f | % 7.3f | % 7.3f | %6s | % 7.3f | % 7.3f ' %( i, x/angstrom, y/angstrom, z/angstrom, self.system.ffatypes[self.system.ffatype_ids[i]], q, R/angstrom ))
def dump_logger(self, print_level=1): if log.log_level < print_level: return with log.section('', print_level): sequence = [ 'bondharm', 'bendaharm', 'bendcharm', 'bendcos', 'torsion', 'torsc2harm', 'dihedharm', 'oopdist', 'cross' ] log.dump('') for label in sequence: lines = [] for term in self.iter_masters(label=label): lines.append(term.to_string(self)) for line in sorted(lines): log.dump(line) log.dump('')
def do_pt_generate(self): ''' Generate perturbation trajectories. ''' with log.section('PTGEN', 2, timer='PT Generate'): #read if an existing file was specified through fn_traj fn_traj = self.settings.fn_traj if fn_traj is not None and os.path.isfile(fn_traj): self.trajectories = pickle.load(open(fn_traj, 'rb')) log.dump('Trajectories read from file %s' % fn_traj) self.update_trajectory_terms() newname = 'updated_' + fn_traj.split('/')[-1] pickle.dump(self.trajectories, open(newname, 'wb')) return #configure self.reset_system() only = self.settings.only_traj if only is None or only == 'PT_ALL' or only == 'pt_all': do_terms = [ term for term in self.valence.terms if term.kind in [0, 2, 11, 12] ] else: if isinstance(only, str): only = [only] do_terms = [] for pattern in only: for term in self.valence.iter_terms(pattern): if term.kind in [0, 2, 11, 12]: do_terms.append(term) trajectories = self.perturbation.prepare(do_terms) #compute log.dump('Constructing trajectories') self.trajectories = paracontext.map( self.perturbation.generate, [traj for traj in trajectories if traj.active]) #write the trajectories to the non-existing file fn_traj if fn_traj is not None: assert not os.path.isfile(fn_traj) pickle.dump(self.trajectories, open(fn_traj, 'wb')) log.dump('Trajectories stored to file %s' % fn_traj)
def do_hc_estimatefc(self, tasks, logger_level=3): ''' Refine force constants using Hessian Cost function. **Arguments** tasks A list of strings identifying which terms should have their force constant estimated from the hessian cost function. Using such a flag, one can distinguish between for example force constant refinement (flag=HC_FC_DIAG) of the diagonal terms and force constant estimation of the cross terms (flag=HC_FC_CROSS). If the string 'all' is present in tasks, all fc's will be estimated. **Optional Arguments** logger_level print level at which the resulting parameters should be dumped to the logger. By default, the parameters will only be dumped at the highest log level. ''' with log.section('HCEST', 2, timer='HC Estimate FC'): self.reset_system() log.dump( 'Estimating force constants from Hessian cost for tasks %s' % ' '.join(tasks)) ffrefs = self.kwargs.get('ffrefs', []) term_indices = [] for index in xrange(self.valence.vlist.nv): term = self.valence.terms[index] flagged = False for flag in tasks: if flag in term.tasks: flagged = True break if flagged: #first check if all rest values and multiplicities have been defined if term.kind == 0: self.valence.check_params(term, ['rv']) if term.kind == 1: self.valence.check_params(term, ['a0', 'a1', 'a2', 'a3']) if term.kind == 3: self.valence.check_params(term, ['rv0', 'rv1']) if term.kind == 4: self.valence.check_params(term, ['rv', 'm']) if term.is_master(): term_indices.append(index) else: #first check if all pars have been defined if term.kind == 0: self.valence.check_params(term, ['fc', 'rv']) if term.kind == 1: self.valence.check_params(term, ['a0', 'a1', 'a2', 'a3']) if term.kind == 3: self.valence.check_params(term, ['fc', 'rv0', 'rv1']) if term.kind == 4: self.valence.check_params(term, ['fc', 'rv', 'm']) cost = HessianFCCost(self.system, self.ai, self.valence, term_indices, ffrefs=ffrefs) fcs = cost.estimate() for index, fc in zip(term_indices, fcs): master = self.valence.terms[index] assert master.is_master() self.valence.set_params(index, fc=fc) for islave in master.slaves: self.valence.set_params(islave, fc=fc) self.valence.dump_logger(print_level=logger_level)
def do_hc_estimatefc(self, tasks, logger_level=3, do_svd=False, svd_rcond=0.0, do_mass_weighting=True): ''' Refine force constants using Hessian Cost function. **Arguments** tasks A list of strings identifying which terms should have their force constant estimated from the hessian cost function. Using such a flag, one can distinguish between for example force constant refinement (flag=HC_FC_DIAG) of the diagonal terms and force constant estimation of the cross terms (flag=HC_FC_CROSS). If the string 'all' is present in tasks, all fc's will be estimated. **Optional Arguments** logger_level print level at which the resulting parameters should be dumped to the logger. By default, the parameters will only be dumped at the highest log level. do_svd whether or not to do an SVD decomposition before solving the set of equations and explicitly throw out the degrees of freedom that correspond to the lowest singular values. do_mass_weighting whether or not to apply mass weighing to the ab initio hessian and the force field contributions before doing the fitting. ''' with log.section('HCEST', 2, timer='HC Estimate FC'): self.reset_system() log.dump( 'Estimating force constants from Hessian cost for tasks %s' % ' '.join(tasks)) term_indices = [] for index in range(self.valence.vlist.nv): term = self.valence.terms[index] flagged = False for flag in tasks: if flag in term.tasks: flagged = True break if flagged: #first check if all rest values and multiplicities have been defined if term.kind == 0: self.valence.check_params(term, ['rv']) if term.kind == 1: self.valence.check_params(term, ['a0', 'a1', 'a2', 'a3']) if term.kind == 3: self.valence.check_params(term, ['rv0', 'rv1']) if term.kind == 4: self.valence.check_params(term, ['rv', 'm']) if term.is_master(): term_indices.append(index) else: #first check if all pars have been defined if term.kind == 0: self.valence.check_params(term, ['fc', 'rv']) if term.kind == 1: self.valence.check_params(term, ['a0', 'a1', 'a2', 'a3']) if term.kind == 3: self.valence.check_params(term, ['fc', 'rv0', 'rv1']) if term.kind == 4: self.valence.check_params(term, ['fc', 'rv', 'm']) if len(term_indices) == 0: log.dump( 'No terms (with task in %s) found to estimate FC from HC' % (str(tasks))) return # Try to estimate force constants; if the remove_dysfunctional_cross # keyword is True, a loop is performed which checks whether there # are cross terms for which corresponding diagonal terms have zero # force constants. If this is the case, those cross terms are removed # from the fit and we try again until such cases do no longer occur max_iter = 100 niter = 0 while niter < max_iter: cost = HessianFCCost(self.system, self.ai, self.valence, term_indices, ffrefs=self.ffrefs, do_mass_weighting=do_mass_weighting) fcs = cost.estimate(do_svd=do_svd, svd_rcond=svd_rcond) # No need to continue, if cross terms with corresponding diagonal # terms with negative force constants are allowed if self.settings.remove_dysfunctional_cross is False: break to_remove = [] for index, fc in zip(term_indices, fcs): term = self.valence.terms[index] if term.basename.startswith('Cross'): # Find force constants of corresponding diagonal terms diag_fcs = np.zeros((2)) for idiag in range(2): diag_index = term.diag_term_indexes[idiag] if diag_index in term_indices: fc_diag = fcs[term_indices.index(diag_index)] else: fc_diag = self.valence.get_params(diag_index, only='fc') diag_fcs[idiag] = fc_diag # If a force constant from any corresponding diagonal term is negative, # we remove the cross term for the next iteration if np.any(diag_fcs <= 0.0): to_remove.append(index) self.valence.set_params(index, fc=0.0) log.dump( 'WARNING! Dysfunctional cross term %s detected, removing from the hessian fit.' % term.basename) if len(to_remove) == 0: break else: for index in to_remove: term_indices.remove(index) niter += 1 assert niter < max_iter, "Could not remove all dysfunctional cross terms in %d iterations, something is seriously wrong" % max_iter for index, fc in zip(term_indices, fcs): master = self.valence.terms[index] assert master.is_master() self.valence.set_params(index, fc=fc) for islave in master.slaves: self.valence.set_params(islave, fc=fc) self.valence.dump_logger(print_level=logger_level)
def do_pt_generate(self): ''' Generate perturbation trajectories. ''' with log.section('PTGEN', 2, timer='PT Generate'): #read if an existing file was specified through fn_traj fn_traj = self.settings.fn_traj if fn_traj is not None and os.path.isfile(fn_traj): self.trajectories = pickle.load(open(fn_traj, 'rb')) log.dump('Trajectories read from file %s' % fn_traj) self.update_trajectory_terms() newname = 'updated_' + fn_traj.split('/')[-1] pickle.dump(self.trajectories, open(newname, 'wb')) return #configure self.reset_system() only = self.settings.only_traj dont_traj = self.settings.dont_traj if sum([only is None, dont_traj is None]) == 0: raise AssertionError( 'The settings only_traj and dont_traj cannot be specified both' ) if (only is None or only == 'PT_ALL' or only == 'pt_all' ) and dont_traj is None: # only=None is equivalent to PT_ALL do_terms = [ term for term in self.valence.terms if term.kind in [0, 2, 11, 12] ] elif only is None and dont_traj is not None: kind2string = { 0: 'bond', 2: 'bend', 11: 'oopdist', 12: 'dihedral' } ffatypes = [ self.system.ffatypes[fid] for fid in self.system.ffatype_ids ] dont_patterns = dont_traj.split(',') # split patterns dont_terms = [] for term in self.valence.terms: if term.kind in [0, 2, 11, 12]: types = term.basename.split('/')[1].split('.') option1 = '.'.join(types) option2 = '.'.join(types[::-1]) for dp in dont_patterns: pattern = re.compile(dp, re.IGNORECASE) if pattern.match(option1) or pattern.match( option2): dont_terms.append(term) do_terms = [ term for term in self.valence.terms if term.kind in [0, 2, 11, 12] and term not in dont_terms ] with log.section('PTNOT', 3): for term in dont_terms: log.dump( 'Taking AI equilibrium rest value instead of generating perturbation trajectory for %s' % term.basename) vterm = self.valence.vlist.vtab[term.index] self.valence.set_params(term.index, fc=0, rv0=self.valence.iclist.ictab[ vterm['ic0']]['value']) else: if isinstance(only, str): only = [only] do_terms = [] for pattern in only: for term in self.valence.iter_terms(pattern): if term.kind in [0, 2, 11, 12]: do_terms.append(term) trajectories = self.perturbation.prepare(do_terms) #compute log.dump('Constructing trajectories') self.trajectories = paracontext.map( self.perturbation.generate, [traj for traj in trajectories if traj.active]) #write the trajectories to the non-existing file fn_traj if fn_traj is not None: assert not os.path.isfile(fn_traj) pickle.dump(self.trajectories, open(fn_traj, 'wb')) log.dump('Trajectories stored to file %s' % fn_traj)
def generate(self, trajectory, remove_com=True): ''' Method to calculate the perturbation trajectory, i.e. the trajectory that scans the geometry along the direction of the ic figuring in the term with the given index (should be a diagonal term). This method should be implemented in the derived classes. **Arguments** trajectory instance of Trajectory class representing the perturbation trajectory **Optional Arguments** remove_com if set to True, removes the center of mass translation from the resulting perturbation trajectories [default=True]. ''' #TODO: find out why system.cell is not parsed correctly when using scoop #force correct rvecs self.system0.cell.update_rvecs(self.system_rvecs) with log.section('PTGEN', 4, timer='PT Generate'): log.dump(' Generating %s(atoms=%s)' %(trajectory.term.basename, trajectory.term.get_atoms())) strain = Strain(self.system, trajectory.term, self.valence.terms) natom = self.system0.natom q0 = self.valence.iclist.ictab[self.valence.vlist.vtab[trajectory.term.index]['ic0']]['value'] diag = np.array([0.1*angstrom,]*3*natom+[abs(q0-trajectory.targets[0])]) sol = None for iq, target in enumerate(trajectory.targets): log.dump(' Frame %i (target=%.3f)' %(iq, target)) strain.constrain_target = target if abs(target-q0)<1e-6: sol = np.zeros([strain.ndof+1],float) #call strain.gradient once to compute/store/log relevant information strain.gradient(sol) else: if sol is not None: init = sol.copy() else: init = np.zeros([3*natom+1], float) init[-1] = np.sign(q0-target) sol, infodict, ier, mesg = scipy.optimize.fsolve(strain.gradient, init, xtol=self.settings.pert_traj_tol, full_output=True, diag=diag) if ier!=1: #fsolve did not converge, try again after adding small random noise log.dump(' %s' %mesg.replace('\n', ' ')) log.dump(' Frame %i (target=%.3f) %s(%s) did not converge. Trying again with slightly perturbed initial conditions.' %( iq, target, trajectory.term.basename, trajectory.term.get_atoms() )) #try one more time init = sol.copy() init[:3*natom] += np.random.normal(0.0, 0.01, [3*natom])*angstrom sol, infodict, ier, mesg = scipy.optimize.fsolve(strain.gradient, init, xtol=self.settings.pert_traj_tol, full_output=True, diag=diag) #fsolve did STILL not converge, flag this frame for deletion if ier!=1: log.dump(' %s' %mesg.replace('\n', ' ')) log.dump(' Frame %i (target=%.3f) %s(%s) STILL did not converge.' %( iq, target, trajectory.term.basename, trajectory.term.get_atoms() )) trajectory.targets[iq] = np.nan continue x = self.system0.pos.copy() + sol[:3*natom].reshape((-1,3)) trajectory.values[iq] = strain.constrain_value log.dump(' Converged (value=%.3f, lagmult=%.3e)' %(strain.constrain_value,sol[3*natom])) if remove_com: com = (x.T*self.system0.masses.copy()).sum(axis=1)/self.system0.masses.sum() for i in range(natom): x[i,:] -= com trajectory.coords[iq,:,:] = x #delete flagged frames targets = [] values = [] coords = [] for target, value, coord in zip(trajectory.targets, trajectory.values, trajectory.coords): if not np.isnan(target): targets.append(target) values.append(value) coords.append(coord) trajectory.targets = np.array(targets) trajectory.values = np.array(values) trajectory.coords = np.array(coords) return trajectory
def project_negative_freqs(hessian, masses, thresshold=0.0): N = len(masses) sqrt_mass_matrix = np.diag( np.sqrt((np.array([masses, masses, masses]).T).ravel())) isqrt_mass_matrix = np.linalg.inv(sqrt_mass_matrix) matrix = np.dot(isqrt_mass_matrix, np.dot(hessian.reshape([3 * N, 3 * N]), isqrt_mass_matrix)) #diagonalize if ((matrix - matrix.T) < 1e-6 * lightspeed / centimeter).all(): evals, evecs = np.linalg.eigh(matrix) else: evals, evecs = np.linalg.eig(matrix) log.dump('20 lowest frequencies [1/cm] before projection:') log.dump(str(evals[:4] / (lightspeed / centimeter))) log.dump(str(evals[4:8] / (lightspeed / centimeter))) log.dump(str(evals[8:12] / (lightspeed / centimeter))) log.dump(str(evals[12:16] / (lightspeed / centimeter))) log.dump(str(evals[16:20] / (lightspeed / centimeter))) #set negative eigenvalues to zero evals[evals < thresshold] = 0.0 projected_matrix = np.dot(evecs, np.dot(np.diag(evals), evecs.T)) projected_hessian = np.dot(sqrt_mass_matrix, np.dot(projected_matrix, sqrt_mass_matrix)) #dump freqs after projection as check evals, evecs = np.linalg.eigh(projected_matrix) log.dump('20 lowest frequencies [1/cm] after projection:') log.dump(str(evals[:4] / (lightspeed / centimeter))) log.dump(str(evals[4:8] / (lightspeed / centimeter))) log.dump(str(evals[8:12] / (lightspeed / centimeter))) log.dump(str(evals[12:16] / (lightspeed / centimeter))) log.dump(str(evals[16:20] / (lightspeed / centimeter))) return projected_hessian.reshape([N, 3, N, 3])
def generate(self, trajectory, remove_com=True): ''' Method to calculate the perturbation trajectory, i.e. the trajectory that scans the geometry along the direction of the ic figuring in the term with the given index (should be a diagonal term). This method should be implemented in the derived classes. **Arguments** trajectory instance of Trajectory class representing the perturbation trajectory **Optional Arguments** remove_com if set to True, removes the center of mass translation from the resulting perturbation trajectories [default=True]. ''' index = trajectory.term.index with log.section('PTGEN', 3, timer='PT Generate'): log.dump(' Generating %s(atoms=%s)' % (self.valence.terms[index].basename, trajectory.term.get_atoms())) strain = self.strains[index] natom = self.system.natom if strain is None: log.warning( 'Strain for term %i (%s) is not initialized, skipping.' % (index, self.valence.terms[index].basename)) return q0 = self.valence.iclist.ictab[self.valence.vlist.vtab[index] ['ic0']]['value'] diag = np.ones([strain.ndof + 1], float) diag[:strain.ndof] *= 0.1 * angstrom diag[strain.ndof] *= abs(q0 - trajectory.targets[0]) sol = None for iq, target in enumerate(trajectory.targets): log.dump(' Frame %i (target=%.3f)' % (iq, target)) strain.constrain_target = target if abs(target - q0) < 1e-6: sol = np.zeros([strain.ndof + 1], float) strain.gradient(sol) else: if sol is not None: init = sol.copy() else: init = np.zeros([strain.ndof + 1], float) init[-1] = np.sign(q0 - target) sol, infodict, ier, mesg = scipy.optimize.fsolve( strain.gradient, init, xtol=1e-3, full_output=True, diag=diag) if ier != 1: #fsolve did not converge, flag this frame for deletion log.dump(' %s' % mesg.replace('\n', ' ')) log.dump( ' Frame %i (target=%.3f) %s(%s) did not converge. Trying again with slightly perturbed initial conditions.' % (iq, target, self.valence.terms[index].basename, trajectory.term.get_atoms())) #try one more time init = sol.copy() init[:3 * natom] += np.random.normal( 0.0, 0.01, [3 * natom]) * angstrom sol, infodict, ier, mesg = scipy.optimize.fsolve( strain.gradient, init, xtol=1e-3, full_output=True, diag=diag) if ier != 1: log.dump(' %s' % mesg.replace('\n', ' ')) log.dump( ' Frame %i (target=%.3f) %s(%s) STILL did not converge.' % (iq, target, self.valence.terms[index].basename, trajectory.term.get_atoms())) trajectory.targets[iq] = np.nan continue x = strain.coords0 + sol[:3 * natom].reshape((-1, 3)) trajectory.values[iq] = strain.constrain_value log.dump(' Converged (value=%.3f, lagmult=%.3e)' % (strain.constrain_value, sol[3 * natom])) if remove_com: com = (x.T * self.system.masses).sum( axis=1) / self.system.masses.sum() for i in xrange(natom): x[i, :] -= com trajectory.coords[iq, :, :] = x #delete flagged frames targets = [] values = [] coords = [] for target, value, coord in zip(trajectory.targets, trajectory.values, trajectory.coords): if not np.isnan(target): targets.append(target) values.append(value) coords.append(coord) trajectory.targets = np.array(targets) trajectory.values = np.array(values) trajectory.coords = np.array(coords) return trajectory
def init_oop_terms(self, thresshold_zero=5e-2 * angstrom): ''' Initialize all out-of-plane terms in the system based on the oops attribute of the system instance. All oops are given harmonic potentials. ''' with log.section('VAL', 3, 'Initializing'): #get all dihedrals from molmod.ic import opbend_dist, _opdist_low ffatypes = [ self.system.ffatypes[fid] for fid in self.system.ffatype_ids ] opdists = {} for opdist in self.system.iter_oops(): opdist, types = term_sort_atypes(ffatypes, opdist, 'opdist') if types in opdists.keys(): opdists[types].append(opdist) else: opdists[types] = [opdist] #loop over all distinct opdist types nharm = 0 nsq = 0 for types, oops in opdists.iteritems(): d0s = np.zeros(len(oops), float) for i, oop in enumerate(oops): if self.system.cell.nvec > 0: d01 = self.system.pos[oop[1]] - self.system.pos[oop[0]] d02 = self.system.pos[oop[2]] - self.system.pos[oop[0]] d03 = self.system.pos[oop[3]] - self.system.pos[oop[0]] self.system.cell.mic(d01) self.system.cell.mic(d02) self.system.cell.mic(d03) d0s[i] = abs(_opdist_low(d01, d02, d03, 0)[0]) else: rs = np.array( [ #mind the order, is(or was) wrongly documented in molmod self.system.pos[oop[0]], self.system.pos[oop[1]], self.system.pos[oop[2]], self.system.pos[oop[3]], ]) d0s[i] = abs(opbend_dist(rs)[0]) if d0s.mean() < thresshold_zero: #TODO: check this thresshold #add regular term harmonic in oopdist for oop in oops: term = self.add_term(Harmonic, [OopDist(*oop)], types, ['HC_FC_DIAG'], ['kjmol/A**2', 'A']) self.set_params(term.index, rv0=0.0) nharm += 1 else: #add term harmonic in square of oopdist log.dump( 'Mean absolute value of OopDist %s is %.3e A, used SQOOPDIST' % ('.'.join(types), d0s.mean() / angstrom)) for oop in oops: self.add_term(Harmonic, [SqOopDist(*oop)], types, ['PT_ALL', 'HC_FC_DIAG'], ['kjmol/A**4', 'A**2']) nsq += 1 log.dump( 'Added %i Harmonic and %i SquareHarmonic out-of-plane distance terms' % (nharm, nsq))
def do_hc_estimatefc(self, tasks, logger_level=3, do_svd=False, svd_rcond=0.0, do_mass_weighting=True): ''' Refine force constants using Hessian Cost function. **Arguments** tasks A list of strings identifying which terms should have their force constant estimated from the hessian cost function. Using such a flag, one can distinguish between for example force constant refinement (flag=HC_FC_DIAG) of the diagonal terms and force constant estimation of the cross terms (flag=HC_FC_CROSS). If the string 'all' is present in tasks, all fc's will be estimated. **Optional Arguments** logger_level print level at which the resulting parameters should be dumped to the logger. By default, the parameters will only be dumped at the highest log level. do_svd whether or not to do an SVD decomposition before solving the set of equations and explicitly throw out the degrees of freedom that correspond to the lowest singular values. do_mass_weighting whether or not to apply mass weighing to the ab initio hessian and the force field contributions before doing the fitting. ''' with log.section('HCEST', 2, timer='HC Estimate FC'): self.reset_system() log.dump('Estimating force constants from Hessian cost for tasks %s' %' '.join(tasks)) term_indices = [] for index in range(self.valence.vlist.nv): term = self.valence.terms[index] flagged = False for flag in tasks: if flag in term.tasks: flagged = True break if flagged: #first check if all rest values and multiplicities have been defined if term.kind==0: self.valence.check_params(term, ['rv']) if term.kind==1: self.valence.check_params(term, ['a0', 'a1', 'a2', 'a3']) if term.kind==3: self.valence.check_params(term, ['rv0','rv1']) if term.kind==4: self.valence.check_params(term, ['rv', 'm']) if term.is_master(): term_indices.append(index) else: #first check if all pars have been defined if term.kind==0: self.valence.check_params(term, ['fc', 'rv']) if term.kind==1: self.valence.check_params(term, ['a0', 'a1', 'a2', 'a3']) if term.kind==3: self.valence.check_params(term, ['fc', 'rv0','rv1']) if term.kind==4: self.valence.check_params(term, ['fc', 'rv', 'm']) if len(term_indices)==0: log.dump('No terms (with task in %s) found to estimate FC from HC' %(str(tasks))) return # Try to estimate force constants; if the remove_dysfunctional_cross # keyword is True, a loop is performed which checks whether there # are cross terms for which corresponding diagonal terms have zero # force constants. If this is the case, those cross terms are removed # from the fit and we try again until such cases do no longer occur max_iter = 100 niter = 0 while niter<max_iter: cost = HessianFCCost(self.system, self.ai, self.valence, term_indices, ffrefs=self.ffrefs, do_mass_weighting=do_mass_weighting) fcs = cost.estimate(do_svd=do_svd, svd_rcond=svd_rcond) # No need to continue, if cross terms with corresponding diagonal # terms with negative force constants are allowed if self.settings.remove_dysfunctional_cross is False: break to_remove = [] for index, fc in zip(term_indices, fcs): term = self.valence.terms[index] if term.basename.startswith('Cross'): # Find force constants of corresponding diagonal terms diag_fcs = np.zeros((2)) for idiag in range(2): diag_index = term.diag_term_indexes[idiag] if diag_index in term_indices: fc_diag = fcs[term_indices.index(diag_index)] else: fc_diag = self.valence.get_params(diag_index, only='fc') diag_fcs[idiag] = fc_diag # If a force constant from any corresponding diagonal term is negative, # we remove the cross term for the next iteration if np.any(diag_fcs<=0.0): to_remove.append(index) self.valence.set_params(index, fc=0.0) log.dump('WARNING! Dysfunctional cross term %s detected, removing from the hessian fit.'%term.basename) if len(to_remove)==0: break else: for index in to_remove: term_indices.remove(index) niter += 1 assert niter<max_iter, "Could not remove all dysfunctional cross terms in %d iterations, something is seriously wrong"%max_iter for index, fc in zip(term_indices, fcs): master = self.valence.terms[index] assert master.is_master() self.valence.set_params(index, fc=fc) for islave in master.slaves: self.valence.set_params(islave, fc=fc) self.valence.dump_logger(print_level=logger_level)
def dump_log(self): sorted_keys = sorted(self.__dict__.keys()) with log.section('SETT', 3): for key in sorted_keys: value = str(self.__dict__[key]) log.dump('%s %s' %(key+' '*(30-len(key)), value))
def do_hc_estimatefc(self, tasks, logger_level=3, do_svd=False, do_mass_weighting=True): ''' Refine force constants using Hessian Cost function. **Arguments** tasks A list of strings identifying which terms should have their force constant estimated from the hessian cost function. Using such a flag, one can distinguish between for example force constant refinement (flag=HC_FC_DIAG) of the diagonal terms and force constant estimation of the cross terms (flag=HC_FC_CROSS). If the string 'all' is present in tasks, all fc's will be estimated. **Optional Arguments** logger_level print level at which the resulting parameters should be dumped to the logger. By default, the parameters will only be dumped at the highest log level. do_svd whether or not to do an SVD decomposition before solving the set of equations and explicitly throw out the degrees of freedom that correspond to the lowest singular values. do_mass_weighting whether or not to apply mass weighing to the ab initio hessian and the force field contributions before doing the fitting. ''' with log.section('HCEST', 2, timer='HC Estimate FC'): self.reset_system() log.dump( 'Estimating force constants from Hessian cost for tasks %s' % ' '.join(tasks)) term_indices = [] for index in range(self.valence.vlist.nv): term = self.valence.terms[index] flagged = False for flag in tasks: if flag in term.tasks: flagged = True break if flagged: #first check if all rest values and multiplicities have been defined if term.kind == 0: self.valence.check_params(term, ['rv']) if term.kind == 1: self.valence.check_params(term, ['a0', 'a1', 'a2', 'a3']) if term.kind == 3: self.valence.check_params(term, ['rv0', 'rv1']) if term.kind == 4: self.valence.check_params(term, ['rv', 'm']) if term.is_master(): term_indices.append(index) else: #first check if all pars have been defined if term.kind == 0: self.valence.check_params(term, ['fc', 'rv']) if term.kind == 1: self.valence.check_params(term, ['a0', 'a1', 'a2', 'a3']) if term.kind == 3: self.valence.check_params(term, ['fc', 'rv0', 'rv1']) if term.kind == 4: self.valence.check_params(term, ['fc', 'rv', 'm']) if len(term_indices) == 0: log.dump( 'No terms (with task in %s) found to estimate FC from HC' % (str(tasks))) return cost = HessianFCCost(self.system, self.ai, self.valence, term_indices, ffrefs=self.ffrefs, do_mass_weighting=do_mass_weighting) fcs = cost.estimate(do_svd=do_svd) for index, fc in zip(term_indices, fcs): master = self.valence.terms[index] assert master.is_master() self.valence.set_params(index, fc=fc) for islave in master.slaves: self.valence.set_params(islave, fc=fc) self.valence.dump_logger(print_level=logger_level)
def __init__(self, system, ai, **kwargs): ''' **Arguments** system a Yaff `System` object defining the system ai a `Reference` instance corresponding to the ab initio input data **Keyword Arguments** ffrefs a list of `Reference` objects corresponding to a priori determined contributions to the force field (such as eg. electrostatics or van der Waals contributions) fn_yaff the name of the file to write the final parameters to in Yaff format. The default is `pars.txt`. fn_charmm22_prm the name of a CHARMM parameter file. If not given, the file is not written fn_charmm22_psf the name of a CHARMM topology file. If not given, the file is not written fn_sys the name of the file to write the system to. The default is `system.chk`. fn_traj a cPickle filename to read/write the perturbation trajectories from/to. If the file exists, the trajectories are read from the file. If the file does not exist, the trajectories are written to the file. only_traj specifier to determine for which terms a perturbation trajectory needs to be constructed. If ONLY_TRAJ is a single string, it is interpreted as a task (only terms that have this task in their tasks attribute will get a trajectory). If ONLY_TRAJ is a list of strings, each string is interpreted as the basename of the term for which a trajectory will be constructed. plot_traj if set to True, all energy contributions along each perturbation trajectory will be plotted using the final force field. xyz_traj if set to True, each perturbation trajectory will be written to an XYZ file. ''' with log.section('PROG', 2, timer='Initializing'): log.dump('Initializing program') self.system = system self.ai = ai self.kwargs = kwargs self.valence = ValenceFF(system) self.perturbation = RelaxedStrain(system, self.valence) self.trajectories = None
def init_dihedral_terms(self, thresshold=20 * deg): ''' Initialize the dihedral potentials from the local topology. The dihedral potential will be one of the two following possibilities: The multiplicity m is determined from the local topology, i.e. the number of neighbors of the central two atoms in the dihedral If the equilibrium value of all instances of the torsion are within `thresshold` of 0 deg or per/2 with per = 180deg/m, the following potential will be chosen: 0.5*K*(1-cos(m*psi-m*psi0)) with psi0 = 0 or 360/(2*m) ''' with log.section('VAL', 3, 'Initializing'): #get all dihedrals from molmod.ic import dihed_angle, _dihed_angle_low ffatypes = [ self.system.ffatypes[fid] for fid in self.system.ffatype_ids ] dihedrals = {} for dihedral in self.system.iter_dihedrals(): dihedral, types = term_sort_atypes(ffatypes, dihedral, 'dihedral') if types in dihedrals.keys(): dihedrals[types].append(dihedral) else: dihedrals[types] = [dihedral] #loop over all distinct dihedral types ncos = 0 for types, diheds in dihedrals.iteritems(): psi0s = np.zeros(len(diheds), float) ms = np.zeros(len(diheds), float) for i, dihed in enumerate(diheds): if self.system.cell.nvec > 0: d10 = self.system.pos[dihed[0]] - self.system.pos[ dihed[1]] d12 = self.system.pos[dihed[2]] - self.system.pos[ dihed[1]] d23 = self.system.pos[dihed[3]] - self.system.pos[ dihed[2]] self.system.cell.mic(d10) self.system.cell.mic(d12) self.system.cell.mic(d23) psi0s[i] = _dihed_angle_low(d10, d12, d23, 0)[0] else: rs = np.array([self.system.pos[j] for j in dihed]) psi0s[i] = dihed_angle(rs)[0] n1 = len(self.system.neighs1[dihed[1]]) n2 = len(self.system.neighs1[dihed[2]]) ms[i] = get_multiplicity(n1, n2) nan = False for m in ms: if np.isnan(m): nan = True if nan or None in ms or ms.std() > 1e-3: ms_string = str(ms) if nan: ms_string = 'nan' log.warning('missing dihedral for %s (m is %s)' % ('.'.join(types), ms_string)) continue m = int(np.round(ms.mean())) rv = get_restvalue(psi0s, m, thresshold=thresshold, mode=1) if rv is not None: #a regular Cosine term is used for the dihedral potential for dihed in diheds: term = self.add_term(Cosine, [DihedAngle(*dihed)], types, ['HC_FC_DIAG'], ['au', 'kjmol', 'deg']) self.set_params(term.index, rv0=rv, m=m) ncos += 1 else: #no dihedral potential could be determine, hence it is ignored log.warning( 'missing dihedral for %s (could not determine rest value from %s)' % ('.'.join(types), str(psi0s / deg))) continue log.dump('Added %i Cosine dihedral terms' % ncos)
def estimate(self, trajectory, ai, ffrefs=[], do_valence=False, energy_noise=None, Nerrorsteps=100): ''' Method to estimate the FF parameters for the relevant ic from the given perturbation trajectory by fitting a harmonic potential to the covalent energy along the trajectory. **Arguments** trajectory a Trajectory instance representing the perturbation trajectory ai an instance of the Reference representing the ab initio input **Optional Arguments** ffrefs a list of Reference instances representing possible a priori determined contributions to the force field (such as eg. electrostatics and van der Waals) do_valence If set to True, the current valence force field (stored in self.valence) will be used to compute the valence contribution energy_noise If set to a float, the parabolic fitting will be repeated Nerrorsteps times including normal noise on top of the reference value. The mean of the noise is 0, while the std equals the number given by energy_noise. The resulting fits give a distribution of force constants and rest values instead of single value, the std is used to identify bad estimates, the mean is used for the actual FF parametrs. If set to nan, the parabolic fit is performed only once without any noise. ''' with log.section('PTEST', 3, timer='PT Estimate'): term = trajectory.term index = term.index basename = term.basename if 'active' in list(trajectory.__dict__.keys()) and not trajectory.active: log.dump('Trajectory of %s was deactivated: skipping' %(basename)) return qs = trajectory.values.copy() AIs = np.zeros(len(trajectory.coords)) FFs = np.zeros(len(trajectory.coords)) RESs = np.zeros(len(trajectory.coords)) for istep, pos in enumerate(trajectory.coords): AIs[istep] = ai.energy(pos) for ref in ffrefs: FFs[istep] += ref.energy(pos) if do_valence: fc = self.valence.get_params(index, only='fc') rv = self.valence.get_params(index, only='rv') self.valence.set_params(index, fc=0.0) self.valence.set_params(index, rv0=0.0) for istep, pos in enumerate(trajectory.coords): RESs[istep] += self.valence.calc_energy(pos) #- 0.5*fc*(qs[istep]-rv)**2 self.valence.set_params(index, fc=fc) self.valence.set_params(index, rv0=rv) pars = fitpar(qs, AIs-FFs-RESs-min(AIs-FFs-RESs), rcond=-1) if energy_noise is None: if pars[0]>0.0: #!=0.0: trajectory.fc = 2.0*pars[0] trajectory.rv = -pars[1]/(2.0*pars[0]) else: trajectory.fc = 0.0 trajectory.rv = qs[len(qs)//2] log.dump('force constant of %s is not positive: force constant set to zero and rest value set to ab initio equilibrium' %basename) else: with log.section('PTEST', 4, timer='PT Estimate'): log.dump('Performing noise analysis for trajectory of %s' %basename) As = [pars[0]] Bs = [pars[1]] for i in range(Nerrorsteps): pars = fitpar(qs, AIs-FFs-RESs-min(AIs-FFs-RESs)+np.random.normal(0.0, energy_noise, size=AIs.shape), rcond=-1) As.append(pars[0]) Bs.append(pars[1]) if 0.0 in As: log.dump(' force constant of zero detected, removing the relevant runs from analysis') Bs = np.array([b for a,b in zip(As,Bs) if a!=0.0]) As = np.array([a for a in As if a!=0.0]) ks = As*2.0 q0s = -Bs/(2.0*As) kunit = trajectory.term.units[0] qunit = trajectory.term.units[1] log.dump(' k = %8.3f +- %6.3f (noisefree: %8.3f) %s' %(ks.mean()/parse_unit(kunit), ks.std()/parse_unit(kunit), ks[0]/parse_unit(kunit), kunit)) log.dump(' q0 = %8.3f +- %6.3f (noisefree: %8.3f) %s' %(q0s.mean()/parse_unit(qunit), q0s.std()/parse_unit(qunit), q0s[0]/parse_unit(qunit), qunit)) if q0s.std()/q0s.mean()>0.01: with log.section('PTEST', 3, timer='PT Estimate'): fc, rv = self.valence.get_params(trajectory.term.index) if rv is None: log.dump('Noise on rest value of %s to high, using ab initio rest value' %basename) pars = fitpar(qs, AIs-FFs-RESs-min(AIs-FFs-RESs)+np.random.normal(0.0, energy_noise, size=AIs.shape), rcond=-1) if pars[0]!=0.0: trajectory.fc = 2.0*pars[0] trajectory.rv = -pars[1]/(2.0*pars[0]) else: trajectory.fc = 0.0 trajectory.rv = qs[len(qs)//2] log.dump('AI force constant of %s is zero: rest value set to middle value' %basename) else: log.dump('Noise on rest value of %s to high, using previous value' %basename) trajectory.fc = fc trajectory.rv = rv else: trajectory.fc = ks.mean() trajectory.rv = q0s.mean() #no negative rest values for all ics except dihedrals and bendcos if term.ics[0].kind not in [1,3,4,11]: if trajectory.rv<0: trajectory.rv = 0.0 log.dump('rest value of %s was negative: set to zero' %basename)
def main(): options, fns = parse() #define logger if options.silent: log.set_level('silent') else: if options.very_verbose: log.set_level('highest') elif options.verbose: log.set_level('high') if options.logfile is not None and isinstance(options.logfile, str): log.write_to_file(options.logfile) with log.section('QFF', 1, timer='Initializing'): log.dump('Initializing system') #read system and ab initio reference system = None energy = 0.0 grad = None hess = None rvecs = None for fn in fns: if fn.endswith('.fchk') or fn.endswith('.xml'): numbers, coords, energy, grad, hess, masses, rvecs, pbc = read_abinitio( fn) if system is None: system = System(numbers, coords, rvecs=rvecs, charges=None, radii=None, masses=masses) else: system.pos = coords.copy() system.cell = Cell(rvecs) system.numbers = numbers.copy() if masses is not None: system.masses = masses.copy() system._init_derived() elif fn.endswith('.chk'): sample = load_chk(fn) if 'energy' in sample.keys(): energy = sample['energy'] if 'grad' in sample.keys(): grad = sample['grad'] elif 'gradient' in sample.keys(): grad = sample['gradient'] if 'hess' in sample.keys(): hess = sample['hess'] elif 'hessian' in sample.keys(): hess = sample['hessian'] if system is None: system = System.from_file(fn) else: if 'pos' in sample.keys(): system.pos = sample['pos'] elif 'coords' in sample.keys(): system.pos = sample['coords'] if 'rvecs' in sample.keys(): system.cell = Cell(sample['rvecs']) elif 'cell' in sample.keys(): system.cell = Cell(sample['cell']) if 'bonds' in sample.keys(): system.bonds = sample['bonds'] if 'ffatypes' in sample.keys(): system.ffatypes = sample['ffatypes'] if 'ffatype_ids' in sample.keys(): system.ffatype_ids = sample['ffatype_ids'] system._init_derived() else: raise NotImplementedError('File format for %s not supported' % fn) assert system is not None, 'No system could be defined from input' assert grad is not None, 'No ab initio gradient found in input' assert hess is not None, 'No ab initio hessian found in input' #complete the system information if system.bonds is None: system.detect_bonds() if system.masses is None: system.set_standard_masses() if system.ffatypes is None: if options.ffatypes in ['low', 'medium', 'high', 'highest']: guess_ffatypes(system, options.ffatypes) elif options.ffatypes is not None: raise NotImplementedError( 'Guessing atom types from %s not implemented' % options.ffatypes) else: raise AssertionError('No atom types defined') #construct ab initio reference ai = SecondOrderTaylor('ai', coords=system.pos.copy(), energy=energy, grad=grad, hess=hess, pbc=pbc) #detect a priori defined contributions to the force field refs = [] if options.ei is not None: if rvecs is None: ff = ForceField.generate(system, options.ei, rcut=50 * angstrom) else: ff = ForceField.generate(system, options.ei, rcut=20 * angstrom, alpha_scale=3.2, gcut_scale=1.5, smooth_ei=True) refs.append(YaffForceField('EI', ff)) if options.vdw is not None: ff = ForceField.generate(system, options.vdw, rcut=20 * angstrom) refs.append(YaffForceField('vdW', ff)) if options.covres is not None: ff = ForceField.generate(system, options.covres) refs.append(YaffForceField('Cov res', ff)) #define quickff program assert options.program_mode in allowed_programs, \ 'Given program mode %s not allowed. Choose one of %s' %( options.program_mode, ', '.join([prog for prog in allowed_programs if not prog=='BaseProgram']) ) mode = program_modes[options.program_mode] only_traj = 'PT_ALL' if options.only_traj is not None: only_traj = options.only_traj.split(',') program = mode(system, ai, ffrefs=refs, fn_traj=options.fn_traj, only_traj=only_traj, plot_traj=options.ener_traj, xyz_traj=options.xyz_traj, suffix=options.suffix) #run program program.run()
def estimate(self, trajectory, ai, ffrefs=[], do_valence=False): ''' Method to estimate the FF parameters for the relevant ic from the given perturbation trajectory by fitting a harmonic potential to the covalent energy along the trajectory. **Arguments** trajectory a Trajectory instance representing the perturbation trajectory ai an instance of the Reference representing the ab initio input **Optional Arguments** ffrefs a list of Reference instances representing possible a priori determined contributions to the force field (such as eg. electrostatics and van der Waals) do_valence If set to True, the current valence force field (stored in self.valence) will be used to compute the valence contribution ''' with log.section('PTEST', 3, timer='PT Estimate'): term = trajectory.term index = term.index basename = term.basename if 'active' in trajectory.__dict__.keys( ) and not trajectory.active: log.dump('Trajectory of %s was deactivated: skipping' % (basename)) return qs = trajectory.values.copy() AIs = np.zeros(len(trajectory.coords)) FFs = np.zeros(len(trajectory.coords)) RESs = np.zeros(len(trajectory.coords)) for istep, pos in enumerate(trajectory.coords): AIs[istep] = ai.energy(pos) for ref in ffrefs: FFs[istep] += ref.energy(pos) if do_valence: fc = self.valence.get_params(index, only='fc') rv = self.valence.get_params(index, only='rv') self.valence.set_params(index, fc=0.0) self.valence.set_params(index, rv0=0.0) for istep, pos in enumerate(trajectory.coords): RESs[istep] += self.valence.calc_energy( pos) #- 0.5*fc*(qs[istep]-rv)**2 self.valence.set_params(index, fc=fc) self.valence.set_params(index, rv0=rv) pars = fitpar(qs, AIs - FFs - RESs - min(AIs - FFs - RESs), rcond=-1) if pars[0] != 0.0: trajectory.fc = 2.0 * pars[0] trajectory.rv = -pars[1] / (2.0 * pars[0]) else: trajectory.fc = 0.0 trajectory.rv = qs[len(qs) / 2] log.dump( 'force constant of %s is zero: rest value set to middle value' % basename) #no negative rest values for all ics except dihedrals and bendcos if term.ics[0].kind not in [1, 3, 4, 11]: if trajectory.rv < 0: trajectory.rv = 0.0 log.dump('rest value of %s was negative: set to zero' % basename)
def qff(args=None): if args is None: args = qff_parse_args() else: args = qff_parse_args(args) #define logger verbosity = None if args.silent: verbosity = 'silent' else: if args.very_verbose: verbosity = 'highest' elif args.verbose: verbosity = 'high' #get settings kwargs = { 'fn_traj': args.fn_traj, 'only_traj': args.only_traj, 'program_mode': args.program_mode, 'plot_traj': args.plot_traj, 'xyz_traj': args.xyz_traj, 'suffix': args.suffix, 'log_level': verbosity, 'log_file': args.logfile, 'ffatypes': args.ffatypes, 'ei': args.ei, 'ei_rcut': args.ei_rcut, 'vdw': args.vdw, 'vdw_rcut': args.vdw_rcut, 'covres': args.covres, } settings = Settings(fn=args.config_file, **kwargs) with log.section('INIT', 1, timer='Initializing'): log.dump('Initializing system') #read system and ab initio reference system = None energy = 0.0 grad = None hess = None pbc = None rvecs = None for fn in args.fn: if fn.endswith('.fchk') or fn.endswith('.xml'): numbers, coords, energy, grad, hess, masses, rvecs, pbc = read_abinitio( fn) if system is None: system = System(numbers, coords, rvecs=rvecs, charges=None, radii=None, masses=masses) else: system.pos = coords.copy() system.cell = Cell(rvecs) system.numbers = numbers.copy() if masses is not None: system.masses = masses.copy() system._init_derived() elif fn.endswith('.chk'): sample = load_chk(fn) if 'energy' in list(sample.keys()): energy = sample['energy'] if 'grad' in list(sample.keys()): grad = sample['grad'] elif 'gradient' in list(sample.keys()): grad = sample['gradient'] if 'hess' in list(sample.keys()): hess = sample['hess'] elif 'hessian' in list(sample.keys()): hess = sample['hessian'] if 'rvecs' in list(sample.keys()): pbc = [1, 1, 1] else: pbc = [0, 0, 0] if system is None: system = System.from_file(fn) else: if 'pos' in list(sample.keys()): system.pos = sample['pos'] elif 'coords' in list(sample.keys()): system.pos = sample['coords'] if 'rvecs' in list(sample.keys()): system.cell = Cell(sample['rvecs']) elif 'cell' in list(sample.keys()): system.cell = Cell(sample['cell']) if 'bonds' in list(sample.keys()): system.bonds = sample['bonds'] if 'ffatypes' in list(sample.keys()): system.ffatypes = sample['ffatypes'] if 'ffatype_ids' in list(sample.keys()): system.ffatype_ids = sample['ffatype_ids'] system._init_derived() else: raise NotImplementedError('File format for %s not supported' % fn) assert system is not None, 'No system could be defined from input' assert grad is not None, 'No ab initio gradient found in input' assert hess is not None, 'No ab initio hessian found in input' #complete the system information if system.bonds is None: system.detect_bonds() if system.masses is None: system.set_standard_masses() if system.ffatypes is None: if settings.ffatypes is not None: set_ffatypes(system, settings.ffatypes) else: raise AssertionError('No atom types defined') if settings.do_hess_negfreq_proj: log.dump( 'Projecting negative frequencies out of the mass-weighted hessian.' ) with log.section('SYS', 3, 'Initializing'): hess = project_negative_freqs(hess, system.masses) #construct ab initio reference ai = SecondOrderTaylor('ai', coords=system.pos.copy(), energy=energy, grad=grad, hess=hess, pbc=pbc) #detect a priori defined contributions to the force field refs = [] if settings.ei is not None: if rvecs is None: if settings.ei_rcut is None: rcut = 50 * angstrom else: rcut = settings.ei_rcut ff = ForceField.generate(system, settings.ei, rcut=rcut) else: if settings.ei_rcut is None: rcut = 20 * angstrom else: rcut = settings.ei_rcut ff = ForceField.generate(system, settings.ei, rcut=rcut, alpha_scale=3.2, gcut_scale=1.5, smooth_ei=True) refs.append(YaffForceField('EI', ff)) if settings.vdw is not None: ff = ForceField.generate(system, settings.vdw, rcut=settings.vdw_rcut) refs.append(YaffForceField('vdW', ff)) if settings.covres is not None: ff = ForceField.generate(system, settings.covres) refs.append(YaffForceField('Cov res', ff)) #define quickff program assert settings.program_mode in allowed_programs, \ 'Given program mode %s not allowed. Choose one of %s' %( settings.program_mode, ', '.join([prog for prog in allowed_programs if not prog=='BaseProgram']) ) mode = program_modes[settings.program_mode] program = mode(system, ai, settings, ffrefs=refs) #run program program.run() return program
def __init__(self, name, ff): log.dump('Initializing Yaff force field reference for %s' %name) self.ff = ff Reference.__init__(self, name)
def estimate(self, trajectory, ai, ffrefs=[], do_valence=False, energy_noise=None, Nerrorsteps=100): ''' Method to estimate the FF parameters for the relevant ic from the given perturbation trajectory by fitting a harmonic potential to the covalent energy along the trajectory. **Arguments** trajectory a Trajectory instance representing the perturbation trajectory ai an instance of the Reference representing the ab initio input **Optional Arguments** ffrefs a list of Reference instances representing possible a priori determined contributions to the force field (such as eg. electrostatics and van der Waals) do_valence If set to True, the current valence force field (stored in self.valence) will be used to compute the valence contribution energy_noise If set to a float, the parabolic fitting will be repeated Nerrorsteps times including normal noise on top of the reference value. The mean of the noise is 0, while the std equals the number given by energy_noise. The resulting fits give a distribution of force constants and rest values instead of single value, the std is used to identify bad estimates, the mean is used for the actual FF parametrs. If set to nan, the parabolic fit is performed only once without any noise. ''' with log.section('PTEST', 3, timer='PT Estimate'): term = trajectory.term index = term.index basename = term.basename if 'active' in list(trajectory.__dict__.keys()) and not trajectory.active: log.dump('Trajectory of %s was deactivated: skipping' %(basename)) return qs = trajectory.values.copy() AIs = np.zeros(len(trajectory.coords)) FFs = np.zeros(len(trajectory.coords)) RESs = np.zeros(len(trajectory.coords)) for istep, pos in enumerate(trajectory.coords): AIs[istep] = ai.energy(pos) for ref in ffrefs: FFs[istep] += ref.energy(pos) if do_valence: fc = self.valence.get_params(index, only='fc') rv = self.valence.get_params(index, only='rv') self.valence.set_params(index, fc=0.0) self.valence.set_params(index, rv0=0.0) for istep, pos in enumerate(trajectory.coords): RESs[istep] += self.valence.calc_energy(pos) #- 0.5*fc*(qs[istep]-rv)**2 self.valence.set_params(index, fc=fc) self.valence.set_params(index, rv0=rv) pars = fitpar(qs, AIs-FFs-RESs-min(AIs-FFs-RESs), rcond=-1) if energy_noise is None: if pars[0]!=0.0: trajectory.fc = 2.0*pars[0] trajectory.rv = -pars[1]/(2.0*pars[0]) else: trajectory.fc = 0.0 trajectory.rv = qs[len(qs)//2] log.dump('force constant of %s is zero: rest value set to middle value' %basename) else: with log.section('PTEST', 4, timer='PT Estimate'): log.dump('Performing noise analysis for trajectory of %s' %basename) As = [pars[0]] Bs = [pars[1]] for i in range(Nerrorsteps): pars = fitpar(qs, AIs-FFs-RESs-min(AIs-FFs-RESs)+np.random.normal(0.0, energy_noise, size=AIs.shape), rcond=-1) As.append(pars[0]) Bs.append(pars[1]) if 0.0 in As: log.dump(' force constant of zero detected, removing the relevant runs from analysis') Bs = np.array([b for a,b in zip(As,Bs) if a!=0.0]) As = np.array([a for a in As if a!=0.0]) ks = As*2.0 q0s = -Bs/(2.0*As) kunit = trajectory.term.units[0] qunit = trajectory.term.units[1] log.dump(' k = %8.3f +- %6.3f (noisefree: %8.3f) %s' %(ks.mean()/parse_unit(kunit), ks.std()/parse_unit(kunit), ks[0]/parse_unit(kunit), kunit)) log.dump(' q0 = %8.3f +- %6.3f (noisefree: %8.3f) %s' %(q0s.mean()/parse_unit(qunit), q0s.std()/parse_unit(qunit), q0s[0]/parse_unit(qunit), qunit)) if q0s.std()/q0s.mean()>0.01: with log.section('PTEST', 3, timer='PT Estimate'): fc, rv = self.valence.get_params(trajectory.term.index) if rv is None: log.dump('Noise on rest value of %s to high, using ab initio rest value' %basename) pars = fitpar(qs, AIs-FFs-RESs-min(AIs-FFs-RESs)+np.random.normal(0.0, energy_noise, size=AIs.shape), rcond=-1) if pars[0]!=0.0: trajectory.fc = 2.0*pars[0] trajectory.rv = -pars[1]/(2.0*pars[0]) else: trajectory.fc = 0.0 trajectory.rv = qs[len(qs)//2] log.dump('AI force constant of %s is zero: rest value set to middle value' %basename) else: log.dump('Noise on rest value of %s to high, using previous value' %basename) trajectory.fc = fc trajectory.rv = rv else: trajectory.fc = ks.mean() trajectory.rv = q0s.mean() #no negative rest values for all ics except dihedrals and bendcos if term.ics[0].kind not in [1,3,4,11]: if trajectory.rv<0: trajectory.rv = 0.0 log.dump('rest value of %s was negative: set to zero' %basename)