Skip to content

Commit 80e10e1

Browse files
committed
v0.3.1: Mini update
1 parent bd257b1 commit 80e10e1

6 files changed

Lines changed: 46 additions & 32 deletions

File tree

decipher/cfg.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ sp_model:
7373
warmup_steps: 1000
7474
loader:
7575
batch_size: 256
76-
num_workers: 4
76+
num_workers: 8
7777
pin_memory: true
7878
persistent_workers: true
7979
num_neighbors: [-1]

decipher/cls.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def register_data(
5858
split_by: str = None,
5959
preprocess: bool = True,
6060
edge_index: np.ndarray = None,
61+
save_data: bool = True,
6162
) -> None:
6263
r"""
6364
Register spatial omics data
@@ -73,10 +74,11 @@ def register_data(
7374
edge_index
7475
use self-defined spatial neighbor edges (PyG format), only for advanced users
7576
"""
76-
# preprocess single cell data
77-
adata, self.batch_idx = omics_data_process(adata, split_by, preprocess)
78-
self.x = adata.X.astype(np.float32)
79-
np.save(self.work_dir / "x.npy", self.x)
77+
# process spatial omics data
78+
self.x, coords, self.batch_idx = omics_data_process(adata, split_by, preprocess)
79+
del adata
80+
if save_data:
81+
np.save(self.work_dir / "x.npy", self.x)
8082

