fusemap 0.0.2__tar.gz → 1.1.0__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.
- {fusemap-0.0.2 → fusemap-1.1.0}/PKG-INFO +3 -2
- {fusemap-0.0.2 → fusemap-1.1.0}/README.md +2 -1
- fusemap-1.1.0/fusemap/__init__.py +13 -0
- fusemap-1.1.0/fusemap/api.py +285 -0
- fusemap-1.1.0/fusemap/config.py +170 -0
- fusemap-1.1.0/fusemap/data/__init__.py +0 -0
- fusemap-1.1.0/fusemap/data/loaders.py +448 -0
- fusemap-1.1.0/fusemap/metrics.py +102 -0
- fusemap-1.1.0/fusemap/models/__init__.py +0 -0
- fusemap-0.0.2/fusemap/loss.py → fusemap-1.1.0/fusemap/models/losses.py +376 -21
- fusemap-1.1.0/fusemap/pl.py +71 -0
- fusemap-1.1.0/fusemap/postprocess/__init__.py +0 -0
- fusemap-1.1.0/fusemap/postprocess/niche_align.py +283 -0
- fusemap-1.1.0/fusemap/postprocess/stage_b_script.py +654 -0
- fusemap-1.1.0/fusemap/training/__init__.py +0 -0
- fusemap-1.1.0/fusemap/training/ddp.py +293 -0
- fusemap-0.0.2/fusemap/spatial_integrate.py → fusemap-1.1.0/fusemap/training/integrate.py +96 -5
- fusemap-0.0.2/fusemap/spatial_map.py → fusemap-1.1.0/fusemap/training/map.py +11 -5
- fusemap-1.1.0/fusemap/training/sharded.py +408 -0
- {fusemap-0.0.2/fusemap → fusemap-1.1.0/fusemap/training}/train_model.py +489 -55
- {fusemap-0.0.2 → fusemap-1.1.0}/fusemap/utils.py +3 -1
- {fusemap-0.0.2 → fusemap-1.1.0}/fusemap.egg-info/PKG-INFO +3 -2
- fusemap-1.1.0/fusemap.egg-info/SOURCES.txt +32 -0
- {fusemap-0.0.2 → fusemap-1.1.0}/setup.py +36 -41
- fusemap-0.0.2/fusemap/__init__.py +0 -10
- fusemap-0.0.2/fusemap/config.py +0 -77
- fusemap-0.0.2/fusemap/dataset.py +0 -204
- fusemap-0.0.2/fusemap/train.py +0 -247
- fusemap-0.0.2/fusemap.egg-info/SOURCES.txt +0 -23
- fusemap-0.0.2/fusemap.egg-info/entry_points.txt +0 -2
- {fusemap-0.0.2 → fusemap-1.1.0}/LICENSE +0 -0
- /fusemap-0.0.2/fusemap/preprocess.py → /fusemap-1.1.0/fusemap/data/graph.py +0 -0
- {fusemap-0.0.2 → fusemap-1.1.0}/fusemap/logger.py +0 -0
- /fusemap-0.0.2/fusemap/model.py → /fusemap-1.1.0/fusemap/models/network.py +0 -0
- {fusemap-0.0.2 → fusemap-1.1.0}/fusemap/permutation.py +0 -0
- /fusemap-0.0.2/fusemap/deconvolution.py → /fusemap-1.1.0/fusemap/postprocess/imputation.py +0 -0
- {fusemap-0.0.2 → fusemap-1.1.0}/fusemap.egg-info/dependency_links.txt +0 -0
- {fusemap-0.0.2 → fusemap-1.1.0}/fusemap.egg-info/requires.txt +0 -0
- {fusemap-0.0.2 → fusemap-1.1.0}/fusemap.egg-info/top_level.txt +0 -0
- {fusemap-0.0.2 → fusemap-1.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fusemap
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Description-Content-Type: text/markdown
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Dist: scanpy==1.9.3
|
|
@@ -71,7 +71,8 @@ seaborn
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
## Installation and Tutorial
|
|
74
|
-
- Read the tutorial [here](https://fusemap.readthedocs.io/en/latest/)
|
|
74
|
+
- Read the FuseMap tutorial [here](https://fusemap.readthedocs.io/en/latest/).
|
|
75
|
+
- FuseMap-Agent set up tutorial [here](https://github.com/wanglab-broad/FuseMap/tree/main/agent_setup).
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
## Citation
|
|
@@ -40,7 +40,8 @@ seaborn
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
## Installation and Tutorial
|
|
43
|
-
- Read the tutorial [here](https://fusemap.readthedocs.io/en/latest/)
|
|
43
|
+
- Read the FuseMap tutorial [here](https://fusemap.readthedocs.io/en/latest/).
|
|
44
|
+
- FuseMap-Agent set up tutorial [here](https://github.com/wanglab-broad/FuseMap/tree/main/agent_setup).
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
## Citation
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from fusemap.training.integrate import *
|
|
2
|
+
from fusemap.training.map import *
|
|
3
|
+
from fusemap.logger import *
|
|
4
|
+
from fusemap.utils import *
|
|
5
|
+
from fusemap.config import *
|
|
6
|
+
from fusemap.models.network import *
|
|
7
|
+
from fusemap.data.loaders import *
|
|
8
|
+
from fusemap.models.losses import *
|
|
9
|
+
from fusemap.training.train_model import *
|
|
10
|
+
from fusemap.data.graph import *
|
|
11
|
+
from fusemap.api import *
|
|
12
|
+
from fusemap import metrics
|
|
13
|
+
from fusemap import pl
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""One-call convenience API.
|
|
2
|
+
|
|
3
|
+
These wrappers make the common workflows single function calls::
|
|
4
|
+
|
|
5
|
+
import fusemap
|
|
6
|
+
|
|
7
|
+
fusemap.integrate("./data", "./output")
|
|
8
|
+
fusemap.map_to_reference("./new_data", "./output_map", "./pretrained_model")
|
|
9
|
+
fusemap.deconvolve_beads("./output", "./data",
|
|
10
|
+
bead_files="slideseq", sig_ref="starmap")
|
|
11
|
+
|
|
12
|
+
Every argument that ``main.py`` accepts on the command line is available as a
|
|
13
|
+
keyword argument; everything else uses the validated defaults.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from types import SimpleNamespace
|
|
20
|
+
|
|
21
|
+
import scanpy as sc
|
|
22
|
+
|
|
23
|
+
from fusemap.logger import setup_logging
|
|
24
|
+
from fusemap.training.integrate import spatial_integrate
|
|
25
|
+
from fusemap.training.map import spatial_map
|
|
26
|
+
from fusemap.utils import seed_all
|
|
27
|
+
|
|
28
|
+
__all__ = ["integrate", "map_to_reference", "deconvolve_beads", "transfer_labels", "read_input_folder"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_input_folder(input_data_folder_path):
|
|
32
|
+
"""Read every ``.h5ad`` in a folder and normalize spatial coordinates.
|
|
33
|
+
|
|
34
|
+
Coordinates are taken from ``obs['x']/obs['y']``, falling back to
|
|
35
|
+
``obs['col']/obs['row']`` and then ``obsm['spatial']``.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
list of :class:`anndata.AnnData`, one per section.
|
|
40
|
+
"""
|
|
41
|
+
folder = Path(input_data_folder_path)
|
|
42
|
+
file_names = sorted(str(p) for p in folder.iterdir() if p.is_file() and p.suffix == ".h5ad")
|
|
43
|
+
if not file_names:
|
|
44
|
+
raise ValueError(f"No .h5ad files found in {folder}")
|
|
45
|
+
|
|
46
|
+
X_input = []
|
|
47
|
+
for ind, file_name_i in enumerate(file_names):
|
|
48
|
+
X = sc.read_h5ad(file_name_i)
|
|
49
|
+
if "x" not in X.obs.columns:
|
|
50
|
+
if "col" in X.obs.columns and "row" in X.obs.columns:
|
|
51
|
+
X.obs["x"] = X.obs["col"]
|
|
52
|
+
X.obs["y"] = X.obs["row"]
|
|
53
|
+
elif "spatial" in X.obsm:
|
|
54
|
+
X.obs["x"] = X.obsm["spatial"][:, 0]
|
|
55
|
+
X.obs["y"] = X.obsm["spatial"][:, 1]
|
|
56
|
+
else:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"{file_name_i}: provide spatial coordinates in obs['x']/obs['y'], "
|
|
59
|
+
"obs['col']/obs['row'], or obsm['spatial']"
|
|
60
|
+
)
|
|
61
|
+
X.obs["name"] = f"section{ind}"
|
|
62
|
+
X.obs["file_name"] = os.path.basename(file_name_i)
|
|
63
|
+
X_input.append(X)
|
|
64
|
+
return X_input
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _make_args(output_save_dir, keep_celltype, keep_tissueregion,
|
|
68
|
+
use_llm_gene_embedding, pretrain_model_path):
|
|
69
|
+
Path(output_save_dir).mkdir(parents=True, exist_ok=True)
|
|
70
|
+
return SimpleNamespace(
|
|
71
|
+
output_save_dir=str(output_save_dir),
|
|
72
|
+
keep_celltype=keep_celltype,
|
|
73
|
+
keep_tissueregion=keep_tissueregion,
|
|
74
|
+
use_llm_gene_embedding=use_llm_gene_embedding,
|
|
75
|
+
pretrain_model_path=str(pretrain_model_path),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def integrate(input_data_folder_path, output_save_dir,
|
|
80
|
+
keep_celltype="", keep_tissueregion="",
|
|
81
|
+
use_llm_gene_embedding="false",
|
|
82
|
+
bead_files=None, sig_ref=None):
|
|
83
|
+
"""Integrate all spatial sections in a folder into shared embeddings.
|
|
84
|
+
|
|
85
|
+
Equivalent to ``python main.py --mode integrate``. Training resumes
|
|
86
|
+
automatically from ``snapshot.pt`` if the same command is re-run.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
input_data_folder_path
|
|
91
|
+
Folder with one ``.h5ad`` file per section.
|
|
92
|
+
output_save_dir
|
|
93
|
+
Output directory; embeddings are written here as
|
|
94
|
+
``ad_celltype_embedding.h5ad``, ``ad_tissueregion_embedding.h5ad``,
|
|
95
|
+
and ``ad_gene_embedding.h5ad``.
|
|
96
|
+
keep_celltype, keep_tissueregion
|
|
97
|
+
Optional ``obs`` column names with existing labels to carry into the
|
|
98
|
+
output embedding files.
|
|
99
|
+
bead_files, sig_ref
|
|
100
|
+
Declare bead/spot-resolution datasets (Slide-seq, Visium ...) and the
|
|
101
|
+
single-cell signature reference(s), as comma-separated file-name
|
|
102
|
+
substrings. When declared, Stage-B deconvolution runs AUTOMATICALLY
|
|
103
|
+
after training (recommended default for any bead platform), and the
|
|
104
|
+
DECOMPOSED embeddings become the canonical outputs
|
|
105
|
+
(``ad_celltype_embedding.h5ad`` / ``ad_tissueregion_embedding.h5ad``);
|
|
106
|
+
the pre-decomposition versions are kept as ``ad_*_embedding_nodeconv.h5ad``.
|
|
107
|
+
Roles are always user-declared, never auto-detected.
|
|
108
|
+
"""
|
|
109
|
+
seed_all(0)
|
|
110
|
+
args = _make_args(output_save_dir, keep_celltype, keep_tissueregion,
|
|
111
|
+
use_llm_gene_embedding, "")
|
|
112
|
+
setup_logging(args.output_save_dir)
|
|
113
|
+
logging.info("Arguments: %s", vars(args))
|
|
114
|
+
|
|
115
|
+
X_input = read_input_folder(input_data_folder_path)
|
|
116
|
+
kneighbor = ["delaunay"] * len(X_input)
|
|
117
|
+
input_identity = ["ST"] * len(X_input)
|
|
118
|
+
spatial_integrate(X_input, args, kneighbor, input_identity)
|
|
119
|
+
|
|
120
|
+
if bead_files:
|
|
121
|
+
if not sig_ref:
|
|
122
|
+
raise ValueError("bead_files declared without sig_ref (single-cell signature reference)")
|
|
123
|
+
logging.info("[FuseMap] bead datasets declared - running Stage-B deconvolution")
|
|
124
|
+
deconvolve_beads(output_save_dir, input_data_folder_path, bead_files, sig_ref)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def map_to_reference(input_data_folder_path, output_save_dir, pretrain_model_path,
|
|
128
|
+
keep_celltype="", keep_tissueregion="",
|
|
129
|
+
use_llm_gene_embedding="false"):
|
|
130
|
+
"""Map each section in a folder onto a pretrained FuseMap model.
|
|
131
|
+
|
|
132
|
+
Equivalent to ``python main.py --mode map``. Results are written to one
|
|
133
|
+
subdirectory of ``output_save_dir`` per input file.
|
|
134
|
+
"""
|
|
135
|
+
import copy as _copy
|
|
136
|
+
|
|
137
|
+
seed_all(0)
|
|
138
|
+
X_input = read_input_folder(input_data_folder_path)
|
|
139
|
+
for X in X_input:
|
|
140
|
+
args_i = _make_args(
|
|
141
|
+
os.path.join(str(output_save_dir), X.obs["file_name"].unique()[0]),
|
|
142
|
+
keep_celltype, keep_tissueregion,
|
|
143
|
+
use_llm_gene_embedding, pretrain_model_path,
|
|
144
|
+
)
|
|
145
|
+
setup_logging(args_i.output_save_dir)
|
|
146
|
+
spatial_map([X], args_i, ["delaunay"], ["ST"])
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def deconvolve_beads(output_save_dir, input_data_folder_path,
|
|
150
|
+
bead_files, sig_ref,
|
|
151
|
+
entropy_weight=None, signature_mode=None):
|
|
152
|
+
"""Deconvolve bead/spot-resolution datasets after an integration run.
|
|
153
|
+
|
|
154
|
+
Each bead is decomposed into a mixture over cell archetypes learned from
|
|
155
|
+
the single-cell sections (Stage-B); its cell and tissue embeddings are
|
|
156
|
+
rebuilt from the mixture. The decomposed embeddings become the canonical
|
|
157
|
+
``ad_celltype_embedding.h5ad`` / ``ad_tissueregion_embedding.h5ad`` (bead
|
|
158
|
+
rows replaced; single-cell rows unchanged). The pre-decomposition versions
|
|
159
|
+
are kept as ``ad_*_embedding_nodeconv.h5ad``; ``ad_*_embedding_stageB.h5ad``
|
|
160
|
+
(identical to canonical) and ``stageB_pi.npz`` (per-bead mixtures) are also
|
|
161
|
+
written into ``output_save_dir``.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
output_save_dir
|
|
166
|
+
Output directory of the finished :func:`integrate` run.
|
|
167
|
+
input_data_folder_path
|
|
168
|
+
The data folder that was integrated.
|
|
169
|
+
bead_files
|
|
170
|
+
Comma-separated file-name substrings, each matching exactly one input
|
|
171
|
+
file: the bead-resolution dataset(s) to deconvolve.
|
|
172
|
+
sig_ref
|
|
173
|
+
Comma-separated substrings selecting the single-cell reference
|
|
174
|
+
dataset(s) used to build archetype signatures.
|
|
175
|
+
entropy_weight
|
|
176
|
+
Entropy regularization of the mixture weights (default ``5e-4``).
|
|
177
|
+
signature_mode
|
|
178
|
+
``"empirical"`` (default, recommended) or ``"decode"``.
|
|
179
|
+
"""
|
|
180
|
+
import runpy
|
|
181
|
+
|
|
182
|
+
os.environ["STAGEB_OUT_DIR"] = str(output_save_dir)
|
|
183
|
+
os.environ["STAGEB_DATA_DIR"] = str(input_data_folder_path)
|
|
184
|
+
os.environ["FUSEMAP_BEAD_FILES"] = bead_files
|
|
185
|
+
os.environ["FUSEMAP_SIG_REF"] = sig_ref
|
|
186
|
+
if entropy_weight is not None:
|
|
187
|
+
os.environ["STAGEB_ENT_W"] = str(entropy_weight)
|
|
188
|
+
if signature_mode is not None:
|
|
189
|
+
os.environ["STAGEB_SIG"] = str(signature_mode)
|
|
190
|
+
|
|
191
|
+
script = Path(__file__).resolve().parent / "postprocess" / "stage_b_script.py"
|
|
192
|
+
runpy.run_path(str(script), run_name="__main__")
|
|
193
|
+
|
|
194
|
+
# Decomposed embeddings become the CANONICAL outputs (default for any run
|
|
195
|
+
# with declared bead datasets). The pre-decomposition versions are kept as
|
|
196
|
+
# *_nodeconv.h5ad; *_stageB.h5ad names are kept for compatibility.
|
|
197
|
+
import shutil as _sh
|
|
198
|
+
for lvl in ("celltype", "tissueregion"):
|
|
199
|
+
canon = os.path.join(str(output_save_dir), f"ad_{lvl}_embedding.h5ad")
|
|
200
|
+
stage = os.path.join(str(output_save_dir), f"ad_{lvl}_embedding_stageB.h5ad")
|
|
201
|
+
keep = os.path.join(str(output_save_dir), f"ad_{lvl}_embedding_nodeconv.h5ad")
|
|
202
|
+
if os.path.exists(stage) and os.path.exists(canon):
|
|
203
|
+
if not os.path.exists(keep):
|
|
204
|
+
os.rename(canon, keep)
|
|
205
|
+
_sh.copyfile(stage, canon)
|
|
206
|
+
logging.info("[FuseMap] Stage-B embeddings promoted to canonical outputs "
|
|
207
|
+
"(pre-decomposition kept as *_nodeconv.h5ad)")
|
|
208
|
+
|
|
209
|
+
def transfer_labels(adata, label_key, batch_size=256, epochs=200,
|
|
210
|
+
unlabeled_values=("nan", "Unannotated", ""), device=None):
|
|
211
|
+
"""Transfer labels between datasets through the shared embedding — one call.
|
|
212
|
+
|
|
213
|
+
Trains a small classifier on all cells that carry a label in
|
|
214
|
+
``obs[label_key]`` (the reference) and predicts that label for every cell
|
|
215
|
+
in ``adata``, adding two columns to ``obs``:
|
|
216
|
+
|
|
217
|
+
- ``transfer_<label_key>`` — the transferred label
|
|
218
|
+
- ``transfer_<label_key>_uncertainty`` — 1 - prediction confidence
|
|
219
|
+
|
|
220
|
+
Works on any embedding AnnData produced by :func:`integrate` or
|
|
221
|
+
:func:`map_to_reference` (latent in ``X``). Cells whose label is missing
|
|
222
|
+
or in ``unlabeled_values`` are excluded from training.
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
dict with ``labels`` (np.ndarray), ``uncertainty`` (np.ndarray) and
|
|
227
|
+
``test_accuracy`` (balanced per-class accuracy on a held-out 10% of the reference).
|
|
228
|
+
"""
|
|
229
|
+
import numpy as np
|
|
230
|
+
import torch
|
|
231
|
+
from sklearn import preprocessing
|
|
232
|
+
from torch import nn
|
|
233
|
+
from torch.utils.data import DataLoader, TensorDataset, random_split
|
|
234
|
+
|
|
235
|
+
from fusemap.models.network import NNTransfer
|
|
236
|
+
from fusemap.utils import NNTransferPredictWithUncertainty, NNTransferTrain
|
|
237
|
+
|
|
238
|
+
if device is None:
|
|
239
|
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
240
|
+
|
|
241
|
+
X = np.asarray(adata.X, dtype=np.float32)
|
|
242
|
+
lab = adata.obs[label_key].astype(str).values
|
|
243
|
+
is_ref = ~np.isin(lab, list(unlabeled_values)) & (lab != "nan")
|
|
244
|
+
if is_ref.sum() < 100:
|
|
245
|
+
raise ValueError(f"only {is_ref.sum()} labeled reference cells for '{label_key}'")
|
|
246
|
+
|
|
247
|
+
le = preprocessing.LabelEncoder()
|
|
248
|
+
y_ref = le.fit_transform(lab[is_ref]).astype(int)
|
|
249
|
+
|
|
250
|
+
ds = TensorDataset(torch.tensor(X[is_ref]), torch.tensor(y_ref).long())
|
|
251
|
+
n_train = int(0.8 * len(ds))
|
|
252
|
+
n_val = int(0.1 * len(ds))
|
|
253
|
+
train_ds, val_ds, test_ds = random_split(
|
|
254
|
+
ds, [n_train, n_val, len(ds) - n_train - n_val])
|
|
255
|
+
loaders = dict(
|
|
256
|
+
train=DataLoader(train_ds, batch_size=batch_size, shuffle=True),
|
|
257
|
+
val=DataLoader(val_ds, batch_size=batch_size, shuffle=False),
|
|
258
|
+
test=DataLoader(test_ds, batch_size=batch_size, shuffle=False),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
import sklearn.utils
|
|
262
|
+
|
|
263
|
+
model = NNTransfer(input_dim=X.shape[1], output_dim=len(le.classes_)).to(device)
|
|
264
|
+
class_weight = torch.Tensor(sklearn.utils.class_weight.compute_class_weight(
|
|
265
|
+
class_weight="balanced", classes=np.unique(y_ref), y=y_ref))
|
|
266
|
+
criterion = nn.CrossEntropyLoss(weight=class_weight.to(device))
|
|
267
|
+
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
|
|
268
|
+
NNTransferTrain(model, criterion, optimizer, loaders["train"], loaders["val"],
|
|
269
|
+
device, epochs=epochs)
|
|
270
|
+
|
|
271
|
+
test_pred, _ = NNTransferPredictWithUncertainty(model, loaders["test"], device)
|
|
272
|
+
test_true = np.array([y for _, y in test_ds])
|
|
273
|
+
test_pred = np.array(test_pred)
|
|
274
|
+
# balanced (per-class mean) accuracy: plain accuracy hides rare classes
|
|
275
|
+
accs = [float((test_pred[test_true == c] == c).mean())
|
|
276
|
+
for c in np.unique(test_true) if (test_true == c).sum() > 0]
|
|
277
|
+
test_acc = float(np.mean(accs))
|
|
278
|
+
|
|
279
|
+
all_loader = DataLoader(TensorDataset(torch.tensor(X)), batch_size=batch_size, shuffle=False)
|
|
280
|
+
pred, unc = NNTransferPredictWithUncertainty(model, all_loader, device)
|
|
281
|
+
labels = le.inverse_transform(np.array(pred))
|
|
282
|
+
|
|
283
|
+
adata.obs[f"transfer_{label_key}"] = labels
|
|
284
|
+
adata.obs[f"transfer_{label_key}_uncertainty"] = np.array(unc)
|
|
285
|
+
return {"labels": labels, "uncertainty": np.array(unc), "test_accuracy": test_acc}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_input_args():
|
|
7
|
+
parser = argparse.ArgumentParser(description="FuseMap")
|
|
8
|
+
|
|
9
|
+
parser.add_argument(
|
|
10
|
+
"--input_data_folder_path",
|
|
11
|
+
type=str,
|
|
12
|
+
required=True,
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"--output_save_dir",
|
|
16
|
+
type=str,
|
|
17
|
+
required=True,
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--mode",
|
|
21
|
+
type=str,
|
|
22
|
+
required=True,
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--keep_celltype",
|
|
26
|
+
type=str,
|
|
27
|
+
default="",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--keep_tissueregion",
|
|
31
|
+
type=str,
|
|
32
|
+
default="",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--use_llm_gene_embedding",
|
|
36
|
+
default='false',
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--pretrain_model_path",
|
|
40
|
+
default="",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--bead_files",
|
|
44
|
+
type=str,
|
|
45
|
+
default="",
|
|
46
|
+
help="comma-separated substrings declaring bead/spot-resolution datasets; Stage-B runs automatically after integration",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--sig_ref",
|
|
50
|
+
type=str,
|
|
51
|
+
default="",
|
|
52
|
+
help="comma-separated substrings selecting single-cell signature reference dataset(s) for Stage-B",
|
|
53
|
+
)
|
|
54
|
+
args = parser.parse_args()
|
|
55
|
+
return args
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FlagConfig:
|
|
59
|
+
lambda_disc_single = 1
|
|
60
|
+
align_anneal = 1e10
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ModelType(Enum):
|
|
64
|
+
pca_dim = 50
|
|
65
|
+
hidden_dim = 512
|
|
66
|
+
latent_dim = 64
|
|
67
|
+
dropout_rate = 0.2
|
|
68
|
+
n_epochs = 16
|
|
69
|
+
batch_size = 64
|
|
70
|
+
learning_rate = 0.001
|
|
71
|
+
optim_kw = "RMSprop"
|
|
72
|
+
use_input = "norm"
|
|
73
|
+
lambda_ae_single = 1
|
|
74
|
+
lambda_disc_spatial = 1
|
|
75
|
+
lambda_ae_spatial = 1
|
|
76
|
+
align_noise_coef = 1.5
|
|
77
|
+
lr_patience_pretrain = 2
|
|
78
|
+
lr_factor_pretrain = 0.5
|
|
79
|
+
lr_limit_pretrain = 0.00001
|
|
80
|
+
patience_limit_final = 5
|
|
81
|
+
lr_patience_final = 3
|
|
82
|
+
lr_factor_final = 0.5
|
|
83
|
+
lr_limit_final = 0.00001
|
|
84
|
+
patience_limit_pretrain = 3
|
|
85
|
+
EPS = 1e-10
|
|
86
|
+
DIS_LAMDA = 2
|
|
87
|
+
TRAIN_WITHOUT_EVAL = 10
|
|
88
|
+
USE_REFERENCE_PCT = 0.02
|
|
89
|
+
verbose = False
|
|
90
|
+
use_key='final'
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class AnchorConfig:
|
|
94
|
+
"""Cross-sample MNN anchor alignment (STAligner-style), loss-level only.
|
|
95
|
+
|
|
96
|
+
This is a plain class (NOT a ``ModelType`` Enum member; the Enum has
|
|
97
|
+
value-aliasing quirks). Setting ``anchor_lambda = 0`` disables every anchor
|
|
98
|
+
code path and reproduces the original FuseMap behavior exactly.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
# default 0.3 = validated optimal (2026-09-01); override via env var
|
|
102
|
+
anchor_lambda = float(os.environ.get("FUSEMAP_ANCHOR_LAMBDA", "0.3"))
|
|
103
|
+
anchor_refresh_epochs = 2 # recompute MNN anchors every N epochs
|
|
104
|
+
anchor_k = 15 # kNN used per direction for MNN
|
|
105
|
+
anchor_start_epoch = int(os.environ.get("FUSEMAP_ANCHOR_START", "2"))
|
|
106
|
+
# quality filters (2026-09-01): drop low-similarity MNN pairs and pairs not
|
|
107
|
+
# stable across consecutive refreshes; disable anchors during pretrain by
|
|
108
|
+
# default (immature latent locks in wrong anchors).
|
|
109
|
+
anchor_sim_threshold = float(os.environ.get("FUSEMAP_ANCHOR_SIM", "0.5"))
|
|
110
|
+
# comma-separated file-name substrings marking QUERY datasets (e.g. bead
|
|
111
|
+
# data): they get pulled toward others, but nothing is pulled toward them.
|
|
112
|
+
anchor_query_files = os.environ.get("FUSEMAP_ANCHOR_QUERY", "")
|
|
113
|
+
anchor_query_atlases = set() # resolved at runtime from input file names
|
|
114
|
+
anchor_stable_only = os.environ.get("FUSEMAP_ANCHOR_STABLE", "1") == "1"
|
|
115
|
+
anchor_in_pretrain = os.environ.get("FUSEMAP_ANCHOR_PRETRAIN", "0") == "1"
|
|
116
|
+
|
|
117
|
+
# Within-dataset structure-preservation triplet loss (opt-in).
|
|
118
|
+
# struct_lambda = 0 keeps the feature fully OFF and reproduces current
|
|
119
|
+
# behavior EXACTLY (no extra RNG draws, no extra compute anywhere).
|
|
120
|
+
# When > 0, spatial_integrate precomputes per-atlas expression-space kNN
|
|
121
|
+
# (stored at runtime as ``AnchorConfig.struct_knn``) and
|
|
122
|
+
# ``fusemap.loss.compute_struct_loss`` adds a triplet term in the final
|
|
123
|
+
# training phase that keeps each cell's expression neighbors closer in
|
|
124
|
+
# latent space than random same-dataset cells.
|
|
125
|
+
struct_lambda = float(os.environ.get("FUSEMAP_STRUCT_LAMBDA", "0"))
|
|
126
|
+
struct_k = int(os.environ.get("FUSEMAP_STRUCT_K", "10"))
|
|
127
|
+
struct_margin = float(os.environ.get("FUSEMAP_STRUCT_MARGIN", "1.0"))
|
|
128
|
+
# Semi-hard negative mining (default ON when struct loss is enabled):
|
|
129
|
+
# uniformly random negatives are already far apart in the compact latent,
|
|
130
|
+
# so the repulsion term is inert and the triplet degenerates into extra
|
|
131
|
+
# compression (measured: type purity 0.578 -> 0.502). Instead mine the
|
|
132
|
+
# hardest VALID negative among struct_neg_candidates random candidates.
|
|
133
|
+
# Set FUSEMAP_STRUCT_HARDNEG=0 to reproduce the legacy random-negative
|
|
134
|
+
# path bit-for-bit (same number and order of RNG draws).
|
|
135
|
+
struct_hardneg = os.environ.get("FUSEMAP_STRUCT_HARDNEG", "1") == "1"
|
|
136
|
+
struct_neg_candidates = int(os.environ.get("FUSEMAP_STRUCT_NEGC", "16"))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class AnchorState:
|
|
140
|
+
"""Runtime container for anchor alignment state.
|
|
141
|
+
|
|
142
|
+
Populated by ``fusemap.train_model.refresh_anchors`` and consumed by the AE
|
|
143
|
+
loss (``fusemap.loss.compute_anchor_loss``). Kept dependency-free (plain
|
|
144
|
+
attributes) so it can live in the config module.
|
|
145
|
+
|
|
146
|
+
Attributes
|
|
147
|
+
----------
|
|
148
|
+
single_cache : list of torch.Tensor or None
|
|
149
|
+
Per-atlas full-dataset single latent (z_mean), detached, [n_obs_i, d].
|
|
150
|
+
spatial_cache : list of torch.Tensor or None
|
|
151
|
+
Per-atlas full-dataset spatial latent (z_spatial), detached.
|
|
152
|
+
partner : dict
|
|
153
|
+
(i, j) -> int64 tensor of length n_obs_i giving, for each cell in atlas
|
|
154
|
+
i, the index of its MNN partner cell in atlas j (or -1 if none).
|
|
155
|
+
n_pairs : int
|
|
156
|
+
Total number of MNN anchor pairs found at the last refresh.
|
|
157
|
+
mean_dist : float
|
|
158
|
+
Mean cross-atlas single-latent distance of anchored pairs at refresh.
|
|
159
|
+
has_anchors : bool
|
|
160
|
+
Whether any anchors are currently stored.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(self):
|
|
164
|
+
self.prev_pairs = {}
|
|
165
|
+
self.single_cache = None
|
|
166
|
+
self.spatial_cache = None
|
|
167
|
+
self.partner = {}
|
|
168
|
+
self.n_pairs = 0
|
|
169
|
+
self.mean_dist = float("nan")
|
|
170
|
+
self.has_anchors = False
|
|
File without changes
|