hypergraphz 0.1.5__py3-none-win_amd64.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.
- hypergraphz/__init__.py +28 -0
- hypergraphz/_binding.py +321 -0
- hypergraphz/_errors.py +55 -0
- hypergraphz/_graph.py +787 -0
- hypergraphz/_query.py +120 -0
- hypergraphz-0.1.5.dist-info/METADATA +163 -0
- hypergraphz-0.1.5.dist-info/RECORD +9 -0
- hypergraphz-0.1.5.dist-info/WHEEL +5 -0
- hypergraphz-0.1.5.dist-info/licenses/LICENSE +19 -0
hypergraphz/_graph.py
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from . import _binding as _b
|
|
9
|
+
from ._errors import raise_for_code
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ._query import HyperedgeQuery, VertexQuery
|
|
13
|
+
|
|
14
|
+
_lib = _b.lib
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _safe_free(ptr: ctypes.POINTER, length: int) -> None:
|
|
18
|
+
"""Free a Zig-owned array only when length > 0.
|
|
19
|
+
|
|
20
|
+
Zig's c_allocator returns undefined (not NULL) for zero-length allocations, so
|
|
21
|
+
calling free() on such a pointer crashes. Any caller must guard via this function.
|
|
22
|
+
"""
|
|
23
|
+
if length > 0:
|
|
24
|
+
_lib.hgz_free(ptr)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _read_ids(ptr: ctypes.POINTER, length: int) -> list[int]:
|
|
28
|
+
"""Copy a Zig-allocated ID array into a Python list, then free the Zig memory."""
|
|
29
|
+
if length == 0:
|
|
30
|
+
return []
|
|
31
|
+
ids = list(ptr[:length])
|
|
32
|
+
_lib.hgz_free(ptr)
|
|
33
|
+
return ids
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Hypergraph:
|
|
37
|
+
"""High-level Python wrapper around the HypergraphZ Zig library.
|
|
38
|
+
|
|
39
|
+
Lifecycle:
|
|
40
|
+
g = Hypergraph() # empty, build phase
|
|
41
|
+
g = Hypergraph.load(path) # loaded, build phase
|
|
42
|
+
g.build() # switch to query phase
|
|
43
|
+
|
|
44
|
+
After build() all query operations are available. Mutations after build()
|
|
45
|
+
maintain the reverse index incrementally.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self) -> None:
|
|
49
|
+
self._ptr = _lib.hgz_create()
|
|
50
|
+
if not self._ptr:
|
|
51
|
+
raise MemoryError("hgz_create returned null")
|
|
52
|
+
|
|
53
|
+
def __del__(self) -> None:
|
|
54
|
+
if self._ptr:
|
|
55
|
+
_lib.hgz_destroy(self._ptr)
|
|
56
|
+
self._ptr = None
|
|
57
|
+
|
|
58
|
+
# ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def build(self) -> None:
|
|
61
|
+
"""Build the reverse index (vertex → hyperedges). Required before queries."""
|
|
62
|
+
raise_for_code(_lib.hgz_build(self._ptr), _lib)
|
|
63
|
+
|
|
64
|
+
def clear(self) -> None:
|
|
65
|
+
"""Remove all vertices and hyperedges, returning to build phase."""
|
|
66
|
+
_lib.hgz_clear(self._ptr)
|
|
67
|
+
|
|
68
|
+
def save(self, path: str | Path) -> None:
|
|
69
|
+
"""Persist the graph to a binary file (.hgpz format)."""
|
|
70
|
+
raise_for_code(_lib.hgz_save(self._ptr, str(path).encode()), _lib)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def load(cls, path: str | Path) -> Hypergraph:
|
|
74
|
+
"""Load a graph from a binary file. Call build() before querying."""
|
|
75
|
+
g = object.__new__(cls)
|
|
76
|
+
g._ptr = None
|
|
77
|
+
handle = ctypes.c_void_p()
|
|
78
|
+
raise_for_code(
|
|
79
|
+
_lib.hgz_load(str(path).encode(), ctypes.byref(handle)),
|
|
80
|
+
_lib,
|
|
81
|
+
)
|
|
82
|
+
g._ptr = handle.value
|
|
83
|
+
return g
|
|
84
|
+
|
|
85
|
+
# ── Vertices ───────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
def add_vertex(self, data: dict) -> int:
|
|
88
|
+
"""Add a vertex with JSON-serialisable data. Returns the vertex ID."""
|
|
89
|
+
raw = json.dumps(data).encode()
|
|
90
|
+
out = _b._Id(0)
|
|
91
|
+
raise_for_code(_lib.hgz_add_vertex(self._ptr, raw, len(raw), ctypes.byref(out)), _lib)
|
|
92
|
+
return out.value
|
|
93
|
+
|
|
94
|
+
def delete_vertex(self, vertex_id: int) -> None:
|
|
95
|
+
raise_for_code(_lib.hgz_delete_vertex(self._ptr, vertex_id), _lib)
|
|
96
|
+
|
|
97
|
+
def update_vertex(self, vertex_id: int, data: dict) -> None:
|
|
98
|
+
raw = json.dumps(data).encode()
|
|
99
|
+
raise_for_code(
|
|
100
|
+
_lib.hgz_update_vertex(self._ptr, vertex_id, raw, len(raw)),
|
|
101
|
+
_lib,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def get_vertex(self, vertex_id: int) -> dict:
|
|
105
|
+
ptr = ctypes.c_char_p()
|
|
106
|
+
length = ctypes.c_size_t(0)
|
|
107
|
+
raise_for_code(
|
|
108
|
+
_lib.hgz_get_vertex_json(self._ptr, vertex_id, ctypes.byref(ptr), ctypes.byref(length)),
|
|
109
|
+
_lib,
|
|
110
|
+
)
|
|
111
|
+
return json.loads(ctypes.string_at(ptr, length.value))
|
|
112
|
+
|
|
113
|
+
def vertex_count(self) -> int:
|
|
114
|
+
return _lib.hgz_vertex_count(self._ptr)
|
|
115
|
+
|
|
116
|
+
def all_vertex_ids(self) -> list[int]:
|
|
117
|
+
ptr = _b._PId()
|
|
118
|
+
length = ctypes.c_size_t(0)
|
|
119
|
+
raise_for_code(
|
|
120
|
+
_lib.hgz_get_all_vertex_ids(self._ptr, ctypes.byref(ptr), ctypes.byref(length)),
|
|
121
|
+
_lib,
|
|
122
|
+
)
|
|
123
|
+
return _read_ids(ptr, length.value)
|
|
124
|
+
|
|
125
|
+
# ── Hyperedges ─────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
def add_hyperedge(self, data: dict) -> int:
|
|
128
|
+
"""Add a hyperedge with JSON-serialisable data. Returns the hyperedge ID."""
|
|
129
|
+
raw = json.dumps(data).encode()
|
|
130
|
+
out = _b._Id(0)
|
|
131
|
+
raise_for_code(_lib.hgz_add_hyperedge(self._ptr, raw, len(raw), ctypes.byref(out)), _lib)
|
|
132
|
+
return out.value
|
|
133
|
+
|
|
134
|
+
def delete_hyperedge(self, hyperedge_id: int, *, drop_vertices: bool = False) -> None:
|
|
135
|
+
raise_for_code(_lib.hgz_delete_hyperedge(self._ptr, hyperedge_id, drop_vertices), _lib)
|
|
136
|
+
|
|
137
|
+
def update_hyperedge(self, hyperedge_id: int, data: dict) -> None:
|
|
138
|
+
raw = json.dumps(data).encode()
|
|
139
|
+
raise_for_code(
|
|
140
|
+
_lib.hgz_update_hyperedge(self._ptr, hyperedge_id, raw, len(raw)),
|
|
141
|
+
_lib,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def get_hyperedge(self, hyperedge_id: int) -> dict:
|
|
145
|
+
ptr = ctypes.c_char_p()
|
|
146
|
+
length = ctypes.c_size_t(0)
|
|
147
|
+
raise_for_code(
|
|
148
|
+
_lib.hgz_get_hyperedge_json(
|
|
149
|
+
self._ptr, hyperedge_id, ctypes.byref(ptr), ctypes.byref(length)
|
|
150
|
+
),
|
|
151
|
+
_lib,
|
|
152
|
+
)
|
|
153
|
+
return json.loads(ctypes.string_at(ptr, length.value))
|
|
154
|
+
|
|
155
|
+
def hyperedge_count(self) -> int:
|
|
156
|
+
return _lib.hgz_hyperedge_count(self._ptr)
|
|
157
|
+
|
|
158
|
+
def all_hyperedge_ids(self) -> list[int]:
|
|
159
|
+
ptr = _b._PId()
|
|
160
|
+
length = ctypes.c_size_t(0)
|
|
161
|
+
raise_for_code(
|
|
162
|
+
_lib.hgz_get_all_hyperedge_ids(self._ptr, ctypes.byref(ptr), ctypes.byref(length)),
|
|
163
|
+
_lib,
|
|
164
|
+
)
|
|
165
|
+
return _read_ids(ptr, length.value)
|
|
166
|
+
|
|
167
|
+
# ── Relations ──────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
def connect(self, hyperedge_id: int, vertex_ids: list[int]) -> None:
|
|
170
|
+
"""Append vertices to a hyperedge (ordered; duplicates allowed)."""
|
|
171
|
+
arr = (_b._Id * len(vertex_ids))(*vertex_ids)
|
|
172
|
+
raise_for_code(
|
|
173
|
+
_lib.hgz_append_vertices(self._ptr, hyperedge_id, arr, len(vertex_ids)),
|
|
174
|
+
_lib,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def hyperedge_vertices(self, hyperedge_id: int) -> list[int]:
|
|
178
|
+
ptr = _b._PId()
|
|
179
|
+
length = ctypes.c_size_t(0)
|
|
180
|
+
raise_for_code(
|
|
181
|
+
_lib.hgz_get_hyperedge_vertex_ids(
|
|
182
|
+
self._ptr, hyperedge_id, ctypes.byref(ptr), ctypes.byref(length)
|
|
183
|
+
),
|
|
184
|
+
_lib,
|
|
185
|
+
)
|
|
186
|
+
return _read_ids(ptr, length.value)
|
|
187
|
+
|
|
188
|
+
def vertex_hyperedges(self, vertex_id: int) -> list[int]:
|
|
189
|
+
ptr = _b._PId()
|
|
190
|
+
length = ctypes.c_size_t(0)
|
|
191
|
+
raise_for_code(
|
|
192
|
+
_lib.hgz_get_vertex_hyperedge_ids(
|
|
193
|
+
self._ptr, vertex_id, ctypes.byref(ptr), ctypes.byref(length)
|
|
194
|
+
),
|
|
195
|
+
_lib,
|
|
196
|
+
)
|
|
197
|
+
return _read_ids(ptr, length.value)
|
|
198
|
+
|
|
199
|
+
# ── Traversal ──────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
def shortest_path(self, from_id: int, to_id: int) -> list[int] | None:
|
|
202
|
+
"""Return the shortest directed path as a vertex ID list, or None if unreachable."""
|
|
203
|
+
ptr = _b._PId()
|
|
204
|
+
length = ctypes.c_size_t(0)
|
|
205
|
+
code = _lib.hgz_find_shortest_path(
|
|
206
|
+
self._ptr, from_id, to_id, ctypes.byref(ptr), ctypes.byref(length)
|
|
207
|
+
)
|
|
208
|
+
if code == -7: # no_path
|
|
209
|
+
return None
|
|
210
|
+
raise_for_code(code, _lib)
|
|
211
|
+
return _read_ids(ptr, length.value)
|
|
212
|
+
|
|
213
|
+
def is_connected(self) -> bool:
|
|
214
|
+
result = _lib.hgz_is_connected(self._ptr)
|
|
215
|
+
if result < 0:
|
|
216
|
+
raise_for_code(result, _lib)
|
|
217
|
+
return bool(result)
|
|
218
|
+
|
|
219
|
+
# ── Algorithms ─────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
def connected_components(self) -> list[list[int]]:
|
|
222
|
+
"""Return each weakly-connected component as a list of vertex IDs."""
|
|
223
|
+
flat_ptr = _b._PId()
|
|
224
|
+
sizes_ptr = ctypes.POINTER(ctypes.c_size_t)()
|
|
225
|
+
count = ctypes.c_size_t(0)
|
|
226
|
+
raise_for_code(
|
|
227
|
+
_lib.hgz_get_connected_components(
|
|
228
|
+
self._ptr,
|
|
229
|
+
ctypes.byref(flat_ptr),
|
|
230
|
+
ctypes.byref(sizes_ptr),
|
|
231
|
+
ctypes.byref(count),
|
|
232
|
+
),
|
|
233
|
+
_lib,
|
|
234
|
+
)
|
|
235
|
+
n = count.value
|
|
236
|
+
sizes = list(sizes_ptr[:n]) if n > 0 else []
|
|
237
|
+
_safe_free(sizes_ptr, n)
|
|
238
|
+
total = sum(sizes)
|
|
239
|
+
flat = list(flat_ptr[:total]) if total > 0 else []
|
|
240
|
+
_safe_free(flat_ptr, total)
|
|
241
|
+
components, offset = [], 0
|
|
242
|
+
for size in sizes:
|
|
243
|
+
components.append(flat[offset : offset + size])
|
|
244
|
+
offset += size
|
|
245
|
+
return components
|
|
246
|
+
|
|
247
|
+
def cut_vertices(self) -> list[int]:
|
|
248
|
+
"""Return vertex IDs whose removal increases the number of connected components."""
|
|
249
|
+
ptr = _b._PId()
|
|
250
|
+
length = ctypes.c_size_t(0)
|
|
251
|
+
raise_for_code(
|
|
252
|
+
_lib.hgz_find_cut_vertices(self._ptr, ctypes.byref(ptr), ctypes.byref(length)),
|
|
253
|
+
_lib,
|
|
254
|
+
)
|
|
255
|
+
return _read_ids(ptr, length.value)
|
|
256
|
+
|
|
257
|
+
def neighborhood(self, vertex_id: int) -> list[int]:
|
|
258
|
+
"""Return IDs of vertices sharing at least one hyperedge (clique-expansion adjacency)."""
|
|
259
|
+
ptr = _b._PId()
|
|
260
|
+
length = ctypes.c_size_t(0)
|
|
261
|
+
raise_for_code(
|
|
262
|
+
_lib.hgz_get_vertex_neighborhood(
|
|
263
|
+
self._ptr, vertex_id, ctypes.byref(ptr), ctypes.byref(length)
|
|
264
|
+
),
|
|
265
|
+
_lib,
|
|
266
|
+
)
|
|
267
|
+
return _read_ids(ptr, length.value)
|
|
268
|
+
|
|
269
|
+
def topological_sort(self) -> list[int]:
|
|
270
|
+
ptr = _b._PId()
|
|
271
|
+
length = ctypes.c_size_t(0)
|
|
272
|
+
raise_for_code(
|
|
273
|
+
_lib.hgz_topological_sort(self._ptr, ctypes.byref(ptr), ctypes.byref(length)),
|
|
274
|
+
_lib,
|
|
275
|
+
)
|
|
276
|
+
return _read_ids(ptr, length.value)
|
|
277
|
+
|
|
278
|
+
# ── Degree ─────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
def vertex_indegree(self, vertex_id: int) -> int:
|
|
281
|
+
"""Number of hyperedges in which this vertex appears as a non-first member."""
|
|
282
|
+
out = ctypes.c_size_t(0)
|
|
283
|
+
raise_for_code(_lib.hgz_get_vertex_indegree(self._ptr, vertex_id, ctypes.byref(out)), _lib)
|
|
284
|
+
return out.value
|
|
285
|
+
|
|
286
|
+
def vertex_outdegree(self, vertex_id: int) -> int:
|
|
287
|
+
"""Number of hyperedges in which this vertex appears as a non-last member."""
|
|
288
|
+
out = ctypes.c_size_t(0)
|
|
289
|
+
raise_for_code(_lib.hgz_get_vertex_outdegree(self._ptr, vertex_id, ctypes.byref(out)), _lib)
|
|
290
|
+
return out.value
|
|
291
|
+
|
|
292
|
+
# ── Boolean queries ────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
def is_reachable(self, from_id: int, to_id: int) -> bool:
|
|
295
|
+
"""Return True if to_id is reachable from from_id via directed hyperedge pairs."""
|
|
296
|
+
result = _lib.hgz_is_reachable(self._ptr, from_id, to_id)
|
|
297
|
+
if result < 0:
|
|
298
|
+
raise_for_code(result, _lib)
|
|
299
|
+
return bool(result)
|
|
300
|
+
|
|
301
|
+
def has_cycle(self) -> bool:
|
|
302
|
+
"""Return True if the directed hypergraph contains a cycle."""
|
|
303
|
+
result = _lib.hgz_has_cycle(self._ptr)
|
|
304
|
+
if result < 0:
|
|
305
|
+
raise_for_code(result, _lib)
|
|
306
|
+
return bool(result)
|
|
307
|
+
|
|
308
|
+
def is_k_uniform(self, k: int) -> bool:
|
|
309
|
+
"""Return True if every hyperedge contains exactly k vertices."""
|
|
310
|
+
result = _lib.hgz_is_k_uniform(self._ptr, k)
|
|
311
|
+
if result < 0:
|
|
312
|
+
raise_for_code(result, _lib)
|
|
313
|
+
return bool(result)
|
|
314
|
+
|
|
315
|
+
# ── Extended traversal ─────────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
def bfs(self, start_id: int) -> list[int]:
|
|
318
|
+
"""Breadth-first search from start_id. Returns visited vertex IDs in BFS order."""
|
|
319
|
+
ptr = _b._PId()
|
|
320
|
+
length = ctypes.c_size_t(0)
|
|
321
|
+
raise_for_code(
|
|
322
|
+
_lib.hgz_bfs(self._ptr, start_id, ctypes.byref(ptr), ctypes.byref(length)), _lib
|
|
323
|
+
)
|
|
324
|
+
return _read_ids(ptr, length.value)
|
|
325
|
+
|
|
326
|
+
def dfs(self, start_id: int) -> list[int]:
|
|
327
|
+
"""Depth-first search from start_id. Returns visited vertex IDs in DFS order."""
|
|
328
|
+
ptr = _b._PId()
|
|
329
|
+
length = ctypes.c_size_t(0)
|
|
330
|
+
raise_for_code(
|
|
331
|
+
_lib.hgz_dfs(self._ptr, start_id, ctypes.byref(ptr), ctypes.byref(length)), _lib
|
|
332
|
+
)
|
|
333
|
+
return _read_ids(ptr, length.value)
|
|
334
|
+
|
|
335
|
+
def random_walk(self, start_id: int, steps: int) -> list[int]:
|
|
336
|
+
"""Random walk of `steps` steps. Returns vertex ID sequence of length steps+1."""
|
|
337
|
+
ptr = _b._PId()
|
|
338
|
+
length = ctypes.c_size_t(0)
|
|
339
|
+
raise_for_code(
|
|
340
|
+
_lib.hgz_random_walk(
|
|
341
|
+
self._ptr, start_id, steps, ctypes.byref(ptr), ctypes.byref(length)
|
|
342
|
+
),
|
|
343
|
+
_lib,
|
|
344
|
+
)
|
|
345
|
+
return _read_ids(ptr, length.value)
|
|
346
|
+
|
|
347
|
+
# ── Orphan queries ─────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
def orphan_vertices(self) -> list[int]:
|
|
350
|
+
"""Return vertex IDs that belong to no hyperedge."""
|
|
351
|
+
ptr = _b._PId()
|
|
352
|
+
length = ctypes.c_size_t(0)
|
|
353
|
+
raise_for_code(
|
|
354
|
+
_lib.hgz_get_orphan_vertices(self._ptr, ctypes.byref(ptr), ctypes.byref(length)), _lib
|
|
355
|
+
)
|
|
356
|
+
return _read_ids(ptr, length.value)
|
|
357
|
+
|
|
358
|
+
def orphan_hyperedges(self) -> list[int]:
|
|
359
|
+
"""Return hyperedge IDs that contain no vertices."""
|
|
360
|
+
ptr = _b._PId()
|
|
361
|
+
length = ctypes.c_size_t(0)
|
|
362
|
+
raise_for_code(
|
|
363
|
+
_lib.hgz_get_orphan_hyperedges(self._ptr, ctypes.byref(ptr), ctypes.byref(length)),
|
|
364
|
+
_lib,
|
|
365
|
+
)
|
|
366
|
+
return _read_ids(ptr, length.value)
|
|
367
|
+
|
|
368
|
+
# ── Set operations ─────────────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
def intersections(self, hyperedge_ids: list[int]) -> list[int]:
|
|
371
|
+
"""Return vertex IDs present in ALL given hyperedges (requires ≥2 IDs)."""
|
|
372
|
+
arr = (_b._Id * len(hyperedge_ids))(*hyperedge_ids)
|
|
373
|
+
ptr = _b._PId()
|
|
374
|
+
length = ctypes.c_size_t(0)
|
|
375
|
+
raise_for_code(
|
|
376
|
+
_lib.hgz_get_intersections(
|
|
377
|
+
self._ptr, arr, len(hyperedge_ids), ctypes.byref(ptr), ctypes.byref(length)
|
|
378
|
+
),
|
|
379
|
+
_lib,
|
|
380
|
+
)
|
|
381
|
+
return _read_ids(ptr, length.value)
|
|
382
|
+
|
|
383
|
+
def hyperedges_connecting(self, v1_id: int, v2_id: int) -> list[int]:
|
|
384
|
+
"""Return IDs of hyperedges that contain both v1_id and v2_id."""
|
|
385
|
+
ptr = _b._PId()
|
|
386
|
+
length = ctypes.c_size_t(0)
|
|
387
|
+
raise_for_code(
|
|
388
|
+
_lib.hgz_get_hyperedges_connecting(
|
|
389
|
+
self._ptr, v1_id, v2_id, ctypes.byref(ptr), ctypes.byref(length)
|
|
390
|
+
),
|
|
391
|
+
_lib,
|
|
392
|
+
)
|
|
393
|
+
return _read_ids(ptr, length.value)
|
|
394
|
+
|
|
395
|
+
# ── Endpoints ──────────────────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
def endpoints(
|
|
398
|
+
self,
|
|
399
|
+
) -> tuple[list[tuple[int, int]], list[tuple[int, int]]]:
|
|
400
|
+
"""Return (initial, terminal) endpoint pairs as (hyperedge_id, vertex_id) tuples."""
|
|
401
|
+
ihe_ptr = _b._PId()
|
|
402
|
+
iv_ptr = _b._PId()
|
|
403
|
+
in_ = ctypes.c_size_t(0)
|
|
404
|
+
the_ptr = _b._PId()
|
|
405
|
+
tv_ptr = _b._PId()
|
|
406
|
+
tn = ctypes.c_size_t(0)
|
|
407
|
+
raise_for_code(
|
|
408
|
+
_lib.hgz_get_endpoints(
|
|
409
|
+
self._ptr,
|
|
410
|
+
ctypes.byref(ihe_ptr),
|
|
411
|
+
ctypes.byref(iv_ptr),
|
|
412
|
+
ctypes.byref(in_),
|
|
413
|
+
ctypes.byref(the_ptr),
|
|
414
|
+
ctypes.byref(tv_ptr),
|
|
415
|
+
ctypes.byref(tn),
|
|
416
|
+
),
|
|
417
|
+
_lib,
|
|
418
|
+
)
|
|
419
|
+
ni, nt = in_.value, tn.value
|
|
420
|
+
initial = list(zip(list(ihe_ptr[:ni]), list(iv_ptr[:ni]), strict=False))
|
|
421
|
+
terminal = list(zip(list(the_ptr[:nt]), list(tv_ptr[:nt]), strict=False))
|
|
422
|
+
_safe_free(ihe_ptr, ni)
|
|
423
|
+
_safe_free(iv_ptr, ni)
|
|
424
|
+
_safe_free(the_ptr, nt)
|
|
425
|
+
_safe_free(tv_ptr, nt)
|
|
426
|
+
return initial, terminal
|
|
427
|
+
|
|
428
|
+
# ── Relation mutations ─────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
def prepend(self, hyperedge_id: int, vertex_ids: list[int]) -> None:
|
|
431
|
+
"""Prepend vertices to the front of a hyperedge's relation list."""
|
|
432
|
+
arr = (_b._Id * len(vertex_ids))(*vertex_ids)
|
|
433
|
+
raise_for_code(
|
|
434
|
+
_lib.hgz_prepend_vertices(self._ptr, hyperedge_id, arr, len(vertex_ids)), _lib
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def insert_vertex(self, hyperedge_id: int, vertex_id: int, index: int) -> None:
|
|
438
|
+
"""Insert a single vertex at the given position in a hyperedge."""
|
|
439
|
+
raise_for_code(_lib.hgz_insert_vertex(self._ptr, hyperedge_id, vertex_id, index), _lib)
|
|
440
|
+
|
|
441
|
+
def insert_many(self, hyperedge_id: int, vertex_ids: list[int], index: int) -> None:
|
|
442
|
+
"""Insert multiple vertices at the given position in a hyperedge."""
|
|
443
|
+
arr = (_b._Id * len(vertex_ids))(*vertex_ids)
|
|
444
|
+
raise_for_code(
|
|
445
|
+
_lib.hgz_insert_vertices(self._ptr, hyperedge_id, arr, len(vertex_ids), index), _lib
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
def disconnect(self, hyperedge_id: int, vertex_id: int) -> None:
|
|
449
|
+
"""Remove the first occurrence of vertex_id from hyperedge_id."""
|
|
450
|
+
raise_for_code(
|
|
451
|
+
_lib.hgz_remove_vertex_from_hyperedge(self._ptr, hyperedge_id, vertex_id), _lib
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
def disconnect_at(self, hyperedge_id: int, index: int) -> None:
|
|
455
|
+
"""Remove the vertex at the given index from hyperedge_id."""
|
|
456
|
+
raise_for_code(_lib.hgz_remove_vertex_at_index(self._ptr, hyperedge_id, index), _lib)
|
|
457
|
+
|
|
458
|
+
# ── Centrality and PageRank ────────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
def centrality(self) -> dict[int, dict[str, float]]:
|
|
461
|
+
"""Compute degree, closeness, and betweenness centrality for all vertices."""
|
|
462
|
+
ids_ptr = _b._PId()
|
|
463
|
+
deg_ptr = _b._PDouble()
|
|
464
|
+
clo_ptr = _b._PDouble()
|
|
465
|
+
bet_ptr = _b._PDouble()
|
|
466
|
+
n = ctypes.c_size_t(0)
|
|
467
|
+
raise_for_code(
|
|
468
|
+
_lib.hgz_compute_centrality(
|
|
469
|
+
self._ptr,
|
|
470
|
+
ctypes.byref(ids_ptr),
|
|
471
|
+
ctypes.byref(deg_ptr),
|
|
472
|
+
ctypes.byref(clo_ptr),
|
|
473
|
+
ctypes.byref(bet_ptr),
|
|
474
|
+
ctypes.byref(n),
|
|
475
|
+
),
|
|
476
|
+
_lib,
|
|
477
|
+
)
|
|
478
|
+
count = n.value
|
|
479
|
+
result = {
|
|
480
|
+
ids_ptr[i]: {
|
|
481
|
+
"degree": deg_ptr[i],
|
|
482
|
+
"closeness": clo_ptr[i],
|
|
483
|
+
"betweenness": bet_ptr[i],
|
|
484
|
+
}
|
|
485
|
+
for i in range(count)
|
|
486
|
+
}
|
|
487
|
+
_safe_free(ids_ptr, count)
|
|
488
|
+
_safe_free(deg_ptr, count)
|
|
489
|
+
_safe_free(clo_ptr, count)
|
|
490
|
+
_safe_free(bet_ptr, count)
|
|
491
|
+
return result
|
|
492
|
+
|
|
493
|
+
def page_rank(
|
|
494
|
+
self,
|
|
495
|
+
damping: float = 0.85,
|
|
496
|
+
max_iterations: int = 100,
|
|
497
|
+
tolerance: float = 1e-6,
|
|
498
|
+
) -> tuple[dict[int, float], int, bool]:
|
|
499
|
+
"""Compute PageRank scores. Returns (scores_by_id, iterations, converged)."""
|
|
500
|
+
ids_ptr = _b._PId()
|
|
501
|
+
scores_ptr = _b._PDouble()
|
|
502
|
+
n = ctypes.c_size_t(0)
|
|
503
|
+
iterations = ctypes.c_size_t(0)
|
|
504
|
+
converged = ctypes.c_bool(False)
|
|
505
|
+
raise_for_code(
|
|
506
|
+
_lib.hgz_compute_page_rank(
|
|
507
|
+
self._ptr,
|
|
508
|
+
damping,
|
|
509
|
+
max_iterations,
|
|
510
|
+
tolerance,
|
|
511
|
+
ctypes.byref(ids_ptr),
|
|
512
|
+
ctypes.byref(scores_ptr),
|
|
513
|
+
ctypes.byref(n),
|
|
514
|
+
ctypes.byref(iterations),
|
|
515
|
+
ctypes.byref(converged),
|
|
516
|
+
),
|
|
517
|
+
_lib,
|
|
518
|
+
)
|
|
519
|
+
count = n.value
|
|
520
|
+
scores = {ids_ptr[i]: scores_ptr[i] for i in range(count)}
|
|
521
|
+
_safe_free(ids_ptr, count)
|
|
522
|
+
_safe_free(scores_ptr, count)
|
|
523
|
+
return scores, iterations.value, converged.value
|
|
524
|
+
|
|
525
|
+
# ── Structural analysis ────────────────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
def inclusions(self) -> list[tuple[int, int]]:
|
|
528
|
+
"""Return (subset_id, superset_id) pairs where one hyperedge's vertices ⊆ another's."""
|
|
529
|
+
subsets_ptr = _b._PId()
|
|
530
|
+
supersets_ptr = _b._PId()
|
|
531
|
+
n = ctypes.c_size_t(0)
|
|
532
|
+
raise_for_code(
|
|
533
|
+
_lib.hgz_get_inclusions(
|
|
534
|
+
self._ptr,
|
|
535
|
+
ctypes.byref(subsets_ptr),
|
|
536
|
+
ctypes.byref(supersets_ptr),
|
|
537
|
+
ctypes.byref(n),
|
|
538
|
+
),
|
|
539
|
+
_lib,
|
|
540
|
+
)
|
|
541
|
+
count = n.value
|
|
542
|
+
result = list(zip(list(subsets_ptr[:count]), list(supersets_ptr[:count]), strict=False))
|
|
543
|
+
_safe_free(subsets_ptr, count)
|
|
544
|
+
_safe_free(supersets_ptr, count)
|
|
545
|
+
return result
|
|
546
|
+
|
|
547
|
+
def nestedness_profile(self) -> list[dict]:
|
|
548
|
+
"""Per-order nestedness profile: list of {size, included, total} dicts."""
|
|
549
|
+
sizes_ptr = ctypes.POINTER(ctypes.c_size_t)()
|
|
550
|
+
included_ptr = ctypes.POINTER(ctypes.c_size_t)()
|
|
551
|
+
total_ptr = ctypes.POINTER(ctypes.c_size_t)()
|
|
552
|
+
n = ctypes.c_size_t(0)
|
|
553
|
+
raise_for_code(
|
|
554
|
+
_lib.hgz_get_nestedness_profile(
|
|
555
|
+
self._ptr,
|
|
556
|
+
ctypes.byref(sizes_ptr),
|
|
557
|
+
ctypes.byref(included_ptr),
|
|
558
|
+
ctypes.byref(total_ptr),
|
|
559
|
+
ctypes.byref(n),
|
|
560
|
+
),
|
|
561
|
+
_lib,
|
|
562
|
+
)
|
|
563
|
+
count = n.value
|
|
564
|
+
result = [
|
|
565
|
+
{"size": sizes_ptr[i], "included": included_ptr[i], "total": total_ptr[i]}
|
|
566
|
+
for i in range(count)
|
|
567
|
+
]
|
|
568
|
+
_safe_free(sizes_ptr, count)
|
|
569
|
+
_safe_free(included_ptr, count)
|
|
570
|
+
_safe_free(total_ptr, count)
|
|
571
|
+
return result
|
|
572
|
+
|
|
573
|
+
def incidence_matrix(self) -> dict:
|
|
574
|
+
"""Dense incidence matrix (vertices × hyperedges).
|
|
575
|
+
|
|
576
|
+
Returns {"data": list[list[int]], "vertex_ids": list[int], "hyperedge_ids": list[int]}.
|
|
577
|
+
"""
|
|
578
|
+
data_ptr = _b._PUInt8()
|
|
579
|
+
vertex_ids_ptr = _b._PId()
|
|
580
|
+
hyperedge_ids_ptr = _b._PId()
|
|
581
|
+
rows = ctypes.c_size_t(0)
|
|
582
|
+
cols = ctypes.c_size_t(0)
|
|
583
|
+
raise_for_code(
|
|
584
|
+
_lib.hgz_incidence_matrix(
|
|
585
|
+
self._ptr,
|
|
586
|
+
ctypes.byref(data_ptr),
|
|
587
|
+
ctypes.byref(vertex_ids_ptr),
|
|
588
|
+
ctypes.byref(hyperedge_ids_ptr),
|
|
589
|
+
ctypes.byref(rows),
|
|
590
|
+
ctypes.byref(cols),
|
|
591
|
+
),
|
|
592
|
+
_lib,
|
|
593
|
+
)
|
|
594
|
+
r, c = rows.value, cols.value
|
|
595
|
+
data = [[data_ptr[i * c + j] for j in range(c)] for i in range(r)]
|
|
596
|
+
vertex_ids = list(vertex_ids_ptr[:r])
|
|
597
|
+
hyperedge_ids = list(hyperedge_ids_ptr[:c])
|
|
598
|
+
_safe_free(data_ptr, r * c)
|
|
599
|
+
_safe_free(vertex_ids_ptr, r)
|
|
600
|
+
_safe_free(hyperedge_ids_ptr, c)
|
|
601
|
+
return {"data": data, "vertex_ids": vertex_ids, "hyperedge_ids": hyperedge_ids}
|
|
602
|
+
|
|
603
|
+
def incidence_matrix_coo(self) -> dict:
|
|
604
|
+
"""Sparse incidence matrix in COO format.
|
|
605
|
+
|
|
606
|
+
Returns {"rows", "cols", "vertex_ids", "hyperedge_ids"} as lists.
|
|
607
|
+
"""
|
|
608
|
+
row_ptr = _b._PId()
|
|
609
|
+
col_ptr = _b._PId()
|
|
610
|
+
vertex_ids_ptr = _b._PId()
|
|
611
|
+
hyperedge_ids_ptr = _b._PId()
|
|
612
|
+
nnz = ctypes.c_size_t(0)
|
|
613
|
+
n_vertices = ctypes.c_size_t(0)
|
|
614
|
+
n_hyperedges = ctypes.c_size_t(0)
|
|
615
|
+
raise_for_code(
|
|
616
|
+
_lib.hgz_incidence_matrix_coo(
|
|
617
|
+
self._ptr,
|
|
618
|
+
ctypes.byref(row_ptr),
|
|
619
|
+
ctypes.byref(col_ptr),
|
|
620
|
+
ctypes.byref(vertex_ids_ptr),
|
|
621
|
+
ctypes.byref(hyperedge_ids_ptr),
|
|
622
|
+
ctypes.byref(nnz),
|
|
623
|
+
ctypes.byref(n_vertices),
|
|
624
|
+
ctypes.byref(n_hyperedges),
|
|
625
|
+
),
|
|
626
|
+
_lib,
|
|
627
|
+
)
|
|
628
|
+
nz = nnz.value
|
|
629
|
+
nv, nhe = n_vertices.value, n_hyperedges.value
|
|
630
|
+
result = {
|
|
631
|
+
"rows": list(row_ptr[:nz]),
|
|
632
|
+
"cols": list(col_ptr[:nz]),
|
|
633
|
+
"vertex_ids": list(vertex_ids_ptr[:nv]),
|
|
634
|
+
"hyperedge_ids": list(hyperedge_ids_ptr[:nhe]),
|
|
635
|
+
}
|
|
636
|
+
_safe_free(row_ptr, nz)
|
|
637
|
+
_safe_free(col_ptr, nz)
|
|
638
|
+
_safe_free(vertex_ids_ptr, nv)
|
|
639
|
+
_safe_free(hyperedge_ids_ptr, nhe)
|
|
640
|
+
return result
|
|
641
|
+
|
|
642
|
+
def laplacian(self, normalized: bool = True) -> dict:
|
|
643
|
+
"""Dense Laplacian matrix (n×n, row-major).
|
|
644
|
+
|
|
645
|
+
Returns {"data": list[list[float]], "vertex_ids": list[int]}.
|
|
646
|
+
normalized=True uses the Zhou normalized variant; False gives unnormalized.
|
|
647
|
+
"""
|
|
648
|
+
variant = ctypes.c_uint8(1 if normalized else 0)
|
|
649
|
+
data_ptr = _b._PDouble()
|
|
650
|
+
vertex_ids_ptr = _b._PId()
|
|
651
|
+
n = ctypes.c_size_t(0)
|
|
652
|
+
raise_for_code(
|
|
653
|
+
_lib.hgz_laplacian(
|
|
654
|
+
self._ptr,
|
|
655
|
+
variant,
|
|
656
|
+
ctypes.byref(data_ptr),
|
|
657
|
+
ctypes.byref(vertex_ids_ptr),
|
|
658
|
+
ctypes.byref(n),
|
|
659
|
+
),
|
|
660
|
+
_lib,
|
|
661
|
+
)
|
|
662
|
+
size = n.value
|
|
663
|
+
data = [[data_ptr[i * size + j] for j in range(size)] for i in range(size)]
|
|
664
|
+
vertex_ids = list(vertex_ids_ptr[:size])
|
|
665
|
+
_safe_free(data_ptr, size * size)
|
|
666
|
+
_safe_free(vertex_ids_ptr, size)
|
|
667
|
+
return {"data": data, "vertex_ids": vertex_ids}
|
|
668
|
+
|
|
669
|
+
def all_paths(self, from_id: int, to_id: int) -> list[list[int]]:
|
|
670
|
+
"""Return all simple directed paths from from_id to to_id."""
|
|
671
|
+
flat_ptr = _b._PId()
|
|
672
|
+
sizes_ptr = ctypes.POINTER(ctypes.c_size_t)()
|
|
673
|
+
count = ctypes.c_size_t(0)
|
|
674
|
+
raise_for_code(
|
|
675
|
+
_lib.hgz_find_all_paths(
|
|
676
|
+
self._ptr,
|
|
677
|
+
from_id,
|
|
678
|
+
to_id,
|
|
679
|
+
ctypes.byref(flat_ptr),
|
|
680
|
+
ctypes.byref(sizes_ptr),
|
|
681
|
+
ctypes.byref(count),
|
|
682
|
+
),
|
|
683
|
+
_lib,
|
|
684
|
+
)
|
|
685
|
+
n = count.value
|
|
686
|
+
sizes = list(sizes_ptr[:n]) if n > 0 else []
|
|
687
|
+
_safe_free(sizes_ptr, n)
|
|
688
|
+
total = sum(sizes)
|
|
689
|
+
flat = list(flat_ptr[:total]) if total > 0 else []
|
|
690
|
+
_safe_free(flat_ptr, total)
|
|
691
|
+
paths: list[list[int]] = []
|
|
692
|
+
offset = 0
|
|
693
|
+
for size in sizes:
|
|
694
|
+
paths.append(flat[offset : offset + size])
|
|
695
|
+
offset += size
|
|
696
|
+
return paths
|
|
697
|
+
|
|
698
|
+
# ── Sub-graph operations ───────────────────────────────────────────────────
|
|
699
|
+
|
|
700
|
+
def _wrap_handle(self, out_handle: ctypes.c_void_p) -> Hypergraph:
|
|
701
|
+
g = object.__new__(Hypergraph)
|
|
702
|
+
g._ptr = out_handle.value
|
|
703
|
+
return g
|
|
704
|
+
|
|
705
|
+
def clone(self) -> Hypergraph:
|
|
706
|
+
"""Return an independent deep copy of this graph."""
|
|
707
|
+
out = ctypes.c_void_p()
|
|
708
|
+
raise_for_code(_lib.hgz_clone(self._ptr, ctypes.byref(out)), _lib)
|
|
709
|
+
return self._wrap_handle(out)
|
|
710
|
+
|
|
711
|
+
def dual(self) -> Hypergraph:
|
|
712
|
+
"""Return the dual hypergraph (vertices ↔ hyperedges)."""
|
|
713
|
+
out = ctypes.c_void_p()
|
|
714
|
+
raise_for_code(_lib.hgz_get_dual(self._ptr, ctypes.byref(out)), _lib)
|
|
715
|
+
return self._wrap_handle(out)
|
|
716
|
+
|
|
717
|
+
def k_skeleton(self, k: int) -> Hypergraph:
|
|
718
|
+
"""Return all vertices and only hyperedges with at most k vertices."""
|
|
719
|
+
out = ctypes.c_void_p()
|
|
720
|
+
raise_for_code(_lib.hgz_get_k_skeleton(self._ptr, k, ctypes.byref(out)), _lib)
|
|
721
|
+
return self._wrap_handle(out)
|
|
722
|
+
|
|
723
|
+
def expand_to_graph(self) -> Hypergraph:
|
|
724
|
+
"""Expand to a simple directed graph via consecutive-pair edges."""
|
|
725
|
+
out = ctypes.c_void_p()
|
|
726
|
+
raise_for_code(_lib.hgz_expand_to_graph(self._ptr, ctypes.byref(out)), _lib)
|
|
727
|
+
return self._wrap_handle(out)
|
|
728
|
+
|
|
729
|
+
def expand_to_star(self) -> Hypergraph:
|
|
730
|
+
"""Expand each hyperedge to a star graph (hub + spoke edges)."""
|
|
731
|
+
out = ctypes.c_void_p()
|
|
732
|
+
raise_for_code(_lib.hgz_expand_to_star(self._ptr, ctypes.byref(out)), _lib)
|
|
733
|
+
return self._wrap_handle(out)
|
|
734
|
+
|
|
735
|
+
def line_graph(self) -> Hypergraph:
|
|
736
|
+
"""Return the line graph (hyperedges become vertices, sharing a vertex = edge)."""
|
|
737
|
+
out = ctypes.c_void_p()
|
|
738
|
+
raise_for_code(_lib.hgz_get_line_graph(self._ptr, ctypes.byref(out)), _lib)
|
|
739
|
+
return self._wrap_handle(out)
|
|
740
|
+
|
|
741
|
+
def vertex_induced_subgraph(self, vertex_ids: list[int]) -> Hypergraph:
|
|
742
|
+
"""Return the subhypergraph induced by the given vertex IDs."""
|
|
743
|
+
arr = (_b._Id * len(vertex_ids))(*vertex_ids)
|
|
744
|
+
out = ctypes.c_void_p()
|
|
745
|
+
raise_for_code(
|
|
746
|
+
_lib.hgz_get_vertex_induced_subhypergraph(
|
|
747
|
+
self._ptr, arr, len(vertex_ids), ctypes.byref(out)
|
|
748
|
+
),
|
|
749
|
+
_lib,
|
|
750
|
+
)
|
|
751
|
+
return self._wrap_handle(out)
|
|
752
|
+
|
|
753
|
+
def edge_induced_subgraph(self, hyperedge_ids: list[int]) -> Hypergraph:
|
|
754
|
+
"""Return the subhypergraph induced by the given hyperedge IDs."""
|
|
755
|
+
arr = (_b._Id * len(hyperedge_ids))(*hyperedge_ids)
|
|
756
|
+
out = ctypes.c_void_p()
|
|
757
|
+
raise_for_code(
|
|
758
|
+
_lib.hgz_get_edge_induced_subhypergraph(
|
|
759
|
+
self._ptr, arr, len(hyperedge_ids), ctypes.byref(out)
|
|
760
|
+
),
|
|
761
|
+
_lib,
|
|
762
|
+
)
|
|
763
|
+
return self._wrap_handle(out)
|
|
764
|
+
|
|
765
|
+
def core(self, s: int, t: int) -> Hypergraph:
|
|
766
|
+
"""Return the (s,t)-core: vertices with degree≥s, hyperedges with size≥t."""
|
|
767
|
+
out = ctypes.c_void_p()
|
|
768
|
+
raise_for_code(_lib.hgz_get_core(self._ptr, s, t, ctypes.byref(out)), _lib)
|
|
769
|
+
return self._wrap_handle(out)
|
|
770
|
+
|
|
771
|
+
def transitive_closure(self) -> Hypergraph:
|
|
772
|
+
"""Return the transitive closure as a hypergraph."""
|
|
773
|
+
out = ctypes.c_void_p()
|
|
774
|
+
raise_for_code(_lib.hgz_get_transitive_closure(self._ptr, ctypes.byref(out)), _lib)
|
|
775
|
+
return self._wrap_handle(out)
|
|
776
|
+
|
|
777
|
+
# ── Fluent query entry points ───────────────────────────────────────────────
|
|
778
|
+
|
|
779
|
+
def vertices(self) -> VertexQuery:
|
|
780
|
+
from ._query import VertexQuery
|
|
781
|
+
|
|
782
|
+
return VertexQuery(self)
|
|
783
|
+
|
|
784
|
+
def hyperedges(self) -> HyperedgeQuery:
|
|
785
|
+
from ._query import HyperedgeQuery
|
|
786
|
+
|
|
787
|
+
return HyperedgeQuery(self)
|