def compute_periodicity_cycle_basis(self): """ Returns: """ my_simple_graph = nx.Graph(self._connected_subgraph) cycles = nx.cycle_basis(my_simple_graph) all_deltas = [] for cyc in cycles: mycyc = list(cyc) mycyc.append(cyc[0]) this_cycle_deltas = [np.zeros(3, np.int)] for (node1, node2) in [(node1, mycyc[inode1 + 1]) for inode1, node1 in enumerate(mycyc[:-1])]: this_cycle_deltas_new = [] for key, edge_data in self._connected_subgraph[node1][ node2].items(): delta = get_delta(node1, node2, edge_data) for current_delta in this_cycle_deltas: this_cycle_deltas_new.append(current_delta + delta) this_cycle_deltas = this_cycle_deltas_new all_deltas.extend(this_cycle_deltas) all_deltas = get_linearly_independent_vectors(all_deltas) if len(all_deltas) == 3: self._periodicity_vectors = all_deltas return # One has to consider pairs of nodes with parallel edges (these are not considered in the simple graph cycles) edges = my_simple_graph.edges() for n1, n2 in edges: if n1 == n2: continue if len(self._connected_subgraph[n1][n2]) == 1: continue elif len(self._connected_subgraph[n1][n2]) > 1: for iedge1, iedge2 in itertools.combinations( self._connected_subgraph[n1][n2], 2): e1data = self._connected_subgraph[n1][n2][iedge1] e2data = self._connected_subgraph[n1][n2][iedge2] current_delta = get_delta(n1, n2, e1data) delta = get_delta(n2, n1, e2data) current_delta += delta all_deltas.append(current_delta) else: raise ValueError('Should not be here ...') all_deltas = get_linearly_independent_vectors(all_deltas) if len(all_deltas) == 3: self._periodicity_vectors = all_deltas return self._periodicity_vectors = all_deltas
def description(self, full=False): """ Args: full (): Returns: """ out = ["Connected component with environment nodes :"] if not full: out.extend([str(en) for en in sorted(self.graph.nodes())]) return "\n".join(out) for en in sorted(self.graph.nodes()): out.append("{}, connected to :".format(str(en))) en_neighbs = nx.neighbors(self.graph, en) for en_neighb in sorted(en_neighbs): out.append(" - {} with delta image cells".format(en_neighb)) all_deltas = sorted( [ get_delta(node1=en, node2=en_neighb, edge_data=edge_data).tolist() for iedge, edge_data in self.graph[en][en_neighb].items() ] ) out.extend([" ({:d} {:d} {:d})".format(delta[0], delta[1], delta[2]) for delta in all_deltas]) return "\n".join(out)
def test_get_delta(self): n1 = FakeNode(3) n2 = FakeNode(7) edge_data = {"start": 3, "end": 7, "delta": [2, 6, 4]} self.assertTrue(np.allclose(get_delta(n1, n2, edge_data), [2, 6, 4])) edge_data = {"start": 7, "end": 3, "delta": [2, 6, 4]} self.assertTrue(np.allclose(get_delta(n1, n2, edge_data), [-2, -6, -4])) with self.assertRaisesRegex( ValueError, "Trying to find a delta between two nodes with an edge that seems not to link these nodes.", ): edge_data = {"start": 6, "end": 3, "delta": [2, 6, 4]} get_delta(n1, n2, edge_data) with self.assertRaisesRegex( ValueError, "Trying to find a delta between two nodes with an edge that seems not to link these nodes.", ): edge_data = {"start": 7, "end": 2, "delta": [2, 6, 4]} get_delta(n1, n2, edge_data)
def test_get_delta(self): n1 = FakeNode(3) n2 = FakeNode(7) edge_data = {'start': 3, 'end': 7, 'delta': [2, 6, 4]} self.assertTrue(np.allclose(get_delta(n1, n2, edge_data), [2, 6, 4])) edge_data = {'start': 7, 'end': 3, 'delta': [2, 6, 4]} self.assertTrue(np.allclose(get_delta(n1, n2, edge_data), [-2, -6, -4])) with self.assertRaisesRegex( ValueError, 'Trying to find a delta between two nodes with an edge ' 'that seems not to link these nodes.'): edge_data = {'start': 6, 'end': 3, 'delta': [2, 6, 4]} get_delta(n1, n2, edge_data) with self.assertRaisesRegex( ValueError, 'Trying to find a delta between two nodes with an edge ' 'that seems not to link these nodes.'): edge_data = {'start': 7, 'end': 2, 'delta': [2, 6, 4]} get_delta(n1, n2, edge_data)
def compute_periodicity_all_simple_paths_algorithm(self): """ Returns: """ self_loop_nodes = list(nx.nodes_with_selfloops(self._connected_subgraph)) all_nodes_independent_cell_image_vectors = [] my_simple_graph = nx.Graph(self._connected_subgraph) for test_node in self._connected_subgraph.nodes(): # TODO: do we need to go through all test nodes ? this_node_cell_img_vectors = [] if test_node in self_loop_nodes: for key, edge_data in self._connected_subgraph[test_node][test_node].items(): if edge_data["delta"] == (0, 0, 0): raise ValueError("There should not be self loops with delta image = (0, 0, 0).") this_node_cell_img_vectors.append(edge_data["delta"]) for d1, d2 in itertools.combinations(this_node_cell_img_vectors, 2): if d1 == d2 or d1 == tuple(-ii for ii in d2): raise ValueError("There should not be self loops with the same (or opposite) delta image.") this_node_cell_img_vectors = get_linearly_independent_vectors(this_node_cell_img_vectors) # Here, we adopt a cutoff equal to the size of the graph, contrary to the default of networkX (size - 1), # because otherwise, the all_simple_paths algorithm fail when the source node is equal to the target node. paths = [] # TODO: its probably possible to do just a dfs or bfs traversal instead of taking all simple paths! test_node_neighbors = my_simple_graph.neighbors(test_node) breaknodeloop = False for test_node_neighbor in test_node_neighbors: # Special case for two nodes if len(self._connected_subgraph[test_node][test_node_neighbor]) > 1: this_path_deltas = [] node_node_neighbor_edges_data = list( self._connected_subgraph[test_node][test_node_neighbor].values() ) for edge1_data, edge2_data in itertools.combinations(node_node_neighbor_edges_data, 2): delta1 = get_delta(test_node, test_node_neighbor, edge1_data) delta2 = get_delta(test_node_neighbor, test_node, edge2_data) this_path_deltas.append(delta1 + delta2) this_node_cell_img_vectors.extend(this_path_deltas) this_node_cell_img_vectors = get_linearly_independent_vectors(this_node_cell_img_vectors) if len(this_node_cell_img_vectors) == 3: break for path in nx.all_simple_paths( my_simple_graph, test_node, test_node_neighbor, cutoff=len(self._connected_subgraph), ): path_indices = [nodepath.isite for nodepath in path] if path_indices == [test_node.isite, test_node_neighbor.isite]: continue path_indices.append(test_node.isite) path_indices = tuple(path_indices) if path_indices not in paths: paths.append(path_indices) else: continue path.append(test_node) # TODO: there are some paths that appears twice for cycles, and there are some paths that should # probably not be considered this_path_deltas = [np.zeros(3, np.int_)] for (node1, node2) in [(node1, path[inode1 + 1]) for inode1, node1 in enumerate(path[:-1])]: this_path_deltas_new = [] for key, edge_data in self._connected_subgraph[node1][node2].items(): delta = get_delta(node1, node2, edge_data) for current_delta in this_path_deltas: this_path_deltas_new.append(current_delta + delta) this_path_deltas = this_path_deltas_new this_node_cell_img_vectors.extend(this_path_deltas) this_node_cell_img_vectors = get_linearly_independent_vectors(this_node_cell_img_vectors) if len(this_node_cell_img_vectors) == 3: breaknodeloop = True break if breaknodeloop: break this_node_cell_img_vectors = get_linearly_independent_vectors(this_node_cell_img_vectors) independent_cell_img_vectors = this_node_cell_img_vectors all_nodes_independent_cell_image_vectors.append(independent_cell_img_vectors) # If we have found that the sub structure network is 3D-connected, we can stop ... if len(independent_cell_img_vectors) == 3: break self._periodicity_vectors = [] if len(all_nodes_independent_cell_image_vectors) != 0: for independent_cell_img_vectors in all_nodes_independent_cell_image_vectors: if len(independent_cell_img_vectors) > len(self._periodicity_vectors): self._periodicity_vectors = independent_cell_img_vectors if len(self._periodicity_vectors) == 3: break
def coordination_sequence(self, source_node, path_size=5, coordination="number", include_source=False): """Get the coordination sequence for a given node. Args: source_node: Node for which the coordination sequence is computed. path_size: Maximum length of the path for the coordination sequence. coordination: Type of coordination sequence. The default ("number") corresponds to the number of environment nodes that are reachable by following paths of sizes between 1 and path_size. For coordination "env:number", this resulting coordination sequence is a sequence of dictionaries mapping the type of environment to the number of such environment reachable by following paths of sizes between 1 and path_size. include_source: Whether to include the source_node in the coordination sequence. Returns: dict: Mapping between the nth "layer" of the connected component with the corresponding coordination. Examples: The corner-sharing octahedral framework (as in perovskites) have the following coordination sequence (up to a path of size 6) : {1: 6, 2: 18, 3: 38, 4: 66, 5: 102, 6: 146} Considering both the octahedrons and the cuboctahedrons of the typical BaTiO3 perovskite, the "env:number" coordination sequence (up to a path of size 6) starting on the Ti octahedron and Ba cuboctahedron are the following : Starting on the Ti octahedron : {1: {'O:6': 6, 'C:12': 8}, 2: {'O:6': 26, 'C:12': 48}, 3: {'O:6': 90, 'C:12': 128}, 4: {'O:6': 194, 'C:12': 248}, 5: {'O:6': 338, 'C:12': 408}, 6: {'O:6': 522, 'C:12': 608}} Starting on the Ba cuboctahedron : {1: {'O:6': 8, 'C:12': 18}, 2: {'O:6': 48, 'C:12': 74}, 3: {'O:6': 128, 'C:12': 170}, 4: {'O:6': 248, 'C:12': 306}, 5: {'O:6': 408, 'C:12': 482}, 6: {'O:6': 608, 'C:12': 698}} If include_source is set to True, the source node is included in the sequence, e.g. for the corner-sharing octahedral framework : {0: 1, 1: 6, 2: 18, 3: 38, 4: 66, 5: 102, 6: 146}. For the "env:number" coordination starting on a Ba cuboctahedron (as shown above), the coordination sequence is then : {0: {'C:12': 1}, 1: {'O:6': 8, 'C:12': 18}, 2: {'O:6': 48, 'C:12': 74}, 3: {'O:6': 128, 'C:12': 170}, 4: {'O:6': 248, 'C:12': 306}, 5: {'O:6': 408, 'C:12': 482}, 6: {'O:6': 608, 'C:12': 698}} """ if source_node not in self._connected_subgraph: raise ValueError("Node not in Connected Component. Cannot find coordination sequence.") # Example of an infinite periodic net in two dimensions consisting of a stacking of # A and B lines : # # * * * * * # * * * * * # * * A * * B * * A * * B * * A * * # * * * * * # * * * * * # * * A * * B * * A * * B * * A * * # * * * * * # * * * * * # * * A * * B * * A * * B * * A * * # * * * * * # * * * * * # * * A * * B * * A * * B * * A * * # * * * * * # * * * * * # * * A * * B * * A * * B * * A * * # * * * * * # * * * * * # # One possible quotient graph of this periodic net : # __ __ # (0,1,0) / \ / \ (0,1,0) # `<--A--->---B--<´ # / (0,0,0) \ # \ / # `--->---´ # (1,0,0) # # The "number" coordination sequence starting from any environment is : 4-8-12-16-... # The "env:number" coordination sequence starting from any environment is : # {A:2, B:2}-{A:4, B:4}-{A:6, B:6}-... current_delta = (0, 0, 0) current_ends = [(source_node, current_delta)] visited = {(source_node.isite, *current_delta)} path_len = 0 cseq = {} if include_source: if coordination == "number": cseq[0] = 1 elif coordination == "env:number": cseq[0] = {source_node.coordination_environment: 1} else: raise ValueError('Coordination type "{}" is not valid for coordination_sequence.'.format(coordination)) while path_len < path_size: new_ends = [] for current_node_end, current_delta_end in current_ends: for nb in self._connected_subgraph.neighbors(current_node_end): for iedge, edata in self._connected_subgraph[current_node_end][nb].items(): new_delta = current_delta_end + get_delta(current_node_end, nb, edata) if (nb.isite, *new_delta) not in visited: new_ends.append((nb, new_delta)) visited.add((nb.isite, *new_delta)) if nb.isite == current_node_end.isite: # Handle self loops new_delta = current_delta_end - get_delta(current_node_end, nb, edata) if (nb.isite, *new_delta) not in visited: new_ends.append((nb, new_delta)) visited.add((nb.isite, *new_delta)) current_ends = new_ends path_len += 1 if coordination == "number": cseq[path_len] = len(current_ends) elif coordination == "env:number": myenvs = [myend.coordination_environment for myend, _ in current_ends] cseq[path_len] = {myenv: myenvs.count(myenv) for myenv in set(myenvs)} else: raise ValueError('Coordination type "{}" is not valid for coordination_sequence.'.format(coordination)) return cseq
def draw_network(env_graph, pos, ax, sg=None, periodicity_vectors=None): """Draw network of environments in a matplotlib figure axes. Args: env_graph: Graph of environments. pos: Positions of the nodes of the environments in the 2D figure. ax: Axes object in which the network should be drawn. sg: Not used currently (drawing of supergraphs). periodicity_vectors: List of periodicity vectors that should be drawn. Returns: None """ for n in env_graph: c = Circle(pos[n], radius=0.02, alpha=0.5) ax.add_patch(c) env_graph.node[n]["patch"] = c x, y = pos[n] ax.annotate(str(n), pos[n], ha="center", va="center", xycoords="data") seen = {} e = None for (u, v, d) in env_graph.edges(data=True): n1 = env_graph.node[u]["patch"] n2 = env_graph.node[v]["patch"] rad = 0.1 if (u, v) in seen: rad = seen.get((u, v)) rad = (rad + np.sign(rad) * 0.1) * -1 alpha = 0.5 color = "k" periodic_color = "r" delta = get_delta(u, v, d) # center = get_center_of_arc(n1.center, n2.center, rad) n1center = np.array(n1.center) n2center = np.array(n2.center) midpoint = (n1center + n2center) / 2 dist = np.sqrt(np.power(n2.center[0] - n1.center[0], 2) + np.power(n2.center[1] - n1.center[1], 2)) n1c_to_n2c = n2center - n1center vv = np.cross( np.array([n1c_to_n2c[0], n1c_to_n2c[1], 0], np.float_), np.array([0, 0, 1], np.float_), ) vv /= np.linalg.norm(vv) midarc = midpoint + rad * dist * np.array([vv[0], vv[1]], np.float_) xytext_offset = 0.1 * dist * np.array([vv[0], vv[1]], np.float_) if periodicity_vectors is not None and len(periodicity_vectors) == 1: if np.all(np.array(delta) == np.array(periodicity_vectors[0])) or np.all( np.array(delta) == -np.array(periodicity_vectors[0]) ): e = FancyArrowPatch( n1center, n2center, patchA=n1, patchB=n2, arrowstyle="-|>", connectionstyle="arc3,rad=%s" % rad, mutation_scale=15.0, lw=2, alpha=alpha, color="r", linestyle="dashed", ) else: e = FancyArrowPatch( n1center, n2center, patchA=n1, patchB=n2, arrowstyle="-|>", connectionstyle="arc3,rad=%s" % rad, mutation_scale=10.0, lw=2, alpha=alpha, color=color, ) else: ecolor = color if np.allclose(np.array(delta), np.zeros(3)) else periodic_color e = FancyArrowPatch( n1center, n2center, patchA=n1, patchB=n2, arrowstyle="-|>", connectionstyle="arc3,rad=%s" % rad, mutation_scale=10.0, lw=2, alpha=alpha, color=ecolor, ) ax.annotate( delta, midarc, ha="center", va="center", xycoords="data", xytext=xytext_offset, textcoords="offset points", ) seen[(u, v)] = rad ax.add_patch(e)
def draw_network(env_graph, pos, ax, sg=None, periodicity_vectors=None): """ Args: env_graph (): pos (): ax (): sg (): periodicity_vectors (): Returns: """ for n in env_graph: c = Circle(pos[n], radius=0.02, alpha=0.5) ax.add_patch(c) env_graph.node[n]['patch'] = c x, y = pos[n] ax.annotate(str(n), pos[n], ha='center', va='center', xycoords='data') seen = {} e = None for (u, v, d) in env_graph.edges(data=True): n1 = env_graph.node[u]['patch'] n2 = env_graph.node[v]['patch'] rad = 0.1 if (u, v) in seen: rad = seen.get((u, v)) rad = (rad + np.sign(rad) * 0.1) * -1 alpha = 0.5 color = 'k' periodic_color = 'r' delta = get_delta(u, v, d) # center = get_center_of_arc(n1.center, n2.center, rad) n1center = np.array(n1.center) n2center = np.array(n2.center) midpoint = (n1center + n2center) / 2 dist = np.sqrt( np.power(n2.center[0] - n1.center[0], 2) + np.power(n2.center[1] - n1.center[1], 2)) n1c_to_n2c = n2center - n1center vv = np.cross(np.array([n1c_to_n2c[0], n1c_to_n2c[1], 0], np.float), np.array([0, 0, 1], np.float)) vv /= np.linalg.norm(vv) midarc = midpoint + rad * dist * np.array([vv[0], vv[1]], np.float) xytext_offset = 0.1 * dist * np.array([vv[0], vv[1]], np.float) if periodicity_vectors is not None and len(periodicity_vectors) == 1: if np.all(np.array(delta) == np.array( periodicity_vectors[0])) or np.all( np.array(delta) == -np.array(periodicity_vectors[0])): e = FancyArrowPatch(n1center, n2center, patchA=n1, patchB=n2, arrowstyle='-|>', connectionstyle='arc3,rad=%s' % rad, mutation_scale=15.0, lw=2, alpha=alpha, color='r', linestyle='dashed') else: e = FancyArrowPatch(n1center, n2center, patchA=n1, patchB=n2, arrowstyle='-|>', connectionstyle='arc3,rad=%s' % rad, mutation_scale=10.0, lw=2, alpha=alpha, color=color) else: ecolor = color if np.allclose(np.array(delta), np.zeros(3)) else periodic_color e = FancyArrowPatch(n1center, n2center, patchA=n1, patchB=n2, arrowstyle='-|>', connectionstyle='arc3,rad=%s' % rad, mutation_scale=10.0, lw=2, alpha=alpha, color=ecolor) ax.annotate(delta, midarc, ha='center', va='center', xycoords='data', xytext=xytext_offset, textcoords='offset points') seen[(u, v)] = rad ax.add_patch(e) return e