• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

peekxc / simplextree-py / 5952964300

23 Aug 2023 02:53PM UTC coverage: 41.833% (+0.7%) from 41.129%
5952964300

push

github

peekxc
unit test changes to reflect chaneg to tuple return types. Should probably eventually move to customizing the returns globally, to swap between lists, tuples, arrays, and simplexes...

105 of 251 relevant lines covered (41.83%)

1.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.78
/simplextree/SimplexTree.py
1
from __future__ import annotations
4✔
2
from typing import *
4✔
3
from numbers import Integral
4✔
4
from numpy.typing import ArrayLike
4✔
5

6
import numpy as np 
4✔
7
from _simplextree import SimplexTree as SimplexTreeCpp
4✔
8

9
class SimplexTree(SimplexTreeCpp):
4✔
10
        """SimplexTree provides lightweight wrapper around a Simplex Tree data structure.
11

12
        This class exposes a native extension module wrapping a simplex tree implemented with modern C++.
13

14
        The Simplex Tree was originally introduced in the paper:
15
                > Boissonnat, Jean-Daniel, and Clément Maria. "The simplex tree: An efficient data structure for general simplicial complexes." Algorithmica 70.3 (2014): 406-427.
16

17
        Attributes:
18
                n_simplices (ndarray): number of simplices 
19
                dimension (int): maximal dimension of the complex
20
                id_policy (str): policy for generating new vertex ids 
21

22
        Attributes: Properties:
23
                vertices (ndarray): vertices of the complex
24
        """    
25
        def __init__(self, simplices: Iterable[Collection] = None) -> None:
4✔
26
                SimplexTreeCpp.__init__(self)
4✔
27
                if simplices is not None: 
4✔
28
                        self.insert(simplices)
4✔
29
                return None
4✔
30

31
        def insert(self, simplices: Iterable[Collection]) -> None:
4✔
32
                """
33
                Inserts simplices into the Simplex Tree. 
34

35
                By definition, inserting a simplex also inserts all of its faces. If the simplex already exists in the complex, the tree is not modified. 
36

37
                Parameters:
38
                        simplices: Iterable of simplices to insert (each of which are SimplexLike)
39

40
                ::: {.callout-note}
41
                         If the iterable is an 2-dim np.ndarray, then a p-simplex is inserted along each contiguous p+1 stride.
42
                        Otherwise, each element of the iterable to casted to a Simplex and then inserted into the tree. 
43
                :::
44
                """
45
                if isinstance(simplices, np.ndarray):
4✔
46
                        simplices = np.sort(simplices, axis=1).astype(np.uint16)
4✔
47
                        assert simplices.ndim in [1,2], "dimensions should be 1 or 2"
4✔
48
                        self._insert(simplices)
4✔
49
                elif isinstance(simplices, Iterable): 
4✔
50
                        self._insert_list(list(map(lambda x: np.asarray(x, dtype=np.uint16), simplices)))
4✔
51
                else: 
52
                        raise ValueError("Invalid type given")
×
53

54
        def remove(self, simplices: Iterable[Collection]):
4✔
55
                """Removes simplices into the Simplex Tree. 
56

57
                By definition, removing a face also removes all of its cofaces. If the simplex does not exist in the complex, the tree is not modified. 
58

59
                Parameters:
60
                        simplices: Iterable of simplices to insert (each of which are SimplexLike).                
61
                
62
                ::: {.callout-note}
63
                         If the iterable is an 2-dim np.ndarray, then a p-simplex is removed along each contiguous p+1 stride.
64
                        Otherwise, each element of the iterable to casted to a Simplex and then removed from the tree. 
65
                :::
66

67
                Examples:
68
                        st = SimplexTree([range(3)])
69
                        print(st)
70
                        st.remove([[0,1]])
71
                        print(st)
72
                """
73
                if isinstance(simplices, np.ndarray):
4✔
74
                        simplices = np.sort(simplices, axis=1).astype(np.uint16)
×
75
                        assert simplices.ndim in [1,2], "dimensions should be 1 or 2"
×
76
                        self._remove(simplices)
×
77
                elif isinstance(simplices, Iterable): 
