beiken1201 commited on
Commit
999e5eb
·
verified ·
1 Parent(s): e8c145d

Fix downstream notebook project paths and unify tutorial links

Browse files

Update the five downstream notebooks to resolve paths from the repository layout, use the bundled BioMod checkpoint paths and supported commands, and read actual task outputs. Keep training and evaluation opt-in. Standardize the main README tutorial column to Notebook links. This commit only changes these six files; model weights, paired configuration, datasets, and training source are unchanged.

BioMod/toturials/cell_annotation.ipynb CHANGED
@@ -1,338 +1,331 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "markdown",
5
- "metadata": {},
6
- "source": [
7
- "# Cell annotation demo: Zheng68K with weight0407 on one GPU\n",
8
- "\n",
9
- "This notebook is a one-GPU tutorial version of the Zheng68K cell-annotation launcher. It follows the parameter style of `weight0526module1/cell_annotaion/zheng68K/start.sh`, but uses the same weight0407 backbone as the zero-shot batch-integration demo."
10
- ]
11
- },
12
- {
13
- "cell_type": "markdown",
14
- "metadata": {},
15
- "source": [
16
- "## Privacy-safe absolute paths\n",
17
- "\n",
18
- "The notebook uses a public-looking alias path rather than a personal project path:\n",
19
- "\n",
20
- "`/data/share/biomodulefm_demo/single_cell_model`\n",
21
- "\n",
22
- "All paths below are absolute and resolve through that alias on the server."
23
- ]
24
- },
25
- {
26
- "cell_type": "markdown",
27
- "metadata": {},
28
- "source": [
29
- "## What this demo runs\n",
30
- "\n",
31
- "- Dataset: `/data/share/biomodulefm_demo/single_cell_model/DATA/singcell/Zheng68K.h5ad`\n",
32
- "- Cell annotation script: `/data/jianqingzhu/BioMod-main/BioMod/cell_annotation/performer_module_finetune.py`\n",
33
- "- Model config: `/data/share/biomodulefm_demo/single_cell_model/weight/0407weight/mae_tost_48M_cross_attn_only_pure_memory.pkl`\n",
34
- "- Model checkpoint: `/data/share/biomodulefm_demo/single_cell_model/weight/0407weight/mae_tost_48M_cross_attn_only_pure_memory_epoch12.pth`\n",
35
- "- Output directory: `/data/share/biomodulefm_demo/tutorial_runs/cell_annotation_zheng68k_weight0407_1gpu_demo_smoke`\n",
36
- "\n",
37
- "Run this notebook with the `sclong` Python environment/kernel. The command below is single-GPU: it sets `CUDA_VISIBLE_DEVICES=0` and runs `python`, not 4-process `torchrun`."
38
- ]
39
- },
40
- {
41
- "cell_type": "code",
42
- "execution_count": null,
43
- "metadata": {},
44
- "outputs": [],
45
- "source": [
46
- "from pathlib import Path\n",
47
- "import json\n",
48
- "import os\n",
49
- "import subprocess\n",
50
- "import sys\n",
51
- "try:\n",
52
- " from IPython.display import display\n",
53
- "except Exception:\n",
54
- " def display(obj):\n",
55
- " print(obj)\n",
56
- "import pandas as pd\n",
57
- "\n",
58
- "BASE = Path('/data/share/biomodulefm_demo/single_cell_model')\n",
59
- "MODULE_MODEL = BASE / 'module_model'\n",
60
- "PROJECT_ROOT = Path('/data/jianqingzhu/BioMod-main/BioMod')\n",
61
- "\n",
62
- "SCRIPT = PROJECT_ROOT / 'cell_annotation' / 'performer_module_finetune.py'\n",
63
- "DATA_PATH = BASE / 'DATA' / 'singcell' / 'Zheng68K.h5ad'\n",
64
- "\n",
65
- "# Use the same weight0407 backbone as the zero-shot batch-integration demo.\n",
66
- "CONFIG_PKL = BASE / 'weight' / '0407weight' / 'mae_tost_48M_cross_attn_only_pure_memory.pkl'\n",
67
- "CKPT_PATH = BASE / 'weight' / '0407weight' / 'mae_tost_48M_cross_attn_only_pure_memory_epoch12.pth'\n",
68
- "\n",
69
- "MODULE_DATA_DIR = PROJECT_ROOT / 'module_data_all'\n",
70
- "GENE_LIST = MODULE_MODEL / 'selected_genes_27k.txt'\n",
71
- "GENE_MAP = MODULE_MODEL / 'gene2ens.csv'\n",
72
- "GENE2VEC = MODULE_MODEL / 'selected_gene2vec_27k.npy'\n",
73
- "\n",
74
- "OUTPUT_PARENT = Path('/data/share/biomodulefm_demo/tutorial_runs')\n",
75
- "RUN_NAME = 'cell_annotation_zheng68k_weight0407_1gpu_demo_smoke'\n",
76
- "RUN_DIR = OUTPUT_PARENT / RUN_NAME\n",
77
- "RUN_DIR.mkdir(parents=True, exist_ok=True)\n",
78
- "\n",
79
- "print('Python:', sys.executable)\n",
80
- "print('Run directory:', RUN_DIR)"
81
- ]
82
- },
83
- {
84
- "cell_type": "code",
85
- "execution_count": null,
86
- "metadata": {},
87
- "outputs": [],
88
- "source": [
89
- "required_files = [\n",
90
- " SCRIPT,\n",
91
- " DATA_PATH,\n",
92
- " CONFIG_PKL,\n",
93
- " CKPT_PATH,\n",
94
- " GENE_LIST,\n",
95
- " GENE_MAP,\n",
96
- " GENE2VEC,\n",
97
- "]\n",
98
- "required_dirs = [PROJECT_ROOT, MODULE_DATA_DIR]\n",
99
- "\n",
100
- "missing = [str(p) for p in required_files if not p.is_file()] + [str(p) for p in required_dirs if not p.is_dir()]\n",
101
- "if missing:\n",
102
- " raise FileNotFoundError('Missing required paths:\\n' + '\\n'.join(missing))\n",
103
- "\n",
104
- "for p in required_files:\n",
105
- " print('[FOUND FILE]', p)\n",
106
- "for p in required_dirs:\n",
107
- " print('[FOUND DIR ]', p)"
108
- ]
109
- },
110
- {
111
- "cell_type": "code",
112
- "execution_count": null,
113
- "metadata": {},
114
- "outputs": [],
115
- "source": [
116
- "# Single-GPU runtime settings.\n",
117
- "os.environ['PYTHONUNBUFFERED'] = '1'\n",
118
- "os.environ['CUDA_VISIBLE_DEVICES'] = '0'\n",
119
- "os.environ.setdefault('OMP_NUM_THREADS', '8')\n",
120
- "os.environ.setdefault('NCCL_DEBUG', 'WARN')\n",
121
- "os.environ.setdefault('NUMBA_CACHE_DIR', f'/tmp/numba_cache_cell_annotation_demo_{os.environ.get(\"USER\", \"unknown\")}_{os.getpid()}')\n",
122
- "Path(os.environ['NUMBA_CACHE_DIR']).mkdir(parents=True, exist_ok=True)\n",
123
- "\n",
124
- "pythonpath_parts = [str(MODULE_MODEL), str(PROJECT_ROOT), str(PROJECT_ROOT.parent / 'model_dependencies')]\n",
125
- "if os.environ.get('PYTHONPATH'):\n",
126
- " pythonpath_parts.append(os.environ['PYTHONPATH'])\n",
127
- "os.environ['PYTHONPATH'] = ':'.join(pythonpath_parts)\n",
128
- "\n",
129
- "print('CUDA_VISIBLE_DEVICES =', os.environ['CUDA_VISIBLE_DEVICES'])\n",
130
- "print('OMP_NUM_THREADS =', os.environ['OMP_NUM_THREADS'])\n",
131
- "print('NUMBA_CACHE_DIR =', os.environ['NUMBA_CACHE_DIR'])\n",
132
- "print('PYTHONPATH =', os.environ['PYTHONPATH'])"
133
- ]
134
- },
135
- {
136
- "cell_type": "markdown",
137
- "metadata": {},
138
- "source": [
139
- "## Training settings\n",
140
- "\n",
141
- "The original shell command used four GPUs with per-device `batch_size=24`, so the effective batch size was about 96. For this one-GPU demo, `batch_size=24` and `grad_accum_steps=4` keeps the effective training batch similar.\n",
142
- "\n",
143
- "For a quick smoke test, set `EPOCHS = 1` and optionally set `MAX_CELLS` to a small number such as `5000`. For a full run matching the training recipe, use `EPOCHS = 300` and `MAX_CELLS = None`."
144
- ]
145
- },
146
- {
147
- "cell_type": "code",
148
- "execution_count": null,
149
- "metadata": {},
150
- "outputs": [],
151
- "source": [
152
- "SEED = 2021\n",
153
- "# Tutorial default: a one-GPU smoke run that completes quickly.\n",
154
- "# For a full run, set EPOCHS = 300, PATIENCE = 10, and MAX_CELLS = None.\n",
155
- "EPOCHS = 1\n",
156
- "PATIENCE = 1\n",
157
- "MAX_CELLS = 5000\n",
158
- "\n",
159
- "BATCH_SIZE = 24\n",
160
- "EVAL_BATCH_SIZE = 48\n",
161
- "GRAD_ACCUM_STEPS = 4\n",
162
- "\n",
163
- "LR_HEAD = '1e-4'\n",
164
- "LR_BACKBONE = '1e-4'\n",
165
- "WEIGHT_DECAY = '1e-2'\n",
166
- "DROPOUT = '0.2'\n",
167
- "\n",
168
- "print({\n",
169
- " 'seed': SEED,\n",
170
- " 'epochs': EPOCHS,\n",
171
- " 'patience': PATIENCE,\n",
172
- " 'max_cells': MAX_CELLS,\n",
173
- " 'batch_size': BATCH_SIZE,\n",
174
- " 'eval_batch_size': EVAL_BATCH_SIZE,\n",
175
- " 'grad_accum_steps': GRAD_ACCUM_STEPS,\n",
176
- "})"
177
- ]
178
- },
179
- {
180
- "cell_type": "code",
181
- "execution_count": null,
182
- "metadata": {},
183
- "outputs": [],
184
- "source": [
185
- "cmd = [\n",
186
- " sys.executable,\n",
187
- " str(SCRIPT),\n",
188
- " '--device', 'cuda',\n",
189
- " '--seed', str(SEED),\n",
190
- " '--data_path', str(DATA_PATH),\n",
191
- " '--label_column', 'celltype',\n",
192
- " '--gene_name_column', 'index',\n",
193
- " '--val_ratio', '0.1',\n",
194
- " '--test_ratio', '0.2',\n",
195
- " '--min_genes', '10',\n",
196
- " '--min_cells', '10',\n",
197
- " '--config_pkl', str(CONFIG_PKL),\n",
198
- " '--performer_checkpoint', str(CKPT_PATH),\n",
199
- " '--module_data_dir', str(MODULE_DATA_DIR),\n",
200
- " '--gene_list_path', str(GENE_LIST),\n",
201
- " '--gene_mapping_path', str(GENE_MAP),\n",
202
- " '--gene2vec_data_path', str(GENE2VEC),\n",
203
- " '--gene2ens_file', str(GENE_MAP),\n",
204
- " '--gene2ens_column', 'Ensembl gene ID',\n",
205
- " '--head_type', 'attn_pool',\n",
206
- " '--finetune_mode', 'full',\n",
207
- " '--cls_decoder_layers', '3',\n",
208
- " '--lr_head', LR_HEAD,\n",
209
- " '--lr_backbone', LR_BACKBONE,\n",
210
- " '--batch_size', str(BATCH_SIZE),\n",
211
- " '--eval_batch_size', str(EVAL_BATCH_SIZE),\n",
212
- " '--grad_accum_steps', str(GRAD_ACCUM_STEPS),\n",
213
- " '--epochs', str(EPOCHS),\n",
214
- " '--patience', str(PATIENCE),\n",
215
- " '--weight_decay', WEIGHT_DECAY,\n",
216
- " '--label_smoothing', '0',\n",
217
- " '--dropout', DROPOUT,\n",
218
- " '--output_dir', str(OUTPUT_PARENT),\n",
219
- " '--run_name', RUN_NAME,\n",
220
- " '--save_test_artifacts',\n",
221
- "]\n",
222
- "\n",
223
- "if MAX_CELLS is not None:\n",
224
- " cmd.extend(['--max_cells', str(MAX_CELLS)])\n",
225
- "\n",
226
- "print('Command:')\n",
227
- "print(' '.join(cmd))"
228
- ]
229
- },
230
- {
231
- "cell_type": "code",
232
- "execution_count": null,
233
- "metadata": {},
234
- "outputs": [],
235
- "source": [
236
- "# Run the one-GPU fine-tuning job.\n",
237
- "# The tutorial default is a smoke run. A full EPOCHS=300 run on all Zheng68K cells can take days in this environment.\n",
238
- "log_path = RUN_DIR / 'manual_launch_1gpu.log'\n",
239
- "print('Writing log to:', log_path)\n",
240
- "\n",
241
- "with log_path.open('w') as log:\n",
242
- " process = subprocess.Popen(\n",
243
- " cmd,\n",
244
- " stdout=subprocess.PIPE,\n",
245
- " stderr=subprocess.STDOUT,\n",
246
- " text=True,\n",
247
- " bufsize=1,\n",
248
- " cwd=str(PROJECT_ROOT.parent),\n",
249
- " env=os.environ.copy(),\n",
250
- " )\n",
251
- " for line in process.stdout:\n",
252
- " print(line, end='')\n",
253
- " log.write(line)\n",
254
- " return_code = process.wait()\n",
255
- "\n",
256
- "if return_code != 0:\n",
257
- " raise RuntimeError(f'cell annotation demo failed with return code {return_code}; see {log_path}')\n",
258
- "\n",
259
- "print('Done.')"
260
- ]
261
- },
262
- {
263
- "cell_type": "markdown",
264
- "metadata": {},
265
- "source": [
266
- "## Inspect outputs\n",
267
- "\n",
268
- "When `--save_test_artifacts` is enabled, the script saves `best_model.pt`, `run_config.json`, `metrics_summary.json`, a classification report, and confusion matrices in the run directory."
269
- ]
270
- },
271
- {
272
- "cell_type": "code",
273
- "execution_count": null,
274
- "metadata": {},
275
- "outputs": [],
276
- "source": [
277
- "for path in sorted(RUN_DIR.glob('*')):\n",
278
- " size_mb = path.stat().st_size / 1024 / 1024 if path.is_file() else 0\n",
279
- " print(f'{path.name:45s} {size_mb:8.2f} MB')"
280
- ]
281
- },
282
- {
283
- "cell_type": "code",
284
- "execution_count": null,
285
- "metadata": {},
286
- "outputs": [],
287
- "source": [
288
- "metrics_path = RUN_DIR / 'metrics_summary.json'\n",
289
- "with metrics_path.open('r', encoding='utf-8') as handle:\n",
290
- " metrics_summary = json.load(handle)\n",
291
- "\n",
292
- "metrics_summary"
293
- ]
294
- },
295
- {
296
- "cell_type": "code",
297
- "execution_count": null,
298
- "metadata": {},
299
- "outputs": [],
300
- "source": [
301
- "report_path = RUN_DIR / 'test_classification_report.txt'\n",
302
- "print(report_path.read_text(encoding='utf-8'))"
303
- ]
304
- },
305
- {
306
- "cell_type": "code",
307
- "execution_count": null,
308
- "metadata": {},
309
- "outputs": [],
310
- "source": [
311
- "conf_path = RUN_DIR / 'test_confusion_matrix_normalized.csv'\n",
312
- "conf_norm = pd.read_csv(conf_path, index_col=0)\n",
313
- "display(conf_norm)"
314
- ]
315
- }
316
- ],
317
- "metadata": {
318
- "kernelspec": {
319
- "display_name": "Python 3",
320
- "language": "python",
321
- "name": "python3"
322
- },
323
- "language_info": {
324
- "codemirror_mode": {
325
- "name": "ipython",
326
- "version": 3
327
- },
328
- "file_extension": ".py",
329
- "mimetype": "text/x-python",
330
- "name": "python",
331
- "nbconvert_exporter": "python",
332
- "pygments_lexer": "ipython3",
333
- "version": "3.9"
334
- }
335
- },
336
- "nbformat": 4,
337
- "nbformat_minor": 5
338
- }
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Cell annotation with BioMod model weights\n",
8
+ "\n",
9
+ "Fine-tune a cell-type classifier on the local Zheng68K dataset using the BioMod model weights. This notebook prepares the paths and command, launches a single-GPU job when explicitly enabled, and reads the classification results."
10
+ ]
11
+ },
12
+ {
13
+ "cell_type": "markdown",
14
+ "metadata": {},
15
+ "source": [
16
+ "## Project paths\n",
17
+ "\n",
18
+ "Start Jupyter from `BioMod/toturials/`. The setup cell uses `Path(\"../..\").resolve()` to select the repository root, following the project-relative path convention used in scGPT tutorials. All code, data, model weight, and output paths are derived from this root and resolved to absolute paths for the training command. If Jupyter starts at the repository root, change that line to `Path(\".\").resolve()`. For another launch location, set `PROJECT_ROOT` to the checkout root."
19
+ ]
20
+ },
21
+ {
22
+ "cell_type": "markdown",
23
+ "metadata": {},
24
+ "source": [
25
+ "## Inputs and workflow\n",
26
+ "\n",
27
+ "- Entry point: `BioMod/cell_annotation/performer_module_finetune.py`.\n",
28
+ "- Dataset: `data/downstream/final_data/Zheng68K.h5ad`.\n",
29
+ "- Model weights and matching configuration: the `.pth` / `.pkl` pair under `weight/0810model/`.\n",
30
+ "- Gene vocabulary, mapping, embeddings, and module definitions: `model_dependencies/`.\n",
31
+ "- Results: `tutorial_runs/cell_annotation_zheng68k_biomod/`.\n",
32
+ "\n",
33
+ "Select a kernel with the project's cell annotation dependencies installed. The script filters cells and genes, aligns input genes to the model vocabulary, and creates stratified training, validation, and test splits. It selects the checkpoint using validation macro-F1, then evaluates that checkpoint on the test split. The input expression values must already use the preprocessing intended for your experiment; this script does not normalize or log-transform them."
34
+ ]
35
+ },
36
+ {
37
+ "cell_type": "code",
38
+ "execution_count": null,
39
+ "metadata": {},
40
+ "outputs": [],
41
+ "source": [
42
+ "from pathlib import Path\n",
43
+ "import json\n",
44
+ "import os\n",
45
+ "import shlex\n",
46
+ "import subprocess\n",
47
+ "import sys\n",
48
+ "import tempfile\n",
49
+ "import pandas as pd\n",
50
+ "try:\n",
51
+ " from IPython.display import display\n",
52
+ "except ImportError:\n",
53
+ " def display(obj):\n",
54
+ " print(obj)\n",
55
+ "\n",
56
+ "PROJECT_ROOT = Path('../..').resolve()\n",
57
+ "if not (PROJECT_ROOT / 'BioMod' / 'models_performer_module.py').is_file() or not (PROJECT_ROOT / 'model_dependencies').is_dir():\n",
58
+ " raise FileNotFoundError(\n",
59
+ " 'Project root not found. Start Jupyter from BioMod/toturials, or set '\n",
60
+ " 'PROJECT_ROOT to your repository root (use Path(\".\").resolve() if '\n",
61
+ " 'Jupyter was started at the repository root).'\n",
62
+ " )\n",
63
+ "\n",
64
+ "\n",
65
+ "SOURCE_DIR = PROJECT_ROOT / 'BioMod'\n",
66
+ "ASSET_ROOT = PROJECT_ROOT / 'model_dependencies'\n",
67
+ "SCRIPT = SOURCE_DIR / 'cell_annotation' / 'performer_module_finetune.py'\n",
68
+ "DATA_PATH = PROJECT_ROOT / 'data' / 'downstream' / 'final_data' / 'Zheng68K.h5ad'\n",
69
+ "\n",
70
+ "# Keep the BioMod model weights and their matching configuration together.\n",
71
+ "WEIGHT_DIR = PROJECT_ROOT / 'weight' / '0810model'\n",
72
+ "CONFIG_PKL = WEIGHT_DIR / 'mae_tost_70M_75mask_ec2.pkl'\n",
73
+ "CKPT_PATH = WEIGHT_DIR / 'mae_tost_70M_75mask_ec2_epoch0.pth'\n",
74
+ "MODULE_DATA_DIR = ASSET_ROOT / 'module_data_all'\n",
75
+ "GENE_LIST = ASSET_ROOT / 'selected_genes_27k.txt'\n",
76
+ "GENE_MAP = ASSET_ROOT / 'gene2ens.csv'\n",
77
+ "GENE2VEC = ASSET_ROOT / 'selected_gene2vec_27k.npy'\n",
78
+ "\n",
79
+ "OUTPUT_PARENT = PROJECT_ROOT / 'tutorial_runs'\n",
80
+ "RUN_NAME = 'cell_annotation_zheng68k_biomod'\n",
81
+ "RUN_DIR = OUTPUT_PARENT / RUN_NAME\n",
82
+ "RUN_TRAINING = False\n",
83
+ "\n",
84
+ "print('Python:', sys.executable)\n",
85
+ "print('Repository:', PROJECT_ROOT)\n",
86
+ "print('Dataset:', DATA_PATH)\n",
87
+ "print('Run directory:', RUN_DIR)"
88
+ ]
89
+ },
90
+ {
91
+ "cell_type": "code",
92
+ "execution_count": null,
93
+ "metadata": {},
94
+ "outputs": [],
95
+ "source": [
96
+ "required_files = [\n",
97
+ " SCRIPT, DATA_PATH, CONFIG_PKL, CKPT_PATH, GENE_LIST, GENE_MAP, GENE2VEC,\n",
98
+ " SOURCE_DIR / 'models_performer_module.py',\n",
99
+ " MODULE_DATA_DIR / 'merged_modules.json',\n",
100
+ "]\n",
101
+ "required_dirs = [SOURCE_DIR, ASSET_ROOT]\n",
102
+ "missing = [str(p) for p in required_files if not p.is_file()]\n",
103
+ "missing += [str(p) for p in required_dirs if not p.is_dir()]\n",
104
+ "if missing:\n",
105
+ " raise FileNotFoundError('Missing required paths:\\n' + '\\n'.join(missing))\n",
106
+ "for path in required_files:\n",
107
+ " print('[FOUND]', path)"
108
+ ]
109
+ },
110
+ {
111
+ "cell_type": "code",
112
+ "execution_count": null,
113
+ "metadata": {},
114
+ "outputs": [],
115
+ "source": [
116
+ "# Configure only the child process; leave the notebook kernel environment unchanged.\n",
117
+ "run_env = os.environ.copy()\n",
118
+ "run_env['PYTHONUNBUFFERED'] = '1'\n",
119
+ "run_env['CUDA_VISIBLE_DEVICES'] = '0'\n",
120
+ "run_env.setdefault('OMP_NUM_THREADS', '8')\n",
121
+ "run_env.setdefault('NCCL_DEBUG', 'WARN')\n",
122
+ "run_env.setdefault('NUMBA_CACHE_DIR', str(Path(tempfile.gettempdir()) / f'biomod_annotation_{os.getpid()}'))\n",
123
+ "pythonpath_parts = [str(PROJECT_ROOT), str(SOURCE_DIR), str(ASSET_ROOT)]\n",
124
+ "if run_env.get('PYTHONPATH'):\n",
125
+ " pythonpath_parts.append(run_env['PYTHONPATH'])\n",
126
+ "run_env['PYTHONPATH'] = os.pathsep.join(pythonpath_parts)\n",
127
+ "print('CUDA_VISIBLE_DEVICES =', run_env['CUDA_VISIBLE_DEVICES'])\n",
128
+ "print('PYTHONPATH =', run_env['PYTHONPATH'])"
129
+ ]
130
+ },
131
+ {
132
+ "cell_type": "markdown",
133
+ "metadata": {},
134
+ "source": [
135
+ "## Training settings\n",
136
+ "\n",
137
+ "Review the hyperparameters below before setting `RUN_TRAINING = True` in the setup cell. Adjust batch sizes to your GPU memory. `MAX_CELLS = None` uses all cells that pass filtering; setting an integer requests a subset. The values below configure an example training run and do not establish a benchmark result or a runtime estimate."
138
+ ]
139
+ },
140
+ {
141
+ "cell_type": "code",
142
+ "execution_count": null,
143
+ "metadata": {},
144
+ "outputs": [],
145
+ "source": [
146
+ "SEED = 2021\n",
147
+ "EPOCHS = 1\n",
148
+ "PATIENCE = 1\n",
149
+ "MAX_CELLS = 5000\n",
150
+ "\n",
151
+ "BATCH_SIZE = 24\n",
152
+ "EVAL_BATCH_SIZE = 48\n",
153
+ "GRAD_ACCUM_STEPS = 4\n",
154
+ "LR_HEAD = '1e-4'\n",
155
+ "LR_BACKBONE = '1e-4'\n",
156
+ "WEIGHT_DECAY = '1e-2'\n",
157
+ "DROPOUT = '0.2'\n",
158
+ "LABEL_COLUMN = 'celltype'\n",
159
+ "GENE_NAME_COLUMN = 'index'\n",
160
+ "\n",
161
+ "print({\n",
162
+ " 'seed': SEED, 'epochs': EPOCHS, 'patience': PATIENCE,\n",
163
+ " 'max_cells': MAX_CELLS, 'batch_size': BATCH_SIZE,\n",
164
+ " 'eval_batch_size': EVAL_BATCH_SIZE, 'grad_accum_steps': GRAD_ACCUM_STEPS,\n",
165
+ " 'label_column': LABEL_COLUMN, 'gene_name_column': GENE_NAME_COLUMN,\n",
166
+ "})"
167
+ ]
168
+ },
169
+ {
170
+ "cell_type": "code",
171
+ "execution_count": null,
172
+ "metadata": {},
173
+ "outputs": [],
174
+ "source": [
175
+ "cmd = [\n",
176
+ " sys.executable, str(SCRIPT),\n",
177
+ " '--device', 'cuda',\n",
178
+ " '--seed', str(SEED),\n",
179
+ " '--data_path', str(DATA_PATH),\n",
180
+ " '--label_column', LABEL_COLUMN,\n",
181
+ " '--gene_name_column', GENE_NAME_COLUMN,\n",
182
+ " '--val_ratio', '0.1',\n",
183
+ " '--test_ratio', '0.2',\n",
184
+ " '--min_genes', '10',\n",
185
+ " '--min_cells', '10',\n",
186
+ " '--config_pkl', str(CONFIG_PKL),\n",
187
+ " '--performer_checkpoint', str(CKPT_PATH),\n",
188
+ " '--module_data_dir', str(MODULE_DATA_DIR),\n",
189
+ " '--gene_list_path', str(GENE_LIST),\n",
190
+ " '--gene_mapping_path', str(GENE_MAP),\n",
191
+ " '--gene2vec_data_path', str(GENE2VEC),\n",
192
+ " '--gene2ens_file', str(GENE_MAP),\n",
193
+ " '--gene2ens_column', 'Ensembl gene ID',\n",
194
+ " '--head_type', 'attn_pool',\n",
195
+ " '--finetune_mode', 'full',\n",
196
+ " '--cls_decoder_layers', '3',\n",
197
+ " '--lr_head', LR_HEAD,\n",
198
+ " '--lr_backbone', LR_BACKBONE,\n",
199
+ " '--batch_size', str(BATCH_SIZE),\n",
200
+ " '--eval_batch_size', str(EVAL_BATCH_SIZE),\n",
201
+ " '--grad_accum_steps', str(GRAD_ACCUM_STEPS),\n",
202
+ " '--epochs', str(EPOCHS),\n",
203
+ " '--patience', str(PATIENCE),\n",
204
+ " '--weight_decay', WEIGHT_DECAY,\n",
205
+ " '--label_smoothing', '0',\n",
206
+ " '--dropout', DROPOUT,\n",
207
+ " '--output_dir', str(OUTPUT_PARENT),\n",
208
+ " '--run_name', RUN_NAME,\n",
209
+ " '--save_test_artifacts',\n",
210
+ "]\n",
211
+ "if MAX_CELLS is not None:\n",
212
+ " cmd.extend(['--max_cells', str(MAX_CELLS)])\n",
213
+ "print('Command:')\n",
214
+ "print(subprocess.list2cmdline(cmd) if os.name == 'nt' else shlex.join(cmd))"
215
+ ]
216
+ },
217
+ {
218
+ "cell_type": "code",
219
+ "execution_count": null,
220
+ "metadata": {},
221
+ "outputs": [],
222
+ "source": [
223
+ "log_path = RUN_DIR / 'training.log'\n",
224
+ "if not RUN_TRAINING:\n",
225
+ " print('Training is disabled. Review the paths and settings, then set RUN_TRAINING = True to launch.')\n",
226
+ "else:\n",
227
+ " RUN_DIR.mkdir(parents=True, exist_ok=True)\n",
228
+ " Path(run_env['NUMBA_CACHE_DIR']).mkdir(parents=True, exist_ok=True)\n",
229
+ " print('Writing log to:', log_path)\n",
230
+ " with log_path.open('w', encoding='utf-8') as log:\n",
231
+ " process = subprocess.Popen(\n",
232
+ " cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n",
233
+ " text=True, encoding='utf-8', errors='replace', bufsize=1,\n",
234
+ " cwd=str(PROJECT_ROOT), env=run_env,\n",
235
+ " )\n",
236
+ " for line in process.stdout:\n",
237
+ " print(line, end='')\n",
238
+ " log.write(line)\n",
239
+ " return_code = process.wait()\n",
240
+ " if return_code != 0:\n",
241
+ " raise RuntimeError(f'Cell annotation failed with return code {return_code}; see {log_path}')\n",
242
+ " print('Training finished.')"
243
+ ]
244
+ },
245
+ {
246
+ "cell_type": "markdown",
247
+ "metadata": {},
248
+ "source": [
249
+ "## Inspect outputs\n",
250
+ "\n",
251
+ "A successful run saves `best_model.pt` and `run_config.json`. With `--save_test_artifacts`, it also writes `metrics_summary.json`, `test_classification_report.txt`, and confusion matrices in TXT, NPY, and CSV formats. The notebook captures the training console output in `training.log`. These files are created in the run directory; the input BioMod model weights are not overwritten. Result cells also work when training is disabled and previously generated outputs are available."
252
+ ]
253
+ },
254
+ {
255
+ "cell_type": "code",
256
+ "execution_count": null,
257
+ "metadata": {},
258
+ "outputs": [],
259
+ "source": [
260
+ "if not RUN_DIR.is_dir():\n",
261
+ " print('No run directory yet:', RUN_DIR)\n",
262
+ "else:\n",
263
+ " for path in sorted(RUN_DIR.iterdir()):\n",
264
+ " size_mb = path.stat().st_size / 1024 / 1024 if path.is_file() else 0\n",
265
+ " print(f'{path.name:45s} {size_mb:8.2f} MB')"
266
+ ]
267
+ },
268
+ {
269
+ "cell_type": "code",
270
+ "execution_count": null,
271
+ "metadata": {},
272
+ "outputs": [],
273
+ "source": [
274
+ "metrics_path = RUN_DIR / 'metrics_summary.json'\n",
275
+ "if metrics_path.is_file():\n",
276
+ " with metrics_path.open('r', encoding='utf-8') as handle:\n",
277
+ " metrics_summary = json.load(handle)\n",
278
+ " display(metrics_summary)\n",
279
+ "else:\n",
280
+ " print('Metrics are not available yet:', metrics_path)"
281
+ ]
282
+ },
283
+ {
284
+ "cell_type": "code",
285
+ "execution_count": null,
286
+ "metadata": {},
287
+ "outputs": [],
288
+ "source": [
289
+ "report_path = RUN_DIR / 'test_classification_report.txt'\n",
290
+ "if report_path.is_file():\n",
291
+ " print(report_path.read_text(encoding='utf-8'))\n",
292
+ "else:\n",
293
+ " print('Classification report is not available yet:', report_path)"
294
+ ]
295
+ },
296
+ {
297
+ "cell_type": "code",
298
+ "execution_count": null,
299
+ "metadata": {},
300
+ "outputs": [],
301
+ "source": [
302
+ "conf_path = RUN_DIR / 'test_confusion_matrix_normalized.csv'\n",
303
+ "if conf_path.is_file():\n",
304
+ " display(pd.read_csv(conf_path, index_col=0))\n",
305
+ "else:\n",
306
+ " print('Confusion matrix is not available yet:', conf_path)"
307
+ ]
308
+ }
309
+ ],
310
+ "metadata": {
311
+ "kernelspec": {
312
+ "display_name": "Python 3",
313
+ "language": "python",
314
+ "name": "python3"
315
+ },
316
+ "language_info": {
317
+ "codemirror_mode": {
318
+ "name": "ipython",
319
+ "version": 3
320
+ },
321
+ "file_extension": ".py",
322
+ "mimetype": "text/x-python",
323
+ "name": "python",
324
+ "nbconvert_exporter": "python",
325
+ "pygments_lexer": "ipython3",
326
+ "version": "3.9"
327
+ }
328
+ },
329
+ "nbformat": 4,
330
+ "nbformat_minor": 5
331
+ }
 
 
 
 
 
 
 
