Download BioMod/zero_shot_batch/evaluate.py from ctcatlas/BioMod: direct link, hf CLI and curl.
- Browser
- Download file 27.3 kB
-
https://huggingface.co/ctcatlas/BioMod/resolve/main/BioMod/zero_shot_batch/evaluate.py
- Command line
-
hf download hf://ctcatlas/BioMod/BioMod/zero_shot_batch/evaluate.py
-
curl -L -o evaluate.py https://huggingface.co/ctcatlas/BioMod/resolve/main/BioMod/zero_shot_batch/evaluate.py
27.3 kB
| """ | |
| Adaptation code of the BioMod model on the zero-shot batch correction task. | |
| The current script mainly supports: | |
| 1. Use the BioMod model (encoder-only) to extract cell embeddings. | |
| 2. Use mean pooling of module encodings as cell embedding. | |
| 3. Supports evaluation based on scIB indicators consistent with the scGPT tutorial. | |
| 4. Supports Lung data set preset and retains all genes by default without going through the HVG 3000 process. | |
| """ | |
| import argparse | |
| import importlib.util | |
| import os | |
| import pickle | |
| import sys | |
| import tempfile | |
| from pathlib import Path | |
| REPO_ROOT = Path(__file__).resolve().parents[2] | |
| ASSET_DIR = REPO_ROOT / "model_dependencies" | |
| DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data" | |
| WEIGHTS_DIR = REPO_ROOT / "weight" | |
| from copy import deepcopy | |
| from typing import Dict, List, Optional, Sequence, Set, Union | |
| os.environ.setdefault("NUMBA_DISABLE_JIT", "1") | |
| os.environ.setdefault("NUMBA_CACHE_DIR", tempfile.mkdtemp(prefix="numba_cache_")) | |
| import numpy as np | |
| import pandas as pd | |
| import scanpy as sc | |
| import scib | |
| import torch | |
| from scanpy import AnnData | |
| from tqdm import tqdm | |
| # Model imports below are anchored to this file, independent of the working directory. | |
| DATASET_PRESETS = { | |
| "auto": {}, | |
| "lung": { | |
| "gene_col": "gene_name", | |
| "batch_key": "sample", | |
| "label_keys": ["cell_type"], | |
| }, | |
| } | |
| def print_sys(message: str) -> None: | |
| print(message, flush=True, file=sys.stderr) | |
| def strip_gene_version(gene_id: str) -> str: | |
| return gene_id.split(".", 1)[0] | |
| def canonicalize_target_gene_id( | |
| raw_gene: object, | |
| symbol_to_ensembl: Dict[str, str], | |
| target_gene_set: Set[str], | |
| ) -> Optional[str]: | |
| if pd.isna(raw_gene): | |
| return None | |
| raw_gene_str = str(raw_gene).strip() | |
| if not raw_gene_str: | |
| return None | |
| direct_candidates = [raw_gene_str, strip_gene_version(raw_gene_str)] | |
| for candidate in direct_candidates: | |
| if candidate in target_gene_set: | |
| return candidate | |
| mapped_gene = symbol_to_ensembl.get(raw_gene_str) | |
| if mapped_gene is None: | |
| mapped_gene = symbol_to_ensembl.get(strip_gene_version(raw_gene_str)) | |
| if mapped_gene is None or pd.isna(mapped_gene): | |
| return None | |
| mapped_gene_str = strip_gene_version(str(mapped_gene).strip()) | |
| return mapped_gene_str or None | |
| def resolve_input_gene_ids( | |
| adata: AnnData, | |
| gene_col: Optional[str], | |
| symbol_to_ensembl: Dict[str, str], | |
| target_gene_set: Set[str], | |
| ) -> List[str]: | |
| if gene_col is None: | |
| raw_genes = [str(gene) for gene in adata.var.index.tolist()] | |
| source_name = "adata.var.index" | |
| else: | |
| if gene_col not in adata.var.columns: | |
| raise KeyError(f"gene_col '{gene_col}' not found in adata.var.columns") | |
| raw_genes = [str(gene) if not pd.isna(gene) else "" for gene in adata.var[gene_col].tolist()] | |
| source_name = f"adata.var['{gene_col}']" | |
| resolved_gene_ids: List[str] = [] | |
| direct_hits = 0 | |
| mapped_hits = 0 | |
| unresolved = 0 | |
| for index, raw_gene in enumerate(raw_genes): | |
| resolved_gene = canonicalize_target_gene_id(raw_gene, symbol_to_ensembl, target_gene_set) | |
| if resolved_gene is None: | |
| resolved_gene = f"unknown{index}" | |
| unresolved += 1 | |
| elif strip_gene_version(raw_gene) == resolved_gene: | |
| direct_hits += 1 | |
| else: | |
| mapped_hits += 1 | |
| resolved_gene_ids.append(resolved_gene) | |
| print_sys(f"Using genes from {source_name}") | |
| print_sys( | |
| "Gene ID resolution summary: " | |
| f"direct_hits={direct_hits}, mapped_hits={mapped_hits}, unresolved={unresolved}" | |
| ) | |
| return resolved_gene_ids | |
| def clear_neighbors(adata: AnnData) -> None: | |
| neighbors = adata.uns.pop("neighbors", None) | |
| if neighbors is not None: | |
| dist_key = neighbors.get("distances_key", "distances") | |
| conn_key = neighbors.get("connectivities_key", "connectivities") | |
| adata.obsp.pop(dist_key, None) | |
| adata.obsp.pop(conn_key, None) | |
| adata.obsp.pop("distances", None) | |
| adata.obsp.pop("connectivities", None) | |
| def prepare_eval_adata( | |
| adata: AnnData, | |
| batch_key: str, | |
| label_key: str, | |
| embedding_key: str, | |
| build_neighbors: bool = False, | |
| ) -> AnnData: | |
| if embedding_key not in adata.obsm: | |
| raise ValueError(f"Embeddings '{embedding_key}' not found in adata.obsm") | |
| if batch_key not in adata.obs.columns: | |
| raise ValueError(f"batch_key '{batch_key}' not found in adata.obs") | |
| if label_key not in adata.obs.columns: | |
| raise ValueError(f"label_key '{label_key}' not found in adata.obs") | |
| valid_mask = adata.obs[batch_key].notna() & adata.obs[label_key].notna() | |
| dropped = int((~valid_mask).sum()) | |
| if dropped > 0: | |
| print_sys( | |
| f"Dropping {dropped} cells with missing batch/label values " | |
| f"for evaluation (batch_key={batch_key}, label_key={label_key})." | |
| ) | |
| if int(valid_mask.sum()) == 0: | |
| raise ValueError( | |
| f"No labeled cells remain after filtering batch_key={batch_key}, label_key={label_key}" | |
| ) | |
| eval_adata = adata[valid_mask].copy() | |
| eval_adata.obs[batch_key] = eval_adata.obs[batch_key].astype("category") | |
| eval_adata.obs[label_key] = eval_adata.obs[label_key].astype("category") | |
| if build_neighbors: | |
| clear_neighbors(eval_adata) | |
| sc.pp.neighbors(eval_adata, use_rep=embedding_key) | |
| return eval_adata | |
| def eval_scib_metrics_simple( | |
| adata: AnnData, | |
| batch_key: str, | |
| label_key: str, | |
| embedding_key: str, | |
| ) -> Dict[str, float]: | |
| eval_adata = prepare_eval_adata( | |
| adata, | |
| batch_key, | |
| label_key, | |
| embedding_key, | |
| build_neighbors=False, | |
| ) | |
| results_dict: Dict[str, float] = {} | |
| if len(eval_adata.obs[batch_key].unique()) > 1: | |
| results_dict["ASW_label/batch"] = scib.metrics.silhouette_batch( | |
| eval_adata, | |
| batch_key, | |
| label_key, | |
| embed=embedding_key, | |
| metric="euclidean", | |
| return_all=False, | |
| verbose=False, | |
| ) | |
| results_dict = {key: value for key, value in results_dict.items() if not np.isnan(value)} | |
| return results_dict | |
| def eval_scib_metrics_scgpt( | |
| adata: AnnData, | |
| batch_key: str, | |
| label_key: str, | |
| embedding_key: str, | |
| ) -> Dict[str, float]: | |
| eval_adata = prepare_eval_adata( | |
| adata, | |
| batch_key, | |
| label_key, | |
| embedding_key, | |
| build_neighbors=True, | |
| ) | |
| results = scib.metrics.metrics( | |
| eval_adata, | |
| adata_int=eval_adata, | |
| batch_key=batch_key, | |
| label_key=label_key, | |
| embed=embedding_key, | |
| isolated_labels_asw_=False, | |
| silhouette_=True, | |
| hvg_score_=False, | |
| graph_conn_=True, | |
| pcr_=True, | |
| isolated_labels_f1_=False, | |
| trajectory_=False, | |
| nmi_=True, | |
| ari_=True, | |
| cell_cycle_=False, | |
| kBET_=False, | |
| ilisi_=False, | |
| clisi_=False, | |
| ) | |
| result_dict = results[0].to_dict() | |
| result_dict["avg_bio"] = np.mean( | |
| [ | |
| result_dict["NMI_cluster/label"], | |
| result_dict["ARI_cluster/label"], | |
| result_dict["ASW_label"], | |
| ] | |
| ) | |
| result_dict["avg_batch"] = np.mean( | |
| [ | |
| result_dict["graph_conn"], | |
| result_dict["ASW_label/batch"], | |
| ] | |
| ) | |
| result_dict = {key: value for key, value in result_dict.items() if not np.isnan(value)} | |
| return result_dict | |
| def evaluate( | |
| adata: AnnData, | |
| batch_key: str, | |
| label_keys: Sequence[str], | |
| embedding_key: str, | |
| metrics_mode: str, | |
| res_path: str, | |
| ) -> pd.DataFrame: | |
| met_df = pd.DataFrame(columns=["metric", "label", "value"]) | |
| unique_label_keys: List[str] = [] | |
| for key in label_keys: | |
| if key not in unique_label_keys: | |
| unique_label_keys.append(key) | |
| if not unique_label_keys: | |
| raise ValueError("No label keys were provided for evaluation") | |
| for label_key in unique_label_keys: | |
| if metrics_mode == "scgpt": | |
| metrics = eval_scib_metrics_scgpt( | |
| adata=adata, | |
| batch_key=batch_key, | |
| label_key=label_key, | |
| embedding_key=embedding_key, | |
| ) | |
| elif metrics_mode == "simple": | |
| metrics = eval_scib_metrics_simple( | |
| adata=adata, | |
| batch_key=batch_key, | |
| label_key=label_key, | |
| embedding_key=embedding_key, | |
| ) | |
| else: | |
| raise ValueError(f"Unknown metrics_mode: {metrics_mode}") | |
| print_sys( | |
| "\n".join( | |
| [f"[{label_key}] {metric_name}: {metric_value:.4f}" for metric_name, metric_value in metrics.items()] | |
| ) | |
| ) | |
| for metric_name, metric_value in metrics.items(): | |
| met_df.loc[len(met_df)] = [metric_name, label_key, metric_value] | |
| met_df.to_csv(res_path, index=False) | |
| np.save(f"{res_path[:-4]}_ce.npy", adata.obsm[embedding_key]) | |
| return met_df | |
| def resolve_with_preset( | |
| cli_value: Optional[object], | |
| preset_value: Optional[object], | |
| ) -> Optional[object]: | |
| return cli_value if cli_value is not None else preset_value | |
| def resolve_batch_key(adata: AnnData, requested_batch_key: Optional[str]) -> str: | |
| if requested_batch_key is not None: | |
| if requested_batch_key not in adata.obs.columns: | |
| raise KeyError(f"batch_key '{requested_batch_key}' not found in adata.obs.columns") | |
| return requested_batch_key | |
| fallback_keys = ["batch", "str_batch", "sample", "Sample Characteristic[individual]"] | |
| for key in fallback_keys: | |
| if key in adata.obs.columns: | |
| print_sys(f"Auto-selected batch_key='{key}'") | |
| return key | |
| raise ValueError( | |
| "Unable to infer batch_key. Please pass --batch_key explicitly." | |
| ) | |
| def resolve_label_keys(adata: AnnData, requested_label_keys: Optional[Sequence[str]]) -> List[str]: | |
| if requested_label_keys: | |
| missing_keys = [key for key in requested_label_keys if key not in adata.obs.columns] | |
| if missing_keys: | |
| raise KeyError(f"label_key(s) not found in adata.obs.columns: {missing_keys}") | |
| return list(requested_label_keys) | |
| fallback_keys = ["celltype", "cell_type", "celltype_original"] | |
| resolved_keys = [key for key in fallback_keys if key in adata.obs.columns] | |
| if resolved_keys: | |
| print_sys(f"Auto-selected label_keys={resolved_keys}") | |
| return resolved_keys | |
| raise ValueError( | |
| "Unable to infer label_key. Please pass --label_key explicitly." | |
| ) | |
| def reindex_tensor_universal( | |
| tensor: torch.Tensor, | |
| index_positions: Sequence[int], | |
| dim: int, | |
| filler: str = "0", | |
| device: Union[str, torch.device] = "cpu", | |
| ) -> torch.Tensor: | |
| index_tensor = torch.tensor(index_positions, device=device) | |
| tensor_shape = list(tensor.shape) | |
| tensor_shape[dim] = len(index_positions) | |
| index_shape = [1] * len(tensor_shape) | |
| index_shape[dim] = len(index_positions) | |
| index_tensor = index_tensor.view(*index_shape).expand(*tensor_shape) | |
| if filler == "0": | |
| padder_shape = deepcopy(tensor_shape) | |
| padder_shape[dim] = 1 | |
| padder = torch.zeros(padder_shape, dtype=tensor.dtype, device=device) | |
| elif filler == "mean": | |
| padder = tensor.mean(dim=dim, keepdim=True) | |
| else: | |
| raise ValueError("filler should be 0 or mean") | |
| expanded_tensor = torch.cat((tensor, padder), dim=dim) | |
| return torch.gather(expanded_tensor, dim, index_tensor) | |
| project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| sys.path.insert(0, project_root) | |
| sys.path.insert(0, os.path.dirname(project_root)) | |
| biomod_dir = project_root | |
| def load_biomod_components(): | |
| models_path = os.path.join(biomod_dir, "models_biomod.py") | |
| if not os.path.exists(models_path): | |
| raise FileNotFoundError(f"Model file not found: {models_path}") | |
| models_spec = importlib.util.spec_from_file_location("models_biomod", models_path) | |
| models_biomod = importlib.util.module_from_spec(models_spec) | |
| models_spec.loader.exec_module(models_biomod) | |
| dataset_path = os.path.join(biomod_dir, "biomod_dataset.py") | |
| if not os.path.exists(dataset_path): | |
| raise FileNotFoundError(f"Dataset file not found: {dataset_path}") | |
| dataset_spec = importlib.util.spec_from_file_location("biomod_dataset", dataset_path) | |
| biomod_dataset = importlib.util.module_from_spec(dataset_spec) | |
| dataset_spec.loader.exec_module(biomod_dataset) | |
| return ( | |
| models_biomod.BioMod, | |
| models_biomod.sanitize_biomod_config, | |
| biomod_dataset.load_biomod_data, | |
| ) | |
| OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "res") | |
| os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--gene2vec_path", type=str, default=str(ASSET_DIR / "selected_gene2vec_27k.npy"), | |
| help="Path to gene2vec file") | |
| parser.add_argument("--scfm_hyper_params_path", type=str, required=True, | |
| help="Path to the matching BioMod model configuration pkl file") | |
| parser.add_argument("--scfm_ckpt_path", type=str, required=True, | |
| help="Path to the BioMod model weights") | |
| parser.add_argument("--biomod_data_path", "--module_data_path", type=str, default=None, | |
| help="Path to module JSON file (single file)", dest='module_data_path') | |
| parser.add_argument("--biomod_data_dir", "--module_data_dir", type=str, default=None, | |
| help="Path to directory containing module JSON files", dest='module_data_dir') | |
| parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_DIR / "gene2ens.csv"), | |
| help="Path to gene symbol to Ensembl ID mapping CSV") | |
| parser.add_argument("--scfm_genes_file", type=str, default=str(ASSET_DIR / "selected_genes_27k.txt"), | |
| help="Path to scfm genes list file") | |
| parser.add_argument("--data_path", type=str, required=True, | |
| help="Path to input h5ad file") | |
| parser.add_argument("--dataset_preset", type=str, default="auto", choices=sorted(DATASET_PRESETS.keys()), | |
| help="Dataset preset for batch/label/gene column defaults") | |
| parser.add_argument("--gene_col", type=str, default=None, | |
| help="Column in adata.var used as the input gene symbol column. " | |
| "If not set, use adata.var.index.") | |
| parser.add_argument("--batch_key", type=str, default=None, | |
| help="Column in adata.obs used as the batch label") | |
| parser.add_argument("--label_key", nargs="+", default=None, | |
| help="One or more columns in adata.obs used as evaluation labels") | |
| parser.add_argument("--output_key", type=str, default="X_sclong_module", | |
| help="Key name for storing embeddings in adata.obsm") | |
| parser.add_argument("--pool_method", type=str, default="auto", choices=["auto", "mean", "cls", "max", "first_module"], | |
| help="Cell embedding pooling. auto prefers cls_encoding when the checkpoint has a trained CLS token; " | |
| "first_module uses the first module after restoring original module order.") | |
| parser.add_argument("--n_cells", type=str, default="all", | |
| help="Number of cells to process (default: all)") | |
| parser.add_argument("--batch_size", type=int, default=4, | |
| help="Batch size for inference") | |
| parser.add_argument("--output_dir", type=str, default=OUTPUT_DIR, | |
| help="Output directory for results") | |
| parser.add_argument("--metrics_mode", type=str, default="scgpt", choices=["scgpt", "simple"], | |
| help="Evaluation metrics mode. Default uses the scGPT tutorial metric bundle.") | |
| parser.add_argument("--min_genes_per_cell", type=int, default=0, | |
| help="Optional Scanpy cell filter. Default 0 keeps all cells.") | |
| parser.add_argument("--min_cells_per_gene", type=int, default=0, | |
| help="Optional Scanpy gene filter. Default 0 keeps all genes.") | |
| parser.add_argument("--shuffle_biomod_genes", "--shuffle_module_genes", action="store_true", | |
| help="Optional downstream-only extra reshuffle on top of module_sets restored from PKL. " | |
| "Do not pass this when reproducing the pretrained backbone as-is.", dest='shuffle_module_genes') | |
| parser.add_argument("--shuffle_seed", type=int, default=2021, | |
| help="Random seed for the downstream extra reshuffle when --shuffle_biomod_genes is enabled.") | |
| parser.add_argument("--save_raw_biomod_emb", "--save_raw_module_emb", action="store_true", | |
| help="Save raw module_encodings with shape (num_cells, num_modules, embed_dim)", dest='save_raw_module_emb') | |
| args = parser.parse_args() | |
| os.makedirs(args.output_dir, exist_ok=True) | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| print_sys(f"Using device: {device}") | |
| preset_config = DATASET_PRESETS[args.dataset_preset] | |
| resolved_gene_col = resolve_with_preset(args.gene_col, preset_config.get("gene_col")) | |
| resolved_batch_key = resolve_with_preset(args.batch_key, preset_config.get("batch_key")) | |
| resolved_label_keys = resolve_with_preset(args.label_key, preset_config.get("label_keys")) | |
| print_sys( | |
| f"dataset_preset={args.dataset_preset}, " | |
| f"gene_col={resolved_gene_col}, batch_key={resolved_batch_key}, " | |
| f"label_keys={resolved_label_keys}, metrics_mode={args.metrics_mode}, " | |
| f"pool_method={args.pool_method}" | |
| ) | |
| BioMod, sanitize_biomod_config, load_biomod_data = load_biomod_components() | |
| print_sys(f"Loading hyper parameters from {args.scfm_hyper_params_path}") | |
| with open(args.scfm_hyper_params_path, "rb") as handle: | |
| scfm_hyper_params = pickle.load(handle) | |
| scfm_hyper_params.pop("module_embed_chunk_padded_gene_budget", None) | |
| scfm_hyper_params = sanitize_biomod_config( | |
| scfm_hyper_params, | |
| source=args.scfm_hyper_params_path, | |
| log_fn=print_sys, | |
| ) | |
| scfm_hyper_params["gene2vec_file"] = args.gene2vec_path | |
| # Read fusion_method from pkl to ensure consistency with pre-training | |
| fusion_method = scfm_hyper_params.get("fusion_method", "multiply") | |
| print_sys(f"Using fusion_method from pkl: {fusion_method}") | |
| if "module_sets" not in scfm_hyper_params or scfm_hyper_params.get("module_sets") is None: | |
| if args.module_data_path or args.module_data_dir: | |
| print_sys("Loading module data...") | |
| module_sets = load_biomod_data( | |
| module_data_path=args.module_data_path, | |
| module_data_dir=args.module_data_dir, | |
| gene_list_path=args.scfm_genes_file, | |
| gene_mapping_path=args.gene_mapping_path, | |
| verbose=True, | |
| ) | |
| scfm_hyper_params["module_sets"] = module_sets | |
| else: | |
| raise ValueError("Model requires module_sets, but neither --biomod_data_path nor --biomod_data_dir was provided") | |
| else: | |
| module_sets = scfm_hyper_params["module_sets"] | |
| print_sys(f"Using module_sets from hyper parameters ({len(module_sets)} modules)") | |
| if args.shuffle_module_genes: | |
| import random | |
| print_sys( | |
| f"Applying downstream-only extra module reshuffle (seed={args.shuffle_seed}). " | |
| f"Pretraining-time module_sets from PKL will be reshuffled again." | |
| ) | |
| all_gene_indices: List[int] = [] | |
| module_sizes: List[int] = [] | |
| for module in module_sets: | |
| all_gene_indices.extend(module) | |
| module_sizes.append(len(module)) | |
| rng = random.Random(args.shuffle_seed) | |
| rng.shuffle(all_gene_indices) | |
| shuffled_module_sets = [] | |
| start_idx = 0 | |
| for size in module_sizes: | |
| shuffled_module_sets.append(all_gene_indices[start_idx:start_idx + size]) | |
| start_idx += size | |
| module_sets = shuffled_module_sets | |
| scfm_hyper_params["module_sets"] = module_sets | |
| print_sys(f"Shuffled {len(all_gene_indices)} genes across {len(module_sets)} modules") | |
| print_sys(f"Model hyper parameters: {list(scfm_hyper_params.keys())}") | |
| pkl_use_gene2vec = scfm_hyper_params.get("use_gene2vec", True) | |
| scfm_hyper_params["use_gene2vec"] = pkl_use_gene2vec | |
| print_sys(f"use_gene2vec={pkl_use_gene2vec} (from PKL)") | |
| print_sys("Creating BioMod model...") | |
| model = BioMod(**scfm_hyper_params).to(device) | |
| print_sys(f"Loading checkpoint from {args.scfm_ckpt_path}") | |
| ckpt = torch.load(args.scfm_ckpt_path, map_location=device) | |
| model_state_dict = ckpt.get("model_state_dict", ckpt) | |
| filtered_state_dict = { | |
| key: value | |
| for key, value in model_state_dict.items() | |
| if "go_conv" not in key and "G_go" not in key.lower() | |
| } | |
| model.load_state_dict(filtered_state_dict, strict=True) | |
| for parameter in model.parameters(): | |
| parameter.requires_grad = False | |
| model.eval() | |
| all_params = sum(parameter.numel() for parameter in model.parameters()) | |
| trainable_params = sum(parameter.numel() for parameter in model.parameters() if parameter.requires_grad) | |
| print_sys(f"Total model parameters: {all_params:,}") | |
| print_sys(f"Trainable parameters during zero-shot: {trainable_params:,}") | |
| gene2ens = pd.read_csv(args.gene_mapping_path, index_col=0) | |
| symbol_to_ensembl = gene2ens["Ensembl gene ID"].to_dict() | |
| with open(args.scfm_genes_file, "r") as handle: | |
| scfm_genes = [line.rstrip("\n") for line in handle.readlines()] | |
| scfm_gene_set = set(scfm_genes) | |
| print_sys(f"Loading data from {args.data_path}") | |
| adata = sc.read_h5ad(args.data_path) | |
| if args.min_genes_per_cell > 0: | |
| before_cells = adata.n_obs | |
| sc.pp.filter_cells(adata, min_genes=args.min_genes_per_cell) | |
| print_sys(f"Filtered cells by min_genes={args.min_genes_per_cell}: {before_cells} -> {adata.n_obs}") | |
| if args.min_cells_per_gene > 0: | |
| before_genes = adata.n_vars | |
| sc.pp.filter_genes(adata, min_cells=args.min_cells_per_gene) | |
| print_sys(f"Filtered genes by min_cells={args.min_cells_per_gene}: {before_genes} -> {adata.n_vars}") | |
| if args.n_cells == "all": | |
| n_cells = adata.n_obs | |
| else: | |
| n_cells = int(args.n_cells) | |
| if adata.n_obs > n_cells: | |
| print_sys(f"Subsampling to {n_cells} cells") | |
| sc.pp.subsample(adata, n_obs=n_cells, copy=False) | |
| n_cells = adata.n_obs | |
| batch_key = resolve_batch_key(adata, resolved_batch_key) | |
| label_keys = resolve_label_keys(adata, resolved_label_keys) | |
| print_sys(f"Processing {n_cells} cells with {adata.n_vars} genes") | |
| print_sys(f"Evaluation batch_key={batch_key}, label_keys={label_keys}") | |
| input_gene_ids = resolve_input_gene_ids( | |
| adata=adata, | |
| gene_col=resolved_gene_col, | |
| symbol_to_ensembl=symbol_to_ensembl, | |
| target_gene_set=scfm_gene_set, | |
| ) | |
| input_mapping = {gene_id: index for index, gene_id in enumerate(input_gene_ids)} | |
| input_gene_num = len(input_gene_ids) | |
| intersect_genes = np.intersect1d(input_gene_ids, scfm_genes) | |
| print_sys(f"Input genes and scfm genes intersect: {len(intersect_genes)} genes") | |
| scfm_genes_pad = scfm_genes + ["PAD"] | |
| scfm_seq_len = len(scfm_genes_pad) | |
| scfm_mapping = {gene_id: index for index, gene_id in enumerate(scfm_genes_pad)} | |
| scfm_index_positions = [input_mapping.get(gene_id, input_gene_num) for gene_id in scfm_genes_pad] | |
| _ = [scfm_mapping.get(gene_id, scfm_seq_len) for gene_id in input_gene_ids] | |
| print_sys("Extracting cell embeddings...") | |
| cell_embeddings = [] | |
| raw_module_embeddings = [] if args.save_raw_module_emb else None | |
| embedding_key = args.output_key | |
| with torch.no_grad(): | |
| for start in tqdm(range(0, n_cells, args.batch_size)): | |
| batch_end = min(start + args.batch_size, n_cells) | |
| batch_matrix = adata.X[start:batch_end, :] | |
| if hasattr(batch_matrix, "toarray"): | |
| batch_matrix = batch_matrix.toarray() | |
| x = torch.tensor(batch_matrix, dtype=torch.float32).to(device) | |
| x_scfm = reindex_tensor_universal(x, scfm_index_positions, dim=1, filler="0", device=device) | |
| out = model(x_scfm, mask_ratio=0.0, return_encodings=True) | |
| if "module_encodings" not in out: | |
| raise ValueError(f"Expected 'module_encodings' in output, got: {list(out.keys())}") | |
| module_encodings = out["module_encodings"] | |
| ids_restore = out.get("ids_restore") | |
| if args.pool_method == "cls": | |
| if "cls_encoding" not in out: | |
| print_sys("Warning: pool_method='cls' but checkpoint has no cls_encoding, using mean pooling instead") | |
| cell_emb = module_encodings.mean(dim=1) | |
| else: | |
| cell_emb = out["cls_encoding"] | |
| elif args.pool_method == "mean": | |
| cell_emb = module_encodings.mean(dim=1) | |
| elif args.pool_method == "max": | |
| cell_emb = module_encodings.max(dim=1)[0] | |
| elif args.pool_method == "first_module": | |
| if ids_restore is None: | |
| raise ValueError("pool_method='first_module' requires ids_restore in model outputs") | |
| restore_index = ids_restore.unsqueeze(-1).expand(-1, -1, module_encodings.shape[-1]) | |
| restored_module_encodings = torch.gather(module_encodings, dim=1, index=restore_index) | |
| cell_emb = restored_module_encodings[:, 0, :] | |
| else: | |
| cell_emb = out["cls_encoding"] if "cls_encoding" in out else module_encodings.mean(dim=1) | |
| if args.save_raw_module_emb: | |
| raw_module_embeddings.append(module_encodings.cpu().numpy()) | |
| cell_embeddings.append(cell_emb.cpu().numpy()) | |
| cell_embeddings = np.concatenate(cell_embeddings, axis=0) | |
| print_sys(f"Cell embeddings shape: {cell_embeddings.shape}") | |
| if args.save_raw_module_emb and raw_module_embeddings is not None: | |
| raw_module_embeddings = np.concatenate(raw_module_embeddings, axis=0) | |
| print_sys(f"Raw module embeddings shape: {raw_module_embeddings.shape}") | |
| raw_module_path = os.path.join(args.output_dir, "sclong_module_raw_module_emb.npy") | |
| np.save(raw_module_path, raw_module_embeddings) | |
| print_sys(f"Raw module embeddings saved to {raw_module_path}") | |
| adata.obsm[embedding_key] = cell_embeddings | |
| print_sys("Evaluating batch correction...") | |
| res = evaluate( | |
| adata=adata, | |
| batch_key=batch_key, | |
| label_keys=label_keys, | |
| embedding_key=embedding_key, | |
| metrics_mode=args.metrics_mode, | |
| res_path=os.path.join(args.output_dir, "sclong_module_batch_cell_emb_mode.csv"), | |
| ) | |
| print_sys("\nEvaluation Results:") | |
| print_sys("=" * 50) | |
| print_sys(res.to_string(index=False)) | |
| print_sys("=" * 50) | |
| np.save(os.path.join(args.output_dir, "sclong_module_batch_cell_emb_mode_ce.npy"), cell_embeddings) | |
| print_sys( | |
| f"Cell embeddings saved to " | |
| f"{os.path.join(args.output_dir, 'sclong_module_batch_cell_emb_mode_ce.npy')}" | |
| ) | |
| print_sys("Zero-shot evaluation completed!") | |