""" The state module. """importosimportmathimportconfigparserimportlogginglogger=logging.getLogger('state')importnumpyfromeon.configimportconfigfromeonimportatomsfromeonimportfileioasiofromeonimportstate
[docs]classAKMCState(state.State):ID,ENERGY,PREFACTOR,PRODUCT,PRODUCT_ENERGY,PRODUCT_PREFACTOR,BARRIER,RATE,REPEATS=list(range(9))processtable_head_fmt="%7s%16s%11s%9s%16s%17s%8s%12s%7s\n"processtable_header=processtable_head_fmt%("proc #","saddle energy","prefactor","product","product energy","product prefactor","barrier","rate","repeats")processtable_line="%7d%16.5f%11.5e%9d%16.5f%17.5e%8.5f%12.5e%7d\n"def__init__(self,statepath,statenumber,statelist,previous_state_num=-1,reactant_path=None):""" Creates a new State, with lazily loaded data. """ifconfig.akmc_server_side_process_search:self.search_result_header="%8s%10s%10s%10s%10s%10s%10s%s\n"%("searchid","type","barrier","max-dist","sad-fcs","mins-fcs","pref-fcs","result")else:self.search_result_header="%8s%10s%10s%10s%10s%10s%10s%s\n"%("wuid","type","barrier","max-dist","sad-fcs","mins-fcs","pref-fcs","result")self.search_result_header+="-"*len(self.search_result_header)+'\n'state.State.__init__(self,statepath,statenumber,statelist,previous_state_num,reactant_path)self.bad_procdata_path=os.path.join(self.path,"badprocdata")self.con_cache={}
[docs]defadd_process(self,result,superbasin=None):""" Adds a process to this State. """state.State.add_process(self,result)self.set_good_saddle_count(self.get_good_saddle_count()+1)resultdata=result["results"]#The information from the result.dat fileif'simulation_time'inresultdata:self.increment_time(resultdata['simulation_time'],resultdata['md_temperature'])# We may not already have the energy for this State. If not, it should be placed in the result data.ifself.get_energy()isNone:# This energy now defines the reference energy for the stateself.set_energy(resultdata["potential_energy_reactant"])reactant_energy=self.get_energy()# Calculate the forward barrier for this process, and abort if the energy is too high.oldlowest=self.get_lowest_barrier()barrier=resultdata["potential_energy_saddle"]-reactant_energylowest=self.update_lowest_barrier(barrier)ediff=(barrier-lowest)-(self.statelist.kT*(self.statelist.thermal_window+self.statelist.max_thermal_window))ifediff>0.0:self.append_search_result(result,"barrier > max_thermal_window",superbasin)returnNone# Determine the number of processes in the process table that have a similar energy.id=self.find_repeat(result["saddle.con"],barrier)ifid!=None:self.append_search_result(result,"repeat-%d"%id,superbasin)self.procs[id]['repeats']+=1self.save_process_table()ifresult['type']=="random"orresult['type']=="dynamics":self.inc_proc_random_count(id)# Do not increase repeats if we are currently in a# superbasin and the process does not lead out of it;# or if the process barrier is outside the thermal window.ifidinself.get_relevant_procids(superbasin):self.inc_repeats()if'simulation_time'inresultdata:current_time=self.get_time()logger.debug("event %3i found at time %f fs"%(id,current_time))returnNone# This appears to be a unique process.# Check if the mode, reactant, saddle, and product are legittry:if'mode'notinresult:io.load_mode(result['mode.dat'])if'reactant'notinresult:io.loadcon(result['reactant.con'])if'saddle'notinresult:io.loadcon(result['saddle.con'])if'product'notinresult:io.loadcon(result['product.con'])except:logger.exception("Mode, reactant, saddle, or product has incorrect format")returnNone# Reset the repeat count.self.reset_repeats()# Respond to finding a new lowest barrier.self.set_unique_saddle_count(self.get_unique_saddle_count()+1)ifbarrier==lowestandbarrier<oldlowest-self.statelist.epsilon_e:logger.info("Found new lowest barrier %f for state %i (type: %s)",lowest,self.number,result['type'])logger.info("Found new barrier %f for state %i (type: %s)",barrier,self.number,result['type'])# Update the search result table.self.append_search_result(result,"good-%d"%self.get_num_procs(),superbasin)# The id of this process is the number of processes.id=self.get_num_procs()if'simulation_time'inresultdata:current_time=self.get_time()logger.debug("new event %3i found at time %f fs"%(id,current_time))# Move the relevant files into the procdata directory.open(self.proc_reactant_path(id),'w').writelines(result['reactant.con'].getvalue())open(self.proc_mode_path(id),'w').writelines(result['mode.dat'].getvalue())open(self.proc_product_path(id),'w').writelines(result['product.con'].getvalue())open(self.proc_saddle_path(id),'w').writelines(result['saddle.con'].getvalue())open(self.proc_results_path(id),'w').writelines(result['results.dat'].getvalue())# Set maximum rate, if defined# forward_rate = resultdata["prefactor_reactant_to_product"] * math.exp(-barrier / self.statelist.kT)# if config.akmc_max_rate > 0 and cur_rate > config.akmc_max_rate:# # print "max rate exceeded: ", cur_rate# forward_rate = config.akmc_max_rate# Set equilibrium rate, if definedforward_rate=resultdata["prefactor_reactant_to_product"]*math.exp(-barrier/self.statelist.kT)reverse_barrier=barrier-(resultdata["potential_energy_product"]-reactant_energy)reverse_rate=resultdata["prefactor_product_to_reactant"]*math.exp(-reverse_barrier/self.statelist.kT)eq_rate_flag=Falseifconfig.akmc_eq_rate>0andforward_rate>config.akmc_eq_rateandreverse_rate>config.akmc_eq_rate:eq_rate_flag=True#print "eq_rate exceeded, forward:", forward_rate, " reverse: ", reverse_rateifforward_rate<reverse_rate:forward_eq_rate=config.akmc_eq_ratereverse_eq_rate=config.akmc_eq_rate*(reverse_rate/forward_rate)else:forward_eq_rate=config.akmc_eq_rate*(forward_rate/reverse_rate)reverse_eq_rate=config.akmc_eq_rate#print "new eq forward rate:", forward_eq_rate, " reverse: ", reverse_eq_rate# Append this barrier to the process table (in memory and on disk).self.append_process_table(id=id,saddle_energy=resultdata["potential_energy_saddle"],prefactor=resultdata["prefactor_reactant_to_product"],product=-1,product_energy=resultdata["potential_energy_product"],product_prefactor=resultdata["prefactor_product_to_reactant"],barrier=barrier,rate=resultdata["prefactor_reactant_to_product"]*math.exp(-barrier/self.statelist.kT),# rate = forward_rate,repeats=0)# If equilibrium rate, change the forward rate as wellifeq_rate_flag:self.procs[id]['rate']=forward_eq_rate# reverse_procs[id]['rate'] = reverse_eq_rate# If this is a random search type, add this proc to the random proc dict.ifresult['type']=="random"orresult['type']=="dynamics":self.inc_proc_random_count(id)# This was a unique process, so return the id.returnid
#except:# logger.warning("Failed to append search result.")
[docs]defget_ratetable(self,superbasin=None):""" Loads the process table if it has not been loaded and generates a rate table according to kT and thermal_window. """self.load_process_table()lowest=self.get_lowest_barrier()# Maximum barrier according to thermal window.max_barrier=lowest+self.statelist.kT*self.statelist.thermal_windowrt=[(id,proc['rate'],proc['prefactor'])forid,procinlist(self.procs.items())ifproc['barrier']<=max_barrier]ifnotsuperbasin:returnrtelse:# Filter out processes that lead to another state in the superbasin.return[entryforentryinrtifself.procs[entry[0]]["product"]notinsuperbasin.state_dict]
[docs]defget_process_table(self):"""Return dictionary of processes (id->proc) inside thermal window."""returndict([id,self.procs[id]]foridinself.get_relevant_procids())
[docs]defget_relevant_procids(self,superbasin=None):"""Get process IDs inside thermal window."""return[entry[0]forentryinself.get_ratetable(superbasin)]
[docs]defget_confidence(self,superbasin=None):"""Confidence that all relevant processes for this state were found. The confidence is a function of the ratio Nf/Ns, where Nf is the number of unique processes and Ns is the number of searches performed. When Nf or Ns are zero, there is no confidence. Zero is returned in this case. As the ratio Nf/Ns decreases, the confidence increases from 0.0 to a limit of 1.0. This is roughly equivalent to the statement: I am more confident that I have found all relevant processes if I have found 10 processes after 1000 searches than if I have found 900 processes after 1000 searches. Nf is calculated to be the number of processes in the rate table. Ns is calculated to be the number of searches that resulted in a process on the rate table. When using recycling or kdb, it is useful to ignore processes that occur outside the hole, the region in which the last process took place. Focusing on this region means you can do possibly far fewer searches to reach confidence. When using the hole to filter processes, Nf and Ns only take into account processes that intersect the hole. If superbasin is passed, that means the state is in that superbasin. The confidence calculation is adjusted so that only processes that lead out of the superbasin are counted. This may be disabled by the config.sb_superbasin_confidence option. """# Possibly disable superbasin feature.#if not config.sb_superbasin_confidence:try:ifnotconfig.sb_superbasin_confidence:superbasin=NoneexceptAttributeError:superbasin=None# Checking to see if all recycling jobs are completeifconfig.recycling_onandconfig.disp_moved_only:job_table_path=os.path.join(config.path_root,"jobs.tbl")job_table=io.Table(job_table_path)ifany([t=='recycling'fortinjob_table.get_column('type')]):return0.0# Load the rate table. If we are in a superbasin, we filter# out all processes leading to another state in the same# superbasin.rt=self.get_ratetable(superbasin)# Load the repeat counts and filter if we are in a superbasin.prc=self.get_proc_random_count()ifsuperbasin:prc=dict([proc,count]forproc,countinlist(prc.items())ifself.procs[proc]["product"]notinsuperbasin.state_dict)alpha=1.0ifconfig.akmc_confidence_correction:mn=1e300mx=0forrinrt:ifr[0]inprc:mn=min(mn,prc[r[0]])mx=max(mx,prc[r[0]])ifmx<1:alpha=1.0else:alpha=float(mn)/mxifconfig.akmc_confidence_scheme=='new':Nf=0.0Ns=0.0forrinrt:ifr[0]inprc:Nf+=1.0Ns+=prc[r[0]]ifNs<1:return0.0ifNf<1:Nf=1.0return1.0+(Nf/(alpha*Ns))*lambertw(-math.exp(-1.0/(Nf/(alpha*Ns)))/(Nf/(alpha*Ns)))elifconfig.akmc_confidence_scheme=='sampling':all_repeats=prcrepeats={}foreventinrt:id=event[0]repeats[id]=all_repeats[id]# number of eventsm=len(repeats)# number of searchesn=sum(repeats.values())ifn<10:return0.0# probabilitiesps=numpy.array(list(repeats.values()),dtype=numpy.float)ps/=sum(ps)C=numpy.zeros(m)foriinrange(n):C+=(1.0-C)*psreturnsum(C)/float(m)elifconfig.akmc_confidence_scheme=='dynamics':#print("into dynamics confidence")# filter out recycled saddles if displace_moved_only is truedyn_saddles=set()ifconfig.disp_moved_only:f=open(self.search_result_path)f.readline()f.readline()forlineinf:status=line.split()[7]ifnot(status.startswith('good')orstatus.startswith('repeat')):continuesearch_type=line.split()[1]ifsearch_type!='dynamics':continuepid=int(status.split('-')[1])dyn_saddles.add(pid)f.close()rt=[entryforentryinrtifentry[0]indyn_saddles]conf=0.0total_rate_estimator=0.0total_rate_found=0.0T1=config.main_temperatureforT2,T2_timeinlist(self.get_time_by_temp().items()):#print("out of get_time_by_temp")ifT2_time==0.0:continue# rates are at T1rates=numpy.array([p[1]forpinrt])prefactors=numpy.array([p[2]forpinrt])iflen(rates)==0:return0.0# extrapolate to T2rates_md=prefactors*(rates/prefactors)**(T1/T2)time=T2_time*1e-15C=1.0-numpy.exp(-time*rates_md)total_rate_found=sum(rates)# Chill confidence#conf += sum(C*rates)/total_rate_found# Lelievre-Jourdain confidencetotal_rate_estimator+=sum(rates/C)if(total_rate_estimator>0):conf=total_rate_found/total_rate_estimatorreturnconfelse:# No superbasin-specific code needed here, the number of# repeats is adjusted for that in the add_process()# method!Nr=self.get_repeats()ifNr<1:return0.0else:returnmax(0.0,1.0-1.0/(alpha*Nr))
[docs]defload_process_table(self):""" Load the process table. If the process table is not loaded, load it. If it is loaded, do nothing. """ifself.procs!=None:returnf=open(self.proctable_path)lines=f.readlines()f.close()self.procs={}forlinlines[1:]:l=l.strip().split()self.procs[int(l[self.ID])]={"saddle_energy":float(l[self.ENERGY]),"prefactor":float(l[self.PREFACTOR]),"product":int(l[self.PRODUCT]),"product_energy":float(l[self.PRODUCT_ENERGY]),"product_prefactor":float(l[self.PRODUCT_PREFACTOR]),"barrier":float(l[self.BARRIER]),"rate":float(l[self.RATE]),"repeats":int(l[self.REPEATS])}try:kT=self.info.get('MetaData','kT')exceptNameError:self.info.set('MetaData','kT',self.statelist.kT)returnifabs(kT-self.statelist.kT)>1e-8:forid,procinlist(self.procs.items()):proc['rate']=proc['prefactor']*math.exp(-proc['barrier']/self.statelist.kT)self.save_process_table()
[docs]defsave_process_table(self):""" If the processtable is present in memory, writes it to disk. """ifself.procs!=None:f=open(self.proctable_path,'w')f.write(self.processtable_header)foridinlist(self.procs.keys()):proc=self.procs[id]f.write(self.processtable_line%(id,proc['saddle_energy'],proc['prefactor'],proc['product'],proc['product_energy'],proc['product_prefactor'],proc['barrier'],proc['rate'],proc['repeats']))f.close()
[docs]defappend_process_table(self,id,saddle_energy,prefactor,product,product_energy,product_prefactor,barrier,rate,repeats):""" Append to the process table. Append a single line to the process table file. If we have loaded the process table, also append it to the process table in memory. """f=open(self.proctable_path,'a')f.write(self.processtable_line%(id,saddle_energy,prefactor,product,product_energy,product_prefactor,barrier,rate,repeats))f.close()ifself.procs!=None:self.procs[id]={"saddle_energy":saddle_energy,"prefactor":prefactor,"product":product,"product_energy":product_energy,"product_prefactor":product_prefactor,"barrier":barrier,"rate":rate,"repeats":repeats}
[docs]defupdate_lowest_barrier(self,barrier):""" Compares the parameter barrier to the lowest barrier stored in info. Updates the lowest barrier stored in info if the barrier parameter is lower and returns the (possibly new) lowest barrier. """lowest=self.info.get("MetaData","lowest barrier",1e300)ifbarrier<lowest:lowest=barrierself.info.set("MetaData","lowest barrier",lowest)returnlowest
[docs]defincrement_time(self,dt,T_search):"""Increment MD search time by dt at temperature T_search."""self.info.set("MetaData","time",self.get_time()+dt)temp_str="%.0f"%T_search# rounded to nearest Kelvinifself.info.has_section("SearchTime"):# Increase time spent at current search temperature.new_time_at_current_temp= \
self.info.get("SearchTime",temp_str,0.0)+dtelse:# The info file must come from an older version of EON,# which didn't have this section, yet. In this case we# assume all prior searches were done at the current# temperature and add the SearchTime section. This makes# this codes backwards compatible and the correctness is# not worse than before.new_time_at_current_temp=self.get_time()# time is already updated, so no +dt!self.info.set("SearchTime",temp_str,new_time_at_current_temp)
[docs]defget_time_by_temp(self):#print("into get_time_by_temp")try:returndict([int(temp),float(time)]fortemp,timeinself.info.items("SearchTime"))exceptconfigparser.NoSectionError:# The "info" file seems to have been produced by an old# version of EON which didn't have the SearchTime# section. We simply upgrade and try again (no recursion# to avoid endless recursion if the exception is raised# again).self.increment_time(0.0,config.saddle_dynamics_temperature)#try:# print(self.info.items('SearchTime', raw=True))#except Exception as e:# print("exception: " + str(e))returndict([int(temp),float(time)]fortemp,timeinself.info.items("SearchTime",raw=True))
[docs]defget_number_of_searches(self):# TODO: this is inefficient!f=open(self.search_result_path)try:n=0f.readline()f.readline()forlineinf:n+=1finally:f.close()returnn
[docs]defregister_bad_saddle(self,result,store=False,superbasin=None):""" Registers a bad saddle. """#print ("bad saddle ",result["results"]["termination_reason"])result_state_code=["Good","Init","Saddle Search No Convex Region","Saddle Search Terminated High Energy","Saddle Search Terminated Concave Iterations","Saddle Search Terminated Total Iterations","Not Connected","Bad Prefactor","Bad Barrier","Minimum Not Converged","Failed Prefactor Calculation","Potential Failed","Nonnegative Displacement Abort","Nonlocal Abort","Negative Barrier","MD Trajectory Too Short","No Negative Mode at Saddle","No Forward Barrier in Minimized Band","MinMode Zero Mode Abort","Optimizer Error"]self.set_bad_saddle_count(self.get_bad_saddle_count()+1)self.append_search_result(result,result_state_code[result["results"]["termination_reason"]],superbasin)# If a MD saddle search is too short add it to the total clock time.if'simulation_time'inresult['results']:ifresult['results']['termination_reason']==15:#too shortself.increment_time(result['results']['simulation_time'],result['results']['md_temperature'])ifstore:ifnotos.path.isdir(self.bad_procdata_path):os.mkdir(self.bad_procdata_path)open(os.path.join(self.bad_procdata_path,"reactant_%d.con"%result['wuid']),'w').writelines(result['reactant.con'].getvalue())open(os.path.join(self.bad_procdata_path,"product_%d.con"%result['wuid']),'w').writelines(result['product.con'].getvalue())open(os.path.join(self.bad_procdata_path,"mode_%d.dat"%result['wuid']),'w').writelines(result['mode.dat'].getvalue())open(os.path.join(self.bad_procdata_path,"results_%d.dat"%result['wuid']),'w').writelines(result['results.dat'].getvalue())open(os.path.join(self.bad_procdata_path,"saddle_%d.con"%result['wuid']),'w').writelines(result['saddle.con'].getvalue())
# Utility functions for loading process .con and mode files.
[docs]deflambertw(z):"""Lambert W function, principal branch"""eps=1.0e-12em1=0.3678794411714423215955237701614608ifz<-em1:logger.error("Tried to evaluate Lambert W function @ < -1/e")raiseValueError()if0.0==z:return0.0ifz<-em1+1e-4:q=z+em1r=math.sqrt(q)q2=q*qq3=q2*qreturn\
-1.0\
+2.331643981597124203363536062168*r\
-1.812187885639363490240191647568*q\
+1.936631114492359755363277457668*r*q\
-2.353551201881614516821543561516*q2\
+3.066858901050631912893148922704*r*q2\
-4.175335600258177138854984177460*q3\
+5.858023729874774148815053846119*r*q3\
-8.401032217523977370984161688514*q3*qifz<1.0:p=math.sqrt(2.0*(2.7182818284590452353602874713526625*z+1.0))w=-1.0+p*(1.0+p*(-0.333333333333333333333+p*0.152777777777777777777777))else:w=math.log(z)ifz>3.0:w-=math.log(w)foriinrange(10):e=math.exp(w)t=w*e-zp=w+1.0t/=e*p-0.5*(p+1.0)*t/pw-=tifabs(t)<eps*(1.0+abs(w)):returnwlogger.error("Failed to converge Lambert W function")raiseValueError()