Skip to content

Commit

Permalink
remove class Loop
Browse files Browse the repository at this point in the history
  • Loading branch information
mozman committed May 27, 2024
1 parent 484990f commit 54915a1
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 119 deletions.
139 changes: 43 additions & 96 deletions src/ezdxf/edgeminer.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,74 +375,6 @@ def type_check(edges: Sequence[Edge]) -> Sequence[Edge]:
return edges


class Loop:
"""Represents connected edges.
Each end vertex of an edge is connected to the start vertex of the following edge.
It is a closed loop when the first edge is connected to the last edge.
(internal class)
"""

__slots__ = ("edges",)

def __init__(self, edges: tuple[Edge, ...]) -> None:
self.edges: tuple[Edge, ...] = edges

def __repr__(self) -> str:
content = ",".join(str(e) for e in self.edges)
return f"Loop([{content}])"

def __len__(self) -> int:
return len(self.edges)

def is_connected(self, edge: Edge, gap_tol=GAP_TOL) -> bool:
"""Returns ``True`` if the last edge of the loop is connected to the given edge.
Args:
edge: edge to be tested
gap_tol: maximum vertex distance to consider two edges as connected
"""
if self.edges:
return isclose(self.edges[-1].end, edge.start, gap_tol)
return False

def is_closed_loop(self, gap_tol=GAP_TOL) -> bool:
"""Returns ``True`` if the first edge is connected to the last edge.
Args:
gap_tol: maximum vertex distance to consider two edges as connected
"""

if len(self.edges) > 1:
return isclose(self.edges[0].start, self.edges[-1].end, gap_tol)
return False

def key(self, reverse=False) -> tuple[int, ...]:
"""Returns a normalized key.
The key is rotated to begin with the smallest edge id.
"""
if len(self.edges) < 2:
raise ValueError("too few edges")
if reverse:
ids = tuple(edge.id for edge in reversed(self.edges))
else:
ids = tuple(edge.id for edge in self.edges)
index = ids.index(min(ids))
if index:
ids = ids[index:] + ids[:index]
return ids

def ordered(self, reverse=False) -> Iterator[Edge]:
"""Returns the loop edges in key order."""
edges = {e.id: e for e in self.edges}
return (edges[eid] for eid in self.key(reverse))


SearchSolutions: TypeAlias = Dict[Tuple[int, ...], Tuple[Edge, ...]]


class EdgeVertexIndex:
"""Index of edges referenced by the id of their start- and end vertices.
Expand Down Expand Up @@ -632,6 +564,9 @@ def edges_linked_to(self, edge: Edge) -> Sequence[Edge]:
return tuple(self._edges[eid] for eid in self._connections[edge.id])


SearchSolutions: TypeAlias = Dict[Tuple[int, ...], Sequence[Edge]]


class LoopFinder:
"""Find closed loops in a network by a recursive backtracking algorithm.
Expand Down Expand Up @@ -677,42 +612,54 @@ def search(self, start: Edge, stop_at_first_loop: bool = False) -> None:
raise ValueError("start edge not in network")
network = self._network
gap_tol = self._gap_tol
solutions = self._solutions

watchdog = Watchdog(self._timeout)
todo: list[Loop] = [Loop((start,))] # "unlimited" recursion stack
todo: list[tuple[Edge, ...]] = [(start,)] # "unlimited" recursion stack
while todo:
if watchdog.has_timed_out:
raise TimeoutError("search process has timed out")
loop = todo.pop()
linked_edges = network.edges_linked_to(loop.edges[-1])
chain = todo.pop()
last_edge = chain[-1]
end_point = last_edge.end
candidates = network.edges_linked_to(last_edge)
# edges must be unique in a loop
for edge in set(linked_edges) - set(loop.edges):
extended_loop: Loop | None = None
if loop.is_connected(edge):
extended_loop = Loop(loop.edges + (edge,))
for edge in set(candidates) - set(chain):
if isclose(end_point, edge.start, gap_tol):
extended_chain = chain + (edge,)
elif isclose(end_point, edge.end, gap_tol):
extended_chain = chain + (edge.reversed(),)
else:
reversed_edge = edge.reversed()
if loop.is_connected(reversed_edge):
extended_loop = Loop(loop.edges + (reversed_edge,))
if extended_loop is None:
continue
if extended_loop.is_closed_loop(gap_tol):
add_search_solution(
solutions, extended_loop, self._discard_reverse_solutions
)
if is_forward_connected(extended_chain[-1], start, gap_tol):
self.add_solution(extended_chain)
if stop_at_first_loop:
return
else:
todo.append(extended_loop)


def add_search_solution(
solutions: SearchSolutions, loop: Loop, discard_reversed_loops: bool
) -> None:
key = loop.key()
if key in solutions:
return
if discard_reversed_loops and loop.key(reverse=True) in solutions:
return
solutions[key] = loop.edges
todo.append(extended_chain)

def add_solution(self, loop: Sequence[Edge]) -> None:
solutions = self._solutions
key = loop_key(loop)
if key in solutions:
return
if (
self._discard_reverse_solutions
and loop_key(loop, reverse=True) in solutions
):
return
solutions[key] = loop


def loop_key(edges: Sequence[Edge], reverse=False) -> tuple[int, ...]:
"""Returns a normalized key.
The key is rotated to begin with the smallest edge id.
"""
if reverse:
ids = tuple(edge.id for edge in reversed(edges))
else:
ids = tuple(edge.id for edge in edges)
index = ids.index(min(ids))
if index:
ids = ids[index:] + ids[:index]
return ids
32 changes: 9 additions & 23 deletions tests/test_05_tools/test_546_edgeminer.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,30 +60,17 @@ class TestLoop:
C = em.Edge((1, 1), (0, 1))
D = em.Edge((0, 1), (0, 0))

def test_is_connected(self):
loop = em.Loop((self.A,))
assert loop.is_connected(self.B) is True
def test_loop_key(self):
loop1 = (self.A, self.B, self.C)
loop2 = (self.B, self.C, self.A) # rotated edges, same loop

def test_is_not_connected(self):
loop = em.Loop((self.A,))
assert loop.is_connected(self.C) is False
assert (
loop.is_connected(self.D) is False
), "should not check reverse connected edges"
assert em.loop_key(loop1) == em.loop_key(loop2)

def test_is_closed_loop(self):
loop = em.Loop((self.A, self.B, self.C, self.D))
assert loop.is_closed_loop() is True

def test_is_not_closed_loop(self):
loop = em.Loop((self.A, self.B))
assert loop.is_closed_loop() is False

def test_key(self):
loop1 = em.Loop((self.A, self.B, self.C))
loop2 = em.Loop((self.B, self.C, self.A)) # rotated edges, same loop

assert loop1.key() == loop2.key()
def ordered_edges(edges: Sequence[em.Edge], reverse=False):
"""Returns the loop edges in key order."""
edge_dict = {e.id: e for e in edges}
return (edge_dict[eid] for eid in em.loop_key(edges, reverse))


def collect_payload(edges: Sequence[em.Edge]) -> str:
Expand All @@ -96,8 +83,7 @@ def collect_payload(edges: Sequence[em.Edge]) -> str:
return ""
elif len(edges) == 1:
return edges[0].payload # type: ignore
loop = em.Loop(edges) # type: ignore
return ",".join([e.payload for e in loop.ordered()])
return ",".join([e.payload for e in ordered_edges(edges)])


class TestFindSequential:
Expand Down

0 comments on commit 54915a1

Please sign in to comment.