From 6a684d297c382af2043de7a058da19bb5e4abbfd Mon Sep 17 00:00:00 2001 From: mozman Date: Sun, 26 May 2024 08:15:46 +0200 Subject: [PATCH] add EdgeDeposit.build_all_networks() --- src/ezdxf/edgeminer.py | 76 ++++++++++++++++++++++- tests/test_05_tools/test_546_edgeminer.py | 30 +++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/src/ezdxf/edgeminer.py b/src/ezdxf/edgeminer.py index f9a44f59c..c6c332989 100644 --- a/src/ezdxf/edgeminer.py +++ b/src/ezdxf/edgeminer.py @@ -240,6 +240,53 @@ def find_all_loops(edges: Sequence[Edge], gap_tol=GAP_TOL) -> Sequence[Sequence[ return tuple(finder) +def find_all_loops_net( + edges: Sequence[Edge], gap_tol=GAP_TOL, timeout=TIMEOUT +) -> Sequence[Sequence[Edge]]: + """Returns all unique closed loops and doesn't include reversed solutions. + + Note: Recursive backtracking algorithm with time complexity of O(n!). + + Args: + edges: edges to be examined + gap_tol: maximum vertex distance to consider two edges as connected + timeout: timeout in seconds + + Raises: + TimeoutError: search process has timed out + TypeError: invalid data in sequence `edges` + """ + deposit = EdgeDeposit(edges, gap_tol=gap_tol) + if len(deposit.edges) < 2: + return tuple() + return find_all_loops_in_deposit(deposit, timeout=timeout) + + +def find_all_loops_in_deposit( + deposit: EdgeDeposit, timeout=TIMEOUT +) -> Sequence[Sequence[Edge]]: + """Returns all unique closed loops in found in the edge deposit and doesn't include + reversed solutions. + + Note: Recursive backtracking algorithm with time complexity of O(n!). + + Args: + deposit: edge deposit + timeout: timeout in seconds + + Raises: + TimeoutError: search process has timed out + """ + gap_tol = deposit.gap_tol + solutions: list[Sequence[Edge]] = [] + for network in deposit.build_all_networks(timeout=timeout): + finder = LoopFinderNet(network, gap_tol=gap_tol, timeout=timeout) + for edge in network: + finder.search(edge) + solutions.extend(finder) + return solutions + + def type_check(edges: Sequence[Edge]) -> Sequence[Edge]: for edge in edges: if not isinstance(edge, Edge): @@ -456,8 +503,9 @@ class EdgeDeposit: def __init__(self, edges: Sequence[Edge], gap_tol=GAP_TOL) -> None: self.gap_tol = gap_tol - self.edge_index = EdgeVertexIndex(edges) - self.search_index = SpatialSearchIndex(edges) + self.edges = type_check(tuple(edges)) + self.edge_index = EdgeVertexIndex(self.edges) + self.search_index = SpatialSearchIndex(self.edges) def edges_linked_to(self, vertex: UVec, radius: float = -1) -> Sequence[Edge]: """Returns all edges linked to `vertex` in range of `radius`. @@ -519,6 +567,27 @@ def process(vertex: Vec3) -> None: return network + def build_all_networks(self, timeout=TIMEOUT) -> Sequence[Network]: + """Returns all separated networks in this deposit. + + Raises: + TimeoutError: build process has timed out + """ + watchdog = Watchdog(timeout) + edges = set(self.edges) + networks: list[Network] = [] + while edges: + if watchdog.has_timed_out: + raise TimeoutError("search process has timed out") + edge = edges.pop() + network = self.build_network(edge, timeout=timeout) + if len(network): + networks.append(network) + edges -= set(network) + else: # solitary edge + edges.discard(edge) + return networks + class Network: """The all edges in a network are reachable from every other edge.""" @@ -555,6 +624,7 @@ def add_connections(self, base: Edge, targets: Iterable[Edge]) -> None: def edges_linked_to(self, edge: Edge) -> Sequence[Edge]: return tuple(self._edges[eid] for eid in self._connections[edge.id]) + class LoopFinderNet: def __init__( self, network: Network, discard_reverse=True, gap_tol=GAP_TOL, timeout=TIMEOUT @@ -574,7 +644,7 @@ def __len__(self) -> int: return len(self._solutions) def find_any_loop(self, start: Edge | None = None) -> Sequence[Edge]: - """Returns the first loop found beginning with the given start edge or an + """Returns the first loop found beginning with the given start edge or an arbitrary edge if `start` is None. """ if start is None: diff --git a/tests/test_05_tools/test_546_edgeminer.py b/tests/test_05_tools/test_546_edgeminer.py index 2b343ba08..54b96552f 100644 --- a/tests/test_05_tools/test_546_edgeminer.py +++ b/tests/test_05_tools/test_546_edgeminer.py @@ -326,6 +326,11 @@ def test_build_network_A_D(self): edges = network.edges_linked_to(self.A) assert len(edges) == 2 + def test_solitary_edge_is_not_a_network(self): + deposit = em.EdgeDeposit([self.A, self.C]) + network = deposit.build_network(self.A) + assert len(network) == 0 + def test_build_network_A_G(self): deposit = em.EdgeDeposit( [self.A, self.B, self.C, self.D, self.E, self.F, self.G] @@ -356,6 +361,31 @@ def test_build_network_A_G(self): edges = network.edges_linked_to(self.G) assert len(edges) == 3 + def test_build_all_networks(self): + deposit = em.EdgeDeposit( + [self.A, self.B, self.C, self.D, self.E, self.F, self.G] + ) + assert len(deposit.build_all_networks()) == 1 + + def test_build_all_disconnected_networks(self): + # 0 1 2 3 + # 1 +-C-+ +-G-+ + # | | | | + # D B H F + # | | | | + # 0 +-A-+ +-E-+ + E = em.Edge((2, 0), (3, 0), payload="E") + F = em.Edge((3, 0), (3, 1), payload="F") + G = em.Edge((3, 1), (2, 1), payload="G") + H = em.Edge((2, 1), (2, 0), payload="H") + + deposit = em.EdgeDeposit([self.A, self.B, self.C, self.D, E, F, G, H]) + assert len(deposit.build_all_networks()) == 2 + + def test_build_all_networks_solitary_edges(self): + deposit = em.EdgeDeposit([self.A, self.C, self.F]) + assert len(deposit.build_all_networks()) == 0 + if __name__ == "__main__": pytest.main([__file__])