def perform_experiment(original_graph: nx.Graph, ftrp: float, exp_name: str, data_path: str): print(exp_name) # removing graph isolates # NoisyGraph is not intended to deal with isolated nodes original_graph.remove_nodes_from(list(nx.isolates(original_graph))) # constructing noisy graph noisy_graph = NoisyGraph(ftrp=ftrp) noisy_graph.construct_graph(original_graph) # algorithm compliance sigma_mean, sigma_variance = noisy_graph.get_sigmas_profile() # uncertainty uncertainty_mean, uncertainty_variance = noisy_graph.get_uncertainty_profile( ) # centrality_metrics dc_distance, dc_correlation, dc_mean_change = noisy_graph.degree_centrality_profile( original_graph) bc_distance, bc_correlation, bc_mean_change = noisy_graph.betweenness_profile( original_graph) cc_distance, cc_correlation, cc_mean_change = noisy_graph.closeness_profile( original_graph) ec_distance, ec_correlation, ec_mean_change = noisy_graph.eigenvector_centrality_profile( original_graph) # create raw file with original edges and noisy edges f = open(f"raw_data/{exp_name[:2]}.txt", "a") f.write(f"Experimental conditions: {exp_name}\n") f.write(f"Real edges: {noisy_graph.edges_if(real=True)}\n") f.write(f"Fake edges: {noisy_graph.edges_if(real=False)}\n\n") f.close() # add result to csv result = f"{exp_name},{sigma_mean},{sigma_variance},{uncertainty_mean},{uncertainty_variance}," result += f"{dc_distance},{dc_correlation},{dc_mean_change}," result += f"{bc_distance},{bc_correlation},{bc_mean_change}," result += f"{cc_distance},{cc_correlation},{cc_mean_change}," result += f"{ec_distance},{ec_correlation},{ec_mean_change}" result += "\n" f = open(data_path, "a") f.write(result) f.close()
def propcess_graph(graph: nx.Graph) -> t.Tuple[pd.DataFrame, t.Dict[str, int]]: """ Process information in graph, returning a table (table data + column names) as a result. Each row of the table represents a side-chain, and each column records one property of the side-chain, such as the number of heavy atoms it contains, whether it is a hydrogen bond donor or acceptor, ect. """ scaffold_nodes = [] for node_id, node in graph.nodes.items(): if 'is_scaffold' in node: scaffold_nodes.append(node_id) # Remove scaffold graph.remove_nodes_from(scaffold_nodes) # Initialize data graph_info = [] # Iterate through disconnected subgraphs for subgraph in connected_components(graph): attached_atom_id = None num_heavy_atoms, is_hbd, is_hba = 0, False, False for node_id in subgraph: node = graph.nodes[node_id] if attached_atom_id is None and 'anchor' in node: attached_atom_id = node['anchor'] if not is_hba and 'is_hba' in node: is_hba = True if not is_hbd and 'is_hbd' in node: is_hbd = True num_heavy_atoms += 1 if attached_atom_id is None: raise ValueError is_hbd_and_hba = is_hbd and is_hba graph_info.append([ attached_atom_id, num_heavy_atoms, int(is_hbd), int(is_hba), int(is_hbd_and_hba) ]) # Convert graph_info to dataframe if not graph_info: raise NoSubstitutionException() graph_info = np.array(graph_info, dtype=np.int32) col_names = [ 'attached_atom_id', 'num_heavy_atoms', 'is_hbd', 'is_hba', 'is_hbd_and_hba' ] col_names = {key: val for val, key in enumerate(col_names)} return graph_info, col_names
def remove_isolates(graph: nx.Graph) -> int: """Removes all isolates nodes in the given graph. Parameters ---------- graph : nx.Graph A graph on which to remove all isolates Returns ------- int Number of removed isolates """ isolates = list(nx.isolates(graph)) graph.remove_nodes_from(isolates) return len(isolates)
def visualize_graph_layer(graph: Graph, layer: int): graph = graph.copy() __pull__overlapping_vertices_apart(graph, 0.05) to_remove = [] for node, data in graph.nodes(data=True): if data['layer'] != layer: to_remove.append(node) graph.remove_nodes_from(to_remove) colors = [__get_color(d) for n, d in graph.nodes(data=True)] networkx.draw(graph, networkx.get_node_attributes(graph, 'position'), labels=networkx.get_node_attributes(graph, 'label'), node_color=colors, with_labels=True)
def draw_graph(g: nx.Graph, seed=6, pos=None, idx2node=None, limit_node=50): g = g.copy() if limit_node is not None: g.remove_nodes_from(nodes=list(g.nodes)[limit_node:]) if pos is None: pos = nx.spring_layout(g, seed=seed) nx.draw(g, pos=pos) if idx2node is not None: labels = {} for u in g.nodes: labels[u] = str(idx2node[u]) nx.draw_networkx_labels(g, pos, labels=labels) else: nx.draw_networkx_labels(g, pos=pos) plt.show()
def filter_graph( G: nx.Graph, min_component_size: int = FamilyNetworkConfig.default_min_component_size, ) -> nx.Graph: # create copy of input graph G = G.copy() # remove discrete components with less than 3 nodes. small_components = [ c for c in nx.connected_components(G) if len(c) < min_component_size ] for component in small_components: G.remove_nodes_from(component) logger.debug( f"Removed discrete components with less than {min_component_size} nodes from graph." ) return G
def top_k_prediction_edges(G: nx.Graph, y_pred, possible_edges_df, top_k, show_acc_on_edge=False, plot_link_pred=False, limit_node=100, idx2node=None): # get top K link prediction # sorted_y_pred, sorted_possible_edges = zip(*sorted(zip(y_pred, possible_egdes))) node_1 = possible_edges_df['node_1'].to_list() node_2 = possible_edges_df['node_2'].to_list() # unconnected_edges = [possible_egdes_df['node_1'].to_list(), possible_egdes_df['node_2'].to_list()] unconnected_edges = [(node_1[i], node_2[i]) for i in range(len(node_1))] sorted_y_pred, sorted_possible_edges = (list(t) for t in zip( *sorted(zip(y_pred, unconnected_edges), reverse=True))) if plot_link_pred: if len(G.nodes) > limit_node: G.remove_nodes_from(nodes=list(G.nodes)[limit_node:]) if show_acc_on_edge: plot_link_prediction_graph( G=G, pred_edges=sorted_possible_edges[:top_k], pred_acc=sorted_y_pred[:top_k], idx2node=idx2node) else: plot_link_prediction_graph( G=G, pred_edges=sorted_possible_edges[:top_k], idx2node=idx2node) if idx2node is None: original_pred_edges = sorted_possible_edges[:top_k] else: original_pred_edges = [(idx2node[u], idx2node[v]) for u, v in sorted_possible_edges[:top_k]] print(f"Top {top_k} predicted edges: edge|accuracy") for i in range(top_k): print(f"{original_pred_edges[i]} : {round(sorted_y_pred[i], 2)}") return original_pred_edges, sorted_y_pred[:top_k]
def plot_network(g: nx.Graph, *, height='1000px', smooth_edges=None, max_node_size=100, min_node_size=5, largest_component=True, interactive=True, controls=False, scale=1, iterations=1000, gravity=1): """Plot a `networkx.Graph` generated by one of the `build_X_graph` functions in this module. Plotting is done using the `pyvis` library. :param height: Height of the plot. :param smooth_edges: Enables curved ('smooth') edges. Looks nice but is heavy on performance. :param min_node_size: The radius of the smallest node. :param max_node_size: The radius of the largest node. :param largest_component: Only plot the largest connected component of the graph. """ if isolates := list(nx.isolates(g)): g = g.copy() g.remove_nodes_from(isolates)
def remove_dead_ends(graph: nx.Graph, node_filter=no_filter, only_strict=False): """Remove dead ends from a given graph. A dead end is defined as a node having only one neighbor. For directed graphs, a strict dead end is a node having a unique predecessor and no successors. A weak dead end is a node having a unique predecessor that is also its unique successor. Parameters ---------- graph : nx.Graph Graph to simplify node_filter : Evaluates to true if a node can be removed, false otherwise. (Default value = no_filter) only_strict : If true, remove only strict dead ends. Used only for directed graphs. (Default value = False) """ nodes_to_remove = get_dead_ends(graph, node_filter, only_strict) while nodes_to_remove: graph.remove_nodes_from(nodes_to_remove) nodes_to_remove = get_dead_ends(graph, node_filter, only_strict)
def graph_to_node_link(g: nx.Graph, user_name_dict: Dict[int, str] = None, min_degree: int = 0) -> dict: # logger.debug("Graph nodes: %s", g.nodes()) # logger.debug("Graph edges: %s", g.edges()) # add usernames before taking stuff out """ transforms the graph to node link for d3js and optionally removes nodes having fewer than a certain number of edges :param g: an instance of networkx.Graph :param user_name_dict: a mapping of user ids to user names :param min_degree: the minimum number of edges for a node to be included :return: a dict/json sufficient for use with d3js force layouts etc """ if user_name_dict: nx.relabel_nodes(g, user_name_dict, copy=False) if min_degree > 0: outdeg = g.degree() to_remove = [n for n in outdeg if outdeg[n] < min_degree] g.remove_nodes_from(to_remove) data = nxjs.node_link_data(g) return data
def clique_exact_find_greedy_eliminate(graph: nx.Graph) -> list: """Perform minimum clique cover by exactly find maximum clique and iteratively eliminate it. Find the maximum clique by enumerating all the cliques and iteratively eliminate it. Args: graph (nx.Graph): graph to solve Returns: list: list of node names for each clique """ graph = graph.copy() clique_list = [] while len(graph.nodes())>0: max_size = 0 max_clique = [] for clique in nx.find_cliques(graph): size = len(clique) if size > max_size: max_size = size max_clique = clique graph.remove_nodes_from(max_clique) clique_list.append(max_clique) return clique_list
def remove_identical_clusters(graph: nx.Graph): """ Removes all nodes from the graph that are only connected to a single other node (ie. identical clusters) :param graph: The graph to manipulate :return: The number of removed nodes """ identical_clusters = list() for node in graph.nodes: neighbors = list(graph.neighbors(node)) if len(neighbors) != 1: continue # make sure the neighbor also only has 1 neighbor neighbors_neighbors = list(graph.neighbors(neighbors[0])) if len(neighbors_neighbors) == 1: identical_clusters.append(node) graph.remove_nodes_from(identical_clusters) return len(identical_clusters)
def preprocess(graph: nx.Graph): # Doing it inplace to reduce memory usage selfloop_edges = nx.selfloop_edges(graph) graph.remove_edges_from(selfloop_edges) # Remove self-loops isolated_nodes = nx.isolates(graph) graph.remove_nodes_from(isolated_nodes) # Remove orphan nodes return isolated_nodes, selfloop_edges
def filter_isolated_nodes(graph: nx.Graph): rejected_nodes = [] for node in graph.nodes: if graph.degree(node) == 0: rejected_nodes.append(node) graph.remove_nodes_from(rejected_nodes)
print('{0}/{1} nodes processed'.format(i, n), end='\r') print('{0}/{1} nodes processed'.format(i, n)) print('Search for orphaned nodes') orphaned = set() n = graph.number_of_nodes() i = 0 for node in graph.nodes_iter(): if graph.degree(node) == 0: orphaned.add(node) i += 1 print('{0}/{1} nodes processed'.format(i, n), end='\r') print('{0}/{1} nodes processed'.format(i, n)) print('Delete {0} orphaned nodes'.format(len(orphaned))) graph.remove_nodes_from(orphaned) print('Calculate offset') points = [node[1]['pos'] for node in graph.nodes(data=True)] min_x = min(points, key=lambda p: p[0])[0] min_y = min(points, key=lambda p: p[1])[1] for node in graph.nodes_iter(): pos = (graph.node[node]['pos'][0] - min_x, graph.node[node]['pos'][1] - min_y) graph.node[node]['pos'] = pos print('Translated data by ({0}, {1})'.format(-min_x, -min_y)) print('Calculate edge weights') n = graph.number_of_edges() i = 0 for edge in graph.edges(): lat1 = math.radians(graph.node[edge[0]]['lat'])
class Gratoms(Atoms): """Graph based atoms object. An Integrated class for an ASE atoms object with a corresponding Networkx Graph. """ def __init__(self, symbols=None, positions=None, numbers=None, tags=None, momenta=None, masses=None, magmoms=None, charges=None, scaled_positions=None, cell=None, pbc=None, celldisp=None, constraint=None, calculator=None, info=None, edges=None): super().__init__(symbols, positions, numbers, tags, momenta, masses, magmoms, charges, scaled_positions, cell, pbc, celldisp, constraint, calculator, info) if isinstance(edges, np.ndarray): if self.pbc.any(): self._graph = MultiGraph(edges) else: self._graph = Graph(edges) else: if self.pbc.any(): self._graph = MultiGraph() else: self._graph = Graph() nodes = [[i, { 'number': n }] for i, n in enumerate(self.arrays['numbers'])] self._graph.add_nodes_from(nodes) if isinstance(edges, list): self._graph.add_edges_from(edges) @property def graph(self): return self._graph @property def nodes(self): return self._graph.nodes @property def edges(self): return self._graph.edges @property def adj(self): return self._graph.adj @property def degree(self): degree = self._graph.degree return np.array([_[1] for _ in degree]) @property def connectivity(self): connectivity = nx.to_numpy_matrix(self._graph).astype(int) return connectivity def get_surface_atoms(self): """Return surface atoms.""" surf_atoms = np.where(self.get_array('surface_atoms') > 0)[0] return surf_atoms def set_surface_atoms(self, top, bottom=None): """Assign surface atoms.""" n = np.zeros(len(self)) if bottom is not None: n[bottom] = -1 # Overwrites bottom indexing n[top] = 1 self.set_array('surface_atoms', n) def get_neighbor_symbols(self, u): """Get chemical symbols for neighboring atoms of u.""" neighbors = list(self._graph[u]) return sym[self.arrays['numbers'][neighbors]] def is_isomorph(self, other): """Check if isomorphic by bond count and atomic number.""" isomorphic = nx.is_isomorphic(self._graph, other._graph, edge_match=em, node_match=nm) return isomorphic def get_chemical_tags(self, rank=2): """Generate a hash descriptive of the chemical formula (rank 0) or include bonding (rank 1). """ cnt = np.bincount(self.arrays['numbers']) composition = ','.join(cnt.astype(str)) if rank == 1: return composition[2:] for adj in self.adj.items(): num = self.arrays['numbers'][list(adj[1].keys())] cnt += np.bincount(num, minlength=len(cnt)) bonding = ','.join(cnt.astype(str)) return composition[2:], bonding[2:] def get_unsaturated_nodes(self, screen=None): unsaturated = [] for node, data in self.nodes(data=True): radicals = data['valence'] if screen in data: continue if radicals > 0: unsaturated += [node] return np.array(unsaturated) def copy(self): """Return a copy.""" atoms = self.__class__(cell=self._cell, pbc=self._pbc, info=self.info) atoms.arrays = {} for name, a in self.arrays.items(): atoms.arrays[name] = a.copy() atoms.constraints = copy.deepcopy(self.constraints) if hasattr(self, '_graph'): atoms._graph = self._graph.copy() return atoms def __getitem__(self, i): """Return a subset of the atoms. i -- scalar integer, list of integers, or slice object describing which atoms to return. If i is a scalar, return an Atom object. If i is a list or a slice, return an Atoms object with the same cell, pbc, and other associated info as the original Atoms object. The indices of the constraints will be shuffled so that they match the indexing in the subset returned. """ if isinstance(i, (int, np.int64)): natoms = len(self) if i < -natoms or i >= natoms: raise IndexError('Index out of range.') return Atom(atoms=self, index=i) elif isinstance(i, list) and len(i) > 0: # Make sure a list of booleans will work correctly and not be # interpreted at 0 and 1 indices. i = np.array(i) conadd = [] # Constraints need to be deepcopied, but only the relevant ones. for con in copy.deepcopy(self.constraints): if isinstance(con, (FixConstraint, FixBondLengths)): try: con.index_shuffle(self, i) conadd.append(con) except IndexError: pass atoms = self.__class__(cell=self._cell, pbc=self._pbc, info=self.info, celldisp=self._celldisp) atoms.arrays = {} for name, a in self.arrays.items(): atoms.arrays[name] = a[i].copy() # Copy the graph, conserving correct indexing if self.nodes: nodes = [[_, { 'number': n }] for _, n in enumerate(self.arrays['numbers'])] atoms.graph.add_nodes_from(nodes) j = i.tolist() for u, v in self.graph.edges(): if u not in i or v not in i: continue atoms.graph.add_edge(j.index(u), j.index(v)) atoms.constraints = conadd return atoms def __iadd__(self, other): """Extend atoms object by appending atoms from *other*.""" if isinstance(other, Atom): other = self.__class__([other]) n1 = len(self) n2 = len(other) for name, a1 in self.arrays.items(): a = np.zeros((n1 + n2, ) + a1.shape[1:], a1.dtype) a[:n1] = a1 if name == 'masses': a2 = other.get_masses() else: a2 = other.arrays.get(name) if a2 is not None: a[n1:] = a2 self.arrays[name] = a for name, a2 in other.arrays.items(): if name in self.arrays: continue a = np.empty((n1 + n2, ) + a2.shape[1:], a2.dtype) a[n1:] = a2 if name == 'masses': a[:n1] = self.get_masses()[:n1] else: a[:n1] = 0 self.set_array(name, a) if isinstance(other, Gratoms): if isinstance(self._graph, nx.MultiGraph) & \ isinstance(other._graph, nx.Graph): other._graph = nx.MultiGraph(other._graph) self._graph = nx.disjoint_union(self._graph, other._graph) return self def __delitem__(self, i): from ase.constraints import FixAtoms for c in self._constraints: if not isinstance(c, FixAtoms): raise RuntimeError('Remove constraint using set_constraint() ' 'before deleting atoms.') if isinstance(i, (list, int)): # Make sure a list of booleans will work correctly and not be # interpreted at 0 and 1 indices. i = np.atleast_1d(i) n = len(self) i = np.arange(n)[i] if len(self._constraints) > 0: if isinstance(i, int): i = [i] constraints = [] for c in self._constraints: c = c.delete_atoms(i, n) if c is not None: constraints.append(c) self.constraints = constraints mask = np.ones(len(self), bool) mask[i] = False for name, a in self.arrays.items(): self.arrays[name] = a[mask] if self.nodes: self._graph.remove_nodes_from(i) mapping = dict(zip(np.where(mask)[0], np.arange(len(self)))) nx.relabel_nodes(self._graph, mapping, copy=False) def __imul__(self, m): """In-place repeat of atoms.""" if isinstance(m, int): m = (m, m, m) for x, vec in zip(m, self._cell): if x != 1 and not vec.any(): raise ValueError( 'Cannot repeat along undefined lattice vector') if self.pbc.any() and len(self.edges()) > 0: warnings.warn(("Edge conservation not currently supported with " "pbc. Remove pbc or edges first.")) M = np.product(m) n = len(self) for name, a in self.arrays.items(): self.arrays[name] = np.tile(a, (M, ) + (1, ) * (len(a.shape) - 1)) cgraph = self._graph.copy() positions = self.arrays['positions'] i0 = 0 for m0 in range(m[0]): for m1 in range(m[1]): for m2 in range(m[2]): i1 = i0 + n positions[i0:i1] += np.dot((m0, m1, m2), self._cell) i0 = i1 if m0 + m1 + m2 != 0: self._graph = nx.disjoint_union(self._graph, cgraph) if self.constraints is not None: self.constraints = [c.repeat(m, n) for c in self.constraints] self._cell = np.array([m[c] * self._cell[c] for c in range(3)]) return self
def remove_isolates(G: nx.Graph): G.remove_nodes_from(list(nx.isolates(G))) return G
class Gratoms(Atoms): """Graph based atoms object. An Integrated class for an ASE atoms object with a corresponding Networkx Graph. """ def __init__(self, symbols=None, positions=None, numbers=None, tags=None, momenta=None, masses=None, magmoms=None, charges=None, scaled_positions=None, cell=None, pbc=None, celldisp=None, constraint=None, calculator=None, info=None, edges=None): super().__init__(symbols, positions, numbers, tags, momenta, masses, magmoms, charges, scaled_positions, cell, pbc, celldisp, constraint, calculator, info) if self.pbc.any(): self._graph = MultiGraph() else: self._graph = Graph() nodes = [[i, { 'number': n }] for i, n in enumerate(self.arrays['numbers'])] self._graph.add_nodes_from(nodes) if edges: self._graph.add_edges_from(edges, bonds=1) self._surface_atoms = None @property def graph(self): return self._graph @property def nodes(self): return self._graph.nodes @property def edges(self): return self._graph.edges @property def adj(self): return self._graph.adj def get_surface_atoms(self): """Return surface atoms.""" return self._surface_atoms def set_surface_atoms(self, surface_atoms): """Assign surface atoms.""" self._surface_atoms = surface_atoms def get_neighbor_symbols(self, u): """Get chemical symbols for neighboring atoms of u.""" neighbors = list(self._graph[u]) return sym[self.arrays['numbers'][neighbors]] def is_isomorph(self, other): """Check if isomorphic by bond count and atomic number.""" isomorphic = nx.is_isomorphic( self._graph, other._graph, edge_match=em, node_match=nm) return isomorphic def get_chemical_tags(self, rank=2): """Generate a hash descriptive of the chemical formula (rank 0) or include bonding (rank 1). """ cnt = np.bincount(self.arrays['numbers']) composition = ','.join(cnt.astype(str)) if rank == 1: return composition[2:] for adj in self.adj.items(): num = self.arrays['numbers'][list(adj[1].keys())] cnt += np.bincount(num, minlength=len(cnt)) bonding = ','.join(cnt.astype(str)) return composition[2:], bonding[2:] def get_unsaturated_nodes(self, screen=None): unsaturated = [] for node, data in self.nodes(data=True): radicals = data['valence'] if screen in data: continue if radicals > 0: unsaturated += [node] return np.array(unsaturated) def copy(self): """Return a copy.""" atoms = self.__class__(cell=self._cell, pbc=self._pbc, info=self.info) atoms.arrays = {} for name, a in self.arrays.items(): atoms.arrays[name] = a.copy() atoms.constraints = copy.deepcopy(self.constraints) atoms._graph = self.graph.copy() return atoms def __iadd__(self, other): """Extend atoms object by appending atoms from *other*.""" if isinstance(other, Atom): other = self.__class__([other]) n1 = len(self) n2 = len(other) for name, a1 in self.arrays.items(): a = np.zeros((n1 + n2, ) + a1.shape[1:], a1.dtype) a[:n1] = a1 if name == 'masses': a2 = other.get_masses() else: a2 = other.arrays.get(name) if a2 is not None: a[n1:] = a2 self.arrays[name] = a for name, a2 in other.arrays.items(): if name in self.arrays: continue a = np.empty((n1 + n2, ) + a2.shape[1:], a2.dtype) a[n1:] = a2 if name == 'masses': a[:n1] = self.get_masses()[:n1] else: a[:n1] = 0 self.set_array(name, a) if isinstance(other, Gratoms): if isinstance(self._graph, nx.MultiGraph) & \ isinstance(other._graph, nx.Graph): other._graph = nx.MultiGraph(other._graph) self._graph = nx.disjoint_union(self._graph, other._graph) return self def __delitem__(self, i): from ase.constraints import FixAtoms for c in self._constraints: if not isinstance(c, FixAtoms): raise RuntimeError('Remove constraint using set_constraint() ' 'before deleting atoms.') if isinstance(i, (list, int)): # Make sure a list of booleans will work correctly and not be # interpreted at 0 and 1 indices. i = np.atleast_1d(i) if len(self._constraints) > 0: n = len(self) i = np.arange(n)[i] if isinstance(i, int): i = [i] constraints = [] for c in self._constraints: c = c.delete_atoms(i, n) if c is not None: constraints.append(c) self.constraints = constraints mask = np.ones(len(self), bool) mask[i] = False for name, a in self.arrays.items(): self.arrays[name] = a[mask] if isinstance(i, slice): i = np.arange(n)[i] self._graph.remove_nodes_from(i) mapping = dict(zip(np.where(mask)[0], np.arange(len(self)))) nx.relabel_nodes(self._graph, mapping, copy=False) def __imul__(self, m): """In-place repeat of atoms.""" if isinstance(m, int): m = (m, m, m) for x, vec in zip(m, self._cell): if x != 1 and not vec.any(): raise ValueError( 'Cannot repeat along undefined lattice vector') if self.pbc.any() and len(self.edges()) > 0: raise ValueError("Edge conservation not currently supported with " "pbc. Remove pbc or edges first.") M = np.product(m) n = len(self) for name, a in self.arrays.items(): self.arrays[name] = np.tile(a, (M, ) + (1, ) * (len(a.shape) - 1)) cgraph = self._graph.copy() positions = self.arrays['positions'] i0 = 0 for m0 in range(m[0]): for m1 in range(m[1]): for m2 in range(m[2]): i1 = i0 + n positions[i0:i1] += np.dot((m0, m1, m2), self._cell) i0 = i1 if m0 + m1 + m2 != 0: self._graph = nx.disjoint_union(self._graph, cgraph) if self.constraints is not None: self.constraints = [c.repeat(m, n) for c in self.constraints] self._cell = np.array([m[c] * self._cell[c] for c in range(3)]) return self
def _dissolve_adjacent(_target_graph: nx.Graph, _parent_node_name: str, _node_group: Union[set, list, tuple], highest_degree=False) -> nx.Graph: # set the new centroid from the centroid of the node group's Multipoint: node_geoms = [] if not highest_degree: for n_uid in _node_group: x = _target_graph.nodes[n_uid]['x'] y = _target_graph.nodes[n_uid]['y'] node_geoms.append(geometry.Point(x, y)) # if by highest_degree, then find the centroid of the highest degree nodes else: highest_degree = 0 for n_uid in _node_group: if n_uid in _target_graph: if nx.degree(_target_graph, n_uid) > highest_degree: highest_degree = nx.degree(_target_graph, n_uid) # aggregate the highest degree nodes node_geoms = [] for n_uid in _node_group: if n_uid not in _target_graph: continue if nx.degree(_target_graph, n_uid) != highest_degree: continue x = _target_graph.nodes[n_uid]['x'] y = _target_graph.nodes[n_uid]['y'] # append geom node_geoms.append(geometry.Point(x, y)) # find the new centroid c = geometry.MultiPoint(node_geoms).centroid _target_graph.add_node(_parent_node_name, x=c.x, y=c.y) # remove old nodes and reassign to new parent node # first determine new edges new_edges = [] for uid in _node_group: for nb_uid in nx.neighbors(_target_graph, uid): # drop geoms between merged nodes # watch for self-loop edge cases if uid in _node_group and nb_uid in _node_group and uid != nb_uid: continue else: if 'geom' not in _target_graph[uid][nb_uid]: raise KeyError( f'Missing "geom" attribute for edge {uid}-{nb_uid}') line_geom = _target_graph[uid][nb_uid]['geom'] if line_geom.type != 'LineString': raise TypeError( f'Expecting LineString geometry but found {line_geom.type} geometry for edge {uid}-{nb_uid}.' ) # first orient geom in correct direction s_x = _target_graph.nodes[uid]['x'] s_y = _target_graph.nodes[uid]['y'] # check geom coordinates directionality - flip if facing backwards direction if not np.allclose( (s_x, s_y), line_geom.coords[0][:2], atol=0.001, rtol=0): line_geom = geometry.LineString(line_geom.coords[::-1]) # double check that coordinates now face the forwards direction if not np.allclose( (s_x, s_y), line_geom.coords[0][:2], atol=0.001, rtol=0): raise ValueError( f'Edge geometry endpoint coordinate mismatch for edge {uid}-{nb_uid}' ) # update geom starting point to new parent node's coordinates coords = list(line_geom.coords) coords[0] = (c.x, c.y) # if self-loop, then the end also needs updating if uid == nb_uid: coords[-1] = (c.x, c.y) target_uid = _parent_node_name else: target_uid = nb_uid new_line_geom = geometry.LineString(coords) new_edges.append( (_parent_node_name, target_uid, new_line_geom)) # remove the nodes from the target graph, this will also implicitly drop related edges _target_graph.remove_nodes_from(_node_group) # add the edges for s, e, geom in new_edges: # when dealing with a collapsed linestring, this should be a rare occurance if geom.length == 0: logger.warning( f'Encountered a geom of length 0m: check edge {s}-{e}.') continue # don't add edge duplicates from respectively merged nodes if (s, e) not in _target_graph.edges(): _target_graph.add_edge(s, e, geom=geom) # however, do add if substantially different geom... else: diff = _target_graph[s][e]['geom'].length / geom.length if abs(diff) > 1.25: _target_graph.add_edge(s, e, geom=geom) return _target_graph