Skip to content
Open
Changes from 4 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a6e1438
remove parallelize from sepal
selmanozleyen Feb 20, 2026
e011ddc
preallocated buffers
selmanozleyen Feb 20, 2026
4aad2c0
Merge branch 'main' into feat/remove-parallelize-minimal
selmanozleyen Feb 20, 2026
d9e3438
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 20, 2026
357966a
Merge branch 'main' into feat/remove-parallelize-minimal
selmanozleyen Feb 24, 2026
8c6df25
add sparse batch support
selmanozleyen Feb 24, 2026
023461d
Merge branch 'feat/remove-parallelize-minimal' of https://github.com/…
selmanozleyen Feb 24, 2026
b0d0792
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 24, 2026
b9605dd
better default
selmanozleyen Feb 24, 2026
2df55b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 24, 2026
c90f80b
Merge branch 'main' into feat/remove-parallelize-minimal
selmanozleyen Feb 26, 2026
e856524
lazy densify
selmanozleyen Feb 27, 2026
d81b098
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 27, 2026
542df63
ignore comment for fau
selmanozleyen Feb 27, 2026
2f48ba2
Merge branch 'main' into feat/remove-parallelize-minimal
selmanozleyen Mar 1, 2026
e7e5579
update
selmanozleyen Mar 1, 2026
f24c1ae
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 1, 2026
2d8a143
update docstrings
selmanozleyen Mar 2, 2026
b283de7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 2, 2026
46558fd
init
selmanozleyen Mar 2, 2026
b64ebc9
rng can't be none inside test
selmanozleyen Mar 2, 2026
deb31f3
Merge branch 'feat/ligrec-rng-threading' into feat/remove-parallelize…
selmanozleyen Mar 2, 2026
5bc7caa
Merge branch 'main' into feat/remove-parallelize-minimal
selmanozleyen Mar 8, 2026
ecf7147
Merge branch 'main' into feat/remove-parallelize-minimal
selmanozleyen Mar 11, 2026
1c34afa
checkout main
selmanozleyen Mar 11, 2026
9ee9462
remove refrence file
selmanozleyen Mar 11, 2026
b7ca301
add progress bar option and deprecate warning for backend
selmanozleyen Mar 11, 2026
9c1d97a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 11, 2026
c384f9b
add deprecated params import
selmanozleyen Mar 11, 2026
3adf5ce
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 11, 2026
103e0b5
revert log message
selmanozleyen Mar 11, 2026
10a6913
Merge branch 'main' into feat/remove-parallelize-minimal
selmanozleyen Mar 11, 2026
65160ad
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 11, 2026
417d3b7
Merge branch 'main' into feat/remove-parallelize-minimal
selmanozleyen Mar 12, 2026
7f8bbac
Merge branch 'main' into feat/remove-parallelize-minimal
selmanozleyen Mar 16, 2026
94e9804
update conf
selmanozleyen Mar 16, 2026
f4ffe34
now use threadpool util
selmanozleyen Mar 16, 2026
6749135
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 16, 2026
a7a5d95
Merge branch 'main' into feat/remove-parallelize-minimal
selmanozleyen Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 61 additions & 64 deletions src/squidpy/gr/_sepal.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
from __future__ import annotations

from collections.abc import Callable, Sequence
from collections.abc import Sequence
from typing import Literal

import numpy as np
import pandas as pd
from anndata import AnnData
from numba import njit
from numba import get_num_threads, get_thread_id, njit, prange
from scanpy import logging as logg
from scipy.sparse import csr_matrix, isspmatrix_csr, spmatrix
from scipy.sparse import csr_matrix, issparse, isspmatrix_csr, spmatrix
from sklearn.metrics import pairwise_distances
from spatialdata import SpatialData