4✔
78
                        self._remove_list(list(map(lambda x: np.asarray(x, dtype=np.uint16), simplices)))
4✔
79
                else: 
80
                        raise ValueError("Invalid type given")
×
81

82
        def find(self, simplices: Iterable[Collection]):
4✔
83
                """
84
                Finds whether simplices exist in Simplex Tree. 
85
                
86
                Parameters:
87
                        simplices: Iterable of simplices to insert (each of which are SimplexLike)
88

89
                Returns:
90
                        found (ndarray) : boolean array indicating whether each simplex was found in the complex
91

92
                ::: {.callout-note}
93
                         If the iterable is an 2-dim np.ndarray, then the p-simplex to find is given by each contiguous p+1 stride.
94
                        Otherwise, each element of the iterable to casted to a Simplex and then searched for in the tree. 
95
                :::
96
                """
97
                if isinstance(simplices, np.ndarray):
4✔
98
                        simplices = np.array(simplices, dtype=np.int16)
×
99
                        assert simplices.ndim in [1,2], "dimensions should be 1 or 2"
×
100
                        return self._find(simplices)
×
101
                elif isinstance(simplices, Iterable): 
4✔
102
                        return self._find_list([tuple(s) for s in simplices])
4✔
103
                else: 
104
                        raise ValueError("Invalid type given")
×
105

106
        def adjacent(self, simplices: Iterable[Collection]):
4✔
107
                """Checks for adjacencies between simplices."""
108
                return self._adjacent(list(map(lambda x: np.asarray(x, dtype=np.uint16), simplices)))
×
109

110
        def collapse(self, tau: Collection, sigma: Collection) -> bool:
4✔
111
                """Performs an elementary collapse on two given simplices. 
112

113
                Checks whether its possible to collapse $\\sigma$ through $\\tau$, and if so, both simplices are removed. 
114
                A simplex $\\sigma$ is said to be collapsible through one of its faces $\\tau$ if $\\sigma$ is the only coface of $\\tau$ (excluding $\\tau$ itself). 
115
                
116
                Parameters:
117
                        sigma: maximal simplex to collapse
118
                        tau: face of sigma to collapse 
119

120
                Returns:
121
                        collapsed (bool): whether the pair was collapsed
122

123
                Examples:
124

125
                        from splex import SimplexTree 
126
                        st = SimplexTree([[0,1,2]])
127
                        print(st)
128

129
                        st.collapse([1,2], [0,1,2])
130

131
                        print(st)
132
                """
133
                # assert tau in boundary(sigma), f"Simplex {tau} is not in the boundary of simplex {sigma}"
134
                success = self._collapse(tau, sigma) 
×
135
                return success 
×
136

137
        def vertex_collapse(self, u: int, v: int, w: int) -> bool:
4✔
138
                """Maps a pair of vertices into a single vertex. 
139
                
140
                Parameters:
141
                        u (int): the first vertex in the free pair.
142
                        v (int): the second vertex in the free pair. 
143
                        w (int): the target vertex to collapse to.
144
                
145
                Returns:
146
                        collapsed: whether the collapse was performed.
147
                """
148
                u,v,w = int(u), int(v), int(w)
×
149
                assert all([isinstance(e, Integral) for e in [u,v,w]]), f"Unknown vertex types given; must be integral"
×
150
                return self._vertex_collapse(u,v,w)
×
151

152
        def degree(self, vertices: Optional[ArrayLike] = None) -> Union[ArrayLike, int]:
4✔
153
                """Computes the degree of select vertices in the trie.
154

155
                Parameters:
156
                        vertices (ArrayLike): Retrieves vertex degrees
157
                                If no vertices are specified, all degrees are computed. Non-existing vertices by default have degree 0. 
158
                
159
                Returns:
160
                        degrees: degree of each vertex id given in 'vertices'.
161
                """
162
                if vertices is None: 
4✔
163
                        return self._degree_default()
4✔
164
                elif isinstance(vertices, Iterable): 
×
165
                        vertices = np.fromiter(iter(vertices), dtype=np.int16)
×
166
                        assert vertices.ndim == 1, "Invalid shape given; Must be flattened array of vertex ids"
