Skip to content

Commit

Permalink
refactor edgeminer module
Browse files Browse the repository at this point in the history
  • Loading branch information
mozman committed May 27, 2024
1 parent 1aa861f commit 1cfa688
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 44 deletions.
84 changes: 45 additions & 39 deletions src/ezdxf/edgeminer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
EdgeMiner
=========
A module for detecting shapes build of linked edges.
A module for detecting linked edges.
The complementary module ezdxf.edgesmith can create entities from the output of this
module.
Expand All @@ -27,16 +27,17 @@
"EdgeDeposit",
"find_all_loops_in_deposit",
"find_all_loops",
"find_all_sequential",
"find_first_loop_in_deposit",
"find_first_loop",
"find_sequential",
"is_backwards_connected",
"is_chain",
"is_forward_connected",
"is_loop",
"length",
"longest_chain",
"sequential_search_all",
"sequential_search",
"Network",
"shortest_chain",
"TimeoutError",
]
Expand All @@ -53,7 +54,6 @@ class TimeoutError(EdgeMinerException):
pass



class Edge:
"""Represents an edge.
Expand Down Expand Up @@ -122,8 +122,8 @@ def isclose(a: Vec3, b: Vec3, gap_tol=GAP_TOL) -> bool:

def is_forward_connected(a: Edge, b: Edge, gap_tol=GAP_TOL) -> bool:
"""Returns ``True`` if the edges have a forward connection.
Forward connection: a.end is connected to b.start
Forward connection: distance from a.end to b.start <= gap_tol
Args:
a: first edge
Expand All @@ -134,9 +134,9 @@ def is_forward_connected(a: Edge, b: Edge, gap_tol=GAP_TOL) -> bool:


def is_backwards_connected(a: Edge, b: Edge, gap_tol=GAP_TOL) -> bool:
"""Returns ``True`` if the edges have a backward connection.
Backwards connection: a.start is connected to b.end
"""Returns ``True`` if the edges have a backwards connection.
Backwards connection: distance from b.end to a.start <= gap_tol
Args:
a: first edge
Expand All @@ -147,9 +147,7 @@ def is_backwards_connected(a: Edge, b: Edge, gap_tol=GAP_TOL) -> bool:


def is_chain(edges: Sequence[Edge], gap_tol=GAP_TOL) -> bool:
"""Returns ``True`` if all edges are connected forward.
Forward connection: edge[n].end is connected to edge[n+1].start
"""Returns ``True`` if all edges have a forward connection.
Args:
edges: sequence of edges
Expand All @@ -159,15 +157,13 @@ def is_chain(edges: Sequence[Edge], gap_tol=GAP_TOL) -> bool:


def is_loop(edges: Sequence[Edge], gap_tol=GAP_TOL, full=True) -> bool:
"""Return ``True`` if the sequence of edges is a closed forward loop.
Forward connection: edge[n].end is connected to edge[n+1].start
"""Return ``True`` if the sequence of edges is a closed loop.
Args:
edges: sequence of edges
gap_tol: maximum vertex distance to consider two edges as connected
full: does a full check if all edges are connected if ``True``, otherwise checks
only if the last edge is connected to the first edge.
full: does a full check if all edges have a forward connection if ``True``,
otherwise checks only if the last edge is connected to the first edge.
"""
if full and not is_chain(edges, gap_tol):
return False
Expand All @@ -182,7 +178,7 @@ def length(edges: Sequence[Edge]) -> float:
def shortest_chain(chains: Iterable[Sequence[Edge]]) -> Sequence[Edge]:
"""Returns the shortest chain of connected edges.
.. Note::
.. note::
This function does not verify if the input sequences are connected edges!
Expand All @@ -207,10 +203,11 @@ def longest_chain(chains: Iterable[Sequence[Edge]]) -> Sequence[Edge]:
return tuple()


def sequential_search(edges: Sequence[Edge], gap_tol=GAP_TOL) -> Sequence[Edge]:
def find_sequential(edges: Sequence[Edge], gap_tol=GAP_TOL) -> Sequence[Edge]:
"""Returns all consecutive connected edges starting from the first edge.
The search stops at the first edge without a connection.
The search stops at the first edge without a froward connection from the previous
edge.
Args:
edges: edges to be examined
Expand All @@ -236,22 +233,22 @@ def sequential_search(edges: Sequence[Edge], gap_tol=GAP_TOL) -> Sequence[Edge]:
return chain


def sequential_search_all(
def find_all_sequential(
edges: Sequence[Edge], gap_tol=GAP_TOL
) -> Iterator[Sequence[Edge]]:
"""Yields all edge strings with consecutive connected edges starting from the first
edge. This search starts a new sequence at every edge without a connection to
the previous sequence. Each sequence has one or more edges, yields no empty sequences.
"""Yields all edge chains with consecutive connected edges starting from the first
edge. This search starts a new sequence at every edge without a forward connection
from the previous sequence. Each sequence has always one or more edges.
Args:
edges: edges to be examined
edges: sequence of edges
gap_tol: maximum vertex distance to consider two edges as connected
Raises:
TypeError: invalid data in sequence `edges`
"""
while edges:
chain = sequential_search(edges, gap_tol)
chain = find_sequential(edges, gap_tol)
edges = edges[len(chain) :]
yield chain

