55
66
77class Point :
8- def __init__ (self , coordinates ) :
8+ def __init__ (self , coordinates : np . ndarray ) -> None :
99 self .coordinates = coordinates
1010
11- def __hash__ (self ):
11+ def __hash__ (self ) -> int :
1212 return hash (self .coordinates .tobytes ())
1313
14- def __eq__ (self , other ) :
14+ def __eq__ (self , other : Point ) -> bool :
1515 return np .array_equal (self .coordinates , other .coordinates )
1616
1717
1818class SubFacet :
19- def __init__ (self , coordinates ) :
19+ def __init__ (self , coordinates : np . ndarray ) -> None :
2020 self .coordinates = coordinates
2121 self .points = frozenset (Point (c ) for c in coordinates )
2222
23- def __hash__ (self ):
23+ def __hash__ (self ) -> int :
2424 return hash (self .points )
2525
26- def __eq__ (self , other ) :
26+ def __eq__ (self , other : SubFacet ) -> bool :
2727 return self .points == other .points
2828
2929
3030class Facet :
31- def __init__ (self , coordinates , normal = None , internal = None ) :
31+ def __init__ (self , coordinates : np . ndarray , internal : np . ndarray ) -> None :
3232 self .coordinates = coordinates
3333 self .center = np .mean (coordinates , axis = 0 )
3434 self .normal = self .compute_normal (internal )
@@ -37,41 +37,68 @@ def __init__(self, coordinates, normal=None, internal=None):
3737 for i in range (self .coordinates .shape [0 ])
3838 )
3939
40- def compute_normal (self , internal ) :
40+ def compute_normal (self , internal : np . ndarray ) -> np . ndarray :
4141 centered = self .coordinates - self .center
4242 _ , _ , vh = np .linalg .svd (centered )
4343 normal = vh [- 1 , :]
4444 normal /= np .linalg .norm (normal )
4545
46+ # If the normal points towards the internal point, flip it!
4647 if np .dot (normal , self .center - internal ) < 0 :
4748 normal *= - 1
4849
4950 return normal
5051
51- def __hash__ (self ):
52+ def __hash__ (self ) -> int :
5253 return hash (self .subfacets )
5354
54- def __eq__ (self , other ) :
55+ def __eq__ (self , other : Facet ) -> bool :
5556 return self .subfacets == other .subfacets
5657
5758
5859class Horizon :
59- def __init__ (self ):
60- self .facets = set ()
61- self .boundary = []
60+ def __init__ (self ) -> None :
61+ self .facets : set [ Facet ] = set ()
62+ self .boundary : list [ SubFacet ] = []
6263
6364
6465class QuickHull :
65- def __init__ (self , tolerance = 1e-5 ):
66- self .facets = []
67- self .removed = set ()
68- self .outside = {}
69- self .neighbors = {}
70- self .unclaimed = None
71- self .internal = None
66+ """
67+ QuickHull algorithm for constructing a convex hull from a set of points.
68+
69+ Parameters
70+ ----------
71+ tolerance: float, optional
72+ A tolerance threshold for determining when points lie on the convex hull (default is 1e-5).
73+
74+ Attributes
75+ ----------
76+ facets: list[Facet]
77+ List of facets considered.
78+ removed: set[Facet]
79+ Set of internal facets that have been removed from the hull during the construction process.
80+ outside: dict[Facet, tuple[np.ndarray, np.ndarray | None]]
81+ Dictionary mapping each facet to its outside points and eye point.
82+ neighbors: dict[SubFacet, set[Facet]]
83+ Mapping of subfacets to their neighboring facets. Each subfacet links precisely two neighbors.
84+ unclaimed: np.ndarray | None
85+ Points that have not yet been classified as inside or outside the current hull.
86+ internal: np.ndarray | None
87+ An internal point (i.e., the center of the initial simplex) used as a reference during hull construction.
88+ tolerance: float
89+ The tolerance used to determine if points are considered outside the current hull.
90+ """
91+
92+ def __init__ (self , tolerance : float = 1e-5 ) -> None :
93+ self .facets : list [Facet ] = []
94+ self .removed : set [Facet ] = set ()
95+ self .outside : dict [Facet , tuple [np .ndarray , np .ndarray | None ]] = {}
96+ self .neighbors : dict [SubFacet , set [Facet ]] = {}
97+ self .unclaimed : np .ndarray | None = None
98+ self .internal : np .ndarray | None = None
7299 self .tolerance = tolerance
73100
74- def initialize (self , points ) :
101+ def initialize (self , points : np . ndarray ) -> None :
75102 # Sample Points
76103 simplex = points [
77104 np .random .choice (points .shape [0 ], points .shape [1 ] + 1 , replace = False )
@@ -90,7 +117,7 @@ def initialize(self, points):
90117 for sf in f .subfacets :
91118 self .neighbors .setdefault (sf , set ()).add (f )
92119
93- def classify (self , facet ) :
120+ def classify (self , facet : Facet ) -> None :
94121 if not self .unclaimed .size :
95122 self .outside [facet ] = (None , None )
96123 return
@@ -106,14 +133,19 @@ def classify(self, facet):
106133 self .outside [facet ] = (outside , eye )
107134 self .unclaimed = self .unclaimed [~ mask ]
108135
109- def compute_horizon (self , eye , start_facet ) :
136+ def compute_horizon (self , eye : np . ndarray , start_facet : Facet ) -> Horizon :
110137 horizon = Horizon ()
111138 self ._recursive_horizon (eye , start_facet , horizon )
112139 return horizon
113140
114- def _recursive_horizon (self , eye , facet , horizon ):
141+ def _recursive_horizon (
142+ self , eye : np .ndarray , facet : Facet , horizon : Horizon
143+ ) -> int :
144+ visible = np .dot (facet .normal , eye - facet .center ) > 0
145+ if not visible :
146+ return False
115147 # If the eye is visible from the facet...
116- if np . dot ( facet . normal , eye - facet . center ) > 0 :
148+ else :
117149 # Label the facet as visible and cross each edge
118150 horizon .facets .add (facet )
119151 for subfacet in facet .subfacets :
@@ -123,11 +155,9 @@ def _recursive_horizon(self, eye, facet, horizon):
123155 eye , neighbor , horizon
124156 ):
125157 horizon .boundary .append (subfacet )
126- return 1
127- else :
128- return 0
158+ return True
129159
130- def build (self , points ) :
160+ def build (self , points : np . ndarray ) -> np . ndarray | None :
131161 num , dim = points .shape
132162 if (dim == 0 ) or (num < dim + 1 ):
133163 raise ValueError ("Not enough points supplied to build Convex Hull!" )
0 commit comments