Skip to content

Commit c7301cb

Browse files
authored
perf: speed up igraph RNG adapter for rng Generators (#4518)
1 parent bc97bc0 commit c7301cb

3 files changed

Lines changed: 15 additions & 8 deletions

File tree

‎docs/release-notes/4518.perf.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Speed up igraph-based functions ({func}`~scanpy.tl.leiden` with `flavor="igraph"`, {func}`~scanpy.tl.draw_graph`, {func}`~scanpy.pl.paga`) when `rng` is a {class}`numpy.random.Generator` {smaller}`P. Angerer`

‎src/scanpy/_utils/random.py‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,16 @@ class _RNGIgraph:
5353

5454
def __init__(self, rng: SeedLike | RNGLike | None) -> None:
5555
self._rng = np.random.default_rng(rng)
56+
# igraph calls `getrandbits` a lot, so bind the fastest source once.
57+
# (`random_raw` is ~10× faster than `.integers(…)`)
58+
self._raw: Callable[[], int] = (
59+
self._rng.state.tomaxint
60+
if isinstance(self._rng, _LegacyRng)
61+
else self._rng.bit_generator.random_raw
62+
)
5663

5764
def getrandbits(self, k: int) -> int:
58-
if isinstance(self._rng, _LegacyRng):
59-
i = self._rng.state.tomaxint()
60-
else:
61-
lims = np.iinfo(np.uint64)
62-
i = int(self._rng.integers(0, lims.max, dtype=np.uint64, endpoint=True))
63-
return i & ((1 << k) - 1)
65+
return self._raw() & ((1 << k) - 1)
6466

6567
def randint(self, a: int, b: int) -> np.int64:
6668
"""Can’t use `endpoint` here as _LegacyRng doesn’t support it."""
@@ -75,6 +77,9 @@ def _set_igraph_rng(rng: SeedLike | RNGLike | None) -> Generator[None]:
7577
ensure_igraph()
7678
import igraph
7779

80+
if rng is None: # unseeded, so igraph’s native (and fastest) RNG is fine
81+
yield None
82+
return
7883
ig_rng = _RNGIgraph(rng)
7984
try:
8085
igraph.set_random_number_generator(ig_rng)

‎src/scanpy/tools/_leiden.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def leiden( # noqa: PLR0913
190190
if n_jobs is None:
191191
# unseeded runs needn’t be reproducible, so they can be parallel
192192
n_jobs = settings.n_jobs if rng is None else 1
193+
seeded = rng is not None
193194
rng = np.random.default_rng(rng)
194195
meta_random_state = (
195196
dict(random_state=rng.arg) if isinstance(rng, _LegacyRng) else {}
@@ -250,7 +251,7 @@ def leiden( # noqa: PLR0913
250251
else:
251252
part = _leiden_igraph(
252253
adjacency,
253-
rng=rng,
254+
rng=rng if seeded else None,
254255
directed=directed,
255256
use_weights=use_weights,
256257
resolution=resolution,
@@ -339,7 +340,7 @@ def _leiden_networkit(
339340
def _leiden_igraph(
340341
adjacency: CSBase,
341342
*,
342-
rng: np.random.Generator,
343+
rng: np.random.Generator | None,
343344
directed: bool,
344345
use_weights: bool,
345346
resolution: float | None,

0 commit comments

Comments
 (0)