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/_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)