def test_connected_components(self): tsgraph = TSGraph(self.db) cc = list(nx.connected_components(tsgraph.graph)) for nodes in cc: for u, v in izip(nodes[:-1], nodes[1:]): self.assertTrue(tsgraph.areConnected(u, v)) for nodes1, nodes2 in izip(cc[:-1], cc[1:]): for u in nodes1: for v in nodes2: self.assertFalse(tsgraph.areConnected(u, v))
def test_connected_components(self): tsgraph = TSGraph(self.db) cc = nx.connected_components(tsgraph.graph) # networkx changed the function so now cc is an iterator over sets cc = [list(c) for c in cc] for nodes in cc: for u, v in zip(nodes[:-1], nodes[1:]): self.assertTrue(tsgraph.areConnected(u, v)) for nodes1, nodes2 in zip(cc[:-1], cc[1:]): for u in nodes1: for v in nodes2: self.assertFalse(tsgraph.areConnected(u, v))
def _build_list(self): if self.verbosity > 0: print "populating list of minima not connected to the central minimum" self.minpairs = deque() central = None # print "self.user_data:", self.user_data for min in self.database.minima(): print "checking min ", min.id() if(min.user_data==self.user_data): print "found central" central = min break if central is None: print "Not found central in database, using the first one to be found" central = self.database.getMinimum(1) # If no central minimum is labelled, assume the first one print "Central minimum is ", central.id() # print "Error: no central minimum supplied" # return graph = TSGraph(self.database) for m in self.database.minima(): if m == central: print "Skipping central minimum" continue if not graph.areConnected(central, m): if self.is_good_pair(central, m): self.minpairs.append((central, m))
def _build_list(self): if self.verbosity > 0: print "populating list of minima not connected to the global minimum" self.minpairs = deque() gmin = self.database.minima()[0] graph = TSGraph(self.database) for m in self.database.minima()[1:]: if not graph.areConnected(gmin, m): if self.is_good_pair(gmin, m): self.minpairs.append((gmin, m))
def on_btn_connect_in_optim_clicked(self, clicked=None): """spawn an OPTIM job and retrieve the minima and transition states it finds""" if clicked is None: return min1, min2 = self.get_selected_minima() # existing_minima = set(self.system.database.minima()) spawner = self.system.get_optim_spawner(min1.coords, min2.coords) spawner.run() db = self.system.database newminima, newts = spawner.load_results(self.system.database) # for m in newminima: # if m not in existing_minima: # self.NewMinimum(m) # now use DoubleEndedConnect to test if they are connected graph = TSGraph(db) if graph.areConnected(min1, min2): # use double ended connect to draw the interpolated path # this is ugly self._doubleEndedConnect(reconnect=False, min1min2=(min1, min2))
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 : pele Database object Used to store the new minima and transition states found. 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. 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=True, 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 = TSGraph(self.database, minima=[self.minstart, self.minend], no_edges=True) else: self.graph = TSGraph(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 if use_all_min or load_no_distances: print "warning: distances were removed from the database but you requested distances to be loaded" self.dist_graph.initialize(self.minstart, self.minend) 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 self.graph.mergeMinima(min1, min2) # merge minima in the database also self.database.mergeMinima(min1, min2) 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. """ return self.dist_graph.getDist(min1, min2) def _addTransitionState(self, ts_ret, min_ret1, min_ret2): """ 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 ts_ret.energy < me1 or ts_ret.energy < me2: logger.warning("trying to add a transition state that has energy lower than its minima.") logger.warning(" TS energy %s %s %s %s", ts_ret.energy, "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 ret in [ min_ret1, min_ret2, ts_ret ]: for check in self.conf_checks: if not check(energy=ret.energy, coords=ret.coords): configs_ok = False break if not configs_ok: break if not configs_ok: return False # Add the minima to the database min1 = self.database.addMinimum(min_ret1.energy, min_ret1.coords) min2 = self.database.addMinimum(min_ret2.energy, min_ret2.coords) # Add the minima to the transition state graph. self.graph.addMinimum(min1) self.graph.addMinimum(min2) 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) # add the transition state to the database ts = self.database.addTransitionState(ts_ret.energy, ts_ret.coords, min1, min2, eigenvec=ts_ret.eigenvec, eigenval=ts_ret.eigenval) # update the transition state graph self.graph.addTransitionState(ts) # 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): """ do a local connect run between min1 and min2 Notes ----- 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, m1ret, m2ret) 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.dist_graph.removeEdge(min1, min2) return nsuccess > 0 def _getNextPair(self): """ return a pair of minima to attempt to connect Notes ----- 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): #stop if we're done if self.graph.areConnected(self.minstart, self.minend): 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 from pele.optimize.optimization_exceptions import LineSearchError try: local_success = self._localConnect(min1, min2) except LineSearchError as err: print err print "caught line search error, aborting connection attempt" break if False 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 ------- 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 the minima are not connected, return (None, None, None) """ 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
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 : pele Database object Used to store the new minima and transition states found. 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. 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 -------- LocalConnect : the core algorithm of this routine """ def __init__(self, min1, min2, pot, mindist, database, verbosity=1, merge_minima=False, max_dist_merge=0.1, local_connect_params=None, fresh_connect=False, longest_first=True, niter=200, conf_checks=None): 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) if local_connect_params is None: local_connect_params = dict() 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 = TSGraph(self.database, minima=[self.minstart, self.minend], no_edges=True) else: self.graph = TSGraph(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): logger.info( "minima are already connected. not initializing distance graph" ) return self.dist_graph.initialize(self.minstart, self.minend) 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 if dist > self.max_dist_merge: logger.info(" minima merge aborted. distance is too large %s", dist) return # merge minima in transition state graph self.graph.mergeMinima(min1, min2) # merge minima in the database also self.database.mergeMinima(min1, min2) 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. """ return self.dist_graph.getDist(min1, min2) def _addTransitionState(self, ts_ret, min_ret1, min_ret2): """ 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 ts_ret.energy < me1 or ts_ret.energy < me2: logger.warning( "trying to add a transition state that has energy lower than its minima." ) logger.warning(" TS energy %s %s %s %s", ts_ret.energy, "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 ret in [min_ret1, min_ret2, ts_ret]: for check in self.conf_checks: if not check(energy=ret.energy, coords=ret.coords): configs_ok = False break if not configs_ok: break if not configs_ok: return False # Add the minima to the database min1 = self.database.addMinimum(min_ret1.energy, min_ret1.coords) min2 = self.database.addMinimum(min_ret2.energy, min_ret2.coords) # Add the minima to the transition state graph. self.graph.addMinimum(min1) self.graph.addMinimum(min2) 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()) # add the transition state to the database ts = self.database.addTransitionState(ts_ret.energy, ts_ret.coords, min1, min2, eigenval=ts_ret.eigenval) # update the transition state graph self.graph.addTransitionState(ts) # 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)) 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): """ do a local connect run between min1 and min2 Notes ----- 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 (min1, min2) in self.pairsNEB: 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, m1ret, m2ret) if goodts: nsuccess += 1 # check results 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.dist_graph.removeEdge(min1, min2) return nsuccess > 0 def _getNextPair(self): """ return a pair of minima to attempt to connect Notes ----- 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): # stop if we're done if self.graph.areConnected(self.minstart, self.minend): 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 from pele.optimize.optimization_exceptions import LineSearchError try: self._localConnect(min1, min2) except LineSearchError as err: print err print "caught line search error, aborting connection attempt" break if False 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 ------- 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 the minima are not connected, return (None, None, None) """ 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