from squidpy._constants._pkg_constants import Key
from squidpy._docs import d, inject_docs
from squidpy._utils import NDArrayA, Signal, SigQueue, _get_n_cores, parallelize
from squidpy._utils import NDArrayA
from squidpy.gr._utils import (
_assert_connectivity_key,
_assert_non_empty_sequence,
Expand Down Expand Up @@ -108,8 +108,6 @@ def sepal(
genes = genes[adata.var["highly_variable"].values]
genes = _assert_non_empty_sequence(genes, name="genes")

n_jobs = _get_n_cores(n_jobs)

g = adata.obsp[connectivity_key]
if not isspmatrix_csr(g):
g = csr_matrix(g)
Expand All @@ -124,27 +122,13 @@ def sepal(

# get counts
vals, genes = _extract_expression(adata, genes=genes, use_raw=use_raw, layer=layer)
start = logg.info(f"Calculating sepal score for `{len(genes)}` genes using `{n_jobs}` core(s)")

score = parallelize(
_score_helper,
collection=np.arange(len(genes)).tolist(),
extractor=np.hstack,
use_ixs=False,
n_jobs=n_jobs,
backend=backend,
show_progress_bar=show_progress_bar,
)(
vals=vals,
max_neighs=max_neighs,
n_iter=n_iter,
sat=sat,
sat_idx=sat_idx,
unsat=unsat,
unsat_idx=unsat_idx,
dt=dt,
thresh=thresh,
)
start = logg.info(f"Calculating sepal score for `{len(genes)}` genes")

vals_dense = vals.toarray() if issparse(vals) else np.asarray(vals)
vals_dense = np.ascontiguousarray(vals_dense, dtype=np.float64)

use_hex = max_neighs == 6
score = _diffusion_batch(vals_dense, use_hex, n_iter, sat, sat_idx, unsat, unsat_idx, dt, thresh)

key_added = "sepal_score"
sepal_score = pd.DataFrame(score, index=genes, columns=[key_added])
Expand All @@ -160,69 +144,82 @@ def sepal(
_save_data(adata, attr="uns", key=key_added, data=sepal_score, time=start)


def _score_helper(
ixs: Sequence[int],
vals: spmatrix | NDArrayA,
max_neighs: int,
@njit(parallel=True)
def _diffusion_batch(
vals_dense: NDArrayA,
use_hex: bool,
n_iter: int,
sat: NDArrayA,
sat_idx: NDArrayA,
unsat: NDArrayA,
unsat_idx: NDArrayA,
dt: float,
thresh: float,
queue: SigQueue | None = None,
) -> NDArrayA:
if max_neighs == 4:
fun = _laplacian_rect
elif max_neighs == 6:
fun = _laplacian_hex
else:
raise NotImplementedError(f"Laplacian for `{max_neighs}` neighbors is not yet implemented.")

score = []
for i in ixs:
if isinstance(vals, spmatrix):
conc = vals[:, i].toarray().flatten() # Safe to call toarray()
else:
conc = vals[:, i].copy() # vals is assumed to be a NumPy array here

time_iter = _diffusion(conc, fun, n_iter, sat, sat_idx, unsat, unsat_idx, dt=dt, thresh=thresh)
score.append(dt * time_iter)

if queue is not None:
queue.put(Signal.UPDATE)

if queue is not None:
queue.put(Signal.FINISH)

return np.array(score)
n_genes = vals_dense.shape[1]
n_cells = vals_dense.shape[0]
sat_shape = sat.shape[0]
n_threads = get_num_threads()

# Pre-allocate per-thread workspace to avoid allocator contention
Copy link
Copy Markdown
Member

@flying-sheep flying-sheep Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there isn’t a way to tell numba to do the pre-allocation instead of doing it manually.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also wondering the same but I couldn't find a way to do it without calling get_num_threads()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Intron7 do you know anything about this?

conc_buf = np.empty((n_threads, n_cells))
entropy_buf = np.empty((n_threads, n_iter))
nhood_buf = np.empty((n_threads, sat_shape))
dcdt_buf = np.empty((n_threads, n_cells))

scores = np.empty(n_genes)
for i in prange(n_genes):
tid = get_thread_id()
conc = conc_buf[tid]
conc[:] = vals_dense[:, i]
time_iter = _diffusion(
conc,
use_hex,
n_iter,
sat,
sat_idx,
unsat,
unsat_idx,
dt,
thresh,
entropy_buf[tid],
nhood_buf[tid],
dcdt_buf[tid],
)
scores[i] = dt * time_iter
return scores


@njit(fastmath=True)
def _diffusion(
conc: NDArrayA,
laplacian: Callable[[NDArrayA, NDArrayA], float],
use_hex: bool,
n_iter: int,
sat: NDArrayA,
sat_idx: NDArrayA,
unsat: NDArrayA,
unsat_idx: NDArrayA,
dt: float = 0.001,
thresh: float = 1e-8,
dt: float,
thresh: float,
entropy_arr: NDArrayA,
nhood: NDArrayA,
dcdt: NDArrayA,
) -> float:
"""Simulate diffusion process on a regular graph."""
sat_shape, conc_shape = sat.shape[0], conc.shape[0]
entropy_arr = np.zeros(n_iter)
sat_shape = sat.shape[0]
entropy_arr[:] = 0.0
nhood[:] = 0.0
prev_ent = 1.0
nhood = np.zeros(sat_shape)

for i in range(n_iter):
for j in range(sat_shape):
nhood[j] = np.sum(conc[sat_idx[j]])
d2 = laplacian(conc[sat], nhood)
if use_hex:
d2 = _laplacian_hex(conc[sat], nhood)
else:
d2 = _laplacian_rect(conc[sat], nhood)

dcdt = np.zeros(conc_shape)
dcdt[:] = 0.0
dcdt[sat] = d2
conc[sat] += dcdt[sat] * dt
conc[unsat] += dcdt[unsat_idx] * dt
Expand Down