BioMod/toturials/drug_combination.ipynb CHANGED
@@ -1,754 +1,673 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "markdown",
5
- "metadata": {},
6
- "source": [
7
- "# Drug combination synergy prediction demo (PerformerModule weight0407)\n",
8
- "\n",
9
- "This notebook prepares a runnable demo command for `drug_combination/train_drug_combination_performer_module.py` using configurable absolute paths.\n",
10
- "\n",
11
- "The drug-combination data are not present in the current project yet, so `RUN_TRAINING` is set to `False` by default. After placing `data_summary/` under the task directory, switch `RUN_TRAINING = True` in the command cell to start training.\n",
12
- "\n",
13
- "The validation set is used for checkpoint selection. Reported metrics describe\n",
14
- "validation performance; this workflow does not produce a separate final test evaluation.\n"
15
- ]
16
- },
17
- {
18
- "cell_type": "markdown",
19
- "metadata": {},
20
- "source": [
21
- "## Expected data layout\n",
22
- "\n",
23
- "Place the drug-combination dataset here:\n",
24
- "\n",
25
- "```text\n",
26
- "$PERFORMER_MODULE_ROOT/drug_combination/data_summary/\n",
27
- "|-- new_labels_0_10.csv\n",
28
- "|-- new_cell_features_954.csv\n",
29
- "|-- smiles_0_10.csv\n",
30
- "`-- test_data/\n",
31
- " |-- independent_input.csv\n",
32
- " `-- independent_cell_features_954.csv\n",
33
- "```\n",
34
- "\n",
35
- "The default demo follows the reported single-fold/domain-generalization setting: `epochs=500`, `lr=1e-4`, `batch_size=256`, `n_folds=1`, and `pool_method=max`."
36
- ]
37
- },
38
- {
39
- "cell_type": "code",
40
- "execution_count": null,
41
- "metadata": {},
42
- "outputs": [],
43
- "source": [
44
- "from pathlib import Path\n",
45
- "import importlib.util\n",
46
- "import os\n",
47
- "import shlex\n",
48
- "import subprocess\n",
49
- "import sys\n",
50
- "\n",
51
- "import pandas as pd\n",
52
- "import numpy as np\n",
53
- "\n",
54
- "try:\n",
55
- " from IPython.display import Image, display\n",
56
- "except Exception:\n",
57
- " Image = None\n",
58
- " def display(obj):\n",
59
- " print(obj)\n",
60
- "\n",
61
- "PROJECT_ROOT = Path(os.environ.get('PERFORMER_MODULE_ROOT', Path.cwd().parent if Path.cwd().name == 'toturials' else Path.cwd())).resolve()\n",
62
- "TASK_ROOT = PROJECT_ROOT / 'drug_combination'\n",
63
- "TRAIN_SCRIPT = TASK_ROOT / 'train_drug_combination_performer_module.py'\n",
64
- "\n",
65
- "# Shared pretrained assets. Set BIOMODULEFM_ASSET_ROOT to the directory that contains\n",
66
- "# weight/0407weight and module_model/.\n",
67
- "ASSET_ROOT = Path(os.environ.get('BIOMODULEFM_ASSET_ROOT', '/data/share/biomodulefm_demo/single_cell_model'))\n",
68
- "LEGACY_MODULE_MODEL = ASSET_ROOT / 'module_model'\n",
69
- "WEIGHT_DIR = ASSET_ROOT / 'weight' / '0407weight'\n",
70
- "\n",
71
- "CHECKPOINT_PATH = WEIGHT_DIR / 'mae_tost_48M_cross_attn_only_pure_memory_epoch12.pth'\n",
72
- "HYPER_PARAMS_PATH = WEIGHT_DIR / 'mae_tost_48M_cross_attn_only_pure_memory.pkl'\n",
73
- "MODULE_DATA_DIR = PROJECT_ROOT / 'module_data_all'\n",
74
- "GENE_LIST_PATH = LEGACY_MODULE_MODEL / 'selected_genes_27k.txt'\n",
75
- "GENE_MAPPING_PATH = LEGACY_MODULE_MODEL / 'gene2ens.csv'\n",
76
- "GENE2VEC_PATH = LEGACY_MODULE_MODEL / 'selected_gene2vec_27k.npy'\n",
77
- "SYMBOL2ENS_PATH = LEGACY_MODULE_MODEL / 'gene2ens.csv'\n",
78
- "PERFORMER_CONT_ROOT = LEGACY_MODULE_MODEL / 'performer_pytorch_cont'\n",
79
- "\n",
80
- "DATA_DIR = TASK_ROOT / 'data_summary'\n",
81
- "RUN_ROOT = Path('/data/share/biomodulefm_demo/tutorial_runs/drug_combination_weight0407')\n",
82
- "OUTPUT_DIR = RUN_ROOT / 'results'\n",
83
- "\n",
84
- "TRAIN_PYTHON = Path('/opt/conda/envs/sclong/bin/python')\n",
85
- "if not TRAIN_PYTHON.exists():\n",
86
- " TRAIN_PYTHON = Path(sys.executable)\n",
87
- "\n",
88
- "for path in [PROJECT_ROOT, LEGACY_MODULE_MODEL, PERFORMER_CONT_ROOT]:\n",
89
- " if path.exists() and str(path) not in sys.path:\n",
90
- " sys.path.insert(0, str(path))\n",
91
- "\n",
92
- "pythonpath_parts = [\n",
93
- " str(PROJECT_ROOT),\n",
94
- " str(LEGACY_MODULE_MODEL),\n",
95
- " str(PERFORMER_CONT_ROOT),\n",
96
- " os.environ.get('PYTHONPATH', ''),\n",
97
- "]\n",
98
- "PYTHONPATH = ':'.join([part for part in pythonpath_parts if part])\n",
99
- "\n",
100
- "print('project root :', PROJECT_ROOT)\n",
101
- "print('task root :', TASK_ROOT)\n",
102
- "print('train script :', TRAIN_SCRIPT)\n",
103
- "print('python :', TRAIN_PYTHON)\n",
104
- "print('data dir :', DATA_DIR)\n",
105
- "print('output dir :', OUTPUT_DIR)\n"
106
- ]
107
- },
108
- {
109
- "cell_type": "code",
110
- "execution_count": null,
111
- "metadata": {},
112
- "outputs": [],
113
- "source": [
114
- "required_code_and_weight_files = {\n",
115
- " 'train_script': TRAIN_SCRIPT,\n",
116
- " 'checkpoint_path': CHECKPOINT_PATH,\n",
117
- " 'hyper_params_path': HYPER_PARAMS_PATH,\n",
118
- " 'module_data_dir': MODULE_DATA_DIR,\n",
119
- " 'gene_list_path': GENE_LIST_PATH,\n",
120
- " 'gene_mapping_path': GENE_MAPPING_PATH,\n",
121
- " 'gene2vec_path': GENE2VEC_PATH,\n",
122
- " 'symbol2ens_path': SYMBOL2ENS_PATH,\n",
123
- " 'performer_pytorch_cont': PERFORMER_CONT_ROOT,\n",
124
- "}\n",
125
- "\n",
126
- "expected_data_files = {\n",
127
- " 'train_labels': DATA_DIR / 'new_labels_0_10.csv',\n",
128
- " 'train_cell_features': DATA_DIR / 'new_cell_features_954.csv',\n",
129
- " 'drug_smiles': DATA_DIR / 'smiles_0_10.csv',\n",
130
- " 'validation_labels': DATA_DIR / 'test_data' / 'independent_input.csv',\n",
131
- " 'validation_cell_features': DATA_DIR / 'test_data' / 'independent_cell_features_954.csv',\n",
132
- "}\n",
133
- "\n",
134
- "def status_table(paths):\n",
135
- " return pd.DataFrame(\n",
136
- " [\n",
137
- " {'name': name, 'path': str(path), 'exists': path.exists()}\n",
138
- " for name, path in paths.items()\n",
139
- " ]\n",
140
- " )\n",
141
- "\n",
142
- "code_weight_status = status_table(required_code_and_weight_files)\n",
143
- "data_status = status_table(expected_data_files)\n",
144
- "DATA_READY = bool(data_status['exists'].all())\n",
145
- "\n",
146
- "display(code_weight_status)\n",
147
- "display(data_status)\n",
148
- "print('DATA_READY =', DATA_READY)\n",
149
- "\n",
150
- "missing_required = code_weight_status.loc[~code_weight_status['exists'], 'path'].tolist()\n",
151
- "if missing_required:\n",
152
- " raise FileNotFoundError('Missing required code/weight assets:\\n' + '\\n'.join(missing_required))\n",
153
- "\n",
154
- "if not DATA_READY:\n",
155
- " print('Data files are not complete yet. This notebook will not launch training until RUN_TRAINING=True and DATA_READY=True.')"
156
- ]
157
- },
158
- {
159
- "cell_type": "code",
160
- "execution_count": null,
161
- "metadata": {},
162
- "outputs": [],
163
- "source": [
164
- "packages = {\n",
165
- " 'torch': 'torch',\n",
166
- " 'torch_geometric': 'torch_geometric',\n",
167
- " 'rdkit': 'rdkit',\n",
168
- " 'networkx': 'networkx',\n",
169
- " 'sklearn': 'scikit-learn',\n",
170
- " 'pandas': 'pandas',\n",
171
- " 'numpy': 'numpy',\n",
172
- "}\n",
173
- "\n",
174
- "dependency_rows = []\n",
175
- "for module_name, package_name in packages.items():\n",
176
- " dependency_rows.append(\n",
177
- " {\n",
178
- " 'module': module_name,\n",
179
- " 'package': package_name,\n",
180
- " 'available': importlib.util.find_spec(module_name) is not None,\n",
181
- " }\n",
182
- " )\n",
183
- "\n",
184
- "dependency_status = pd.DataFrame(dependency_rows)\n",
185
- "display(dependency_status)\n",
186
- "\n",
187
- "missing_packages = dependency_status.loc[~dependency_status['available'], 'package'].tolist()\n",
188
- "if missing_packages:\n",
189
- " print('Install missing packages in the sclong environment, for example:')\n",
190
- " print('python -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple ' + ' '.join(missing_packages))"
191
- ]
192
- },
193
- {
194
- "cell_type": "code",
195
- "execution_count": null,
196
- "metadata": {},
197
- "outputs": [],
198
- "source": [
199
- "# Training parameters matching the previous reported command.\n",
200
- "EPOCHS = 500\n",
201
- "BATCH_SIZE = 256\n",
202
- "LR = 1e-4\n",
203
- "N_FOLDS = 1\n",
204
- "POOL_METHOD = 'max'\n",
205
- "EVAL_INTERVAL = 5\n",
206
- "SEED = 42\n",
207
- "\n",
208
- "# Domain-generalization mode is the script default. Set to False for k-fold CV.\n",
209
- "DOMAIN_GENERALIZATION = True\n",
210
- "FORCE_PREPROCESS = False\n",
211
- "\n",
212
- "# Keep False while data_summary/ is absent.\n",
213
- "RUN_TRAINING = False\n",
214
- "\n",
215
- "cmd = [\n",
216
- " str(TRAIN_PYTHON), str(TRAIN_SCRIPT),\n",
217
- " '--checkpoint_path', str(CHECKPOINT_PATH),\n",
218
- " '--hyper_params_path', str(HYPER_PARAMS_PATH),\n",
219
- " '--module_data_dir', str(MODULE_DATA_DIR),\n",
220
- " '--gene_list_path', str(GENE_LIST_PATH),\n",
221
- " '--gene_mapping_path', str(GENE_MAPPING_PATH),\n",
222
- " '--gene2vec_path', str(GENE2VEC_PATH),\n",
223
- " '--symbol2ens_path', str(SYMBOL2ENS_PATH),\n",
224
- " '--data_dir', str(DATA_DIR),\n",
225
- " '--output_dir', str(OUTPUT_DIR),\n",
226
- " '--epochs', str(EPOCHS),\n",
227
- " '--batch_size', str(BATCH_SIZE),\n",
228
- " '--lr', str(LR),\n",
229
- " '--n_folds', str(N_FOLDS),\n",
230
- " '--pool_method', POOL_METHOD,\n",
231
- " '--eval_interval', str(EVAL_INTERVAL),\n",
232
- " '--seed', str(SEED),\n",
233
- "]\n",
234
- "\n",
235
- "if not DOMAIN_GENERALIZATION:\n",
236
- " cmd.append('--no_domain_gen')\n",
237
- "if FORCE_PREPROCESS:\n",
238
- " cmd.append('--force_preprocess')\n",
239
- "\n",
240
- "print(' '.join(shlex.quote(x) for x in cmd))"
241
- ]
242
- },
243
- {
244
- "cell_type": "code",
245
- "execution_count": null,
246
- "metadata": {},
247
- "outputs": [],
248
- "source": [
249
- "env = os.environ.copy()\n",
250
- "env['PYTHONPATH'] = PYTHONPATH\n",
251
- "OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n",
252
- "\n",
253
- "if RUN_TRAINING:\n",
254
- " if not DATA_READY:\n",
255
- " missing_data = data_status.loc[~data_status['exists'], 'path'].tolist()\n",
256
- " raise FileNotFoundError('Data are incomplete:\\n' + '\\n'.join(missing_data))\n",
257
- " result = subprocess.run(\n",
258
- " cmd,\n",
259
- " cwd=str(TASK_ROOT),\n",
260
- " env=env,\n",
261
- " text=True,\n",
262
- " stdout=subprocess.PIPE,\n",
263
- " stderr=subprocess.STDOUT,\n",
264
- " check=False,\n",
265
- " )\n",
266
- " print(result.stdout)\n",
267
- " if result.returncode != 0:\n",
268
- " raise RuntimeError(f'Training failed with return code {result.returncode}')\n",
269
- "else:\n",
270
- " print('RUN_TRAINING is False, so no training/smokecheck was launched.')\n",
271
- " print('Set RUN_TRAINING=True after data_summary/ is available.')"
272
- ]
273
- },
274
- {
275
- "cell_type": "code",
276
- "execution_count": null,
277
- "metadata": {},
278
- "outputs": [],
279
- "source": [
280
- "if OUTPUT_DIR.exists():\n",
281
- " output_files = sorted(OUTPUT_DIR.glob('*'))\n",
282
- " display(pd.DataFrame(\n",
283
- " [\n",
284
- " {\n",
285
- " 'file': f.name,\n",
286
- " 'size_mb': round(f.stat().st_size / 1024 / 1024, 3),\n",
287
- " 'path': str(f),\n",
288
- " }\n",
289
- " for f in output_files\n",
290
- " ]\n",
291
- " ))\n",
292
- "else:\n",
293
- " print('No output directory yet:', OUTPUT_DIR)"
294
- ]
295
- },
296
- {
297
- "cell_type": "markdown",
298
- "metadata": {},
299
- "source": [
300
- "## Output files\n",
301
- "\n",
302
- "When training finishes, the script writes results under `OUTPUT_DIR`, including:\n",
303
- "\n",
304
- "- `best_model_fold{i}.pt`\n",
305
- "- `loss_fold{i}.csv`\n",
306
- "- `loss_curve_fold{i}.png`\n",
307
- "- `metrics_fold{i}.csv`\n",
308
- "- `roc_curve_fold{i}.png`\n",
309
- "- `fpr_tpr_fold{i}.pkl`\n",
310
- "- `cell_emb_cache_{pool_method}.npy`\n",
311
- "\n",
312
- "The plotting cells below regenerate publication-friendly PDF/PNG/SVG summaries from these files and also support older DeepDDS-style prediction CSV files when they are present. The README notes a previous reference setting of `lr=1e-4`, `epochs=500`, `pool_method=max`, `n_folds=1`, with reported AUROC around `0.6824` for the old run.\n"
313
- ]
314
- },
315
- {
316
- "cell_type": "markdown",
317
- "metadata": {},
318
- "source": [
319
- "## Data overview plots\n",
320
- "\n",
321
- "These plots summarize the drug-combination dataset before training. They are useful for quick notebook demos and GitHub documentation because they do not require a completed model run.\n"
322
- ]
323
- },
324
- {
325
- "cell_type": "code",
326
- "execution_count": null,
327
- "metadata": {},
328
- "outputs": [],
329
- "source": [
330
- "import matplotlib\n",
331
- "matplotlib.use('Agg')\n",
332
- "import matplotlib.pyplot as plt\n",
333
- "from matplotlib.ticker import MaxNLocator\n",
334
- "\n",
335
- "PLOT_DIR = OUTPUT_DIR / 'notebook_plots'\n",
336
- "PLOT_DIR.mkdir(parents=True, exist_ok=True)\n",
337
- "\n",
338
- "plt.rcParams.update({\n",
339
- " 'font.family': 'sans-serif',\n",
340
- " 'font.sans-serif': ['Arial', 'DejaVu Sans'],\n",
341
- " 'font.size': 7,\n",
342
- " 'axes.labelsize': 7,\n",
343
- " 'axes.titlesize': 8,\n",
344
- " 'xtick.labelsize': 6,\n",
345
- " 'ytick.labelsize': 6,\n",
346
- " 'legend.fontsize': 6,\n",
347
- " 'axes.linewidth': 0.7,\n",
348
- " 'pdf.fonttype': 42,\n",
349
- " 'ps.fonttype': 42,\n",
350
- " 'svg.fonttype': 'none',\n",
351
- "})\n",
352
- "\n",
353
- "\n",
354
- "def save_fig(fig, stem: str, dpi: int = 600, show: bool = True):\n",
355
- " png_path = PLOT_DIR / f'{stem}.png'\n",
356
- " pdf_path = PLOT_DIR / f'{stem}.pdf'\n",
357
- " fig.savefig(png_path, dpi=dpi, bbox_inches='tight')\n",
358
- " fig.savefig(pdf_path, bbox_inches='tight')\n",
359
- " plt.close(fig)\n",
360
- " print('Saved:', png_path)\n",
361
- " print('Saved:', pdf_path)\n",
362
- " if show and Image is not None:\n",
363
- " display(Image(filename=str(png_path)))\n",
364
- " return png_path, pdf_path\n",
365
- "\n",
366
- "\n",
367
- "def despine(ax):\n",
368
- " ax.spines['top'].set_visible(False)\n",
369
- " ax.spines['right'].set_visible(False)\n",
370
- " ax.tick_params(width=0.7, length=2.5)\n",
371
- "\n",
372
- "\n",
373
- "def read_label_table(path: Path) -> pd.DataFrame:\n",
374
- " df = pd.read_csv(path)\n",
375
- " lower_map = {str(col).lower(): col for col in df.columns}\n",
376
- " for required in ['drug1', 'drug2', 'cell', 'label']:\n",
377
- " if required not in lower_map:\n",
378
- " raise ValueError(f'{path} is missing a required column similar to {required!r}. Columns: {list(df.columns)}')\n",
379
- " df = df.rename(columns={lower_map['drug1']: 'drug1', lower_map['drug2']: 'drug2', lower_map['cell']: 'cell', lower_map['label']: 'label'})\n",
380
- " return df[['drug1', 'drug2', 'cell', 'label']].copy()\n",
381
- "\n",
382
- "\n",
383
- "data_overview = []\n",
384
- "train_labels = validation_labels = None\n",
385
- "if expected_data_files['train_labels'].exists():\n",
386
- " train_labels = read_label_table(expected_data_files['train_labels'])\n",
387
- " data_overview.append({\n",
388
- " 'split': 'train',\n",
389
- " 'rows': len(train_labels),\n",
390
- " 'positive': int((train_labels['label'] == 1).sum()),\n",
391
- " 'negative': int((train_labels['label'] == 0).sum()),\n",
392
- " 'unique_cells': train_labels['cell'].nunique(),\n",
393
- " 'unique_drug1': train_labels['drug1'].nunique(),\n",
394
- " 'unique_drug2': train_labels['drug2'].nunique(),\n",
395
- " 'unique_pairs': train_labels[['drug1', 'drug2']].drop_duplicates().shape[0],\n",
396
- " })\n",
397
- "if expected_data_files['validation_labels'].exists():\n",
398
- " validation_labels = read_label_table(expected_data_files['validation_labels'])\n",
399
- " data_overview.append({\n",
400
- " 'split': 'validation',\n",
401
- " 'rows': len(validation_labels),\n",
402
- " 'positive': int((validation_labels['label'] == 1).sum()),\n",
403
- " 'negative': int((validation_labels['label'] == 0).sum()),\n",
404
- " 'unique_cells': validation_labels['cell'].nunique(),\n",
405
- " 'unique_drug1': validation_labels['drug1'].nunique(),\n",
406
- " 'unique_drug2': validation_labels['drug2'].nunique(),\n",
407
- " 'unique_pairs': validation_labels[['drug1', 'drug2']].drop_duplicates().shape[0],\n",
408
- " })\n",
409
- "\n",
410
- "data_overview_df = pd.DataFrame(data_overview)\n",
411
- "if data_overview_df.empty:\n",
412
- " print('No label files found yet, so data overview plots were skipped.')\n",
413
- "else:\n",
414
- " data_overview_df.to_csv(PLOT_DIR / 'drug_combination_data_overview.csv', index=False)\n",
415
- " display(data_overview_df)\n",
416
- "\n",
417
- " label_rows = []\n",
418
- " if train_labels is not None:\n",
419
- " label_rows.extend([{'split': 'train', 'label': int(k), 'count': int(v)} for k, v in train_labels['label'].value_counts().sort_index().items()])\n",
420
- " if validation_labels is not None:\n",
421
- " label_rows.extend([{'split': 'validation', 'label': int(k), 'count': int(v)} for k, v in validation_labels['label'].value_counts().sort_index().items()])\n",
422
- " label_df = pd.DataFrame(label_rows)\n",
423
- " label_df.to_csv(PLOT_DIR / 'drug_combination_label_counts.csv', index=False)\n",
424
- "\n",
425
- " fig, ax = plt.subplots(figsize=(3.5, 2.4))\n",
426
- " splits = list(label_df['split'].drop_duplicates())\n",
427
- " x = np.arange(len(splits))\n",
428
- " width = 0.34\n",
429
- " colors = {0: '#4E79A7', 1: '#E15759'}\n",
430
- " for offset, label in zip([-width / 2, width / 2], [0, 1]):\n",
431
- " values = [int(label_df[(label_df['split'] == split) & (label_df['label'] == label)]['count'].sum()) for split in splits]\n",
432
- " ax.bar(x + offset, values, width=width, color=colors[label], edgecolor='#333333', linewidth=0.4, label=f'label {label}')\n",
433
- " for xpos, value in zip(x + offset, values):\n",
434
- " ax.text(xpos, value, str(value), ha='center', va='bottom', fontsize=6)\n",
435
- " ax.set_xticks(x)\n",
436
- " ax.set_xticklabels(splits, rotation=15, ha='right')\n",
437
- " ax.set_ylabel('Number of combinations')\n",
438
- " ax.set_title('Drug-combination label balance')\n",
439
- " ax.legend(frameon=False)\n",
440
- " ax.yaxis.set_major_locator(MaxNLocator(integer=True))\n",
441
- " despine(ax)\n",
442
- " fig.tight_layout()\n",
443
- " save_fig(fig, 'drug_combination_label_balance')\n",
444
- "\n",
445
- " fig, ax = plt.subplots(figsize=(4.1, 2.5))\n",
446
- " overview_long = data_overview_df.melt(id_vars='split', value_vars=['unique_cells', 'unique_drug1', 'unique_drug2', 'unique_pairs'], var_name='feature', value_name='count')\n",
447
- " features = ['unique_cells', 'unique_drug1', 'unique_drug2', 'unique_pairs']\n",
448
- " x = np.arange(len(features))\n",
449
- " width = 0.36 if len(splits) > 1 else 0.5\n",
450
- " palette = ['#59A14F', '#F28E2B']\n",
451
- " for i, split in enumerate(splits):\n",
452
- " values = [int(overview_long[(overview_long['split'] == split) & (overview_long['feature'] == feature)]['count'].sum()) for feature in features]\n",
453
- " ax.bar(x + (i - (len(splits) - 1) / 2) * width, values, width=width, color=palette[i % len(palette)], edgecolor='#333333', linewidth=0.4, label=split)\n",
454
- " ax.set_xticks(x)\n",
455
- " ax.set_xticklabels(['cells', 'drug1', 'drug2', 'drug pairs'], rotation=20, ha='right')\n",
456
- " ax.set_ylabel('Unique count')\n",
457
- " ax.set_title('Dataset coverage')\n",
458
- " ax.legend(frameon=False)\n",
459
- " ax.yaxis.set_major_locator(MaxNLocator(integer=True))\n",
460
- " despine(ax)\n",
461
- " fig.tight_layout()\n",
462
- " save_fig(fig, 'drug_combination_dataset_coverage')\n"
463
- ]
464
- },
465
- {
466
- "cell_type": "markdown",
467
- "metadata": {},
468
- "source": [
469
- "## Result plots from training outputs\n",
470
- "\n",
471
- "This section reuses the evaluation signals from the training script and the older DeepDDS/scLong plotting convention: AUROC, PR-AUC, accuracy, balanced accuracy, precision, TPR, kappa, recall, ROC curves, and confusion matrices when prediction CSV files are available.\n"
472
- ]
473
- },
474
- {
475
- "cell_type": "code",
476
- "execution_count": null,
477
- "metadata": {},
478
- "outputs": [],
479
- "source": [
480
- "import pickle\n",
481
- "from sklearn import metrics\n",
482
- "from sklearn.metrics import confusion_matrix\n",
483
- "\n",
484
- "\n",
485
- "def find_fold_files(pattern: str):\n",
486
- " return sorted(OUTPUT_DIR.glob(pattern))\n",
487
- "\n",
488
- "\n",
489
- "def load_metrics_files():\n",
490
- " frames = []\n",
491
- " for path in find_fold_files('metrics_fold*.csv'):\n",
492
- " df = pd.read_csv(path)\n",
493
- " stem = path.stem.replace('metrics_fold', '')\n",
494
- " try:\n",
495
- " fold = int(stem)\n",
496
- " except ValueError:\n",
497
- " fold = len(frames)\n",
498
- " df['fold'] = fold\n",
499
- " df['source_file'] = str(path)\n",
500
- " frames.append(df)\n",
501
- " return pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()\n",
502
- "\n",
503
- "\n",
504
- "def load_loss_files():\n",
505
- " frames = []\n",
506
- " for path in find_fold_files('loss_fold*.csv'):\n",
507
- " df = pd.read_csv(path)\n",
508
- " stem = path.stem.replace('loss_fold', '')\n",
509
- " try:\n",
510
- " fold = int(stem)\n",
511
- " except ValueError:\n",
512
- " fold = len(frames)\n",
513
- " df['fold'] = fold\n",
514
- " frames.append(df)\n",
515
- " return pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()\n",
516
- "\n",
517
- "\n",
518
- "metrics_df = load_metrics_files()\n",
519
- "loss_df = load_loss_files()\n",
520
- "\n",
521
- "if metrics_df.empty:\n",
522
- " print('No metrics_fold*.csv files found yet:', OUTPUT_DIR)\n",
523
- "else:\n",
524
- " metric_cols = [col for col in ['AUC', 'PR_AUC', 'ACC', 'BACC', 'PREC', 'TPR', 'KAPPA', 'RECALL'] if col in metrics_df.columns]\n",
525
- " best_rows = metrics_df.sort_values(['fold', 'AUC'], ascending=[True, False]).groupby('fold', as_index=False).head(1)\n",
526
- " summary_rows = []\n",
527
- " for metric in metric_cols:\n",
528
- " values = best_rows[metric].astype(float).to_numpy()\n",
529
- " summary_rows.append({'metric': metric, 'mean': float(np.mean(values)), 'std': float(np.std(values)), 'n_folds': int(len(values))})\n",
530
- " summary_df = pd.DataFrame(summary_rows)\n",
531
- " best_rows.to_csv(PLOT_DIR / 'drug_combination_best_metrics_by_fold.csv', index=False)\n",
532
- " summary_df.to_csv(PLOT_DIR / 'drug_combination_metric_summary.csv', index=False)\n",
533
- " display(best_rows)\n",
534
- " display(summary_df)\n",
535
- "\n",
536
- " fig, ax = plt.subplots(figsize=(4.1, 2.6))\n",
537
- " for metric, color in [('AUC', '#4E79A7'), ('PR_AUC', '#E15759'), ('ACC', '#59A14F'), ('BACC', '#F28E2B')]:\n",
538
- " if metric in metrics_df.columns:\n",
539
- " for fold, frame in metrics_df.groupby('fold'):\n",
540
- " ax.plot(frame['epoch'], frame[metric], linewidth=0.8, alpha=0.65, color=color)\n",
541
- " mean_by_epoch = metrics_df.groupby('epoch')[metric].mean().reset_index()\n",
542
- " ax.plot(mean_by_epoch['epoch'], mean_by_epoch[metric], linewidth=1.8, color=color, label=metric)\n",
543
- " ax.set_xlabel('Epoch')\n",
544
- " ax.set_ylabel('Score')\n",
545
- " ax.set_ylim(0, 1.02)\n",
546
- " ax.set_title('Validation metrics over training')\n",
547
- " ax.legend(frameon=False, ncol=2)\n",
548
- " ax.grid(axis='y', linestyle='--', linewidth=0.4, alpha=0.3)\n",
549
- " ax.set_axisbelow(True)\n",
550
- " despine(ax)\n",
551
- " fig.tight_layout()\n",
552
- " save_fig(fig, 'drug_combination_metric_curves')\n",
553
- "\n",
554
- " fig, ax = plt.subplots(figsize=(3.8, 2.5))\n",
555
- " plot_metrics = [m for m in ['AUC', 'PR_AUC', 'ACC', 'BACC', 'PREC', 'TPR', 'KAPPA', 'RECALL'] if m in best_rows.columns]\n",
556
- " values = [float(best_rows[m].mean()) for m in plot_metrics]\n",
557
- " colors = ['#4E79A7', '#E15759', '#59A14F', '#F28E2B', '#76B7B2', '#B07AA1', '#9C755F', '#BAB0AC'][:len(plot_metrics)]\n",
558
- " bars = ax.bar(np.arange(len(plot_metrics)), values, color=colors, edgecolor='#333333', linewidth=0.4)\n",
559
- " for bar, value in zip(bars, values):\n",
560
- " ax.text(bar.get_x() + bar.get_width() / 2, value + 0.015, f'{value:.3f}', ha='center', va='bottom', fontsize=6)\n",
561
- " ax.set_xticks(np.arange(len(plot_metrics)))\n",
562
- " ax.set_xticklabels(plot_metrics, rotation=25, ha='right')\n",
563
- " ax.set_ylim(0, 1.05)\n",
564
- " ax.set_ylabel('Best-fold score')\n",
565
- " ax.set_title('Drug-combination prediction summary')\n",
566
- " ax.grid(axis='y', linestyle='--', linewidth=0.4, alpha=0.3)\n",
567
- " ax.set_axisbelow(True)\n",
568
- " despine(ax)\n",
569
- " fig.tight_layout()\n",
570
- " save_fig(fig, 'drug_combination_best_metric_summary')\n",
571
- "\n",
572
- "if loss_df.empty:\n",
573
- " print('No loss_fold*.csv files found yet:', OUTPUT_DIR)\n",
574
- "else:\n",
575
- " fig, ax = plt.subplots(figsize=(4.1, 2.5))\n",
576
- " for fold, frame in loss_df.groupby('fold'):\n",
577
- " ax.plot(frame['epoch'], frame['avg_loss'], linewidth=0.8, alpha=0.65, label=f'fold {fold}')\n",
578
- " mean_loss = loss_df.groupby('epoch')['avg_loss'].mean().reset_index()\n",
579
- " ax.plot(mean_loss['epoch'], mean_loss['avg_loss'], linewidth=1.8, color='#222222', label='mean')\n",
580
- " ax.set_xlabel('Epoch')\n",
581
- " ax.set_ylabel('Average loss')\n",
582
- " ax.set_title('Training loss')\n",
583
- " ax.legend(frameon=False, ncol=2)\n",
584
- " ax.grid(axis='y', linestyle='--', linewidth=0.4, alpha=0.3)\n",
585
- " ax.set_axisbelow(True)\n",
586
- " despine(ax)\n",
587
- " fig.tight_layout()\n",
588
- " save_fig(fig, 'drug_combination_loss_curves')\n",
589
- "\n",
590
- "roc_files = find_fold_files('fpr_tpr_fold*.pkl')\n",
591
- "if not roc_files:\n",
592
- " print('No fpr_tpr_fold*.pkl files found yet:', OUTPUT_DIR)\n",
593
- "else:\n",
594
- " fig, ax = plt.subplots(figsize=(3.2, 3.2))\n",
595
- " roc_rows = []\n",
596
- " for path in roc_files:\n",
597
- " with path.open('rb') as handle:\n",
598
- " fpr, tpr = pickle.load(handle)\n",
599
- " stem = path.stem.replace('fpr_tpr_fold', '')\n",
600
- " try:\n",
601
- " fold = int(stem)\n",
602
- " except ValueError:\n",
603
- " fold = len(roc_rows)\n",
604
- " auc_value = float(metrics.auc(fpr, tpr))\n",
605
- " roc_rows.append({'fold': fold, 'auc': auc_value})\n",
606
- " ax.plot(fpr, tpr, linewidth=1.0, label=f'fold {fold}, AUROC={auc_value:.3f}')\n",
607
- " ax.plot([0, 1], [0, 1], linestyle='--', color='#333333', linewidth=0.8, alpha=0.7)\n",
608
- " ax.set_xlabel('False positive rate')\n",
609
- " ax.set_ylabel('True positive rate')\n",
610
- " ax.set_xlim(0, 1)\n",
611
- " ax.set_ylim(0, 1.02)\n",
612
- " ax.set_aspect('equal', adjustable='box')\n",
613
- " ax.legend(frameon=False, loc='lower right')\n",
614
- " ax.set_title('ROC curve')\n",
615
- " despine(ax)\n",
616
- " fig.tight_layout()\n",
617
- " save_fig(fig, 'drug_combination_roc_curves')\n",
618
- " pd.DataFrame(roc_rows).to_csv(PLOT_DIR / 'drug_combination_roc_auc_from_curves.csv', index=False)\n",
619
- "\n",
620
- "\n",
621
- "def load_legacy_prediction_csv(path: Path):\n",
622
- " raw = pd.read_csv(path, header=None)\n",
623
- " arr = raw.to_numpy()\n",
624
- " candidates = []\n",
625
- " # Old DeepDDS files are often saved as rows: labels, predicted labels, scores or index, labels, predicted labels, scores.\n",
626
- " if arr.shape[0] >= 3:\n",
627
- " candidates.append(('row_0_2', arr[0], None, arr[2]))\n",
628
- " if arr.shape[0] >= 4:\n",
629
- " candidates.append(('row_1_3', arr[1], arr[2], arr[3]))\n",
630
- " if arr.shape[1] >= 3:\n",
631
- " candidates.append(('col_0_2', arr[:, 0], None, arr[:, 2]))\n",
632
- " if arr.shape[1] >= 4:\n",
633
- " candidates.append(('col_1_3', arr[:, 1], arr[:, 2], arr[:, 3]))\n",
634
- " for mode, labels, pred_labels, scores in candidates:\n",
635
- " labels = pd.to_numeric(pd.Series(labels), errors='coerce').dropna().to_numpy(dtype=float)\n",
636
- " scores = pd.to_numeric(pd.Series(scores), errors='coerce').dropna().to_numpy(dtype=float)\n",
637
- " if len(labels) != len(scores) or len(labels) < 2:\n",
638
- " continue\n",
639
- " unique_labels = set(np.unique(labels).astype(int).tolist())\n",
640
- " if unique_labels.issubset({0, 1}) and len(unique_labels) == 2:\n",
641
- " if pred_labels is None or len(pred_labels) != len(labels):\n",
642
- " pred_labels = (scores >= 0.5).astype(int)\n",
643
- " else:\n",
644
- " pred_labels = pd.to_numeric(pd.Series(pred_labels), errors='coerce').fillna(0).to_numpy(dtype=float).astype(int)\n",
645
- " return labels.astype(int), scores.astype(float), pred_labels.astype(int), mode\n",
646
- " return None\n",
647
- "\n",
648
- "legacy_files = sorted(OUTPUT_DIR.glob('*result*.csv')) + sorted(OUTPUT_DIR.glob('*ROC*.csv'))\n",
649
- "legacy_records = []\n",
650
- "for path in legacy_files:\n",
651
- " loaded = load_legacy_prediction_csv(path)\n",
652
- " if loaded is None:\n",
653
- " continue\n",
654
- " labels, scores, pred_labels, mode = loaded\n",
655
- " fpr, tpr, _ = metrics.roc_curve(labels, scores)\n",
656
- " precision, recall, _ = metrics.precision_recall_curve(labels, scores)\n",
657
- " auc_value = float(metrics.auc(fpr, tpr))\n",
658
- " pr_auc = float(metrics.auc(recall, precision))\n",
659
- " cm = confusion_matrix(labels, pred_labels)\n",
660
- " legacy_records.append({'path': path, 'mode': mode, 'labels': labels, 'scores': scores, 'pred_labels': pred_labels, 'fpr': fpr, 'tpr': tpr, 'precision': precision, 'recall': recall, 'auc': auc_value, 'pr_auc': pr_auc, 'cm': cm})\n",
661
- "\n",
662
- "if not legacy_records:\n",
663
- " print('No legacy prediction CSV with labels/scores found. PR and confusion-matrix plots will be available after prediction CSV files are saved.')\n",
664
- "else:\n",
665
- " legacy_summary = pd.DataFrame([{'file': r['path'].name, 'mode': r['mode'], 'auc': r['auc'], 'pr_auc': r['pr_auc'], 'n': len(r['labels'])} for r in legacy_records])\n",
666
- " legacy_summary.to_csv(PLOT_DIR / 'drug_combination_legacy_prediction_summary.csv', index=False)\n",
667
- " display(legacy_summary)\n",
668
- "\n",
669
- " fig, ax = plt.subplots(figsize=(3.2, 3.2))\n",
670
- " for r in legacy_records:\n",
671
- " ax.plot(r['fpr'], r['tpr'], linewidth=1.0, label=f'{r[\"path\"].stem}, AUROC={r[\"auc\"]:.3f}')\n",
672
- " ax.plot([0, 1], [0, 1], linestyle='--', color='#333333', linewidth=0.8, alpha=0.7)\n",
673
- " ax.set_xlabel('False positive rate')\n",
674
- " ax.set_ylabel('True positive rate')\n",
675
- " ax.set_xlim(0, 1)\n",
676
- " ax.set_ylim(0, 1.02)\n",
677
- " ax.set_aspect('equal', adjustable='box')\n",
678
- " ax.legend(frameon=False, loc='lower right')\n",
679
- " ax.set_title('ROC from prediction CSV')\n",
680
- " despine(ax)\n",
681
- " fig.tight_layout()\n",
682
- " save_fig(fig, 'drug_combination_legacy_prediction_roc')\n",
683
- "\n",
684
- " fig, ax = plt.subplots(figsize=(3.2, 3.0))\n",
685
- " for r in legacy_records:\n",
686
- " ax.plot(r['recall'], r['precision'], linewidth=1.0, label=f'{r[\"path\"].stem}, AUPR={r[\"pr_auc\"]:.3f}')\n",
687
- " ax.set_xlabel('Recall')\n",
688
- " ax.set_ylabel('Precision')\n",
689
- " ax.set_xlim(0, 1)\n",
690
- " ax.set_ylim(0, 1.02)\n",
691
- " ax.legend(frameon=False, loc='lower left')\n",
692
- " ax.set_title('Precision-recall curve')\n",
693
- " despine(ax)\n",
694
- " fig.tight_layout()\n",
695
- " save_fig(fig, 'drug_combination_legacy_prediction_pr_curve')\n",
696
- "\n",
697
- " first = legacy_records[0]\n",
698
- " fig, ax = plt.subplots(figsize=(2.4, 2.3))\n",
699
- " im = ax.imshow(first['cm'], cmap='Blues')\n",
700
- " for row in range(first['cm'].shape[0]):\n",
701
- " for col in range(first['cm'].shape[1]):\n",
702
- " ax.text(col, row, int(first['cm'][row, col]), ha='center', va='center', fontsize=7)\n",
703
- " ax.set_xticks([0, 1])\n",
704
- " ax.set_yticks([0, 1])\n",
705
- " ax.set_xticklabels(['0', '1'])\n",
706
- " ax.set_yticklabels(['0', '1'])\n",
707
- " ax.set_xlabel('Predicted label')\n",
708
- " ax.set_ylabel('True label')\n",
709
- " ax.set_title('Confusion matrix')\n",
710
- " fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)\n",
711
- " fig.tight_layout()\n",
712
- " save_fig(fig, 'drug_combination_legacy_confusion_matrix')\n"
713
- ]
714
- },
715
- {
716
- "cell_type": "markdown",
717
- "metadata": {},
718
- "source": [
719
- "## Generated plot files\n",
720
- "\n",
721
- "This cell lists the figures and source tables generated by the notebook plotting sections.\n"
722
- ]
723
- },
724
- {
725
- "cell_type": "code",
726
- "execution_count": null,
727
- "metadata": {},
728
- "outputs": [],
729
- "source": [
730
- "plot_files = sorted(PLOT_DIR.glob('*'))\n",
731
- "if plot_files:\n",
732
- " display(pd.DataFrame([\n",
733
- " {'file': path.name, 'size_kb': round(path.stat().st_size / 1024, 2), 'path': str(path)}\n",
734
- " for path in plot_files\n",
735
- " ]))\n",
736
- "else:\n",
737
- " print('No notebook plot files were generated yet.')\n"
738
- ]
739
- }
740
- ],
741
- "metadata": {
742
- "kernelspec": {
743
- "display_name": "Python 3 (sclong)",
744
- "language": "python",
745
- "name": "python3"
746
- },
747
- "language_info": {
748
- "name": "python",
749
- "pygments_lexer": "ipython3"
750
- }
751
- },
752
- "nbformat": 4,
753
- "nbformat_minor": 5
754
- }
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Drug combination synergy prediction with BioMod\n",
8
+ "\n",
9
+ "This notebook checks the bundled drug-combination inputs, prepares a command for\n",
10
+ "`BioMod/drug_combination/train_drug_combination_performer_module.py`, and plots\n",
11
+ "available training outputs. It uses the BioMod model weights and configuration.\n",
12
+ "\n",
13
+ "`RUN_TRAINING = False` by default. Set it to `True` only when you are ready to train\n",
14
+ "in the active notebook environment. The validation set selects the checkpoint;\n",
15
+ "this workflow does not produce a separate final test evaluation.\n"
16
+ ]
17
+ },
18
+ {
19
+ "cell_type": "markdown",
20
+ "metadata": {},
21
+ "source": [
22
+ "## Project paths and dataset layout\n",
23
+ "\n",
24
+ "Start Jupyter from `BioMod/toturials/`. The path cell uses `Path('../..')` for the\n",
25
+ "repository root and resolves project-relative paths for the training subprocess.\n",
26
+ "If Jupyter starts at the repository root, use `Path('.')` instead. The dataset is\n",
27
+ "already included in the project:\n",
28
+ "\n",
29
+ "```text\n",
30
+ "data/downstream/final_data/drug_combination_synergy/data_summary/\n",
31
+ "|-- new_labels_0_10.csv\n",
32
+ "|-- new_cell_features_954.csv\n",
33
+ "|-- smiles_0_10.csv\n",
34
+ "`-- test_data/\n",
35
+ " |-- independent_input.csv\n",
36
+ " `-- independent_cell_features_954.csv\n",
37
+ "```\n",
38
+ "\n",
39
+ "The historical folder name `test_data` is retained; these files are used as the\n",
40
+ "validation set. The example uses the domain-generalization mode with one run,\n",
41
+ "500 epochs, a batch size of 256, and max pooling. These are example settings,\n",
42
+ "not a verified performance claim for the current weights.\n"
43
+ ]
44
+ },
45
+ {
46
+ "cell_type": "code",
47
+ "execution_count": null,
48
+ "metadata": {},
49
+ "outputs": [],
50
+ "source": [
51
+ "from pathlib import Path\n",
52
+ "import importlib.util\n",
53
+ "import os\n",
54
+ "import shlex\n",
55
+ "import subprocess\n",
56
+ "import sys\n",
57
+ "\n",
58
+ "import numpy as np\n",
59
+ "import pandas as pd\n",
60
+ "\n",
61
+ "try:\n",
62
+ " from IPython.display import Image, display\n",
63
+ "except ImportError:\n",
64
+ " Image = None\n",
65
+ " def display(obj):\n",
66
+ " print(obj)\n",
67
+ "\n",
68
+ "# Start Jupyter with BioMod/toturials as the working directory.\n",
69
+ "# If Jupyter starts at the repository root, change this line to Path('.').resolve().\n",
70
+ "PROJECT_ROOT = Path('../..').resolve()\n",
71
+ "SOURCE_DIR = PROJECT_ROOT / 'BioMod'\n",
72
+ "if not (SOURCE_DIR / 'models_performer_module.py').is_file() or not (PROJECT_ROOT / 'model_dependencies').is_dir():\n",
73
+ " raise FileNotFoundError(\n",
74
+ " 'Start Jupyter from BioMod/toturials, or set PROJECT_ROOT to the repository root '\n",
75
+ " \"(use Path('.').resolve() if Jupyter was launched at that root).\"\n",
76
+ " )\n",
77
+ "REPO_ROOT = PROJECT_ROOT\n",
78
+ "\n",
79
+ "DEPENDENCY_ROOT = REPO_ROOT / 'model_dependencies'\n",
80
+ "WEIGHT_DIR = REPO_ROOT / 'weight' / '0810model'\n",
81
+ "MODULE_DATA_DIR = DEPENDENCY_ROOT / 'module_data_all'\n",
82
+ "TRAIN_PYTHON = Path(sys.executable).resolve()\n",
83
+ "PYTHONPATH = os.pathsep.join(\n",
84
+ " [str(SOURCE_DIR), str(DEPENDENCY_ROOT)]\n",
85
+ " + ([os.environ['PYTHONPATH']] if os.environ.get('PYTHONPATH') else [])\n",
86
+ ")\n",
87
+ "\n",
88
+ "TASK_ROOT = SOURCE_DIR / 'drug_combination'\n",
89
+ "TRAIN_SCRIPT = TASK_ROOT / 'train_drug_combination_performer_module.py'\n",
90
+ "CHECKPOINT_PATH = WEIGHT_DIR / 'mae_tost_70M_75mask_ec2_epoch0.pth'\n",
91
+ "HYPER_PARAMS_PATH = WEIGHT_DIR / 'mae_tost_70M_75mask_ec2.pkl'\n",
92
+ "GENE_LIST_PATH = DEPENDENCY_ROOT / 'selected_genes_27k.txt'\n",
93
+ "GENE_MAPPING_PATH = DEPENDENCY_ROOT / 'gene2ens.csv'\n",
94
+ "GENE2VEC_PATH = DEPENDENCY_ROOT / 'selected_gene2vec_27k.npy'\n",
95
+ "SYMBOL2ENS_PATH = GENE_MAPPING_PATH\n",
96
+ "PERFORMER_CONT_ROOT = DEPENDENCY_ROOT / 'performer_pytorch_cont'\n",
97
+ "DATA_DIR = REPO_ROOT / 'data/downstream/final_data/drug_combination_synergy/data_summary'\n",
98
+ "RUN_ROOT = REPO_ROOT / 'tutorial_runs/drug_combination'\n",
99
+ "OUTPUT_DIR = RUN_ROOT / 'results'\n",
100
+ "\n",
101
+ "print('repository :', REPO_ROOT)\n",
102
+ "print('train script :', TRAIN_SCRIPT)\n",
103
+ "print('python :', TRAIN_PYTHON)\n",
104
+ "print('data dir :', DATA_DIR)\n",
105
+ "print('output dir :', OUTPUT_DIR)\n"
106
+ ]
107
+ },
108
+ {
109
+ "cell_type": "code",
110
+ "execution_count": null,
111
+ "metadata": {},
112
+ "outputs": [],
113
+ "source": [
114
+ "required_code_and_weight_files = {\n",
115
+ " 'train_script': TRAIN_SCRIPT,\n",
116
+ " 'checkpoint_path': CHECKPOINT_PATH,\n",
117
+ " 'hyper_params_path': HYPER_PARAMS_PATH,\n",
118
+ " 'module_data_dir': MODULE_DATA_DIR,\n",
119
+ " 'gene_list_path': GENE_LIST_PATH,\n",
120
+ " 'gene_mapping_path': GENE_MAPPING_PATH,\n",
121
+ " 'gene2vec_path': GENE2VEC_PATH,\n",
122
+ " 'symbol2ens_path': SYMBOL2ENS_PATH,\n",
123
+ " 'performer_pytorch_cont': PERFORMER_CONT_ROOT,\n",
124
+ "}\n",
125
+ "\n",
126
+ "expected_data_files = {\n",
127
+ " 'train_labels': DATA_DIR / 'new_labels_0_10.csv',\n",
128
+ " 'train_cell_features': DATA_DIR / 'new_cell_features_954.csv',\n",
129
+ " 'drug_smiles': DATA_DIR / 'smiles_0_10.csv',\n",
130
+ " 'validation_labels': DATA_DIR / 'test_data' / 'independent_input.csv',\n",
131
+ " 'validation_cell_features': DATA_DIR / 'test_data' / 'independent_cell_features_954.csv',\n",
132
+ "}\n",
133
+ "\n",
134
+ "def status_table(paths):\n",
135
+ " return pd.DataFrame(\n",
136
+ " [\n",
137
+ " {'name': name, 'path': str(path), 'exists': path.exists()}\n",
138
+ " for name, path in paths.items()\n",
139
+ " ]\n",
140
+ " )\n",
141
+ "\n",
142
+ "code_weight_status = status_table(required_code_and_weight_files)\n",
143
+ "data_status = status_table(expected_data_files)\n",
144
+ "DATA_READY = bool(data_status['exists'].all())\n",
145
+ "\n",
146
+ "display(code_weight_status)\n",
147
+ "display(data_status)\n",
148
+ "print('DATA_READY =', DATA_READY)\n",
149
+ "\n",
150
+ "missing_required = code_weight_status.loc[~code_weight_status['exists'], 'path'].tolist()\n",
151
+ "if missing_required:\n",
152
+ " raise FileNotFoundError('Missing required code/weight assets:\\n' + '\\n'.join(missing_required))\n",
153
+ "\n",
154
+ "if not DATA_READY:\n",
155
+ " print('Some dataset files are missing. This notebook will not launch training until RUN_TRAINING=True and DATA_READY=True.')\n"
156
+ ]
157
+ },
158
+ {
159
+ "cell_type": "code",
160
+ "execution_count": null,
161
+ "metadata": {},
162
+ "outputs": [],
163
+ "source": [
164
+ "packages = {\n",
165
+ " 'torch': 'torch',\n",
166
+ " 'torch_geometric': 'torch_geometric',\n",
167
+ " 'rdkit': 'rdkit',\n",
168
+ " 'networkx': 'networkx',\n",
169
+ " 'sklearn': 'scikit-learn',\n",
170
+ " 'pandas': 'pandas',\n",
171
+ " 'numpy': 'numpy',\n",
172
+ " 'matplotlib': 'matplotlib',\n",
173
+ " 'scipy': 'scipy',\n",
174
+ " 'einops': 'einops',\n",
175
+ " 'local_attention': 'local-attention',\n",
176
+ "}\n",
177
+ "\n",
178
+ "dependency_rows = []\n",
179
+ "for module_name, package_name in packages.items():\n",
180
+ " dependency_rows.append(\n",
181
+ " {\n",
182
+ " 'module': module_name,\n",
183
+ " 'package': package_name,\n",
184
+ " 'available': importlib.util.find_spec(module_name) is not None,\n",
185
+ " }\n",
186
+ " )\n",
187
+ "\n",
188
+ "dependency_status = pd.DataFrame(dependency_rows)\n",
189
+ "display(dependency_status)\n",
190
+ "\n",
191
+ "missing_packages = dependency_status.loc[~dependency_status['available'], 'package'].tolist()\n",
192
+ "if missing_packages:\n",
193
+ " print('Install missing packages in the active notebook environment:')\n",
194
+ " install_cmd = [str(TRAIN_PYTHON), '-m', 'pip', 'install', *missing_packages]\n",
195
+ " print(subprocess.list2cmdline(install_cmd) if os.name == 'nt' else shlex.join(install_cmd))\n"
196
+ ]
197
+ },
198
+ {
199
+ "cell_type": "code",
200
+ "execution_count": null,
201
+ "metadata": {},
202
+ "outputs": [],
203
+ "source": [
204
+ "# Example training parameters.\n",
205
+ "EPOCHS = 500\n",
206
+ "BATCH_SIZE = 256\n",
207
+ "LR = 1e-4\n",
208
+ "N_FOLDS = 1\n",
209
+ "POOL_METHOD = 'max'\n",
210
+ "EVAL_INTERVAL = 5\n",
211
+ "SEED = 42\n",
212
+ "\n",
213
+ "# Domain-generalization mode is the script default. Set to False for k-fold CV.\n",
214
+ "DOMAIN_GENERALIZATION = True\n",
215
+ "FORCE_PREPROCESS = False\n",
216
+ "\n",
217
+ "# Keep False to inspect inputs and commands without starting training.\n",
218
+ "RUN_TRAINING = False\n",
219
+ "\n",
220
+ "cmd = [\n",
221
+ " str(TRAIN_PYTHON), str(TRAIN_SCRIPT),\n",
222
+ " '--checkpoint_path', str(CHECKPOINT_PATH),\n",
223
+ " '--hyper_params_path', str(HYPER_PARAMS_PATH),\n",
224
+ " '--module_data_dir', str(MODULE_DATA_DIR),\n",
225
+ " '--gene_list_path', str(GENE_LIST_PATH),\n",
226
+ " '--gene_mapping_path', str(GENE_MAPPING_PATH),\n",
227
+ " '--gene2vec_path', str(GENE2VEC_PATH),\n",
228
+ " '--symbol2ens_path', str(SYMBOL2ENS_PATH),\n",
229
+ " '--data_dir', str(DATA_DIR),\n",
230
+ " '--output_dir', str(OUTPUT_DIR),\n",
231
+ " '--epochs', str(EPOCHS),\n",
232
+ " '--batch_size', str(BATCH_SIZE),\n",
233
+ " '--lr', str(LR),\n",
234
+ " '--n_folds', str(N_FOLDS),\n",
235
+ " '--pool_method', POOL_METHOD,\n",
236
+ " '--eval_interval', str(EVAL_INTERVAL),\n",
237
+ " '--seed', str(SEED),\n",
238
+ "]\n",
239
+ "\n",
240
+ "if not DOMAIN_GENERALIZATION:\n",
241
+ " cmd.append('--no_domain_gen')\n",
242
+ "if FORCE_PREPROCESS:\n",
243
+ " cmd.append('--force_preprocess')\n",
244
+ "\n",
245
+ "if EVAL_INTERVAL < 1 or EPOCHS < EVAL_INTERVAL:\n",
246
+ " raise ValueError('Use at least one validation interval: EPOCHS >= EVAL_INTERVAL >= 1.')\n",
247
+ "if not DOMAIN_GENERALIZATION and N_FOLDS < 2:\n",
248
+ " raise ValueError('Cross-validation requires N_FOLDS >= 2.')\n",
249
+ "print(subprocess.list2cmdline(cmd) if os.name == 'nt' else shlex.join(cmd))\n"
250
+ ]
251
+ },
252
+ {
253
+ "cell_type": "code",
254
+ "execution_count": null,
255
+ "metadata": {},
256
+ "outputs": [],
257
+ "source": [
258
+ "env = os.environ.copy()\n",
259
+ "env['PYTHONPATH'] = PYTHONPATH\n",
260
+ "\n",
261
+ "if RUN_TRAINING:\n",
262
+ " OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n",
263
+ " if not DATA_READY:\n",
264
+ " missing_data = data_status.loc[~data_status['exists'], 'path'].tolist()\n",
265
+ " raise FileNotFoundError('Data are incomplete:\\n' + '\\n'.join(missing_data))\n",
266
+ " result = subprocess.run(\n",
267
+ " cmd,\n",
268
+ " cwd=str(TASK_ROOT),\n",
269
+ " env=env,\n",
270
+ " text=True,\n",
271
+ " stdout=subprocess.PIPE,\n",
272
+ " stderr=subprocess.STDOUT,\n",
273
+ " check=False,\n",
274
+ " )\n",
275
+ " print(result.stdout)\n",
276
+ " if result.returncode != 0:\n",
277
+ " raise RuntimeError(f'Training failed with return code {result.returncode}')\n",
278
+ "else:\n",
279
+ " print('RUN_TRAINING is False, so no training/smokecheck was launched.')\n",
280
+ " print('Set RUN_TRAINING=True after reviewing the inputs, dependencies, and command.')\n"
281
+ ]
282
+ },
283
+ {
284
+ "cell_type": "code",
285
+ "execution_count": null,
286
+ "metadata": {},
287
+ "outputs": [],
288
+ "source": [
289
+ "if OUTPUT_DIR.exists():\n",
290
+ " output_files = sorted(OUTPUT_DIR.glob('*'))\n",
291
+ " display(pd.DataFrame(\n",
292
+ " [\n",
293
+ " {\n",
294
+ " 'file': f.name,\n",
295
+ " 'size_mb': round(f.stat().st_size / 1024 / 1024, 3),\n",
296
+ " 'path': str(f),\n",
297
+ " }\n",
298
+ " for f in output_files\n",
299
+ " ]\n",
300
+ " ))\n",
301
+ "else:\n",
302
+ " print('No output directory yet:', OUTPUT_DIR)"
303
+ ]
304
+ },
305
+ {
306
+ "cell_type": "markdown",
307
+ "metadata": {},
308
+ "source": [
309
+ "## Output files\n",
310
+ "\n",
311
+ "The training script writes the following files under `OUTPUT_DIR`:\n",
312
+ "\n",
313
+ "- `best_model_fold{i}.pt`: the selected downstream model.\n",
314
+ "- `loss_fold{i}.csv` and `loss_curve_fold{i}.png`: training loss (`epoch`, `avg_loss`).\n",
315
+ "- `metrics_fold{i}.csv`: validation metrics (`epoch`, `AUC`, `PR_AUC`, `ACC`,\n",
316
+ " `BACC`, `PREC`, `TPR`, `KAPPA`, `RECALL`).\n",
317
+ "- `roc_curve_fold{i}.png` and `fpr_tpr_fold{i}.pkl`: the selected validation ROC.\n",
318
+ "- `cell_emb_cache_{pool_method}.npy` and `cell_emb_dim_{pool_method}.txt`: cached\n",
319
+ " cell features when caching is enabled.\n",
320
+ "\n",
321
+ "The following cells use only available local outputs. Missing results are skipped.\n",
322
+ "After changing the weights or input data, use a fresh output directory and set\n",
323
+ "`FORCE_PREPROCESS = True` to rebuild the script's processed-data cache.\n"
324
+ ]
325
+ },
326
+ {
327
+ "cell_type": "markdown",
328
+ "metadata": {},
329
+ "source": [
330
+ "## Data overview plots\n",
331
+ "\n",
332
+ "These plots summarize the drug-combination dataset before training. They are useful for quick notebook demos and GitHub documentation because they do not require a completed model run.\n"
333
+ ]
334
+ },
335
+ {
336
+ "cell_type": "code",
337
+ "execution_count": null,
338
+ "metadata": {},
339
+ "outputs": [],
340
+ "source": [
341
+ "import matplotlib\n",
342
+ "matplotlib.use('Agg')\n",
343
+ "import matplotlib.pyplot as plt\n",
344
+ "from matplotlib.ticker import MaxNLocator\n",
345
+ "\n",
346
+ "PLOT_DIR = OUTPUT_DIR / 'notebook_plots'\n",
347
+ "PLOT_DIR.mkdir(parents=True, exist_ok=True)\n",
348
+ "\n",
349
+ "plt.rcParams.update({\n",
350
+ " 'font.family': 'sans-serif',\n",
351
+ " 'font.sans-serif': ['Arial', 'DejaVu Sans'],\n",
352
+ " 'font.size': 7,\n",
353
+ " 'axes.labelsize': 7,\n",
354
+ " 'axes.titlesize': 8,\n",
355
+ " 'xtick.labelsize': 6,\n",
356
+ " 'ytick.labelsize': 6,\n",
357
+ " 'legend.fontsize': 6,\n",
358
+ " 'axes.linewidth': 0.7,\n",
359
+ " 'pdf.fonttype': 42,\n",
360
+ " 'ps.fonttype': 42,\n",
361
+ " 'svg.fonttype': 'none',\n",
362
+ "})\n",
363
+ "\n",
364
+ "\n",
365
+ "def save_fig(fig, stem: str, dpi: int = 600, show: bool = True):\n",
366
+ " png_path = PLOT_DIR / f'{stem}.png'\n",
367
+ " pdf_path = PLOT_DIR / f'{stem}.pdf'\n",
368
+ " fig.savefig(png_path, dpi=dpi, bbox_inches='tight')\n",
369
+ " fig.savefig(pdf_path, bbox_inches='tight')\n",
370
+ " plt.close(fig)\n",
371
+ " print('Saved:', png_path)\n",
372
+ " print('Saved:', pdf_path)\n",
373
+ " if show and Image is not None:\n",
374
+ " display(Image(filename=str(png_path)))\n",
375
+ " return png_path, pdf_path\n",
376
+ "\n",
377
+ "\n",
378
+ "def despine(ax):\n",
379
+ " ax.spines['top'].set_visible(False)\n",
380
+ " ax.spines['right'].set_visible(False)\n",
381
+ " ax.tick_params(width=0.7, length=2.5)\n",
382
+ "\n",
383
+ "\n",
384
+ "def read_label_table(path: Path) -> pd.DataFrame:\n",
385
+ " df = pd.read_csv(path)\n",
386
+ " lower_map = {str(col).lower(): col for col in df.columns}\n",
387
+ " for required in ['drug1', 'drug2', 'cell', 'label']:\n",
388
+ " if required not in lower_map:\n",
389
+ " raise ValueError(f'{path} is missing a required column similar to {required!r}. Columns: {list(df.columns)}')\n",
390
+ " df = df.rename(columns={lower_map['drug1']: 'drug1', lower_map['drug2']: 'drug2', lower_map['cell']: 'cell', lower_map['label']: 'label'})\n",
391
+ " return df[['drug1', 'drug2', 'cell', 'label']].copy()\n",
392
+ "\n",
393
+ "\n",
394
+ "data_overview = []\n",
395
+ "train_labels = validation_labels = None\n",
396
+ "if expected_data_files['train_labels'].exists():\n",
397
+ " train_labels = read_label_table(expected_data_files['train_labels'])\n",
398
+ " data_overview.append({\n",
399
+ " 'split': 'train',\n",
400
+ " 'rows': len(train_labels),\n",
401
+ " 'positive': int((train_labels['label'] == 1).sum()),\n",
402
+ " 'negative': int((train_labels['label'] == 0).sum()),\n",
403
+ " 'unique_cells': train_labels['cell'].nunique(),\n",
404
+ " 'unique_drug1': train_labels['drug1'].nunique(),\n",
405
+ " 'unique_drug2': train_labels['drug2'].nunique(),\n",
406
+ " 'unique_pairs': train_labels[['drug1', 'drug2']].drop_duplicates().shape[0],\n",
407
+ " })\n",
408
+ "if expected_data_files['validation_labels'].exists():\n",
409
+ " validation_labels = read_label_table(expected_data_files['validation_labels'])\n",
410
+ " data_overview.append({\n",
411
+ " 'split': 'validation',\n",
412
+ " 'rows': len(validation_labels),\n",
413
+ " 'positive': int((validation_labels['label'] == 1).sum()),\n",
414
+ " 'negative': int((validation_labels['label'] == 0).sum()),\n",
415
+ " 'unique_cells': validation_labels['cell'].nunique(),\n",
416
+ " 'unique_drug1': validation_labels['drug1'].nunique(),\n",
417
+ " 'unique_drug2': validation_labels['drug2'].nunique(),\n",
418
+ " 'unique_pairs': validation_labels[['drug1', 'drug2']].drop_duplicates().shape[0],\n",
419
+ " })\n",
420
+ "\n",
421
+ "data_overview_df = pd.DataFrame(data_overview)\n",
422
+ "if data_overview_df.empty:\n",
423
+ " print('No label files found yet, so data overview plots were skipped.')\n",
424
+ "else:\n",
425
+ " data_overview_df.to_csv(PLOT_DIR / 'drug_combination_data_overview.csv', index=False)\n",
426
+ " display(data_overview_df)\n",
427
+ "\n",
428
+ " label_rows = []\n",
429
+ " if train_labels is not None:\n",
430
+ " label_rows.extend([{'split': 'train', 'label': int(k), 'count': int(v)} for k, v in train_labels['label'].value_counts().sort_index().items()])\n",
431
+ " if validation_labels is not None:\n",
432
+ " label_rows.extend([{'split': 'validation', 'label': int(k), 'count': int(v)} for k, v in validation_labels['label'].value_counts().sort_index().items()])\n",
433
+ " label_df = pd.DataFrame(label_rows)\n",
434
+ " label_df.to_csv(PLOT_DIR / 'drug_combination_label_counts.csv', index=False)\n",
435
+ "\n",
436
+ " fig, ax = plt.subplots(figsize=(3.5, 2.4))\n",
437
+ " splits = list(label_df['split'].drop_duplicates())\n",
438
+ " x = np.arange(len(splits))\n",
439
+ " width = 0.34\n",
440
+ " colors = {0: '#4E79A7', 1: '#E15759'}\n",
441
+ " for offset, label in zip([-width / 2, width / 2], [0, 1]):\n",
442
+ " values = [int(label_df[(label_df['split'] == split) & (label_df['label'] == label)]['count'].sum()) for split in splits]\n",
443
+ " ax.bar(x + offset, values, width=width, color=colors[label], edgecolor='#333333', linewidth=0.4, label=f'label {label}')\n",
444
+ " for xpos, value in zip(x + offset, values):\n",
445
+ " ax.text(xpos, value, str(value), ha='center', va='bottom', fontsize=6)\n",
446
+ " ax.set_xticks(x)\n",
447
+ " ax.set_xticklabels(splits, rotation=15, ha='right')\n",
448
+ " ax.set_ylabel('Number of combinations')\n",
449
+ " ax.set_title('Drug-combination label balance')\n",
450
+ " ax.legend(frameon=False)\n",
451
+ " ax.yaxis.set_major_locator(MaxNLocator(integer=True))\n",
452
+ " despine(ax)\n",
453
+ " fig.tight_layout()\n",
454
+ " save_fig(fig, 'drug_combination_label_balance')\n",
455
+ "\n",
456
+ " fig, ax = plt.subplots(figsize=(4.1, 2.5))\n",
457
+ " overview_long = data_overview_df.melt(id_vars='split', value_vars=['unique_cells', 'unique_drug1', 'unique_drug2', 'unique_pairs'], var_name='feature', value_name='count')\n",
458
+ " features = ['unique_cells', 'unique_drug1', 'unique_drug2', 'unique_pairs']\n",
459
+ " x = np.arange(len(features))\n",
460
+ " width = 0.36 if len(splits) > 1 else 0.5\n",
461
+ " palette = ['#59A14F', '#F28E2B']\n",
462
+ " for i, split in enumerate(splits):\n",
463
+ " values = [int(overview_long[(overview_long['split'] == split) & (overview_long['feature'] == feature)]['count'].sum()) for feature in features]\n",
464
+ " ax.bar(x + (i - (len(splits) - 1) / 2) * width, values, width=width, color=palette[i % len(palette)], edgecolor='#333333', linewidth=0.4, label=split)\n",
465
+ " ax.set_xticks(x)\n",
466
+ " ax.set_xticklabels(['cells', 'drug1', 'drug2', 'drug pairs'], rotation=20, ha='right')\n",
467
+ " ax.set_ylabel('Unique count')\n",
468
+ " ax.set_title('Dataset coverage')\n",
469
+ " ax.legend(frameon=False)\n",
470
+ " ax.yaxis.set_major_locator(MaxNLocator(integer=True))\n",
471
+ " despine(ax)\n",
472
+ " fig.tight_layout()\n",
473
+ " save_fig(fig, 'drug_combination_dataset_coverage')\n"
474
+ ]
475
+ },
476
+ {
477
+ "cell_type": "markdown",
478
+ "metadata": {},
479
+ "source": [
480
+ "## Result plots from training outputs\n",
481
+ "\n",
482
+ "Plot the saved training loss, validation metric history, and ROC curves. The\n",
483
+ "current script does not export per-example prediction CSV files, so this section\n",
484
+ "does not assume that precision-recall or confusion-matrix inputs are available.\n",
485
+ "Only open ROC pickle files produced by your own trusted training run.\n"
486
+ ]
487
+ },
488
+ {
489
+ "cell_type": "code",
490
+ "execution_count": null,
491
+ "metadata": {},
492
+ "outputs": [],
493
+ "source": [
494
+ "import pickle\n",
495
+ "from sklearn import metrics\n",
496
+ "\n",
497
+ "\n",
498
+ "def find_fold_files(pattern: str):\n",
499
+ " return sorted(OUTPUT_DIR.glob(pattern))\n",
500
+ "\n",
501
+ "\n",
502
+ "def load_metrics_files():\n",
503
+ " frames = []\n",
504
+ " for path in find_fold_files('metrics_fold*.csv'):\n",
505
+ " df = pd.read_csv(path)\n",
506
+ " stem = path.stem.replace('metrics_fold', '')\n",
507
+ " try:\n",
508
+ " fold = int(stem)\n",
509
+ " except ValueError:\n",
510
+ " fold = len(frames)\n",
511
+ " df['fold'] = fold\n",
512
+ " df['source_file'] = str(path)\n",
513
+ " frames.append(df)\n",
514
+ " return pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()\n",
515
+ "\n",
516
+ "\n",
517
+ "def load_loss_files():\n",
518
+ " frames = []\n",
519
+ " for path in find_fold_files('loss_fold*.csv'):\n",
520
+ " df = pd.read_csv(path)\n",
521
+ " stem = path.stem.replace('loss_fold', '')\n",
522
+ " try:\n",
523
+ " fold = int(stem)\n",
524
+ " except ValueError:\n",
525
+ " fold = len(frames)\n",
526
+ " df['fold'] = fold\n",
527
+ " frames.append(df)\n",
528
+ " return pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()\n",
529
+ "\n",
530
+ "\n",
531
+ "metrics_df = load_metrics_files()\n",
532
+ "loss_df = load_loss_files()\n",
533
+ "\n",
534
+ "if metrics_df.empty:\n",
535
+ " print('No metrics_fold*.csv files found yet:', OUTPUT_DIR)\n",
536
+ "else:\n",
537
+ " metric_cols = [col for col in ['AUC', 'PR_AUC', 'ACC', 'BACC', 'PREC', 'TPR', 'KAPPA', 'RECALL'] if col in metrics_df.columns]\n",
538
+ " best_rows = metrics_df.sort_values(['fold', 'AUC'], ascending=[True, False]).groupby('fold', as_index=False).head(1)\n",
539
+ " summary_rows = []\n",
540
+ " for metric in metric_cols:\n",
541
+ " values = best_rows[metric].astype(float).to_numpy()\n",
542
+ " summary_rows.append({'metric': metric, 'mean': float(np.mean(values)), 'std': float(np.std(values)), 'n_folds': int(len(values))})\n",
543
+ " summary_df = pd.DataFrame(summary_rows)\n",
544
+ " best_rows.to_csv(PLOT_DIR / 'drug_combination_best_metrics_by_fold.csv', index=False)\n",
545
+ " summary_df.to_csv(PLOT_DIR / 'drug_combination_metric_summary.csv', index=False)\n",
546
+ " display(best_rows)\n",
547
+ " display(summary_df)\n",
548
+ "\n",
549
+ " fig, ax = plt.subplots(figsize=(4.1, 2.6))\n",
550
+ " for metric, color in [('AUC', '#4E79A7'), ('PR_AUC', '#E15759'), ('ACC', '#59A14F'), ('BACC', '#F28E2B')]:\n",
551
+ " if metric in metrics_df.columns:\n",
552
+ " for fold, frame in metrics_df.groupby('fold'):\n",
553
+ " ax.plot(frame['epoch'], frame[metric], linewidth=0.8, alpha=0.65, color=color)\n",
554
+ " mean_by_epoch = metrics_df.groupby('epoch')[metric].mean().reset_index()\n",
555
+ " ax.plot(mean_by_epoch['epoch'], mean_by_epoch[metric], linewidth=1.8, color=color, label=metric)\n",
556
+ " ax.set_xlabel('Epoch')\n",
557
+ " ax.set_ylabel('Score')\n",
558
+ " ax.set_ylim(0, 1.02)\n",
559
+ " ax.set_title('Validation metrics over training')\n",
560
+ " ax.legend(frameon=False, ncol=2)\n",
561
+ " ax.grid(axis='y', linestyle='--', linewidth=0.4, alpha=0.3)\n",
562
+ " ax.set_axisbelow(True)\n",
563
+ " despine(ax)\n",
564
+ " fig.tight_layout()\n",
565
+ " save_fig(fig, 'drug_combination_metric_curves')\n",
566
+ "\n",
567
+ " fig, ax = plt.subplots(figsize=(3.8, 2.5))\n",
568
+ " plot_metrics = [m for m in ['AUC', 'PR_AUC', 'ACC', 'BACC', 'PREC', 'TPR', 'KAPPA', 'RECALL'] if m in best_rows.columns]\n",
569
+ " values = [float(best_rows[m].mean()) for m in plot_metrics]\n",
570
+ " colors = ['#4E79A7', '#E15759', '#59A14F', '#F28E2B', '#76B7B2', '#B07AA1', '#9C755F', '#BAB0AC'][:len(plot_metrics)]\n",
571
+ " bars = ax.bar(np.arange(len(plot_metrics)), values, color=colors, edgecolor='#333333', linewidth=0.4)\n",
572
+ " for bar, value in zip(bars, values):\n",
573
+ " ax.text(bar.get_x() + bar.get_width() / 2, value + 0.015, f'{value:.3f}', ha='center', va='bottom', fontsize=6)\n",
574
+ " ax.set_xticks(np.arange(len(plot_metrics)))\n",
575
+ " ax.set_xticklabels(plot_metrics, rotation=25, ha='right')\n",
576
+ " ax.set_ylim(0, 1.05)\n",
577
+ " ax.set_ylabel('Best-fold score')\n",
578
+ " ax.set_title('Drug-combination prediction summary')\n",
579
+ " ax.grid(axis='y', linestyle='--', linewidth=0.4, alpha=0.3)\n",
580
+ " ax.set_axisbelow(True)\n",
581
+ " despine(ax)\n",
582
+ " fig.tight_layout()\n",
583
+ " save_fig(fig, 'drug_combination_best_metric_summary')\n",
584
+ "\n",
585
+ "if loss_df.empty:\n",
586
+ " print('No loss_fold*.csv files found yet:', OUTPUT_DIR)\n",
587
+ "else:\n",
588
+ " fig, ax = plt.subplots(figsize=(4.1, 2.5))\n",
589
+ " for fold, frame in loss_df.groupby('fold'):\n",
590
+ " ax.plot(frame['epoch'], frame['avg_loss'], linewidth=0.8, alpha=0.65, label=f'fold {fold}')\n",
591
+ " mean_loss = loss_df.groupby('epoch')['avg_loss'].mean().reset_index()\n",
592
+ " ax.plot(mean_loss['epoch'], mean_loss['avg_loss'], linewidth=1.8, color='#222222', label='mean')\n",
593
+ " ax.set_xlabel('Epoch')\n",
594
+ " ax.set_ylabel('Average loss')\n",
595
+ " ax.set_title('Training loss')\n",
596
+ " ax.legend(frameon=False, ncol=2)\n",
597
+ " ax.grid(axis='y', linestyle='--', linewidth=0.4, alpha=0.3)\n",
598
+ " ax.set_axisbelow(True)\n",
599
+ " despine(ax)\n",
600
+ " fig.tight_layout()\n",
601
+ " save_fig(fig, 'drug_combination_loss_curves')\n",
602
+ "\n",
603
+ "roc_files = find_fold_files('fpr_tpr_fold*.pkl')\n",
604
+ "if not roc_files:\n",
605
+ " print('No fpr_tpr_fold*.pkl files found yet:', OUTPUT_DIR)\n",
606
+ "else:\n",
607
+ " fig, ax = plt.subplots(figsize=(3.2, 3.2))\n",
608
+ " roc_rows = []\n",
609
+ " for path in roc_files:\n",
610
+ " with path.open('rb') as handle:\n",
611
+ " fpr, tpr = pickle.load(handle)\n",
612
+ " stem = path.stem.replace('fpr_tpr_fold', '')\n",
613
+ " try:\n",
614
+ " fold = int(stem)\n",
615
+ " except ValueError:\n",
616
+ " fold = len(roc_rows)\n",
617
+ " auc_value = float(metrics.auc(fpr, tpr))\n",
618
+ " roc_rows.append({'fold': fold, 'auc': auc_value})\n",
619
+ " ax.plot(fpr, tpr, linewidth=1.0, label=f'fold {fold}, AUROC={auc_value:.3f}')\n",
620
+ " ax.plot([0, 1], [0, 1], linestyle='--', color='#333333', linewidth=0.8, alpha=0.7)\n",
621
+ " ax.set_xlabel('False positive rate')\n",
622
+ " ax.set_ylabel('True positive rate')\n",
623
+ " ax.set_xlim(0, 1)\n",
624
+ " ax.set_ylim(0, 1.02)\n",
625
+ " ax.set_aspect('equal', adjustable='box')\n",
626
+ " ax.legend(frameon=False, loc='lower right')\n",
627
+ " ax.set_title('ROC curve')\n",
628
+ " despine(ax)\n",
629
+ " fig.tight_layout()\n",
630
+ " save_fig(fig, 'drug_combination_roc_curves')\n",
631
+ " pd.DataFrame(roc_rows).to_csv(PLOT_DIR / 'drug_combination_roc_auc_from_curves.csv', index=False)\n"
632
+ ]
633
+ },
634
+ {
635
+ "cell_type": "markdown",
636
+ "metadata": {},
637
+ "source": [
638
+ "## Generated plot files\n",
639
+ "\n",
640
+ "This cell lists the figures and source tables generated by the notebook plotting sections.\n"
641
+ ]
642
+ },
643
+ {
644
+ "cell_type": "code",
645
+ "execution_count": null,
646
+ "metadata": {},
647
+ "outputs": [],
648
+ "source": [
649
+ "plot_files = sorted(PLOT_DIR.glob('*'))\n",
650
+ "if plot_files:\n",
651
+ " display(pd.DataFrame([\n",
652
+ " {'file': path.name, 'size_kb': round(path.stat().st_size / 1024, 2), 'path': str(path)}\n",
653
+ " for path in plot_files\n",
654
+ " ]))\n",
655
+ "else:\n",
656
+ " print('No notebook plot files were generated yet.')\n"
657
+ ]
658
+ }
659
+ ],
660
+ "metadata": {
661
+ "kernelspec": {
662
+ "display_name": "Python 3 (sclong)",
663
+ "language": "python",
664
+ "name": "python3"
665
+ },
666
+ "language_info": {
667
+ "name": "python",
668
+ "pygments_lexer": "ipython3"
669
+ }
670
+ },
671
+ "nbformat": 4,
672
+ "nbformat_minor": 5
673
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BioMod/toturials/drug_response_prediction.ipynb CHANGED
@@ -1,906 +1,871 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "markdown",
5
- "metadata": {},
6
- "source": [
7
- "# Drug response prediction demo: GDSC/CCLE with weight0407 on one GPU\n",
8
- "\n",
9
- "This notebook is a tutorial version of the drug-response launcher based on `train_drug_performer_module.py`. It uses the weight0407 PerformerModule backbone, drug graph features, and CCLE/GDSC response data.\n",
10
- "\n",
11
- "By default, the notebook does not relaunch the full 300-epoch training job. It checks the inputs, builds the exact command, reads the existing `weight0407/drug` outputs, computes metrics, and writes a small tutorial scatter plot. To retrain, set `RUN_TRAINING = True` in the training cell.\n",
12
- "\n",
13
- "The validation set is used for checkpoint selection. Reported metrics describe\n",
14
- "validation performance; this workflow does not produce a separate final test evaluation.\n"
15
- ]
16
- },
17
- {
18
- "cell_type": "markdown",
19
- "metadata": {},
20
- "source": [
21
- "## Privacy-safe absolute paths\n",
22
- "\n",
23
- "The notebook uses this alias path instead of a personal project path:\n",
24
- "\n",
25
- "`/data/share/biomodulefm_demo/single_cell_model`\n",
26
- "\n",
27
- "All paths below are absolute and resolve through that alias on the server."
28
- ]
29
- },
30
- {
31
- "cell_type": "markdown",
32
- "metadata": {},
33
- "source": [
34
- "## What this demo runs\n",
35
- "\n",
36
- "- Task code: `/data/jianqingzhu/BioMod-main/BioMod/drug_response/train_drug_performer_module.py`\n",
37
- "- Drug-response data: `/data/share/biomodulefm_demo/single_cell_model/DATA/drug_data`\n",
38
- "- Weight0407 checkpoint: `/data/share/biomodulefm_demo/single_cell_model/weight/0407weight/mae_tost_48M_cross_attn_only_pure_memory_epoch12.pth`\n",
39
- "- Weight0407 config: `/data/share/biomodulefm_demo/single_cell_model/weight/0407weight/mae_tost_48M_cross_attn_only_pure_memory.pkl`\n",
40
- "- Default tutorial output: `/data/share/biomodulefm_demo/tutorial_runs/drug_response_prediction_weight0407_1gpu_demo`\n",
41
- "- Existing full-run output used for display when tutorial output is absent: `/data/share/biomodulefm_demo/single_cell_model/module_model/result/weight0407/drug`"
42
- ]
43
- },
44
- {
45
- "cell_type": "code",
46
- "execution_count": null,
47
- "metadata": {},
48
- "outputs": [],
49
- "source": [
50
- "from pathlib import Path\n",
51
- "import os\n",
52
- "import shlex\n",
53
- "import subprocess\n",
54
- "import sys\n",
55
- "\n",
56
- "try:\n",
57
- " from IPython.display import Image, display\n",
58
- "except Exception:\n",
59
- " Image = None\n",
60
- " def display(obj):\n",
61
- " print(obj)\n",
62
- "import numpy as np\n",
63
- "import pandas as pd\n",
64
- "from scipy.stats import pearsonr\n",
65
- "\n",
66
- "BASE = Path('/data/share/biomodulefm_demo/single_cell_model')\n",
67
- "MODULE_MODEL = BASE / 'module_model'\n",
68
- "PROJECT_ROOT = Path('/data/jianqingzhu/BioMod-main/BioMod')\n",
69
- "TASK_ROOT = PROJECT_ROOT / 'drug_response'\n",
70
- "SCRIPT = TASK_ROOT / 'train_drug_performer_module.py'\n",
71
- "\n",
72
- "DATA_ROOT = BASE / 'DATA' / 'drug_data'\n",
73
- "WEIGHT_DIR = BASE / 'weight' / '0407weight'\n",
74
- "CKPT_PATH = WEIGHT_DIR / 'mae_tost_48M_cross_attn_only_pure_memory_epoch12.pth'\n",
75
- "CONFIG_PKL = WEIGHT_DIR / 'mae_tost_48M_cross_attn_only_pure_memory.pkl'\n",
76
- "\n",
77
- "MODULE_DATA_DIR = PROJECT_ROOT / 'module_data_all'\n",
78
- "GENE2VEC = MODULE_MODEL / 'selected_gene2vec_27k.npy'\n",
79
- "GENE_MAP = MODULE_MODEL / 'gene2ens.csv'\n",
80
- "GENE_LIST = MODULE_MODEL / 'selected_genes_27k.txt'\n",
81
- "\n",
82
- "TUTORIAL_RUN_ROOT = Path('/data/share/biomodulefm_demo/tutorial_runs')\n",
83
- "RESULT_DIR = TUTORIAL_RUN_ROOT / 'drug_response_prediction_weight0407_1gpu_demo'\n",
84
- "REFERENCE_RESULT_DIR = MODULE_MODEL / 'result' / 'weight0407' / 'drug'\n",
85
- "RESULT_DIR.mkdir(parents=True, exist_ok=True)\n",
86
- "\n",
87
- "paths = pd.DataFrame([\n",
88
- " {'name': 'project_alias', 'path': BASE},\n",
89
- " {'name': 'script', 'path': SCRIPT},\n",
90
- " {'name': 'data_root', 'path': DATA_ROOT},\n",
91
- " {'name': 'checkpoint', 'path': CKPT_PATH},\n",
92
- " {'name': 'config_pkl', 'path': CONFIG_PKL},\n",
93
- " {'name': 'module_data_dir', 'path': MODULE_DATA_DIR},\n",
94
- " {'name': 'gene_list', 'path': GENE_LIST},\n",
95
- " {'name': 'gene_map', 'path': GENE_MAP},\n",
96
- " {'name': 'gene2vec', 'path': GENE2VEC},\n",
97
- " {'name': 'tutorial_result_dir', 'path': RESULT_DIR},\n",
98
- " {'name': 'reference_result_dir', 'path': REFERENCE_RESULT_DIR},\n",
99
- "])\n",
100
- "display(paths)\n"
101
- ]
102
- },
103
- {
104
- "cell_type": "code",
105
- "execution_count": null,
106
- "metadata": {},
107
- "outputs": [],
108
- "source": [
109
- "required_files = [\n",
110
- " SCRIPT,\n",
111
- " CKPT_PATH,\n",
112
- " CONFIG_PKL,\n",
113
- " GENE_LIST,\n",
114
- " GENE_MAP,\n",
115
- " GENE2VEC,\n",
116
- " DATA_ROOT / 'CCLE' / 'GDSC_IC50.csv',\n",
117
- " DATA_ROOT / 'CCLE' / 'genomic_expression_561celllines_697genes_demap_features.csv',\n",
118
- " DATA_ROOT / 'CCLE' / 'genomic_mutation_34673_demap_features.csv',\n",
119
- " DATA_ROOT / 'CCLE' / 'genomic_copynumber_561celllines_710genes_demap_features.csv',\n",
120
- " DATA_ROOT / 'CCLE' / 'genomic_methylation_561celllines_808genes_demap_features.csv',\n",
121
- " DATA_ROOT / 'GDSC' / 'drug_graph_feat.zip',\n",
122
- "]\n",
123
- "required_dirs = [PROJECT_ROOT, TASK_ROOT, DATA_ROOT, MODULE_DATA_DIR]\n",
124
- "\n",
125
- "missing = [str(p) for p in required_files if not p.exists()]\n",
126
- "missing += [str(p) for p in required_dirs if not p.is_dir()]\n",
127
- "if missing:\n",
128
- " raise FileNotFoundError('Missing required inputs:\\n' + '\\n'.join(missing))\n",
129
- "\n",
130
- "print('All required files and directories are present.')\n"
131
- ]
132
- },
133
- {
134
- "cell_type": "markdown",
135
- "metadata": {},
136
- "source": [
137
- "## Runtime setup\n",
138
- "\n",
139
- "The full training script was originally run in the `sclong` conda environment. If this notebook kernel is different, the command cell below still points the subprocess to `/opt/conda/envs/sclong/bin/python` when that interpreter exists."
140
- ]
141
- },
142
- {
143
- "cell_type": "code",
144
- "execution_count": null,
145
- "metadata": {},
146
- "outputs": [],
147
- "source": [
148
- "os.environ['CUDA_VISIBLE_DEVICES'] = '0'\n",
149
- "os.environ['PYTHONUNBUFFERED'] = '1'\n",
150
- "os.environ.setdefault('OMP_NUM_THREADS', '8')\n",
151
- "os.environ.setdefault('OPENBLAS_NUM_THREADS', '8')\n",
152
- "os.environ.setdefault('MKL_NUM_THREADS', '8')\n",
153
- "\n",
154
- "SCLONG_PYTHON = Path('/opt/conda/envs/sclong/bin/python')\n",
155
- "TRAIN_PYTHON = SCLONG_PYTHON if SCLONG_PYTHON.exists() else Path(sys.executable)\n",
156
- "print('Notebook Python:', sys.executable)\n",
157
- "print('Training Python:', TRAIN_PYTHON)\n",
158
- "\n",
159
- "try:\n",
160
- " import torch\n",
161
- " print('torch:', torch.__version__)\n",
162
- " print('cuda available:', torch.cuda.is_available())\n",
163
- " if torch.cuda.is_available():\n",
164
- " print('cuda device:', torch.cuda.get_device_name(0))\n",
165
- "except Exception as exc:\n",
166
- " print('Torch check skipped:', repr(exc))\n",
167
- "\n",
168
- "try:\n",
169
- " import hickle\n",
170
- " print('hickle:', getattr(hickle, '__version__', 'ok'))\n",
171
- "except Exception:\n",
172
- " subprocess.check_call([\n",
173
- " str(TRAIN_PYTHON), '-m', 'pip', 'install',\n",
174
- " '-i', 'https://pypi.tuna.tsinghua.edu.cn/simple',\n",
175
- " 'hickle',\n",
176
- " ])\n",
177
- " import hickle\n",
178
- " print('hickle:', getattr(hickle, '__version__', 'ok'))\n"
179
- ]
180
- },
181
- {
182
- "cell_type": "markdown",
183
- "metadata": {},
184
- "source": [
185
- "## Inspect input files\n",
186
- "\n",
187
- "This cell gives a quick sanity check for the drug-response files without loading the large full matrices into memory."
188
- ]
189
- },
190
- {
191
- "cell_type": "code",
192
- "execution_count": null,
193
- "metadata": {},
194
- "outputs": [],
195
- "source": [
196
- "def size_mb(path: Path) -> float:\n",
197
- " return path.stat().st_size / 1024 / 1024\n",
198
- "\n",
199
- "input_summary = []\n",
200
- "for path in sorted((DATA_ROOT / 'CCLE').glob('*')):\n",
201
- " if path.is_file():\n",
202
- " input_summary.append({'file': str(path.relative_to(DATA_ROOT)), 'size_mb': round(size_mb(path), 2)})\n",
203
- "for path in sorted((DATA_ROOT / 'GDSC').glob('*')):\n",
204
- " if path.is_file():\n",
205
- " input_summary.append({'file': str(path.relative_to(DATA_ROOT)), 'size_mb': round(size_mb(path), 2)})\n",
206
- "\n",
207
- "display(pd.DataFrame(input_summary))\n",
208
- "\n",
209
- "ic50_path = DATA_ROOT / 'CCLE' / 'GDSC_IC50.csv'\n",
210
- "ic50_preview = pd.read_csv(ic50_path, nrows=5)\n",
211
- "display(ic50_preview)\n"
212
- ]
213
- },
214
- {
215
- "cell_type": "markdown",
216
- "metadata": {},
217
- "source": [
218
- "## Training command\n",
219
- "\n",
220
- "This is the weight0407 absolute-path version of the older launch command. The tutorial result directory is separate from the existing full-run result directory so reruns do not overwrite paper-facing outputs by accident."
221
- ]
222
- },
223
- {
224
- "cell_type": "code",
225
- "execution_count": null,
226
- "metadata": {},
227
- "outputs": [],
228
- "source": [
229
- "EPOCHS = 300\n",
230
- "BATCH_SIZE = 32\n",
231
- "POOL_METHOD = 'max'\n",
232
- "\n",
233
- "cmd = [\n",
234
- " str(TRAIN_PYTHON), str(SCRIPT),\n",
235
- " '--gpu_id', '0',\n",
236
- " '--data_path', str(DATA_ROOT),\n",
237
- " '--checkpoint_path', str(CKPT_PATH),\n",
238
- " '--hyper_params_path', str(CONFIG_PKL),\n",
239
- " '--epochs', str(EPOCHS),\n",
240
- " '--batch_size', str(BATCH_SIZE),\n",
241
- " '--module_data_dir', str(MODULE_DATA_DIR),\n",
242
- " '--gene_list_path', str(GENE_LIST),\n",
243
- " '--gene_mapping_path', str(GENE_MAP),\n",
244
- " '--symbol2ens_path', str(GENE_MAP),\n",
245
- " '--gene2vec_path', str(GENE2VEC),\n",
246
- " '--save_dir', str(RESULT_DIR),\n",
247
- " '--pool_method', POOL_METHOD,\n",
248
- "]\n",
249
- "\n",
250
- "print('cd', shlex.quote(str(TASK_ROOT)))\n",
251
- "print(' '.join(shlex.quote(part) for part in cmd))\n"
252
- ]
253
- },
254
- {
255
- "cell_type": "code",
256
- "execution_count": null,
257
- "metadata": {},
258
- "outputs": [],
259
- "source": [
260
- "RUN_TRAINING = False\n",
261
- "\n",
262
- "if RUN_TRAINING:\n",
263
- " RESULT_DIR.mkdir(parents=True, exist_ok=True)\n",
264
- " log_path = RESULT_DIR / 'train.log'\n",
265
- " print('Writing log to:', log_path)\n",
266
- " with log_path.open('w') as log_file:\n",
267
- " proc = subprocess.Popen(\n",
268
- " cmd,\n",
269
- " cwd=str(TASK_ROOT),\n",
270
- " stdout=subprocess.PIPE,\n",
271
- " stderr=subprocess.STDOUT,\n",
272
- " text=True,\n",
273
- " bufsize=1,\n",
274
- " )\n",
275
- " assert proc.stdout is not None\n",
276
- " for line in proc.stdout:\n",
277
- " print(line, end='')\n",
278
- " log_file.write(line)\n",
279
- " return_code = proc.wait()\n",
280
- " if return_code != 0:\n",
281
- " raise RuntimeError(f'Training failed with exit code {return_code}')\n",
282
- "else:\n",
283
- " print('Training was not launched. Set RUN_TRAINING = True to run the full job.')\n",
284
- " print('The following cells will use tutorial outputs if present; otherwise they use the existing weight0407/drug outputs.')\n"
285
- ]
286
- },
287
- {
288
- "cell_type": "markdown",
289
- "metadata": {},
290
- "source": [
291
- "## Inspect outputs\n",
292
- "\n",
293
- "The active result directory is chosen in this order: first the tutorial run directory, then the existing `weight0407/drug` full-run directory."
294
- ]
295
- },
296
- {
297
- "cell_type": "code",
298
- "execution_count": null,
299
- "metadata": {},
300
- "outputs": [],
301
- "source": [
302
- "candidate_dirs = [RESULT_DIR, REFERENCE_RESULT_DIR]\n",
303
- "ACTIVE_RESULT_DIR = None\n",
304
- "for candidate in candidate_dirs:\n",
305
- " if (candidate / 'predictions.npz').exists():\n",
306
- " ACTIVE_RESULT_DIR = candidate\n",
307
- " break\n",
308
- "\n",
309
- "if ACTIVE_RESULT_DIR is None:\n",
310
- " raise FileNotFoundError('No predictions.npz found. Run training first or check REFERENCE_RESULT_DIR.')\n",
311
- "\n",
312
- "print('Active result directory:', ACTIVE_RESULT_DIR)\n",
313
- "rows = []\n",
314
- "for path in sorted(ACTIVE_RESULT_DIR.rglob('*')):\n",
315
- " if path.is_file():\n",
316
- " rows.append({'file': str(path.relative_to(ACTIVE_RESULT_DIR)), 'size_mb': round(size_mb(path), 3)})\n",
317
- "display(pd.DataFrame(rows))\n"
318
- ]
319
- },
320
- {
321
- "cell_type": "code",
322
- "execution_count": null,
323
- "metadata": {},
324
- "outputs": [],
325
- "source": [
326
- "pred_path = ACTIVE_RESULT_DIR / 'predictions.npz'\n",
327
- "pred_npz = np.load(pred_path, allow_pickle=True)\n",
328
- "print('prediction keys:', pred_npz.files)\n",
329
- "\n",
330
- "pred = pred_npz['pred'] if 'pred' in pred_npz.files else pred_npz['preds']\n",
331
- "# Prefer the validation key; accept historical output files as well.\n",
332
- "if 'validation' in pred_npz.files:\n",
333
- " true = pred_npz['validation']\n",
334
- "elif 'test' in pred_npz.files:\n",
335
- " true = pred_npz['test']\n",
336
- "else:\n",
337
- " true = pred_npz['true']\n",
338
- "\n",
339
- "pcc = pearsonr(pred, true)[0]\n",
340
- "mae = np.mean(np.abs(pred - true))\n",
341
- "rmse = np.sqrt(np.mean((pred - true) ** 2))\n",
342
- "summary = pd.DataFrame([{\n",
343
- " 'n_validation': len(pred),\n",
344
- " 'pcc': pcc,\n",
345
- " 'mae': mae,\n",
346
- " 'rmse': rmse,\n",
347
- "}])\n",
348
- "display(summary)\n",
349
- "\n",
350
- "meta = pd.DataFrame({\n",
351
- " 'pred': pred,\n",
352
- " 'true': true,\n",
353
- " 'drug_id': pred_npz['drug_id'] if 'drug_id' in pred_npz.files else ['NA'] * len(pred),\n",
354
- " 'cell_line_id': pred_npz['cell_line_id'] if 'cell_line_id' in pred_npz.files else ['NA'] * len(pred),\n",
355
- " 'cancer_type': pred_npz['cancer_type'] if 'cancer_type' in pred_npz.files else ['NA'] * len(pred),\n",
356
- "})\n",
357
- "display(meta.head())\n"
358
- ]
359
- },
360
- {
361
- "cell_type": "code",
362
- "execution_count": null,
363
- "metadata": {},
364
- "outputs": [],
365
- "source": [
366
- "import random\n",
367
- "from scipy.stats import spearmanr\n",
368
- "\n",
369
- "TCGA_LABEL_SET = [\n",
370
- " 'ALL', 'BLCA', 'BRCA', 'CESC', 'DLBC', 'LIHC', 'LUAD', 'ESCA', 'GBM',\n",
371
- " 'HNSC', 'KIRC', 'LAML', 'LCML', 'LGG', 'LUSC', 'MESO', 'MM', 'NB',\n",
372
- " 'OV', 'PAAD', 'SCLC', 'SKCM', 'STAD', 'THCA', 'COAD/READ',\n",
373
- "]\n",
374
- "\n",
375
- "\n",
376
- "def pearson_corr(x, y):\n",
377
- " x = np.asarray(x, dtype=float).reshape(-1)\n",
378
- " y = np.asarray(y, dtype=float).reshape(-1)\n",
379
- " mask = np.isfinite(x) & np.isfinite(y)\n",
380
- " x = x[mask]\n",
381
- " y = y[mask]\n",
382
- " if len(x) < 2 or np.std(x) == 0 or np.std(y) == 0:\n",
383
- " return np.nan\n",
384
- " return float(np.corrcoef(x, y)[0, 1])\n",
385
- "\n",
386
- "\n",
387
- "def regression_metrics(frame: pd.DataFrame) -> dict:\n",
388
- " y_true = frame['true'].astype(float).to_numpy()\n",
389
- " y_pred = frame['pred'].astype(float).to_numpy()\n",
390
- " return {\n",
391
- " 'n': int(len(frame)),\n",
392
- " 'pcc': pearson_corr(y_true, y_pred),\n",
393
- " 'spearman': float(spearmanr(y_true, y_pred).correlation) if len(frame) >= 2 else np.nan,\n",
394
- " 'mae': float(np.mean(np.abs(y_pred - y_true))),\n",
395
- " 'mse': float(np.mean((y_pred - y_true) ** 2)),\n",
396
- " 'rmse': float(np.sqrt(np.mean((y_pred - y_true) ** 2))),\n",
397
- " }\n",
398
- "\n",
399
- "\n",
400
- "def reconstruct_validation_metadata(data_root: Path, n_expected: int, ratio: float = 0.95, seed: int = 0) -> pd.DataFrame:\n",
401
- " \"\"\"Rebuild the validation split metadata using the same deterministic split as the training script.\"\"\"\n",
402
- " drug_info_file = data_root / 'GDSC' / '1.Drug_listMon Jun 24 09_00_55 2019.csv'\n",
403
- " cell_line_info_file = data_root / 'CCLE' / 'Cell_lines_annotations_20181226.txt'\n",
404
- " drug_feature_dir = data_root / 'GDSC' / 'drug_graph_feat'\n",
405
- " gene_expression_file = data_root / 'CCLE' / 'genomic_expression_561celllines_697genes_demap_features.csv'\n",
406
- " ic50_file = data_root / 'CCLE' / 'GDSC_IC50.csv'\n",
407
- "\n",
408
- " for required in [drug_info_file, cell_line_info_file, drug_feature_dir, gene_expression_file, ic50_file]:\n",
409
- " if not required.exists():\n",
410
- " raise FileNotFoundError(required)\n",
411
- "\n",
412
- " drug_info = pd.read_csv(drug_info_file)\n",
413
- " drugid2pubchemid = {\n",
414
- " str(row.iloc[0]): str(row.iloc[5])\n",
415
- " for _, row in drug_info.iterrows()\n",
416
- " if str(row.iloc[5]).isdigit()\n",
417
- " }\n",
418
- "\n",
419
- " cellline2cancertype = {}\n",
420
- " with cell_line_info_file.open() as handle:\n",
421
- " for line in handle.readlines()[1:]:\n",
422
- " parts = line.rstrip('\\n').split('\\t')\n",
423
- " if len(parts) >= 2:\n",
424
- " cellline2cancertype[parts[1]] = parts[-1]\n",
425
- "\n",
426
- " gexpr_index = set(pd.read_csv(gene_expression_file, usecols=[0]).iloc[:, 0].astype(str))\n",
427
- " drug_pubchem_ids = {path.stem for path in drug_feature_dir.glob('*.hkl')}\n",
428
- "\n",
429
- " ic50 = pd.read_csv(ic50_file, header=0, index_col=0)\n",
430
- " drug_match_list = [idx for idx in ic50.index if idx.split(':')[1] in drugid2pubchemid]\n",
431
- " ic50 = ic50.loc[drug_match_list]\n",
432
- "\n",
433
- " data_idx = []\n",
434
- " for each_drug in ic50.index:\n",
435
- " pubchem_id = drugid2pubchemid[each_drug.split(':')[-1]]\n",
436
- " if pubchem_id not in drug_pubchem_ids:\n",
437
- " continue\n",
438
- " for each_cellline in ic50.columns:\n",
439
- " if each_cellline not in gexpr_index or each_cellline not in cellline2cancertype:\n",
440
- " continue\n",
441
- " value = ic50.loc[each_drug, each_cellline]\n",
442
- " if pd.isna(value):\n",
443
- " continue\n",
444
- " data_idx.append((each_cellline, pubchem_id, float(value), cellline2cancertype[each_cellline]))\n",
445
- "\n",
446
- " rng = random.Random(seed)\n",
447
- " validation_idx = []\n",
448
- " for each_type in TCGA_LABEL_SET:\n",
449
- " subtype = [item for item in data_idx if item[-1] == each_type]\n",
450
- " if not subtype:\n",
451
- " continue\n",
452
- " k = max(1, int(ratio * len(subtype)))\n",
453
- " train_list = rng.sample(subtype, k)\n",
454
- " validation_idx.extend([item for item in subtype if item not in train_list])\n",
455
- "\n",
456
- " reconstructed = pd.DataFrame(validation_idx, columns=['cell_line_id', 'drug_id', 'true_from_metadata', 'cancer_type'])\n",
457
- " if len(reconstructed) != n_expected:\n",
458
- " raise ValueError(f'Reconstructed {len(reconstructed)} rows, but predictions contain {n_expected} rows.')\n",
459
- " return reconstructed[['drug_id', 'cell_line_id', 'cancer_type']]\n",
460
- "\n",
461
- "\n",
462
- "analysis_df = meta.copy()\n",
463
- "analysis_df['pred'] = pd.to_numeric(analysis_df['pred'], errors='coerce')\n",
464
- "analysis_df['true'] = pd.to_numeric(analysis_df['true'], errors='coerce')\n",
465
- "analysis_df = analysis_df.replace([np.inf, -np.inf], np.nan).dropna(subset=['pred', 'true']).reset_index(drop=True)\n",
466
- "\n",
467
- "metadata_cols = ['drug_id', 'cell_line_id', 'cancer_type']\n",
468
- "metadata_missing = any(col not in analysis_df.columns for col in metadata_cols) or all(\n",
469
- " analysis_df[col].astype(str).isin(['NA', 'nan', 'None', '']).all() for col in metadata_cols if col in analysis_df.columns\n",
470
- ")\n",
471
- "\n",
472
- "if metadata_missing:\n",
473
- " try:\n",
474
- " recovered_meta = reconstruct_validation_metadata(DATA_ROOT, n_expected=len(analysis_df), ratio=0.95, seed=0)\n",
475
- " for col in metadata_cols:\n",
476
- " analysis_df[col] = recovered_meta[col].to_numpy()\n",
477
- " print('Recovered validation metadata from the original GDSC/CCLE split.')\n",
478
- " except Exception as exc:\n",
479
- " print('Metadata recovery skipped:', repr(exc))\n",
480
- " for col in metadata_cols:\n",
481
- " if col not in analysis_df.columns:\n",
482
- " analysis_df[col] = 'NA'\n",
483
- "else:\n",
484
- " print('Using drug/cell/cancer metadata stored in predictions.npz.')\n",
485
- "\n",
486
- "overall_metrics = pd.DataFrame([regression_metrics(analysis_df)])\n",
487
- "display(overall_metrics)\n",
488
- "display(analysis_df.head())\n",
489
- "\n",
490
- "metric_columns = ['pred', 'true']\n",
491
- "by_cancer = (\n",
492
- " analysis_df.groupby('cancer_type', dropna=False)[metric_columns]\n",
493
- " .apply(lambda frame: pd.Series(regression_metrics(frame)))\n",
494
- " .reset_index()\n",
495
- " .sort_values('n', ascending=False)\n",
496
- ")\n",
497
- "by_drug = (\n",
498
- " analysis_df.groupby('drug_id', dropna=False)[metric_columns]\n",
499
- " .apply(lambda frame: pd.Series(regression_metrics(frame)))\n",
500
- " .reset_index()\n",
501
- " .sort_values('pcc', ascending=False)\n",
502
- ")\n",
503
- "\n",
504
- "display(by_cancer.head(12))\n",
505
- "display(by_drug.head(10))\n"
506
- ]
507
- },
508
- {
509
- "cell_type": "markdown",
510
- "metadata": {},
511
- "source": [
512
- "## Tutorial scatter plot\n",
513
- "\n",
514
- "This cell creates a compact PDF and PNG summary of predicted versus measured drug response. The output is written to the tutorial result directory even when metrics are read from the existing full-run directory."
515
- ]
516
- },
517
- {
518
- "cell_type": "code",
519
- "execution_count": null,
520
- "metadata": {},
521
- "outputs": [],
522
- "source": [
523
- "import matplotlib.pyplot as plt\n",
524
- "\n",
525
- "plt.rcParams.update({\n",
526
- " 'font.family': 'sans-serif',\n",
527
- " 'font.sans-serif': ['Arial', 'DejaVu Sans'],\n",
528
- " 'font.size': 7,\n",
529
- " 'pdf.fonttype': 42,\n",
530
- " 'ps.fonttype': 42,\n",
531
- "})\n",
532
- "\n",
533
- "scatter_png = RESULT_DIR / 'drug_response_prediction_scatter.png'\n",
534
- "scatter_pdf = RESULT_DIR / 'drug_response_prediction_scatter.pdf'\n",
535
- "\n",
536
- "low = float(min(np.min(true), np.min(pred)))\n",
537
- "high = float(max(np.max(true), np.max(pred)))\n",
538
- "pad = 0.05 * (high - low)\n",
539
- "lims = (low - pad, high + pad)\n",
540
- "\n",
541
- "fig, ax = plt.subplots(figsize=(3.35, 3.1))\n",
542
- "ax.scatter(true, pred, s=7, alpha=0.45, linewidths=0, color='#2f6fb0')\n",
543
- "ax.plot(lims, lims, color='#444444', linewidth=0.8, linestyle='--')\n",
544
- "ax.set_xlim(lims)\n",
545
- "ax.set_ylim(lims)\n",
546
- "ax.set_xlabel('Measured drug response')\n",
547
- "ax.set_ylabel('Predicted drug response')\n",
548
- "ax.set_title('Drug response prediction')\n",
549
- "ax.text(0.03, 0.97, f'PCC = {pcc:.3f}\\nN = {len(pred)}', transform=ax.transAxes, va='top')\n",
550
- "ax.spines['top'].set_visible(False)\n",
551
- "ax.spines['right'].set_visible(False)\n",
552
- "fig.tight_layout()\n",
553
- "fig.savefig(scatter_png, dpi=300, bbox_inches='tight')\n",
554
- "fig.savefig(scatter_pdf, bbox_inches='tight')\n",
555
- "plt.close(fig)\n",
556
- "\n",
557
- "print('Saved:', scatter_png)\n",
558
- "print('Saved:', scatter_pdf)\n",
559
- "if Image is not None:\n",
560
- " display(Image(filename=str(scatter_png)))\n"
561
- ]
562
- },
563
- {
564
- "cell_type": "markdown",
565
- "metadata": {},
566
- "source": [
567
- "## Paper-style drug-response plots\n",
568
- "\n",
569
- "The original plotting scripts under `module_model/draw` use the same core outputs: `predictions.npz`, Pearson correlation, per-drug grouping, and a median-threshold ROC. The cells below bring those plotting routines into the notebook and save editable PDF plus high-resolution PNG copies under the tutorial output directory.\n"
570
- ]
571
- },
572
- {
573
- "cell_type": "code",
574
- "execution_count": null,
575
- "metadata": {},
576
- "outputs": [],
577
- "source": [
578
- "from matplotlib.ticker import FormatStrFormatter\n",
579
- "\n",
580
- "try:\n",
581
- " from sklearn.metrics import roc_curve, roc_auc_score\n",
582
- "except Exception as exc:\n",
583
- " roc_curve = None\n",
584
- " roc_auc_score = None\n",
585
- " print('ROC dependencies unavailable:', repr(exc))\n",
586
- "\n",
587
- "PAPER_PLOT_DIR = RESULT_DIR / 'drug_response_paper_plots'\n",
588
- "PAPER_PLOT_DIR.mkdir(parents=True, exist_ok=True)\n",
589
- "\n",
590
- "plt.rcParams.update({\n",
591
- " 'font.family': 'sans-serif',\n",
592
- " 'font.sans-serif': ['Arial', 'DejaVu Sans'],\n",
593
- " 'font.size': 7,\n",
594
- " 'axes.labelsize': 7,\n",
595
- " 'axes.titlesize': 8,\n",
596
- " 'xtick.labelsize': 6,\n",
597
- " 'ytick.labelsize': 6,\n",
598
- " 'legend.fontsize': 6,\n",
599
- " 'axes.linewidth': 0.7,\n",
600
- " 'pdf.fonttype': 42,\n",
601
- " 'ps.fonttype': 42,\n",
602
- " 'svg.fonttype': 'none',\n",
603
- "})\n",
604
- "\n",
605
- "\n",
606
- "def save_figure(fig, stem: str, dpi: int = 600):\n",
607
- " png_path = PAPER_PLOT_DIR / f'{stem}.png'\n",
608
- " pdf_path = PAPER_PLOT_DIR / f'{stem}.pdf'\n",
609
- " fig.savefig(png_path, dpi=dpi, bbox_inches='tight')\n",
610
- " fig.savefig(pdf_path, bbox_inches='tight')\n",
611
- " plt.close(fig)\n",
612
- " print('Saved:', png_path)\n",
613
- " print('Saved:', pdf_path)\n",
614
- " return png_path, pdf_path\n",
615
- "\n",
616
- "\n",
617
- "def despine(ax):\n",
618
- " ax.spines['top'].set_visible(False)\n",
619
- " ax.spines['right'].set_visible(False)\n",
620
- " ax.tick_params(width=0.7, length=2.5)\n",
621
- "\n",
622
- "\n",
623
- "def finite_xy(frame: pd.DataFrame):\n",
624
- " y_true = pd.to_numeric(frame['true'], errors='coerce').to_numpy(dtype=float)\n",
625
- " y_pred = pd.to_numeric(frame['pred'], errors='coerce').to_numpy(dtype=float)\n",
626
- " mask = np.isfinite(y_true) & np.isfinite(y_pred)\n",
627
- " return y_true[mask], y_pred[mask]\n",
628
- "\n",
629
- "\n",
630
- "# 1) Overall measured-vs-predicted scatter.\n",
631
- "y_true, y_pred = finite_xy(analysis_df)\n",
632
- "low = float(min(y_true.min(), y_pred.min()))\n",
633
- "high = float(max(y_true.max(), y_pred.max()))\n",
634
- "pad = 0.05 * (high - low)\n",
635
- "lims = (low - pad, high + pad)\n",
636
- "metrics = regression_metrics(analysis_df)\n",
637
- "\n",
638
- "fig, ax = plt.subplots(figsize=(3.35, 3.15))\n",
639
- "ax.scatter(y_true, y_pred, s=5, alpha=0.35, linewidths=0, color='#2F6FB0', rasterized=True)\n",
640
- "ax.plot(lims, lims, color='#333333', linewidth=0.8, linestyle='--')\n",
641
- "ax.set_xlim(lims)\n",
642
- "ax.set_ylim(lims)\n",
643
- "ax.set_xlabel('Measured drug response')\n",
644
- "ax.set_ylabel('Predicted drug response')\n",
645
- "ax.set_title('BioMod drug response')\n",
646
- "ax.text(\n",
647
- " 0.03, 0.97,\n",
648
- " f'PCC = {metrics[\"pcc\"]:.3f}\\nRMSE = {metrics[\"rmse\"]:.3f}\\nN = {metrics[\"n\"]}',\n",
649
- " transform=ax.transAxes,\n",
650
- " va='top',\n",
651
- " ha='left',\n",
652
- ")\n",
653
- "despine(ax)\n",
654
- "fig.tight_layout()\n",
655
- "overall_scatter_png, overall_scatter_pdf = save_figure(fig, 'drug_response_overall_scatter')\n",
656
- "\n",
657
- "# 2) Residual distribution.\n",
658
- "residual = y_pred - y_true\n",
659
- "fig, ax = plt.subplots(figsize=(3.35, 2.35))\n",
660
- "ax.hist(residual, bins=45, color='#76B7B2', edgecolor='white', linewidth=0.3)\n",
661
- "ax.axvline(0, color='#333333', linestyle='--', linewidth=0.8)\n",
662
- "ax.set_xlabel('Prediction residual')\n",
663
- "ax.set_ylabel('Number of samples')\n",
664
- "ax.set_title('Residual distribution')\n",
665
- "despine(ax)\n",
666
- "fig.tight_layout()\n",
667
- "residual_png, residual_pdf = save_figure(fig, 'drug_response_residual_distribution')\n",
668
- "\n",
669
- "# 3) Top-5 most precise drugs, matching the logic of plot_drug_response_panel_d.py.\n",
670
- "MIN_SAMPLES_PER_DRUG = 3\n",
671
- "valid_drug_stats = by_drug[(by_drug['drug_id'].astype(str) != 'NA') & (by_drug['n'] >= MIN_SAMPLES_PER_DRUG)].dropna(subset=['pcc'])\n",
672
- "if len(valid_drug_stats) > 0:\n",
673
- " top5_drugs = valid_drug_stats.sort_values('pcc', ascending=False).head(5)['drug_id'].astype(str).tolist()\n",
674
- " top5_summary = valid_drug_stats[valid_drug_stats['drug_id'].astype(str).isin(top5_drugs)].copy()\n",
675
- " top5_points = analysis_df[analysis_df['drug_id'].astype(str).isin(top5_drugs)].copy()\n",
676
- " top5_summary.to_csv(PAPER_PLOT_DIR / 'top5_drug_pcc_summary.csv', index=False)\n",
677
- " top5_points.to_csv(PAPER_PLOT_DIR / 'top5_drug_selected_points.csv', index=False)\n",
678
- "\n",
679
- " fig, ax = plt.subplots(figsize=(3.65, 3.25))\n",
680
- " palette = plt.get_cmap('tab10')\n",
681
- " for idx, drug_id in enumerate(top5_drugs):\n",
682
- " sub = top5_points[top5_points['drug_id'].astype(str) == drug_id]\n",
683
- " ax.scatter(\n",
684
- " sub['true'], sub['pred'],\n",
685
- " s=10, alpha=0.75, linewidths=0,\n",
686
- " color=palette(idx), label=str(drug_id), rasterized=True,\n",
687
- " )\n",
688
- " t_true, t_pred = finite_xy(top5_points)\n",
689
- " low = float(min(t_true.min(), t_pred.min()))\n",
690
- " high = float(max(t_true.max(), t_pred.max()))\n",
691
- " pad = 0.05 * (high - low)\n",
692
- " lims = (low - pad, high + pad)\n",
693
- " ax.plot(lims, lims, color='#333333', linewidth=0.8, linestyle='--')\n",
694
- " ax.set_xlim(lims)\n",
695
- " ax.set_ylim(lims)\n",
696
- " ax.set_xlabel('Measured drug response')\n",
697
- " ax.set_ylabel('Predicted drug response')\n",
698
- " ax.set_title('Most precise 5 drugs')\n",
699
- " ax.legend(title='PubChem ID', frameon=False, loc='best', handletextpad=0.3, borderpad=0.2)\n",
700
- " despine(ax)\n",
701
- " fig.tight_layout()\n",
702
- " top5_png, top5_pdf = save_figure(fig, 'drug_response_top5_drugs')\n",
703
- "else:\n",
704
- " top5_png = top5_pdf = None\n",
705
- " print('Top-5 drug panel skipped: drug_id metadata is unavailable or too sparse.')\n",
706
- "\n",
707
- "# 4) Per-cancer-type PCC bar plot.\n",
708
- "valid_cancer = by_cancer[(by_cancer['cancer_type'].astype(str) != 'NA') & (by_cancer['n'] >= 2)].dropna(subset=['pcc']).copy()\n",
709
- "if len(valid_cancer) > 0:\n",
710
- " valid_cancer = valid_cancer.sort_values('pcc', ascending=True)\n",
711
- " valid_cancer.to_csv(PAPER_PLOT_DIR / 'per_cancer_type_metrics.csv', index=False)\n",
712
- " fig_height = max(2.2, 0.16 * len(valid_cancer) + 0.7)\n",
713
- " fig, ax = plt.subplots(figsize=(3.7, fig_height))\n",
714
- " colors = ['#4E79A7' if value < valid_cancer['pcc'].median() else '#59A14F' for value in valid_cancer['pcc']]\n",
715
- " ax.barh(valid_cancer['cancer_type'].astype(str), valid_cancer['pcc'], color=colors, edgecolor='#333333', linewidth=0.35)\n",
716
- " ax.set_xlabel('Pearson correlation')\n",
717
- " ax.set_ylabel('Cancer type')\n",
718
- " ax.set_xlim(min(0, float(valid_cancer['pcc'].min()) - 0.05), 1.0)\n",
719
- " ax.xaxis.set_major_formatter(FormatStrFormatter('%.2f'))\n",
720
- " ax.grid(axis='x', linestyle='--', linewidth=0.4, alpha=0.35)\n",
721
- " ax.set_axisbelow(True)\n",
722
- " ax.set_title('Performance by cancer type')\n",
723
- " despine(ax)\n",
724
- " fig.tight_layout()\n",
725
- " cancer_png, cancer_pdf = save_figure(fig, 'drug_response_per_cancer_type_pcc')\n",
726
- "else:\n",
727
- " cancer_png = cancer_pdf = None\n",
728
- " print('Cancer-type PCC plot skipped: cancer_type metadata is unavailable or too sparse.')\n",
729
- "\n",
730
- "# 5) Median-threshold ROC, adapted from plot_drug_response_roc_rewrite.py.\n",
731
- "if roc_curve is not None and roc_auc_score is not None:\n",
732
- " threshold = float(np.median(y_true))\n",
733
- " y_binary = (y_true <= threshold).astype(int)\n",
734
- " roc_score = -y_pred\n",
735
- " if len(np.unique(y_binary)) == 2:\n",
736
- " fpr, tpr, roc_thresholds = roc_curve(y_binary, roc_score)\n",
737
- " auc_value = float(roc_auc_score(y_binary, roc_score))\n",
738
- " roc_table = pd.DataFrame({'fpr': fpr, 'tpr': tpr, 'threshold': roc_thresholds})\n",
739
- " roc_table.to_csv(PAPER_PLOT_DIR / 'drug_response_roc_curve.csv', index=False)\n",
740
- "\n",
741
- " fig, ax = plt.subplots(figsize=(3.1, 3.1))\n",
742
- " ax.plot([0, 1], [0, 1], linestyle='--', color='#333333', linewidth=0.8, alpha=0.7)\n",
743
- " ax.plot(fpr, tpr, color='#9C6ADE', linewidth=1.1, label=f'AUROC = {auc_value:.3f}')\n",
744
- " ax.set_xlabel('False positive rate')\n",
745
- " ax.set_ylabel('True positive rate')\n",
746
- " ax.set_xlim(0, 1)\n",
747
- " ax.set_ylim(0, 1)\n",
748
- " ax.set_aspect('equal', adjustable='box')\n",
749
- " ax.legend(frameon=False, loc='lower right')\n",
750
- " ax.set_title('Median-threshold ROC')\n",
751
- " despine(ax)\n",
752
- " fig.tight_layout()\n",
753
- " roc_png, roc_pdf = save_figure(fig, 'drug_response_median_threshold_roc')\n",
754
- " fig_svg = PAPER_PLOT_DIR / 'drug_response_median_threshold_roc.svg'\n",
755
- " fig, ax = plt.subplots(figsize=(3.1, 3.1))\n",
756
- " ax.plot([0, 1], [0, 1], linestyle='--', color='#333333', linewidth=0.8, alpha=0.7)\n",
757
- " ax.plot(fpr, tpr, color='#9C6ADE', linewidth=1.1, label=f'AUROC = {auc_value:.3f}')\n",
758
- " ax.set_xlabel('False positive rate')\n",
759
- " ax.set_ylabel('True positive rate')\n",
760
- " ax.set_xlim(0, 1)\n",
761
- " ax.set_ylim(0, 1)\n",
762
- " ax.set_aspect('equal', adjustable='box')\n",
763
- " ax.legend(frameon=False, loc='lower right')\n",
764
- " ax.set_title('Median-threshold ROC')\n",
765
- " despine(ax)\n",
766
- " fig.tight_layout()\n",
767
- " fig.savefig(fig_svg, bbox_inches='tight')\n",
768
- " plt.close(fig)\n",
769
- " print('Saved:', fig_svg)\n",
770
- " else:\n",
771
- " roc_png = roc_pdf = None\n",
772
- " print('ROC plot skipped: median split produced a single class.')\n",
773
- "else:\n",
774
- " roc_png = roc_pdf = None\n",
775
- "\n",
776
- "# 6) Reported model-comparison bar from plot_cancer_drug_response_nature_color.py.\n",
777
- "reported_scores = pd.DataFrame({\n",
778
- " 'model': ['scBERT', 'scGPT', 'Geneformer', 'scFoundation', 'scLong', 'BioMod'],\n",
779
- " 'score': [0.85, 0.89, 0.89, 0.87, 0.88, 0.93],\n",
780
- "})\n",
781
- "reported_scores.to_csv(PAPER_PLOT_DIR / 'reported_model_comparison_scores.csv', index=False)\n",
782
- "fig, ax = plt.subplots(figsize=(4.2, 2.55))\n",
783
- "colors = ['#4E79A7', '#59A14F', '#F28E2B', '#76B7B2', '#B07AA1', '#E15759']\n",
784
- "x = np.arange(len(reported_scores))\n",
785
- "bars = ax.bar(x, reported_scores['score'], width=0.62, color=colors, edgecolor='#333333', linewidth=0.45)\n",
786
- "for bar, value, model_name in zip(bars, reported_scores['score'], reported_scores['model']):\n",
787
- " ax.text(\n",
788
- " bar.get_x() + bar.get_width() / 2,\n",
789
- " value + 0.003,\n",
790
- " f'{value:.2f}',\n",
791
- " ha='center',\n",
792
- " va='bottom',\n",
793
- " fontsize=6,\n",
794
- " fontweight='bold' if model_name == 'BioMod' else 'normal',\n",
795
- " )\n",
796
- "ax.set_ylabel('Drug-response score')\n",
797
- "ax.set_xticks(x)\n",
798
- "ax.set_xticklabels(reported_scores['model'], rotation=25, ha='right')\n",
799
- "ax.set_ylim(0.80, 0.95)\n",
800
- "ax.set_yticks(np.arange(0.80, 0.951, 0.03))\n",
801
- "ax.yaxis.set_major_formatter(FormatStrFormatter('%.2f'))\n",
802
- "for tick in ax.get_xticklabels():\n",
803
- " if tick.get_text() == 'BioMod':\n",
804
- " tick.set_fontweight('bold')\n",
805
- "ax.grid(axis='y', linestyle='--', linewidth=0.4, alpha=0.30)\n",
806
- "ax.set_axisbelow(True)\n",
807
- "despine(ax)\n",
808
- "fig.tight_layout()\n",
809
- "comparison_png, comparison_pdf = save_figure(fig, 'drug_response_reported_model_comparison')\n",
810
- "\n",
811
- "plot_outputs = [\n",
812
- " overall_scatter_png, residual_png, top5_png, cancer_png, roc_png, comparison_png,\n",
813
- "]\n",
814
- "plot_outputs = [path for path in plot_outputs if path is not None]\n",
815
- "display(pd.DataFrame({'plot': plot_outputs}))\n"
816
- ]
817
- },
818
- {
819
- "cell_type": "markdown",
820
- "metadata": {},
821
- "source": [
822
- "## Existing figure outputs\n",
823
- "\n",
824
- "If the full-run figure files are present, this cell displays the available PNG panels. The corresponding PDF files remain in the same folders."
825
- ]
826
- },
827
- {
828
- "cell_type": "code",
829
- "execution_count": null,
830
- "metadata": {},
831
- "outputs": [],
832
- "source": [
833
- "figure_candidates = [\n",
834
- " scatter_png,\n",
835
- " RESULT_DIR / 'drug_response_paper_plots' / 'drug_response_overall_scatter.png',\n",
836
- " RESULT_DIR / 'drug_response_paper_plots' / 'drug_response_residual_distribution.png',\n",
837
- " RESULT_DIR / 'drug_response_paper_plots' / 'drug_response_top5_drugs.png',\n",
838
- " RESULT_DIR / 'drug_response_paper_plots' / 'drug_response_per_cancer_type_pcc.png',\n",
839
- " RESULT_DIR / 'drug_response_paper_plots' / 'drug_response_median_threshold_roc.png',\n",
840
- " RESULT_DIR / 'drug_response_paper_plots' / 'drug_response_reported_model_comparison.png',\n",
841
- " ACTIVE_RESULT_DIR / 'panel_d' / 'panel_d_most_precise_5_drugs.png',\n",
842
- " ACTIVE_RESULT_DIR / 'roc_plot' / 'drug_response_roc.png',\n",
843
- " ACTIVE_RESULT_DIR / 'roc_plot' / 'predictions_roc.png',\n",
844
- "]\n",
845
- "\n",
846
- "for fig_path in figure_candidates:\n",
847
- " if fig_path.exists():\n",
848
- " print(fig_path)\n",
849
- " if Image is not None:\n",
850
- " display(Image(filename=str(fig_path)))\n",
851
- " else:\n",
852
- " print('Missing:', fig_path)\n"
853
- ]
854
- },
855
- {
856
- "cell_type": "markdown",
857
- "metadata": {},
858
- "source": [
859
- "## Related plotting scripts\n",
860
- "\n",
861
- "The full project also contains dedicated plotting scripts for paper panels. This notebook reproduces the self-contained `predictions.npz` plots directly. The leave-drug blind and GSEA-style panels require additional CSV/GMT inputs and should be run only after those files are prepared.\n"
862
- ]
863
- },
864
- {
865
- "cell_type": "code",
866
- "execution_count": null,
867
- "metadata": {},
868
- "outputs": [],
869
- "source": [
870
- "DRAW_DIR = MODULE_MODEL / 'draw'\n",
871
- "draw_scripts = [\n",
872
- " 'plot_cancer_drug_response_nature_color.py',\n",
873
- " 'plot_drug_response_panel_d.py',\n",
874
- " 'plot_drug_response_panel_e.py',\n",
875
- " 'plot_drug_response_panel_f.py',\n",
876
- " 'plot_drug_response_roc_rewrite.py',\n",
877
- "]\n",
878
- "display(pd.DataFrame([\n",
879
- " {'script': name, 'path': DRAW_DIR / name, 'exists': (DRAW_DIR / name).exists()}\n",
880
- " for name in draw_scripts\n",
881
- "]))\n"
882
- ]
883
- }
884
- ],
885
- "metadata": {
886
- "kernelspec": {
887
- "display_name": "Python 3",
888
- "language": "python",
889
- "name": "python3"
890
- },
891
- "language_info": {
892
- "codemirror_mode": {
893
- "name": "ipython",
894
- "version": 3
895
- },
896
- "file_extension": ".py",
897
- "mimetype": "text/x-python",
898
- "name": "python",
899
- "nbconvert_exporter": "python",
900
- "pygments_lexer": "ipython3",
901
- "version": "3.9"
902
- }
903
- },
904
- "nbformat": 4,
905
- "nbformat_minor": 5
906
- }
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Drug response prediction with BioMod\n",
8
+ "\n",
9
+ "This notebook uses the BioMod model weights, drug graph features, and the bundled\n",
10
+ "GDSC/CCLE response data with `train_drug_performer_module.py`.\n",
11
+ "\n",
12
+ "By default, `RUN_TRAINING = False`: the notebook checks inputs and prepares the\n",
13
+ "training command. Result and plotting cells run only when `predictions.npz`\n",
14
+ "exists in the configured output directory. Set `RUN_TRAINING = True` to train.\n",
15
+ "\n",
16
+ "The validation set selects the checkpoint. Reported metrics describe validation\n",
17
+ "performance; this workflow does not produce a separate final test evaluation.\n"
18
+ ]
19
+ },
20
+ {
21
+ "cell_type": "markdown",
22
+ "metadata": {},
23
+ "source": [
24
+ "## Project paths\n",
25
+ "\n",
26
+ "Start Jupyter from `BioMod/toturials/`. The next cell uses `Path('../..')` for the\n",
27
+ "repository root and resolves project-relative paths for the training subprocess.\n",
28
+ "If Jupyter starts at the repository root, use `Path('.')` instead. All inputs come\n",
29
+ "from this project; no private server alias or external reference run is required.\n"
30
+ ]
31
+ },
32
+ {
33
+ "cell_type": "markdown",
34
+ "metadata": {},
35
+ "source": [
36
+ "## Inputs and outputs\n",
37
+ "\n",
38
+ "- Training script: `BioMod/drug_response/train_drug_performer_module.py`\n",
39
+ "- Dataset: `data/downstream/final_data/single_drug_response/data/`\n",
40
+ "- BioMod model weights and configuration: `weight/0810model/`\n",
41
+ "- Shared model assets: `model_dependencies/`\n",
42
+ "- Tutorial output: `tutorial_runs/drug_response_prediction/`\n",
43
+ "\n",
44
+ "These paths are relative to the repository in this overview. The command cell\n",
45
+ "prints their resolved absolute paths. The drug graph files are already extracted\n",
46
+ "under `GDSC/drug_graph_feat/`; no ZIP archive or additional omics matrices are\n",
47
+ "required by this training script.\n"
48
+ ]
49
+ },
50
+ {
51
+ "cell_type": "code",
52
+ "execution_count": null,
53
+ "metadata": {},
54
+ "outputs": [],
55
+ "source": [
56
+ "from pathlib import Path\n",
57
+ "import importlib.util\n",
58
+ "import os\n",
59
+ "import shlex\n",
60
+ "import subprocess\n",
61
+ "import sys\n",
62
+ "\n",
63
+ "import numpy as np\n",
64
+ "import pandas as pd\n",
65
+ "\n",
66
+ "try:\n",
67
+ " from IPython.display import Image, display\n",
68
+ "except ImportError:\n",
69
+ " Image = None\n",
70
+ " def display(obj):\n",
71
+ " print(obj)\n",
72
+ "\n",
73
+ "from scipy.stats import pearsonr\n",
74
+ "\n",
75
+ "# Start Jupyter with BioMod/toturials as the working directory.\n",
76
+ "# If Jupyter starts at the repository root, change this line to Path('.').resolve().\n",
77
+ "PROJECT_ROOT = Path('../..').resolve()\n",
78
+ "SOURCE_DIR = PROJECT_ROOT / 'BioMod'\n",
79
+ "if not (SOURCE_DIR / 'models_performer_module.py').is_file() or not (PROJECT_ROOT / 'model_dependencies').is_dir():\n",
80
+ " raise FileNotFoundError(\n",
81
+ " 'Start Jupyter from BioMod/toturials, or set PROJECT_ROOT to the repository root '\n",
82
+ " \"(use Path('.').resolve() if Jupyter was launched at that root).\"\n",
83
+ " )\n",
84
+ "REPO_ROOT = PROJECT_ROOT\n",
85
+ "\n",
86
+ "DEPENDENCY_ROOT = REPO_ROOT / 'model_dependencies'\n",
87
+ "WEIGHT_DIR = REPO_ROOT / 'weight' / '0810model'\n",
88
+ "MODULE_DATA_DIR = DEPENDENCY_ROOT / 'module_data_all'\n",
89
+ "TRAIN_PYTHON = Path(sys.executable).resolve()\n",
90
+ "PYTHONPATH = os.pathsep.join(\n",
91
+ " [str(SOURCE_DIR), str(DEPENDENCY_ROOT)]\n",
92
+ " + ([os.environ['PYTHONPATH']] if os.environ.get('PYTHONPATH') else [])\n",
93
+ ")\n",
94
+ "\n",
95
+ "TASK_ROOT = SOURCE_DIR / 'drug_response'\n",
96
+ "SCRIPT = TASK_ROOT / 'train_drug_performer_module.py'\n",
97
+ "DATA_ROOT = REPO_ROOT / 'data/downstream/final_data/single_drug_response/data'\n",
98
+ "CKPT_PATH = WEIGHT_DIR / 'mae_tost_70M_75mask_ec2_epoch0.pth'\n",
99
+ "CONFIG_PKL = WEIGHT_DIR / 'mae_tost_70M_75mask_ec2.pkl'\n",
100
+ "GENE2VEC = DEPENDENCY_ROOT / 'selected_gene2vec_27k.npy'\n",
101
+ "GENE_MAP = DEPENDENCY_ROOT / 'gene2ens.csv'\n",
102
+ "GENE_LIST = DEPENDENCY_ROOT / 'selected_genes_27k.txt'\n",
103
+ "RESULT_DIR = REPO_ROOT / 'tutorial_runs/drug_response_prediction'\n",
104
+ "\n",
105
+ "paths = pd.DataFrame([\n",
106
+ " {'name': name, 'path': str(path), 'exists': path.exists()}\n",
107
+ " for name, path in {\n",
108
+ " 'repository': REPO_ROOT, 'script': SCRIPT, 'data_root': DATA_ROOT,\n",
109
+ " 'checkpoint': CKPT_PATH, 'config_pkl': CONFIG_PKL,\n",
110
+ " 'module_data_dir': MODULE_DATA_DIR, 'gene_list': GENE_LIST,\n",
111
+ " 'gene_map': GENE_MAP, 'gene2vec': GENE2VEC,\n",
112
+ " 'tutorial_result_dir': RESULT_DIR,\n",
113
+ " }.items()\n",
114
+ "])\n",
115
+ "display(paths)\n"
116
+ ]
117
+ },
118
+ {
119
+ "cell_type": "code",
120
+ "execution_count": null,
121
+ "metadata": {},
122
+ "outputs": [],
123
+ "source": [
124
+ "required_files = [\n",
125
+ " SCRIPT, CKPT_PATH, CONFIG_PKL, GENE_LIST, GENE_MAP, GENE2VEC,\n",
126
+ " DATA_ROOT / 'CCLE/GDSC_IC50.csv',\n",
127
+ " DATA_ROOT / 'CCLE/Cell_lines_annotations_20181226.txt',\n",
128
+ " DATA_ROOT / 'CCLE/genomic_expression_561celllines_697genes_demap_features.csv',\n",
129
+ " DATA_ROOT / 'GDSC/1.Drug_listMon Jun 24 09_00_55 2019.csv',\n",
130
+ "]\n",
131
+ "required_dirs = [PROJECT_ROOT, TASK_ROOT, MODULE_DATA_DIR,\n",
132
+ " DEPENDENCY_ROOT / 'performer_pytorch_cont', DATA_ROOT / 'GDSC/drug_graph_feat']\n",
133
+ "missing = [str(path) for path in required_files if not path.is_file()]\n",
134
+ "missing += [str(path) for path in required_dirs if not path.is_dir()]\n",
135
+ "if missing:\n",
136
+ " raise FileNotFoundError('Missing required inputs:\\n' + '\\n'.join(missing))\n",
137
+ "if not any((DATA_ROOT / 'GDSC/drug_graph_feat').glob('*.hkl')):\n",
138
+ " raise FileNotFoundError('No .hkl drug graph files found under GDSC/drug_graph_feat.')\n",
139
+ "print('All required inputs are present. Weight/configuration files have not been loaded.')\n"
140
+ ]
141
+ },
142
+ {
143
+ "cell_type": "markdown",
144
+ "metadata": {},
145
+ "source": [
146
+ "## Runtime setup\n",
147
+ "\n",
148
+ "Training uses the active notebook kernel's Python interpreter (`sys.executable`).\n",
149
+ "The dependency check reports missing packages without installing them automatically.\n",
150
+ "Install them in this environment before enabling training.\n"
151
+ ]
152
+ },
153
+ {
154
+ "cell_type": "code",
155
+ "execution_count": null,
156
+ "metadata": {},
157
+ "outputs": [],
158
+ "source": [
159
+ "print('Notebook and training Python:', TRAIN_PYTHON)\n",
160
+ "packages = {\n",
161
+ " 'torch': 'torch', 'hickle': 'hickle', 'scipy': 'scipy',\n",
162
+ " 'numpy': 'numpy', 'pandas': 'pandas', 'matplotlib': 'matplotlib',\n",
163
+ " 'sklearn': 'scikit-learn', 'einops': 'einops', 'local_attention': 'local-attention',\n",
164
+ "}\n",
165
+ "dependency_status = pd.DataFrame([\n",
166
+ " {'module': module, 'package': package,\n",
167
+ " 'available': importlib.util.find_spec(module) is not None}\n",
168
+ " for module, package in packages.items()\n",
169
+ "])\n",
170
+ "display(dependency_status)\n",
171
+ "missing_packages = dependency_status.loc[~dependency_status['available'], 'package'].tolist()\n",
172
+ "if missing_packages:\n",
173
+ " install_cmd = [str(TRAIN_PYTHON), '-m', 'pip', 'install', *missing_packages]\n",
174
+ " print(subprocess.list2cmdline(install_cmd) if os.name == 'nt' else shlex.join(install_cmd))\n"
175
+ ]
176
+ },
177
+ {
178
+ "cell_type": "markdown",
179
+ "metadata": {},
180
+ "source": [
181
+ "## Inspect input files\n",
182
+ "\n",
183
+ "This cell gives a quick sanity check for the drug-response files without loading the large full matrices into memory."
184
+ ]
185
+ },
186
+ {
187
+ "cell_type": "code",
188
+ "execution_count": null,
189
+ "metadata": {},
190
+ "outputs": [],
191
+ "source": [
192
+ "def size_mb(path: Path) -> float:\n",
193
+ " return path.stat().st_size / 1024 / 1024\n",
194
+ "\n",
195
+ "input_summary = []\n",
196
+ "for path in sorted((DATA_ROOT / 'CCLE').glob('*')):\n",
197
+ " if path.is_file():\n",
198
+ " input_summary.append({'file': str(path.relative_to(DATA_ROOT)), 'size_mb': round(size_mb(path), 2)})\n",
199
+ "for path in sorted((DATA_ROOT / 'GDSC').glob('*')):\n",
200
+ " if path.is_file():\n",
201
+ " input_summary.append({'file': str(path.relative_to(DATA_ROOT)), 'size_mb': round(size_mb(path), 2)})\n",
202
+ "\n",
203
+ "display(pd.DataFrame(input_summary))\n",
204
+ "\n",
205
+ "ic50_path = DATA_ROOT / 'CCLE' / 'GDSC_IC50.csv'\n",
206
+ "ic50_preview = pd.read_csv(ic50_path, nrows=5)\n",
207
+ "display(ic50_preview)\n"
208
+ ]
209
+ },
210
+ {
211
+ "cell_type": "markdown",
212
+ "metadata": {},
213
+ "source": [
214
+ "## Training command\n",
215
+ "\n",
216
+ "Review the resolved input and output paths below. Outputs are written to this\n",
217
+ "project's tutorial run directory. Change `RESULT_DIR` to a different run directory\n",
218
+ "before launching an experiment whose outputs you want to keep separate.\n"
219
+ ]
220
+ },
221
+ {
222
+ "cell_type": "code",
223
+ "execution_count": null,
224
+ "metadata": {},
225
+ "outputs": [],
226
+ "source": [
227
+ "EPOCHS = 300\n",
228
+ "BATCH_SIZE = 32\n",
229
+ "POOL_METHOD = 'max'\n",
230
+ "\n",
231
+ "cmd = [\n",
232
+ " str(TRAIN_PYTHON), str(SCRIPT),\n",
233
+ " '--gpu_id', '0',\n",
234
+ " '--data_path', str(DATA_ROOT),\n",
235
+ " '--checkpoint_path', str(CKPT_PATH),\n",
236
+ " '--hyper_params_path', str(CONFIG_PKL),\n",
237
+ " '--epochs', str(EPOCHS),\n",
238
+ " '--batch_size', str(BATCH_SIZE),\n",
239
+ " '--module_data_dir', str(MODULE_DATA_DIR),\n",
240
+ " '--gene_list_path', str(GENE_LIST),\n",
241
+ " '--gene_mapping_path', str(GENE_MAP),\n",
242
+ " '--symbol2ens_path', str(GENE_MAP),\n",
243
+ " '--gene2vec_path', str(GENE2VEC),\n",
244
+ " '--save_dir', str(RESULT_DIR),\n",
245
+ " '--pool_method', POOL_METHOD,\n",
246
+ "]\n",
247
+ "\n",
248
+ "print('Working directory:', TASK_ROOT)\n",
249
+ "print(subprocess.list2cmdline(cmd) if os.name == 'nt' else shlex.join(cmd))\n"
250
+ ]
251
+ },
252
+ {
253
+ "cell_type": "code",
254
+ "execution_count": null,
255
+ "metadata": {},
256
+ "outputs": [],
257
+ "source": [
258
+ "RUN_TRAINING = False\n",
259
+ "env = os.environ.copy()\n",
260
+ "env['PYTHONPATH'] = PYTHONPATH\n",
261
+ "env['PYTHONUNBUFFERED'] = '1'\n",
262
+ "\n",
263
+ "if RUN_TRAINING:\n",
264
+ " RESULT_DIR.mkdir(parents=True, exist_ok=True)\n",
265
+ " log_path = RESULT_DIR / 'train.log'\n",
266
+ " print('Writing log to:', log_path)\n",
267
+ " with log_path.open('w') as log_file:\n",
268
+ " proc = subprocess.Popen(\n",
269
+ " cmd,\n",
270
+ " cwd=str(TASK_ROOT),\n",
271
+ " env=env,\n",
272
+ " stdout=subprocess.PIPE,\n",
273
+ " stderr=subprocess.STDOUT,\n",
274
+ " text=True,\n",
275
+ " bufsize=1,\n",
276
+ " )\n",
277
+ " assert proc.stdout is not None\n",
278
+ " for line in proc.stdout:\n",
279
+ " print(line, end='')\n",
280
+ " log_file.write(line)\n",
281
+ " return_code = proc.wait()\n",
282
+ " if return_code != 0:\n",
283
+ " raise RuntimeError(f'Training failed with exit code {return_code}')\n",
284
+ "else:\n",
285
+ " print('Training was not launched. Set RUN_TRAINING = True to run the full job.')\n",
286
+ " print('The following cells inspect results only if predictions.npz exists in RESULT_DIR.')\n"
287
+ ]
288
+ },
289
+ {
290
+ "cell_type": "markdown",
291
+ "metadata": {},
292
+ "source": [
293
+ "## Inspect outputs\n",
294
+ "\n",
295
+ "Only the configured `RESULT_DIR` is used. The current script saves numerical arrays\n",
296
+ "under the `pred` and `validation` keys in `predictions.npz`. The reader also accepts\n",
297
+ "the historical `preds`, `test`, and `true` key names. Missing results are skipped.\n"
298
+ ]
299
+ },
300
+ {
301
+ "cell_type": "code",
302
+ "execution_count": null,
303
+ "metadata": {},
304
+ "outputs": [],
305
+ "source": [
306
+ "ACTIVE_RESULT_DIR = RESULT_DIR\n",
307
+ "pred_path = ACTIVE_RESULT_DIR / 'predictions.npz'\n",
308
+ "RESULTS_READY = pred_path.is_file()\n",
309
+ "if RESULTS_READY:\n",
310
+ " print('Active result directory:', ACTIVE_RESULT_DIR)\n",
311
+ " display(pd.DataFrame([\n",
312
+ " {'file': str(path.relative_to(ACTIVE_RESULT_DIR)), 'size_mb': round(size_mb(path), 3)}\n",
313
+ " for path in sorted(ACTIVE_RESULT_DIR.rglob('*')) if path.is_file()\n",
314
+ " ]))\n",
315
+ "else:\n",
316
+ " print('No predictions.npz found; result analysis and plots will be skipped:', pred_path)\n"
317
+ ]
318
+ },
319
+ {
320
+ "cell_type": "code",
321
+ "execution_count": null,
322
+ "metadata": {},
323
+ "outputs": [],
324
+ "source": [
325
+ "meta = pd.DataFrame(columns=['pred', 'true', 'drug_id', 'cell_line_id', 'cancer_type'])\n",
326
+ "pred = true = np.array([], dtype=float)\n",
327
+ "if RESULTS_READY:\n",
328
+ " with np.load(pred_path, allow_pickle=True) as pred_npz:\n",
329
+ " print('Prediction keys:', pred_npz.files)\n",
330
+ " pred_key = next((key for key in ('pred', 'preds') if key in pred_npz.files), None)\n",
331
+ " true_key = next((key for key in ('validation', 'test', 'true') if key in pred_npz.files), None)\n",
332
+ " if pred_key is None or true_key is None:\n",
333
+ " raise ValueError('Prediction file needs pred/preds and validation/test/true arrays.')\n",
334
+ " pred = np.asarray(pred_npz[pred_key], dtype=float).reshape(-1)\n",
335
+ " true = np.asarray(pred_npz[true_key], dtype=float).reshape(-1)\n",
336
+ " if len(pred) != len(true):\n",
337
+ " raise ValueError('Prediction and target lengths do not match.')\n",
338
+ " meta = pd.DataFrame({'pred': pred, 'true': true})\n",
339
+ " for column in ['drug_id', 'cell_line_id', 'cancer_type']:\n",
340
+ " meta[column] = np.asarray(pred_npz[column]).reshape(-1) if column in pred_npz.files else 'NA'\n",
341
+ " finite = np.isfinite(pred) & np.isfinite(true)\n",
342
+ " pred, true = pred[finite], true[finite]\n",
343
+ " RESULTS_READY = bool(len(pred))\n",
344
+ " if RESULTS_READY:\n",
345
+ " pcc = float(pearsonr(pred, true)[0]) if len(pred) >= 2 and np.std(pred) > 0 and np.std(true) > 0 else np.nan\n",
346
+ " summary = pd.DataFrame([{'n_validation': len(pred), 'pcc': pcc,\n",
347
+ " 'mae': float(np.mean(np.abs(pred - true))),\n",
348
+ " 'rmse': float(np.sqrt(np.mean((pred - true) ** 2)))}])\n",
349
+ " display(summary)\n",
350
+ " display(meta.head())\n",
351
+ " else:\n",
352
+ " print('No finite prediction/target pairs; result plots will be skipped.')\n",
353
+ "else:\n",
354
+ " print('Prediction metrics skipped: no output file is available.')\n"
355
+ ]
356
+ },
357
+ {
358
+ "cell_type": "code",
359
+ "execution_count": null,
360
+ "metadata": {},
361
+ "outputs": [],
362
+ "source": [
363
+ "if RESULTS_READY:\n",
364
+ " import random\n",
365
+ " from scipy.stats import spearmanr\n",
366
+ "\n",
367
+ " TCGA_LABEL_SET = [\n",
368
+ " 'ALL', 'BLCA', 'BRCA', 'CESC', 'DLBC', 'LIHC', 'LUAD', 'ESCA', 'GBM',\n",
369
+ " 'HNSC', 'KIRC', 'LAML', 'LCML', 'LGG', 'LUSC', 'MESO', 'MM', 'NB',\n",
370
+ " 'OV', 'PAAD', 'SCLC', 'SKCM', 'STAD', 'THCA', 'COAD/READ',\n",
371
+ " ]\n",
372
+ "\n",
373
+ "\n",
374
+ " def pearson_corr(x, y):\n",
375
+ " x = np.asarray(x, dtype=float).reshape(-1)\n",
376
+ " y = np.asarray(y, dtype=float).reshape(-1)\n",
377
+ " mask = np.isfinite(x) & np.isfinite(y)\n",
378
+ " x = x[mask]\n",
379
+ " y = y[mask]\n",
380
+ " if len(x) < 2 or np.std(x) == 0 or np.std(y) == 0:\n",
381
+ " return np.nan\n",
382
+ " return float(np.corrcoef(x, y)[0, 1])\n",
383
+ "\n",
384
+ "\n",
385
+ " def regression_metrics(frame: pd.DataFrame) -> dict:\n",
386
+ " y_true = frame['true'].astype(float).to_numpy()\n",
387
+ " y_pred = frame['pred'].astype(float).to_numpy()\n",
388
+ " return {\n",
389
+ " 'n': int(len(frame)),\n",
390
+ " 'pcc': pearson_corr(y_true, y_pred),\n",
391
+ " 'spearman': float(spearmanr(y_true, y_pred).correlation) if len(frame) >= 2 else np.nan,\n",
392
+ " 'mae': float(np.mean(np.abs(y_pred - y_true))),\n",
393
+ " 'mse': float(np.mean((y_pred - y_true) ** 2)),\n",
394
+ " 'rmse': float(np.sqrt(np.mean((y_pred - y_true) ** 2))),\n",
395
+ " }\n",
396
+ "\n",
397
+ "\n",
398
+ " def reconstruct_validation_metadata(data_root: Path, n_expected: int, ratio: float = 0.95, seed: int = 0) -> pd.DataFrame:\n",
399
+ " \"\"\"Rebuild the validation split metadata using the same deterministic split as the training script.\"\"\"\n",
400
+ " drug_info_file = data_root / 'GDSC' / '1.Drug_listMon Jun 24 09_00_55 2019.csv'\n",
401
+ " cell_line_info_file = data_root / 'CCLE' / 'Cell_lines_annotations_20181226.txt'\n",
402
+ " drug_feature_dir = data_root / 'GDSC' / 'drug_graph_feat'\n",
403
+ " gene_expression_file = data_root / 'CCLE' / 'genomic_expression_561celllines_697genes_demap_features.csv'\n",
404
+ " ic50_file = data_root / 'CCLE' / 'GDSC_IC50.csv'\n",
405
+ "\n",
406
+ " for required in [drug_info_file, cell_line_info_file, drug_feature_dir, gene_expression_file, ic50_file]:\n",
407
+ " if not required.exists():\n",
408
+ " raise FileNotFoundError(required)\n",
409
+ "\n",
410
+ " drug_info = pd.read_csv(drug_info_file)\n",
411
+ " drugid2pubchemid = {\n",
412
+ " str(row.iloc[0]): str(row.iloc[5])\n",
413
+ " for _, row in drug_info.iterrows()\n",
414
+ " if str(row.iloc[5]).isdigit()\n",
415
+ " }\n",
416
+ "\n",
417
+ " cellline2cancertype = {}\n",
418
+ " with cell_line_info_file.open() as handle:\n",
419
+ " for line in handle.readlines()[1:]:\n",
420
+ " parts = line.rstrip('\\n').split('\\t')\n",
421
+ " if len(parts) >= 2:\n",
422
+ " cellline2cancertype[parts[1]] = parts[-1]\n",
423
+ "\n",
424
+ " gexpr_index = set(pd.read_csv(gene_expression_file, usecols=[0]).iloc[:, 0].astype(str))\n",
425
+ " drug_pubchem_ids = {path.stem for path in drug_feature_dir.glob('*.hkl')}\n",
426
+ "\n",
427
+ " ic50 = pd.read_csv(ic50_file, header=0, index_col=0)\n",
428
+ " drug_match_list = [idx for idx in ic50.index if idx.split(':')[1] in drugid2pubchemid]\n",
429
+ " ic50 = ic50.loc[drug_match_list]\n",
430
+ "\n",
431
+ " data_idx = []\n",
432
+ " for each_drug in ic50.index:\n",
433
+ " pubchem_id = drugid2pubchemid[each_drug.split(':')[-1]]\n",
434
+ " if pubchem_id not in drug_pubchem_ids:\n",
435
+ " continue\n",
436
+ " for each_cellline in ic50.columns:\n",
437
+ " if each_cellline not in gexpr_index or each_cellline not in cellline2cancertype:\n",
438
+ " continue\n",
439
+ " value = ic50.loc[each_drug, each_cellline]\n",
440
+ " if pd.isna(value):\n",
441
+ " continue\n",
442
+ " data_idx.append((each_cellline, pubchem_id, float(value), cellline2cancertype[each_cellline]))\n",
443
+ "\n",
444
+ " rng = random.Random(seed)\n",
445
+ " validation_idx = []\n",
446
+ " for each_type in TCGA_LABEL_SET:\n",
447
+ " subtype = [item for item in data_idx if item[-1] == each_type]\n",
448
+ " if not subtype:\n",
449
+ " continue\n",
450
+ " k = max(1, int(ratio * len(subtype)))\n",
451
+ " train_list = rng.sample(subtype, k)\n",
452
+ " validation_idx.extend([item for item in subtype if item not in train_list])\n",
453
+ "\n",
454
+ " reconstructed = pd.DataFrame(validation_idx, columns=['cell_line_id', 'drug_id', 'true_from_metadata', 'cancer_type'])\n",
455
+ " if len(reconstructed) != n_expected:\n",
456
+ " raise ValueError(f'Reconstructed {len(reconstructed)} rows, but predictions contain {n_expected} rows.')\n",
457
+ " return reconstructed\n",
458
+ "\n",
459
+ "\n",
460
+ " analysis_df = meta.copy()\n",
461
+ " analysis_df['pred'] = pd.to_numeric(analysis_df['pred'], errors='coerce')\n",
462
+ " analysis_df['true'] = pd.to_numeric(analysis_df['true'], errors='coerce')\n",
463
+ "\n",
464
+ " metadata_cols = ['drug_id', 'cell_line_id', 'cancer_type']\n",
465
+ " metadata_missing = any(col not in analysis_df.columns for col in metadata_cols) or all(\n",
466
+ " analysis_df[col].astype(str).isin(['NA', 'nan', 'None', '']).all() for col in metadata_cols if col in analysis_df.columns\n",
467
+ " )\n",
468
+ "\n",
469
+ " if metadata_missing:\n",
470
+ " try:\n",
471
+ " recovered_meta = reconstruct_validation_metadata(DATA_ROOT, n_expected=len(analysis_df), ratio=0.95, seed=0)\n",
472
+ " if not np.allclose(analysis_df['true'].to_numpy(dtype=float), recovered_meta['true_from_metadata'].to_numpy(dtype=float), rtol=1e-5, atol=1e-6, equal_nan=True):\n",
473
+ " raise ValueError('Reconstructed targets do not match the prediction rows; metadata will not be assigned.')\n",
474
+ " for col in metadata_cols:\n",
475
+ " analysis_df[col] = recovered_meta[col].to_numpy()\n",
476
+ " print('Recovered validation metadata from the original GDSC/CCLE split.')\n",
477
+ " except Exception as exc:\n",
478
+ " print('Metadata recovery skipped:', repr(exc))\n",
479
+ " for col in metadata_cols:\n",
480
+ " if col not in analysis_df.columns:\n",
481
+ " analysis_df[col] = 'NA'\n",
482
+ " else:\n",
483
+ " print('Using drug/cell/cancer metadata stored in predictions.npz.')\n",
484
+ "\n",
485
+ " analysis_df = analysis_df.replace([np.inf, -np.inf], np.nan).dropna(subset=['pred', 'true']).reset_index(drop=True)\n",
486
+ "\n",
487
+ " overall_metrics = pd.DataFrame([regression_metrics(analysis_df)])\n",
488
+ " display(overall_metrics)\n",
489
+ " display(analysis_df.head())\n",
490
+ "\n",
491
+ " metric_columns = ['pred', 'true']\n",
492
+ " by_cancer = (\n",
493
+ " analysis_df.groupby('cancer_type', dropna=False)[metric_columns]\n",
494
+ " .apply(lambda frame: pd.Series(regression_metrics(frame)))\n",
495
+ " .reset_index()\n",
496
+ " .sort_values('n', ascending=False)\n",
497
+ " )\n",
498
+ " by_drug = (\n",
499
+ " analysis_df.groupby('drug_id', dropna=False)[metric_columns]\n",
500
+ " .apply(lambda frame: pd.Series(regression_metrics(frame)))\n",
501
+ " .reset_index()\n",
502
+ " .sort_values('pcc', ascending=False)\n",
503
+ " )\n",
504
+ "\n",
505
+ " display(by_cancer.head(12))\n",
506
+ " display(by_drug.head(10))\n",
507
+ "else:\n",
508
+ " print('Grouped analysis skipped: no usable predictions are available.')\n"
509
+ ]
510
+ },
511
+ {
512
+ "cell_type": "markdown",
513
+ "metadata": {},
514
+ "source": [
515
+ "## Validation scatter plot\n",
516
+ "\n",
517
+ "When predictions exist, this cell saves a PDF and PNG of predicted versus measured\n",
518
+ "drug response under `RESULT_DIR`. No external reference-run directory is used.\n"
519
+ ]
520
+ },
521
+ {
522
+ "cell_type": "code",
523
+ "execution_count": null,
524
+ "metadata": {},
525
+ "outputs": [],
526
+ "source": [
527
+ "if RESULTS_READY:\n",
528
+ " import matplotlib.pyplot as plt\n",
529
+ "\n",
530
+ " plt.rcParams.update({\n",
531
+ " 'font.family': 'sans-serif',\n",
532
+ " 'font.sans-serif': ['Arial', 'DejaVu Sans'],\n",
533
+ " 'font.size': 7,\n",
534
+ " 'pdf.fonttype': 42,\n",
535
+ " 'ps.fonttype': 42,\n",
536
+ " })\n",
537
+ "\n",
538
+ " RESULT_DIR.mkdir(parents=True, exist_ok=True)\n",
539
+ " scatter_png = RESULT_DIR / 'drug_response_prediction_scatter.png'\n",
540
+ " scatter_pdf = RESULT_DIR / 'drug_response_prediction_scatter.pdf'\n",
541
+ "\n",
542
+ " low = float(min(np.min(true), np.min(pred)))\n",
543
+ " high = float(max(np.max(true), np.max(pred)))\n",
544
+ " pad = max(0.05 * (high - low), 0.05)\n",
545
+ " lims = (low - pad, high + pad)\n",
546
+ "\n",
547
+ " fig, ax = plt.subplots(figsize=(3.35, 3.1))\n",
548
+ " ax.scatter(true, pred, s=7, alpha=0.45, linewidths=0, color='#2f6fb0')\n",
549
+ " ax.plot(lims, lims, color='#444444', linewidth=0.8, linestyle='--')\n",
550
+ " ax.set_xlim(lims)\n",
551
+ " ax.set_ylim(lims)\n",
552
+ " ax.set_xlabel('Measured drug response')\n",
553
+ " ax.set_ylabel('Predicted drug response')\n",
554
+ " ax.set_title('Drug response prediction')\n",
555
+ " ax.text(0.03, 0.97, f'PCC = {pcc:.3f}\\nN = {len(pred)}', transform=ax.transAxes, va='top')\n",
556
+ " ax.spines['top'].set_visible(False)\n",
557
+ " ax.spines['right'].set_visible(False)\n",
558
+ " fig.tight_layout()\n",
559
+ " fig.savefig(scatter_png, dpi=300, bbox_inches='tight')\n",
560
+ " fig.savefig(scatter_pdf, bbox_inches='tight')\n",
561
+ " plt.close(fig)\n",
562
+ "\n",
563
+ " print('Saved:', scatter_png)\n",
564
+ " print('Saved:', scatter_pdf)\n",
565
+ " if Image is not None:\n",
566
+ " display(Image(filename=str(scatter_png)))\n",
567
+ "else:\n",
568
+ " print('Scatter plot skipped: no usable predictions are available.')\n"
569
+ ]
570
+ },
571
+ {
572
+ "cell_type": "markdown",
573
+ "metadata": {},
574
+ "source": [
575
+ "## Additional validation plots\n",
576
+ "\n",
577
+ "These self-contained plots use the current `predictions.npz`: overall scatter,\n",
578
+ "residuals, per-drug and per-cancer summaries when matching metadata is available,\n",
579
+ "and an exploratory median-threshold ROC. The median-threshold classification is a\n",
580
+ "derived visualization of the regression outputs, not an independent test result.\n",
581
+ "No hardcoded model-comparison scores or external plotting scripts are used.\n"
582
+ ]
583
+ },
584
+ {
585
+ "cell_type": "code",
586
+ "execution_count": null,
587
+ "metadata": {},
588
+ "outputs": [],
589
+ "source": [
590
+ "if RESULTS_READY:\n",
591
+ " from matplotlib.ticker import FormatStrFormatter\n",
592
+ "\n",
593
+ " try:\n",
594
+ " from sklearn.metrics import roc_curve, roc_auc_score\n",
595
+ " except Exception as exc:\n",
596
+ " roc_curve = None\n",
597
+ " roc_auc_score = None\n",
598
+ " print('ROC dependencies unavailable:', repr(exc))\n",
599
+ "\n",
600
+ " PAPER_PLOT_DIR = RESULT_DIR / 'drug_response_paper_plots'\n",
601
+ " PAPER_PLOT_DIR.mkdir(parents=True, exist_ok=True)\n",
602
+ "\n",
603
+ " plt.rcParams.update({\n",
604
+ " 'font.family': 'sans-serif',\n",
605
+ " 'font.sans-serif': ['Arial', 'DejaVu Sans'],\n",
606
+ " 'font.size': 7,\n",
607
+ " 'axes.labelsize': 7,\n",
608
+ " 'axes.titlesize': 8,\n",
609
+ " 'xtick.labelsize': 6,\n",
610
+ " 'ytick.labelsize': 6,\n",
611
+ " 'legend.fontsize': 6,\n",
612
+ " 'axes.linewidth': 0.7,\n",
613
+ " 'pdf.fonttype': 42,\n",
614
+ " 'ps.fonttype': 42,\n",
615
+ " 'svg.fonttype': 'none',\n",
616
+ " })\n",
617
+ "\n",
618
+ "\n",
619
+ " def save_figure(fig, stem: str, dpi: int = 600):\n",
620
+ " png_path = PAPER_PLOT_DIR / f'{stem}.png'\n",
621
+ " pdf_path = PAPER_PLOT_DIR / f'{stem}.pdf'\n",
622
+ " fig.savefig(png_path, dpi=dpi, bbox_inches='tight')\n",
623
+ " fig.savefig(pdf_path, bbox_inches='tight')\n",
624
+ " plt.close(fig)\n",
625
+ " print('Saved:', png_path)\n",
626
+ " print('Saved:', pdf_path)\n",
627
+ " return png_path, pdf_path\n",
628
+ "\n",
629
+ "\n",
630
+ " def despine(ax):\n",
631
+ " ax.spines['top'].set_visible(False)\n",
632
+ " ax.spines['right'].set_visible(False)\n",
633
+ " ax.tick_params(width=0.7, length=2.5)\n",
634
+ "\n",
635
+ "\n",
636
+ " def finite_xy(frame: pd.DataFrame):\n",
637
+ " y_true = pd.to_numeric(frame['true'], errors='coerce').to_numpy(dtype=float)\n",
638
+ " y_pred = pd.to_numeric(frame['pred'], errors='coerce').to_numpy(dtype=float)\n",
639
+ " mask = np.isfinite(y_true) & np.isfinite(y_pred)\n",
640
+ " return y_true[mask], y_pred[mask]\n",
641
+ "\n",
642
+ "\n",
643
+ " # 1) Overall measured-vs-predicted scatter.\n",
644
+ " y_true, y_pred = finite_xy(analysis_df)\n",
645
+ " low = float(min(y_true.min(), y_pred.min()))\n",
646
+ " high = float(max(y_true.max(), y_pred.max()))\n",
647
+ " pad = max(0.05 * (high - low), 0.05)\n",
648
+ " lims = (low - pad, high + pad)\n",
649
+ " metrics = regression_metrics(analysis_df)\n",
650
+ "\n",
651
+ " fig, ax = plt.subplots(figsize=(3.35, 3.15))\n",
652
+ " ax.scatter(y_true, y_pred, s=5, alpha=0.35, linewidths=0, color='#2F6FB0', rasterized=True)\n",
653
+ " ax.plot(lims, lims, color='#333333', linewidth=0.8, linestyle='--')\n",
654
+ " ax.set_xlim(lims)\n",
655
+ " ax.set_ylim(lims)\n",
656
+ " ax.set_xlabel('Measured drug response')\n",
657
+ " ax.set_ylabel('Predicted drug response')\n",
658
+ " ax.set_title('BioMod drug response')\n",
659
+ " ax.text(\n",
660
+ " 0.03, 0.97,\n",
661
+ " f'PCC = {metrics[\"pcc\"]:.3f}\\nRMSE = {metrics[\"rmse\"]:.3f}\\nN = {metrics[\"n\"]}',\n",
662
+ " transform=ax.transAxes,\n",
663
+ " va='top',\n",
664
+ " ha='left',\n",
665
+ " )\n",
666
+ " despine(ax)\n",
667
+ " fig.tight_layout()\n",
668
+ " overall_scatter_png, overall_scatter_pdf = save_figure(fig, 'drug_response_overall_scatter')\n",
669
+ "\n",
670
+ " # 2) Residual distribution.\n",
671
+ " residual = y_pred - y_true\n",
672
+ " fig, ax = plt.subplots(figsize=(3.35, 2.35))\n",
673
+ " ax.hist(residual, bins=45, color='#76B7B2', edgecolor='white', linewidth=0.3)\n",
674
+ " ax.axvline(0, color='#333333', linestyle='--', linewidth=0.8)\n",
675
+ " ax.set_xlabel('Prediction residual')\n",
676
+ " ax.set_ylabel('Number of samples')\n",
677
+ " ax.set_title('Residual distribution')\n",
678
+ " despine(ax)\n",
679
+ " fig.tight_layout()\n",
680
+ " residual_png, residual_pdf = save_figure(fig, 'drug_response_residual_distribution')\n",
681
+ "\n",
682
+ " # 3) Top-5 most precise drugs, among drugs with sufficient validation samples.\n",
683
+ " MIN_SAMPLES_PER_DRUG = 3\n",
684
+ " valid_drug_stats = by_drug[(by_drug['drug_id'].astype(str) != 'NA') & (by_drug['n'] >= MIN_SAMPLES_PER_DRUG)].dropna(subset=['pcc'])\n",
685
+ " if len(valid_drug_stats) > 0:\n",
686
+ " top5_drugs = valid_drug_stats.sort_values('pcc', ascending=False).head(5)['drug_id'].astype(str).tolist()\n",
687
+ " top5_summary = valid_drug_stats[valid_drug_stats['drug_id'].astype(str).isin(top5_drugs)].copy()\n",
688
+ " top5_points = analysis_df[analysis_df['drug_id'].astype(str).isin(top5_drugs)].copy()\n",
689
+ " top5_summary.to_csv(PAPER_PLOT_DIR / 'top5_drug_pcc_summary.csv', index=False)\n",
690
+ " top5_points.to_csv(PAPER_PLOT_DIR / 'top5_drug_selected_points.csv', index=False)\n",
691
+ "\n",
692
+ " fig, ax = plt.subplots(figsize=(3.65, 3.25))\n",
693
+ " palette = plt.get_cmap('tab10')\n",
694
+ " for idx, drug_id in enumerate(top5_drugs):\n",
695
+ " sub = top5_points[top5_points['drug_id'].astype(str) == drug_id]\n",
696
+ " ax.scatter(\n",
697
+ " sub['true'], sub['pred'],\n",
698
+ " s=10, alpha=0.75, linewidths=0,\n",
699
+ " color=palette(idx), label=str(drug_id), rasterized=True,\n",
700
+ " )\n",
701
+ " t_true, t_pred = finite_xy(top5_points)\n",
702
+ " low = float(min(t_true.min(), t_pred.min()))\n",
703
+ " high = float(max(t_true.max(), t_pred.max()))\n",
704
+ " pad = max(0.05 * (high - low), 0.05)\n",
705
+ " lims = (low - pad, high + pad)\n",
706
+ " ax.plot(lims, lims, color='#333333', linewidth=0.8, linestyle='--')\n",
707
+ " ax.set_xlim(lims)\n",
708
+ " ax.set_ylim(lims)\n",
709
+ " ax.set_xlabel('Measured drug response')\n",
710
+ " ax.set_ylabel('Predicted drug response')\n",
711
+ " ax.set_title('Most precise 5 drugs')\n",
712
+ " ax.legend(title='PubChem ID', frameon=False, loc='best', handletextpad=0.3, borderpad=0.2)\n",
713
+ " despine(ax)\n",
714
+ " fig.tight_layout()\n",
715
+ " top5_png, top5_pdf = save_figure(fig, 'drug_response_top5_drugs')\n",
716
+ " else:\n",
717
+ " top5_png = top5_pdf = None\n",
718
+ " print('Top-5 drug panel skipped: drug_id metadata is unavailable or too sparse.')\n",
719
+ "\n",
720
+ " # 4) Per-cancer-type PCC bar plot.\n",
721
+ " valid_cancer = by_cancer[(by_cancer['cancer_type'].astype(str) != 'NA') & (by_cancer['n'] >= 2)].dropna(subset=['pcc']).copy()\n",
722
+ " if len(valid_cancer) > 0:\n",
723
+ " valid_cancer = valid_cancer.sort_values('pcc', ascending=True)\n",
724
+ " valid_cancer.to_csv(PAPER_PLOT_DIR / 'per_cancer_type_metrics.csv', index=False)\n",
725
+ " fig_height = max(2.2, 0.16 * len(valid_cancer) + 0.7)\n",
726
+ " fig, ax = plt.subplots(figsize=(3.7, fig_height))\n",
727
+ " colors = ['#4E79A7' if value < valid_cancer['pcc'].median() else '#59A14F' for value in valid_cancer['pcc']]\n",
728
+ " ax.barh(valid_cancer['cancer_type'].astype(str), valid_cancer['pcc'], color=colors, edgecolor='#333333', linewidth=0.35)\n",
729
+ " ax.set_xlabel('Pearson correlation')\n",
730
+ " ax.set_ylabel('Cancer type')\n",
731
+ " ax.set_xlim(min(0, float(valid_cancer['pcc'].min()) - 0.05), 1.0)\n",
732
+ " ax.xaxis.set_major_formatter(FormatStrFormatter('%.2f'))\n",
733
+ " ax.grid(axis='x', linestyle='--', linewidth=0.4, alpha=0.35)\n",
734
+ " ax.set_axisbelow(True)\n",
735
+ " ax.set_title('Performance by cancer type')\n",
736
+ " despine(ax)\n",
737
+ " fig.tight_layout()\n",
738
+ " cancer_png, cancer_pdf = save_figure(fig, 'drug_response_per_cancer_type_pcc')\n",
739
+ " else:\n",
740
+ " cancer_png = cancer_pdf = None\n",
741
+ " print('Cancer-type PCC plot skipped: cancer_type metadata is unavailable or too sparse.')\n",
742
+ "\n",
743
+ " # 5) Median-threshold ROC, an exploratory view of the regression outputs.\n",
744
+ " if roc_curve is not None and roc_auc_score is not None:\n",
745
+ " threshold = float(np.median(y_true))\n",
746
+ " y_binary = (y_true <= threshold).astype(int)\n",
747
+ " roc_score = -y_pred\n",
748
+ " if len(np.unique(y_binary)) == 2:\n",
749
+ " fpr, tpr, roc_thresholds = roc_curve(y_binary, roc_score)\n",
750
+ " auc_value = float(roc_auc_score(y_binary, roc_score))\n",
751
+ " roc_table = pd.DataFrame({'fpr': fpr, 'tpr': tpr, 'threshold': roc_thresholds})\n",
752
+ " roc_table.to_csv(PAPER_PLOT_DIR / 'drug_response_roc_curve.csv', index=False)\n",
753
+ "\n",
754
+ " fig, ax = plt.subplots(figsize=(3.1, 3.1))\n",
755
+ " ax.plot([0, 1], [0, 1], linestyle='--', color='#333333', linewidth=0.8, alpha=0.7)\n",
756
+ " ax.plot(fpr, tpr, color='#9C6ADE', linewidth=1.1, label=f'AUROC = {auc_value:.3f}')\n",
757
+ " ax.set_xlabel('False positive rate')\n",
758
+ " ax.set_ylabel('True positive rate')\n",
759
+ " ax.set_xlim(0, 1)\n",
760
+ " ax.set_ylim(0, 1)\n",
761
+ " ax.set_aspect('equal', adjustable='box')\n",
762
+ " ax.legend(frameon=False, loc='lower right')\n",
763
+ " ax.set_title('Median-threshold ROC')\n",
764
+ " despine(ax)\n",
765
+ " fig.tight_layout()\n",
766
+ " roc_png, roc_pdf = save_figure(fig, 'drug_response_median_threshold_roc')\n",
767
+ " fig_svg = PAPER_PLOT_DIR / 'drug_response_median_threshold_roc.svg'\n",
768
+ " fig, ax = plt.subplots(figsize=(3.1, 3.1))\n",
769
+ " ax.plot([0, 1], [0, 1], linestyle='--', color='#333333', linewidth=0.8, alpha=0.7)\n",
770
+ " ax.plot(fpr, tpr, color='#9C6ADE', linewidth=1.1, label=f'AUROC = {auc_value:.3f}')\n",
771
+ " ax.set_xlabel('False positive rate')\n",
772
+ " ax.set_ylabel('True positive rate')\n",
773
+ " ax.set_xlim(0, 1)\n",
774
+ " ax.set_ylim(0, 1)\n",
775
+ " ax.set_aspect('equal', adjustable='box')\n",
776
+ " ax.legend(frameon=False, loc='lower right')\n",
777
+ " ax.set_title('Median-threshold ROC')\n",
778
+ " despine(ax)\n",
779
+ " fig.tight_layout()\n",
780
+ " fig.savefig(fig_svg, bbox_inches='tight')\n",
781
+ " plt.close(fig)\n",
782
+ " print('Saved:', fig_svg)\n",
783
+ " else:\n",
784
+ " roc_png = roc_pdf = None\n",
785
+ " print('ROC plot skipped: median split produced a single class.')\n",
786
+ " else:\n",
787
+ " roc_png = roc_pdf = None\n",
788
+ "\n",
789
+ "\n",
790
+ " plot_outputs = [overall_scatter_png, residual_png, top5_png, cancer_png, roc_png]\n",
791
+ " plot_outputs = [path for path in plot_outputs if path is not None]\n",
792
+ " display(pd.DataFrame({'plot': plot_outputs}))\n",
793
+ "else:\n",
794
+ " print('Additional plots skipped: no usable predictions are available.')\n"
795
+ ]
796
+ },
797
+ {
798
+ "cell_type": "markdown",
799
+ "metadata": {},
800
+ "source": [
801
+ "## Available figure outputs\n",
802
+ "\n",
803
+ "Display figures that exist under the configured tutorial output directory.\n"
804
+ ]
805
+ },
806
+ {
807
+ "cell_type": "code",
808
+ "execution_count": null,
809
+ "metadata": {},
810
+ "outputs": [],
811
+ "source": [
812
+ "figure_candidates = [RESULT_DIR / 'drug_response_prediction_scatter.png']\n",
813
+ "figure_candidates += sorted((RESULT_DIR / 'drug_response_paper_plots').glob('*.png'))\n",
814
+ "available_figures = [path for path in figure_candidates if path.is_file()]\n",
815
+ "if available_figures:\n",
816
+ " for fig_path in available_figures:\n",
817
+ " print(fig_path)\n",
818
+ " if Image is not None:\n",
819
+ " display(Image(filename=str(fig_path)))\n",
820
+ "else:\n",
821
+ " print('No generated figures are available in:', RESULT_DIR)\n"
822
+ ]
823
+ },
824
+ {
825
+ "cell_type": "markdown",
826
+ "metadata": {},
827
+ "source": [
828
+ "## Generated files\n",
829
+ "\n",
830
+ "List local files produced by training or the notebook's plotting cells.\n"
831
+ ]
832
+ },
833
+ {
834
+ "cell_type": "code",
835
+ "execution_count": null,
836
+ "metadata": {},
837
+ "outputs": [],
838
+ "source": [
839
+ "generated_files = [path for path in sorted(RESULT_DIR.rglob('*')) if path.is_file()]\n",
840
+ "if generated_files:\n",
841
+ " display(pd.DataFrame([\n",
842
+ " {'file': str(path.relative_to(RESULT_DIR)), 'path': str(path)}\n",
843
+ " for path in generated_files\n",
844
+ " ]))\n",
845
+ "else:\n",
846
+ " print('No outputs have been generated yet.')\n"
847
+ ]
848
+ }
849
+ ],
850
+ "metadata": {
851
+ "kernelspec": {
852
+ "display_name": "Python 3",
853
+ "language": "python",
854
+ "name": "python3"
855
+ },
856
+ "language_info": {
857
+ "codemirror_mode": {
858
+ "name": "ipython",
859
+ "version": 3
860
+ },
861
+ "file_extension": ".py",
862
+ "mimetype": "text/x-python",
863
+ "name": "python",
864
+ "nbconvert_exporter": "python",
865
+ "pygments_lexer": "ipython3",
866
+ "version": "3.9"
867
+ }
868
+ },
869
+ "nbformat": 4,
870
+ "nbformat_minor": 5
871
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BioMod/toturials/gene_perturbation.ipynb CHANGED
@@ -1,359 +1,318 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "markdown",
5
- "metadata": {},
6
- "source": [
7
- "# Gene perturbation demo: Norman smoke with weight0407 on one GPU\n",
8
- "\n",
9
- "This notebook is a tutorial version of the Norman smoke-test launcher for the gene perturbation task. It keeps the single-GPU smoke-test recipe from `gene_perturbations/smoke.sh`, uses the weight0407 backbone, and writes outputs under the tutorials result folder."
10
- ]
11
- },
12
- {
13
- "cell_type": "markdown",
14
- "metadata": {},
15
- "source": [
16
- "## Privacy-safe absolute paths\n",
17
- "\n",
18
- "The notebook uses this alias path instead of a personal project path:\n",
19
- "\n",
20
- "`/data/share/biomodulefm_demo/single_cell_model`\n",
21
- "\n",
22
- "All paths below are absolute and resolve through that alias on the server."
23
- ]
24
- },
25
- {
26
- "cell_type": "markdown",
27
- "metadata": {},
28
- "source": [
29
- "## What this demo runs\n",
30
- "\n",
31
- "- Task code: `/data/jianqingzhu/BioMod-main/BioMod/gene_perturbation/train_module.py`\n",
32
- "- Dataset root: `/data/share/biomodulefm_demo/single_cell_model/DATA/GEARS_data`\n",
33
- "- Dataset name: `norman`\n",
34
- "- Split: `simulation`\n",
35
- "- Split seed: `1`\n",
36
- "- Backbone config: `/data/share/biomodulefm_demo/single_cell_model/weight/0407weight/mae_tost_48M_cross_attn_only_pure_memory.pkl`\n",
37
- "- Backbone checkpoint: `/data/share/biomodulefm_demo/single_cell_model/weight/0407weight/mae_tost_48M_cross_attn_only_pure_memory_epoch12.pth`\n",
38
- "- Output directory: `/data/share/biomodulefm_demo/tutorial_runs/gene_perturbation_norman_smoke_weight0407_1gpu_demo`\n",
39
- "\n",
40
- "Run this notebook with the `sclong_h200` Python environment/kernel, or another environment that can run the original gene perturbation smoke test."
41
- ]
42
- },
43
- {
44
- "cell_type": "code",
45
- "execution_count": null,
46
- "metadata": {},
47
- "outputs": [],
48
- "source": [
49
- "from pathlib import Path\n",
50
- "import importlib\n",
51
- "import os\n",
52
- "import shutil\n",
53
- "import subprocess\n",
54
- "import sys\n",
55
- "import pandas as pd\n",
56
- "try:\n",
57
- " from IPython.display import display\n",
58
- "except Exception:\n",
59
- " def display(obj):\n",
60
- " print(obj)\n",
61
- "\n",
62
- "BASE = Path('/data/share/biomodulefm_demo/single_cell_model')\n",
63
- "MODULE_MODEL = BASE / 'module_model'\n",
64
- "\n",
65
- "# Run the current module-named source tree.\n",
66
- "PROJECT_ROOT = Path('/data/jianqingzhu/BioMod-main/BioMod')\n",
67
- "TASK_ROOT = PROJECT_ROOT / 'gene_perturbation'\n",
68
- "TRAIN_SCRIPT = TASK_ROOT / 'train_module.py'\n",
69
- "\n",
70
- "DATA_DIR = BASE / 'DATA' / 'GEARS_data'\n",
71
- "DATA_NAME = 'norman'\n",
72
- "SPLIT = 'simulation'\n",
73
- "SEED = 1\n",
74
- "TRAIN_GENE_SET_SIZE = 0.75\n",
75
- "\n",
76
- "# Use the same weight0407 backbone as the zero-shot and cell-annotation tutorial notebooks.\n",
77
- "SCFM_HYPER = BASE / 'weight' / '0407weight' / 'mae_tost_48M_cross_attn_only_pure_memory.pkl'\n",
78
- "SCFM_CKPT = BASE / 'weight' / '0407weight' / 'mae_tost_48M_cross_attn_only_pure_memory_epoch12.pth'\n",
79
- "SCFM_GENES = MODULE_MODEL / 'selected_genes_27k.txt'\n",
80
- "SCFM_GENE2VEC = MODULE_MODEL / 'selected_gene2vec_27k.npy'\n",
81
- "GENE_MAP = MODULE_MODEL / 'gene2ens.csv'\n",
82
- "MODULE_DATA_DIR = PROJECT_ROOT / 'module_data_all'\n",
83
- "PERFORMER_CONT = PROJECT_ROOT.parent / 'model_dependencies' / 'performer_pytorch_cont'\n",
84
- "\n",
85
- "OUTPUT_PARENT = Path('/data/share/biomodulefm_demo/tutorial_runs')\n",
86
- "RUN_NAME = 'gene_perturbation_norman_smoke_weight0407_1gpu_demo'\n",
87
- "RESULT_DIR = OUTPUT_PARENT / RUN_NAME\n",
88
- "RESULT_DIR.mkdir(parents=True, exist_ok=True)\n",
89
- "VERBOSE_OUTPUT = RESULT_DIR / 'norman_verbose.txt'\n",
90
- "\n",
91
- "SPLIT_FILE = DATA_DIR / DATA_NAME / 'splits' / f'{DATA_NAME}_{SPLIT}_{SEED}_{TRAIN_GENE_SET_SIZE}.pkl'\n",
92
- "TARGET_H5AD = DATA_DIR / DATA_NAME / 'perturb_processed.h5ad'\n",
93
- "\n",
94
- "print('Python:', sys.executable)\n",
95
- "print('Task root:', TASK_ROOT)\n",
96
- "print('Run directory:', RESULT_DIR)"
97
- ]
98
- },
99
- {
100
- "cell_type": "code",
101
- "execution_count": null,
102
- "metadata": {},
103
- "outputs": [],
104
- "source": [
105
- "required_files = [\n",
106
- " TRAIN_SCRIPT,\n",
107
- " TARGET_H5AD,\n",
108
- " SPLIT_FILE,\n",
109
- " SCFM_HYPER,\n",
110
- " SCFM_CKPT,\n",
111
- " SCFM_GENES,\n",
112
- " SCFM_GENE2VEC,\n",
113
- " GENE_MAP,\n",
114
- " ROOT / 'models_performer_module.py',\n",
115
- "]\n",
116
- "required_dirs = [TASK_ROOT, MODULE_DATA_DIR]\n",
117
- "\n",
118
- "missing = [str(p) for p in required_files if not p.is_file()] + [str(p) for p in required_dirs if not p.is_dir()]\n",
119
- "if missing:\n",
120
- " raise FileNotFoundError('Missing required paths:\\n' + '\\n'.join(missing))\n",
121
- "\n",
122
- "for p in required_files:\n",
123
- " print('[FOUND FILE]', p)\n",
124
- "for p in required_dirs:\n",
125
- " print('[FOUND DIR ]', p)"
126
- ]
127
- },
128
- {
129
- "cell_type": "markdown",
130
- "metadata": {},
131
- "source": [
132
- "## Runtime setup\n",
133
- "\n",
134
- "The original smoke script creates a temporary `performer_module` package shim so that `performer_module.models_performer_module` resolves to the 0418 code snapshot. This avoids import conflicts with other project copies of similarly named modules."
135
- ]
136
- },
137
- {
138
- "cell_type": "code",
139
- "execution_count": null,
140
- "metadata": {},
141
- "outputs": [],
142
- "source": [
143
- "os.environ['CUDA_VISIBLE_DEVICES'] = '0'\n",
144
- "os.environ['PYTHONUNBUFFERED'] = '1'\n",
145
- "os.environ['OMP_NUM_THREADS'] = '1'\n",
146
- "os.environ['NUMBA_NUM_THREADS'] = '1'\n",
147
- "os.environ['NUMBA_DISABLE_JIT'] = '1'\n",
148
- "os.environ['NUMBA_CACHE_DIR'] = f'/tmp/numba_cache_gene_perturbation_demo_{os.environ.get(\"USER\", \"unknown\")}_{os.getpid()}'\n",
149
- "os.environ['WANDB_MODE'] = 'offline'\n",
150
- "os.environ['TORCH_NCCL_ASYNC_ERROR_HANDLING'] = '1'\n",
151
- "os.environ.pop('NCCL_ASYNC_ERROR_HANDLING', None)\n",
152
- "os.environ['NCCL_DEBUG'] = 'WARN'\n",
153
- "os.environ['TORCH_CPP_LOG_LEVEL'] = 'WARNING'\n",
154
- "os.environ['MASTER_ADDR'] = '127.0.0.1'\n",
155
- "os.environ['MASTER_PORT'] = os.environ.get('MASTER_PORT', '29621')\n",
156
- "os.environ['NCCL_SOCKET_IFNAME'] = 'eth0'\n",
157
- "os.environ['GLOO_SOCKET_IFNAME'] = 'eth0'\n",
158
- "Path(os.environ['NUMBA_CACHE_DIR']).mkdir(parents=True, exist_ok=True)\n",
159
- "\n",
160
- "PKG_SHIM = Path(f'/tmp/performer_module_pkg_gene_perturbation_demo_{os.getpid()}')\n",
161
- "if PKG_SHIM.exists() or PKG_SHIM.is_symlink():\n",
162
- " shutil.rmtree(PKG_SHIM, ignore_errors=True)\n",
163
- "(PKG_SHIM / 'performer_module').mkdir(parents=True, exist_ok=True)\n",
164
- "os.symlink(ROOT / 'models_performer_module.py', PKG_SHIM / 'performer_module' / 'models_performer_module.py')\n",
165
- "(PKG_SHIM / 'performer_module' / '__init__.py').write_text('', encoding='utf-8')\n",
166
- "\n",
167
- "pythonpath_parts = [\n",
168
- " str(PKG_SHIM),\n",
169
- " str(TASK_ROOT),\n",
170
- " str(PERFORMER_CONT),\n",
171
- " str(PROJECT_ROOT),\n",
172
- "]\n",
173
- "if os.environ.get('PYTHONPATH'):\n",
174
- " pythonpath_parts.append(os.environ['PYTHONPATH'])\n",
175
- "os.environ['PYTHONPATH'] = ':'.join(pythonpath_parts)\n",
176
- "for path_part in reversed(pythonpath_parts):\n",
177
- " if path_part and path_part not in sys.path:\n",
178
- " sys.path.insert(0, path_part)\n",
179
- "\n",
180
- "print('CUDA_VISIBLE_DEVICES =', os.environ['CUDA_VISIBLE_DEVICES'])\n",
181
- "print('NUMBA_CACHE_DIR =', os.environ['NUMBA_CACHE_DIR'])\n",
182
- "print('PKG_SHIM =', PKG_SHIM)\n",
183
- "print('PYTHONPATH =', os.environ['PYTHONPATH'])\n",
184
- "print('sys.path head =', sys.path[:4])"
185
- ]
186
- },
187
- {
188
- "cell_type": "code",
189
- "execution_count": null,
190
- "metadata": {},
191
- "outputs": [],
192
- "source": [
193
- "# Pre-check that the current module source is importable.\n",
194
- "module = importlib.import_module('models_performer_module')\n",
195
- "print('[CHECK] module file:', module.__file__)\n",
196
- "print('[CHECK] has sanitize_performer_module_config:', hasattr(module, 'sanitize_performer_module_config'))\n",
197
- "if not hasattr(module, 'sanitize_performer_module_config'):\n",
198
- " raise RuntimeError('sanitize_performer_module_config not found in imported models_performer_module')"
199
- ]
200
- },
201
- {
202
- "cell_type": "markdown",
203
- "metadata": {},
204
- "source": [
205
- "## Smoke-test training command\n",
206
- "\n",
207
- "`train_module.py` initializes PyTorch distributed training unconditionally, so even the one-GPU version should be launched with one process. This notebook uses `python -m torch.distributed.run --nproc_per_node=1`, equivalent to a single-process `torchrun`."
208
- ]
209
- },
210
- {
211
- "cell_type": "code",
212
- "execution_count": null,
213
- "metadata": {},
214
- "outputs": [],
215
- "source": [
216
- "EPOCHS = 1\n",
217
- "BATCH_SIZE = 16\n",
218
- "TEST_BATCH_SIZE = 32\n",
219
- "HIDDEN_SIZE = 1024\n",
220
- "ACCUMULATION_STEPS = 8\n",
221
- "LR = '1e-3'\n",
222
- "MASTER_PORT = os.environ['MASTER_PORT']\n",
223
- "\n",
224
- "cmd = [\n",
225
- " sys.executable,\n",
226
- " '-m', 'torch.distributed.run',\n",
227
- " '--standalone',\n",
228
- " '--nnodes=1',\n",
229
- " '--nproc_per_node=1',\n",
230
- " f'--master_port={MASTER_PORT}',\n",
231
- " str(TRAIN_SCRIPT),\n",
232
- " '--data_dir', str(DATA_DIR),\n",
233
- " '--data_name', DATA_NAME,\n",
234
- " '--split', SPLIT,\n",
235
- " '--result_dir', str(RESULT_DIR),\n",
236
- " '--seed', str(SEED),\n",
237
- " '--epochs', str(EPOCHS),\n",
238
- " '--batch_size', str(BATCH_SIZE),\n",
239
- " '--test_batch_size', str(TEST_BATCH_SIZE),\n",
240
- " '--train_gene_set_size', str(TRAIN_GENE_SET_SIZE),\n",
241
- " '--hidden_size', str(HIDDEN_SIZE),\n",
242
- " '--run_name', 'weight0407_norman_smoke_1gpu_demo',\n",
243
- " '--scfm_hyper_params_path', str(SCFM_HYPER),\n",
244
- " '--scfm_ckpt_path', str(SCFM_CKPT),\n",
245
- " '--scfm_genes_list_path', str(SCFM_GENES),\n",
246
- " '--gene_mapping_path', str(GENE_MAP),\n",
247
- " '--scfm_gene2vec_file', str(SCFM_GENE2VEC),\n",
248
- " '--accumulation_steps', str(ACCUMULATION_STEPS),\n",
249
- " '--module_data_dir', str(MODULE_DATA_DIR),\n",
250
- " '--verbose_output_path', str(VERBOSE_OUTPUT),\n",
251
- " '--lr', LR,\n",
252
- "]\n",
253
- "\n",
254
- "print('Command:')\n",
255
- "print(' '.join(cmd))"
256
- ]
257
- },
258
- {
259
- "cell_type": "code",
260
- "execution_count": null,
261
- "metadata": {},
262
- "outputs": [],
263
- "source": [
264
- "# Run the one-GPU smoke test. It should be much shorter than a full gene perturbation training run.\n",
265
- "log_path = RESULT_DIR / 'manual_launch_1gpu.log'\n",
266
- "print('Writing log to:', log_path)\n",
267
- "\n",
268
- "with log_path.open('w') as log:\n",
269
- " process = subprocess.Popen(\n",
270
- " cmd,\n",
271
- " stdout=subprocess.PIPE,\n",
272
- " stderr=subprocess.STDOUT,\n",
273
- " text=True,\n",
274
- " bufsize=1,\n",
275
- " cwd=str(TASK_ROOT),\n",
276
- " env=os.environ.copy(),\n",
277
- " )\n",
278
- " for line in process.stdout:\n",
279
- " print(line, end='')\n",
280
- " log.write(line)\n",
281
- " return_code = process.wait()\n",
282
- "\n",
283
- "if return_code != 0:\n",
284
- " raise RuntimeError(f'gene perturbation smoke demo failed with return code {return_code}; see {log_path}')\n",
285
- "\n",
286
- "print('Done.')"
287
- ]
288
- },
289
- {
290
- "cell_type": "markdown",
291
- "metadata": {},
292
- "source": [
293
- "## Inspect outputs\n",
294
- "\n",
295
- "The smoke run usually writes `config.pkl`, `model.pt`, `params.csv`, the notebook log, and `norman_verbose.txt` when `--verbose_output_path` is enabled."
296
- ]
297
- },
298
- {
299
- "cell_type": "code",
300
- "execution_count": null,
301
- "metadata": {},
302
- "outputs": [],
303
- "source": [
304
- "for path in sorted(RESULT_DIR.glob('*')):\n",
305
- " size_mb = path.stat().st_size / 1024 / 1024 if path.is_file() else 0\n",
306
- " print(f'{path.name:45s} {size_mb:8.2f} MB')"
307
- ]
308
- },
309
- {
310
- "cell_type": "code",
311
- "execution_count": null,
312
- "metadata": {},
313
- "outputs": [],
314
- "source": [
315
- "params_path = RESULT_DIR / 'params.csv'\n",
316
- "if params_path.exists():\n",
317
- " params = pd.read_csv(params_path, index_col=0)\n",
318
- " display(params)\n",
319
- "else:\n",
320
- " print('params.csv not found yet:', params_path)"
321
- ]
322
- },
323
- {
324
- "cell_type": "code",
325
- "execution_count": null,
326
- "metadata": {},
327
- "outputs": [],
328
- "source": [
329
- "if VERBOSE_OUTPUT.exists():\n",
330
- " with VERBOSE_OUTPUT.open('r', encoding='utf-8', errors='replace') as handle:\n",
331
- " for _, line in zip(range(20), handle):\n",
332
- " print(line.rstrip())\n",
333
- "else:\n",
334
- " print('Verbose output not found yet:', VERBOSE_OUTPUT)"
335
- ]
336
- }
337
- ],
338
- "metadata": {
339
- "kernelspec": {
340
- "display_name": "Python 3",
341
- "language": "python",
342
- "name": "python3"
343
- },
344
- "language_info": {
345
- "codemirror_mode": {
346
- "name": "ipython",
347
- "version": 3
348
- },
349
- "file_extension": ".py",
350
- "mimetype": "text/x-python",
351
- "name": "python",
352
- "nbconvert_exporter": "python",
353
- "pygments_lexer": "ipython3",
354
- "version": "3.9"
355
- }
356
- },
357
- "nbformat": 4,
358
- "nbformat_minor": 5
359
- }
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Gene perturbation prediction with BioMod model weights\n",
8
+ "\n",
9
+ "Train the gene perturbation model on the local Norman dataset with a BioMod backbone. This notebook prepares the project paths, configures the simulation split, launches training when explicitly enabled, and inspects the saved outputs."
10
+ ]
11
+ },
12
+ {
13
+ "cell_type": "markdown",
14
+ "metadata": {},
15
+ "source": [
16
+ "## Project paths\n",
17
+ "\n",
18
+ "Start Jupyter from `BioMod/toturials/`. The setup cell uses `Path(\"../..\").resolve()` to select the repository root, following the project-relative path convention used in scGPT tutorials. All code, data, model weight, and output paths are derived from this root and resolved to absolute paths for the training command. If Jupyter starts at the repository root, change that line to `Path(\".\").resolve()`. For another launch location, set `PROJECT_ROOT` to the checkout root."
19
+ ]
20
+ },
21
+ {
22
+ "cell_type": "markdown",
23
+ "metadata": {},
24
+ "source": [
25
+ "## Inputs and workflow\n",
26
+ "\n",
27
+ "- Entry point: `BioMod/gene_perturbation/train_module.py`.\n",
28
+ "- Dataset parent: `data/downstream/final_data/`; the loader appends `norman/`.\n",
29
+ "- Processed expression: `norman/perturb_processed.h5ad` under that parent.\n",
30
+ "- Split: `simulation`, seed `1`, training-gene fraction `0.75`.\n",
31
+ "- Model weights and matching configuration: the `.pth` / `.pkl` pair under `weight/0810model/`.\n",
32
+ "- Gene vocabulary, mapping, embeddings, and module definitions: `model_dependencies/`.\n",
33
+ "- Results: `tutorial_runs/gene_perturbation_norman_biomod/`.\n",
34
+ "\n",
35
+ "Use a kernel with the project's gene perturbation dependencies installed. The current training entry point initializes NCCL and requires a Linux CUDA environment, including a suitably configured WSL environment. The notebook can prepare paths on Windows, but native Windows cannot run this NCCL training entry point. Use paths valid inside the selected training environment."
36
+ ]
37
+ },
38
+ {
39
+ "cell_type": "code",
40
+ "execution_count": null,
41
+ "metadata": {},
42
+ "outputs": [],
43
+ "source": [
44
+ "from pathlib import Path\n",
45
+ "import json\n",
46
+ "import os\n",
47
+ "import shlex\n",
48
+ "import subprocess\n",
49
+ "import sys\n",
50
+ "import tempfile\n",
51
+ "import pandas as pd\n",
52
+ "try:\n",
53
+ " from IPython.display import display\n",
54
+ "except ImportError:\n",
55
+ " def display(obj):\n",
56
+ " print(obj)\n",
57
+ "\n",
58
+ "PROJECT_ROOT = Path('../..').resolve()\n",
59
+ "if not (PROJECT_ROOT / 'BioMod' / 'models_performer_module.py').is_file() or not (PROJECT_ROOT / 'model_dependencies').is_dir():\n",
60
+ " raise FileNotFoundError(\n",
61
+ " 'Project root not found. Start Jupyter from BioMod/toturials, or set '\n",
62
+ " 'PROJECT_ROOT to your repository root (use Path(\".\").resolve() if '\n",
63
+ " 'Jupyter was started at the repository root).'\n",
64
+ " )\n",
65
+ "\n",
66
+ "\n",
67
+ "SOURCE_DIR = PROJECT_ROOT / 'BioMod'\n",
68
+ "ASSET_ROOT = PROJECT_ROOT / 'model_dependencies'\n",
69
+ "TASK_ROOT = SOURCE_DIR / 'gene_perturbation'\n",
70
+ "TRAIN_SCRIPT = TASK_ROOT / 'train_module.py'\n",
71
+ "DATA_DIR = PROJECT_ROOT / 'data' / 'downstream' / 'final_data'\n",
72
+ "DATA_NAME = 'norman'\n",
73
+ "SPLIT = 'simulation'\n",
74
+ "SEED = 1\n",
75
+ "TRAIN_GENE_SET_SIZE = 0.75\n",
76
+ "\n",
77
+ "# Keep the BioMod model weights and their matching configuration together.\n",
78
+ "WEIGHT_DIR = PROJECT_ROOT / 'weight' / '0810model'\n",
79
+ "SCFM_HYPER = WEIGHT_DIR / 'mae_tost_70M_75mask_ec2.pkl'\n",
80
+ "SCFM_CKPT = WEIGHT_DIR / 'mae_tost_70M_75mask_ec2_epoch0.pth'\n",
81
+ "SCFM_GENES = ASSET_ROOT / 'selected_genes_27k.txt'\n",
82
+ "SCFM_GENE2VEC = ASSET_ROOT / 'selected_gene2vec_27k.npy'\n",
83
+ "GENE_MAP = ASSET_ROOT / 'gene2ens.csv'\n",
84
+ "MODULE_DATA_DIR = ASSET_ROOT / 'module_data_all'\n",
85
+ "\n",
86
+ "OUTPUT_PARENT = PROJECT_ROOT / 'tutorial_runs'\n",
87
+ "RUN_NAME = 'gene_perturbation_norman_biomod'\n",
88
+ "RESULT_DIR = OUTPUT_PARENT / RUN_NAME\n",
89
+ "RUN_TRAINING = False\n",
90
+ "SPLIT_FILE = DATA_DIR / DATA_NAME / 'splits' / f'{DATA_NAME}_{SPLIT}_{SEED}_{TRAIN_GENE_SET_SIZE}.pkl'\n",
91
+ "TARGET_H5AD = DATA_DIR / DATA_NAME / 'perturb_processed.h5ad'\n",
92
+ "\n",
93
+ "print('Python:', sys.executable)\n",
94
+ "print('Repository:', PROJECT_ROOT)\n",
95
+ "print('Dataset:', TARGET_H5AD)\n",
96
+ "print('Run directory:', RESULT_DIR)"
97
+ ]
98
+ },
99
+ {
100
+ "cell_type": "code",
101
+ "execution_count": null,
102
+ "metadata": {},
103
+ "outputs": [],
104
+ "source": [
105
+ "required_files = [\n",
106
+ " TRAIN_SCRIPT, TARGET_H5AD, SPLIT_FILE, SCFM_HYPER, SCFM_CKPT,\n",
107
+ " SCFM_GENES, SCFM_GENE2VEC, GENE_MAP,\n",
108
+ " SOURCE_DIR / 'models_performer_module.py',\n",
109
+ " MODULE_DATA_DIR / 'merged_modules.json',\n",
110
+ "]\n",
111
+ "required_dirs = [TASK_ROOT, ASSET_ROOT, DATA_DIR / DATA_NAME / 'data_pyg']\n",
112
+ "missing = [str(p) for p in required_files if not p.is_file()]\n",
113
+ "missing += [str(p) for p in required_dirs if not p.is_dir()]\n",
114
+ "if missing:\n",
115
+ " raise FileNotFoundError('Missing required paths:\\n' + '\\n'.join(missing))\n",
116
+ "for path in required_files:\n",
117
+ " print('[FOUND]', path)"
118
+ ]
119
+ },
120
+ {
121
+ "cell_type": "markdown",
122
+ "metadata": {},
123
+ "source": [
124
+ "## Runtime setup\n",
125
+ "\n",
126
+ "The current source imports `BioMod.models_performer_module` directly. The child process uses the repository and bundled dependency directories on `PYTHONPATH`; no temporary package or symlink is needed. The notebook leaves the kernel environment unchanged."
127
+ ]
128
+ },
129
+ {
130
+ "cell_type": "code",
131
+ "execution_count": null,
132
+ "metadata": {},
133
+ "outputs": [],
134
+ "source": [
135
+ "run_env = os.environ.copy()\n",
136
+ "run_env['CUDA_VISIBLE_DEVICES'] = '0'\n",
137
+ "run_env['PYTHONUNBUFFERED'] = '1'\n",
138
+ "run_env.setdefault('OMP_NUM_THREADS', '1')\n",
139
+ "run_env.setdefault('NUMBA_NUM_THREADS', '1')\n",
140
+ "run_env['NUMBA_DISABLE_JIT'] = '1'\n",
141
+ "run_env.setdefault('NUMBA_CACHE_DIR', str(Path(tempfile.gettempdir()) / f'biomod_perturbation_{os.getpid()}'))\n",
142
+ "run_env['WANDB_MODE'] = 'offline'\n",
143
+ "run_env.setdefault('TORCH_NCCL_ASYNC_ERROR_HANDLING', '1')\n",
144
+ "run_env.pop('NCCL_ASYNC_ERROR_HANDLING', None)\n",
145
+ "run_env.setdefault('NCCL_DEBUG', 'WARN')\n",
146
+ "pythonpath_parts = [str(TASK_ROOT), str(PROJECT_ROOT), str(SOURCE_DIR), str(ASSET_ROOT)]\n",
147
+ "if run_env.get('PYTHONPATH'):\n",
148
+ " pythonpath_parts.append(run_env['PYTHONPATH'])\n",
149
+ "run_env['PYTHONPATH'] = os.pathsep.join(pythonpath_parts)\n",
150
+ "print('CUDA_VISIBLE_DEVICES =', run_env['CUDA_VISIBLE_DEVICES'])\n",
151
+ "print('PYTHONPATH =', run_env['PYTHONPATH'])"
152
+ ]
153
+ },
154
+ {
155
+ "cell_type": "code",
156
+ "execution_count": null,
157
+ "metadata": {},
158
+ "outputs": [],
159
+ "source": [
160
+ "# File checks do not load the model weights or deserialize the configuration.\n",
161
+ "print('Backbone source:', SOURCE_DIR / 'models_performer_module.py')\n",
162
+ "print('Bundled Performer:', ASSET_ROOT / 'performer_pytorch_cont')\n",
163
+ "print('Training enabled:', RUN_TRAINING)"
164
+ ]
165
+ },
166
+ {
167
+ "cell_type": "markdown",
168
+ "metadata": {},
169
+ "source": [
170
+ "## Training command\n",
171
+ "\n",
172
+ "`train_module.py` initializes distributed training even for one GPU. The command therefore uses `python -m torch.distributed.run --standalone --nproc_per_node=1`. Review the settings below, then set `RUN_TRAINING = True` in the setup cell to launch. The training script selects a checkpoint using validation MSE on the top 20 differentially expressed genes, then evaluates the selected checkpoint on the test split. Batch sizes, epoch count, and learning rate are configurable examples, not a verified benchmark recipe."
173
+ ]
174
+ },
175
+ {
176
+ "cell_type": "code",
177
+ "execution_count": null,
178
+ "metadata": {},
179
+ "outputs": [],
180
+ "source": [
181
+ "EPOCHS = 1\n",
182
+ "BATCH_SIZE = 16\n",
183
+ "TEST_BATCH_SIZE = 32\n",
184
+ "HIDDEN_SIZE = 1024\n",
185
+ "ACCUMULATION_STEPS = 8\n",
186
+ "LR = '1e-3'\n",
187
+ "\n",
188
+ "cmd = [\n",
189
+ " sys.executable, '-m', 'torch.distributed.run',\n",
190
+ " '--standalone', '--nnodes=1', '--nproc_per_node=1',\n",
191
+ " str(TRAIN_SCRIPT),\n",
192
+ " '--data_dir', str(DATA_DIR),\n",
193
+ " '--data_name', DATA_NAME,\n",
194
+ " '--split', SPLIT,\n",
195
+ " '--result_dir', str(RESULT_DIR),\n",
196
+ " '--seed', str(SEED),\n",
197
+ " '--epochs', str(EPOCHS),\n",
198
+ " '--batch_size', str(BATCH_SIZE),\n",
199
+ " '--test_batch_size', str(TEST_BATCH_SIZE),\n",
200
+ " '--train_gene_set_size', str(TRAIN_GENE_SET_SIZE),\n",
201
+ " '--hidden_size', str(HIDDEN_SIZE),\n",
202
+ " '--run_name', RUN_NAME,\n",
203
+ " '--scfm_hyper_params_path', str(SCFM_HYPER),\n",
204
+ " '--scfm_ckpt_path', str(SCFM_CKPT),\n",
205
+ " '--scfm_genes_list_path', str(SCFM_GENES),\n",
206
+ " '--gene_mapping_path', str(GENE_MAP),\n",
207
+ " '--scfm_gene2vec_file', str(SCFM_GENE2VEC),\n",
208
+ " '--accumulation_steps', str(ACCUMULATION_STEPS),\n",
209
+ " '--module_data_dir', str(MODULE_DATA_DIR),\n",
210
+ " '--lr', LR,\n",
211
+ "]\n",
212
+ "print('Command:')\n",
213
+ "print(subprocess.list2cmdline(cmd) if os.name == 'nt' else shlex.join(cmd))"
214
+ ]
215
+ },
216
+ {
217
+ "cell_type": "code",
218
+ "execution_count": null,
219
+ "metadata": {},
220
+ "outputs": [],
221
+ "source": [
222
+ "log_path = RESULT_DIR / 'training.log'\n",
223
+ "if not RUN_TRAINING:\n",
224
+ " print('Training is disabled. Review the paths and settings, then set RUN_TRAINING = True to launch.')\n",
225
+ "else:\n",
226
+ " if sys.platform != 'linux':\n",
227
+ " raise RuntimeError('The training script requires Linux with CUDA/NCCL. Use a compatible kernel and project paths.')\n",
228
+ " RESULT_DIR.mkdir(parents=True, exist_ok=True)\n",
229
+ " Path(run_env['NUMBA_CACHE_DIR']).mkdir(parents=True, exist_ok=True)\n",
230
+ " print('Writing log to:', log_path)\n",
231
+ " with log_path.open('w', encoding='utf-8') as log:\n",
232
+ " process = subprocess.Popen(\n",
233
+ " cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n",
234
+ " text=True, encoding='utf-8', errors='replace', bufsize=1,\n",
235
+ " cwd=str(TASK_ROOT), env=run_env,\n",
236
+ " )\n",
237
+ " for line in process.stdout:\n",
238
+ " print(line, end='')\n",
239
+ " log.write(line)\n",
240
+ " return_code = process.wait()\n",
241
+ " if return_code != 0:\n",
242
+ " raise RuntimeError(f'Gene perturbation training failed with return code {return_code}; see {log_path}')\n",
243
+ " print('Training finished.')"
244
+ ]
245
+ },
246
+ {
247
+ "cell_type": "markdown",
248
+ "metadata": {},
249
+ "source": [
250
+ "## Inspect outputs\n",
251
+ "\n",
252
+ "A successful run saves the selected model as `model.pt`, its configuration as `config.pkl`, and the command parameters as `params.csv`. The notebook captures console metrics in `training.log`; the current script does not provide a separate verbose-output argument. These outputs are written to the run directory and do not overwrite the input BioMod model weights. Result cells can also inspect a previous completed run."
253
+ ]
254
+ },
255
+ {
256
+ "cell_type": "code",
257
+ "execution_count": null,
258
+ "metadata": {},
259
+ "outputs": [],
260
+ "source": [
261
+ "if not RESULT_DIR.is_dir():\n",
262
+ " print('No run directory yet:', RESULT_DIR)\n",
263
+ "else:\n",
264
+ " for path in sorted(RESULT_DIR.iterdir()):\n",
265
+ " size_mb = path.stat().st_size / 1024 / 1024 if path.is_file() else 0\n",
266
+ " print(f'{path.name:45s} {size_mb:8.2f} MB')"
267
+ ]
268
+ },
269
+ {
270
+ "cell_type": "code",
271
+ "execution_count": null,
272
+ "metadata": {},
273
+ "outputs": [],
274
+ "source": [
275
+ "params_path = RESULT_DIR / 'params.csv'\n",
276
+ "if params_path.is_file():\n",
277
+ " display(pd.read_csv(params_path, index_col=0))\n",
278
+ "else:\n",
279
+ " print('Parameters are not available yet:', params_path)"
280
+ ]
281
+ },
282
+ {
283
+ "cell_type": "code",
284
+ "execution_count": null,
285
+ "metadata": {},
286
+ "outputs": [],
287
+ "source": [
288
+ "if log_path.is_file():\n",
289
+ " from collections import deque\n",
290
+ " with log_path.open('r', encoding='utf-8', errors='replace') as handle:\n",
291
+ " print(''.join(deque(handle, maxlen=40)))\n",
292
+ "else:\n",
293
+ " print('Training log is not available yet:', log_path)"
294
+ ]
295
+ }
296
+ ],
297
+ "metadata": {
298
+ "kernelspec": {
299
+ "display_name": "Python 3",
300
+ "language": "python",
301
+ "name": "python3"
302
+ },
303
+ "language_info": {
304
+ "codemirror_mode": {
305
+ "name": "ipython",
306
+ "version": 3
307
+ },
308
+ "file_extension": ".py",
309
+ "mimetype": "text/x-python",
310
+ "name": "python",
311
+ "nbconvert_exporter": "python",
312
+ "pygments_lexer": "ipython3",
313
+ "version": "3.9"
314
+ }
315
+ },
316
+ "nbformat": 4,
317
+ "nbformat_minor": 5
318
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BioMod/toturials/zero_shot_batch_integration.ipynb CHANGED
@@ -1,351 +1,340 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "markdown",
5
- "metadata": {},
6
- "source": [
7
- "# Zero-shot batch integration demo: Kim2020 Lung no malignant\n",
8
- "\n",
9
- "This notebook is a single-dataset tutorial version of `run_zero_shot_all_weight0407_5times.sh`. It runs the weight0407 model on the `Kim2020_Lung_noMalignant.h5ad` dataset, computes scIB metrics, and generates UMAP visualizations."
10
- ]
11
- },
12
- {
13
- "cell_type": "markdown",
14
- "metadata": {},
15
- "source": [
16
- "## What this demo uses\n",
17
- "\n",
18
- "- Dataset: `/data/share/biomodulefm_demo/single_cell_model/DATA/singcell/Kim2020_Lung_noMalignant.h5ad`\n",
19
- "- Model config: `/data/share/biomodulefm_demo/single_cell_model/weight/0407weight/mae_tost_48M_cross_attn_only_pure_memory.pkl`\n",
20
- "- Model checkpoint: `/data/share/biomodulefm_demo/single_cell_model/weight/0407weight/mae_tost_48M_cross_attn_only_pure_memory_epoch12.pth`\n",
21
- "- Runner script: `/data/share/biomodulefm_demo/single_cell_model/module_model/draw/zero_shot_umap_scib.py`\n",
22
- "- Output directory: `/data/share/biomodulefm_demo/tutorial_runs/zero_shot_kim_lung_no_malignant_demo`\n",
23
- "\n",
24
- "Run this notebook with the `sclong` Python environment/kernel."
25
- ]
26
- },
27
- {
28
- "cell_type": "code",
29
- "execution_count": null,
30
- "metadata": {},
31
- "outputs": [],
32
- "source": [
33
- "from pathlib import Path\n",
34
- "import importlib.util\n",
35
- "import os\n",
36
- "import subprocess\n",
37
- "import sys\n",
38
- "try:\n",
39
- " from IPython.display import Image, display\n",
40
- "except Exception:\n",
41
- " Image = None\n",
42
- " def display(obj):\n",
43
- " print(obj)\n",
44
- "import pandas as pd\n",
45
- "\n",
46
- "BASE = Path('/data/share/biomodulefm_demo/single_cell_model')\n",
47
- "MODULE_MODEL = BASE / 'module_model'\n",
48
- "PROJECT_ROOT = Path('/data/jianqingzhu/BioMod-main/BioMod')\n",
49
- "\n",
50
- "SCRIPT = MODULE_MODEL / 'draw' / 'zero_shot_umap_scib.py'\n",
51
- "DATA_PATH = BASE / 'DATA' / 'singcell' / 'Kim2020_Lung_noMalignant.h5ad'\n",
52
- "\n",
53
- "CONFIG_PKL = BASE / 'weight' / '0407weight' / 'mae_tost_48M_cross_attn_only_pure_memory.pkl'\n",
54
- "CKPT_PATH = BASE / 'weight' / '0407weight' / 'mae_tost_48M_cross_attn_only_pure_memory_epoch12.pth'\n",
55
- "\n",
56
- "GENE2VEC = MODULE_MODEL / 'selected_gene2vec_27k.npy'\n",
57
- "GENE_MAP = MODULE_MODEL / 'gene2ens.csv'\n",
58
- "GENE_LIST = MODULE_MODEL / 'selected_genes_27k.txt'\n",
59
- "MODULE_DATA_DIR = PROJECT_ROOT / 'module_data_all'\n",
60
- "PERFORMER_CONT = MODULE_MODEL / 'performer_pytorch_cont'\n",
61
- "\n",
62
- "TUTORIAL_RUN_ROOT = Path('/data/share/biomodulefm_demo/tutorial_runs')\n",
63
- "OUT_DIR = TUTORIAL_RUN_ROOT / 'zero_shot_kim_lung_no_malignant_demo'\n",
64
- "OUT_DIR.mkdir(parents=True, exist_ok=True)\n",
65
- "\n",
66
- "OUTPUT_PREFIX = 'kim_lung_no_malignant_zero_shot_demo_seed0'\n",
67
- "\n",
68
- "# Use 'all' to match the shell launcher. For a quick smoke test, change this to a smaller number such as '2000'.\n",
69
- "N_CELLS = 'all'\n",
70
- "BATCH_SIZE = 8\n",
71
- "UMAP_RANDOM_STATE = 0\n",
72
- "\n",
73
- "print('Python:', sys.executable)\n",
74
- "print('Output directory:', OUT_DIR)"
75
- ]
76
- },
77
- {
78
- "cell_type": "code",
79
- "execution_count": null,
80
- "metadata": {},
81
- "outputs": [],
82
- "source": [
83
- "required_files = [\n",
84
- " SCRIPT,\n",
85
- " DATA_PATH,\n",
86
- " CONFIG_PKL,\n",
87
- " CKPT_PATH,\n",
88
- " GENE2VEC,\n",
89
- " GENE_MAP,\n",
90
- " GENE_LIST,\n",
91
- "]\n",
92
- "required_dirs = [PROJECT_ROOT, MODULE_DATA_DIR]\n",
93
- "\n",
94
- "missing = [str(p) for p in required_files if not p.is_file()] + [str(p) for p in required_dirs if not p.is_dir()]\n",
95
- "if missing:\n",
96
- " raise FileNotFoundError('Missing required paths:\\n' + '\\n'.join(missing))\n",
97
- "\n",
98
- "for p in required_files:\n",
99
- " print('[FOUND FILE]', p)\n",
100
- "for p in required_dirs:\n",
101
- " print('[FOUND DIR ]', p)"
102
- ]
103
- },
104
- {
105
- "cell_type": "code",
106
- "execution_count": null,
107
- "metadata": {},
108
- "outputs": [],
109
- "source": [
110
- "# Match the runtime environment used by the shell launcher.\n",
111
- "os.environ['PYTHONUNBUFFERED'] = '1'\n",
112
- "os.environ.setdefault('CUDA_VISIBLE_DEVICES', '0')\n",
113
- "os.environ.setdefault('OMP_NUM_THREADS', '8')\n",
114
- "os.environ.setdefault('NUMBA_CACHE_DIR', f'/tmp/numba_cache_zero_shot_demo_{os.environ.get(\"USER\", \"unknown\")}_{os.getpid()}')\n",
115
- "Path(os.environ['NUMBA_CACHE_DIR']).mkdir(parents=True, exist_ok=True)\n",
116
- "\n",
117
- "pythonpath_parts = [str(MODULE_MODEL), str(PROJECT_ROOT)]\n",
118
- "if PERFORMER_CONT.is_dir():\n",
119
- " pythonpath_parts.append(str(PERFORMER_CONT))\n",
120
- "if os.environ.get('PYTHONPATH'):\n",
121
- " pythonpath_parts.append(os.environ['PYTHONPATH'])\n",
122
- "os.environ['PYTHONPATH'] = ':'.join(pythonpath_parts)\n",
123
- "\n",
124
- "print('CUDA_VISIBLE_DEVICES =', os.environ['CUDA_VISIBLE_DEVICES'])\n",
125
- "print('OMP_NUM_THREADS =', os.environ['OMP_NUM_THREADS'])\n",
126
- "print('NUMBA_CACHE_DIR =', os.environ['NUMBA_CACHE_DIR'])\n",
127
- "print('PYTHONPATH =', os.environ['PYTHONPATH'])"
128
- ]
129
- },
130
- {
131
- "cell_type": "code",
132
- "execution_count": null,
133
- "metadata": {},
134
- "outputs": [],
135
- "source": [
136
- "# Install scIB from the Tsinghua PyPI mirror if the current kernel does not have it.\n",
137
- "if importlib.util.find_spec('scib') is None:\n",
138
- " subprocess.check_call([\n",
139
- " sys.executable, '-m', 'pip', 'install', 'scib',\n",
140
- " '-i', 'https://pypi.tuna.tsinghua.edu.cn/simple',\n",
141
- " '--trusted-host', 'pypi.tuna.tsinghua.edu.cn',\n",
142
- " '--retries', '10',\n",
143
- " '--timeout', '60',\n",
144
- " ])\n",
145
- "\n",
146
- "import scib\n",
147
- "print('scib import ok:', getattr(scib, '__version__', 'unknown'))"
148
- ]
149
- },
150
- {
151
- "cell_type": "code",
152
- "execution_count": null,
153
- "metadata": {},
154
- "outputs": [],
155
- "source": [
156
- "cmd = [\n",
157
- " sys.executable,\n",
158
- " str(SCRIPT),\n",
159
- " '--data_path', str(DATA_PATH),\n",
160
- " '--gene2vec_path', str(GENE2VEC),\n",
161
- " '--scfm_hyper_params_path', str(CONFIG_PKL),\n",
162
- " '--scfm_ckpt_path', str(CKPT_PATH),\n",
163
- " '--module_data_dir', str(MODULE_DATA_DIR),\n",
164
- " '--gene_mapping_path', str(GENE_MAP),\n",
165
- " '--scfm_genes_file', str(GENE_LIST),\n",
166
- " '--project_root', str(PROJECT_ROOT),\n",
167
- " '--output_dir', str(OUT_DIR),\n",
168
- " '--output_prefix', OUTPUT_PREFIX,\n",
169
- " '--gene_col', 'gene_name',\n",
170
- " '--batch_key', 'sample',\n",
171
- " '--label_key', 'cell_type',\n",
172
- " '--output_key', 'X_sclong_module',\n",
173
- " '--pool_method', 'max',\n",
174
- " '--n_cells', N_CELLS,\n",
175
- " '--batch_size', str(BATCH_SIZE),\n",
176
- " '--umap_n_neighbors', '15',\n",
177
- " '--umap_min_dist', '0.5',\n",
178
- " '--umap_random_state', str(UMAP_RANDOM_STATE),\n",
179
- " '--umap_celltype_title', 'Kim2020 Lung no malignant | cell type',\n",
180
- " '--umap_batch_title', 'Kim2020 Lung no malignant | sample',\n",
181
- "]\n",
182
- "\n",
183
- "print('Command:')\n",
184
- "print(' '.join(cmd))"
185
- ]
186
- },
187
- {
188
- "cell_type": "code",
189
- "execution_count": null,
190
- "metadata": {},
191
- "outputs": [],
192
- "source": [
193
- "log_path = OUT_DIR / f'{OUTPUT_PREFIX}.log'\n",
194
- "print('Writing log to:', log_path)\n",
195
- "\n",
196
- "with log_path.open('w') as log:\n",
197
- " process = subprocess.Popen(\n",
198
- " cmd,\n",
199
- " stdout=subprocess.PIPE,\n",
200
- " stderr=subprocess.STDOUT,\n",
201
- " text=True,\n",
202
- " bufsize=1,\n",
203
- " env=os.environ.copy(),\n",
204
- " )\n",
205
- " for line in process.stdout:\n",
206
- " print(line, end='')\n",
207
- " log.write(line)\n",
208
- " return_code = process.wait()\n",
209
- "\n",
210
- "if return_code != 0:\n",
211
- " raise RuntimeError(f'zero-shot demo failed with return code {return_code}; see {log_path}')\n",
212
- "\n",
213
- "print('Done.')"
214
- ]
215
- },
216
- {
217
- "cell_type": "code",
218
- "execution_count": null,
219
- "metadata": {},
220
- "outputs": [],
221
- "source": [
222
- "metrics_path = OUT_DIR / f'{OUTPUT_PREFIX}_scib_metrics.csv'\n",
223
- "metrics = pd.read_csv(metrics_path)\n",
224
- "metrics"
225
- ]
226
- },
227
- {
228
- "cell_type": "code",
229
- "execution_count": null,
230
- "metadata": {},
231
- "outputs": [],
232
- "source": [
233
- "avg_metrics = metrics[metrics['metric'].isin(['avg_batch', 'avg_bio', 'avg_score'])].copy()\n",
234
- "avg_metrics"
235
- ]
236
- },
237
- {
238
- "cell_type": "code",
239
- "execution_count": null,
240
- "metadata": {},
241
- "outputs": [],
242
- "source": [
243
- "celltype_png = OUT_DIR / f'{OUTPUT_PREFIX}_umap_by_cell_type.png'\n",
244
- "batch_png = OUT_DIR / f'{OUTPUT_PREFIX}_umap_by_batch.png'\n",
245
- "\n",
246
- "print(celltype_png)\n",
247
- "if Image is not None:\n",
248
- " display(Image(filename=str(celltype_png)))\n",
249
- "\n",
250
- "print(batch_png)\n",
251
- "if Image is not None:\n",
252
- " display(Image(filename=str(batch_png)))"
253
- ]
254
- },
255
- {
256
- "cell_type": "markdown",
257
- "metadata": {},
258
- "source": [
259
- "## Optional: export an A4 PDF/PNG UMAP panel\n",
260
- "\n",
261
- "The runner already outputs two PNG files. The following cell reads the saved UMAP `.h5ad` and combines cell-type and batch/sample UMAPs into one A4 landscape panel using 7 pt Arial-style settings. If Arial is unavailable on the server, matplotlib will fall back to the nearest available font."
262
- ]
263
- },
264
- {
265
- "cell_type": "code",
266
- "execution_count": null,
267
- "id": "b67f908e",
268
- "metadata": {},
269
- "outputs": [],
270
- "source": [
271
- "import math\n",
272
- "import numpy as np\n",
273
- "import matplotlib.pyplot as plt\n",
274
- "import scanpy as sc\n",
275
- "\n",
276
- "plt.rcParams.update({\n",
277
- " 'font.family': ['Arial', 'DejaVu Sans'],\n",
278
- " 'font.size': 7,\n",
279
- " 'axes.titlesize': 7,\n",
280
- " 'axes.labelsize': 7,\n",
281
- " 'xtick.labelsize': 7,\n",
282
- " 'ytick.labelsize': 7,\n",
283
- " 'legend.fontsize': 6,\n",
284
- " 'pdf.fonttype': 42,\n",
285
- " 'ps.fonttype': 42,\n",
286
- "})\n",
287
- "\n",
288
- "def plot_umap_axis(ax, adata, key, title):\n",
289
- " coords = np.asarray(adata.obsm['X_umap'])\n",
290
- " values = adata.obs[key].astype('category')\n",
291
- " categories = list(values.cat.categories)\n",
292
- " palette = []\n",
293
- " for cmap_name in ['tab20', 'tab20b', 'tab20c']:\n",
294
- " cmap = plt.get_cmap(cmap_name)\n",
295
- " palette.extend([cmap(i) for i in range(cmap.N)])\n",
296
- " for color, category in zip(palette, categories):\n",
297
- " mask = values.astype(str).to_numpy() == str(category)\n",
298
- " ax.scatter(coords[mask, 0], coords[mask, 1], s=2.2, c=[color], alpha=0.86, linewidths=0, rasterized=True, label=str(category))\n",
299
- " ax.set_title(title)\n",
300
- " ax.set_xticks([])\n",
301
- " ax.set_yticks([])\n",
302
- " for spine in ax.spines.values():\n",
303
- " spine.set_visible(False)\n",
304
- " ax.set_aspect('equal', adjustable='datalim')\n",
305
- " ncol = max(1, min(5, math.ceil(max(len(categories), 1) / 10)))\n",
306
- " ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.025), ncol=ncol, frameon=False, markerscale=2.4, handlelength=0.8, handletextpad=0.25, columnspacing=0.65, borderaxespad=0)\n",
307
- "\n",
308
- "umap_h5ad = OUT_DIR / f'{OUTPUT_PREFIX}_umap.h5ad'\n",
309
- "adata_umap = sc.read_h5ad(umap_h5ad)\n",
310
- "\n",
311
- "fig, axes = plt.subplots(1, 2, figsize=(11.69, 8.27))\n",
312
- "fig.subplots_adjust(left=0.025, right=0.985, bottom=0.30, top=0.90, wspace=0.04)\n",
313
- "plot_umap_axis(axes[0], adata_umap, 'cell_type', 'Kim2020 Lung no malignant | cell type')\n",
314
- "plot_umap_axis(axes[1], adata_umap, 'sample', 'Kim2020 Lung no malignant | sample')\n",
315
- "fig.suptitle('Zero-shot batch integration demo | weight0407', fontsize=7)\n",
316
- "\n",
317
- "a4_pdf = OUT_DIR / f'{OUTPUT_PREFIX}_umap_a4.pdf'\n",
318
- "a4_png = OUT_DIR / f'{OUTPUT_PREFIX}_umap_a4.png'\n",
319
- "fig.savefig(a4_pdf)\n",
320
- "fig.savefig(a4_png, dpi=300)\n",
321
- "plt.close(fig)\n",
322
- "\n",
323
- "print('Saved:', a4_pdf)\n",
324
- "print('Saved:', a4_png)\n",
325
- "if Image is not None:\n",
326
- " display(Image(filename=str(a4_png)))"
327
- ]
328
- }
329
- ],
330
- "metadata": {
331
- "kernelspec": {
332
- "display_name": "Python 3",
333
- "language": "python",
334
- "name": "python3"
335
- },
336
- "language_info": {
337
- "codemirror_mode": {
338
- "name": "ipython",
339
- "version": 3
340
- },
341
- "file_extension": ".py",
342
- "mimetype": "text/x-python",
343
- "name": "python",
344
- "nbconvert_exporter": "python",
345
- "pygments_lexer": "ipython3",
346
- "version": "3.9"
347
- }
348
- },
349
- "nbformat": 4,
350
- "nbformat_minor": 5
351
- }
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Zero-shot batch integration: Kim2020 Lung\n",
8
+ "\n",
9
+ "This notebook extracts cell embeddings with the BioMod model weights, evaluates batch integration with the bundled runner, and optionally plots UMAPs. It uses the full `Kim2020_Lung_noMalignant.h5ad` dataset. Evaluation does not train or update the pretrained model.\n"
10
+ ]
11
+ },
12
+ {
13
+ "cell_type": "markdown",
14
+ "metadata": {},
15
+ "source": [
16
+ "## Project paths and environment\n",
17
+ "\n",
18
+ "Start Jupyter with `BioMod/toturials` as its working directory and select the environment described in [the project README](../../README.md). Paths below are relative to this notebook directory, following the project-relative layout used in scGPT tutorials. `Path.resolve()` turns them into absolute paths for subprocesses. If Jupyter was started at the repository root, set `PROJECT_ROOT = Path('.').resolve()` instead.\n",
19
+ "\n",
20
+ "- Data: `../../data/downstream/final_data/Kim2020_Lung_noMalignant.h5ad`\n",
21
+ "- BioMod model weights and configuration: `../../weight/0810model/`\n",
22
+ "- Shared assets: `../../model_dependencies/`\n",
23
+ "- Runner: `../zero_shot_batch/performer_module_zero_shot.py`\n",
24
+ "- Results: `../../tutorial_runs/zero_shot_kim_lung_no_malignant/`\n",
25
+ "\n",
26
+ "Review the configuration, then set `RUN_EVALUATION = True` to run inference. The runner consumes `adata.X` without normalization or log transformation; check that its expression preprocessing is appropriate for the checkpoint and your experiment. It loads the input dataset into memory before batching inference.\n"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "from pathlib import Path\n",
36
+ "import importlib.util\n",
37
+ "import os\n",
38
+ "import subprocess\n",
39
+ "import sys\n",
40
+ "\n",
41
+ "PROJECT_ROOT = Path('../..').resolve()\n",
42
+ "if not (PROJECT_ROOT / 'BioMod').is_dir() or not (PROJECT_ROOT / 'model_dependencies').is_dir():\n",
43
+ " raise FileNotFoundError(\n",
44
+ " 'Start Jupyter from BioMod/toturials, or set PROJECT_ROOT to the repository root '\n",
45
+ " \"(use Path('.').resolve() when launched at the repository root).\"\n",
46
+ " )\n",
47
+ "SOURCE_DIR = PROJECT_ROOT / 'BioMod'\n",
48
+ "DEPENDENCIES_DIR = PROJECT_ROOT / 'model_dependencies'\n",
49
+ "SCRIPT = SOURCE_DIR / 'zero_shot_batch' / 'performer_module_zero_shot.py'\n",
50
+ "DATA_PATH = PROJECT_ROOT / 'data' / 'downstream' / 'final_data' / 'Kim2020_Lung_noMalignant.h5ad'\n",
51
+ "WEIGHT_DIR = PROJECT_ROOT / 'weight' / '0810model'\n",
52
+ "CONFIG_PKL = WEIGHT_DIR / 'mae_tost_70M_75mask_ec2.pkl'\n",
53
+ "CKPT_PATH = WEIGHT_DIR / 'mae_tost_70M_75mask_ec2_epoch0.pth'\n",
54
+ "GENE2VEC = DEPENDENCIES_DIR / 'selected_gene2vec_27k.npy'\n",
55
+ "GENE_MAP = DEPENDENCIES_DIR / 'gene2ens.csv'\n",
56
+ "GENE_LIST = DEPENDENCIES_DIR / 'selected_genes_27k.txt'\n",
57
+ "MODULE_DATA_DIR = DEPENDENCIES_DIR / 'module_data_all'\n",
58
+ "OUT_DIR = PROJECT_ROOT / 'tutorial_runs' / 'zero_shot_kim_lung_no_malignant'\n",
59
+ "\n",
60
+ "RUN_EVALUATION = False\n",
61
+ "MAKE_UMAP = False\n",
62
+ "N_CELLS = 'all'\n",
63
+ "BATCH_SIZE = 8\n",
64
+ "POOL_METHOD = 'max'\n",
65
+ "METRICS_MODE = 'scgpt'\n",
66
+ "UMAP_RANDOM_STATE = 0\n",
67
+ "\n",
68
+ "print('Python:', sys.executable)\n",
69
+ "print('Repository root:', PROJECT_ROOT)\n",
70
+ "print('Output directory:', OUT_DIR)\n"
71
+ ]
72
+ },
73
+ {
74
+ "cell_type": "code",
75
+ "execution_count": null,
76
+ "metadata": {},
77
+ "outputs": [],
78
+ "source": [
79
+ "required_files = [SCRIPT, DATA_PATH, CONFIG_PKL, CKPT_PATH, GENE2VEC, GENE_MAP, GENE_LIST,\n",
80
+ " MODULE_DATA_DIR / 'merged_modules.json']\n",
81
+ "required_dirs = [DEPENDENCIES_DIR / 'performer_pytorch_cont']\n",
82
+ "missing = [str(p) for p in required_files if not p.is_file()]\n",
83
+ "missing += [str(p) for p in required_dirs if not p.is_dir()]\n",
84
+ "if missing:\n",
85
+ " raise FileNotFoundError('Missing required project paths:\\n' + '\\n'.join(missing))\n",
86
+ "for p in required_files + required_dirs:\n",
87
+ " print('[FOUND]', p)\n"
88
+ ]
89
+ },
90
+ {
91
+ "cell_type": "code",
92
+ "execution_count": null,
93
+ "metadata": {},
94
+ "outputs": [],
95
+ "source": [
96
+ "# Configure only the child process; retain the active kernel's Python environment.\n",
97
+ "run_env = os.environ.copy()\n",
98
+ "run_env['PYTHONUNBUFFERED'] = '1'\n",
99
+ "run_env.setdefault('OMP_NUM_THREADS', '8')\n",
100
+ "pythonpath_parts = [str(SOURCE_DIR), str(DEPENDENCIES_DIR)]\n",
101
+ "if run_env.get('PYTHONPATH'):\n",
102
+ " pythonpath_parts.append(run_env['PYTHONPATH'])\n",
103
+ "run_env['PYTHONPATH'] = os.pathsep.join(pythonpath_parts)\n",
104
+ "print('PYTHONPATH:', run_env['PYTHONPATH'])\n"
105
+ ]
106
+ },
107
+ {
108
+ "cell_type": "code",
109
+ "execution_count": null,
110
+ "metadata": {},
111
+ "outputs": [],
112
+ "source": [
113
+ "# Install project dependencies in the selected kernel environment before evaluation.\n",
114
+ "required_modules = ['numpy', 'pandas', 'torch', 'scanpy', 'scib', 'einops']\n",
115
+ "missing_modules = [name for name in required_modules if importlib.util.find_spec(name) is None]\n",
116
+ "if missing_modules:\n",
117
+ " message = ('Missing dependencies: ' + ', '.join(missing_modules)\n",
118
+ " + '. Follow the installation instructions in the project README.')\n",
119
+ " if RUN_EVALUATION:\n",
120
+ " raise ImportError(message)\n",
121
+ " print(message)\n",
122
+ "else:\n",
123
+ " print('Core evaluation dependencies are available.')\n"
124
+ ]
125
+ },
126
+ {
127
+ "cell_type": "code",
128
+ "execution_count": null,
129
+ "metadata": {},
130
+ "outputs": [],
131
+ "source": [
132
+ "cmd = [\n",
133
+ " sys.executable, str(SCRIPT),\n",
134
+ " '--data_path', str(DATA_PATH),\n",
135
+ " '--gene2vec_path', str(GENE2VEC),\n",
136
+ " '--scfm_hyper_params_path', str(CONFIG_PKL),\n",
137
+ " '--scfm_ckpt_path', str(CKPT_PATH),\n",
138
+ " '--module_data_dir', str(MODULE_DATA_DIR),\n",
139
+ " '--gene_mapping_path', str(GENE_MAP),\n",
140
+ " '--scfm_genes_file', str(GENE_LIST),\n",
141
+ " '--output_dir', str(OUT_DIR),\n",
142
+ " '--dataset_preset', 'lung',\n",
143
+ " '--gene_col', 'gene_name',\n",
144
+ " '--batch_key', 'sample',\n",
145
+ " '--label_key', 'cell_type',\n",
146
+ " '--output_key', 'X_sclong_module',\n",
147
+ " '--pool_method', POOL_METHOD,\n",
148
+ " '--metrics_mode', METRICS_MODE,\n",
149
+ " '--n_cells', N_CELLS,\n",
150
+ " '--batch_size', str(BATCH_SIZE),\n",
151
+ " '--min_genes_per_cell', '0',\n",
152
+ " '--min_cells_per_gene', '0',\n",
153
+ "]\n",
154
+ "print('Command arguments:')\n",
155
+ "print(cmd)\n"
156
+ ]
157
+ },
158
+ {
159
+ "cell_type": "code",
160
+ "execution_count": null,
161
+ "metadata": {},
162
+ "outputs": [],
163
+ "source": [
164
+ "log_path = OUT_DIR / 'zero_shot.log'\n",
165
+ "if not RUN_EVALUATION:\n",
166
+ " print('Evaluation is disabled. Set RUN_EVALUATION = True and rerun the configuration cells to start.')\n",
167
+ "else:\n",
168
+ " OUT_DIR.mkdir(parents=True, exist_ok=True)\n",
169
+ " with log_path.open('w', encoding='utf-8') as log:\n",
170
+ " process = subprocess.Popen(\n",
171
+ " cmd, cwd=SCRIPT.parent, env=run_env,\n",
172
+ " stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n",
173
+ " text=True, encoding='utf-8', errors='replace', bufsize=1,\n",
174
+ " )\n",
175
+ " for line in process.stdout:\n",
176
+ " print(line, end='')\n",
177
+ " log.write(line)\n",
178
+ " return_code = process.wait()\n",
179
+ " if return_code != 0:\n",
180
+ " raise RuntimeError(f'Evaluation failed with code {return_code}; see {log_path}')\n",
181
+ " print('Evaluation completed:', OUT_DIR)\n"
182
+ ]
183
+ },
184
+ {
185
+ "cell_type": "code",
186
+ "execution_count": null,
187
+ "metadata": {},
188
+ "outputs": [],
189
+ "source": [
190
+ "metrics_path = OUT_DIR / 'sclong_module_batch_cell_emb_mode.csv'\n",
191
+ "embedding_path = OUT_DIR / 'sclong_module_batch_cell_emb_mode_ce.npy'\n",
192
+ "metrics = None\n",
193
+ "if metrics_path.is_file():\n",
194
+ " import pandas as pd\n",
195
+ " from IPython.display import display\n",
196
+ " metrics = pd.read_csv(metrics_path)\n",
197
+ " required_columns = {'metric', 'label', 'value'}\n",
198
+ " if not required_columns.issubset(metrics.columns):\n",
199
+ " raise ValueError(f'Unexpected metrics columns: {list(metrics.columns)}')\n",
200
+ " display(metrics)\n",
201
+ "else:\n",
202
+ " print('No evaluation metrics yet:', metrics_path)\n"
203
+ ]
204
+ },
205
+ {
206
+ "cell_type": "code",
207
+ "execution_count": null,
208
+ "metadata": {},
209
+ "outputs": [],
210
+ "source": [
211
+ "if metrics is not None:\n",
212
+ " # The bundled scGPT metric mode reports these two aggregates; it does not emit avg_score.\n",
213
+ " avg_metrics = metrics[metrics['metric'].isin(['avg_batch', 'avg_bio'])].copy()\n",
214
+ " if avg_metrics.empty:\n",
215
+ " print('No aggregate metrics in this result; inspect the individual metrics above.')\n",
216
+ " else:\n",
217
+ " display(avg_metrics)\n"
218
+ ]
219
+ },
220
+ {
221
+ "cell_type": "code",
222
+ "execution_count": null,
223
+ "metadata": {},
224
+ "outputs": [],
225
+ "source": [
226
+ "print('Metrics:', metrics_path)\n",
227
+ "print('Cell embeddings:', embedding_path)\n",
228
+ "print('Run log:', log_path)\n",
229
+ "print('The runner saves CSV/NPY results. UMAP figures are created by the optional cell below.')\n"
230
+ ]
231
+ },
232
+ {
233
+ "cell_type": "markdown",
234
+ "metadata": {},
235
+ "source": [
236
+ "## Optional: plot UMAP and export a PDF/PNG panel\n",
237
+ "\n",
238
+ "After a successful evaluation, set `MAKE_UMAP = True` in the configuration cell. The following cell combines the saved cell embeddings with the input dataset's observation metadata. It requires the full dataset (`N_CELLS = 'all'`) with no cell filtering, as configured above, so the rows retain the input order. Use embeddings from this dataset and this configuration; the runner does not save cell identifiers alongside the NPY array. Subsampled or filtered embeddings require their matching observation metadata and cannot be plotted by this cell.\n",
239
+ "\n",
240
+ "The optional plot requires Scanpy and Matplotlib. It computes UMAP from the embeddings and writes `umap_a4.pdf`, `umap_a4.png`, and `umap.h5ad` to the result directory.\n"
241
+ ]
242
+ },
243
+ {
244
+ "cell_type": "code",
245
+ "execution_count": null,
246
+ "id": "b67f908e",
247
+ "metadata": {},
248
+ "outputs": [],
249
+ "source": [
250
+ "if not MAKE_UMAP:\n",
251
+ " print('UMAP export is disabled. Set MAKE_UMAP = True after evaluation.')\n",
252
+ "elif not embedding_path.is_file():\n",
253
+ " print('No cell embeddings yet:', embedding_path)\n",
254
+ "else:\n",
255
+ " if N_CELLS != 'all':\n",
256
+ " raise ValueError('UMAP export requires the full, unfiltered dataset in input row order.')\n",
257
+ " import math\n",
258
+ " import numpy as np\n",
259
+ " import matplotlib.pyplot as plt\n",
260
+ " import scanpy as sc\n",
261
+ " from IPython.display import Image, display\n",
262
+ "\n",
263
+ " source = sc.read_h5ad(DATA_PATH, backed='r')\n",
264
+ " try:\n",
265
+ " obs = source.obs.copy()\n",
266
+ " finally:\n",
267
+ " source.file.close()\n",
268
+ " embeddings = np.load(embedding_path, allow_pickle=False)\n",
269
+ " if embeddings.ndim != 2 or embeddings.shape[0] != len(obs):\n",
270
+ " raise ValueError('Embedding rows do not match the full input dataset.')\n",
271
+ " if not {'sample', 'cell_type'}.issubset(obs.columns):\n",
272
+ " raise KeyError('The input metadata must contain sample and cell_type.')\n",
273
+ " if len(obs) < 3 or not np.isfinite(embeddings).all():\n",
274
+ " raise ValueError('UMAP requires at least three cells and finite embeddings.')\n",
275
+ " adata_umap = sc.AnnData(X=embeddings, obs=obs)\n",
276
+ " sc.pp.neighbors(adata_umap, use_rep='X', n_pcs=None,\n",
277
+ " n_neighbors=min(15, len(obs) - 1), random_state=UMAP_RANDOM_STATE)\n",
278
+ " sc.tl.umap(adata_umap, min_dist=0.5, random_state=UMAP_RANDOM_STATE)\n",
279
+ "\n",
280
+ " def plot_umap_axis(ax, adata, key, title):\n",
281
+ " coords = np.asarray(adata.obsm['X_umap'])\n",
282
+ " values = adata.obs[key].astype('string').fillna('Missing')\n",
283
+ " categories = sorted(values.unique())\n",
284
+ " palette = [plt.get_cmap(name)(i) for name in ['tab20', 'tab20b', 'tab20c']\n",
285
+ " for i in range(plt.get_cmap(name).N)]\n",
286
+ " for index, category in enumerate(categories):\n",
287
+ " mask = (values == category).to_numpy(dtype=bool)\n",
288
+ " ax.scatter(coords[mask, 0], coords[mask, 1], s=2.2,\n",
289
+ " color=palette[index % len(palette)], alpha=0.86,\n",
290
+ " linewidths=0, rasterized=True, label=str(category))\n",
291
+ " ax.set_title(title)\n",
292
+ " ax.set_xticks([])\n",
293
+ " ax.set_yticks([])\n",
294
+ " for spine in ax.spines.values():\n",
295
+ " spine.set_visible(False)\n",
296
+ " ax.set_aspect('equal', adjustable='datalim')\n",
297
+ " ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.025),\n",
298
+ " ncol=max(1, min(5, math.ceil(len(categories) / 10))),\n",
299
+ " frameon=False, markerscale=2.4, fontsize=6)\n",
300
+ "\n",
301
+ " with plt.rc_context({'font.family': 'DejaVu Sans', 'font.size': 7, 'pdf.fonttype': 42}):\n",
302
+ " fig, axes = plt.subplots(1, 2, figsize=(11.69, 8.27))\n",
303
+ " fig.subplots_adjust(left=0.025, right=0.985, bottom=0.30, top=0.90, wspace=0.04)\n",
304
+ " plot_umap_axis(axes[0], adata_umap, 'cell_type', 'Cell type')\n",
305
+ " plot_umap_axis(axes[1], adata_umap, 'sample', 'Sample')\n",
306
+ " fig.suptitle('Zero-shot batch integration | BioMod model weights')\n",
307
+ " a4_pdf = OUT_DIR / 'umap_a4.pdf'\n",
308
+ " a4_png = OUT_DIR / 'umap_a4.png'\n",
309
+ " fig.savefig(a4_pdf)\n",
310
+ " fig.savefig(a4_png, dpi=300)\n",
311
+ " plt.close(fig)\n",
312
+ " adata_umap.write_h5ad(OUT_DIR / 'umap.h5ad')\n",
313
+ " print('Saved:', a4_pdf)\n",
314
+ " print('Saved:', a4_png)\n",
315
+ " display(Image(filename=str(a4_png)))\n"
316
+ ]
317
+ }
318
+ ],
319
+ "metadata": {
320
+ "kernelspec": {
321
+ "display_name": "Python 3",
322
+ "language": "python",
323
+ "name": "python3"
324
+ },
325
+ "language_info": {
326
+ "codemirror_mode": {
327
+ "name": "ipython",
328
+ "version": 3
329
+ },
330
+ "file_extension": ".py",
331
+ "mimetype": "text/x-python",
332
+ "name": "python",
333
+ "nbconvert_exporter": "python",
334
+ "pygments_lexer": "ipython3",
335
+ "version": "3.9"
336
+ }
337
+ },
338
+ "nbformat": 4,
339
+ "nbformat_minor": 5
340
+ }
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -80,13 +80,13 @@ The following tasks have implementations in this repository. The links identify
80
  available source code and examples; these workflows have not been validated end