×
167
                        return self._degree(vertices)
×
168
                else: 
169
                        raise ValueError(f"Invalid type {type(vertices)} given")
×
170

171
        # PREORDER = 0, LEVEL_ORDER = 1, FACES = 2, COFACES = 3, COFACE_ROOTS = 4, 
172
        # K_SKELETON = 5, K_SIMPLICES = 6, MAXIMAL = 7, LINK = 8
173
        def traverse(self, order: str = "preorder", f: Callable = print, sigma: Collection = [], p: int = 0) -> None:
4✔
174
                """Traverses the simplex tree in the specified order, calling `f` on each simplex encountered.
175

176
                Supported traversals include breadth-first / level order ("bfs", "levelorder"), depth-first / prefix ("dfs", "preorder").
177
                faces, cofaces, coface roots ("coface_roots"), p-skeleton, p-simplices, maximal simplices ("maximal"), and link. 
178
                
179
                Where applicable, each traversal begins its traversal `sigma`, which defaults to the empty set (root node). 
180

181
                Parameters:
182
                        order: the type of traversal of the simplex tree to execute.
183
                        f: a function to evaluate on every simplex in the traversal. Defaults to print. 
184
                        sigma: simplex to start the traversal at, where applicable. Defaults to the root node (empty set).
185
                        p: dimension of simplices to restrict to, where applicable. Defaults to 0. 
186
                """
187
                # todo: handle kwargs
188
                assert isinstance(order, str)
4✔
189
                order = order.lower() 
4✔
190
                if order in ["dfs", "preorder"]:
4✔
191
                        order = 0
4✔
192
                elif order in ["bfs", "level_order", "levelorder"]:
4✔
193
                        order = 1
4✔
194
                elif order == "faces":
4✔
195
                        order = 2
×
196
                elif order == "cofaces":
4✔
197
                        order = 3
4✔
198
                elif order == "coface_roots":
4✔
199
                        order = 4
4✔
200
                elif order == "p-skeleton":
4✔
201
                        order = 5
4✔
202
                elif order == "p-simplices":
4✔
203
                        order = 6
4✔
204
                elif order == "maximal":
4✔
205
                        order = 7
4✔
206
                elif order == "link":
×
207
                        order = 8
×
208
                else: 
209
                        raise ValueError(f"Unknown order '{order}' specified")
×
210
                self._traverse(order, lambda s: f(s), sigma, p) # order, f, init, k
4✔
211

212
        def cofaces(self, sigma: Collection = []) -> list[Collection]:
4✔
213
                """Returns the cofaces of `sigma`.
214

215
                Note, by definition, `sigma` itself is considered as a coface.
216
                
217
                Parameters:
218
                        sigma: the simplex to obtain cofaces of.
219

220
                Returns:
221
                        cofaces: the cofaces of `sigma`.
222
                """
223
                if sigma == [] or len(sigma) == 0:
4✔
224
                        return self.simplices()
×
225
                F = []
4✔
226
                self._traverse(3, lambda s: F.append(s), sigma, 0) # order, f, init, k
4✔
227
                return F
4✔
228
        
229
        def coface_roots(self, sigma: Collection = []) -> Iterable[Collection]:
4✔
230
                """Returns the roots whose subtrees span the cofaces of `sigma`.
231

232
                Note that `sigma` itself is included in the set of its cofaces. 
233

234
                Parameters:
235
                        sigma: the simplex to obtain cofaces of. Defaults to the empty set (root node).
236

237
                Returns:
238
                        coface roots: the coface roots of `sigma`
239
                """
240
                F = []
4✔
241
                self._traverse(4, lambda s: F.append(s), sigma, 0) # order, f, init, k
4✔
242
                return F
4✔
243

244
        def skeleton(self, p: int = None, sigma: Collection = []) -> Iterable[Collection]:
4✔
245
                """Returns the simplices in the p-skeleton of `sigma`.
246
                
247
                Note that, when dim(`sigma`) <= `p`, `sigma` is included in the skeleton. 
248

249
                Parameters:
250
                        p: the dimension of the skeleton.
251
                        sigma: the simplex to obtain cofaces of. Defaults to the empty set (root node).
252

253
                Returns:
254
                        list: the simplices in the p-skeleton of `sigma`.
255
                """
