diff --git a/mapmaker/maker.py b/mapmaker/maker.py index 237e8aed..a6fdac96 100644 --- a/mapmaker/maker.py +++ b/mapmaker/maker.py @@ -530,6 +530,8 @@ def __save_metadata(self): tile_db.add_metadata(pathways=json.dumps(self.__flatmap.connectivity())) # Save annotations in metadata tile_db.add_metadata(annotations=json.dumps(self.__flatmap.annotations, default=set_as_list)) + # Save node_hierarchy in metadata + tile_db.add_metadata(node_hierarchy=json.dumps(self.__flatmap.properties_store.node_hierarchy)) # Commit updates to the database tile_db.execute("COMMIT") diff --git a/mapmaker/output/__init__.py b/mapmaker/output/__init__.py index 18d504a1..3029ac72 100644 --- a/mapmaker/output/__init__.py +++ b/mapmaker/output/__init__.py @@ -64,7 +64,9 @@ 'missing-nodes', 'alert', 'biological-sex', - 'anatomical-nodes' # list[[str, list[str]]] + 'anatomical-nodes', # list[[str, list[str]]] + 'rendered', + 'sckan' ] #=============================================================================== diff --git a/mapmaker/properties/__init__.py b/mapmaker/properties/__init__.py index 2076f445..50ff3eed 100644 --- a/mapmaker/properties/__init__.py +++ b/mapmaker/properties/__init__.py @@ -129,6 +129,10 @@ def pathways(self): def proxies(self): return self.__proxies + @property + def node_hierarchy(self): + return self.__pathways.node_hierarchy + def network_feature(self, feature): #================================== # Is the ``feature`` included in some network? diff --git a/mapmaker/properties/pathways.py b/mapmaker/properties/pathways.py index 34cde5fe..5983f1b5 100644 --- a/mapmaker/properties/pathways.py +++ b/mapmaker/properties/pathways.py @@ -119,6 +119,7 @@ def __init__(self): self.__nodes = set() self.__models = None self.__centrelines = set() + self.__connectivity = set() @property def as_dict(self) -> dict[str, Any] : @@ -129,7 +130,8 @@ def as_dict(self) -> dict[str, Any] : 'lines': list(self.__lines), 'nerves': list(self.__nerves), 'nodes': list(self.__nodes), - 'models': self.__models + 'models': self.__models, + 'connectivity': list(self.__connectivity) } if len(self.__centrelines): result['centrelines'] = list(self.__centrelines) @@ -182,6 +184,17 @@ def set_model_id(self, model_id: str): """ self.__models = model_id + def extend_connectivity(self, connectivity: list[tuple]): + """ + Associate rendered connectivity with the path. + + Arguments: + ---------- + connectivity + Rendered connectivity edges + """ + self.__connectivity.update(connectivity) + #=============================================================================== class Route: @@ -258,6 +271,7 @@ def __resolve_nodes_for_path(self, path_id, node_feature_ids): def add_connectivity(self, path_id: str, line_geojson_ids: list[int], model: str, path_type: PATH_TYPE, node_feature_ids: set[str], nerve_features: list[Feature], + rendered_connectivity: list[tuple], centrelines: Optional[list[str]]=None): resolved_path = self.__paths[path_id] if model is not None: @@ -266,6 +280,7 @@ def add_connectivity(self, path_id: str, line_geojson_ids: list[int], resolved_path.extend_nodes(self.__resolve_nodes_for_path(path_id, node_feature_ids)) resolved_path.extend_lines(line_geojson_ids) resolved_path.extend_nerves([f.geojson_id for f in nerve_features]) + resolved_path.extend_connectivity(rendered_connectivity) if centrelines is not None: resolved_path.add_centrelines(centrelines) @@ -496,6 +511,7 @@ def __init__(self, flatmap, paths_list): self.__connectivity_models = [] self.__active_nerve_ids: set[str] = set() ### Manual layout only??? self.__connection_sets: list[ConnectionSet] = [] + self.__node_hierarchy = defaultdict(set) if len(paths_list): self.add_connectivity({'paths': paths_list}) @@ -533,6 +549,14 @@ def connectivity(self): connectivity['type-paths'][path_type].extend(paths) return connectivity + @property + def node_hierarchy(self): + node_hierarchy = { + 'nodes': [{'id':node} for node in self.__node_hierarchy['nodes']], + 'links': [{'source':link[0], 'target':link[1]} for link in self.__node_hierarchy['links']] + } + return node_hierarchy + def add_connection_set(self, connection_set): #============================================ if len(connection_set): @@ -643,6 +667,31 @@ def add_connectivity(self, connectivity): for nerve_id in nerves: self.__paths_by_nerve_id[nerve_id].append(path_id) + def __extract_rendered_connectivity(self, node_feature_ids, connectivity_graph): + # restructure connectivity graph so it aligns to self.__resolved_pathways + removed_nodes = [node for node, node_dict in connectivity_graph.nodes(data=True) + if node_dict.get('type') == 'feature' and + not {f.id for f in node_dict.get('features', [])} & node_feature_ids] + for node in removed_nodes: + neighbors = list(connectivity_graph.neighbors(node)) + predecessors = [n for n in neighbors if n == connectivity_graph.edges[(node, n)]['predecessor']] + successors = [n for n in neighbors if n == connectivity_graph.edges[(node, n)]['successor']] + predecessors, successors = (predecessors or neighbors), (successors or neighbors) + connectivity_graph.add_edges_from([(e_0, e_1, {'predecessor': e_0, 'successor': e_1}) + for e_0 in predecessors for e_1 in successors if e_0 != e_1]) + connectivity_graph.remove_nodes_from(removed_nodes) + + # extract hierarchy + for node_dict in connectivity_graph.nodes.values(): + self.__node_hierarchy['nodes'].add(source := node_dict['node']) + while len(target := source[1]) > 0: + target = (target[0], target[1:]) + self.__node_hierarchy['links'].add((source, target)) + self.__node_hierarchy['nodes'].add(source := target) + + return [(connectivity_graph.nodes[edge[0]]['node'], connectivity_graph.nodes[edge[1]]['node']) for edge in connectivity_graph.edges] + + def __route_network_connectivity(self, network: Network): #======================================================== if self.__resolved_pathways is None: @@ -725,12 +774,14 @@ def __route_network_connectivity(self, network: Network): nerve_feature_ids = routed_path.nerve_feature_ids nerve_features = [self.__flatmap.get_feature(nerve_id) for nerve_id in nerve_feature_ids] active_nerve_features.update(nerve_features) + rendered_connectivity = self.__extract_rendered_connectivity(routed_path.node_feature_ids, route_graphs[path_id].graph['connectivity']) self.__resolved_pathways.add_connectivity(path_id, path_geojson_ids, path.models, path.path_type, routed_path.node_feature_ids, nerve_features, + rendered_connectivity, centrelines=routed_path.centrelines) for feature in active_nerve_features: if feature.get_property('type') == 'nerve' and feature.geom_type == 'LineString': diff --git a/mapmaker/routing/__init__.py b/mapmaker/routing/__init__.py index 3c7c3b52..ddd3face 100644 --- a/mapmaker/routing/__init__.py +++ b/mapmaker/routing/__init__.py @@ -846,7 +846,7 @@ def bypass_missing_node(ms_node): g_node = ref_nodes[0] ref_nodes = ref_nodes[1:] for ref_node in ref_nodes: - connectivity_graph = nx.contracted_nodes(connectivity_graph, g_node, ref_node, self_loops=False) + nx.contracted_nodes(connectivity_graph, g_node, ref_node, self_loops=False, copy=False) if path.trace: for node, node_dict in connectivity_graph.nodes(data=True): @@ -1515,6 +1515,26 @@ def set_direction(upstream_node): if len(min_degree_nodes & set(self.__missing_identifiers)): self.__log.warning('Path is not rendered due to partial rendering', path=path.id) route_graph.remove_nodes_from(list(route_graph.nodes)) + connectivity_graph.remove_nodes_from(list(connectivity_graph.nodes)) + + # Add 'sckan' and 'rendered' properties to features corresponding to connectivity_graph nodes. + for node, node_dict in connectivity_graph.nodes(data=True): + features = node_dict.get('features', set()) | { + feature + for nerve_id in node_dict.get('nerve-ids', []) + if (feature := self.__flatmap.get_feature(nerve_id)) + } | { + feature + for edge in subgraph.edges + if edge in route_graph.edges + if (feature := self.__flatmap.get_feature(route_graph.edges[edge].get('centreline'))) + } if (subgraph := node_dict.get('subgraph')) else set() + for feature in features: + feature.append_property('sckan', (path.id, node)) + feature.append_property('rendered', (path.id, node_dict['node'])) + + # Assign connectivity_node as a route_graph property, which will be used along with ResolvedPathways. + route_graph.graph['connectivity'] = connectivity_graph if debug: return (route_graph, G, connectivity_graph, terminal_graphs) # type: ignore