81
  to end with the bundled checkpoint.
82
 
83
- | Task | Implementation | Notebook or guide |
84
  | --- | --- | --- |
85
  | Cell-type annotation | [Fine-tuning script](BioMod/cell_annotation/performer_module_finetune.py) | [Notebook](BioMod/toturials/cell_annotation.ipynb) |
86
  | Zero-shot cell embeddings and batch-effect evaluation | [Embedding and evaluation script](BioMod/zero_shot_batch/performer_module_zero_shot.py) | [Notebook](BioMod/toturials/zero_shot_batch_integration.ipynb) |
87
  | Single-drug response regression (IC50) | [Training script](BioMod/drug_response/train_drug_performer_module.py) | [Notebook](BioMod/toturials/drug_response_prediction.ipynb) |
88
- | Drug-pair synergy classification | [Training script](BioMod/drug_combination/train_drug_combination_performer_module.py) | [Task guide](BioMod/drug_combination/README.md), [notebook](BioMod/toturials/drug_combination.ipynb) |
89
- | Gene perturbation response prediction | [Training script](BioMod/gene_perturbation/train_module.py) | [Task guide](BioMod/gene_perturbation/README.md), [notebook](BioMod/toturials/gene_perturbation.ipynb) |
90
 
91
  ### Pretraining, evaluation, and analysis scripts