256
                F = []
4✔
257
                self._traverse(5, lambda s: F.append(s), sigma, p)
4✔
258
                return F 
4✔
259

260
        def simplices(self, p: int = None) -> Iterable[Collection]:
4✔
261
                """Returns the p-simplices in the complex."""
262
                F = []
4✔
263
                if p is None: 
4✔
264
                        self._traverse(1, lambda s: F.append(s), [], 0) # order, f, init, k
4✔
265
                else: 
266
                        assert isinstance(int(p), int), f"Invalid argument type '{type(p)}', must be integral"
4✔
267
                        self._traverse(6, lambda s: F.append(s), [], p) # order, f, init, k
4✔
268
                return F
4✔
269
        
270
        def faces(self, p: int = None, sigma: Collection = [], **kwargs) -> Iterable[Collection]:
4✔
271
                """Wrapper for simplices function."""
272
                return self.skeleton(p, sigma)
×
273
        
274
        def maximal(self) -> Iterable[Collection]:
4✔
275
                """Returns the maximal simplices in the complex."""
276
                F = []
4✔
277
                self._traverse(7, lambda s: F.append(s), [], 0)
4✔
278
                return F
4✔
279

280
        def link(self, sigma: Collection = []) -> Iterable[Collection]:
4✔
281
                """Returns the simplices in the link of `sigma`."""
282
                F = []
4✔
283
                self._traverse(8, lambda s: F.append(s), sigma, 0)
4✔
284
                return F
4✔
285

286
        def expand(self, k: int, f: Callable[[Collection], bool] = None) -> None:
4✔
287
                """Performs a k-expansion of the complex.
288

289
                This function is particularly useful for expanding clique complexes beyond their 1-skeleton. 
290
                
291
                Parameters:
292
                        k: maximum dimension to expand to. 
293
                        f: boolean predicate which returns whether a simplex should added to the complex (and further expanded). 
294

295
                Examples:
296
                        from simplextree import SimplexTree 
297
                        from itertools import combinations 
298
                        st = SimplexTree(combinations(range(8), 2))
299
                        print(st)
300
                        
301
                        ## Expand only triangles containing 2 as a vertex
302
                        st.expand(k=2, lambda s: 2 in s) 
303
                        print(st)
304

305
                        ## Expand all 2-cliques
306
                        st.expand(k=2)
307
                        print(st)
308
                """
309
                assert int(k) >= 0, f"Invalid expansion dimension k={k} given"
4✔
310
                if f is None:
4✔
311
                        self._expand(int(k))
4✔
312
                else:
313
                        assert isinstance(f([]), bool), "Supplied callable must be boolean-valued"
4✔
314
                        self._expand_f(int(k), f)
4✔
315

316
        def __repr__(self) -> str:
4✔
317
                if len(self.n_simplices) == 0:
4✔
318
                        return "< Empty simplex tree >"
4✔
319
                return f"Simplex Tree with {tuple(self.n_simplices)} {tuple(range(0,self.dimension+1))}-simplices"
4✔
320

321
        def __iter__(self) -> Iterator[Collection]:
4✔
322
                yield from self.simplices()
4✔
323

324
        def __contains__(self, s: Collection) -> bool:
4✔
325
                return bool(self.find([s])[0])
4✔
326

327
        def __len__(self) -> int:
4✔
328
                return int(sum(self.n_simplices))
×
329

330
        def card(self, p: int = None) -> Union[int, tuple]:
4✔
331
                """Returns the cardinality of various skeleta of the complex.
332
                
333
                Parameters:
334
                        p: dimension parameter. Defaults to None.
335
                
336
                Returns:
337
                        cardinalities: if p is an integer, the number of p-simplices in the complex. Otherwise a tuple indicating the number of simplices of all dimensions.
338
                """
339
                if p is None: 
×
340
                        return tuple(self.n_simplices)
×
341
                else: 
342
                        assert isinstance(p, int), "Invalid p"
×
343
                        return 0 if p < 0 or p >= len(self.n_simplices) else self.n_simplices[p]
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc