Source code for eon.movie

"""AKMC movie writers.

Visualization output is VASP 5 POSCAR (`movie.poscar`, `dynamics.poscar`,
and the other names below). That is the one structure format that does
not go through readcon: external viewers read POSCAR, and `loadposcar`
accepts both this VASP 5 layout and the VASP 4 files kdb produces.
"""

from io import StringIO
from pathlib import Path

from eon import fileio as io
from eon.concorpus import frame_texts_for_paths


[docs] def make_movie(movie_type, path_root, states, separate_files=False): movie_path = "movie.poscar" if movie_type == "dynamics": atoms_list = dynamics(path_root, states) movie_path = "dynamics.poscar" elif movie_type == "states": atoms_list = dynamics(path_root, states, True) movie_path = "states.poscar" elif movie_type == "fastestpath": atoms_list = fastest_path(path_root, states) movie_path = "fastestpath.poscar" elif movie_type == "fastestfullpath": atoms_list = fastest_path(path_root, states, full=True) movie_path = "fastestfullpath.poscar" elif movie_type.split(",")[0] == "processes": parts = movie_type.split(",") if len(parts) < 2 or parts[1] == "": raise ValueError("must give a state number") try: statenr = int(parts[1]) except ValueError as exc: raise ValueError("state number must be an integer") from exc limit = int(parts[2]) if len(parts) > 2 else 0 atoms_list = processes(states, statenr, limit) movie_path = "processes_%i.poscar" % statenr elif movie_type == "graph": s = dot(path_root, states) graph = Path("graph.dot") if graph.is_file(): raise FileExistsError(f"file {graph} already exists") graph.write_text(s) return else: raise ValueError(f"unknown movie type {movie_type}") movie_path = Path(movie_path) if separate_files: movie_dir = Path("movies") movie_dir.mkdir(exist_ok=True) movie_path = movie_dir / movie_path.name for old_poscar in movie_dir.glob(movie_path.name + ".*"): old_poscar.unlink() for i, atoms in enumerate(atoms_list): path_i = movie_dir / (movie_path.name + (".%010d" % i)) io.saveposcar(str(path_i), atoms, "w") print("Saved %i frames to %s.*" % (len(atoms_list), movie_path)) else: if movie_path.is_file(): movie_path.unlink() for atoms in atoms_list: io.saveposcar(str(movie_path), atoms, "a") print("Saved %i frames to %s" % (len(atoms_list), movie_path))
[docs] def get_trajectory(trajectory_path): trajectory = [0] with Path(trajectory_path).open() as f: header = f.readline().split() f.readline() try: product_col = header.index("product-id") except ValueError: product_col = 3 for line in f: fields = line.split() if len(fields) <= product_col: continue trajectory.append(int(fields[product_col])) return trajectory
[docs] def processes(states, statenr, limit): try: state = states.get_state(statenr) except OSError as exc: raise FileNotFoundError( f"cannot make a movie for state {statenr}" ) from exc process_table = state.get_process_table() for k, v in list(process_table.items()): process_table[k]["id"] = k process_table[k]["reactant"] = state.get_process_reactant(k) process_table[k]["saddle"] = state.get_process_saddle(k) process_table[k]["product"] = state.get_process_product(k) processes = list(process_table.values()) sorted_processes = sorted(processes, key=lambda a: a["rate"]) sorted_processes.reverse() atoms_list = [] print("%4s %16s %16s %16s" % ("ID", "Rate", "Barrier", "Prefactor")) print("-------------------------------------------------------") for p in sorted_processes: atoms_list.append(p["reactant"]) atoms_list.append(p["saddle"]) atoms_list.append(p["product"]) print( "%4i %16.5e %16.5f %16.5e" % (p["id"], p["rate"], p["barrier"], p["prefactor"]) ) limit -= 1 if limit == 0: break return atoms_list
[docs] def dot(path_root, states): G = make_graph(states) return G.dot()
[docs] def dynamics(path_root, states, unique=False): if not unique: trajectory_path = Path(path_root) / "dynamics.txt" trajectory = get_trajectory(str(trajectory_path)) else: trajectory = list(range(states.get_num_states())) if len(trajectory) == 0: raise ValueError("there have been no dynamics steps") paths = [] for n in trajectory: paths.append(states.get_state(n).reactant_path) # Files stay authoritative. The corpus is a read-only index of this tree. texts = frame_texts_for_paths(path_root, paths) return [io.loadcon(StringIO(text)) for text in texts]
[docs] def make_graph(states): # Build the graph. G = Graph() for statenr in range(states.get_num_states()): state = states.get_state(statenr) G.add_node(state) ptable = state.get_process_table() for i, p in list(ptable.items()): if p["product"] != -1: rate = p["rate"] if rate <= 0: raise ValueError( f"state {state.number} process {i} has no positive rate" ) neighbor_state = states.get_state(p["product"]) G.add_node(neighbor_state) G.add_edge(state, neighbor_state, weight=1.0 / rate) return G
[docs] def fastest_path(path_root, states, full=False): G = make_graph(states) state_list = [states.get_state(0)] if full: nodes = sorted(G.nodes(), key=lambda a: a.number) state_pairs = [nodes[i : i + 2] for i in range(0, len(nodes) - 1)] for s1, s2 in state_pairs: path = G.shortest_path(s1, s2)[1:] for s in path: state_list.append(s) else: end = states.get_state(states.get_num_states() - 1) state_list = G.shortest_path(state_list[0], end) atoms_list = [] for i in range(len(state_list)): atoms_list.append(state_list[i].get_reactant()) if len(state_list) - 1 == i: continue atoms_list.append( state_list[i].get_process_saddle( get_fastest_process_id(state_list[i], state_list[i + 1]) ) ) time = 0.0 for i in range(len(state_list) - 1): ratesum = 0.0 for j in list(state_list[i].get_process_table().values()): ratesum += j["rate"] print(time, state_list[i].number) time += 1 / ratesum return atoms_list
[docs] def get_fastest_process_id(state1, state2): ptable = state1.get_process_table() fastest = None for i, p in list(ptable.items()): if p["product"] == state2.number: if not fastest or fastest[1] < p["rate"]: fastest = (i, p["rate"]) if fastest is None: raise ValueError( f"no process from state {state1.number} to {state2.number}" ) return fastest[0]
[docs] def get_fastest_process_rate(state1, state2): ptable = state1.get_process_table() fastest = None for i, p in list(ptable.items()): if p["product"] == state2.number: if not fastest or fastest[1] < p["rate"]: fastest = (i, p["rate"]) if fastest is None: raise ValueError( f"no process from state {state1.number} to {state2.number}" ) return fastest[1]
[docs] class Graph: def __init__(self, name=""): self.name = name self.graph = {} def __repr__(self): return str(self.graph)
[docs] def dot(self): unique_edges = set() for node, edgedict in list(self.graph.items()): edgelist = list(edgedict.keys()) for edge in edgelist: if node.number < edge.number: unique_edges.add((node.number, edge.number)) else: unique_edges.add((edge.number, node.number)) s = "graph akmc {\n" for edge in unique_edges: s += "%s -- %s;\n" % edge s += "}\n" return s
[docs] def add_node(self, node): if node not in self.graph: self.graph[node] = {}
[docs] def add_edge(self, node1, node2, weight=1.0): if node1 not in self.graph: self.add_node(node1) if node2 not in self.graph: self.add_node(node2) self.graph[node1][node2] = weight
[docs] def neighbors(self, node): if node in self.graph: return list(self.graph[node].keys()) else: return None
[docs] def nodes(self): return list(self.graph.keys())
[docs] def dijkstra(self, start, end=None): G = self.graph D = {} # dictionary of final distances P = {} # dictionary of predecessors Q = priorityDictionary() # est.dist. of non-final vert. Q[start] = 0 for v in Q: D[v] = Q[v] if v == end: break for w in G[v]: vwLength = D[v] + G[v][w] if w in D: if vwLength < D[w]: raise ValueError( "Dijkstra: found better path to already-final vertex" ) elif w not in Q or vwLength < Q[w]: Q[w] = vwLength P[w] = v return (D, P)
[docs] def shortest_path(self, start, end): D, P = self.dijkstra(start, end) path = [] while 1: path.append(end) if end == start: break end = P[end] path.reverse() return path
[docs] class priorityDictionary(dict): def __init__(self): """Initialize priorityDictionary by creating binary heap of pairs (value,key). Note that changing or removing a dict entry will not remove the old pair from the heap until it is found by smallest() or until the heap is rebuilt. """ self.__heap = [] dict.__init__(self)
[docs] def smallest(self): """Find smallest item after removing deleted items from heap.""" if len(self) == 0: raise IndexError("smallest of empty priorityDictionary") heap = self.__heap while heap[0][1] not in self or self[heap[0][1]] != heap[0][0]: lastItem = heap.pop() insertionPoint = 0 while 1: smallChild = 2 * insertionPoint + 1 if ( smallChild + 1 < len(heap) and heap[smallChild] > heap[smallChild + 1] ): smallChild += 1 if smallChild >= len(heap) or lastItem <= heap[smallChild]: heap[insertionPoint] = lastItem break heap[insertionPoint] = heap[smallChild] insertionPoint = smallChild return heap[0][1]
def __iter__(self): """Create destructive sorted iterator of priorityDictionary.""" def iterfn(): while len(self) > 0: x = self.smallest() yield x del self[x] return iterfn() def __setitem__(self, key, val): """Change value stored in dictionary and add corresponding pair to heap. Rebuilds the heap if the number of deleted items grows too large, to avoid memory leakage. """ dict.__setitem__(self, key, val) heap = self.__heap if len(heap) > 2 * len(self): self.__heap = [(v, k) for k, v in list(self.items())] self.__heap.sort() # builtin sort likely faster than O(n) heapify else: newPair = (val, key) insertionPoint = len(heap) heap.append(None) while insertionPoint > 0 and newPair < heap[(insertionPoint - 1) // 2]: heap[insertionPoint] = heap[(insertionPoint - 1) // 2] insertionPoint = (insertionPoint - 1) // 2 heap[insertionPoint] = newPair
[docs] def setdefault(self, key, val): """Reimplement setdefault to call our customized __setitem__.""" if key not in self: self[key] = val return self[key]