pysimplicial 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pysimplicial/__init__.py +27 -0
- pysimplicial/_errors.py +13 -0
- pysimplicial/_solvers/__init__.py +0 -0
- pysimplicial/_solvers/cuda_restarts.py +507 -0
- pysimplicial/_solvers/exact.py +1375 -0
- pysimplicial/_solvers/set_cover.py +136 -0
- pysimplicial/category.py +290 -0
- pysimplicial/collapse.py +52 -0
- pysimplicial/complex.py +80 -0
- pysimplicial/contiguity.py +97 -0
- pysimplicial/datasets/__init__.py +19 -0
- pysimplicial/datasets/graphs.py +48 -0
- pysimplicial/datasets/random_complexes.py +29 -0
- pysimplicial/datasets/rips.py +185 -0
- pysimplicial/datasets/triangulations.json +1 -0
- pysimplicial/datasets/triangulations.py +44 -0
- pysimplicial/maps.py +81 -0
- pysimplicial/operations.py +105 -0
- pysimplicial/visualization/__init__.py +36 -0
- pysimplicial/visualization/_layout.py +824 -0
- pysimplicial/visualization/_points.py +46 -0
- pysimplicial/visualization/_surface.py +1459 -0
- pysimplicial/visualization/_two_d.py +603 -0
- pysimplicial-0.2.0.dist-info/METADATA +297 -0
- pysimplicial-0.2.0.dist-info/RECORD +27 -0
- pysimplicial-0.2.0.dist-info/WHEEL +5 -0
- pysimplicial-0.2.0.dist-info/top_level.txt +1 -0
pysimplicial/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Finite simplicial complexes, contiguity and simplicial category."""
|
|
2
|
+
|
|
3
|
+
from ._errors import CudaUnavailableError, SearchLimitExceeded
|
|
4
|
+
from .category import (
|
|
5
|
+
CategoryResult,
|
|
6
|
+
GSCatResult,
|
|
7
|
+
contiguity_distance,
|
|
8
|
+
cuda_status,
|
|
9
|
+
gscat,
|
|
10
|
+
is_categorical,
|
|
11
|
+
scat,
|
|
12
|
+
verify_gscat_cover,
|
|
13
|
+
)
|
|
14
|
+
from .collapse import core, dominators, is_strongly_collapsible, strong_core
|
|
15
|
+
from .complex import SimplicialComplex
|
|
16
|
+
from .contiguity import contiguity_chain, contiguity_class, is_contiguous, same_contiguity_class
|
|
17
|
+
from .maps import SimplicialMap
|
|
18
|
+
from .operations import categorical_product, connected_components, disjoint_union, graph_join, join, link, relabel, wedge
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"CategoryResult", "CudaUnavailableError", "GSCatResult", "SearchLimitExceeded",
|
|
22
|
+
"SimplicialComplex", "SimplicialMap", "categorical_product", "connected_components",
|
|
23
|
+
"contiguity_chain", "contiguity_class", "contiguity_distance", "core", "cuda_status",
|
|
24
|
+
"disjoint_union", "dominators", "graph_join", "gscat", "is_categorical",
|
|
25
|
+
"is_contiguous", "is_strongly_collapsible", "join", "link", "relabel",
|
|
26
|
+
"same_contiguity_class", "scat", "strong_core", "verify_gscat_cover", "wedge",
|
|
27
|
+
]
|
pysimplicial/_errors.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class SearchLimitExceeded(RuntimeError):
|
|
2
|
+
"""A search budget expired before minimality or reachability was decided."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, message, *, gpu_used=False, cover_lower_bound=None, cover_masks=()):
|
|
5
|
+
super().__init__(message)
|
|
6
|
+
self.gpu_used = gpu_used
|
|
7
|
+
self.cover_lower_bound = cover_lower_bound
|
|
8
|
+
self.cover_masks = cover_masks
|
|
9
|
+
self.cover_witnesses = ()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CudaUnavailableError(RuntimeError):
|
|
13
|
+
"""The explicitly requested CUDA backend is unavailable."""
|
|
File without changes
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""Randomized CUDA search for a small geometric cover."""
|
|
2
|
+
|
|
3
|
+
from time import monotonic
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .._errors import CudaUnavailableError
|
|
8
|
+
from ..collapse import _vertex_key
|
|
9
|
+
from ..complex import SimplicialComplex
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from numba import cuda, int16, int32, uint8, uint64
|
|
13
|
+
except Exception as error: # An optional accelerator can fail to load.
|
|
14
|
+
cuda = None
|
|
15
|
+
_NUMBA_IMPORT_ERROR = error
|
|
16
|
+
else:
|
|
17
|
+
_NUMBA_IMPORT_ERROR = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
MAX_VERTICES = 64
|
|
21
|
+
MAX_FACETS = 512
|
|
22
|
+
|
|
23
|
+
_NEIGHBOR_NONE = 0
|
|
24
|
+
_NEIGHBOR_PREFER = 1
|
|
25
|
+
_NEIGHBOR_STRICT = 2
|
|
26
|
+
_NEIGHBOR_CODES = {
|
|
27
|
+
"none": _NEIGHBOR_NONE,
|
|
28
|
+
"prefer": _NEIGHBOR_PREFER,
|
|
29
|
+
"strict": _NEIGHBOR_STRICT,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cuda_status() -> dict[str, bool | str | int]:
|
|
34
|
+
"""Return dependency/device status without launching a kernel."""
|
|
35
|
+
|
|
36
|
+
if cuda is None:
|
|
37
|
+
return {
|
|
38
|
+
"numba_imported": False,
|
|
39
|
+
"cuda_available": False,
|
|
40
|
+
"reason": repr(_NUMBA_IMPORT_ERROR),
|
|
41
|
+
"max_vertices": MAX_VERTICES,
|
|
42
|
+
"max_facets": MAX_FACETS,
|
|
43
|
+
}
|
|
44
|
+
try:
|
|
45
|
+
available = bool(cuda.is_available())
|
|
46
|
+
except Exception as error: # Driver initialization failures vary by host.
|
|
47
|
+
return {
|
|
48
|
+
"numba_imported": True,
|
|
49
|
+
"cuda_available": False,
|
|
50
|
+
"reason": repr(error),
|
|
51
|
+
"max_vertices": MAX_VERTICES,
|
|
52
|
+
"max_facets": MAX_FACETS,
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
"numba_imported": True,
|
|
56
|
+
"cuda_available": available,
|
|
57
|
+
"reason": "ok" if available else "CUDA driver/device is unavailable",
|
|
58
|
+
"max_vertices": MAX_VERTICES,
|
|
59
|
+
"max_facets": MAX_FACETS,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if cuda is not None:
|
|
64
|
+
|
|
65
|
+
@cuda.jit(device=True, inline=True)
|
|
66
|
+
def _popcount64(value):
|
|
67
|
+
count = 0
|
|
68
|
+
while value != 0:
|
|
69
|
+
value &= value - uint64(1)
|
|
70
|
+
count += 1
|
|
71
|
+
return count
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@cuda.jit(device=True, inline=True)
|
|
75
|
+
def _xorshift64(state):
|
|
76
|
+
state ^= state << 13
|
|
77
|
+
state ^= state >> 7
|
|
78
|
+
state ^= state << 17
|
|
79
|
+
return state
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@cuda.jit(device=True, inline=True)
|
|
83
|
+
def _are_neighbors(first, second):
|
|
84
|
+
first_size = _popcount64(first)
|
|
85
|
+
second_size = _popcount64(second)
|
|
86
|
+
required = first_size
|
|
87
|
+
if second_size < required:
|
|
88
|
+
required = second_size
|
|
89
|
+
required -= 1
|
|
90
|
+
if required < 1:
|
|
91
|
+
return False
|
|
92
|
+
return _popcount64(first & second) >= required
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@cuda.jit(device=True)
|
|
96
|
+
def _normalize_maximal_faces(work_faces, live, facet_count):
|
|
97
|
+
"""Mark exactly one copy of every inclusion-maximal nonempty face."""
|
|
98
|
+
|
|
99
|
+
for first in range(facet_count):
|
|
100
|
+
live[first] = uint8(0)
|
|
101
|
+
first_face = work_faces[first]
|
|
102
|
+
if first_face == 0:
|
|
103
|
+
continue
|
|
104
|
+
keep = True
|
|
105
|
+
for second in range(facet_count):
|
|
106
|
+
if first == second:
|
|
107
|
+
continue
|
|
108
|
+
second_face = work_faces[second]
|
|
109
|
+
if second_face == 0:
|
|
110
|
+
continue
|
|
111
|
+
if (first_face & second_face) == first_face:
|
|
112
|
+
if first_face != second_face or second < first:
|
|
113
|
+
keep = False
|
|
114
|
+
break
|
|
115
|
+
if keep:
|
|
116
|
+
live[first] = uint8(1)
|
|
117
|
+
# Non-maximal generators must be discarded permanently. Allowing a
|
|
118
|
+
# discarded face to reappear after a later vertex deletion would no
|
|
119
|
+
# longer represent the elementary strong collapse of the current
|
|
120
|
+
# complex.
|
|
121
|
+
for first in range(facet_count):
|
|
122
|
+
if live[first] == 0:
|
|
123
|
+
work_faces[first] = uint64(0)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@cuda.jit(device=True)
|
|
127
|
+
def _selection_is_strongly_collapsible(
|
|
128
|
+
facet_masks,
|
|
129
|
+
assignment,
|
|
130
|
+
facet_count,
|
|
131
|
+
vertex_count,
|
|
132
|
+
first_part,
|
|
133
|
+
second_part,
|
|
134
|
+
added_facet,
|
|
135
|
+
work_faces,
|
|
136
|
+
live,
|
|
137
|
+
):
|
|
138
|
+
"""Exact dominated-vertex test for one facet-generated subcomplex."""
|
|
139
|
+
|
|
140
|
+
for facet_index in range(facet_count):
|
|
141
|
+
selected = assignment[facet_index] == first_part
|
|
142
|
+
if second_part >= 0 and assignment[facet_index] == second_part:
|
|
143
|
+
selected = True
|
|
144
|
+
if facet_index == added_facet:
|
|
145
|
+
selected = True
|
|
146
|
+
if selected:
|
|
147
|
+
work_faces[facet_index] = facet_masks[facet_index]
|
|
148
|
+
else:
|
|
149
|
+
work_faces[facet_index] = uint64(0)
|
|
150
|
+
|
|
151
|
+
_normalize_maximal_faces(work_faces, live, facet_count)
|
|
152
|
+
|
|
153
|
+
# At most vertex_count-1 elementary strong collapses are possible.
|
|
154
|
+
for _ in range(vertex_count + 1):
|
|
155
|
+
active_vertices = uint64(0)
|
|
156
|
+
for facet_index in range(facet_count):
|
|
157
|
+
if live[facet_index] != 0:
|
|
158
|
+
active_vertices |= work_faces[facet_index]
|
|
159
|
+
|
|
160
|
+
if active_vertices == 0:
|
|
161
|
+
return False
|
|
162
|
+
if _popcount64(active_vertices) == 1:
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
dominated_vertex = -1
|
|
166
|
+
for vertex in range(vertex_count):
|
|
167
|
+
vertex_bit = uint64(1) << vertex
|
|
168
|
+
if (active_vertices & vertex_bit) == 0:
|
|
169
|
+
continue
|
|
170
|
+
for apex in range(vertex_count):
|
|
171
|
+
if apex == vertex:
|
|
172
|
+
continue
|
|
173
|
+
apex_bit = uint64(1) << apex
|
|
174
|
+
if (active_vertices & apex_bit) == 0:
|
|
175
|
+
continue
|
|
176
|
+
dominated = True
|
|
177
|
+
for facet_index in range(facet_count):
|
|
178
|
+
if live[facet_index] == 0:
|
|
179
|
+
continue
|
|
180
|
+
face = work_faces[facet_index]
|
|
181
|
+
if (face & vertex_bit) != 0 and (face & apex_bit) == 0:
|
|
182
|
+
dominated = False
|
|
183
|
+
break
|
|
184
|
+
if dominated:
|
|
185
|
+
dominated_vertex = vertex
|
|
186
|
+
break
|
|
187
|
+
if dominated_vertex >= 0:
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
if dominated_vertex < 0:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
deleted_bit = uint64(1) << dominated_vertex
|
|
194
|
+
for facet_index in range(facet_count):
|
|
195
|
+
if live[facet_index] != 0:
|
|
196
|
+
work_faces[facet_index] &= ~deleted_bit
|
|
197
|
+
_normalize_maximal_faces(work_faces, live, facet_count)
|
|
198
|
+
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@cuda.jit(device=True)
|
|
203
|
+
def _parts_share_a_vertex(
|
|
204
|
+
facet_masks,
|
|
205
|
+
assignment,
|
|
206
|
+
facet_count,
|
|
207
|
+
first_part,
|
|
208
|
+
second_part,
|
|
209
|
+
):
|
|
210
|
+
first_vertices = uint64(0)
|
|
211
|
+
second_vertices = uint64(0)
|
|
212
|
+
for facet_index in range(facet_count):
|
|
213
|
+
part = assignment[facet_index]
|
|
214
|
+
if part == first_part:
|
|
215
|
+
first_vertices |= facet_masks[facet_index]
|
|
216
|
+
elif part == second_part:
|
|
217
|
+
second_vertices |= facet_masks[facet_index]
|
|
218
|
+
return (first_vertices & second_vertices) != 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@cuda.jit
|
|
222
|
+
def _greedy_restarts_kernel(
|
|
223
|
+
facet_masks,
|
|
224
|
+
facet_count,
|
|
225
|
+
vertex_count,
|
|
226
|
+
restart_offset,
|
|
227
|
+
restart_count,
|
|
228
|
+
base_seed,
|
|
229
|
+
neighbor_mode,
|
|
230
|
+
do_merge,
|
|
231
|
+
output_counts,
|
|
232
|
+
output_assignment,
|
|
233
|
+
):
|
|
234
|
+
local_restart = cuda.grid(1)
|
|
235
|
+
if local_restart >= restart_count:
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
restart = restart_offset + local_restart
|
|
239
|
+
state = base_seed ^ (
|
|
240
|
+
uint64(restart + 1) * uint64(0x9E3779B97F4A7C15)
|
|
241
|
+
)
|
|
242
|
+
if state == 0:
|
|
243
|
+
state = uint64(0xD1B54A32D192ED03)
|
|
244
|
+
|
|
245
|
+
order = cuda.local.array(MAX_FACETS, dtype=int16)
|
|
246
|
+
assignment = cuda.local.array(MAX_FACETS, dtype=int16)
|
|
247
|
+
part_order = cuda.local.array(MAX_FACETS, dtype=int16)
|
|
248
|
+
part_sizes = cuda.local.array(MAX_FACETS, dtype=int16)
|
|
249
|
+
neighbor_scores = cuda.local.array(MAX_FACETS, dtype=int16)
|
|
250
|
+
used = cuda.local.array(MAX_FACETS, dtype=uint8)
|
|
251
|
+
frontier = cuda.local.array(MAX_FACETS, dtype=uint8)
|
|
252
|
+
work_faces = cuda.local.array(MAX_FACETS, dtype=uint64)
|
|
253
|
+
live = cuda.local.array(MAX_FACETS, dtype=uint8)
|
|
254
|
+
|
|
255
|
+
for index in range(facet_count):
|
|
256
|
+
order[index] = int16(index)
|
|
257
|
+
assignment[index] = int16(-1)
|
|
258
|
+
part_sizes[index] = int16(0)
|
|
259
|
+
used[index] = uint8(0)
|
|
260
|
+
frontier[index] = uint8(0)
|
|
261
|
+
|
|
262
|
+
if restart == 1:
|
|
263
|
+
for index in range(facet_count):
|
|
264
|
+
order[index] = int16(facet_count - 1 - index)
|
|
265
|
+
elif restart >= 2 and neighbor_mode != _NEIGHBOR_NONE and restart % 3 == 2:
|
|
266
|
+
# Random walk over the codimension-one facet adjacency frontier.
|
|
267
|
+
state = _xorshift64(state)
|
|
268
|
+
current = int32(state % uint64(facet_count))
|
|
269
|
+
for position in range(facet_count):
|
|
270
|
+
if position > 0:
|
|
271
|
+
candidate_count = 0
|
|
272
|
+
for candidate in range(facet_count):
|
|
273
|
+
if used[candidate] == 0 and frontier[candidate] != 0:
|
|
274
|
+
candidate_count += 1
|
|
275
|
+
if candidate_count == 0:
|
|
276
|
+
for candidate in range(facet_count):
|
|
277
|
+
if used[candidate] == 0:
|
|
278
|
+
candidate_count += 1
|
|
279
|
+
state = _xorshift64(state)
|
|
280
|
+
selected_rank = int32(state % uint64(candidate_count))
|
|
281
|
+
rank = 0
|
|
282
|
+
for candidate in range(facet_count):
|
|
283
|
+
eligible = used[candidate] == 0
|
|
284
|
+
if candidate_count < facet_count - position:
|
|
285
|
+
eligible = eligible and frontier[candidate] != 0
|
|
286
|
+
if eligible:
|
|
287
|
+
if rank == selected_rank:
|
|
288
|
+
current = candidate
|
|
289
|
+
break
|
|
290
|
+
rank += 1
|
|
291
|
+
|
|
292
|
+
order[position] = int16(current)
|
|
293
|
+
used[current] = uint8(1)
|
|
294
|
+
frontier[current] = uint8(0)
|
|
295
|
+
current_mask = facet_masks[current]
|
|
296
|
+
for candidate in range(facet_count):
|
|
297
|
+
if used[candidate] == 0 and _are_neighbors(
|
|
298
|
+
current_mask,
|
|
299
|
+
facet_masks[candidate],
|
|
300
|
+
):
|
|
301
|
+
frontier[candidate] = uint8(1)
|
|
302
|
+
elif restart >= 2:
|
|
303
|
+
# Fisher-Yates shuffle.
|
|
304
|
+
for index in range(facet_count - 1, 0, -1):
|
|
305
|
+
state = _xorshift64(state)
|
|
306
|
+
other = int32(state % uint64(index + 1))
|
|
307
|
+
temporary = order[index]
|
|
308
|
+
order[index] = order[other]
|
|
309
|
+
order[other] = temporary
|
|
310
|
+
|
|
311
|
+
part_count = 0
|
|
312
|
+
for position in range(facet_count):
|
|
313
|
+
facet_index = int32(order[position])
|
|
314
|
+
|
|
315
|
+
for part in range(part_count):
|
|
316
|
+
neighbor_scores[part] = int16(0)
|
|
317
|
+
part_order[part] = int16(part)
|
|
318
|
+
|
|
319
|
+
if neighbor_mode != _NEIGHBOR_NONE:
|
|
320
|
+
facet_mask = facet_masks[facet_index]
|
|
321
|
+
for previous_position in range(position):
|
|
322
|
+
previous_facet = int32(order[previous_position])
|
|
323
|
+
if _are_neighbors(facet_mask, facet_masks[previous_facet]):
|
|
324
|
+
previous_part = int32(assignment[previous_facet])
|
|
325
|
+
neighbor_scores[previous_part] += int16(1)
|
|
326
|
+
|
|
327
|
+
# Shuffle before a stable insertion sort to randomize equal scores.
|
|
328
|
+
for index in range(part_count - 1, 0, -1):
|
|
329
|
+
state = _xorshift64(state)
|
|
330
|
+
other = int32(state % uint64(index + 1))
|
|
331
|
+
temporary = part_order[index]
|
|
332
|
+
part_order[index] = part_order[other]
|
|
333
|
+
part_order[other] = temporary
|
|
334
|
+
|
|
335
|
+
for index in range(1, part_count):
|
|
336
|
+
key = int32(part_order[index])
|
|
337
|
+
cursor = index - 1
|
|
338
|
+
while cursor >= 0:
|
|
339
|
+
previous = int32(part_order[cursor])
|
|
340
|
+
key_score = int32(neighbor_scores[key])
|
|
341
|
+
previous_score = int32(neighbor_scores[previous])
|
|
342
|
+
key_size = int32(part_sizes[key])
|
|
343
|
+
previous_size = int32(part_sizes[previous])
|
|
344
|
+
if neighbor_mode == _NEIGHBOR_NONE:
|
|
345
|
+
key_score = 0
|
|
346
|
+
previous_score = 0
|
|
347
|
+
if key_score < previous_score:
|
|
348
|
+
break
|
|
349
|
+
if key_score == previous_score and key_size <= previous_size:
|
|
350
|
+
break
|
|
351
|
+
part_order[cursor + 1] = part_order[cursor]
|
|
352
|
+
cursor -= 1
|
|
353
|
+
part_order[cursor + 1] = int16(key)
|
|
354
|
+
|
|
355
|
+
placed = False
|
|
356
|
+
for target_position in range(part_count):
|
|
357
|
+
target_part = int32(part_order[target_position])
|
|
358
|
+
if (
|
|
359
|
+
neighbor_mode == _NEIGHBOR_STRICT
|
|
360
|
+
and neighbor_scores[target_part] == 0
|
|
361
|
+
):
|
|
362
|
+
continue
|
|
363
|
+
if _selection_is_strongly_collapsible(
|
|
364
|
+
facet_masks,
|
|
365
|
+
assignment,
|
|
366
|
+
facet_count,
|
|
367
|
+
vertex_count,
|
|
368
|
+
target_part,
|
|
369
|
+
-1,
|
|
370
|
+
facet_index,
|
|
371
|
+
work_faces,
|
|
372
|
+
live,
|
|
373
|
+
):
|
|
374
|
+
assignment[facet_index] = int16(target_part)
|
|
375
|
+
part_sizes[target_part] += int16(1)
|
|
376
|
+
placed = True
|
|
377
|
+
break
|
|
378
|
+
|
|
379
|
+
if not placed:
|
|
380
|
+
assignment[facet_index] = int16(part_count)
|
|
381
|
+
part_sizes[part_count] = int16(1)
|
|
382
|
+
part_count += 1
|
|
383
|
+
|
|
384
|
+
if do_merge != 0:
|
|
385
|
+
merged = True
|
|
386
|
+
while merged and part_count > 1:
|
|
387
|
+
merged = False
|
|
388
|
+
state = _xorshift64(state)
|
|
389
|
+
first_start = int32(state % uint64(part_count))
|
|
390
|
+
for first_offset in range(part_count):
|
|
391
|
+
first = (first_start + first_offset) % part_count
|
|
392
|
+
if merged:
|
|
393
|
+
break
|
|
394
|
+
for second_offset in range(1, part_count):
|
|
395
|
+
second = (first + second_offset) % part_count
|
|
396
|
+
if not _parts_share_a_vertex(
|
|
397
|
+
facet_masks,
|
|
398
|
+
assignment,
|
|
399
|
+
facet_count,
|
|
400
|
+
first,
|
|
401
|
+
second,
|
|
402
|
+
):
|
|
403
|
+
continue
|
|
404
|
+
if not _selection_is_strongly_collapsible(
|
|
405
|
+
facet_masks,
|
|
406
|
+
assignment,
|
|
407
|
+
facet_count,
|
|
408
|
+
vertex_count,
|
|
409
|
+
first,
|
|
410
|
+
second,
|
|
411
|
+
-1,
|
|
412
|
+
work_faces,
|
|
413
|
+
live,
|
|
414
|
+
):
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
last = part_count - 1
|
|
418
|
+
for facet_index in range(facet_count):
|
|
419
|
+
if assignment[facet_index] == second:
|
|
420
|
+
assignment[facet_index] = int16(first)
|
|
421
|
+
if second != last:
|
|
422
|
+
for facet_index in range(facet_count):
|
|
423
|
+
if assignment[facet_index] == last:
|
|
424
|
+
assignment[facet_index] = int16(second)
|
|
425
|
+
part_count -= 1
|
|
426
|
+
merged = True
|
|
427
|
+
break
|
|
428
|
+
|
|
429
|
+
output_counts[local_restart] = int16(part_count)
|
|
430
|
+
for facet_index in range(facet_count):
|
|
431
|
+
output_assignment[local_restart, facet_index] = assignment[facet_index]
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _require_cuda(device_id: int) -> None:
|
|
435
|
+
if cuda is None:
|
|
436
|
+
raise CudaUnavailableError(
|
|
437
|
+
"Numba CUDA is not installed. For CUDA 12 run: "
|
|
438
|
+
"python -m pip install numpy 'numba-cuda[cu12]'"
|
|
439
|
+
) from _NUMBA_IMPORT_ERROR
|
|
440
|
+
try:
|
|
441
|
+
if not cuda.is_available():
|
|
442
|
+
raise CudaUnavailableError(
|
|
443
|
+
"Numba cannot access an NVIDIA CUDA device. Check the NVIDIA "
|
|
444
|
+
"driver and run pysimplicial.cuda_status()."
|
|
445
|
+
)
|
|
446
|
+
cuda.select_device(device_id)
|
|
447
|
+
except CudaUnavailableError:
|
|
448
|
+
raise
|
|
449
|
+
except Exception as error:
|
|
450
|
+
raise CudaUnavailableError(
|
|
451
|
+
f"Failed to initialize CUDA device {device_id}: {error}"
|
|
452
|
+
) from error
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def randomized_cover(
|
|
457
|
+
complex_, *, restarts, seed, lower_bound, batch_size, progress,
|
|
458
|
+
neighbor_mode="prefer", merge=True,
|
|
459
|
+
):
|
|
460
|
+
"""Return the best facet partition and the number of executed restarts."""
|
|
461
|
+
labels = sorted(complex_.vertices, key=_vertex_key)
|
|
462
|
+
indices = {vertex: index for index, vertex in enumerate(labels)}
|
|
463
|
+
encoded = sorted(
|
|
464
|
+
(sum(1 << indices[v] for v in facet), facet)
|
|
465
|
+
for facet in complex_.facets
|
|
466
|
+
)
|
|
467
|
+
masks, facets = zip(*encoded)
|
|
468
|
+
facet_count = len(facets)
|
|
469
|
+
device_facets = cuda.to_device(np.asarray(masks, dtype=np.uint64))
|
|
470
|
+
neighbor_code = _NEIGHBOR_CODES[neighbor_mode]
|
|
471
|
+
threads = 32
|
|
472
|
+
best_count = facet_count
|
|
473
|
+
best_assignment = np.arange(facet_count, dtype=np.int16)
|
|
474
|
+
completed = 0
|
|
475
|
+
started = monotonic()
|
|
476
|
+
while completed < restarts and best_count - 1 > lower_bound:
|
|
477
|
+
count = min(batch_size, restarts - completed)
|
|
478
|
+
device_counts = cuda.device_array(count, dtype=np.int16)
|
|
479
|
+
device_assignment = cuda.device_array((count, facet_count), dtype=np.int16)
|
|
480
|
+
blocks = (count + threads - 1) // threads
|
|
481
|
+
_greedy_restarts_kernel[blocks, threads](
|
|
482
|
+
device_facets, facet_count, len(labels), completed, count,
|
|
483
|
+
np.uint64(seed), neighbor_code, int(merge),
|
|
484
|
+
device_counts, device_assignment,
|
|
485
|
+
)
|
|
486
|
+
counts = device_counts.copy_to_host()
|
|
487
|
+
best = int(np.argmin(counts))
|
|
488
|
+
local_count = int(counts[best])
|
|
489
|
+
if local_count < best_count:
|
|
490
|
+
best_count = local_count
|
|
491
|
+
best_assignment = device_assignment[best].copy_to_host()
|
|
492
|
+
completed += count
|
|
493
|
+
if progress:
|
|
494
|
+
progress({
|
|
495
|
+
"phase": "cuda_restarts",
|
|
496
|
+
"restarts": completed,
|
|
497
|
+
"upper_bound": best_count - 1,
|
|
498
|
+
"seconds": monotonic() - started,
|
|
499
|
+
})
|
|
500
|
+
groups = [[] for _ in range(best_count)]
|
|
501
|
+
for facet, part in zip(facets, best_assignment):
|
|
502
|
+
if not 0 <= part < best_count:
|
|
503
|
+
raise RuntimeError("CUDA returned an invalid cover assignment.")
|
|
504
|
+
groups[int(part)].append(facet)
|
|
505
|
+
if any(not group for group in groups):
|
|
506
|
+
raise RuntimeError("CUDA returned an empty cover member.")
|
|
507
|
+
return tuple(SimplicialComplex(group) for group in groups), completed
|