diff --git a/src/archunitpython/common/projection/cycles/cycle_utils.py b/src/archunitpython/common/projection/cycles/cycle_utils.py index 2544f16..eb5841a 100644 --- a/src/archunitpython/common/projection/cycles/cycle_utils.py +++ b/src/archunitpython/common/projection/cycles/cycle_utils.py @@ -23,16 +23,13 @@ def get_outgoing_neighbours( def transform_edge_data(edges: list[NumberEdge]) -> list[NumberNode]: """Convert a list of edges into a list of nodes with in/out edges.""" unique_ids = CycleUtils.find_unique_nodes(edges) - nodes = [] - for node_id in unique_ids: - nodes.append( - NumberNode( - node=node_id, - incoming=[e for e in edges if e.to_node == node_id], - outgoing=[e for e in edges if e.from_node == node_id], - ) - ) - return nodes + node_id_map = {node_id: NumberNode(node = node_id) for node_id in unique_ids} + + for edge in edges: + node_id_map[edge.to_node].incoming.append(edge) + node_id_map[edge.from_node].outgoing.append(edge) + + return list(node_id_map.values()) @staticmethod def find_unique_nodes(edges: list[NumberEdge]) -> list[int]: diff --git a/src/archunitpython/common/projection/cycles/johnsons_apsp.py b/src/archunitpython/common/projection/cycles/johnsons_apsp.py index 91e0601..a9ab061 100644 --- a/src/archunitpython/common/projection/cycles/johnsons_apsp.py +++ b/src/archunitpython/common/projection/cycles/johnsons_apsp.py @@ -59,11 +59,11 @@ def _explore_neighbours(self, current_node: NumberNode) -> None: def _unblock(self, node: NumberNode) -> None: self._blocked = [n for n in self._blocked if n is not node] - to_remove: list[_BlockedBy] = [] + to_remove: set[_BlockedBy] = set() for blocker in self._blocked_map: if blocker.by is node: self._unblock(blocker.blocked) - to_remove.append(blocker) + to_remove.add(blocker) self._blocked_map = [b for b in self._blocked_map if b not in to_remove] def _is_part_of_current_start_cycle(self, current_node: NumberNode) -> bool: diff --git a/src/archunitpython/common/projection/cycles/tarjan_scc.py b/src/archunitpython/common/projection/cycles/tarjan_scc.py index eabea08..827ce38 100644 --- a/src/archunitpython/common/projection/cycles/tarjan_scc.py +++ b/src/archunitpython/common/projection/cycles/tarjan_scc.py @@ -2,6 +2,9 @@ from __future__ import annotations +from collections import defaultdict +from typing import Dict, Set + from archunitpython.common.projection.cycles.model import NumberEdge @@ -12,7 +15,7 @@ def __init__(self, node_id: int) -> None: self.id = node_id self.index = -1 self.lowlink = -1 - self.neighbours: list[int] = [] + self.neighbours: Set[int] = set() class TarjanSCC: @@ -27,8 +30,11 @@ def find_strongly_connected_components(self, edges: list[NumberEdge]) -> list[li self._graph: dict[int, _Vertex] = {} self._index = 0 self._stack: list[_Vertex] = [] + self._on_stack: Set[int] = set() self._sccs: list[list[NumberEdge]] = [] - self._edges = edges + self._outgoing_edge_map: Dict[int,list[NumberEdge]] = defaultdict(list) + for edge in edges: + self._outgoing_edge_map[edge.from_node].append(edge) self._init(edges) @@ -47,8 +53,7 @@ def _init(self, edges: list[NumberEdge]) -> None: self._graph[edge.to_node] = _Vertex(edge.to_node) v = self._graph[edge.from_node] - if edge.to_node not in v.neighbours: - v.neighbours.append(edge.to_node) + v.neighbours.add(edge.to_node) def _visit(self, vertex: _Vertex) -> None: """DFS visit for Tarjan's algorithm.""" @@ -56,27 +61,30 @@ def _visit(self, vertex: _Vertex) -> None: vertex.lowlink = self._index self._index += 1 self._stack.append(vertex) + self._on_stack.add(vertex.id) for neighbour_id in vertex.neighbours: w = self._graph[neighbour_id] if w.index < 0: self._visit(w) vertex.lowlink = min(vertex.lowlink, w.lowlink) - elif w in self._stack: + elif w.id in self._on_stack: vertex.lowlink = min(vertex.lowlink, w.index) if vertex.lowlink == vertex.index: - scc_vertices: list[_Vertex] = [] + scc_ids: Set[int] = set() while True: w = self._stack.pop() - scc_vertices.append(w) + self._on_stack.remove(w.id) + scc_ids.add(w.id) if w.id == vertex.id: break - if scc_vertices: - scc_ids = {v.id for v in scc_vertices} - scc_edges = [ - e for e in self._edges if e.from_node in scc_ids and e.to_node in scc_ids - ] + if scc_ids: + scc_edges: list[NumberEdge] = [] + for id in scc_ids: + for edge in self._outgoing_edge_map[id]: + if edge.to_node in scc_ids: + scc_edges.append(edge) if scc_edges: self._sccs.append(scc_edges) diff --git a/src/archunitpython/common/projection/project_nodes.py b/src/archunitpython/common/projection/project_nodes.py index cc2ce2a..5ef25d9 100644 --- a/src/archunitpython/common/projection/project_nodes.py +++ b/src/archunitpython/common/projection/project_nodes.py @@ -27,23 +27,21 @@ def project_to_nodes( all_labels: set[str] = set() for edge in graph: - if edge.external and not include_externals: - # Still record the source (internal file) - all_labels.add(edge.source) - outgoing[edge.source].append(edge) - continue - all_labels.add(edge.source) - all_labels.add(edge.target) - outgoing[edge.source].append(edge) if edge.source != edge.target: # Don't count self-edges as incoming - incoming[edge.target].append(edge) + outgoing[edge.source].append(edge) + + if (edge.external and include_externals) or not edge.external: + # Record the target + all_labels.add(edge.target) + if edge.source != edge.target: # Don't count self-edges as incoming + incoming[edge.target].append(edge) return [ ProjectedNode( label=label, incoming=incoming.get(label, []), - outgoing=[e for e in outgoing.get(label, []) if e.source != e.target], + outgoing=outgoing.get(label, []), ) for label in sorted(all_labels) ]