Expand All @@ -275,12 +272,12 @@ def find_first_loop(
) -> Sequence[Edge]:
"""Returns the first closed loop found in `edges`.
.. Note::
.. note::
Recursive backtracking algorithm with time complexity of O(n!).
Args:
edges: edges to be examined
edges: sequence of edges
gap_tol: maximum vertex distance to consider two edges as connected
timeout: timeout in seconds
Expand All @@ -297,8 +294,8 @@ def find_first_loop(
def find_first_loop_in_deposit(deposit: EdgeDeposit, timeout=TIMEOUT) -> Sequence[Edge]:
"""Returns the first closed loop found in edge `deposit`.
.. Note::
.. note::
Recursive backtracking algorithm with time complexity of O(n!).
Args:
Expand All @@ -325,12 +322,12 @@ def find_all_loops(
) -> Sequence[Sequence[Edge]]:
"""Returns all unique closed loops and doesn't include reversed solutions.
.. Note::
.. note::
Recursive backtracking algorithm with time complexity of O(n!).
Args:
edges: edges to be examined
edges: sequence of edges
gap_tol: maximum vertex distance to consider two edges as connected
timeout: timeout in seconds
Expand All @@ -350,8 +347,8 @@ def find_all_loops_in_deposit(
"""Returns all unique closed loops in found in the edge `deposit` and doesn't
include reversed solutions.
.. Note::
.. note::
Recursive backtracking algorithm with time complexity of O(n!).
Args:
Expand Down Expand Up @@ -384,7 +381,7 @@ class Loop:
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 helper class)
(internal class)
"""

def __init__(self, edges: tuple[Edge, ...]) -> None:
Expand All @@ -401,7 +398,7 @@ 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 examined
edge: edge to be tested
gap_tol: maximum vertex distance to consider two edges as connected
"""
if self.edges:
Expand Down Expand Up @@ -451,6 +448,7 @@ class EdgeVertexIndex:
The id of the vertices is indexed not the location!
(internal class)
"""

def __init__(self, edges: Sequence[Edge]) -> None:
Expand All @@ -472,7 +470,10 @@ def find_edges(self, vertices: Iterable[Vec3]) -> Sequence[Edge]:


class SpatialSearchIndex:
"""Spatial search index of all edge vertices."""
"""Spatial search index of all edge vertices.
(internal class)
"""

def __init__(self, edges: Sequence[Edge]) -> None:
vertices: list[Vec3] = []
Expand Down Expand Up @@ -630,6 +631,11 @@ def edges_linked_to(self, edge: Edge) -> Sequence[Edge]:


class LoopFinder:
"""Find closed loops in a network by a recursive backtracking algorithm.
(internal class)
"""

def __init__(
self, network: Network, discard_reverse=True, gap_tol=GAP_TOL, timeout=TIMEOUT
) -> None:
Expand Down
10 changes: 5 additions & 5 deletions tests/test_05_tools/test_546_edgeminer.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def collect_payload(edges: Sequence[em.Edge]) -> str:
return ",".join([e.payload for e in loop.ordered()])


class TestSequentialSearch:
class TestFindSequential:
# 0 1 2
# 1 +-E-+-D-+
# | |
Expand All @@ -124,15 +124,15 @@ def test_is_backwards_connected(self):
assert em.is_backwards_connected(self.A, self.B) is False
assert em.is_backwards_connected(self.D, self.F) is False

def test_sequential_forward_search(self):
def test_find_sequential(self):
edges = [self.A, self.B, self.C, self.D, self.E, self.F]
result = em.sequential_search(edges)
result = em.find_sequential(edges)
assert len(result) == 6
assert result[0] is self.A
assert result[-1] is self.F


def test_sequential_search_all():
def test_find_all_sequential():
# 0 1 2 3
# 1 +-C-+-I-+-G-+
# | | | |
Expand All @@ -152,7 +152,7 @@ def test_sequential_search_all():
J = em.Edge((1, 0), (2, 0), payload="J")

edges = [A, B, C, D, E, F, G, H, I, J]
result = list(em.sequential_search_all(edges))
result = list(em.find_all_sequential(edges))
assert len(result) == 4
assert collect_payload(result[0]) == "A,B,C,D"
assert collect_payload(result[1]) == "E,F,G,H"
Expand Down

0 comments on commit 1cfa688

Please sign in to comment.