8183
# mnn correction
8284
if self.batch_idx is not None:
@@ -87,9 +89,10 @@ def register_data(
8789

8890
# build spatial graph
8991
if edge_index is None:
90-
self.edge_index = build_graph(adata.obsm["spatial"], self.batch_idx, **CFG.graph)
92+
self.edge_index = build_graph(coords, self.batch_idx, **CFG.graph)
9193
else:
9294
logger.info("Use self-defined edge index.")
95+
assert edge_index.max() < self.x.shape[0], "Edge index out of range."
9396
self.edge_index = edge_index
9497
np.save(self.work_dir / "edge_index.npy", self.edge_index.numpy())
9598

decipher/data/process.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414

1515
def omics_data_process(
16-
adata: list[AnnData] | AnnData,
16+
adata_raw: list[AnnData] | AnnData,
1717
split_by: str = None,
1818
preprocess: bool = True,
1919
) -> tuple[AnnData, np.ndarray | None]:
@@ -34,20 +34,22 @@ def omics_data_process(
3434
Preprocessed AnnData
3535
"""
3636
batch_idx = None
37-
if isinstance(adata, AnnData):
38-
if split_by is not None and split_by in adata.obs.columns:
39-
batch_idx = LabelEncoder().fit_transform(adata.obs[split_by])
37+
if isinstance(adata_raw, AnnData):
38+
if split_by is not None and split_by in adata_raw.obs.columns:
39+
batch_idx = LabelEncoder().fit_transform(adata_raw.obs[split_by])
40+
adata = adata_raw.copy()
4041
else:
41-
adata = adata[0].concatenate(adata[1:], batch_key="batch", uns_merge="same")
42+
adata = adata_raw[0].concatenate(adata_raw[1:], batch_key="batch", uns_merge="same")
4243
batch_idx = LabelEncoder().fit_transform(adata.obs["batch"])
44+
del adata_raw
4345

4446
if batch_idx is not None and not CFG.pp.ignore_batch:
4547
logger.info(f"Detected {np.unique(batch_idx).shape[0]} batches.")
4648
adata.obs["_batch"] = batch_idx
4749

4850
if not preprocess:
4951
logger.warning("Skip preprocessing steps, only for advanced users.")
50-
return adata.copy(), batch_idx
52+
return adata.X.astype(np.float32), adata.obsm["spatial"], batch_idx
5153

5254
logger.info(f"Preprocessing {adata.n_obs} cells.")
5355
start_time = time.time()
@@ -79,4 +81,4 @@ def omics_data_process(
7981
sc.pp.log1p(adata)
8082
sc.pp.scale(adata, max_value=10)
8183
logger.success(f"Preprocessing finished in {time.time() - start_time:.2f} seconds.")
82-
return adata.copy(), batch_idx
84+
return adata.X.astype(np.float32), adata.obsm["spatial"], batch_idx

decipher/explain/gene/lr.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414

1515
def get_lr_expr(
1616
adata: sc.AnnData,
17-
lr: pd.DataFrame,
17+
lr_df: pd.DataFrame,
1818
radius: float,
1919
aggr: str = "mean",
2020
binary: bool = False,
21+
threshold: float = 0.01,
2122
) -> tuple[torch.Tensor, pd.DataFrame]:
2223
r"""
2324
Get ligand-receptor activity from spatial data.
@@ -26,23 +27,24 @@ def get_lr_expr(
2627
2728
Args:
2829
adata: AnnData object
29-
lr: Ligand receptor pairs, must contain columns `ligand.symbol` and `receptor.symbol`
30+
lr_df: Ligand receptor pairs, must contain columns `ligand.symbol` and `receptor.symbol`
3031
radius: float radius for valid ligand
3132
aggr: str aggregation method of ligand expression, default is `mean`
3233
binary: bool if convert the expression to binary, default is `False`
34+
threshold: float threshold to filter low-expressed LR pairs, default is `0.01`
3335
Returns:
3436
LR activity: Ligand-receptor pair activity
35-
lr: filtered ligand receptor pairs
37+
lr_df: filtered ligand receptor pairs
3638
Note:
3739
Not support batched data yet.
3840
"""
3941
# check
40-
assert "ligand.symbol" in lr.columns
41-
assert "receptor.symbol" in lr.columns
42+
assert "ligand.symbol" in lr_df.columns
43+
assert "receptor.symbol" in lr_df.columns
4244

4345
# filter non-LR genes (reduce memory usage)
4446
lr_genes = []
45-
for _, row in lr.iterrows():
47+
for _, row in lr_df.iterrows():
4648
ligand = row["ligand.symbol"]
4749
if "," in ligand:
4850
lr_genes += ligand.split(",")
@@ -57,20 +59,21 @@ def get_lr_expr(
5759
lr_genes = list(set(lr_genes))
5860
lr_idx = [x in lr_genes for x in adata.var.index]
5961
adata = adata[:, lr_idx]
60-
sc.pp.filter_genes(adata, min_cells=30)
62+
6163
logger.info(f"Find {adata.n_vars} LR-related genes.")
6264

6365
# merge neighbor expr
6466
edge_index = build_graph(adata.obsm["spatial"], radius=radius, mode="radius")
6567
aggr_net = SimpleConv(aggr=aggr, combine_root=None)
6668
expr = torch.from_numpy(adata.X.toarray()) if isspmatrix(adata.X) else torch.from_numpy(adata.X)
69+
# expr = expr.to(torch.float32)
6770
expr_neighbors = aggr_net(expr, edge_index)
6871

6972
# get the ligand and receptor gene index in adata.var
7073
lr_activity_list = []
7174
gene_set = set(adata.var.index)
72-
lr_filter = np.zeros(len(lr), dtype=bool)
73-
for i, lr_pair in lr.iterrows():
75+
lr_filter = np.zeros(len(lr_df), dtype=bool)
76+
for i, lr_pair in lr_df.iterrows():
7477
ligand = lr_pair["ligand.symbol"]
7578
receptor = lr_pair["receptor.symbol"]
7679
ligand = [ligand] if "," not in ligand else ligand.split(",")
@@ -82,10 +85,16 @@ def get_lr_expr(
8285
receptor_idx = [adata.var.index.get_loc(x) for x in receptor if x in gene_set]
8386
ligand_expr = expr_neighbors[:, ligand_idx].mean(dim=1) # from neighbor
8487
receptor_expr = expr[:, receptor_idx].mean(dim=1) # from cell
88+
lr_activity = ligand_expr * receptor_expr
89+
if (lr_activity > 0).float().mean().item() < threshold:
90+
continue
8591
lr_activity_list.append(ligand_expr * receptor_expr)
8692
lr_filter[i] = True
93+
if len(lr_activity_list) == 0:
94+
logger.warning(f"No ligand-receptor pairs (threshold: {threshold}).")
95+
return None, None
8796
lr_activity = torch.stack(lr_activity_list, dim=1)
8897
if binary:
8998
lr_activity = (lr_activity > 0).float()
90-
lr = lr[lr_filter]
91-
return lr_activity, lr
99+
lr_df = lr_df[lr_filter]
100+
return lr_activity, lr_df

decipher/graphic/build.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from loguru import logger
88
from torch import Tensor
99
from torch_geometric.nn import knn_graph, radius_graph
10-
from torch_geometric.utils import to_undirected
10+
from torch_geometric.utils import sort_edge_index, to_undirected
1111

1212
from ..utils import estimate_spot_distance
1313
from .knn import knn
@@ -108,13 +108,13 @@ def build_graph(
108108
)
109109
if mode == "radius":
110110
num_neighbors = edge_index[0].bincount().float()
111-
logger.info(f"Mean number of neighbors: {num_neighbors.mean().item():.2f}")
112-
logger.info(f"Max number of neighbors: {num_neighbors.max().item()}")
113-
logger.info(f"Min number of neighbors: {num_neighbors.min().item()}")
114-
logger.info(f"Median number of neighbors: {num_neighbors.median().item()}")
111+
logger.debug(f"Mean number of neighbors: {num_neighbors.mean().item():.2f}")
112+
logger.debug(f"Max number of neighbors: {num_neighbors.max().item()}")
113+
logger.debug(f"Min number of neighbors: {num_neighbors.min().item()}")
114+
logger.debug(f"Median number of neighbors: {num_neighbors.median().item()}")
115115
for percentile in [0.05, 0.25, 0.75, 0.95]:
116116
tile = num_neighbors.kthvalue(int(num_neighbors.numel() * percentile)).values.item()
117-
logger.info(f"{percentile * 100}th percentile of number of neighbors: {tile}")
117+
logger.debug(f"{percentile * 100}th percentile of number of neighbors: {tile}")
118118
return edge_index
119119

120120

@@ -135,4 +135,4 @@ def knn_to_edge_index(knn_result: np.ndarray) -> Tensor:
135135

136136
edge_index = torch.stack([src_nodes, dst_nodes], dim=0)
137137

138-
return edge_index
138+
return sort_edge_index(edge_index)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "poetry.core.masonry.api"
66
[tool.poetry]
77
name = "cell-decipher"
88
packages = [{include = "decipher"}]
9-
version = "0.3.0"
9+
version = "0.3.1"
1010
description = "DECIPHER for learning disentangled cellular embeddings in large-scale heterogeneous spatial omics data"
1111
readme = "README.md"
1212
license = "MIT"

0 commit comments

Comments
 (0)