cellucid 0.0.1a0__py3-none-any.whl
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.
- cellucid/__init__.py +12 -0
- cellucid/anndata_variations.py +149 -0
- cellucid/prepare_data.py +960 -0
- cellucid/repo_to_text.py +186 -0
- cellucid-0.0.1a0.dist-info/METADATA +105 -0
- cellucid-0.0.1a0.dist-info/RECORD +8 -0
- cellucid-0.0.1a0.dist-info/WHEEL +5 -0
- cellucid-0.0.1a0.dist-info/top_level.txt +1 -0
cellucid/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Utilities for exporting AnnData/array data for the 3D UMAP web viewer."""
|
|
2
|
+
|
|
3
|
+
from importlib import metadata as _metadata
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = _metadata.version("cellucid")
|
|
7
|
+
except _metadata.PackageNotFoundError: # pragma: no cover - handled in dev installs
|
|
8
|
+
__version__ = "0.0.0"
|
|
9
|
+
|
|
10
|
+
from .prepare_data import export_data_for_web
|
|
11
|
+
|
|
12
|
+
__all__ = ["export_data_for_web", "__version__"]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import itertools
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import scanpy as sc
|
|
9
|
+
except ImportError as exc: # pragma: no cover - optional dependency
|
|
10
|
+
raise ImportError(
|
|
11
|
+
"scanpy is required for cellucid.anndata_variations; install with "
|
|
12
|
+
"`pip install cellucid[analysis]` or add scanpy to your environment."
|
|
13
|
+
) from exc
|
|
14
|
+
|
|
15
|
+
def _find_repo_root() -> Path:
|
|
16
|
+
"""Locate the python project root (contains pyproject.toml and data/)."""
|
|
17
|
+
for candidate in Path(__file__).resolve().parents:
|
|
18
|
+
if (candidate / "pyproject.toml").exists() and (candidate / "data").exists():
|
|
19
|
+
return candidate
|
|
20
|
+
return Path.cwd()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
REPO_ROOT = _find_repo_root()
|
|
24
|
+
RAW_DATA_DIR = REPO_ROOT / "data" / "raw"
|
|
25
|
+
EXPERIMENT_DIR = REPO_ROOT / "data" / "experiments"
|
|
26
|
+
|
|
27
|
+
INPUT_H5AD = RAW_DATA_DIR / "adata_latent_scvi-adata_unified_20250925_001_integration-supergpu02.scidom.de-bb886d36409e78a3.h5ad"
|
|
28
|
+
METADATA_PICKLE = RAW_DATA_DIR / "adata_obs_extracted.pickle"
|
|
29
|
+
OUTPUT_DIR = EXPERIMENT_DIR / "umap_parameter_sweep"
|
|
30
|
+
|
|
31
|
+
MIN_DISTS = [0.1, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50]
|
|
32
|
+
N_NEIGHBORS_LIST = [5, 10, 15, 30, 50, 90, 120]
|
|
33
|
+
RANDOM_SEED = 42
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _print_setup():
|
|
37
|
+
"""Log resolved paths so it's obvious what is being used."""
|
|
38
|
+
print("==== AnnData UMAP sweep setup ====")
|
|
39
|
+
print(f"Current working dir : {Path.cwd()}")
|
|
40
|
+
print(f"Repo root : {REPO_ROOT}")
|
|
41
|
+
print(f"Input h5ad : {INPUT_H5AD} (exists: {INPUT_H5AD.exists()})")
|
|
42
|
+
print(f"Metadata pickle : {METADATA_PICKLE} (exists: {METADATA_PICKLE.exists()})")
|
|
43
|
+
print(f"Output directory : {OUTPUT_DIR}")
|
|
44
|
+
print("==================================")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def validate_inputs() -> None:
|
|
48
|
+
"""Ensure required files and folders exist before heavy work."""
|
|
49
|
+
missing = [path for path in (INPUT_H5AD, METADATA_PICKLE) if not path.exists()]
|
|
50
|
+
if missing:
|
|
51
|
+
missing_list = "\n - ".join(str(path) for path in missing)
|
|
52
|
+
raise FileNotFoundError(f"Missing required input files:\n - {missing_list}")
|
|
53
|
+
|
|
54
|
+
EXPERIMENT_DIR.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_and_preprocess_data():
|
|
60
|
+
validate_inputs()
|
|
61
|
+
print("Loading and preparing data (this happens only once)...")
|
|
62
|
+
adata = sc.read_h5ad(INPUT_H5AD)
|
|
63
|
+
adata_complete = pd.read_pickle(METADATA_PICKLE)
|
|
64
|
+
|
|
65
|
+
if "qc_fail" not in adata_complete.columns:
|
|
66
|
+
raise KeyError("Metadata pickle missing required column 'qc_fail'.")
|
|
67
|
+
|
|
68
|
+
missing_cells = set(adata.obs_names) - set(adata_complete.index)
|
|
69
|
+
if missing_cells:
|
|
70
|
+
raise ValueError(f"Metadata missing {len(missing_cells)} cells (e.g., {next(iter(missing_cells))}).")
|
|
71
|
+
|
|
72
|
+
adata_complete = adata_complete.loc[adata.obs_names]
|
|
73
|
+
qc_mask = ~adata_complete["qc_fail"].fillna(False)
|
|
74
|
+
|
|
75
|
+
# Apply QC filtering to both the matrix and obs
|
|
76
|
+
adata = adata[qc_mask].copy()
|
|
77
|
+
adata_complete = adata_complete.loc[qc_mask]
|
|
78
|
+
adata.obs = adata_complete
|
|
79
|
+
|
|
80
|
+
# Drop heavy derived fields to reduce per-run memory
|
|
81
|
+
for container in (adata.obsm, adata.obsp):
|
|
82
|
+
for key in list(container.keys()):
|
|
83
|
+
del container[key]
|
|
84
|
+
adata.uns.clear()
|
|
85
|
+
|
|
86
|
+
print(f"Data prepared. Final: {adata}")
|
|
87
|
+
print(f"Cells kept after QC : {adata.n_obs}")
|
|
88
|
+
print(f"Features : {adata.n_vars}")
|
|
89
|
+
return adata
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def run_parameter_sweep(_adata):
|
|
93
|
+
"""
|
|
94
|
+
Iterates through parameters, calculating UMAPs and saving results.
|
|
95
|
+
"""
|
|
96
|
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
param_combinations = list(itertools.product(N_NEIGHBORS_LIST, MIN_DISTS))
|
|
98
|
+
print(f"Starting sweep over {len(param_combinations)} combinations...")
|
|
99
|
+
print(f"Writing outputs to: {OUTPUT_DIR}")
|
|
100
|
+
|
|
101
|
+
existing_before = 0
|
|
102
|
+
for k, min_dist in param_combinations:
|
|
103
|
+
dist_str = f"{min_dist:.2f}".replace('.', '_')
|
|
104
|
+
filename = OUTPUT_DIR / f"adata|k__{k}|min_dist_{dist_str}.h5ad"
|
|
105
|
+
if filename.exists():
|
|
106
|
+
existing_before += 1
|
|
107
|
+
to_compute = len(param_combinations) - existing_before
|
|
108
|
+
print(f"Found {existing_before} outputs already present; {to_compute} will be computed.")
|
|
109
|
+
|
|
110
|
+
if to_compute == 0:
|
|
111
|
+
print("All combinations already exist. Nothing to do.")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
skipped = 0
|
|
115
|
+
created = 0
|
|
116
|
+
|
|
117
|
+
for k, min_dist in tqdm(param_combinations, desc="Processing"):
|
|
118
|
+
dist_str = f"{min_dist:.2f}".replace('.', '_')
|
|
119
|
+
filename = OUTPUT_DIR / f"adata|k__{k}|min_dist_{dist_str}.h5ad"
|
|
120
|
+
if filename.exists():
|
|
121
|
+
skipped += 1
|
|
122
|
+
tqdm.write(f"Exists, skipping: {filename.name}")
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
adata = _adata.copy()
|
|
126
|
+
try:
|
|
127
|
+
sc.pp.neighbors(adata, n_neighbors=k, random_state=RANDOM_SEED)
|
|
128
|
+
sc.tl.umap(
|
|
129
|
+
adata,
|
|
130
|
+
n_components=3,
|
|
131
|
+
min_dist=min_dist,
|
|
132
|
+
random_state=RANDOM_SEED,
|
|
133
|
+
)
|
|
134
|
+
adata.write_h5ad(filename)
|
|
135
|
+
created += 1
|
|
136
|
+
except Exception as e:
|
|
137
|
+
tqdm.write(f"Calculation error for {filename.name}: {e}")
|
|
138
|
+
gc.collect()
|
|
139
|
+
|
|
140
|
+
print(f"Finished sweep. Created {created}, skipped {skipped}, total {len(param_combinations)}.")
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
try:
|
|
144
|
+
_print_setup()
|
|
145
|
+
clean_adata = load_and_preprocess_data()
|
|
146
|
+
run_parameter_sweep(clean_adata)
|
|
147
|
+
print("Processing complete.")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print(f"An error occurred: {e}")
|