scCS-py 0.3.2__tar.gz
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.
- sccs_py-0.3.2/PKG-INFO +31 -0
- sccs_py-0.3.2/README.md +36 -0
- sccs_py-0.3.2/pyproject.toml +36 -0
- sccs_py-0.3.2/scCS/__init__.py +154 -0
- sccs_py-0.3.2/scCS/bifurcation.py +226 -0
- sccs_py-0.3.2/scCS/drivers.py +237 -0
- sccs_py-0.3.2/scCS/embedding.py +621 -0
- sccs_py-0.3.2/scCS/enrichment.py +289 -0
- sccs_py-0.3.2/scCS/plot.py +1153 -0
- sccs_py-0.3.2/scCS/scores.py +752 -0
- sccs_py-0.3.2/scCS/trajectory.py +761 -0
- sccs_py-0.3.2/scCS_py.egg-info/PKG-INFO +31 -0
- sccs_py-0.3.2/scCS_py.egg-info/SOURCES.txt +16 -0
- sccs_py-0.3.2/scCS_py.egg-info/dependency_links.txt +1 -0
- sccs_py-0.3.2/scCS_py.egg-info/requires.txt +30 -0
- sccs_py-0.3.2/scCS_py.egg-info/top_level.txt +1 -0
- sccs_py-0.3.2/setup.cfg +4 -0
- sccs_py-0.3.2/tests/test_scores.py +1230 -0
sccs_py-0.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scCS-py
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Single-cell Commitment Scores with radial star embedding
|
|
5
|
+
Author: Emil Kriukov
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: numpy>=1.24
|
|
8
|
+
Requires-Dist: pandas>=1.5
|
|
9
|
+
Requires-Dist: scipy>=1.10
|
|
10
|
+
Requires-Dist: anndata>=0.9
|
|
11
|
+
Requires-Dist: scanpy>=1.9
|
|
12
|
+
Requires-Dist: matplotlib>=3.7
|
|
13
|
+
Requires-Dist: seaborn>=0.12
|
|
14
|
+
Requires-Dist: scikit-learn>=1.2
|
|
15
|
+
Provides-Extra: velocity
|
|
16
|
+
Requires-Dist: scvelo>=0.2.5; extra == "velocity"
|
|
17
|
+
Provides-Extra: enrichment
|
|
18
|
+
Requires-Dist: gseapy>=1.0; extra == "enrichment"
|
|
19
|
+
Provides-Extra: all
|
|
20
|
+
Requires-Dist: scvelo>=0.2.5; extra == "all"
|
|
21
|
+
Requires-Dist: gseapy>=1.0; extra == "all"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
25
|
+
Provides-Extra: docs
|
|
26
|
+
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
27
|
+
Requires-Dist: sphinx-autoapi>=3.0; extra == "docs"
|
|
28
|
+
Requires-Dist: furo; extra == "docs"
|
|
29
|
+
Requires-Dist: numpydoc; extra == "docs"
|
|
30
|
+
Requires-Dist: nbsphinx; extra == "docs"
|
|
31
|
+
Requires-Dist: ipykernel; extra == "docs"
|
sccs_py-0.3.2/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# scCS — Single-Cell Commitment Scores
|
|
2
|
+
|
|
3
|
+
[](https://sccs-py.readthedocs.io/en/latest/)
|
|
4
|
+
|
|
5
|
+
**scCS** computes RNA velocity-based commitment scores for single-cell data,
|
|
6
|
+
generalizing the 2-state (homeostatic/activated) framework from
|
|
7
|
+
Kriukov et al. (2025) to arbitrary k-furcations.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
pip install git+https://github.com/mcrewcow/scCS.git
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
import scCS
|
|
16
|
+
import scanpy as sc
|
|
17
|
+
|
|
18
|
+
adata = sc.read_h5ad("your_data.h5ad")
|
|
19
|
+
scorer = scCS.CommitmentScorer(adata, cluster_key="cell_type")
|
|
20
|
+
result = scorer.score(compute_cell_level=True)
|
|
21
|
+
scorer.plot_star(result, color_by="fate")
|
|
22
|
+
scorer.plot_commitment_bar(result)
|
|
23
|
+
|
|
24
|
+
## Key features
|
|
25
|
+
|
|
26
|
+
- k-furcation support — works for any number of fate branches
|
|
27
|
+
- Star embedding — radial layout with one arm per fate
|
|
28
|
+
- unCS / nCS — unnormalized and cell-count-corrected commitment scores
|
|
29
|
+
- Expression trends — gene expression vs commitment axis
|
|
30
|
+
- Color map support — preserves your original scanpy cluster colors
|
|
31
|
+
|
|
32
|
+
## Citation
|
|
33
|
+
|
|
34
|
+
Kriukov et al. (2025) Single-cell transcriptome of myeloid cells in response
|
|
35
|
+
to transplantation of human retinal neurons reveals reversibility of microglial
|
|
36
|
+
activation. DOI: 10.XXXX
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scCS-py"
|
|
7
|
+
version = "0.3.2"
|
|
8
|
+
description = "Single-cell Commitment Scores with radial star embedding"
|
|
9
|
+
authors = [{ name = "Emil Kriukov" }]
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"numpy>=1.24",
|
|
13
|
+
"pandas>=1.5",
|
|
14
|
+
"scipy>=1.10",
|
|
15
|
+
"anndata>=0.9",
|
|
16
|
+
"scanpy>=1.9",
|
|
17
|
+
"matplotlib>=3.7",
|
|
18
|
+
"seaborn>=0.12",
|
|
19
|
+
"scikit-learn>=1.2",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
velocity = ["scvelo>=0.2.5"]
|
|
24
|
+
enrichment = ["gseapy>=1.0"]
|
|
25
|
+
all = ["scvelo>=0.2.5", "gseapy>=1.0"]
|
|
26
|
+
dev = ["pytest>=7.0", "pytest-cov"]
|
|
27
|
+
docs = [
|
|
28
|
+
"sphinx>=7.0",
|
|
29
|
+
"sphinx-autoapi>=3.0",
|
|
30
|
+
"furo",
|
|
31
|
+
"numpydoc",
|
|
32
|
+
"nbsphinx",
|
|
33
|
+
"ipykernel",
|
|
34
|
+
]
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
scCS — Single-cell Commitment Scores with radial star embedding.
|
|
3
|
+
|
|
4
|
+
Generalizes the 2-state commitment score framework from:
|
|
5
|
+
|
|
6
|
+
Kriukov et al. (2025) "Single-cell transcriptome of myeloid cells in
|
|
7
|
+
response to transplantation of human retinal neurons reveals reversibility
|
|
8
|
+
of microglial activation"
|
|
9
|
+
|
|
10
|
+
to any number of cell fates (k-furcations), with:
|
|
11
|
+
- User-supplied bifurcation cluster (e.g., leiden cluster '17')
|
|
12
|
+
- Radial star embedding: progenitor at origin, each fate on its own arm
|
|
13
|
+
- Cells ordered along arms by differentiation metric (pseudotime,
|
|
14
|
+
CytoTRACE2, pathway score, or any custom per-cell score)
|
|
15
|
+
- Population-level scores: unCS, nCS, commitment vector, entropy
|
|
16
|
+
- Per-cell fate affinity scores
|
|
17
|
+
|
|
18
|
+
Quick start
|
|
19
|
+
-----------
|
|
20
|
+
>>> import scCS
|
|
21
|
+
>>> scorer = scCS.CommitmentScorer(
|
|
22
|
+
... adata,
|
|
23
|
+
... bifurcation_cluster='17', # leiden cluster at the bifurcation
|
|
24
|
+
... terminal_cell_types=['FateA', 'FateB', 'FateC'],
|
|
25
|
+
... cluster_key='leiden',
|
|
26
|
+
... )
|
|
27
|
+
>>> scorer.build_embedding(differentiation_metric='pseudotime')
|
|
28
|
+
>>> scorer.fit()
|
|
29
|
+
>>> result = scorer.score()
|
|
30
|
+
>>> print(result.summary())
|
|
31
|
+
>>> scorer.plot_star(result)
|
|
32
|
+
|
|
33
|
+
For k=2 (reproducing manuscript):
|
|
34
|
+
>>> scorer = scCS.CommitmentScorer(
|
|
35
|
+
... adata,
|
|
36
|
+
... bifurcation_cluster='17',
|
|
37
|
+
... terminal_cell_types=['homeostatic', 'activated'],
|
|
38
|
+
... cluster_key='leiden',
|
|
39
|
+
... )
|
|
40
|
+
>>> scorer.build_embedding(differentiation_metric='pseudotime')
|
|
41
|
+
>>> scorer.fit()
|
|
42
|
+
>>> result = scorer.score()
|
|
43
|
+
>>> # result.pairwise_nCS[0, 1] should be ~8.066 (manuscript value)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
__version__ = "0.3.2"
|
|
47
|
+
__author__ = "Emil Kriukov"
|
|
48
|
+
|
|
49
|
+
# Main API
|
|
50
|
+
from .trajectory import CommitmentScorer
|
|
51
|
+
|
|
52
|
+
# Fate map
|
|
53
|
+
from .bifurcation import FateMap, build_fate_map
|
|
54
|
+
|
|
55
|
+
# Embedding
|
|
56
|
+
from .embedding import (
|
|
57
|
+
build_star_embedding,
|
|
58
|
+
project_velocity_star,
|
|
59
|
+
run_velocity_pipeline,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Core math (for advanced users)
|
|
63
|
+
from .scores import (
|
|
64
|
+
CommitmentScoreResult,
|
|
65
|
+
compute_magnitudes,
|
|
66
|
+
compute_angles,
|
|
67
|
+
bin_angles,
|
|
68
|
+
equal_sectors,
|
|
69
|
+
centroid_sectors,
|
|
70
|
+
compute_sector_magnitudes,
|
|
71
|
+
compute_unCS,
|
|
72
|
+
compute_nCS,
|
|
73
|
+
compute_commitment_vector,
|
|
74
|
+
# Entropy
|
|
75
|
+
compute_population_entropy, # aggregate velocity-mass entropy
|
|
76
|
+
compute_mean_cell_entropy, # mean per-cell k-way entropy
|
|
77
|
+
compute_per_fate_cell_entropy, # per-fate binary cell entropy, shape (k,)
|
|
78
|
+
compute_nn_cell_entropy, # NN-smoothed per-cell entropy, shape (n_cells,)
|
|
79
|
+
compute_commitment_entropy, # backward-compat alias for compute_population_entropy
|
|
80
|
+
compute_pairwise_cs_matrix,
|
|
81
|
+
compute_cell_scores,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Driver genes
|
|
85
|
+
from .drivers import (
|
|
86
|
+
get_velocity_drivers,
|
|
87
|
+
get_deg_drivers,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Pathway enrichment
|
|
91
|
+
from .enrichment import (
|
|
92
|
+
run_enrichment_per_fate,
|
|
93
|
+
export_enrichment_tables,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Plotting
|
|
97
|
+
from .plot import (
|
|
98
|
+
plot_star_embedding,
|
|
99
|
+
plot_star_panels,
|
|
100
|
+
plot_rose,
|
|
101
|
+
plot_pairwise_cs,
|
|
102
|
+
plot_commitment_bar,
|
|
103
|
+
plot_commitment_heatmap,
|
|
104
|
+
plot_subset_comparison,
|
|
105
|
+
plot_expression_trends,
|
|
106
|
+
plot_nn_entropy_elbow,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
__all__ = [
|
|
110
|
+
# Main class
|
|
111
|
+
"CommitmentScorer",
|
|
112
|
+
# Fate map
|
|
113
|
+
"FateMap",
|
|
114
|
+
"build_fate_map",
|
|
115
|
+
# Embedding
|
|
116
|
+
"build_star_embedding",
|
|
117
|
+
"project_velocity_star",
|
|
118
|
+
"run_velocity_pipeline",
|
|
119
|
+
# Results
|
|
120
|
+
"CommitmentScoreResult",
|
|
121
|
+
# Core math
|
|
122
|
+
"compute_magnitudes",
|
|
123
|
+
"compute_angles",
|
|
124
|
+
"bin_angles",
|
|
125
|
+
"equal_sectors",
|
|
126
|
+
"centroid_sectors",
|
|
127
|
+
"compute_sector_magnitudes",
|
|
128
|
+
"compute_unCS",
|
|
129
|
+
"compute_nCS",
|
|
130
|
+
"compute_commitment_vector",
|
|
131
|
+
"compute_population_entropy",
|
|
132
|
+
"compute_mean_cell_entropy",
|
|
133
|
+
"compute_per_fate_cell_entropy",
|
|
134
|
+
"compute_nn_cell_entropy",
|
|
135
|
+
"compute_commitment_entropy", # backward-compat alias
|
|
136
|
+
"compute_pairwise_cs_matrix",
|
|
137
|
+
"compute_cell_scores",
|
|
138
|
+
# Driver genes
|
|
139
|
+
"get_velocity_drivers",
|
|
140
|
+
"get_deg_drivers",
|
|
141
|
+
# Pathway enrichment
|
|
142
|
+
"run_enrichment_per_fate",
|
|
143
|
+
"export_enrichment_tables",
|
|
144
|
+
# Plots
|
|
145
|
+
"plot_star_embedding",
|
|
146
|
+
"plot_star_panels",
|
|
147
|
+
"plot_rose",
|
|
148
|
+
"plot_pairwise_cs",
|
|
149
|
+
"plot_commitment_bar",
|
|
150
|
+
"plot_commitment_heatmap",
|
|
151
|
+
"plot_subset_comparison",
|
|
152
|
+
"plot_expression_trends",
|
|
153
|
+
"plot_nn_entropy_elbow",
|
|
154
|
+
]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
bifurcation.py — Cluster-level fate map construction for scCS.
|
|
3
|
+
|
|
4
|
+
In scCS, the bifurcation point is explicitly defined by the user as a
|
|
5
|
+
single cluster (e.g., leiden cluster '17'). There is no automatic
|
|
6
|
+
fate detection — the user supplies:
|
|
7
|
+
|
|
8
|
+
bifurcation_cluster : the progenitor/root cluster label
|
|
9
|
+
terminal_cell_types : list of terminal fate cluster labels
|
|
10
|
+
|
|
11
|
+
This module builds a standardized FateMap from those labels, computing
|
|
12
|
+
centroids in the scCS star embedding space (X_sccs) and collecting
|
|
13
|
+
per-fate cell indices.
|
|
14
|
+
|
|
15
|
+
The FateMap is the single source of truth consumed by CommitmentScorer.score().
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import warnings
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import List, Optional
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# FateMap dataclass
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class FateMap:
|
|
33
|
+
"""Standardized description of k cell fates for commitment scoring.
|
|
34
|
+
|
|
35
|
+
Attributes
|
|
36
|
+
----------
|
|
37
|
+
bifurcation_cluster : str
|
|
38
|
+
Label of the progenitor/root cluster supplied by the user.
|
|
39
|
+
fate_names : list of str
|
|
40
|
+
Human-readable labels for each terminal fate (length k).
|
|
41
|
+
fate_centroids : np.ndarray, shape (k, 2)
|
|
42
|
+
Mean 2D position of each fate's cells in the scCS embedding.
|
|
43
|
+
root_centroid : np.ndarray, shape (2,)
|
|
44
|
+
Mean 2D position of the bifurcation cluster cells.
|
|
45
|
+
In the scCS star embedding this is always near (0, 0).
|
|
46
|
+
root_cells : np.ndarray of int
|
|
47
|
+
Indices of bifurcation cluster cells in adata.
|
|
48
|
+
fate_cell_indices : list of np.ndarray
|
|
49
|
+
Per-fate arrays of cell indices.
|
|
50
|
+
arm_angles_deg : np.ndarray, shape (k,)
|
|
51
|
+
Angle (degrees) of each fate's radial arm in the star embedding.
|
|
52
|
+
cluster_key : str
|
|
53
|
+
The obs column used for cluster labels.
|
|
54
|
+
k : int
|
|
55
|
+
Number of fates (read-only property).
|
|
56
|
+
"""
|
|
57
|
+
bifurcation_cluster: str
|
|
58
|
+
fate_names: List[str]
|
|
59
|
+
fate_centroids: np.ndarray
|
|
60
|
+
root_centroid: np.ndarray
|
|
61
|
+
root_cells: np.ndarray
|
|
62
|
+
fate_cell_indices: List[np.ndarray]
|
|
63
|
+
arm_angles_deg: np.ndarray
|
|
64
|
+
cluster_key: str
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def k(self) -> int:
|
|
68
|
+
return len(self.fate_names)
|
|
69
|
+
|
|
70
|
+
def summary(self) -> str:
|
|
71
|
+
lines = [
|
|
72
|
+
f"FateMap (bifurcation_cluster='{self.bifurcation_cluster}', k={self.k})",
|
|
73
|
+
f" Cluster key : '{self.cluster_key}'",
|
|
74
|
+
f" Root cells : {len(self.root_cells)}",
|
|
75
|
+
f" Root centroid: ({self.root_centroid[0]:.3f}, {self.root_centroid[1]:.3f})",
|
|
76
|
+
]
|
|
77
|
+
for j, name in enumerate(self.fate_names):
|
|
78
|
+
n = len(self.fate_cell_indices[j])
|
|
79
|
+
c = self.fate_centroids[j]
|
|
80
|
+
a = self.arm_angles_deg[j]
|
|
81
|
+
lines.append(
|
|
82
|
+
f" Fate {j}: '{name}' n_cells={n} "
|
|
83
|
+
f"centroid=({c[0]:.2f}, {c[1]:.2f}) arm_angle={a:.1f}°"
|
|
84
|
+
)
|
|
85
|
+
return "\n".join(lines)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# FateMap construction
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
def build_fate_map(
|
|
93
|
+
adata,
|
|
94
|
+
bifurcation_cluster: str,
|
|
95
|
+
terminal_cell_types: List[str],
|
|
96
|
+
cluster_key: str = "leiden",
|
|
97
|
+
verbose: bool = True,
|
|
98
|
+
) -> FateMap:
|
|
99
|
+
"""Build a FateMap from user-supplied cluster labels.
|
|
100
|
+
|
|
101
|
+
This is the only fate-detection strategy in scCS. The user explicitly
|
|
102
|
+
names the bifurcation cluster and all terminal fate clusters.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
adata : AnnData
|
|
107
|
+
Must have X_sccs in obsm (built by build_star_embedding).
|
|
108
|
+
bifurcation_cluster : str
|
|
109
|
+
Label of the progenitor cluster in adata.obs[cluster_key].
|
|
110
|
+
Example: '17' (leiden cluster 17)
|
|
111
|
+
terminal_cell_types : list of str
|
|
112
|
+
Labels of the k terminal fate clusters.
|
|
113
|
+
Example: ['Monocyte', 'DC', 'Neutrophil']
|
|
114
|
+
cluster_key : str
|
|
115
|
+
Column in adata.obs with cluster labels.
|
|
116
|
+
verbose : bool
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
FateMap
|
|
121
|
+
"""
|
|
122
|
+
if "X_sccs" not in adata.obsm:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
"X_sccs embedding not found in adata.obsm. "
|
|
125
|
+
"Run CommitmentScorer.build_embedding() before build_fate_map()."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
obs_labels = adata.obs[cluster_key].astype(str).values
|
|
129
|
+
embedding = np.array(adata.obsm["X_sccs"])
|
|
130
|
+
|
|
131
|
+
# --- Validate bifurcation cluster ---
|
|
132
|
+
bif_mask = obs_labels == str(bifurcation_cluster)
|
|
133
|
+
if bif_mask.sum() == 0:
|
|
134
|
+
available = sorted(set(obs_labels))
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"Bifurcation cluster '{bifurcation_cluster}' not found in "
|
|
137
|
+
f"adata.obs['{cluster_key}']. "
|
|
138
|
+
f"Available labels: {available}"
|
|
139
|
+
)
|
|
140
|
+
root_cells = np.where(bif_mask)[0]
|
|
141
|
+
root_centroid = embedding[root_cells].mean(axis=0)
|
|
142
|
+
|
|
143
|
+
if verbose:
|
|
144
|
+
print(
|
|
145
|
+
f"[scCS] Bifurcation cluster '{bifurcation_cluster}': "
|
|
146
|
+
f"{len(root_cells)} cells, "
|
|
147
|
+
f"centroid=({root_centroid[0]:.2f}, {root_centroid[1]:.2f})"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# --- Validate and collect terminal fates ---
|
|
151
|
+
fate_names = []
|
|
152
|
+
fate_centroids = []
|
|
153
|
+
fate_cell_indices = []
|
|
154
|
+
skipped = []
|
|
155
|
+
|
|
156
|
+
for name in terminal_cell_types:
|
|
157
|
+
mask = obs_labels == str(name)
|
|
158
|
+
n = mask.sum()
|
|
159
|
+
if n == 0:
|
|
160
|
+
warnings.warn(
|
|
161
|
+
f"Terminal fate '{name}' not found in adata.obs['{cluster_key}']. "
|
|
162
|
+
"Skipping.",
|
|
163
|
+
stacklevel=2,
|
|
164
|
+
)
|
|
165
|
+
skipped.append(name)
|
|
166
|
+
continue
|
|
167
|
+
idx = np.where(mask)[0]
|
|
168
|
+
fate_names.append(str(name))
|
|
169
|
+
fate_cell_indices.append(idx)
|
|
170
|
+
fate_centroids.append(embedding[idx].mean(axis=0))
|
|
171
|
+
|
|
172
|
+
if verbose:
|
|
173
|
+
c = embedding[idx].mean(axis=0)
|
|
174
|
+
print(f"[scCS] Fate '{name}': {n} cells, centroid=({c[0]:.2f}, {c[1]:.2f})")
|
|
175
|
+
|
|
176
|
+
if len(fate_names) == 0:
|
|
177
|
+
raise ValueError(
|
|
178
|
+
"No valid terminal fate clusters found. "
|
|
179
|
+
f"Skipped: {skipped}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if skipped:
|
|
183
|
+
warnings.warn(
|
|
184
|
+
f"Skipped {len(skipped)} fate(s) not found in data: {skipped}",
|
|
185
|
+
stacklevel=2,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
fate_centroids = np.array(fate_centroids)
|
|
189
|
+
|
|
190
|
+
# --- Retrieve arm angles from embedding metadata ---
|
|
191
|
+
# build_star_embedding stores these in adata.uns['sccs']
|
|
192
|
+
sccs_meta = adata.uns.get("sccs", {})
|
|
193
|
+
stored_fates = sccs_meta.get("fate_names", [])
|
|
194
|
+
stored_angles = sccs_meta.get("arm_angles_deg", None)
|
|
195
|
+
|
|
196
|
+
arm_angles_deg = np.zeros(len(fate_names))
|
|
197
|
+
if stored_angles is not None and len(stored_fates) == len(stored_angles):
|
|
198
|
+
fate_to_angle = dict(zip(stored_fates, stored_angles))
|
|
199
|
+
for j, name in enumerate(fate_names):
|
|
200
|
+
if name in fate_to_angle:
|
|
201
|
+
arm_angles_deg[j] = fate_to_angle[name]
|
|
202
|
+
else:
|
|
203
|
+
# Compute from centroid direction
|
|
204
|
+
delta = fate_centroids[j] - root_centroid
|
|
205
|
+
arm_angles_deg[j] = np.degrees(np.arctan2(delta[1], delta[0])) % 360.0
|
|
206
|
+
else:
|
|
207
|
+
# Compute from centroid directions
|
|
208
|
+
for j in range(len(fate_names)):
|
|
209
|
+
delta = fate_centroids[j] - root_centroid
|
|
210
|
+
arm_angles_deg[j] = np.degrees(np.arctan2(delta[1], delta[0])) % 360.0
|
|
211
|
+
|
|
212
|
+
fate_map = FateMap(
|
|
213
|
+
bifurcation_cluster=str(bifurcation_cluster),
|
|
214
|
+
fate_names=fate_names,
|
|
215
|
+
fate_centroids=fate_centroids,
|
|
216
|
+
root_centroid=root_centroid,
|
|
217
|
+
root_cells=root_cells,
|
|
218
|
+
fate_cell_indices=fate_cell_indices,
|
|
219
|
+
arm_angles_deg=arm_angles_deg,
|
|
220
|
+
cluster_key=cluster_key,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if verbose:
|
|
224
|
+
print(f"[scCS] FateMap built: k={fate_map.k} fates")
|
|
225
|
+
|
|
226
|
+
return fate_map
|