92
 
 
80
  available source code and examples; these workflows have not been validated end
81
  to end with the bundled checkpoint.
82
 
83
+ | Task | Implementation | Notebook |
84
  | --- | --- | --- |
85
  | Cell-type annotation | [Fine-tuning script](BioMod/cell_annotation/performer_module_finetune.py) | [Notebook](BioMod/toturials/cell_annotation.ipynb) |
86
  | Zero-shot cell embeddings and batch-effect evaluation | [Embedding and evaluation script](BioMod/zero_shot_batch/performer_module_zero_shot.py) | [Notebook](BioMod/toturials/zero_shot_batch_integration.ipynb) |
87
  | Single-drug response regression (IC50) | [Training script](BioMod/drug_response/train_drug_performer_module.py) | [Notebook](BioMod/toturials/drug_response_prediction.ipynb) |
88
+ | Drug-pair synergy classification | [Training script](BioMod/drug_combination/train_drug_combination_performer_module.py) | [Notebook](BioMod/toturials/drug_combination.ipynb) |
89
+ | Gene perturbation response prediction | [Training script](BioMod/gene_perturbation/train_module.py) | [Notebook](BioMod/toturials/gene_perturbation.ipynb) |
90
 
91
  ### Pretraining, evaluation, and analysis scripts
92