Skip to content

Commit

Permalink
add EdgeDeposit.build_all_networks()
Browse files Browse the repository at this point in the history
  • Loading branch information
mozman committed May 26, 2024
1 parent 7a8858b commit 6a684d2
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 3 deletions.
76 changes: 73 additions & 3 deletions src/ezdxf/edgeminer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
30 changes: 30 additions & 0 deletions tests/test_05_tools/test_546_edgeminer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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__])

0 comments on commit 6a684d2

Please sign in to comment.