def __init__(self, min1, min2, pot, mindist, database, use_all_min=False, verbosity=1, merge_minima=False, max_dist_merge=0.1, local_connect_params=dict(), fresh_connect=False, longest_first=False, niter=200, conf_checks=None, load_no_distances=False ): self.minstart = min1 assert min1._id == min1, "minima must compare equal with their id %d %s %s" % (min1._id, str(min1), str(min1.__hash__())) self.minend = min2 self.pot = pot self.mindist = mindist self.pairsNEB = dict() self.longest_first = longest_first self.niter = niter if conf_checks is None: self.conf_checks = [] else: self.conf_checks = conf_checks self.verbosity = int(verbosity) self.local_connect_params = dict([("verbosity",verbosity)] + local_connect_params.items()) self.database = database self.fresh_connect = fresh_connect if self.fresh_connect: self.graph = Graph(self.database, minima=[self.minstart, self.minend], no_edges=True) else: self.graph = Graph(self.database) self.merge_minima = merge_minima self.max_dist_merge = float(max_dist_merge) self.load_no_distances = load_no_distances self.dist_graph = _DistanceGraph(self.database, self.graph, self.mindist, self.verbosity) #check if a connection exists before initializing distance Graph if self.graph.areConnected(self.minstart, self.minend): logger.info("minima are already connected. not initializing distance graph") return self.dist_graph.initialize(self.minstart, self.minend, use_all_min=use_all_min, load_no_distances=self.load_no_distances) if self.verbosity > 0: logger.info("************************************************************") logger.info("starting a double ended connect run between") logger.info(" minimum 1: id %d energy %f" % (self.minstart._id, self.minstart.energy)) logger.info(" minimum 2: id %d energy %f" % (self.minend._id, self.minend.energy)) logger.info(" dist %f" % self.getDist(self.minstart, self.minend)) logger.info("************************************************************")
def do_next_connect(self): self.is_running = True minima = self.database.minima() self.min1 = minima[0] graph = Graph(self.database) all_connected = True for m2 in minima[1:]: if not graph.areConnected(self.min1, m2): if (self.min1, m2) in self.failed_pairs or (m2, self.min1) in self.failed_pairs: continue all_connected = False break if all_connected: print "minima are all connected, ending" self.textEdit_summary.insertPlainText("minima are all connected, ending\n") self.is_running = False return self.min2 = m2 self.do_one_connection(self.min1, m2)
def __init__(self, min1, min2, pot, mindist, database, use_all_min=False, verbosity=1, merge_minima=False, max_dist_merge=0.1, local_connect_params=dict(), fresh_connect=False, longest_first=False, ): self.minstart = min1 assert min1._id == min1, "minima must compare equal with their id %d %s %s" % (min1._id, str(min1), str(min1.__hash__())) self.minend = min2 self.pot = pot self.mindist = mindist self.pairsNEB = dict() self.longest_first = longest_first self.verbosity = int(verbosity) self.local_connect_params = dict([("verbosity",verbosity)] + local_connect_params.items()) self.database = database self.fresh_connect = fresh_connect if self.fresh_connect: self.graph = Graph(self.database, minima=[self.minstart, self.minend]) else: self.graph = Graph(self.database) self.merge_minima = merge_minima self.max_dist_merge = float(max_dist_merge) self.dist_graph = _DistanceGraph(self.database, self.graph, self.mindist, self.verbosity) #check if a connection exists before initializing distance Graph if self.graph.areConnected(self.minstart, self.minend): print "minima are already connected. not initializing distance graph" return self.dist_graph.initialize(self.minstart, self.minend, use_all_min) print "************************************************************" print "starting a double ended connect run between" print " minimum 1: id %d energy %f" % (self.minstart._id, self.minstart.energy) print " minimum 2: id %d energy %f" % (self.minend._id, self.minend.energy) print " dist %f" % self.getDist(self.minstart, self.minend) print "************************************************************"
class DoubleEndedConnect(object): """ Find a connected network of minima and transition states between min1 and min2 Parameters ---------- min1, min2 : Mimumum() objects the two minima to try to connect pot : potential object the potential mindist : callable the function which returns the optimized minimum distance between two structures database : Database() object the database object, used to save distance calculations so mindist() need only be called once for each minima pair. *Note* the use of this and graph is a bit redundant, this should be cleaned up use_all_min : bool if True, then all known minima and transition states in graph will be used to try to connect min1 and min2. This requires a mindist() call (or a retrieveal operation from database) for every pair which can take a very long time if many minima are known. niter : int, optional maximum number of iterations verbosity : int this controls how many status messages are printed. (not really implemented yet) merge_minima : bool if True, minima for which NEB finds no transition state candidates between them will be merged max_dist_merge : float merging minima will be aborted if the distance between them is greater than max_dist_merge local_connect_params : dict parameters passed to the local connect algorithm. This includes all NEB and all transition state search parameters, along with, e.g. now many times to retry a local connect run. See documentation for LocalConnect for details. fresh_connect : bool if true, ignore all existing minima and transition states in the database and try to find a new path longest_first : bool if true, always try to connect the longest segment in the path guess first conf_checks : list of callables a list of callable function that determine if a configuration is valid. They must return a bool, and accept the keyword parameters conf_check(energy=energy, coords=coords) If any configuration in a minimum-transition_state-minimum triplet fails a test then the whole triplet is rejected. load_no_distances : bool, optional if True, then no distances will be loaded from the database Notes ----- The algorithm is iterative, with each iteration composed of While min1 and min2 are not connected: 1) choose a pair of known minima to try to connect 2) use NEB to get a guess for the transition states between them 3) refine the transition states to desired accuracy 4) fall off either side of the transition states to find the two minima associated with that candidate 5) add the transition states and associated minima to the known network Of the above, steps 1 and 2 and 3 are the most involved. 2, 3, 4 are wrapped into a separate class called LocalConnect. See this class and the NEB and FindTransitionState classes for detailed descriptions of these procedures. An important note is that the NEB is used only to get a *guess* for the transition state. Thus we only want to put enough time and energy into the NEB routine to get the guess close enough that FindTransitionState can refine it to the correct transition state. FindTransitionState is very fast if the initial guess is good, but can be very slow otherwise. Choose a pair: Here I will describe step 1), the algorithm to find a pair of known minima to try to connect. This choice will keep in mind that the ultimate goal is to connect min1 and min2. In addition to the input parameter "graph", we keep a second graph "Gdist" (now wrapped in a separate class _DistanceGraph) which also has minima as the vertices. Gdist has an edge between every pair of nodes. The edge weight between vertices u and v is if u and v are connected by transition states: weight(u, v) = 0. elif we have already tried local_connect on (u,v): weight(u, v) = Infinity else: weight(u, v) = dist(u, v)**2 This edge weight is set to Infinity to ensure we don't repeat LocalConnect runs over and over again. The minimum weight path between min1 and min2 in Gdist gives a good guess for the best way to try connect min1 and min2. So the algorithm to find a pair of know minima (trial1, trial2) to try to connect is path = Gdist.minimum_weight_path(min1, min2) trial1, trial2 = minima pair in path with lowest nonzero edge weight. (note: if parameter longest_first is True) then the edgepair with the largest edge weight will be selected) todo: allow user to pass graph See Also -------- DoubleEndedConnectPar : parallel version of this class LocalConnect : the core algorithm of this routine """ def __init__(self, min1, min2, pot, mindist, database, use_all_min=False, verbosity=1, merge_minima=False, max_dist_merge=0.1, local_connect_params=dict(), fresh_connect=False, longest_first=False, niter=200, conf_checks=None, load_no_distances=False ): self.minstart = min1 assert min1._id == min1, "minima must compare equal with their id %d %s %s" % (min1._id, str(min1), str(min1.__hash__())) self.minend = min2 self.pot = pot self.mindist = mindist self.pairsNEB = dict() self.longest_first = longest_first self.niter = niter if conf_checks is None: self.conf_checks = [] else: self.conf_checks = conf_checks self.verbosity = int(verbosity) self.local_connect_params = dict([("verbosity",verbosity)] + local_connect_params.items()) self.database = database self.fresh_connect = fresh_connect if self.fresh_connect: self.graph = Graph(self.database, minima=[self.minstart, self.minend], no_edges=True) else: self.graph = Graph(self.database) self.merge_minima = merge_minima self.max_dist_merge = float(max_dist_merge) self.load_no_distances = load_no_distances self.dist_graph = _DistanceGraph(self.database, self.graph, self.mindist, self.verbosity) #check if a connection exists before initializing distance Graph if self.graph.areConnected(self.minstart, self.minend): logger.info("minima are already connected. not initializing distance graph") return self.dist_graph.initialize(self.minstart, self.minend, use_all_min=use_all_min, load_no_distances=self.load_no_distances) if self.verbosity > 0: logger.info("************************************************************") logger.info("starting a double ended connect run between") logger.info(" minimum 1: id %d energy %f" % (self.minstart._id, self.minstart.energy)) logger.info(" minimum 2: id %d energy %f" % (self.minend._id, self.minend.energy)) logger.info(" dist %f" % self.getDist(self.minstart, self.minend)) logger.info("************************************************************") def mergeMinima(self, min1, min2): """merge two minimum objects This will delete min2 and make everything that pointed to min2 point to min1. """ #prefer to delete the minima with the large id. this potentially will be easier if min2._id < min1._id: min1, min2 = min2, min1 debug = False dist = self.getDist(min1, min2) logger.info( "merging minima %s %s %s %s %s", min1._id, min2._id, dist, "E1-E2", min1.energy - min2.energy) #deal with the case where min1 and/or min2 are the same as minstart and/or minend #make sure the one that is deleted (min2) is not minstart or minend if ((min1 == self.minstart and min2 == self.minend) or (min2 == self.minstart and min1 == self.minend)): logger.error( "ERROR: trying to merge the start and end minima. aborting") return if min2 == self.minstart or min2 == self.minend: min1, min2 = min2, min1 #print "min1 min2", min1, min2 if dist > self.max_dist_merge: logger.info( " minima merge aborted. distance is too large %s", dist) return # if debug: # #testing # if min2 in self.database.minima(): # print "error, min2 is still in database" # for ts in self.database.transition_states(): # if min2 == ts.minimum1 or min2 == ts.minimum2: # print "error, a transition state attached to min2 is still in database", ts.minimum1._id, ts.minimum2._id #merge minima in transition state graph #note, this will merge minima in the database also self.graph.mergeMinima(min1, min2, update_database=True) if debug: #testing if min2 in self.graph.graph.nodes(): logger.error( "error, min2 is still in self.graph.graph") logger.debug( "self.graph.graph. nnodes %s", self.graph.graph.number_of_nodes() ) #merge minima in distance graph self.dist_graph.mergeMinima(min1, min2) def getDist(self, min1, min2): """ get the distance between min1 and min2. Try first to get distances from the dictionary distmatrix as this is the fastest access method. Then try to get distances from the database if they exist, else calculate the distance and save it to the database and distmatrix """ return self.dist_graph.getDist(min1, min2) def _addTransitionState(self, E, coords, min_ret1, min_ret2, eigenvec, eigenval): """ add a transition state to the database, the transition state graph and the distance graph """ # if isinstance(min_ret1, tuple): # for compatability with old and new quenchers # min_ret1 = min_ret1[4] # if isinstance(min_ret2, tuple): # for compatability with old and new quenchers # min_ret2 = min_ret2[4] # if isinstance(min1_ret) #sanity check for the energies me1, me2 = min_ret1.energy, min_ret2.energy if E < me1 or E < me2: logger.warning("trying to add a transition state that has energy lower than its minima.") logger.warning(" TS energy %s %s %s %s", E, "minima energy", me1, me2 ) logger.warning(" aborting" ) return False #check the minima and transition states are valid configurations. #if any fail, then don't add anything. configs_ok = True for xtrial, etrial in [ (min_ret1.coords, min_ret1.energy), (min_ret2.coords, min_ret2.energy), (coords, E) ]: for check in self.conf_checks: if not check(energy=etrial, coords=xtrial): configs_ok = False break if not configs_ok: break if not configs_ok: return False #add the minima to the transition state graph. #This step is important to do first because it returns a Database Minimum object. min1 = self.graph.addMinimum(min_ret1.energy, min_ret1.coords) min2 = self.graph.addMinimum(min_ret2.energy, min_ret2.coords) if min1 == min2: logger.warning( "stepping off the transition state resulted in twice the same minima %s", min1._id) return False logger.info("adding transition state %s %s", min1._id, min2._id) #update the transition state graph #this also updates the database and returns a TransitionState object ts = self.graph.addTransitionState(E, coords, min1, min2, eigenvec=eigenvec, eigenval=eigenval) # self.graph.refresh() #update the distance graph self.dist_graph.addMinimum(min1) self.dist_graph.addMinimum(min2) self.dist_graph.setTransitionStateConnection(min1, min2) if self.verbosity > 1: #print some information dse = self.getDist(self.minend, self.minstart) msid = self.minstart._id meid = self.minend._id m1id = min1._id m2id = min2._id if min1 != self.minstart and min1 != self.minend: ds = self.getDist(min1, self.minstart) de = self.getDist(min1, self.minend) if ds < dse > de: triangle = "" else: triangle = ": new minima not in between start and end" logger.info(" distances: %4d -> %4d = %f %4d -> %4d = %f %4d -> %4d = %f %s" % (msid, m1id, ds, m1id, meid, de, m1id, m2id, dse, triangle)) #print " dist new min 1 to minstart, minend ", ds, de, dse if min2 != self.minstart and min2 != self.minend: ds = self.getDist(min2, self.minstart) de = self.getDist(min2, self.minend) if ds < dse > de: triangle = "" else: triangle = ": new minima not in between start and end" logger.info( " distances: %4d -> %4d = %f %4d -> %4d = %f %4d -> %4d = %f" % (msid, m2id, ds, m2id, meid, de, m2id, m2id, dse)) return True def _getLocalConnectObject(self): return LocalConnect(self.pot, self.mindist, **self.local_connect_params) def _localConnect(self, min1, min2): """ 1) NEB to find transition state candidates. for each transition state candidate: 2) refine the transition state candidates 3) if successful, fall off either side of the transition state to find the minima the transition state connects. Add the new transition state and minima to the graph """ #Make sure we haven't already tried this pair and #record some data so we don't try it again in the future if self.pairsNEB.has_key((min1, min2)): logger.warning("WARNING: redoing NEB for minima %s %s", min1._id, min2._id) logger.warning(" aborting NEB") #self._remove_edgeGdist(min1, min2) self.dist_graph.removeEdge(min1, min2) return True self.pairsNEB[(min1, min2)] = True self.pairsNEB[(min2, min1)] = True #Make sure they're not already connected. sanity test if self.graph.areConnected(min1, min2): logger.warning("in _local_connect, but minima are already connected. aborting %s %s %s", min1._id, min2._id, self.getDist(min1, min2)) self.dist_graph.setTransitionStateConnection(min1, min2) self.dist_graph.checkGraph() return True #do local connect run local_connect = self._getLocalConnectObject() res = local_connect.connect(min1, min2) #now add each new transition state to the graph and database. nsuccess = 0 for tsret, m1ret, m2ret in res.new_transition_states: goodts = self._addTransitionState(tsret.energy, tsret.coords, m1ret, m2ret, tsret.eigenvec, tsret.eigenval) if goodts: nsuccess += 1 #check results #nclimbing = len(climbing_images) #print "from NEB search found", nclimbing, "transition state candidates" if nsuccess == 0: dist = self.getDist(min1, min2) if dist < self.max_dist_merge: logger.warning("local connect failed and the minima are close. Are the minima really the same?") logger.warning(" energies: %s %s %s %s", min1.energy, min2.energy, "distance", dist) if self.merge_minima: self.mergeMinima(min1, min2) else: logger.warning(" set merge_minima=True to merge the minima") return False #remove this edge from Gdist so we don't try this pair again again #self._remove_edgeGdist(min1, min2) self.dist_graph.removeEdge(min1, min2) return nsuccess > 0 def _getNextPair(self): """ this is the function which attempts to find a clever pair of minima to try to connect with the ultimate goal of connecting minstart and minend this method can be described as follows: make a new graph Gnew which is complete (all vetices connected). The edges have a weight given by if self.graph.areConnected(u,v): weight(u,v) = 0. else: weight(u,v) = mindist(u,v) if an NEB has been attempted between u and v then the edge is removed. we then find the shortest path between minstart and minend. We return the pair in this path which has edge weight with the smallest non zero value update: find the shortest path weighted by distance squared. This penalizes finding the NEB between minima that are very far away. (Does this too much favor long paths?) """ logger.info("finding a good pair to try to connect") #get the shortest path on dist_graph between minstart and minend if True: logger.debug("Gdist has %s %s %s %s", self.dist_graph.Gdist.number_of_nodes(), "nodes and", self.dist_graph.Gdist.number_of_edges(), "edges") path, weights = self.dist_graph.shortestPath(self.minstart, self.minend) weightsum = sum(weights) if path is None or weightsum >= 10e9: logger.warning("Can't find any way to try to connect the minima") return None, None #get the weights of the path segements weightlist = [] for i in range(1,len(path)): min1 = path[i-1] min2 = path[i] # w = weights.get((min1,min2)) # if w is None: # w = weights.get((min2,min1)) w = weights[i-1] weightlist.append( (w, min1, min2) ) if True: #print the path logger.info("best guess for path. (dist=0.0 means the path is known)") for w, min1, min2 in weightlist: if w > 1e-6: dist = self.getDist(min1, min2) else: dist = w logger.info(" path guess %s %s %s", min1._id, min2._id, dist) #select which minima pair to return if self.longest_first: weightlist.sort() w, min1, min2 = weightlist[-1] else: weightlist.sort() for w, min1, min2 in weightlist: if w > 1e-6: break return min1, min2 def connect(self): """ the main loop of the algorithm """ self.NEBattempts = 2; for i in range(self.niter): #do some book keeping self.dist_graph.updateDatabase() #stop if we're done if self.graph.areConnected(self.minstart, self.minend): self.dist_graph.updateDatabase(force=True) logger.info("found connection!") return logger.info("") logger.info("======== starting connect cycle %s %s", i, "========") #get pair of minima to try to connect min1, min2 = self._getNextPair() #fail if we can't find a good pair to try if min1 is None or min2 is None: break #try to connect those minima local_success = self._localConnect(min1, min2) if True and i % 10 == 0: #do some sanity checks self.dist_graph.checkGraph() logger.info("failed to find connection between %s %s", self.minstart._id, self.minend._id) def success(self): return self.graph.areConnected(self.minstart, self.minend) def returnPath(self): """return information about the path Returns ------- If the minima are not connected, return (None, None, None) mints : list of Minimum and TransitionStates a list of Minimum, TransitionState, Minimum objects that make up the path S : list of float numpy array of the distance along the path. len(S) == len(mints) energies : list of float numpy array of the energies along the path """ if not self.graph.areConnected(self.minstart, self.minend): return None, None, None minima = nx.shortest_path(self.graph.graph, self.minstart, self.minend) transition_states = [] mints = [minima[0]] for i in range(1,len(minima)): m1 = minima[i-1] m2 = minima[i] ts = self.database.getTransitionState(m1, m2) transition_states.append(ts) mints.append(ts) mints.append(m2) S = np.zeros(len(mints)) for i in range(1,len(mints)): coords1 = mints[i-1].coords coords2 = mints[i].coords dist, c1, c2 = self.mindist(coords1, coords2) S[i] = S[i-1] + dist energies = np.array([m.energy for m in mints]) return mints, S, energies
reac = minima[i] for j in range(len(lines)): line = lines[j].split() prod = int(line[3]) if prod == -1: continue prod = minima[prod] e = float(line[1]) proc_id = int(line[0]) coords = int('%d%d' % (i + 1, j)) if [reac, prod] not in transitions and [prod, reac ] not in transitions: db.addTransitionState(e, coords, reac, prod) transitions.append([reac, prod]) graphwrapper = Graph(db) graph = graphwrapper.graph dg = disconnectivity_graph.DisconnectivityGraph(graph, subgraph_size=0, nlevels=nlevels, center_gmin=True) dg.calculate() print 'Plotting (this can take a while)...' dg.plot() print 'Saving tree.pdf...' plt.savefig("tree.pdf") print 'Displaying plot...' plt.show() print 'Done.'